diff --git a/e2e/home-navigation.spec.ts b/e2e/home-navigation.spec.ts index d4d85b65..43a92f65 100644 --- a/e2e/home-navigation.spec.ts +++ b/e2e/home-navigation.spec.ts @@ -87,7 +87,8 @@ test('onboarding keeps its original start schedule after a delayed lazy chunk', 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 }) => { +test('onboarding stays closed on a protected tool and starts when it reaches home', async ({ page }) => { + await page.clock.install(); await page.addInitScript(() => { window.localStorage.setItem('debate-trainer-onboarding', JSON.stringify({ state: { hasCompletedOnboarding: false, isTourRunning: false }, @@ -96,12 +97,14 @@ test('onboarding starts a fresh delay when a protected route reaches home', asyn }); await page.goto('/app/generate', { waitUntil: 'domcontentloaded' }); await expect(page.getByRole('heading', { name: 'Your case will appear here' })).toBeVisible({ timeout: 15_000 }); - // eslint-disable-next-line no-restricted-properties -- observation window: let the first onboarding delay run partway before navigating. - await page.waitForTimeout(650); + // Advance past the tour's 500ms deadline while still on the tool. The tour + // now starts when its Home target exists, rather than after a fixed delay. + await page.clock.runFor(1_000); + await expect(page.locator('.react-joyride__tooltip')).toHaveCount(0); 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('[data-tour="current-debate"]')).toBeVisible(); await expect(page.locator('.react-joyride__tooltip')).toBeVisible({ timeout: 15_000 }); }); diff --git a/e2e/hungarian-parity.spec.ts b/e2e/hungarian-parity.spec.ts index 207a2caa..5e456cc0 100644 --- a/e2e/hungarian-parity.spec.ts +++ b/e2e/hungarian-parity.spec.ts @@ -36,7 +36,7 @@ test.describe('Hungarian parity', () => { await expectHungarianDocument(page); await expect(page.getByRole('heading', { level: 1, name: `${site.product}: MI-alapú vitafelkészítés és gyakorlás` })).toBeVisible(); - await expect(page.getByText(/^Attól függ, mire van szükséged\. 2026\. október állapot szerint/)).toBeAttached(); + await expect(page.locator('#short-answer-text')).toContainText('Egy konkrét indítványra készülve'); if ((page.viewportSize()?.width ?? 0) < 768) { await expect(page.getByRole('button', { name: 'Menü megnyitása' })).toBeVisible(); } else { diff --git a/e2e/loading-performance.spec.ts b/e2e/loading-performance.spec.ts index 74b12e43..cb844124 100644 --- a/e2e/loading-performance.spec.ts +++ b/e2e/loading-performance.spec.ts @@ -70,7 +70,7 @@ test('authenticated routes keep the shell and current page after refresh', async ] as const) { await page.goto(path); await page.reload(); - await expect(page.locator('header')).toBeVisible(); + await expect(page.getByRole('banner')).toBeVisible(); await expect(page.getByRole('heading', { name: heading, exact: true })).toBeVisible(); await expect(page.getByText('DebateOwl is the official')).toHaveCount(0); } diff --git a/e2e/localization.spec.ts b/e2e/localization.spec.ts index f4ce56ac..93647a36 100644 --- a/e2e/localization.spec.ts +++ b/e2e/localization.spec.ts @@ -110,12 +110,12 @@ test('sign-in and not-found pages carry the language switcher', async ({ page }) await shot(page, '02-login-hu'); await page.goto('/no-such-page'); - await expect(page.getByRole('heading', { level: 1, name: hu.notFound.title })).toBeVisible(); + await expect(page.getByRole('heading', { level: 1, name: hu.notFound.message })).toBeVisible(); await expect(page).toHaveTitle(brand(hu.pageHead.notFound.title)); await shot(page, '03-not-found-hu'); await choose(page, 'es'); await expect(page).toHaveTitle(brand(es.pageHead.notFound.title)); - await expect(page.getByText(es.notFound.message)).toBeVisible(); + await expect(page.getByRole('heading', { level: 1, name: es.notFound.message })).toBeVisible(); await shot(page, '04-not-found-es'); }); diff --git a/e2e/navigation-performance.spec.ts b/e2e/navigation-performance.spec.ts index 4afcc650..ea8f3633 100644 --- a/e2e/navigation-performance.spec.ts +++ b/e2e/navigation-performance.spec.ts @@ -94,7 +94,8 @@ test('workspaces stay unavailable without a collaboration endpoint', async ({ pa await expect(page.locator('a[href="/app/workspaces"]')).toHaveCount(0); await page.goto('/app/workspaces', { waitUntil: 'domcontentloaded' }); - await expect(page.getByRole('heading', { name: '404' })).toBeVisible(); + await expect(page.getByText('404', { exact: true })).toBeVisible(); + await expect(page.getByRole('heading', { level: 1, name: 'Page not found' })).toBeVisible(); }); test('a delayed public route keeps the main landmark', async ({ page }) => { @@ -177,7 +178,9 @@ test('a failed route chunk recovers after the offered reload', async ({ page }) let attempts = 0; await page.route('**/src/pages/AttackPage.tsx*', async (route) => { attempts += 1; - if (attempts === 1) { + // The first failure reloads automatically. Fail that retry too, so the + // boundary offers its manual Reload action instead of recovering first. + if (attempts <= 2) { await route.fulfill({ status: 503, contentType: 'text/plain', body: 'chunk unavailable' }); return; } @@ -190,8 +193,9 @@ test('a failed route chunk recovers after the offered reload', async ({ page }) await homeToolLink(page, 'attack').click(); await expect(page.getByRole('heading', { name: 'Unable to load this page' })).toBeVisible(); + expect(attempts).toBe(2); await page.getByRole('button', { name: 'Reload' }).click(); - await expect.poll(() => attempts).toBe(2); + await expect.poll(() => attempts).toBe(3); await waitForContent(page, attackContent); await expect(page.getByRole('banner')).toBeVisible(); }); @@ -365,14 +369,23 @@ test('subscription reads stay cached across sibling tool routes', async ({ page await route.fulfill({ json: { announcements: [] } }); return; } + if (url.pathname.endsWith('/rest/v1/rpc/ai_usage_today')) { + subscriptionRequests.push('ai_usage_today'); + await route.fulfill({ json: 0 }); + return; + } if (url.pathname.includes('/rest/v1/')) { const select = url.searchParams.get('select') ?? ''; if (url.pathname.endsWith('/user_settings') && select.includes('subscription_tier')) { subscriptionRequests.push('user_settings'); } else if (url.pathname.endsWith('/subscriptions')) { subscriptionRequests.push('subscriptions'); - } else if (url.pathname.endsWith('/ai_requests')) { - subscriptionRequests.push('ai_requests'); + } + // An empty single-row read is a PostgREST error, not a truthy array. + // Otherwise settings hydration treats [] as an account and reads billing. + if ((route.request().headers().accept ?? '').includes('vnd.pgrst.object')) { + await route.fulfill({ status: 406, json: { code: 'PGRST116', message: 'No rows returned' } }); + return; } await route.fulfill({ status: 200, @@ -387,7 +400,7 @@ test('subscription reads stay cached across sibling tool routes', async ({ page await page.goto('/app/generate'); await waitForContent(page, { selector: 'main h1', text: 'Generate' }); - await expect.poll(() => subscriptionRequests.filter((name) => name === 'ai_requests').length).toBe(1); + await expect.poll(() => subscriptionRequests.filter((name) => name === 'ai_usage_today').length).toBe(1); await openHome(page); await homeToolLink(page, 'attack').click(); @@ -395,5 +408,5 @@ test('subscription reads stay cached across sibling tool routes', async ({ page // eslint-disable-next-line no-restricted-properties -- observation window: no extra subscription request may follow. await page.waitForTimeout(100); - expect(subscriptionRequests).toEqual(['user_settings', 'ai_requests']); + expect(subscriptionRequests).toEqual(['user_settings', 'ai_usage_today']); }); diff --git a/e2e/quality-backlog.spec.ts b/e2e/quality-backlog.spec.ts index 1613c225..9b126584 100644 --- a/e2e/quality-backlog.spec.ts +++ b/e2e/quality-backlog.spec.ts @@ -91,9 +91,9 @@ test('Flowchart deletes a selected node at once and Undo puts it back', async ({ await page.getByText('Argument 1', { exact: true }).click(); const remove = page.getByRole('button', { name: 'Delete' }).last(); if (testInfo.project.name === 'mobile-chromium') { - const box = await remove.boundingBox(); - expect(box).not.toBeNull(); - await page.touchscreen.tap(box!.x + box!.width / 2, box!.y + box!.height / 2); + // Locator taps scroll into view and wait for layout stability, just like + // clicks. Raw viewport coordinates can hit the canvas after selection reflows. + await remove.tap(); } else { await remove.click(); } diff --git a/e2e/search-quality.spec.ts b/e2e/search-quality.spec.ts index 239950f3..0d2788dc 100644 --- a/e2e/search-quality.spec.ts +++ b/e2e/search-quality.spec.ts @@ -80,11 +80,13 @@ test('authenticated Factoids shows the consulted-source quality panel', async ({ await page.getByRole('textbox', { name: /statement/i }).fill('Solar energy costs are falling'); await page.getByRole('button', { name: /find factoids/i }).click(); - await expect(page.getByRole('heading', { name: /researched sources/i })).toBeVisible(); - await expect(page.getByRole('link', { name: /official energy statistics/i }).first()).toHaveAttribute('href', strongSourceUrl); - await expect(page.getByRole('link', { name: /broad commentary/i })).toHaveAttribute('href', weakSourceUrl); - await expect(page.getByText('Strong', { exact: true })).toBeVisible(); - await expect(page.getByText('Weak', { exact: true })).toBeVisible(); - await expect(page.getByText('fallback context only', { exact: true })).toBeVisible(); + const sources = page.locator('details').filter({ has: page.locator('summary', { hasText: 'Researched sources (2)' }) }); + await sources.locator('summary').click(); + await expect(sources).toHaveAttribute('open', ''); + await expect(sources.getByRole('link', { name: /official energy statistics/i })).toHaveAttribute('href', strongSourceUrl); + await expect(sources.getByRole('link', { name: /broad commentary/i })).toHaveAttribute('href', weakSourceUrl); + await expect(sources.getByText('Strong', { exact: true })).toBeVisible(); + await expect(sources.getByText('Weak', { exact: true })).toBeVisible(); + await expect(sources.getByText('fallback context only', { exact: true })).toBeVisible(); await expect(page.getByText('Solar costs fell by 42 percent')).toBeVisible(); });