FileManagerSlicedNamedAsSource.test.tsx 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120
  1. /**
  2. * A sliced 3MF named as a project must still offer Print (#2993).
  3. *
  4. * Download an archive that carries the green GCODE badge, re-import it, and the
  5. * file came back without a Print button. Nothing was lost from the file -- the
  6. * download serves the stored bytes verbatim -- but the library decided what it
  7. * was from the filename, and a per-plate export or a cloud-dispatched print
  8. * arrives as `Foo.3mf` with its G-code intact.
  9. *
  10. * The backend has always been willing to print these (`library.py` takes both
  11. * `3mf` and `gcode.3mf` on the G-code path), so this was the UI refusing to
  12. * offer something that would have worked.
  13. */
  14. import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
  15. import { screen, waitFor, within } from '@testing-library/react';
  16. import { render } from '../utils';
  17. import { FileManagerPage } from '../../pages/FileManagerPage';
  18. import { http, HttpResponse } from 'msw';
  19. import { server } from '../mocks/server';
  20. const base = {
  21. file_path: '/library/x',
  22. file_size: 1048576,
  23. folder_id: null,
  24. thumbnail_path: null,
  25. print_name: null,
  26. print_time_seconds: null,
  27. print_count: 0,
  28. duplicate_count: 0,
  29. created_at: '2024-01-01T00:00:00Z',
  30. };
  31. /** All three named `.3mf`, so only file_type can tell them apart. */
  32. const files = [
  33. // The reported file: sliced, named as a project.
  34. { ...base, id: 1, filename: 'Labyrinth - Plate 3.3mf', file_type: 'gcode.3mf' },
  35. // A genuine project. Must NOT gain a Print button.
  36. { ...base, id: 2, filename: 'Labyrinth source.3mf', file_type: '3mf' },
  37. ];
  38. function serve(rows: unknown[]) {
  39. server.use(
  40. http.get('/api/v1/library/folders', () => HttpResponse.json([])),
  41. http.get('/api/v1/library/files', () => HttpResponse.json(rows)),
  42. http.get('/api/v1/library/stats', () =>
  43. HttpResponse.json({
  44. total_files: rows.length,
  45. total_folders: 0,
  46. total_size_bytes: 1,
  47. disk_free_bytes: 1,
  48. disk_total_bytes: 2,
  49. }),
  50. ),
  51. );
  52. }
  53. /** The row's own action strip, so one file's buttons can't answer for another. */
  54. async function rowFor(filename: string): Promise<HTMLElement> {
  55. const label = await screen.findByText(filename);
  56. const row = label.closest('div[class*="grid-cols-"]');
  57. if (!row) throw new Error(`no list row around ${filename}`);
  58. return row as HTMLElement;
  59. }
  60. describe('FileManagerPage — a sliced 3MF that is not named like one (#2993)', () => {
  61. beforeEach(() => {
  62. // localStorage is a module-global vi.fn mock (see __tests__/setup.ts), so
  63. // the view mode has to be programmed rather than written. List view is the
  64. // one that puts the row actions on screen without opening a kebab menu.
  65. (localStorage.getItem as ReturnType<typeof vi.fn>).mockImplementation((key: string) =>
  66. key === 'library-view-mode' ? 'list' : null,
  67. );
  68. serve(files);
  69. });
  70. afterEach(() => {
  71. (localStorage.getItem as ReturnType<typeof vi.fn>).mockReset();
  72. });
  73. it('offers Print for the file whose type says it is sliced', async () => {
  74. render(<FileManagerPage />);
  75. await waitFor(() => expect(screen.getByText('Labyrinth - Plate 3.3mf')).toBeInTheDocument());
  76. const row = await rowFor('Labyrinth - Plate 3.3mf');
  77. await waitFor(() => {
  78. expect(within(row).getByTitle('Print')).toBeInTheDocument();
  79. });
  80. });
  81. it('does not also offer to slice it, which would re-slice its own G-code', async () => {
  82. // The Slice gate refused a `.gcode.3mf` *name*, so without this the file
  83. // would gain a Print button and a Slice button at the same time.
  84. render(<FileManagerPage />);
  85. await waitFor(() => expect(screen.getByText('Labyrinth - Plate 3.3mf')).toBeInTheDocument());
  86. const row = await rowFor('Labyrinth - Plate 3.3mf');
  87. expect(within(row).queryByTitle('Slice')).not.toBeInTheDocument();
  88. });
  89. it('still offers Slice on the genuine project', async () => {
  90. render(<FileManagerPage />);
  91. await waitFor(() => expect(screen.getByText('Labyrinth source.3mf')).toBeInTheDocument());
  92. const row = await rowFor('Labyrinth source.3mf');
  93. expect(within(row).getByTitle('Slice')).toBeInTheDocument();
  94. });
  95. it('does not offer Print for a genuine project of the same shape', async () => {
  96. render(<FileManagerPage />);
  97. await waitFor(() => expect(screen.getByText('Labyrinth source.3mf')).toBeInTheDocument());
  98. const row = await rowFor('Labyrinth source.3mf');
  99. expect(within(row).queryByTitle('Print')).not.toBeInTheDocument();
  100. });
  101. });