@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.
- 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,97 @@
|
|
|
1
|
+
import { ParsedStep } from '@sun-asterisk/sungen';
|
|
2
|
+
import { StepPattern, StepTemplateData } from '@sun-asterisk/sungen';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Capture & collection patterns (P5) — enable cross-screen data consistency and
|
|
6
|
+
* filter-result correctness that plain single-element assertions can't express.
|
|
7
|
+
*
|
|
8
|
+
* 1. Capture: `User remember [Product Name] text as {{selected_product_name}}`
|
|
9
|
+
* → stores the element's text/value into a runtime variable so a
|
|
10
|
+
* later step (on another screen) can assert against it.
|
|
11
|
+
* REQUIRES runtime data mode (default) — emits `testData.set(...)`.
|
|
12
|
+
*
|
|
13
|
+
* 2. List: `User see all [Product Name] contain {{selected_category}}`
|
|
14
|
+
* → asserts EVERY matching element's text contains the value
|
|
15
|
+
* (e.g. all products belong to the selected category/brand).
|
|
16
|
+
*
|
|
17
|
+
* 3. All-card: `User see all [Product Card] contain [Add To Cart] button`
|
|
18
|
+
* → asserts EVERY container element holds the child element
|
|
19
|
+
* (structural per-card proof: every card has name/price/action).
|
|
20
|
+
* Roadmap Q2 — the fix for "each card exposes X" claims.
|
|
21
|
+
*/
|
|
22
|
+
export const capturePatterns: StepPattern[] = [
|
|
23
|
+
{
|
|
24
|
+
name: 'capture-variable',
|
|
25
|
+
matcher: (step: ParsedStep) =>
|
|
26
|
+
/\bremember\b/i.test(step.text) && /\bas\b/i.test(step.text) && !!step.selectorRef && !!step.dataRef,
|
|
27
|
+
resolver: (step, context): StepTemplateData => {
|
|
28
|
+
const resolved = context.selectorResolver.resolveSelector(
|
|
29
|
+
step.selectorRef!, undefined, step.elementType, step.nth,
|
|
30
|
+
);
|
|
31
|
+
const varName = step.dataRef!;
|
|
32
|
+
const isValue = /\bvalue\b/i.test(step.text);
|
|
33
|
+
// Register so later `{{varName}}` references resolve to testData.get(varName)
|
|
34
|
+
// and skip YAML validation.
|
|
35
|
+
context.dataResolver.registerCaptured(varName);
|
|
36
|
+
return {
|
|
37
|
+
templateName: 'capture-variable',
|
|
38
|
+
data: { ...resolved, varName, capture: isValue ? 'inputValue' : 'textContent' },
|
|
39
|
+
comment: `Remember ${step.selectorRef} ${isValue ? 'value' : 'text'} as ${varName}`,
|
|
40
|
+
};
|
|
41
|
+
},
|
|
42
|
+
priority: 35,
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
name: 'all-contain-assertion',
|
|
46
|
+
matcher: (step: ParsedStep) =>
|
|
47
|
+
/\b(see|sees)\b/i.test(step.text) &&
|
|
48
|
+
/\ball\b/i.test(step.text) &&
|
|
49
|
+
/(contain|contains|match|matches|belong)/i.test(step.text) &&
|
|
50
|
+
!!step.selectorRef && !!(step.value || step.dataRef),
|
|
51
|
+
resolver: (step, context): StepTemplateData => {
|
|
52
|
+
const resolved = context.selectorResolver.resolveSelector(
|
|
53
|
+
step.selectorRef!, undefined, step.elementType, step.nth,
|
|
54
|
+
);
|
|
55
|
+
const expectedText = step.value || context.dataResolver.resolveData(step.dataRef!, context.featureName);
|
|
56
|
+
return {
|
|
57
|
+
templateName: 'all-contain-assertion',
|
|
58
|
+
data: { ...resolved, expectedText, selectorRef: step.selectorRef, stepCounter: context.stepCounter },
|
|
59
|
+
comment: `Assert all ${step.selectorRef} contain "${step.value || step.dataRef}"`,
|
|
60
|
+
};
|
|
61
|
+
},
|
|
62
|
+
priority: 34,
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
// Q2 — all-card structural assertion: every container holds the child element.
|
|
66
|
+
// Distinguished from `all-contain-assertion` by a second [ref] (childRef) and the
|
|
67
|
+
// ABSENCE of a value/data (which would make it a text-contains assertion instead).
|
|
68
|
+
name: 'all-contain-element',
|
|
69
|
+
matcher: (step: ParsedStep) =>
|
|
70
|
+
/\b(see|sees)\b/i.test(step.text) &&
|
|
71
|
+
/\ball\b/i.test(step.text) &&
|
|
72
|
+
/(contain|contains|include|includes)/i.test(step.text) &&
|
|
73
|
+
!!step.selectorRef && !!step.childRef && !step.value && !step.dataRef,
|
|
74
|
+
resolver: (step, context): StepTemplateData => {
|
|
75
|
+
const container = context.selectorResolver.resolveSelector(
|
|
76
|
+
step.selectorRef!, undefined, step.elementType, step.nth,
|
|
77
|
+
);
|
|
78
|
+
const child = context.selectorResolver.resolveSelector(
|
|
79
|
+
step.childRef!, undefined, step.childElementType, 0,
|
|
80
|
+
);
|
|
81
|
+
const sc = context.stepCounter;
|
|
82
|
+
return {
|
|
83
|
+
templateName: 'all-contain-element',
|
|
84
|
+
data: {
|
|
85
|
+
...container,
|
|
86
|
+
selectorRef: step.selectorRef,
|
|
87
|
+
childRef: step.childRef,
|
|
88
|
+
stepCounter: sc,
|
|
89
|
+
// Render the child RELATIVE to each container row via locator.hbs's parentLocator branch.
|
|
90
|
+
child: { ...child, parentLocator: `__cards_${sc}.nth(__i_${sc})` },
|
|
91
|
+
},
|
|
92
|
+
comment: `Assert all ${step.selectorRef} contain [${step.childRef}]`,
|
|
93
|
+
};
|
|
94
|
+
},
|
|
95
|
+
priority: 36,
|
|
96
|
+
},
|
|
97
|
+
];
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { ParsedStep } from '@sun-asterisk/sungen';
|
|
2
|
+
import { StepPattern, StepTemplateData } from '@sun-asterisk/sungen';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Form action patterns: fill, type, select, check, uncheck
|
|
6
|
+
*/
|
|
7
|
+
export const formPatterns: StepPattern[] = [
|
|
8
|
+
{
|
|
9
|
+
name: 'upload-file',
|
|
10
|
+
matcher: (step: ParsedStep) =>
|
|
11
|
+
(step.text.includes('fill') || step.text.includes('fills')) &&
|
|
12
|
+
step.elementType === 'uploader' &&
|
|
13
|
+
!!step.selectorRef &&
|
|
14
|
+
!!(step.dataRef || step.value),
|
|
15
|
+
resolver: (step, context) => {
|
|
16
|
+
const resolved = context.selectorResolver.resolveSelector(step.selectorRef!, undefined, step.elementType, step.nth);
|
|
17
|
+
|
|
18
|
+
let fileName: string;
|
|
19
|
+
if (step.dataRef) {
|
|
20
|
+
try {
|
|
21
|
+
fileName = context.dataResolver.resolveData(step.dataRef, context.featureName);
|
|
22
|
+
} catch (error) {
|
|
23
|
+
fileName = `\${${step.dataRef}}`;
|
|
24
|
+
}
|
|
25
|
+
} else {
|
|
26
|
+
fileName = step.value!;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return {
|
|
30
|
+
templateName: 'upload-action',
|
|
31
|
+
data: { ...resolved, selectorRef: step.selectorRef, fileName },
|
|
32
|
+
comment: `Upload file ${fileName} via ${step.selectorRef}`,
|
|
33
|
+
};
|
|
34
|
+
},
|
|
35
|
+
priority: 12,
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
name: 'fill-input',
|
|
39
|
+
matcher: (step: ParsedStep) =>
|
|
40
|
+
(step.text.includes('fills') || step.text.includes('fill') || step.text.includes('inputs') || step.text.includes('input')) && !!step.selectorRef && !!(step.dataRef || step.value),
|
|
41
|
+
resolver: (step, context) => {
|
|
42
|
+
const resolved = context.selectorResolver.resolveSelector(step.selectorRef!, undefined, step.elementType, step.nth);
|
|
43
|
+
|
|
44
|
+
let value: string;
|
|
45
|
+
if (step.dataRef) {
|
|
46
|
+
try {
|
|
47
|
+
value = context.dataResolver.resolveData(step.dataRef, context.featureName);
|
|
48
|
+
} catch (error) {
|
|
49
|
+
value = `\${${step.dataRef}}`;
|
|
50
|
+
}
|
|
51
|
+
} else {
|
|
52
|
+
value = step.value!;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Use pressSequentially template for contenteditable/rich-text editors (auto-detected by live-scan)
|
|
56
|
+
// or when Gherkin explicitly uses 'editor' element type
|
|
57
|
+
const isEditor = resolved.inputMethod === 'pressSequentially' || step.elementType === 'editor';
|
|
58
|
+
const templateName = isEditor ? 'fill-editor-action' : 'fill-action';
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
templateName,
|
|
62
|
+
data: { ...resolved, selectorRef: step.selectorRef, fillValue: value },
|
|
63
|
+
comment: isEditor
|
|
64
|
+
? `Fill rich text editor ${step.selectorRef} with ${step.dataRef || step.value}`
|
|
65
|
+
: `Fill ${step.selectorRef} with ${step.dataRef || step.value}`,
|
|
66
|
+
};
|
|
67
|
+
},
|
|
68
|
+
priority: 10,
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
name: 'check-checkbox',
|
|
72
|
+
matcher: (step: ParsedStep) =>
|
|
73
|
+
(step.text.includes('checks') || step.text.match(/\bcheck\b/)) && !!step.selectorRef,
|
|
74
|
+
resolver: (step, context) => {
|
|
75
|
+
const resolved = context.selectorResolver.resolveSelector(step.selectorRef!, undefined, step.elementType, step.nth);
|
|
76
|
+
return {
|
|
77
|
+
templateName: 'check-action',
|
|
78
|
+
data: { ...resolved, selectorRef: step.selectorRef },
|
|
79
|
+
comment: `Check ${step.selectorRef}`,
|
|
80
|
+
};
|
|
81
|
+
},
|
|
82
|
+
priority: 8,
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
name: 'uncheck-checkbox',
|
|
86
|
+
matcher: (step: ParsedStep) =>
|
|
87
|
+
(step.text.includes('unchecks') || step.text.match(/\buncheck\b/)) && !!step.selectorRef,
|
|
88
|
+
resolver: (step, context) => {
|
|
89
|
+
const resolved = context.selectorResolver.resolveSelector(step.selectorRef!, undefined, step.elementType, step.nth);
|
|
90
|
+
return {
|
|
91
|
+
templateName: 'uncheck-action',
|
|
92
|
+
data: { ...resolved, selectorRef: step.selectorRef },
|
|
93
|
+
comment: `Uncheck ${step.selectorRef}`,
|
|
94
|
+
};
|
|
95
|
+
},
|
|
96
|
+
priority: 8,
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
name: 'select-dropdown',
|
|
100
|
+
matcher: (step: ParsedStep) =>
|
|
101
|
+
(step.text.includes('selects') || step.text.match(/\bselect\b/)) &&
|
|
102
|
+
!!step.selectorRef &&
|
|
103
|
+
!!(step.dataRef || step.value),
|
|
104
|
+
resolver: (step, context): StepTemplateData => {
|
|
105
|
+
const resolved = context.selectorResolver.resolveSelector(step.selectorRef!, undefined, step.elementType, step.nth);
|
|
106
|
+
|
|
107
|
+
let value: string;
|
|
108
|
+
if (step.dataRef) {
|
|
109
|
+
try {
|
|
110
|
+
value = context.dataResolver.resolveData(step.dataRef, context.featureName);
|
|
111
|
+
} catch (error) {
|
|
112
|
+
value = `\${${step.dataRef}}`;
|
|
113
|
+
}
|
|
114
|
+
} else {
|
|
115
|
+
value = step.value!;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const isRadio = resolved.role === 'radio' || step.text.includes('radio');
|
|
119
|
+
const isCheckbox = resolved.role === 'checkbox' || step.text.includes('checkbox');
|
|
120
|
+
|
|
121
|
+
let templateName: string;
|
|
122
|
+
let data: Record<string, any>;
|
|
123
|
+
|
|
124
|
+
if (isRadio) {
|
|
125
|
+
templateName = 'radio-select-action';
|
|
126
|
+
data = { ...resolved, selectorRef: step.selectorRef, selectValue: value };
|
|
127
|
+
} else if (isCheckbox) {
|
|
128
|
+
templateName = 'check-action';
|
|
129
|
+
data = { ...resolved, selectorRef: step.selectorRef };
|
|
130
|
+
} else {
|
|
131
|
+
const nativeSelectRoles = ['combobox', 'listbox', 'select'];
|
|
132
|
+
// A native <select> is detected by its ARIA role, or by an explicit `variant: native`
|
|
133
|
+
// on the selector entry (lets a CSS/#id-located select opt into .selectOption()).
|
|
134
|
+
const isNativeSelect = nativeSelectRoles.includes(resolved.role) || resolved.variant === 'native';
|
|
135
|
+
|
|
136
|
+
if (isNativeSelect) {
|
|
137
|
+
templateName = 'select-action';
|
|
138
|
+
data = { ...resolved, selectorRef: step.selectorRef, selectValue: value };
|
|
139
|
+
} else {
|
|
140
|
+
templateName = 'click-select-action';
|
|
141
|
+
data = { ...resolved, selectorRef: step.selectorRef, selectValue: value };
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return {
|
|
146
|
+
templateName,
|
|
147
|
+
data,
|
|
148
|
+
comment: `Select ${step.dataRef || step.value} in ${step.selectorRef}`,
|
|
149
|
+
};
|
|
150
|
+
},
|
|
151
|
+
priority: 8,
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
name: 'clear-input',
|
|
155
|
+
matcher: (step: ParsedStep) =>
|
|
156
|
+
(step.text.includes('clears') || step.text.match(/\bclear\b/)) && !!step.selectorRef,
|
|
157
|
+
resolver: (step, context) => {
|
|
158
|
+
const resolved = context.selectorResolver.resolveSelector(step.selectorRef!, undefined, step.elementType, step.nth);
|
|
159
|
+
return {
|
|
160
|
+
templateName: 'clear-action',
|
|
161
|
+
data: { ...resolved, selectorRef: step.selectorRef },
|
|
162
|
+
comment: `Clear ${step.selectorRef}`,
|
|
163
|
+
};
|
|
164
|
+
},
|
|
165
|
+
priority: 7,
|
|
166
|
+
},
|
|
167
|
+
];
|