open-design/apps/web/tests/components/DesignFilesPanel.test.tsx
Patrick A 9146dc1c57
fix(web): persist design files view state across navigation (#2303)
* fix(web): persist design-files view state across navigation

pageSize, sortKey, sortDir, and kindFilter reset on every navigation
because DesignFilesPanel remounts via key={projectId}. Persist them to
localStorage under od:design-files:view-state:v1:<projectId> so each
project's view prefs survive tab-switching.

- Read persisted state via lazy useState initializers (SSR-safe try/catch)
- Write back in a single useEffect keyed on all four values
- Scoped per-project so proj-a settings never bleed into proj-b
- Schema-guarded: invalid/missing fields fall through to defaults
- Red spec: apps/web/tests/components/DesignFilesPanel.view-state-persist.test.tsx

* fix(web): address review feedback on view-state persistence

- Add typeof window guard in readViewState for explicit SSR safety
- Consolidate 4 separate localStorage reads into a single useRef read at
  mount time; each lazy useState initializer now reads from savedViewState.current
  instead of re-parsing localStorage independently

* fix(web): harden design-files view-state persistence

- Validate restored kindFilter values against the current ProjectFileKind
  union via isProjectFileKind() so stale stored values from a prior schema
  are dropped silently instead of being cast unchecked.

- Introduce DEFAULT_SORT_KEY/SORT_DIR/PAGE_SIZE constants so the useState
  initialisers and the new validation guard share a single source of truth.

- Add viewStateHasMounted ref to skip the first-render write in the persist
  useEffect. Without this guard every project the user visits accumulates a
  default-value entry in localStorage on mount, growing stale-key garbage
  unboundedly and making future field additions silently inject defaults into
  every existing entry.

- Harden kindFilter test: replace the silent early-return-on-missing-trigger
  with expect(filterTrigger).not.toBeNull() so a render failure surfaces as
  a real test failure rather than a passing no-op.

* test(e2e): design files view state persists across navigation and reload

Adds a Playwright UI smoke test in e2e/ui/ that exercises the three key
guarantees of the view-state persistence fix:

  (a) Tab-away / tab-back: navigating to a file tab and returning remounts
      DesignFilesPanel (conditionally rendered); all four prefs (sortKey,
      sortDir, pageSize, kindFilter) are restored from localStorage.

  (b) Hard reload: localStorage survives page.reload(); prefs are intact on
      the next mount.

  (c) Per-project key isolation: a second project starts with defaults and
      does not inherit values from the first project's localStorage entry.

The test uses OD_PORT=18011 / OD_WEB_PORT=18012 to avoid port conflicts with
the default development ports.

Also fixes a race in DesignFilesPanel: the stale-kind cleanup useEffect was
running against an empty availableKinds set before the async file list arrived
on mount, which cleared a kindFilter correctly restored from localStorage.
Guard added: skip the cleanup when availableKinds is empty.

Red on origin/main (no persistence logic exists there); green on this branch.

* fix(e2e): address code-reviewer feedback on view-state-persist test

- Add data-testid='df-page-size-select' to per-page <select> in
  DesignFilesPanel (W2: decouple test from i18n string 'Show')
- Add StrictMode comment to viewStateHasMounted guard explaining
  the dev-mode double-write behaviour (W1: document the invariant)
- Switch nav-away from dblclick to single-click + Open button,
  matching the pattern used in app-design-files.test.ts (W4)
- Raise timeout from 60s to 90s for cold CI runners (W3)
- Unify seedTextFile/seedPngFile into shared seedFile helper (N3)
- Add home-hero-input assertion in gotoEntryHome (N2)
- Switch waitForPageSizeSelect to use data-testid (W2)

* test(e2e): split design-files persist into nav, reload, and per-project scenarios

* fix(web): tighten isPageSize to discrete option set, add invalid-value regression test

* fix(web): isolate DesignFilesPanel.test.tsx from persisted view-state key
2026-05-30 03:39:27 +00:00

635 lines
22 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// @vitest-environment jsdom
import { act, cleanup, fireEvent, render, screen, waitFor, within } from '@testing-library/react';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { DesignFilesPanel } from '../../src/components/DesignFilesPanel';
import type { ProjectFile, ProjectFileKind } from '../../src/types';
// Stub localStorage so the component's view-state persistence writes to an
// in-memory store. Cleared in beforeEach so no test bleeds state into the next.
const lsStore = new Map<string, string>();
vi.stubGlobal('localStorage', {
getItem: (key: string) => lsStore.get(key) ?? null,
setItem: (key: string, value: string) => { lsStore.set(key, value); },
removeItem: (key: string) => { lsStore.delete(key); },
clear: () => { lsStore.clear(); },
});
function extForKind(kind: ProjectFileKind): string {
if (kind === 'html') return 'html';
if (kind === 'image') return 'png';
if (kind === 'sketch') return 'sketch.json';
if (kind === 'text') return 'txt';
if (kind === 'code') return 'ts';
if (kind === 'pdf') return 'pdf';
return 'bin';
}
function file(overrides: Partial<ProjectFile> & Pick<ProjectFile, 'name'>): ProjectFile {
return {
path: overrides.name,
type: 'file',
size: 1024,
mtime: Date.now(),
kind: 'html',
mime: 'text/html',
...overrides,
};
}
function generateFiles(count: number): ProjectFile[] {
const kinds: ProjectFileKind[] = ['html', 'image', 'sketch', 'text', 'code', 'pdf'];
return Array.from({ length: count }, (_, i) => {
const kind = kinds[i % kinds.length]!;
return file({
name: `file-${i + 1}.${extForKind(kind)}`,
kind,
size: 1024 * (i + 1),
mtime: Date.now() - i * 60_000,
mime: 'text/plain',
});
});
}
function renderPanel(files: ProjectFile[]) {
const onOpenFile = vi.fn();
const onDeleteFiles = vi.fn();
const result = render(
<DesignFilesPanel
projectId="test-project"
files={files}
liveArtifacts={[]}
onRefreshFiles={vi.fn()}
onOpenFile={onOpenFile}
onOpenLiveArtifact={vi.fn()}
onRenameFile={vi.fn()}
onDeleteFile={vi.fn()}
onDeleteFiles={onDeleteFiles}
onUpload={vi.fn()}
onUploadFiles={vi.fn()}
onPaste={vi.fn()}
onNewSketch={vi.fn()}
/>,
);
return { ...result, onDeleteFiles, onOpenFile };
}
function getPageInfo(container: HTMLElement): string {
const el = container.querySelector('.df-page-info');
return el?.textContent?.trim() ?? '';
}
/** page-btn order: bottom-Prev=0, bottom-Next=1 */
function getPageBtns(container: HTMLElement) {
return Array.from(container.querySelectorAll<HTMLButtonElement>('.df-page-btn'));
}
function getSelects(container: HTMLElement) {
return Array.from(container.querySelectorAll<HTMLSelectElement>('select'));
}
describe('DesignFilesPanel grouping', () => {
beforeEach(() => {
lsStore.clear();
});
afterEach(() => {
cleanup();
vi.useRealTimers();
});
it('does not show grouping controls when only live artifacts are available', () => {
render(
<DesignFilesPanel
projectId="project-1"
files={[]}
liveArtifacts={[
{
kind: 'live-artifact',
artifactId: 'artifact-1',
tabId: 'live:artifact-1',
projectId: 'project-1',
title: 'Live Preview',
slug: 'live-preview',
status: 'active',
refreshStatus: 'idle',
pinned: false,
preview: { type: 'html', entry: 'index.html' },
hasDocument: true,
updatedAt: '2026-05-09T12:00:00.000Z',
},
]}
onRefreshFiles={vi.fn()}
onOpenFile={vi.fn()}
onOpenLiveArtifact={vi.fn()}
onRenameFile={vi.fn()}
onDeleteFile={vi.fn()}
onDeleteFiles={vi.fn()}
onUpload={vi.fn()}
onUploadFiles={vi.fn()}
onPaste={vi.fn()}
onNewSketch={vi.fn()}
/>,
);
expect(screen.queryByRole('group', { name: 'Group by' })).toBeNull();
expect(screen.getByTestId('design-file-row-live:artifact-1')).toBeTruthy();
});
it('groups files by kind when kind grouping is selected', () => {
renderPanel([
file({ name: 'page.html', kind: 'html', mime: 'text/html' }),
file({ name: 'chart.png', kind: 'image', mime: 'image/png' }),
]);
const sectionLabels = Array.from(
document.querySelectorAll<HTMLElement>('.df-section-label'),
).map((el) => el.textContent ?? '');
expect(sectionLabels.some((text) => text.includes('HTML page'))).toBe(true);
expect(sectionLabels.some((text) => text.includes('Image'))).toBe(true);
expect(screen.getByTestId('design-file-row-page.html')).toBeTruthy();
expect(screen.getByTestId('design-file-row-chart.png')).toBeTruthy();
expect(screen.queryByText('Today')).toBeNull();
});
it('keeps kind grouping selected by default', () => {
renderPanel([
file({ name: 'page.html', kind: 'html', mime: 'text/html' }),
file({ name: 'chart.png', kind: 'image', mime: 'image/png' }),
]);
const groupControls = screen.getByRole('group', { name: 'Group by' });
const kindGroupButton = within(groupControls).getByRole('button', { name: 'Kind' });
expect(kindGroupButton.getAttribute('aria-pressed')).toBe('true');
expect(screen.getByText('Name')).toBeTruthy();
expect(document.querySelector('.df-th-kind')?.textContent).toContain('Kind');
expect(screen.queryByText('Today')).toBeNull();
});
it('can group files by modified date and collapse a date group', () => {
const now = new Date(2026, 4, 9, 12).getTime();
vi.useFakeTimers();
vi.setSystemTime(now);
renderPanel([
file({ name: 'today.html', mtime: new Date(2026, 4, 9, 11).getTime() }),
file({ name: 'yesterday.html', mtime: new Date(2026, 4, 8, 12).getTime() }),
]);
expect(screen.queryByText('Today')).toBeNull();
fireEvent.click(screen.getByRole('button', { name: 'Modified' }));
expect(screen.getByText('Today')).toBeTruthy();
expect(screen.getByText('Yesterday')).toBeTruthy();
expect(screen.getByTestId('design-file-row-today.html')).toBeTruthy();
expect(screen.getByTestId('design-file-row-yesterday.html')).toBeTruthy();
fireEvent.click(screen.getByRole('button', { name: /Collapse Today/i }));
expect(screen.queryByTestId('design-file-row-today.html')).toBeNull();
expect(screen.getByTestId('design-file-row-yesterday.html')).toBeTruthy();
});
it('keeps files from seven calendar days ago in the previous 7 days group', () => {
const now = new Date(2026, 4, 9, 12).getTime();
vi.useFakeTimers();
vi.setSystemTime(now);
renderPanel([file({ name: 'week-old.html', mtime: new Date(2026, 4, 2, 12).getTime() })]);
fireEvent.click(screen.getByRole('button', { name: 'Modified' }));
expect(screen.getByText('Previous 7 days')).toBeTruthy();
expect(screen.queryByText('Previous 30 days')).toBeNull();
expect(screen.getByTestId('design-file-row-week-old.html')).toBeTruthy();
});
it('keeps files at the seven calendar day boundary in the previous 7 days group', () => {
const now = new Date(2026, 4, 9, 12).getTime();
vi.useFakeTimers();
vi.setSystemTime(now);
renderPanel([
file({ name: 'week-boundary.html', mtime: new Date(2026, 4, 2, 0, 0, 0, 0).getTime() }),
]);
fireEvent.click(screen.getByRole('button', { name: 'Modified' }));
expect(screen.getByText('Previous 7 days')).toBeTruthy();
expect(screen.queryByText('Previous 30 days')).toBeNull();
expect(screen.getByTestId('design-file-row-week-boundary.html')).toBeTruthy();
});
it('keeps files from thirty calendar days ago in the previous 30 days group', () => {
const now = new Date(2026, 4, 9, 12).getTime();
vi.useFakeTimers();
vi.setSystemTime(now);
renderPanel([
file({ name: 'month-old.html', mtime: new Date(2026, 3, 9, 12).getTime() }),
]);
fireEvent.click(screen.getByRole('button', { name: 'Modified' }));
expect(screen.getByText('Previous 30 days')).toBeTruthy();
expect(screen.queryByText('Older')).toBeNull();
expect(screen.getByTestId('design-file-row-month-old.html')).toBeTruthy();
});
it('keeps files at the thirty calendar day boundary in the previous 30 days group', () => {
const now = new Date(2026, 4, 9, 12).getTime();
vi.useFakeTimers();
vi.setSystemTime(now);
renderPanel([
file({
name: 'month-boundary.html',
mtime: new Date(2026, 3, 9, 0, 0, 0, 0).getTime(),
}),
]);
fireEvent.click(screen.getByRole('button', { name: 'Modified' }));
expect(screen.getByText('Previous 30 days')).toBeTruthy();
expect(screen.queryByText('Older')).toBeNull();
expect(screen.getByTestId('design-file-row-month-boundary.html')).toBeTruthy();
});
it('groups files older than thirty calendar days into older', () => {
const now = new Date(2026, 4, 9, 12).getTime();
vi.useFakeTimers();
vi.setSystemTime(now);
renderPanel([file({ name: 'archive.html', mtime: new Date(2026, 3, 8, 12).getTime() })]);
fireEvent.click(screen.getByRole('button', { name: 'Modified' }));
expect(screen.getByText('Older')).toBeTruthy();
expect(screen.queryByText('Previous 30 days')).toBeNull();
expect(screen.getByTestId('design-file-row-archive.html')).toBeTruthy();
});
it('groups only the current page so large file lists stay paginated', () => {
const now = new Date(2026, 4, 9, 12).getTime();
vi.useFakeTimers();
vi.setSystemTime(now);
renderPanel(
Array.from({ length: 31 }, (_, i) =>
file({ name: `today-${String(i + 1).padStart(2, '0')}.html`, mtime: now - i }),
),
);
fireEvent.click(screen.getByRole('button', { name: 'Modified' }));
expect(screen.getByTestId('design-file-row-today-01.html')).toBeTruthy();
expect(screen.queryByTestId('design-file-row-today-31.html')).toBeNull();
expect(getPageInfo(document.body)).toContain('130 of 31');
});
it('updates modified date groups when the local day changes', () => {
vi.useFakeTimers();
vi.setSystemTime(new Date(2026, 4, 9, 23, 59, 50));
renderPanel([file({ name: 'late-edit.html', mtime: new Date(2026, 4, 9, 23, 59).getTime() })]);
fireEvent.click(screen.getByRole('button', { name: 'Modified' }));
expect(screen.getByText('Today')).toBeTruthy();
expect(screen.queryByText('Yesterday')).toBeNull();
act(() => {
vi.advanceTimersByTime(10_001);
});
expect(screen.getByText('Yesterday')).toBeTruthy();
expect(screen.queryByText('Today')).toBeNull();
expect(screen.getByTestId('design-file-row-late-edit.html')).toBeTruthy();
});
});
describe('DesignFilesPanel large-list regression', () => {
beforeEach(() => {
lsStore.clear();
});
afterEach(() => {
cleanup();
});
it('renders only the default page size (30) rows with 500 files', () => {
const files = generateFiles(500);
const { container } = renderPanel(files);
expect(container.querySelectorAll('.df-file-row').length).toBe(30);
});
it('shows all 500 rows when page size is set to All', () => {
const files = generateFiles(500);
const { container } = renderPanel(files);
const selects = getSelects(container);
fireEvent.change(selects[0]!, { target: { value: 'all' } });
expect(container.querySelectorAll('.df-file-row').length).toBe(500);
});
it('shows 60 rows when page size is changed to 60', () => {
const files = generateFiles(500);
const { container } = renderPanel(files);
const selects = getSelects(container);
fireEvent.change(selects[0]!, { target: { value: '60' } });
expect(container.querySelectorAll('.df-file-row').length).toBe(60);
});
it('navigates pages with Next button and updates row content', () => {
const files = generateFiles(500);
const { container } = renderPanel(files);
expect(container.querySelectorAll('.df-file-row').length).toBe(30);
expect(container.querySelector('.df-file-row')!.textContent).toContain('file-1');
const btns = getPageBtns(container);
fireEvent.click(btns[1]!);
expect(container.querySelectorAll('.df-file-row').length).toBe(30);
expect(container.querySelector('.df-file-row')!.textContent).toContain('file-31');
});
it('shows disabled Previous on first page and Next on last page', () => {
const files = generateFiles(45);
const { container } = renderPanel(files);
const btns = getPageBtns(container);
expect(btns[0]!.disabled).toBe(true);
expect(btns[1]!.disabled).toBe(false);
fireEvent.click(btns[1]!);
const btns2 = getPageBtns(container);
expect(btns2[0]!.disabled).toBe(false);
fireEvent.click(getPageBtns(container)[1]!);
fireEvent.click(getPageBtns(container)[1]!);
expect(getPageBtns(container)[1]!.disabled).toBe(true);
});
it('jumps to a specific page via page dropdown at bottom', () => {
const files = generateFiles(200);
const { container } = renderPanel(files);
const selects = getSelects(container);
fireEvent.change(selects[1]!, { target: { value: '3' } });
expect(container.querySelector('.df-file-row')!.textContent).toContain('file-91');
});
it('updates page info text when navigating', () => {
const files = generateFiles(500);
const { container } = renderPanel(files);
expect(getPageInfo(container)).toContain('130 of 500');
const btns = getPageBtns(container);
fireEvent.click(btns[1]!);
expect(getPageInfo(container)).toContain('3160 of 500');
});
it('keeps the bulk toolbar focused on the all-files action instead of duplicating page select', () => {
const { container } = renderPanel(generateFiles(20));
const toolbar = container.querySelector('.df-select-bar');
expect(toolbar?.textContent).toContain('Select everything');
expect(toolbar?.textContent).not.toContain('Select all on page');
});
it('hides redundant pagination controls for a single small page', () => {
const { container } = renderPanel(generateFiles(3));
expect(container.querySelector('.df-pagination')).toBeNull();
expect(container.querySelector('.df-page-btn')).toBeNull();
expect(container.querySelector('.df-select-bar')).toBeNull();
});
it('uses non-control table cells as file row click targets', () => {
const files = generateFiles(1);
const { container, onOpenFile } = renderPanel(files);
const row = container.querySelector('.df-file-row')!;
fireEvent.click(row.querySelector('.df-cell-icon')!);
expect(container.querySelector('[data-testid="design-file-preview"]')?.textContent).toContain(
'file-1.html',
);
fireEvent.click(row.querySelector('.df-cell-kind')!);
expect(container.querySelector('[data-testid="design-file-preview"]')?.textContent).toContain(
'file-1.html',
);
fireEvent.click(row.querySelector('.df-cell-name')!);
expect(container.querySelector('[data-testid="design-file-preview"]')?.textContent).toContain(
'file-1.html',
);
fireEvent.doubleClick(row.querySelector('.df-cell-name')!);
expect(onOpenFile).toHaveBeenCalledWith('file-1.html');
onOpenFile.mockClear();
fireEvent.doubleClick(row.querySelector('.df-cell-time')!);
expect(onOpenFile).toHaveBeenCalledWith('file-1.html');
});
it('does not preview or open files from row controls', () => {
const files = generateFiles(1);
const { container, onOpenFile } = renderPanel(files);
const row = container.querySelector('.df-file-row')!;
fireEvent.click(row.querySelector('.df-row-check')!);
expect(container.querySelector('[data-testid="design-file-preview"]')).toBeNull();
expect(onOpenFile).not.toHaveBeenCalled();
fireEvent.click(row.querySelector('.df-row-menu')!);
expect(container.querySelector('[data-testid="design-file-preview"]')).toBeNull();
expect(onOpenFile).not.toHaveBeenCalled();
});
it('renders sketch files with the static sketch preview instead of a broken image', async () => {
const fetchMock = vi.fn(async () => new Response(JSON.stringify({
version: 1,
items: [
{
kind: 'rect',
x: 20,
y: 16,
w: 120,
h: 72,
color: '#1c1b1a',
size: 2,
},
],
}), {
status: 200,
headers: { 'Content-Type': 'application/json; charset=utf-8' },
}));
vi.stubGlobal('fetch', fetchMock);
const sketchFile = file({
name: 'board.sketch.json',
path: 'board.sketch.json',
kind: 'sketch',
mime: 'application/json; charset=utf-8',
});
const { container } = renderPanel([sketchFile]);
fireEvent.click(container.querySelector('.df-file-row .df-row-name-btn')!);
await waitFor(() => {
expect(container.querySelector('[data-testid="sketch-preview-svg"]')).toBeTruthy();
});
expect(container.querySelector('.df-preview-thumb img')).toBeNull();
expect(fetchMock).toHaveBeenCalledWith('/api/projects/test-project/raw/board.sketch.json', { cache: 'no-store' });
});
it('passes every selected file to batch delete', () => {
const files = generateFiles(3);
const { container, onDeleteFiles } = renderPanel(files);
const rows = Array.from(container.querySelectorAll('.df-file-row'));
const firstName = rows[0]!.getAttribute('data-testid')!.replace(/^design-file-row-/, '');
const secondName = rows[1]!.getAttribute('data-testid')!.replace(/^design-file-row-/, '');
fireEvent.click(rows[0]!.querySelector('.df-row-check')!);
fireEvent.click(rows[1]!.querySelector('.df-row-check')!);
fireEvent.click(container.querySelector('[data-testid="design-files-batch-delete"]')!);
expect(onDeleteFiles).toHaveBeenCalledTimes(1);
expect(onDeleteFiles).toHaveBeenCalledWith([firstName, secondName]);
});
it('renders 500 files within a reasonable time', () => {
const files = generateFiles(500);
const start = performance.now();
renderPanel(files);
const elapsed = performance.now() - start;
expect(elapsed).toBeLessThan(2000);
});
});
describe('DesignFilesPanel directory navigation', () => {
afterEach(() => {
cleanup();
});
it('collapses nested files into a single folder row at root with correct descendant count', () => {
renderPanel([
file({ name: 'assets/logo.png', kind: 'image' }),
file({ name: 'assets/icons/star.svg', kind: 'image' }),
]);
const dirRows = document.querySelectorAll('.df-dir-row');
expect(dirRows.length).toBe(1);
expect(dirRows[0]!.textContent).toContain('assets');
expect(dirRows[0]!.textContent).toContain('2');
});
it('clicking a folder row navigates into it and shows only basenames and nested dirs', () => {
renderPanel([
file({ name: 'assets/logo.png', kind: 'image' }),
file({ name: 'assets/icons/star.svg', kind: 'image' }),
]);
fireEvent.click(document.querySelector('.df-dir-row .df-row-name-btn')!);
expect(document.querySelector('.df-breadcrumbs')).toBeTruthy();
expect(document.querySelector('.df-breadcrumb-current')?.textContent).toBe('assets');
const fileRow = screen.getByTestId('design-file-row-assets/logo.png');
expect(fileRow.querySelector('.df-row-name')?.textContent).toBe('logo.png');
expect(fileRow.querySelector('.df-row-name')?.textContent).not.toContain('assets/');
const dirRows = document.querySelectorAll('.df-dir-row');
expect(dirRows.length).toBe(1);
expect(dirRows[0]!.textContent).toContain('icons');
});
it('clicking the root breadcrumb navigates back to root', () => {
renderPanel([
file({ name: 'assets/logo.png', kind: 'image' }),
file({ name: 'top.html', kind: 'html' }),
]);
fireEvent.click(document.querySelector('.df-dir-row .df-row-name-btn')!);
expect(document.querySelector('.df-breadcrumbs')).toBeTruthy();
fireEvent.click(document.querySelector('.df-breadcrumb-btn')!);
expect(document.querySelector('.df-breadcrumbs')).toBeNull();
expect(screen.getByTestId('design-file-row-top.html')).toBeTruthy();
expect(document.querySelectorAll('.df-dir-row').length).toBe(1);
});
it('clears selection and resets page when navigating into or out of a directory', () => {
renderPanel([
file({ name: 'assets/logo.png', kind: 'image' }),
file({ name: 'top.html', kind: 'html' }),
]);
const topRow = screen.getByTestId('design-file-row-top.html');
fireEvent.click(topRow.querySelector('.df-row-check')!);
expect(topRow.classList.contains('selected')).toBe(true);
fireEvent.click(document.querySelector('.df-dir-row .df-row-name-btn')!);
expect(document.querySelectorAll('.df-file-row.selected').length).toBe(0);
fireEvent.click(document.querySelector('.df-breadcrumb-btn')!);
expect(document.querySelectorAll('.df-file-row.selected').length).toBe(0);
});
it('resets currentDir automatically when all files in the current subdirectory are removed', () => {
function makePanel(files: ProjectFile[]) {
return (
<DesignFilesPanel
projectId="test-project"
files={files}
liveArtifacts={[]}
onRefreshFiles={vi.fn()}
onOpenFile={vi.fn()}
onOpenLiveArtifact={vi.fn()}
onRenameFile={vi.fn()}
onDeleteFile={vi.fn()}
onDeleteFiles={vi.fn()}
onUpload={vi.fn()}
onUploadFiles={vi.fn()}
onPaste={vi.fn()}
onNewSketch={vi.fn()}
/>
);
}
const { rerender } = render(
makePanel([
file({ name: 'assets/logo.png', kind: 'image' }),
file({ name: 'top.html', kind: 'html' }),
]),
);
fireEvent.click(document.querySelector('.df-dir-row .df-row-name-btn')!);
expect(document.querySelector('.df-breadcrumb-current')?.textContent).toBe('assets');
rerender(makePanel([file({ name: 'top.html', kind: 'html' })]));
expect(document.querySelector('.df-breadcrumbs')).toBeNull();
expect(screen.getByTestId('design-file-row-top.html')).toBeTruthy();
});
it('does not show the select-all header as checked when the page contains only directory rows', () => {
renderPanel([
file({ name: 'assets/logo.png', kind: 'image' }),
]);
const headerCheck = document.querySelector('.df-th-check .df-row-check');
expect(headerCheck?.textContent).toBe('☐');
});
});