@sungen/driver-ui 3.2.12 → 3.2.14
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.14",
|
|
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.14"
|
|
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', {
|
|
@@ -378,6 +466,26 @@ export const assertionPatterns: StepPattern[] = [
|
|
|
378
466
|
};
|
|
379
467
|
}
|
|
380
468
|
|
|
469
|
+
// List-member roles: `see [X] item with {{v}}` on a dynamic list/dropdown means
|
|
470
|
+
// "an item whose text is v exists" — filter the (many-matching) locator by the value
|
|
471
|
+
// then assert visible, mirroring click-element-with-text. Plain toHaveText on the bare
|
|
472
|
+
// getByRole('listitem') strict-mode-fails when many match. SCOPED to role-based
|
|
473
|
+
// list-member roles only (listitem/option/cell) — unique labelled elements keep
|
|
474
|
+
// toHaveText (value check), and locator-type lists are unchanged. `row` is excluded
|
|
475
|
+
// (table rows have dedicated existence patterns).
|
|
476
|
+
const LIST_MEMBER_ROLES = new Set(['listitem', 'option', 'cell']);
|
|
477
|
+
if (resolved.strategy === 'role' && resolved.role && LIST_MEMBER_ROLES.has(resolved.role)) {
|
|
478
|
+
const { name: _name, ...resolvedWithoutName } = resolved;
|
|
479
|
+
const code = context.templateEngine.renderStep('visible-filtered-assertion', {
|
|
480
|
+
...resolvedWithoutName,
|
|
481
|
+
dataValue,
|
|
482
|
+
});
|
|
483
|
+
return {
|
|
484
|
+
code,
|
|
485
|
+
comment: `Assert ${step.selectorRef} with text ${step.dataRef} is present`,
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
|
|
381
489
|
// Role-based selector
|
|
382
490
|
if (resolved.strategy === 'role' && resolved.role) {
|
|
383
491
|
const hasName = resolved.name && resolved.name.trim();
|
|
@@ -229,6 +229,42 @@ export const interactionPatterns: StepPattern[] = [
|
|
|
229
229
|
},
|
|
230
230
|
priority: 11,
|
|
231
231
|
},
|
|
232
|
+
{
|
|
233
|
+
// `hover [X] item with {{v}}` — pick the element whose text matches the value, mirroring
|
|
234
|
+
// click-element-with-text. Without this, hover fell through to hover-element which IGNORED
|
|
235
|
+
// the value and hovered the first match (strict-mode / wrong element on dynamic lists).
|
|
236
|
+
name: 'hover-element-with-text',
|
|
237
|
+
matcher: (step: ParsedStep) =>
|
|
238
|
+
(step.text.includes('hovers') || step.text.match(/\bhover\b/))
|
|
239
|
+
&& !!step.selectorRef
|
|
240
|
+
&& !!(step.dataRef || step.value)
|
|
241
|
+
&& step.text.includes('with'),
|
|
242
|
+
resolver: (step, context) => {
|
|
243
|
+
const resolved = context.selectorResolver.resolveSelector(
|
|
244
|
+
step.selectorRef!, undefined, step.elementType, step.nth
|
|
245
|
+
);
|
|
246
|
+
|
|
247
|
+
let dataValue: string;
|
|
248
|
+
if (step.value) {
|
|
249
|
+
dataValue = step.value;
|
|
250
|
+
} else {
|
|
251
|
+
try {
|
|
252
|
+
dataValue = context.dataResolver.resolveData(step.dataRef!, context.featureName);
|
|
253
|
+
} catch {
|
|
254
|
+
dataValue = `\${${step.dataRef}}`;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const { name: _name, ...resolvedWithoutName } = resolved;
|
|
259
|
+
|
|
260
|
+
return {
|
|
261
|
+
templateName: 'hover-element-with-text',
|
|
262
|
+
data: { ...resolvedWithoutName, dataValue },
|
|
263
|
+
comment: `Hover ${step.selectorRef} with text ${step.dataRef || step.value}`,
|
|
264
|
+
};
|
|
265
|
+
},
|
|
266
|
+
priority: 9, // above hover-element (8) so `with {{v}}` filters instead of hovering the first
|
|
267
|
+
},
|
|
232
268
|
{
|
|
233
269
|
name: 'hover-element',
|
|
234
270
|
matcher: (step: ParsedStep) =>
|