Files
Oxicloud/frontend/src/routes/groups/page.test.ts
T
Bradley Nelson e3823ce470 test(e2e): Playwright + Vitest coverage harness and test instrumentation
Add an end-to-end and unit test suite for the SvelteKit frontend:

- Playwright e2e specs (tests/e2e/spa) with a throwaway container stack,
  codegen scenarios, and an Istanbul-based coverage report pipeline.
- Vitest unit tests across API endpoints, components, stores and composables.
- `data-testid` hooks on interactive elements (AppShell, FileViewer,
  ShareDialog, search, photos, files breadcrumbs, login/Nextcloud flows,
  public share pages) so the e2e suite can target them deterministically.
- Serve the SPA app-shell CSP from a <meta> policy (svelte.config.js) plus a
  middleware that skips the CSP header on HTML; move the Nextcloud Login Flow
  v2 grant page to the SvelteKit /nextcloud/login route.
- `just front-codegen` recipe and start-server-spa.sh harness.

Make the test environment robust and consistent:
- Install a deterministic in-memory localStorage/sessionStorage in the Vitest
  setup so storage behaves identically across Node versions (Node 26 ships a
  native Web Storage global that otherwise shadows jsdom's).
- Pin devenv to Node 26 + PostgreSQL 18 and pin every CI job to Node 26.3.0
  so the dev shell and CI run the same toolchain versions.

Repair the API/WebDAV (hurl) suite, which had drifted from the backend:
- Migrate the removed `/api/folders/{id}/listing` endpoint to `/resources`
  (cursor-paginated `{items:[{resource_type,resource}]}` shape) across the
  batch-copy, grants, nested-group, and WebDAV NC tests + the dav_helpers
  wipe routine.
- Stop photos_etag from uploading the dedup-tracked fixture so the dedup
  blob-lifecycle test can own its content-addressed blob exclusively.
- dedup_create now asserts the idempotent same-content re-upload (201 +
  existing file id) instead of the stale 409 expectation.

Generated coverage reports, nyc output and the e2e server runtime data dir
are gitignored rather than committed.
2026-06-22 00:05:06 -06:00

57 lines
2.0 KiB
TypeScript

import { it, expect, vi, beforeEach } from 'vitest';
import { render, screen, fireEvent, waitFor } from '@testing-library/svelte';
const { ui, promptDialog } = vi.hoisted(() => ({ ui: { notify: vi.fn() }, promptDialog: vi.fn() }));
vi.mock('$lib/stores/ui.svelte', () => ({ ui }));
vi.mock('$lib/stores/dialogs.svelte', () => ({ promptDialog }));
vi.mock('$lib/api/endpoints/groups', () => ({
INTERNAL_GROUP_ID: '00000000-0000-0000-0000-000000000001',
createGroup: vi.fn(),
deleteGroup: vi.fn(),
addGroupMember: vi.fn(),
addUserMember: vi.fn(),
groupDescription: (g: { description?: string | null }) => g.description ?? null,
groupDisplayName: (g: { name: string }) => g.name,
groupIconName: () => 'fa-users',
listGroupsPage: vi.fn(),
listMembers: vi.fn(),
removeGroupMember: vi.fn(),
removeUserMember: vi.fn(),
renameGroup: vi.fn()
}));
vi.mock('$lib/api/endpoints/recipients', () => ({
ensureResolvers: vi.fn(),
resolveRecipient: (_t: string, id: string) => ({ id, label: id }),
searchRecipients: vi.fn(async () => [])
}));
import { listGroupsPage, createGroup, listMembers } from '$lib/api/endpoints/groups';
import GroupsPage from './+page.svelte';
const m = (fn: unknown) => fn as ReturnType<typeof vi.fn>;
beforeEach(() => {
vi.clearAllMocks();
m(listMembers).mockResolvedValue([]);
});
it('renders groups returned by the API', async () => {
m(listGroupsPage).mockResolvedValue({
items: [{ id: 'g1', name: 'Engineers', member_count: 2 }],
total: 1
});
render(GroupsPage);
await waitFor(() => expect(listGroupsPage).toHaveBeenCalled());
await waitFor(() => expect(screen.getByText('Engineers')).toBeTruthy());
});
it('creates a new group', async () => {
m(listGroupsPage).mockResolvedValue({ items: [], total: 0 });
m(createGroup).mockResolvedValue(undefined);
promptDialog.mockResolvedValue('New Team');
render(GroupsPage);
await waitFor(() => expect(listGroupsPage).toHaveBeenCalled());
await fireEvent.click(screen.getByTestId('groups-create-btn'));
await waitFor(() => expect(createGroup).toHaveBeenCalledWith('New Team'));
});