ugly-app 0.1.874 → 0.1.875
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/dist/client/components/DeviceSimulator.d.ts.map +1 -1
- package/dist/client/components/DeviceSimulator.js +1 -12
- package/dist/client/components/DeviceSimulator.js.map +1 -1
- package/dist/client/deviceProfiles.d.ts +47 -0
- package/dist/client/deviceProfiles.d.ts.map +1 -0
- package/dist/client/deviceProfiles.js +22 -0
- package/dist/client/deviceProfiles.js.map +1 -0
- package/dist/client/inspect/keyboard.js +28 -4
- package/dist/client/inspect/keyboard.js.map +1 -1
- package/dist/playwright/index.d.ts +2 -0
- package/dist/playwright/index.d.ts.map +1 -1
- package/dist/playwright/index.js +5 -0
- package/dist/playwright/index.js.map +1 -1
- package/dist/playwright/mobile.d.ts +45 -0
- package/dist/playwright/mobile.d.ts.map +1 -0
- package/dist/playwright/mobile.js +114 -0
- package/dist/playwright/mobile.js.map +1 -0
- package/package.json +1 -1
- package/src/cli/version.ts +1 -1
- package/src/client/components/DeviceSimulator.tsx +4 -15
- package/src/client/deviceProfiles.ts +32 -0
- package/src/client/inspect/keyboard.ts +28 -4
- package/src/playwright/index.ts +14 -0
- package/src/playwright/mobile.ts +148 -0
- package/templates/.claude/skills/eval-swarm/SKILL.md +155 -14
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.875";
|
|
2
2
|
//# sourceMappingURL=version.d.ts.map
|
package/dist/cli/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DeviceSimulator.d.ts","sourceRoot":"","sources":["../../../src/client/components/DeviceSimulator.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAsB,KAAK,SAAS,EAAuB,MAAM,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"DeviceSimulator.d.ts","sourceRoot":"","sources":["../../../src/client/components/DeviceSimulator.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAsB,KAAK,SAAS,EAAuB,MAAM,OAAO,CAAC;AA6ChF,wBAAgB,eAAe,CAAC,EAC9B,QAAQ,GACT,EAAE;IACD,QAAQ,EAAE,SAAS,CAAC;CACrB,GAAG,KAAK,CAAC,SAAS,CA0KlB"}
|
|
@@ -1,17 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useEffect, useState } from 'react';
|
|
3
|
-
|
|
4
|
-
ios: {
|
|
5
|
-
safeArea: { top: 59, right: 0, bottom: 34, left: 0 },
|
|
6
|
-
keyboardHeight: 320,
|
|
7
|
-
viewport: { width: 390, height: 844 },
|
|
8
|
-
},
|
|
9
|
-
android: {
|
|
10
|
-
safeArea: { top: 25, right: 0, bottom: 48, left: 0 },
|
|
11
|
-
keyboardHeight: 300,
|
|
12
|
-
viewport: { width: 360, height: 780 },
|
|
13
|
-
},
|
|
14
|
-
};
|
|
3
|
+
import { SIMULATOR_PROFILES, } from '../deviceProfiles.js';
|
|
15
4
|
function isKeyboardInput(el) {
|
|
16
5
|
if (!el)
|
|
17
6
|
return false;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DeviceSimulator.js","sourceRoot":"","sources":["../../../src/client/components/DeviceSimulator.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAsC,SAAS,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"DeviceSimulator.js","sourceRoot":"","sources":["../../../src/client/components/DeviceSimulator.tsx"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
export interface DeviceProfile {
|
|
2
|
+
safeArea: {
|
|
3
|
+
top: number;
|
|
4
|
+
right: number;
|
|
5
|
+
bottom: number;
|
|
6
|
+
left: number;
|
|
7
|
+
};
|
|
8
|
+
keyboardHeight: number;
|
|
9
|
+
viewport: {
|
|
10
|
+
width: number;
|
|
11
|
+
height: number;
|
|
12
|
+
};
|
|
13
|
+
/** CSS device-pixel-ratio to emulate (physical density). */
|
|
14
|
+
deviceScaleFactor: number;
|
|
15
|
+
}
|
|
16
|
+
export declare const SIMULATOR_PROFILES: {
|
|
17
|
+
readonly ios: {
|
|
18
|
+
readonly safeArea: {
|
|
19
|
+
readonly top: 59;
|
|
20
|
+
readonly right: 0;
|
|
21
|
+
readonly bottom: 34;
|
|
22
|
+
readonly left: 0;
|
|
23
|
+
};
|
|
24
|
+
readonly keyboardHeight: 320;
|
|
25
|
+
readonly viewport: {
|
|
26
|
+
readonly width: 390;
|
|
27
|
+
readonly height: 844;
|
|
28
|
+
};
|
|
29
|
+
readonly deviceScaleFactor: 3;
|
|
30
|
+
};
|
|
31
|
+
readonly android: {
|
|
32
|
+
readonly safeArea: {
|
|
33
|
+
readonly top: 25;
|
|
34
|
+
readonly right: 0;
|
|
35
|
+
readonly bottom: 48;
|
|
36
|
+
readonly left: 0;
|
|
37
|
+
};
|
|
38
|
+
readonly keyboardHeight: 300;
|
|
39
|
+
readonly viewport: {
|
|
40
|
+
readonly width: 360;
|
|
41
|
+
readonly height: 780;
|
|
42
|
+
};
|
|
43
|
+
readonly deviceScaleFactor: 3;
|
|
44
|
+
};
|
|
45
|
+
};
|
|
46
|
+
export type SimulatorPlatform = keyof typeof SIMULATOR_PROFILES;
|
|
47
|
+
//# sourceMappingURL=deviceProfiles.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"deviceProfiles.d.ts","sourceRoot":"","sources":["../../src/client/deviceProfiles.ts"],"names":[],"mappings":"AAQA,MAAM,WAAW,aAAa;IAC5B,QAAQ,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;IACvE,cAAc,EAAE,MAAM,CAAC;IACvB,QAAQ,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;IAC5C,4DAA4D;IAC5D,iBAAiB,EAAE,MAAM,CAAC;CAC3B;AAED,eAAO,MAAM,kBAAkB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAamB,CAAC;AAEnD,MAAM,MAAM,iBAAiB,GAAG,MAAM,OAAO,kBAAkB,CAAC"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// Shared device-emulation profiles for the mobile simulator + the Playwright test
|
|
2
|
+
// harness. One source of truth so the in-app `DeviceSimulator` component and the
|
|
3
|
+
// `ugly-app/playwright` `createMobileBotPage` helper agree on the exact safe-area
|
|
4
|
+
// insets, keyboard height, and viewport size for each platform.
|
|
5
|
+
//
|
|
6
|
+
// The keyboard height and the bottom device inset are combined with `max()`, never
|
|
7
|
+
// added — see KeyboardProvider.
|
|
8
|
+
export const SIMULATOR_PROFILES = {
|
|
9
|
+
ios: {
|
|
10
|
+
safeArea: { top: 59, right: 0, bottom: 34, left: 0 },
|
|
11
|
+
keyboardHeight: 320,
|
|
12
|
+
viewport: { width: 390, height: 844 },
|
|
13
|
+
deviceScaleFactor: 3,
|
|
14
|
+
},
|
|
15
|
+
android: {
|
|
16
|
+
safeArea: { top: 25, right: 0, bottom: 48, left: 0 },
|
|
17
|
+
keyboardHeight: 300,
|
|
18
|
+
viewport: { width: 360, height: 780 },
|
|
19
|
+
deviceScaleFactor: 3,
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
//# sourceMappingURL=deviceProfiles.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"deviceProfiles.js","sourceRoot":"","sources":["../../src/client/deviceProfiles.ts"],"names":[],"mappings":"AAAA,kFAAkF;AAClF,iFAAiF;AACjF,kFAAkF;AAClF,gEAAgE;AAChE,EAAE;AACF,mFAAmF;AACnF,gCAAgC;AAUhC,MAAM,CAAC,MAAM,kBAAkB,GAAG;IAChC,GAAG,EAAE;QACH,QAAQ,EAAE,EAAE,GAAG,EAAE,EAAE,EAAE,KAAK,EAAE,CAAC,EAAE,MAAM,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC,EAAE;QACpD,cAAc,EAAE,GAAG;QACnB,QAAQ,EAAE,EAAE,KAAK,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE;QACrC,iBAAiB,EAAE,CAAC;KACrB;IACD,OAAO,EAAE;QACP,QAAQ,EAAE,EAAE,GAAG,EAAE,EAAE,EAAE,KAAK,EAAE,CAAC,EAAE,MAAM,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC,EAAE;QACpD,cAAc,EAAE,GAAG;QACnB,QAAQ,EAAE,EAAE,KAAK,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE;QACrC,iBAAiB,EAAE,CAAC;KACrB;CAC+C,CAAC"}
|
|
@@ -12,12 +12,36 @@ export function installKeyboard(state) {
|
|
|
12
12
|
};
|
|
13
13
|
}
|
|
14
14
|
function currentKeyboardState() {
|
|
15
|
-
if (typeof window === 'undefined'
|
|
15
|
+
if (typeof window === 'undefined') {
|
|
16
16
|
return { height: 0, isOpen: false };
|
|
17
17
|
}
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
// Primary source: real visualViewport occlusion (browser / PWA / WKWebView).
|
|
19
|
+
if (window.visualViewport) {
|
|
20
|
+
const diff = window.innerHeight - window.visualViewport.height;
|
|
21
|
+
if (diff > 100) {
|
|
22
|
+
const height = Math.round(diff);
|
|
23
|
+
return { height, isOpen: true };
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
// Fallback: an explicit keyboard height (native shell, ugly-studio preview, or the
|
|
27
|
+
// Playwright mobile helper) — the simulated keyboard cannot shrink visualViewport,
|
|
28
|
+
// so read `window.__keyboardHeight` / the `--keyboard-inset-height` CSS var instead.
|
|
29
|
+
const explicit = explicitKeyboardHeight();
|
|
30
|
+
return { height: explicit, isOpen: explicit > 0 };
|
|
31
|
+
}
|
|
32
|
+
function explicitKeyboardHeight() {
|
|
33
|
+
const fromWindow = window
|
|
34
|
+
.__keyboardHeight;
|
|
35
|
+
if (typeof fromWindow === 'number' && fromWindow > 0) {
|
|
36
|
+
return Math.round(fromWindow);
|
|
37
|
+
}
|
|
38
|
+
if (typeof document === 'undefined')
|
|
39
|
+
return 0;
|
|
40
|
+
const raw = window
|
|
41
|
+
.getComputedStyle(document.documentElement)
|
|
42
|
+
.getPropertyValue('--keyboard-inset-height');
|
|
43
|
+
const parsed = parseInt(raw || '0', 10);
|
|
44
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : 0;
|
|
21
45
|
}
|
|
22
46
|
function scanCoveredInputs(kb, state) {
|
|
23
47
|
if (!kb.isOpen)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"keyboard.js","sourceRoot":"","sources":["../../../src/client/inspect/keyboard.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAqB,MAAM,aAAa,CAAC;AACtD,OAAO,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AAQjD,MAAM,UAAU,eAAe,CAAC,KAAmB;IACjD,KAAK,CAAC,gBAAgB,GAAG,GAAG,EAAE;QAC5B,MAAM,EAAE,GAAG,oBAAoB,EAAE,CAAC;QAClC,MAAM,aAAa,GAAG,iBAAiB,CAAC,EAAE,EAAE,KAAK,CAAC,CAAC;QACnD,OAAO;YACL,MAAM,EAAE,EAAE,CAAC,MAAM;YACjB,MAAM,EAAE,EAAE,CAAC,MAAM;YACjB,aAAa;SACd,CAAC;IACJ,CAAC,CAAC;AACJ,CAAC;AAED,SAAS,oBAAoB;IAC3B,IAAI,OAAO,MAAM,KAAK,WAAW,
|
|
1
|
+
{"version":3,"file":"keyboard.js","sourceRoot":"","sources":["../../../src/client/inspect/keyboard.ts"],"names":[],"mappings":"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"}
|
|
@@ -42,4 +42,6 @@ export { installRealNative, makePathResolver } from './real-native.js';
|
|
|
42
42
|
export type { RealNativeOptions } from './real-native.js';
|
|
43
43
|
export { loadDevAuth, authenticateReal } from './dev-auth.js';
|
|
44
44
|
export type { DevAuth } from './dev-auth.js';
|
|
45
|
+
export { createMobileBotPage, installMobileFrame, raiseKeyboard, dismissKeyboard, } from './mobile.js';
|
|
46
|
+
export type { MobileBotOptions, SimulatorPlatform } from './mobile.js';
|
|
45
47
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/playwright/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AACtD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,4BAA4B,CAAC;AAEpE,YAAY,EAAE,iBAAiB,EAAE,MAAM,4BAA4B,CAAC;AACpE,YAAY,EACV,cAAc,EACd,QAAQ,EACR,YAAY,EACZ,WAAW,EACX,aAAa,EACb,YAAY,EACZ,UAAU,EACV,eAAe,EACf,iBAAiB,EACjB,iBAAiB,GAClB,MAAM,4BAA4B,CAAC;AAIpC;;;;;;;;GAQG;AACH,wBAAsB,gBAAgB,CACpC,IAAI,EAAE,IAAI,EACV,KAAK,EAAE,MAAM,GACZ,OAAO,CAAC,IAAI,CAAC,CAYf;AAED;;GAEG;AACH,wBAAsB,aAAa,CACjC,OAAO,EAAE,OAAO,EAChB,KAAK,EAAE,MAAM,GACZ,OAAO,CAAC,IAAI,CAAC,CAIf;AAOD,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,wBAAsB,UAAU,CAC9B,IAAI,EAAE,IAAI,EACV,IAAI,CAAC,EAAE;IAAE,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,GAC5B,OAAO,CAAC,IAAI,CAAC,CAIf;AAED,wBAAsB,WAAW,CAAC,IAAI,EAAE,IAAI,GAAG,OAAO,CAAC,MAAM,CAAC,CAI7D;AAED,wBAAsB,WAAW,CAC/B,IAAI,EAAE,IAAI,EACV,KAAK,EAAE,MAAM,GACZ,OAAO,CAAC,iBAAiB,CAAC,CAU5B;AAED,wBAAsB,aAAa,CACjC,IAAI,EAAE,IAAI,EACV,QAAQ,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,EAC7B,IAAI,CAAC,EAAE,oBAAoB,GAC1B,OAAO,CAAC,iBAAiB,CAAC,CAK5B;AAED,wBAAsB,SAAS,CAC7B,IAAI,EAAE,IAAI,EACV,MAAM,EAAE,SAAS,GAAG,KAAK,GAAG,SAAS,GACpC,OAAO,CAAC,IAAI,CAAC,CASf;AAED,wBAAsB,gBAAgB,CACpC,IAAI,EAAE,IAAI,EACV,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,IAAI,CAAC,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/playwright/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AACtD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,4BAA4B,CAAC;AAEpE,YAAY,EAAE,iBAAiB,EAAE,MAAM,4BAA4B,CAAC;AACpE,YAAY,EACV,cAAc,EACd,QAAQ,EACR,YAAY,EACZ,WAAW,EACX,aAAa,EACb,YAAY,EACZ,UAAU,EACV,eAAe,EACf,iBAAiB,EACjB,iBAAiB,GAClB,MAAM,4BAA4B,CAAC;AAIpC;;;;;;;;GAQG;AACH,wBAAsB,gBAAgB,CACpC,IAAI,EAAE,IAAI,EACV,KAAK,EAAE,MAAM,GACZ,OAAO,CAAC,IAAI,CAAC,CAYf;AAED;;GAEG;AACH,wBAAsB,aAAa,CACjC,OAAO,EAAE,OAAO,EAChB,KAAK,EAAE,MAAM,GACZ,OAAO,CAAC,IAAI,CAAC,CAIf;AAOD,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,wBAAsB,UAAU,CAC9B,IAAI,EAAE,IAAI,EACV,IAAI,CAAC,EAAE;IAAE,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,GAC5B,OAAO,CAAC,IAAI,CAAC,CAIf;AAED,wBAAsB,WAAW,CAAC,IAAI,EAAE,IAAI,GAAG,OAAO,CAAC,MAAM,CAAC,CAI7D;AAED,wBAAsB,WAAW,CAC/B,IAAI,EAAE,IAAI,EACV,KAAK,EAAE,MAAM,GACZ,OAAO,CAAC,iBAAiB,CAAC,CAU5B;AAED,wBAAsB,aAAa,CACjC,IAAI,EAAE,IAAI,EACV,QAAQ,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,EAC7B,IAAI,CAAC,EAAE,oBAAoB,GAC1B,OAAO,CAAC,iBAAiB,CAAC,CAK5B;AAED,wBAAsB,SAAS,CAC7B,IAAI,EAAE,IAAI,EACV,MAAM,EAAE,SAAS,GAAG,KAAK,GAAG,SAAS,GACpC,OAAO,CAAC,IAAI,CAAC,CASf;AAED,wBAAsB,gBAAgB,CACpC,IAAI,EAAE,IAAI,EACV,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,IAAI,CAAC,CAaf;AAED,MAAM,WAAW,kBAAkB;IACjC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,uBAAuB,CAAC,EAAE,OAAO,CAAC;IAClC,qBAAqB,CAAC,EAAE,OAAO,CAAC;IAChC,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED,wBAAsB,WAAW,CAC/B,IAAI,EAAE,IAAI,EACV,IAAI,CAAC,EAAE,kBAAkB,GACxB,OAAO,CAAC,IAAI,CAAC,CA2Df;AAED,OAAO,EAAE,qBAAqB,EAAE,MAAM,kBAAkB,CAAC;AACzD,YAAY,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AACvD,OAAO,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AACvE,YAAY,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAC1D,OAAO,EAAE,WAAW,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAC9D,YAAY,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;AAC7C,OAAO,EACL,mBAAmB,EACnB,kBAAkB,EAClB,aAAa,EACb,eAAe,GAChB,MAAM,aAAa,CAAC;AACrB,YAAY,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC"}
|
package/dist/playwright/index.js
CHANGED
|
@@ -67,6 +67,10 @@ export async function setDevice(page, device) {
|
|
|
67
67
|
}
|
|
68
68
|
export async function simulateKeyboard(page, heightPx) {
|
|
69
69
|
await page.evaluate((h) => {
|
|
70
|
+
// Set the explicit signal the layout AND the scanCoveredInputs inspector read
|
|
71
|
+
// (the simulated keyboard can't shrink visualViewport), then dispatch the event.
|
|
72
|
+
window.__keyboardHeight = h;
|
|
73
|
+
document.documentElement.style.setProperty('--keyboard-inset-height', `${h}px`);
|
|
70
74
|
window.dispatchEvent(new CustomEvent('keyboard-height', { detail: { height: h } }));
|
|
71
75
|
}, heightPx);
|
|
72
76
|
}
|
|
@@ -115,4 +119,5 @@ export async function expectClean(page, opts) {
|
|
|
115
119
|
export { installUglyNativeMock } from './native-mock.js';
|
|
116
120
|
export { installRealNative, makePathResolver } from './real-native.js';
|
|
117
121
|
export { loadDevAuth, authenticateReal } from './dev-auth.js';
|
|
122
|
+
export { createMobileBotPage, installMobileFrame, raiseKeyboard, dismissKeyboard, } from './mobile.js';
|
|
118
123
|
//# sourceMappingURL=index.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/playwright/index.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/playwright/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { Browser, BrowserContext, Page } from '@playwright/test';
|
|
2
|
+
import { type SimulatorPlatform } from '../client/deviceProfiles.js';
|
|
3
|
+
export type { SimulatorPlatform } from '../client/deviceProfiles.js';
|
|
4
|
+
export interface MobileBotOptions {
|
|
5
|
+
/** Cookie domain the auth token is scoped to. Default 'localhost' (local dev). */
|
|
6
|
+
domain?: string;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Apply mobile device emulation (viewport, touch, safe-area injection) to a
|
|
10
|
+
* pre-existing context — useful when you already created the context yourself.
|
|
11
|
+
*/
|
|
12
|
+
export declare function installMobileFrame(context: BrowserContext, platform: SimulatorPlatform): Promise<void>;
|
|
13
|
+
/**
|
|
14
|
+
* Create a page emulating a phone (iOS/Android) with a realistic top/bottom
|
|
15
|
+
* safe-area and on-screen keyboard, pre-authenticated with the given token.
|
|
16
|
+
*
|
|
17
|
+
* The layout responds via the globally-mounted `KeyboardProvider`, and the
|
|
18
|
+
* `scanSafeArea` / `scanCoveredInputs` inspectors (behind `expectClean`) see the
|
|
19
|
+
* simulated insets/keyboard. Navigate with `?debugSafeArea=true` to also render the
|
|
20
|
+
* visible top/bottom/keyboard bands for a screenshot vision pass. Works against any
|
|
21
|
+
* ugly-app child app, local or prod, with no per-app wiring.
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* const page = await createMobileBotPage(browser, token, 'ios')
|
|
25
|
+
* await page.goto(`${BASE_URL}/?debugSafeArea=true`)
|
|
26
|
+
* await waitForApp(page)
|
|
27
|
+
* await page.locator('textarea').first().focus()
|
|
28
|
+
* await raiseKeyboard(page, 'ios')
|
|
29
|
+
* await expectClean(page, { allowSafeAreaViolations: false, allowKeyboardCoverage: false })
|
|
30
|
+
*/
|
|
31
|
+
export declare function createMobileBotPage(browser: Browser, token: string, platform: SimulatorPlatform, opts?: MobileBotOptions): Promise<Page>;
|
|
32
|
+
/**
|
|
33
|
+
* Raise the simulated on-screen keyboard. Sets `--keyboard-inset-height` +
|
|
34
|
+
* `window.__keyboardHeight` AND dispatches the `keyboard-height` CustomEvent, so
|
|
35
|
+
* both the layout (`KeyboardProvider`) and the `scanCoveredInputs` inspector agree.
|
|
36
|
+
* We drive this explicitly rather than relying on `visualViewport` shrinking, which
|
|
37
|
+
* is not possible from Playwright.
|
|
38
|
+
*
|
|
39
|
+
* @param platformOrHeight a platform ('ios'/'android') to use its profile height,
|
|
40
|
+
* or an explicit pixel height.
|
|
41
|
+
*/
|
|
42
|
+
export declare function raiseKeyboard(page: Page, platformOrHeight?: SimulatorPlatform | number): Promise<void>;
|
|
43
|
+
/** Dismiss the simulated keyboard (height 0). */
|
|
44
|
+
export declare function dismissKeyboard(page: Page): Promise<void>;
|
|
45
|
+
//# sourceMappingURL=mobile.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mobile.d.ts","sourceRoot":"","sources":["../../src/playwright/mobile.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,cAAc,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AACtE,OAAO,EAEL,KAAK,iBAAiB,EACvB,MAAM,6BAA6B,CAAC;AAErC,YAAY,EAAE,iBAAiB,EAAE,MAAM,6BAA6B,CAAC;AAIrE,MAAM,WAAW,gBAAgB;IAC/B,kFAAkF;IAClF,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAwCD;;;GAGG;AACH,wBAAsB,kBAAkB,CACtC,OAAO,EAAE,cAAc,EACvB,QAAQ,EAAE,iBAAiB,GAC1B,OAAO,CAAC,IAAI,CAAC,CAEf;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAsB,mBAAmB,CACvC,OAAO,EAAE,OAAO,EAChB,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,iBAAiB,EAC3B,IAAI,CAAC,EAAE,gBAAgB,GACtB,OAAO,CAAC,IAAI,CAAC,CAqBf;AAED;;;;;;;;;GASG;AACH,wBAAsB,aAAa,CACjC,IAAI,EAAE,IAAI,EACV,gBAAgB,GAAE,iBAAiB,GAAG,MAAc,GACnD,OAAO,CAAC,IAAI,CAAC,CAMf;AAED,iDAAiD;AACjD,wBAAsB,eAAe,CAAC,IAAI,EAAE,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAE/D"}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { SIMULATOR_PROFILES, } from '../client/deviceProfiles.js';
|
|
2
|
+
const COOKIE_NAME = 'auth_token';
|
|
3
|
+
/**
|
|
4
|
+
* Build the init script that injects a realistic device safe-area into the page
|
|
5
|
+
* BEFORE app JS runs, mirroring `uglyStudioPreviewBridge` / the native shell:
|
|
6
|
+
* - `window.__safeAreaInsets` — KeyboardProvider seeds its "native override" from
|
|
7
|
+
* this on mount (authoritative path), so `useSafeAreaInsets()` pads correctly.
|
|
8
|
+
* - `--safe-area-inset-*` CSS vars on `:root` — so raw `.safe-area` consumers AND
|
|
9
|
+
* the `scanSafeArea` inspector (which reads these) see the insets.
|
|
10
|
+
* - `--keyboard-inset-height: 0px` — initial keyboard-closed state.
|
|
11
|
+
* A `keyboard-height` of 0 is dispatched last so any already-mounted listener resets.
|
|
12
|
+
*/
|
|
13
|
+
function safeAreaInitScript(platform) {
|
|
14
|
+
const { safeArea } = SIMULATOR_PROFILES[platform];
|
|
15
|
+
const insets = JSON.stringify(safeArea);
|
|
16
|
+
return `(() => {
|
|
17
|
+
const insets = ${insets};
|
|
18
|
+
try { window.__safeAreaInsets = insets; } catch {}
|
|
19
|
+
const apply = () => {
|
|
20
|
+
const root = document.documentElement;
|
|
21
|
+
if (!root) return;
|
|
22
|
+
root.style.setProperty('--safe-area-inset-top', insets.top + 'px');
|
|
23
|
+
root.style.setProperty('--safe-area-inset-right', insets.right + 'px');
|
|
24
|
+
root.style.setProperty('--safe-area-inset-bottom', insets.bottom + 'px');
|
|
25
|
+
root.style.setProperty('--safe-area-inset-left', insets.left + 'px');
|
|
26
|
+
root.style.setProperty('--keyboard-inset-height', '0px');
|
|
27
|
+
};
|
|
28
|
+
apply();
|
|
29
|
+
if (document.readyState === 'loading') {
|
|
30
|
+
document.addEventListener('DOMContentLoaded', apply, { once: true });
|
|
31
|
+
}
|
|
32
|
+
window.addEventListener('load', () => {
|
|
33
|
+
apply();
|
|
34
|
+
window.dispatchEvent(new CustomEvent('safe-area-insets', { detail: insets }));
|
|
35
|
+
window.dispatchEvent(new CustomEvent('keyboard-height', { detail: { height: 0 } }));
|
|
36
|
+
window.dispatchEvent(new CustomEvent('loaded', { detail: '' }));
|
|
37
|
+
}, { once: true });
|
|
38
|
+
})();`;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Apply mobile device emulation (viewport, touch, safe-area injection) to a
|
|
42
|
+
* pre-existing context — useful when you already created the context yourself.
|
|
43
|
+
*/
|
|
44
|
+
export async function installMobileFrame(context, platform) {
|
|
45
|
+
await context.addInitScript(safeAreaInitScript(platform));
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Create a page emulating a phone (iOS/Android) with a realistic top/bottom
|
|
49
|
+
* safe-area and on-screen keyboard, pre-authenticated with the given token.
|
|
50
|
+
*
|
|
51
|
+
* The layout responds via the globally-mounted `KeyboardProvider`, and the
|
|
52
|
+
* `scanSafeArea` / `scanCoveredInputs` inspectors (behind `expectClean`) see the
|
|
53
|
+
* simulated insets/keyboard. Navigate with `?debugSafeArea=true` to also render the
|
|
54
|
+
* visible top/bottom/keyboard bands for a screenshot vision pass. Works against any
|
|
55
|
+
* ugly-app child app, local or prod, with no per-app wiring.
|
|
56
|
+
*
|
|
57
|
+
* @example
|
|
58
|
+
* const page = await createMobileBotPage(browser, token, 'ios')
|
|
59
|
+
* await page.goto(`${BASE_URL}/?debugSafeArea=true`)
|
|
60
|
+
* await waitForApp(page)
|
|
61
|
+
* await page.locator('textarea').first().focus()
|
|
62
|
+
* await raiseKeyboard(page, 'ios')
|
|
63
|
+
* await expectClean(page, { allowSafeAreaViolations: false, allowKeyboardCoverage: false })
|
|
64
|
+
*/
|
|
65
|
+
export async function createMobileBotPage(browser, token, platform, opts) {
|
|
66
|
+
const profile = SIMULATOR_PROFILES[platform];
|
|
67
|
+
const context = await browser.newContext({
|
|
68
|
+
viewport: { ...profile.viewport },
|
|
69
|
+
deviceScaleFactor: profile.deviceScaleFactor,
|
|
70
|
+
isMobile: true,
|
|
71
|
+
hasTouch: true,
|
|
72
|
+
});
|
|
73
|
+
await context.addCookies([
|
|
74
|
+
{
|
|
75
|
+
name: COOKIE_NAME,
|
|
76
|
+
value: token,
|
|
77
|
+
domain: opts?.domain ?? 'localhost',
|
|
78
|
+
path: '/',
|
|
79
|
+
httpOnly: true,
|
|
80
|
+
secure: false,
|
|
81
|
+
sameSite: 'Lax',
|
|
82
|
+
},
|
|
83
|
+
]);
|
|
84
|
+
await installMobileFrame(context, platform);
|
|
85
|
+
return await context.newPage();
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Raise the simulated on-screen keyboard. Sets `--keyboard-inset-height` +
|
|
89
|
+
* `window.__keyboardHeight` AND dispatches the `keyboard-height` CustomEvent, so
|
|
90
|
+
* both the layout (`KeyboardProvider`) and the `scanCoveredInputs` inspector agree.
|
|
91
|
+
* We drive this explicitly rather than relying on `visualViewport` shrinking, which
|
|
92
|
+
* is not possible from Playwright.
|
|
93
|
+
*
|
|
94
|
+
* @param platformOrHeight a platform ('ios'/'android') to use its profile height,
|
|
95
|
+
* or an explicit pixel height.
|
|
96
|
+
*/
|
|
97
|
+
export async function raiseKeyboard(page, platformOrHeight = 'ios') {
|
|
98
|
+
const height = typeof platformOrHeight === 'number'
|
|
99
|
+
? platformOrHeight
|
|
100
|
+
: SIMULATOR_PROFILES[platformOrHeight].keyboardHeight;
|
|
101
|
+
await setKeyboardHeight(page, height);
|
|
102
|
+
}
|
|
103
|
+
/** Dismiss the simulated keyboard (height 0). */
|
|
104
|
+
export async function dismissKeyboard(page) {
|
|
105
|
+
await setKeyboardHeight(page, 0);
|
|
106
|
+
}
|
|
107
|
+
async function setKeyboardHeight(page, height) {
|
|
108
|
+
await page.evaluate((h) => {
|
|
109
|
+
window.__keyboardHeight = h;
|
|
110
|
+
document.documentElement.style.setProperty('--keyboard-inset-height', `${h}px`);
|
|
111
|
+
window.dispatchEvent(new CustomEvent('keyboard-height', { detail: { height: h } }));
|
|
112
|
+
}, height);
|
|
113
|
+
}
|
|
114
|
+
//# sourceMappingURL=mobile.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mobile.js","sourceRoot":"","sources":["../../src/playwright/mobile.ts"],"names":[],"mappings":"AACA,OAAO,EACL,kBAAkB,GAEnB,MAAM,6BAA6B,CAAC;AAIrC,MAAM,WAAW,GAAG,YAAY,CAAC;AAOjC;;;;;;;;;GASG;AACH,SAAS,kBAAkB,CAAC,QAA2B;IACrD,MAAM,EAAE,QAAQ,EAAE,GAAG,kBAAkB,CAAC,QAAQ,CAAC,CAAC;IAClD,MAAM,MAAM,GAAG,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC;IACxC,OAAO;qBACY,MAAM;;;;;;;;;;;;;;;;;;;;;QAqBnB,CAAC;AACT,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,kBAAkB,CACtC,OAAuB,EACvB,QAA2B;IAE3B,MAAM,OAAO,CAAC,aAAa,CAAC,kBAAkB,CAAC,QAAQ,CAAC,CAAC,CAAC;AAC5D,CAAC;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,CAAC,KAAK,UAAU,mBAAmB,CACvC,OAAgB,EAChB,KAAa,EACb,QAA2B,EAC3B,IAAuB;IAEvB,MAAM,OAAO,GAAG,kBAAkB,CAAC,QAAQ,CAAC,CAAC;IAC7C,MAAM,OAAO,GAAG,MAAM,OAAO,CAAC,UAAU,CAAC;QACvC,QAAQ,EAAE,EAAE,GAAG,OAAO,CAAC,QAAQ,EAAE;QACjC,iBAAiB,EAAE,OAAO,CAAC,iBAAiB;QAC5C,QAAQ,EAAE,IAAI;QACd,QAAQ,EAAE,IAAI;KACf,CAAC,CAAC;IACH,MAAM,OAAO,CAAC,UAAU,CAAC;QACvB;YACE,IAAI,EAAE,WAAW;YACjB,KAAK,EAAE,KAAK;YACZ,MAAM,EAAE,IAAI,EAAE,MAAM,IAAI,WAAW;YACnC,IAAI,EAAE,GAAG;YACT,QAAQ,EAAE,IAAI;YACd,MAAM,EAAE,KAAK;YACb,QAAQ,EAAE,KAAK;SAChB;KACF,CAAC,CAAC;IACH,MAAM,kBAAkB,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC;IAC5C,OAAO,MAAM,OAAO,CAAC,OAAO,EAAE,CAAC;AACjC,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,CAAC,KAAK,UAAU,aAAa,CACjC,IAAU,EACV,mBAA+C,KAAK;IAEpD,MAAM,MAAM,GACV,OAAO,gBAAgB,KAAK,QAAQ;QAClC,CAAC,CAAC,gBAAgB;QAClB,CAAC,CAAC,kBAAkB,CAAC,gBAAgB,CAAC,CAAC,cAAc,CAAC;IAC1D,MAAM,iBAAiB,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;AACxC,CAAC;AAED,iDAAiD;AACjD,MAAM,CAAC,KAAK,UAAU,eAAe,CAAC,IAAU;IAC9C,MAAM,iBAAiB,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC;AACnC,CAAC;AAED,KAAK,UAAU,iBAAiB,CAAC,IAAU,EAAE,MAAc;IACzD,MAAM,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAS,EAAE,EAAE;QAC/B,MAAmD,CAAC,gBAAgB,GAAG,CAAC,CAAC;QAC1E,QAAQ,CAAC,eAAe,CAAC,KAAK,CAAC,WAAW,CACxC,yBAAyB,EACzB,GAAG,CAAC,IAAI,CACT,CAAC;QACF,MAAM,CAAC,aAAa,CAClB,IAAI,WAAW,CAAC,iBAAiB,EAAE,EAAE,MAAM,EAAE,EAAE,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC,CAC9D,CAAC;IACJ,CAAC,EAAE,MAAM,CAAC,CAAC;AACb,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ugly-app",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.875",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"comment:files": "Allowlist what ships to npm. dist = runtime; src = sourcemap targets (dist/*.js.map reference ../../src/); templates = CLI scaffold. Everything else at repo root (.pgdata local Postgres, coverage, assets/icons sources, test/, test-results/) is excluded by omission. The !negations strip the scaffold's installed deps + cruft (templates/node_modules is 200MB+ and must never ship). package.json/README/LICENSE ship automatically.",
|
|
6
6
|
"files": [
|
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.875";
|
|
@@ -1,19 +1,8 @@
|
|
|
1
1
|
import { type CSSProperties, type ReactNode, useEffect, useState } from 'react';
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
keyboardHeight: 320,
|
|
7
|
-
viewport: { width: 390, height: 844 },
|
|
8
|
-
},
|
|
9
|
-
android: {
|
|
10
|
-
safeArea: { top: 25, right: 0, bottom: 48, left: 0 },
|
|
11
|
-
keyboardHeight: 300,
|
|
12
|
-
viewport: { width: 360, height: 780 },
|
|
13
|
-
},
|
|
14
|
-
} as const;
|
|
15
|
-
|
|
16
|
-
type SimulatorPlatform = keyof typeof SIMULATOR_PROFILES;
|
|
2
|
+
import {
|
|
3
|
+
SIMULATOR_PROFILES,
|
|
4
|
+
type SimulatorPlatform,
|
|
5
|
+
} from '../deviceProfiles.js';
|
|
17
6
|
|
|
18
7
|
function isKeyboardInput(el: Element | null): boolean {
|
|
19
8
|
if (!el) return false;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// Shared device-emulation profiles for the mobile simulator + the Playwright test
|
|
2
|
+
// harness. One source of truth so the in-app `DeviceSimulator` component and the
|
|
3
|
+
// `ugly-app/playwright` `createMobileBotPage` helper agree on the exact safe-area
|
|
4
|
+
// insets, keyboard height, and viewport size for each platform.
|
|
5
|
+
//
|
|
6
|
+
// The keyboard height and the bottom device inset are combined with `max()`, never
|
|
7
|
+
// added — see KeyboardProvider.
|
|
8
|
+
|
|
9
|
+
export interface DeviceProfile {
|
|
10
|
+
safeArea: { top: number; right: number; bottom: number; left: number };
|
|
11
|
+
keyboardHeight: number;
|
|
12
|
+
viewport: { width: number; height: number };
|
|
13
|
+
/** CSS device-pixel-ratio to emulate (physical density). */
|
|
14
|
+
deviceScaleFactor: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const SIMULATOR_PROFILES = {
|
|
18
|
+
ios: {
|
|
19
|
+
safeArea: { top: 59, right: 0, bottom: 34, left: 0 },
|
|
20
|
+
keyboardHeight: 320,
|
|
21
|
+
viewport: { width: 390, height: 844 },
|
|
22
|
+
deviceScaleFactor: 3,
|
|
23
|
+
},
|
|
24
|
+
android: {
|
|
25
|
+
safeArea: { top: 25, right: 0, bottom: 48, left: 0 },
|
|
26
|
+
keyboardHeight: 300,
|
|
27
|
+
viewport: { width: 360, height: 780 },
|
|
28
|
+
deviceScaleFactor: 3,
|
|
29
|
+
},
|
|
30
|
+
} as const satisfies Record<string, DeviceProfile>;
|
|
31
|
+
|
|
32
|
+
export type SimulatorPlatform = keyof typeof SIMULATOR_PROFILES;
|
|
@@ -20,12 +20,36 @@ export function installKeyboard(state: InspectState): void {
|
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
function currentKeyboardState(): KeyboardState {
|
|
23
|
-
if (typeof window === 'undefined'
|
|
23
|
+
if (typeof window === 'undefined') {
|
|
24
24
|
return { height: 0, isOpen: false };
|
|
25
25
|
}
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
26
|
+
// Primary source: real visualViewport occlusion (browser / PWA / WKWebView).
|
|
27
|
+
if (window.visualViewport) {
|
|
28
|
+
const diff = window.innerHeight - window.visualViewport.height;
|
|
29
|
+
if (diff > 100) {
|
|
30
|
+
const height = Math.round(diff);
|
|
31
|
+
return { height, isOpen: true };
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
// Fallback: an explicit keyboard height (native shell, ugly-studio preview, or the
|
|
35
|
+
// Playwright mobile helper) — the simulated keyboard cannot shrink visualViewport,
|
|
36
|
+
// so read `window.__keyboardHeight` / the `--keyboard-inset-height` CSS var instead.
|
|
37
|
+
const explicit = explicitKeyboardHeight();
|
|
38
|
+
return { height: explicit, isOpen: explicit > 0 };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function explicitKeyboardHeight(): number {
|
|
42
|
+
const fromWindow = (window as unknown as { __keyboardHeight?: number })
|
|
43
|
+
.__keyboardHeight;
|
|
44
|
+
if (typeof fromWindow === 'number' && fromWindow > 0) {
|
|
45
|
+
return Math.round(fromWindow);
|
|
46
|
+
}
|
|
47
|
+
if (typeof document === 'undefined') return 0;
|
|
48
|
+
const raw = window
|
|
49
|
+
.getComputedStyle(document.documentElement)
|
|
50
|
+
.getPropertyValue('--keyboard-inset-height');
|
|
51
|
+
const parsed = parseInt(raw || '0', 10);
|
|
52
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : 0;
|
|
29
53
|
}
|
|
30
54
|
|
|
31
55
|
function scanCoveredInputs(
|
package/src/playwright/index.ts
CHANGED
|
@@ -124,6 +124,13 @@ export async function simulateKeyboard(
|
|
|
124
124
|
heightPx: number,
|
|
125
125
|
): Promise<void> {
|
|
126
126
|
await page.evaluate((h: number) => {
|
|
127
|
+
// Set the explicit signal the layout AND the scanCoveredInputs inspector read
|
|
128
|
+
// (the simulated keyboard can't shrink visualViewport), then dispatch the event.
|
|
129
|
+
(window as unknown as { __keyboardHeight?: number }).__keyboardHeight = h;
|
|
130
|
+
document.documentElement.style.setProperty(
|
|
131
|
+
'--keyboard-inset-height',
|
|
132
|
+
`${h}px`,
|
|
133
|
+
);
|
|
127
134
|
window.dispatchEvent(
|
|
128
135
|
new CustomEvent('keyboard-height', { detail: { height: h } }),
|
|
129
136
|
);
|
|
@@ -209,3 +216,10 @@ export { installRealNative, makePathResolver } from './real-native.js';
|
|
|
209
216
|
export type { RealNativeOptions } from './real-native.js';
|
|
210
217
|
export { loadDevAuth, authenticateReal } from './dev-auth.js';
|
|
211
218
|
export type { DevAuth } from './dev-auth.js';
|
|
219
|
+
export {
|
|
220
|
+
createMobileBotPage,
|
|
221
|
+
installMobileFrame,
|
|
222
|
+
raiseKeyboard,
|
|
223
|
+
dismissKeyboard,
|
|
224
|
+
} from './mobile.js';
|
|
225
|
+
export type { MobileBotOptions, SimulatorPlatform } from './mobile.js';
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import type { Browser, BrowserContext, Page } from '@playwright/test';
|
|
2
|
+
import {
|
|
3
|
+
SIMULATOR_PROFILES,
|
|
4
|
+
type SimulatorPlatform,
|
|
5
|
+
} from '../client/deviceProfiles.js';
|
|
6
|
+
|
|
7
|
+
export type { SimulatorPlatform } from '../client/deviceProfiles.js';
|
|
8
|
+
|
|
9
|
+
const COOKIE_NAME = 'auth_token';
|
|
10
|
+
|
|
11
|
+
export interface MobileBotOptions {
|
|
12
|
+
/** Cookie domain the auth token is scoped to. Default 'localhost' (local dev). */
|
|
13
|
+
domain?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Build the init script that injects a realistic device safe-area into the page
|
|
18
|
+
* BEFORE app JS runs, mirroring `uglyStudioPreviewBridge` / the native shell:
|
|
19
|
+
* - `window.__safeAreaInsets` — KeyboardProvider seeds its "native override" from
|
|
20
|
+
* this on mount (authoritative path), so `useSafeAreaInsets()` pads correctly.
|
|
21
|
+
* - `--safe-area-inset-*` CSS vars on `:root` — so raw `.safe-area` consumers AND
|
|
22
|
+
* the `scanSafeArea` inspector (which reads these) see the insets.
|
|
23
|
+
* - `--keyboard-inset-height: 0px` — initial keyboard-closed state.
|
|
24
|
+
* A `keyboard-height` of 0 is dispatched last so any already-mounted listener resets.
|
|
25
|
+
*/
|
|
26
|
+
function safeAreaInitScript(platform: SimulatorPlatform): string {
|
|
27
|
+
const { safeArea } = SIMULATOR_PROFILES[platform];
|
|
28
|
+
const insets = JSON.stringify(safeArea);
|
|
29
|
+
return `(() => {
|
|
30
|
+
const insets = ${insets};
|
|
31
|
+
try { window.__safeAreaInsets = insets; } catch {}
|
|
32
|
+
const apply = () => {
|
|
33
|
+
const root = document.documentElement;
|
|
34
|
+
if (!root) return;
|
|
35
|
+
root.style.setProperty('--safe-area-inset-top', insets.top + 'px');
|
|
36
|
+
root.style.setProperty('--safe-area-inset-right', insets.right + 'px');
|
|
37
|
+
root.style.setProperty('--safe-area-inset-bottom', insets.bottom + 'px');
|
|
38
|
+
root.style.setProperty('--safe-area-inset-left', insets.left + 'px');
|
|
39
|
+
root.style.setProperty('--keyboard-inset-height', '0px');
|
|
40
|
+
};
|
|
41
|
+
apply();
|
|
42
|
+
if (document.readyState === 'loading') {
|
|
43
|
+
document.addEventListener('DOMContentLoaded', apply, { once: true });
|
|
44
|
+
}
|
|
45
|
+
window.addEventListener('load', () => {
|
|
46
|
+
apply();
|
|
47
|
+
window.dispatchEvent(new CustomEvent('safe-area-insets', { detail: insets }));
|
|
48
|
+
window.dispatchEvent(new CustomEvent('keyboard-height', { detail: { height: 0 } }));
|
|
49
|
+
window.dispatchEvent(new CustomEvent('loaded', { detail: '' }));
|
|
50
|
+
}, { once: true });
|
|
51
|
+
})();`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Apply mobile device emulation (viewport, touch, safe-area injection) to a
|
|
56
|
+
* pre-existing context — useful when you already created the context yourself.
|
|
57
|
+
*/
|
|
58
|
+
export async function installMobileFrame(
|
|
59
|
+
context: BrowserContext,
|
|
60
|
+
platform: SimulatorPlatform,
|
|
61
|
+
): Promise<void> {
|
|
62
|
+
await context.addInitScript(safeAreaInitScript(platform));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Create a page emulating a phone (iOS/Android) with a realistic top/bottom
|
|
67
|
+
* safe-area and on-screen keyboard, pre-authenticated with the given token.
|
|
68
|
+
*
|
|
69
|
+
* The layout responds via the globally-mounted `KeyboardProvider`, and the
|
|
70
|
+
* `scanSafeArea` / `scanCoveredInputs` inspectors (behind `expectClean`) see the
|
|
71
|
+
* simulated insets/keyboard. Navigate with `?debugSafeArea=true` to also render the
|
|
72
|
+
* visible top/bottom/keyboard bands for a screenshot vision pass. Works against any
|
|
73
|
+
* ugly-app child app, local or prod, with no per-app wiring.
|
|
74
|
+
*
|
|
75
|
+
* @example
|
|
76
|
+
* const page = await createMobileBotPage(browser, token, 'ios')
|
|
77
|
+
* await page.goto(`${BASE_URL}/?debugSafeArea=true`)
|
|
78
|
+
* await waitForApp(page)
|
|
79
|
+
* await page.locator('textarea').first().focus()
|
|
80
|
+
* await raiseKeyboard(page, 'ios')
|
|
81
|
+
* await expectClean(page, { allowSafeAreaViolations: false, allowKeyboardCoverage: false })
|
|
82
|
+
*/
|
|
83
|
+
export async function createMobileBotPage(
|
|
84
|
+
browser: Browser,
|
|
85
|
+
token: string,
|
|
86
|
+
platform: SimulatorPlatform,
|
|
87
|
+
opts?: MobileBotOptions,
|
|
88
|
+
): Promise<Page> {
|
|
89
|
+
const profile = SIMULATOR_PROFILES[platform];
|
|
90
|
+
const context = await browser.newContext({
|
|
91
|
+
viewport: { ...profile.viewport },
|
|
92
|
+
deviceScaleFactor: profile.deviceScaleFactor,
|
|
93
|
+
isMobile: true,
|
|
94
|
+
hasTouch: true,
|
|
95
|
+
});
|
|
96
|
+
await context.addCookies([
|
|
97
|
+
{
|
|
98
|
+
name: COOKIE_NAME,
|
|
99
|
+
value: token,
|
|
100
|
+
domain: opts?.domain ?? 'localhost',
|
|
101
|
+
path: '/',
|
|
102
|
+
httpOnly: true,
|
|
103
|
+
secure: false,
|
|
104
|
+
sameSite: 'Lax',
|
|
105
|
+
},
|
|
106
|
+
]);
|
|
107
|
+
await installMobileFrame(context, platform);
|
|
108
|
+
return await context.newPage();
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Raise the simulated on-screen keyboard. Sets `--keyboard-inset-height` +
|
|
113
|
+
* `window.__keyboardHeight` AND dispatches the `keyboard-height` CustomEvent, so
|
|
114
|
+
* both the layout (`KeyboardProvider`) and the `scanCoveredInputs` inspector agree.
|
|
115
|
+
* We drive this explicitly rather than relying on `visualViewport` shrinking, which
|
|
116
|
+
* is not possible from Playwright.
|
|
117
|
+
*
|
|
118
|
+
* @param platformOrHeight a platform ('ios'/'android') to use its profile height,
|
|
119
|
+
* or an explicit pixel height.
|
|
120
|
+
*/
|
|
121
|
+
export async function raiseKeyboard(
|
|
122
|
+
page: Page,
|
|
123
|
+
platformOrHeight: SimulatorPlatform | number = 'ios',
|
|
124
|
+
): Promise<void> {
|
|
125
|
+
const height =
|
|
126
|
+
typeof platformOrHeight === 'number'
|
|
127
|
+
? platformOrHeight
|
|
128
|
+
: SIMULATOR_PROFILES[platformOrHeight].keyboardHeight;
|
|
129
|
+
await setKeyboardHeight(page, height);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Dismiss the simulated keyboard (height 0). */
|
|
133
|
+
export async function dismissKeyboard(page: Page): Promise<void> {
|
|
134
|
+
await setKeyboardHeight(page, 0);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async function setKeyboardHeight(page: Page, height: number): Promise<void> {
|
|
138
|
+
await page.evaluate((h: number) => {
|
|
139
|
+
(window as unknown as { __keyboardHeight?: number }).__keyboardHeight = h;
|
|
140
|
+
document.documentElement.style.setProperty(
|
|
141
|
+
'--keyboard-inset-height',
|
|
142
|
+
`${h}px`,
|
|
143
|
+
);
|
|
144
|
+
window.dispatchEvent(
|
|
145
|
+
new CustomEvent('keyboard-height', { detail: { height: h } }),
|
|
146
|
+
);
|
|
147
|
+
}, height);
|
|
148
|
+
}
|
|
@@ -35,6 +35,14 @@ the real feature, on real data, and *see* the result with its own eyes produces
|
|
|
35
35
|
imagination — dead weight that pollutes the signal. Every guardrail below exists to keep
|
|
36
36
|
each finding anchored to something the persona actually did and saw.
|
|
37
37
|
|
|
38
|
+
**Corollary — grade the OUTCOME, never the UI's self-report.** A green check, a "done"
|
|
39
|
+
status, and a completed turn are claims by the thing under test; they are not evidence.
|
|
40
|
+
Assert ground truth outside the app: read the file on disk, query the row, diff the repo.
|
|
41
|
+
An app that politely tells you it did nothing still renders as success — that exact hole
|
|
42
|
+
let five ugly-code rounds score a coding agent that could not search (see Step 0.6). When
|
|
43
|
+
a persona's goal implies a durable change, the verdict must be checked against that
|
|
44
|
+
change, not against the transcript describing it.
|
|
45
|
+
|
|
38
46
|
---
|
|
39
47
|
|
|
40
48
|
## Billing
|
|
@@ -87,6 +95,21 @@ fake findings, which is worse than no findings.
|
|
|
87
95
|
with any cost-control env var prepended (see Billing), NOT `--watch` (the swarm reads a
|
|
88
96
|
stable build). Then poll `$(npx ugly-app url)` until it answers.
|
|
89
97
|
|
|
98
|
+
6. **Does the app's machinery actually WORK, or just not crash?** A broken environment
|
|
99
|
+
usually announces itself; a **silently degraded** one does not, and that is the
|
|
100
|
+
dangerous case — the run completes, every check is green, and the rating is fiction.
|
|
101
|
+
Before dispatching, prove each capability the personas will exercise is *live*, not
|
|
102
|
+
merely present: run the smallest real operation and look at its output.
|
|
103
|
+
- **Real precedent:** ugly-code evals ran five rounds and earned ★★★★★ while the coding
|
|
104
|
+
agent's `glob`/`grep` were **dead** — `rg` was missing from the task child's PATH, the
|
|
105
|
+
failed spawn rendered as a green "no matches" card, and the agent concluded each
|
|
106
|
+
project was empty. Nothing errored. The harness now refuses to start when a required
|
|
107
|
+
binary is unresolvable (`assertAgentToolchain`) — **prefer a preflight that makes the
|
|
108
|
+
harness refuse over one that warns.**
|
|
109
|
+
- Generalize it: if the app shells out to a binary, resolve it; if it depends on an
|
|
110
|
+
index/model/migration, query it for a known-present row. "It returned successfully"
|
|
111
|
+
is not evidence — "it returned the thing I planted" is.
|
|
112
|
+
|
|
90
113
|
Store `BASE_URL="$(npx ugly-app url)"` for the run. Everything the swarm reads and writes
|
|
91
114
|
lives on **this local server** — the framework read CLIs (`ugly-app feedback`, `errors`,
|
|
92
115
|
`logs`) query **prod**, not local, so don't use them to inspect the run.
|
|
@@ -130,9 +153,33 @@ Do not use a stock roster. Open the app and design personas around what it actua
|
|
|
130
153
|
- **consume-persona** — it needs *existing* data to work on (a reader needs a
|
|
131
154
|
manuscript; a coach-user needs a draft). Requires seed — see Step 2.
|
|
132
155
|
|
|
156
|
+
4. **Always include a first-run newcomer.** One persona must arrive COLD — has never used
|
|
157
|
+
the app — and judge the ONBOARDING itself: landing → creating / opening the first project
|
|
158
|
+
→ writing the **first prompt** → seeing the first result. This cold-start is the
|
|
159
|
+
make-or-break moment for adoption (a powerful tool that's confusing to *start* loses users
|
|
160
|
+
at the door) and it's the single thing builders are most blind to. Have them narrate every
|
|
161
|
+
hesitation out loud — "what do I click?", "what do I type here?", "wait, did that even
|
|
162
|
+
work?" — and screenshot each dead-end.
|
|
163
|
+
|
|
164
|
+
Vary **UX tolerance** as a real axis, not just goals: an impatient newcomer who bails at the
|
|
165
|
+
first confusion surfaces different problems than a patient power user who pushes through
|
|
166
|
+
friction. The gap between their verdicts IS your ease-of-use signal — design at least two
|
|
167
|
+
personas whose *patience* differs, not just their task.
|
|
168
|
+
|
|
169
|
+
5. **Pick each persona's `device`: `desktop` | `ios` | `android`.** These apps ship to
|
|
170
|
+
phones, where a whole class of bugs lives that a desktop viewport never shows — content
|
|
171
|
+
bleeding under the notch/status bar, a docked composer or CTA hidden under the
|
|
172
|
+
home-indicator, an input covered by the on-screen keyboard. A desktop-only swarm is
|
|
173
|
+
**blind** to all of it. So give **at least two personas a mobile device** — the cold-start
|
|
174
|
+
newcomer especially (first-run pain is worst on a phone), plus one persona whose goal leans
|
|
175
|
+
on a text input / composer (the keyboard-overlap case). Mobile costs the same AI spend as
|
|
176
|
+
desktop; it only changes the viewport and screenshots. A mobile persona runs the **Mobile
|
|
177
|
+
safe-area + keyboard pass** in its contract (Step 3).
|
|
178
|
+
|
|
133
179
|
Write each persona as a short brief you will hand to its subagent: who they are, their
|
|
134
180
|
goal for this session, the exact features they should exercise, and what would make them
|
|
135
|
-
say "this tool works" vs. "this is broken
|
|
181
|
+
say "this tool works" vs. "this is broken" — AND what would make them say "this felt
|
|
182
|
+
effortless" vs. "I nearly gave up."
|
|
136
183
|
|
|
137
184
|
*(Optional repeatability: an app may ship `bots/eval-swarm.json` — `{ personas, seed,
|
|
138
185
|
model }`. If present, use it instead of inventing. See Config.)*
|
|
@@ -171,12 +218,22 @@ calls). Never batch-author findings for several personas in one pass — that is
|
|
|
171
218
|
shortcut that produces imagination instead of usage. Give each subagent this contract:
|
|
172
219
|
|
|
173
220
|
> You are **<Persona Name>**: <one-line brief>. Your goal this session: <goal>.
|
|
174
|
-
> Base URL: `<BASE_URL>`. Your account token: `<token>`.
|
|
221
|
+
> Base URL: `<BASE_URL>`. Your account token: `<token>`. Your device: `<desktop|ios|android>`.
|
|
175
222
|
>
|
|
176
223
|
> **Authenticate**, then actually use the product to pursue your goal:
|
|
177
|
-
> 1. In headless Chromium (`pnpm exec playwright` / the Playwright MCP if available)
|
|
178
|
-
> cookie `auth_token=<token>` for `<BASE_URL>`, then `goto` the app
|
|
179
|
-
>
|
|
224
|
+
> 1. In headless Chromium (`pnpm exec playwright` / the Playwright MCP if available):
|
|
225
|
+
> - **`desktop`** → set cookie `auth_token=<token>` for `<BASE_URL>`, then `goto` the app.
|
|
226
|
+
> - **`ios` / `android`** → drive a real phone frame. In a `.mjs` script inside the app dir
|
|
227
|
+
> (`import { createMobileBotPage, raiseKeyboard, dismissKeyboard, waitForApp, expectClean }
|
|
228
|
+
> from 'ugly-app/playwright'`), do
|
|
229
|
+
> `const page = await createMobileBotPage(browser, '<token>', '<ios|android>')` — it sets a
|
|
230
|
+
> phone viewport (390×844 iOS / 360×780 Android), touch emulation, and injects the top +
|
|
231
|
+
> bottom device safe-area so the layout pads for real. `goto` **`<BASE_URL>/?debugSafeArea=true`**
|
|
232
|
+
> so the red top / blue bottom / green keyboard bands render for your vision pass.
|
|
233
|
+
> - Either way, wait for the hydration selector `<WAIT_SEL>` (see Screenshot recipe), never
|
|
234
|
+
> `networkidle`. (If `ugly-app/playwright` lacks `createMobileBotPage`, the app is on an old
|
|
235
|
+
> `ugly-app` — tell the orchestrator to bump it; do NOT fall back to a desktop viewport and
|
|
236
|
+
> call it a mobile run.)
|
|
180
237
|
> 2. Drive the real features toward your goal. **Trigger the real AI** — generate, coach,
|
|
181
238
|
> extract, chat, whatever this app does — and **wait for real responses**. This spends
|
|
182
239
|
> real (owner-billed) tokens; that is expected. Budget ~10–20 AI interactions.
|
|
@@ -185,22 +242,63 @@ shortcut that produces imagination instead of usage. Give each subagent this con
|
|
|
185
242
|
> without a logged look at the thing you are critiquing is invalid — discard it.
|
|
186
243
|
> 4. Judge as your persona: did the feature do what you came for? Was the AI output good,
|
|
187
244
|
> wrong, empty, slow, truncated, off-tone? Was anything broken, confusing, or missing?
|
|
245
|
+
> 5. **Judge the EXPERIENCE, not just the output.** Beyond "did it work": at each step, was it
|
|
246
|
+
> obvious what to do next? Where did you hesitate, backtrack, misread a control, hunt for a
|
|
247
|
+
> button, or feel lost? What did the app assume you already knew? What (if anything)
|
|
248
|
+
> delighted you? Ease-of-use is a first-class finding — a feature that *works* but is
|
|
249
|
+
> confusing to reach is still a real problem worth ranking (`type: 'design'`). Narrate this
|
|
250
|
+
> in the user's own voice. **If you are the newcomer, walk the FULL cold start** — first
|
|
251
|
+
> screen → create/open a project → write the first prompt → first result — and log every
|
|
252
|
+
> friction point with a screenshot; that onboarding narrative is your most valuable output.
|
|
253
|
+
> 6. **Mobile safe-area + keyboard pass (only if your `device` is `ios`/`android`).** A phone
|
|
254
|
+
> clips content the desktop layout never does. At each key screen you visit, judge three
|
|
255
|
+
> states — screenshot and LOOK at each against the debug bands:
|
|
256
|
+
> - **Top (notch / status bar):** is the header, back button, or top nav clear of the red
|
|
257
|
+
> top band, or does it hide under it?
|
|
258
|
+
> - **Bottom (home-indicator):** is a docked composer, send button, tab bar, or primary CTA
|
|
259
|
+
> clear of the blue bottom band, or clipped by it?
|
|
260
|
+
> - **Keyboard open:** focus the main text input, then `await raiseKeyboard(page, '<ios|android>')`.
|
|
261
|
+
> Is the focused input — and its Send/submit button — still visible above the green keyboard
|
|
262
|
+
> band, or hidden behind it? (`await dismissKeyboard(page)` when done.)
|
|
263
|
+
> Then get the **hard signal**: `await expectClean(page, { allowSafeAreaViolations: false,
|
|
264
|
+
> allowKeyboardCoverage: false })`. It throws listing any interactive element crossing a
|
|
265
|
+
> safe-area edge (`safeAreaViolations`) or covered by the keyboard (`keyboard.coveredInputs`) —
|
|
266
|
+
> quote those selectors as evidence. File each as `--type design`; a clipped/covered control on
|
|
267
|
+
> a **core** flow is high-severity (it caps the rating — see rubric), not a nit.
|
|
188
268
|
>
|
|
189
269
|
> **File each real finding** with
|
|
190
270
|
> `npx ugly-app feedback:submit --type <bug|design|feature> --message "<what you did →
|
|
191
271
|
> what you saw → why it's wrong>" --token <token> --url <page>`. Every message must name
|
|
192
|
-
> the concrete thing you did and saw.
|
|
272
|
+
> the concrete thing you did and saw. UX-friction findings are `--type design`.
|
|
193
273
|
>
|
|
194
|
-
> **Return to me** a JSON
|
|
195
|
-
>
|
|
196
|
-
>
|
|
274
|
+
> **Return to me** a JSON object as your final message (data for aggregation, not a human
|
|
275
|
+
> report):
|
|
276
|
+
> `{ findings: [{type, severity, feature, message, evidence}], ease: <1–5, how effortless was
|
|
277
|
+
> it to get what you came for>, firstRun: "<one-paragraph narrative of the onboarding + first-
|
|
278
|
+
> prompt experience, every hesitation named>", friction: ["<a moment you felt lost/confused/
|
|
279
|
+
> slowed>", …], delight: ["<anything that felt genuinely good>", …], verdict: "<does this tool
|
|
280
|
+
> work for you — AND did it feel good to use?>" }`. Findings must include design/UX friction
|
|
281
|
+
> (`type:'design'`), not only functional bugs. If your device is mobile, add
|
|
282
|
+
> `mobile: { device, safeAreaViolations: [...], keyboardCoveredInputs: [...], verdict: "<did the
|
|
283
|
+
> phone layout hold up?>" }` from your Mobile pass.
|
|
197
284
|
|
|
198
285
|
**Guardrails to state in every dispatch:**
|
|
286
|
+
- **A mobile persona that never checked safe-area or the keyboard did not do its job.** A phone
|
|
287
|
+
run whose findings are all desktop-shaped (no notch / home-indicator / keyboard-overlap check)
|
|
288
|
+
wasted the slot — run the Mobile pass and report `expectClean`'s result, even if it's clean.
|
|
199
289
|
- Real AI only. If you find yourself imagining a response instead of waiting for one, stop
|
|
200
290
|
and actually run the feature.
|
|
201
291
|
- Vision before verdict. No finding about a screen you did not look at.
|
|
202
292
|
- Ground every message in "I did X, I saw Y." Vague wishes ("would be nice if…") that
|
|
203
293
|
could have been written without opening the app are noise — cut them.
|
|
294
|
+
- **Green is a claim, not a result.** When your goal implies a durable change (a file
|
|
295
|
+
edited, a row written, a doc saved), verify it OUTSIDE the app — read the file, query
|
|
296
|
+
the row — before crediting it. If the app reports success and the change isn't there,
|
|
297
|
+
that is a **blocker**, and it's the single most valuable finding you can file.
|
|
298
|
+
- **"There's nothing here" is a red flag, not a finding.** If the app claims the data you
|
|
299
|
+
planted doesn't exist, suspect your environment before believing it (Step 0.6): a
|
|
300
|
+
silently dead dependency looks exactly like an empty app. Report it as a possible
|
|
301
|
+
harness failure so the orchestrator can check — don't grade the app on it.
|
|
204
302
|
|
|
205
303
|
Timeout ~20 min per persona. A persona that fails or times out does not block the others;
|
|
206
304
|
record which succeeded.
|
|
@@ -222,10 +320,16 @@ rows. Don't hand off by telling a fixer to "go read the feedback" — it can't.)
|
|
|
222
320
|
output, then UX friction, then wishes.
|
|
223
321
|
4. Present one ranked list: for each item — feature, severity, how many personas hit it,
|
|
224
322
|
the sharpest one-line evidence, and the persona verdicts (did the tool work?).
|
|
225
|
-
5. **
|
|
323
|
+
5. **Synthesize the EXPERIENCE, separately from the bug list.** Report the average `ease`
|
|
324
|
+
score and its spread (a wide gap between the impatient and the patient persona is itself a
|
|
325
|
+
finding — it means the app only works if you already know it). Give a short **first-run
|
|
326
|
+
digest**: the friction points every newcomer hit walking new-project → first-prompt →
|
|
327
|
+
first-result, plus anything that delighted. Onboarding friction rarely files as a "bug" but
|
|
328
|
+
is the #1 reason a new user leaves — surface it as prominently as the top functional finding.
|
|
329
|
+
6. **Rate the product 1–5 stars** (see rubric) with a one-line justification. Lead the
|
|
226
330
|
presentation with the rating — it is the headline the user wants; the ranked list is the
|
|
227
331
|
evidence behind it.
|
|
228
|
-
|
|
332
|
+
7. Hand off the ranked list **as content** for fixing — either fix the items directly in
|
|
229
333
|
this session, or paste the list into a `/fix-feedback`-style pass (using its ambition
|
|
230
334
|
policy), since the fixer cannot pull these local rows itself.
|
|
231
335
|
|
|
@@ -262,8 +366,10 @@ moved. That's how a product climbs from ★★★ to ★★★★★.
|
|
|
262
366
|
### Rating rubric (1–5 stars)
|
|
263
367
|
|
|
264
368
|
Rate the product as a whole, computed from the **worst core-feature outcome**, **how many
|
|
265
|
-
personas completed their primary goal**,
|
|
266
|
-
|
|
369
|
+
personas completed their primary goal**, the **persona verdicts**, and the **ease-of-use /
|
|
370
|
+
first-run experience** (the `ease` scores + onboarding friction). A broken feature caps the
|
|
371
|
+
score — polish can't buy back a blocker; and a feature that works but that a newcomer can't
|
|
372
|
+
figure out how to *reach* is a real defect, not a nit.
|
|
267
373
|
|
|
268
374
|
| Stars | Meaning | Gate |
|
|
269
375
|
|-------|---------|------|
|
|
@@ -273,10 +379,31 @@ caps the score — polish can't buy back a blocker.
|
|
|
273
379
|
| ★★★★☆ **4 — Solid** | All core jobs work; findings are UX polish, edge cases, or wishes. | All personas completed their goal; no blocker/high on a core path. |
|
|
274
380
|
| ★★★★★ **5 — Excellent** | Every persona completed their goal; outputs accurate and trustworthy; only cosmetic nits. | Every verdict "works"; no finding above `low`. |
|
|
275
381
|
|
|
382
|
+
**Validity gate — publish no rating you can't vouch for the environment of.** Stars are a
|
|
383
|
+
claim about the product; they're only true if the product was the thing being measured.
|
|
384
|
+
Before you report a number, confirm Step 0.6 passed and that at least one persona's durable
|
|
385
|
+
change was verified outside the app. If a capability was silently dead, the round measured
|
|
386
|
+
your harness — say so and re-run; do NOT publish the score with a caveat. A wrong ★★★★★ is
|
|
387
|
+
more damaging than no rating, because it ends the investigation.
|
|
388
|
+
|
|
276
389
|
Caps are hard: one blocker on a primary feature means **≤2 stars no matter how good the
|
|
277
390
|
rest is**; a broken secondary feature caps at **3**. Half-stars are fine (e.g. ★★★½).
|
|
278
391
|
State the single finding that set the ceiling.
|
|
279
392
|
|
|
393
|
+
**Ease-of-use caps too.** If the first-run newcomer couldn't figure out how to start —
|
|
394
|
+
create/open a project, or write and send their first prompt — without trial-and-error, cap at
|
|
395
|
+
**3** however well the features work (a tool you can't figure out isn't "solid"). **5 requires
|
|
396
|
+
the cold start to feel effortless**, not merely for the features to be correct: average
|
|
397
|
+
`ease ≥ 4.5`, no newcomer left confused at a step, and no "I nearly gave up" verdict.
|
|
398
|
+
|
|
399
|
+
**Mobile safe-area / keyboard caps too.** If a mobile persona found an interactive control on a
|
|
400
|
+
**core** flow clipped under the notch/home-indicator or hidden behind the keyboard (a confirmed
|
|
401
|
+
`safeAreaViolations` / `keyboard.coveredInputs` entry, or a screenshot showing it), cap at **3**
|
|
402
|
+
however well the desktop layout works — a real phone user literally cannot tap what they can't
|
|
403
|
+
reach. Name the offending control as the ceiling finding. **5 requires a clean mobile pass**: no
|
|
404
|
+
core-flow safe-area or keyboard-coverage violation on any device a persona ran. (Cosmetic edge
|
|
405
|
+
clipping of a non-interactive decoration is a `low` nit, not a cap.)
|
|
406
|
+
|
|
280
407
|
---
|
|
281
408
|
|
|
282
409
|
## Screenshot recipe — always wait for hydration
|
|
@@ -309,6 +436,15 @@ writes a `.mjs` script instead. Two gotchas that will `ERR_MODULE_NOT_FOUND` you
|
|
|
309
436
|
resolves bare imports from the script's own folder upward, so a script in a scratchpad
|
|
310
437
|
can't find the app's `node_modules`. Delete it when done.
|
|
311
438
|
|
|
439
|
+
**Mobile personas — phone frame + safe-area.** Don't screenshot a mobile persona in a desktop
|
|
440
|
+
viewport. Use `createMobileBotPage(browser, token, 'ios'|'android')` from `ugly-app/playwright`
|
|
441
|
+
(phone viewport + touch + injected top/bottom safe-area), and load with `?debugSafeArea=true` so
|
|
442
|
+
the red top / blue bottom / green keyboard bands are visible in the PNG for your vision pass.
|
|
443
|
+
`raiseKeyboard(page, platform)` / `dismissKeyboard(page)` toggle the keyboard; `expectClean(page,
|
|
444
|
+
{ allowSafeAreaViolations: false, allowKeyboardCoverage: false })` is the hard gate that lists
|
|
445
|
+
clipped/covered interactive elements. (Requires a recent `ugly-app`; see TESTING.md → "Mobile
|
|
446
|
+
safe-area + keyboard".)
|
|
447
|
+
|
|
312
448
|
---
|
|
313
449
|
|
|
314
450
|
## Config (optional, per-app)
|
|
@@ -318,7 +454,8 @@ An app can ship `bots/eval-swarm.json` for repeatable runs:
|
|
|
318
454
|
```json
|
|
319
455
|
{
|
|
320
456
|
"personas": [
|
|
321
|
-
{ "slug": "plotter", "name": "The Plotter", "brief": "...", "goal": "...", "needs": "consume" }
|
|
457
|
+
{ "slug": "plotter", "name": "The Plotter", "brief": "...", "goal": "...", "needs": "consume", "device": "desktop" },
|
|
458
|
+
{ "slug": "newcomer", "name": "The Newcomer", "brief": "...", "goal": "...", "needs": "create", "device": "ios" }
|
|
322
459
|
],
|
|
323
460
|
"seed": "bots/seed-story.mjs",
|
|
324
461
|
"model": "deepseek_v4_flash"
|
|
@@ -329,6 +466,8 @@ An app can ship `bots/eval-swarm.json` for repeatable runs:
|
|
|
329
466
|
data is absent after it; the skill does not auto-run seeders unless the app documents it).
|
|
330
467
|
- `model` — a cheap-model override env the app honors for eval runs (e.g. ugly-ink's
|
|
331
468
|
`INK_FORCE_MODEL`). Cost control only; default is the app's real production model.
|
|
469
|
+
- `device` — per-persona `desktop` | `ios` | `android` (default `desktop`). Mobile personas
|
|
470
|
+
run the Mobile safe-area + keyboard pass (Step 3). Give at least two personas a phone.
|
|
332
471
|
|
|
333
472
|
If no config exists, invent personas per Step 1 — that is the normal path.
|
|
334
473
|
|
|
@@ -392,6 +531,8 @@ native/mobile work, or cross-app infra), say so — that's a real-cost item, not
|
|
|
392
531
|
| "I can skip the screenshot, I know what it says" | No finding about a screen you didn't look at. Vision before verdict. |
|
|
393
532
|
| "`login` failed, let me retry it headless" | It deadlocks on browser auth. Stop and tell the user. |
|
|
394
533
|
| "Stubs would be cheaper" | Stubbed AI produces fake critiques. This skill is real-AI by design; use `verify:e2e` for hermetic checks. |
|
|
534
|
+
| "I'll test this mobile-first app on desktop only" | A desktop viewport is blind to notch / home-indicator / keyboard clipping. Give ≥2 personas a phone `device` and run the Mobile pass. |
|
|
535
|
+
| "The mobile screenshot looks fine, skip `expectClean`" | Eyeballing misses sub-pixel overlaps. `expectClean` names the exact clipped/covered element — run it for the hard signal. |
|
|
395
536
|
|
|
396
537
|
## Completion
|
|
397
538
|
|