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