Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
56fbe86
docs: document performance loading decisions
Programcode Sep 19, 2026
88540f5
docs: correct route transition guidance
Programcode Sep 19, 2026
59c1982
docs: define DebateLab loading UX rules
Programcode Sep 19, 2026
c68c6c3
test: capture smooth navigation baseline
Programcode Sep 19, 2026
56d57d1
test: correct smooth navigation baseline accounting
Programcode Sep 19, 2026
2291fa3
test: define smooth route loading contracts
Programcode Sep 19, 2026
299e646
test: refine smooth route loading contracts
Programcode Sep 19, 2026
69488b8
perf: share lazy route import promises
Programcode Sep 19, 2026
2f35374
fix: share cached route loader promises
Programcode Sep 20, 2026
38ed02a
perf: preload routes on user intent
Programcode Sep 20, 2026
2fe547c
fix: preserve route prefetch intent semantics
Programcode Sep 20, 2026
2dbb3f0
fix: cover route prefetch edge cases
Programcode Sep 20, 2026
dddc348
feat: add route-shaped loading skeletons
Programcode Sep 20, 2026
aff4c07
fix: separate skeleton status from busy region
Programcode Sep 20, 2026
f2c1f6a
perf: keep route loading inside page content
Programcode Sep 20, 2026
80098b8
fix: localize and verify route fallbacks
Programcode Sep 20, 2026
9b151c6
fix: normalize shared route fallbacks
Programcode Sep 20, 2026
77bb92f
perf: keep layout data stable across routes
Programcode Sep 20, 2026
368858a
fix: scope stable announcements by user
Programcode Sep 20, 2026
2e40767
fix: retry failed announcement loads
Programcode Sep 20, 2026
5b6a252
perf: defer onboarding tour until needed
Programcode Sep 20, 2026
30b1367
fix: preserve deferred onboarding timing
Programcode Sep 20, 2026
506cc6d
docs: refresh optional entry evidence
Programcode Sep 20, 2026
d692b02
Merge remote-tracking branch 'origin/main' into t3code/smooth-perform…
Programcode Sep 20, 2026
aa6a763
fix: start deferred onboarding from eligibility
Programcode Sep 20, 2026
68987d7
fix: align performance release artifacts
Programcode Sep 20, 2026
8f4cced
docs: align route prefetch fallback policy
Programcode Sep 20, 2026
779188f
fix: resolve performance release review findings
Programcode Sep 20, 2026
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
515 changes: 515 additions & 0 deletions docs/performance/performance-playbook.md

Large diffs are not rendered by default.

396 changes: 396 additions & 0 deletions docs/performance/smooth-navigation-baseline/README.md

Large diffs are not rendered by default.

Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file not shown.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file not shown.
6,975 changes: 6,975 additions & 0 deletions docs/performance/smooth-navigation-baseline/run-1.json

Large diffs are not rendered by default.

6,995 changes: 6,995 additions & 0 deletions docs/performance/smooth-navigation-baseline/run-2.json

Large diffs are not rendered by default.

6,914 changes: 6,914 additions & 0 deletions docs/performance/smooth-navigation-baseline/task-10-after-final.json

Large diffs are not rendered by default.

6,859 changes: 6,859 additions & 0 deletions docs/performance/smooth-navigation-baseline/task-10-before.json

Large diffs are not rendered by default.

78 changes: 77 additions & 1 deletion e2e/admin-announcements.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { expect, test } from '@playwright/test';
import { expect, test, type Page } from '@playwright/test';
import { stubSupabaseSession } from './helpers/auth';

const ANNOUNCEMENT_ID = 'aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee';
Expand All @@ -14,6 +14,29 @@ test.beforeEach(async ({ page }) => {
});
});

async function openAppNav(page: Page, label: string) {
if ((page.viewportSize()?.width ?? 1280) < 1024) {
await page.getByRole('button', { name: 'Open menu' }).click();
await page.getByRole('navigation', { name: 'Mobile navigation' })
.getByRole('link', { name: new RegExp(`^${label}`) })
.click();
return;
}

await page.getByRole('navigation', { name: 'Main navigation' })
.getByRole('link', { name: label })
.click();
}

function countAnnouncementListRequests(page: Page) {
let count = 0;
page.on('request', (request) => {
const url = new URL(request.url());
if (request.method() === 'GET' && url.pathname.endsWith('/functions/v1/announcements')) count += 1;
});
return () => count;
}

