@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,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scroll Patterns
|
|
3
|
+
* Handles: scroll to [Target] type
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { StepPattern } from '@sun-asterisk/sungen';
|
|
7
|
+
|
|
8
|
+
export const scrollPatterns: StepPattern[] = [
|
|
9
|
+
{
|
|
10
|
+
name: 'scroll-to-element',
|
|
11
|
+
matcher: (step) => {
|
|
12
|
+
return /\bscroll(?:s)?\s+to\b/i.test(step.text) && !!step.selectorRef;
|
|
13
|
+
},
|
|
14
|
+
resolver: (step, context) => {
|
|
15
|
+
const resolved = context.selectorResolver.resolveSelector(
|
|
16
|
+
step.selectorRef!, context.featureName, step.elementType, step.nth
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
return {
|
|
20
|
+
templateName: 'scroll-action',
|
|
21
|
+
data: { ...resolved },
|
|
22
|
+
comment: `Scroll to ${step.selectorRef}`,
|
|
23
|
+
};
|
|
24
|
+
},
|
|
25
|
+
priority: 8,
|
|
26
|
+
},
|
|
27
|
+
];
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { ParsedStep } from '@sun-asterisk/sungen';
|
|
2
|
+
import { StepPattern } from '@sun-asterisk/sungen';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Setup and precondition patterns: application setup, authentication state, etc.
|
|
6
|
+
*/
|
|
7
|
+
export const setupPatterns: StepPattern[] = [
|
|
8
|
+
{
|
|
9
|
+
name: 'application-running',
|
|
10
|
+
matcher: (step: ParsedStep) =>
|
|
11
|
+
step.text.includes('application is running') ||
|
|
12
|
+
step.text.includes('application running') ||
|
|
13
|
+
step.text.includes('app is running'),
|
|
14
|
+
resolver: (step, context) => ({
|
|
15
|
+
templateName: 'application-running',
|
|
16
|
+
data: {},
|
|
17
|
+
comment: `Setup: Application is running`,
|
|
18
|
+
}),
|
|
19
|
+
priority: 5,
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
name: 'user-not-logged-in',
|
|
23
|
+
matcher: (step: ParsedStep) =>
|
|
24
|
+
step.text.includes('user is not logged in') ||
|
|
25
|
+
step.text.includes('not logged in') ||
|
|
26
|
+
step.text.includes('logged out'),
|
|
27
|
+
resolver: (step, context) => ({
|
|
28
|
+
templateName: 'clear-auth',
|
|
29
|
+
data: {},
|
|
30
|
+
comment: `Clear authentication state (ensure user is logged out)`,
|
|
31
|
+
}),
|
|
32
|
+
priority: 9,
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
name: 'user-logged-in',
|
|
36
|
+
matcher: (step: ParsedStep) =>
|
|
37
|
+
(step.text.includes('user is logged in') ||
|
|
38
|
+
step.text.includes('logged in as')) &&
|
|
39
|
+
!step.text.includes('not logged in'),
|
|
40
|
+
resolver: (step, context) => {
|
|
41
|
+
const userRef = step.dataRef || 'valid_user';
|
|
42
|
+
return {
|
|
43
|
+
templateName: 'user-login-todo',
|
|
44
|
+
data: { userRef },
|
|
45
|
+
comment: `Setup: User logged in as ${userRef}`,
|
|
46
|
+
};
|
|
47
|
+
},
|
|
48
|
+
priority: 9,
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
name: 'clear-database',
|
|
52
|
+
matcher: (step: ParsedStep) =>
|
|
53
|
+
step.text.includes('database is empty') ||
|
|
54
|
+
step.text.includes('database is cleared') ||
|
|
55
|
+
step.text.includes('clear database'),
|
|
56
|
+
resolver: (step, context) => ({
|
|
57
|
+
templateName: 'clear-database',
|
|
58
|
+
data: {},
|
|
59
|
+
comment: `Setup: Clear/reset database`,
|
|
60
|
+
}),
|
|
61
|
+
priority: 7,
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
name: 'browser-state-clean',
|
|
65
|
+
matcher: (step: ParsedStep) =>
|
|
66
|
+
step.text.includes('clean browser state') ||
|
|
67
|
+
step.text.includes('fresh browser') ||
|
|
68
|
+
step.text.includes('new browser session'),
|
|
69
|
+
resolver: (step, context) => ({
|
|
70
|
+
templateName: 'clear-browser-state',
|
|
71
|
+
data: {},
|
|
72
|
+
comment: `Clear browser state (cookies, storage)`,
|
|
73
|
+
}),
|
|
74
|
+
priority: 8,
|
|
75
|
+
},
|
|
76
|
+
];
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Table Patterns
|
|
3
|
+
* Handles: table row assertions, table cell lookups, actions in table rows
|
|
4
|
+
*
|
|
5
|
+
* Syntax (v2.4 final):
|
|
6
|
+
* - User see [Col] column in [Table] table # column exists
|
|
7
|
+
* - User see [Ref] row in [Table] table with {{value}} # row exists (enters row scope)
|
|
8
|
+
* - User see [Ref] row in [Table] table with {{value}} is hidden # row hidden
|
|
9
|
+
* - User see [Table] table with {{count}} # row count
|
|
10
|
+
* - User see [Table] table is empty # empty table
|
|
11
|
+
* - User see [Col] column with {{value}} # cell value (row scoped — handled in step-mapper)
|
|
12
|
+
* - User click [Act] button in [Table] table with {{filter}} # action in row
|
|
13
|
+
* - User see [Table] table match data: # exact table match with inline DataTable
|
|
14
|
+
* | Header1 | Header2 |
|
|
15
|
+
* | {{value1}} | {{value2}} |
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { StepPattern, PatternContext } from '@sun-asterisk/sungen';
|
|
19
|
+
|
|
20
|
+
/** Helper: check if current step is in a Given context */
|
|
21
|
+
function isGivenContext(context: PatternContext): boolean {
|
|
22
|
+
return context.effectiveKeyword === 'Given';
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Map Gherkin element type to Playwright ARIA role */
|
|
26
|
+
const typeToRole: Record<string, string> = {
|
|
27
|
+
button: 'button',
|
|
28
|
+
link: 'link',
|
|
29
|
+
checkbox: 'checkbox',
|
|
30
|
+
icon: 'img',
|
|
31
|
+
image: 'img',
|
|
32
|
+
radio: 'radio',
|
|
33
|
+
switch: 'switch',
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export const tablePatterns: StepPattern[] = [
|
|
37
|
+
// "User see [Table] table with {{count}}" — row count (no "rows" keyword needed)
|
|
38
|
+
{
|
|
39
|
+
name: 'table-row-count',
|
|
40
|
+
matcher: (step) => {
|
|
41
|
+
return step.elementType === 'table' &&
|
|
42
|
+
/\btable\s+with\b/i.test(step.text) &&
|
|
43
|
+
!!step.dataRef &&
|
|
44
|
+
!/\brow\b/i.test(step.text) &&
|
|
45
|
+
!/\bcolumn\b/i.test(step.text) &&
|
|
46
|
+
!/\bempty\b/i.test(step.text);
|
|
47
|
+
},
|
|
48
|
+
resolver: (step, context) => {
|
|
49
|
+
const resolved = context.selectorResolver.resolveSelector(
|
|
50
|
+
step.selectorRef!, context.featureName, 'table', step.nth
|
|
51
|
+
);
|
|
52
|
+
const count = context.dataResolver.resolveData(step.dataRef!, context.featureName);
|
|
53
|
+
|
|
54
|
+
const isGiven = isGivenContext(context);
|
|
55
|
+
return {
|
|
56
|
+
templateName: 'table-row-count',
|
|
57
|
+
data: { ...resolved, expectedCount: count, isGiven },
|
|
58
|
+
comment: `${isGiven ? 'Wait' : 'Assert'} ${step.selectorRef} table has ${count} rows`,
|
|
59
|
+
};
|
|
60
|
+
},
|
|
61
|
+
priority: 16,
|
|
62
|
+
},
|
|
63
|
+
|
|
64
|
+
// "User see [Col] column in [Table] table"
|
|
65
|
+
{
|
|
66
|
+
name: 'table-column-exists',
|
|
67
|
+
matcher: (step) => {
|
|
68
|
+
return /\bcolumn\s+in\b/i.test(step.text) &&
|
|
69
|
+
/\btable\b/i.test(step.text) &&
|
|
70
|
+
!step.dataRef;
|
|
71
|
+
},
|
|
72
|
+
resolver: (step, context) => {
|
|
73
|
+
// brackets[0] = Col, brackets[1] = Table
|
|
74
|
+
const brackets = step.text.match(/\[([^\]]+)\]/g) || [];
|
|
75
|
+
const columnName = brackets[0]?.replace(/[\[\]]/g, '') || 'Unknown';
|
|
76
|
+
const tableName = brackets.length >= 2
|
|
77
|
+
? brackets[1].replace(/[\[\]]/g, '')
|
|
78
|
+
: brackets[0]?.replace(/[\[\]]/g, '') || '';
|
|
79
|
+
|
|
80
|
+
const resolved = context.selectorResolver.resolveSelector(
|
|
81
|
+
tableName, context.featureName, 'table', step.nth
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
const isGiven = isGivenContext(context);
|
|
85
|
+
return {
|
|
86
|
+
templateName: 'table-column-exists',
|
|
87
|
+
data: { ...resolved, columnName, isGiven },
|
|
88
|
+
comment: `${isGiven ? 'Wait' : 'Assert'} ${columnName} column in ${tableName} table`,
|
|
89
|
+
};
|
|
90
|
+
},
|
|
91
|
+
priority: 16,
|
|
92
|
+
},
|
|
93
|
+
|
|
94
|
+
// "User see [Table] table is empty"
|
|
95
|
+
{
|
|
96
|
+
name: 'table-is-empty',
|
|
97
|
+
matcher: (step) => {
|
|
98
|
+
return /\btable\s+is\s+empty\b/i.test(step.text);
|
|
99
|
+
},
|
|
100
|
+
resolver: (step, context) => {
|
|
101
|
+
const resolved = context.selectorResolver.resolveSelector(
|
|
102
|
+
step.selectorRef!, context.featureName, 'table', step.nth
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
const isGiven = isGivenContext(context);
|
|
106
|
+
return {
|
|
107
|
+
templateName: 'table-empty',
|
|
108
|
+
data: { ...resolved, isGiven },
|
|
109
|
+
comment: `${isGiven ? 'Wait' : 'Assert'} ${step.selectorRef} table is empty`,
|
|
110
|
+
};
|
|
111
|
+
},
|
|
112
|
+
priority: 16,
|
|
113
|
+
},
|
|
114
|
+
|
|
115
|
+
// "User see [Ref] row in [Table] table with {{value}}" — row exists (enters row scope)
|
|
116
|
+
{
|
|
117
|
+
name: 'table-row-exists',
|
|
118
|
+
matcher: (step) => {
|
|
119
|
+
return step.elementType === 'row' &&
|
|
120
|
+
/\bin\b.*\btable\b/i.test(step.text) &&
|
|
121
|
+
/\bwith\b/i.test(step.text) &&
|
|
122
|
+
!!step.dataRef &&
|
|
123
|
+
!/\bis\s+hidden\b/i.test(step.text);
|
|
124
|
+
},
|
|
125
|
+
resolver: (step, context) => {
|
|
126
|
+
// brackets[0] = Ref (row label), brackets[1] = Table
|
|
127
|
+
const brackets = step.text.match(/\[([^\]]+)\]/g) || [];
|
|
128
|
+
const rowLabel = brackets[0]?.replace(/[\[\]]/g, '') || '';
|
|
129
|
+
const tableName = brackets.length >= 2
|
|
130
|
+
? brackets[1].replace(/[\[\]]/g, '')
|
|
131
|
+
: '';
|
|
132
|
+
|
|
133
|
+
const resolved = context.selectorResolver.resolveSelector(
|
|
134
|
+
tableName, context.featureName, 'table', step.nth
|
|
135
|
+
);
|
|
136
|
+
const filterValue = context.dataResolver.resolveData(step.dataRef!, context.featureName);
|
|
137
|
+
|
|
138
|
+
const isGiven = isGivenContext(context);
|
|
139
|
+
return {
|
|
140
|
+
templateName: 'table-row-exists',
|
|
141
|
+
data: { ...resolved, filterValue, isGiven },
|
|
142
|
+
comment: `${isGiven ? 'Wait' : 'Assert'} ${rowLabel} row in ${tableName} table with ${step.dataRef}`,
|
|
143
|
+
};
|
|
144
|
+
},
|
|
145
|
+
priority: 17,
|
|
146
|
+
},
|
|
147
|
+
|
|
148
|
+
// "User see [Ref] row in [Table] table with {{value}} is hidden"
|
|
149
|
+
{
|
|
150
|
+
name: 'table-row-hidden',
|
|
151
|
+
matcher: (step) => {
|
|
152
|
+
return step.elementType === 'row' &&
|
|
153
|
+
/\bin\b.*\btable\b/i.test(step.text) &&
|
|
154
|
+
/\bwith\b/i.test(step.text) &&
|
|
155
|
+
!!step.dataRef &&
|
|
156
|
+
/\bis\s+hidden\b/i.test(step.text);
|
|
157
|
+
},
|
|
158
|
+
resolver: (step, context) => {
|
|
159
|
+
// brackets[0] = Ref (row label), brackets[1] = Table
|
|
160
|
+
const brackets = step.text.match(/\[([^\]]+)\]/g) || [];
|
|
161
|
+
const rowLabel = brackets[0]?.replace(/[\[\]]/g, '') || '';
|
|
162
|
+
const tableName = brackets.length >= 2
|
|
163
|
+
? brackets[1].replace(/[\[\]]/g, '')
|
|
164
|
+
: '';
|
|
165
|
+
|
|
166
|
+
const resolved = context.selectorResolver.resolveSelector(
|
|
167
|
+
tableName, context.featureName, 'table', step.nth
|
|
168
|
+
);
|
|
169
|
+
const filterValue = context.dataResolver.resolveData(step.dataRef!, context.featureName);
|
|
170
|
+
|
|
171
|
+
const isGiven = isGivenContext(context);
|
|
172
|
+
return {
|
|
173
|
+
templateName: 'table-row-not-exists',
|
|
174
|
+
data: { ...resolved, filterValue, isGiven },
|
|
175
|
+
comment: `${isGiven ? 'Wait' : 'Assert'} ${rowLabel} row in ${tableName} table is hidden`,
|
|
176
|
+
};
|
|
177
|
+
},
|
|
178
|
+
priority: 19,
|
|
179
|
+
},
|
|
180
|
+
|
|
181
|
+
// "User click [Act] button in [Table] table with {{filter}}" — action in row
|
|
182
|
+
{
|
|
183
|
+
name: 'table-action-in-row',
|
|
184
|
+
matcher: (step) => {
|
|
185
|
+
return /\bin\b.*\btable\b/i.test(step.text) &&
|
|
186
|
+
/\bwith\b/i.test(step.text) &&
|
|
187
|
+
!!step.dataRef &&
|
|
188
|
+
step.elementType !== 'column' &&
|
|
189
|
+
step.elementType !== 'row' &&
|
|
190
|
+
step.elementType !== 'table';
|
|
191
|
+
},
|
|
192
|
+
resolver: (step, context) => {
|
|
193
|
+
// brackets[0] = Act (element name), brackets[1] = Table
|
|
194
|
+
const brackets = step.text.match(/\[([^\]]+)\]/g) || [];
|
|
195
|
+
const elementName = brackets[0]?.replace(/[\[\]]/g, '') || '';
|
|
196
|
+
const tableName = brackets.length >= 2
|
|
197
|
+
? brackets[1].replace(/[\[\]]/g, '')
|
|
198
|
+
: '';
|
|
199
|
+
|
|
200
|
+
const filterValue = step.dataRef
|
|
201
|
+
? context.dataResolver.resolveData(step.dataRef, context.featureName)
|
|
202
|
+
: '';
|
|
203
|
+
|
|
204
|
+
const tableResolved = context.selectorResolver.resolveSelector(
|
|
205
|
+
tableName, context.featureName, 'table', 0
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
// Determine action from step text
|
|
209
|
+
const actionMatch = step.text.match(/\b(click|check|uncheck)\b/i);
|
|
210
|
+
const action = actionMatch ? actionMatch[1].toLowerCase() : 'click';
|
|
211
|
+
|
|
212
|
+
// Extract element type (word after [Act]) and map to ARIA role
|
|
213
|
+
const elementTypeMatch = step.text.match(/\]\s+([\w-]+)\s+in\b/i);
|
|
214
|
+
const elementType = elementTypeMatch ? elementTypeMatch[1].toLowerCase() : 'button';
|
|
215
|
+
const elementRole = typeToRole[elementType] || elementType;
|
|
216
|
+
|
|
217
|
+
return {
|
|
218
|
+
templateName: 'table-action-in-row',
|
|
219
|
+
data: { ...tableResolved, elementName, filterValue, action, elementRole },
|
|
220
|
+
comment: `${action} ${elementName} in ${tableName} table row with ${step.dataRef}`,
|
|
221
|
+
};
|
|
222
|
+
},
|
|
223
|
+
priority: 17,
|
|
224
|
+
},
|
|
225
|
+
|
|
226
|
+
// "User see [Table] table match data:" — filter-based table match with inline DataTable
|
|
227
|
+
// First row = headers, remaining rows = expected data
|
|
228
|
+
// Uses filter({ hasText }) per row — resilient to data changes, row ordering, extra rows
|
|
229
|
+
{
|
|
230
|
+
name: 'table-match-data',
|
|
231
|
+
matcher: (step) => {
|
|
232
|
+
return step.elementType === 'table' &&
|
|
233
|
+
/\btable\s+match\s+data\b/i.test(step.text) &&
|
|
234
|
+
!!step.dataTable;
|
|
235
|
+
},
|
|
236
|
+
resolver: (step, context) => {
|
|
237
|
+
const resolved = context.selectorResolver.resolveSelector(
|
|
238
|
+
step.selectorRef!, context.featureName, 'table', step.nth
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
const dataTable = step.dataTable!;
|
|
242
|
+
|
|
243
|
+
// Resolve {{variable}} references in cell values
|
|
244
|
+
const resolvedRows = dataTable.rows.map(row => ({
|
|
245
|
+
cells: row.cells.map(cell => {
|
|
246
|
+
const varMatch = cell.match(/^\{\{([a-z0-9\-\.\_]+)\}\}$/i);
|
|
247
|
+
if (varMatch) {
|
|
248
|
+
return context.dataResolver.resolveData(varMatch[1], context.featureName);
|
|
249
|
+
}
|
|
250
|
+
return cell;
|
|
251
|
+
}),
|
|
252
|
+
}));
|
|
253
|
+
|
|
254
|
+
// Pre-compute filter-based assertion lines
|
|
255
|
+
// For each expected row: chain .filter({ hasText }) for all cell values, then assert visible
|
|
256
|
+
const assertions: string[] = [];
|
|
257
|
+
for (const row of resolvedRows) {
|
|
258
|
+
const filters = row.cells
|
|
259
|
+
.map(cell => `.filter({ hasText: '${cell.replace(/'/g, "\\'")}' })`)
|
|
260
|
+
.join('');
|
|
261
|
+
assertions.push(
|
|
262
|
+
`await expect(rows${filters}).toBeVisible();`
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const isGiven = isGivenContext(context);
|
|
267
|
+
return {
|
|
268
|
+
templateName: 'table-match-data',
|
|
269
|
+
data: {
|
|
270
|
+
...resolved,
|
|
271
|
+
assertions,
|
|
272
|
+
isGiven,
|
|
273
|
+
},
|
|
274
|
+
comment: `${isGiven ? 'Wait' : 'Assert'} ${step.selectorRef} table contains ${resolvedRows.length} expected row(s)`,
|
|
275
|
+
};
|
|
276
|
+
},
|
|
277
|
+
priority: 20, // Higher than other table patterns to match first
|
|
278
|
+
},
|
|
279
|
+
];
|