diff --git a/e2e/animation-polish.spec.ts b/e2e/animation-polish.spec.ts index 9061985a7..41235dcad 100644 --- a/e2e/animation-polish.spec.ts +++ b/e2e/animation-polish.spec.ts @@ -65,23 +65,6 @@ async function openFeatureRoute(page: Page, route: 'brainstorm' | 'factoids' | ' await expect(page).toHaveURL(new RegExp(`/app/${route}$`)); } -test('keeps route and feature-state motion short and stable', async ({ page }, testInfo) => { - await prepareMotionFixture(page, 'no-preference'); - const options = await openGenerateOptions(page); - - await expect(page.locator('[data-route-transition-ms]')).toHaveCount(0); - const transitionMs = await options.evaluate((element) => Number.parseFloat(getComputedStyle(element).transitionDuration) * 1000); - expect(transitionMs).toBeGreaterThan(0); - await page.screenshot({ path: testInfo.outputPath('normal-empty-options.png'), fullPage: true }); - - await generateMockCase(page); - await expect(page.locator('#generate-results')).toBeVisible(); - await expect.poll(() => page.evaluate(() => window.scrollY)).toBeGreaterThan(0); - await expect(page.locator('main.page-enter')).toHaveCount(0); - await expect(page.getByText('Generate complete', { exact: true })).toBeHidden({ timeout: 10_000 }); - await page.screenshot({ path: testInfo.outputPath('normal-result.png'), fullPage: true }); -}); - test('removes non-essential route and state movement for reduced motion', async ({ page }, testInfo) => { await prepareMotionFixture(page, 'reduce'); const options = await openGenerateOptions(page); diff --git a/e2e/home-navigation.spec.ts b/e2e/home-navigation.spec.ts index 82aa52824..9f74a749e 100644 --- a/e2e/home-navigation.spec.ts +++ b/e2e/home-navigation.spec.ts @@ -124,20 +124,6 @@ test('header keeps the brand and controls inside a 320px viewport', async ({ pag expect(await page.evaluate(() => document.documentElement.scrollWidth)).toBe(320); }); -test('desktop header holds Home, Current debate, Library, and History with no tool links', async ({ page }) => { - test.skip((page.viewportSize()?.width ?? 0) < 1024, 'Desktop-only header check'); - await page.goto('/app', { waitUntil: 'domcontentloaded' }); - - const navigation = page.getByRole('navigation', { name: 'Main navigation' }); - await expect(navigation.getByRole('link')).toHaveText(['Home', 'Library', 'History']); - await expect(navigation.getByTestId('current-debate-desktop')).toHaveText('Start debate'); - for (const tool of ['Generate', 'Attack', 'Ask', 'Crossfire', 'Sparring', 'More']) { - await expect(navigation.getByText(tool, { exact: true })).toHaveCount(0); - } - await expect(page.getByRole('link', { name: 'Settings' })).toBeVisible(); - expect(await page.evaluate(() => document.documentElement.scrollWidth)).toBeLessThanOrEqual(await page.evaluate(() => window.innerWidth)); -}); - test('desktop header fits without internal or document overflow at supported widths', async ({ page }) => { for (const width of [1024, 1280, 1440, 1920]) { await page.setViewportSize({ width, height: 900 }); diff --git a/e2e/loading-performance.spec.ts b/e2e/loading-performance.spec.ts index 4dfb1a311..475afd891 100644 --- a/e2e/loading-performance.spec.ts +++ b/e2e/loading-performance.spec.ts @@ -60,40 +60,3 @@ test('landing CTA enters the app without replacing the document', async ({ page expect(await page.evaluate(() => performance.getEntriesByType('navigation'))).toHaveLength(1); expect(await page.evaluate(() => document.documentElement.dataset.e2eDocument)).toBe('original'); }); - -test('protected sibling navigation keeps AppLayout mounted while the page loads', async ({ page }) => { - await setLocale(page, 'en'); - await stubSupabaseSession(page); - await page.addInitScript(() => { - window.localStorage.setItem('debate-trainer-onboarding', JSON.stringify({ - state: { hasCompletedOnboarding: true, isTourRunning: false }, - version: 0, - })); - }); - await page.goto('/app'); - const header = page.locator('header'); - await expect(header).toBeVisible(); - await header.evaluate((element) => { element.dataset.e2eLayout = 'original'; }); - - let releaseAttackModule: (() => void) | undefined; - const attackModuleReleased = new Promise((resolve) => { releaseAttackModule = resolve; }); - let attackModuleRequested = false; - await page.route('**/src/pages/AttackPage.tsx*', async (route) => { - attackModuleRequested = true; - await attackModuleReleased; - await route.continue(); - }); - - // Tools open from Home; the header no longer lists them. - await page.locator('main [data-tour="mode-attack"]').click(); - await expect.poll(() => attackModuleRequested).toBe(true); - await page.waitForTimeout(250); - - await expect(header).toBeVisible(); - expect(await header.getAttribute('data-e2e-layout')).toBe('original'); - - releaseAttackModule?.(); - await expect(page).toHaveURL(/\/app\/attack$/); - await expect(page.getByRole('heading', { name: /Attack/i }).first()).toBeVisible(); - expect(await header.getAttribute('data-e2e-layout')).toBe('original'); -}); diff --git a/e2e/navigation-performance.spec.ts b/e2e/navigation-performance.spec.ts index 9d3b914c1..d6de7af2f 100644 --- a/e2e/navigation-performance.spec.ts +++ b/e2e/navigation-performance.spec.ts @@ -5,7 +5,6 @@ declare global { interface Window { __baselineAppLayout?: Element | null; __baselineShell?: Element | null; - __baselineClickAt?: number | null; __genericLoaderObserver?: MutationObserver; __genericLoaderSeen?: boolean; } @@ -125,13 +124,6 @@ test('a delayed destination chunk exposes the route fallback without replacing t await page.evaluate(() => { window.__baselineAppLayout = document.querySelector('header')?.parentElement; window.__baselineShell = document.querySelector('header'); - window.__baselineClickAt = null; - document.addEventListener('click', (event) => { - const anchor = event.target instanceof Element ? event.target.closest('a[href]') : null; - if (anchor && new URL(anchor.href, window.location.href).pathname === '/app/attack') { - window.__baselineClickAt = performance.now(); - } - }, true); }); let destinationRequested = false; await page.route('**/src/pages/AttackPage.tsx*', async (route) => { @@ -178,27 +170,6 @@ test('a delayed destination chunk exposes the route fallback without replacing t appLayout: document.querySelector('header')?.parentElement === window.__baselineAppLayout, header: document.querySelector('header') === window.__baselineShell, }))).toEqual({ appLayout: true, header: true }); - - const transitionMetrics = await page.evaluate(() => ({ - clickTime: window.__baselineClickAt ?? null, - route: location.pathname, - shellDomIdentitySurvived: document.querySelector('header') === window.__baselineShell, - allRequestCount: performance.getEntriesByType('resource').length, - measuredRequestCount: performance.getEntriesByType('resource') - .filter((entry) => window.__baselineClickAt !== null && entry.startTime >= window.__baselineClickAt).length, - sourceRequestCount: performance.getEntriesByType('resource') - .filter((entry) => window.__baselineClickAt !== null && entry.startTime < window.__baselineClickAt).length, - allLongTaskCount: performance.getEntriesByType('longtask').length, - measuredLongTaskCount: performance.getEntriesByType('longtask') - .filter((entry) => window.__baselineClickAt !== null && entry.startTime >= window.__baselineClickAt).length, - })); - expect(transitionMetrics.clickTime).not.toBeNull(); - expect(transitionMetrics.route).toBe('/app/attack'); - expect(transitionMetrics.shellDomIdentitySurvived).toBe(true); - expect(transitionMetrics.sourceRequestCount).toBeGreaterThan(0); - expect(transitionMetrics.measuredRequestCount).toBeGreaterThan(0); - expect(transitionMetrics.measuredRequestCount).toBeLessThan(transitionMetrics.allRequestCount); - expect(transitionMetrics.measuredLongTaskCount).toBeLessThanOrEqual(transitionMetrics.allLongTaskCount); }); test('a failed route chunk recovers after the offered reload', async ({ page }) => { @@ -226,35 +197,6 @@ test('a failed route chunk recovers after the offered reload', async ({ page }) await expect(page.locator('header')).toBeVisible(); }); -for (const [locale, openingLabel] of [ - ['es', 'Abriendo Atacar'], - ['hu', 'Támadás megnyitása'], -] as const) { - test(`${locale} delayed route announces the localized destination`, async ({ page }) => { - await setLocale(page, locale); - await stubSupabaseSession(page); - await skipOnboarding(page); - await page.goto('/app', { waitUntil: 'domcontentloaded' }); - await expect(homeToolLink(page, 'attack')).toBeVisible(); - - let destinationRequested = false; - await page.route('**/src/pages/AttackPage.tsx*', async (route) => { - destinationRequested = true; - await new Promise((resolve) => setTimeout(resolve, 1500)); - await route.continue(); - }); - - await homeToolLink(page, 'attack').click(); - - await expect.poll(() => destinationRequested).toBe(true); - const status = page.getByRole('status', { name: openingLabel, exact: true }); - await expect(status).toHaveCount(1); - await expect(status).toHaveText(openingLabel); - await expect(status).not.toHaveText('Opening Attack'); - await expect(page.locator('[data-route-skeleton="tool-form"][aria-busy="true"]')).toBeVisible(); - }); -} - async function gotoHome(page: Page) { await setLocale(page, 'en'); await stubSupabaseSession(page); diff --git a/e2e/public-pages.spec.ts b/e2e/public-pages.spec.ts index 44178595f..63f602f68 100644 --- a/e2e/public-pages.spec.ts +++ b/e2e/public-pages.spec.ts @@ -39,22 +39,3 @@ for (const locale of LOCALES) { } }); } - -test('landing logos use the sized display asset with intrinsic dimensions', async ({ page }) => { - await setLocale(page, 'en'); - await page.goto('/'); - - const logos = page.locator('img[alt="DebateLab logo"]'); - await expect(logos).toHaveCount(2); - expect(await logos.evaluateAll((images) => images.map((image) => { - const logo = image as HTMLImageElement; - return { - path: new URL(logo.currentSrc).pathname, - width: logo.getAttribute('width'), - height: logo.getAttribute('height'), - }; - }))).toEqual([ - { path: '/favicon-72.png', width: '36', height: '36' }, - { path: '/favicon-72.png', width: '32', height: '32' }, - ]); -}); diff --git a/e2e/sparring.spec.ts b/e2e/sparring.spec.ts index f3db31788..8facd239c 100644 --- a/e2e/sparring.spec.ts +++ b/e2e/sparring.spec.ts @@ -132,24 +132,8 @@ test('keeps the setup usable on mobile with reduced motion', async ({ page }) => await page.emulateMedia({ reducedMotion: 'reduce' }); await page.goto('/app/sparring', { waitUntil: 'domcontentloaded' }); await expect(page.getByRole('heading', { name: 'Sparring' })).toBeVisible(); - const logo = page.getByAltText('DebateLab logo'); - await expect(logo).toHaveAttribute('src', '/favicon-72.png'); - await expect(logo).toHaveAttribute('width', '36'); - await expect(logo).toHaveAttribute('height', '36'); - await expect.poll(() => logo.evaluate((image: HTMLImageElement) => image.naturalWidth)).toBeGreaterThan(0); await expect(page.getByRole('combobox', { name: 'Round format' })).toHaveText('School 4-6-6-3 · 8 speeches'); - await expect(page.getByText('Write the resolution as you would hear it in the room.')).toHaveCount(0); - await expect(page.getByText('A precise motion gives the opponent something concrete to attack.')).toHaveCount(0); await expect(page.getByText('Pro Introductor')).toBeHidden(); - const speakingSummary = page.locator('main details > summary'); - const summaryMetrics = await speakingSummary.evaluate((element) => { - const icon = element.querySelector('svg'); - if (!icon) throw new Error('Speaking order disclosure is missing its chevron.'); - const summaryBox = element.getBoundingClientRect(); - const iconBox = icon.getBoundingClientRect(); - return Math.abs((summaryBox.top + summaryBox.height / 2) - (iconBox.top + iconBox.height / 2)); - }); - expect(summaryMetrics).toBeLessThanOrEqual(1); await page.getByText('Speaking order', { exact: true }).click(); await expect(page.getByText('Pro Introductor')).toBeVisible(); await expect(page.getByText('Pro Conclusor')).toBeVisible(); diff --git a/e2e/startup-shell.spec.ts b/e2e/startup-shell.spec.ts index ad252945b..4fd7f0461 100644 --- a/e2e/startup-shell.spec.ts +++ b/e2e/startup-shell.spec.ts @@ -16,7 +16,6 @@ test('shows a dark private startup shell while the entry module is delayed', { t await page.goto('/app', { waitUntil: 'commit' }); await expect(page.locator('[data-startup-shell]')).toBeVisible(); - await expect(page.locator('#root > .seo-fallback')).toBeHidden(); await expect(page.locator('[data-startup-status]')).toHaveText('Loading DebateLab'); await expect.poll(() => page.evaluate(() => ({ appShell: document.documentElement.classList.contains('app-shell'), @@ -24,19 +23,6 @@ test('shows a dark private startup shell while the entry module is delayed', { t }))).toEqual({ appShell: true, dark: true }); }); -test('shows the startup shell on login while the entry module is delayed', { tag: '@smoke' }, async ({ page }) => { - await setLocale(page, 'en'); - await page.route('**/src/bootstrap.ts*', async (route) => { - await new Promise((resolve) => setTimeout(resolve, 1500)); - await route.continue(); - }); - - await page.goto('/login', { waitUntil: 'commit' }); - await expect(page.locator('[data-startup-shell]')).toBeVisible(); - await expect(page.locator('#root > .seo-fallback')).toBeHidden(); - await expect.poll(() => page.evaluate(() => document.documentElement.classList.contains('app-shell'))).toBe(true); -}); - test('mounts the app before Jam recorder modules resolve', { tag: '@smoke' }, async ({ page }) => { await setLocale(page, 'en'); await stubSupabaseSession(page); @@ -52,7 +38,6 @@ test('mounts the app before Jam recorder modules resolve', { tag: '@smoke' }, as await page.goto('/app', { waitUntil: 'domcontentloaded' }); await expect(page.getByRole('main').getByRole('heading', { name: 'DebateLab', exact: true, level: 1 })).toBeVisible({ timeout: 5_000 }); - await expect(page.locator('#root > .seo-fallback')).toHaveCount(0); expect(await page.evaluate(() => { const jam = (window as Window & { __jam__?: { exports?: { metadata?: unknown } } }).__jam__; return typeof jam?.exports?.metadata === 'function'; @@ -67,15 +52,3 @@ test('shows a recoverable message when the static main entry fails', { tag: '@sm await expect(page.getByRole('heading', { name: 'Unable to load DebateLab' })).toBeVisible(); await expect(page.getByRole('button', { name: 'Reload' })).toBeEnabled(); }); - -test('keeps the branded shell visible before a delayed public entry module', { tag: '@smoke' }, async ({ page }) => { - await setLocale(page, 'en'); - await page.route('**/src/bootstrap.ts*', async (route) => { - await new Promise((resolve) => setTimeout(resolve, 1500)); - await route.continue(); - }); - - await page.goto('/', { waitUntil: 'commit' }); - await expect(page.locator('[data-startup-shell]')).toBeVisible(); - await expect(page.getByText('DebateLab by Programcode is the official')).toHaveCount(0); -}); diff --git a/services/collaboration/src/postgres-store.test.ts b/services/collaboration/src/postgres-store.test.ts index e98af150c..44919fc78 100644 --- a/services/collaboration/src/postgres-store.test.ts +++ b/services/collaboration/src/postgres-store.test.ts @@ -224,6 +224,15 @@ describe("Postgres collaboration writer locks", () => { documentId: "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa", epoch: 3, })).resolves.toBe(true); + expect(client.query).toHaveBeenCalledWith( + expect.stringContaining("member.user_id = $3"), + [ + "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa", + 3, + "cccccccc-cccc-4ccc-8ccc-cccccccccccc", + false, + ], + ); expect(client.release).toHaveBeenCalledTimes(1); }); diff --git a/src/test/components/EnvironmentSwitcher.test.tsx b/src/test/components/EnvironmentSwitcher.test.tsx index 43c39350e..8a4526c55 100644 --- a/src/test/components/EnvironmentSwitcher.test.tsx +++ b/src/test/components/EnvironmentSwitcher.test.tsx @@ -55,12 +55,6 @@ describe('EnvironmentSwitcher', () => { expect(reload).not.toHaveBeenCalled(); }); - it('offers production while staging is active', () => { - mocks.env = 'staging'; - render(); - expect(screen.getByRole('button', { name: 'Switch to Production' })).toBeInTheDocument(); - }); - it('still switches when sign-out fails, so nobody is stranded on a broken project', async () => { mocks.env = 'staging'; mocks.signOut.mockRejectedValue(new Error('staging unreachable')); diff --git a/src/test/debate/debate-context.test.tsx b/src/test/debate/debate-context.test.tsx index b10e29126..24a8669cf 100644 --- a/src/test/debate/debate-context.test.tsx +++ b/src/test/debate/debate-context.test.tsx @@ -172,8 +172,13 @@ describe('Current Debate context in requests and the Review control', () => { } }); - it('sends no debate material in one-off mode and says so above the button', () => { + it('sends no debate material in one-off mode and says so above the button', async () => { enableOneOffMode(); + setMockInvokeResponse({ output: { needsMoreInfo: false, clarifyingQuestions: [], topFive: [], nextSteps: [], warningsAndTradeoffs: [] } }); + await brainstorm(brainstormInput, config); + expect(lastBody().debateContext).toBeUndefined(); + expect(lastBody().sourceIds).toEqual([]); + render(); expect(screen.getByTestId('debate-context-control')).toHaveTextContent('One-off mode: no Current Debate material will be used or saved.'); }); diff --git a/src/test/pages/GeneratePage.test.tsx b/src/test/pages/GeneratePage.test.tsx index e21870df6..46d90150d 100644 --- a/src/test/pages/GeneratePage.test.tsx +++ b/src/test/pages/GeneratePage.test.tsx @@ -130,53 +130,6 @@ describe('GeneratePage Integration', () => { mockGenerateForm = { motion: 'Existing motion', side: 'pro', format: 'bp', role: 'intro', length: 'brief', audience: 'beginner', tone: 'formal' }; }); - it('renders the empty state initially', () => { - // Override store mock for this test if needed, but default is null result - render( - - - - ); - - expect(screen.getByText('Ready to Generate')).toBeInTheDocument(); - expect(screen.getByTestId('generate-form')).toBeInTheDocument(); - expect(screen.queryByTestId('generate-results')).not.toBeInTheDocument(); - }); - - it('handles form submission and successful generation', async () => { - // Setup API mock - const mockResult = { - caseStructure: { thesis: 'Test Thesis' }, - contentions: [] as any[], - predictedOpponentCase: {}, - counters: [] as any[] - }; - (generateCase as Mock).mockResolvedValue({ result: mockResult, discoveredSources: [] }); - - render( - - - - ); - - // Trigger submit - fireEvent.click(screen.getByTestId('submit-button')); - - // Verify loading state calls - expect(mockSetGenerateLoading).toHaveBeenCalledWith(true); - - // Wait for API call - await waitFor(() => { - expect(generateCase).toHaveBeenCalled(); - }); - - // Verify result setting and completion - await waitFor(() => { - expect(mockSetGenerateResult).toHaveBeenCalledWith(mockResult); - expect(mockSetGenerateLoading).toHaveBeenCalledWith(false); - }); - }); - it('reports a rejected case save instead of confirming success', async () => { mockGenerateResult = { caseStructure: { thesis: 'Existing Thesis' }, @@ -202,34 +155,6 @@ describe('GeneratePage Integration', () => { expect(mockToast).not.toHaveBeenCalledWith(expect.objectContaining({ title: 'Case Saved' })); }); - it('displays results when store has data', () => { - mockGenerateResult = { - caseStructure: { thesis: 'Existing Thesis' }, - contentions: [] as any[], - predictedOpponentCase: { - likelyArguments: [] as any[], - strategicWeaknesses: [] as any[], - keyClashes: [] as any[], - }, - counters: [] as any[], - metadata: { - side: 'pro', - format: 'bp', - generatedAt: '2024-01-01T00:00:00Z', - }, - }; - - render( - - - - ); - - expect(screen.getByTestId('generate-results')).toBeInTheDocument(); - expect(screen.getByText('Results for Existing Thesis')).toBeInTheDocument(); - expect(screen.queryByText('Ready to Generate')).not.toBeInTheDocument(); - }); - it('opens Crossfire with the generated resolution and argument', () => { mockGenerateResult = { caseStructure: { thesis: 'Existing Thesis', framework: 'Framework', keyDefinitions: [], burdenOfProof: 'Burden' }, diff --git a/src/test/sparring/formats.test.ts b/src/test/sparring/formats.test.ts index 23483db0b..6be2f0af1 100644 --- a/src/test/sparring/formats.test.ts +++ b/src/test/sparring/formats.test.ts @@ -65,18 +65,11 @@ describe('sparring formats', () => { expect(definition.turnCount).toBe(expected.length); expect(definition.name).toBeTruthy(); expect(definition.shortDescription).toBeTruthy(); - expect(definition.approximateMinutes).toBeGreaterThan(0); + const speakingSeconds = expected.reduce((total, turn) => total + turn.seconds, 0); + expect(definition.approximateMinutes).toBe(Math.round(speakingSeconds / 60)); } }); - it('presents the school format as the primary 4-6-6-3 format', () => { - expect(getSparringFormat('standard')).toMatchObject({ - name: 'School 4-6-6-3', - turnCount: 8, - approximateMinutes: 38, - }); - }); - it.each([ [0, '0:00'], [1, '0:01'], diff --git a/src/test/vercel-deployment-config.test.ts b/src/test/vercel-deployment-config.test.ts index a7da83d1c..2da68064f 100644 --- a/src/test/vercel-deployment-config.test.ts +++ b/src/test/vercel-deployment-config.test.ts @@ -8,10 +8,6 @@ type HeaderRule = { }; type VercelConfig = { - $schema?: string; - framework?: string; - buildCommand?: string; - outputDirectory?: string; rewrites?: Array<{ source: string; destination: string }>; headers?: HeaderRule[]; }; @@ -21,18 +17,10 @@ function loadConfig(): VercelConfig { return JSON.parse(readFileSync(path, "utf8")) as VercelConfig; } -function loadPackageJson(): { - engines?: { node?: string }; - dependencies?: Record; - scripts?: Record; -} { +function loadPackageJson(): { scripts?: Record } { return JSON.parse(readFileSync(resolve(process.cwd(), "package.json"), "utf8")); } -function loadMainSource(): string { - return readFileSync(resolve(process.cwd(), "src/main.tsx"), "utf8"); -} - function readProjectFile(path: string): string { return readFileSync(resolve(process.cwd(), path), "utf8"); } @@ -44,15 +32,6 @@ function headersFor(config: VercelConfig, source: string) { } describe("Vercel frontend deployment configuration", () => { - it("builds DebateLab as a Vite static site", () => { - const config = loadConfig(); - - expect(config.$schema).toBe("https://openapi.vercel.sh/vercel.json"); - expect(config.framework).toBe("vite"); - expect(config.buildCommand).toBe("npm run build"); - expect(config.outputDirectory).toBe("dist"); - }); - it("falls back to the SPA shell after Vercel checks the filesystem", () => { const config = loadConfig(); @@ -119,17 +98,6 @@ describe("Vercel frontend deployment configuration", () => { expect(indexHtml).toContain(''); }); - it("starts with a branded boot shell instead of visible crawl-only copy", () => { - const indexHtml = readProjectFile("index.html"); - const mainSource = loadMainSource(); - const rootMarkup = indexHtml.match(/
([\s\S]*?)<\/div>/)?.[1] ?? ""; - - expect(indexHtml).toContain('aria-label="DebateLab is loading"'); - expect(rootMarkup).not.toContain("DebateLab by Programcode is the official"); - expect(indexHtml).toMatch(/