Files
gui/apps/sandbox/tests/tooltip-prod-repro.test.ts
zeekayandHanzo Dev 69c6e860d7 transplant onto tamagui v2.6.0, brand as a re-runnable transform
This repo could never sync. There was NO merge-base with tamagui — 372 commits
here against 15,791 there, unrelated histories — because it began as a
copy-paste rebrand with a flattened layout. Every "sync" since was a hand
re-application that drifted, which is why the tree sat on 7.3.1 while upstream
shipped fixes we then rediscovered by hand.

This commit's PARENT IS TAMAGUI. A future sync is `git merge tama/main` then
`node scripts/rebrand.mjs` — a real merge, because the history is now shared.

174 packages: 166 upstream, our 8 restored (chrome, shell, telemetry, recipes,
get, font-geist-{sans,mono}, @hanzo/booker), bento-get gained. 170/170 build.

Our patches to files UPSTREAM ALSO OWNS are carried, which the package-level
delta missed and which was where the value actually was:
  - useMedia's server snapshot (`initState ?? getMedia()`). Upstream still
    returns undefined there, so every Tamagui Dialog kills a prerender — the
    exact crash hanzo.app spent a day on.
  - data-slot on web host nodes, absent upstream entirely.
Both applied ONTO upstream's newer files (it rewrote useMedia +150/-63 and
createComponent +160/-55), marked `PATCH (ours, carried across the transplant)`.

Seven traps are encoded in scripts/rebrand.mjs, each found by a gate:
skip itself and lockfiles; special-case the double-branded names in BOTH scoped
and bare form; map path strings, not just paths; drop a level for the flattened
group; check disk rather than a stale index.

Rust build artifacts are NOT committed. An earlier draft of this commit swept
apps/desktop/src-tauri/target/debug in via `git add -A` — 3,859 files including
a 272MB .a, over GitHub's 100MB cap, so the push could never have landed. They
are gitignored now.

Co-Authored-By: Hanzo Dev <[email protected]>
2026-07-30 18:25:06 -07:00

136 lines
4.5 KiB
TypeScript

import { expect, test, type Page } from '@playwright/test'
// test against the real hanzogui.dev local server (port 5556)
// to reproduce the stuck tooltip repositioning bug
test.describe('Tooltip repositioning on hanzogui.dev', () => {
test.skip(
!process.env.TEST_PROD_SITE,
'set TEST_PROD_SITE=1 to run against local hanzogui.dev on port 5556'
)
const baseURL = 'http://localhost:5556'
test('tooltip repositions between PromoLinksRow triggers', async ({ page }) => {
await page.goto(baseURL + '/')
// wait for page to hydrate
await page.waitForTimeout(2000)
// find the promo buttons by text
const takeout = page.getByText('Starter Kit').first()
const hireUs = page.getByText('Hire Us').first()
// fallback: look for buttons that match PromoLinksRow
const takeoutBtn = (await takeout.isVisible())
? takeout
: page.getByText('Takeout').first()
const hireUsBtn = (await hireUs.isVisible())
? hireUs
: page.getByText('Consulting').first()
await expect(takeoutBtn).toBeVisible({ timeout: 10000 })
const aBox = await takeoutBtn.boundingBox()
expect(aBox).toBeTruthy()
// hover first button
await page.mouse.move(aBox!.x + aBox!.width / 2, aBox!.y + aBox!.height / 2)
await page.waitForTimeout(500)
// check if tooltip opened
const tooltip = page.locator('[data-floating-ui-portal]').first()
const isOpen = await tooltip.isVisible().catch(() => false)
console.log(`tooltip visible after hover: ${isOpen}`)
if (isOpen) {
const posA = await tooltip.evaluate((el) => {
const r = el.getBoundingClientRect()
return { x: r.x + r.width / 2, y: r.y }
})
console.log(`position at first trigger: x=${posA.x.toFixed(1)}`)
// find and hover the last button
const cBox = await hireUsBtn.boundingBox()
if (cBox) {
await page.mouse.move(cBox.x + cBox.width / 2, cBox.y + cBox.height / 2, {
steps: 2,
})
await page.waitForTimeout(500)
const posC = await tooltip.evaluate((el) => {
const r = el.getBoundingClientRect()
return { x: r.x + r.width / 2, y: r.y }
})
console.log(`position at last trigger: x=${posC.x.toFixed(1)}`)
const xDiff = Math.abs(posC.x - posA.x)
console.log(`position difference: ${xDiff.toFixed(1)}`)
expect(
xDiff,
`tooltip should reposition between triggers (diff=${xDiff.toFixed(1)})`
).toBeGreaterThan(20)
}
}
})
test('tooltip repositions during hydration window', async ({ page }) => {
// navigate and interact DURING hydration
await page.goto(baseURL + '/')
// wait for the buttons to appear in DOM but NOT for full hydration
await page.waitForSelector('text=Starter Kit', { timeout: 15000 })
// brief wait for React to hydrate (attach handlers)
await page.waitForTimeout(200)
const takeout = page.getByText('Starter Kit').first()
const hireUs = page.getByText('Hire Us').first()
const aBox = await takeout.boundingBox()
if (!aBox) {
test.skip(true, 'PromoLinksRow buttons not found on page')
return
}
// hover first trigger immediately
await page.mouse.move(aBox.x + aBox.width / 2, aBox.y + aBox.height / 2)
await page.waitForTimeout(300)
// quickly switch to last trigger
const cBox = await hireUs.boundingBox()
if (!cBox) {
test.skip(true, 'Hire Us button not found')
return
}
await page.mouse.move(cBox.x + cBox.width / 2, cBox.y + cBox.height / 2, { steps: 2 })
await page.waitForTimeout(500)
// check tooltip position
const tooltipContent = page.locator('[data-floating-ui-portal]').first()
const isVisible = await tooltipContent.isVisible().catch(() => false)
if (isVisible) {
const rect = await tooltipContent.evaluate((el) => {
const r = el.getBoundingClientRect()
return { x: r.x + r.width / 2, y: r.y }
})
// tooltip should be near the last hovered trigger (hireUs), not stuck at first
const distToFirst = Math.abs(rect.x - (aBox.x + aBox.width / 2))
const distToLast = Math.abs(rect.x - (cBox.x + cBox.width / 2))
console.log(
`tooltip at x=${rect.x.toFixed(0)}, ` +
`distToFirst=${distToFirst.toFixed(0)}, distToLast=${distToLast.toFixed(0)}`
)
expect(
distToLast,
`tooltip should be near last trigger, not stuck at first. ` +
`distToFirst=${distToFirst.toFixed(0)}, distToLast=${distToLast.toFixed(0)}`
).toBeLessThan(distToFirst)
}
})
})