create-backlist 10.0.7 → 10.0.9
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/bin/index.js +1 -1
- package/package.json +1 -1
- package/src/qa/browser/crawler.js +63 -47
- package/src/qa/browser/interactions.js +108 -46
- package/src/qa/qa-engine.js +1137 -751
package/bin/index.js
CHANGED
|
@@ -272,7 +272,7 @@ async function printAnimatedBanner() {
|
|
|
272
272
|
' ║ / /_/ / / ___ |/ /___/ /| | / /____/ / ___/ / ║',
|
|
273
273
|
' ║/_____/ /_/ |_|\\____/_/ |_| /_____/___//____/ ║',
|
|
274
274
|
' ║ ║',
|
|
275
|
-
' ║ ⚡
|
|
275
|
+
' ║ ⚡ v10.0-ULTRA — BACKLIST NEXTOG ENTERPRICE CLI ⚡ ║',
|
|
276
276
|
' ║ Real Testing · AI Powered · Zero Fake Data · Playwright ║',
|
|
277
277
|
' ╚══════════════════════════════════════════════════════════════╝',
|
|
278
278
|
];
|
package/package.json
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
// Smart crawler with HTTP fallback when browser unavailable
|
|
2
|
+
import chalk from 'chalk';
|
|
2
3
|
import { URL } from 'node:url';
|
|
3
4
|
import { shortId } from '../qa-engine.js';
|
|
4
5
|
import { getBrowserLaunchOptions } from './installer.js';
|
|
@@ -19,9 +20,9 @@ export class HTTPCrawler {
|
|
|
19
20
|
while (queue.length > 0 && routes.length < maxPages) {
|
|
20
21
|
const { url, depth } = queue.shift();
|
|
21
22
|
const norm = this.#norm(url);
|
|
22
|
-
if (!norm || this.#visited.has(norm))
|
|
23
|
-
if (!this.#sameOrigin(norm, baseUrl))
|
|
24
|
-
if (depth > 3)
|
|
23
|
+
if (!norm || this.#visited.has(norm)) continue;
|
|
24
|
+
if (!this.#sameOrigin(norm, baseUrl)) continue;
|
|
25
|
+
if (depth > 3) continue;
|
|
25
26
|
|
|
26
27
|
this.#visited.add(norm);
|
|
27
28
|
|
|
@@ -29,18 +30,15 @@ export class HTTPCrawler {
|
|
|
29
30
|
routes.push(route);
|
|
30
31
|
if (onRoute) onRoute(route);
|
|
31
32
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
if (ln && !this.#visited.has(ln) && this.#sameOrigin(ln, baseUrl)) {
|
|
37
|
-
queue.push({ url: ln, depth: depth + 1 });
|
|
38
|
-
}
|
|
33
|
+
for (const link of (route.links || [])) {
|
|
34
|
+
const ln = this.#norm(link);
|
|
35
|
+
if (ln && !this.#visited.has(ln) && this.#sameOrigin(ln, baseUrl)) {
|
|
36
|
+
queue.push({ url: ln, depth: depth + 1 });
|
|
39
37
|
}
|
|
40
38
|
}
|
|
41
39
|
}
|
|
42
40
|
|
|
43
|
-
//
|
|
41
|
+
// Probe common API paths
|
|
44
42
|
const apiPaths = [
|
|
45
43
|
'/api/health', '/api/status', '/api/v1/health',
|
|
46
44
|
'/api/v1/users', '/api/v1/products', '/health',
|
|
@@ -48,15 +46,17 @@ export class HTTPCrawler {
|
|
|
48
46
|
];
|
|
49
47
|
|
|
50
48
|
for (const p of apiPaths) {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
49
|
+
try {
|
|
50
|
+
const url = new URL(p, baseUrl).toString();
|
|
51
|
+
const norm = this.#norm(url);
|
|
52
|
+
if (this.#visited.has(norm)) continue;
|
|
53
|
+
this.#visited.add(norm);
|
|
54
|
+
const route = await this.#probeURL(url, 0);
|
|
55
|
+
if (route.status > 0 && route.status < 500) {
|
|
56
|
+
routes.push(route);
|
|
57
|
+
if (onRoute) onRoute(route);
|
|
58
|
+
}
|
|
59
|
+
} catch {}
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
return routes;
|
|
@@ -75,7 +75,7 @@ export class HTTPCrawler {
|
|
|
75
75
|
});
|
|
76
76
|
clearTimeout(timer);
|
|
77
77
|
|
|
78
|
-
const ct
|
|
78
|
+
const ct = res.headers.get('content-type') || '';
|
|
79
79
|
const duration = Date.now() - t0;
|
|
80
80
|
const headers = {};
|
|
81
81
|
res.headers.forEach((v, k) => { headers[k] = v; });
|
|
@@ -151,25 +151,24 @@ export class HTTPCrawler {
|
|
|
151
151
|
}
|
|
152
152
|
|
|
153
153
|
#detectType(url, ct, status) {
|
|
154
|
-
if (status >= 400)
|
|
154
|
+
if (status >= 400) return 'error-page';
|
|
155
155
|
if (ct.includes('json') || url.includes('/api/')) return 'api';
|
|
156
156
|
if (url.endsWith('.xml') || url.endsWith('.txt')) return 'resource';
|
|
157
|
-
if (/\/(login|signin|auth)/i.test(url))
|
|
158
|
-
if (/\/(admin)/i.test(url))
|
|
159
|
-
if (/\/(dashboard)/i.test(url))
|
|
157
|
+
if (/\/(login|signin|auth)/i.test(url)) return 'auth';
|
|
158
|
+
if (/\/(admin)/i.test(url)) return 'admin';
|
|
159
|
+
if (/\/(dashboard)/i.test(url)) return 'dashboard';
|
|
160
160
|
return 'page';
|
|
161
161
|
}
|
|
162
162
|
}
|
|
163
163
|
|
|
164
164
|
// ── Browser-powered crawler (Playwright) ──────────────────────────────────
|
|
165
165
|
export class SmartCrawler {
|
|
166
|
-
#visited
|
|
166
|
+
#visited = new Set();
|
|
167
167
|
#launchOpts = null;
|
|
168
168
|
|
|
169
|
-
constructor(_playwright) {}
|
|
169
|
+
constructor(_playwright) {}
|
|
170
170
|
|
|
171
171
|
async crawl(baseUrl, { maxPages = 60, maxDepth = 4, onRoute } = {}) {
|
|
172
|
-
// Resolve launch options including auto-install
|
|
173
172
|
if (!this.#launchOpts) {
|
|
174
173
|
this.#launchOpts = await getBrowserLaunchOptions();
|
|
175
174
|
}
|
|
@@ -213,9 +212,9 @@ export class SmartCrawler {
|
|
|
213
212
|
while (queue.length > 0 && routes.length < maxPages) {
|
|
214
213
|
const { url, depth } = queue.shift();
|
|
215
214
|
const norm = this.#norm(url);
|
|
216
|
-
if (!norm || this.#visited.has(norm))
|
|
217
|
-
if (!this.#sameOrigin(norm, baseUrl))
|
|
218
|
-
if (depth > maxDepth)
|
|
215
|
+
if (!norm || this.#visited.has(norm)) continue;
|
|
216
|
+
if (!this.#sameOrigin(norm, baseUrl)) continue;
|
|
217
|
+
if (depth > maxDepth) continue;
|
|
219
218
|
this.#visited.add(norm);
|
|
220
219
|
|
|
221
220
|
const route = await this.#probePage(context, norm, depth, baseUrl);
|
|
@@ -240,9 +239,9 @@ export class SmartCrawler {
|
|
|
240
239
|
}
|
|
241
240
|
|
|
242
241
|
async #probePage(context, url, depth, baseUrl) {
|
|
243
|
-
const page
|
|
244
|
-
const networkRequests
|
|
245
|
-
const links
|
|
242
|
+
const page = await context.newPage();
|
|
243
|
+
const networkRequests = [];
|
|
244
|
+
const links = new Set();
|
|
246
245
|
|
|
247
246
|
page.on('request', req => {
|
|
248
247
|
const u = req.url();
|
|
@@ -257,27 +256,44 @@ export class SmartCrawler {
|
|
|
257
256
|
const ct = response?.headers()['content-type'] || '';
|
|
258
257
|
|
|
259
258
|
if (!ct.includes('text/html') && !ct.includes('application/xhtml')) {
|
|
260
|
-
return {
|
|
259
|
+
return {
|
|
260
|
+
id: shortId(), url,
|
|
261
|
+
type : this.#detectType(url, ct, status),
|
|
262
|
+
status, depth, links: [], forms: [],
|
|
263
|
+
};
|
|
261
264
|
}
|
|
262
265
|
|
|
263
|
-
const hrefs = await page.$$eval('a[href]', els => els.map(e => e.href).filter(Boolean))
|
|
266
|
+
const hrefs = await page.$$eval('a[href]', els => els.map(e => e.href).filter(Boolean))
|
|
267
|
+
.catch(() => []);
|
|
264
268
|
hrefs.forEach(h => links.add(h));
|
|
265
269
|
|
|
266
270
|
const forms = await page.$$eval('form', els => els.map(f => ({
|
|
267
|
-
action: f.action,
|
|
271
|
+
action: f.action,
|
|
272
|
+
method: f.method || 'GET',
|
|
268
273
|
fields: Array.from(f.elements).map(el => ({
|
|
269
|
-
name: el.name,
|
|
274
|
+
name : el.name,
|
|
275
|
+
type : el.type,
|
|
276
|
+
required: el.required,
|
|
270
277
|
})).filter(f => f.name),
|
|
271
278
|
}))).catch(() => []);
|
|
272
279
|
|
|
273
|
-
networkRequests.forEach(r => {
|
|
280
|
+
networkRequests.forEach(r => {
|
|
281
|
+
if (this.#sameOrigin(r.url, baseUrl)) links.add(r.url);
|
|
282
|
+
});
|
|
274
283
|
|
|
275
284
|
return {
|
|
276
|
-
id: shortId(), url,
|
|
277
|
-
|
|
285
|
+
id: shortId(), url,
|
|
286
|
+
type : this.#detectType(url, ct, status),
|
|
287
|
+
status, depth,
|
|
288
|
+
links : [...links],
|
|
289
|
+
forms,
|
|
290
|
+
contentType: ct,
|
|
278
291
|
};
|
|
279
292
|
} catch (err) {
|
|
280
|
-
return {
|
|
293
|
+
return {
|
|
294
|
+
id: shortId(), url, type: 'error', status: 0,
|
|
295
|
+
depth, links: [], forms: [], error: err.message,
|
|
296
|
+
};
|
|
281
297
|
} finally {
|
|
282
298
|
await page.close().catch(() => {});
|
|
283
299
|
}
|
|
@@ -294,12 +310,12 @@ export class SmartCrawler {
|
|
|
294
310
|
}
|
|
295
311
|
|
|
296
312
|
#detectType(url, ct, status) {
|
|
297
|
-
if (status >= 400)
|
|
313
|
+
if (status >= 400) return 'error-page';
|
|
298
314
|
if (ct.includes('json') || url.includes('/api/')) return 'api';
|
|
299
315
|
if (url.endsWith('.xml') || url.endsWith('.txt')) return 'resource';
|
|
300
|
-
if (/\/(login|signin|auth)/i.test(url))
|
|
301
|
-
if (/\/(admin)/i.test(url))
|
|
302
|
-
if (/\/(dashboard)/i.test(url))
|
|
316
|
+
if (/\/(login|signin|auth)/i.test(url)) return 'auth';
|
|
317
|
+
if (/\/(admin)/i.test(url)) return 'admin';
|
|
318
|
+
if (/\/(dashboard)/i.test(url)) return 'dashboard';
|
|
303
319
|
return 'page';
|
|
304
320
|
}
|
|
305
|
-
}
|
|
321
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// Real browser interactions with HTTP fallback
|
|
2
|
-
import
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import { shortId, sleep } from '../qa-engine.js';
|
|
3
4
|
import { getBrowserLaunchOptions } from './installer.js';
|
|
4
5
|
|
|
5
6
|
// ── HTTP-only page tester (fallback) ──────────────────────────────────────
|
|
@@ -27,30 +28,28 @@ export class HTTPPageTester {
|
|
|
27
28
|
try { text = await res.text(); } catch {}
|
|
28
29
|
}
|
|
29
30
|
|
|
30
|
-
const pass
|
|
31
|
-
|
|
32
|
-
// Extract forms from HTML
|
|
31
|
+
const pass = status >= 200 && status < 400;
|
|
33
32
|
const forms = this.#extractForms(text);
|
|
34
33
|
|
|
35
34
|
onNetworkEvent?.({ type: 'response', url, status, headers });
|
|
36
35
|
|
|
37
36
|
return {
|
|
38
37
|
pass,
|
|
39
|
-
failReason
|
|
40
|
-
page
|
|
41
|
-
loadTime
|
|
42
|
-
consoleErrors
|
|
43
|
-
networkErrors
|
|
44
|
-
jsErrors
|
|
45
|
-
resourcesFailed
|
|
38
|
+
failReason : pass ? null : `HTTP ${status}`,
|
|
39
|
+
page : null,
|
|
40
|
+
loadTime : duration,
|
|
41
|
+
consoleErrors : [],
|
|
42
|
+
networkErrors : [],
|
|
43
|
+
jsErrors : [],
|
|
44
|
+
resourcesFailed : [],
|
|
46
45
|
interactedElements: [],
|
|
47
|
-
renderTime
|
|
48
|
-
domContentLoaded: null,
|
|
46
|
+
renderTime : null,
|
|
47
|
+
domContentLoaded : null,
|
|
49
48
|
forms,
|
|
50
49
|
status,
|
|
51
50
|
headers,
|
|
52
51
|
message: pass ? `HTTP ${status} in ${duration}ms` : `HTTP ${status}`,
|
|
53
|
-
mode
|
|
52
|
+
mode : 'http-fallback',
|
|
54
53
|
};
|
|
55
54
|
} catch (err) {
|
|
56
55
|
return {
|
|
@@ -63,13 +62,22 @@ export class HTTPPageTester {
|
|
|
63
62
|
}
|
|
64
63
|
|
|
65
64
|
async testForm(page, form) {
|
|
66
|
-
return {
|
|
65
|
+
return {
|
|
66
|
+
pass : true,
|
|
67
|
+
validationOk: true,
|
|
68
|
+
submissionOk: true,
|
|
69
|
+
errors : [],
|
|
70
|
+
duration : 0,
|
|
71
|
+
message : 'HTTP mode — form structure detected',
|
|
72
|
+
};
|
|
67
73
|
}
|
|
68
74
|
|
|
69
75
|
async testAuthFlow(page, url, opts) {
|
|
70
|
-
// HTTP-mode auth check: verify login page exists + returns 200
|
|
71
76
|
try {
|
|
72
|
-
const res = await fetch(url, {
|
|
77
|
+
const res = await fetch(url, {
|
|
78
|
+
redirect: 'follow',
|
|
79
|
+
signal : AbortSignal.timeout(8000),
|
|
80
|
+
});
|
|
73
81
|
return {
|
|
74
82
|
pass : res.status === 200,
|
|
75
83
|
details: [{ url, status: res.status, note: 'Login page reachable (HTTP mode)' }],
|
|
@@ -93,7 +101,7 @@ export class HTTPPageTester {
|
|
|
93
101
|
const method = (attrs.match(/method=["']([^"']+)["']/) || [])[1] || 'GET';
|
|
94
102
|
const fields = [];
|
|
95
103
|
const inpRe = /<input([^>]*)>/gi;
|
|
96
|
-
let
|
|
104
|
+
let inp;
|
|
97
105
|
while ((inp = inpRe.exec(body)) !== null) {
|
|
98
106
|
const name = (inp[1].match(/name=["']([^"']+)["']/) || [])[1];
|
|
99
107
|
const type = (inp[1].match(/type=["']([^"']+)["']/) || [])[1] || 'text';
|
|
@@ -110,9 +118,9 @@ export class HTTPPageTester {
|
|
|
110
118
|
export class BrowserInteractor {
|
|
111
119
|
#playwright;
|
|
112
120
|
#session;
|
|
113
|
-
#browser
|
|
114
|
-
#context
|
|
115
|
-
#launchOpts
|
|
121
|
+
#browser = null;
|
|
122
|
+
#context = null;
|
|
123
|
+
#launchOpts = null;
|
|
116
124
|
#httpFallback;
|
|
117
125
|
#useFallback = false;
|
|
118
126
|
|
|
@@ -133,9 +141,20 @@ export class BrowserInteractor {
|
|
|
133
141
|
return;
|
|
134
142
|
}
|
|
135
143
|
|
|
144
|
+
// If playwright module wasn't loaded, fall back
|
|
145
|
+
if (!this.#playwright) {
|
|
146
|
+
this.#useFallback = true;
|
|
147
|
+
console.log(chalk.yellow('\n ⚠ Playwright module not available — using HTTP-only mode\n'));
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
|
|
136
151
|
try {
|
|
137
152
|
const { executablePath, headless, args } = this.#launchOpts;
|
|
138
|
-
this.#browser
|
|
153
|
+
this.#browser = await this.#playwright.chromium.launch({
|
|
154
|
+
executablePath,
|
|
155
|
+
headless,
|
|
156
|
+
args,
|
|
157
|
+
});
|
|
139
158
|
this.#context = await this.#browser.newContext({
|
|
140
159
|
viewport : { width: 1280, height: 800 },
|
|
141
160
|
ignoreHTTPSErrors: true,
|
|
@@ -160,11 +179,11 @@ export class BrowserInteractor {
|
|
|
160
179
|
return this.#httpFallback.testPage(url, opts);
|
|
161
180
|
}
|
|
162
181
|
|
|
163
|
-
const page
|
|
164
|
-
const consoleErrors
|
|
165
|
-
const networkErrors
|
|
166
|
-
const jsErrors
|
|
167
|
-
const resourcesFailed
|
|
182
|
+
const page = await this.#context.newPage();
|
|
183
|
+
const consoleErrors = [];
|
|
184
|
+
const networkErrors = [];
|
|
185
|
+
const jsErrors = [];
|
|
186
|
+
const resourcesFailed = [];
|
|
168
187
|
const interactedElements = [];
|
|
169
188
|
|
|
170
189
|
page.on('console', msg => {
|
|
@@ -182,7 +201,12 @@ export class BrowserInteractor {
|
|
|
182
201
|
});
|
|
183
202
|
|
|
184
203
|
page.on('requestfailed', req => {
|
|
185
|
-
const entry = {
|
|
204
|
+
const entry = {
|
|
205
|
+
url : req.url(),
|
|
206
|
+
method : req.method(),
|
|
207
|
+
failure : req.failure()?.errorText || 'unknown',
|
|
208
|
+
timestamp: Date.now(),
|
|
209
|
+
};
|
|
186
210
|
networkErrors.push(entry);
|
|
187
211
|
resourcesFailed.push({ url: req.url(), type: req.resourceType(), failure: entry.failure });
|
|
188
212
|
opts.onNetworkEvent?.({ type: 'failed', ...entry });
|
|
@@ -199,27 +223,33 @@ export class BrowserInteractor {
|
|
|
199
223
|
const response = await page.goto(url, { waitUntil: 'networkidle', timeout: 20_000 });
|
|
200
224
|
const status = response?.status() || 0;
|
|
201
225
|
|
|
202
|
-
if
|
|
226
|
+
if (status >= 500) { pass = false; failReason = `Server error: HTTP ${status}`; }
|
|
203
227
|
else if (status >= 400) { pass = false; failReason = `Client error: HTTP ${status}`; }
|
|
204
228
|
|
|
205
229
|
const timing = await page.evaluate(() => {
|
|
206
230
|
const t = window.performance?.timing;
|
|
207
231
|
if (!t) return null;
|
|
208
|
-
return {
|
|
232
|
+
return {
|
|
233
|
+
renderTime : t.domComplete - t.navigationStart,
|
|
234
|
+
domContentLoaded: t.domContentLoadedEventEnd - t.navigationStart,
|
|
235
|
+
};
|
|
209
236
|
}).catch(() => null);
|
|
210
237
|
|
|
211
238
|
renderTime = timing?.renderTime;
|
|
212
239
|
domContentLoaded = timing?.domContentLoaded;
|
|
213
240
|
|
|
214
241
|
const forms = await page.$$eval('form', els => els.map(f => ({
|
|
215
|
-
action: f.action,
|
|
242
|
+
action: f.action,
|
|
243
|
+
method: f.method || 'GET',
|
|
216
244
|
fields: Array.from(f.elements).map(el => ({
|
|
217
|
-
name: el.name,
|
|
245
|
+
name : el.name,
|
|
246
|
+
type : el.type,
|
|
247
|
+
required: el.required,
|
|
218
248
|
tagName: el.tagName?.toLowerCase(),
|
|
219
249
|
})).filter(f => f.name),
|
|
220
250
|
}))).catch(() => []);
|
|
221
251
|
|
|
222
|
-
// Real interactions
|
|
252
|
+
// Real hover interactions on nav links
|
|
223
253
|
const navLinks = await page.$$('nav a, header a').catch(() => []);
|
|
224
254
|
for (const link of navLinks.slice(0, 5)) {
|
|
225
255
|
try {
|
|
@@ -243,8 +273,10 @@ export class BrowserInteractor {
|
|
|
243
273
|
|
|
244
274
|
} catch (err) {
|
|
245
275
|
return {
|
|
246
|
-
pass: false,
|
|
247
|
-
|
|
276
|
+
pass : false,
|
|
277
|
+
failReason: err.message,
|
|
278
|
+
page,
|
|
279
|
+
loadTime : Date.now() - t0,
|
|
248
280
|
consoleErrors, networkErrors, jsErrors,
|
|
249
281
|
resourcesFailed, interactedElements,
|
|
250
282
|
forms: [], message: err.message, mode: 'browser',
|
|
@@ -270,10 +302,24 @@ export class BrowserInteractor {
|
|
|
270
302
|
els => els.map(e => e.textContent?.trim()).filter(Boolean)
|
|
271
303
|
).catch(() => []);
|
|
272
304
|
|
|
273
|
-
return {
|
|
305
|
+
return {
|
|
306
|
+
pass : true,
|
|
307
|
+
validationOk: msgs.length > 0,
|
|
308
|
+
submissionOk: true,
|
|
309
|
+
errors,
|
|
310
|
+
duration : Date.now() - t0,
|
|
311
|
+
message : 'Form tested',
|
|
312
|
+
};
|
|
274
313
|
} catch (err) {
|
|
275
314
|
errors.push(err.message);
|
|
276
|
-
return {
|
|
315
|
+
return {
|
|
316
|
+
pass : false,
|
|
317
|
+
validationOk: false,
|
|
318
|
+
submissionOk: false,
|
|
319
|
+
errors,
|
|
320
|
+
duration : Date.now() - t0,
|
|
321
|
+
message : err.message,
|
|
322
|
+
};
|
|
277
323
|
}
|
|
278
324
|
}
|
|
279
325
|
|
|
@@ -286,35 +332,51 @@ export class BrowserInteractor {
|
|
|
286
332
|
for (const cred of (opts.testCredentials || [])) {
|
|
287
333
|
try {
|
|
288
334
|
await page.goto(url, { waitUntil: 'networkidle', timeout: 15_000 });
|
|
335
|
+
|
|
289
336
|
const uf = await page.$('input[type="email"],input[name="email"],input[name="username"]');
|
|
290
337
|
const pf = await page.$('input[type="password"]');
|
|
291
338
|
if (!uf || !pf) { details.push({ note: 'Login fields not found' }); continue; }
|
|
339
|
+
|
|
292
340
|
await uf.fill(cred.username);
|
|
293
341
|
await pf.fill(cred.password);
|
|
342
|
+
|
|
294
343
|
const btn = await page.$('[type="submit"]');
|
|
295
344
|
if (btn) { await btn.click(); await page.waitForTimeout(2000); }
|
|
296
345
|
|
|
297
346
|
const body = await page.textContent('body').catch(() => '');
|
|
298
347
|
const curUrl = page.url();
|
|
299
|
-
const rejected = /invalid|incorrect|error|wrong|fail/i.test(body)
|
|
348
|
+
const rejected = /invalid|incorrect|error|wrong|fail/i.test(body)
|
|
349
|
+
|| curUrl.includes('login');
|
|
350
|
+
|
|
351
|
+
details.push({
|
|
352
|
+
credentials: cred.username.slice(0, 8) + '...',
|
|
353
|
+
expectFail : cred.expectFail,
|
|
354
|
+
wasRejected: rejected,
|
|
355
|
+
currentUrl : curUrl,
|
|
356
|
+
});
|
|
300
357
|
|
|
301
|
-
details.push({ credentials: cred.username.slice(0, 8) + '...', expectFail: cred.expectFail, wasRejected: rejected, currentUrl: curUrl });
|
|
302
358
|
if (cred.expectFail && !rejected) { pass = false; }
|
|
303
359
|
} catch (err) {
|
|
304
360
|
details.push({ error: err.message });
|
|
305
361
|
}
|
|
306
362
|
}
|
|
307
|
-
|
|
363
|
+
|
|
364
|
+
return {
|
|
365
|
+
pass,
|
|
366
|
+
details,
|
|
367
|
+
duration: Date.now() - t0,
|
|
368
|
+
message : pass ? 'Auth flow validated' : 'Auth issues detected',
|
|
369
|
+
};
|
|
308
370
|
}
|
|
309
371
|
|
|
310
372
|
#testValue(field) {
|
|
311
373
|
const t = (field.type || 'text').toLowerCase();
|
|
312
374
|
const n = (field.name || '').toLowerCase();
|
|
313
|
-
if (t === 'email'
|
|
314
|
-
if (t === 'password' || n.includes('pass'))
|
|
315
|
-
if (t === 'tel'
|
|
316
|
-
if (t === 'number')
|
|
317
|
-
if (n.includes('name'))
|
|
375
|
+
if (t === 'email' || n.includes('email')) return 'test@backlist-qa.dev';
|
|
376
|
+
if (t === 'password' || n.includes('pass')) return 'TestPass123!';
|
|
377
|
+
if (t === 'tel' || n.includes('phone')) return '+1-555-000-0000';
|
|
378
|
+
if (t === 'number') return '42';
|
|
379
|
+
if (n.includes('name')) return 'QA Test User';
|
|
318
380
|
return 'backlist-qa-test';
|
|
319
381
|
}
|
|
320
|
-
}
|
|
382
|
+
}
|