diff --git a/docs/design/CHAT-BROWSER-ACCEPTANCE.md b/docs/design/CHAT-BROWSER-ACCEPTANCE.md new file mode 100644 index 00000000..31bf2555 --- /dev/null +++ b/docs/design/CHAT-BROWSER-ACCEPTANCE.md @@ -0,0 +1,7 @@ +# Chat browser acceptance + +Mobile touch behavior and desktop window resizing use separate browser contexts. The mobile check verifies content clearance, touch targets, menus, opening and closing Chat, and retained drafts. The desktop check resizes from narrow to wide and back with enlarged text, retaining drafts and verifying that close controls remain actionable. + +Do not resize a phone-emulated context to a desktop window as a substitute for desktop coverage. Chromium can retain a zoomed visual viewport and offset its automation coordinates after the focused composer is resized. The regression measured a 160px difference between the DOM close-button position and the automation bounding box, with visual viewport scale 1.125. This is not a reason to disable user zoom, force a click, or change application layout. + +The checks live in `e2e/mobile-chat-entry.spec.ts`. Their task fixture has a distinct title from the readability suite so failed cleanup cannot make unrelated heading selectors ambiguous. These checks do not establish packaged, signed, installed-app, documentation-media, or release acceptance. diff --git a/e2e/mobile-chat-entry.spec.ts b/e2e/mobile-chat-entry.spec.ts index f2d0f3e9..e41e7d61 100644 --- a/e2e/mobile-chat-entry.spec.ts +++ b/e2e/mobile-chat-entry.spec.ts @@ -6,11 +6,11 @@ test.beforeEach(async ({ page }) => bypassAuth(page)); test.afterEach(async ({ page }) => cleanupRoutes(page)); test('mobile chat entry does not cover task content', async ({ page }, testInfo) => { - const task = await seedTestTask(page, { title: 'Readable task summary', type: 'code' }); + const task = await seedTestTask(page, { title: 'Mobile chat entry fixture', type: 'code' }); try { await page.goto('/'); await page.addStyleTag({ content: ':root { font-size: 20px !important; }' }); - const title = page.getByRole('heading', { name: 'Readable task summary', exact: true }); + const title = page.getByRole('heading', { name: 'Mobile chat entry fixture', exact: true }); await expect(title).toBeVisible(); const trigger = page.getByRole('button', { name: 'Open chat', exact: true }); await expect(trigger).toBeVisible(); @@ -44,7 +44,7 @@ test('mobile chat entry does not cover task content', async ({ page }, testInfo) ).toBe(true); } const status = page.getByRole('combobox', { - name: 'Change status for Readable task summary', + name: 'Change status for Mobile chat entry fixture', exact: true, }); await status.evaluate((el) => { @@ -71,13 +71,9 @@ test('mobile chat entry does not cover task content', async ({ page }, testInfo) body: await page.screenshot(), contentType: 'image/png', }); - await page.setViewportSize({ width: 1440, height: 900 }); - await expect(composer).toHaveValue('Unsent mobile draft'); await page.getByRole('button', { name: 'Close Board Chat panel', exact: true }).click(); await expect(composer).not.toBeVisible(); await expect(trigger).toBeVisible(); - expect(await trigger.evaluate((el) => getComputedStyle(el).position)).toBe('fixed'); - await page.setViewportSize({ width: 320, height: 844 }); await trigger.click(); await expect(composer).toHaveValue('Unsent mobile draft'); await page.getByRole('button', { name: 'Close Board Chat panel', exact: true }).click(); @@ -88,6 +84,30 @@ test('mobile chat entry does not cover task content', async ({ page }, testInfo) test.describe('wide browser chat entry', () => { test.use({ viewport: { width: 1440, height: 900 }, isMobile: false, hasTouch: false }); + test('retains drafts and actionable controls through narrow-wide-narrow window resizing', async ({ + page, + }) => { + // Desktop window resizing must not retain a phone-emulation pinch-zoom viewport. + await page.setViewportSize({ width: 320, height: 844 }); + await page.goto('/'); + await page.addStyleTag({ content: ':root { font-size: 20px !important; }' }); + const trigger = page.getByRole('button', { name: 'Open chat', exact: true }); + await trigger.click(); + const composer = page.getByRole('textbox', { name: 'Message Board Chat', exact: true }); + await composer.fill('Unsent resizing draft'); + await page.setViewportSize({ width: 1440, height: 900 }); + await expect(composer).toHaveValue('Unsent resizing draft'); + await page.getByRole('button', { name: 'Close Board Chat panel', exact: true }).click(); + await expect(composer).not.toBeVisible(); + await expect(trigger).toBeVisible(); + expect(await trigger.evaluate((el) => getComputedStyle(el).position)).toBe('fixed'); + await page.setViewportSize({ width: 320, height: 844 }); + await trigger.click(); + await expect(composer).toHaveValue('Unsent resizing draft'); + await page.getByRole('button', { name: 'Close Board Chat panel', exact: true }).click(); + await expect(composer).not.toBeVisible(); + }); + test('retains the floating chat control without mobile navigation', async ({ page }) => { await page.goto('/'); const trigger = page.getByRole('button', { name: 'Open chat', exact: true });