test: playkit v0.3.1 network monitors, authed filters, admin-frontend Vitest
Bump @levkin/playkit to v0.3.1 (network interception helpers) and land the work it unblocked: startNetworkErrorMonitor on gallery/search/upload/manage- users, Favorites + People filter UI e2e (storageState), and a fix for the lingering `res.data: unknown` / Mailpit-vs-Mailtrap type errors in the mail and catalog specs. Also adds Vitest coverage + a CI job for admin-frontend, which had no test infra before this.
This commit is contained in:
@@ -4,10 +4,15 @@ import { test, expect } from '../fixtures';
|
||||
const PeopleList = z.object({
|
||||
items: z.array(z.object({ id: z.number() }).passthrough()).min(1),
|
||||
});
|
||||
type PeopleList = z.infer<typeof PeopleList>;
|
||||
|
||||
const TagsList = z.object({
|
||||
items: z.array(z.object({ id: z.number() }).passthrough()).min(1),
|
||||
});
|
||||
type TagsList = z.infer<typeof TagsList>;
|
||||
|
||||
const ItemsList = z.object({ items: z.array(z.unknown()) });
|
||||
type ItemsList = z.infer<typeof ItemsList>;
|
||||
|
||||
/**
|
||||
* Public / lightly-gated FastAPI surfaces — shape + status contracts via playkit ApiClient.
|
||||
@@ -16,16 +21,16 @@ const TagsList = z.object({
|
||||
test.describe('api catalog @smoke', () => {
|
||||
test('GET /api/v1/people returns items list', async ({ api, timings }) => {
|
||||
const res = await timings.measure('api_people', () =>
|
||||
api.get('/api/v1/people', { expectedStatus: 200, schema: PeopleList }),
|
||||
api.get<PeopleList>('/api/v1/people', { expectedStatus: 200, schema: PeopleList }),
|
||||
);
|
||||
expect(res.data.items[0]).toEqual(expect.objectContaining({ id: expect.any(Number) }));
|
||||
});
|
||||
|
||||
test('GET /api/v1/people/with-faces returns items list', async ({ api, timings }) => {
|
||||
const res = await timings.measure('api_people_faces', () =>
|
||||
api.get('/api/v1/people/with-faces', {
|
||||
api.get<ItemsList>('/api/v1/people/with-faces', {
|
||||
expectedStatus: 200,
|
||||
schema: z.object({ items: z.array(z.unknown()) }),
|
||||
schema: ItemsList,
|
||||
}),
|
||||
);
|
||||
expect(Array.isArray(res.data.items)).toBe(true);
|
||||
@@ -33,7 +38,7 @@ test.describe('api catalog @smoke', () => {
|
||||
|
||||
test('GET /api/v1/tags returns items list', async ({ api, timings }) => {
|
||||
const res = await timings.measure('api_tags', () =>
|
||||
api.get('/api/v1/tags', { expectedStatus: 200, schema: TagsList }),
|
||||
api.get<TagsList>('/api/v1/tags', { expectedStatus: 200, schema: TagsList }),
|
||||
);
|
||||
expect(res.data.items[0]).toEqual(expect.objectContaining({ id: expect.any(Number) }));
|
||||
});
|
||||
|
||||
@@ -3,6 +3,8 @@ import {
|
||||
firstLinkMatching,
|
||||
readMailHtml,
|
||||
waitForUrlHost,
|
||||
type MailpitMessage,
|
||||
type MailtrapMessage,
|
||||
} from '@levkin/playkit';
|
||||
import { test, expect } from '../fixtures';
|
||||
|
||||
@@ -40,7 +42,7 @@ test.describe('mail @smoke', () => {
|
||||
);
|
||||
expect(res.ok()).toBeTruthy();
|
||||
|
||||
const msg = await timings.measure('mail_wait', () =>
|
||||
const msg = await timings.measure<MailpitMessage | MailtrapMessage>('mail_wait', () =>
|
||||
mail!.waitForEmail({
|
||||
to,
|
||||
subject: /reset|password/i,
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
import path from 'node:path';
|
||||
import { test, expect } from '../fixtures';
|
||||
|
||||
/**
|
||||
* Logged-in-only gallery filters: Favorites and People are hidden from the
|
||||
* `FilterPanel` for anonymous visitors (see `FilterPanel.tsx` `isLoggedIn`
|
||||
* gate), so these need the saved admin `storageState` from `auth.setup.ts`.
|
||||
* Public tag/person query-param filtering is covered without login in
|
||||
* `gallery.search-filters.spec.ts`.
|
||||
*/
|
||||
test.describe('gallery filters (authed) @smoke', () => {
|
||||
test.use({ storageState: path.join(__dirname, '../.auth/admin.json') });
|
||||
|
||||
test('favorites filter checkbox is visible and toggles the URL param', async ({
|
||||
page,
|
||||
playkitConfig,
|
||||
timings,
|
||||
}) => {
|
||||
await timings.measure('open_search', async () => {
|
||||
await page.goto(`${playkitConfig.baseUrl}/search`);
|
||||
});
|
||||
|
||||
const favoritesCheckbox = page.locator('#favorites-only');
|
||||
await expect(favoritesCheckbox).toBeVisible({ timeout: 20_000 });
|
||||
await expect(favoritesCheckbox).not.toBeChecked();
|
||||
|
||||
await page.getByText('Show favorites only').click();
|
||||
|
||||
await expect(favoritesCheckbox).toBeChecked();
|
||||
await expect(page).toHaveURL(/favoritesOnly=true/);
|
||||
await expect(
|
||||
page.getByText(/Found \d+ photos?|No photos found matching your filters/i).first(),
|
||||
).toBeVisible({ timeout: 20_000 });
|
||||
|
||||
// Toggling back off should drop the param again.
|
||||
await page.getByText('Show favorites only').click();
|
||||
await expect(favoritesCheckbox).not.toBeChecked();
|
||||
await expect(page).not.toHaveURL(/favoritesOnly=true/);
|
||||
});
|
||||
|
||||
test('people filter UI: selecting a person updates URL and result count', async ({
|
||||
page,
|
||||
playkitConfig,
|
||||
api,
|
||||
timings,
|
||||
}) => {
|
||||
const peopleRes = await api.get<{ items: Array<{ id: number; first_name: string; last_name: string }> }>(
|
||||
'/api/v1/people',
|
||||
{ expectedStatus: 200 },
|
||||
);
|
||||
const person = peopleRes.data.items[0];
|
||||
expect(person).toBeTruthy();
|
||||
|
||||
await timings.measure('open_search', async () => {
|
||||
await page.goto(`${playkitConfig.baseUrl}/search`);
|
||||
});
|
||||
|
||||
await page.getByRole('button', { name: /select people/i }).click();
|
||||
await page.getByPlaceholder('Search people...').fill(person.first_name);
|
||||
await page
|
||||
.getByText(`${person.first_name} ${person.last_name}`, { exact: true })
|
||||
.click();
|
||||
await page.keyboard.press('Escape');
|
||||
|
||||
await expect(page).toHaveURL(new RegExp(`people=${person.id}(&|$)`));
|
||||
await expect(
|
||||
page.getByText(/Found \d+ photos?|No photos found matching your filters/i).first(),
|
||||
).toBeVisible({ timeout: 20_000 });
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,84 @@
|
||||
import path from 'node:path';
|
||||
import { startNetworkErrorMonitor } from '@levkin/playkit';
|
||||
import { test, expect } from '../fixtures';
|
||||
|
||||
/**
|
||||
* Silent-failure guard: these pages can *look* fine in the UI while a
|
||||
* background XHR/fetch quietly 4xx/5xxs (the exact class of bug
|
||||
* `startNetworkErrorMonitor` exists to catch — see playkit `docs/NETWORK.md`).
|
||||
* Requires playkit >= 0.3.1 (network helpers landed after the 0.3.0 tag).
|
||||
*
|
||||
* `/api/auth/session` 401s on the anonymous project by design (no cookie) —
|
||||
* excluded rather than asserted on, since it's not the failure mode we're
|
||||
* guarding against here.
|
||||
*/
|
||||
const excludeAnonSession = [/\/api\/auth\/session/];
|
||||
|
||||
test.describe('network errors (public pages) @smoke', () => {
|
||||
test('gallery home stays clean', async ({ page, playkitConfig, timings }) => {
|
||||
const net = startNetworkErrorMonitor(page, { excludePatterns: excludeAnonSession });
|
||||
try {
|
||||
await timings.measure('open_gallery', async () => {
|
||||
await page.goto(`${playkitConfig.baseUrl}/`);
|
||||
await expect(page.getByLabel('Account menu').or(page.getByRole('button', { name: /Sign in/i })).first()).toBeVisible({
|
||||
timeout: 20_000,
|
||||
});
|
||||
});
|
||||
} finally {
|
||||
net.assertNoErrors();
|
||||
}
|
||||
});
|
||||
|
||||
test('search page with a tag filter stays clean', async ({ page, playkitConfig, api, timings }) => {
|
||||
const tagsRes = await api.get<{ items: Array<{ id: number; tag_name: string }> }>(
|
||||
'/api/v1/tags',
|
||||
{ expectedStatus: 200 },
|
||||
);
|
||||
const tag = tagsRes.data.items[0];
|
||||
expect(tag).toBeTruthy();
|
||||
|
||||
const net = startNetworkErrorMonitor(page, { excludePatterns: excludeAnonSession });
|
||||
try {
|
||||
await timings.measure('search_tag_filter', async () => {
|
||||
await page.goto(`${playkitConfig.baseUrl}/search?tags=${tag.id}`);
|
||||
await expect(
|
||||
page.getByText(/Found \d+ photos?|No photos found matching your filters/i).first(),
|
||||
).toBeVisible({ timeout: 20_000 });
|
||||
});
|
||||
} finally {
|
||||
net.assertNoErrors();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('network errors (authed) @smoke', () => {
|
||||
test.use({ storageState: path.join(__dirname, '../.auth/admin.json') });
|
||||
|
||||
test('upload page stays clean on load', async ({ page, playkitConfig, timings }) => {
|
||||
const net = startNetworkErrorMonitor(page);
|
||||
try {
|
||||
await timings.measure('open_upload', async () => {
|
||||
await page.goto(`${playkitConfig.baseUrl}/upload`);
|
||||
await expect(page.getByLabel('Account menu')).toBeVisible({ timeout: 20_000 });
|
||||
});
|
||||
} finally {
|
||||
net.assertNoErrors();
|
||||
}
|
||||
});
|
||||
|
||||
test('manage users overlay stays clean', async ({ page, playkitConfig, accountMenu, timings }) => {
|
||||
await page.goto(`${playkitConfig.baseUrl}/`);
|
||||
await expect(page.getByLabel('Account menu')).toBeVisible({ timeout: 20_000 });
|
||||
|
||||
const net = startNetworkErrorMonitor(page);
|
||||
try {
|
||||
await timings.measure('manage_users', async () => {
|
||||
await accountMenu.openManageUsers();
|
||||
await expect(page.getByRole('heading', { name: /Manage Users/i })).toBeVisible();
|
||||
await accountMenu.closeManageUsers();
|
||||
});
|
||||
} finally {
|
||||
net.assertNoErrors();
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -3,6 +3,8 @@ import {
|
||||
firstLinkMatching,
|
||||
readMailHtml,
|
||||
waitForUrlHost,
|
||||
type MailpitMessage,
|
||||
type MailtrapMessage,
|
||||
} from '@levkin/playkit';
|
||||
import { test, expect } from '../fixtures';
|
||||
|
||||
@@ -48,7 +50,7 @@ test.describe('auth register @mail', () => {
|
||||
).toBeVisible({ timeout: 20_000 });
|
||||
await waitForUrlHost(page, playkitConfig.expectedHost);
|
||||
|
||||
const msg = await timings.measure('mail_wait', () =>
|
||||
const msg = await timings.measure<MailpitMessage | MailtrapMessage>('mail_wait', () =>
|
||||
mail!.waitForEmail({
|
||||
to: email,
|
||||
subject: /confirm|verify|welcome|account/i,
|
||||
|
||||
@@ -3,6 +3,8 @@ import {
|
||||
firstLinkMatching,
|
||||
readMailHtml,
|
||||
waitForUrlHost,
|
||||
type MailpitMessage,
|
||||
type MailtrapMessage,
|
||||
} from '@levkin/playkit';
|
||||
import { test, expect } from '../fixtures';
|
||||
|
||||
@@ -36,7 +38,7 @@ test.describe('auth reset @mail', () => {
|
||||
});
|
||||
expect(res.ok()).toBeTruthy();
|
||||
|
||||
const msg = await timings.measure('mail_wait', () =>
|
||||
const msg = await timings.measure<MailpitMessage | MailtrapMessage>('mail_wait', () =>
|
||||
mail!.waitForEmail({ to, subject: /reset|password/i, after, timeoutMs: 90_000 }),
|
||||
);
|
||||
const html = await readMailHtml(mail!, msg as { ID?: string; id?: number; HTML?: string });
|
||||
|
||||
Reference in New Issue
Block a user