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.
- package/dist/cli-runner.js +13 -2
- package/dist/cli.d.ts +21 -0
- package/dist/cli.js +48 -1
- package/dist/execution-schema.d.ts +382 -182
- package/dist/execution-schema.js +40 -13
- package/dist/execution-types.d.ts +64 -33
- package/dist/llm-healer.js +28 -16
- package/dist/opcode-actions.js +139 -42
- package/dist/opcode-runner.d.ts +10 -1
- package/dist/opcode-runner.js +1 -0
- package/dist/program-signing.d.ts +47 -7
- package/dist/recovery-chain.js +51 -12
- package/dist/selector-resolver.d.ts +19 -4
- package/dist/selector-resolver.js +39 -3
- package/dist/semantic-resolver.d.ts +8 -0
- package/dist/semantic-resolver.js +0 -0
- package/dist/video-narration-schema.d.ts +47 -7
- package/dist/web-playwright-local.d.ts +12 -0
- package/dist/web-playwright-local.js +53 -0
- package/package.json +1 -1
package/dist/execution-schema.js
CHANGED
|
@@ -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
|
|
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
|
-
*
|
|
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
|
-
/**
|
|
143
|
-
|
|
144
|
-
|
|
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
|
|
156
|
-
selector
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
317
|
-
/** Semantic
|
|
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
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
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: {
|
package/dist/llm-healer.js
CHANGED
|
@@ -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 =
|
|
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 ?? '<
|
|
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.
|
|
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": "
|
|
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
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
&&
|
|
167
|
-
&& opcode.
|
|
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 (!
|
|
182
|
+
if (!isPatchableOpcode(failedOpcode) || !parsed.selector) {
|
|
171
183
|
return null;
|
|
172
184
|
}
|
|
173
185
|
switch (failedOpcode.kind) {
|
package/dist/opcode-actions.js
CHANGED
|
@@ -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
|
-
|
|
72
|
-
|
|
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
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
-
|
|
96
|
-
|
|
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
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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':
|