Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -238,7 +238,7 @@ npm run test:unit:coverage # With coverage (run in CI)

Vitest has two projects (`vite.config.ts`):

- `client` — `*.svelte.test.ts` / `*.svelte.spec.ts`, run in headless Chromium (component and rune tests)
- `client` — `*.svelte.test.ts` / `*.svelte.spec.ts`, run in headless Chromium (component and rune tests). `src/test/setup-client.ts` loads `src/app.css`, so Tailwind/DaisyUI apply and hidden elements are really hidden. The viewport is 1280×800; test mobile layouts with `page.viewport()` and restore it afterwards
- `server` — all other `*.test.ts` / `*.spec.ts`, run in Node

Place unit tests next to the code they test. Add them for new server logic, stores and non-trivial components. Every test must assert (`requireAssertions`); use `src/lib/test-utils/mock-logger.ts` when testing server code that logs. The client project needs the Playwright Chromium browser.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,10 @@ describe('StorageSearch', () => {
render(StorageSearchWrapper, { state, currentBucket: 'alpha' });
await page.getByRole('button', { name: 'Open search' }).click();
await page.getByText('Advanced options').click();
await userEvent.type(page.getByLabelText('Max folder depth'), '1e');
// Wait for the collapse to finish opening; typing earlier leaves focus on the summary.
const depth = page.getByLabelText('Max folder depth');
await expect.element(depth).toBeVisible();
await userEvent.type(depth, '1e');
await expect.element(page.getByText('Enter a whole number from 1 to 20.')).toBeVisible();
});

