qa360 1.2.2 → 1.3.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/README.md CHANGED
@@ -32,33 +32,110 @@ npx qa360@latest doctor
32
32
 
33
33
  **Note**: On first run, Playwright browsers will be automatically downloaded (~100MB). This happens only once.
34
34
 
35
+ ## Quick Start
36
+
37
+ ### 1. Generate a test pack
38
+
39
+ ```bash
40
+ # Option 1: Interactive mode (recommended for first-time users)
41
+ qa360 init
42
+
43
+ # Option 2: Use a template directly
44
+ qa360 init --template api-basic --yes
45
+
46
+ # Option 3: Copy an example
47
+ qa360 examples copy api-basic
48
+ qa360 examples list # See all available examples
49
+ ```
50
+
51
+ ### 2. Run tests
52
+
53
+ ```bash
54
+ qa360 run
55
+ ```
56
+
57
+ ### 3. Verify proof
58
+
59
+ ```bash
60
+ qa360 verify .qa360/runs/
61
+ ```
62
+
35
63
  ## Usage
36
64
 
37
65
  ```bash
66
+ # Generate test pack (interactive)
67
+ qa360 init
68
+
69
+ # Generate with template
70
+ qa360 init --template api-basic
71
+ qa360 init --template fullstack
72
+ qa360 init --template security
73
+ qa360 init --template complete
74
+
75
+ # Examples commands
76
+ qa360 examples list # List all examples
77
+ qa360 examples copy api-basic # Copy example to qa360.yml
78
+ qa360 examples show fullstack # Preview example content
79
+
38
80
  # System health check
39
81
  qa360 doctor
40
82
 
41
83
  # Run test pack
42
- qa360 run test.yml
84
+ qa360 run # Auto-detects qa360.yml
85
+ qa360 run custom-pack.yml # Use specific file
43
86
 
44
87
  # Verify proof bundle
88
+ qa360 verify .qa360/runs/
45
89
  qa360 verify proof.json
46
90
 
91
+ # View run history
92
+ qa360 history list
93
+ qa360 history show <run-id>
94
+
47
95
  # With JSON output (CI-friendly)
48
96
  qa360 doctor --json
49
97
  qa360 verify proof.json --json
50
98
  ```
51
99
 
100
+ ## Available Templates
101
+
102
+ | Template | Description | Gates |
103
+ |----------|-------------|-------|
104
+ | **api-basic** | Simple API smoke tests | api_smoke |
105
+ | **ui-basic** | Basic UI/E2E tests | ui |
106
+ | **fullstack** | API + UI + Performance | api_smoke, ui, perf |
107
+ | **security** | Security testing suite | sast, dast, secrets, deps |
108
+ | **accessibility** | Accessibility testing | ui, a11y |
109
+ | **complete** | All quality gates | All (8 gates) |
110
+
111
+ ## Example Files
112
+
113
+ Pre-made examples are included in the package:
114
+
115
+ ```bash
116
+ # List examples
117
+ ls examples/
118
+
119
+ # Copy an example
120
+ cp examples/api-basic.yml qa360.yml
121
+
122
+ # Run example directly
123
+ qa360 run examples/api-basic.yml
124
+ ```
125
+
52
126
  ## Commands
53
127
 
54
128
  | Command | Description |
55
129
  |---------|-------------|
56
- | `doctor` | Check system health, auto-fix issues |
57
- | `verify` | Verify cryptographic proof bundles |
130
+ | `init` | Generate test pack interactively |
131
+ | `examples` | Manage example templates (list, copy, show) |
58
132
  | `run` | Execute test pack |
59
- | `serve` | Start observability server |
133
+ | `verify` | Verify cryptographic proof bundles |
134
+ | `doctor` | Check system health, auto-fix issues |
135
+ | `history` | View run history and results |
60
136
  | `report` | Generate reports |
61
137
  | `secrets` | Manage encrypted secrets |
138
+ | `pack` | Pack validation and linting |
62
139
 
63
140
  ## Exit Codes
64
141
 
