From 40e2c478fb1ee3aa85178917820d12a55bb10d42 Mon Sep 17 00:00:00 2001 From: Andras Schmelczer Date: Sun, 31 May 2026 10:49:26 +0100 Subject: [PATCH] test(e2e): update smoke and visual specs --- frontend/e2e/smoke.spec.ts | 134 +++++++++++++++++++++++++++++++---- frontend/e2e/visuals.spec.ts | 77 ++++++++++++++++---- 2 files changed, 184 insertions(+), 27 deletions(-) diff --git a/frontend/e2e/smoke.spec.ts b/frontend/e2e/smoke.spec.ts index 4a20b30..2493014 100644 --- a/frontend/e2e/smoke.spec.ts +++ b/frontend/e2e/smoke.spec.ts @@ -1,4 +1,21 @@ -import { test, expect } from '@playwright/test'; +import { test, expect, type Page } from '@playwright/test'; + +async function expectTaskListOpen(page: Page, description: string): Promise { + const tasks = page.locator('lt-tasks', { hasText: description }).first(); + const taskBody = tasks.locator('.all-task'); + const taskRow = tasks.locator('.task-container', { hasText: description }).first(); + + await expect(tasks.locator('.header')).toHaveCount(0); + await expect + .poll(async () => + taskBody.evaluate((el) => { + const height = el.getBoundingClientRect().height; + return height / Math.max(1, el.scrollHeight); + }), + ) + .toBeGreaterThan(0.9); + await taskRow.locator('.tickbox').click({ trial: true }); +} /** * Smoke test: drives the legacy-styled UI end-to-end. @@ -11,8 +28,11 @@ test.describe('Life Towers smoke test', () => { test('create page → tower → block, mark done, reload, persists', async ({ page }) => { await page.goto('/'); - // Wait for the empty-state hint that means init() completed. - await expect(page.getByText('Add a new page to get started!')).toBeVisible({ timeout: 15000 }); + // Wait for init, then dismiss the welcome modal so the page controls are reachable. + await expect(page.getByText('Welcome to Life Towers')).toBeVisible({ timeout: 15000 }); + await page.getByRole('button', { name: 'Start empty' }).click(); + await page.waitForSelector('section.modal', { state: 'detached' }); + await expect(page.getByText('Add a new page to get started!')).toBeVisible(); // Create a page via the select-add dropdown. await page.locator('lt-select-add .top').first().click(); @@ -24,7 +44,7 @@ test.describe('Life Towers smoke test', () => { // Create a tower. await page.locator('img[alt="Add tower"]').click(); - await page.locator('input[placeholder="Tower name…"]').fill('Side projects'); + await page.locator('input[placeholder="New tower"]').fill('Side projects'); await page.locator('lt-tower-settings button[type="submit"]').click(); // Tower's name input is rendered with the tower name as its value. @@ -43,27 +63,25 @@ test.describe('Life Towers smoke test', () => { await page.locator('textarea[placeholder="Write a description here…"]').fill( 'Modernise the towers app', ); - await page.getByRole('button', { name: 'Create and exit' }).click(); + await page.getByLabel('Already done').uncheck(); + await page.getByRole('button', { name: 'Create and exit', exact: true }).click(); // New block is pending → appears in the tasks accordion. // (Tasks header shows N tasks.) await expect(page.locator('lt-tasks')).toContainText('1'); // Open the tasks accordion + click the task to edit it, then flip done. - await page.locator('lt-tasks .container').click(); - await page.locator('lt-tasks .task-container').click(); + await page.locator('lt-tasks .header').click(); + await page.locator('lt-tasks .task-description').click(); - // Toggle done in the block-edit modal — the right label is "Done". + // Toggle done in the block-edit modal. const putLanded = page.waitForResponse( (r) => r.url().endsWith('/api/v1/data') && r.request().method() === 'PUT' && r.ok(), ); - // Toggle uses verbose labels — "Goal accomplished" flips it to done. - await page - .locator('lt-block-edit lt-toggle span') - .filter({ hasText: 'Goal accomplished' }) - .click(); - await page.getByRole('button', { name: 'Create and exit' }).click(); + await page.locator('lt-block-edit .card.active').getByLabel('Already done').check(); await putLanded; + await page.locator('lt-block-edit .card.active .exit').click(); + await page.waitForSelector('section.modal', { state: 'detached' }); // Done block now appears as a colored square in the tower's falling stack. await expect(page.locator('lt-tower lt-block').first()).toBeVisible(); @@ -73,7 +91,93 @@ test.describe('Life Towers smoke test', () => { await expect(page.locator('lt-select-add .top').first()).toContainText('Hobbies', { timeout: 15000, }); - await expect(page.getByDisplayValue('Side projects')).toBeVisible(); + await expect(page.locator('lt-tower input').first()).toHaveValue('Side projects'); await expect(page.locator('lt-tower lt-block').first()).toBeVisible(); }); + + test('keep tasks open shows new pending tasks and survives immediate reload', async ({ page }) => { + await page.goto('/'); + + await expect(page.getByText('Welcome to Life Towers')).toBeVisible({ timeout: 15000 }); + await page.getByRole('button', { name: 'Start empty' }).click(); + await page.waitForSelector('section.modal', { state: 'detached' }); + + await page.locator('lt-select-add .top').first().click(); + await page.locator('lt-select-add input[placeholder="Add a value…"]').fill('Work'); + await page.locator('lt-select-add input[placeholder="Add a value…"]').press('Enter'); + + await page.locator('img[alt="Add tower"]').click(); + await page.locator('input[placeholder="New tower"]').fill('Today'); + await page.locator('lt-tower-settings button[type="submit"]').click(); + await page.waitForSelector('section.modal', { state: 'detached' }); + + await page.getByRole('button', { name: 'Settings' }).click(); + await page.getByText('Keep tasks open').click(); + await page.locator('lt-settings .exit').click(); + await page.waitForSelector('section.modal', { state: 'detached' }); + + await page.locator('img[alt="Add block"]').first().click(); + const createCard = page.locator('lt-block-edit .create-card'); + await expect(createCard.getByLabel('Already done')).not.toBeChecked(); + await createCard.locator('lt-select-add .top').click(); + await createCard.locator('lt-select-add input[placeholder="Add a value…"]').fill('ops'); + await createCard.locator('lt-select-add input[placeholder="Add a value…"]').press('Enter'); + await createCard + .locator('textarea[placeholder="Write a description here…"]') + .fill('Review deploy notes'); + await page.getByRole('button', { name: 'Create and exit', exact: true }).click(); + await page.waitForSelector('section.modal', { state: 'detached' }); + + await expectTaskListOpen(page, 'Review deploy notes'); + + await page.reload(); + + await expect(page.locator('lt-select-add .top').first()).toContainText('Work', { + timeout: 15000, + }); + await expectTaskListOpen(page, 'Review deploy notes'); + + await page.locator('img[alt="Add block"]').first().click(); + await expect(page.locator('lt-block-edit .create-card').getByLabel('Already done')).not.toBeChecked(); + }); + + test('keep tasks open expands existing pending tasks after reload', async ({ page }) => { + await page.goto('/'); + + await expect(page.getByText('Welcome to Life Towers')).toBeVisible({ timeout: 15000 }); + await page.getByRole('button', { name: 'Start empty' }).click(); + await page.waitForSelector('section.modal', { state: 'detached' }); + + await page.locator('lt-select-add .top').first().click(); + await page.locator('lt-select-add input[placeholder="Add a value…"]').fill('Ops'); + await page.locator('lt-select-add input[placeholder="Add a value…"]').press('Enter'); + + await page.locator('img[alt="Add tower"]').click(); + await page.locator('input[placeholder="New tower"]').fill('Queue'); + await page.locator('lt-tower-settings button[type="submit"]').click(); + await page.waitForSelector('section.modal', { state: 'detached' }); + + await page.locator('img[alt="Add block"]').first().click(); + await page.locator('lt-block-edit lt-select-add .top').click(); + await page.locator('lt-block-edit lt-select-add input[placeholder="Add a value…"]').fill('triage'); + await page.locator('lt-block-edit lt-select-add input[placeholder="Add a value…"]').press('Enter'); + await page + .locator('textarea[placeholder="Write a description here…"]') + .fill('Clean up alerts'); + await page.getByLabel('Already done').uncheck(); + await page.getByRole('button', { name: 'Create and exit', exact: true }).click(); + await page.waitForSelector('section.modal', { state: 'detached' }); + + await page.getByRole('button', { name: 'Settings' }).click(); + await page.getByText('Keep tasks open').click(); + await page.locator('lt-settings .exit').click(); + await page.waitForSelector('section.modal', { state: 'detached' }); + + await page.reload(); + + await expect(page.locator('lt-select-add .top').first()).toContainText('Ops', { + timeout: 15000, + }); + await expectTaskListOpen(page, 'Clean up alerts'); + }); }); diff --git a/frontend/e2e/visuals.spec.ts b/frontend/e2e/visuals.spec.ts index 95f94ba..dc538ab 100644 --- a/frontend/e2e/visuals.spec.ts +++ b/frontend/e2e/visuals.spec.ts @@ -1,5 +1,10 @@ import { test } from '@playwright/test'; +test.skip( + process.env['CAPTURE_VISUALS'] !== '1', + 'Set CAPTURE_VISUALS=1 to run the visual screenshot capture suite.', +); + /** * Visual capture: drives the UI into key states and writes screenshots * for human review of the legacy-styled design. @@ -7,8 +12,15 @@ import { test } from '@playwright/test'; test.describe('Life Towers visuals', () => { test('capture key UI states', async ({ page }) => { await page.goto('/'); - await page.waitForSelector('text=Add a new page to get started!', { timeout: 15000 }); - await page.screenshot({ path: 'visuals/01-empty-state.png', fullPage: true }); + await page.waitForSelector('text=Welcome to Life Towers', { timeout: 15000 }); + await page.waitForTimeout(350); // let the welcome modal finish fade-in + await page.screenshot({ path: 'visuals/01-welcome-modal.png', fullPage: true }); + + // Dismiss the welcome modal with Start empty, then continue. + await page.getByRole('button', { name: 'Start empty' }).click(); + await page.waitForSelector('section.modal', { state: 'detached' }); + + await page.screenshot({ path: 'visuals/01b-empty-state-after-dismiss.png', fullPage: true }); // Open the page dropdown (without creating a page yet). await page.locator('lt-select-add .top').first().click(); @@ -26,7 +38,7 @@ test.describe('Life Towers visuals', () => { await page.locator('img[alt="Add tower"]').click(); await page.waitForSelector('section.modal.active'); await page.waitForTimeout(350); - await page.locator('input[placeholder="Tower name…"]').fill('Reading'); + await page.locator('input[placeholder="New tower"]').fill('Reading'); await page.screenshot({ path: 'visuals/03-new-tower-modal.png', fullPage: true }); await page.locator('lt-tower-settings button[type="submit"]').click(); await page.waitForSelector('section.modal', { state: 'detached' }); @@ -46,20 +58,26 @@ test.describe('Life Towers visuals', () => { await createCard .locator('textarea[placeholder="Write a description here…"]') .fill('Finish The Brothers Karamazov'); - // Toggle to "Task hasn't been finished yet" so this becomes a pending task. - await createCard - .locator('lt-toggle span') - .filter({ hasText: "This task hasn't been finished yet" }) - .click(); - await page.getByRole('button', { name: 'Create and exit' }).click(); + // Uncheck "Already done" so this becomes a pending task. + await createCard.getByLabel('Already done').uncheck(); + await page.getByRole('button', { name: 'Create and exit', exact: true }).click(); await page.waitForSelector('section.modal', { state: 'detached' }); // Open the tasks accordion to show the new tickbox. await page.waitForTimeout(200); - await page.locator('lt-tasks .container').click(); + await page.locator('lt-tasks .header').click(); await page.waitForTimeout(300); await page.screenshot({ path: 'visuals/04b-tasks-accordion-with-tickbox.png', fullPage: true }); + // Hover the tickbox: must NOT pop a scrollbar in the accordion, must NOT + // paint the global button-underline bar across the top, and the ✓ must stay + // centred (regression guard — see tasks.component .tickbox::after). + await page.locator('lt-tasks .tickbox').first().hover(); + await page.waitForTimeout(350); + await page.locator('lt-tasks .container').screenshot({ + path: 'visuals/04c-tasks-tickbox-hover.png', + }); + // Add a couple more blocks. for (const desc of ['Read about WebAssembly GC', 'Re-read "Out of the Tar Pit"']) { await page.locator('img[alt="Add block"]').first().click(); @@ -70,7 +88,7 @@ test.describe('Life Towers visuals', () => { await cc.locator('lt-select-add .top').click(); await page.waitForTimeout(100); await cc.locator('textarea[placeholder="Write a description here…"]').fill(desc); - await page.getByRole('button', { name: 'Create and exit' }).click(); + await page.getByRole('button', { name: 'Create and exit', exact: true }).click(); await page.waitForSelector('section.modal', { state: 'detached' }); } @@ -100,7 +118,7 @@ test.describe('Life Towers visuals', () => { await page.locator('img[alt="Add tower"]').click(); await page.waitForSelector('section.modal.active'); await page.waitForTimeout(350); - await page.locator('input[placeholder="Tower name…"]').fill('Side projects'); + await page.locator('input[placeholder="New tower"]').fill('Side projects'); await page.locator('lt-tower-settings button[type="submit"]').click(); await page.waitForSelector('section.modal', { state: 'detached' }); await page.waitForTimeout(300); @@ -157,4 +175,39 @@ test.describe('Life Towers visuals', () => { await page.waitForTimeout(350); await page.screenshot({ path: 'visuals/11-settings-modal.png', fullPage: true }); }); + + test('"Load sample towers" populates a sample page', async ({ page }) => { + await page.goto('/'); + await page.waitForSelector('text=Welcome to Life Towers', { timeout: 15000 }); + await page.waitForTimeout(350); + await page.getByRole('button', { name: 'Load sample towers' }).click(); + await page.waitForSelector('section.modal', { state: 'detached' }); + await page.waitForTimeout(1800); + await page.screenshot({ path: 'visuals/12-example-data.png', fullPage: true }); + }); + + test('Mobile viewport — welcome + example + carousel', async ({ browser }) => { + const ctx = await browser.newContext({ viewport: { width: 390, height: 844 } }); + const page = await ctx.newPage(); + + await page.goto((process.env['PLAYWRIGHT_BASE_URL'] ?? 'http://localhost:8000') + '/'); + await page.waitForSelector('text=Welcome to Life Towers', { timeout: 15000 }); + await page.waitForTimeout(350); + await page.screenshot({ path: 'visuals/13-mobile-welcome.png', fullPage: true }); + + await page.getByRole('button', { name: 'Load sample towers' }).click(); + await page.waitForSelector('section.modal', { state: 'detached' }); + await page.waitForTimeout(1800); + await page.screenshot({ path: 'visuals/14-mobile-populated.png', fullPage: true }); + + // Open the block-edit carousel for the first tower's first task. + await page.locator('lt-tasks .header').first().click(); + await page.waitForTimeout(400); + await page.locator('lt-tasks .task-description').first().click(); + await page.waitForSelector('section.modal.active'); + await page.waitForTimeout(400); + await page.screenshot({ path: 'visuals/15-mobile-carousel.png', fullPage: true }); + + await ctx.close(); + }); });