mirror of
https://github.com/hanzo-apps/hanzo.ai.git
synced 2026-10-29 07:20:43 +00:00
89 lines
4.0 KiB
TypeScript
89 lines
4.0 KiB
TypeScript
import { expect, test } from '@playwright/test'
|
|
import { credential, signedIn } from './credential'
|
|
|
|
/**
|
|
* A COMPOSER'S CONTROLS STAY WHERE A HAND CAN REACH THEM.
|
|
*
|
|
* The defect this guards is not a style: the model picker's trigger is
|
|
* `width: 100%`, and in a flex row that resolves against whatever it is given —
|
|
* unbounded, that is the whole row, and the mic and send are laid out past the
|
|
* capsule and off the window. Measured at 1280 the picker was 750 wide in a 750
|
|
* row and send began at 1283. Nothing about that reads as broken in the source.
|
|
*
|
|
* Both composers are checked, because this site has two and they answer to
|
|
* different rules: /chat draws @hanzo/ui's `Composer` (a field over a footer
|
|
* row), and the landing and /dev draw @hanzo/composer's gui components
|
|
* (`Composer`, `Control`, `Field`). A guard on one says nothing about
|
|
* the other.
|
|
*
|
|
* Four widths, because the failure was invisible at the widest and total at the
|
|
* narrowest.
|
|
*/
|
|
const SIZES = [
|
|
{ w: 1440, h: 900 },
|
|
{ w: 1280, h: 720 },
|
|
{ w: 834, h: 1112 },
|
|
{ w: 390, h: 844 },
|
|
]
|
|
|
|
|
|
/** Everything in a composer that a reader presses or types into. */
|
|
const CONTROLS = 'input, textarea, [data-slot="prism-control"], [role="combobox"], [aria-label="Send"]'
|
|
|
|
test.describe('composer', () => {
|
|
test('the workspace composer keeps its controls inside the capsule', async ({ page }) => {
|
|
const TOKEN = credential()
|
|
test.skip(!TOKEN, 'no local credential: the room needs somebody signed in')
|
|
await page.addInitScript(signedIn(TOKEN), TOKEN)
|
|
|
|
for (const s of SIZES) {
|
|
await page.setViewportSize({ width: s.w, height: s.h })
|
|
await page.goto('/chat')
|
|
await page.waitForLoadState('networkidle')
|
|
await page.evaluate(() => document.fonts.ready)
|
|
const look = () => page.evaluate((sel) => {
|
|
const field = document.querySelector('textarea')
|
|
if (!field) return ['no composer']
|
|
// The capsule is the first ancestor as wide as the field's own column.
|
|
// Relative, because on a phone nothing is 700 wide and an absolute floor
|
|
// walks off the top of the document.
|
|
let cap: HTMLElement = field.parentElement!
|
|
const want = Math.min(700, innerWidth * 0.6)
|
|
while (cap.parentElement && cap.getBoundingClientRect().width < want) cap = cap.parentElement
|
|
const edge = cap.getBoundingClientRect().right
|
|
return [...cap.querySelectorAll(sel)]
|
|
.map((e) => ({ name: e.getAttribute('aria-label') || e.tagName, r: e.getBoundingClientRect() }))
|
|
.filter((c) => c.r.width > 0 && (c.r.right > edge + 1 || c.r.right > innerWidth + 1 || c.r.bottom > innerHeight + 1))
|
|
.map((c) => c.name)
|
|
}, CONTROLS)
|
|
await expect.poll(look, { timeout: 10_000, message: `at ${s.w}x${s.h}` }).toEqual([])
|
|
}
|
|
})
|
|
|
|
for (const route of ['/']) {
|
|
test(`the composer on ${route} keeps its controls inside the form`, async ({ page }) => {
|
|
test.slow()
|
|
for (const s of SIZES) {
|
|
await page.setViewportSize({ width: s.w, height: s.h })
|
|
await page.goto(route)
|
|
await page.waitForLoadState('networkidle')
|
|
await page.evaluate(() => document.fonts.ready)
|
|
const look = () => page.evaluate((sel) => {
|
|
const form = document.querySelector('[data-slot="prism"]')
|
|
if (!form) return ['no composer']
|
|
// The fold takes the whole first screen now, so the composer starts
|
|
// below it: bring it on screen, because a control under the fold is
|
|
// not one outside its form.
|
|
form.scrollIntoView({ block: 'center' })
|
|
const edge = form.getBoundingClientRect().right
|
|
return [...form.querySelectorAll(sel)]
|
|
.map((e) => ({ name: e.getAttribute('aria-label') || e.tagName, r: e.getBoundingClientRect() }))
|
|
.filter((c) => c.r.width > 0 && (c.r.right > edge + 1 || c.r.right > innerWidth + 1 || c.r.bottom > innerHeight + 1))
|
|
.map((c) => c.name)
|
|
}, CONTROLS)
|
|
await expect.poll(look, { timeout: 10_000, message: `at ${s.w}x${s.h}` }).toEqual([])
|
|
}
|
|
})
|
|
}
|
|
})
|