mirror of
https://github.com/hanzoai/gui.git
synced 2026-10-25 02:31:41 +00:00
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]>
230 lines
7.5 KiB
TypeScript
230 lines
7.5 KiB
TypeScript
import { expect, test, type Page } from '@playwright/test'
|
|
import { setupPage } from './test-utils'
|
|
|
|
/**
|
|
* TOOLTIP POSITION JUMP TEST (Motion Driver Only)
|
|
*
|
|
* Bug: When using animatePosition with a scoped tooltip,
|
|
* rapidly moving between triggers causes the tooltip to JUMP to wrong position
|
|
* (often near origin/top-left) before animating back.
|
|
*
|
|
* Key: Must actually MOVE the mouse across, not just call hover() which teleports.
|
|
*/
|
|
|
|
test.describe('Tooltip Position Jump', () => {
|
|
test.beforeEach(async ({ page }, testInfo) => {
|
|
// only test with motion driver - this is where the bug manifests
|
|
const driver = (testInfo.project?.metadata as any)?.animationDriver
|
|
if (driver !== 'motion') {
|
|
test.skip()
|
|
return
|
|
}
|
|
|
|
await setupPage(page, { name: 'TooltipPositionJumpCase', type: 'useCase' })
|
|
await page.waitForTimeout(500)
|
|
})
|
|
|
|
test('scoped tooltip should not jump when moving quickly between triggers', async ({
|
|
page,
|
|
}) => {
|
|
// get button positions
|
|
const hireBtn = page.locator('[data-testid="tooltip-trigger-hire"]')
|
|
const bentoBtn = page.locator('[data-testid="tooltip-trigger-bento"]')
|
|
const takeoutBtn = page.locator('[data-testid="tooltip-trigger-takeout"]')
|
|
|
|
const hireBox = await hireBtn.boundingBox()
|
|
const bentoBox = await bentoBtn.boundingBox()
|
|
const takeoutBox = await takeoutBtn.boundingBox()
|
|
|
|
if (!hireBox || !bentoBox || !takeoutBox) {
|
|
throw new Error('Could not find button positions')
|
|
}
|
|
|
|
// inject position tracking script
|
|
await page.evaluate(() => {
|
|
;(window as any).__tooltipPositions = []
|
|
;(window as any).__trackTooltip = () => {
|
|
const el = document.querySelector(
|
|
'[data-testid="tooltip-jump-content"]'
|
|
) as HTMLElement
|
|
if (!el) return null
|
|
|
|
const style = getComputedStyle(el)
|
|
const transform = style.transform
|
|
if (!transform || transform === 'none') return null
|
|
|
|
const match = transform.match(
|
|
/matrix\([^,]+,\s*[^,]+,\s*[^,]+,\s*[^,]+,\s*([^,]+),\s*([^)]+)\)/
|
|
)
|
|
if (!match) return null
|
|
|
|
const pos = { x: parseFloat(match[1]), y: parseFloat(match[2]), time: Date.now() }
|
|
;(window as any).__tooltipPositions.push(pos)
|
|
return pos
|
|
}
|
|
|
|
// start tracking at 60fps
|
|
const track = () => {
|
|
;(window as any).__trackTooltip()
|
|
requestAnimationFrame(track)
|
|
}
|
|
requestAnimationFrame(track)
|
|
})
|
|
|
|
// step 1: hover on rightmost button (HIRE) and wait for tooltip to fully appear
|
|
const hireCenter = {
|
|
x: hireBox.x + hireBox.width / 2,
|
|
y: hireBox.y + hireBox.height / 2,
|
|
}
|
|
await page.mouse.move(hireCenter.x, hireCenter.y)
|
|
await page.waitForTimeout(800)
|
|
|
|
// step 2: move mouse QUICKLY across to leftmost button (TAKEOUT)
|
|
// must actually traverse the path, not teleport
|
|
const takeoutCenter = {
|
|
x: takeoutBox.x + takeoutBox.width / 2,
|
|
y: takeoutBox.y + takeoutBox.height / 2,
|
|
}
|
|
|
|
// do fast sweep: HIRE -> TAKEOUT in ~100ms total
|
|
const steps = 10
|
|
const totalTime = 100
|
|
const stepDelay = totalTime / steps
|
|
|
|
for (let i = 1; i <= steps; i++) {
|
|
const t = i / steps
|
|
const x = hireCenter.x + (takeoutCenter.x - hireCenter.x) * t
|
|
const y = hireCenter.y + (takeoutCenter.y - hireCenter.y) * t
|
|
await page.mouse.move(x, y)
|
|
await page.waitForTimeout(stepDelay)
|
|
}
|
|
|
|
await page.waitForTimeout(300)
|
|
|
|
// analyze for jumps
|
|
const positions = await page.evaluate(() => (window as any).__tooltipPositions)
|
|
const jumps: any[] = []
|
|
|
|
for (let i = 1; i < positions.length; i++) {
|
|
const prev = positions[i - 1]
|
|
const curr = positions[i]
|
|
const dx = Math.abs(curr.x - prev.x)
|
|
const dy = Math.abs(curr.y - prev.y)
|
|
const delta = Math.sqrt(dx * dx + dy * dy)
|
|
const timeDelta = curr.time - prev.time
|
|
|
|
// jump: large delta in short time, going toward origin
|
|
if (timeDelta < 50 && delta > 100) {
|
|
const jumpingLeft = curr.x < prev.x - 100
|
|
const jumpingUp = curr.y < prev.y - 50
|
|
const jumpToNearOrigin = curr.x < 200 && curr.y < 200 && prev.x > 300
|
|
|
|
if (jumpingLeft || jumpingUp || jumpToNearOrigin) {
|
|
jumps.push({ from: prev, to: curr, delta: Math.round(delta) })
|
|
}
|
|
}
|
|
}
|
|
|
|
console.log(`Collected ${positions.length} positions, found ${jumps.length} jumps`)
|
|
if (jumps.length > 0) {
|
|
console.log('Jumps:', JSON.stringify(jumps, null, 2))
|
|
}
|
|
|
|
expect(jumps.length, `Detected ${jumps.length} position jumps!`).toBe(0)
|
|
})
|
|
|
|
test('rapid back-and-forth should not cause jumps', async ({ page }) => {
|
|
const hireBtn = page.locator('[data-testid="tooltip-trigger-hire"]')
|
|
const takeoutBtn = page.locator('[data-testid="tooltip-trigger-takeout"]')
|
|
|
|
const hireBox = await hireBtn.boundingBox()
|
|
const takeoutBox = await takeoutBtn.boundingBox()
|
|
|
|
if (!hireBox || !takeoutBox) {
|
|
throw new Error('Could not find button positions')
|
|
}
|
|
|
|
// inject tracking
|
|
await page.evaluate(() => {
|
|
;(window as any).__tooltipPositions = []
|
|
const track = () => {
|
|
const el = document.querySelector(
|
|
'[data-testid="tooltip-jump-content"]'
|
|
) as HTMLElement
|
|
if (el) {
|
|
const transform = getComputedStyle(el).transform
|
|
if (transform && transform !== 'none') {
|
|
const match = transform.match(
|
|
/matrix\([^,]+,\s*[^,]+,\s*[^,]+,\s*[^,]+,\s*([^,]+),\s*([^)]+)\)/
|
|
)
|
|
if (match) {
|
|
;(window as any).__tooltipPositions.push({
|
|
x: parseFloat(match[1]),
|
|
y: parseFloat(match[2]),
|
|
time: Date.now(),
|
|
})
|
|
}
|
|
}
|
|
}
|
|
requestAnimationFrame(track)
|
|
}
|
|
requestAnimationFrame(track)
|
|
})
|
|
|
|
const hireCenter = {
|
|
x: hireBox.x + hireBox.width / 2,
|
|
y: hireBox.y + hireBox.height / 2,
|
|
}
|
|
const takeoutCenter = {
|
|
x: takeoutBox.x + takeoutBox.width / 2,
|
|
y: takeoutBox.y + takeoutBox.height / 2,
|
|
}
|
|
|
|
// hover HIRE first
|
|
await page.mouse.move(hireCenter.x, hireCenter.y)
|
|
await page.waitForTimeout(600)
|
|
|
|
// do 3 rapid back-and-forth sweeps
|
|
for (let sweep = 0; sweep < 3; sweep++) {
|
|
for (let i = 0; i <= 8; i++) {
|
|
const t = i / 8
|
|
await page.mouse.move(
|
|
hireCenter.x + (takeoutCenter.x - hireCenter.x) * t,
|
|
hireCenter.y + (takeoutCenter.y - hireCenter.y) * t
|
|
)
|
|
await page.waitForTimeout(10)
|
|
}
|
|
for (let i = 0; i <= 8; i++) {
|
|
const t = i / 8
|
|
await page.mouse.move(
|
|
takeoutCenter.x + (hireCenter.x - takeoutCenter.x) * t,
|
|
takeoutCenter.y + (hireCenter.y - takeoutCenter.y) * t
|
|
)
|
|
await page.waitForTimeout(10)
|
|
}
|
|
}
|
|
|
|
await page.waitForTimeout(300)
|
|
|
|
const positions = await page.evaluate(() => (window as any).__tooltipPositions)
|
|
const jumps: any[] = []
|
|
|
|
for (let i = 1; i < positions.length; i++) {
|
|
const prev = positions[i - 1]
|
|
const curr = positions[i]
|
|
const delta = Math.sqrt((curr.x - prev.x) ** 2 + (curr.y - prev.y) ** 2)
|
|
const timeDelta = curr.time - prev.time
|
|
|
|
if (timeDelta < 50 && delta > 100) {
|
|
const jumpingTowardOrigin = curr.x < prev.x - 50 && curr.x < 200
|
|
if (jumpingTowardOrigin) {
|
|
jumps.push({ from: prev, to: curr, delta: Math.round(delta) })
|
|
}
|
|
}
|
|
}
|
|
|
|
console.log(`Found ${jumps.length} jumps in back-and-forth test`)
|
|
expect(jumps.length).toBe(0)
|
|
})
|
|
})
|