test('user sees and permanently dismisses an active announcement', async ({ page }) => {
let dismissBody: Record<string, unknown> | null = null;
await page.route('**/functions/v1/announcements*', async (route) => {
Expand Down Expand Up @@ -48,6 +71,59 @@ test('user sees and permanently dismisses an active announcement', async ({ page
expect(dismissBody).toEqual({ announcementId: ANNOUNCEMENT_ID });
});

test('keeps announcements stable across routes and refreshes on a full reload', async ({ page }) => {
// The auth session is a browser-only stub. This verifies client request sequencing, not backend auth.
const listRequestCount = countAnnouncementListRequests(page);
let listResponseCount = 0;
await page.route('**/functions/v1/announcements*', async (route) => {
if (route.request().method() !== 'GET') {
await route.fulfill({ json: { success: true } });
return;
}

listResponseCount += 1;
const current = listResponseCount === 1
? {
id: ANNOUNCEMENT_ID,
title: 'Scheduled maintenance',
body: 'DebateLab will be unavailable for ten minutes.',
starts_at: '2026-08-31T12:00:00.000Z',
expires_at: null,
created_at: '2026-08-31T11:00:00.000Z',
}
: {
id: 'bbbbbbbb-cccc-4ddd-8eee-ffffffffffff',
title: 'Current announcement',
body: 'This message is current after refresh.',
starts_at: '2026-08-31T12:00:00.000Z',
expires_at: null,
created_at: '2026-08-31T11:00:00.000Z',
};
await route.fulfill({ json: { announcements: [current] } });
});

await page.goto('/app/generate');
await expect(page.getByRole('heading', { name: 'Generate', exact: true })).toBeVisible();
await expect(page.getByRole('heading', { name: 'Scheduled maintenance' })).toBeVisible();
expect(listRequestCount()).toBe(1);

await page.getByRole('button', { name: 'Dismiss announcement: Scheduled maintenance' }).click();
await expect(page.getByRole('heading', { name: 'Scheduled maintenance' })).toHaveCount(0);

await openAppNav(page, 'Attack');
await expect(page.getByRole('heading', { name: 'Attack', exact: true })).toBeVisible();
await openAppNav(page, 'Home');
await expect(page.getByRole('main').getByRole('heading', { name: 'DebateLab', exact: true })).toBeVisible();
expect(listRequestCount()).toBe(1);
expect(listResponseCount).toBe(1);
await expect(page.getByRole('heading', { name: 'Scheduled maintenance' })).toHaveCount(0);

await page.reload();
await expect(page.getByRole('heading', { name: 'Current announcement' })).toBeVisible();
await expect.poll(() => listRequestCount()).toBe(2);
expect(listResponseCount).toBe(2);
});

test('admin can deactivate without losing history, then explicitly delete', async ({ page }) => {
let createBody: Record<string, unknown> | null = null;
let deactivateBody: Record<string, unknown> | null = null;
Expand Down
36 changes: 36 additions & 0 deletions e2e/animation-polish.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,42 @@ test('removes non-essential route and state movement for reduced motion', async
await page.screenshot({ path: testInfo.outputPath('reduced-result.png'), fullPage: true });
});

test('keeps a delayed route skeleton static under reduced motion', async ({ page }, testInfo) => {
await prepareMotionFixture(page, 'reduce');
await page.goto('/app/generate', { waitUntil: 'domcontentloaded' });
await expect(page.getByRole('heading', { name: 'Generate', exact: true })).toBeVisible({ timeout: STATE_TIMEOUT_MS });
await page.screenshot({ path: testInfo.outputPath('reduced-route-before.png'), fullPage: true });

let destinationRequested = false;
await page.route('**/src/pages/AttackPage.tsx*', async (route) => {
destinationRequested = true;
await new Promise((resolve) => setTimeout(resolve, 1500));
await route.continue();
});

const attackLink = (page.viewportSize()?.width ?? 1280) < 1024
? await (async () => {
await page.getByRole('button', { name: 'Open menu' }).click();
return page.getByRole('navigation', { name: 'Mobile navigation' }).getByRole('link', { name: /^Attack/ });
})()
: page.getByRole('navigation', { name: 'Main navigation' }).getByRole('link', { name: 'Attack' });
await attackLink.click();
await expect.poll(() => destinationRequested).toBe(true);

const pendingStatus = page.getByRole('status', { name: 'Opening Attack', exact: true });
const pendingSkeleton = page.locator('[data-route-skeleton="tool-form"][aria-busy="true"]');
await expect(pendingStatus).toHaveCount(1);
await expect(pendingStatus).toHaveAttribute('aria-live', 'polite');
await expect(pendingStatus).toHaveText('Opening Attack');
await expect(pendingSkeleton).toBeVisible();
await expect(page.locator('.motion-loading-shell')).toHaveCount(0);
await expect.poll(() => page.evaluate(() => document.getAnimations().filter((animation) => animation.playState === 'running').length)).toBe(0);
await page.screenshot({ path: testInfo.outputPath('reduced-route-pending.png'), fullPage: true });

await expect(page.getByRole('heading', { name: 'Attack', exact: true })).toBeVisible({ timeout: STATE_TIMEOUT_MS });
await page.screenshot({ path: testInfo.outputPath('reduced-route-ready.png'), fullPage: true });
});

test('audits applicable empty, loading, result, and option states across feature routes', async ({ page }, testInfo) => {
test.setTimeout(90_000);
await prepareMotionFixture(page, 'no-preference');
Expand Down
59 changes: 59 additions & 0 deletions e2e/home-navigation.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,65 @@ test('onboarding follows visible home targets when secondary navigation is close
await page.locator('[data-test-id="button-skip"]').click();
});

test('onboarding keeps its original start schedule after a delayed lazy chunk', async ({ page }) => {
await page.addInitScript(() => {
const originalSetItem = Storage.prototype.setItem;
Storage.prototype.setItem = function setItem(key, value) {
if (key === 'debate-trainer-onboarding') {
try {
if (JSON.parse(value).state?.isTourRunning === true) {
originalSetItem.call(this, '__onboarding-tour-started-at', String(Date.now()));
}
} catch {
// Ignore malformed test-only storage writes.
}
}
originalSetItem.call(this, key, value);
};
window.localStorage.setItem('debate-trainer-onboarding', JSON.stringify({
state: { hasCompletedOnboarding: false, isTourRunning: false },
version: 0,
}));
});
await page.route('**/src/components/features/onboarding/OnboardingTour.tsx*', async (route) => {
const response = await route.fetch();
const body = `${await response.text()}\nwindow.localStorage.setItem('__onboarding-module-evaluated-at', String(Date.now()));`;
await new Promise((resolve) => setTimeout(resolve, 750));
await route.fulfill({ response, body });
});
await page.route('**/node_modules/.vite/deps/react-joyride.js*', async (route) => {
const response = await route.fetch();
const body = await response.body();
await route.fulfill({ response, body });
});

await page.goto('/app', { waitUntil: 'domcontentloaded' });

await expect.poll(async () => Number(await page.evaluate(() => window.localStorage.getItem('__onboarding-module-evaluated-at') ?? 0)), { timeout: 15_000 }).toBeGreaterThan(0);
await expect.poll(async () => Number(await page.evaluate(() => window.localStorage.getItem('__onboarding-tour-started-at') ?? 0)), { timeout: 15_000 }).toBeGreaterThan(0);
const moduleEvaluatedAt = await page.evaluate(() => Number(window.localStorage.getItem('__onboarding-module-evaluated-at')));
const startedAt = await page.evaluate(() => Number(window.localStorage.getItem('__onboarding-tour-started-at')));
expect(startedAt - moduleEvaluatedAt).toBeLessThan(450);
await expect(page.locator('.react-joyride__tooltip')).toBeVisible({ timeout: 15_000 });
});

test('onboarding starts a fresh delay when a protected route reaches home', async ({ page }) => {
await page.addInitScript(() => {
window.localStorage.setItem('debate-trainer-onboarding', JSON.stringify({
state: { hasCompletedOnboarding: false, isTourRunning: false },
version: 0,
}));
});
await page.goto('/app/generate', { waitUntil: 'domcontentloaded' });
await expect(page.getByRole('heading', { name: 'Ready to Generate' })).toBeVisible({ timeout: 15_000 });
await page.waitForTimeout(650);

await page.locator('header a[href="/app"]').first().click();
await expect(page).toHaveURL(/\/app$/);
await expect(page.locator('.react-joyride__tooltip')).toHaveCount(0, { timeout: 100 });
await expect(page.locator('.react-joyride__tooltip')).toBeVisible({ timeout: 15_000 });
});

test('header keeps the brand and controls inside a 320px viewport', async ({ page }) => {
await page.setViewportSize({ width: 320, height: 844 });
await page.goto('/app', { waitUntil: 'domcontentloaded' });
Expand Down
Loading
Loading