@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@limrun/ui",
3
- "version": "0.9.0-rc.7",
3
+ "version": "0.9.0-rc.8",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -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, then confirm the target developer device.',
15
+ description: 'Choose Apple ID login or upload certificates for a registered developer device.',
16
16
  },
17
17
  {
18
- id: 'connect',
19
- title: 'Connect and pair',
20
- description: 'Connect the iPhone with WebUSB, then pair this browser so installs can use the device.',
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: 'build',
24
- title: 'Check and build',
25
- description: 'Verify the device and provisioning profile are ready, then start the signed build.',
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, connect and pair the device, build, then install from this browser.</p>
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 the build starts.
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. Connect the iPhone over USB, approve the
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 the build check.'
369
- : 'Pair this browser once before building and installing.'}
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} pendingText="Ready to verify" />
378
- <StatusLine label="USB device" ready={!!deviceInstall.device} />
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
- axElementSelectorObject,
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
- // "Copy selector" returns the raw selector hash as JSON (the same shape
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 | null, key: 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={!selectorJson}
340
- title={selectorJson ?? 'No usable selector for this element'}
341
- onClick={() => handleCopy(selectorJson, 'selector')}
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
- {cliCommand && (
346
- <button
347
- type="button"
348
- className={clsx('rc-inspect-card-btn', copied === 'command' && 'rc-inspect-card-btn-copied')}
349
- title={cliCommand}
350
- onClick={() => handleCopy(cliCommand, 'command')}
351
- >
352
- {copied === 'command' ? 'Copied!' : 'Copy command'}
353
- </button>
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 command), ESC
69
- * clears. The cursor turns into a crosshair while inspecting and the
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) => {
@@ -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');
@@ -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