mirror of
https://github.com/hanzoai/chat.git
synced 2026-09-30 20:51:17 +00:00
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]>
60 lines
2.5 KiB
TypeScript
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('');
|
|
});
|
|
});
|