@sungen/driver-ui 3.1.2-beta.100

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.
@@ -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
+ ];