PrintModalCrossModel.test.tsx 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143
  1. /**
  2. * PrintModal in cross-model mode (#671).
  3. *
  4. * Selecting several sliced files puts the modal in model-based assignment with
  5. * no single target model. That combination used to fall through every gate the
  6. * override UI depends on, leaving the user with *less* control than the
  7. * ordinary "Any X1C" flow — no AMS mapping (correct, there is no printer yet)
  8. * and no filament override either (wrong).
  9. */
  10. import { describe, it, expect, beforeEach } from 'vitest';
  11. import { screen, waitFor } from '@testing-library/react';
  12. import { http, HttpResponse } from 'msw';
  13. import { render } from '../utils';
  14. import { server } from '../mocks/server';
  15. import { PrintModal } from '../../components/PrintModal';
  16. const CANDIDATES = [
  17. { id: 11, filename: 'x1c.gcode.3mf', sliced_for_model: 'X1C' },
  18. { id: 12, filename: 'h2d.gcode.3mf', sliced_for_model: 'H2D' },
  19. ];
  20. /** Loaded filaments differ per model — the union is what the user may pick.
  21. * The dropdown only ever offers the slot's own material (overriding PLA with
  22. * PETG is not a colour choice), so the spool that proves the union works has
  23. * to be a PLA the X1C does not have. */
  24. const BY_MODEL: Record<string, Array<Record<string, unknown>>> = {
  25. X1C: [{ type: 'PLA', color: '#FFFFFF', tray_info_idx: 'GFA00', tray_sub_brands: 'PLA Basic', extruder_id: null }],
  26. H2D: [
  27. { type: 'PLA', color: '#FFFFFF', tray_info_idx: 'GFA00', tray_sub_brands: 'PLA Basic', extruder_id: null },
  28. { type: 'PLA', color: '#00FF00', tray_info_idx: 'GFA01', tray_sub_brands: 'PLA Matte', extruder_id: null },
  29. ],
  30. };
  31. function mockBackend() {
  32. server.use(
  33. http.get('/api/v1/printers/', () =>
  34. HttpResponse.json([
  35. { id: 1, name: 'X1C-1', model: 'X1C', ip_address: '10.0.0.1', is_active: true, enabled: true },
  36. { id: 2, name: 'H2D-1', model: 'H2D', ip_address: '10.0.0.2', is_active: true, enabled: true },
  37. ]),
  38. ),
  39. http.get('/api/v1/printers/available-filaments', ({ request }) => {
  40. const model = new URL(request.url).searchParams.get('model') ?? '';
  41. return HttpResponse.json(BY_MODEL[model] ?? []);
  42. }),
  43. http.get('/api/v1/library/files/:id', ({ params }) =>
  44. HttpResponse.json({
  45. id: Number(params.id),
  46. filename: 'x1c.gcode.3mf',
  47. file_type: 'gcode.3mf',
  48. sliced_for_model: 'X1C',
  49. }),
  50. ),
  51. http.get('/api/v1/library/files/:id/plates', ({ params }) =>
  52. HttpResponse.json({ file_id: Number(params.id), filename: 'x', plates: [], is_multi_plate: false }),
  53. ),
  54. http.get('/api/v1/library/files/:id/filament-requirements', () =>
  55. HttpResponse.json({
  56. filaments: [{ slot_id: 1, type: 'PLA', color: '#FFFFFF', used_grams: 15, used_meters: 5 }],
  57. }),
  58. ),
  59. );
  60. }
  61. function renderCrossModel() {
  62. render(
  63. <PrintModal
  64. mode="create"
  65. libraryFileId={CANDIDATES[0].id}
  66. variantFiles={CANDIDATES}
  67. archiveName="bracket"
  68. onClose={() => {}}
  69. />,
  70. );
  71. }
  72. describe('PrintModal cross-model mode', () => {
  73. beforeEach(() => mockBackend());
  74. it('replaces the printer picker with the candidate list', async () => {
  75. renderCrossModel();
  76. expect(await screen.findByText('x1c.gcode.3mf')).toBeInTheDocument();
  77. expect(screen.getByText('h2d.gcode.3mf')).toBeInTheDocument();
  78. // Choosing these files already answered "which printer".
  79. expect(screen.queryByText('Select Printer')).not.toBeInTheDocument();
  80. });
  81. it('offers filament overrides drawn from every candidate model', async () => {
  82. renderCrossModel();
  83. expect(await screen.findByText('Filament Override')).toBeInTheDocument();
  84. // PLA Matte is loaded only on the H2D. It has to be offered anyway: the job
  85. // can land there, and choosing it simply narrows which candidates match.
  86. await waitFor(() => {
  87. const options = screen.getAllByRole('option').map((o) => o.textContent ?? '');
  88. expect(options.some((o) => o.includes('PLA Matte'))).toBe(true);
  89. expect(options.some((o) => o.includes('PLA Basic'))).toBe(true);
  90. });
  91. });
  92. it('shows a queued job its alternatives instead of a printer picker', async () => {
  93. // Before this, editing a cross-model item showed "Any H2D" with a live
  94. // Target Model dropdown and a Specific Printer toggle. Saving that left a
  95. // row with variants AND a printer_id, and the fixed-printer branch of the
  96. // scheduler wins — dispatching a row whose library_file_id is still null.
  97. render(
  98. <PrintModal
  99. mode="edit-queue-item"
  100. libraryFileId={CANDIDATES[0].id}
  101. archiveName="bracket"
  102. queueItem={
  103. {
  104. id: 9,
  105. printer_id: null,
  106. target_model: 'H2D',
  107. status: 'pending',
  108. variants: [
  109. { library_file_id: 12, filename: 'h2d.gcode.3mf', target_model: 'H2D', position: 0 },
  110. { library_file_id: 11, filename: 'x1c.gcode.3mf', target_model: 'X1C', position: 1 },
  111. ],
  112. } as never
  113. }
  114. onClose={() => {}}
  115. />,
  116. );
  117. expect(await screen.findByText('h2d.gcode.3mf')).toBeInTheDocument();
  118. expect(screen.getByText('x1c.gcode.3mf')).toBeInTheDocument();
  119. expect(screen.queryByText('Target Model')).not.toBeInTheDocument();
  120. // Read-only: reordering after queueing would need a variant-level API.
  121. expect(screen.queryByLabelText('Move down')).not.toBeInTheDocument();
  122. });
  123. it('shows no AMS slot mapping, because no printer has been chosen yet', async () => {
  124. renderCrossModel();
  125. await screen.findByText('x1c.gcode.3mf');
  126. // The scheduler derives the mapping against whichever printer it picks —
  127. // collecting tray numbers here would only be thrown away.
  128. expect(screen.queryByText('Filament Mapping')).not.toBeInTheDocument();
  129. });
  130. });