PlatePickerModal.test.tsx 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100
  1. /**
  2. * Tests for PlatePickerModal.
  3. *
  4. * The modal lets the user pick a plate before the GCode viewer opens.
  5. * Only shown for multi-plate archives with sliced gcode.
  6. */
  7. import { describe, it, expect, vi } from 'vitest';
  8. import { screen } from '@testing-library/react';
  9. import userEvent from '@testing-library/user-event';
  10. import { render } from '../utils';
  11. import { PlatePickerModal } from '../../components/PlatePickerModal';
  12. import type { PlateMetadata } from '../../types/plates';
  13. const makePlate = (overrides: Partial<PlateMetadata>): PlateMetadata => ({
  14. index: 1,
  15. name: null,
  16. objects: [],
  17. object_count: 0,
  18. has_thumbnail: false,
  19. thumbnail_url: null,
  20. print_time_seconds: null,
  21. filament_used_grams: null,
  22. filaments: [],
  23. ...overrides,
  24. });
  25. describe('PlatePickerModal', () => {
  26. it('renders one row per plate with the plate label', () => {
  27. const plates = [makePlate({ index: 1 }), makePlate({ index: 2 }), makePlate({ index: 3 })];
  28. render(<PlatePickerModal plates={plates} onSelect={() => {}} onClose={() => {}} />);
  29. // Each plate index gets its own row — check all three are present.
  30. expect(screen.getByText(/plate 1/i)).toBeInTheDocument();
  31. expect(screen.getByText(/plate 2/i)).toBeInTheDocument();
  32. expect(screen.getByText(/plate 3/i)).toBeInTheDocument();
  33. });
  34. it('renders the plate name alongside the index when set', () => {
  35. const plates = [makePlate({ index: 4, name: 'Spinner Nose' })];
  36. render(<PlatePickerModal plates={plates} onSelect={() => {}} onClose={() => {}} />);
  37. // Label combines the plate number with the user-defined name.
  38. expect(screen.getByText(/spinner nose/i)).toBeInTheDocument();
  39. });
  40. it('passes the clicked plate index to onSelect', async () => {
  41. const user = userEvent.setup();
  42. const onSelect = vi.fn();
  43. const plates = [makePlate({ index: 7 }), makePlate({ index: 12 })];
  44. render(<PlatePickerModal plates={plates} onSelect={onSelect} onClose={() => {}} />);
  45. await user.click(screen.getByText(/plate 12/i));
  46. // The handler receives the raw plate index — that's what the URL param
  47. // needs (so `?plate=12` maps to the archive's plate_12.gcode).
  48. expect(onSelect).toHaveBeenCalledWith(12);
  49. });
  50. it('calls onClose when the backdrop is clicked', async () => {
  51. const user = userEvent.setup();
  52. const onClose = vi.fn();
  53. render(<PlatePickerModal plates={[makePlate({})]} onSelect={() => {}} onClose={onClose} />);
  54. // The outermost div is the backdrop; clicking it fires onClose.
  55. // Plate rows stop propagation so they can't accidentally close the modal.
  56. const backdrop = document.querySelector('[class*="fixed"]') as HTMLElement;
  57. expect(backdrop).toBeTruthy();
  58. await user.click(backdrop);
  59. expect(onClose).toHaveBeenCalled();
  60. });
  61. it('falls back to a layer-icon placeholder when a plate has no thumbnail', () => {
  62. const plates = [makePlate({ index: 1, has_thumbnail: false, thumbnail_url: null })];
  63. render(<PlatePickerModal plates={plates} onSelect={() => {}} onClose={() => {}} />);
  64. // No <img> rendered for the thumbnail; the placeholder div takes its slot.
  65. // This guards against a regression where a missing-thumbnail plate
  66. // accidentally renders a broken-image icon instead of the fallback.
  67. expect(screen.queryByRole('img')).not.toBeInTheDocument();
  68. });
  69. it('shows the thumbnail image when the plate has one', () => {
  70. const plates = [
  71. makePlate({
  72. index: 1,
  73. has_thumbnail: true,
  74. thumbnail_url: '/api/v1/archives/42/plate-thumbnail/1',
  75. }),
  76. ];
  77. render(<PlatePickerModal plates={plates} onSelect={() => {}} onClose={() => {}} />);
  78. // The <img> is present and its src was transformed by withStreamToken,
  79. // which appends ?token=... even on a bare placeholder — we just want the
  80. // base path preserved.
  81. const img = screen.getByAltText(/plate 1/i) as HTMLImageElement;
  82. expect(img.src).toContain('/api/v1/archives/42/plate-thumbnail/1');
  83. });
  84. });