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.
@@ -1,2 +1,2 @@
1
- export declare const CLI_VERSION = "0.1.874";
1
+ export declare const CLI_VERSION = "0.1.875";
2
2
  //# sourceMappingURL=version.d.ts.map
@@ -1,3 +1,3 @@
1
1
  // Auto-generated by prebuild — do not edit manually
2
- export const CLI_VERSION = "0.1.874";
2
+ export const CLI_VERSION = "0.1.875";
3
3
  //# sourceMappingURL=version.js.map
@@ -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;AAwDhF,wBAAgB,eAAe,CAAC,EAC9B,QAAQ,GACT,EAAE;IACD,QAAQ,EAAE,SAAS,CAAC;CACrB,GAAG,KAAK,CAAC,SAAS,CA0KlB"}
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
- const SIMULATOR_PROFILES = {
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":"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"}
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;AAChF,OAAO,EACL,kBAAkB,GAEnB,MAAM,sBAAsB,CAAC;AAE9B,SAAS,eAAe,CAAC,EAAkB;IACzC,IAAI,CAAC,EAAE;QAAE,OAAO,KAAK,CAAC;IACtB,IAAI,EAAE,CAAC,OAAO,KAAK,UAAU;QAAE,OAAO,IAAI,CAAC;IAC3C,IAAI,EAAE,CAAC,YAAY,CAAC,iBAAiB,CAAC,KAAK,MAAM;QAAE,OAAO,IAAI,CAAC;IAC/D,IAAI,EAAE,CAAC,SAAS,CAAC,QAAQ,CAAC,aAAa,CAAC;QAAE,OAAO,IAAI,CAAC;IACtD,IAAI,EAAE,CAAC,OAAO,KAAK,OAAO,EAAE,CAAC;QAC3B,MAAM,IAAI,GAAI,EAAuB,CAAC,IAAI,CAAC;QAC3C,MAAM,gBAAgB,GAAG;YACvB,UAAU;YACV,OAAO;YACP,OAAO;YACP,QAAQ;YACR,QAAQ;YACR,OAAO;YACP,MAAM;YACN,OAAO;SACR,CAAC;QACF,OAAO,CAAC,gBAAgB,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;IAC1C,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,SAAS,kBAAkB;IAIzB,IAAI,OAAO,MAAM,KAAK,WAAW;QAAE,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC;IAC7E,MAAM,MAAM,GAAG,IAAI,eAAe,CAAC,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;IAC3D,MAAM,GAAG,GAAG,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;IAC9B,MAAM,OAAO,GAAG,MAAM,CAAC,GAAG,CAAC,SAAS,CAAC,KAAK,MAAM,CAAC;IACjD,IAAI,GAAG,KAAK,KAAK,IAAI,GAAG,KAAK,SAAS;QAAE,OAAO,EAAE,QAAQ,EAAE,GAAG,EAAE,OAAO,EAAE,CAAC;IAC1E,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC;AAC5C,CAAC;AAOD,MAAM,UAAU,eAAe,CAAC,EAC9B,QAAQ,GAGT;IACC,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,GAAG,QAAQ,CAAkB;QACpD,QAAQ,EAAE,IAAI;QACd,OAAO,EAAE,KAAK;KACf,CAAC,CAAC;IACH,MAAM,CAAC,eAAe,EAAE,kBAAkB,CAAC,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC;IAE9D,SAAS,CAAC,GAAG,EAAE;QACb,SAAS,CAAC,kBAAkB,EAAE,CAAC,CAAC;IAClC,CAAC,EAAE,EAAE,CAAC,CAAC;IAEP,MAAM,QAAQ,GAAG,MAAM,CAAC,QAAQ,CAAC;IAEjC,SAAS,CAAC,GAAG,EAAE;QACb,IAAI,CAAC,QAAQ;YAAE,OAAO;QAEtB,MAAM,OAAO,GAAG,kBAAkB,CAAC,QAAQ,CAAC,CAAC;QAE7C,4EAA4E;QAC5E,MAAM,aAAa,GAAG,UAAU,CAAC,GAAG,EAAE;YACpC,MAAM,CAAC,aAAa,CAClB,IAAI,WAAW,CAAC,kBAAkB,EAAE,EAAE,MAAM,EAAE,OAAO,CAAC,QAAQ,EAAE,CAAC,CAClE,CAAC;YACF,MAAM,CAAC,aAAa,CAAC,IAAI,WAAW,CAAC,QAAQ,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC,CAAC,CAAC;QAClE,CAAC,EAAE,CAAC,CAAC,CAAC;QAEN,2BAA2B;QAC3B,IAAI,aAAa,GAAyC,IAAI,CAAC;QAE/D,SAAS,SAAS;YAChB,IAAI,aAAa,EAAE,CAAC;gBAClB,YAAY,CAAC,aAAa,CAAC,CAAC;gBAC5B,aAAa,GAAG,IAAI,CAAC;YACvB,CAAC;YACD,IAAI,eAAe,CAAC,QAAQ,CAAC,aAAa,CAAC,EAAE,CAAC;gBAC5C,kBAAkB,CAAC,IAAI,CAAC,CAAC;gBACzB,MAAM,CAAC,aAAa,CAClB,IAAI,WAAW,CAAC,iBAAiB,EAAE;oBACjC,MAAM,EAAE,EAAE,MAAM,EAAE,OAAO,CAAC,cAAc,EAAE;iBAC3C,CAAC,CACH,CAAC;YACJ,CAAC;QACH,CAAC;QAED,SAAS,UAAU;YACjB,IAAI,aAAa;gBAAE,YAAY,CAAC,aAAa,CAAC,CAAC;YAC/C,aAAa,GAAG,UAAU,CAAC,GAAG,EAAE;gBAC9B,IAAI,CAAC,eAAe,CAAC,QAAQ,CAAC,aAAa,CAAC,EAAE,CAAC;oBAC7C,kBAAkB,CAAC,KAAK,CAAC,CAAC;oBAC1B,MAAM,CAAC,aAAa,CAClB,IAAI,WAAW,CAAC,iBAAiB,EAAE,EAAE,MAAM,EAAE,EAAE,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC,CAC9D,CAAC;gBACJ,CAAC;YACH,CAAC,EAAE,GAAG,CAAC,CAAC;QACV,CAAC;QAED,QAAQ,CAAC,gBAAgB,CAAC,SAAS,EAAE,SAAS,CAAC,CAAC;QAChD,QAAQ,CAAC,gBAAgB,CAAC,UAAU,EAAE,UAAU,CAAC,CAAC;QAElD,OAAO,GAAG,EAAE;YACV,YAAY,CAAC,aAAa,CAAC,CAAC;YAC5B,IAAI,aAAa;gBAAE,YAAY,CAAC,aAAa,CAAC,CAAC;YAC/C,QAAQ,CAAC,mBAAmB,CAAC,SAAS,EAAE,SAAS,CAAC,CAAC;YACnD,QAAQ,CAAC,mBAAmB,CAAC,UAAU,EAAE,UAAU,CAAC,CAAC;QACvD,CAAC,CAAC;IACJ,CAAC,EAAE,CAAC,QAAQ,CAAC,CAAC,CAAC;IAEf,IAAI,CAAC,QAAQ;QAAE,OAAO,QAAQ,CAAC;IAE/B,MAAM,OAAO,GAAG,kBAAkB,CAAC,QAAQ,CAAC,CAAC;IAC7C,MAAM,SAAS,GAAG,CAAC,MAAM,CAAC,OAAO,CAAC;IAElC,MAAM,oBAAoB,GAAkB;QAC1C,QAAQ,EAAE,UAAU;QACpB,IAAI,EAAE,CAAC;QACP,KAAK,EAAE,CAAC;QACR,eAAe,EAAE,uBAAuB;QACxC,aAAa,EAAE,MAAM;QACrB,OAAO,EAAE,MAAM;QACf,UAAU,EAAE,QAAQ;QACpB,cAAc,EAAE,QAAQ;KACzB,CAAC;IAEF,MAAM,QAAQ,GAAG,CACf,8BACG,OAAO,CAAC,QAAQ,CAAC,GAAG,GAAG,CAAC,IAAI,CAC3B,cACE,KAAK,EAAE;oBACL,GAAG,oBAAoB;oBACvB,GAAG,EAAE,CAAC;oBACN,MAAM,EAAE,OAAO,CAAC,QAAQ,CAAC,GAAG;iBAC7B,GACD,CACH,EACA,OAAO,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,IAAI,CAAC,eAAe,IAAI,CAClD,cACE,KAAK,EAAE;oBACL,GAAG,oBAAoB;oBACvB,MAAM,EAAE,CAAC;oBACT,MAAM,EAAE,OAAO,CAAC,QAAQ,CAAC,MAAM;iBAChC,GACD,CACH,EACA,eAAe,IAAI,CAClB,cACE,KAAK,EAAE;oBACL,QAAQ,EAAE,UAAU;oBACpB,MAAM,EAAE,CAAC;oBACT,IAAI,EAAE,CAAC;oBACP,KAAK,EAAE,CAAC;oBACR,MAAM,EAAE,OAAO,CAAC,cAAc;oBAC9B,eAAe,EAAE,SAAS;oBAC1B,OAAO,EAAE,MAAM;oBACf,UAAU,EAAE,QAAQ;oBACpB,cAAc,EAAE,QAAQ;oBACxB,MAAM,EAAE,KAAK;iBACd,YAED,eACE,KAAK,EAAE;wBACL,KAAK,EAAE,SAAS;wBAChB,QAAQ,EAAE,EAAE;wBACZ,UAAU,EAAE,MAAM;wBAClB,aAAa,EAAE,CAAC;qBACjB,yBAGI,GACH,CACP,IACA,CACJ,CAAC;IAEF,IAAI,SAAS,EAAE,CAAC;QACd,OAAO,CACL,cACE,KAAK,EAAE;gBACL,KAAK,EAAE,OAAO;gBACd,MAAM,EAAE,OAAO;gBACf,OAAO,EAAE,MAAM;gBACf,UAAU,EAAE,QAAQ;gBACpB,cAAc,EAAE,QAAQ;gBACxB,eAAe,EAAE,SAAS;aAC3B,YAED,eACE,KAAK,EAAE;oBACL,KAAK,EAAE,OAAO,CAAC,QAAQ,CAAC,KAAK;oBAC7B,MAAM,EAAE,OAAO,CAAC,QAAQ,CAAC,MAAM;oBAC/B,QAAQ,EAAE,UAAU;oBACpB,QAAQ,EAAE,QAAQ;oBAClB,YAAY,EAAE,EAAE;oBAChB,MAAM,EAAE,gBAAgB;oBACxB,SAAS,EAAE,4BAA4B;iBACxC,aAEA,QAAQ,EACR,QAAQ,IACL,GACF,CACP,CAAC;IACJ,CAAC;IAED,8DAA8D;IAC9D,OAAO,CACL,8BACG,QAAQ,EACR,QAAQ,IACR,CACJ,CAAC;AACJ,CAAC"}
@@ -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' || !window.visualViewport) {
15
+ if (typeof window === 'undefined') {
16
16
  return { height: 0, isOpen: false };
17
17
  }
18
- const diff = window.innerHeight - window.visualViewport.height;
19
- const height = diff > 100 ? Math.round(diff) : 0;
20
- return { height, isOpen: height > 0 };
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":"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"}
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,CAMf;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"}
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"}
@@ -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":"AAiBA,MAAM,WAAW,GAAG,YAAY,CAAC;AAEjC;;;;;;;;GAQG;AACH,MAAM,CAAC,KAAK,UAAU,gBAAgB,CACpC,IAAU,EACV,KAAa;IAEb,MAAM,IAAI,CAAC,OAAO,EAAE,CAAC,UAAU,CAAC;QAC9B;YACE,IAAI,EAAE,WAAW;YACjB,KAAK,EAAE,KAAK;YACZ,MAAM,EAAE,WAAW;YACnB,IAAI,EAAE,GAAG;YACT,QAAQ,EAAE,IAAI;YACd,MAAM,EAAE,KAAK;YACb,QAAQ,EAAE,KAAK;SAChB;KACF,CAAC,CAAC;AACL,CAAC;AAED;;GAEG;AACH,MAAM,CAAC,KAAK,UAAU,aAAa,CACjC,OAAgB,EAChB,KAAa;IAEb,MAAM,IAAI,GAAG,MAAM,OAAO,CAAC,OAAO,EAAE,CAAC;IACrC,MAAM,gBAAgB,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;IACpC,OAAO,IAAI,CAAC;AACd,CAAC;AAWD,MAAM,CAAC,KAAK,UAAU,UAAU,CAC9B,IAAU,EACV,IAA6B;IAE7B,MAAM,IAAI,CAAC,eAAe,CAAC,WAAW,EAAE;QACtC,OAAO,EAAE,IAAI,EAAE,SAAS,IAAI,MAAM;KACnC,CAAC,CAAC;AACL,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,WAAW,CAAC,IAAU;IAC1C,OAAO,MAAM,IAAI,CAAC,QAAQ,CAAC,GAAG,EAAE;QAC9B,OAAO,MAAM,CAAC,iBAAiB,EAAE,EAAE,IAAI,IAAI,CAAC,GAAG,EAAE,CAAC;IACpD,CAAC,CAAC,CAAC;AACL,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,WAAW,CAC/B,IAAU,EACV,KAAa;IAEb,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAS,EAAE,EAAE;QAC/C,IAAI,CAAC,MAAM,CAAC,aAAa,EAAE,CAAC;YAC1B,MAAM,IAAI,KAAK,CACb,8DAA8D,CAC/D,CAAC;QACJ,CAAC;QACD,OAAO,MAAM,CAAC,aAAa,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC,CAAC;IAC5C,CAAC,EAAE,KAAK,CAAC,CAAC;IACV,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,aAAa,CACjC,IAAU,EACV,QAA6B,EAC7B,IAA2B;IAE3B,MAAM,IAAI,GAAG,MAAM,WAAW,CAAC,IAAI,CAAC,CAAC;IACrC,MAAM,QAAQ,EAAE,CAAC;IACjB,MAAM,IAAI,CAAC,cAAc,CAAC,IAAI,EAAE,QAAQ,IAAI,GAAG,CAAC,CAAC;IACjD,OAAO,MAAM,WAAW,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;AACvC,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,SAAS,CAC7B,IAAU,EACV,MAAqC;IAErC,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC;IAChC,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;QACzB,GAAG,CAAC,YAAY,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IACjC,CAAC;SAAM,CAAC;QACN,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC;IACtC,CAAC;IACD,MAAM,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,QAAQ,EAAE,CAAC,CAAC;IAChC,MAAM,UAAU,CAAC,IAAI,CAAC,CAAC;AACzB,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,gBAAgB,CACpC,IAAU,EACV,QAAgB;IAEhB,MAAM,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAS,EAAE,EAAE;QAChC,MAAM,CAAC,aAAa,CAClB,IAAI,WAAW,CAAC,iBAAiB,EAAE,EAAE,MAAM,EAAE,EAAE,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC,CAC9D,CAAC;IACJ,CAAC,EAAE,QAAQ,CAAC,CAAC;AACf,CAAC;AAWD,MAAM,CAAC,KAAK,UAAU,WAAW,CAC/B,IAAU,EACV,IAAyB;IAEzB,MAAM,IAAI,GAAG,MAAM,WAAW,CAAC,IAAI,CAAC,CAAC;IACrC,MAAM,MAAM,GAAG,MAAM,WAAW,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;IAE7C,MAAM,MAAM,GAAG,IAAI,EAAE,MAAM,IAAI,GAAG,CAAC;IACnC,MAAM,YAAY,GAAG,IAAI,EAAE,YAAY,IAAI,CAAC,CAAC;IAC7C,MAAM,aAAa,GAAG,IAAI,EAAE,aAAa,IAAI,KAAK,CAAC;IACnD,MAAM,uBAAuB,GAAG,IAAI,EAAE,uBAAuB,IAAI,KAAK,CAAC;IACvE,MAAM,qBAAqB,GAAG,IAAI,EAAE,qBAAqB,IAAI,KAAK,CAAC;IACnE,MAAM,gBAAgB,GAAG,IAAI,EAAE,gBAAgB,IAAI,CAAC,CAAC;IAErD,MAAM,QAAQ,GAAa,EAAE,CAAC;IAC9B,IAAI,MAAM,CAAC,GAAG,CAAC,KAAK,GAAG,MAAM,EAAE,CAAC;QAC9B,QAAQ,CAAC,IAAI,CACX,aAAa,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,MAAM,aAAa,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC,MAAM,GAAG,CAC7F,CAAC;IACJ,CAAC;IACD,IAAI,MAAM,CAAC,SAAS,CAAC,MAAM,GAAG,YAAY,EAAE,CAAC;QAC3C,QAAQ,CAAC,IAAI,CACX,aAAa,MAAM,CAAC,SAAS,CAAC,MAAM,MAAM,YAAY,SAAS,IAAI,CAAC,GAAG,CAAC,GAAG,MAAM,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,KAAK,CACpI,CAAC;IACJ,CAAC;IACD,IAAI,CAAC,aAAa,IAAI,MAAM,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACjD,QAAQ,CAAC,IAAI,CACX,aAAa,MAAM,CAAC,QAAQ;aACzB,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC;aACX,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,IAAI,CAAC,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAC;aAC7C,IAAI,CAAC,IAAI,CAAC,EAAE,CAChB,CAAC;IACJ,CAAC;IACD,IAAI,CAAC,uBAAuB,IAAI,MAAM,CAAC,kBAAkB,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACrE,QAAQ,CAAC,IAAI,CACX,uBAAuB,MAAM,CAAC,kBAAkB;aAC7C,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC;aACX,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,CAAC,QAAQ,IAAI,CAAC,CAAC,IAAI,GAAG,CAAC;aACtC,IAAI,CAAC,IAAI,CAAC,EAAE,CAChB,CAAC;IACJ,CAAC;IACD,IAAI,CAAC,qBAAqB,IAAI,MAAM,CAAC,QAAQ,CAAC,aAAa,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACvE,QAAQ,CAAC,IAAI,CACX,2BAA2B,MAAM,CAAC,QAAQ,CAAC,aAAa;aACrD,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC;aACX,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,CAAC,QAAQ,IAAI,CAAC,CAAC,SAAS,KAAK,CAAC;aAC7C,IAAI,CAAC,IAAI,CAAC,EAAE,CAChB,CAAC;IACJ,CAAC;IACD,MAAM,aAAa,GAAG,MAAM,CAAC,UAAU,CAAC,MAAM,CAC5C,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,aAAa,EAC7B,CAAC,CACF,CAAC;IACF,IAAI,aAAa,GAAG,gBAAgB,EAAE,CAAC;QACrC,QAAQ,CAAC,IAAI,CACX,kBAAkB,aAAa,MAAM,gBAAgB,WAAW,MAAM,CAAC,UAAU,CAAC,MAAM,eAAe,CACxG,CAAC;IACJ,CAAC;IAED,IAAI,QAAQ,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACxB,MAAM,IAAI,KAAK,CAAC,4BAA4B,QAAQ,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC;IACzE,CAAC;AACH,CAAC;AAED,OAAO,EAAE,qBAAqB,EAAE,MAAM,kBAAkB,CAAC;AAEzD,OAAO,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAEvE,OAAO,EAAE,WAAW,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC"}
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.874",
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": [
@@ -1,2 +1,2 @@
1
1
  // Auto-generated by prebuild — do not edit manually
2
- export const CLI_VERSION = "0.1.874";
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
- const SIMULATOR_PROFILES = {
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
- } 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' || !window.visualViewport) {
23
+ if (typeof window === 'undefined') {
24
24
  return { height: 0, isOpen: false };
25
25
  }
26
- const diff = window.innerHeight - window.visualViewport.height;
27
- const height = diff > 100 ? Math.round(diff) : 0;
28
- return { height, isOpen: height > 0 };
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(
@@ -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), set
178
- > cookie `auth_token=<token>` for `<BASE_URL>`, then `goto` the app and wait for the
179
- > hydration selector `<WAIT_SEL>` (see Screenshot recipe), never `networkidle`.
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 array of your findings (`{type, severity, feature, message,
195
- > evidence}`) plus a one-line verdict: does this tool work for you? Return the findings as
196
- > your final message they are data for aggregation, not a human report.
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. **Rate the product 1–5 stars** (see rubric) with a one-line justification. Lead the
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
- 6. Hand off the ranked list **as content** for fixing — either fix the items directly in
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**, and the **persona verdicts**. A broken feature
266
- caps the score polish can't buy back a blocker.
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