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 +81 -4
- package/dist/commands/examples.d.ts +34 -0
- package/dist/commands/examples.d.ts.map +1 -0
- package/dist/commands/examples.js +190 -0
- package/dist/commands/init.d.ts +66 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +307 -0
- package/dist/commands/run.d.ts.map +1 -1
- package/dist/commands/run.js +19 -2
- package/dist/core/adapters/playwright-api.d.ts +1 -0
- package/dist/core/adapters/playwright-api.d.ts.map +1 -1
- package/dist/core/adapters/playwright-api.js +14 -2
- package/dist/index.js +36 -0
- package/examples/README.md +122 -0
- package/examples/accessibility.yml +25 -0
- package/examples/api-basic.yml +23 -0
- package/examples/complete.yml +59 -0
- package/examples/fullstack.yml +42 -0
- package/examples/security.yml +32 -0
- package/examples/ui-basic.yml +20 -0
- package/package.json +2 -3
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
|
|
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
|
-
| `
|
|
57
|
-
| `
|
|
130
|
+
| `init` | Generate test pack interactively |
|
|
131
|
+
| `examples` | Manage example templates (list, copy, show) |
|
|
58
132
|
| `run` | Execute test pack |
|
|
59
|
-
| `
|
|
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,
|
|
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"}
|
package/dist/commands/run.js
CHANGED
|
@@ -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 =>
|
|
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
|
-
|
|
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
|
}
|
|
@@ -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;
|
|
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
|
-
|
|
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.
|
|
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",
|