Files
bambuddy/frontend/src/__tests__/components/spoolbuddy/SpoolBuddyLayout.test.tsx
T
maziggy 4ef6772c51 fix(ui): keep the progress toast on-screen in the installed iPhone PWA (#2612)
The dispatch progress toast is a fixed 420px wide and the toast viewport is
anchored 80px from the right (to clear the bug-report bubble). On a 390px-wide
phone that overflows the left edge by ~110px, so in the Home-Screen PWA the
toast was clipped off the left, with text bleeding past the edge.

Cap every toast to a viewport-relative max-width (calc(100vw - 6rem - safe-area
insets)) so it can't exceed the screen; desktop keeps the 420px. Make the
viewport position safe-area-aware (env(safe-area-inset-*) on bottom/right) so an
installed PWA clears the home indicator and a landscape notch, and add
min-w-0/shrink-0 to the per-job filename row so long names truncate instead of
widening the toast at the narrower phone width.

Frontend-only; no backend, schema, or i18n change. Covered by a test pinning the
width cap; the suppression test's viewport lookup moved to a stable data-testid.
2026-07-20 11:05:39 +02:00

123 lines
4.0 KiB
TypeScript

/**
* Tests for SpoolBuddyLayout component:
* - Renders without crashing
*/
import { describe, it, expect, vi } from 'vitest';
import { render } from '@testing-library/react';
import React from 'react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { MemoryRouter, Route, Routes } from 'react-router-dom';
import { SpoolBuddyLayout } from '../../../components/spoolbuddy/SpoolBuddyLayout';
import { ToastProvider } from '../../../contexts/ToastContext';
vi.mock('react-i18next', () => ({
useTranslation: () => ({
t: (_key: string, fallback: string) => fallback,
i18n: { language: 'en', changeLanguage: vi.fn() },
}),
}));
vi.mock('../../../api/client', () => ({
api: {
getPrinters: vi.fn().mockResolvedValue([]),
getPrinterStatus: vi.fn().mockResolvedValue({ connected: false }),
getSettings: vi.fn().mockResolvedValue({ time_format: 'system', language: 'en' }),
},
spoolbuddyApi: {
getDevices: vi.fn().mockResolvedValue([]),
},
}));
vi.mock('../../../utils/date', () => ({
formatTimeOnly: () => '12:00',
}));
vi.mock('lucide-react', () => {
const Stub = (props: Record<string, unknown>) => <span {...props} />;
return {
WifiOff: (props: Record<string, unknown>) => <span data-testid="wifi-off" {...props} />,
// ToastProvider, brought in by SpoolBuddyLayout's useToast(), imports these.
AlertCircle: Stub,
CheckCircle: Stub,
ChevronDown: Stub,
ChevronUp: Stub,
Info: Stub,
Loader2: Stub,
X: Stub,
XCircle: Stub,
};
});
vi.mock('../../../components/VirtualKeyboard', () => ({
VirtualKeyboard: () => <div data-testid="virtual-keyboard" />,
}));
function renderLayout() {
const qc = new QueryClient({ defaultOptions: { queries: { retry: false, gcTime: 0 } } });
return render(
<ToastProvider>
<QueryClientProvider client={qc}>
<MemoryRouter initialEntries={['/spoolbuddy']}>
<Routes>
<Route path="spoolbuddy" element={<SpoolBuddyLayout />}>
<Route index element={<div data-testid="child-page">Child</div>} />
</Route>
</Routes>
</MemoryRouter>
</QueryClientProvider>
</ToastProvider>
);
}
describe('SpoolBuddyLayout', () => {
it('renders without crashing', () => {
const { container } = renderLayout();
expect(container.firstChild).not.toBeNull();
});
it('renders the top bar with logo', () => {
renderLayout();
const img = document.querySelector('img[alt="SpoolBuddy"]');
expect(img).not.toBeNull();
});
it('renders the bottom nav', () => {
renderLayout();
const nav = document.querySelector('nav');
expect(nav).not.toBeNull();
});
it('renders the status bar', () => {
renderLayout();
// Status bar shows "System Ready" by default (device offline triggers warning later via useEffect)
// Just check the status bar container exists
const statusBar = document.querySelector('.shrink-0.h-9');
expect(statusBar).not.toBeNull();
});
it('renders child outlet content', () => {
renderLayout();
const child = document.querySelector('[data-testid="child-page"]');
expect(child).not.toBeNull();
});
it('suppresses the global toast viewport while mounted', () => {
const { unmount } = renderLayout();
// Visible viewport gets `hidden` class while the kiosk is up. Position is
// set via safe-area calc() (#2612) so match the stable data-testid.
const viewport = document.querySelector('[data-testid="toast-viewport"]');
expect(viewport?.className).toContain('hidden');
// Cleanup restores the viewport when the kiosk unmounts (e.g. user
// navigates back to the main app).
unmount();
const viewportAfter = document.querySelector('[data-testid="toast-viewport"]');
// After unmount the toast container is gone with the provider; the
// important guarantee is the suppression flag was untoggled, which the
// ToastContext tests pin directly. Here we only assert no crash on
// unmount during cleanup.
expect(viewportAfter).toBeNull();
});
});