Files
chat/e2e/specs/starters.spec.ts
zeekayandHanzo Dev 21fd0f78c3 fix(footer): a decorative strip is not a pointer target
The footer's host is `relative w-full` with no height of its own, so
`absolute bottom-0` pins the strip OVER whatever renders above it — on the
landing, the row of conversation starters. While the strip was a pointer target
it swallowed clicks aimed at the chips' lower half: the chip looked clickable
and did nothing. Same defect class as the starter bug just fixed — the affordance
lies about itself.

`pointer-events-none` on the strip, re-armed by `pointer-events-auto` on the
links, so the overlay stops intercepting and only its links stay clickable. Not
a z-index nudge: raising the chips would leave a decorative bar stealing
pointers from anything else that ever renders beneath it. The three duplicated
link class strings collapse into one LINK constant.

Tests: e2e/specs/starters.spec.ts asserts the geometry — `elementFromPoint` at
the chip's exact CENTER must resolve to the chip, then a plain center click must
send. It fails on the old geometry, naming the footer as the element on top.
Footer.spec.tsx pins the contract in jsdom, which does no layout.

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-26 18:12:55 -07:00

60 lines
2.5 KiB
TypeScript

import { expect, test } from '@playwright/test';
/**
* A starter chip must be clickable where a user actually clicks it: the middle.
*
* The footer strip ("Powered by Hanzo AI · Privacy policy · Terms of service")
* is an overlay — its host has no height, so `absolute bottom-0` pins it over
* the row of starter chips. While it was a pointer target it swallowed clicks
* aimed at the chips' lower half: the chip looked clickable and did nothing.
*
* These assertions fail on that geometry. `elementFromPoint` at the chip's exact
* center names the culprit, and the plain `click()` below is the user's gesture —
* Playwright verifies the hit target at the click point, so an intercepted chip
* throws rather than silently passing.
*
* Runs against E2E_BASE_URL so the same spec can be pointed at a deployment.
*/
const BASE = process.env.E2E_BASE_URL ?? 'http://localhost:3080';
test.describe('Conversation starters', () => {
test('a starter chip owns its own center — the footer does not intercept', async ({ page }) => {
await page.goto(BASE, { waitUntil: 'domcontentloaded' });
// The landing opens in Search; the starters live on the Chat surface.
const modeChip = page.getByRole('button', { name: 'Chat', exact: true }).first();
await modeChip.waitFor({ state: 'visible', timeout: 60000 });
await modeChip.click();
const chip = page.getByRole('button', { name: 'Explain', exact: true }).first();
await chip.waitFor({ state: 'visible' });
// 1. Geometry: the chip's exact center must resolve to the chip itself.
const box = (await chip.boundingBox())!;
expect(box).not.toBeNull();
const owner = await page.evaluate(
([x, y]) => {
const el = document.elementFromPoint(x, y);
const btn = el?.closest('button');
return {
isChip: btn?.textContent?.trim() === 'Explain',
// Names the overlay when it is the one on top.
top: el?.closest('[role="contentinfo"]') ? 'footer(contentinfo)' : (el?.tagName ?? 'none'),
};
},
[box.x + box.width / 2, box.y + box.height / 2],
);
expect(owner.top).not.toBe('footer(contentinfo)');
expect(owner.isChip).toBe(true);
// 2. The gesture: a plain center click, with Playwright's hit-target check.
await chip.click();
// 3. And it sent — the chip's text becomes a turn, the composer clears.
await expect(page.getByText('Explain how HTTPS keeps a connection private')).toBeVisible({
timeout: 20000,
});
await expect(page.locator('textarea').first()).toHaveValue('');
});
});