HMSErrorModalWiring.test.tsx 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147
  1. /**
  2. * HMSErrorModal wiring: the requests behind "Clear Errors" and the per-fault
  3. * action buttons (Resume / Retry / Ignore ...).
  4. *
  5. * HMSErrorModal.test.tsx covers rendering and the clear button's visibility;
  6. * this pins what is actually sent and to which printer.
  7. */
  8. import { describe, it, expect, vi } from 'vitest';
  9. import { screen, waitFor } from '@testing-library/react';
  10. import userEvent from '@testing-library/user-event';
  11. import { http, HttpResponse } from 'msw';
  12. import { server } from '../mocks/server';
  13. import { render } from '../utils';
  14. import { HMSErrorModal } from '../../components/HMSErrorModal';
  15. import type { HMSError } from '../../api/client';
  16. const PRINTER_ID = 42;
  17. // A 16-char hms[] fault with actions and a job id.
  18. const actionable: HMSError = {
  19. attr: 0x07000700,
  20. code: '0x20009',
  21. module: 7,
  22. severity: 2,
  23. full_code: '0700070000020009',
  24. description: 'AMS filament ran out.',
  25. actions: ['IGNORE_RESUME', 'RETRY_PROBLEM_SOLVED'],
  26. job_id: 'job-777',
  27. };
  28. // A print_error-style fault without full_code: the 8-char short code is sent.
  29. const shortOnly: HMSError = {
  30. attr: 0x0300,
  31. code: '0x400C',
  32. module: 3,
  33. severity: 1,
  34. description: 'The task was canceled.',
  35. actions: ['DBL_CHECK_RETRY'],
  36. };
  37. type Captured = { url: string; body: unknown };
  38. function capture() {
  39. const clears: string[] = [];
  40. const actions: Captured[] = [];
  41. server.use(
  42. http.post('/api/v1/printers/:id/hms/clear', ({ params }) => {
  43. clears.push(params.id as string);
  44. return HttpResponse.json({ success: true, message: 'ok' });
  45. }),
  46. http.post('/api/v1/printers/:id/hms/execute-action', async ({ request, params }) => {
  47. actions.push({ url: params.id as string, body: await request.json() });
  48. return HttpResponse.json({ success: true, message: 'ok' });
  49. }),
  50. );
  51. return { clears, actions };
  52. }
  53. const allow = (granted: boolean) =>
  54. vi.fn().mockReturnValue(granted) as unknown as (permission: 'printers:control') => boolean;
  55. function renderModal(errors: HMSError[], granted = true) {
  56. const onClose = vi.fn();
  57. const hasPermission = allow(granted);
  58. render(
  59. <HMSErrorModal
  60. printerName="Shop H2D"
  61. printerId={PRINTER_ID}
  62. errors={errors}
  63. onClose={onClose}
  64. hasPermission={hasPermission}
  65. />,
  66. );
  67. return { onClose, hasPermission };
  68. }
  69. describe('HMSErrorModal wiring', () => {
  70. it('Clear Errors posts hms/clear for this printer only', async () => {
  71. const user = userEvent.setup();
  72. const { clears, actions } = capture();
  73. const { onClose } = renderModal([actionable]);
  74. await user.click(screen.getByRole('button', { name: 'Clear Errors' }));
  75. await waitFor(() => expect(clears).toEqual([String(PRINTER_ID)]));
  76. expect(actions).toEqual([]);
  77. await waitFor(() => expect(onClose).toHaveBeenCalledTimes(1));
  78. });
  79. it('an action button sends the action, the full code and the job id', async () => {
  80. const user = userEvent.setup();
  81. const { clears, actions } = capture();
  82. const { onClose } = renderModal([actionable]);
  83. await user.click(screen.getByRole('button', { name: 'Ignore this and Resume' }));
  84. await waitFor(() => expect(actions).toHaveLength(1));
  85. expect(actions[0]).toEqual({
  86. url: String(PRINTER_ID),
  87. body: { action: 'IGNORE_RESUME', print_error: '0700070000020009', job_id: 'job-777' },
  88. });
  89. expect(clears).toEqual([]);
  90. await waitFor(() => expect(onClose).toHaveBeenCalledTimes(1));
  91. });
  92. it('falls back to the 8-char short code and a null job id', async () => {
  93. const user = userEvent.setup();
  94. const { actions } = capture();
  95. renderModal([shortOnly]);
  96. await user.click(screen.getByRole('button', { name: 'Retry' }));
  97. await waitFor(() => expect(actions).toHaveLength(1));
  98. expect(actions[0].body).toEqual({ action: 'DBL_CHECK_RETRY', print_error: '0300400C', job_id: null });
  99. });
  100. it('sends the action of the fault whose button was clicked', async () => {
  101. const user = userEvent.setup();
  102. const { actions } = capture();
  103. renderModal([actionable]);
  104. // Two buttons on the same fault: Retry, not Ignore.
  105. await user.click(screen.getByRole('button', { name: 'Retry (problem solved)' }));
  106. await waitFor(() => expect(actions).toHaveLength(1));
  107. expect((actions[0].body as { action: string }).action).toBe('RETRY_PROBLEM_SOLVED');
  108. });
  109. it('without printers:control, action and clear buttons are disabled and send nothing', async () => {
  110. const user = userEvent.setup();
  111. const { clears, actions } = capture();
  112. const { hasPermission } = renderModal([actionable], false);
  113. const ignore = screen.getByRole('button', { name: 'Ignore this and Resume' });
  114. const clear = screen.getByRole('button', { name: 'Clear Errors' });
  115. expect(ignore).toBeDisabled();
  116. expect(clear).toBeDisabled();
  117. expect(hasPermission).toHaveBeenCalledWith('printers:control');
  118. await user.click(ignore);
  119. await user.click(clear);
  120. // Give any stray request a chance to land.
  121. await new Promise((r) => setTimeout(r, 20));
  122. expect(actions).toEqual([]);
  123. expect(clears).toEqual([]);
  124. });
  125. });