@@ -0,0 +1,34 @@
1
+ /**
2
+ * QA360 Examples Command - Manage example templates
3
+ *
4
+ * Usage:
5
+ * qa360 examples list
6
+ * qa360 examples copy api-basic
7
+ * qa360 examples show fullstack
8
+ */
9
+ /**
10
+ * CLI options for examples command
11
+ */
12
+ export interface ExamplesOptions {
13
+ output?: string;
14
+ force?: boolean;
15
+ }
16
+ /**
17
+ * List available examples
18
+ */
19
+ export declare function examplesListCommand(): Promise<void>;
20
+ /**
21
+ * Show example content
22
+ */
23
+ export declare function examplesShowCommand(templateName: string): Promise<void>;
24
+ /**
25
+ * Copy example to current directory
26
+ */
27
+ export declare function examplesCopyCommand(templateName: string, options?: ExamplesOptions): Promise<void>;
28
+ declare const _default: {
29
+ list: typeof examplesListCommand;
30
+ show: typeof examplesShowCommand;
31
+ copy: typeof examplesCopyCommand;
32
+ };
33
+ export default _default;
34
+ //# sourceMappingURL=examples.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"examples.d.ts","sourceRoot":"","sources":["../../src/commands/examples.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAWH;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AA8DD;;GAEG;AACH,wBAAsB,mBAAmB,IAAI,OAAO,CAAC,IAAI,CAAC,CAkCzD;AAED;;GAEG;AACH,wBAAsB,mBAAmB,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAwC7E;AAED;;GAEG;AACH,wBAAsB,mBAAmB,CACvC,YAAY,EAAE,MAAM,EACpB,OAAO,GAAE,eAAoB,GAC5B,OAAO,CAAC,IAAI,CAAC,CA6Df;;;;;;AAED,wBAIE"}
@@ -0,0 +1,190 @@
1
+ /**
2
+ * QA360 Examples Command - Manage example templates
3
+ *
4
+ * Usage:
5
+ * qa360 examples list
6
+ * qa360 examples copy api-basic
7
+ * qa360 examples show fullstack
8
+ */
9
+ import { existsSync, readFileSync, writeFileSync, readdirSync } from 'fs';
10
+ import { join, dirname } from 'path';
11
+ import { fileURLToPath } from 'url';
12
+ import chalk from 'chalk';
13
+ import inquirer from 'inquirer';
14
+ const __filename = fileURLToPath(import.meta.url);
15
+ const __dirname = dirname(__filename);
16
+ /**
17
+ * Get path to examples directory
18
+ */
19
+ function getExamplesDir() {
20
+ // In development: /Users/.../QA360/cli/src/commands -> ../../examples
21
+ // In production: /usr/local/lib/node_modules/qa360/dist/commands -> ../../examples
22
+ const examplesDir = join(__dirname, '../../examples');
23
+ if (!existsSync(examplesDir)) {
24
+ throw new Error(`Examples directory not found at: ${examplesDir}`);
25
+ }
26
+ return examplesDir;
27
+ }
28
+ /**
29
+ * Parse example file for metadata
30
+ */
31
+ function parseExampleMetadata(filePath) {
32
+ try {
33
+ const content = readFileSync(filePath, 'utf-8');
34
+ const lines = content.split('\n');
35
+ // Extract description from first comment line
36
+ const descLine = lines.find(l => l.startsWith('# QA360 Example:'));
37
+ const description = descLine
38
+ ? descLine.replace('# QA360 Example:', '').trim()
39
+ : 'No description';
40
+ // Extract gates from content
41
+ const gatesMatch = content.match(/gates:\s*\n((?:\s*-\s*.+\n)*)/);
42
+ const gates = [];
43
+ if (gatesMatch) {
44
+ const gatesBlock = gatesMatch[1];
45
+ const gateLines = gatesBlock.match(/- (.+)/g);
46
+ if (gateLines) {
47
+ gates.push(...gateLines.map(line => line.replace('- ', '').trim()));
48
+ }
49
+ }
50
+ return {
51
+ name: filePath.split('/').pop()?.replace('.yml', '') || '',
52
+ description,
53
+ gates,
54
+ };
55
+ }
56
+ catch {
57
+ return null;
58
+ }
59
+ }
60
+ /**
61
+ * List available examples
62
+ */
63
+ export async function examplesListCommand() {
64
+ try {
65
+ console.log(chalk.bold.cyan('\n📚 Available QA360 Examples\n'));
66
+ const examplesDir = getExamplesDir();
67
+ const files = readdirSync(examplesDir)
68
+ .filter(f => f.endsWith('.yml') && f !== 'README.md')
69
+ .sort();
70
+ if (files.length === 0) {
71
+ console.log(chalk.yellow('No examples found.'));
72
+ return;
73
+ }
74
+ files.forEach(file => {
75
+ const filePath = join(examplesDir, file);
76
+ const metadata = parseExampleMetadata(filePath);
77
+ if (metadata) {
78
+ const templateName = file.replace('.yml', '');
79
+ console.log(chalk.bold.green(`📄 ${templateName}`));
80
+ console.log(chalk.gray(` ${metadata.description}`));
81
+ console.log(chalk.blue(` Gates: ${metadata.gates.join(', ')}`));
82
+ console.log(chalk.gray(` Copy: qa360 examples copy ${templateName}\n`));
83
+ }
84
+ });
85
+ console.log(chalk.cyan('💡 Tip: Use "qa360 examples copy <name>" to copy an example\n'));
86
+ }
87
+ catch (error) {
88
+ console.error(chalk.red('\n❌ Failed to list examples:'));
89
+ console.error(chalk.red(` ${error instanceof Error ? error.message : 'Unknown error'}`));
90
+ process.exit(1);
91
+ }
92
+ }
93
+ /**
94
+ * Show example content
95
+ */
96
+ export async function examplesShowCommand(templateName) {
97
+ try {
98
+ if (!templateName) {
99
+ console.error(chalk.red('\n❌ Example name required'));
100
+ console.log(chalk.gray(' Usage: qa360 examples show <name>'));
101
+ console.log(chalk.gray(' Run "qa360 examples list" to see available examples\n'));
102
+ process.exit(1);
103
+ }
104
+ const examplesDir = getExamplesDir();
105
+ const fileName = templateName.endsWith('.yml') ? templateName : `${templateName}.yml`;
106
+ const filePath = join(examplesDir, fileName);
107
+ if (!existsSync(filePath)) {
108
+ console.error(chalk.red(`\n❌ Example not found: ${templateName}`));
109
+ console.log(chalk.gray(' Run "qa360 examples list" to see available examples\n'));
110
+ process.exit(1);
111
+ }
112
+ const content = readFileSync(filePath, 'utf-8');
113
+ const metadata = parseExampleMetadata(filePath);
114
+ console.log(chalk.bold.cyan(`\n📄 Example: ${templateName}\n`));
115
+ if (metadata) {
116
+ console.log(chalk.gray(`Description: ${metadata.description}`));
117
+ console.log(chalk.gray(`Gates: ${metadata.gates.join(', ')}\n`));
118
+ }
119
+ console.log(chalk.dim('─'.repeat(60)));
120
+ console.log(content);
121
+ console.log(chalk.dim('─'.repeat(60)));
122
+ console.log(chalk.cyan(`\n💡 Copy this example: qa360 examples copy ${templateName}\n`));
123
+ }
124
+ catch (error) {
125
+ console.error(chalk.red('\n❌ Failed to show example:'));
126
+ console.error(chalk.red(` ${error instanceof Error ? error.message : 'Unknown error'}`));
127
+ process.exit(1);
128
+ }
129
+ }
130
+ /**
131
+ * Copy example to current directory
132
+ */
133
+ export async function examplesCopyCommand(templateName, options = {}) {
134
+ try {
135
+ if (!templateName) {
136
+ console.error(chalk.red('\n❌ Example name required'));
137
+ console.log(chalk.gray(' Usage: qa360 examples copy <name>'));
138
+ console.log(chalk.gray(' Run "qa360 examples list" to see available examples\n'));
139
+ process.exit(1);
140
+ }
141
+ const examplesDir = getExamplesDir();
142
+ const fileName = templateName.endsWith('.yml') ? templateName : `${templateName}.yml`;
143
+ const sourcePath = join(examplesDir, fileName);
144
+ if (!existsSync(sourcePath)) {
145
+ console.error(chalk.red(`\n❌ Example not found: ${templateName}`));
146
+ console.log(chalk.gray(' Run "qa360 examples list" to see available examples\n'));
147
+ process.exit(1);
148
+ }
149
+ // Determine output path
150
+ const outputFile = options.output || join(process.cwd(), 'qa360.yml');
151
+ // Check if file exists
152
+ if (existsSync(outputFile) && !options.force) {
153
+ const overwrite = await inquirer.prompt([
154
+ {
155
+ type: 'confirm',
156
+ name: 'overwrite',
157
+ message: `File ${outputFile} already exists. Overwrite?`,
158
+ default: false,
159
+ },
160
+ ]);
161
+ if (!overwrite.overwrite) {
162
+ console.log(chalk.yellow('\n⚠️ Aborted. Use --force to overwrite.\n'));
163
+ return;
164
+ }
165
+ }
166
+ // Copy file
167
+ const content = readFileSync(sourcePath, 'utf-8');
168
+ writeFileSync(outputFile, content, 'utf-8');
169
+ console.log(chalk.green.bold('\n✅ Example copied successfully!'));
170
+ console.log(chalk.gray(`\n📄 File: ${outputFile}`));
171
+ const metadata = parseExampleMetadata(sourcePath);
172
+ if (metadata) {
173
+ console.log(chalk.gray(`📋 Gates: ${metadata.gates.join(', ')}`));
174
+ }
175
+ console.log(chalk.cyan('\n📚 Next steps:'));
176
+ console.log(chalk.gray(' 1. Edit qa360.yml to customize for your project'));
177
+ console.log(chalk.gray(' 2. Run: qa360 run'));
178
+ console.log(chalk.gray(' 3. Verify: qa360 verify .qa360/runs/\n'));
179
+ }
180
+ catch (error) {
181
+ console.error(chalk.red('\n❌ Failed to copy example:'));
182
+ console.error(chalk.red(` ${error instanceof Error ? error.message : 'Unknown error'}`));
183
+ process.exit(1);
184
+ }
185
+ }
186
+ export default {
187
+ list: examplesListCommand,
188
+ show: examplesShowCommand,
189
+ copy: examplesCopyCommand,
190
+ };
@@ -0,0 +1,66 @@
1
+ /**
2
+ * QA360 Init Command - Interactive pack generator
3
+ *
4
+ * Usage:
5
+ * qa360 init
6
+ * qa360 init --template api-basic
7
+ * qa360 init --output custom-pack.yml
8
+ */
9
+ /**
10
+ * CLI options for init command
11
+ */
12
+ export interface InitOptions {
13
+ template?: string;
14
+ output?: string;
15
+ force?: boolean;
16
+ yes?: boolean;
17
+ }
18
+ /**
19
+ * Available templates
20
+ */
21
+ export declare const TEMPLATES: {
22
+ 'api-basic': {
23
+ name: string;
24
+ description: string;
25
+ gates: string[];
26
+ };
27
+ 'ui-basic': {
28
+ name: string;
29
+ description: string;
30
+ gates: string[];
31
+ };
32
+ fullstack: {
33
+ name: string;
34
+ description: string;
35
+ gates: string[];
36
+ };
37
+ security: {
38
+ name: string;
39
+ description: string;
40
+ gates: string[];
41
+ };
42
+ complete: {
43
+ name: string;
44
+ description: string;
45
+ gates: string[];
46
+ };
47
+ };
48
+ /**
49
+ * Gate descriptions
50
+ */
51
+ export declare const GATE_DESCRIPTIONS: {
52
+ api_smoke: string;
53
+ ui: string;
54
+ a11y: string;
55
+ perf: string;
56
+ sast: string;
57
+ dast: string;
58
+ secrets: string;
59
+ deps: string;
60
+ };
61
+ /**
62
+ * Main init command
63
+ */
64
+ export declare function initCommand(options?: InitOptions): Promise<void>;
65
+ export default initCommand;
66
+ //# sourceMappingURL=init.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../src/commands/init.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AASH;;GAEG;AACH,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,GAAG,CAAC,EAAE,OAAO,CAAC;CACf;AAED;;GAEG;AACH,eAAO,MAAM,SAAS;;;;;;;;;;;;;;;;;;;;;;;;;;CA0BrB,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,iBAAiB;;;;;;;;;CAS7B,CAAC;AAkNF;;GAEG;AACH,wBAAsB,WAAW,CAAC,OAAO,GAAE,WAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CAwE1E;AAED,eAAe,WAAW,CAAC"}
@@ -0,0 +1,307 @@
1
+ /**
2
+ * QA360 Init Command - Interactive pack generator
3
+ *
4
+ * Usage:
5
+ * qa360 init
6
+ * qa360 init --template api-basic
7
+ * qa360 init --output custom-pack.yml
8
+ */
9
+ import { existsSync, writeFileSync, mkdirSync } from 'fs';
10
+ import { join, resolve } from 'path';
11
+ import chalk from 'chalk';
12
+ import inquirer from 'inquirer';
13
+ import { dump } from 'js-yaml';
14
+ /**
15
+ * Available templates
16
+ */
17
+ export const TEMPLATES = {
18
+ 'api-basic': {
19
+ name: 'API Basic',
20
+ description: 'Simple API smoke tests (REST/GraphQL)',
21
+ gates: ['api_smoke'],
22
+ },
23
+ 'ui-basic': {
24
+ name: 'UI Basic',
25
+ description: 'Basic UI/E2E browser tests',
26
+ gates: ['ui'],
27
+ },
28
+ 'fullstack': {
29
+ name: 'Full Stack',
30
+ description: 'API + UI + Performance tests',
31
+ gates: ['api_smoke', 'ui', 'perf'],
32
+ },
33
+ 'security': {
34
+ name: 'Security',
35
+ description: 'SAST, DAST, secrets scanning, dependency checks',
36
+ gates: ['sast', 'dast', 'secrets', 'deps'],
37
+ },
38
+ 'complete': {
39
+ name: 'Complete',
40
+ description: 'All quality gates (API, UI, Performance, Security, Accessibility)',
41
+ gates: ['api_smoke', 'ui', 'a11y', 'perf', 'sast', 'dast', 'secrets', 'deps'],
42
+ },
43
+ };
44
+ /**
45
+ * Gate descriptions
46
+ */
47
+ export const GATE_DESCRIPTIONS = {
48
+ api_smoke: 'API smoke tests (REST/GraphQL health checks)',
49
+ ui: 'UI/E2E tests (browser automation)',
50
+ a11y: 'Accessibility tests (WCAG compliance)',
51
+ perf: 'Performance tests (load/stress testing)',
52
+ sast: 'Static security analysis (code scanning)',
53
+ dast: 'Dynamic security testing (runtime scanning)',
54
+ secrets: 'Secrets detection (credential scanning)',
55
+ deps: 'Dependency vulnerability checks',
56
+ };
57
+ /**
58
+ * Interactive prompts
59
+ */
60
+ async function promptForConfig() {
61
+ const answers = await inquirer.prompt([
62
+ {
63
+ type: 'input',
64
+ name: 'name',
65
+ message: 'Pack name:',
66
+ default: 'my-qa360-tests',
67
+ validate: (input) => input.length > 0 || 'Name is required',
68
+ },
69
+ {
70
+ type: 'list',
71
+ name: 'template',
72
+ message: 'Choose a template:',
73
+ choices: Object.entries(TEMPLATES).map(([key, value]) => ({
74
+ name: `${value.name} - ${value.description}`,
75
+ value: key,
76
+ })),
77
+ },
78
+ {
79
+ type: 'confirm',
80
+ name: 'customizeGates',
81
+ message: 'Customize quality gates?',
82
+ default: false,
83
+ },
84
+ ]);
85
+ let gates = TEMPLATES[answers.template].gates;
86
+ if (answers.customizeGates) {
87
+ const gateAnswers = await inquirer.prompt([
88
+ {
89
+ type: 'checkbox',
90
+ name: 'gates',
91
+ message: 'Select quality gates to enable:',
92
+ choices: Object.entries(GATE_DESCRIPTIONS).map(([key, desc]) => ({
93
+ name: `${key} - ${desc}`,
94
+ value: key,
95
+ checked: gates.includes(key),
96
+ })),
97
+ validate: (input) => input.length > 0 || 'Select at least one gate',
98
+ },
99
+ ]);
100
+ gates = gateAnswers.gates;
101
+ }
102
+ // Prompt for target URLs based on selected gates
103
+ const targets = {};
104
+ if (gates.includes('api_smoke')) {
105
+ const apiAnswers = await inquirer.prompt([
106
+ {
107
+ type: 'input',
108
+ name: 'apiUrl',
109
+ message: 'API base URL:',
110
+ default: 'https://api.example.com',
111
+ validate: (input) => input.startsWith('http') || 'Must be a valid URL',
112
+ },
113
+ {
114
+ type: 'input',
115
+ name: 'smokeTests',
116
+ message: 'Smoke test endpoints (comma-separated):',
117
+ default: 'GET /health -> 200, GET /status -> 200',
118
+ },
119
+ ]);
120
+ targets.api = {
121
+ baseUrl: apiAnswers.apiUrl,
122
+ smoke: apiAnswers.smokeTests.split(',').map((s) => s.trim()),
123
+ };
124
+ }
125
+ if (gates.includes('ui') || gates.includes('a11y')) {
126
+ const uiAnswers = await inquirer.prompt([
127
+ {
128
+ type: 'input',
129
+ name: 'webUrl',
130
+ message: 'Web application URL:',
131
+ default: 'https://example.com',
132
+ validate: (input) => input.startsWith('http') || 'Must be a valid URL',
133
+ },
134
+ {
135
+ type: 'input',
136
+ name: 'pages',
137
+ message: 'Pages to test (comma-separated):',
138
+ default: 'https://example.com, https://example.com/about',
139
+ },
140
+ ]);
141
+ targets.web = {
142
+ baseUrl: uiAnswers.webUrl,
143
+ pages: uiAnswers.pages.split(',').map((s) => s.trim()),
144
+ };
145
+ }
146
+ // Build pack config
147
+ const pack = {
148
+ version: 1,
149
+ name: answers.name,
150
+ gates: gates,
151
+ targets,
152
+ };
153
+ // Add budgets if perf/a11y gates enabled
154
+ if (gates.includes('perf') || gates.includes('a11y')) {
155
+ pack.budgets = {};
156
+ if (gates.includes('perf')) {
157
+ pack.budgets.perf_p95_ms = 2000;
158
+ }
159
+ if (gates.includes('a11y')) {
160
+ pack.budgets.a11y_min = 90;
161
+ }
162
+ }
163
+ // Add security config if security gates enabled
164
+ if (gates.some((g) => ['sast', 'dast', 'secrets', 'deps'].includes(g))) {
165
+ pack.security = {};
166
+ if (gates.includes('sast')) {
167
+ pack.security.sast = {
168
+ max_critical: 0,
169
+ max_high: 3,
170
+ };
171
+ }
172
+ if (gates.includes('dast')) {
173
+ pack.security.dast = {
174
+ max_high: 5,
175
+ };
176
+ }
177
+ }
178
+ return pack;
179
+ }
180
+ /**
181
+ * Generate pack from template
182
+ */
183
+ function generateFromTemplate(templateKey, name) {
184
+ const template = TEMPLATES[templateKey];
185
+ if (!template) {
186
+ throw new Error(`Unknown template: ${templateKey}`);
187
+ }
188
+ const pack = {
189
+ version: 1,
190
+ name: name || `${templateKey}-tests`,
191
+ gates: template.gates,
192
+ targets: {},
193
+ };
194
+ // Add default targets based on gates
195
+ if (template.gates.includes('api_smoke')) {
196
+ pack.targets.api = {
197
+ baseUrl: 'https://api.example.com',
198
+ smoke: [
199
+ 'GET /health -> 200',
200
+ 'GET /status -> 200',
201
+ ],
202
+ };
203
+ }
204
+ if (template.gates.includes('ui') || template.gates.includes('a11y')) {
205
+ pack.targets.web = {
206
+ baseUrl: 'https://example.com',
207
+ pages: [
208
+ 'https://example.com',
209
+ ],
210
+ };
211
+ }
212
+ // Add budgets
213
+ if (template.gates.includes('perf') || template.gates.includes('a11y')) {
214
+ pack.budgets = {};
215
+ if (template.gates.includes('perf')) {
216
+ pack.budgets.perf_p95_ms = 2000;
217
+ }
218
+ if (template.gates.includes('a11y')) {
219
+ pack.budgets.a11y_min = 90;
220
+ }
221
+ }
222
+ // Add security config
223
+ if (template.gates.some((g) => ['sast', 'dast', 'secrets', 'deps'].includes(g))) {
224
+ pack.security = {};
225
+ if (template.gates.includes('sast')) {
226
+ pack.security.sast = {
227
+ max_critical: 0,
228
+ max_high: 3,
229
+ };
230
+ }
231
+ if (template.gates.includes('dast')) {
232
+ pack.security.dast = {
233
+ max_high: 5,
234
+ };
235
+ }
236
+ }
237
+ return pack;
238
+ }
239
+ /**
240
+ * Main init command
241
+ */
242
+ export async function initCommand(options = {}) {
243
+ try {
244
+ console.log(chalk.bold.cyan('\n🚀 QA360 Pack Generator\n'));
245
+ // Determine output file
246
+ const outputFile = options.output
247
+ ? resolve(options.output)
248
+ : join(process.cwd(), 'qa360.yml');
249
+ // Check if file exists
250
+ if (existsSync(outputFile) && !options.force) {
251
+ const overwrite = await inquirer.prompt([
252
+ {
253
+ type: 'confirm',
254
+ name: 'overwrite',
255
+ message: `File ${outputFile} already exists. Overwrite?`,
256
+ default: false,
257
+ },
258
+ ]);
259
+ if (!overwrite.overwrite) {
260
+ console.log(chalk.yellow('\n⚠️ Aborted. Use --force to overwrite.'));
261
+ return;
262
+ }
263
+ }
264
+ // Generate pack config
265
+ let pack;
266
+ if (options.template) {
267
+ // Use template directly
268
+ pack = generateFromTemplate(options.template);
269
+ console.log(chalk.green(`✅ Using template: ${TEMPLATES[options.template]?.name || options.template}`));
270
+ }
271
+ else if (options.yes) {
272
+ // Use default template (api-basic)
273
+ pack = generateFromTemplate('api-basic');
274
+ console.log(chalk.green('✅ Using default template: API Basic'));
275
+ }
276
+ else {
277
+ // Interactive mode
278
+ pack = await promptForConfig();
279
+ }
280
+ // Convert to YAML
281
+ const yaml = dump(pack, {
282
+ indent: 2,
283
+ lineWidth: 120,
284
+ noRefs: true,
285
+ });
286
+ // Ensure directory exists
287
+ const dir = resolve(outputFile, '..');
288
+ if (!existsSync(dir)) {
289
+ mkdirSync(dir, { recursive: true });
290
+ }
291
+ // Write file
292
+ writeFileSync(outputFile, yaml, 'utf-8');
293
+ console.log(chalk.green.bold('\n✅ Pack created successfully!'));
294
+ console.log(chalk.gray(`\n📄 File: ${outputFile}`));
295
+ console.log(chalk.gray(`📋 Gates: ${pack.gates?.join(', ')}`));
296
+ console.log(chalk.cyan('\n📚 Next steps:'));
297
+ console.log(chalk.gray(' 1. Edit the pack file to customize your tests'));
298
+ console.log(chalk.gray(' 2. Run: qa360 run'));
299
+ console.log(chalk.gray(' 3. View results: qa360 verify .qa360/runs/\n'));
300
+ }
301
+ catch (error) {
302
+ console.error(chalk.red('\n❌ Init failed:'));
303
+ console.error(chalk.red(` ${error instanceof Error ? error.message : 'Unknown error'}`));
304
+ process.exit(1);
305
+ }
306
+ }
307
+ export default initCommand;
@@ -1 +1 @@
1
- {"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../../src/commands/run.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAMH,OAAO,EAAgB,KAAK,eAAe,EAAkC,KAAK,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAEzH;;GAEG;AACH,MAAM,WAAW,UAAU;IACzB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED;;GAEG;AACH,wBAAsB,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,CAwBtE;AAED;;GAEG;AACH,wBAAgB,YAAY,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,CA0BrD;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,eAAe,GAAG,IAAI,CA8B5D;AAED;;GAEG;AACH,wBAAsB,UAAU,CAC9B,OAAO,CAAC,EAAE,MAAM,EAChB,OAAO,GAAE,UAAe,GACvB,OAAO,CAAC,IAAI,CAAC,CAiDf;AAGD,eAAe,UAAU,CAAC"}
1
+ {"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../../src/commands/run.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAMH,OAAO,EAAgB,KAAK,eAAe,EAAkC,KAAK,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAEzH;;GAEG;AACH,MAAM,WAAW,UAAU;IACzB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED;;GAEG;AACH,wBAAsB,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,CAsCtE;AAED;;GAEG;AACH,wBAAgB,YAAY,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,CA0BrD;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,eAAe,GAAG,IAAI,CA+B5D;AAED;;GAEG;AACH,wBAAsB,UAAU,CAC9B,OAAO,CAAC,EAAE,MAAM,EAChB,OAAO,GAAE,UAAe,GACvB,OAAO,CAAC,IAAI,CAAC,CAiDf;AAGD,eAAe,UAAU,CAAC"}
@@ -24,7 +24,23 @@ export async function loadPack(packPath) {
24
24
  const validator = new PackValidator();
25
25
  const validation = await validator.validate(pack);
26
26
  if (!validation.valid) {
27
- const errors = validation.errors?.map(e => ` • ${e}`).join('\n');
27
+ const errors = validation.errors?.map(e => {
28
+ // Handle both string and object errors
29
+ if (typeof e === 'string') {
30
+ return ` • ${e}`;
31
+ }
32
+ else if (e && typeof e === 'object') {
33
+ // Extract meaningful info from error objects
34
+ const errObj = e;
35
+ if (errObj.message)
36
+ return ` • ${errObj.message}`;
37
+ if (errObj.instancePath && errObj.message) {
38
+ return ` • ${errObj.instancePath}: ${errObj.message}`;
39
+ }
40
+ return ` • ${JSON.stringify(e)}`;
41
+ }
42
+ return ` • ${String(e)}`;
43
+ }).join('\n') || ' • Unknown validation error';
28
44
  throw new Error(`Invalid pack configuration:\n${errors}`);
29
45
  }
30
46
  // Show warnings if any
@@ -83,7 +99,8 @@ export function displayResults(result) {
83
99
  if (failed.length > 0) {
84
100
  console.log(chalk.red('\nFailed gates:'));
85
101
  failed.forEach((g) => {
86
- console.log(chalk.red(` • ${g.gate}: ${g.error || 'Unknown error'}`));
102
+ const errorMessage = g.error || 'Test failed - check logs above for details';
103
+ console.log(chalk.red(` • ${g.gate}: ${errorMessage}`));
87
104
  });
88
105
  }
89
106
  }
@@ -29,6 +29,7 @@ export interface ApiSmokeResult {
29
29
  avgResponseTime: number;
30
30
  };
31
31
  junit?: string;
32
+ error?: string;
32
33
  }
