qa360 1.4.5 → 2.0.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 +1 -1
- package/dist/commands/ai.d.ts +41 -0
- package/dist/commands/ai.js +499 -0
- package/dist/commands/ask.js +12 -12
- package/dist/commands/coverage.d.ts +8 -0
- package/dist/commands/coverage.js +252 -0
- package/dist/commands/explain.d.ts +27 -0
- package/dist/commands/explain.js +630 -0
- package/dist/commands/flakiness.d.ts +73 -0
- package/dist/commands/flakiness.js +435 -0
- package/dist/commands/generate.d.ts +66 -0
- package/dist/commands/generate.js +438 -0
- package/dist/commands/init.d.ts +56 -9
- package/dist/commands/init.js +217 -10
- package/dist/commands/monitor.d.ts +27 -0
- package/dist/commands/monitor.js +225 -0
- package/dist/commands/ollama.d.ts +40 -0
- package/dist/commands/ollama.js +301 -0
- package/dist/commands/pack.d.ts +37 -9
- package/dist/commands/pack.js +240 -141
- package/dist/commands/regression.d.ts +8 -0
- package/dist/commands/regression.js +340 -0
- package/dist/commands/repair.d.ts +26 -0
- package/dist/commands/repair.js +307 -0
- package/dist/commands/retry.d.ts +43 -0
- package/dist/commands/retry.js +275 -0
- package/dist/commands/run.d.ts +8 -3
- package/dist/commands/run.js +45 -31
- package/dist/commands/slo.d.ts +8 -0
- package/dist/commands/slo.js +327 -0
- package/dist/core/adapters/playwright-native-api.d.ts +183 -0
- package/dist/core/adapters/playwright-native-api.js +461 -0
- package/dist/core/adapters/playwright-ui.d.ts +7 -0
- package/dist/core/adapters/playwright-ui.js +29 -1
- package/dist/core/ai/anthropic-provider.d.ts +50 -0
- package/dist/core/ai/anthropic-provider.js +211 -0
- package/dist/core/ai/deepseek-provider.d.ts +81 -0
- package/dist/core/ai/deepseek-provider.js +254 -0
- package/dist/core/ai/index.d.ts +60 -0
- package/dist/core/ai/index.js +18 -0
- package/dist/core/ai/llm-client.d.ts +45 -0
- package/dist/core/ai/llm-client.js +7 -0
- package/dist/core/ai/mock-provider.d.ts +49 -0
- package/dist/core/ai/mock-provider.js +121 -0
- package/dist/core/ai/ollama-provider.d.ts +78 -0
- package/dist/core/ai/ollama-provider.js +192 -0
- package/dist/core/ai/openai-provider.d.ts +48 -0
- package/dist/core/ai/openai-provider.js +188 -0
- package/dist/core/ai/provider-factory.d.ts +160 -0
- package/dist/core/ai/provider-factory.js +269 -0
- package/dist/core/auth/api-key-provider.d.ts +16 -0
- package/dist/core/auth/api-key-provider.js +63 -0
- package/dist/core/auth/aws-iam-provider.d.ts +35 -0
- package/dist/core/auth/aws-iam-provider.js +177 -0
- package/dist/core/auth/azure-ad-provider.d.ts +15 -0
- package/dist/core/auth/azure-ad-provider.js +99 -0
- package/dist/core/auth/basic-auth-provider.d.ts +26 -0
- package/dist/core/auth/basic-auth-provider.js +111 -0
- package/dist/core/auth/gcp-adc-provider.d.ts +27 -0
- package/dist/core/auth/gcp-adc-provider.js +126 -0
- package/dist/core/auth/index.d.ts +238 -0
- package/dist/core/auth/index.js +82 -0
- package/dist/core/auth/jwt-provider.d.ts +19 -0
- package/dist/core/auth/jwt-provider.js +160 -0
- package/dist/core/auth/manager.d.ts +84 -0
- package/dist/core/auth/manager.js +230 -0
- package/dist/core/auth/oauth2-provider.d.ts +17 -0
- package/dist/core/auth/oauth2-provider.js +114 -0
- package/dist/core/auth/totp-provider.d.ts +31 -0
- package/dist/core/auth/totp-provider.js +134 -0
- package/dist/core/auth/ui-login-provider.d.ts +26 -0
- package/dist/core/auth/ui-login-provider.js +198 -0
- package/dist/core/cache/index.d.ts +7 -0
- package/dist/core/cache/index.js +6 -0
- package/dist/core/cache/lru-cache.d.ts +203 -0
- package/dist/core/cache/lru-cache.js +397 -0
- package/dist/core/coverage/analyzer.d.ts +101 -0
- package/dist/core/coverage/analyzer.js +415 -0
- package/dist/core/coverage/collector.d.ts +74 -0
- package/dist/core/coverage/collector.js +459 -0
- package/dist/core/coverage/config.d.ts +37 -0
- package/dist/core/coverage/config.js +156 -0
- package/dist/core/coverage/index.d.ts +11 -0
- package/dist/core/coverage/index.js +15 -0
- package/dist/core/coverage/types.d.ts +267 -0
- package/dist/core/coverage/types.js +6 -0
- package/dist/core/coverage/vault.d.ts +95 -0
- package/dist/core/coverage/vault.js +405 -0
- package/dist/core/dashboard/assets.d.ts +6 -0
- package/dist/core/dashboard/assets.js +690 -0
- package/dist/core/dashboard/index.d.ts +6 -0
- package/dist/core/dashboard/index.js +5 -0
- package/dist/core/dashboard/server.d.ts +72 -0
- package/dist/core/dashboard/server.js +354 -0
- package/dist/core/dashboard/types.d.ts +70 -0
- package/dist/core/dashboard/types.js +5 -0
- package/dist/core/discoverer/index.d.ts +115 -0
- package/dist/core/discoverer/index.js +250 -0
- package/dist/core/flakiness/index.d.ts +228 -0
- package/dist/core/flakiness/index.js +384 -0
- package/dist/core/generation/code-formatter.d.ts +111 -0
- package/dist/core/generation/code-formatter.js +307 -0
- package/dist/core/generation/code-generator.d.ts +144 -0
- package/dist/core/generation/code-generator.js +293 -0
- package/dist/core/generation/generator.d.ts +40 -0
- package/dist/core/generation/generator.js +76 -0
- package/dist/core/generation/index.d.ts +30 -0
- package/dist/core/generation/index.js +28 -0
- package/dist/core/generation/pack-generator.d.ts +107 -0
- package/dist/core/generation/pack-generator.js +416 -0
- package/dist/core/generation/prompt-builder.d.ts +132 -0
- package/dist/core/generation/prompt-builder.js +672 -0
- package/dist/core/generation/source-analyzer.d.ts +213 -0
- package/dist/core/generation/source-analyzer.js +657 -0
- package/dist/core/generation/test-optimizer.d.ts +117 -0
- package/dist/core/generation/test-optimizer.js +328 -0
- package/dist/core/generation/types.d.ts +214 -0
- package/dist/core/generation/types.js +4 -0
- package/dist/core/index.d.ts +23 -1
- package/dist/core/index.js +39 -0
- package/dist/core/pack/validator.js +31 -1
- package/dist/core/pack-v2/index.d.ts +9 -0
- package/dist/core/pack-v2/index.js +8 -0
- package/dist/core/pack-v2/loader.d.ts +62 -0
- package/dist/core/pack-v2/loader.js +231 -0
- package/dist/core/pack-v2/migrator.d.ts +56 -0
- package/dist/core/pack-v2/migrator.js +455 -0
- package/dist/core/pack-v2/validator.d.ts +61 -0
- package/dist/core/pack-v2/validator.js +577 -0
- package/dist/core/regression/detector.d.ts +107 -0
- package/dist/core/regression/detector.js +497 -0
- package/dist/core/regression/index.d.ts +9 -0
- package/dist/core/regression/index.js +11 -0
- package/dist/core/regression/trend-analyzer.d.ts +102 -0
- package/dist/core/regression/trend-analyzer.js +345 -0
- package/dist/core/regression/types.d.ts +222 -0
- package/dist/core/regression/types.js +7 -0
- package/dist/core/regression/vault.d.ts +87 -0
- package/dist/core/regression/vault.js +289 -0
- package/dist/core/repair/engine/fixer.d.ts +24 -0
- package/dist/core/repair/engine/fixer.js +226 -0
- package/dist/core/repair/engine/suggestion-engine.d.ts +18 -0
- package/dist/core/repair/engine/suggestion-engine.js +187 -0
- package/dist/core/repair/index.d.ts +10 -0
- package/dist/core/repair/index.js +13 -0
- package/dist/core/repair/repairer.d.ts +90 -0
- package/dist/core/repair/repairer.js +284 -0
- package/dist/core/repair/types.d.ts +91 -0
- package/dist/core/repair/types.js +6 -0
- package/dist/core/repair/utils/error-analyzer.d.ts +28 -0
- package/dist/core/repair/utils/error-analyzer.js +264 -0
- package/dist/core/retry/flakiness-integration.d.ts +60 -0
- package/dist/core/retry/flakiness-integration.js +228 -0
- package/dist/core/retry/index.d.ts +14 -0
- package/dist/core/retry/index.js +16 -0
- package/dist/core/retry/retry-engine.d.ts +80 -0
- package/dist/core/retry/retry-engine.js +296 -0
- package/dist/core/retry/types.d.ts +178 -0
- package/dist/core/retry/types.js +52 -0
- package/dist/core/retry/vault.d.ts +77 -0
- package/dist/core/retry/vault.js +304 -0
- package/dist/core/runner/e2e-helpers.d.ts +102 -0
- package/dist/core/runner/e2e-helpers.js +153 -0
- package/dist/core/runner/phase3-runner.d.ts +101 -2
- package/dist/core/runner/phase3-runner.js +559 -24
- package/dist/core/self-healing/assertion-healer.d.ts +97 -0
- package/dist/core/self-healing/assertion-healer.js +371 -0
- package/dist/core/self-healing/engine.d.ts +122 -0
- package/dist/core/self-healing/engine.js +538 -0
- package/dist/core/self-healing/index.d.ts +10 -0
- package/dist/core/self-healing/index.js +11 -0
- package/dist/core/self-healing/selector-healer.d.ts +103 -0
- package/dist/core/self-healing/selector-healer.js +372 -0
- package/dist/core/self-healing/types.d.ts +152 -0
- package/dist/core/self-healing/types.js +6 -0
- package/dist/core/slo/config.d.ts +107 -0
- package/dist/core/slo/config.js +360 -0
- package/dist/core/slo/index.d.ts +11 -0
- package/dist/core/slo/index.js +15 -0
- package/dist/core/slo/sli-calculator.d.ts +92 -0
- package/dist/core/slo/sli-calculator.js +364 -0
- package/dist/core/slo/slo-tracker.d.ts +148 -0
- package/dist/core/slo/slo-tracker.js +379 -0
- package/dist/core/slo/types.d.ts +281 -0
- package/dist/core/slo/types.js +7 -0
- package/dist/core/slo/vault.d.ts +102 -0
- package/dist/core/slo/vault.js +427 -0
- package/dist/core/tui/index.d.ts +7 -0
- package/dist/core/tui/index.js +6 -0
- package/dist/core/tui/monitor.d.ts +92 -0
- package/dist/core/tui/monitor.js +271 -0
- package/dist/core/tui/renderer.d.ts +33 -0
- package/dist/core/tui/renderer.js +218 -0
- package/dist/core/tui/types.d.ts +63 -0
- package/dist/core/tui/types.js +5 -0
- package/dist/core/types/pack-v2.d.ts +425 -0
- package/dist/core/types/pack-v2.js +8 -0
- package/dist/core/vault/index.d.ts +116 -0
- package/dist/core/vault/index.js +400 -5
- package/dist/core/watch/index.d.ts +7 -0
- package/dist/core/watch/index.js +6 -0
- package/dist/core/watch/watch-mode.d.ts +213 -0
- package/dist/core/watch/watch-mode.js +389 -0
- package/dist/index.js +68 -68
- package/dist/utils/config.d.ts +5 -0
- package/dist/utils/config.js +136 -0
- package/package.json +5 -1
- package/dist/core/adapters/playwright-api.d.ts +0 -82
- package/dist/core/adapters/playwright-api.js +0 -264
|
@@ -0,0 +1,461 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* QA360 Playwright Native API Adapter
|
|
3
|
+
*
|
|
4
|
+
* Uses Playwright's native APIRequestContext for HTTP testing.
|
|
5
|
+
* No browser launch required - lighter and faster than browser-based approach.
|
|
6
|
+
*
|
|
7
|
+
* @see https://playwright.dev/docs/api/class-apirequestcontext
|
|
8
|
+
*
|
|
9
|
+
* Benefits over browser-based approach:
|
|
10
|
+
* - No browser overhead (faster startup, lower memory)
|
|
11
|
+
* - Direct HTTP requests
|
|
12
|
+
* - Same API testing capabilities
|
|
13
|
+
* - Better for CI/CD environments
|
|
14
|
+
*/
|
|
15
|
+
import * as playwright from 'playwright';
|
|
16
|
+
import { SecurityRedactor } from '../security/redactor.js';
|
|
17
|
+
import { ResponseCache, generateCacheKey } from '../cache/lru-cache.js';
|
|
18
|
+
/**
|
|
19
|
+
* Playwright Native API Adapter
|
|
20
|
+
*
|
|
21
|
+
* Uses playwright.request.newContext() for direct HTTP requests
|
|
22
|
+
* without launching a browser.
|
|
23
|
+
*/
|
|
24
|
+
export class PlaywrightNativeApiAdapter {
|
|
25
|
+
requestContext;
|
|
26
|
+
redactor;
|
|
27
|
+
auth;
|
|
28
|
+
cache;
|
|
29
|
+
constructor() {
|
|
30
|
+
this.redactor = SecurityRedactor.forLogs();
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Set authentication credentials for requests
|
|
34
|
+
*/
|
|
35
|
+
setAuth(credentials) {
|
|
36
|
+
this.auth = credentials;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Execute API smoke tests using Playwright's native API
|
|
40
|
+
*/
|
|
41
|
+
async runSmokeTests(config) {
|
|
42
|
+
try {
|
|
43
|
+
this.auth = config.auth;
|
|
44
|
+
// Initialize cache if enabled
|
|
45
|
+
if (config.cache?.enabled) {
|
|
46
|
+
this.cache = new ResponseCache({
|
|
47
|
+
maxSize: config.cache.maxSize ?? 100,
|
|
48
|
+
ttl: config.cache.ttl ?? 300000,
|
|
49
|
+
maxSizeBytes: config.cache.maxSizeBytes ?? 10 * 1024 * 1024,
|
|
50
|
+
verbose: false
|
|
51
|
+
});
|
|
52
|
+
console.log(`📦 Cache enabled: max=${config.cache.maxSize ?? 100}, ttl=${config.cache.ttl ?? 300000}ms`);
|
|
53
|
+
}
|
|
54
|
+
await this.setupRequestContext(config);
|
|
55
|
+
const results = [];
|
|
56
|
+
const smokeTests = config.target.smoke || [`GET ${config.target.baseUrl}/health -> 200`];
|
|
57
|
+
console.log(`🌐 Running API smoke tests with Playwright Native (${smokeTests.length} endpoints)`);
|
|
58
|
+
for (const test of smokeTests) {
|
|
59
|
+
// Check cache first if enabled
|
|
60
|
+
const cacheKey = this.cache ? generateCacheKey({
|
|
61
|
+
method: test.split(' ')[0],
|
|
62
|
+
url: config.target.baseUrl,
|
|
63
|
+
headers: this.auth?.headers
|
|
64
|
+
}) : '';
|
|
65
|
+
let testResult;
|
|
66
|
+
if (this.cache) {
|
|
67
|
+
testResult = await this.cache.getOrFetch(cacheKey, () => this.executeApiTest(test, config));
|
|
68
|
+
const cached = this.cache.has(cacheKey);
|
|
69
|
+
const cacheIndicator = cached ? '📦' : '🌐';
|
|
70
|
+
if (testResult.success) {
|
|
71
|
+
console.log(` ${cacheIndicator} ✅ ${testResult.method} ${testResult.endpoint} -> ${testResult.status} (${testResult.responseTime}ms)`);
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
const errorMsg = testResult.error || 'Request failed';
|
|
75
|
+
const retryInfo = testResult.attempts && testResult.attempts > 1
|
|
76
|
+
? ` (after ${testResult.attempts} attempts)`
|
|
77
|
+
: '';
|
|
78
|
+
console.log(` ${cacheIndicator} ❌ ${testResult.method} ${testResult.endpoint} -> ${errorMsg}${retryInfo}`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
testResult = await this.executeApiTest(test, config);
|
|
83
|
+
if (testResult.success) {
|
|
84
|
+
console.log(` ✅ ${testResult.method} ${testResult.endpoint} -> ${testResult.status} (${testResult.responseTime}ms)`);
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
const errorMsg = testResult.error || 'Request failed';
|
|
88
|
+
const retryInfo = testResult.attempts && testResult.attempts > 1
|
|
89
|
+
? ` (after ${testResult.attempts} attempts)`
|
|
90
|
+
: '';
|
|
91
|
+
console.log(` ❌ ${testResult.method} ${testResult.endpoint} -> ${errorMsg}${retryInfo}`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
results.push(testResult);
|
|
95
|
+
}
|
|
96
|
+
const summary = this.calculateSummary(results);
|
|
97
|
+
const junit = this.generateJUnit(results);
|
|
98
|
+
let error;
|
|
99
|
+
if (summary.failed > 0) {
|
|
100
|
+
const failedTests = results.filter(r => !r.success);
|
|
101
|
+
const errorMessages = failedTests.map(t => t.error).filter(Boolean);
|
|
102
|
+
error = errorMessages.length > 0
|
|
103
|
+
? `${summary.failed} endpoint(s) failed: ${errorMessages[0]}${errorMessages.length > 1 ? ` (and ${errorMessages.length - 1} more)` : ''}`
|
|
104
|
+
: `${summary.failed} endpoint(s) failed`;
|
|
105
|
+
}
|
|
106
|
+
return {
|
|
107
|
+
success: summary.failed === 0,
|
|
108
|
+
results,
|
|
109
|
+
summary,
|
|
110
|
+
junit,
|
|
111
|
+
error
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
finally {
|
|
115
|
+
await this.cleanup();
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Setup Playwright APIRequestContext for HTTP requests
|
|
120
|
+
*/
|
|
121
|
+
async setupRequestContext(config) {
|
|
122
|
+
const baseOptions = {
|
|
123
|
+
userAgent: 'QA360-Native-API/1.0',
|
|
124
|
+
};
|
|
125
|
+
const headers = {
|
|
126
|
+
'Accept': 'application/json, text/plain, */*',
|
|
127
|
+
'Accept-Encoding': 'gzip, deflate, br',
|
|
128
|
+
};
|
|
129
|
+
// Add auth headers if provided
|
|
130
|
+
if (this.auth?.headers) {
|
|
131
|
+
Object.assign(headers, this.auth.headers);
|
|
132
|
+
}
|
|
133
|
+
baseOptions.extraHTTPHeaders = headers;
|
|
134
|
+
// Add storage state if provided
|
|
135
|
+
if (config.storageState) {
|
|
136
|
+
baseOptions.storageState = config.storageState;
|
|
137
|
+
}
|
|
138
|
+
// Set timeout
|
|
139
|
+
if (config.timeout) {
|
|
140
|
+
baseOptions.timeout = config.timeout;
|
|
141
|
+
}
|
|
142
|
+
// Create API request context using Playwright
|
|
143
|
+
this.requestContext = await playwright.request.newContext(baseOptions);
|
|
144
|
+
// Set basic auth if provided in config
|
|
145
|
+
if (config.basicAuth) {
|
|
146
|
+
// Note: setHTTPCredentials may not exist on APIRequestContext
|
|
147
|
+
// Using HTTPCredentials via newContext options instead
|
|
148
|
+
const creds = { username: config.basicAuth.username, password: config.basicAuth.password };
|
|
149
|
+
// Set in context options - basic auth is handled via headers
|
|
150
|
+
const authHeader = 'Basic ' + Buffer.from(`${creds.username}:${creds.password}`).toString('base64');
|
|
151
|
+
headers['Authorization'] = authHeader;
|
|
152
|
+
baseOptions.extraHTTPHeaders = headers;
|
|
153
|
+
// Recreate context with auth headers
|
|
154
|
+
await this.requestContext.dispose();
|
|
155
|
+
this.requestContext = await playwright.request.newContext(baseOptions);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Execute single API test with retry logic using native API
|
|
160
|
+
*/
|
|
161
|
+
async executeApiTest(testSpec, config) {
|
|
162
|
+
const { method, endpoint, expectedStatus } = this.parseTestSpec(testSpec, config.target.baseUrl);
|
|
163
|
+
const maxRetries = config.retries || 0;
|
|
164
|
+
let lastError = '';
|
|
165
|
+
let lastResponse = null;
|
|
166
|
+
let attempts = 0;
|
|
167
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
168
|
+
attempts++;
|
|
169
|
+
const startTime = Date.now();
|
|
170
|
+
try {
|
|
171
|
+
// Build request options
|
|
172
|
+
const requestOptions = {
|
|
173
|
+
method: method,
|
|
174
|
+
timeout: config.timeout || 30000,
|
|
175
|
+
failOnStatusCode: false, // Don't throw on non-2xx responses
|
|
176
|
+
};
|
|
177
|
+
// Add body for POST/PUT/PATCH (from config)
|
|
178
|
+
if (['POST', 'PUT', 'PATCH'].includes(method) && config.body) {
|
|
179
|
+
requestOptions.data = config.body;
|
|
180
|
+
}
|
|
181
|
+
// Make the request using Playwright's native API
|
|
182
|
+
lastResponse = await this.requestContext.fetch(endpoint, requestOptions);
|
|
183
|
+
const responseTime = Date.now() - startTime;
|
|
184
|
+
const status = lastResponse.status();
|
|
185
|
+
const success = status === expectedStatus;
|
|
186
|
+
// Get response headers
|
|
187
|
+
const responseHeaders = lastResponse.headers();
|
|
188
|
+
const headers = {};
|
|
189
|
+
for (const [key, value] of Object.entries(responseHeaders)) {
|
|
190
|
+
headers[key] = value;
|
|
191
|
+
}
|
|
192
|
+
// Get response body safely
|
|
193
|
+
let body;
|
|
194
|
+
try {
|
|
195
|
+
const contentType = headers['content-type'] || '';
|
|
196
|
+
if (contentType.includes('application/json')) {
|
|
197
|
+
body = await lastResponse.json();
|
|
198
|
+
}
|
|
199
|
+
else {
|
|
200
|
+
const text = await lastResponse.text();
|
|
201
|
+
body = text.substring(0, 500); // Limit body size
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
catch {
|
|
205
|
+
body = '[Response body not readable]';
|
|
206
|
+
}
|
|
207
|
+
return {
|
|
208
|
+
endpoint,
|
|
209
|
+
method,
|
|
210
|
+
status,
|
|
211
|
+
responseTime,
|
|
212
|
+
success,
|
|
213
|
+
error: success ? undefined : `Expected status ${expectedStatus}, got ${status}`,
|
|
214
|
+
headers,
|
|
215
|
+
body: this.redactor.redactObject(body),
|
|
216
|
+
attempts
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
catch (error) {
|
|
220
|
+
lastError = error instanceof Error ? error.message : 'Unknown error';
|
|
221
|
+
// Check if this is a retryable error
|
|
222
|
+
const isRetryable = this.isRetryableError(lastError);
|
|
223
|
+
if (isRetryable && attempt < maxRetries) {
|
|
224
|
+
const waitTime = 1000 * Math.pow(2, attempt); // Exponential backoff
|
|
225
|
+
console.log(` 🔄 Retry ${attempt + 1}/${maxRetries} for ${method} ${endpoint} (${lastError}) - waiting ${waitTime}ms`);
|
|
226
|
+
await new Promise(resolve => setTimeout(resolve, waitTime));
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
break;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
// If we get here, all retries failed or request threw an error
|
|
233
|
+
return {
|
|
234
|
+
endpoint,
|
|
235
|
+
method,
|
|
236
|
+
status: lastResponse?.status() || 0,
|
|
237
|
+
responseTime: 0,
|
|
238
|
+
success: false,
|
|
239
|
+
error: this.redactor.redact(lastError),
|
|
240
|
+
attempts
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Parse test specification string
|
|
245
|
+
* Format: "GET /path -> 200" or "POST /api/users -> 201"
|
|
246
|
+
*/
|
|
247
|
+
parseTestSpec(spec, baseUrl) {
|
|
248
|
+
const match = spec.match(/^(\w+)\s+(.+?)\s*->\s*(\d+)$/);
|
|
249
|
+
if (!match) {
|
|
250
|
+
throw new Error(`Invalid test spec format: ${spec}. Expected: "METHOD /path -> STATUS"`);
|
|
251
|
+
}
|
|
252
|
+
const [, method, path, statusStr] = match;
|
|
253
|
+
const expectedStatus = parseInt(statusStr, 10);
|
|
254
|
+
// Build full endpoint URL
|
|
255
|
+
let endpoint = path;
|
|
256
|
+
if (!path.startsWith('http')) {
|
|
257
|
+
endpoint = baseUrl.replace(/\/$/, '') + (path.startsWith('/') ? path : `/${path}`);
|
|
258
|
+
}
|
|
259
|
+
return {
|
|
260
|
+
method: method.toUpperCase(),
|
|
261
|
+
endpoint,
|
|
262
|
+
expectedStatus
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* Check if error is retryable
|
|
267
|
+
*/
|
|
268
|
+
isRetryableError(error) {
|
|
269
|
+
const retryablePatterns = [
|
|
270
|
+
/ECONNRESET/,
|
|
271
|
+
/ETIMEDOUT/,
|
|
272
|
+
/ECONNREFUSED/,
|
|
273
|
+
/502 Bad Gateway/,
|
|
274
|
+
/503 Service Unavailable/,
|
|
275
|
+
/504 Gateway Timeout/,
|
|
276
|
+
/timeout/i,
|
|
277
|
+
/network/i,
|
|
278
|
+
/fetch failed/i,
|
|
279
|
+
/EPROTO/,
|
|
280
|
+
];
|
|
281
|
+
return retryablePatterns.some(pattern => pattern.test(error));
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* Calculate test summary
|
|
285
|
+
*/
|
|
286
|
+
calculateSummary(results) {
|
|
287
|
+
const total = results.length;
|
|
288
|
+
const passed = results.filter(r => r.success).length;
|
|
289
|
+
const failed = total - passed;
|
|
290
|
+
const avgResponseTime = total > 0 ?
|
|
291
|
+
Math.round(results.reduce((sum, r) => sum + r.responseTime, 0) / total) : 0;
|
|
292
|
+
const totalAttempts = results.reduce((sum, r) => sum + (r.attempts || 1), 0);
|
|
293
|
+
return { total, passed, failed, avgResponseTime, totalAttempts };
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* Generate JUnit XML fragment
|
|
297
|
+
*/
|
|
298
|
+
generateJUnit(results) {
|
|
299
|
+
const summary = this.calculateSummary(results);
|
|
300
|
+
const timestamp = new Date().toISOString();
|
|
301
|
+
let junit = `<?xml version="1.0" encoding="UTF-8"?>
|
|
302
|
+
<testsuite name="API Smoke Tests (Playwright Native)" tests="${summary.total}" failures="${summary.failed}" time="${summary.avgResponseTime / 1000}" timestamp="${timestamp}">
|
|
303
|
+
`;
|
|
304
|
+
for (const result of results) {
|
|
305
|
+
const testName = `${result.method} ${result.endpoint}`;
|
|
306
|
+
const time = result.responseTime / 1000;
|
|
307
|
+
junit += ` <testcase name="${this.escapeXml(testName)}" time="${time}">
|
|
308
|
+
`;
|
|
309
|
+
if (!result.success) {
|
|
310
|
+
const failureDetails = {
|
|
311
|
+
error: result.error,
|
|
312
|
+
status: result.status,
|
|
313
|
+
attempts: result.attempts || 1
|
|
314
|
+
};
|
|
315
|
+
junit += ` <failure message="${this.escapeXml(result.error || 'Test failed')}">${this.escapeXml(JSON.stringify(failureDetails, null, 2))}</failure>
|
|
316
|
+
`;
|
|
317
|
+
}
|
|
318
|
+
junit += ` </testcase>
|
|
319
|
+
`;
|
|
320
|
+
}
|
|
321
|
+
junit += `</testsuite>`;
|
|
322
|
+
return junit;
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
* Escape XML special characters
|
|
326
|
+
*/
|
|
327
|
+
escapeXml(str) {
|
|
328
|
+
return str
|
|
329
|
+
.replace(/&/g, '&')
|
|
330
|
+
.replace(/</g, '<')
|
|
331
|
+
.replace(/>/g, '>')
|
|
332
|
+
.replace(/"/g, '"')
|
|
333
|
+
.replace(/'/g, ''');
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* Cleanup request context
|
|
337
|
+
*/
|
|
338
|
+
async cleanup() {
|
|
339
|
+
if (this.requestContext) {
|
|
340
|
+
await this.requestContext.dispose();
|
|
341
|
+
this.requestContext = undefined;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
/**
|
|
345
|
+
* Validate API target configuration
|
|
346
|
+
*/
|
|
347
|
+
static validateConfig(target) {
|
|
348
|
+
const errors = [];
|
|
349
|
+
if (!target.baseUrl) {
|
|
350
|
+
errors.push('API target requires baseUrl');
|
|
351
|
+
}
|
|
352
|
+
else {
|
|
353
|
+
try {
|
|
354
|
+
new URL(target.baseUrl);
|
|
355
|
+
}
|
|
356
|
+
catch {
|
|
357
|
+
errors.push('API target baseUrl must be a valid URL');
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
if (target.smoke) {
|
|
361
|
+
for (const test of target.smoke) {
|
|
362
|
+
if (!/^\w+\s+.+\s*->\s*\d+$/.test(test)) {
|
|
363
|
+
errors.push(`Invalid smoke test format: ${test}. Expected: "METHOD /path -> STATUS"`);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
return {
|
|
368
|
+
valid: errors.length === 0,
|
|
369
|
+
errors
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
/**
|
|
373
|
+
* Health check using native API
|
|
374
|
+
* Quick check if the API is accessible without running full tests
|
|
375
|
+
*/
|
|
376
|
+
async healthCheck(baseUrl, path = '/health', timeout = 5000) {
|
|
377
|
+
try {
|
|
378
|
+
await this.setupRequestContext({
|
|
379
|
+
target: { baseUrl },
|
|
380
|
+
timeout
|
|
381
|
+
});
|
|
382
|
+
const endpoint = baseUrl.replace(/\/$/, '') + (path.startsWith('/') ? path : `/${path}`);
|
|
383
|
+
const response = await this.requestContext.fetch(endpoint, {
|
|
384
|
+
method: 'GET',
|
|
385
|
+
timeout,
|
|
386
|
+
failOnStatusCode: false
|
|
387
|
+
});
|
|
388
|
+
// Consider 2xx and 3xx as healthy
|
|
389
|
+
return response.status() >= 200 && response.status() < 400;
|
|
390
|
+
}
|
|
391
|
+
catch {
|
|
392
|
+
return false;
|
|
393
|
+
}
|
|
394
|
+
finally {
|
|
395
|
+
await this.cleanup();
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
/**
|
|
399
|
+
* Get API info (useful for debugging)
|
|
400
|
+
* Returns version and capabilities
|
|
401
|
+
*/
|
|
402
|
+
static getCapabilities() {
|
|
403
|
+
return {
|
|
404
|
+
adapter: 'playwright-native',
|
|
405
|
+
browserless: true,
|
|
406
|
+
features: [
|
|
407
|
+
'REST API testing',
|
|
408
|
+
'GraphQL support',
|
|
409
|
+
'Basic auth',
|
|
410
|
+
'Bearer token auth',
|
|
411
|
+
'Custom headers',
|
|
412
|
+
'Request/response interception',
|
|
413
|
+
'Retry logic with exponential backoff',
|
|
414
|
+
'Storage state for cookie persistence',
|
|
415
|
+
'Timeout configuration',
|
|
416
|
+
'LRU cache for HTTP responses'
|
|
417
|
+
]
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
/**
|
|
421
|
+
* Get cache statistics if cache is enabled
|
|
422
|
+
*/
|
|
423
|
+
getCacheStats() {
|
|
424
|
+
if (!this.cache) {
|
|
425
|
+
return { enabled: false };
|
|
426
|
+
}
|
|
427
|
+
return {
|
|
428
|
+
enabled: true,
|
|
429
|
+
stats: this.cache.getStats()
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
/**
|
|
433
|
+
* Clear the cache if enabled
|
|
434
|
+
*/
|
|
435
|
+
clearCache() {
|
|
436
|
+
if (this.cache) {
|
|
437
|
+
this.cache.clear();
|
|
438
|
+
console.log('📦 Cache cleared');
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
/**
|
|
443
|
+
* Factory function to create a native API adapter
|
|
444
|
+
*/
|
|
445
|
+
export function createPlaywrightNativeApiAdapter() {
|
|
446
|
+
return new PlaywrightNativeApiAdapter();
|
|
447
|
+
}
|
|
448
|
+
/**
|
|
449
|
+
* Error class for native API adapter
|
|
450
|
+
*/
|
|
451
|
+
export class PlaywrightNativeApiError extends Error {
|
|
452
|
+
code = 'PLAYWRIGHT_NATIVE_API_ERROR';
|
|
453
|
+
endpoint;
|
|
454
|
+
method;
|
|
455
|
+
constructor(message, endpoint, method) {
|
|
456
|
+
super(message);
|
|
457
|
+
this.name = 'PlaywrightNativeApiError';
|
|
458
|
+
this.endpoint = endpoint;
|
|
459
|
+
this.method = method;
|
|
460
|
+
}
|
|
461
|
+
}
|
|
@@ -3,10 +3,12 @@
|
|
|
3
3
|
* UI smoke tests + accessibility via axe-core
|
|
4
4
|
*/
|
|
5
5
|
import { WebTarget, PackBudgets } from '../types/pack-v1.js';
|
|
6
|
+
import { AuthCredentials } from '../auth/index.js';
|
|
6
7
|
export interface UiTestConfig {
|
|
7
8
|
target: WebTarget;
|
|
8
9
|
budgets?: PackBudgets;
|
|
9
10
|
timeout?: number;
|
|
11
|
+
auth?: AuthCredentials;
|
|
10
12
|
login?: {
|
|
11
13
|
url?: string;
|
|
12
14
|
username?: string;
|
|
@@ -59,7 +61,12 @@ export declare class PlaywrightUiAdapter {
|
|
|
59
61
|
private context?;
|
|
60
62
|
private page?;
|
|
61
63
|
private redactor;
|
|
64
|
+
private auth?;
|
|
62
65
|
constructor();
|
|
66
|
+
/**
|
|
67
|
+
* Set authentication credentials for requests
|
|
68
|
+
*/
|
|
69
|
+
setAuth(credentials?: AuthCredentials): void;
|
|
63
70
|
/**
|
|
64
71
|
* Execute UI smoke tests with accessibility
|
|
65
72
|
*/
|
|
@@ -9,14 +9,23 @@ export class PlaywrightUiAdapter {
|
|
|
9
9
|
context;
|
|
10
10
|
page;
|
|
11
11
|
redactor;
|
|
12
|
+
auth;
|
|
12
13
|
constructor() {
|
|
13
14
|
this.redactor = SecurityRedactor.forLogs();
|
|
14
15
|
}
|
|
16
|
+
/**
|
|
17
|
+
* Set authentication credentials for requests
|
|
18
|
+
*/
|
|
19
|
+
setAuth(credentials) {
|
|
20
|
+
this.auth = credentials;
|
|
21
|
+
}
|
|
15
22
|
/**
|
|
16
23
|
* Execute UI smoke tests with accessibility
|
|
17
24
|
*/
|
|
18
25
|
async runSmokeTests(config) {
|
|
19
26
|
try {
|
|
27
|
+
// Store auth config
|
|
28
|
+
this.auth = config.auth;
|
|
20
29
|
await this.setupBrowser();
|
|
21
30
|
const results = [];
|
|
22
31
|
const pages = config.target.pages || [config.target.baseUrl];
|
|
@@ -292,10 +301,29 @@ export class PlaywrightUiAdapter {
|
|
|
292
301
|
headless: true,
|
|
293
302
|
args: ['--no-sandbox', '--disable-dev-shm-usage']
|
|
294
303
|
});
|
|
304
|
+
// Build extra HTTP headers with auth
|
|
305
|
+
const extraHTTPHeaders = {
|
|
306
|
+
'User-Agent': 'QA360-UI-Smoke/1.0'
|
|
307
|
+
};
|
|
308
|
+
if (this.auth?.headers) {
|
|
309
|
+
Object.assign(extraHTTPHeaders, this.auth.headers);
|
|
310
|
+
}
|
|
295
311
|
this.context = await this.browser.newContext({
|
|
296
312
|
viewport: { width: 1280, height: 720 },
|
|
297
|
-
userAgent: 'QA360-UI-Smoke/1.0'
|
|
313
|
+
userAgent: 'QA360-UI-Smoke/1.0',
|
|
314
|
+
extraHTTPHeaders
|
|
298
315
|
});
|
|
316
|
+
// Add cookies from auth credentials after context creation
|
|
317
|
+
if (this.auth?.cookies && this.auth.cookies.length > 0) {
|
|
318
|
+
await this.context.addCookies(this.auth.cookies.map(c => ({
|
|
319
|
+
name: c.name,
|
|
320
|
+
value: c.value,
|
|
321
|
+
domain: c.domain || '',
|
|
322
|
+
path: c.path || '/',
|
|
323
|
+
httpOnly: c.httpOnly || false,
|
|
324
|
+
secure: c.secure || false
|
|
325
|
+
})));
|
|
326
|
+
}
|
|
299
327
|
this.page = await this.context.newPage();
|
|
300
328
|
}
|
|
301
329
|
/**
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Anthropic LLM Provider
|
|
3
|
+
*
|
|
4
|
+
* Provider for Anthropic API (https://api.anthropic.com).
|
|
5
|
+
* Supports Claude 3.5 Sonnet, Claude 3 Opus, and Claude 3 Haiku.
|
|
6
|
+
*
|
|
7
|
+
* Environment variables:
|
|
8
|
+
* - ANTHROPIC_API_KEY: Required API key
|
|
9
|
+
* - ANTHROPIC_BASE_URL: Optional base URL (default: https://api.anthropic.com)
|
|
10
|
+
* - ANTHROPIC_MODEL: Model to use (default: claude-3-5-sonnet-20241022)
|
|
11
|
+
*/
|
|
12
|
+
import type { LLMProvider, GenerationRequest, GenerationResponse } from './index.js';
|
|
13
|
+
export interface AnthropicConfig {
|
|
14
|
+
apiKey?: string;
|
|
15
|
+
baseURL?: string;
|
|
16
|
+
model?: string;
|
|
17
|
+
timeout?: number;
|
|
18
|
+
}
|
|
19
|
+
export declare class AnthropicProvider implements LLMProvider {
|
|
20
|
+
name: string;
|
|
21
|
+
models: string[];
|
|
22
|
+
private readonly apiKey;
|
|
23
|
+
private readonly baseURL;
|
|
24
|
+
private readonly defaultModel;
|
|
25
|
+
private readonly timeout;
|
|
26
|
+
private readonly version;
|
|
27
|
+
constructor(config?: AnthropicConfig);
|
|
28
|
+
isAvailable(): Promise<boolean>;
|
|
29
|
+
generate(request: GenerationRequest): Promise<GenerationResponse>;
|
|
30
|
+
stream(request: GenerationRequest): AsyncIterable<string>;
|
|
31
|
+
countTokens(text: string): number;
|
|
32
|
+
private buildMessages;
|
|
33
|
+
private extractContent;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Anthropic-specific error
|
|
37
|
+
*/
|
|
38
|
+
export declare class AnthropicError extends Error {
|
|
39
|
+
code: string;
|
|
40
|
+
details?: Record<string, unknown>;
|
|
41
|
+
constructor(message: string, details?: Record<string, unknown>);
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Create Anthropic provider with default settings
|
|
45
|
+
*/
|
|
46
|
+
export declare function createAnthropicProvider(config?: AnthropicConfig): AnthropicProvider;
|
|
47
|
+
/**
|
|
48
|
+
* Check if Anthropic is available and configured
|
|
49
|
+
*/
|
|
50
|
+
export declare function checkAnthropicSetup(): Promise<boolean>;
|