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
package/dist/commands/init.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* QA360 Init Command - Interactive pack generator
|
|
2
|
+
* QA360 Init Command - Interactive pack generator (v2)
|
|
3
3
|
*
|
|
4
4
|
* Usage:
|
|
5
5
|
* qa360 init
|
|
@@ -15,97 +15,295 @@ import { dump } from 'js-yaml';
|
|
|
15
15
|
// Use createRequire for CommonJS module inquirer
|
|
16
16
|
const require = createRequire(import.meta.url);
|
|
17
17
|
const inquirer = require('inquirer');
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
// ============================================================
|
|
19
|
+
// Available templates (v2)
|
|
20
|
+
// ============================================================
|
|
21
21
|
export const TEMPLATES = {
|
|
22
22
|
'api-basic': {
|
|
23
23
|
name: 'API Basic',
|
|
24
24
|
description: 'Simple API smoke tests (REST/GraphQL)',
|
|
25
|
-
gates: ['
|
|
25
|
+
gates: ['api-smoke'],
|
|
26
26
|
icon: 'š',
|
|
27
27
|
},
|
|
28
28
|
'ui-basic': {
|
|
29
29
|
name: 'UI Basic',
|
|
30
30
|
description: 'Basic UI/E2E browser tests',
|
|
31
|
-
gates: ['ui'],
|
|
31
|
+
gates: ['ui-smoke', 'a11y'],
|
|
32
32
|
icon: 'š',
|
|
33
33
|
},
|
|
34
34
|
'fullstack': {
|
|
35
35
|
name: 'Full Stack',
|
|
36
36
|
description: 'API + UI + Performance tests',
|
|
37
|
-
gates: ['
|
|
37
|
+
gates: ['api-smoke', 'api-crud', 'ui-smoke', 'perf'],
|
|
38
38
|
icon: 'šÆ',
|
|
39
39
|
},
|
|
40
40
|
'security': {
|
|
41
41
|
name: 'Security',
|
|
42
|
-
description: 'SAST, DAST, secrets scanning
|
|
43
|
-
gates: ['
|
|
42
|
+
description: 'SAST, DAST, secrets scanning',
|
|
43
|
+
gates: ['api-smoke', 'sast', 'dast', 'secrets'],
|
|
44
44
|
icon: 'š”ļø',
|
|
45
45
|
},
|
|
46
46
|
'complete': {
|
|
47
47
|
name: 'Complete',
|
|
48
48
|
description: 'All quality gates (API, UI, Performance, Security, Accessibility)',
|
|
49
|
-
gates: ['
|
|
49
|
+
gates: ['api-smoke', 'api-crud', 'ui-smoke', 'a11y', 'perf', 'sast', 'dast', 'secrets'],
|
|
50
50
|
icon: 'āØ',
|
|
51
51
|
},
|
|
52
52
|
};
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
53
|
+
// ============================================================
|
|
54
|
+
// Gate descriptions with beginner-friendly explanations
|
|
55
|
+
// ============================================================
|
|
56
56
|
export const GATE_DESCRIPTIONS = {
|
|
57
|
-
|
|
57
|
+
'api-smoke': {
|
|
58
58
|
short: 'API smoke tests (REST/GraphQL health checks)',
|
|
59
59
|
beginner: 'Tests if your API is alive and responds correctly. Like checking if the server is up.',
|
|
60
60
|
example: 'Checks if GET /api/health returns 200 OK',
|
|
61
61
|
icon: 'š',
|
|
62
|
+
adapter: 'playwright-api',
|
|
63
|
+
},
|
|
64
|
+
'api-crud': {
|
|
65
|
+
short: 'API CRUD tests (Create, Read, Update, Delete)',
|
|
66
|
+
beginner: 'Tests the full lifecycle of your data operations.',
|
|
67
|
+
example: 'Tests POST /users, GET /users, PUT /users/1, DELETE /users/1',
|
|
68
|
+
icon: 'š§',
|
|
69
|
+
adapter: 'playwright-api',
|
|
62
70
|
},
|
|
63
|
-
ui: {
|
|
71
|
+
'ui-smoke': {
|
|
64
72
|
short: 'UI/E2E tests (browser automation)',
|
|
65
73
|
beginner: 'Tests your website in a real browser. Checks if pages load and buttons work.',
|
|
66
74
|
example: 'Opens your site and verifies the login form appears',
|
|
67
75
|
icon: 'š',
|
|
76
|
+
adapter: 'playwright-ui',
|
|
68
77
|
},
|
|
69
|
-
a11y: {
|
|
78
|
+
'a11y': {
|
|
70
79
|
short: 'Accessibility tests (WCAG compliance)',
|
|
71
80
|
beginner: 'Tests if your site is usable by people with disabilities (screen readers, color blindness).',
|
|
72
81
|
example: 'Checks if images have alt text for blind users',
|
|
73
82
|
icon: 'āæ',
|
|
83
|
+
adapter: 'playwright-ui',
|
|
74
84
|
},
|
|
75
|
-
perf: {
|
|
85
|
+
'perf': {
|
|
76
86
|
short: 'Performance tests (load/stress testing)',
|
|
77
87
|
beginner: 'Tests if your site stays fast when many people visit at once.',
|
|
78
88
|
example: 'Simulates 100 users visiting simultaneously',
|
|
79
89
|
icon: 'ā”',
|
|
90
|
+
adapter: 'k6-perf',
|
|
80
91
|
},
|
|
81
|
-
sast: {
|
|
92
|
+
'sast': {
|
|
82
93
|
short: 'Static security analysis (code scanning)',
|
|
83
94
|
beginner: 'Analyzes your source code for security vulnerabilities without running it.',
|
|
84
95
|
example: 'Finds hardcoded passwords or unsafe functions',
|
|
85
96
|
icon: 'š',
|
|
97
|
+
adapter: 'semgrep-sast',
|
|
86
98
|
},
|
|
87
|
-
dast: {
|
|
99
|
+
'dast': {
|
|
88
100
|
short: 'Dynamic security testing (runtime scanning)',
|
|
89
101
|
beginner: 'Attacks your running application to find security weaknesses.',
|
|
90
102
|
example: 'Tries common attacks like SQL injection',
|
|
91
103
|
icon: 'āļø',
|
|
104
|
+
adapter: 'zap-dast',
|
|
92
105
|
},
|
|
93
|
-
secrets: {
|
|
106
|
+
'secrets': {
|
|
94
107
|
short: 'Secrets detection (credential scanning)',
|
|
95
108
|
beginner: 'Scans for accidentally committed passwords, API keys, or tokens.',
|
|
96
109
|
example: 'Finds AWS keys in your code',
|
|
97
110
|
icon: 'š',
|
|
98
|
-
|
|
99
|
-
deps: {
|
|
100
|
-
short: 'Dependency vulnerability checks',
|
|
101
|
-
beginner: 'Checks if your libraries have known security issues.',
|
|
102
|
-
example: 'Checks if you use an old version of a package with bugs',
|
|
103
|
-
icon: 'š¦',
|
|
111
|
+
adapter: 'gitleaks-secrets',
|
|
104
112
|
},
|
|
105
113
|
};
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
114
|
+
// ============================================================
|
|
115
|
+
// Helper: Create gate config
|
|
116
|
+
// ============================================================
|
|
117
|
+
function createGate(adapter, baseUrl, extraConfig = {}) {
|
|
118
|
+
return {
|
|
119
|
+
adapter,
|
|
120
|
+
enabled: true,
|
|
121
|
+
config: {
|
|
122
|
+
baseUrl,
|
|
123
|
+
...extraConfig,
|
|
124
|
+
},
|
|
125
|
+
options: {
|
|
126
|
+
timeout: 30000,
|
|
127
|
+
retries: 2,
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
// ============================================================
|
|
132
|
+
// Helper: Generate v2 pack from selected gates
|
|
133
|
+
// ============================================================
|
|
134
|
+
function generateV2Pack(name, gates, apiTarget, webTarget) {
|
|
135
|
+
const pack = {
|
|
136
|
+
version: 2,
|
|
137
|
+
name,
|
|
138
|
+
description: `QA360 test pack for ${name}`,
|
|
139
|
+
gates: {},
|
|
140
|
+
execution: {
|
|
141
|
+
default_timeout: 30000,
|
|
142
|
+
default_retries: 2,
|
|
143
|
+
on_failure: 'continue',
|
|
144
|
+
parallel: true,
|
|
145
|
+
},
|
|
146
|
+
};
|
|
147
|
+
const apiBaseUrl = apiTarget ? `\${BASE_URL:-${apiTarget}}` : '${BASE_URL:-https://api.example.com}';
|
|
148
|
+
const webBaseUrl = webTarget ? `\${BASE_URL:-${webTarget}}` : '${BASE_URL:-https://example.com}';
|
|
149
|
+
// Build gates
|
|
150
|
+
for (const gate of gates) {
|
|
151
|
+
const gateInfo = GATE_DESCRIPTIONS[gate];
|
|
152
|
+
if (!gateInfo)
|
|
153
|
+
continue;
|
|
154
|
+
switch (gate) {
|
|
155
|
+
case 'api-smoke':
|
|
156
|
+
pack.gates['api-health'] = createGate('playwright-api', apiBaseUrl, {
|
|
157
|
+
smoke: [
|
|
158
|
+
'GET /health -> 200',
|
|
159
|
+
'GET /status -> 200',
|
|
160
|
+
],
|
|
161
|
+
});
|
|
162
|
+
break;
|
|
163
|
+
case 'api-crud':
|
|
164
|
+
pack.gates['api-crud'] = createGate('playwright-api', apiBaseUrl, {
|
|
165
|
+
smoke: [
|
|
166
|
+
'GET /api/v1/users -> 200',
|
|
167
|
+
'POST /api/v1/users -> 201',
|
|
168
|
+
'PUT /api/v1/users/1 -> 200',
|
|
169
|
+
'DELETE /api/v1/users/1 -> 204',
|
|
170
|
+
],
|
|
171
|
+
});
|
|
172
|
+
pack.gates['api-crud'].budgets = {
|
|
173
|
+
p95_ms: 1000,
|
|
174
|
+
error_rate: 0.01,
|
|
175
|
+
};
|
|
176
|
+
break;
|
|
177
|
+
case 'ui-smoke':
|
|
178
|
+
pack.gates['ui-smoke'] = createGate('playwright-ui', webBaseUrl, {
|
|
179
|
+
pages: [
|
|
180
|
+
{
|
|
181
|
+
url: '/',
|
|
182
|
+
expectedElements: ['body', 'main'],
|
|
183
|
+
},
|
|
184
|
+
],
|
|
185
|
+
});
|
|
186
|
+
break;
|
|
187
|
+
case 'a11y':
|
|
188
|
+
pack.gates['a11y'] = {
|
|
189
|
+
adapter: 'playwright-ui',
|
|
190
|
+
enabled: true,
|
|
191
|
+
config: {
|
|
192
|
+
baseUrl: webBaseUrl,
|
|
193
|
+
pages: [
|
|
194
|
+
{
|
|
195
|
+
url: '/',
|
|
196
|
+
a11yRules: ['wcag2a', 'wcag2aa'],
|
|
197
|
+
},
|
|
198
|
+
],
|
|
199
|
+
},
|
|
200
|
+
budgets: {
|
|
201
|
+
violations: 10,
|
|
202
|
+
a11y_score: 90,
|
|
203
|
+
},
|
|
204
|
+
};
|
|
205
|
+
break;
|
|
206
|
+
case 'perf':
|
|
207
|
+
pack.gates['perf'] = {
|
|
208
|
+
adapter: 'k6-perf',
|
|
209
|
+
enabled: true,
|
|
210
|
+
config: {
|
|
211
|
+
script: `import http from 'k6/http';
|
|
212
|
+
import { check } from 'k6';
|
|
213
|
+
|
|
214
|
+
export const options = {
|
|
215
|
+
stages: [
|
|
216
|
+
{ duration: '10s', target: 10 },
|
|
217
|
+
{ duration: '20s', target: 50 },
|
|
218
|
+
{ duration: '10s', target: 0 },
|
|
219
|
+
],
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
export default function () {
|
|
223
|
+
const res = http.get('${apiBaseUrl}/health');
|
|
224
|
+
check(res, {
|
|
225
|
+
'status is 200': (r) => r.status === 200,
|
|
226
|
+
'response time < 500ms': (r) => r.timings.duration < 500,
|
|
227
|
+
});
|
|
228
|
+
}`,
|
|
229
|
+
},
|
|
230
|
+
budgets: {
|
|
231
|
+
p95_ms: 2000,
|
|
232
|
+
p99_ms: 5000,
|
|
233
|
+
error_rate: 0.05,
|
|
234
|
+
},
|
|
235
|
+
options: {
|
|
236
|
+
timeout: 60000,
|
|
237
|
+
retries: 1,
|
|
238
|
+
},
|
|
239
|
+
};
|
|
240
|
+
break;
|
|
241
|
+
case 'sast':
|
|
242
|
+
pack.gates['sast'] = {
|
|
243
|
+
adapter: 'semgrep-sast',
|
|
244
|
+
enabled: true,
|
|
245
|
+
config: {
|
|
246
|
+
rules: ['security', 'owasp-top-10'],
|
|
247
|
+
paths: ['src/', 'lib/'],
|
|
248
|
+
},
|
|
249
|
+
budgets: {
|
|
250
|
+
high_findings: 0,
|
|
251
|
+
medium_findings: 10,
|
|
252
|
+
},
|
|
253
|
+
};
|
|
254
|
+
break;
|
|
255
|
+
case 'dast':
|
|
256
|
+
pack.gates['dast'] = {
|
|
257
|
+
adapter: 'zap-dast',
|
|
258
|
+
enabled: true,
|
|
259
|
+
config: {
|
|
260
|
+
target: 'api',
|
|
261
|
+
profile: 'baseline',
|
|
262
|
+
url: apiBaseUrl,
|
|
263
|
+
scanType: 'baseline',
|
|
264
|
+
},
|
|
265
|
+
budgets: {
|
|
266
|
+
high_findings: 5,
|
|
267
|
+
medium_findings: 20,
|
|
268
|
+
},
|
|
269
|
+
options: {
|
|
270
|
+
timeout: 120000,
|
|
271
|
+
},
|
|
272
|
+
};
|
|
273
|
+
break;
|
|
274
|
+
case 'secrets':
|
|
275
|
+
pack.gates['secrets'] = {
|
|
276
|
+
adapter: 'gitleaks-secrets',
|
|
277
|
+
enabled: true,
|
|
278
|
+
config: {
|
|
279
|
+
paths: ['.', 'src/', 'config/'],
|
|
280
|
+
},
|
|
281
|
+
budgets: {
|
|
282
|
+
critical_findings: 0,
|
|
283
|
+
},
|
|
284
|
+
};
|
|
285
|
+
break;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
// Add hooks if needed
|
|
289
|
+
if (gates.includes('perf') || gates.includes('dast')) {
|
|
290
|
+
pack.hooks = {
|
|
291
|
+
beforeAll: [
|
|
292
|
+
{
|
|
293
|
+
type: 'wait_on',
|
|
294
|
+
wait_for: {
|
|
295
|
+
resource: `${apiBaseUrl}/health`,
|
|
296
|
+
timeout: 30000,
|
|
297
|
+
},
|
|
298
|
+
},
|
|
299
|
+
],
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
return pack;
|
|
303
|
+
}
|
|
304
|
+
// ============================================================
|
|
305
|
+
// Beginner mode - Step by step guided experience
|
|
306
|
+
// ============================================================
|
|
109
307
|
async function beginnerMode() {
|
|
110
308
|
console.log(chalk.bold.cyan('\nāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā'));
|
|
111
309
|
console.log(chalk.bold.cyan('ā š Welcome to QA360 - Your Quality Assistant! ā'));
|
|
@@ -136,25 +334,27 @@ async function beginnerMode() {
|
|
|
136
334
|
name: 'appType',
|
|
137
335
|
message: 'What type of application are you testing?',
|
|
138
336
|
choices: [
|
|
139
|
-
{ name: 'š API / Backend - REST or GraphQL API', value: 'api' },
|
|
140
|
-
{ name: 'š Website / Frontend - Web application', value: '
|
|
337
|
+
{ name: 'š API / Backend - REST or GraphQL API', value: 'api-basic' },
|
|
338
|
+
{ name: 'š Website / Frontend - Web application', value: 'ui-basic' },
|
|
141
339
|
{ name: 'šÆ Full Stack - Both API and Website', value: 'fullstack' },
|
|
142
340
|
{ name: 'š”ļø Security Scan - Check for vulnerabilities', value: 'security' },
|
|
143
341
|
{ name: '⨠Everything - All tests enabled', value: 'complete' },
|
|
144
342
|
],
|
|
145
343
|
},
|
|
146
344
|
]);
|
|
147
|
-
console.log(chalk.green(` ā You chose: ${appType}\n`));
|
|
345
|
+
console.log(chalk.green(` ā You chose: ${TEMPLATES[appType].name}\n`));
|
|
148
346
|
// Step 3: Gates explanation and selection
|
|
149
|
-
const suggestedGates = TEMPLATES[appType
|
|
347
|
+
const suggestedGates = TEMPLATES[appType].gates;
|
|
150
348
|
console.log(chalk.bold('Step 3ļøā£ - Quality Gates'));
|
|
151
349
|
console.log(chalk.gray(' Gates are different types of tests. We\'ll suggest some based on your choice.\n'));
|
|
152
350
|
console.log(chalk.cyan(' Suggested gates for you:\n'));
|
|
153
351
|
for (const gate of suggestedGates) {
|
|
154
352
|
const info = GATE_DESCRIPTIONS[gate];
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
353
|
+
if (info) {
|
|
354
|
+
console.log(chalk.cyan(` ${info.icon} ${gate}`));
|
|
355
|
+
console.log(chalk.gray(` ${info.beginner}`));
|
|
356
|
+
console.log(chalk.gray(` Example: ${info.example}\n`));
|
|
357
|
+
}
|
|
158
358
|
}
|
|
159
359
|
const { useSuggested } = await inquirer.prompt([
|
|
160
360
|
{
|
|
@@ -165,6 +365,8 @@ async function beginnerMode() {
|
|
|
165
365
|
},
|
|
166
366
|
]);
|
|
167
367
|
let gates = suggestedGates;
|
|
368
|
+
let apiTarget;
|
|
369
|
+
let webTarget;
|
|
168
370
|
if (!useSuggested) {
|
|
169
371
|
const { customGates } = await inquirer.prompt([
|
|
170
372
|
{
|
|
@@ -172,7 +374,7 @@ async function beginnerMode() {
|
|
|
172
374
|
name: 'customGates',
|
|
173
375
|
message: 'Select the gates you want:',
|
|
174
376
|
choices: Object.entries(GATE_DESCRIPTIONS).map(([key, info]) => ({
|
|
175
|
-
name: `${info
|
|
377
|
+
name: `${info.icon} ${key} - ${info.short}`,
|
|
176
378
|
value: key,
|
|
177
379
|
})),
|
|
178
380
|
validate: (input) => input.length > 0 || 'Select at least one gate',
|
|
@@ -184,8 +386,9 @@ async function beginnerMode() {
|
|
|
184
386
|
// Step 4: URLs
|
|
185
387
|
console.log(chalk.bold('Step 4ļøā£ - Application URLs'));
|
|
186
388
|
console.log(chalk.gray(' Where is your application running?\n'));
|
|
187
|
-
const
|
|
188
|
-
|
|
389
|
+
const hasApiGates = gates.some((g) => ['api-smoke', 'api-crud', 'perf', 'dast'].includes(g));
|
|
390
|
+
const hasUiGates = gates.some((g) => ['ui-smoke', 'a11y'].includes(g));
|
|
391
|
+
if (hasApiGates) {
|
|
189
392
|
console.log(chalk.cyan(' š” API Configuration'));
|
|
190
393
|
const apiAnswers = await inquirer.prompt([
|
|
191
394
|
{
|
|
@@ -195,20 +398,11 @@ async function beginnerMode() {
|
|
|
195
398
|
default: 'https://api.example.com',
|
|
196
399
|
validate: (input) => input.startsWith('http') || 'Must start with http:// or https://',
|
|
197
400
|
},
|
|
198
|
-
{
|
|
199
|
-
type: 'input',
|
|
200
|
-
name: 'smokeTests',
|
|
201
|
-
message: 'Which endpoints should we test? (format: "METHOD /path -> status")',
|
|
202
|
-
default: 'GET /health -> 200',
|
|
203
|
-
},
|
|
204
401
|
]);
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
smoke: [apiAnswers.smokeTests],
|
|
208
|
-
};
|
|
209
|
-
console.log(chalk.green(` ā API: ${apiAnswers.apiUrl}\n`));
|
|
402
|
+
apiTarget = apiAnswers.apiUrl;
|
|
403
|
+
console.log(chalk.green(` ā API: ${apiTarget}\n`));
|
|
210
404
|
}
|
|
211
|
-
if (
|
|
405
|
+
if (hasUiGates) {
|
|
212
406
|
console.log(chalk.cyan(' š Website Configuration'));
|
|
213
407
|
const uiAnswers = await inquirer.prompt([
|
|
214
408
|
{
|
|
@@ -219,50 +413,22 @@ async function beginnerMode() {
|
|
|
219
413
|
validate: (input) => input.startsWith('http') || 'Must start with http:// or https://',
|
|
220
414
|
},
|
|
221
415
|
]);
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
pages: [uiAnswers.webUrl],
|
|
225
|
-
};
|
|
226
|
-
console.log(chalk.green(` ā Website: ${uiAnswers.webUrl}\n`));
|
|
416
|
+
webTarget = uiAnswers.webUrl;
|
|
417
|
+
console.log(chalk.green(` ā Website: ${webTarget}\n`));
|
|
227
418
|
}
|
|
228
419
|
// Build pack
|
|
229
|
-
const pack =
|
|
230
|
-
version: 1,
|
|
231
|
-
name,
|
|
232
|
-
gates: gates,
|
|
233
|
-
targets,
|
|
234
|
-
};
|
|
235
|
-
// Add budgets
|
|
236
|
-
if (gates.includes('perf') || gates.includes('a11y')) {
|
|
237
|
-
pack.budgets = {};
|
|
238
|
-
if (gates.includes('perf')) {
|
|
239
|
-
pack.budgets.perf_p95_ms = 2000;
|
|
240
|
-
}
|
|
241
|
-
if (gates.includes('a11y')) {
|
|
242
|
-
pack.budgets.a11y_min = 90;
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
// Add security config
|
|
246
|
-
if (gates.some((g) => ['sast', 'dast', 'secrets', 'deps'].includes(g))) {
|
|
247
|
-
pack.security = {};
|
|
248
|
-
if (gates.includes('sast')) {
|
|
249
|
-
pack.security.sast = { max_critical: 0, max_high: 3 };
|
|
250
|
-
}
|
|
251
|
-
if (gates.includes('dast')) {
|
|
252
|
-
pack.security.dast = { max_high: 5 };
|
|
253
|
-
}
|
|
254
|
-
}
|
|
420
|
+
const pack = generateV2Pack(name, gates, apiTarget, webTarget);
|
|
255
421
|
// Final summary
|
|
256
422
|
console.log(chalk.bold.cyan('\n⨠Almost done! Here\'s your test pack summary:\n'));
|
|
257
423
|
console.log(chalk.white(` Name: ${chalk.bold(name)}`));
|
|
258
424
|
console.log(chalk.white(` Gates: ${gates.map(g => `${GATE_DESCRIPTIONS[g]?.icon} ${g}`).join(', ')}`));
|
|
259
|
-
console.log(chalk.white(` API: ${
|
|
260
|
-
console.log(chalk.white(` Website: ${
|
|
425
|
+
console.log(chalk.white(` API: ${apiTarget || 'N/A'}`));
|
|
426
|
+
console.log(chalk.white(` Website: ${webTarget || 'N/A'}`));
|
|
261
427
|
return pack;
|
|
262
428
|
}
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
429
|
+
// ============================================================
|
|
430
|
+
// Interactive prompts
|
|
431
|
+
// ============================================================
|
|
266
432
|
async function promptForConfig() {
|
|
267
433
|
const answers = await inquirer.prompt([
|
|
268
434
|
{
|
|
@@ -277,7 +443,7 @@ async function promptForConfig() {
|
|
|
277
443
|
name: 'template',
|
|
278
444
|
message: 'Choose a template:',
|
|
279
445
|
choices: Object.entries(TEMPLATES).map(([key, value]) => ({
|
|
280
|
-
name: `${value.name} - ${value.description}`,
|
|
446
|
+
name: `${value.icon} ${value.name} - ${value.description}`,
|
|
281
447
|
value: key,
|
|
282
448
|
})),
|
|
283
449
|
},
|
|
@@ -296,7 +462,7 @@ async function promptForConfig() {
|
|
|
296
462
|
name: 'gates',
|
|
297
463
|
message: 'Select quality gates to enable:',
|
|
298
464
|
choices: Object.entries(GATE_DESCRIPTIONS).map(([key, desc]) => ({
|
|
299
|
-
name: `${key} - ${desc.short}`,
|
|
465
|
+
name: `${desc.icon} ${key} - ${desc.short}`,
|
|
300
466
|
value: key,
|
|
301
467
|
checked: gates.includes(key),
|
|
302
468
|
})),
|
|
@@ -307,7 +473,9 @@ async function promptForConfig() {
|
|
|
307
473
|
}
|
|
308
474
|
// Prompt for target URLs based on selected gates
|
|
309
475
|
const targets = {};
|
|
310
|
-
|
|
476
|
+
const hasApiGates = gates.some((g) => ['api-smoke', 'api-crud', 'perf', 'dast'].includes(g));
|
|
477
|
+
const hasUiGates = gates.some((g) => ['ui-smoke', 'a11y'].includes(g));
|
|
478
|
+
if (hasApiGates) {
|
|
311
479
|
const apiAnswers = await inquirer.prompt([
|
|
312
480
|
{
|
|
313
481
|
type: 'input',
|
|
@@ -316,19 +484,10 @@ async function promptForConfig() {
|
|
|
316
484
|
default: 'https://api.example.com',
|
|
317
485
|
validate: (input) => input.startsWith('http') || 'Must be a valid URL',
|
|
318
486
|
},
|
|
319
|
-
{
|
|
320
|
-
type: 'input',
|
|
321
|
-
name: 'smokeTests',
|
|
322
|
-
message: 'Smoke test endpoints (comma-separated):',
|
|
323
|
-
default: 'GET /health -> 200, GET /status -> 200',
|
|
324
|
-
},
|
|
325
487
|
]);
|
|
326
|
-
targets.api =
|
|
327
|
-
baseUrl: apiAnswers.apiUrl,
|
|
328
|
-
smoke: apiAnswers.smokeTests.split(',').map((s) => s.trim()),
|
|
329
|
-
};
|
|
488
|
+
targets.api = apiAnswers.apiUrl;
|
|
330
489
|
}
|
|
331
|
-
if (
|
|
490
|
+
if (hasUiGates) {
|
|
332
491
|
const uiAnswers = await inquirer.prompt([
|
|
333
492
|
{
|
|
334
493
|
type: 'input',
|
|
@@ -337,117 +496,38 @@ async function promptForConfig() {
|
|
|
337
496
|
default: 'https://example.com',
|
|
338
497
|
validate: (input) => input.startsWith('http') || 'Must be a valid URL',
|
|
339
498
|
},
|
|
340
|
-
{
|
|
341
|
-
type: 'input',
|
|
342
|
-
name: 'pages',
|
|
343
|
-
message: 'Pages to test (comma-separated):',
|
|
344
|
-
default: 'https://example.com, https://example.com/about',
|
|
345
|
-
},
|
|
346
499
|
]);
|
|
347
|
-
targets.web =
|
|
348
|
-
baseUrl: uiAnswers.webUrl,
|
|
349
|
-
pages: uiAnswers.pages.split(',').map((s) => s.trim()),
|
|
350
|
-
};
|
|
351
|
-
}
|
|
352
|
-
// Build pack config
|
|
353
|
-
const pack = {
|
|
354
|
-
version: 1,
|
|
355
|
-
name: answers.name,
|
|
356
|
-
gates: gates,
|
|
357
|
-
targets,
|
|
358
|
-
};
|
|
359
|
-
// Add budgets if perf/a11y gates enabled
|
|
360
|
-
if (gates.includes('perf') || gates.includes('a11y')) {
|
|
361
|
-
pack.budgets = {};
|
|
362
|
-
if (gates.includes('perf')) {
|
|
363
|
-
pack.budgets.perf_p95_ms = 2000;
|
|
364
|
-
}
|
|
365
|
-
if (gates.includes('a11y')) {
|
|
366
|
-
pack.budgets.a11y_min = 90;
|
|
367
|
-
}
|
|
500
|
+
targets.web = uiAnswers.webUrl;
|
|
368
501
|
}
|
|
369
|
-
|
|
370
|
-
if (gates.some((g) => ['sast', 'dast', 'secrets', 'deps'].includes(g))) {
|
|
371
|
-
pack.security = {};
|
|
372
|
-
if (gates.includes('sast')) {
|
|
373
|
-
pack.security.sast = {
|
|
374
|
-
max_critical: 0,
|
|
375
|
-
max_high: 3,
|
|
376
|
-
};
|
|
377
|
-
}
|
|
378
|
-
if (gates.includes('dast')) {
|
|
379
|
-
pack.security.dast = {
|
|
380
|
-
max_high: 5,
|
|
381
|
-
};
|
|
382
|
-
}
|
|
383
|
-
}
|
|
384
|
-
return pack;
|
|
502
|
+
return generateV2Pack(answers.name, gates, targets.api, targets.web);
|
|
385
503
|
}
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
504
|
+
// ============================================================
|
|
505
|
+
// Generate pack from template
|
|
506
|
+
// ============================================================
|
|
389
507
|
function generateFromTemplate(templateKey, name) {
|
|
390
508
|
const template = TEMPLATES[templateKey];
|
|
391
509
|
if (!template) {
|
|
392
510
|
throw new Error(`Unknown template: ${templateKey}`);
|
|
393
511
|
}
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
if (template.gates.includes('api_smoke')) {
|
|
402
|
-
pack.targets.api = {
|
|
403
|
-
baseUrl: 'https://api.example.com',
|
|
404
|
-
smoke: [
|
|
405
|
-
'GET /health -> 200',
|
|
406
|
-
'GET /status -> 200',
|
|
407
|
-
],
|
|
408
|
-
};
|
|
512
|
+
// Determine targets based on template
|
|
513
|
+
let apiTarget;
|
|
514
|
+
let webTarget;
|
|
515
|
+
const hasApiGates = template.gates.some((g) => ['api-smoke', 'api-crud', 'perf', 'dast'].includes(g));
|
|
516
|
+
const hasUiGates = template.gates.some((g) => ['ui-smoke', 'a11y'].includes(g));
|
|
517
|
+
if (hasApiGates) {
|
|
518
|
+
apiTarget = 'https://api.example.com';
|
|
409
519
|
}
|
|
410
|
-
if (
|
|
411
|
-
|
|
412
|
-
baseUrl: 'https://example.com',
|
|
413
|
-
pages: [
|
|
414
|
-
'https://example.com',
|
|
415
|
-
],
|
|
416
|
-
};
|
|
520
|
+
if (hasUiGates) {
|
|
521
|
+
webTarget = 'https://example.com';
|
|
417
522
|
}
|
|
418
|
-
|
|
419
|
-
if (template.gates.includes('perf') || template.gates.includes('a11y')) {
|
|
420
|
-
pack.budgets = {};
|
|
421
|
-
if (template.gates.includes('perf')) {
|
|
422
|
-
pack.budgets.perf_p95_ms = 2000;
|
|
423
|
-
}
|
|
424
|
-
if (template.gates.includes('a11y')) {
|
|
425
|
-
pack.budgets.a11y_min = 90;
|
|
426
|
-
}
|
|
427
|
-
}
|
|
428
|
-
// Add security config
|
|
429
|
-
if (template.gates.some((g) => ['sast', 'dast', 'secrets', 'deps'].includes(g))) {
|
|
430
|
-
pack.security = {};
|
|
431
|
-
if (template.gates.includes('sast')) {
|
|
432
|
-
pack.security.sast = {
|
|
433
|
-
max_critical: 0,
|
|
434
|
-
max_high: 3,
|
|
435
|
-
};
|
|
436
|
-
}
|
|
437
|
-
if (template.gates.includes('dast')) {
|
|
438
|
-
pack.security.dast = {
|
|
439
|
-
max_high: 5,
|
|
440
|
-
};
|
|
441
|
-
}
|
|
442
|
-
}
|
|
443
|
-
return pack;
|
|
523
|
+
return generateV2Pack(name || `${templateKey}-tests`, template.gates, apiTarget, webTarget);
|
|
444
524
|
}
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
525
|
+
// ============================================================
|
|
526
|
+
// Main init command
|
|
527
|
+
// ============================================================
|
|
448
528
|
export async function initCommand(options = {}) {
|
|
449
529
|
try {
|
|
450
|
-
console.log(chalk.bold.cyan('\nš QA360 Pack Generator\n'));
|
|
530
|
+
console.log(chalk.bold.cyan('\nš QA360 Pack Generator (v2)\n'));
|
|
451
531
|
// Determine output file
|
|
452
532
|
const outputFile = options.output
|
|
453
533
|
? resolve(options.output)
|
|
@@ -492,6 +572,8 @@ export async function initCommand(options = {}) {
|
|
|
492
572
|
indent: 2,
|
|
493
573
|
lineWidth: 120,
|
|
494
574
|
noRefs: true,
|
|
575
|
+
quotingType: '"',
|
|
576
|
+
forceQuotes: false,
|
|
495
577
|
});
|
|
496
578
|
// Ensure directory exists
|
|
497
579
|
const dir = resolve(outputFile, '..');
|
|
@@ -502,7 +584,7 @@ export async function initCommand(options = {}) {
|
|
|
502
584
|
writeFileSync(outputFile, yaml, 'utf-8');
|
|
503
585
|
console.log(chalk.green.bold('\nā
Pack created successfully!'));
|
|
504
586
|
console.log(chalk.gray(`\nš File: ${outputFile}`));
|
|
505
|
-
console.log(chalk.gray(`š Gates: ${pack.gates
|
|
587
|
+
console.log(chalk.gray(`š Gates: ${Object.keys(pack.gates).join(', ')}`));
|
|
506
588
|
console.log(chalk.cyan('\nš Next steps:'));
|
|
507
589
|
console.log(chalk.gray(' 1. Edit the pack file to customize your tests'));
|
|
508
590
|
console.log(chalk.gray(' 2. Run: qa360 run'));
|