ugly-app 0.1.489 → 0.1.490
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/version.d.ts +1 -1
- package/dist/cli/version.js +1 -1
- package/package.json +1 -1
- package/src/cli/version.ts +1 -1
- package/templates/.claude/skills/bot-swarm/SKILL.md +11 -2
- package/templates/.claude/skills/verify-ux/SKILL.md +72 -0
- package/templates/CLAUDE.md +16 -0
- package/templates/bots/feedback/run-bot.mjs +45 -1
- package/templates/client/allPages.ts +4 -0
- package/templates/client/pages/InspectFixtureOtherPage.tsx +14 -0
- package/templates/client/pages/InspectFixturePage.tsx +216 -0
- package/templates/shared/pages.ts +4 -0
package/dist/cli/version.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare const CLI_VERSION = "0.1.
|
|
1
|
+
export declare const CLI_VERSION = "0.1.490";
|
|
2
2
|
//# sourceMappingURL=version.d.ts.map
|
package/dist/cli/version.js
CHANGED
package/package.json
CHANGED
package/src/cli/version.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// Auto-generated by prebuild — do not edit manually
|
|
2
|
-
export const CLI_VERSION = "0.1.
|
|
2
|
+
export const CLI_VERSION = "0.1.490";
|
|
@@ -231,9 +231,9 @@ Dispatch ALL active personas as **parallel subagents** (`delegate_parallel`). If
|
|
|
231
231
|
node bots/feedback/run-bot.mjs {slug} --capture
|
|
232
232
|
```
|
|
233
233
|
|
|
234
|
-
This writes fresh PNGs + element maps to `bots/journal/cycles/screenshots/{slug}/<page>.png` and `<page>.element-map.json` reflecting the CURRENT site. No feedback is submitted yet.
|
|
234
|
+
This writes fresh PNGs + element maps to `bots/journal/cycles/screenshots/{slug}/<page>.png` and `<page>.element-map.json` reflecting the CURRENT site. **As of ugly-app 0.1.489 the driver also writes `<page>.ux-report.json`** — a structured `UglyInspectReport` capturing layout shift, animation jank, overlapping interactive controls, safe-area violations, and mobile keyboard coverage during a measurement window that includes the page's default interaction script (click first interactive `[data-id]`, wait, scroll). No feedback is submitted yet.
|
|
235
235
|
|
|
236
|
-
3. **LOOK at each page.** For each PNG under `bots/journal/cycles/screenshots/{slug}/`, inspect it using vision — the persona **must actually see** the page before writing findings:
|
|
236
|
+
3. **LOOK at each page AND READ the UX report.** For each PNG under `bots/journal/cycles/screenshots/{slug}/`, inspect it using vision — the persona **must actually see** the page before writing findings — AND read the sibling `<page>.ux-report.json` to ground the visual critique in measured defects:
|
|
237
237
|
|
|
238
238
|
- If `analyze_image` is available (ugly-studio coding-agent):
|
|
239
239
|
|
|
@@ -251,6 +251,15 @@ Dispatch ALL active personas as **parallel subagents** (`delegate_parallel`). If
|
|
|
251
251
|
|
|
252
252
|
- Cross-reference with the matching `<page>.element-map.json` to resolve specific `dataId` / source-file references when you find something worth reporting.
|
|
253
253
|
|
|
254
|
+
- **Mine `<page>.ux-report.json`** for objective defects the screenshot alone cannot show. Treat each of these as a finding worth a feedback row — quote the offending selector in the message:
|
|
255
|
+
- `safeAreaViolations[]` → `{ "type": "bug", "message": "<selector> spills outside iOS safe area on <side> edge" }`
|
|
256
|
+
- `overlaps[]` → `{ "type": "design", "message": "<a.selector> and <b.selector> overlap by Npx — likely a z-index or layout bug" }`
|
|
257
|
+
- `keyboard.coveredInputs[]` → `{ "type": "bug", "message": "<selector> is covered by the on-screen keyboard when focused" }`
|
|
258
|
+
- `cls.total > 0.1` → `{ "type": "design", "message": "page shifts significantly during load: <cls.total> across N spikes" }`
|
|
259
|
+
- `animations[].droppedFrames > 2` → `{ "type": "bug", "message": "<element> animation drops M frames over Tms" }`
|
|
260
|
+
|
|
261
|
+
Visual critique still flows from the screenshot — the structured signals add the movement-and-mobile dimension a single frame can't show.
|
|
262
|
+
|
|
254
263
|
**Hard rule — no exceptions, including cycle 001:** every active persona must perform ≥1 vision call (`analyze_image` or Read on a PNG) against its **own** captured screenshots before its findings.json is written. Writing findings.json for a persona without a logged vision call for that same persona is a **cycle failure** — stop the cycle and surface it. "All bots see the same blank canvas, I'll just author findings in a batch" is the exact shortcut this rule exists to prevent: feedback authored from imagination instead of from what the persona actually saw is dead weight that pollutes the swarm's signal.
|
|
255
264
|
|
|
256
265
|
4. **AUTHOR findings.json.** Write 1–5 findings to `bots/feedback/active/{slug}/findings.json`:
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: verify-ux
|
|
3
|
+
description: Probe the running app for objective UX defects (CLS, animation jank, control overlap, safe-area violations, keyboard covering inputs) after UI changes
|
|
4
|
+
user-invocable: true
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
After meaningful UI changes — new page, new popup, layout edits, animation work, mobile preview switch — call `inspect_ux` and act on what it finds. Don't declare UI work done without one clean probe.
|
|
8
|
+
|
|
9
|
+
## When to run
|
|
10
|
+
|
|
11
|
+
- Created or edited a page → probe its primary route + each interactive flow
|
|
12
|
+
- Added/modified a popup, modal, toast, or accordion → probe the open transition
|
|
13
|
+
- Touched animations, transitions, or scroll-driven effects → probe with `actions` driving the trigger
|
|
14
|
+
- Switched a screen to a new device profile → probe `device: 'ios'` and `device: 'android'`
|
|
15
|
+
- Added a form with focusable inputs near the bottom of a page → probe `simulate_keyboard: true`
|
|
16
|
+
|
|
17
|
+
## How to call
|
|
18
|
+
|
|
19
|
+
`inspect_ux` runs `window.__uglyInspect()` inside an off-screen browser pointed at your session's dev server. Observers run continuously from page load; the tool reads a measurement WINDOW so you can drive interactions and see what happened during them.
|
|
20
|
+
|
|
21
|
+
Examples:
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
// Initial page load on iOS
|
|
25
|
+
inspect_ux({ url_path: '/feed', device: 'ios' })
|
|
26
|
+
|
|
27
|
+
// SPA navigation jank
|
|
28
|
+
inspect_ux({ actions: [{ click: '[data-id=settings-link]' }, { wait: 800 }] })
|
|
29
|
+
|
|
30
|
+
// Popup entry transition
|
|
31
|
+
inspect_ux({ url_path: '/profile', actions: [{ click: '[data-id=edit-btn]' }, { wait: 600 }] })
|
|
32
|
+
|
|
33
|
+
// Mobile keyboard covering input
|
|
34
|
+
inspect_ux({
|
|
35
|
+
url_path: '/signup',
|
|
36
|
+
device: 'ios',
|
|
37
|
+
actions: [
|
|
38
|
+
{ focus: '[data-id=email-field]' },
|
|
39
|
+
{ simulate_keyboard: true },
|
|
40
|
+
{ wait: 400 },
|
|
41
|
+
],
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
// Scroll-driven animations
|
|
45
|
+
inspect_ux({ actions: [{ scroll: { to: 2000 } }, { wait: 1200 }] })
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## What to act on
|
|
49
|
+
|
|
50
|
+
The returned `UglyInspectReport` is structured JSON. Treat each of these as a build failure that must be fixed before declaring done:
|
|
51
|
+
|
|
52
|
+
| Field | Defect threshold |
|
|
53
|
+
|---|---|
|
|
54
|
+
| `cls.total` | > 0.1 — significant layout shift during the window |
|
|
55
|
+
| `longTasks[]` | Any entry > 100ms while an animation is in flight (`during` set) |
|
|
56
|
+
| `overlaps[]` | Any entry — two interactive controls share screen space |
|
|
57
|
+
| `safeAreaViolations[]` | Any entry — interactive element spills under notch / home indicator |
|
|
58
|
+
| `keyboard.coveredInputs[]` | Any entry — focused input rendered behind the on-screen keyboard |
|
|
59
|
+
| `animations[].droppedFrames` | > 2 dropped per animation |
|
|
60
|
+
| `popups[].droppedFrames` | > 2 — popup entry stutters |
|
|
61
|
+
|
|
62
|
+
For each defect:
|
|
63
|
+
|
|
64
|
+
1. Quote the offending `selector` from the report in your fix description.
|
|
65
|
+
2. Make the smallest CSS / layout change that resolves it (move the element inside the safe rect, raise z-index, defer the layout work, add a placeholder for async content, etc.).
|
|
66
|
+
3. Re-run `inspect_ux` with the same arguments to confirm the defect is gone before moving on.
|
|
67
|
+
|
|
68
|
+
## Don't
|
|
69
|
+
|
|
70
|
+
- Don't use `inspect_ux` for *aesthetic* judgments ("does this look pretty?"). For those, use `dev_server_screenshot` + `analyze_image`. `inspect_ux` is for objective, measurable defects only.
|
|
71
|
+
- Don't gate trivial fixes (typo, copy change, color tweak) on a clean inspect_ux run.
|
|
72
|
+
- Don't accept "the report has overlaps but they're decorative" without checking — if the report flagged it, the elements are tagged as interactive (`[data-id]`, `[role]`, `button`, `input`, `a`).
|
package/templates/CLAUDE.md
CHANGED
|
@@ -106,6 +106,22 @@ Every endpoint is accessible via both WebSocket (`socket.request(name, input)`)
|
|
|
106
106
|
versions which include accessibility attributes and element map support
|
|
107
107
|
- **Always** set `aria-label` on icon-only buttons and non-text interactive elements
|
|
108
108
|
|
|
109
|
+
## UX inspection (`window.__uglyInspect()`)
|
|
110
|
+
|
|
111
|
+
Every page exposes a runtime inspection API installed by `bootstrapApp` at boot. It reports objective UX defects: layout shift (CLS), animation jank, overlapping interactive controls, safe-area violations, mobile keyboard coverage, popup mount cost, SPA route transitions. Observers run continuously from page load and keep a rolling 500-entry ring buffer per signal.
|
|
112
|
+
|
|
113
|
+
**Call from:**
|
|
114
|
+
- **Playwright tests / bots** — `import { inspectWindow, expectClean, setDevice, simulateKeyboard, waitForApp } from 'ugly-app/playwright'`
|
|
115
|
+
- **Browser console during dev** — `window.__uglyInspect({ since?: number })` returns a `UglyInspectReport` JSON; `window.__uglyInspectMark()` returns a timestamp for windowed reads
|
|
116
|
+
- **Studio coding agent** — call the `inspect_ux` tool with optional `actions[]` to drive navigation/clicks/keyboard before reading
|
|
117
|
+
|
|
118
|
+
`UglyInspectReport` type and all entry types are exported from `ugly-app/client` and re-exported from `ugly-app/playwright` so tests don't have to reach into client code.
|
|
119
|
+
|
|
120
|
+
**Rules:**
|
|
121
|
+
- **Never** call `page.evaluate('window.__uglyInspect(...)')` directly — use `ugly-app/playwright` so renames and shape changes propagate
|
|
122
|
+
- **Treat as build failures** in test code: any `safeAreaViolations`, `overlaps`, `keyboard.coveredInputs`, or `animations[].droppedFrames > 2`
|
|
123
|
+
- The expensive scans (overlap, safe-area) only run when `__uglyInspect()` is called — observers themselves are essentially free
|
|
124
|
+
|
|
109
125
|
## Feedback system
|
|
110
126
|
Feedback button is always at `[data-id="feedback-button"]` (bottom-right).
|
|
111
127
|
User feedback history: `GET /my_feedback` (requires auth cookie).
|
|
@@ -188,21 +188,65 @@ function routeFilenames(route) {
|
|
|
188
188
|
slugForFile,
|
|
189
189
|
screenshotPath: path.join(SCREENSHOTS_DIR, `${slugForFile}.png`),
|
|
190
190
|
elementMapPath: path.join(SCREENSHOTS_DIR, `${slugForFile}.element-map.json`),
|
|
191
|
+
uxReportPath: path.join(SCREENSHOTS_DIR, `${slugForFile}.ux-report.json`),
|
|
191
192
|
url: `${BASE_URL}${route}`,
|
|
192
193
|
};
|
|
193
194
|
}
|
|
194
195
|
|
|
195
196
|
async function captureRoute(page, route) {
|
|
196
|
-
const { screenshotPath, elementMapPath, url } =
|
|
197
|
+
const { screenshotPath, elementMapPath, uxReportPath, url } =
|
|
198
|
+
routeFilenames(route);
|
|
197
199
|
try {
|
|
198
200
|
await page.goto(url, { waitUntil: 'networkidle', timeout: 15_000 });
|
|
199
201
|
} catch (err) {
|
|
200
202
|
console.error(`[${SLUG}] failed to load ${url}: ${err.message}`);
|
|
201
203
|
return false;
|
|
202
204
|
}
|
|
205
|
+
// Open a UX measurement window, drive a default interaction script
|
|
206
|
+
// (click first interactive [data-id], wait, scroll halfway, back to
|
|
207
|
+
// top), then read window.__uglyInspect() inside the page so the
|
|
208
|
+
// report covers actual movement, not just static layout.
|
|
209
|
+
let uxReport = null;
|
|
210
|
+
try {
|
|
211
|
+
const mark = await page.evaluate(
|
|
212
|
+
() => window.__uglyInspectMark?.() ?? Date.now(),
|
|
213
|
+
);
|
|
214
|
+
// Default interaction: nudge the page so transitions / scroll-driven
|
|
215
|
+
// animations fire. Personas can override by editing this script
|
|
216
|
+
// alongside pages.json.
|
|
217
|
+
await page
|
|
218
|
+
.evaluate(async () => {
|
|
219
|
+
const firstInteractive = document.querySelector(
|
|
220
|
+
'[data-id]:not([data-id=feedback-button])',
|
|
221
|
+
);
|
|
222
|
+
if (
|
|
223
|
+
firstInteractive &&
|
|
224
|
+
firstInteractive.tagName !== 'INPUT' &&
|
|
225
|
+
firstInteractive.tagName !== 'TEXTAREA'
|
|
226
|
+
) {
|
|
227
|
+
firstInteractive.click?.();
|
|
228
|
+
}
|
|
229
|
+
await new Promise((r) => setTimeout(r, 400));
|
|
230
|
+
window.scrollTo({ top: 800, behavior: 'smooth' });
|
|
231
|
+
await new Promise((r) => setTimeout(r, 600));
|
|
232
|
+
window.scrollTo({ top: 0, behavior: 'smooth' });
|
|
233
|
+
})
|
|
234
|
+
.catch(() => undefined);
|
|
235
|
+
await page.waitForTimeout(800);
|
|
236
|
+
uxReport = await page.evaluate(
|
|
237
|
+
(s) => window.__uglyInspect?.({ since: s }) ?? null,
|
|
238
|
+
mark,
|
|
239
|
+
);
|
|
240
|
+
} catch {
|
|
241
|
+
// window.__uglyInspect not installed (pre-0.1.489 ugly-app) — leave
|
|
242
|
+
// uxReport null so downstream skills can detect the absence.
|
|
243
|
+
}
|
|
203
244
|
await page.screenshot({ path: screenshotPath, fullPage: true });
|
|
204
245
|
const elementMap = await captureElementMap(page);
|
|
205
246
|
fs.writeFileSync(elementMapPath, JSON.stringify(elementMap, null, 2));
|
|
247
|
+
if (uxReport) {
|
|
248
|
+
fs.writeFileSync(uxReportPath, JSON.stringify(uxReport, null, 2));
|
|
249
|
+
}
|
|
206
250
|
return true;
|
|
207
251
|
}
|
|
208
252
|
|
|
@@ -36,4 +36,8 @@ export const allPages = {
|
|
|
36
36
|
['test/worker']: lazyPage(() => import('./pages/WorkerTestPage')),
|
|
37
37
|
['test/strings']: lazyPage(() => import('./pages/StringsTestPage')),
|
|
38
38
|
['test/safe-area']: lazyPage(() => import('./pages/SafeAreaTestPage')),
|
|
39
|
+
['test/inspect-fixture']: lazyPage(() => import('./pages/InspectFixturePage')),
|
|
40
|
+
['test/inspect-fixture-other']: lazyPage(
|
|
41
|
+
() => import('./pages/InspectFixtureOtherPage'),
|
|
42
|
+
),
|
|
39
43
|
} satisfies PageMap<AppPages>;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { PageLayout, Text } from 'ugly-app/client';
|
|
2
|
+
|
|
3
|
+
/** SPA-nav target for inspect-fixture tests. */
|
|
4
|
+
export default function InspectFixtureOtherPage() {
|
|
5
|
+
return (
|
|
6
|
+
<PageLayout>
|
|
7
|
+
<div style={{ padding: 16 }}>
|
|
8
|
+
<Text data-id="other-page-title" size="xl" weight="bold">
|
|
9
|
+
Other page
|
|
10
|
+
</Text>
|
|
11
|
+
</div>
|
|
12
|
+
</PageLayout>
|
|
13
|
+
);
|
|
14
|
+
}
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
2
|
+
import { Input, PageLayout, Text, useRouter } from 'ugly-app/client';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Test fixture for `window.__uglyInspect()` / `ugly-app/playwright`.
|
|
6
|
+
* Each `?simulate=` value renders a known-bad layout so the inspect
|
|
7
|
+
* helper can assert "yes the report caught it".
|
|
8
|
+
*
|
|
9
|
+
* Used by:
|
|
10
|
+
* - templates/tests/e2e/inspect.spec.ts (Tier 2 in the studio spec)
|
|
11
|
+
* - the studio integration test in app/studio/tests/inspect-ux-end-to-end.test.ts (Tier 3)
|
|
12
|
+
*/
|
|
13
|
+
export default function InspectFixturePage() {
|
|
14
|
+
const router = useRouter();
|
|
15
|
+
const params =
|
|
16
|
+
typeof window === 'undefined'
|
|
17
|
+
? new URLSearchParams()
|
|
18
|
+
: new URLSearchParams(window.location.search);
|
|
19
|
+
const simulate = params.get('simulate') ?? '';
|
|
20
|
+
const [delayedMounted, setDelayedMounted] = useState(false);
|
|
21
|
+
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
if (simulate === 'cls') {
|
|
24
|
+
const t = setTimeout(() => setDelayedMounted(true), 400);
|
|
25
|
+
return () => clearTimeout(t);
|
|
26
|
+
}
|
|
27
|
+
return undefined;
|
|
28
|
+
}, [simulate]);
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<PageLayout>
|
|
32
|
+
<div
|
|
33
|
+
style={{
|
|
34
|
+
padding: 16,
|
|
35
|
+
display: 'flex',
|
|
36
|
+
flexDirection: 'column',
|
|
37
|
+
gap: 12,
|
|
38
|
+
}}
|
|
39
|
+
>
|
|
40
|
+
<Text size="xl" weight="bold">
|
|
41
|
+
Inspect Fixture · simulate={simulate || 'none'}
|
|
42
|
+
</Text>
|
|
43
|
+
|
|
44
|
+
{/* Default content — always rendered so [data-id] selector fires */}
|
|
45
|
+
<button
|
|
46
|
+
data-id="nav-to-other"
|
|
47
|
+
onClick={() => router.push('test/inspect-fixture-other', {})}
|
|
48
|
+
>
|
|
49
|
+
Navigate to other page
|
|
50
|
+
</button>
|
|
51
|
+
|
|
52
|
+
<button
|
|
53
|
+
data-id="nav-to-cls-page"
|
|
54
|
+
onClick={() =>
|
|
55
|
+
router.push('test/inspect-fixture', { simulate: 'cls' })
|
|
56
|
+
}
|
|
57
|
+
>
|
|
58
|
+
Navigate (re-load) with CLS sim
|
|
59
|
+
</button>
|
|
60
|
+
|
|
61
|
+
{simulate === 'cls' && (
|
|
62
|
+
<>
|
|
63
|
+
{/* Forces a layout shift when the delayed block mounts and
|
|
64
|
+
shoves siblings down by ~240px. */}
|
|
65
|
+
{delayedMounted && (
|
|
66
|
+
<div
|
|
67
|
+
data-id="delayed-banner"
|
|
68
|
+
style={{
|
|
69
|
+
height: 240,
|
|
70
|
+
background: 'crimson',
|
|
71
|
+
color: 'white',
|
|
72
|
+
padding: 16,
|
|
73
|
+
}}
|
|
74
|
+
>
|
|
75
|
+
Delayed mount — pushes content down
|
|
76
|
+
</div>
|
|
77
|
+
)}
|
|
78
|
+
<div data-id="below-the-shift" style={{ padding: 24 }}>
|
|
79
|
+
This block moves when the delayed banner mounts.
|
|
80
|
+
</div>
|
|
81
|
+
</>
|
|
82
|
+
)}
|
|
83
|
+
|
|
84
|
+
{simulate === 'overlap' && (
|
|
85
|
+
<div style={{ position: 'relative', height: 200 }}>
|
|
86
|
+
<button
|
|
87
|
+
data-id="overlap-a"
|
|
88
|
+
style={{
|
|
89
|
+
position: 'absolute',
|
|
90
|
+
left: 40,
|
|
91
|
+
top: 40,
|
|
92
|
+
width: 160,
|
|
93
|
+
height: 60,
|
|
94
|
+
}}
|
|
95
|
+
>
|
|
96
|
+
Button A
|
|
97
|
+
</button>
|
|
98
|
+
<button
|
|
99
|
+
data-id="overlap-b"
|
|
100
|
+
style={{
|
|
101
|
+
position: 'absolute',
|
|
102
|
+
left: 120,
|
|
103
|
+
top: 60,
|
|
104
|
+
width: 160,
|
|
105
|
+
height: 60,
|
|
106
|
+
}}
|
|
107
|
+
>
|
|
108
|
+
Button B
|
|
109
|
+
</button>
|
|
110
|
+
</div>
|
|
111
|
+
)}
|
|
112
|
+
|
|
113
|
+
{simulate === 'safearea' && (
|
|
114
|
+
<button
|
|
115
|
+
data-id="fab"
|
|
116
|
+
style={{
|
|
117
|
+
position: 'fixed',
|
|
118
|
+
left: 16,
|
|
119
|
+
right: 16,
|
|
120
|
+
bottom: 0,
|
|
121
|
+
height: 56,
|
|
122
|
+
background: '#0a84ff',
|
|
123
|
+
color: 'white',
|
|
124
|
+
border: 0,
|
|
125
|
+
}}
|
|
126
|
+
>
|
|
127
|
+
Floating action button (no bottom safe-area padding)
|
|
128
|
+
</button>
|
|
129
|
+
)}
|
|
130
|
+
|
|
131
|
+
{simulate === 'keyboard' && (
|
|
132
|
+
<div style={{ paddingTop: 600 }}>
|
|
133
|
+
<Input
|
|
134
|
+
data-id="fixture-input"
|
|
135
|
+
value=""
|
|
136
|
+
onChange={() => {}}
|
|
137
|
+
placeholder="Focus me to test keyboard coverage"
|
|
138
|
+
/>
|
|
139
|
+
</div>
|
|
140
|
+
)}
|
|
141
|
+
|
|
142
|
+
{simulate === 'jank' && <JankSim />}
|
|
143
|
+
|
|
144
|
+
{simulate === 'popup' && <PopupSim />}
|
|
145
|
+
</div>
|
|
146
|
+
</PageLayout>
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function JankSim() {
|
|
151
|
+
const [running, setRunning] = useState(false);
|
|
152
|
+
return (
|
|
153
|
+
<>
|
|
154
|
+
<button
|
|
155
|
+
data-id="start-jank"
|
|
156
|
+
onClick={() => {
|
|
157
|
+
setRunning(true);
|
|
158
|
+
// Spawn a long task during the animation.
|
|
159
|
+
const start = performance.now();
|
|
160
|
+
while (performance.now() - start < 250) {
|
|
161
|
+
// Busy-loop — guaranteed long task.
|
|
162
|
+
}
|
|
163
|
+
setTimeout(() => setRunning(false), 1000);
|
|
164
|
+
}}
|
|
165
|
+
>
|
|
166
|
+
Start janky animation
|
|
167
|
+
</button>
|
|
168
|
+
{running && (
|
|
169
|
+
<div
|
|
170
|
+
data-id="janky-animation"
|
|
171
|
+
style={{
|
|
172
|
+
width: 100,
|
|
173
|
+
height: 100,
|
|
174
|
+
background: 'orange',
|
|
175
|
+
animation: 'spin 800ms linear',
|
|
176
|
+
}}
|
|
177
|
+
/>
|
|
178
|
+
)}
|
|
179
|
+
<style>{`@keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }`}</style>
|
|
180
|
+
</>
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function PopupSim() {
|
|
185
|
+
const [open, setOpen] = useState(false);
|
|
186
|
+
return (
|
|
187
|
+
<>
|
|
188
|
+
<button data-id="open-modal" onClick={() => setOpen(true)}>
|
|
189
|
+
Open modal
|
|
190
|
+
</button>
|
|
191
|
+
{open && (
|
|
192
|
+
<div
|
|
193
|
+
role="dialog"
|
|
194
|
+
data-id="modal-content"
|
|
195
|
+
style={{
|
|
196
|
+
position: 'fixed',
|
|
197
|
+
inset: 0,
|
|
198
|
+
display: 'flex',
|
|
199
|
+
alignItems: 'center',
|
|
200
|
+
justifyContent: 'center',
|
|
201
|
+
background: 'rgba(0,0,0,0.5)',
|
|
202
|
+
animation: 'fade-in 300ms ease',
|
|
203
|
+
}}
|
|
204
|
+
>
|
|
205
|
+
<div style={{ background: 'white', padding: 24, borderRadius: 8 }}>
|
|
206
|
+
<Text>Modal body</Text>
|
|
207
|
+
<button data-id="close-modal" onClick={() => setOpen(false)}>
|
|
208
|
+
Close
|
|
209
|
+
</button>
|
|
210
|
+
</div>
|
|
211
|
+
</div>
|
|
212
|
+
)}
|
|
213
|
+
<style>{`@keyframes fade-in { from { opacity: 0; } to { opacity: 1; } }`}</style>
|
|
214
|
+
</>
|
|
215
|
+
);
|
|
216
|
+
}
|
|
@@ -35,6 +35,10 @@ export const pages = definePages({
|
|
|
35
35
|
'test/worker': definePage<{}>({ auth: true }),
|
|
36
36
|
'test/strings': definePage<{}>({ auth: false }),
|
|
37
37
|
'test/safe-area': definePage<{}>({ auth: false }),
|
|
38
|
+
'test/inspect-fixture': definePage<{
|
|
39
|
+
simulate?: 'cls' | 'overlap' | 'safearea' | 'keyboard' | 'jank' | 'popup';
|
|
40
|
+
}>({ auth: false }),
|
|
41
|
+
'test/inspect-fixture-other': definePage<{}>({ auth: false }),
|
|
38
42
|
});
|
|
39
43
|
|
|
40
44
|
export type AppPages = typeof pages;
|