@limrun/ui 0.9.0-rc.7 → 0.9.0-rc.8
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/components/inspect-overlay.d.ts +0 -1
- package/dist/components/remote-control.d.ts +2 -13
- package/dist/core/ax-tree.d.ts +0 -2
- package/dist/device-install/index.cjs +1 -1
- package/dist/device-install/index.js +2 -2
- package/dist/device-install/react.cjs +1 -1
- package/dist/device-install/react.js +1 -1
- package/dist/device-install-dialog-DGn2ZdBB.js +2 -0
- package/dist/{device-install-dialog-CnyDWf0q.mjs → device-install-dialog-DgWsZF6o.mjs} +54 -73
- package/dist/index.cjs +1 -1
- package/dist/index.css +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.js +830 -871
- package/dist/use-device-install-ByUSmeYz.js +31 -0
- package/dist/{use-device-install-j1Gekpl4.mjs → use-device-install-Y42p84we.mjs} +1096 -1095
- package/package.json +1 -1
- package/src/components/device-install/device-install-dialog.tsx +16 -34
- package/src/components/inspect-overlay.css +0 -6
- package/src/components/inspect-overlay.tsx +15 -46
- package/src/components/remote-control.tsx +2 -16
- package/src/core/ax-tree.test.ts +0 -124
- package/src/core/ax-tree.ts +0 -107
- package/src/hooks/use-device-install.ts +35 -26
- package/src/index.ts +3 -4
- package/dist/device-install-dialog-86RDdoK9.js +0 -2
- package/dist/use-device-install-CbGVvwPp.js +0 -31
package/package.json
CHANGED
|
@@ -12,17 +12,17 @@ const steps: Array<{ id: DeviceInstallStep; title: string; description: string }
|
|
|
12
12
|
{
|
|
13
13
|
id: 'signing',
|
|
14
14
|
title: 'Prepare signing',
|
|
15
|
-
description: 'Choose Apple ID login or upload certificates
|
|
15
|
+
description: 'Choose Apple ID login or upload certificates for a registered developer device.',
|
|
16
16
|
},
|
|
17
17
|
{
|
|
18
|
-
id: '
|
|
19
|
-
title: '
|
|
20
|
-
description: '
|
|
18
|
+
id: 'build',
|
|
19
|
+
title: 'Build for device',
|
|
20
|
+
description: 'Start the signed iPhone build before connecting over USB.',
|
|
21
21
|
},
|
|
22
22
|
{
|
|
23
|
-
id: '
|
|
24
|
-
title: '
|
|
25
|
-
description: '
|
|
23
|
+
id: 'connect',
|
|
24
|
+
title: 'Connect and pair',
|
|
25
|
+
description: 'After the build succeeds, connect the iPhone with WebUSB and pair this browser.',
|
|
26
26
|
},
|
|
27
27
|
{
|
|
28
28
|
id: 'install',
|
|
@@ -78,7 +78,7 @@ export function DeviceInstallDialog({
|
|
|
78
78
|
<header className="lr-device-install__header">
|
|
79
79
|
<div>
|
|
80
80
|
<h2 id={dialogTitleId}>Install to a real iPhone</h2>
|
|
81
|
-
<p>Prepare signing,
|
|
81
|
+
<p>Prepare signing, build for the registered device, connect and pair, then install from this browser.</p>
|
|
82
82
|
</div>
|
|
83
83
|
<button type="button" className="lr-device-install__icon-button" onClick={() => setOpen(false)}>
|
|
84
84
|
Close
|
|
@@ -110,7 +110,7 @@ export function DeviceInstallDialog({
|
|
|
110
110
|
onClick={() => setSigningSection('apple-id')}
|
|
111
111
|
>
|
|
112
112
|
<strong>Apple ID login</strong>
|
|
113
|
-
<span>Sign in, choose team, bundle ID, devices, then generate signing assets.</span>
|
|
113
|
+
<span>Sign in, choose team, bundle ID, and registered devices, then generate signing assets.</span>
|
|
114
114
|
</button>
|
|
115
115
|
<button
|
|
116
116
|
type="button"
|
|
@@ -311,7 +311,7 @@ export function DeviceInstallDialog({
|
|
|
311
311
|
</label>
|
|
312
312
|
</div>
|
|
313
313
|
<p className="lr-device-install__hint">
|
|
314
|
-
The provisioning profile will be checked against the connected iPhone before
|
|
314
|
+
The provisioning profile will be checked against the connected iPhone before installation.
|
|
315
315
|
</p>
|
|
316
316
|
</div>
|
|
317
317
|
)}
|
|
@@ -325,8 +325,8 @@ export function DeviceInstallDialog({
|
|
|
325
325
|
{step.id === 'connect' && (
|
|
326
326
|
<div className="lr-device-install__step-body">
|
|
327
327
|
<p>
|
|
328
|
-
WebUSB works in Chromium browsers on secure origins.
|
|
329
|
-
browser permission prompt, then pair this browser.
|
|
328
|
+
WebUSB works in Chromium browsers on secure origins. Once the build succeeds, connect the iPhone
|
|
329
|
+
over USB, approve the browser permission prompt, then pair this browser.
|
|
330
330
|
</p>
|
|
331
331
|
<div className="lr-device-install__actions">
|
|
332
332
|
<button
|
|
@@ -365,8 +365,8 @@ export function DeviceInstallDialog({
|
|
|
365
365
|
)}
|
|
366
366
|
<p>
|
|
367
367
|
{deviceInstall.hasPairRecord
|
|
368
|
-
? 'Pair record is stored locally. Continue to
|
|
369
|
-
: 'Pair this browser once before
|
|
368
|
+
? 'Pair record is stored locally. Continue to installation.'
|
|
369
|
+
: 'Pair this browser once before installing.'}
|
|
370
370
|
</p>
|
|
371
371
|
</div>
|
|
372
372
|
)}
|
|
@@ -374,27 +374,9 @@ export function DeviceInstallDialog({
|
|
|
374
374
|
{step.id === 'build' && (
|
|
375
375
|
<div className="lr-device-install__step-body">
|
|
376
376
|
<div className="lr-device-install__checklist">
|
|
377
|
-
<StatusLine label="Signing assets" ready={deviceInstall.hasSigningInputs}
|
|
378
|
-
<StatusLine label="
|
|
379
|
-
<StatusLine label="Pair record" ready={deviceInstall.hasPairRecord} />
|
|
380
|
-
<StatusLine
|
|
381
|
-
label="Profile includes connected device"
|
|
382
|
-
ready={deviceInstall.connectedDeviceInProfile}
|
|
383
|
-
pendingText="Checked when the build starts"
|
|
384
|
-
/>
|
|
377
|
+
<StatusLine label="Signing assets" ready={deviceInstall.hasSigningInputs} />
|
|
378
|
+
<StatusLine label="Device build" ready={deviceInstall.buildStatus === 'succeeded' ? true : undefined} pendingText="Not started" />
|
|
385
379
|
</div>
|
|
386
|
-
{deviceInstall.device &&
|
|
387
|
-
deviceInstall.appleTeams.length > 0 &&
|
|
388
|
-
!deviceInstall.connectedAppleDeviceRegistered && (
|
|
389
|
-
<button
|
|
390
|
-
type="button"
|
|
391
|
-
className="lr-device-install__secondary"
|
|
392
|
-
disabled={disabled || !!deviceInstall.busyAction}
|
|
393
|
-
onClick={() => void deviceInstall.registerConnectedAppleDevice()}
|
|
394
|
-
>
|
|
395
|
-
Register connected iPhone
|
|
396
|
-
</button>
|
|
397
|
-
)}
|
|
398
380
|
<button
|
|
399
381
|
type="button"
|
|
400
382
|
className="lr-device-install__primary"
|
|
@@ -9,11 +9,6 @@
|
|
|
9
9
|
/* When click-to-select is enabled, the container also captures clicks
|
|
10
10
|
that fall outside any box so we can clear selection. */
|
|
11
11
|
pointer-events: auto;
|
|
12
|
-
/* Match Chrome DevTools' crosshair cursor while inspect mode is active.
|
|
13
|
-
The merged `.rc-inspect-overlay-select .rc-inspect-box` rule below
|
|
14
|
-
restates the cursor on each inner box, since `.rc-inspect-box` sets
|
|
15
|
-
its own `cursor: pointer`. */
|
|
16
|
-
cursor: crosshair;
|
|
17
12
|
}
|
|
18
13
|
|
|
19
14
|
.rc-inspect-box {
|
|
@@ -37,7 +32,6 @@
|
|
|
37
32
|
|
|
38
33
|
.rc-inspect-overlay-select .rc-inspect-box {
|
|
39
34
|
pointer-events: auto;
|
|
40
|
-
cursor: crosshair;
|
|
41
35
|
}
|
|
42
36
|
|
|
43
37
|
.rc-inspect-box-disabled {
|
|
@@ -7,9 +7,8 @@ import {
|
|
|
7
7
|
AxElement,
|
|
8
8
|
AxPlatform,
|
|
9
9
|
AxSnapshot,
|
|
10
|
-
axCliTapCommand,
|
|
11
10
|
axElementRoleLabel,
|
|
12
|
-
|
|
11
|
+
axElementSelectorExpression,
|
|
13
12
|
axElementSummary,
|
|
14
13
|
axElementsEqual,
|
|
15
14
|
clampAxFrameForScreen,
|
|
@@ -38,10 +37,6 @@ export interface InspectOverlayProps {
|
|
|
38
37
|
highlightedId: string | null;
|
|
39
38
|
selectedId: string | null;
|
|
40
39
|
mode: InspectMode;
|
|
41
|
-
// Optional instance id used to render the "Copy command" CLI snippet
|
|
42
|
-
// (e.g. `lim ios tap-element --ax-label 'Sign in' --id <instanceId>`)
|
|
43
|
-
// in the info card. When omitted the Copy command button is hidden.
|
|
44
|
-
instanceId?: string;
|
|
45
40
|
// Current pointer position in viewport coordinates (clientX/Y). Drives the
|
|
46
41
|
// cursor-anchored preview card while hovering. null when the pointer is
|
|
47
42
|
// outside the device.
|
|
@@ -122,10 +117,6 @@ const InspectBox = memo(
|
|
|
122
117
|
if (!selectable) return;
|
|
123
118
|
e.preventDefault();
|
|
124
119
|
e.stopPropagation();
|
|
125
|
-
// Chrome DevTools parity: clicking the already-selected element
|
|
126
|
-
// is a no-op. Re-firing the selection change callback would
|
|
127
|
-
// needlessly reset the card anchor and consumer-side state.
|
|
128
|
-
if (selected) return;
|
|
129
120
|
onClick(element, { x: e.clientX, y: e.clientY });
|
|
130
121
|
}}
|
|
131
122
|
style={{
|
|
@@ -160,9 +151,6 @@ interface InfoCardProps {
|
|
|
160
151
|
anchor: { x: number; y: number };
|
|
161
152
|
cursorAnchored: boolean;
|
|
162
153
|
showActions: boolean;
|
|
163
|
-
// Used to render the "Copy command" CLI snippet. When absent the button
|
|
164
|
-
// is hidden.
|
|
165
|
-
instanceId?: string;
|
|
166
154
|
// Receives the element AND the viewport-space coordinate to tap at
|
|
167
155
|
// (the frozen click position). Tapping at the click point — rather than
|
|
168
156
|
// the element's frame center — preserves the user's aim when the
|
|
@@ -192,7 +180,6 @@ const InfoCard = memo(function InfoCard({
|
|
|
192
180
|
anchor,
|
|
193
181
|
cursorAnchored,
|
|
194
182
|
showActions,
|
|
195
|
-
instanceId,
|
|
196
183
|
onTap,
|
|
197
184
|
}: InfoCardProps) {
|
|
198
185
|
const [copied, setCopied] = useState<string | null>(null);
|
|
@@ -248,26 +235,11 @@ const InfoCard = memo(function InfoCard({
|
|
|
248
235
|
return { left: `${left}px`, top: `${top}px`, transform };
|
|
249
236
|
}, [anchor.x, anchor.y]);
|
|
250
237
|
|
|
251
|
-
|
|
252
|
-
// accepted by the CLI / instance API). Every available selector field is
|
|
253
|
-
// included so the consumer can disambiguate (e.g. `AXLabel + type`).
|
|
254
|
-
const selectorObject = useMemo(() => axElementSelectorObject(element, platform), [element, platform]);
|
|
255
|
-
const selectorJson = useMemo(
|
|
256
|
-
() => (selectorObject ? JSON.stringify(selectorObject, null, 2) : null),
|
|
257
|
-
[selectorObject],
|
|
258
|
-
);
|
|
259
|
-
// "Copy command" emits a selector-based `lim … tap-element …` invocation
|
|
260
|
-
// that targets this exact element on this exact instance, robust to
|
|
261
|
-
// layout changes that would invalidate a coordinate-based tap.
|
|
262
|
-
const cliCommand = useMemo(
|
|
263
|
-
() => (instanceId ? axCliTapCommand(element, platform, instanceId) : null),
|
|
264
|
-
[element, platform, instanceId],
|
|
265
|
-
);
|
|
266
|
-
|
|
238
|
+
const selectorExpr = useMemo(() => axElementSelectorExpression(element, platform), [element, platform]);
|
|
267
239
|
const primaryIdField = platform === 'ios' ? element.selectors.AXUniqueId : element.selectors.resourceId;
|
|
268
240
|
const primaryIdLabel = platform === 'ios' ? 'AXUniqueId' : 'resourceId';
|
|
269
241
|
|
|
270
|
-
const handleCopy = useCallback(async (text: string |
|
|
242
|
+
const handleCopy = useCallback(async (text: string | undefined, key: string) => {
|
|
271
243
|
if (!text) return;
|
|
272
244
|
const ok = await copyToClipboard(text);
|
|
273
245
|
if (ok) setCopied(key);
|
|
@@ -336,22 +308,21 @@ const InfoCard = memo(function InfoCard({
|
|
|
336
308
|
<button
|
|
337
309
|
type="button"
|
|
338
310
|
className={clsx('rc-inspect-card-btn', copied === 'selector' && 'rc-inspect-card-btn-copied')}
|
|
339
|
-
disabled={!
|
|
340
|
-
title={
|
|
341
|
-
onClick={() => handleCopy(
|
|
311
|
+
disabled={!selectorExpr}
|
|
312
|
+
title={selectorExpr ?? 'No usable selector for this element'}
|
|
313
|
+
onClick={() => handleCopy(selectorExpr ?? undefined, 'selector')}
|
|
342
314
|
>
|
|
343
315
|
{copied === 'selector' ? 'Copied!' : 'Copy selector'}
|
|
344
316
|
</button>
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
)}
|
|
317
|
+
<button
|
|
318
|
+
type="button"
|
|
319
|
+
className={clsx('rc-inspect-card-btn', copied === 'id' && 'rc-inspect-card-btn-copied')}
|
|
320
|
+
disabled={!primaryIdField}
|
|
321
|
+
title={primaryIdField ?? `No ${primaryIdLabel}`}
|
|
322
|
+
onClick={() => handleCopy(primaryIdField, 'id')}
|
|
323
|
+
>
|
|
324
|
+
{copied === 'id' ? 'Copied!' : `Copy ${primaryIdLabel}`}
|
|
325
|
+
</button>
|
|
355
326
|
</div>
|
|
356
327
|
)}
|
|
357
328
|
</div>,
|
|
@@ -369,7 +340,6 @@ export const InspectOverlay = memo(function InspectOverlay({
|
|
|
369
340
|
highlightedId,
|
|
370
341
|
selectedId,
|
|
371
342
|
mode,
|
|
372
|
-
instanceId,
|
|
373
343
|
cursorPosition,
|
|
374
344
|
frozenCursorPosition,
|
|
375
345
|
onSelectChange,
|
|
@@ -459,7 +429,6 @@ export const InspectOverlay = memo(function InspectOverlay({
|
|
|
459
429
|
anchor={anchor}
|
|
460
430
|
cursorAnchored={cursorAnchored}
|
|
461
431
|
showActions={showActions}
|
|
462
|
-
instanceId={instanceId}
|
|
463
432
|
onTap={onTapElement}
|
|
464
433
|
/>
|
|
465
434
|
)}
|
|
@@ -65,10 +65,8 @@ interface RemoteControlProps {
|
|
|
65
65
|
* video stream.
|
|
66
66
|
*
|
|
67
67
|
* - `true` — Select mode. Boxes are clickable, click pins a selection
|
|
68
|
-
* with action buttons (Tap / Copy selector / Copy
|
|
69
|
-
*
|
|
70
|
-
* info card hangs off the pointer. Device input is blocked while in
|
|
71
|
-
* this mode.
|
|
68
|
+
* with action buttons (Tap / Copy selector / Copy id), ESC clears.
|
|
69
|
+
* Device input is blocked while in this mode.
|
|
72
70
|
* - `'hover-only'` — Boxes follow the cursor as a visual preview. Device
|
|
73
71
|
* input still passes through, so you can drive the simulator while
|
|
74
72
|
* inspecting.
|
|
@@ -76,16 +74,6 @@ interface RemoteControlProps {
|
|
|
76
74
|
*/
|
|
77
75
|
inspectMode?: boolean | 'hover-only';
|
|
78
76
|
|
|
79
|
-
/**
|
|
80
|
-
* Optional instance id used to render the "Copy command" button in the
|
|
81
|
-
* inspect-mode info card. When provided, the button copies a CLI
|
|
82
|
-
* invocation like
|
|
83
|
-
* `lim ios tap-element --ax-label 'Sign in' --type Button --id <instanceId>`
|
|
84
|
-
* (or its Android equivalent) that targets this exact element on this
|
|
85
|
-
* exact instance. When omitted the button is hidden.
|
|
86
|
-
*/
|
|
87
|
-
instanceId?: string;
|
|
88
|
-
|
|
89
77
|
/**
|
|
90
78
|
* Fires whenever a fresh accessibility snapshot is delivered.
|
|
91
79
|
*
|
|
@@ -345,7 +333,6 @@ export const RemoteControl = forwardRef<RemoteControlHandle, RemoteControlProps>
|
|
|
345
333
|
showFrame = true,
|
|
346
334
|
autoReconnect = false,
|
|
347
335
|
inspectMode,
|
|
348
|
-
instanceId,
|
|
349
336
|
onAxSnapshotChange,
|
|
350
337
|
onInspectSelectionChange,
|
|
351
338
|
onAxStatusChange,
|
|
@@ -2542,7 +2529,6 @@ export const RemoteControl = forwardRef<RemoteControlHandle, RemoteControlProps>
|
|
|
2542
2529
|
highlightedId={axHighlightedId}
|
|
2543
2530
|
selectedId={axSelectedId}
|
|
2544
2531
|
mode={inspectModeResolved}
|
|
2545
|
-
instanceId={instanceId}
|
|
2546
2532
|
cursorPosition={axCursorPosition}
|
|
2547
2533
|
frozenCursorPosition={axFrozenCursorPosition}
|
|
2548
2534
|
onSelectChange={(element, clickPosition) => {
|
package/src/core/ax-tree.test.ts
CHANGED
|
@@ -11,11 +11,9 @@ import {
|
|
|
11
11
|
AxElement,
|
|
12
12
|
AxSnapshot,
|
|
13
13
|
AX_UNAVAILABLE_ERROR,
|
|
14
|
-
axCliTapCommand,
|
|
15
14
|
axElementAtPoint,
|
|
16
15
|
axElementRoleLabel,
|
|
17
16
|
axElementSelectorExpression,
|
|
18
|
-
axElementSelectorObject,
|
|
19
17
|
axElementSummary,
|
|
20
18
|
axElementsEqual,
|
|
21
19
|
axSnapshotsEqual,
|
|
@@ -485,128 +483,6 @@ describe('axElementSelectorExpression', () => {
|
|
|
485
483
|
});
|
|
486
484
|
});
|
|
487
485
|
|
|
488
|
-
describe('axElementSelectorObject', () => {
|
|
489
|
-
const mk = (sel: Partial<AxElement['selectors']>, value = ''): AxElement => ({
|
|
490
|
-
id: '0',
|
|
491
|
-
path: '0',
|
|
492
|
-
label: '',
|
|
493
|
-
value,
|
|
494
|
-
role: '',
|
|
495
|
-
type: '',
|
|
496
|
-
enabled: true,
|
|
497
|
-
focused: false,
|
|
498
|
-
frame: { x: 0, y: 0, width: 1, height: 1 },
|
|
499
|
-
selectors: sel,
|
|
500
|
-
raw: {},
|
|
501
|
-
});
|
|
502
|
-
|
|
503
|
-
test('iOS: includes every available selector field, mapping className to type', () => {
|
|
504
|
-
expect(
|
|
505
|
-
axElementSelectorObject(mk({ AXUniqueId: 'signin', AXLabel: 'Sign in', className: 'Button' }), 'ios'),
|
|
506
|
-
).toEqual({ AXUniqueId: 'signin', AXLabel: 'Sign in', type: 'Button' });
|
|
507
|
-
});
|
|
508
|
-
|
|
509
|
-
test('iOS: falls back to AXLabel + type when AXUniqueId is missing', () => {
|
|
510
|
-
expect(axElementSelectorObject(mk({ AXLabel: 'OK', className: 'Button' }), 'ios')).toEqual({
|
|
511
|
-
AXLabel: 'OK',
|
|
512
|
-
type: 'Button',
|
|
513
|
-
});
|
|
514
|
-
});
|
|
515
|
-
|
|
516
|
-
test('iOS: returns null when only a generic type/className is available', () => {
|
|
517
|
-
expect(axElementSelectorObject(mk({ className: 'Button' }), 'ios')).toBeNull();
|
|
518
|
-
expect(axElementSelectorObject(mk({}), 'ios')).toBeNull();
|
|
519
|
-
});
|
|
520
|
-
|
|
521
|
-
test('Android: includes every available selector field', () => {
|
|
522
|
-
expect(
|
|
523
|
-
axElementSelectorObject(
|
|
524
|
-
mk({
|
|
525
|
-
resourceId: 'com.example:id/submit',
|
|
526
|
-
contentDesc: 'Submit',
|
|
527
|
-
text: 'Submit',
|
|
528
|
-
className: 'android.widget.Button',
|
|
529
|
-
}),
|
|
530
|
-
'android',
|
|
531
|
-
),
|
|
532
|
-
).toEqual({
|
|
533
|
-
resourceId: 'com.example:id/submit',
|
|
534
|
-
contentDesc: 'Submit',
|
|
535
|
-
text: 'Submit',
|
|
536
|
-
className: 'android.widget.Button',
|
|
537
|
-
});
|
|
538
|
-
});
|
|
539
|
-
|
|
540
|
-
test('Android: returns null when only a className is present', () => {
|
|
541
|
-
expect(axElementSelectorObject(mk({ className: 'android.widget.View' }), 'android')).toBeNull();
|
|
542
|
-
expect(axElementSelectorObject(mk({}), 'android')).toBeNull();
|
|
543
|
-
});
|
|
544
|
-
});
|
|
545
|
-
|
|
546
|
-
describe('axCliTapCommand', () => {
|
|
547
|
-
const mk = (sel: Partial<AxElement['selectors']>): AxElement => ({
|
|
548
|
-
id: '0',
|
|
549
|
-
path: '0',
|
|
550
|
-
label: '',
|
|
551
|
-
value: '',
|
|
552
|
-
role: '',
|
|
553
|
-
type: '',
|
|
554
|
-
enabled: true,
|
|
555
|
-
focused: false,
|
|
556
|
-
frame: { x: 0, y: 0, width: 1, height: 1 },
|
|
557
|
-
selectors: sel,
|
|
558
|
-
raw: {},
|
|
559
|
-
});
|
|
560
|
-
|
|
561
|
-
test('iOS: emits tap-element with every available selector flag', () => {
|
|
562
|
-
expect(
|
|
563
|
-
axCliTapCommand(
|
|
564
|
-
mk({ AXUniqueId: 'signin', AXLabel: 'Sign in', className: 'Button' }),
|
|
565
|
-
'ios',
|
|
566
|
-
'ios_abc',
|
|
567
|
-
),
|
|
568
|
-
).toBe(`lim ios tap-element --ax-unique-id signin --ax-label 'Sign in' --type Button --id ios_abc`);
|
|
569
|
-
});
|
|
570
|
-
|
|
571
|
-
test('iOS: AXLabel + type fallback when AXUniqueId is missing', () => {
|
|
572
|
-
expect(axCliTapCommand(mk({ AXLabel: 'OK', className: 'Button' }), 'ios', 'ios_abc')).toBe(
|
|
573
|
-
`lim ios tap-element --ax-label OK --type Button --id ios_abc`,
|
|
574
|
-
);
|
|
575
|
-
});
|
|
576
|
-
|
|
577
|
-
test('Android: emits tap-element with resource-id / content-desc / text / class-name', () => {
|
|
578
|
-
expect(
|
|
579
|
-
axCliTapCommand(
|
|
580
|
-
mk({
|
|
581
|
-
resourceId: 'com.example:id/submit',
|
|
582
|
-
contentDesc: 'Submit button',
|
|
583
|
-
text: 'Submit',
|
|
584
|
-
className: 'android.widget.Button',
|
|
585
|
-
}),
|
|
586
|
-
'android',
|
|
587
|
-
'and_1',
|
|
588
|
-
),
|
|
589
|
-
).toBe(
|
|
590
|
-
`lim android tap-element --resource-id com.example:id/submit --content-desc 'Submit button' --text Submit --class-name android.widget.Button --id and_1`,
|
|
591
|
-
);
|
|
592
|
-
});
|
|
593
|
-
|
|
594
|
-
test('shell-quotes values containing apostrophes', () => {
|
|
595
|
-
expect(axCliTapCommand(mk({ AXLabel: `Bob's button` }), 'ios', 'ios_abc')).toBe(
|
|
596
|
-
`lim ios tap-element --ax-label 'Bob'\\''s button' --id ios_abc`,
|
|
597
|
-
);
|
|
598
|
-
});
|
|
599
|
-
|
|
600
|
-
test('returns null when no specific selector is available', () => {
|
|
601
|
-
expect(axCliTapCommand(mk({ className: 'Button' }), 'ios', 'ios_abc')).toBeNull();
|
|
602
|
-
expect(axCliTapCommand(mk({}), 'android', 'and_1')).toBeNull();
|
|
603
|
-
});
|
|
604
|
-
|
|
605
|
-
test('returns null when instanceId is empty', () => {
|
|
606
|
-
expect(axCliTapCommand(mk({ AXUniqueId: 'signin' }), 'ios', '')).toBeNull();
|
|
607
|
-
});
|
|
608
|
-
});
|
|
609
|
-
|
|
610
486
|
describe('AX_UNAVAILABLE_ERROR', () => {
|
|
611
487
|
test('is a non-empty string customers can compare against', () => {
|
|
612
488
|
expect(typeof AX_UNAVAILABLE_ERROR).toBe('string');
|
package/src/core/ax-tree.ts
CHANGED
|
@@ -382,113 +382,6 @@ export function axElementSelectorExpression(el: AxElement, platform: AxPlatform)
|
|
|
382
382
|
return null;
|
|
383
383
|
}
|
|
384
384
|
|
|
385
|
-
// Returns a plain selector object for the element using the same key names the
|
|
386
|
-
// CLI / instance API expect (`AXUniqueId`, `AXLabel`, `type` on iOS;
|
|
387
|
-
// `resourceId`, `contentDesc`, `text`, `className` on Android). Includes every
|
|
388
|
-
// CLI-supportable selector field present on the element so the consumer can
|
|
389
|
-
// disambiguate (e.g. `{AXLabel:"Submit", type:"Button"}`).
|
|
390
|
-
//
|
|
391
|
-
// Returns null when the element has no anchor field specific enough to drive
|
|
392
|
-
// a tap — a lone `type` / `className` is intentionally treated as
|
|
393
|
-
// non-actionable to avoid copying a near-useless selector. This mirrors the
|
|
394
|
-
// precedence used by `axElementSelectorExpression`.
|
|
395
|
-
export function axElementSelectorObject(el: AxElement, platform: AxPlatform): Record<string, string> | null {
|
|
396
|
-
const out: Record<string, string> = {};
|
|
397
|
-
let hasSpecific = false;
|
|
398
|
-
if (platform === 'ios') {
|
|
399
|
-
if (el.selectors.AXUniqueId) {
|
|
400
|
-
out.AXUniqueId = el.selectors.AXUniqueId;
|
|
401
|
-
hasSpecific = true;
|
|
402
|
-
}
|
|
403
|
-
if (el.selectors.AXLabel) {
|
|
404
|
-
out.AXLabel = el.selectors.AXLabel;
|
|
405
|
-
hasSpecific = true;
|
|
406
|
-
}
|
|
407
|
-
// `selectors.className` carries the iOS element `type` (e.g. "Button").
|
|
408
|
-
// The CLI / API expose it under the key `type`, so rename here.
|
|
409
|
-
if (el.selectors.className) {
|
|
410
|
-
out.type = el.selectors.className;
|
|
411
|
-
}
|
|
412
|
-
} else {
|
|
413
|
-
if (el.selectors.resourceId) {
|
|
414
|
-
out.resourceId = el.selectors.resourceId;
|
|
415
|
-
hasSpecific = true;
|
|
416
|
-
}
|
|
417
|
-
if (el.selectors.contentDesc) {
|
|
418
|
-
out.contentDesc = el.selectors.contentDesc;
|
|
419
|
-
hasSpecific = true;
|
|
420
|
-
}
|
|
421
|
-
if (el.selectors.text) {
|
|
422
|
-
out.text = el.selectors.text;
|
|
423
|
-
hasSpecific = true;
|
|
424
|
-
}
|
|
425
|
-
if (el.selectors.className) {
|
|
426
|
-
out.className = el.selectors.className;
|
|
427
|
-
}
|
|
428
|
-
}
|
|
429
|
-
return hasSpecific ? out : null;
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
// POSIX-safe single quoting for shell command rendering. Bare-prints "safe"
|
|
433
|
-
// tokens (alphanumerics, slashes, dots, etc.) so common identifiers like
|
|
434
|
-
// UUIDs and Android resource IDs (`com.example:id/submit`) stay readable;
|
|
435
|
-
// everything else gets single-quoted with internal apostrophes escaped as
|
|
436
|
-
// `'\''`.
|
|
437
|
-
const shellQuote = (value: string): string => {
|
|
438
|
-
if (value === '') return "''";
|
|
439
|
-
if (/^[A-Za-z0-9_./\-:@%+=,]+$/.test(value)) return value;
|
|
440
|
-
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
441
|
-
};
|
|
442
|
-
|
|
443
|
-
// Maps `axElementSelectorObject` keys to their `lim … tap-element` CLI
|
|
444
|
-
// flags. Scoped to the keys emitted by `axElementSelectorObject` — any
|
|
445
|
-
// future selector field added there must also be added here.
|
|
446
|
-
const CLI_FLAG_BY_SELECTOR_KEY: Record<AxPlatform, Record<string, string>> = {
|
|
447
|
-
ios: {
|
|
448
|
-
AXUniqueId: '--ax-unique-id',
|
|
449
|
-
AXLabel: '--ax-label',
|
|
450
|
-
type: '--type',
|
|
451
|
-
},
|
|
452
|
-
android: {
|
|
453
|
-
resourceId: '--resource-id',
|
|
454
|
-
contentDesc: '--content-desc',
|
|
455
|
-
text: '--text',
|
|
456
|
-
className: '--class-name',
|
|
457
|
-
},
|
|
458
|
-
};
|
|
459
|
-
|
|
460
|
-
// Renders the `lim` CLI command that taps the element via selector flags
|
|
461
|
-
// rather than coordinates. Includes every CLI-supportable selector field
|
|
462
|
-
// present on the element (same precedence as `axElementSelectorObject`) so
|
|
463
|
-
// matches are unambiguous and resilient to layout changes.
|
|
464
|
-
//
|
|
465
|
-
// Example outputs:
|
|
466
|
-
// lim ios tap-element --ax-unique-id signin --id <id>
|
|
467
|
-
// lim ios tap-element --ax-label 'Sign in' --type Button --id <id>
|
|
468
|
-
// lim android tap-element --resource-id com.example:id/submit --id <id>
|
|
469
|
-
//
|
|
470
|
-
// Returns null when:
|
|
471
|
-
// - `instanceId` is empty, or
|
|
472
|
-
// - the element has no field specific enough to drive a selector tap
|
|
473
|
-
// (matches `axElementSelectorObject` returning null — e.g. only a
|
|
474
|
-
// generic className/type is available).
|
|
475
|
-
export function axCliTapCommand(el: AxElement, platform: AxPlatform, instanceId: string): string | null {
|
|
476
|
-
if (!instanceId) return null;
|
|
477
|
-
const selectors = axElementSelectorObject(el, platform);
|
|
478
|
-
if (!selectors) return null;
|
|
479
|
-
|
|
480
|
-
const flagMap = CLI_FLAG_BY_SELECTOR_KEY[platform];
|
|
481
|
-
const parts: string[] = ['lim', platform, 'tap-element'];
|
|
482
|
-
for (const [key, flag] of Object.entries(flagMap)) {
|
|
483
|
-
const value = selectors[key];
|
|
484
|
-
if (value) {
|
|
485
|
-
parts.push(flag, shellQuote(value));
|
|
486
|
-
}
|
|
487
|
-
}
|
|
488
|
-
parts.push('--id', shellQuote(instanceId));
|
|
489
|
-
return parts.join(' ');
|
|
490
|
-
}
|
|
491
|
-
|
|
492
385
|
// Cleans up internal-looking role tokens. iOS's `role_description` can be
|
|
493
386
|
// raw strings like "AXGenericElement" or empty when AppKit doesn't have a
|
|
494
387
|
// description for the role. Android sets role to the className which is
|