SpoolBuddyLayout.test.tsx 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122
  1. /**
  2. * Tests for SpoolBuddyLayout component:
  3. * - Renders without crashing
  4. */
  5. import { describe, it, expect, vi } from 'vitest';
  6. import { render } from '@testing-library/react';
  7. import React from 'react';
  8. import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
  9. import { MemoryRouter, Route, Routes } from 'react-router-dom';
  10. import { SpoolBuddyLayout } from '../../../components/spoolbuddy/SpoolBuddyLayout';
  11. import { ToastProvider } from '../../../contexts/ToastContext';
  12. vi.mock('react-i18next', () => ({
  13. useTranslation: () => ({
  14. t: (_key: string, fallback: string) => fallback,
  15. i18n: { language: 'en', changeLanguage: vi.fn() },
  16. }),
  17. }));
  18. vi.mock('../../../api/client', () => ({
  19. api: {
  20. getPrinters: vi.fn().mockResolvedValue([]),
  21. getPrinterStatus: vi.fn().mockResolvedValue({ connected: false }),
  22. getSettings: vi.fn().mockResolvedValue({ time_format: 'system', language: 'en' }),
  23. },
  24. spoolbuddyApi: {
  25. getDevices: vi.fn().mockResolvedValue([]),
  26. },
  27. }));
  28. vi.mock('../../../utils/date', () => ({
  29. formatTimeOnly: () => '12:00',
  30. }));
  31. vi.mock('lucide-react', () => {
  32. const Stub = (props: Record<string, unknown>) => <span {...props} />;
  33. return {
  34. WifiOff: (props: Record<string, unknown>) => <span data-testid="wifi-off" {...props} />,
  35. // ToastProvider, brought in by SpoolBuddyLayout's useToast(), imports these.
  36. AlertCircle: Stub,
  37. CheckCircle: Stub,
  38. ChevronDown: Stub,
  39. ChevronUp: Stub,
  40. Info: Stub,
  41. Loader2: Stub,
  42. X: Stub,
  43. XCircle: Stub,
  44. };
  45. });
  46. vi.mock('../../../components/VirtualKeyboard', () => ({
  47. VirtualKeyboard: () => <div data-testid="virtual-keyboard" />,
  48. }));
  49. function renderLayout() {
  50. const qc = new QueryClient({ defaultOptions: { queries: { retry: false, gcTime: 0 } } });
  51. return render(
  52. <ToastProvider>
  53. <QueryClientProvider client={qc}>
  54. <MemoryRouter initialEntries={['/spoolbuddy']}>
  55. <Routes>
  56. <Route path="spoolbuddy" element={<SpoolBuddyLayout />}>
  57. <Route index element={<div data-testid="child-page">Child</div>} />
  58. </Route>
  59. </Routes>
  60. </MemoryRouter>
  61. </QueryClientProvider>
  62. </ToastProvider>
  63. );
  64. }
  65. describe('SpoolBuddyLayout', () => {
  66. it('renders without crashing', () => {
  67. const { container } = renderLayout();
  68. expect(container.firstChild).not.toBeNull();
  69. });
  70. it('renders the top bar with logo', () => {
  71. renderLayout();
  72. const img = document.querySelector('img[alt="SpoolBuddy"]');
  73. expect(img).not.toBeNull();
  74. });
  75. it('renders the bottom nav', () => {
  76. renderLayout();
  77. const nav = document.querySelector('nav');
  78. expect(nav).not.toBeNull();
  79. });
  80. it('renders the status bar', () => {
  81. renderLayout();
  82. // Status bar shows "System Ready" by default (device offline triggers warning later via useEffect)
  83. // Just check the status bar container exists
  84. const statusBar = document.querySelector('.shrink-0.h-9');
  85. expect(statusBar).not.toBeNull();
  86. });
  87. it('renders child outlet content', () => {
  88. renderLayout();
  89. const child = document.querySelector('[data-testid="child-page"]');
  90. expect(child).not.toBeNull();
  91. });
  92. it('suppresses the global toast viewport while mounted', () => {
  93. const { unmount } = renderLayout();
  94. // Visible viewport gets `hidden` class while the kiosk is up. Position is
  95. // set via safe-area calc() (#2612) so match the stable data-testid.
  96. const viewport = document.querySelector('[data-testid="toast-viewport"]');
  97. expect(viewport?.className).toContain('hidden');
  98. // Cleanup restores the viewport when the kiosk unmounts (e.g. user
  99. // navigates back to the main app).
  100. unmount();
  101. const viewportAfter = document.querySelector('[data-testid="toast-viewport"]');
  102. // After unmount the toast container is gone with the provider; the
  103. // important guarantee is the suppression flag was untoggled, which the
  104. // ToastContext tests pin directly. Here we only assert no crash on
  105. // unmount during cleanup.
  106. expect(viewportAfter).toBeNull();
  107. });
  108. });