33
34
  export declare class PlaywrightApiAdapter {
34
35
  private browser?;
@@ -1 +1 @@
1
- {"version":3,"file":"playwright-api.d.ts","sourceRoot":"","sources":["../../../src/core/adapters/playwright-api.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAG7D,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,SAAS,CAAC;IAClB,OAAO,CAAC,EAAE,WAAW,CAAC;IACtB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,aAAa;IAC5B,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,YAAY,EAAE,MAAM,CAAC;IACrB,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,IAAI,CAAC,EAAE,GAAG,CAAC;CACZ;AAED,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,aAAa,EAAE,CAAC;IACzB,OAAO,EAAE;QACP,KAAK,EAAE,MAAM,CAAC;QACd,MAAM,EAAE,MAAM,CAAC;QACf,MAAM,EAAE,MAAM,CAAC;QACf,eAAe,EAAE,MAAM,CAAC;KACzB,CAAC;IACF,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,qBAAa,oBAAoB;IAC/B,OAAO,CAAC,OAAO,CAAC,CAAU;IAC1B,OAAO,CAAC,OAAO,CAAC,CAAiB;IACjC,OAAO,CAAC,QAAQ,CAAmB;;IAMnC;;OAEG;IACG,aAAa,CAAC,MAAM,EAAE,aAAa,GAAG,OAAO,CAAC,cAAc,CAAC;IAmCnE;;OAEG;YACW,cAAc;IA0E5B;;OAEG;IACH,OAAO,CAAC,aAAa;IA4BrB;;OAEG;IACH,OAAO,CAAC,gBAAgB;IAexB;;OAEG;IACH,OAAO,CAAC,gBAAgB;IAexB;;OAEG;IACH,OAAO,CAAC,aAAa;IA6BrB;;OAEG;IACH,OAAO,CAAC,SAAS;IASjB;;OAEG;YACW,YAAY;IAc1B;;OAEG;YACW,OAAO;IASrB;;OAEG;IACH,MAAM,CAAC,cAAc,CAAC,MAAM,EAAE,SAAS,GAAG;QAAE,KAAK,EAAE,OAAO,CAAC;QAAC,MAAM,EAAE,MAAM,EAAE,CAAA;KAAE;CA0B/E"}
1
+ {"version":3,"file":"playwright-api.d.ts","sourceRoot":"","sources":["../../../src/core/adapters/playwright-api.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAG7D,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,SAAS,CAAC;IAClB,OAAO,CAAC,EAAE,WAAW,CAAC;IACtB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,aAAa;IAC5B,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,YAAY,EAAE,MAAM,CAAC;IACrB,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,IAAI,CAAC,EAAE,GAAG,CAAC;CACZ;AAED,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,aAAa,EAAE,CAAC;IACzB,OAAO,EAAE;QACP,KAAK,EAAE,MAAM,CAAC;QACd,MAAM,EAAE,MAAM,CAAC;QACf,MAAM,EAAE,MAAM,CAAC;QACf,eAAe,EAAE,MAAM,CAAC;KACzB,CAAC;IACF,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,qBAAa,oBAAoB;IAC/B,OAAO,CAAC,OAAO,CAAC,CAAU;IAC1B,OAAO,CAAC,OAAO,CAAC,CAAiB;IACjC,OAAO,CAAC,QAAQ,CAAmB;;IAMnC;;OAEG;IACG,aAAa,CAAC,MAAM,EAAE,aAAa,GAAG,OAAO,CAAC,cAAc,CAAC;IAgDnE;;OAEG;YACW,cAAc;IA0E5B;;OAEG;IACH,OAAO,CAAC,aAAa;IA4BrB;;OAEG;IACH,OAAO,CAAC,gBAAgB;IAexB;;OAEG;IACH,OAAO,CAAC,gBAAgB;IAexB;;OAEG;IACH,OAAO,CAAC,aAAa;IA6BrB;;OAEG;IACH,OAAO,CAAC,SAAS;IASjB;;OAEG;YACW,YAAY;IAc1B;;OAEG;YACW,OAAO;IASrB;;OAEG;IACH,MAAM,CAAC,cAAc,CAAC,MAAM,EAAE,SAAS,GAAG;QAAE,KAAK,EAAE,OAAO,CAAC;QAAC,MAAM,EAAE,MAAM,EAAE,CAAA;KAAE;CA0B/E"}
@@ -27,16 +27,28 @@ export class PlaywrightApiAdapter {
27
27
  console.log(` ✅ ${testResult.method} ${testResult.endpoint} -> ${testResult.status} (${testResult.responseTime}ms)`);
28
28
  }
29
29
  else {
30
- console.log(` ❌ ${testResult.method} ${testResult.endpoint} -> ${testResult.error}`);
30
+ // Show clear error message with actual vs expected
31
+ const errorMsg = testResult.error || 'Request failed';
32
+ console.log(` ❌ ${testResult.method} ${testResult.endpoint} -> ${errorMsg}`);
31
33
  }
32
34
  }
33
35
  const summary = this.calculateSummary(results);
34
36
  const junit = this.generateJUnit(results);
37
+ // Generate error message from failed tests
38
+ let error;
39
+ if (summary.failed > 0) {
40
+ const failedTests = results.filter(r => !r.success);
41
+ const errorMessages = failedTests.map(t => t.error).filter(Boolean);
42
+ error = errorMessages.length > 0
43
+ ? `${summary.failed} endpoint(s) failed: ${errorMessages[0]}${errorMessages.length > 1 ? ` (and ${errorMessages.length - 1} more)` : ''}`
44
+ : `${summary.failed} endpoint(s) failed`;
45
+ }
35
46
  return {
36
47
  success: summary.failed === 0,
37
48
  results,
38
49
  summary,
39
- junit
50
+ junit,
51
+ error
40
52
  };
41
53
  }
42
54
  finally {
package/dist/index.js CHANGED
@@ -16,6 +16,8 @@ const packageJson = JSON.parse(readFileSync(join(__dirname, '../package.json'),
16
16
  const version = packageJson.version;
17
17
  import { doctorCommand } from './commands/doctor.js';
18
18
  import { askCommand } from './commands/ask.js';
19
+ import { initCommand } from './commands/init.js';
20
+ import { examplesListCommand, examplesCopyCommand, examplesShowCommand } from './commands/examples.js';
19
21
  import { runCommand } from './commands/run.js';
20
22
  import { reportCommand } from './commands/report.js';
21
23
  import { verifyCommand } from './commands/verify.js';
@@ -37,6 +39,40 @@ program
37
39
  .action(async (options) => {
38
40
  await doctorCommand(options);
39
41
  });
42
+ program
43
+ .command('init')
44
+ .description('Generate pack.yml interactively')
45
+ .option('--template <name>', 'Use template: api-basic, ui-basic, fullstack, security, complete')
46
+ .option('--output <file>', 'Output file path', 'qa360.yml')
47
+ .option('--force', 'Overwrite existing file')
48
+ .option('-y, --yes', 'Skip prompts and use defaults')
49
+ .action(async (options) => {
50
+ await initCommand(options);
51
+ });
52
+ // Examples commands
53
+ const examplesCommand = program
54
+ .command('examples')
55
+ .description('Manage example templates');
56
+ examplesCommand
57
+ .command('list')
58
+ .description('List all available example templates')
59
+ .action(async () => {
60
+ await examplesListCommand();
61
+ });
62
+ examplesCommand
63
+ .command('copy <template>')
64
+ .description('Copy example template to current directory')
65
+ .option('--output <file>', 'Output file path', 'qa360.yml')
66
+ .option('--force', 'Overwrite existing file')
67
+ .action(async (template, options) => {
68
+ await examplesCopyCommand(template, options);
69
+ });
70
+ examplesCommand
71
+ .command('show <template>')
72
+ .description('Show example template content')
73
+ .action(async (template) => {
74
+ await examplesShowCommand(template);
75
+ });
40
76
  program
41
77
  .command('ask [query]')
42
78
  .description('Natural language test requests - generates pack.yml')
@@ -0,0 +1,122 @@
1
+ # QA360 Examples
2
+
3
+ This directory contains example pack files for different testing scenarios.
4
+
5
+ ## Quick Start
6
+
7
+ Copy an example to your project:
8
+
9
+ ```bash
10
+ # Copy API basic example
11
+ cp examples/api-basic.yml qa360.yml
12
+
13
+ # Edit and customize
14
+ vim qa360.yml
15
+
16
+ # Run tests
17
+ qa360 run
18
+ ```
19
+
20
+ ## Available Examples
21
+
22
+ ### 1. **api-basic.yml** - API Smoke Tests
23
+ Simple REST/GraphQL health checks.
24
+
25
+ **Gates**: `api_smoke`
26
+
27
+ **Use case**: Verify API endpoints are responding correctly
28
+
29
+ ```bash
30
+ qa360 run examples/api-basic.yml
31
+ ```
32
+
33
+ ---
34
+
35
+ ### 2. **ui-basic.yml** - UI/E2E Tests
36
+ Basic browser automation tests.
37
+
38
+ **Gates**: `ui`
39
+
40
+ **Use case**: Test web pages load correctly
41
+
42
+ ```bash
43
+ qa360 run examples/ui-basic.yml
44
+ ```
45
+
46
+ ---
47
+
48
+ ### 3. **fullstack.yml** - Full Stack Tests
49
+ API + UI + Performance testing.
50
+
51
+ **Gates**: `api_smoke`, `ui`, `perf`
52
+
53
+ **Use case**: Complete application testing with Docker Compose integration
54
+
55
+ ```bash
56
+ qa360 run examples/fullstack.yml
57
+ ```
58
+
59
+ ---
60
+
61
+ ### 4. **security.yml** - Security Suite
62
+ Comprehensive security scanning.
63
+
64
+ **Gates**: `sast`, `dast`, `secrets`, `deps`
65
+
66
+ **Use case**: Security audit with vulnerability scanning
67
+
68
+ ```bash
69
+ qa360 run examples/security.yml
70
+ ```
71
+
72
+ ---
73
+
74
+ ### 5. **accessibility.yml** - Accessibility Tests
75
+ WCAG compliance testing.
76
+
77
+ **Gates**: `ui`, `a11y`
78
+
79
+ **Use case**: Ensure website meets accessibility standards
80
+
81
+ ```bash
82
+ qa360 run examples/accessibility.yml
83
+ ```
84
+
85
+ ---
86
+
87
+ ### 6. **complete.yml** - Complete Test Suite
88
+ All quality gates enabled.
89
+
90
+ **Gates**: All (8 gates)
91
+
92
+ **Use case**: Comprehensive quality assurance for production-ready applications
93
+
94
+ ```bash
95
+ qa360 run examples/complete.yml
96
+ ```
97
+
98
+ ---
99
+
100
+ ## Interactive Generator
101
+
102
+ Generate a custom pack with:
103
+
104
+ ```bash
105
+ qa360 init
106
+ ```
107
+
108
+ This will walk you through creating a pack file tailored to your needs.
109
+
110
+ ## Customization
111
+
112
+ All examples use placeholder URLs. Replace with your actual endpoints:
113
+
114
+ - `https://api.example.com` → Your API URL
115
+ - `https://example.com` → Your web application URL
116
+ - `http://localhost:3000` → Your local dev server
117
+
118
+ ## Learn More
119
+
120
+ - [Pack Schema Documentation](https://github.com/xyqotech/qa360/blob/main/docs/pack-schema.md)
121
+ - [Quality Gates Reference](https://github.com/xyqotech/qa360/blob/main/docs/gates.md)
122
+ - [Full Documentation](https://github.com/xyqotech/qa360/blob/main/docs/README.md)
@@ -0,0 +1,25 @@
1
+ # QA360 Example: Accessibility Tests
2
+ # WCAG compliance and accessibility testing
3
+
4
+ version: 1
5
+ name: "Accessibility Tests"
6
+
7
+ gates:
8
+ - ui
9
+ - a11y
10
+
11
+ targets:
12
+ web:
13
+ baseUrl: "https://example.com"
14
+ pages:
15
+ - "https://example.com"
16
+ - "https://example.com/about"
17
+ - "https://example.com/contact"
18
+
19
+ # Accessibility budget
20
+ budgets:
21
+ a11y_min: 90 # Minimum accessibility score (0-100)
22
+
23
+ execution:
24
+ timeout: 60000
25
+ max_retries: 1
@@ -0,0 +1,23 @@
1
+ # QA360 Example: API Basic
2
+ # Simple API smoke tests for REST/GraphQL endpoints
3
+
4
+ version: 1
5
+ name: "API Health Check"
6
+
7
+ gates:
8
+ - api_smoke
9
+
10
+ targets:
11
+ api:
12
+ baseUrl: "https://httpbin.org"
13
+ smoke:
14
+ - "GET /status/200 -> 200"
15
+ - "GET /json -> 200"
16
+ - "GET /uuid -> 200"
17
+ - "POST /post -> 200"
18
+
19
+ # Optional: Execution settings
20
+ execution:
21
+ timeout: 30000 # 30 seconds
22
+ max_retries: 2
23
+ on_failure: continue # or 'stop'
@@ -0,0 +1,59 @@
1
+ # QA360 Example: Complete Test Suite
2
+ # All quality gates enabled - comprehensive quality assurance
3
+
4
+ version: 1
5
+ name: "Complete QA Suite"
6
+
7
+ gates:
8
+ - api_smoke # API health checks
9
+ - ui # Browser E2E tests
10
+ - a11y # Accessibility (WCAG)
11
+ - perf # Performance testing
12
+ - sast # Static security analysis
13
+ - dast # Dynamic security testing
14
+ - secrets # Credentials scanning
15
+ - deps # Dependency vulnerabilities
16
+
17
+ targets:
18
+ api:
19
+ baseUrl: "https://api.example.com"
20
+ smoke:
21
+ - "GET /health -> 200"
22
+ - "GET /status -> 200"
23
+ - "POST /login -> 200"
24
+
25
+ web:
26
+ baseUrl: "https://example.com"
27
+ pages:
28
+ - "https://example.com"
29
+ - "https://example.com/dashboard"
30
+
31
+ # Quality budgets
32
+ budgets:
33
+ perf_p95_ms: 2000 # P95 latency budget
34
+ a11y_min: 90 # Minimum accessibility score
35
+
36
+ # Security thresholds
37
+ security:
38
+ sast:
39
+ max_critical: 0
40
+ max_high: 3
41
+
42
+ dast:
43
+ max_high: 5
44
+
45
+ # Docker Compose integration
46
+ hooks:
47
+ beforeAll:
48
+ - compose: up
49
+ timeout: 30000
50
+ - wait_on: http://localhost:3000
51
+
52
+ afterAll:
53
+ - compose: down
54
+
55
+ # Execution settings
56
+ execution:
57
+ timeout: 120000 # 2 minutes
58
+ max_retries: 2
59
+ on_failure: continue
@@ -0,0 +1,42 @@
1
+ # QA360 Example: Full Stack
2
+ # Complete testing suite with API, UI, and Performance
3
+
4
+ version: 1
5
+ name: "Full Stack Tests"
6
+
7
+ gates:
8
+ - api_smoke
9
+ - ui
10
+ - perf
11
+
12
+ targets:
13
+ api:
14
+ baseUrl: "https://httpbin.org"
15
+ smoke:
16
+ - "GET /status/200 -> 200"
17
+ - "GET /json -> 200"
18
+ - "POST /post -> 200"
19
+
20
+ web:
21
+ baseUrl: "https://example.com"
22
+ pages:
23
+ - "https://example.com"
24
+
25
+ # Performance budgets
26
+ budgets:
27
+ perf_p95_ms: 2000 # P95 latency must be < 2000ms
28
+
29
+ # Hooks for local development
30
+ hooks:
31
+ beforeAll:
32
+ - compose: up
33
+ timeout: 30000
34
+ - wait_on: http://localhost:3000
35
+
36
+ afterAll:
37
+ - compose: down
38
+
39
+ execution:
40
+ timeout: 60000
41
+ max_retries: 2
42
+ on_failure: continue
@@ -0,0 +1,32 @@
1
+ # QA360 Example: Security Suite
2
+ # Comprehensive security testing (SAST, DAST, secrets, dependencies)
3
+
4
+ version: 1
5
+ name: "Security Test Suite"
6
+
7
+ gates:
8
+ - sast # Static Application Security Testing
9
+ - dast # Dynamic Application Security Testing
10
+ - secrets # Secrets detection
11
+ - deps # Dependency vulnerability scanning
12
+
13
+ targets:
14
+ api:
15
+ baseUrl: "https://api.example.com"
16
+
17
+ web:
18
+ baseUrl: "https://example.com"
19
+
20
+ # Security thresholds
21
+ security:
22
+ sast:
23
+ max_critical: 0 # Zero critical vulnerabilities allowed
24
+ max_high: 3 # Maximum 3 high-severity issues
25
+
26
+ dast:
27
+ max_high: 5 # Maximum 5 high-severity runtime issues
28
+
29
+ # Execution settings
30
+ execution:
31
+ timeout: 120000 # Security scans can take longer
32
+ on_failure: stop # Stop on first critical finding
@@ -0,0 +1,20 @@
1
+ # QA360 Example: UI Basic
2
+ # Basic UI/E2E browser tests
3
+
4
+ version: 1
5
+ name: "UI Browser Tests"
6
+
7
+ gates:
8
+ - ui
9
+
10
+ targets:
11
+ web:
12
+ baseUrl: "https://example.com"
13
+ pages:
14
+ - "https://example.com"
15
+ - "https://example.com/about"
16
+
17
+ # Optional: Browser configuration
18
+ execution:
19
+ timeout: 60000
20
+ max_retries: 1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "qa360",
3
- "version": "1.2.2",
3
+ "version": "1.3.1",
4
4
  "description": "QA360 Proof CLI - Quality as Cryptographic Proof",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -27,8 +27,7 @@
27
27
  "prepublishOnly": "npm run build:bundle && sed -i.bak 's/\"qa360-core\": \"workspace:\\*\",//g' package.json && rm -f package.json.bak",
28
28
  "postpublish": "git checkout package.json 2>/dev/null || true",
29
29
  "publish:dry": "npm run build:bundle && sed -i.bak 's/\"qa360-core\": \"workspace:\\*\",//g' package.json && npm publish --dry-run; git checkout package.json",
30
- "publish:real": "npm run build:bundle && npm publish --access public",
31
- "postinstall": "npx playwright install chromium --with-deps 2>/dev/null || echo 'Playwright browsers will be installed on first run'"
30
+ "publish:real": "npm run build:bundle && npm publish --access public --provenance"
32
31
  },
33
32
  "dependencies": {
34
33
  "@playwright/test": "^1.49.0",