SkipObjectsModalWiring.test.tsx 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170
  1. /**
  2. * SkipObjectsModal wiring: the request it sends when objects are skipped.
  3. *
  4. * The printer identifies objects by their slicer object id, not by their
  5. * position in the list, so picking the 1st and 3rd object must send their ids.
  6. * Already-skipped objects can't be picked again, and the submit button is gated
  7. * on printers:control and on the print being past layer 1.
  8. */
  9. import { describe, it, expect, vi, beforeEach } from 'vitest';
  10. import { screen, waitFor } from '@testing-library/react';
  11. import userEvent from '@testing-library/user-event';
  12. import { http, HttpResponse } from 'msw';
  13. import { server } from '../mocks/server';
  14. const permissions = { granted: ['printers:control'] as string[] };
  15. const mockUseAuth = {
  16. user: { id: 1, username: 'operator', permissions: [] as string[] },
  17. authEnabled: true,
  18. loading: false,
  19. isAdmin: false,
  20. login: vi.fn(),
  21. loginWithToken: vi.fn(),
  22. logout: vi.fn(),
  23. refreshAuth: vi.fn(),
  24. hasPermission: vi.fn((permission: string) => permissions.granted.includes(permission)),
  25. hasAnyPermission: vi.fn(() => true),
  26. canModify: vi.fn(() => true),
  27. };
  28. vi.mock('../../contexts/AuthContext', async (importOriginal) => {
  29. const actual = await importOriginal<typeof import('../../contexts/AuthContext')>();
  30. return { ...actual, useAuth: () => mockUseAuth };
  31. });
  32. import { render } from '../utils';
  33. import { SkipObjectsModal } from '../../components/SkipObjectsModal';
  34. const PRINTER_ID = 7;
  35. // Ids deliberately unlike list positions.
  36. const objects = [
  37. { id: 412, name: 'Bracket A', x: 10, y: 10, skipped: false },
  38. { id: 518, name: 'Bracket B', x: 20, y: 20, skipped: true },
  39. { id: 903, name: 'Bracket C', x: 30, y: 30, skipped: false },
  40. { id: 1207, name: 'Bracket D', x: 40, y: 40, skipped: false },
  41. ];
  42. let skipCalls: { printerId: string; body: unknown }[] = [];
  43. function setup({ layer = 12 }: { layer?: number } = {}) {
  44. skipCalls = [];
  45. server.use(
  46. http.get(`/api/v1/printers/${PRINTER_ID}/status`, () =>
  47. HttpResponse.json({ connected: true, state: 'RUNNING', layer_num: layer, cover_url: null }),
  48. ),
  49. http.get(`/api/v1/printers/${PRINTER_ID}/print/objects`, () =>
  50. HttpResponse.json({
  51. objects,
  52. total: objects.length,
  53. skipped_count: 1,
  54. is_printing: true,
  55. bbox_all: null,
  56. }),
  57. ),
  58. http.post('/api/v1/printers/:id/print/skip-objects', async ({ request, params }) => {
  59. skipCalls.push({ printerId: params.id as string, body: await request.json() });
  60. return HttpResponse.json({ success: true, message: 'Skipped', skipped_objects: [] });
  61. }),
  62. );
  63. const onClose = vi.fn();
  64. render(<SkipObjectsModal printerId={PRINTER_ID} isOpen onClose={onClose} />);
  65. return { onClose };
  66. }
  67. const row = (name: string) => screen.getByText(name).closest('button') as HTMLButtonElement;
  68. const submit = () => screen.getByRole('button', { name: 'Skip Selected' });
  69. describe('SkipObjectsModal wiring', () => {
  70. beforeEach(() => {
  71. permissions.granted = ['printers:control'];
  72. });
  73. it('skips the picked objects by their object ids', async () => {
  74. const user = userEvent.setup();
  75. const { onClose } = setup();
  76. await screen.findByText('Bracket A');
  77. await waitFor(() => expect(submit()).toBeDisabled());
  78. await user.click(row('Bracket A'));
  79. await user.click(row('Bracket C'));
  80. await waitFor(() => expect(submit()).toBeEnabled());
  81. await user.click(submit());
  82. // Confirm dialog: the confirm button carries the same label.
  83. expect(await screen.findByText('Skip 2 selected objects? This cannot be undone.')).toBeInTheDocument();
  84. const confirmButtons = screen.getAllByRole('button', { name: 'Skip Selected' });
  85. await user.click(confirmButtons[confirmButtons.length - 1]);
  86. await waitFor(() => expect(skipCalls).toHaveLength(1));
  87. expect(skipCalls[0].printerId).toBe(String(PRINTER_ID));
  88. expect([...(skipCalls[0].body as number[])].sort((a, b) => a - b)).toEqual([412, 903]);
  89. await waitFor(() => expect(onClose).toHaveBeenCalled());
  90. });
  91. it('sends nothing when the confirm is cancelled', async () => {
  92. const user = userEvent.setup();
  93. setup();
  94. await screen.findByText('Bracket A');
  95. await user.click(row('Bracket D'));
  96. await waitFor(() => expect(submit()).toBeEnabled());
  97. await user.click(submit());
  98. expect(await screen.findByText('Are you sure you want to skip "Bracket D"? This cannot be undone.')).toBeInTheDocument();
  99. const cancels = screen.getAllByRole('button', { name: 'Cancel' });
  100. await user.click(cancels[cancels.length - 1]);
  101. await waitFor(() => expect(screen.queryByText(/Are you sure you want to skip/)).toBeNull());
  102. expect(skipCalls).toEqual([]);
  103. });
  104. it('does not let an already-skipped object be picked', async () => {
  105. const user = userEvent.setup();
  106. setup();
  107. await screen.findByText('Bracket B');
  108. expect(row('Bracket B')).toBeDisabled();
  109. await user.click(row('Bracket B'));
  110. expect(screen.getByText('No objects selected')).toBeInTheDocument();
  111. expect(submit()).toBeDisabled();
  112. });
  113. it('select all picks only the objects still printing', async () => {
  114. const user = userEvent.setup();
  115. setup();
  116. await screen.findByText('Bracket A');
  117. await user.click(screen.getByRole('button', { name: /Select All/ }));
  118. await waitFor(() => expect(submit()).toBeEnabled());
  119. await user.click(submit());
  120. // All remaining objects selected -> the stronger warning.
  121. expect(await screen.findByText(/All remaining objects are selected/)).toBeInTheDocument();
  122. const confirmButtons = screen.getAllByRole('button', { name: 'Skip Selected' });
  123. await user.click(confirmButtons[confirmButtons.length - 1]);
  124. await waitFor(() => expect(skipCalls).toHaveLength(1));
  125. expect([...(skipCalls[0].body as number[])].sort((a, b) => a - b)).toEqual([412, 903, 1207]);
  126. });
  127. it('keeps submit disabled without printers:control', async () => {
  128. permissions.granted = [];
  129. const user = userEvent.setup();
  130. setup();
  131. await screen.findByText('Bracket A');
  132. await user.click(row('Bracket A'));
  133. expect(screen.getByText('1/3', { selector: 'footer span' })).toBeInTheDocument();
  134. expect(submit()).toBeDisabled();
  135. await user.click(submit());
  136. expect(screen.queryByText(/Are you sure you want to skip/)).toBeNull();
  137. expect(skipCalls).toEqual([]);
  138. });
  139. it('keeps submit disabled until the print is past layer 1', async () => {
  140. const user = userEvent.setup();
  141. setup({ layer: 1 });
  142. expect(await screen.findByText('Wait for layer 2+ to skip objects (currently layer 1)')).toBeInTheDocument();
  143. await user.click(row('Bracket A'));
  144. expect(submit()).toBeDisabled();
  145. await user.click(submit());
  146. expect(skipCalls).toEqual([]);
  147. });
  148. });