FolderReadmePanel.test.tsx 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189
  1. /**
  2. * Tests for FolderReadmePanel (#1268).
  3. */
  4. import { describe, it, expect, beforeEach, vi } from 'vitest';
  5. import { screen, waitFor } from '@testing-library/react';
  6. import userEvent from '@testing-library/user-event';
  7. import { http, HttpResponse } from 'msw';
  8. import { render } from '../utils';
  9. import { FolderReadmePanel } from '../../components/FolderReadmePanel';
  10. import { server } from '../mocks/server';
  11. describe('FolderReadmePanel', () => {
  12. beforeEach(() => {
  13. // localStorage is a vi.fn() mock in setup.ts (no real persistence) and
  14. // calls/return values leak across tests — reset it so each test starts
  15. // with the collapse preference unset (expanded).
  16. vi.mocked(localStorage.getItem).mockReturnValue(null);
  17. vi.mocked(localStorage.setItem).mockClear();
  18. });
  19. it('renders nothing when the folder has no markdown (404)', async () => {
  20. server.use(
  21. http.get('/api/v1/library/folders/:id/readme', () =>
  22. HttpResponse.json({ detail: 'No markdown' }, { status: 404 }),
  23. ),
  24. );
  25. render(<FolderReadmePanel folderId={1} />);
  26. // Wait briefly so the query has time to resolve, then confirm no panel
  27. // chrome leaked into the DOM (the test render util mounts toast/provider
  28. // wrappers, so we can't assert `container.firstChild === null`).
  29. await waitFor(() => {
  30. expect(screen.queryByText('Truncated')).not.toBeInTheDocument();
  31. expect(document.querySelector('button[type="button"] svg.lucide-file-text')).toBeNull();
  32. });
  33. });
  34. it('renders markdown content and the filename when present', async () => {
  35. server.use(
  36. http.get('/api/v1/library/folders/:id/readme', () =>
  37. HttpResponse.json({
  38. filename: 'README.md',
  39. content: '# Robot model\n\nA cute robot.',
  40. truncated: false,
  41. }),
  42. ),
  43. );
  44. render(<FolderReadmePanel folderId={42} />);
  45. expect(await screen.findByText('README.md')).toBeInTheDocument();
  46. expect(await screen.findByRole('heading', { name: 'Robot model' })).toBeInTheDocument();
  47. expect(screen.getByText('A cute robot.')).toBeInTheDocument();
  48. });
  49. it('shows a Truncated chip when the API flags the content as clipped', async () => {
  50. server.use(
  51. http.get('/api/v1/library/folders/:id/readme', () =>
  52. HttpResponse.json({
  53. filename: 'description.md',
  54. content: 'very long content',
  55. truncated: true,
  56. }),
  57. ),
  58. );
  59. render(<FolderReadmePanel folderId={7} />);
  60. expect(await screen.findByText('Truncated')).toBeInTheDocument();
  61. });
  62. it('collapses to a reopen control and hides the content, persisting the choice (#2520)', async () => {
  63. server.use(
  64. http.get('/api/v1/library/folders/:id/readme', () =>
  65. HttpResponse.json({
  66. filename: 'README.md',
  67. content: '# Robot model\n\nA cute robot.',
  68. truncated: false,
  69. }),
  70. ),
  71. );
  72. const user = userEvent.setup();
  73. render(<FolderReadmePanel folderId={99} />);
  74. // Expanded by default: content visible.
  75. expect(await screen.findByRole('heading', { name: 'Robot model' })).toBeInTheDocument();
  76. // Collapse hides the markdown body...
  77. await user.click(screen.getByRole('button', { name: 'Hide README' }));
  78. await waitFor(() => {
  79. expect(screen.queryByRole('heading', { name: 'Robot model' })).not.toBeInTheDocument();
  80. });
  81. // ...and offers a reopen control + persists the choice.
  82. expect(screen.getAllByRole('button', { name: 'Show README' }).length).toBeGreaterThan(0);
  83. expect(localStorage.setItem).toHaveBeenCalledWith('fileManager.readmeCollapsed', '1');
  84. });
  85. it('starts collapsed when the persisted preference is collapsed (#2520)', async () => {
  86. vi.mocked(localStorage.getItem).mockImplementation((k) =>
  87. k === 'fileManager.readmeCollapsed' ? '1' : null,
  88. );
  89. server.use(
  90. http.get('/api/v1/library/folders/:id/readme', () =>
  91. HttpResponse.json({
  92. filename: 'README.md',
  93. content: '# Robot model\n\nA cute robot.',
  94. truncated: false,
  95. }),
  96. ),
  97. );
  98. render(<FolderReadmePanel folderId={5} />);
  99. // Reopen control is present; the markdown body is not rendered.
  100. expect((await screen.findAllByRole('button', { name: 'Show README' })).length).toBeGreaterThan(0);
  101. expect(screen.queryByRole('heading', { name: 'Robot model' })).not.toBeInTheDocument();
  102. });
  103. /**
  104. * The panel renders GFM through `remarkGfmNoAutolink` rather than
  105. * `remark-gfm`, because `remark-gfm` reaches a regex lookbehind that iOS
  106. * 16.0-16.3 cannot parse and that blanked the entire app (#2971). These pin
  107. * the two halves of that trade: every GFM feature a README uses still works,
  108. * and the one feature we gave up stays given up on purpose rather than
  109. * creeping back in with a future dependency bump.
  110. */
  111. describe('GFM support without autolink literals (#2971)', () => {
  112. const withReadme = (content: string) =>
  113. server.use(
  114. http.get('/api/v1/library/folders/:id/readme', () =>
  115. HttpResponse.json({ filename: 'README.md', content, truncated: false }),
  116. ),
  117. );
  118. it('renders GFM tables', async () => {
  119. withReadme('| Part | Filament |\n| --- | --- |\n| Body | PLA |');
  120. render(<FolderReadmePanel folderId={11} />);
  121. expect(await screen.findByRole('columnheader', { name: 'Part' })).toBeInTheDocument();
  122. expect(screen.getByRole('columnheader', { name: 'Filament' })).toBeInTheDocument();
  123. expect(screen.getByRole('cell', { name: 'Body' })).toBeInTheDocument();
  124. });
  125. it('renders GFM strikethrough', async () => {
  126. withReadme('Print at ~~0.2mm~~ 0.16mm.');
  127. const { container } = render(<FolderReadmePanel folderId={12} />);
  128. await waitFor(() => {
  129. expect(container.querySelector('del')).toHaveTextContent('0.2mm');
  130. });
  131. });
  132. it('renders GFM task lists', async () => {
  133. withReadme('- [x] Sliced\n- [ ] Printed');
  134. render(<FolderReadmePanel folderId={13} />);
  135. const boxes = await screen.findAllByRole('checkbox');
  136. expect(boxes).toHaveLength(2);
  137. expect(boxes[0]).toBeChecked();
  138. expect(boxes[1]).not.toBeChecked();
  139. });
  140. it('renders GFM footnotes', async () => {
  141. withReadme('Supports supports[^1]\n\n[^1]: Tree, 0.4mm.');
  142. const { container } = render(<FolderReadmePanel folderId={14} />);
  143. await waitFor(() => {
  144. expect(container.querySelector('a[href="#user-content-fn-1"]')).toBeInTheDocument();
  145. });
  146. expect(screen.getByText(/Tree, 0.4mm./)).toBeInTheDocument();
  147. });
  148. it('keeps core-markdown links working', async () => {
  149. withReadme('See [the model](https://example.com/model) and <https://example.com/raw>.');
  150. const { container } = render(<FolderReadmePanel folderId={15} />);
  151. expect(await screen.findByRole('link', { name: 'the model' })).toHaveAttribute(
  152. 'href',
  153. 'https://example.com/model',
  154. );
  155. expect(container.querySelector('a[href="https://example.com/raw"]')).toBeInTheDocument();
  156. });
  157. it('leaves a bare URL as plain text - the deliberate cost of dropping autolink literals', async () => {
  158. withReadme('Grab it from https://example.com/model today.');
  159. const { container } = render(<FolderReadmePanel folderId={16} />);
  160. // Wait for the body to render before asserting on an absence.
  161. expect(await screen.findByText(/Grab it from/)).toBeInTheDocument();
  162. expect(container.querySelector('a')).toBeNull();
  163. });
  164. it('leaves a bare email as plain text', async () => {
  165. withReadme('Questions to nobody@example.com please.');
  166. const { container } = render(<FolderReadmePanel folderId={17} />);
  167. expect(await screen.findByText(/Questions to/)).toBeInTheDocument();
  168. expect(container.querySelector('a')).toBeNull();
  169. });
  170. });
  171. });