|
@@ -7,7 +7,7 @@
|
|
|
*/
|
|
*/
|
|
|
|
|
|
|
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
|
-import { screen, waitFor } from '@testing-library/react';
|
|
|
|
|
|
|
+import { screen, waitFor, fireEvent } from '@testing-library/react';
|
|
|
import React from 'react';
|
|
import React from 'react';
|
|
|
import { render } from '@testing-library/react';
|
|
import { render } from '@testing-library/react';
|
|
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
|
@@ -26,6 +26,7 @@ vi.mock('../../api/client', () => ({
|
|
|
getPrinterStatus: vi.fn().mockResolvedValue({ connected: false }),
|
|
getPrinterStatus: vi.fn().mockResolvedValue({ connected: false }),
|
|
|
linkTagToSpool: vi.fn().mockResolvedValue({}),
|
|
linkTagToSpool: vi.fn().mockResolvedValue({}),
|
|
|
createSpool: vi.fn().mockResolvedValue({ id: 4 }),
|
|
createSpool: vi.fn().mockResolvedValue({ id: 4 }),
|
|
|
|
|
+ clearPlate: vi.fn().mockResolvedValue({}),
|
|
|
},
|
|
},
|
|
|
spoolbuddyApi: {
|
|
spoolbuddyApi: {
|
|
|
getDevices: vi.fn().mockResolvedValue([]),
|
|
getDevices: vi.fn().mockResolvedValue([]),
|
|
@@ -34,7 +35,12 @@ vi.mock('../../api/client', () => ({
|
|
|
|
|
|
|
|
vi.mock('react-i18next', () => ({
|
|
vi.mock('react-i18next', () => ({
|
|
|
useTranslation: () => ({
|
|
useTranslation: () => ({
|
|
|
- t: (_key: string, fallback: string) => fallback,
|
|
|
|
|
|
|
+ // Mirrors i18next's (key, defaultValue, options) signature with simple
|
|
|
|
|
+ // {{var}} interpolation so tests can assert on the rendered text.
|
|
|
|
|
+ t: (_key: string, fallback: string, options?: Record<string, unknown>) => {
|
|
|
|
|
+ if (!options) return fallback;
|
|
|
|
|
+ return fallback.replace(/\{\{(\w+)\}\}/g, (_m, k) => String(options[k] ?? ''));
|
|
|
|
|
+ },
|
|
|
i18n: { language: 'en', changeLanguage: vi.fn() },
|
|
i18n: { language: 'en', changeLanguage: vi.fn() },
|
|
|
}),
|
|
}),
|
|
|
}));
|
|
}));
|
|
@@ -137,4 +143,106 @@ describe('SpoolBuddyDashboard', () => {
|
|
|
expect(screen.getByText('Current Spool')).toBeDefined();
|
|
expect(screen.getByText('Current Spool')).toBeDefined();
|
|
|
});
|
|
});
|
|
|
});
|
|
});
|
|
|
|
|
+
|
|
|
|
|
+ describe('plate-clear row', () => {
|
|
|
|
|
+ // We re-mock api.getPrinters / getPrinterStatus per test so each scenario
|
|
|
|
|
+ // controls exactly which printers report awaiting_plate_clear.
|
|
|
|
|
+ it('does not render the plate-clear button when no printer needs it', async () => {
|
|
|
|
|
+ const { api } = await import('../../api/client');
|
|
|
|
|
+ (api.getPrinters as ReturnType<typeof vi.fn>).mockResolvedValueOnce([
|
|
|
|
|
+ { id: 1, name: 'X1C' },
|
|
|
|
|
+ ]);
|
|
|
|
|
+ (api.getPrinterStatus as ReturnType<typeof vi.fn>).mockResolvedValue({
|
|
|
|
|
+ connected: true,
|
|
|
|
|
+ awaiting_plate_clear: false,
|
|
|
|
|
+ });
|
|
|
|
|
+ renderPage();
|
|
|
|
|
+ await waitFor(() => {
|
|
|
|
|
+ expect(screen.getByText('X1C')).toBeDefined();
|
|
|
|
|
+ });
|
|
|
|
|
+ expect(screen.queryByTestId('plate-clear-section')).toBeNull();
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ it('renders a plate-clear pill only for printers with awaiting_plate_clear=true', async () => {
|
|
|
|
|
+ const { api } = await import('../../api/client');
|
|
|
|
|
+ (api.getPrinters as ReturnType<typeof vi.fn>).mockResolvedValueOnce([
|
|
|
|
|
+ { id: 1, name: 'X1C' },
|
|
|
|
|
+ { id: 2, name: 'P1S' },
|
|
|
|
|
+ ]);
|
|
|
|
|
+ (api.getPrinterStatus as ReturnType<typeof vi.fn>).mockImplementation((printerId: number) =>
|
|
|
|
|
+ Promise.resolve({
|
|
|
|
|
+ connected: true,
|
|
|
|
|
+ awaiting_plate_clear: printerId === 2,
|
|
|
|
|
+ })
|
|
|
|
|
+ );
|
|
|
|
|
+ renderPage();
|
|
|
|
|
+ await waitFor(() => {
|
|
|
|
|
+ expect(screen.getByTestId('plate-clear-button-2')).toBeDefined();
|
|
|
|
|
+ });
|
|
|
|
|
+ expect(screen.queryByTestId('plate-clear-button-1')).toBeNull();
|
|
|
|
|
+ // Pill content: printer name + "Clear" label, plus full "Plate ready: P1S" in title attr.
|
|
|
|
|
+ const pill = screen.getByTestId('plate-clear-button-2');
|
|
|
|
|
+ expect(pill.getAttribute('title')).toBe('Plate ready: P1S');
|
|
|
|
|
+ expect(pill.textContent).toContain('P1S');
|
|
|
|
|
+ expect(pill.textContent).toContain('Clear');
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ it('renders multiple plate-clear pills inline when several printers are pending', async () => {
|
|
|
|
|
+ const { api } = await import('../../api/client');
|
|
|
|
|
+ (api.getPrinters as ReturnType<typeof vi.fn>).mockResolvedValueOnce([
|
|
|
|
|
+ { id: 1, name: 'A' },
|
|
|
|
|
+ { id: 2, name: 'B' },
|
|
|
|
|
+ { id: 3, name: 'C' },
|
|
|
|
|
+ ]);
|
|
|
|
|
+ (api.getPrinterStatus as ReturnType<typeof vi.fn>).mockResolvedValue({
|
|
|
|
|
+ connected: true,
|
|
|
|
|
+ awaiting_plate_clear: true,
|
|
|
|
|
+ });
|
|
|
|
|
+ renderPage();
|
|
|
|
|
+ await waitFor(() => {
|
|
|
|
|
+ expect(screen.getByTestId('plate-clear-button-1')).toBeDefined();
|
|
|
|
|
+ expect(screen.getByTestId('plate-clear-button-2')).toBeDefined();
|
|
|
|
|
+ expect(screen.getByTestId('plate-clear-button-3')).toBeDefined();
|
|
|
|
|
+ });
|
|
|
|
|
+ // Pills sit in the same flex-wrap container so they flow inline.
|
|
|
|
|
+ const section = screen.getByTestId('plate-clear-section');
|
|
|
|
|
+ expect(section.className).toContain('flex-wrap');
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ it('calls api.clearPlate with the printer id when clicked', async () => {
|
|
|
|
|
+ const { api } = await import('../../api/client');
|
|
|
|
|
+ (api.getPrinters as ReturnType<typeof vi.fn>).mockResolvedValueOnce([
|
|
|
|
|
+ { id: 7, name: 'H2D' },
|
|
|
|
|
+ ]);
|
|
|
|
|
+ (api.getPrinterStatus as ReturnType<typeof vi.fn>).mockResolvedValue({
|
|
|
|
|
+ connected: true,
|
|
|
|
|
+ awaiting_plate_clear: true,
|
|
|
|
|
+ });
|
|
|
|
|
+ renderPage();
|
|
|
|
|
+ const btn = await waitFor(() => screen.getByTestId('plate-clear-button-7'));
|
|
|
|
|
+ fireEvent.click(btn);
|
|
|
|
|
+ await waitFor(() => {
|
|
|
|
|
+ expect(api.clearPlate).toHaveBeenCalledWith(7);
|
|
|
|
|
+ });
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ it('hides the row optimistically after a successful click without a refetch', async () => {
|
|
|
|
|
+ const { api } = await import('../../api/client');
|
|
|
|
|
+ (api.getPrinters as ReturnType<typeof vi.fn>).mockResolvedValueOnce([
|
|
|
|
|
+ { id: 9, name: 'X1E' },
|
|
|
|
|
+ ]);
|
|
|
|
|
+ // Stable resolve — even if refetch happens it would still report pending,
|
|
|
|
|
+ // so a disappearing row proves the optimistic cache write worked.
|
|
|
|
|
+ (api.getPrinterStatus as ReturnType<typeof vi.fn>).mockResolvedValue({
|
|
|
|
|
+ connected: true,
|
|
|
|
|
+ awaiting_plate_clear: true,
|
|
|
|
|
+ });
|
|
|
|
|
+ renderPage();
|
|
|
|
|
+ const btn = await waitFor(() => screen.getByTestId('plate-clear-button-9'));
|
|
|
|
|
+ fireEvent.click(btn);
|
|
|
|
|
+ await waitFor(() => {
|
|
|
|
|
+ expect(screen.queryByTestId('plate-clear-button-9')).toBeNull();
|
|
|
|
|
+ });
|
|
|
|
|
+ });
|
|
|
|
|
+ });
|
|
|
});
|
|
});
|