@sungen/driver-ui 3.1.2-beta.97
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 +25 -0
- package/src/index.ts +119 -0
- package/src/patterns/assertion-patterns.ts +691 -0
- package/src/patterns/capture-patterns.ts +97 -0
- package/src/patterns/form-patterns.ts +167 -0
- package/src/patterns/interaction-patterns.ts +465 -0
- package/src/patterns/keyboard-patterns.ts +51 -0
- package/src/patterns/navigation-patterns.ts +140 -0
- package/src/patterns/scope-patterns.ts +40 -0
- package/src/patterns/scroll-patterns.ts +27 -0
- package/src/patterns/setup-patterns.ts +76 -0
- package/src/patterns/table-patterns.ts +279 -0
|
@@ -0,0 +1,691 @@
|
|
|
1
|
+
import { ParsedStep } from '@sun-asterisk/sungen';
|
|
2
|
+
import { StepPattern, StepTemplateData } from '@sun-asterisk/sungen';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Assertion patterns: visibility, text content, state, attributes
|
|
6
|
+
* Uses template engine for framework-agnostic code generation
|
|
7
|
+
*/
|
|
8
|
+
export const assertionPatterns: StepPattern[] = [
|
|
9
|
+
// Browser alert text assertion: "see [Are you sure?] alert"
|
|
10
|
+
{
|
|
11
|
+
name: 'alert-text-assertion',
|
|
12
|
+
matcher: (step: ParsedStep) =>
|
|
13
|
+
(step.text.includes('see') || step.text.includes('sees')) &&
|
|
14
|
+
step.elementType === 'alert' &&
|
|
15
|
+
!!step.selectorRef,
|
|
16
|
+
resolver: (step, context): StepTemplateData => {
|
|
17
|
+
let dataValue = step.selectorRef || '';
|
|
18
|
+
if (step.dataRef) {
|
|
19
|
+
try {
|
|
20
|
+
dataValue = context.dataResolver.resolveData(step.dataRef, context.featureName);
|
|
21
|
+
} catch {
|
|
22
|
+
dataValue = `\${${step.dataRef}}`;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return {
|
|
26
|
+
templateName: 'alert-text-assertion',
|
|
27
|
+
data: { dataValue, stepCounter: context.stepCounter },
|
|
28
|
+
comment: `Assert browser alert contains "${step.selectorRef}"`,
|
|
29
|
+
};
|
|
30
|
+
},
|
|
31
|
+
priority: 21,
|
|
32
|
+
},
|
|
33
|
+
// Column cell assertion: "see [Department] column 1 with {{value}}" -> check table cell text
|
|
34
|
+
{
|
|
35
|
+
name: 'column-cell-assertion',
|
|
36
|
+
matcher: (step: ParsedStep) =>
|
|
37
|
+
(step.text.includes('should see') || step.text.match(/\b(see|sees)\s+\[/)) &&
|
|
38
|
+
!!step.selectorRef &&
|
|
39
|
+
!!step.dataRef &&
|
|
40
|
+
(step.elementType === 'column' || step.elementType === 'columnheader'),
|
|
41
|
+
resolver: (step, context): StepTemplateData => {
|
|
42
|
+
let dataValue: string;
|
|
43
|
+
try {
|
|
44
|
+
dataValue = context.dataResolver.resolveData(step.dataRef!, context.featureName);
|
|
45
|
+
} catch (error) {
|
|
46
|
+
dataValue = `\${${step.dataRef}}`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// getByRole('row') includes header row as nth(0), so Gherkin nth=1 maps to locator nth(1)
|
|
50
|
+
const rowNth = step.nth || 1;
|
|
51
|
+
// Create a safe variable name from column name
|
|
52
|
+
const columnIndexVar = `colIndex${step.selectorRef!.replace(/[^a-zA-Z0-9]/g, '')}`;
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
templateName: 'column-cell-assertion',
|
|
56
|
+
data: {
|
|
57
|
+
columnName: step.selectorRef,
|
|
58
|
+
rowNth,
|
|
59
|
+
columnIndexVar,
|
|
60
|
+
dataValue,
|
|
61
|
+
},
|
|
62
|
+
comment: `Assert ${step.selectorRef} column row ${step.nth || 1} has text ${step.dataRef}`,
|
|
63
|
+
};
|
|
64
|
+
},
|
|
65
|
+
priority: 15, // Higher than all other assertion patterns
|
|
66
|
+
},
|
|
67
|
+
// Page assertion pattern: "see [home] page" -> check URL matches selector value
|
|
68
|
+
{
|
|
69
|
+
name: 'page-assertion',
|
|
70
|
+
matcher: (step: ParsedStep) =>
|
|
71
|
+
(step.text.includes('should see') ||
|
|
72
|
+
step.text.includes('see') ||
|
|
73
|
+
step.text.includes('sees')) &&
|
|
74
|
+
step.elementType === 'page',
|
|
75
|
+
resolver: (step, context): StepTemplateData => {
|
|
76
|
+
let path = step.featurePath || '/';
|
|
77
|
+
|
|
78
|
+
// If selector is present, extract path from selector's value attribute
|
|
79
|
+
if (step.selectorRef) {
|
|
80
|
+
try {
|
|
81
|
+
const resolved = context.selectorResolver.resolveSelector(
|
|
82
|
+
step.selectorRef,
|
|
83
|
+
context.featureName,
|
|
84
|
+
step.elementType,
|
|
85
|
+
step.nth
|
|
86
|
+
);
|
|
87
|
+
// Use selector's value as the path for URL assertion
|
|
88
|
+
path = resolved.value || path;
|
|
89
|
+
} catch (error) {
|
|
90
|
+
// Fallback to feature path if selector not found
|
|
91
|
+
path = step.featurePath || '/';
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Convert path to regex-safe string
|
|
96
|
+
const pathRegex = path.replace(/[.*+?^${}()|[\]\\/]/g, '\\$&');
|
|
97
|
+
return {
|
|
98
|
+
templateName: 'page-assertion',
|
|
99
|
+
data: { pathRegex },
|
|
100
|
+
comment: step.selectorRef ? `Assert on ${step.selectorRef} page` : `Assert on page`,
|
|
101
|
+
};
|
|
102
|
+
},
|
|
103
|
+
priority: 13, // Higher priority than general visibility
|
|
104
|
+
},
|
|
105
|
+
// Pattern: Then User see [panel] dialog with {{kudo.title}} hidden -> toBeHidden()
|
|
106
|
+
{
|
|
107
|
+
name: 'see-with-variable-hidden',
|
|
108
|
+
matcher: (step: ParsedStep) =>
|
|
109
|
+
(step.text.includes('should see') || step.text.match(/\b(see|sees)\s+\[/)) &&
|
|
110
|
+
!!step.selectorRef &&
|
|
111
|
+
!!step.dataRef &&
|
|
112
|
+
step.text.includes('with') &&
|
|
113
|
+
/\bis hidden\b/.test(step.text),
|
|
114
|
+
generator: (step, context) => {
|
|
115
|
+
let resolved: any = {};
|
|
116
|
+
try {
|
|
117
|
+
resolved = context.selectorResolver.resolveSelector(
|
|
118
|
+
step.selectorRef!,
|
|
119
|
+
context.featureName,
|
|
120
|
+
step.elementType,
|
|
121
|
+
step.nth
|
|
122
|
+
);
|
|
123
|
+
} catch (error) {}
|
|
124
|
+
|
|
125
|
+
let dataValue: string;
|
|
126
|
+
try {
|
|
127
|
+
dataValue = context.dataResolver.resolveData(step.dataRef!, context.featureName);
|
|
128
|
+
} catch (error) {
|
|
129
|
+
dataValue = `\${${step.dataRef}}`;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const escapedVariable = dataValue.replace(/[.*+?^${}()|[\]\\/]/g, '\\$&');
|
|
133
|
+
|
|
134
|
+
if (resolved.strategy === 'locator') {
|
|
135
|
+
const code = context.templateEngine.renderStep('hidden-with-filter-assertion', {
|
|
136
|
+
...resolved, dataValue, nth: resolved.nth,
|
|
137
|
+
});
|
|
138
|
+
return { code, comment: `Assert ${step.selectorRef} hidden with ${step.dataRef}` };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (resolved.strategy === 'role' && resolved.role) {
|
|
142
|
+
// Dialog role: check heading inside dialog instead of filter on full text content
|
|
143
|
+
if (resolved.role === 'dialog') {
|
|
144
|
+
const code = context.templateEngine.renderStep('hidden-dialog-heading-assertion', {
|
|
145
|
+
dataValue,
|
|
146
|
+
});
|
|
147
|
+
return { code, comment: `Assert ${step.selectorRef} dialog hidden with heading ${step.dataRef}` };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const code = context.templateEngine.renderStep('hidden-with-role-variable-assertion', {
|
|
151
|
+
role: resolved.role,
|
|
152
|
+
name: resolved.name && resolved.name.trim() ? resolved.name : undefined,
|
|
153
|
+
dataValue,
|
|
154
|
+
nth: resolved.nth,
|
|
155
|
+
});
|
|
156
|
+
return { code, comment: `Assert ${step.selectorRef} hidden with ${step.dataRef}` };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const selectorValue = resolved.value || '';
|
|
160
|
+
if (selectorValue && selectorValue.trim()) {
|
|
161
|
+
const code = context.templateEngine.renderStep('hidden-with-filter-assertion', {
|
|
162
|
+
...resolved, dataValue, nth: resolved.nth,
|
|
163
|
+
});
|
|
164
|
+
return { code, comment: `Assert ${step.selectorRef} hidden with ${step.dataRef}` };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const code = context.templateEngine.renderStep('hidden-with-variable-assertion', {
|
|
168
|
+
selectorRef: step.selectorRef,
|
|
169
|
+
selectorValue: resolved.value || '',
|
|
170
|
+
dataValue,
|
|
171
|
+
nth: resolved.nth,
|
|
172
|
+
});
|
|
173
|
+
return { code, comment: `Assert ${step.selectorRef} hidden with ${step.dataRef}` };
|
|
174
|
+
},
|
|
175
|
+
priority: 14,
|
|
176
|
+
},
|
|
177
|
+
// Pattern: Then User see [submit] button with {{label}} disabled -> toBeDisabled()
|
|
178
|
+
{
|
|
179
|
+
name: 'see-with-variable-disabled',
|
|
180
|
+
matcher: (step: ParsedStep) =>
|
|
181
|
+
(step.text.includes('should see') || step.text.match(/\b(see|sees)\s+\[/)) &&
|
|
182
|
+
!!step.selectorRef &&
|
|
183
|
+
!!step.dataRef &&
|
|
184
|
+
step.text.includes('with') &&
|
|
185
|
+
/\bis disabled\b/.test(step.text),
|
|
186
|
+
generator: (step, context) => {
|
|
187
|
+
let resolved: any = {};
|
|
188
|
+
try {
|
|
189
|
+
resolved = context.selectorResolver.resolveSelector(
|
|
190
|
+
step.selectorRef!,
|
|
191
|
+
context.featureName,
|
|
192
|
+
step.elementType,
|
|
193
|
+
step.nth
|
|
194
|
+
);
|
|
195
|
+
} catch (error) {}
|
|
196
|
+
|
|
197
|
+
let dataValue: string;
|
|
198
|
+
try {
|
|
199
|
+
dataValue = context.dataResolver.resolveData(step.dataRef!, context.featureName);
|
|
200
|
+
} catch (error) {
|
|
201
|
+
dataValue = `\${${step.dataRef}}`;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const escapedVariable = dataValue.replace(/[.*+?^${}()|[\]\\/]/g, '\\$&');
|
|
205
|
+
|
|
206
|
+
if (resolved.strategy === 'locator') {
|
|
207
|
+
const code = context.templateEngine.renderStep('disabled-with-filter-assertion', {
|
|
208
|
+
...resolved, dataValue, nth: resolved.nth,
|
|
209
|
+
});
|
|
210
|
+
return { code, comment: `Assert ${step.selectorRef} disabled with ${step.dataRef}` };
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (resolved.strategy === 'role' && resolved.role) {
|
|
214
|
+
const code = context.templateEngine.renderStep('disabled-with-role-variable-assertion', {
|
|
215
|
+
role: resolved.role,
|
|
216
|
+
name: resolved.name && resolved.name.trim() ? resolved.name : undefined,
|
|
217
|
+
dataValue,
|
|
218
|
+
nth: resolved.nth,
|
|
219
|
+
});
|
|
220
|
+
return { code, comment: `Assert ${step.selectorRef} disabled with ${step.dataRef}` };
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const selectorValue = resolved.value || '';
|
|
224
|
+
if (selectorValue && selectorValue.trim()) {
|
|
225
|
+
const code = context.templateEngine.renderStep('disabled-with-filter-assertion', {
|
|
226
|
+
...resolved, dataValue, nth: resolved.nth,
|
|
227
|
+
});
|
|
228
|
+
return { code, comment: `Assert ${step.selectorRef} disabled with ${step.dataRef}` };
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const code = context.templateEngine.renderStep('disabled-with-variable-assertion', {
|
|
232
|
+
selectorRef: step.selectorRef,
|
|
233
|
+
selectorValue: resolved.value || '',
|
|
234
|
+
dataValue,
|
|
235
|
+
nth: resolved.nth,
|
|
236
|
+
});
|
|
237
|
+
return { code, comment: `Assert ${step.selectorRef} disabled with ${step.dataRef}` };
|
|
238
|
+
},
|
|
239
|
+
priority: 14,
|
|
240
|
+
},
|
|
241
|
+
// NEW: Assertion with variable (handles both empty selector and static + variable)
|
|
242
|
+
// Pattern: Then User see [error] message with {{fail_message}}
|
|
243
|
+
// If selector YAML has empty value, uses variable-only matching
|
|
244
|
+
// If selector has value, combines both (static text + variable)
|
|
245
|
+
// Input types → toHaveValue, everything else → toHaveText
|
|
246
|
+
{
|
|
247
|
+
name: 'see-with-variable',
|
|
248
|
+
matcher: (step: ParsedStep) =>
|
|
249
|
+
(step.text.includes('should see') ||
|
|
250
|
+
step.text.match(/\b(see|sees)\s+\[/)) &&
|
|
251
|
+
!!step.selectorRef &&
|
|
252
|
+
!!step.dataRef &&
|
|
253
|
+
step.text.includes('with'),
|
|
254
|
+
generator: (step, context) => {
|
|
255
|
+
// Input element types use toHaveValue instead of toHaveText
|
|
256
|
+
const INPUT_TYPES = new Set([
|
|
257
|
+
'field', 'textarea', 'search', 'dropdown', 'slider', 'date-picker',
|
|
258
|
+
'input', 'textbox', 'editor', 'select', 'combobox',
|
|
259
|
+
]);
|
|
260
|
+
|
|
261
|
+
// Resolve data variable value
|
|
262
|
+
let dataValue: string;
|
|
263
|
+
try {
|
|
264
|
+
dataValue = context.dataResolver.resolveData(step.dataRef!, context.featureName);
|
|
265
|
+
} catch (error) {
|
|
266
|
+
dataValue = `\${${step.dataRef}}`;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Resolve selector from YAML with feature context
|
|
270
|
+
let resolved: any = {};
|
|
271
|
+
try {
|
|
272
|
+
resolved = context.selectorResolver.resolveSelector(
|
|
273
|
+
step.selectorRef!,
|
|
274
|
+
context.featureName,
|
|
275
|
+
step.elementType,
|
|
276
|
+
step.nth
|
|
277
|
+
);
|
|
278
|
+
} catch (error) {
|
|
279
|
+
// Selector not in YAML or context issue - will use variable-only
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// --- Attribute assertion: toHaveAttribute ---
|
|
283
|
+
// When selector YAML has `attribute` field (e.g., attribute: 'src', 'href')
|
|
284
|
+
if (resolved.attribute) {
|
|
285
|
+
const code = context.templateEngine.renderStep('attribute-assertion', {
|
|
286
|
+
...resolved,
|
|
287
|
+
dataValue,
|
|
288
|
+
});
|
|
289
|
+
return {
|
|
290
|
+
code,
|
|
291
|
+
comment: `Assert ${step.selectorRef} ${resolved.attribute} matches ${step.dataRef}`,
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// --- Input types: toHaveValue ---
|
|
296
|
+
if (step.elementType && INPUT_TYPES.has(step.elementType)) {
|
|
297
|
+
const code = context.templateEngine.renderStep('have-value-assertion', {
|
|
298
|
+
...resolved,
|
|
299
|
+
dataValue,
|
|
300
|
+
});
|
|
301
|
+
return {
|
|
302
|
+
code,
|
|
303
|
+
comment: `Assert ${step.selectorRef} has value ${step.dataRef}`,
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// --- Label-value pattern: "User see [X] label with {{Y}}" ---
|
|
308
|
+
if (step.elementType === 'label' && step.dataRef) {
|
|
309
|
+
let label: string | undefined = step.selectorRef;
|
|
310
|
+
try {
|
|
311
|
+
const labelResolved = context.selectorResolver.resolveSelector(
|
|
312
|
+
step.selectorRef!,
|
|
313
|
+
context.featureName,
|
|
314
|
+
step.elementType,
|
|
315
|
+
step.nth
|
|
316
|
+
);
|
|
317
|
+
if (labelResolved.value !== undefined && labelResolved.value.trim() === '') {
|
|
318
|
+
label = undefined;
|
|
319
|
+
}
|
|
320
|
+
} catch {
|
|
321
|
+
// No selector entry — use label from selectorRef
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const code = context.templateEngine.renderStep('label-value-assertion', {
|
|
325
|
+
label,
|
|
326
|
+
dataValue,
|
|
327
|
+
});
|
|
328
|
+
return {
|
|
329
|
+
code,
|
|
330
|
+
comment: `Assert ${step.selectorRef} label with value ${step.dataRef}`,
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// --- Dialog role: check heading inside dialog ---
|
|
335
|
+
if (resolved.strategy === 'role' && resolved.role === 'dialog') {
|
|
336
|
+
const code = context.templateEngine.renderStep('visible-dialog-heading-assertion', {
|
|
337
|
+
dataValue,
|
|
338
|
+
});
|
|
339
|
+
return {
|
|
340
|
+
code,
|
|
341
|
+
comment: `Assert ${step.selectorRef} dialog with heading ${step.dataRef}`,
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// --- Image role: images have no text, use name ---
|
|
346
|
+
if (resolved.strategy === 'role' && resolved.role === 'img') {
|
|
347
|
+
const hasName = resolved.name && resolved.name.trim();
|
|
348
|
+
const code = context.templateEngine.renderStep('visible-with-role-variable-assertion', {
|
|
349
|
+
role: resolved.role,
|
|
350
|
+
name: hasName ? resolved.name : dataValue,
|
|
351
|
+
exact: resolved.exact || false,
|
|
352
|
+
nth: resolved.nth,
|
|
353
|
+
});
|
|
354
|
+
return {
|
|
355
|
+
code,
|
|
356
|
+
comment: `Assert ${step.selectorRef} image with name ${step.dataRef}`,
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// --- Everything else: toHaveText (exact full match) ---
|
|
361
|
+
// Locator strategy
|
|
362
|
+
if (resolved.strategy === 'locator') {
|
|
363
|
+
const code = context.templateEngine.renderStep('have-text-assertion', {
|
|
364
|
+
...resolved,
|
|
365
|
+
expectedText: dataValue,
|
|
366
|
+
});
|
|
367
|
+
return {
|
|
368
|
+
code,
|
|
369
|
+
comment: `Assert ${step.selectorRef} has text ${step.dataRef}`,
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// Role-based selector
|
|
374
|
+
if (resolved.strategy === 'role' && resolved.role) {
|
|
375
|
+
const hasName = resolved.name && resolved.name.trim();
|
|
376
|
+
const code = context.templateEngine.renderStep('have-text-assertion', {
|
|
377
|
+
...resolved,
|
|
378
|
+
expectedText: dataValue,
|
|
379
|
+
});
|
|
380
|
+
return {
|
|
381
|
+
code,
|
|
382
|
+
comment: `Assert ${step.selectorRef} has text ${step.dataRef}`,
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// Text/placeholder/testid/other strategies
|
|
387
|
+
const code = context.templateEngine.renderStep('have-text-assertion', {
|
|
388
|
+
...resolved,
|
|
389
|
+
expectedText: dataValue,
|
|
390
|
+
});
|
|
391
|
+
return {
|
|
392
|
+
code,
|
|
393
|
+
comment: `Assert ${step.selectorRef} has text ${step.dataRef}`,
|
|
394
|
+
};
|
|
395
|
+
},
|
|
396
|
+
priority: 13,
|
|
397
|
+
},
|
|
398
|
+
{
|
|
399
|
+
name: 'should-be-visible',
|
|
400
|
+
matcher: (step: ParsedStep) =>
|
|
401
|
+
(step.text.includes('should be visible') ||
|
|
402
|
+
step.text.includes('should be displayed') ||
|
|
403
|
+
step.text.includes('should see') ||
|
|
404
|
+
step.text.match(/\b(see|sees)\s+\[/)) &&
|
|
405
|
+
!!step.selectorRef,
|
|
406
|
+
resolver: (step, context): StepTemplateData => {
|
|
407
|
+
const resolved = context.selectorResolver.resolveSelector(step.selectorRef!, undefined, step.elementType, step.nth);
|
|
408
|
+
return {
|
|
409
|
+
templateName: 'visible-assertion',
|
|
410
|
+
data: { ...resolved, selectorRef: step.selectorRef },
|
|
411
|
+
comment: `Assert ${step.selectorRef} is visible`,
|
|
412
|
+
};
|
|
413
|
+
},
|
|
414
|
+
priority: 10,
|
|
415
|
+
},
|
|
416
|
+
{
|
|
417
|
+
name: 'is-hidden',
|
|
418
|
+
// "see [target] is hidden" — no variable version; with-variable handled by see-with-variable-hidden
|
|
419
|
+
matcher: (step: ParsedStep) =>
|
|
420
|
+
/\b(see|sees)\s+\[/.test(step.text) && /\bis hidden\b/.test(step.text) &&
|
|
421
|
+
!step.dataRef && !!step.selectorRef,
|
|
422
|
+
resolver: (step, context): StepTemplateData => {
|
|
423
|
+
const resolved = context.selectorResolver.resolveSelector(step.selectorRef!, undefined, step.elementType, step.nth);
|
|
424
|
+
return {
|
|
425
|
+
templateName: 'is-hidden-assertion',
|
|
426
|
+
data: { ...resolved, selectorRef: step.selectorRef },
|
|
427
|
+
comment: `Assert ${step.selectorRef} is not visible`,
|
|
428
|
+
};
|
|
429
|
+
},
|
|
430
|
+
priority: 11,
|
|
431
|
+
},
|
|
432
|
+
{
|
|
433
|
+
name: 'should-be-enabled',
|
|
434
|
+
matcher: (step: ParsedStep) =>
|
|
435
|
+
/\b(see|sees)\s+\[/.test(step.text) && /\bis enabled\b/.test(step.text) && !!step.selectorRef,
|
|
436
|
+
resolver: (step, context): StepTemplateData => {
|
|
437
|
+
const resolved = context.selectorResolver.resolveSelector(step.selectorRef!, undefined, step.elementType, step.nth);
|
|
438
|
+
return {
|
|
439
|
+
templateName: 'enabled-assertion',
|
|
440
|
+
data: { ...resolved, selectorRef: step.selectorRef },
|
|
441
|
+
comment: `Assert ${step.selectorRef} is enabled`,
|
|
442
|
+
};
|
|
443
|
+
},
|
|
444
|
+
priority: 11,
|
|
445
|
+
},
|
|
446
|
+
{
|
|
447
|
+
name: 'should-be-disabled',
|
|
448
|
+
matcher: (step: ParsedStep) =>
|
|
449
|
+
/\b(see|sees)\s+\[/.test(step.text) && /\bis disabled\b/.test(step.text) && !!step.selectorRef,
|
|
450
|
+
resolver: (step, context): StepTemplateData => {
|
|
451
|
+
const resolved = context.selectorResolver.resolveSelector(step.selectorRef!, undefined, step.elementType, step.nth);
|
|
452
|
+
return {
|
|
453
|
+
templateName: 'disabled-assertion',
|
|
454
|
+
data: { ...resolved, selectorRef: step.selectorRef },
|
|
455
|
+
comment: `Assert ${step.selectorRef} is disabled`,
|
|
456
|
+
};
|
|
457
|
+
},
|
|
458
|
+
priority: 11,
|
|
459
|
+
},
|
|
460
|
+
{
|
|
461
|
+
name: 'should-contain-text',
|
|
462
|
+
matcher: (step: ParsedStep) =>
|
|
463
|
+
(step.text.includes('should contain') || step.text.includes('contains')) && !!step.selectorRef && !!(step.value || step.dataRef),
|
|
464
|
+
resolver: (step, context): StepTemplateData => {
|
|
465
|
+
const resolved = context.selectorResolver.resolveSelector(step.selectorRef!, undefined, step.elementType, step.nth);
|
|
466
|
+
const expectedText = step.value || context.dataResolver.resolveData(step.dataRef!, context.featureName);
|
|
467
|
+
return {
|
|
468
|
+
templateName: 'contain-text-assertion',
|
|
469
|
+
data: { ...resolved, expectedText },
|
|
470
|
+
comment: `Assert ${step.selectorRef} contains "${step.value || step.dataRef}"`,
|
|
471
|
+
};
|
|
472
|
+
},
|
|
473
|
+
priority: 12,
|
|
474
|
+
},
|
|
475
|
+
{
|
|
476
|
+
name: 'should-have-text',
|
|
477
|
+
matcher: (step: ParsedStep) =>
|
|
478
|
+
(step.text.includes('should have text') || step.text.includes('has text')) && !!step.selectorRef && !!(step.value || step.dataRef),
|
|
479
|
+
resolver: (step, context): StepTemplateData => {
|
|
480
|
+
const resolved = context.selectorResolver.resolveSelector(step.selectorRef!, undefined, step.elementType, step.nth);
|
|
481
|
+
const expectedText = step.value || context.dataResolver.resolveData(step.dataRef!, context.featureName);
|
|
482
|
+
return {
|
|
483
|
+
templateName: 'have-text-assertion',
|
|
484
|
+
data: { ...resolved, expectedText },
|
|
485
|
+
comment: `Assert ${step.selectorRef} has text "${step.value || step.dataRef}"`,
|
|
486
|
+
};
|
|
487
|
+
},
|
|
488
|
+
priority: 12,
|
|
489
|
+
},
|
|
490
|
+
{
|
|
491
|
+
name: 'is-empty',
|
|
492
|
+
matcher: (step: ParsedStep) =>
|
|
493
|
+
/\b(see|sees)\s+\[/.test(step.text) && /\bis empty\b/.test(step.text) && !!step.selectorRef,
|
|
494
|
+
resolver: (step, context): StepTemplateData => {
|
|
495
|
+
const resolved = context.selectorResolver.resolveSelector(step.selectorRef!, undefined, step.elementType, step.nth);
|
|
496
|
+
return {
|
|
497
|
+
templateName: 'empty-assertion',
|
|
498
|
+
data: { ...resolved, selectorRef: step.selectorRef },
|
|
499
|
+
comment: `Assert ${step.selectorRef} is empty`,
|
|
500
|
+
};
|
|
501
|
+
},
|
|
502
|
+
priority: 11,
|
|
503
|
+
},
|
|
504
|
+
{
|
|
505
|
+
name: 'is-checked',
|
|
506
|
+
matcher: (step: ParsedStep) =>
|
|
507
|
+
/\b(see|sees)\s+\[/.test(step.text) && /\bis checked\b/.test(step.text) && !!step.selectorRef,
|
|
508
|
+
resolver: (step, context): StepTemplateData => {
|
|
509
|
+
const resolved = context.selectorResolver.resolveSelector(step.selectorRef!, undefined, step.elementType, step.nth);
|
|
510
|
+
return {
|
|
511
|
+
templateName: 'checked-assertion',
|
|
512
|
+
data: { ...resolved, selectorRef: step.selectorRef },
|
|
513
|
+
comment: `Assert ${step.selectorRef} is checked`,
|
|
514
|
+
};
|
|
515
|
+
},
|
|
516
|
+
priority: 11,
|
|
517
|
+
},
|
|
518
|
+
{
|
|
519
|
+
name: 'is-unchecked',
|
|
520
|
+
matcher: (step: ParsedStep) =>
|
|
521
|
+
/\b(see|sees)\s+\[/.test(step.text) && /\bis unchecked\b/.test(step.text) && !!step.selectorRef,
|
|
522
|
+
resolver: (step, context): StepTemplateData => {
|
|
523
|
+
const resolved = context.selectorResolver.resolveSelector(step.selectorRef!, undefined, step.elementType, step.nth);
|
|
524
|
+
return {
|
|
525
|
+
templateName: 'not-checked-assertion',
|
|
526
|
+
data: { ...resolved, selectorRef: step.selectorRef },
|
|
527
|
+
comment: `Assert ${step.selectorRef} is unchecked`,
|
|
528
|
+
};
|
|
529
|
+
},
|
|
530
|
+
priority: 11,
|
|
531
|
+
},
|
|
532
|
+
{
|
|
533
|
+
name: 'is-focused',
|
|
534
|
+
matcher: (step: ParsedStep) =>
|
|
535
|
+
/\b(see|sees)\s+\[/.test(step.text) && /\bis focused\b/.test(step.text) && !!step.selectorRef,
|
|
536
|
+
resolver: (step, context): StepTemplateData => {
|
|
537
|
+
const resolved = context.selectorResolver.resolveSelector(step.selectorRef!, undefined, step.elementType, step.nth);
|
|
538
|
+
return {
|
|
539
|
+
templateName: 'focused-assertion',
|
|
540
|
+
data: { ...resolved, selectorRef: step.selectorRef },
|
|
541
|
+
comment: `Assert ${step.selectorRef} is focused`,
|
|
542
|
+
};
|
|
543
|
+
},
|
|
544
|
+
priority: 11,
|
|
545
|
+
},
|
|
546
|
+
{
|
|
547
|
+
name: 'see-data-text',
|
|
548
|
+
matcher: (step: ParsedStep) =>
|
|
549
|
+
(step.text.includes('should see') ||
|
|
550
|
+
step.text.match(/\b(see|sees)\b/)) &&
|
|
551
|
+
!step.selectorRef &&
|
|
552
|
+
!!step.dataRef,
|
|
553
|
+
resolver: (step, context): StepTemplateData => {
|
|
554
|
+
// Resolve data reference to actual value
|
|
555
|
+
let dataValue: string;
|
|
556
|
+
try {
|
|
557
|
+
dataValue = context.dataResolver.resolveData(step.dataRef!, context.featureName);
|
|
558
|
+
} catch (error) {
|
|
559
|
+
dataValue = `\${${step.dataRef}}`;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
return {
|
|
563
|
+
templateName: 'visible-assertion',
|
|
564
|
+
data: {
|
|
565
|
+
strategy: 'text',
|
|
566
|
+
value: dataValue,
|
|
567
|
+
},
|
|
568
|
+
comment: `Assert ${step.dataRef} is visible`,
|
|
569
|
+
};
|
|
570
|
+
},
|
|
571
|
+
priority: 5, // Lower than selector-based assertions
|
|
572
|
+
},
|
|
573
|
+
{
|
|
574
|
+
name: 'list-item-count',
|
|
575
|
+
matcher: (step: ParsedStep) =>
|
|
576
|
+
step.text.includes('should have') && step.text.includes('count') &&
|
|
577
|
+
!!step.selectorRef && step.text.includes('items') &&
|
|
578
|
+
(step.elementType === 'list' || step.elementType === 'list-item' || step.elementType === 'listitem'),
|
|
579
|
+
resolver: (step, context): StepTemplateData => {
|
|
580
|
+
let expectedCount: number | string = 1;
|
|
581
|
+
|
|
582
|
+
const match = step.text.match(/count\s+(\d+)/);
|
|
583
|
+
if (match) {
|
|
584
|
+
expectedCount = parseInt(match[1]);
|
|
585
|
+
} else if (step.dataRef) {
|
|
586
|
+
try {
|
|
587
|
+
const resolvedData = context.dataResolver.resolveData(step.dataRef, context.featureName);
|
|
588
|
+
const parsed = parseInt(resolvedData);
|
|
589
|
+
expectedCount = isNaN(parsed) ? 1 : parsed;
|
|
590
|
+
} catch {
|
|
591
|
+
expectedCount = `\${${step.dataRef}}`;
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
const resolved = context.selectorResolver.resolveSelector(step.selectorRef!, undefined, step.elementType, step.nth);
|
|
596
|
+
return {
|
|
597
|
+
templateName: 'list-item-count-assertion',
|
|
598
|
+
data: { ...resolved, expectedCount },
|
|
599
|
+
comment: `Assert ${step.selectorRef} list has ${expectedCount} items`,
|
|
600
|
+
};
|
|
601
|
+
},
|
|
602
|
+
priority: 9, // Higher than should-have-count (8)
|
|
603
|
+
},
|
|
604
|
+
{
|
|
605
|
+
name: 'should-have-count',
|
|
606
|
+
matcher: (step: ParsedStep) =>
|
|
607
|
+
step.text.includes('should have') && step.text.includes('count') && !!step.selectorRef,
|
|
608
|
+
resolver: (step, context): StepTemplateData => {
|
|
609
|
+
let expectedCount: number | string = 1;
|
|
610
|
+
|
|
611
|
+
// Try literal digit first: "count 10"
|
|
612
|
+
const match = step.text.match(/count\s+(\d+)/);
|
|
613
|
+
if (match) {
|
|
614
|
+
expectedCount = parseInt(match[1]);
|
|
615
|
+
} else if (step.dataRef) {
|
|
616
|
+
// Resolve data reference: "count {{number_items}}"
|
|
617
|
+
try {
|
|
618
|
+
const resolvedData = context.dataResolver.resolveData(step.dataRef, context.featureName);
|
|
619
|
+
const parsed = parseInt(resolvedData);
|
|
620
|
+
expectedCount = isNaN(parsed) ? 1 : parsed;
|
|
621
|
+
} catch {
|
|
622
|
+
expectedCount = `\${${step.dataRef}}`;
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
const resolved = context.selectorResolver.resolveSelector(step.selectorRef!, undefined, step.elementType, step.nth);
|
|
627
|
+
return {
|
|
628
|
+
templateName: 'count-assertion',
|
|
629
|
+
data: { ...resolved, expectedCount },
|
|
630
|
+
comment: `Assert ${step.selectorRef} has count ${expectedCount}`,
|
|
631
|
+
};
|
|
632
|
+
},
|
|
633
|
+
priority: 8,
|
|
634
|
+
},
|
|
635
|
+
{
|
|
636
|
+
name: 'is-loading',
|
|
637
|
+
matcher: (step: ParsedStep) =>
|
|
638
|
+
/\b(see|sees)\s+\[/.test(step.text) && /\bis loading\b/.test(step.text) && !!step.selectorRef,
|
|
639
|
+
resolver: (step, context): StepTemplateData => {
|
|
640
|
+
const resolved = context.selectorResolver.resolveSelector(step.selectorRef!, undefined, step.elementType, step.nth);
|
|
641
|
+
return {
|
|
642
|
+
templateName: 'loading-assertion',
|
|
643
|
+
data: { ...resolved, selectorRef: step.selectorRef },
|
|
644
|
+
comment: `Assert ${step.selectorRef} is loading`,
|
|
645
|
+
};
|
|
646
|
+
},
|
|
647
|
+
priority: 11,
|
|
648
|
+
},
|
|
649
|
+
{
|
|
650
|
+
name: 'is-selected',
|
|
651
|
+
matcher: (step: ParsedStep) =>
|
|
652
|
+
/\b(see|sees)\s+\[/.test(step.text) && /\bis selected\b/.test(step.text) && !!step.selectorRef,
|
|
653
|
+
resolver: (step, context): StepTemplateData => {
|
|
654
|
+
const resolved = context.selectorResolver.resolveSelector(step.selectorRef!, undefined, step.elementType, step.nth);
|
|
655
|
+
return {
|
|
656
|
+
templateName: 'selected-assertion',
|
|
657
|
+
data: { ...resolved, selectorRef: step.selectorRef },
|
|
658
|
+
comment: `Assert ${step.selectorRef} is selected`,
|
|
659
|
+
};
|
|
660
|
+
},
|
|
661
|
+
priority: 11,
|
|
662
|
+
},
|
|
663
|
+
{
|
|
664
|
+
name: 'is-sorted-ascending',
|
|
665
|
+
matcher: (step: ParsedStep) =>
|
|
666
|
+
/\b(see|sees)\s+\[/.test(step.text) && /\bsorted ascending\b/.test(step.text) && !!step.selectorRef,
|
|
667
|
+
resolver: (step, context): StepTemplateData => {
|
|
668
|
+
const resolved = context.selectorResolver.resolveSelector(step.selectorRef!, undefined, step.elementType, step.nth);
|
|
669
|
+
return {
|
|
670
|
+
templateName: 'sorted-assertion',
|
|
671
|
+
data: { ...resolved, selectorRef: step.selectorRef, sortDirection: 'ascending' },
|
|
672
|
+
comment: `Assert ${step.selectorRef} is sorted ascending`,
|
|
673
|
+
};
|
|
674
|
+
},
|
|
675
|
+
priority: 12,
|
|
676
|
+
},
|
|
677
|
+
{
|
|
678
|
+
name: 'is-sorted-descending',
|
|
679
|
+
matcher: (step: ParsedStep) =>
|
|
680
|
+
/\b(see|sees)\s+\[/.test(step.text) && /\bsorted descending\b/.test(step.text) && !!step.selectorRef,
|
|
681
|
+
resolver: (step, context): StepTemplateData => {
|
|
682
|
+
const resolved = context.selectorResolver.resolveSelector(step.selectorRef!, undefined, step.elementType, step.nth);
|
|
683
|
+
return {
|
|
684
|
+
templateName: 'sorted-assertion',
|
|
685
|
+
data: { ...resolved, selectorRef: step.selectorRef, sortDirection: 'descending' },
|
|
686
|
+
comment: `Assert ${step.selectorRef} is sorted descending`,
|
|
687
|
+
};
|
|
688
|
+
},
|
|
689
|
+
priority: 12,
|
|
690
|
+
},
|
|
691
|
+
];
|