Sprint D: hardening e2e, /search redirect, scroll restore fix.
CI / skip-ci-check (pull_request) Successful in 29s
CI / docker-ci (pull_request) Successful in 32s
CI / python-lint (pull_request) Successful in 32s
CI / secret-scan (pull_request) Successful in 37s
CI / viewer-unit (pull_request) Successful in 1m53s
CI / admin-unit (pull_request) Successful in 2m39s
CI / e2e (pull_request) Failing after 3m19s

Admin review smoke tests, CWV budgets, unified gallery at /, and explicit
body unlock when closing the photo modal so scroll position restores.
This commit is contained in:
2026-08-04 22:17:43 -04:00
parent 071bda243b
commit 26faf33a95
15 changed files with 265 additions and 342 deletions
+55
View File
@@ -0,0 +1,55 @@
import { test, expect } from '../fixtures';
import { AdminLoginPage } from '../pages/AdminLoginPage';
import { BUDGET_MS, expectWithinBudget } from '../timing-budgets';
import { DEFAULT_ADMIN_BASE_URL } from '../env-defaults';
/**
* FastAPI-admin UI smoke: core review workflows load without error.
* Uses E2E_API_USERNAME/PASSWORD (admin FastAPI user — not NextAuth viewer creds).
*/
test.describe('admin review pages @smoke', () => {
const adminBaseUrl = process.env.PLAYKIT_ADMIN_BASE_URL || DEFAULT_ADMIN_BASE_URL;
test('identify, auto-match, and approve pages load for admin', async ({
page,
timings,
}) => {
const username = process.env.E2E_API_USERNAME || '';
const password = process.env.E2E_API_PASSWORD || '';
test.skip(!username || !password, 'E2E_API_USERNAME/PASSWORD required for admin UI');
const login = new AdminLoginPage(page, adminBaseUrl);
await timings.measure('admin_login', async () => {
await login.openLogin();
await login.signIn(username, password);
await expect(page.getByRole('heading', { name: /Home Page/i })).toBeVisible({
timeout: 20_000,
});
});
expectWithinBudget(timings, 'admin_login', BUDGET_MS.uiLogin);
await timings.measure('admin_identify', async () => {
await page.goto(`${adminBaseUrl}/identify`);
await expect(page.getByRole('heading', { name: /Identify/i })).toBeVisible();
await expect(page.getByRole('button', { name: /Identify Faces/i })).toBeVisible();
});
expectWithinBudget(timings, 'admin_identify', BUDGET_MS.uiAction);
await timings.measure('admin_auto_match', async () => {
await page.goto(`${adminBaseUrl}/auto-match`);
await expect(page.getByRole('heading', { name: /Auto-Match/i })).toBeVisible();
await expect(page.getByRole('button', { name: /Run Auto-Match/i })).toBeVisible();
});
expectWithinBudget(timings, 'admin_auto_match', BUDGET_MS.uiAction);
await timings.measure('admin_approve', async () => {
await page.goto(`${adminBaseUrl}/approve-identified`);
await expect(page.getByRole('heading', { name: /Approve Identified/i })).toBeVisible();
await expect(
page.getByText(/Total pending identifications/i).or(page.getByText(/Loading identified people/i)),
).toBeVisible({ timeout: 20_000 });
});
expectWithinBudget(timings, 'admin_approve', BUDGET_MS.uiAction);
});
});
+4 -4
View File
@@ -18,7 +18,7 @@ test.describe('gallery filters (authed) @smoke', () => {
timings,
}) => {
await timings.measure('open_search', async () => {
await page.goto(`${playkitConfig.baseUrl}/search`);
await page.goto(`${playkitConfig.baseUrl}/`);
});
expectWithinBudget(timings, 'open_search', BUDGET_MS.uiAction);
@@ -31,7 +31,7 @@ test.describe('gallery filters (authed) @smoke', () => {
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(),
page.getByText(/Found \d+ photo|Showing \d+ photo|No photos found/i).first(),
).toBeVisible({ timeout: 20_000 });
// Toggling back off should drop the param again.
@@ -54,7 +54,7 @@ test.describe('gallery filters (authed) @smoke', () => {
expect(person).toBeTruthy();
await timings.measure('open_search', async () => {
await page.goto(`${playkitConfig.baseUrl}/search`);
await page.goto(`${playkitConfig.baseUrl}/`);
});
expectWithinBudget(timings, 'open_search', BUDGET_MS.uiAction);
@@ -67,7 +67,7 @@ test.describe('gallery filters (authed) @smoke', () => {
await expect(page).toHaveURL(new RegExp(`people=${person.id}(&|$)`));
await expect(
page.getByText(/Found \d+ photos?|No photos found matching your filters/i).first(),
page.getByText(/Found \d+ photo|Showing \d+ photo|No photos found/i).first(),
).toBeVisible({ timeout: 20_000 });
});
});
+9 -2
View File
@@ -133,7 +133,7 @@ test.describe('gallery search filters @smoke', () => {
expect(tag).toBeTruthy();
await timings.measure('open_search', async () => {
await page.goto(`${playkitConfig.baseUrl}/search`);
await page.goto(`${playkitConfig.baseUrl}/`);
});
expectWithinBudget(timings, 'open_search', BUDGET_MS.uiAction);
@@ -158,7 +158,14 @@ test.describe('gallery search filters @smoke', () => {
await expect(page).toHaveURL(new RegExp(`tags=${tag.id}(&|$)`));
await expect(
page.getByText(/Found \d+ photos?|No photos found matching your filters/i).first(),
page.getByText(/Found \d+ photo|Showing \d+ photo|No photos found/i).first(),
).toBeVisible({ timeout: 20_000 });
});
test('/search legacy URL redirects to home with filters preserved', async ({ page, playkitConfig }) => {
await page.goto(`${playkitConfig.baseUrl}/search?tags=4&peopleMode=all`);
await expect(page).toHaveURL(/tags=4/);
await expect(page).toHaveURL(/peopleMode=all/);
expect(new URL(page.url()).pathname).toBe('/');
});
});
+2 -2
View File
@@ -42,9 +42,9 @@ test.describe('network errors (public pages) @smoke', () => {
const net = startNetworkErrorMonitor(page, { excludePatterns: excludeAnonSession });
try {
await timings.measure('search_tag_filter', async () => {
await page.goto(`${playkitConfig.baseUrl}/search?tags=${tag.id}`);
await page.goto(`${playkitConfig.baseUrl}/?tags=${tag.id}`);
await expect(
page.getByText(/Found \d+ photos?|No photos found matching your filters/i).first(),
page.getByText(/Found \d+ photo|Showing \d+ photo|No photos found/i).first(),
).toBeVisible({ timeout: 20_000 });
});
expectWithinBudget(timings, 'search_tag_filter', BUDGET_MS.uiAction);
+35
View File
@@ -0,0 +1,35 @@
import { test, expect } from '../fixtures';
import { BUDGET_MS, expectWithinBudget } from '../timing-budgets';
test.describe('gallery scroll restore @smoke', () => {
test('closing photo modal restores scroll position', async ({ page, playkitConfig, timings }) => {
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 });
});
expectWithinBudget(timings, 'open_gallery', BUDGET_MS.uiAction);
for (let i = 0; i < 4; i++) {
await page.mouse.wheel(0, 700);
await page.waitForTimeout(150);
}
const scrollBefore = await page.evaluate(() => window.scrollY);
expect(scrollBefore).toBeGreaterThan(200);
const photoButton = page.locator('main .aspect-square button').first();
await expect(photoButton).toBeVisible({ timeout: 15_000 });
await photoButton.click();
await expect(page.getByRole('dialog', { name: /Photo viewer/i })).toBeVisible({ timeout: 15_000 });
await expect(page).toHaveURL(/[?&]photo=\d+/, { timeout: 15_000 });
await page.getByRole('dialog', { name: /Photo viewer/i }).getByRole('button', { name: 'Close' }).click();
await expect(page.getByRole('dialog', { name: /Photo viewer/i })).toBeHidden({ timeout: 15_000 });
await expect(page).not.toHaveURL(/[?&]photo=\d+/, { timeout: 15_000 });
await expect
.poll(async () => page.evaluate(() => window.scrollY), { timeout: 5_000 })
.toBeGreaterThan(scrollBefore - 80);
});
});
+33
View File
@@ -0,0 +1,33 @@
import { test } from '../fixtures';
import { BUDGET_MS, expectWithinBudget } from '../timing-budgets';
import {
CWV_BUDGET,
expectWithinCwvBudget,
installWebVitalsCollector,
readWebVitals,
} from '../web-vitals-budgets';
test.describe('viewer web vitals @smoke', () => {
test.beforeEach(async ({ page }) => {
await installWebVitalsCollector(page);
});
test('home gallery LCP and CLS within budget', async ({ page, playkitConfig, timings }) => {
await timings.measure('open_gallery_cwv', async () => {
await page.goto(`${playkitConfig.baseUrl}/`);
await page
.getByLabel('Account menu')
.or(page.getByRole('button', { name: /Sign in/i }))
.first()
.waitFor({ state: 'visible', timeout: 20_000 });
});
expectWithinBudget(timings, 'open_gallery_cwv', BUDGET_MS.uiAction);
const vitals = await readWebVitals(page);
expectWithinCwvBudget(vitals);
test.info().annotations.push({
type: 'cwv',
description: `LCP=${Math.round(vitals.lcp)}ms CLS=${vitals.cls.toFixed(3)} budgets LCP<=${CWV_BUDGET.lcp} CLS<=${CWV_BUDGET.cls}`,
});
});
});