useMultiPrinterFilamentMapping.test.tsx 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158
  1. /**
  2. * Regression tests for the cross-printer AMS mapping bug (#2799).
  3. *
  4. * A mapping entry is a global tray ID, and those only mean something on the
  5. * printer they were resolved against. The shared mapping panel only renders for
  6. * a single selected printer, so its edits belong to that printer alone — but
  7. * every selected printer used to receive them, and the match badge was computed
  8. * from them too. Two failures fall out of that: a printer prints from whatever
  9. * sits in the borrowed tray, and the panel reports a match it is not going to
  10. * submit.
  11. *
  12. * The reproduction is the real incident: the file needs light-blue PETG in slot
  13. * 1 and dark-blue PETG in slot 3. P2S-4 holds them in trays 2 and 1, P2S-5 in
  14. * trays 0 and 2, with ASA in tray 1.
  15. */
  16. import { afterEach, describe, expect, it, vi } from 'vitest';
  17. import { renderHook } from '@testing-library/react';
  18. import type { PrinterStatus } from '../../api/client';
  19. const statusByPrinter: Record<number, PrinterStatus> = {};
  20. vi.mock('@tanstack/react-query', () => ({
  21. useQueries: ({ queries }: { queries: { queryKey: unknown[] }[] }) => queries.map((q) => ({
  22. data: statusByPrinter[q.queryKey[1] as number],
  23. isLoading: false,
  24. })),
  25. }));
  26. vi.mock('../../api/client', () => ({ api: { getPrinterStatus: vi.fn() } }));
  27. const { useMultiPrinterFilamentMapping } = await import('../../hooks/useMultiPrinterFilamentMapping');
  28. const status = (trays: { type: string; color: string }[]): PrinterStatus =>
  29. ({
  30. ams: [{ id: 0, tray: trays.map((t, id) => ({ id, tray_type: t.type, tray_color: t.color })) }],
  31. }) as unknown as PrinterStatus;
  32. // Same spools, different slot order.
  33. const P2S_4 = status([
  34. { type: 'ASA', color: '161616FF' },
  35. { type: 'PETG', color: '2850E0FF' },
  36. { type: 'PETG', color: '76D9F4FF' },
  37. ]);
  38. const P2S_5 = status([
  39. { type: 'PETG', color: '76D9F4FF' },
  40. { type: 'ASA', color: '161616FF' },
  41. { type: 'PETG', color: '2850E0FF' },
  42. ]);
  43. const FILAMENT_REQS = {
  44. filaments: [
  45. { slot_id: 1, type: 'PETG', color: '#76D9F4' },
  46. { slot_id: 3, type: 'PETG', color: '#2850E0' },
  47. ],
  48. } as never;
  49. const printers = [
  50. { id: 9, name: 'P2S-4' },
  51. { id: 10, name: 'P2S-5' },
  52. ] as never;
  53. function render(
  54. selected: number[],
  55. defaultMappings: Record<number, number>,
  56. defaultMappingsPrinterId: number | null,
  57. ) {
  58. statusByPrinter[9] = P2S_4;
  59. statusByPrinter[10] = P2S_5;
  60. return renderHook(() =>
  61. useMultiPrinterFilamentMapping(
  62. selected,
  63. printers,
  64. FILAMENT_REQS,
  65. defaultMappings,
  66. {},
  67. vi.fn(),
  68. false,
  69. undefined,
  70. defaultMappingsPrinterId,
  71. ),
  72. );
  73. }
  74. describe('useMultiPrinterFilamentMapping — per-printer mapping (#2799)', () => {
  75. afterEach(() => {
  76. statusByPrinter[9] = P2S_4;
  77. statusByPrinter[10] = P2S_5;
  78. });
  79. it('maps each printer against its own trays when nothing was overridden', () => {
  80. const { result } = render([9, 10], {}, null);
  81. expect(result.current.getFinalMapping(9)).toEqual([2, -1, 1]);
  82. expect(result.current.getFinalMapping(10)).toEqual([0, -1, 2]);
  83. });
  84. it('does not lend one printer\'s shared mapping to another', () => {
  85. // Authored while only P2S-4 was selected: slot 1 -> tray 2, slot 3 -> tray 1.
  86. const shared = { 1: 2, 3: 1 };
  87. const { result } = render([9, 10], shared, 9);
  88. // Correct on the printer it was authored against...
  89. expect(result.current.getFinalMapping(9)).toEqual([2, -1, 1]);
  90. // ...and NOT applied to the other, where tray 1 is ASA.
  91. expect(result.current.getFinalMapping(10)).toEqual([0, -1, 2]);
  92. });
  93. it('keeps the shared mapping on the printer it was authored against', () => {
  94. // A deliberate choice that auto-matching would not make on its own:
  95. // slot 3 pinned to tray 0 (ASA) on P2S-4.
  96. const { result } = render([9, 10], { 3: 0 }, 9);
  97. expect(result.current.getFinalMapping(9)?.[2]).toBe(0);
  98. // The other printer is unaffected and still matches on type+colour.
  99. expect(result.current.getFinalMapping(10)).toEqual([0, -1, 2]);
  100. });
  101. it('reports a match badge derived from the mapping it will actually submit', () => {
  102. // Before the fix the badge for P2S-5 was computed from P2S-4's tray IDs and
  103. // claimed a full match while a different mapping went out.
  104. const { result } = render([9, 10], { 1: 2, 3: 1 }, 9);
  105. const p2s5 = result.current.printerResults.find((r) => r.printerId === 10)!;
  106. expect(p2s5.matchStatus).toBe('full');
  107. expect(p2s5.exactMatches).toBe(2);
  108. // The badge's own input agrees with the submitted mapping.
  109. expect(result.current.getFinalMapping(10)).toEqual([0, -1, 2]);
  110. });
  111. it('does not hand shared mappings to anyone when their owner is unknown', () => {
  112. // Editing a model-based queue item seeds the shared mappings from its
  113. // stored ams_mapping while its printer_id is null, so there is no owner.
  114. // "Unknown" must not read as "every printer".
  115. const { result } = render([9, 10], { 1: 2, 3: 1 }, null);
  116. expect(result.current.getFinalMapping(9)).toEqual([2, -1, 1]);
  117. expect(result.current.getFinalMapping(10)).toEqual([0, -1, 2]);
  118. });
  119. it('flags a printer that cannot satisfy a slot', () => {
  120. // Restored in afterEach — the next test to be added here would otherwise
  121. // inherit this two-tray P2S-5 and fail for a reason that is not its own.
  122. statusByPrinter[10] = status([
  123. { type: 'PETG', color: '76D9F4FF' },
  124. { type: 'ASA', color: '161616FF' },
  125. ]);
  126. const { result } = renderHook(() =>
  127. useMultiPrinterFilamentMapping([10], printers, FILAMENT_REQS, {}, {}, vi.fn(), false, undefined, null),
  128. );
  129. const p2s5 = result.current.printerResults[0];
  130. expect(p2s5.missingTypes).toBe(1);
  131. expect(p2s5.matchStatus).toBe('missing');
  132. expect(result.current.allPrintersReady).toBe(false);
  133. expect(result.current.getFinalMapping(10)?.[2]).toBe(-1);
  134. });
  135. });