qa360 2.2.0 → 2.2.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/.qa360/vault.db +0 -0
- package/.qa360/vault.db-shm +0 -0
- package/.qa360/vault.db-wal +0 -0
- package/cli/package.json +1 -1
- package/cli/src/commands/run.ts +5 -3
- package/core/package.json +1 -1
- package/core/src/fixtures/loader.ts +13 -2
- package/core/src/pack-v2/__tests__/validator.test.ts +3 -3
- package/core/src/pack-v2/validator.ts +10 -5
- package/core/src/runner/phase3-runner.ts +307 -7
- package/core/src/types/pack-v2.ts +24 -0
- package/package.json +2 -1
- package/packages/mcp/package.json +1 -1
package/.qa360/vault.db
ADDED
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/cli/package.json
CHANGED
package/cli/src/commands/run.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { existsSync, readFileSync } from 'fs';
|
|
11
|
-
import { join, resolve } from 'path';
|
|
11
|
+
import { join, resolve, dirname } from 'path';
|
|
12
12
|
import chalk from 'chalk';
|
|
13
13
|
import { load } from 'js-yaml';
|
|
14
14
|
import { Phase3Runner, PackLoaderV2, type Phase3RunResult, type GateResult, type PackConfigV1, type PackConfigV2 } from 'qa360-core';
|
|
@@ -183,13 +183,15 @@ export async function runCommand(
|
|
|
183
183
|
|
|
184
184
|
// Step 4: Create and run Phase3Runner
|
|
185
185
|
const workingDir = process.cwd();
|
|
186
|
-
const
|
|
187
|
-
|
|
186
|
+
const packDir = dirname(packPath); // Directory containing the pack file
|
|
187
|
+
const outputDir = options.output
|
|
188
|
+
? resolve(options.output)
|
|
188
189
|
: join(workingDir, '.qa360', 'runs');
|
|
189
190
|
|
|
190
191
|
const runner = new Phase3Runner({
|
|
191
192
|
workingDir,
|
|
192
193
|
pack,
|
|
194
|
+
packDir, // Pass pack directory for resolving fixtures
|
|
193
195
|
outputDir,
|
|
194
196
|
});
|
|
195
197
|
|
package/core/package.json
CHANGED
|
@@ -84,9 +84,20 @@ export class FixtureLoader {
|
|
|
84
84
|
);
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
-
//
|
|
87
|
+
// Merge fixture data into registry (keys become directly accessible)
|
|
88
|
+
// This allows $fixtures.environments.production.baseUrl instead of
|
|
89
|
+
// $fixtures.fixtures.environments.production.baseUrl
|
|
88
90
|
const fixtureName = this.getFixtureName(fixturePath);
|
|
89
|
-
this.registry[fixtureName] = fixtureData;
|
|
91
|
+
this.registry[fixtureName] = fixtureData; // Keep filename reference for debug
|
|
92
|
+
|
|
93
|
+
// Also merge top-level keys for direct access
|
|
94
|
+
for (const [key, value] of Object.entries(fixtureData)) {
|
|
95
|
+
// Skip if key already exists (first file wins)
|
|
96
|
+
// We need to cast because FixtureValue is broader than FixtureFile
|
|
97
|
+
if (!(key in this.registry)) {
|
|
98
|
+
(this.registry as Record<string, unknown>)[key] = value;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
90
101
|
|
|
91
102
|
return fixtureData;
|
|
92
103
|
} catch (error) {
|
|
@@ -569,7 +569,7 @@ describe('PackValidatorV2', () => {
|
|
|
569
569
|
expect(result.valid).toBe(true);
|
|
570
570
|
});
|
|
571
571
|
|
|
572
|
-
it('should
|
|
572
|
+
it('should warn about fixture path with parent directory reference', async () => {
|
|
573
573
|
const pack: PackConfigV2 = {
|
|
574
574
|
version: 2,
|
|
575
575
|
name: 'test-pack',
|
|
@@ -577,8 +577,8 @@ describe('PackValidatorV2', () => {
|
|
|
577
577
|
gates: { smoke: { adapter: 'playwright-api' } }
|
|
578
578
|
};
|
|
579
579
|
const result = await validator.validate(pack);
|
|
580
|
-
expect(result.valid).toBe(
|
|
581
|
-
expect(result.
|
|
580
|
+
expect(result.valid).toBe(true); // Changed from false to true - now just a warning
|
|
581
|
+
expect(result.warnings.some(e => e.code === 'QP2V038')).toBe(true); // Changed from errors to warnings
|
|
582
582
|
});
|
|
583
583
|
|
|
584
584
|
it('should warn about unknown file extension', async () => {
|
|
@@ -186,13 +186,14 @@ export class PackValidatorV2 {
|
|
|
186
186
|
continue;
|
|
187
187
|
}
|
|
188
188
|
|
|
189
|
-
// Check for
|
|
189
|
+
// Check for patterns that might cause issues
|
|
190
190
|
if (fixturePath.includes('..')) {
|
|
191
|
-
|
|
191
|
+
// Changed from error to warning - parent directory references are sometimes needed
|
|
192
|
+
warnings.push({
|
|
192
193
|
code: 'QP2V038',
|
|
193
|
-
message:
|
|
194
|
+
message: `Fixture path contains parent directory reference "..": ${fixturePath}`,
|
|
194
195
|
path: `fixtures.${fixturePath}`,
|
|
195
|
-
suggestion: '
|
|
196
|
+
suggestion: 'Consider using absolute paths or placing fixtures in the same directory as the pack'
|
|
196
197
|
});
|
|
197
198
|
}
|
|
198
199
|
|
|
@@ -384,7 +385,11 @@ export class PackValidatorV2 {
|
|
|
384
385
|
'semgrep-sast',
|
|
385
386
|
'zap-dast',
|
|
386
387
|
'gitleaks-secrets',
|
|
387
|
-
'osv-deps'
|
|
388
|
+
'osv-deps',
|
|
389
|
+
// v2.2.0 Unit Test Adapters
|
|
390
|
+
'vitest',
|
|
391
|
+
'jest',
|
|
392
|
+
'pytest'
|
|
388
393
|
];
|
|
389
394
|
|
|
390
395
|
for (const [gateName, gate] of Object.entries(gates)) {
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { existsSync, writeFileSync, mkdirSync } from 'fs';
|
|
8
|
-
import { join } from 'path';
|
|
8
|
+
import { join, resolve } from 'path';
|
|
9
9
|
import { createHash } from 'crypto';
|
|
10
10
|
import chalk from 'chalk';
|
|
11
11
|
import { PackConfigV1 } from '../types/pack-v1.js';
|
|
@@ -15,6 +15,14 @@ import { PlaywrightNativeApiAdapter } from '../adapters/playwright-native-api.js
|
|
|
15
15
|
import { PlaywrightUiAdapter } from '../adapters/playwright-ui.js';
|
|
16
16
|
import { K6PerfAdapter } from '../adapters/k6-perf.js';
|
|
17
17
|
import { SemgrepSastAdapter } from '../adapters/semgrep-sast.js';
|
|
18
|
+
// v2.2.0 Unit Test Adapters
|
|
19
|
+
import { VitestAdapter } from '../adapters/vitest-adapter.js';
|
|
20
|
+
import { JestAdapter } from '../adapters/jest-adapter.js';
|
|
21
|
+
import { PytestAdapter } from '../adapters/pytest-adapter.js';
|
|
22
|
+
// v2.2.0 Data Fixtures
|
|
23
|
+
import { FixtureLoader } from '../fixtures/loader.js';
|
|
24
|
+
import { FixtureResolver } from '../fixtures/resolver.js';
|
|
25
|
+
import { PageObjectLoader } from '../pom/loader.js';
|
|
18
26
|
import { SecurityRedactor } from '../security/redactor.js';
|
|
19
27
|
import { initializeKeys, sign, KeyPair } from '../proof/signer.js';
|
|
20
28
|
import { canonicalize } from '../proof/canonicalize.js';
|
|
@@ -35,6 +43,7 @@ export type PackConfig = PackConfigV1 | PackConfigV2;
|
|
|
35
43
|
export interface Phase3RunnerOptions {
|
|
36
44
|
workingDir: string;
|
|
37
45
|
pack: PackConfig;
|
|
46
|
+
packDir?: string; // Directory containing the pack file (for resolving fixtures)
|
|
38
47
|
outputDir?: string;
|
|
39
48
|
/** Enable flakiness detection (runs tests N times consecutively) */
|
|
40
49
|
flakyDetect?: boolean;
|
|
@@ -80,6 +89,7 @@ export interface Phase3RunResult {
|
|
|
80
89
|
export class Phase3Runner {
|
|
81
90
|
private workingDir: string;
|
|
82
91
|
private pack: PackConfig;
|
|
92
|
+
private packDir: string; // Directory containing the pack file
|
|
83
93
|
private outputDir: string;
|
|
84
94
|
private redactor: SecurityRedactor;
|
|
85
95
|
private hooksRunner: HooksRunner;
|
|
@@ -90,10 +100,15 @@ export class Phase3Runner {
|
|
|
90
100
|
private flakyDetect: boolean;
|
|
91
101
|
private flakyRuns: number;
|
|
92
102
|
private flakinessDetector: FlakinessDetector;
|
|
103
|
+
// v2.2.0 Data Fixtures & POM
|
|
104
|
+
private fixtureLoader?: FixtureLoader;
|
|
105
|
+
private fixtureResolver?: FixtureResolver;
|
|
106
|
+
private pageObjectLoader?: PageObjectLoader;
|
|
93
107
|
|
|
94
108
|
constructor(options: Phase3RunnerOptions) {
|
|
95
109
|
this.workingDir = options.workingDir;
|
|
96
110
|
this.pack = options.pack;
|
|
111
|
+
this.packDir = options.packDir || options.workingDir;
|
|
97
112
|
this.outputDir = options.outputDir || join(this.workingDir, '.qa360', 'runs');
|
|
98
113
|
this.redactor = SecurityRedactor.forLogs();
|
|
99
114
|
this.authManager = new AuthManager();
|
|
@@ -121,6 +136,101 @@ export class Phase3Runner {
|
|
|
121
136
|
if (this.isPackV2(options.pack) && options.pack.auth?.profiles) {
|
|
122
137
|
this.registerAuthProfiles(options.pack.auth.profiles);
|
|
123
138
|
}
|
|
139
|
+
|
|
140
|
+
// Load data fixtures from pack v2
|
|
141
|
+
if (this.isPackV2(options.pack) && options.pack.fixtures && options.pack.fixtures.length > 0) {
|
|
142
|
+
this.initializeFixtures(options.pack.fixtures);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Initialize Page Object Model from pack v2
|
|
146
|
+
if (this.isPackV2(options.pack) && options.pack.pageObjects) {
|
|
147
|
+
this.initializePageObjects(options.pack.pageObjects);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Initialize Page Object Model from the pack configuration
|
|
153
|
+
*/
|
|
154
|
+
private initializePageObjects(config: { directory?: string; pattern?: string; baseUrl?: string }): void {
|
|
155
|
+
try {
|
|
156
|
+
const pomDir = config.directory ? resolve(this.packDir, config.directory) : this.packDir;
|
|
157
|
+
this.pageObjectLoader = new PageObjectLoader({ cwd: pomDir });
|
|
158
|
+
console.log(chalk.gray(` 📄 POM loader initialized for: ${pomDir}`));
|
|
159
|
+
} catch (error) {
|
|
160
|
+
console.log(chalk.yellow(`⚠️ Failed to initialize POM loader: ${error instanceof Error ? error.message : 'Unknown error'}`));
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Load Page Objects before execution
|
|
166
|
+
*/
|
|
167
|
+
private async loadPageObjects(): Promise<void> {
|
|
168
|
+
if (!this.pageObjectLoader || !this.isPackV2(this.pack) || !this.pack.pageObjects) {
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
try {
|
|
173
|
+
const pattern = this.pack.pageObjects.pattern || '**/*.page.{ts,js}';
|
|
174
|
+
const result = await this.pageObjectLoader.loadFromDirectory(this.packDir, { pattern });
|
|
175
|
+
console.log(chalk.green(` ✅ Page Objects loaded: ${result.pagesCount} pages`));
|
|
176
|
+
} catch (error) {
|
|
177
|
+
console.log(chalk.yellow(`⚠️ Failed to load page objects: ${error instanceof Error ? error.message : 'Unknown error'}`));
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Initialize data fixtures from the pack configuration
|
|
183
|
+
*/
|
|
184
|
+
private initializeFixtures(fixturePaths: string[]): void {
|
|
185
|
+
try {
|
|
186
|
+
this.fixtureLoader = new FixtureLoader(this.packDir);
|
|
187
|
+
// Load fixtures synchronously - they're already validated
|
|
188
|
+
// We'll load them before execution starts
|
|
189
|
+
this.fixtureResolver = new FixtureResolver(this.fixtureLoader, this.packDir);
|
|
190
|
+
} catch (error) {
|
|
191
|
+
console.log(chalk.yellow(`⚠️ Failed to initialize fixtures: ${error instanceof Error ? error.message : 'Unknown error'}`));
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Load fixtures before execution
|
|
197
|
+
*/
|
|
198
|
+
private async loadFixtures(): Promise<void> {
|
|
199
|
+
if (!this.fixtureLoader || !this.isPackV2(this.pack) || !this.pack.fixtures) {
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
try {
|
|
204
|
+
await this.fixtureLoader.load(this.pack.fixtures);
|
|
205
|
+
console.log(chalk.green(` ✅ Fixtures loaded: ${this.fixtureLoader.getFixtureNames().join(', ')}`));
|
|
206
|
+
} catch (error) {
|
|
207
|
+
console.log(chalk.yellow(`⚠️ Failed to load fixtures: ${error instanceof Error ? error.message : 'Unknown error'}`));
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Resolve fixture references in a configuration object
|
|
213
|
+
*/
|
|
214
|
+
private resolveFixturesInConfig(config: any): any {
|
|
215
|
+
if (!this.fixtureResolver || !config) {
|
|
216
|
+
return config;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
try {
|
|
220
|
+
const result = this.fixtureResolver.resolve(config, {
|
|
221
|
+
baseDir: this.workingDir,
|
|
222
|
+
keepUnresolved: false
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
if (result.hasFixtures) {
|
|
226
|
+
console.log(chalk.gray(` 🔧 Fixtures resolved in config`));
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return result.value;
|
|
230
|
+
} catch (error) {
|
|
231
|
+
console.log(chalk.yellow(`⚠️ Failed to resolve fixtures: ${error instanceof Error ? error.message : 'Unknown error'}`));
|
|
232
|
+
return config;
|
|
233
|
+
}
|
|
124
234
|
}
|
|
125
235
|
|
|
126
236
|
/**
|
|
@@ -269,11 +379,17 @@ export class Phase3Runner {
|
|
|
269
379
|
|
|
270
380
|
console.log(chalk.bold.blue(`\n🚀 QA360 Phase 3 Runner - ${this.pack.name}`));
|
|
271
381
|
console.log(chalk.gray(`Gates: ${gatesArray.join(', ')}`));
|
|
272
|
-
|
|
382
|
+
|
|
273
383
|
try {
|
|
274
384
|
// Ensure output directory exists
|
|
275
385
|
this.ensureOutputDir();
|
|
276
|
-
|
|
386
|
+
|
|
387
|
+
// Load data fixtures if configured
|
|
388
|
+
await this.loadFixtures();
|
|
389
|
+
|
|
390
|
+
// Load page objects if configured
|
|
391
|
+
await this.loadPageObjects();
|
|
392
|
+
|
|
277
393
|
// Initialize cryptographic keys
|
|
278
394
|
console.log(chalk.blue('\n🔑 Initializing Ed25519 keys...'));
|
|
279
395
|
this.keyPair = await initializeKeys();
|
|
@@ -548,6 +664,9 @@ export class Phase3Runner {
|
|
|
548
664
|
throw new Error(`Gate '${gateName}' must specify an adapter`);
|
|
549
665
|
}
|
|
550
666
|
|
|
667
|
+
// Resolve fixtures in gate configuration
|
|
668
|
+
const resolvedGateConfig = this.resolveFixturesInConfig(gateConfig);
|
|
669
|
+
|
|
551
670
|
// Get auth credentials for this gate
|
|
552
671
|
const credentials = await this.getCredentialsForGate(gateName);
|
|
553
672
|
|
|
@@ -555,21 +674,34 @@ export class Phase3Runner {
|
|
|
555
674
|
let result: GateResult;
|
|
556
675
|
switch (adapterType) {
|
|
557
676
|
case 'playwright-api':
|
|
558
|
-
result = await this.executePlaywrightApiGate(gateName,
|
|
677
|
+
result = await this.executePlaywrightApiGate(gateName, resolvedGateConfig, credentials);
|
|
559
678
|
break;
|
|
560
679
|
|
|
561
680
|
case 'playwright-ui':
|
|
562
|
-
result = await this.executePlaywrightUiGate(gateName,
|
|
681
|
+
result = await this.executePlaywrightUiGate(gateName, resolvedGateConfig, credentials);
|
|
563
682
|
break;
|
|
564
683
|
|
|
565
684
|
case 'k6':
|
|
566
685
|
case 'k6-perf':
|
|
567
|
-
result = await this.executeK6PerfGate(gateName,
|
|
686
|
+
result = await this.executeK6PerfGate(gateName, resolvedGateConfig);
|
|
568
687
|
break;
|
|
569
688
|
|
|
570
689
|
case 'semgrep':
|
|
571
690
|
case 'sast':
|
|
572
|
-
result = await this.executeSemgrepSastGate(gateName,
|
|
691
|
+
result = await this.executeSemgrepSastGate(gateName, resolvedGateConfig);
|
|
692
|
+
break;
|
|
693
|
+
|
|
694
|
+
// v2.2.0 Unit Test Adapters
|
|
695
|
+
case 'vitest':
|
|
696
|
+
result = await this.executeVitestGate(gateName, resolvedGateConfig);
|
|
697
|
+
break;
|
|
698
|
+
|
|
699
|
+
case 'jest':
|
|
700
|
+
result = await this.executeJestGate(gateName, resolvedGateConfig);
|
|
701
|
+
break;
|
|
702
|
+
|
|
703
|
+
case 'pytest':
|
|
704
|
+
result = await this.executePytestGate(gateName, resolvedGateConfig);
|
|
573
705
|
break;
|
|
574
706
|
|
|
575
707
|
default:
|
|
@@ -731,6 +863,174 @@ export class Phase3Runner {
|
|
|
731
863
|
};
|
|
732
864
|
}
|
|
733
865
|
|
|
866
|
+
/**
|
|
867
|
+
* Execute Vitest unit tests gate (v2.2.0)
|
|
868
|
+
*/
|
|
869
|
+
private async executeVitestGate(gateName: string, gateConfig: any): Promise<GateResult> {
|
|
870
|
+
const adapter = new VitestAdapter(this.workingDir);
|
|
871
|
+
|
|
872
|
+
const config: any = {
|
|
873
|
+
cwd: this.workingDir,
|
|
874
|
+
...gateConfig.config
|
|
875
|
+
};
|
|
876
|
+
|
|
877
|
+
const result = await adapter.execute(config);
|
|
878
|
+
|
|
879
|
+
return {
|
|
880
|
+
gate: gateName,
|
|
881
|
+
success: result.success,
|
|
882
|
+
duration: result.duration,
|
|
883
|
+
adapter: 'vitest',
|
|
884
|
+
results: {
|
|
885
|
+
total: result.total,
|
|
886
|
+
passed: result.passed,
|
|
887
|
+
failed: result.failed,
|
|
888
|
+
skipped: result.skipped,
|
|
889
|
+
tests: result.tests.map(t => ({
|
|
890
|
+
name: t.name,
|
|
891
|
+
status: t.status,
|
|
892
|
+
duration: t.duration,
|
|
893
|
+
error: t.error
|
|
894
|
+
}))
|
|
895
|
+
},
|
|
896
|
+
junit: this.buildJunitFromVitest(result)
|
|
897
|
+
};
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
/**
|
|
901
|
+
* Execute Jest unit tests gate (v2.2.0)
|
|
902
|
+
*/
|
|
903
|
+
private async executeJestGate(gateName: string, gateConfig: any): Promise<GateResult> {
|
|
904
|
+
const adapter = new JestAdapter(this.workingDir);
|
|
905
|
+
|
|
906
|
+
const config: any = {
|
|
907
|
+
cwd: this.workingDir,
|
|
908
|
+
...gateConfig.config
|
|
909
|
+
};
|
|
910
|
+
|
|
911
|
+
const result = await adapter.execute(config);
|
|
912
|
+
|
|
913
|
+
return {
|
|
914
|
+
gate: gateName,
|
|
915
|
+
success: result.success,
|
|
916
|
+
duration: result.duration,
|
|
917
|
+
adapter: 'jest',
|
|
918
|
+
results: {
|
|
919
|
+
total: result.total,
|
|
920
|
+
passed: result.passed,
|
|
921
|
+
failed: result.failed,
|
|
922
|
+
skipped: result.skipped + result.pending,
|
|
923
|
+
tests: result.tests.map(t => ({
|
|
924
|
+
name: t.name,
|
|
925
|
+
status: t.status,
|
|
926
|
+
duration: t.duration,
|
|
927
|
+
error: t.error
|
|
928
|
+
}))
|
|
929
|
+
},
|
|
930
|
+
junit: this.buildJunitFromJest(result)
|
|
931
|
+
};
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
/**
|
|
935
|
+
* Execute Pytest unit tests gate (v2.2.0)
|
|
936
|
+
*/
|
|
937
|
+
private async executePytestGate(gateName: string, gateConfig: any): Promise<GateResult> {
|
|
938
|
+
const adapter = new PytestAdapter(this.workingDir);
|
|
939
|
+
|
|
940
|
+
const config: any = {
|
|
941
|
+
cwd: this.workingDir,
|
|
942
|
+
...gateConfig.config
|
|
943
|
+
};
|
|
944
|
+
|
|
945
|
+
const result = await adapter.execute(config);
|
|
946
|
+
|
|
947
|
+
return {
|
|
948
|
+
gate: gateName,
|
|
949
|
+
success: result.success,
|
|
950
|
+
duration: result.duration,
|
|
951
|
+
adapter: 'pytest',
|
|
952
|
+
results: {
|
|
953
|
+
total: result.total,
|
|
954
|
+
passed: result.passed,
|
|
955
|
+
failed: result.failed,
|
|
956
|
+
skipped: result.skipped,
|
|
957
|
+
tests: result.tests.map(t => ({
|
|
958
|
+
name: t.name,
|
|
959
|
+
status: t.status,
|
|
960
|
+
duration: t.duration,
|
|
961
|
+
error: t.error
|
|
962
|
+
}))
|
|
963
|
+
},
|
|
964
|
+
junit: this.buildJunitFromPytest(result)
|
|
965
|
+
};
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
/**
|
|
969
|
+
* Build JUnit format from Vitest results
|
|
970
|
+
*/
|
|
971
|
+
private buildJunitFromVitest(result: any): any {
|
|
972
|
+
// Convert adapter result to JUnit-like format
|
|
973
|
+
return {
|
|
974
|
+
testsuites: [{
|
|
975
|
+
name: 'vitest',
|
|
976
|
+
tests: result.total,
|
|
977
|
+
failures: result.failed,
|
|
978
|
+
skipped: result.skipped,
|
|
979
|
+
time: result.duration / 1000,
|
|
980
|
+
testcases: result.tests.map((t: any) => ({
|
|
981
|
+
name: t.name,
|
|
982
|
+
classname: t.file,
|
|
983
|
+
time: t.duration / 1000,
|
|
984
|
+
failure: t.error ? { message: t.error } : undefined,
|
|
985
|
+
skipped: t.status === 'skipped' ? { message: 'Skipped' } : undefined
|
|
986
|
+
}))
|
|
987
|
+
}]
|
|
988
|
+
};
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
/**
|
|
992
|
+
* Build JUnit format from Jest results
|
|
993
|
+
*/
|
|
994
|
+
private buildJunitFromJest(result: any): any {
|
|
995
|
+
return {
|
|
996
|
+
testsuites: [{
|
|
997
|
+
name: 'jest',
|
|
998
|
+
tests: result.total,
|
|
999
|
+
failures: result.failed,
|
|
1000
|
+
skipped: result.pending,
|
|
1001
|
+
time: result.duration / 1000,
|
|
1002
|
+
testcases: result.tests.map((t: any) => ({
|
|
1003
|
+
name: t.name,
|
|
1004
|
+
time: t.duration / 1000,
|
|
1005
|
+
failure: t.error ? { message: t.error } : undefined,
|
|
1006
|
+
skipped: t.status === 'pending' || t.status === 'skipped' ? { message: 'Skipped' } : undefined
|
|
1007
|
+
}))
|
|
1008
|
+
}]
|
|
1009
|
+
};
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
/**
|
|
1013
|
+
* Build JUnit format from Pytest results
|
|
1014
|
+
*/
|
|
1015
|
+
private buildJunitFromPytest(result: any): any {
|
|
1016
|
+
return {
|
|
1017
|
+
testsuites: [{
|
|
1018
|
+
name: 'pytest',
|
|
1019
|
+
tests: result.total,
|
|
1020
|
+
failures: result.failed,
|
|
1021
|
+
skipped: result.skipped,
|
|
1022
|
+
time: result.duration / 1000,
|
|
1023
|
+
testcases: result.tests.map((t: any) => ({
|
|
1024
|
+
name: t.name,
|
|
1025
|
+
classname: t.file,
|
|
1026
|
+
time: t.duration / 1000,
|
|
1027
|
+
failure: t.error ? { message: t.error } : undefined,
|
|
1028
|
+
skipped: t.status === 'skipped' ? { message: 'Skipped' } : undefined
|
|
1029
|
+
}))
|
|
1030
|
+
}]
|
|
1031
|
+
};
|
|
1032
|
+
}
|
|
1033
|
+
|
|
734
1034
|
/**
|
|
735
1035
|
* Run API smoke gate
|
|
736
1036
|
* Uses PlaywrightNativeApiAdapter for zero-overhead HTTP testing
|
|
@@ -6,6 +6,26 @@
|
|
|
6
6
|
* AI-generated code support and comprehensive authentication.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
+
/**
|
|
10
|
+
* Page Object Model configuration
|
|
11
|
+
*/
|
|
12
|
+
export interface PageObjectConfigV2 {
|
|
13
|
+
/**
|
|
14
|
+
* Directory containing page object files
|
|
15
|
+
*/
|
|
16
|
+
directory?: string;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Glob pattern for page object files
|
|
20
|
+
*/
|
|
21
|
+
pattern?: string;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Base URL for page objects (can be overridden per page)
|
|
25
|
+
*/
|
|
26
|
+
baseUrl?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
9
29
|
/**
|
|
10
30
|
* Pack Configuration v2
|
|
11
31
|
*/
|
|
@@ -24,6 +44,10 @@ export interface PackConfigV2 {
|
|
|
24
44
|
* - ./fixtures/products.json
|
|
25
45
|
*/
|
|
26
46
|
fixtures?: string[];
|
|
47
|
+
/**
|
|
48
|
+
* Page Object Model configuration
|
|
49
|
+
*/
|
|
50
|
+
pageObjects?: PageObjectConfigV2;
|
|
27
51
|
auth?: AuthConfigV2;
|
|
28
52
|
gates: Record<string, GateConfigV2>;
|
|
29
53
|
hooks?: HooksConfig;
|
package/package.json
CHANGED