autokap 1.9.10 → 2.0.1

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.
@@ -132,34 +132,59 @@ const AssertSurfaceOpcodeSchema = z.object({
132
132
  path: ['postcondition'],
133
133
  });
134
134
  });
135
+ /** Locale-keyed strings for multilingual semantic targets ("en", "fr", "fr-FR" …) */
136
+ const localeStringMapSchema = z.record(z.string().min(1).max(16), z.string().min(1));
137
+ const hasLocaleEntries = (map) => Boolean(map && Object.values(map).some((value) => value.trim().length > 0));
135
138
  const SemanticTargetSchema = z.object({
136
139
  text: z.string().optional(),
140
+ textByLocale: localeStringMapSchema.optional(),
137
141
  role: z.string().optional(),
138
142
  label: z.string().optional(),
143
+ labelByLocale: localeStringMapSchema.optional(),
139
144
  near: z.string().optional(),
145
+ nearByLocale: localeStringMapSchema.optional(),
140
146
  placeholder: z.string().optional(),
147
+ placeholderByLocale: localeStringMapSchema.optional(),
141
148
  exact: z.boolean().optional(),
142
- }).strict().refine((value) => Boolean(value.text || value.role || value.label || value.near || value.placeholder), 'semantic targets need at least one identifying field');
149
+ }).strict().refine((value) => Boolean(value.text || value.role || value.label || value.near || value.placeholder
150
+ || hasLocaleEntries(value.textByLocale)
151
+ || hasLocaleEntries(value.labelByLocale)
152
+ || hasLocaleEntries(value.nearByLocale)
153
+ || hasLocaleEntries(value.placeholderByLocale)), 'semantic targets need at least one identifying field');
154
+ /**
155
+ * Interactive opcodes accept `selector` (an attribute that already exists in
156
+ * the app) or a semantic `target` — at least one is required. Programs
157
+ * generated without touching the captured app's code are target-first.
158
+ */
159
+ const requireSelectorOrTarget = (kind) => (value, ctx) => {
160
+ if (!value.selector && !value.target) {
161
+ ctx.addIssue({
162
+ code: z.ZodIssueCode.custom,
163
+ message: `${kind} requires either \`selector\` or \`target\``,
164
+ path: ['selector'],
165
+ });
166
+ }
167
+ };
143
168
  const ClickOpcodeSchema = z.object({
144
169
  kind: z.literal('CLICK'),
145
170
  ...opcodeBase,
146
- selector: z.string().min(1),
171
+ selector: z.string().min(1).optional(),
147
172
  target: SemanticTargetSchema.optional(),
148
173
  button: z.enum(['right', 'middle']).optional(),
149
174
  fingerprint: z.string().optional(),
150
175
  selectorAlternates: z.array(z.string().min(1)).optional(),
151
- }).strict();
176
+ }).strict().superRefine(requireSelectorOrTarget('CLICK'));
152
177
  const TypeOpcodeSchema = z.object({
153
178
  kind: z.literal('TYPE'),
154
179
  ...opcodeBase,
155
- selector: z.string().min(1),
180
+ selector: z.string().min(1).optional(),
156
181
  target: SemanticTargetSchema.optional(),
157
182
  text: z.string(),
158
183
  textByLocale: z.record(z.string().min(1), z.string()).optional(),
159
184
  clearFirst: z.boolean(),
160
185
  fingerprint: z.string().optional(),
161
186
  selectorAlternates: z.array(z.string().min(1)).optional(),
162
- }).strict();
187
+ }).strict().superRefine(requireSelectorOrTarget('TYPE'));
163
188
  const PressKeyOpcodeSchema = z.object({
164
189
  kind: z.literal('PRESS_KEY'),
165
190
  ...opcodeBase,
@@ -298,15 +323,15 @@ const EndClipOpcodeSchema = z.object({
298
323
  const HoverOpcodeSchema = z.object({
299
324
  kind: z.literal('HOVER'),
300
325
  ...opcodeBase,
301
- selector: z.string().min(1),
326
+ selector: z.string().min(1).optional(),
302
327
  target: SemanticTargetSchema.optional(),
303
328
  fingerprint: z.string().optional(),
304
329
  selectorAlternates: z.array(z.string().min(1)).optional(),
305
- }).strict();
330
+ }).strict().superRefine(requireSelectorOrTarget('HOVER'));
306
331
  const SelectOptionOpcodeSchema = z.object({
307
332
  kind: z.literal('SELECT_OPTION'),
308
333
  ...opcodeBase,
309
- selector: z.string().min(1),
334
+ selector: z.string().min(1).optional(),
310
335
  target: SemanticTargetSchema.optional(),
311
336
  optionLabel: z.string().optional(),
312
337
  optionValue: z.string().optional(),
@@ -314,6 +339,7 @@ const SelectOptionOpcodeSchema = z.object({
314
339
  fingerprint: z.string().optional(),
315
340
  selectorAlternates: z.array(z.string().min(1)).optional(),
316
341
  }).strict().superRefine((value, ctx) => {
342
+ requireSelectorOrTarget('SELECT_OPTION')(value, ctx);
317
343
  if (value.optionLabel === undefined && value.optionValue === undefined && value.optionIndex === undefined) {
318
344
  ctx.addIssue({
319
345
  code: z.ZodIssueCode.custom,
@@ -324,24 +350,24 @@ const SelectOptionOpcodeSchema = z.object({
324
350
  const CheckOpcodeSchema = z.object({
325
351
  kind: z.literal('CHECK'),
326
352
  ...opcodeBase,
327
- selector: z.string().min(1),
353
+ selector: z.string().min(1).optional(),
328
354
  target: SemanticTargetSchema.optional(),
329
355
  checked: z.boolean(),
330
356
  fingerprint: z.string().optional(),
331
357
  selectorAlternates: z.array(z.string().min(1)).optional(),
332
- }).strict();
358
+ }).strict().superRefine(requireSelectorOrTarget('CHECK'));
333
359
  const DoubleClickOpcodeSchema = z.object({
334
360
  kind: z.literal('DOUBLE_CLICK'),
335
361
  ...opcodeBase,
336
- selector: z.string().min(1),
362
+ selector: z.string().min(1).optional(),
337
363
  target: SemanticTargetSchema.optional(),
338
364
  fingerprint: z.string().optional(),
339
365
  selectorAlternates: z.array(z.string().min(1)).optional(),
340
- }).strict();
366
+ }).strict().superRefine(requireSelectorOrTarget('DOUBLE_CLICK'));
341
367
  const DragOpcodeSchema = z.object({
342
368
  kind: z.literal('DRAG'),
343
369
  ...opcodeBase,
344
- selector: z.string().min(1),
370
+ selector: z.string().min(1).optional(),
345
371
  target: SemanticTargetSchema.optional(),
346
372
  fingerprint: z.string().optional(),
347
373
  selectorAlternates: z.array(z.string().min(1)).optional(),
@@ -353,6 +379,7 @@ const DragOpcodeSchema = z.object({
353
379
  dy: z.number(),
354
380
  }).strict().optional(),
355
381
  }).strict().superRefine((value, ctx) => {
382
+ requireSelectorOrTarget('DRAG')(value, ctx);
356
383
  const hasElementDest = Boolean(value.toSelector || value.toTarget);
357
384
  const hasOffsetDest = Boolean(value.offset);
358
385
  if (!hasElementDest && !hasOffsetDest) {
@@ -71,19 +71,36 @@ export interface PostconditionSpec {
71
71
  * The runtime resolves the target using Playwright semantic locators
72
72
  * (getByRole, getByText, getByLabel) then falls back to AKTree fuzzy matching.
73
73
  *
74
- * At least one of text/role/label should be provided.
74
+ * This is the primary targeting mechanism for programs generated without
75
+ * modifying the captured app's source code: the descriptor is grounded in
76
+ * what is visible on screen (role, accessible name, text) rather than in a
77
+ * pre-planted automation attribute.
78
+ *
79
+ * At least one of text/role/label (or a locale-keyed variant) should be
80
+ * provided. For multilingual apps, the `*ByLocale` maps carry the per-locale
81
+ * strings extracted from the app's i18n catalogs; the runtime picks the value
82
+ * matching the active variant locale (exact tag, then primary subtag) and
83
+ * falls back to the base field.
75
84
  */
76
85
  export interface SemanticTarget {
77
86
  /** Visible text content of the element (exact or partial match) */
78
87
  text?: string;
88
+ /** Locale-keyed visible text (BCP-47-ish keys: "en", "fr", "fr-FR") */
89
+ textByLocale?: Record<string, string>;
79
90
  /** ARIA role: "button", "link", "textbox", "checkbox", "tab", "menuitem", etc. */
80
91
  role?: string;
81
92
  /** Accessible name: aria-label, associated label text, or title attribute */
82
93
  label?: string;
94
+ /** Locale-keyed accessible name */
95
+ labelByLocale?: Record<string, string>;
83
96
  /** Nearby text or heading for disambiguation (e.g. "in the pricing section") */
84
97
  near?: string;
98
+ /** Locale-keyed nearby text */
99
+ nearByLocale?: Record<string, string>;
85
100
  /** Placeholder text (for inputs) */
86
101
  placeholder?: string;
102
+ /** Locale-keyed placeholder text */
103
+ placeholderByLocale?: Record<string, string>;
87
104
  /** Whether the text match should be exact. Default: false (substring match) */
88
105
  exact?: boolean;
89
106
  }
@@ -139,9 +156,13 @@ export interface AssertSurfaceOpcode extends OpcodeBase {
139
156
  }
140
157
  export interface ClickOpcode extends OpcodeBase {
141
158
  kind: 'CLICK';
142
- /** CSS selector — typically a data-ak attribute set by the user's AI assistant */
143
- selector: string;
144
- /** Semantic target fallback used by recovery chain when selector fails */
159
+ /**
160
+ * CSS selector built from attributes that already exist in the app
161
+ * (id, data-testid, aria-label, href …). Optional when absent, `target`
162
+ * drives resolution. At least one of `selector`/`target` is required.
163
+ */
164
+ selector?: string;
165
+ /** Semantic on-screen target — primary when `selector` is absent, fallback when it fails */
145
166
  target?: SemanticTarget;
146
167
  /** Mouse button: 'right' for context menus, 'middle' for new tab. Default: left click */
147
168
  button?: 'right' | 'middle';
@@ -152,9 +173,9 @@ export interface ClickOpcode extends OpcodeBase {
152
173
  }
153
174
  export interface TypeOpcode extends OpcodeBase {
154
175
  kind: 'TYPE';
155
- /** CSS selector typically a data-ak attribute set by the user's AI assistant */
156
- selector: string;
157
- /** Semantic target fallback */
176
+ /** CSS selector from existing attributes. Optional when `target` is provided. */
177
+ selector?: string;
178
+ /** Semantic on-screen target — primary when `selector` is absent, fallback when it fails */
158
179
  target?: SemanticTarget;
159
180
  text: string;
160
181
  /** Locale-keyed text overrides. When the current variant has a matching locale key,
@@ -267,14 +288,16 @@ export interface EndClipOpcode extends OpcodeBase {
267
288
  }
268
289
  export interface HoverOpcode extends OpcodeBase {
269
290
  kind: 'HOVER';
270
- selector: string;
291
+ /** Optional when `target` is provided */
292
+ selector?: string;
271
293
  target?: SemanticTarget;
272
294
  fingerprint?: string;
273
295
  selectorAlternates?: string[];
274
296
  }
275
297
  export interface SelectOptionOpcode extends OpcodeBase {
276
298
  kind: 'SELECT_OPTION';
277
- selector: string;
299
+ /** Optional when `target` is provided */
300
+ selector?: string;
278
301
  target?: SemanticTarget;
279
302
  /** Select by visible label text */
280
303
  optionLabel?: string;
@@ -287,7 +310,8 @@ export interface SelectOptionOpcode extends OpcodeBase {
287
310
  }
288
311
  export interface CheckOpcode extends OpcodeBase {
289
312
  kind: 'CHECK';
290
- selector: string;
313
+ /** Optional when `target` is provided */
314
+ selector?: string;
291
315
  target?: SemanticTarget;
292
316
  /** Desired state: true = checked, false = unchecked */
293
317
  checked: boolean;
@@ -296,7 +320,8 @@ export interface CheckOpcode extends OpcodeBase {
296
320
  }
297
321
  export interface DoubleClickOpcode extends OpcodeBase {
298
322
  kind: 'DOUBLE_CLICK';
299
- selector: string;
323
+ /** Optional when `target` is provided */
324
+ selector?: string;
300
325
  target?: SemanticTarget;
301
326
  fingerprint?: string;
302
327
  selectorAlternates?: string[];
@@ -312,9 +337,9 @@ export interface DoubleClickOpcode extends OpcodeBase {
312
337
  */
313
338
  export interface DragOpcode extends OpcodeBase {
314
339
  kind: 'DRAG';
315
- /** CSS selector of the source element (the one being dragged) */
316
- selector: string;
317
- /** Semantic fallback for the source element */
340
+ /** CSS selector of the source element (the one being dragged). Optional when `target` is provided. */
341
+ selector?: string;
342
+ /** Semantic target for the source element — primary when `selector` is absent */
318
343
  target?: SemanticTarget;
319
344
  fingerprint?: string;
320
345
  selectorAlternates?: string[];
@@ -906,8 +931,18 @@ export interface ClickByTargetOptions {
906
931
  selector?: string;
907
932
  target?: SemanticTarget;
908
933
  selectorAlternates?: string[];
934
+ /** Active variant locale, used to resolve the target's `*ByLocale` maps */
935
+ locale?: string;
909
936
  onClick?: (timestampMs: number) => void;
910
937
  }
938
+ /** Shared options for the semantic `*ByTarget` adapter methods. */
939
+ export interface TargetResolveOptions {
940
+ selector?: string;
941
+ target?: SemanticTarget;
942
+ selectorAlternates?: string[];
943
+ /** Active variant locale, used to resolve the target's `*ByLocale` maps */
944
+ locale?: string;
945
+ }
911
946
  export interface MouseActionOptions {
912
947
  /** Same semantics as `ClickOptions.onClick` — fires right before the
913
948
  * actual click is dispatched (CHECK / DOUBLE_CLICK). */
@@ -1020,29 +1055,23 @@ export interface RuntimeAdapter {
1020
1055
  /** Click an element by semantic target. Falls back to selector if target not found. */
1021
1056
  clickByTarget?(opts: ClickByTargetOptions): Promise<void>;
1022
1057
  /** Type into an element by semantic target. */
1023
- typeByTarget?(opts: {
1024
- selector?: string;
1025
- target?: SemanticTarget;
1026
- selectorAlternates?: string[];
1027
- }, text: string, clearFirst?: boolean, typeOpts?: TypeOptions): Promise<void>;
1058
+ typeByTarget?(opts: TargetResolveOptions, text: string, clearFirst?: boolean, typeOpts?: TypeOptions): Promise<void>;
1028
1059
  /** Wait for an element by semantic target. */
1029
- waitForTarget?(opts: {
1030
- selector?: string;
1031
- target?: SemanticTarget;
1032
- selectorAlternates?: string[];
1033
- }, timeoutMs?: number): Promise<boolean>;
1060
+ waitForTarget?(opts: TargetResolveOptions, timeoutMs?: number): Promise<boolean>;
1034
1061
  /** Scroll an element into view by semantic target. */
1035
- scrollIntoViewByTarget?(opts: {
1036
- selector?: string;
1037
- target?: SemanticTarget;
1038
- selectorAlternates?: string[];
1062
+ scrollIntoViewByTarget?(opts: TargetResolveOptions): Promise<void>;
1063
+ /** Select a dropdown option on an element resolved by semantic target. */
1064
+ selectOptionByTarget?(opts: TargetResolveOptions, option: {
1065
+ label?: string;
1066
+ value?: string;
1067
+ index?: number;
1039
1068
  }): Promise<void>;
1069
+ /** Check/uncheck an element resolved by semantic target. */
1070
+ checkByTarget?(opts: TargetResolveOptions, checked: boolean, mouseOpts?: MouseActionOptions): Promise<void>;
1071
+ /** Double-click an element resolved by semantic target. */
1072
+ doubleClickByTarget?(opts: TargetResolveOptions, mouseOpts?: MouseActionOptions): Promise<void>;
1040
1073
  hover?(selector: string): Promise<void>;
1041
- hoverByTarget?(opts: {
1042
- selector?: string;
1043
- target?: SemanticTarget;
1044
- selectorAlternates?: string[];
1045
- }): Promise<void>;
1074
+ hoverByTarget?(opts: TargetResolveOptions): Promise<void>;
1046
1075
  selectOption?(selector: string, option: {
1047
1076
  label?: string;
1048
1077
  value?: string;
@@ -1066,6 +1095,8 @@ export interface RuntimeAdapter {
1066
1095
  dx: number;
1067
1096
  dy: number;
1068
1097
  };
1098
+ /** Active variant locale, used to resolve the targets' `*ByLocale` maps */
1099
+ locale?: string;
1069
1100
  }): Promise<void>;
1070
1101
  /** Clone an element N times into a container. Throws if either selector misses. */
1071
1102
  cloneElement?(opts: {
@@ -59,7 +59,8 @@ export class LLMHealer {
59
59
  }
60
60
  function buildHealerPrompt(context) {
61
61
  const { failedOpcode, errorMessage, akTreeSerialized, currentUrl, surroundingOpcodes } = context;
62
- const selector = hasPatchableSelector(failedOpcode) ? failedOpcode.selector : null;
62
+ const selector = isPatchableOpcode(failedOpcode) ? failedOpcode.selector ?? null : null;
63
+ const target = isPatchableOpcode(failedOpcode) ? failedOpcode.target ?? null : null;
63
64
  const surroundingDesc = surroundingOpcodes
64
65
  .map((op, i) => ` ${i + 1}. ${op.kind}: ${sanitizePromptText(op.description)}`)
65
66
  .join('\n');
@@ -68,7 +69,8 @@ function buildHealerPrompt(context) {
68
69
  ## Failed Opcode
69
70
  Kind: ${failedOpcode.kind}
70
71
  Description: ${sanitizePromptText(failedOpcode.description)}
71
- Selector: ${selector ?? '<unsupported>'}
72
+ Selector: ${selector ?? '<none — this opcode targets the element semantically>'}
73
+ Semantic target: ${target ? sanitizePromptText(JSON.stringify(target)) : '<none>'}
72
74
 
73
75
  ## Error
74
76
  <<<ERROR>>>
@@ -90,15 +92,16 @@ ${surroundingDesc}
90
92
  1. You MUST NOT emit opcodes or navigation targets.
91
93
  2. You may only return a selector patch for the existing opcode.
92
94
  3. You may optionally return a bounded interaction mode: default, keyboard, js_dispatch, or coordinates.
93
- 4. Use selectors you can actually see in the AKTree above.
94
- 5. If the opcode kind is unsupported or there is no reliable selector, return cannotHeal=true.
95
+ 4. Use selectors you can actually see in the AKTree above — existing ids, data-testid, aria-label, name, href, or text. Do NOT assume the app contains automation attributes (data-ak) unless they appear in the AKTree.
96
+ 5. When the failed opcode has a semantic target, pick the element that matches its intent (role, accessible name, text — in any language the page may be displayed in).
97
+ 6. If the opcode kind is unsupported or there is no reliable selector, return cannotHeal=true.
95
98
 
96
99
  ## Response Format
97
100
  Respond with a JSON object:
98
101
  {
99
102
  "cannotHeal": false,
100
103
  "reason": "explanation of what you changed",
101
- "selector": "[data-ak='submit']",
104
+ "selector": "#submit-button",
102
105
  "selectorAlternates": ["button[type='submit']"],
103
106
  "interactionMode": "default"
104
107
  }
@@ -155,19 +158,28 @@ function parseHealerResponse(response) {
155
158
  return null;
156
159
  }
157
160
  }
158
- function hasPatchableSelector(opcode) {
159
- return ((opcode.kind === 'CLICK'
160
- || opcode.kind === 'TYPE'
161
- || opcode.kind === 'WAIT_FOR'
162
- || opcode.kind === 'HOVER'
163
- || opcode.kind === 'SELECT_OPTION'
164
- || opcode.kind === 'CHECK'
165
- || opcode.kind === 'DOUBLE_CLICK')
166
- && typeof opcode.selector === 'string'
167
- && opcode.selector.trim().length > 0);
161
+ /**
162
+ * An opcode the healer can patch: a supported kind that carries a selector
163
+ * OR a semantic target. Target-only opcodes are patched by ADDING a concrete
164
+ * selector resolved from the live AKTree (the original target is kept as
165
+ * fallback on the replacement opcode).
166
+ */
167
+ function isPatchableOpcode(opcode) {
168
+ if (opcode.kind !== 'CLICK'
169
+ && opcode.kind !== 'TYPE'
170
+ && opcode.kind !== 'WAIT_FOR'
171
+ && opcode.kind !== 'HOVER'
172
+ && opcode.kind !== 'SELECT_OPTION'
173
+ && opcode.kind !== 'CHECK'
174
+ && opcode.kind !== 'DOUBLE_CLICK') {
175
+ return false;
176
+ }
177
+ const hasSelector = typeof opcode.selector === 'string' && opcode.selector.trim().length > 0;
178
+ const hasTarget = Boolean(opcode.target);
179
+ return hasSelector || hasTarget;
168
180
  }
169
181
  function buildReplacementOpcode(failedOpcode, parsed) {
170
- if (!hasPatchableSelector(failedOpcode) || !parsed.selector) {
182
+ if (!isPatchableOpcode(failedOpcode) || !parsed.selector) {
171
183
  return null;
172
184
  }
173
185
  switch (failedOpcode.kind) {
@@ -44,6 +44,20 @@ export function findUnresolvedCredentialPlaceholders(text, credentials) {
44
44
  }
45
45
  return missing;
46
46
  }
47
+ /**
48
+ * Guard for selector-less opcodes: the schema guarantees a `target` is
49
+ * present when `selector` is absent, and the adapter must expose the matching
50
+ * semantic `*ByTarget` method. Throws a clear error instead of letting a
51
+ * `undefined selector` crash deep inside Playwright.
52
+ */
53
+ function requireByTargetSupport(kind, target, byTargetMethod) {
54
+ if (!target) {
55
+ throw new Error(`${kind} has neither selector nor target`);
56
+ }
57
+ if (typeof byTargetMethod !== 'function') {
58
+ throw new Error(`adapter does not support semantic targets for ${kind}`);
59
+ }
60
+ }
47
61
  export async function executeOpcodeCoreAction(opcode, adapter, context = {}) {
48
62
  try {
49
63
  switch (opcode.kind) {
@@ -68,18 +82,26 @@ export async function executeOpcodeCoreAction(opcode, adapter, context = {}) {
68
82
  const onClick = (timestampMs) => {
69
83
  clickTimestampsMs.push(timestampMs);
70
84
  };
71
- try {
72
- await adapter.click(opcode.selector, { ...(opcode.button ? { button: opcode.button } : {}), onClick });
85
+ const byTargetOpts = {
86
+ selector: opcode.selector,
87
+ target: opcode.target,
88
+ selectorAlternates: opcode.selectorAlternates,
89
+ locale: context.currentVariant?.locale,
90
+ onClick,
91
+ };
92
+ if (!opcode.selector) {
93
+ requireByTargetSupport('CLICK', opcode.target, adapter.clickByTarget);
94
+ await adapter.clickByTarget(byTargetOpts);
73
95
  }
74
- catch (error) {
75
- if (!opcode.target || !adapter.clickByTarget)
76
- throw error;
77
- await adapter.clickByTarget({
78
- selector: opcode.selector,
79
- target: opcode.target,
80
- selectorAlternates: opcode.selectorAlternates,
81
- onClick,
82
- });
96
+ else {
97
+ try {
98
+ await adapter.click(opcode.selector, { ...(opcode.button ? { button: opcode.button } : {}), onClick });
99
+ }
100
+ catch (error) {
101
+ if (!opcode.target || !adapter.clickByTarget)
102
+ throw error;
103
+ await adapter.clickByTarget(byTargetOpts);
104
+ }
83
105
  }
84
106
  return { success: true, clickTimestampsMs };
85
107
  }
@@ -92,17 +114,25 @@ export async function executeOpcodeCoreAction(opcode, adapter, context = {}) {
92
114
  const onKeystroke = (timestampMs) => {
93
115
  keystrokeTimestampsMs.push(timestampMs);
94
116
  };
95
- try {
96
- await adapter.type(opcode.selector, text, opcode.clearFirst, { onKeystroke });
117
+ const byTargetOpts = {
118
+ selector: opcode.selector,
119
+ target: opcode.target,
120
+ selectorAlternates: opcode.selectorAlternates,
121
+ locale: context.currentVariant?.locale,
122
+ };
123
+ if (!opcode.selector) {
124
+ requireByTargetSupport('TYPE', opcode.target, adapter.typeByTarget);
125
+ await adapter.typeByTarget(byTargetOpts, text, opcode.clearFirst, { onKeystroke });
97
126
  }
98
- catch (error) {
99
- if (!opcode.target || !adapter.typeByTarget)
100
- throw error;
101
- await adapter.typeByTarget({
102
- selector: opcode.selector,
103
- target: opcode.target,
104
- selectorAlternates: opcode.selectorAlternates,
105
- }, text, opcode.clearFirst, { onKeystroke });
127
+ else {
128
+ try {
129
+ await adapter.type(opcode.selector, text, opcode.clearFirst, { onKeystroke });
130
+ }
131
+ catch (error) {
132
+ if (!opcode.target || !adapter.typeByTarget)
133
+ throw error;
134
+ await adapter.typeByTarget(byTargetOpts, text, opcode.clearFirst, { onKeystroke });
135
+ }
106
136
  }
107
137
  return { success: true, keystrokeTimestampsMs };
108
138
  }
@@ -122,7 +152,7 @@ export async function executeOpcodeCoreAction(opcode, adapter, context = {}) {
122
152
  }
123
153
  }
124
154
  else if (opcode.target && adapter.waitForTarget) {
125
- const found = await adapter.waitForTarget({ target: opcode.target }, opcode.timeoutMs);
155
+ const found = await adapter.waitForTarget({ target: opcode.target, locale: context.currentVariant?.locale }, opcode.timeoutMs);
126
156
  if (!found) {
127
157
  return { success: false, error: `target "${opcode.target.text ?? opcode.target.role ?? 'unknown'}" not found within timeout` };
128
158
  }
@@ -150,13 +180,24 @@ export async function executeOpcodeCoreAction(opcode, adapter, context = {}) {
150
180
  await adapter.scrollIntoView(opcode.targetSelector);
151
181
  }
152
182
  else if (opcode.target && adapter.scrollIntoViewByTarget) {
153
- await adapter.scrollIntoViewByTarget({ target: opcode.target });
183
+ await adapter.scrollIntoViewByTarget({ target: opcode.target, locale: context.currentVariant?.locale });
154
184
  }
155
185
  else {
156
186
  await adapter.scroll(opcode.direction, opcode.amount);
157
187
  }
158
188
  break;
159
- case 'HOVER':
189
+ case 'HOVER': {
190
+ const byTargetOpts = {
191
+ selector: opcode.selector,
192
+ target: opcode.target,
193
+ selectorAlternates: opcode.selectorAlternates,
194
+ locale: context.currentVariant?.locale,
195
+ };
196
+ if (!opcode.selector) {
197
+ requireByTargetSupport('HOVER', opcode.target, adapter.hoverByTarget);
198
+ await adapter.hoverByTarget(byTargetOpts);
199
+ break;
200
+ }
160
201
  if (!adapter.hover)
161
202
  return { success: false, error: 'adapter does not support HOVER' };
162
203
  try {
@@ -165,38 +206,93 @@ export async function executeOpcodeCoreAction(opcode, adapter, context = {}) {
165
206
  catch (error) {
166
207
  if (!opcode.target || !adapter.hoverByTarget)
167
208
  throw error;
168
- await adapter.hoverByTarget({
169
- selector: opcode.selector,
170
- target: opcode.target,
171
- selectorAlternates: opcode.selectorAlternates,
172
- });
209
+ await adapter.hoverByTarget(byTargetOpts);
173
210
  }
174
211
  break;
175
- case 'SELECT_OPTION':
176
- if (!adapter.selectOption)
177
- return { success: false, error: 'adapter does not support SELECT_OPTION' };
178
- await adapter.selectOption(opcode.selector, {
212
+ }
213
+ case 'SELECT_OPTION': {
214
+ const option = {
179
215
  label: opcode.optionLabel,
180
216
  value: opcode.optionValue,
181
217
  index: opcode.optionIndex,
182
- });
218
+ };
219
+ const byTargetOpts = {
220
+ selector: opcode.selector,
221
+ target: opcode.target,
222
+ selectorAlternates: opcode.selectorAlternates,
223
+ locale: context.currentVariant?.locale,
224
+ };
225
+ if (!opcode.selector) {
226
+ requireByTargetSupport('SELECT_OPTION', opcode.target, adapter.selectOptionByTarget);
227
+ await adapter.selectOptionByTarget(byTargetOpts, option);
228
+ break;
229
+ }
230
+ if (!adapter.selectOption)
231
+ return { success: false, error: 'adapter does not support SELECT_OPTION' };
232
+ try {
233
+ await adapter.selectOption(opcode.selector, option);
234
+ }
235
+ catch (error) {
236
+ if (!opcode.target || !adapter.selectOptionByTarget)
237
+ throw error;
238
+ await adapter.selectOptionByTarget(byTargetOpts, option);
239
+ }
183
240
  break;
241
+ }
184
242
  case 'CHECK': {
185
- if (!adapter.check)
186
- return { success: false, error: 'adapter does not support CHECK' };
187
243
  const clickTimestampsMs = [];
188
- await adapter.check(opcode.selector, opcode.checked, {
244
+ const mouseOpts = {
189
245
  onClick: (timestampMs) => clickTimestampsMs.push(timestampMs),
190
- });
246
+ };
247
+ const byTargetOpts = {
248
+ selector: opcode.selector,
249
+ target: opcode.target,
250
+ selectorAlternates: opcode.selectorAlternates,
251
+ locale: context.currentVariant?.locale,
252
+ };
253
+ if (!opcode.selector) {
254
+ requireByTargetSupport('CHECK', opcode.target, adapter.checkByTarget);
255
+ await adapter.checkByTarget(byTargetOpts, opcode.checked, mouseOpts);
256
+ return { success: true, clickTimestampsMs };
257
+ }
258
+ if (!adapter.check)
259
+ return { success: false, error: 'adapter does not support CHECK' };
260
+ try {
261
+ await adapter.check(opcode.selector, opcode.checked, mouseOpts);
262
+ }
263
+ catch (error) {
264
+ if (!opcode.target || !adapter.checkByTarget)
265
+ throw error;
266
+ await adapter.checkByTarget(byTargetOpts, opcode.checked, mouseOpts);
267
+ }
191
268
  return { success: true, clickTimestampsMs };
192
269
  }
193
270
  case 'DOUBLE_CLICK': {
194
- if (!adapter.doubleClick)
195
- return { success: false, error: 'adapter does not support DOUBLE_CLICK' };
196
271
  const clickTimestampsMs = [];
197
- await adapter.doubleClick(opcode.selector, {
272
+ const mouseOpts = {
198
273
  onClick: (timestampMs) => clickTimestampsMs.push(timestampMs),
199
- });
274
+ };
275
+ const byTargetOpts = {
276
+ selector: opcode.selector,
277
+ target: opcode.target,
278
+ selectorAlternates: opcode.selectorAlternates,
279
+ locale: context.currentVariant?.locale,
280
+ };
281
+ if (!opcode.selector) {
282
+ requireByTargetSupport('DOUBLE_CLICK', opcode.target, adapter.doubleClickByTarget);
283
+ await adapter.doubleClickByTarget(byTargetOpts, mouseOpts);
284
+ return { success: true, clickTimestampsMs };
285
+ }
286
+ if (!adapter.doubleClick)
287
+ return { success: false, error: 'adapter does not support DOUBLE_CLICK' };
288
+ try {
289
+ await adapter.doubleClick(opcode.selector, mouseOpts);
290
+ }
291
+ catch (error) {
292
+ if (!opcode.target || !adapter.doubleClickByTarget)
293
+ throw error;
294
+ await adapter.doubleClickByTarget(byTargetOpts, mouseOpts);
295
+ }
200
296
  return { success: true, clickTimestampsMs };
201
297
  }
202
298
  case 'DRAG':
@@ -210,6 +306,7 @@ export async function executeOpcodeCoreAction(opcode, adapter, context = {}) {
210
306
  toTarget: opcode.toTarget,
211
307
  toSelectorAlternates: opcode.toSelectorAlternates,
212
308
  offset: opcode.offset,
309
+ locale: context.currentVariant?.locale,
213
310
  });
214
311
  break;
215
312
  case 'CLONE_ELEMENT':