@sungen/driver-ui 3.2.34-beta.1 → 3.3.0-alpha.1
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/dist/app-profile-presets.d.ts +12 -0
- package/dist/app-profile-presets.d.ts.map +1 -0
- package/dist/app-profile-presets.js +85 -0
- package/dist/app-profile-presets.js.map +1 -0
- package/dist/index.d.ts +37 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +93 -0
- package/dist/index.js.map +1 -0
- package/dist/patterns/assertion-patterns.d.ts +13 -0
- package/dist/patterns/assertion-patterns.d.ts.map +1 -0
- package/dist/patterns/assertion-patterns.js +903 -0
- package/dist/patterns/assertion-patterns.js.map +1 -0
- package/dist/patterns/capture-patterns.d.ts +21 -0
- package/dist/patterns/capture-patterns.d.ts.map +1 -0
- package/dist/patterns/capture-patterns.js +174 -0
- package/dist/patterns/capture-patterns.js.map +1 -0
- package/dist/patterns/focus-patterns.d.ts +22 -0
- package/dist/patterns/focus-patterns.d.ts.map +1 -0
- package/dist/patterns/focus-patterns.js +79 -0
- package/dist/patterns/focus-patterns.js.map +1 -0
- package/dist/patterns/form-patterns.d.ts +6 -0
- package/dist/patterns/form-patterns.d.ts.map +1 -0
- package/dist/patterns/form-patterns.js +239 -0
- package/dist/patterns/form-patterns.js.map +1 -0
- package/dist/patterns/interaction-patterns.d.ts +20 -0
- package/dist/patterns/interaction-patterns.d.ts.map +1 -0
- package/dist/patterns/interaction-patterns.js +633 -0
- package/dist/patterns/interaction-patterns.js.map +1 -0
- package/dist/patterns/keyboard-patterns.d.ts +7 -0
- package/dist/patterns/keyboard-patterns.d.ts.map +1 -0
- package/dist/patterns/keyboard-patterns.js +57 -0
- package/dist/patterns/keyboard-patterns.js.map +1 -0
- package/dist/patterns/navigation-patterns.d.ts +6 -0
- package/dist/patterns/navigation-patterns.d.ts.map +1 -0
- package/dist/patterns/navigation-patterns.js +185 -0
- package/dist/patterns/navigation-patterns.js.map +1 -0
- package/dist/patterns/scope-patterns.d.ts +7 -0
- package/dist/patterns/scope-patterns.d.ts.map +1 -0
- package/dist/patterns/scope-patterns.js +36 -0
- package/dist/patterns/scope-patterns.js.map +1 -0
- package/dist/patterns/scroll-patterns.d.ts +7 -0
- package/dist/patterns/scroll-patterns.d.ts.map +1 -0
- package/dist/patterns/scroll-patterns.js +25 -0
- package/dist/patterns/scroll-patterns.js.map +1 -0
- package/dist/patterns/setup-patterns.d.ts +6 -0
- package/dist/patterns/setup-patterns.d.ts.map +1 -0
- package/dist/patterns/setup-patterns.js +75 -0
- package/dist/patterns/setup-patterns.js.map +1 -0
- package/dist/patterns/storage-patterns.d.ts +26 -0
- package/dist/patterns/storage-patterns.d.ts.map +1 -0
- package/dist/patterns/storage-patterns.js +142 -0
- package/dist/patterns/storage-patterns.js.map +1 -0
- package/dist/patterns/table-pagination-patterns.d.ts +42 -0
- package/dist/patterns/table-pagination-patterns.d.ts.map +1 -0
- package/dist/patterns/table-pagination-patterns.js +93 -0
- package/dist/patterns/table-pagination-patterns.js.map +1 -0
- package/dist/patterns/table-patterns.d.ts +19 -0
- package/dist/patterns/table-patterns.d.ts.map +1 -0
- package/dist/patterns/table-patterns.js +594 -0
- package/dist/patterns/table-patterns.js.map +1 -0
- package/dist/trace-identity/aria-snapshot-head.d.ts +23 -0
- package/dist/trace-identity/aria-snapshot-head.d.ts.map +1 -0
- package/dist/trace-identity/aria-snapshot-head.js +26 -0
- package/dist/trace-identity/aria-snapshot-head.js.map +1 -0
- package/dist/trace-identity/playwright-identity-resolver.d.ts +10 -0
- package/dist/trace-identity/playwright-identity-resolver.d.ts.map +1 -0
- package/dist/trace-identity/playwright-identity-resolver.js +130 -0
- package/dist/trace-identity/playwright-identity-resolver.js.map +1 -0
- package/dist/trace-identity/trace-run-record-reporter.d.ts +39 -0
- package/dist/trace-identity/trace-run-record-reporter.d.ts.map +1 -0
- package/dist/trace-identity/trace-run-record-reporter.js +113 -0
- package/dist/trace-identity/trace-run-record-reporter.js.map +1 -0
- package/dist/utils/column-header.d.ts +37 -0
- package/dist/utils/column-header.d.ts.map +1 -0
- package/dist/utils/column-header.js +46 -0
- package/dist/utils/column-header.js.map +1 -0
- package/dist/utils/row-index.d.ts +18 -0
- package/dist/utils/row-index.d.ts.map +1 -0
- package/dist/utils/row-index.js +39 -0
- package/dist/utils/row-index.js.map +1 -0
- package/dist/utils/url-path-regex.d.ts +45 -0
- package/dist/utils/url-path-regex.d.ts.map +1 -0
- package/dist/utils/url-path-regex.js +68 -0
- package/dist/utils/url-path-regex.js.map +1 -0
- package/package.json +7 -3
- package/src/index.ts +5 -0
- package/src/trace-identity/aria-snapshot-head.ts +33 -0
- package/src/trace-identity/playwright-identity-resolver.ts +123 -0
- package/src/trace-identity/trace-run-record-reporter.ts +103 -0
|
@@ -0,0 +1,903 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.assertionPatterns = void 0;
|
|
4
|
+
const sungen_1 = require("@sun-asterisk/sungen");
|
|
5
|
+
const sungen_2 = require("@sun-asterisk/sungen");
|
|
6
|
+
const sungen_3 = require("@sun-asterisk/sungen");
|
|
7
|
+
const url_path_regex_1 = require("../utils/url-path-regex");
|
|
8
|
+
const column_header_1 = require("../utils/column-header");
|
|
9
|
+
/**
|
|
10
|
+
* Assertion patterns: visibility, text content, state, attributes
|
|
11
|
+
* Uses template engine for framework-agnostic code generation
|
|
12
|
+
*/
|
|
13
|
+
/** State qualifiers that may follow `with {{value}}` and need value-filtered handling
|
|
14
|
+
* (see-with-variable-state). `is disabled`/`is hidden` are EXCLUDED — they keep their
|
|
15
|
+
* dedicated -disabled/-hidden twins. `is unchecked` is tested before `is checked` so
|
|
16
|
+
* the substring never misclassifies. `selected` probes at runtime (aria-selected vs
|
|
17
|
+
* checked) exactly like selected-assertion. */
|
|
18
|
+
function qualifiedState(text) {
|
|
19
|
+
if (/\bis enabled\b/.test(text))
|
|
20
|
+
return { assertion: 'toBeEnabled()', label: 'enabled' };
|
|
21
|
+
if (/\bis unchecked\b/.test(text))
|
|
22
|
+
return { assertion: 'toBeChecked({ checked: false })', label: 'unchecked' };
|
|
23
|
+
if (/\bis checked\b/.test(text))
|
|
24
|
+
return { assertion: 'toBeChecked()', label: 'checked' };
|
|
25
|
+
if (/\bis focused\b/.test(text))
|
|
26
|
+
return { assertion: 'toBeFocused()', label: 'focused' };
|
|
27
|
+
if (/\bis empty\b/.test(text))
|
|
28
|
+
return { assertion: 'toBeEmpty()', label: 'empty' };
|
|
29
|
+
if (/\bis selected\b/.test(text))
|
|
30
|
+
return { assertion: '', label: 'selected', probe: true };
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
/** Locator-strategy element-type tokens that name a LIST MEMBER (many-matching by design —
|
|
34
|
+
* custom widgets with no role, e.g. a 96-slot time picker). Text assertions on these must
|
|
35
|
+
* filter the multi-locator by the value first: a bare toHaveText/toContainText on the raw
|
|
36
|
+
* locator strict-mode-fails. The role-strategy counterpart is LIST_MEMBER_ROLES. */
|
|
37
|
+
const LOCATOR_LIST_MEMBER_TYPES = new Set(['item', 'listitem', 'option', 'cell']);
|
|
38
|
+
/** Resolved strategies that pin a node by IDENTITY rather than by the text it renders. A
|
|
39
|
+
* `see [X] label with {{v}}` step honours these as real locators and asserts their text (#666).
|
|
40
|
+
*
|
|
41
|
+
* The omissions are deliberate, not an oversight: `text` finds the node by the very caption the
|
|
42
|
+
* label pattern is about, so honouring it would assert the caption contains the value; `label`
|
|
43
|
+
* (getByLabel) and `placeholder` resolve the labelled CONTROL, whose content is a `value`, not
|
|
44
|
+
* text; `role` names an element through its accessible name, which for a caption IS the caption.
|
|
45
|
+
* Each of those keeps the caption+value text reading. */
|
|
46
|
+
const IDENTITY_STRATEGIES = new Set([
|
|
47
|
+
'testid', 'id', 'css', 'locator', 'xpath',
|
|
48
|
+
'accessibility-id', 'android-uiautomator', 'ios-predicate',
|
|
49
|
+
]);
|
|
50
|
+
/**
|
|
51
|
+
* INVARIANT — a pattern whose generated code accounts for a trailing state clause (`… is hidden`)
|
|
52
|
+
* MUST declare it in `states`. Undeclared, the clause reaches SG-E021 and the step compiles to a
|
|
53
|
+
* `throw`; over-declared, the clause is silently dropped and the assertion reads as something it is
|
|
54
|
+
* not. Determine the truth from the TEMPLATE the resolver names, never from the matcher.
|
|
55
|
+
*
|
|
56
|
+
* Deliberately NOT declared, because each really does drop the clause today: `page-assertion`
|
|
57
|
+
* (a page has no state — this was the original defect), `wait-for-page`, `table-row-exists`,
|
|
58
|
+
* `table-column-exists`, `see-with-variable`, `alert-text-assertion`, `all-contain-element`.
|
|
59
|
+
*/
|
|
60
|
+
exports.assertionPatterns = [
|
|
61
|
+
// Browser alert text assertion: "see [Are you sure?] alert"
|
|
62
|
+
{
|
|
63
|
+
name: 'alert-text-assertion',
|
|
64
|
+
matcher: (step) => (step.text.includes('see') || step.text.includes('sees')) &&
|
|
65
|
+
step.elementType === 'alert' &&
|
|
66
|
+
!!step.selectorRef,
|
|
67
|
+
resolver: (step, context) => {
|
|
68
|
+
let dataValue = step.selectorRef || '';
|
|
69
|
+
if (step.dataRef) {
|
|
70
|
+
dataValue = context.dataResolver.resolveData(step.dataRef, context.featureName);
|
|
71
|
+
}
|
|
72
|
+
return {
|
|
73
|
+
templateName: 'alert-text-assertion',
|
|
74
|
+
data: { dataValue, stepCounter: context.stepCounter },
|
|
75
|
+
comment: `Assert browser alert contains "${step.selectorRef}"`,
|
|
76
|
+
};
|
|
77
|
+
},
|
|
78
|
+
priority: 21,
|
|
79
|
+
},
|
|
80
|
+
// Column cell assertion: "see [Department] column 1 with {{value}}" -> check table cell text
|
|
81
|
+
{
|
|
82
|
+
name: 'column-cell-assertion',
|
|
83
|
+
platforms: ['web'],
|
|
84
|
+
platformAlternative: 'HTML table roles have no native equivalent - assert the row element itself by accessibility-id (`Then User see [Row 2 Amount] has text {{v}}`), or tag the scenario @manual',
|
|
85
|
+
matcher: (step) => (step.text.includes('should see') || step.text.match(/\b(see|sees)\s+\[/)) &&
|
|
86
|
+
!!step.selectorRef &&
|
|
87
|
+
!!step.dataRef &&
|
|
88
|
+
(step.elementType === 'column' || step.elementType === 'columnheader'),
|
|
89
|
+
resolver: (step, context) => {
|
|
90
|
+
let dataValue;
|
|
91
|
+
dataValue = context.dataResolver.resolveData(step.dataRef, context.featureName);
|
|
92
|
+
// getByRole('row') includes header row as nth(0), so Gherkin nth=1 maps to locator nth(1)
|
|
93
|
+
const rowNth = step.nth || 1;
|
|
94
|
+
// Create a safe variable name from column name
|
|
95
|
+
const columnIndexVar = `colIndex${step.selectorRef.replace(/[^a-zA-Z0-9]/g, '')}`;
|
|
96
|
+
// The header TEXT this template matches on — both the visibility assertion and the
|
|
97
|
+
// findIndex scan — is declarable; the bracket label is only the fallback (issue #674).
|
|
98
|
+
// No table is named by this step form, so only the `<col>--columnheader` entry applies.
|
|
99
|
+
const columnName = (0, column_header_1.resolveColumnHeaderName)(step.selectorRef, context);
|
|
100
|
+
return {
|
|
101
|
+
templateName: 'column-cell-assertion',
|
|
102
|
+
data: {
|
|
103
|
+
columnName,
|
|
104
|
+
rowNth,
|
|
105
|
+
columnIndexVar,
|
|
106
|
+
dataValue,
|
|
107
|
+
},
|
|
108
|
+
comment: `Assert ${step.selectorRef} column row ${step.nth || 1} has text ${step.dataRef}`,
|
|
109
|
+
};
|
|
110
|
+
},
|
|
111
|
+
priority: 15, // Higher than all other assertion patterns
|
|
112
|
+
},
|
|
113
|
+
// Page assertion pattern: "see [home] page" -> check URL matches selector value
|
|
114
|
+
{
|
|
115
|
+
name: 'page-assertion',
|
|
116
|
+
matcher: (step) => (step.text.includes('should see') ||
|
|
117
|
+
step.text.includes('see') ||
|
|
118
|
+
step.text.includes('sees')) &&
|
|
119
|
+
step.elementType === 'page',
|
|
120
|
+
resolver: (step, context) => {
|
|
121
|
+
// Fallback is the feature's declared Path (context.featurePath) — the page under test.
|
|
122
|
+
// `step.featurePath` is never populated by the parser, so it always degraded to '/'.
|
|
123
|
+
const fallbackPath = context.featurePath || '/';
|
|
124
|
+
let path = fallbackPath;
|
|
125
|
+
// The page's declared `anchor:` — the element that proves the page rendered. Without it the
|
|
126
|
+
// step is a URL-only oracle, which a client-side router can satisfy for a few hundred
|
|
127
|
+
// milliseconds before a guard bounces the user away (#662).
|
|
128
|
+
let anchor;
|
|
129
|
+
// The page entry's `query: any` (#748) — set below, once the entry is known to BE a page.
|
|
130
|
+
let ignoreQuery = false;
|
|
131
|
+
// If selector is present, extract path from selector's value attribute
|
|
132
|
+
if (step.selectorRef) {
|
|
133
|
+
try {
|
|
134
|
+
const resolved = context.selectorResolver.resolveSelector(step.selectorRef, context.featureName, step.elementType, step.nth);
|
|
135
|
+
// Only a `page`-type entry carries a URL in `value`. Selector keys can collide across
|
|
136
|
+
// types (a single `読取結果一覧` entry of type `role` reused for a page assertion), and a
|
|
137
|
+
// key-only lookup happily returns that button entry — whose `value` ('button') is not a
|
|
138
|
+
// URL. Guard on the entry kind: use `value` only for a page selector, else fall back to
|
|
139
|
+
// the feature path rather than compiling `toHaveURL(/button/)`.
|
|
140
|
+
path = resolved.selectorType === 'page' ? (resolved.value || fallbackPath) : fallbackPath;
|
|
141
|
+
if (resolved.selectorType === 'page' && resolved.anchor)
|
|
142
|
+
anchor = { ...resolved.anchor };
|
|
143
|
+
// `query: any` — the query is not part of this page's identity (#748). Gated on the entry
|
|
144
|
+
// KIND for the same reason `value` and `anchor` are: a key collision can hand back a
|
|
145
|
+
// non-page entry, and reading a stray field off it would silently weaken the assertion.
|
|
146
|
+
if (resolved.selectorType === 'page' && resolved.query === 'any')
|
|
147
|
+
ignoreQuery = true;
|
|
148
|
+
}
|
|
149
|
+
catch (error) {
|
|
150
|
+
// Fallback to feature path if selector not found
|
|
151
|
+
path = fallbackPath;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
// `see [X] page with {{v}}` — the query/fragment value the parser put in dataRef. It used
|
|
155
|
+
// to be dropped here: the step compiled to a bare base-path toHaveURL that PASSES even when
|
|
156
|
+
// the query is wrong or gone, so a filter/search regression read as green. The `is on` twin
|
|
157
|
+
// has asserted it since issue #464; this is the same assertion, so it asserts the same way.
|
|
158
|
+
let query = '';
|
|
159
|
+
if (step.dataRef) {
|
|
160
|
+
query = context.dataResolver.resolveData(step.dataRef, context.featureName);
|
|
161
|
+
}
|
|
162
|
+
// Convert path to regex-safe string (dynamic :id segments → [^/]+ wildcard). An ABSOLUTE
|
|
163
|
+
// page value also yields a host pattern: matching only the path let this assertion pass on
|
|
164
|
+
// any host, so `see [Admin Jobs] page` declared as https://admin.example.com/jobs was
|
|
165
|
+
// satisfied by the public site's /jobs.
|
|
166
|
+
const { pathRegex, hostRegex } = (0, url_path_regex_1.urlToPatternParts)(path);
|
|
167
|
+
const withSuffix = step.dataRef ? ` with ${step.dataRef}` : '';
|
|
168
|
+
return {
|
|
169
|
+
templateName: 'page-assertion',
|
|
170
|
+
// `dataRef` rides along so a runtime failure can name the test-data key an author must go
|
|
171
|
+
// fix — an empty/absent value is otherwise the hardest kind of red to trace.
|
|
172
|
+
data: { pathRegex, hostRegex, query, dataRef: step.dataRef || '', anchor, ignoreQuery },
|
|
173
|
+
comment: step.selectorRef ? `Assert on ${step.selectorRef} page${withSuffix}` : `Assert on page${withSuffix}`,
|
|
174
|
+
};
|
|
175
|
+
},
|
|
176
|
+
priority: 13, // Higher priority than general visibility
|
|
177
|
+
},
|
|
178
|
+
// Pattern: Then User see [panel] dialog with {{kudo.title}} hidden -> toBeHidden()
|
|
179
|
+
{
|
|
180
|
+
name: 'see-with-variable-hidden',
|
|
181
|
+
states: ['hidden'],
|
|
182
|
+
matcher: (step) => (step.text.includes('should see') || step.text.match(/\b(see|sees)\s+\[/)) &&
|
|
183
|
+
!!step.selectorRef &&
|
|
184
|
+
!!step.dataRef &&
|
|
185
|
+
step.text.includes('with') &&
|
|
186
|
+
/\bis hidden\b/.test(step.text),
|
|
187
|
+
generator: (step, context) => {
|
|
188
|
+
let resolved = {};
|
|
189
|
+
try {
|
|
190
|
+
resolved = context.selectorResolver.resolveSelector(step.selectorRef, context.featureName, step.elementType, step.nth);
|
|
191
|
+
}
|
|
192
|
+
catch (error) { }
|
|
193
|
+
let dataValue;
|
|
194
|
+
dataValue = context.dataResolver.resolveData(step.dataRef, context.featureName);
|
|
195
|
+
const escapedVariable = dataValue.replace(/[.*+?^${}()|[\]\\/]/g, '\\$&');
|
|
196
|
+
if (resolved.strategy === 'locator') {
|
|
197
|
+
const code = context.templateEngine.renderStep('hidden-with-filter-assertion', {
|
|
198
|
+
...resolved, dataValue, nth: resolved.nth,
|
|
199
|
+
});
|
|
200
|
+
return { code, comment: `Assert ${step.selectorRef} hidden with ${step.dataRef}` };
|
|
201
|
+
}
|
|
202
|
+
if (resolved.strategy === 'role' && resolved.role) {
|
|
203
|
+
// Dialog role: check heading inside dialog instead of filter on full text content
|
|
204
|
+
if (resolved.role === 'dialog') {
|
|
205
|
+
const code = context.templateEngine.renderStep('hidden-dialog-heading-assertion', {
|
|
206
|
+
dataValue,
|
|
207
|
+
});
|
|
208
|
+
return { code, comment: `Assert ${step.selectorRef} dialog hidden with heading ${step.dataRef}` };
|
|
209
|
+
}
|
|
210
|
+
const code = context.templateEngine.renderStep('hidden-with-role-variable-assertion', {
|
|
211
|
+
role: resolved.role,
|
|
212
|
+
name: resolved.name && resolved.name.trim() ? resolved.name : undefined,
|
|
213
|
+
dataValue,
|
|
214
|
+
nth: resolved.nth,
|
|
215
|
+
});
|
|
216
|
+
return { code, comment: `Assert ${step.selectorRef} hidden with ${step.dataRef}` };
|
|
217
|
+
}
|
|
218
|
+
const selectorValue = resolved.value || '';
|
|
219
|
+
if (selectorValue && selectorValue.trim()) {
|
|
220
|
+
const code = context.templateEngine.renderStep('hidden-with-filter-assertion', {
|
|
221
|
+
...resolved, dataValue, nth: resolved.nth,
|
|
222
|
+
});
|
|
223
|
+
return { code, comment: `Assert ${step.selectorRef} hidden with ${step.dataRef}` };
|
|
224
|
+
}
|
|
225
|
+
const code = context.templateEngine.renderStep('hidden-with-variable-assertion', {
|
|
226
|
+
selectorRef: step.selectorRef,
|
|
227
|
+
selectorValue: resolved.value || '',
|
|
228
|
+
dataValue,
|
|
229
|
+
nth: resolved.nth,
|
|
230
|
+
});
|
|
231
|
+
return { code, comment: `Assert ${step.selectorRef} hidden with ${step.dataRef}` };
|
|
232
|
+
},
|
|
233
|
+
priority: 14,
|
|
234
|
+
},
|
|
235
|
+
// Pattern: Then User see [submit] button with {{label}} disabled -> toBeDisabled()
|
|
236
|
+
{
|
|
237
|
+
name: 'see-with-variable-disabled',
|
|
238
|
+
states: ['disabled'],
|
|
239
|
+
matcher: (step) => (step.text.includes('should see') || step.text.match(/\b(see|sees)\s+\[/)) &&
|
|
240
|
+
!!step.selectorRef &&
|
|
241
|
+
!!step.dataRef &&
|
|
242
|
+
step.text.includes('with') &&
|
|
243
|
+
/\bis disabled\b/.test(step.text),
|
|
244
|
+
generator: (step, context) => {
|
|
245
|
+
let resolved = {};
|
|
246
|
+
try {
|
|
247
|
+
resolved = context.selectorResolver.resolveSelector(step.selectorRef, context.featureName, step.elementType, step.nth);
|
|
248
|
+
}
|
|
249
|
+
catch (error) { }
|
|
250
|
+
let dataValue;
|
|
251
|
+
dataValue = context.dataResolver.resolveData(step.dataRef, context.featureName);
|
|
252
|
+
const escapedVariable = dataValue.replace(/[.*+?^${}()|[\]\\/]/g, '\\$&');
|
|
253
|
+
if (resolved.strategy === 'locator') {
|
|
254
|
+
const code = context.templateEngine.renderStep('disabled-with-filter-assertion', {
|
|
255
|
+
...resolved, dataValue, nth: resolved.nth,
|
|
256
|
+
});
|
|
257
|
+
return { code, comment: `Assert ${step.selectorRef} disabled with ${step.dataRef}` };
|
|
258
|
+
}
|
|
259
|
+
if (resolved.strategy === 'role' && resolved.role) {
|
|
260
|
+
const code = context.templateEngine.renderStep('disabled-with-role-variable-assertion', {
|
|
261
|
+
role: resolved.role,
|
|
262
|
+
name: resolved.name && resolved.name.trim() ? resolved.name : undefined,
|
|
263
|
+
dataValue,
|
|
264
|
+
nth: resolved.nth,
|
|
265
|
+
});
|
|
266
|
+
return { code, comment: `Assert ${step.selectorRef} disabled with ${step.dataRef}` };
|
|
267
|
+
}
|
|
268
|
+
const selectorValue = resolved.value || '';
|
|
269
|
+
if (selectorValue && selectorValue.trim()) {
|
|
270
|
+
const code = context.templateEngine.renderStep('disabled-with-filter-assertion', {
|
|
271
|
+
...resolved, dataValue, nth: resolved.nth,
|
|
272
|
+
});
|
|
273
|
+
return { code, comment: `Assert ${step.selectorRef} disabled with ${step.dataRef}` };
|
|
274
|
+
}
|
|
275
|
+
const code = context.templateEngine.renderStep('disabled-with-variable-assertion', {
|
|
276
|
+
selectorRef: step.selectorRef,
|
|
277
|
+
selectorValue: resolved.value || '',
|
|
278
|
+
dataValue,
|
|
279
|
+
nth: resolved.nth,
|
|
280
|
+
});
|
|
281
|
+
return { code, comment: `Assert ${step.selectorRef} disabled with ${step.dataRef}` };
|
|
282
|
+
},
|
|
283
|
+
priority: 14,
|
|
284
|
+
},
|
|
285
|
+
// Pattern: Then User see [Time Slot] option with {{time}} is enabled|selected|checked|…
|
|
286
|
+
// The value-qualified STATE family. Mirrors see-with-variable-hidden/-disabled (both p14):
|
|
287
|
+
// without this twin the generic see-with-variable (p13) swallowed the step and emitted a
|
|
288
|
+
// bare toHaveText — the state qualifier was silently DROPPED, and on a multi-match
|
|
289
|
+
// `type: locator` selector (e.g. a 96-slot time picker) the unfiltered locator also
|
|
290
|
+
// strict-mode-failed. Filter by the EXACT value first, then assert the state.
|
|
291
|
+
// `is disabled` / `is hidden` keep their existing dedicated twins — not matched here.
|
|
292
|
+
{
|
|
293
|
+
name: 'see-with-variable-state',
|
|
294
|
+
// Exactly the labels `qualifiedState()` above resolves — `hidden`/`disabled` are excluded
|
|
295
|
+
// there and owned by the -hidden/-disabled twins.
|
|
296
|
+
states: ['checked', 'empty', 'enabled', 'focused', 'selected', 'unchecked'],
|
|
297
|
+
matcher: (step) => (step.text.includes('should see') || !!step.text.match(/\b(see|sees)\s+\[/)) &&
|
|
298
|
+
!!step.selectorRef &&
|
|
299
|
+
!!step.dataRef &&
|
|
300
|
+
step.text.includes('with') &&
|
|
301
|
+
qualifiedState(step.text) !== null,
|
|
302
|
+
generator: (step, context) => {
|
|
303
|
+
const state = qualifiedState(step.text);
|
|
304
|
+
let resolved = {};
|
|
305
|
+
try {
|
|
306
|
+
resolved = context.selectorResolver.resolveSelector(step.selectorRef, context.featureName, step.elementType, step.nth);
|
|
307
|
+
}
|
|
308
|
+
catch (error) { }
|
|
309
|
+
let dataValue;
|
|
310
|
+
dataValue = context.dataResolver.resolveData(step.dataRef, context.featureName);
|
|
311
|
+
// "selected" mirrors selected-assertion: checkbox/radio/switch roles have no
|
|
312
|
+
// aria-selected — their state is `checked`; other targets probe at runtime.
|
|
313
|
+
let stateAssertion = state.assertion;
|
|
314
|
+
let selectedProbe = state.probe === true;
|
|
315
|
+
// A checked/unchecked assertion must NOT compile to toBeChecked() — it throws
|
|
316
|
+
// "Not a checkbox or radio button" on an aria-pressed toggle exactly as the action side did
|
|
317
|
+
// (#675). Flag it so the template can route it through expectCheckedState instead, which
|
|
318
|
+
// reads the same signals setCheckedState drives.
|
|
319
|
+
let isCheckedAssertion = state.label === 'checked' || state.label === 'unchecked';
|
|
320
|
+
let checkedWant = state.label === 'checked';
|
|
321
|
+
if (selectedProbe && resolved.role && ['checkbox', 'radio', 'switch'].includes(resolved.role)) {
|
|
322
|
+
stateAssertion = 'toBeChecked()';
|
|
323
|
+
selectedProbe = false;
|
|
324
|
+
isCheckedAssertion = true;
|
|
325
|
+
checkedWant = true;
|
|
326
|
+
}
|
|
327
|
+
const code = context.templateEngine.renderStep('state-with-filter-assertion', {
|
|
328
|
+
...resolved,
|
|
329
|
+
selectorRef: step.selectorRef,
|
|
330
|
+
dataValue,
|
|
331
|
+
stateAssertion,
|
|
332
|
+
selectedProbe,
|
|
333
|
+
isCheckedAssertion,
|
|
334
|
+
checkedWant,
|
|
335
|
+
});
|
|
336
|
+
return { code, comment: `Assert ${step.selectorRef} with ${step.dataRef} is ${state.label}` };
|
|
337
|
+
},
|
|
338
|
+
priority: 14,
|
|
339
|
+
},
|
|
340
|
+
// NEW: Assertion with variable (handles both empty selector and static + variable)
|
|
341
|
+
// Pattern: Then User see [error] message with {{fail_message}}
|
|
342
|
+
// If selector YAML has empty value, uses variable-only matching
|
|
343
|
+
// If selector has value, combines both (static text + variable)
|
|
344
|
+
// Input types → toHaveValue, everything else → toHaveText
|
|
345
|
+
{
|
|
346
|
+
name: 'see-with-variable',
|
|
347
|
+
matcher: (step) => (step.text.includes('should see') ||
|
|
348
|
+
step.text.match(/\b(see|sees)\s+\[/)) &&
|
|
349
|
+
!!step.selectorRef &&
|
|
350
|
+
!!step.dataRef &&
|
|
351
|
+
step.text.includes('with'),
|
|
352
|
+
generator: (step, context) => {
|
|
353
|
+
// Input element types use toHaveValue instead of toHaveText
|
|
354
|
+
const INPUT_TYPES = new Set([
|
|
355
|
+
'field', 'textarea', 'search', 'dropdown', 'slider', 'date-picker',
|
|
356
|
+
'input', 'textbox', 'editor', 'select', 'combobox',
|
|
357
|
+
]);
|
|
358
|
+
// Subset of INPUT_TYPES whose DOM implementation cannot be inferred from the token:
|
|
359
|
+
// a component library may render any of these as a non-input trigger element.
|
|
360
|
+
const AMBIGUOUS_VALUE_TYPES = new Set(['dropdown', 'select', 'combobox']);
|
|
361
|
+
// Resolve data variable value
|
|
362
|
+
let dataValue;
|
|
363
|
+
dataValue = context.dataResolver.resolveData(step.dataRef, context.featureName);
|
|
364
|
+
// Resolve selector from YAML with feature context
|
|
365
|
+
let resolved = {};
|
|
366
|
+
try {
|
|
367
|
+
resolved = context.selectorResolver.resolveSelector(step.selectorRef, context.featureName, step.elementType, step.nth);
|
|
368
|
+
}
|
|
369
|
+
catch (error) {
|
|
370
|
+
// Selector not in YAML or context issue - will use variable-only
|
|
371
|
+
}
|
|
372
|
+
// --- Attribute assertion: toHaveAttribute ---
|
|
373
|
+
// When selector YAML has `attribute` field (e.g., attribute: 'src', 'href')
|
|
374
|
+
if (resolved.attribute) {
|
|
375
|
+
const code = context.templateEngine.renderStep('attribute-assertion', {
|
|
376
|
+
...resolved,
|
|
377
|
+
dataValue,
|
|
378
|
+
});
|
|
379
|
+
return {
|
|
380
|
+
code,
|
|
381
|
+
comment: `Assert ${step.selectorRef} ${resolved.attribute} matches ${step.dataRef}`,
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
// --- Input types: toHaveValue ---
|
|
385
|
+
// `dropdown`/`select`/`combobox` are AMBIGUOUS: ARIA role `combobox` is worn both by a
|
|
386
|
+
// native <select> and by a `<button role="combobox">` trigger (Radix/shadcn, MUI,
|
|
387
|
+
// Headless UI). toHaveValue() reads the DOM `value` property, which a button has not got
|
|
388
|
+
// — hence the runtime "Not an input element". Resolution follows the app-profile layering
|
|
389
|
+
// (sungen_app_profile_spec.md §3): per-element `variant:` / app-profile recipe wins, and
|
|
390
|
+
// the built-in default probes the tag at runtime instead of assuming a native input.
|
|
391
|
+
if (step.elementType && INPUT_TYPES.has(step.elementType)) {
|
|
392
|
+
const ambiguous = (AMBIGUOUS_VALUE_TYPES.has(step.elementType) ||
|
|
393
|
+
['combobox', 'listbox'].includes(resolved.role)) &&
|
|
394
|
+
resolved.variant !== 'native';
|
|
395
|
+
if (ambiguous) {
|
|
396
|
+
// `assertValue` is the third recipe intent the app-profile spec defines alongside
|
|
397
|
+
// `fill`/`select` (spec §3.1). Same lookup shape as the select-dropdown fork.
|
|
398
|
+
const variantName = resolved.variant || context.appProfile?.widgetVariants?.['select'];
|
|
399
|
+
const recipeSteps = variantName && variantName !== 'native'
|
|
400
|
+
? context.appProfile?.recipes?.[variantName]?.['assertValue']
|
|
401
|
+
: undefined;
|
|
402
|
+
if (recipeSteps) {
|
|
403
|
+
try {
|
|
404
|
+
const steps = (0, sungen_2.expandRecipe)(recipeSteps, {
|
|
405
|
+
triggerCode: context.renderLocator(resolved),
|
|
406
|
+
value: dataValue,
|
|
407
|
+
});
|
|
408
|
+
return {
|
|
409
|
+
code: context.templateEngine.renderStep('recipe-sequence', { steps }),
|
|
410
|
+
comment: `Assert ${step.selectorRef} has value ${step.dataRef} (${variantName} recipe)`,
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
catch {
|
|
414
|
+
// malformed recipe -> fall through to the runtime probe below
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
const code = context.templateEngine.renderStep('have-value-assertion', {
|
|
419
|
+
...resolved,
|
|
420
|
+
dataValue,
|
|
421
|
+
probeNative: ambiguous,
|
|
422
|
+
});
|
|
423
|
+
return {
|
|
424
|
+
code,
|
|
425
|
+
comment: `Assert ${step.selectorRef} has value ${step.dataRef}`,
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
// --- Label-value pattern: "User see [X] label with {{Y}}" ---
|
|
429
|
+
if (step.elementType === 'label' && step.dataRef) {
|
|
430
|
+
// A `label` ref reads two different ways, and the selectors.yaml entry decides which.
|
|
431
|
+
//
|
|
432
|
+
// NO ENTRY, or an entry that finds its element BY TEXT — the ref names on-screen
|
|
433
|
+
// CAPTION text rather than an element, so the assertion is "the screen shows
|
|
434
|
+
// `X <value>`". That is what label-value-assertion renders, and it stays the default.
|
|
435
|
+
//
|
|
436
|
+
// AN ENTRY NAMING A CONCRETE NODE (xpath / accessibility-id / testid / id / css / …) —
|
|
437
|
+
// the author has said WHERE the value lives. That locator used to be resolved and then
|
|
438
|
+
// thrown away, leaving a page-wide text match: on Android `UiSelector().text(v)`, which
|
|
439
|
+
// reads only @text, so a Flutter/RN node exposing @content-desc never matched. The
|
|
440
|
+
// entry was correct and the step failed every time (#666). Honouring it puts `label`
|
|
441
|
+
// alongside `header`, which never had the special case.
|
|
442
|
+
//
|
|
443
|
+
// CONTAINS, not exact: an entry for a label/value pair points legitimately either at
|
|
444
|
+
// the value node ("JD-001") or at the row holding both ("Job No: JD-001"), and the web
|
|
445
|
+
// regex it replaces was itself anchored only at the end. `exact: true` opts into the
|
|
446
|
+
// strict comparison.
|
|
447
|
+
//
|
|
448
|
+
// `resolved` is the SAME lookup, already performed above — this branch used to repeat it.
|
|
449
|
+
if (IDENTITY_STRATEGIES.has(resolved.strategy)) {
|
|
450
|
+
const code = context.templateEngine.renderStep(resolved.exact ? 'have-text-assertion' : 'contain-text-assertion', { ...resolved, expectedText: dataValue });
|
|
451
|
+
return {
|
|
452
|
+
code,
|
|
453
|
+
comment: `Assert ${step.selectorRef} label ${resolved.exact ? 'has' : 'contains'} value ${step.dataRef}`,
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
// `value: ''` is the declared opt-out: drop the caption prefix, match the value alone.
|
|
457
|
+
const label = resolved.value !== undefined && resolved.value.trim() === ''
|
|
458
|
+
? undefined
|
|
459
|
+
: step.selectorRef;
|
|
460
|
+
// The locator here is built from the CAPTION, not from `resolved` — the common case has no
|
|
461
|
+
// selectors.yaml entry at all, so `resolved` is `{}` and carries nothing. The scope
|
|
462
|
+
// therefore comes off the context, which `withParentScope` sets whether or not the entry
|
|
463
|
+
// lookup succeeded; without it `… in [Education Card 6] card` was dropped silently and the
|
|
464
|
+
// assertion read whichever card the page happened to render first (#747).
|
|
465
|
+
const code = context.templateEngine.renderStep('label-value-assertion', {
|
|
466
|
+
label,
|
|
467
|
+
dataValue,
|
|
468
|
+
parentLocator: context.parentLocator,
|
|
469
|
+
});
|
|
470
|
+
return {
|
|
471
|
+
code,
|
|
472
|
+
comment: `Assert ${step.selectorRef} label with value ${step.dataRef}`,
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
// --- Dialog role: check heading inside dialog ---
|
|
476
|
+
if (resolved.strategy === 'role' && resolved.role === 'dialog') {
|
|
477
|
+
const code = context.templateEngine.renderStep('visible-dialog-heading-assertion', {
|
|
478
|
+
dataValue,
|
|
479
|
+
});
|
|
480
|
+
return {
|
|
481
|
+
code,
|
|
482
|
+
comment: `Assert ${step.selectorRef} dialog with heading ${step.dataRef}`,
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
// --- Image role: images have no text, use name ---
|
|
486
|
+
if (resolved.strategy === 'role' && resolved.role === 'img') {
|
|
487
|
+
const hasName = resolved.name && resolved.name.trim();
|
|
488
|
+
const code = context.templateEngine.renderStep('visible-with-role-variable-assertion', {
|
|
489
|
+
role: resolved.role,
|
|
490
|
+
name: hasName ? resolved.name : dataValue,
|
|
491
|
+
exact: resolved.exact || false,
|
|
492
|
+
nth: resolved.nth,
|
|
493
|
+
// Rebuilt field by field rather than spread from `resolved`, so the scope `withParentScope`
|
|
494
|
+
// stamped there never arrives — the label branch's twin defect (#747).
|
|
495
|
+
parentLocator: resolved.parentLocator ?? context.parentLocator,
|
|
496
|
+
});
|
|
497
|
+
return {
|
|
498
|
+
code,
|
|
499
|
+
comment: `Assert ${step.selectorRef} image with name ${step.dataRef}`,
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
// --- Everything else: toHaveText (exact full match) ---
|
|
503
|
+
// Locator-strategy LIST MEMBERS first: `see [X] item|option|cell with {{v}}` on a
|
|
504
|
+
// CSS-locator list means "a member whose text is v exists". A bare toHaveText on the
|
|
505
|
+
// multi-matching locator strict-mode-fails, so filter by the value then assert
|
|
506
|
+
// visible — the locator counterpart of the role-based branch below. The filter
|
|
507
|
+
// matches by SUBSTRING (Playwright's standard hasText) unless the selector YAML
|
|
508
|
+
// declares `exact: true` — the same contract as the role branch; a hardcoded exact
|
|
509
|
+
// here rejected members whose rendered text wraps the value (icons, counts, i18n
|
|
510
|
+
// suffixes) with no way to opt out. Unique-element types (text/message/…)
|
|
511
|
+
// keep toHaveText: that is a VALUE assertion and must not be weakened to existence.
|
|
512
|
+
if (resolved.strategy === 'locator' && step.elementType && LOCATOR_LIST_MEMBER_TYPES.has(step.elementType)) {
|
|
513
|
+
const code = context.templateEngine.renderStep('visible-filtered-assertion', {
|
|
514
|
+
...resolved,
|
|
515
|
+
dataValue,
|
|
516
|
+
});
|
|
517
|
+
return {
|
|
518
|
+
code,
|
|
519
|
+
comment: `Assert ${step.selectorRef} with text ${step.dataRef} is present`,
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
// Locator strategy
|
|
523
|
+
if (resolved.strategy === 'locator') {
|
|
524
|
+
const code = context.templateEngine.renderStep('have-text-assertion', {
|
|
525
|
+
...resolved,
|
|
526
|
+
expectedText: dataValue,
|
|
527
|
+
});
|
|
528
|
+
return {
|
|
529
|
+
code,
|
|
530
|
+
comment: `Assert ${step.selectorRef} has text ${step.dataRef}`,
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
// List-member roles: `see [X] item with {{v}}` on a dynamic list/dropdown means
|
|
534
|
+
// "an item whose text is v exists" — filter the (many-matching) locator by the value
|
|
535
|
+
// then assert visible, mirroring click-element-with-text. Plain toHaveText on the bare
|
|
536
|
+
// getByRole('listitem') strict-mode-fails when many match. SCOPED to role-based
|
|
537
|
+
// list-member roles only (listitem/option/cell) — unique labelled elements keep
|
|
538
|
+
// toHaveText (value check), and locator-type lists are unchanged. `row` is excluded
|
|
539
|
+
// (table rows have dedicated existence patterns).
|
|
540
|
+
// The name is NOT stripped here any more. It used to be dropped unconditionally, which also
|
|
541
|
+
// discarded a `name:` the user had declared explicitly — and the strip only ever protected
|
|
542
|
+
// THIS branch, leaving `is hidden` / `is disabled` to compile the invented name into a locator
|
|
543
|
+
// that matches nothing. The resolver now withholds the invented name at its source
|
|
544
|
+
// (LIST_MEMBER_ROLES in selector-resolver), so every branch is covered and an explicit name
|
|
545
|
+
// still reaches the locator. What stays here is the ROUTING: filter-then-visible instead of
|
|
546
|
+
// toHaveText, because a bare getByRole('listitem') strict-mode-fails when many match.
|
|
547
|
+
if (resolved.strategy === 'role' && resolved.role && sungen_3.LIST_MEMBER_ROLES.has(resolved.role)) {
|
|
548
|
+
const code = context.templateEngine.renderStep('visible-filtered-assertion', {
|
|
549
|
+
...resolved,
|
|
550
|
+
dataValue,
|
|
551
|
+
});
|
|
552
|
+
return {
|
|
553
|
+
code,
|
|
554
|
+
comment: `Assert ${step.selectorRef} with text ${step.dataRef} is present`,
|
|
555
|
+
};
|
|
556
|
+
}
|
|
557
|
+
// Role-based selector
|
|
558
|
+
if (resolved.strategy === 'role' && resolved.role) {
|
|
559
|
+
const hasName = resolved.name && resolved.name.trim();
|
|
560
|
+
const code = context.templateEngine.renderStep('have-text-assertion', {
|
|
561
|
+
...resolved,
|
|
562
|
+
expectedText: dataValue,
|
|
563
|
+
});
|
|
564
|
+
return {
|
|
565
|
+
code,
|
|
566
|
+
comment: `Assert ${step.selectorRef} has text ${step.dataRef}`,
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
// Text/placeholder/testid/other strategies
|
|
570
|
+
const code = context.templateEngine.renderStep('have-text-assertion', {
|
|
571
|
+
...resolved,
|
|
572
|
+
expectedText: dataValue,
|
|
573
|
+
});
|
|
574
|
+
return {
|
|
575
|
+
code,
|
|
576
|
+
comment: `Assert ${step.selectorRef} has text ${step.dataRef}`,
|
|
577
|
+
};
|
|
578
|
+
},
|
|
579
|
+
priority: 13,
|
|
580
|
+
},
|
|
581
|
+
{
|
|
582
|
+
name: 'should-be-visible',
|
|
583
|
+
// `is visible` is redundant (bare `see [X] type` already asserts it) but harmless — the review
|
|
584
|
+
// checklist flags the redundancy; the compiler must not turn it into a hard failure.
|
|
585
|
+
states: ['visible'],
|
|
586
|
+
matcher: (step) => (step.text.includes('should be visible') ||
|
|
587
|
+
step.text.includes('should be displayed') ||
|
|
588
|
+
step.text.includes('should see') ||
|
|
589
|
+
step.text.match(/\b(see|sees)\s+\[/)) &&
|
|
590
|
+
!!step.selectorRef,
|
|
591
|
+
resolver: (step, context) => {
|
|
592
|
+
const resolved = context.selectorResolver.resolveSelector(step.selectorRef, undefined, step.elementType, step.nth);
|
|
593
|
+
return {
|
|
594
|
+
templateName: 'visible-assertion',
|
|
595
|
+
data: { ...resolved, selectorRef: step.selectorRef },
|
|
596
|
+
comment: `Assert ${step.selectorRef} is visible`,
|
|
597
|
+
};
|
|
598
|
+
},
|
|
599
|
+
priority: 10,
|
|
600
|
+
},
|
|
601
|
+
{
|
|
602
|
+
name: 'is-hidden',
|
|
603
|
+
states: ['hidden'],
|
|
604
|
+
// "see [target] is hidden" — no variable version; with-variable handled by see-with-variable-hidden
|
|
605
|
+
matcher: (step) => /\b(see|sees)\s+\[/.test(step.text) && /\bis hidden\b/.test(step.text) &&
|
|
606
|
+
!step.dataRef && !!step.selectorRef,
|
|
607
|
+
resolver: (step, context) => {
|
|
608
|
+
const resolved = context.selectorResolver.resolveSelector(step.selectorRef, undefined, step.elementType, step.nth);
|
|
609
|
+
return {
|
|
610
|
+
templateName: 'is-hidden-assertion',
|
|
611
|
+
data: { ...resolved, selectorRef: step.selectorRef },
|
|
612
|
+
comment: `Assert ${step.selectorRef} is not visible`,
|
|
613
|
+
};
|
|
614
|
+
},
|
|
615
|
+
priority: 11,
|
|
616
|
+
},
|
|
617
|
+
{
|
|
618
|
+
name: 'should-be-enabled',
|
|
619
|
+
states: ['enabled'],
|
|
620
|
+
matcher: (step) => /\b(see|sees)\s+\[/.test(step.text) && /\bis enabled\b/.test(step.text) && !!step.selectorRef,
|
|
621
|
+
resolver: (step, context) => {
|
|
622
|
+
const resolved = context.selectorResolver.resolveSelector(step.selectorRef, undefined, step.elementType, step.nth);
|
|
623
|
+
return {
|
|
624
|
+
templateName: 'enabled-assertion',
|
|
625
|
+
data: { ...resolved, selectorRef: step.selectorRef },
|
|
626
|
+
comment: `Assert ${step.selectorRef} is enabled`,
|
|
627
|
+
};
|
|
628
|
+
},
|
|
629
|
+
priority: 11,
|
|
630
|
+
},
|
|
631
|
+
{
|
|
632
|
+
name: 'should-be-disabled',
|
|
633
|
+
states: ['disabled'],
|
|
634
|
+
matcher: (step) => /\b(see|sees)\s+\[/.test(step.text) && /\bis disabled\b/.test(step.text) && !!step.selectorRef,
|
|
635
|
+
resolver: (step, context) => {
|
|
636
|
+
const resolved = context.selectorResolver.resolveSelector(step.selectorRef, undefined, step.elementType, step.nth);
|
|
637
|
+
return {
|
|
638
|
+
templateName: 'disabled-assertion',
|
|
639
|
+
data: { ...resolved, selectorRef: step.selectorRef },
|
|
640
|
+
comment: `Assert ${step.selectorRef} is disabled`,
|
|
641
|
+
};
|
|
642
|
+
},
|
|
643
|
+
priority: 11,
|
|
644
|
+
},
|
|
645
|
+
{
|
|
646
|
+
// `matches "<format>"` — assert the SHAPE of a label, not its value. A spec states a label's
|
|
647
|
+
// contract as "Format: yyyy/mm/dd ~ yyyy/mm/dd" or "Format: 残りX個"; neither existing text
|
|
648
|
+
// assertion can carry that (an exact value pins data that changes daily, `contains` passes on
|
|
649
|
+
// anything). Requires the quoted form so it cannot claim the API-side `matches schema [Ref]`.
|
|
650
|
+
// Priority above should-have-text / should-contain-text (both 12): those match on the words
|
|
651
|
+
// "has text" / "contains", which a format step does not use, but the generic visibility
|
|
652
|
+
// catch-all would otherwise take an unrecognised `see [X] label matches …`.
|
|
653
|
+
name: 'matches-format',
|
|
654
|
+
matcher: (step) => /\bmatches\s+"/i.test(step.text) && !!step.selectorRef && !!step.value,
|
|
655
|
+
resolver: (step, context) => {
|
|
656
|
+
const resolved = context.selectorResolver.resolveSelector(step.selectorRef, undefined, step.elementType, step.nth);
|
|
657
|
+
return {
|
|
658
|
+
templateName: 'matches-format-assertion',
|
|
659
|
+
// `format` rides along so a failure message can name the shape the author asked for —
|
|
660
|
+
// a bare regex in the error is the hardest kind of red for a QA to read.
|
|
661
|
+
data: { ...resolved, selectorRef: step.selectorRef, formatRegex: (0, sungen_1.formatToRegexLiteral)(step.value), format: step.value },
|
|
662
|
+
comment: `Assert ${step.selectorRef} matches the format "${step.value}"`,
|
|
663
|
+
};
|
|
664
|
+
},
|
|
665
|
+
priority: 16,
|
|
666
|
+
},
|
|
667
|
+
{
|
|
668
|
+
name: 'should-contain-text',
|
|
669
|
+
matcher: (step) => (step.text.includes('should contain') || step.text.includes('contains')) && !!step.selectorRef && !!(step.value || step.dataRef),
|
|
670
|
+
resolver: (step, context) => {
|
|
671
|
+
const resolved = context.selectorResolver.resolveSelector(step.selectorRef, undefined, step.elementType, step.nth);
|
|
672
|
+
const expectedText = step.value || context.dataResolver.resolveData(step.dataRef, context.featureName);
|
|
673
|
+
const comment = `Assert ${step.selectorRef} contains "${step.value || step.dataRef}"`;
|
|
674
|
+
// LIST MEMBERS (locator item/option/cell types, role listitem/option/cell): the
|
|
675
|
+
// locator matches many elements by design, so a bare toContainText strict-mode-fails.
|
|
676
|
+
// `contains` on a list means "a member containing v exists" — filter by the substring
|
|
677
|
+
// then assert visible, the same routing the with-{{v}} existence branches use.
|
|
678
|
+
// Always substring here (never the YAML `exact`): the step itself says "contains".
|
|
679
|
+
// Unique elements keep toContainText below — a VALUE assertion is not weakened.
|
|
680
|
+
// A role entry pinned to ONE element (explicit `name:` or `nth:`) counts as unique:
|
|
681
|
+
// filter-then-visible would turn its value check into a visibility gate (a collapsed
|
|
682
|
+
// but present member would flip from pass to fail with a worse failure message).
|
|
683
|
+
const isListMember = (resolved.strategy === 'locator' && step.elementType && LOCATOR_LIST_MEMBER_TYPES.has(step.elementType)) ||
|
|
684
|
+
(resolved.strategy === 'role' && !!resolved.role && sungen_3.LIST_MEMBER_ROLES.has(resolved.role) &&
|
|
685
|
+
!(resolved.name && resolved.name.trim()) && resolved.nth == null);
|
|
686
|
+
if (isListMember) {
|
|
687
|
+
return {
|
|
688
|
+
templateName: 'visible-filtered-assertion',
|
|
689
|
+
data: { ...resolved, exact: false, dataValue: expectedText },
|
|
690
|
+
comment,
|
|
691
|
+
};
|
|
692
|
+
}
|
|
693
|
+
return {
|
|
694
|
+
templateName: 'contain-text-assertion',
|
|
695
|
+
data: { ...resolved, expectedText },
|
|
696
|
+
comment,
|
|
697
|
+
};
|
|
698
|
+
},
|
|
699
|
+
priority: 12,
|
|
700
|
+
},
|
|
701
|
+
{
|
|
702
|
+
name: 'should-have-text',
|
|
703
|
+
matcher: (step) => (step.text.includes('should have text') || step.text.includes('has text')) && !!step.selectorRef && !!(step.value || step.dataRef),
|
|
704
|
+
resolver: (step, context) => {
|
|
705
|
+
const resolved = context.selectorResolver.resolveSelector(step.selectorRef, undefined, step.elementType, step.nth);
|
|
706
|
+
const expectedText = step.value || context.dataResolver.resolveData(step.dataRef, context.featureName);
|
|
707
|
+
return {
|
|
708
|
+
templateName: 'have-text-assertion',
|
|
709
|
+
data: { ...resolved, expectedText },
|
|
710
|
+
comment: `Assert ${step.selectorRef} has text "${step.value || step.dataRef}"`,
|
|
711
|
+
};
|
|
712
|
+
},
|
|
713
|
+
priority: 12,
|
|
714
|
+
},
|
|
715
|
+
{
|
|
716
|
+
name: 'is-empty',
|
|
717
|
+
states: ['empty'],
|
|
718
|
+
matcher: (step) => /\b(see|sees)\s+\[/.test(step.text) && /\bis empty\b/.test(step.text) && !!step.selectorRef,
|
|
719
|
+
resolver: (step, context) => {
|
|
720
|
+
const resolved = context.selectorResolver.resolveSelector(step.selectorRef, undefined, step.elementType, step.nth);
|
|
721
|
+
return {
|
|
722
|
+
templateName: 'empty-assertion',
|
|
723
|
+
data: { ...resolved, selectorRef: step.selectorRef },
|
|
724
|
+
comment: `Assert ${step.selectorRef} is empty`,
|
|
725
|
+
};
|
|
726
|
+
},
|
|
727
|
+
priority: 11,
|
|
728
|
+
},
|
|
729
|
+
{
|
|
730
|
+
name: 'is-checked',
|
|
731
|
+
states: ['checked'],
|
|
732
|
+
matcher: (step) => /\b(see|sees)\s+\[/.test(step.text) && /\bis checked\b/.test(step.text) && !!step.selectorRef,
|
|
733
|
+
resolver: (step, context) => {
|
|
734
|
+
const resolved = context.selectorResolver.resolveSelector(step.selectorRef, undefined, step.elementType, step.nth);
|
|
735
|
+
return {
|
|
736
|
+
templateName: 'checked-assertion',
|
|
737
|
+
data: { ...resolved, selectorRef: step.selectorRef },
|
|
738
|
+
comment: `Assert ${step.selectorRef} is checked`,
|
|
739
|
+
};
|
|
740
|
+
},
|
|
741
|
+
priority: 11,
|
|
742
|
+
},
|
|
743
|
+
{
|
|
744
|
+
name: 'is-unchecked',
|
|
745
|
+
states: ['unchecked'],
|
|
746
|
+
matcher: (step) => /\b(see|sees)\s+\[/.test(step.text) && /\bis unchecked\b/.test(step.text) && !!step.selectorRef,
|
|
747
|
+
resolver: (step, context) => {
|
|
748
|
+
const resolved = context.selectorResolver.resolveSelector(step.selectorRef, undefined, step.elementType, step.nth);
|
|
749
|
+
return {
|
|
750
|
+
templateName: 'not-checked-assertion',
|
|
751
|
+
data: { ...resolved, selectorRef: step.selectorRef },
|
|
752
|
+
comment: `Assert ${step.selectorRef} is unchecked`,
|
|
753
|
+
};
|
|
754
|
+
},
|
|
755
|
+
priority: 11,
|
|
756
|
+
},
|
|
757
|
+
{
|
|
758
|
+
name: 'is-focused',
|
|
759
|
+
states: ['focused'],
|
|
760
|
+
matcher: (step) => /\b(see|sees)\s+\[/.test(step.text) && /\bis focused\b/.test(step.text) && !!step.selectorRef,
|
|
761
|
+
resolver: (step, context) => {
|
|
762
|
+
const resolved = context.selectorResolver.resolveSelector(step.selectorRef, undefined, step.elementType, step.nth);
|
|
763
|
+
return {
|
|
764
|
+
templateName: 'focused-assertion',
|
|
765
|
+
data: { ...resolved, selectorRef: step.selectorRef },
|
|
766
|
+
comment: `Assert ${step.selectorRef} is focused`,
|
|
767
|
+
};
|
|
768
|
+
},
|
|
769
|
+
priority: 11,
|
|
770
|
+
},
|
|
771
|
+
{
|
|
772
|
+
name: 'see-data-text',
|
|
773
|
+
matcher: (step) => (step.text.includes('should see') ||
|
|
774
|
+
step.text.match(/\b(see|sees)\b/)) &&
|
|
775
|
+
!step.selectorRef &&
|
|
776
|
+
!!step.dataRef,
|
|
777
|
+
resolver: (step, context) => {
|
|
778
|
+
// Resolve data reference to actual value
|
|
779
|
+
let dataValue;
|
|
780
|
+
dataValue = context.dataResolver.resolveData(step.dataRef, context.featureName);
|
|
781
|
+
return {
|
|
782
|
+
templateName: 'visible-assertion',
|
|
783
|
+
data: {
|
|
784
|
+
strategy: 'text',
|
|
785
|
+
value: dataValue,
|
|
786
|
+
},
|
|
787
|
+
comment: `Assert ${step.dataRef} is visible`,
|
|
788
|
+
};
|
|
789
|
+
},
|
|
790
|
+
priority: 5, // Lower than selector-based assertions
|
|
791
|
+
},
|
|
792
|
+
{
|
|
793
|
+
name: 'list-item-count',
|
|
794
|
+
matcher: (step) => step.text.includes('should have') && step.text.includes('count') &&
|
|
795
|
+
!!step.selectorRef && step.text.includes('items') &&
|
|
796
|
+
(step.elementType === 'list' || step.elementType === 'list-item' || step.elementType === 'listitem'),
|
|
797
|
+
resolver: (step, context) => {
|
|
798
|
+
let expectedCount = 1;
|
|
799
|
+
const match = step.text.match(/count\s+(\d+)/);
|
|
800
|
+
if (match) {
|
|
801
|
+
expectedCount = parseInt(match[1]);
|
|
802
|
+
}
|
|
803
|
+
else if (step.dataRef) {
|
|
804
|
+
const resolvedData = context.dataResolver.resolveData(step.dataRef, context.featureName);
|
|
805
|
+
const parsed = parseInt(resolvedData);
|
|
806
|
+
expectedCount = isNaN(parsed) ? 1 : parsed;
|
|
807
|
+
}
|
|
808
|
+
const resolved = context.selectorResolver.resolveSelector(step.selectorRef, undefined, step.elementType, step.nth);
|
|
809
|
+
return {
|
|
810
|
+
templateName: 'list-item-count-assertion',
|
|
811
|
+
data: { ...resolved, expectedCount },
|
|
812
|
+
comment: `Assert ${step.selectorRef} list has ${expectedCount} items`,
|
|
813
|
+
};
|
|
814
|
+
},
|
|
815
|
+
priority: 9, // Higher than should-have-count (8)
|
|
816
|
+
},
|
|
817
|
+
{
|
|
818
|
+
name: 'should-have-count',
|
|
819
|
+
matcher: (step) => step.text.includes('should have') && step.text.includes('count') && !!step.selectorRef,
|
|
820
|
+
resolver: (step, context) => {
|
|
821
|
+
let expectedCount = 1;
|
|
822
|
+
// Try literal digit first: "count 10"
|
|
823
|
+
const match = step.text.match(/count\s+(\d+)/);
|
|
824
|
+
if (match) {
|
|
825
|
+
expectedCount = parseInt(match[1]);
|
|
826
|
+
}
|
|
827
|
+
else if (step.dataRef) {
|
|
828
|
+
// Resolve data reference: "count {{number_items}}"
|
|
829
|
+
const resolvedData = context.dataResolver.resolveData(step.dataRef, context.featureName);
|
|
830
|
+
const parsed = parseInt(resolvedData);
|
|
831
|
+
expectedCount = isNaN(parsed) ? 1 : parsed;
|
|
832
|
+
}
|
|
833
|
+
const resolved = context.selectorResolver.resolveSelector(step.selectorRef, undefined, step.elementType, step.nth);
|
|
834
|
+
return {
|
|
835
|
+
templateName: 'count-assertion',
|
|
836
|
+
data: { ...resolved, expectedCount },
|
|
837
|
+
comment: `Assert ${step.selectorRef} has count ${expectedCount}`,
|
|
838
|
+
};
|
|
839
|
+
},
|
|
840
|
+
priority: 8,
|
|
841
|
+
},
|
|
842
|
+
{
|
|
843
|
+
name: 'is-loading',
|
|
844
|
+
states: ['loading'],
|
|
845
|
+
matcher: (step) => /\b(see|sees)\s+\[/.test(step.text) && /\bis loading\b/.test(step.text) && !!step.selectorRef,
|
|
846
|
+
resolver: (step, context) => {
|
|
847
|
+
const resolved = context.selectorResolver.resolveSelector(step.selectorRef, undefined, step.elementType, step.nth);
|
|
848
|
+
return {
|
|
849
|
+
templateName: 'loading-assertion',
|
|
850
|
+
data: { ...resolved, selectorRef: step.selectorRef },
|
|
851
|
+
comment: `Assert ${step.selectorRef} is loading`,
|
|
852
|
+
};
|
|
853
|
+
},
|
|
854
|
+
priority: 11,
|
|
855
|
+
},
|
|
856
|
+
{
|
|
857
|
+
name: 'is-selected',
|
|
858
|
+
states: ['selected'],
|
|
859
|
+
matcher: (step) => /\b(see|sees)\s+\[/.test(step.text) && /\bis selected\b/.test(step.text) && !!step.selectorRef,
|
|
860
|
+
resolver: (step, context) => {
|
|
861
|
+
const resolved = context.selectorResolver.resolveSelector(step.selectorRef, undefined, step.elementType, step.nth);
|
|
862
|
+
return {
|
|
863
|
+
templateName: 'selected-assertion',
|
|
864
|
+
data: { ...resolved, selectorRef: step.selectorRef },
|
|
865
|
+
comment: `Assert ${step.selectorRef} is selected`,
|
|
866
|
+
};
|
|
867
|
+
},
|
|
868
|
+
priority: 11,
|
|
869
|
+
},
|
|
870
|
+
{
|
|
871
|
+
name: 'is-sorted-ascending',
|
|
872
|
+
platforms: ['web'],
|
|
873
|
+
platformAlternative: 'aria-sort has no native equivalent - read the item texts with per-element assertions and compare them, or tag the scenario @manual',
|
|
874
|
+
states: ['sorted ascending'],
|
|
875
|
+
matcher: (step) => /\b(see|sees)\s+\[/.test(step.text) && /\bsorted ascending\b/.test(step.text) && !!step.selectorRef,
|
|
876
|
+
resolver: (step, context) => {
|
|
877
|
+
const resolved = context.selectorResolver.resolveSelector(step.selectorRef, undefined, step.elementType, step.nth);
|
|
878
|
+
return {
|
|
879
|
+
templateName: 'sorted-assertion',
|
|
880
|
+
data: { ...resolved, selectorRef: step.selectorRef, sortDirection: 'ascending' },
|
|
881
|
+
comment: `Assert ${step.selectorRef} is sorted ascending`,
|
|
882
|
+
};
|
|
883
|
+
},
|
|
884
|
+
priority: 12,
|
|
885
|
+
},
|
|
886
|
+
{
|
|
887
|
+
name: 'is-sorted-descending',
|
|
888
|
+
platforms: ['web'],
|
|
889
|
+
platformAlternative: 'aria-sort has no native equivalent - read the item texts with per-element assertions and compare them, or tag the scenario @manual',
|
|
890
|
+
states: ['sorted descending'],
|
|
891
|
+
matcher: (step) => /\b(see|sees)\s+\[/.test(step.text) && /\bsorted descending\b/.test(step.text) && !!step.selectorRef,
|
|
892
|
+
resolver: (step, context) => {
|
|
893
|
+
const resolved = context.selectorResolver.resolveSelector(step.selectorRef, undefined, step.elementType, step.nth);
|
|
894
|
+
return {
|
|
895
|
+
templateName: 'sorted-assertion',
|
|
896
|
+
data: { ...resolved, selectorRef: step.selectorRef, sortDirection: 'descending' },
|
|
897
|
+
comment: `Assert ${step.selectorRef} is sorted descending`,
|
|
898
|
+
};
|
|
899
|
+
},
|
|
900
|
+
priority: 12,
|
|
901
|
+
},
|
|
902
|
+
];
|
|
903
|
+
//# sourceMappingURL=assertion-patterns.js.map
|