@sungen/driver-ui 3.2.13 → 3.2.15
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
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sungen/driver-ui",
|
|
3
|
-
"version": "3.2.
|
|
3
|
+
"version": "3.2.15",
|
|
4
4
|
"description": "Sungen UI capability (Playwright) — step patterns, viewpoint gate, and phase hooks. Plugs into @sun-asterisk/sungen via the capability SPI.",
|
|
5
5
|
"main": "src/index.ts",
|
|
6
6
|
"types": "src/index.ts",
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"author": "eqe team (engineer & quality) — Sun Asterisk",
|
|
14
14
|
"license": "MIT",
|
|
15
15
|
"dependencies": {
|
|
16
|
-
"@sun-asterisk/sungen": "3.2.
|
|
16
|
+
"@sun-asterisk/sungen": "3.2.15"
|
|
17
17
|
},
|
|
18
18
|
"peerDependencies": {
|
|
19
19
|
"@playwright/test": "^1.57.0"
|
|
@@ -6,6 +6,22 @@ import { pathToRegexSource } from '../utils/url-path-regex';
|
|
|
6
6
|
* Assertion patterns: visibility, text content, state, attributes
|
|
7
7
|
* Uses template engine for framework-agnostic code generation
|
|
8
8
|
*/
|
|
9
|
+
|
|
10
|
+
/** State qualifiers that may follow `with {{value}}` and need value-filtered handling
|
|
11
|
+
* (see-with-variable-state). `is disabled`/`is hidden` are EXCLUDED — they keep their
|
|
12
|
+
* dedicated -disabled/-hidden twins. `is unchecked` is tested before `is checked` so
|
|
13
|
+
* the substring never misclassifies. `selected` probes at runtime (aria-selected vs
|
|
14
|
+
* checked) exactly like selected-assertion. */
|
|
15
|
+
function qualifiedState(text: string): { assertion: string; label: string; probe?: boolean } | null {
|
|
16
|
+
if (/\bis enabled\b/.test(text)) return { assertion: 'toBeEnabled()', label: 'enabled' };
|
|
17
|
+
if (/\bis unchecked\b/.test(text)) return { assertion: 'toBeChecked({ checked: false })', label: 'unchecked' };
|
|
18
|
+
if (/\bis checked\b/.test(text)) return { assertion: 'toBeChecked()', label: 'checked' };
|
|
19
|
+
if (/\bis focused\b/.test(text)) return { assertion: 'toBeFocused()', label: 'focused' };
|
|
20
|
+
if (/\bis empty\b/.test(text)) return { assertion: 'toBeEmpty()', label: 'empty' };
|
|
21
|
+
if (/\bis selected\b/.test(text)) return { assertion: '', label: 'selected', probe: true };
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
|
|
9
25
|
export const assertionPatterns: StepPattern[] = [
|
|
10
26
|
// Browser alert text assertion: "see [Are you sure?] alert"
|
|
11
27
|
{
|
|
@@ -245,6 +261,60 @@ export const assertionPatterns: StepPattern[] = [
|
|
|
245
261
|
return { code, comment: `Assert ${step.selectorRef} disabled with ${step.dataRef}` };
|
|
246
262
|
},
|
|
247
263
|
priority: 14,
|
|
264
|
+
},
|
|
265
|
+
// Pattern: Then User see [Time Slot] option with {{time}} is enabled|selected|checked|…
|
|
266
|
+
// The value-qualified STATE family. Mirrors see-with-variable-hidden/-disabled (both p14):
|
|
267
|
+
// without this twin the generic see-with-variable (p13) swallowed the step and emitted a
|
|
268
|
+
// bare toHaveText — the state qualifier was silently DROPPED, and on a multi-match
|
|
269
|
+
// `type: locator` selector (e.g. a 96-slot time picker) the unfiltered locator also
|
|
270
|
+
// strict-mode-failed. Filter by the EXACT value first, then assert the state.
|
|
271
|
+
// `is disabled` / `is hidden` keep their existing dedicated twins — not matched here.
|
|
272
|
+
{
|
|
273
|
+
name: 'see-with-variable-state',
|
|
274
|
+
matcher: (step: ParsedStep) =>
|
|
275
|
+
(step.text.includes('should see') || !!step.text.match(/\b(see|sees)\s+\[/)) &&
|
|
276
|
+
!!step.selectorRef &&
|
|
277
|
+
!!step.dataRef &&
|
|
278
|
+
step.text.includes('with') &&
|
|
279
|
+
qualifiedState(step.text) !== null,
|
|
280
|
+
generator: (step, context) => {
|
|
281
|
+
const state = qualifiedState(step.text)!;
|
|
282
|
+
|
|
283
|
+
let resolved: any = {};
|
|
284
|
+
try {
|
|
285
|
+
resolved = context.selectorResolver.resolveSelector(
|
|
286
|
+
step.selectorRef!,
|
|
287
|
+
context.featureName,
|
|
288
|
+
step.elementType,
|
|
289
|
+
step.nth
|
|
290
|
+
);
|
|
291
|
+
} catch (error) {}
|
|
292
|
+
|
|
293
|
+
let dataValue: string;
|
|
294
|
+
try {
|
|
295
|
+
dataValue = context.dataResolver.resolveData(step.dataRef!, context.featureName);
|
|
296
|
+
} catch (error) {
|
|
297
|
+
dataValue = `\${${step.dataRef}}`;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// "selected" mirrors selected-assertion: checkbox/radio/switch roles have no
|
|
301
|
+
// aria-selected — their state is `checked`; other targets probe at runtime.
|
|
302
|
+
let stateAssertion = state.assertion;
|
|
303
|
+
let selectedProbe = state.probe === true;
|
|
304
|
+
if (selectedProbe && resolved.role && ['checkbox', 'radio', 'switch'].includes(resolved.role)) {
|
|
305
|
+
stateAssertion = 'toBeChecked()';
|
|
306
|
+
selectedProbe = false;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const code = context.templateEngine.renderStep('state-with-filter-assertion', {
|
|
310
|
+
...resolved,
|
|
311
|
+
dataValue,
|
|
312
|
+
stateAssertion,
|
|
313
|
+
selectedProbe,
|
|
314
|
+
});
|
|
315
|
+
return { code, comment: `Assert ${step.selectorRef} with ${step.dataRef} is ${state.label}` };
|
|
316
|
+
},
|
|
317
|
+
priority: 14,
|
|
248
318
|
},
|
|
249
319
|
// NEW: Assertion with variable (handles both empty selector and static + variable)
|
|
250
320
|
// Pattern: Then User see [error] message with {{fail_message}}
|
|
@@ -366,6 +436,24 @@ export const assertionPatterns: StepPattern[] = [
|
|
|
366
436
|
}
|
|
367
437
|
|
|
368
438
|
// --- Everything else: toHaveText (exact full match) ---
|
|
439
|
+
// Locator-strategy LIST MEMBERS first: `see [X] item|option|cell with {{v}}` on a
|
|
440
|
+
// CSS-locator list (custom widgets with no role — e.g. a 96-slot time picker) means
|
|
441
|
+
// "a member whose text is v exists". A bare toHaveText on the multi-matching locator
|
|
442
|
+
// strict-mode-fails, so filter by the EXACT value then assert visible — the locator
|
|
443
|
+
// counterpart of the role-based branch below. Unique-element types (text/message/…)
|
|
444
|
+
// keep toHaveText: that is a VALUE assertion and must not be weakened to existence.
|
|
445
|
+
const LOCATOR_LIST_MEMBER_TYPES = new Set(['item', 'listitem', 'option', 'cell']);
|
|
446
|
+
if (resolved.strategy === 'locator' && step.elementType && LOCATOR_LIST_MEMBER_TYPES.has(step.elementType)) {
|
|
447
|
+
const code = context.templateEngine.renderStep('visible-filtered-assertion', {
|
|
448
|
+
...resolved,
|
|
449
|
+
dataValue,
|
|
450
|
+
exact: true,
|
|
451
|
+
});
|
|
452
|
+
return {
|
|
453
|
+
code,
|
|
454
|
+
comment: `Assert ${step.selectorRef} with text ${step.dataRef} is present`,
|
|
455
|
+
};
|
|
456
|
+
}
|
|
369
457
|
// Locator strategy
|
|
370
458
|
if (resolved.strategy === 'locator') {
|
|
371
459
|
const code = context.templateEngine.renderStep('have-text-assertion', {
|
|
@@ -136,11 +136,35 @@ export const tablePatterns: StepPattern[] = [
|
|
|
136
136
|
);
|
|
137
137
|
const filterValue = context.dataResolver.resolveData(step.dataRef!, context.featureName);
|
|
138
138
|
|
|
139
|
+
// Field report: the row label used to be DECORATIVE (comment-only) — a
|
|
140
|
+
// positional intent like `[First Contact Row]` silently degraded to an
|
|
141
|
+
// anywhere-in-the-table existence check, so ordering rules never failed.
|
|
142
|
+
// Now the label follows the same "resolve via selectors.yaml, fall back
|
|
143
|
+
// when absent" convention as every other [Ref]: a `type: locator` entry
|
|
144
|
+
// (RELATIVE to the table, e.g. `tbody tr:first-child`) positions the row,
|
|
145
|
+
// and the assertion becomes toContainText on that exact row. No entry →
|
|
146
|
+
// the historical filter-by-value behavior, byte-identical.
|
|
147
|
+
let rowSelector: string | undefined;
|
|
148
|
+
if (rowLabel && rowLabel !== tableName) {
|
|
149
|
+
try {
|
|
150
|
+
const rowResolved = context.selectorResolver.resolveSelector(
|
|
151
|
+
rowLabel, context.featureName, 'row', 0
|
|
152
|
+
);
|
|
153
|
+
if (rowResolved.strategy === 'locator' && rowResolved.value && rowResolved.value.trim()) {
|
|
154
|
+
rowSelector = rowResolved.value;
|
|
155
|
+
}
|
|
156
|
+
} catch {
|
|
157
|
+
// no selectors.yaml entry for the row label — display-only label (fallback)
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
139
161
|
const isGiven = isGivenContext(context);
|
|
140
162
|
return {
|
|
141
163
|
templateName: 'table-row-exists',
|
|
142
|
-
data: { ...resolved, filterValue, isGiven },
|
|
143
|
-
comment:
|
|
164
|
+
data: { ...resolved, filterValue, isGiven, rowSelector },
|
|
165
|
+
comment: rowSelector
|
|
166
|
+
? `${isGiven ? 'Wait' : 'Assert'} ${rowLabel} (positioned row) in ${tableName} table contains ${step.dataRef}`
|
|
167
|
+
: `${isGiven ? 'Wait' : 'Assert'} ${rowLabel} row in ${tableName} table with ${step.dataRef}`,
|
|
144
168
|
};
|
|
145
169
|
},
|
|
146
170
|
priority: 17,
|
|
@@ -310,27 +334,55 @@ export const tablePatterns: StepPattern[] = [
|
|
|
310
334
|
|
|
311
335
|
const dataTable = step.dataTable!;
|
|
312
336
|
|
|
313
|
-
// Resolve {{variable}} references in cell
|
|
314
|
-
const
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
}));
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
//
|
|
337
|
+
// Resolve {{variable}} references in a cell (headers included — i18n headers are legal)
|
|
338
|
+
const resolveCell = (cell: string): string => {
|
|
339
|
+
const varMatch = cell.match(/^\{\{([a-z0-9\-\.\_]+)\}\}$/i);
|
|
340
|
+
if (varMatch) {
|
|
341
|
+
return context.dataResolver.resolveData(varMatch[1], context.featureName);
|
|
342
|
+
}
|
|
343
|
+
return cell;
|
|
344
|
+
};
|
|
345
|
+
const headers = dataTable.headers.map(resolveCell);
|
|
346
|
+
const resolvedRows = dataTable.rows.map(row => ({ cells: row.cells.map(resolveCell) }));
|
|
347
|
+
const esc = (s: string) => s.replace(/'/g, "\\'");
|
|
348
|
+
|
|
349
|
+
// Header → cell mapping (field report: the header row was parsed and then IGNORED —
|
|
350
|
+
// rows were asserted as chained row-level hasText filters, so column-swapped data and
|
|
351
|
+
// duplicate values like '-' still passed). Now:
|
|
352
|
+
// 1. span guard — rowspan/colspan shifts ordinal mapping; fail loud instead of
|
|
353
|
+
// asserting the wrong cells (span support is a documented follow-up);
|
|
354
|
+
// 2. resolve each DataTable header to its DOM column index AT RUNTIME by header text
|
|
355
|
+
// (exact after trim, else a UNIQUE contains-match to tolerate sort icons); a
|
|
356
|
+
// missing/ambiguous header fails with the actual header row in the message —
|
|
357
|
+
// robust to extra DOM columns (leading checkbox…) that break compile-time nth;
|
|
358
|
+
// 3. locate each expected row by the chained co-occurrence filters (kept — resilient
|
|
359
|
+
// to row order and extra rows), then prove EVERY cell under its own header with
|
|
360
|
+
// an exact toHaveText. `td, th` ordinals count all DOM cells on both the header
|
|
361
|
+
// and body sides, so indexes stay aligned even with hidden columns.
|
|
326
362
|
const assertions: string[] = [];
|
|
363
|
+
assertions.push(
|
|
364
|
+
`await expect(table.locator('[rowspan]:not([rowspan="1"]), [colspan]:not([colspan="1"])'), 'table match data: rowspan/colspan tables are not supported yet — header-to-cell mapping would be wrong').toHaveCount(0);`
|
|
365
|
+
);
|
|
366
|
+
assertions.push(
|
|
367
|
+
`const __headers = (await table.locator('thead tr').first().locator('th, td').allTextContents()).map((t) => t.trim());`
|
|
368
|
+
);
|
|
369
|
+
assertions.push(
|
|
370
|
+
`const __colIdx = (h: string): number => { const i = __headers.indexOf(h); if (i !== -1) return i; const c = __headers.filter((t) => t.includes(h)); if (c.length === 1) return __headers.indexOf(c[0]); throw new Error(\`table match data: header "\${h}" \${c.length ? 'is ambiguous' : 'not found'} in table header row [\${__headers.join(' | ')}]\`); };`
|
|
371
|
+
);
|
|
327
372
|
for (const row of resolvedRows) {
|
|
328
373
|
const filters = row.cells
|
|
329
|
-
.map(cell => `.filter({ hasText: '${cell
|
|
374
|
+
.map(cell => `.filter({ hasText: '${esc(cell)}' })`)
|
|
330
375
|
.join('');
|
|
331
|
-
assertions.push(
|
|
332
|
-
|
|
333
|
-
)
|
|
376
|
+
assertions.push(`{`);
|
|
377
|
+
assertions.push(` const row = rows${filters};`);
|
|
378
|
+
row.cells.forEach((cell, i) => {
|
|
379
|
+
const header = headers[i];
|
|
380
|
+
if (header === undefined) return;
|
|
381
|
+
assertions.push(
|
|
382
|
+
` await expect(row.locator('td, th').nth(__colIdx('${esc(header)}'))).toHaveText('${esc(cell)}');`
|
|
383
|
+
);
|
|
384
|
+
});
|
|
385
|
+
assertions.push(`}`);
|
|
334
386
|
}
|
|
335
387
|
|
|
336
388
|
const isGiven = isGivenContext(context);
|