Expand Down Expand Up @@ -249,7 +252,7 @@ describe('StorageSearch', () => {
await page.getByLabelText('Search query').fill('report');
await page.getByRole('button', { name: 'Search', exact: true }).click();
await expect.element(page.getByText('2 results ·')).toBeVisible();
await page.getByRole('button', { name: /reportsreports\// }).click();
await page.getByRole('button', { name: 'reports reports/', exact: true }).click();
expect(goto).toHaveBeenCalledWith('/storage/browse/s3.example.com/alpha/reports');

render(StorageSearchWrapper, { state, currentBucket: 'alpha' });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -179,10 +179,11 @@ describe('ContextMenu', () => {
selectedKeys: ['test.txt']
});
const spy = vi.spyOn(state, 'closeContextMenu');
render(ContextMenuWrapper, { state });
render(ContextMenuWrapper, { state, title: 'test.txt' });

const menu = page.getByRole('menu');
await menu.click();
// Click the non-interactive title: the menu's centre is a menu item, which would run an
// action and close the menu.
await page.getByRole('menu').getByText('test.txt').click();
expect(spy).not.toHaveBeenCalled();
});

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import '../../../../../app.css';
import { page } from 'vitest/browser';
import { describe, expect, it, vi } from 'vitest';
import { render } from 'vitest-browser-svelte';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,13 +87,15 @@ describe('FileRow', () => {
await expect.element(checkbox).not.toBeDisabled();
});

it('should have disabled checkbox when showCheckboxes is false', async () => {
it('should hide and disable the checkbox when showCheckboxes is false', async () => {
const file = makeFile({ key: 'file.txt', contentType: 'text/plain' });
const state = createState([file]);
render(FileRowWrapper, { state, file });

const checkbox = page.getByRole('checkbox', { name: 'Select file.txt' });
// Hidden with `invisible` outside selection mode, so it drops out of the accessibility tree.
const checkbox = page.getByRole('checkbox', { name: 'Select file.txt', includeHidden: true });
await expect.element(checkbox).toBeDisabled();
await expect.element(checkbox).not.toBeVisible();
});

it('should highlight row when selected', async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,13 +93,15 @@ describe('FolderRow', () => {
await expect.element(checkbox).not.toBeDisabled();
});

it('should have disabled checkbox when showCheckboxes is false', async () => {
it('should hide and disable the checkbox when showCheckboxes is false', async () => {
const folder = makeFolder('backup/');
const state = createState([folder]);
render(FolderRowWrapper, { state, folder });

const checkbox = page.getByRole('checkbox', { name: 'Select backup' });
// Hidden with `invisible` outside selection mode, so it drops out of the accessibility tree.
const checkbox = page.getByRole('checkbox', { name: 'Select backup', includeHidden: true });
await expect.element(checkbox).toBeDisabled();
await expect.element(checkbox).not.toBeVisible();
});

it('should highlight row when selected', async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -160,12 +160,14 @@ describe('ObjectTable', () => {
expect(spy).toHaveBeenCalledWith(true);
});

it('should disable select-all checkbox when not in selection mode', async () => {
it('should hide and disable the select-all checkbox when not in selection mode', async () => {
const state = createState([makeFile('a.txt')]);
render(ObjectTableWrapper, { state });

const checkbox = page.getByRole('checkbox', { name: 'Select all' });
// Hidden with `invisible` outside selection mode, so it drops out of the accessibility tree.
const checkbox = page.getByRole('checkbox', { name: 'Select all', includeHidden: true });
await expect.element(checkbox).toBeDisabled();
await expect.element(checkbox).not.toBeVisible();
});

it('should render many items', async () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { page, userEvent } from 'vitest/browser';
import { page, userEvent, type Locator } from 'vitest/browser';
import { describe, expect, it, vi } from 'vitest';
import { render } from 'vitest-browser-svelte';
import { faker } from '@faker-js/faker';
Expand Down Expand Up @@ -26,6 +26,21 @@ function makeFiles(count: number): StorageObject[] {
}));
}

/**
* Waits until the element's centre is not covered or clipped, i.e. a pointer would hit it.
* Playwright's visibility check ignores clipping by an `overflow-hidden` ancestor.
*/
async function expectPointerReachable(locator: Locator) {
await expect
.poll(() => {
const el = locator.element();
const rect = el.getBoundingClientRect();
const hit = document.elementFromPoint(rect.x + rect.width / 2, rect.y + rect.height / 2);
return hit !== null && el.contains(hit);
})
.toBe(true);
}

function createState(
opts: {
bucket?: string;
Expand Down Expand Up @@ -284,9 +299,9 @@ describe('StorageBreadcrumb', () => {
const spy = vi.spyOn(state, 'navigate');
render(StorageBreadcrumbWrapper, { state });

// Click 'alpha' in the collapsed dropdown
const alphaBtn = page.getByText('alpha');
await alphaBtn.click();
// Open the collapsed dropdown, then click 'alpha' in it
await page.getByRole('button', { name: 'More folders' }).click();
await page.getByRole('button', { name: 'alpha', exact: true }).click();
expect(spy).toHaveBeenCalledWith('alpha/');
});

Expand Down Expand Up @@ -320,7 +335,10 @@ describe('StorageBreadcrumb', () => {
const spy = vi.spyOn(state.bookmarks, 'pin');
render(StorageBreadcrumbWrapper, { state });

const pinBtn = page.getByRole('button', { name: /pin/i }).first();
// The pin button's wrapper only expands (after a delay) while its segment is hovered.
await page.getByText('test-bucket').hover();
const pinBtn = page.getByRole('button', { name: 'Pin this location' });
await expectPointerReachable(pinBtn);
await pinBtn.click();
expect(spy).toHaveBeenCalledWith('test-bucket', '');
});
Expand All @@ -333,7 +351,9 @@ describe('StorageBreadcrumb', () => {
const spy = vi.spyOn(state.bookmarks, 'unpin');
render(StorageBreadcrumbWrapper, { state });

const pinBtn = page.getByRole('button', { name: /unpin/i }).first();
await page.getByText('test-bucket').hover();
const pinBtn = page.getByRole('button', { name: 'Unpin' });
await expectPointerReachable(pinBtn);
await pinBtn.click();
expect(spy).toHaveBeenCalledWith('test-bucket', '');
});
Expand Down Expand Up @@ -445,8 +465,8 @@ describe('StorageBreadcrumb', () => {
const state = createState({ prefix: 'alpha/bravo/charlie/delta/' });
render(StorageBreadcrumbWrapper, { state });

const partEl = page.getByText('alpha');
await partEl.click({ button: 'right' });
await page.getByRole('button', { name: 'More folders' }).click();
await page.getByRole('button', { name: 'alpha', exact: true }).click({ button: 'right' });

const menuItem = page.getByRole('menuitem');
await expect.element(menuItem.first()).toBeInTheDocument();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -141,14 +141,28 @@ describe('StorageConnectForm', () => {
await expect.element(page.getByText('My MinIO').first()).toBeInTheDocument();
});

it('should show delete buttons for saved connections', async () => {
it('should offer forgetting a saved connection via its options menu', async () => {
renderForm({}, [
makeConnection({ id: 'conn-1', name: 'Test S3', endpoint: 'https://s3.test.io' })
]);

await expect
.element(page.getByRole('button', { name: 'Forget connection to Test S3' }))
.toBeInTheDocument();
await page.getByRole('button', { name: 'More options' }).click();
await expect.element(page.getByRole('menuitem', { name: 'Forget' })).toBeVisible();
});

it('should show delete buttons for saved connections on mobile', async () => {
await page.viewport(414, 896);
try {
renderForm({}, [
makeConnection({ id: 'conn-1', name: 'Test S3', endpoint: 'https://s3.test.io' })
]);

await expect
.element(page.getByRole('button', { name: 'Forget connection to Test S3' }))
.toBeVisible();
} finally {
await page.viewport(1280, 800); // default from vite.config.ts
}
});

it('should render multiple saved connections', async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -478,7 +478,7 @@ describe('PreviewModal basics', () => {
await expect.element(page.getByText('file.parquet')).toBeInTheDocument();

// Click the Data tab
await page.getByRole('tab', { name: 'Data' }).first().click();
await page.getByRole('tab', { name: 'Data', exact: true }).click();

// Parquet preview table should be visible with column headers
await expect
Expand Down
2 changes: 2 additions & 0 deletions src/test/setup-client.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import '../app.css';

import { vi, beforeEach } from 'vitest';

// Mock client feature flags and config so tests don't try to resolve the
Expand Down
3 changes: 2 additions & 1 deletion vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,8 @@ export default defineConfig({
browser: {
enabled: true,
provider: playwright(),
instances: [{ browser: 'chromium', headless: true }]
instances: [{ browser: 'chromium', headless: true }],
viewport: { width: 1280, height: 800 }
},
include: ['src/**/*.svelte.{test,spec}.{js,ts}'],
exclude: ['src/lib/server/**', 'src/architecture-tests/**'],
Expand Down
Loading