qa360 2.0.11 → 2.0.13
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/commands/ai.js +26 -14
- package/dist/commands/ask.d.ts +75 -23
- package/dist/commands/ask.js +413 -265
- package/dist/commands/crawl.d.ts +24 -0
- package/dist/commands/crawl.js +121 -0
- package/dist/commands/history.js +38 -3
- package/dist/commands/init.d.ts +89 -95
- package/dist/commands/init.js +282 -200
- package/dist/commands/run.d.ts +1 -0
- package/dist/core/adapters/playwright-ui.d.ts +45 -7
- package/dist/core/adapters/playwright-ui.js +365 -59
- package/dist/core/assertions/engine.d.ts +51 -0
- package/dist/core/assertions/engine.js +530 -0
- package/dist/core/assertions/index.d.ts +11 -0
- package/dist/core/assertions/index.js +11 -0
- package/dist/core/assertions/types.d.ts +121 -0
- package/dist/core/assertions/types.js +37 -0
- package/dist/core/crawler/index.d.ts +57 -0
- package/dist/core/crawler/index.js +281 -0
- package/dist/core/crawler/journey-generator.d.ts +49 -0
- package/dist/core/crawler/journey-generator.js +412 -0
- package/dist/core/crawler/page-analyzer.d.ts +88 -0
- package/dist/core/crawler/page-analyzer.js +709 -0
- package/dist/core/crawler/selector-generator.d.ts +34 -0
- package/dist/core/crawler/selector-generator.js +240 -0
- package/dist/core/crawler/types.d.ts +353 -0
- package/dist/core/crawler/types.js +6 -0
- package/dist/core/generation/crawler-pack-generator.d.ts +44 -0
- package/dist/core/generation/crawler-pack-generator.js +231 -0
- package/dist/core/generation/index.d.ts +2 -0
- package/dist/core/generation/index.js +2 -0
- package/dist/core/index.d.ts +3 -0
- package/dist/core/index.js +4 -0
- package/dist/core/types/pack-v1.d.ts +90 -0
- package/dist/index.js +6 -2
- package/examples/accessibility.yml +39 -16
- package/examples/api-basic.yml +19 -14
- package/examples/complete.yml +134 -42
- package/examples/fullstack.yml +66 -31
- package/examples/security.yml +47 -15
- package/examples/ui-basic.yml +16 -12
- package/package.json +3 -2
|
@@ -0,0 +1,530 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* QA360 Assertions Engine
|
|
3
|
+
*
|
|
4
|
+
* Executes assertions against Playwright Page objects
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Assertions Engine class
|
|
8
|
+
*/
|
|
9
|
+
export class AssertionsEngine {
|
|
10
|
+
page;
|
|
11
|
+
defaultTimeout;
|
|
12
|
+
constructor(page, defaultTimeout = 5000) {
|
|
13
|
+
this.page = page;
|
|
14
|
+
this.defaultTimeout = defaultTimeout;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Run a single assertion
|
|
18
|
+
*/
|
|
19
|
+
async runAssertion(assertion) {
|
|
20
|
+
const startTime = Date.now();
|
|
21
|
+
const timeout = assertion.timeout ?? this.defaultTimeout;
|
|
22
|
+
try {
|
|
23
|
+
let actual;
|
|
24
|
+
let passed = false;
|
|
25
|
+
let error;
|
|
26
|
+
switch (assertion.type) {
|
|
27
|
+
// Visibility assertions
|
|
28
|
+
case 'visible':
|
|
29
|
+
passed = await this.waitForVisible(assertion.selector, timeout, !assertion.not);
|
|
30
|
+
actual = await this.isVisible(assertion.selector);
|
|
31
|
+
break;
|
|
32
|
+
case 'hidden':
|
|
33
|
+
passed = await this.waitForHidden(assertion.selector, timeout, !assertion.not);
|
|
34
|
+
actual = await this.isHidden(assertion.selector);
|
|
35
|
+
break;
|
|
36
|
+
case 'attached':
|
|
37
|
+
actual = await this.isAttached(assertion.selector);
|
|
38
|
+
passed = assertion.not ? !actual : actual;
|
|
39
|
+
break;
|
|
40
|
+
case 'detached':
|
|
41
|
+
actual = await this.isAttached(assertion.selector);
|
|
42
|
+
passed = assertion.not ? actual : !actual;
|
|
43
|
+
break;
|
|
44
|
+
// Text content assertions
|
|
45
|
+
case 'text':
|
|
46
|
+
actual = await this.getTextContent(assertion.selector);
|
|
47
|
+
passed = this.compare(actual, assertion.expected, assertion.operator || 'eq');
|
|
48
|
+
break;
|
|
49
|
+
case 'contains':
|
|
50
|
+
actual = await this.getTextContent(assertion.selector);
|
|
51
|
+
passed = assertion.not
|
|
52
|
+
? !this.contains(actual, assertion.expected)
|
|
53
|
+
: this.contains(actual, assertion.expected);
|
|
54
|
+
break;
|
|
55
|
+
case 'matches':
|
|
56
|
+
actual = await this.getTextContent(assertion.selector);
|
|
57
|
+
passed = this.matches(actual, assertion.expected, assertion.not ?? false);
|
|
58
|
+
break;
|
|
59
|
+
// Value assertions
|
|
60
|
+
case 'value':
|
|
61
|
+
actual = await this.getInputValue(assertion.selector);
|
|
62
|
+
passed = this.compare(actual, assertion.expected, assertion.operator || 'eq');
|
|
63
|
+
break;
|
|
64
|
+
case 'empty':
|
|
65
|
+
actual = (await this.getTextContent(assertion.selector))?.trim() || '';
|
|
66
|
+
passed = assertion.not ? actual !== '' : actual === '';
|
|
67
|
+
break;
|
|
68
|
+
case 'notEmpty':
|
|
69
|
+
actual = (await this.getTextContent(assertion.selector))?.trim() || '';
|
|
70
|
+
passed = assertion.not ? actual === '' : actual !== '';
|
|
71
|
+
break;
|
|
72
|
+
// Attribute assertions
|
|
73
|
+
case 'attribute':
|
|
74
|
+
actual = await this.getAttribute(assertion.selector, assertion.expected.name);
|
|
75
|
+
passed = this.compare(actual, assertion.expected.value, assertion.operator || 'eq');
|
|
76
|
+
break;
|
|
77
|
+
case 'hasAttribute':
|
|
78
|
+
actual = await this.hasAttribute(assertion.selector, assertion.expected);
|
|
79
|
+
passed = assertion.not ? !actual : actual;
|
|
80
|
+
break;
|
|
81
|
+
case 'class':
|
|
82
|
+
actual = await this.getClasses(assertion.selector);
|
|
83
|
+
passed = assertion.not
|
|
84
|
+
? !this.contains(actual, assertion.expected)
|
|
85
|
+
: this.contains(actual, assertion.expected);
|
|
86
|
+
break;
|
|
87
|
+
case 'href':
|
|
88
|
+
actual = await this.getAttribute(assertion.selector, 'href');
|
|
89
|
+
passed = this.compare(actual, assertion.expected, assertion.operator || 'eq');
|
|
90
|
+
break;
|
|
91
|
+
case 'src':
|
|
92
|
+
actual = await this.getAttribute(assertion.selector, 'src');
|
|
93
|
+
passed = this.compare(actual, assertion.expected, assertion.operator || 'eq');
|
|
94
|
+
break;
|
|
95
|
+
case 'placeholder':
|
|
96
|
+
actual = await this.getAttribute(assertion.selector, 'placeholder');
|
|
97
|
+
passed = this.compare(actual, assertion.expected, assertion.operator || 'eq');
|
|
98
|
+
break;
|
|
99
|
+
case 'id':
|
|
100
|
+
actual = await this.getAttribute(assertion.selector, 'id');
|
|
101
|
+
passed = this.compare(actual, assertion.expected, assertion.operator || 'eq');
|
|
102
|
+
break;
|
|
103
|
+
case 'tagName':
|
|
104
|
+
actual = await this.getTagName(assertion.selector);
|
|
105
|
+
passed = this.compare(actual?.toLowerCase(), assertion.expected?.toLowerCase(), assertion.operator || 'eq');
|
|
106
|
+
break;
|
|
107
|
+
// Count assertions
|
|
108
|
+
case 'count':
|
|
109
|
+
actual = await this.count(assertion.selector);
|
|
110
|
+
passed = this.compare(actual, assertion.expected, assertion.operator || 'eq');
|
|
111
|
+
break;
|
|
112
|
+
// Page-level assertions
|
|
113
|
+
case 'url':
|
|
114
|
+
actual = this.page.url();
|
|
115
|
+
passed = this.compare(actual, assertion.expected, assertion.operator || 'eq');
|
|
116
|
+
break;
|
|
117
|
+
case 'urlContains':
|
|
118
|
+
actual = this.page.url();
|
|
119
|
+
passed = assertion.not
|
|
120
|
+
? !this.contains(actual, assertion.expected)
|
|
121
|
+
: this.contains(actual, assertion.expected);
|
|
122
|
+
break;
|
|
123
|
+
case 'title':
|
|
124
|
+
actual = await this.page.title();
|
|
125
|
+
passed = this.compare(actual, assertion.expected, assertion.operator || 'eq');
|
|
126
|
+
break;
|
|
127
|
+
case 'titleContains':
|
|
128
|
+
actual = await this.page.title();
|
|
129
|
+
passed = assertion.not
|
|
130
|
+
? !this.contains(actual, assertion.expected)
|
|
131
|
+
: this.contains(actual, assertion.expected);
|
|
132
|
+
break;
|
|
133
|
+
// State assertions
|
|
134
|
+
case 'enabled':
|
|
135
|
+
actual = await this.isEnabled(assertion.selector);
|
|
136
|
+
passed = assertion.not ? !actual : actual;
|
|
137
|
+
break;
|
|
138
|
+
case 'disabled':
|
|
139
|
+
actual = await this.isEnabled(assertion.selector);
|
|
140
|
+
passed = assertion.not ? actual : !actual;
|
|
141
|
+
break;
|
|
142
|
+
case 'checked':
|
|
143
|
+
actual = await this.isChecked(assertion.selector);
|
|
144
|
+
passed = assertion.not ? !actual : actual;
|
|
145
|
+
break;
|
|
146
|
+
case 'unchecked':
|
|
147
|
+
actual = await this.isChecked(assertion.selector);
|
|
148
|
+
passed = assertion.not ? actual : !actual;
|
|
149
|
+
break;
|
|
150
|
+
case 'focused':
|
|
151
|
+
actual = await this.isFocused(assertion.selector);
|
|
152
|
+
passed = assertion.not ? !actual : actual;
|
|
153
|
+
break;
|
|
154
|
+
case 'readOnly':
|
|
155
|
+
actual = await this.isReadOnly(assertion.selector);
|
|
156
|
+
passed = assertion.not ? !actual : actual;
|
|
157
|
+
break;
|
|
158
|
+
case 'editable':
|
|
159
|
+
actual = await this.isReadOnly(assertion.selector);
|
|
160
|
+
passed = assertion.not ? actual : !actual;
|
|
161
|
+
break;
|
|
162
|
+
case 'selected':
|
|
163
|
+
actual = await this.isSelected(assertion.selector);
|
|
164
|
+
passed = assertion.not ? !actual : actual;
|
|
165
|
+
break;
|
|
166
|
+
// CSS assertions
|
|
167
|
+
case 'css':
|
|
168
|
+
actual = await this.getCssProperty(assertion.selector, assertion.expected.property);
|
|
169
|
+
passed = this.compare(actual, assertion.expected.value, assertion.operator || 'eq');
|
|
170
|
+
break;
|
|
171
|
+
case 'inViewport':
|
|
172
|
+
actual = await this.isInViewport(assertion.selector);
|
|
173
|
+
passed = assertion.not ? !actual : actual;
|
|
174
|
+
break;
|
|
175
|
+
case 'boundingBox':
|
|
176
|
+
actual = await this.getBoundingBox(assertion.selector);
|
|
177
|
+
if (assertion.expected) {
|
|
178
|
+
const { x, y, width, height } = assertion.expected;
|
|
179
|
+
if (x !== undefined)
|
|
180
|
+
passed = this.compare(actual.x, x, assertion.operator || 'eq');
|
|
181
|
+
if (y !== undefined)
|
|
182
|
+
passed = passed && this.compare(actual.y, y, assertion.operator || 'eq');
|
|
183
|
+
if (width !== undefined)
|
|
184
|
+
passed = passed && this.compare(actual.width, width, assertion.operator || 'eq');
|
|
185
|
+
if (height !== undefined)
|
|
186
|
+
passed = passed && this.compare(actual.height, height, assertion.operator || 'eq');
|
|
187
|
+
}
|
|
188
|
+
else {
|
|
189
|
+
passed = actual !== null;
|
|
190
|
+
}
|
|
191
|
+
break;
|
|
192
|
+
default:
|
|
193
|
+
throw new Error(`Unknown assertion type: ${assertion.type}`);
|
|
194
|
+
}
|
|
195
|
+
if (!passed) {
|
|
196
|
+
error = this.formatError(assertion, actual, assertion.expected);
|
|
197
|
+
}
|
|
198
|
+
return {
|
|
199
|
+
assertion,
|
|
200
|
+
passed,
|
|
201
|
+
actual,
|
|
202
|
+
expected: assertion.expected,
|
|
203
|
+
error,
|
|
204
|
+
duration: Date.now() - startTime,
|
|
205
|
+
timestamp: new Date().toISOString(),
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
catch (err) {
|
|
209
|
+
return {
|
|
210
|
+
assertion,
|
|
211
|
+
passed: false,
|
|
212
|
+
expected: assertion.expected,
|
|
213
|
+
error: err instanceof Error ? err.message : String(err),
|
|
214
|
+
duration: Date.now() - startTime,
|
|
215
|
+
timestamp: new Date().toISOString(),
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Run multiple assertions
|
|
221
|
+
*/
|
|
222
|
+
async runAssertions(assertions, options = {}) {
|
|
223
|
+
const startTime = Date.now();
|
|
224
|
+
const results = [];
|
|
225
|
+
const opts = {
|
|
226
|
+
page: this.page,
|
|
227
|
+
timeout: this.defaultTimeout,
|
|
228
|
+
throwOnFailure: false,
|
|
229
|
+
...options,
|
|
230
|
+
};
|
|
231
|
+
const stopOnFailure = options.stopOnFailure ?? false;
|
|
232
|
+
const parallel = options.parallel ?? false;
|
|
233
|
+
if (parallel) {
|
|
234
|
+
// Run all assertions in parallel
|
|
235
|
+
const promiseResults = await Promise.all(assertions.map(a => this.runAssertion(a)));
|
|
236
|
+
results.push(...promiseResults);
|
|
237
|
+
}
|
|
238
|
+
else {
|
|
239
|
+
// Run assertions sequentially
|
|
240
|
+
for (const assertion of assertions) {
|
|
241
|
+
const result = await this.runAssertion(assertion);
|
|
242
|
+
results.push(result);
|
|
243
|
+
opts.onResult?.(result);
|
|
244
|
+
if (stopOnFailure && !result.passed && !result.assertion.soft) {
|
|
245
|
+
break;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
const failed = results.filter(r => !r.passed && !r.assertion.soft);
|
|
250
|
+
const passed = results.length - failed.length;
|
|
251
|
+
return {
|
|
252
|
+
name: options.name || 'Assertion Group',
|
|
253
|
+
results,
|
|
254
|
+
passed: failed.length === 0,
|
|
255
|
+
duration: Date.now() - startTime,
|
|
256
|
+
passedCount: passed,
|
|
257
|
+
failedCount: failed.length,
|
|
258
|
+
skippedCount: 0,
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
// ========== Helper methods for element state ==========
|
|
262
|
+
async isVisible(selector) {
|
|
263
|
+
try {
|
|
264
|
+
const element = await this.page.$(selector);
|
|
265
|
+
if (!element)
|
|
266
|
+
return false;
|
|
267
|
+
return await element.isVisible();
|
|
268
|
+
}
|
|
269
|
+
catch {
|
|
270
|
+
return false;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
async waitForVisible(selector, timeout, negate) {
|
|
274
|
+
try {
|
|
275
|
+
if (negate) {
|
|
276
|
+
await this.page.waitForSelector(selector, { state: 'hidden', timeout });
|
|
277
|
+
}
|
|
278
|
+
else {
|
|
279
|
+
await this.page.waitForSelector(selector, { state: 'visible', timeout });
|
|
280
|
+
}
|
|
281
|
+
return true;
|
|
282
|
+
}
|
|
283
|
+
catch {
|
|
284
|
+
return false;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
async isHidden(selector) {
|
|
288
|
+
return !(await this.isVisible(selector));
|
|
289
|
+
}
|
|
290
|
+
async waitForHidden(selector, timeout, negate) {
|
|
291
|
+
try {
|
|
292
|
+
if (negate) {
|
|
293
|
+
await this.page.waitForSelector(selector, { state: 'visible', timeout });
|
|
294
|
+
}
|
|
295
|
+
else {
|
|
296
|
+
await this.page.waitForSelector(selector, { state: 'hidden', timeout });
|
|
297
|
+
}
|
|
298
|
+
return true;
|
|
299
|
+
}
|
|
300
|
+
catch {
|
|
301
|
+
return false;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
async isAttached(selector) {
|
|
305
|
+
try {
|
|
306
|
+
const element = await this.page.$(selector);
|
|
307
|
+
return element !== null;
|
|
308
|
+
}
|
|
309
|
+
catch {
|
|
310
|
+
return false;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
async getTextContent(selector) {
|
|
314
|
+
try {
|
|
315
|
+
const element = await this.page.$(selector);
|
|
316
|
+
if (!element)
|
|
317
|
+
return '';
|
|
318
|
+
return (await element.textContent()) || '';
|
|
319
|
+
}
|
|
320
|
+
catch {
|
|
321
|
+
return '';
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
async getInputValue(selector) {
|
|
325
|
+
try {
|
|
326
|
+
const element = await this.page.$(selector);
|
|
327
|
+
if (!element)
|
|
328
|
+
return '';
|
|
329
|
+
return await element.inputValue();
|
|
330
|
+
}
|
|
331
|
+
catch {
|
|
332
|
+
return '';
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
async getAttribute(selector, attributeName) {
|
|
336
|
+
try {
|
|
337
|
+
const element = await this.page.$(selector);
|
|
338
|
+
if (!element)
|
|
339
|
+
return null;
|
|
340
|
+
return await element.getAttribute(attributeName);
|
|
341
|
+
}
|
|
342
|
+
catch {
|
|
343
|
+
return null;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
async hasAttribute(selector, attributeName) {
|
|
347
|
+
const attr = await this.getAttribute(selector, attributeName);
|
|
348
|
+
return attr !== null;
|
|
349
|
+
}
|
|
350
|
+
async getClasses(selector) {
|
|
351
|
+
try {
|
|
352
|
+
const element = await this.page.$(selector);
|
|
353
|
+
if (!element)
|
|
354
|
+
return [];
|
|
355
|
+
const className = await element.getAttribute('class');
|
|
356
|
+
return className ? className.trim().split(/\s+/) : [];
|
|
357
|
+
}
|
|
358
|
+
catch {
|
|
359
|
+
return [];
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
async getTagName(selector) {
|
|
363
|
+
try {
|
|
364
|
+
const element = await this.page.$(selector);
|
|
365
|
+
if (!element)
|
|
366
|
+
return null;
|
|
367
|
+
return await element.evaluate((el) => el.tagName);
|
|
368
|
+
}
|
|
369
|
+
catch {
|
|
370
|
+
return null;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
async count(selector) {
|
|
374
|
+
try {
|
|
375
|
+
return await this.page.locator(selector).count();
|
|
376
|
+
}
|
|
377
|
+
catch {
|
|
378
|
+
return 0;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
async isEnabled(selector) {
|
|
382
|
+
try {
|
|
383
|
+
const element = await this.page.$(selector);
|
|
384
|
+
if (!element)
|
|
385
|
+
return false;
|
|
386
|
+
return await element.isEnabled();
|
|
387
|
+
}
|
|
388
|
+
catch {
|
|
389
|
+
return false;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
async isChecked(selector) {
|
|
393
|
+
try {
|
|
394
|
+
const element = await this.page.$(selector);
|
|
395
|
+
if (!element)
|
|
396
|
+
return false;
|
|
397
|
+
return await element.isChecked();
|
|
398
|
+
}
|
|
399
|
+
catch {
|
|
400
|
+
return false;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
async isFocused(selector) {
|
|
404
|
+
try {
|
|
405
|
+
const element = await this.page.$(selector);
|
|
406
|
+
if (!element)
|
|
407
|
+
return false;
|
|
408
|
+
return await element.evaluate((el) => document.activeElement === el);
|
|
409
|
+
}
|
|
410
|
+
catch {
|
|
411
|
+
return false;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
async isReadOnly(selector) {
|
|
415
|
+
try {
|
|
416
|
+
const element = await this.page.$(selector);
|
|
417
|
+
if (!element)
|
|
418
|
+
return false;
|
|
419
|
+
const readOnly = await element.getAttribute('readonly');
|
|
420
|
+
const disabled = await element.getAttribute('disabled');
|
|
421
|
+
return readOnly !== null || disabled !== null;
|
|
422
|
+
}
|
|
423
|
+
catch {
|
|
424
|
+
return false;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
async isSelected(selector) {
|
|
428
|
+
try {
|
|
429
|
+
const element = await this.page.$(selector);
|
|
430
|
+
if (!element)
|
|
431
|
+
return false;
|
|
432
|
+
return await element.evaluate((el) => el.selected);
|
|
433
|
+
}
|
|
434
|
+
catch {
|
|
435
|
+
return false;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
async getCssProperty(selector, property) {
|
|
439
|
+
try {
|
|
440
|
+
const element = await this.page.$(selector);
|
|
441
|
+
if (!element)
|
|
442
|
+
return '';
|
|
443
|
+
return await element.evaluate((el, prop) => {
|
|
444
|
+
return window.getComputedStyle(el).getPropertyValue(prop);
|
|
445
|
+
}, property);
|
|
446
|
+
}
|
|
447
|
+
catch {
|
|
448
|
+
return '';
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
async isInViewport(selector) {
|
|
452
|
+
try {
|
|
453
|
+
const element = await this.page.$(selector);
|
|
454
|
+
if (!element)
|
|
455
|
+
return false;
|
|
456
|
+
return await element.isIntersectingViewport();
|
|
457
|
+
}
|
|
458
|
+
catch {
|
|
459
|
+
return false;
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
async getBoundingBox(selector) {
|
|
463
|
+
try {
|
|
464
|
+
const element = await this.page.$(selector);
|
|
465
|
+
if (!element)
|
|
466
|
+
return null;
|
|
467
|
+
const box = await element.boundingBox();
|
|
468
|
+
return box || null;
|
|
469
|
+
}
|
|
470
|
+
catch {
|
|
471
|
+
return null;
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
// ========== Comparison helpers ==========
|
|
475
|
+
compare(actual, expected, operator) {
|
|
476
|
+
switch (operator) {
|
|
477
|
+
case 'eq':
|
|
478
|
+
return actual == expected;
|
|
479
|
+
case 'ne':
|
|
480
|
+
return actual != expected;
|
|
481
|
+
case 'gt':
|
|
482
|
+
return Number(actual) > Number(expected);
|
|
483
|
+
case 'gte':
|
|
484
|
+
return Number(actual) >= Number(expected);
|
|
485
|
+
case 'lt':
|
|
486
|
+
return Number(actual) < Number(expected);
|
|
487
|
+
case 'lte':
|
|
488
|
+
return Number(actual) <= Number(expected);
|
|
489
|
+
case 'startsWith':
|
|
490
|
+
return String(actual).startsWith(String(expected));
|
|
491
|
+
case 'endsWith':
|
|
492
|
+
return String(actual).endsWith(String(expected));
|
|
493
|
+
case 'isEmpty':
|
|
494
|
+
return !actual || actual.toString().trim() === '';
|
|
495
|
+
case 'notEmpty':
|
|
496
|
+
return actual && actual.toString().trim() !== '';
|
|
497
|
+
default:
|
|
498
|
+
return actual == expected;
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
contains(actual, expected) {
|
|
502
|
+
if (actual === null || actual === undefined)
|
|
503
|
+
return false;
|
|
504
|
+
return String(actual).includes(String(expected));
|
|
505
|
+
}
|
|
506
|
+
matches(actual, expected, negate) {
|
|
507
|
+
try {
|
|
508
|
+
const regex = new RegExp(expected);
|
|
509
|
+
const matched = regex.test(String(actual));
|
|
510
|
+
return negate ? !matched : matched;
|
|
511
|
+
}
|
|
512
|
+
catch {
|
|
513
|
+
return false;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
formatError(assertion, actual, expected) {
|
|
517
|
+
const message = assertion.message || `Assertion "${assertion.type}" failed`;
|
|
518
|
+
const expectedStr = expected !== undefined ? ` (expected: ${JSON.stringify(expected)})` : '';
|
|
519
|
+
const actualStr = actual !== undefined ? ` (actual: ${JSON.stringify(actual)})` : '';
|
|
520
|
+
return `${message}${expectedStr}${actualStr}`;
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
/**
|
|
524
|
+
* Create an assertions engine
|
|
525
|
+
*/
|
|
526
|
+
export function createAssertionsEngine(page, timeout) {
|
|
527
|
+
return new AssertionsEngine(page, timeout);
|
|
528
|
+
}
|
|
529
|
+
// Re-export types
|
|
530
|
+
export * from './types.js';
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* QA360 Assertions Engine
|
|
3
|
+
*
|
|
4
|
+
* Comprehensive assertions for UI testing with Playwright
|
|
5
|
+
*/
|
|
6
|
+
export * from './types.js';
|
|
7
|
+
export * from './engine.js';
|
|
8
|
+
/**
|
|
9
|
+
* Convenience function to create assertions
|
|
10
|
+
*/
|
|
11
|
+
export { createAssertionsEngine } from './engine.js';
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* QA360 Assertions Engine
|
|
3
|
+
*
|
|
4
|
+
* Comprehensive assertions for UI testing with Playwright
|
|
5
|
+
*/
|
|
6
|
+
export * from './types.js';
|
|
7
|
+
export * from './engine.js';
|
|
8
|
+
/**
|
|
9
|
+
* Convenience function to create assertions
|
|
10
|
+
*/
|
|
11
|
+
export { createAssertionsEngine } from './engine.js';
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* QA360 Assertions Engine - Type Definitions
|
|
3
|
+
*
|
|
4
|
+
* Comprehensive assertions for UI testing
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Assertion types
|
|
8
|
+
*/
|
|
9
|
+
export type AssertionType = 'visible' | 'hidden' | 'attached' | 'detached' | 'text' | 'contains' | 'matches' | 'value' | 'attribute' | 'class' | 'count' | 'url' | 'urlContains' | 'title' | 'titleContains' | 'enabled' | 'disabled' | 'checked' | 'unchecked' | 'focused' | 'empty' | 'notEmpty' | 'readOnly' | 'editable' | 'selected' | 'href' | 'src' | 'placeholder' | 'id' | 'tagName' | 'css' | 'boundingBox' | 'inViewport' | 'hasAttribute';
|
|
10
|
+
/**
|
|
11
|
+
* Assertion operators
|
|
12
|
+
*/
|
|
13
|
+
export type AssertionOperator = 'eq' | 'ne' | 'contains' | 'notContains' | 'matches' | 'notMatches' | 'gt' | 'gte' | 'lt' | 'lte' | 'startsWith' | 'endsWith' | 'isEmpty' | 'notEmpty';
|
|
14
|
+
/**
|
|
15
|
+
* Assertion definition
|
|
16
|
+
*/
|
|
17
|
+
export interface Assertion {
|
|
18
|
+
/** Assertion type */
|
|
19
|
+
type: AssertionType;
|
|
20
|
+
/** CSS selector for element (not needed for url, title) */
|
|
21
|
+
selector?: string;
|
|
22
|
+
/** Expected value */
|
|
23
|
+
expected?: any;
|
|
24
|
+
/** Operator for comparison (default: 'eq') */
|
|
25
|
+
operator?: AssertionOperator;
|
|
26
|
+
/** Whether to negate the assertion */
|
|
27
|
+
not?: boolean;
|
|
28
|
+
/** Timeout in milliseconds (default: 5000) */
|
|
29
|
+
timeout?: number;
|
|
30
|
+
/** Assertion message/description */
|
|
31
|
+
message?: string;
|
|
32
|
+
/** Whether this is a soft assertion (doesn't stop test) */
|
|
33
|
+
soft?: boolean;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Assertion result
|
|
37
|
+
*/
|
|
38
|
+
export interface AssertionResult {
|
|
39
|
+
/** The assertion that was run */
|
|
40
|
+
assertion: Assertion;
|
|
41
|
+
/** Whether the assertion passed */
|
|
42
|
+
passed: boolean;
|
|
43
|
+
/** Actual value received */
|
|
44
|
+
actual?: any;
|
|
45
|
+
/** Expected value */
|
|
46
|
+
expected?: any;
|
|
47
|
+
/** Error message if failed */
|
|
48
|
+
error?: string;
|
|
49
|
+
/** Duration in milliseconds */
|
|
50
|
+
duration: number;
|
|
51
|
+
/** Timestamp */
|
|
52
|
+
timestamp: string;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Assertion suite
|
|
56
|
+
*/
|
|
57
|
+
export interface AssertionSuite {
|
|
58
|
+
/** Suite name */
|
|
59
|
+
name: string;
|
|
60
|
+
/** List of assertions */
|
|
61
|
+
assertions: Assertion[];
|
|
62
|
+
/** Run all assertions in parallel */
|
|
63
|
+
parallel?: boolean;
|
|
64
|
+
/** Stop on first failure */
|
|
65
|
+
stopOnFailure?: boolean;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Assertion run options
|
|
69
|
+
*/
|
|
70
|
+
export interface AssertionRunOptions {
|
|
71
|
+
/** Playwright Page object */
|
|
72
|
+
page: any;
|
|
73
|
+
/** Default timeout for assertions */
|
|
74
|
+
timeout?: number;
|
|
75
|
+
/** Whether to throw on failure */
|
|
76
|
+
throwOnFailure?: boolean;
|
|
77
|
+
/** Callback for assertion results */
|
|
78
|
+
onResult?: (result: AssertionResult) => void;
|
|
79
|
+
/** Stop on first failure */
|
|
80
|
+
stopOnFailure?: boolean;
|
|
81
|
+
/** Run assertions in parallel */
|
|
82
|
+
parallel?: boolean;
|
|
83
|
+
/** Name for the assertion group */
|
|
84
|
+
name?: string;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Assertion error
|
|
88
|
+
*/
|
|
89
|
+
export declare class AssertionError extends Error {
|
|
90
|
+
assertion: Assertion;
|
|
91
|
+
actual?: any;
|
|
92
|
+
expected?: any;
|
|
93
|
+
constructor(message: string, assertion: Assertion, actual?: any, expected?: any);
|
|
94
|
+
toString(): string;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Soft assertion collection
|
|
98
|
+
*/
|
|
99
|
+
export declare class SoftAssertionError extends Error {
|
|
100
|
+
results: AssertionResult[];
|
|
101
|
+
constructor(results: AssertionResult[]);
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Assertion group result
|
|
105
|
+
*/
|
|
106
|
+
export interface AssertionGroupResult {
|
|
107
|
+
/** Group name */
|
|
108
|
+
name: string;
|
|
109
|
+
/** Individual results */
|
|
110
|
+
results: AssertionResult[];
|
|
111
|
+
/** Overall pass status */
|
|
112
|
+
passed: boolean;
|
|
113
|
+
/** Total duration */
|
|
114
|
+
duration: number;
|
|
115
|
+
/** Number of passed assertions */
|
|
116
|
+
passedCount: number;
|
|
117
|
+
/** Number of failed assertions */
|
|
118
|
+
failedCount: number;
|
|
119
|
+
/** Number of skipped assertions */
|
|
120
|
+
skippedCount: number;
|
|
121
|
+
}
|