qai-cli 3.0.0
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/.claude/mcp-config.json +12 -0
- package/.claude/qa-engineer-prompt.md +194 -0
- package/.eslintrc.json +69 -0
- package/.github/ISSUE_TEMPLATE/bug_report.md +79 -0
- package/.github/ISSUE_TEMPLATE/feature_request.md +50 -0
- package/.github/ISSUE_TEMPLATE/security.md +43 -0
- package/.github/dependabot.yml +51 -0
- package/.github/pull_request_template.md +11 -0
- package/.github/workflows/lint.yml +35 -0
- package/.github/workflows/playwright-qa.yml +223 -0
- package/.github/workflows/qa-engineer.yml +309 -0
- package/.github/workflows/visual-regression.yml +192 -0
- package/.prettierrc.json +10 -0
- package/README.md +111 -0
- package/action.yml +149 -0
- package/docs/BUGS.md +43 -0
- package/docs/app.js +101 -0
- package/docs/index.html +129 -0
- package/docs/style.css +315 -0
- package/examples/workflow-local.yml +22 -0
- package/examples/workflow-with-vercel.yml +40 -0
- package/package.json +83 -0
- package/qa-report-agent.md +30 -0
- package/qa-report-kudos.md +35 -0
- package/scripts/aria-snapshot.js +328 -0
- package/scripts/page-utils.js +357 -0
- package/scripts/visual-regression.cjs +339 -0
- package/src/analyze.js +365 -0
- package/src/capture.js +133 -0
- package/src/index.js +204 -0
- package/src/providers/anthropic.js +59 -0
- package/src/providers/base.js +164 -0
- package/src/providers/gemini.js +42 -0
- package/src/providers/index.js +132 -0
- package/src/providers/ollama.js +49 -0
- package/src/providers/openai.js +54 -0
- package/src/types.d.ts +148 -0
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
const AnthropicProvider = require('./anthropic');
|
|
2
|
+
const OpenAIProvider = require('./openai');
|
|
3
|
+
const GeminiProvider = require('./gemini');
|
|
4
|
+
const OllamaProvider = require('./ollama');
|
|
5
|
+
|
|
6
|
+
const PROVIDERS = {
|
|
7
|
+
anthropic: AnthropicProvider,
|
|
8
|
+
openai: OpenAIProvider,
|
|
9
|
+
codex: OpenAIProvider, // Codex uses OpenAI API
|
|
10
|
+
gemini: GeminiProvider,
|
|
11
|
+
ollama: OllamaProvider,
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Auto-detect provider from environment variables
|
|
16
|
+
* Supports both standard env vars (ANTHROPIC_API_KEY) and GitHub Actions format (INPUT_ANTHROPIC_API_KEY)
|
|
17
|
+
* @returns {{ provider: string, apiKey: string, options: Object } | null}
|
|
18
|
+
*/
|
|
19
|
+
function detectProvider() {
|
|
20
|
+
const env = process.env;
|
|
21
|
+
|
|
22
|
+
// Check for explicit provider + api_key (both formats)
|
|
23
|
+
const provider = env.PROVIDER || env.INPUT_PROVIDER;
|
|
24
|
+
const apiKey = env.API_KEY || env.INPUT_API_KEY;
|
|
25
|
+
if (provider && apiKey) {
|
|
26
|
+
return {
|
|
27
|
+
provider: provider.toLowerCase(),
|
|
28
|
+
apiKey: apiKey,
|
|
29
|
+
options: {},
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Check for provider-specific keys (standard format first, then INPUT_ format)
|
|
34
|
+
const anthropicKey = env.ANTHROPIC_API_KEY || env.INPUT_ANTHROPIC_API_KEY;
|
|
35
|
+
if (anthropicKey) {
|
|
36
|
+
return {
|
|
37
|
+
provider: 'anthropic',
|
|
38
|
+
apiKey: anthropicKey,
|
|
39
|
+
options: {},
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const openaiKey = env.OPENAI_API_KEY || env.INPUT_OPENAI_API_KEY;
|
|
44
|
+
if (openaiKey) {
|
|
45
|
+
return {
|
|
46
|
+
provider: 'openai',
|
|
47
|
+
apiKey: openaiKey,
|
|
48
|
+
options: {},
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const codexKey = env.CODEX_API_KEY || env.INPUT_CODEX_API_KEY;
|
|
53
|
+
if (codexKey) {
|
|
54
|
+
return {
|
|
55
|
+
provider: 'codex',
|
|
56
|
+
apiKey: codexKey,
|
|
57
|
+
options: { model: 'codex-mini-latest' },
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const geminiKey = env.GEMINI_API_KEY || env.INPUT_GEMINI_API_KEY;
|
|
62
|
+
if (geminiKey) {
|
|
63
|
+
return {
|
|
64
|
+
provider: 'gemini',
|
|
65
|
+
apiKey: geminiKey,
|
|
66
|
+
options: {},
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Ollama doesn't need an API key
|
|
71
|
+
if (provider === 'ollama') {
|
|
72
|
+
return {
|
|
73
|
+
provider: 'ollama',
|
|
74
|
+
apiKey: null,
|
|
75
|
+
options: {
|
|
76
|
+
baseUrl: env.OLLAMA_BASE_URL || env.INPUT_OLLAMA_BASE_URL || 'http://localhost:11434',
|
|
77
|
+
model: env.OLLAMA_MODEL || env.INPUT_OLLAMA_MODEL || 'llava',
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Create a provider instance
|
|
87
|
+
* @param {string} providerName - Provider name
|
|
88
|
+
* @param {string} apiKey - API key
|
|
89
|
+
* @param {Object} options - Provider options
|
|
90
|
+
* @returns {BaseProvider}
|
|
91
|
+
*/
|
|
92
|
+
function createProvider(providerName, apiKey, options = {}) {
|
|
93
|
+
const ProviderClass = PROVIDERS[providerName.toLowerCase()];
|
|
94
|
+
|
|
95
|
+
if (!ProviderClass) {
|
|
96
|
+
throw new Error(
|
|
97
|
+
`Unknown provider: ${providerName}. Supported: ${Object.keys(PROVIDERS).join(', ')}`,
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return new ProviderClass(apiKey, options);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Get provider from environment (auto-detect or explicit)
|
|
106
|
+
* @returns {BaseProvider}
|
|
107
|
+
*/
|
|
108
|
+
function getProvider() {
|
|
109
|
+
const detected = detectProvider();
|
|
110
|
+
|
|
111
|
+
if (!detected) {
|
|
112
|
+
throw new Error(
|
|
113
|
+
'No API key provided. Set one of: ' +
|
|
114
|
+
'ANTHROPIC_API_KEY, OPENAI_API_KEY, CODEX_API_KEY, GEMINI_API_KEY, ' +
|
|
115
|
+
'or PROVIDER with API_KEY',
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
console.log(`Using provider: ${detected.provider}`);
|
|
120
|
+
return createProvider(detected.provider, detected.apiKey, detected.options);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
module.exports = {
|
|
124
|
+
PROVIDERS,
|
|
125
|
+
detectProvider,
|
|
126
|
+
createProvider,
|
|
127
|
+
getProvider,
|
|
128
|
+
AnthropicProvider,
|
|
129
|
+
OpenAIProvider,
|
|
130
|
+
GeminiProvider,
|
|
131
|
+
OllamaProvider,
|
|
132
|
+
};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
const BaseProvider = require('./base');
|
|
2
|
+
|
|
3
|
+
class OllamaProvider extends BaseProvider {
|
|
4
|
+
constructor(apiKey, options = {}) {
|
|
5
|
+
super(apiKey, options);
|
|
6
|
+
this.baseUrl = options.baseUrl || 'http://localhost:11434';
|
|
7
|
+
this.model = options.model || 'llava';
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
async analyze(captureData, options = {}) {
|
|
11
|
+
const prompt = this.buildPrompt(captureData, options);
|
|
12
|
+
|
|
13
|
+
// Ollama expects images as base64 strings in the images array
|
|
14
|
+
const images = captureData.screenshots.map((s) => s.buffer.toString('base64'));
|
|
15
|
+
|
|
16
|
+
// Build the full prompt with screenshot descriptions
|
|
17
|
+
let fullPrompt = '';
|
|
18
|
+
for (const screenshot of captureData.screenshots) {
|
|
19
|
+
const { viewport, width, height } = screenshot;
|
|
20
|
+
fullPrompt += `[Screenshot: ${viewport} - ${width}x${height}]\n`;
|
|
21
|
+
}
|
|
22
|
+
fullPrompt += '\n' + prompt;
|
|
23
|
+
|
|
24
|
+
const response = await fetch(`${this.baseUrl}/api/generate`, {
|
|
25
|
+
method: 'POST',
|
|
26
|
+
headers: {
|
|
27
|
+
'Content-Type': 'application/json',
|
|
28
|
+
},
|
|
29
|
+
body: JSON.stringify({
|
|
30
|
+
model: this.model,
|
|
31
|
+
prompt: fullPrompt,
|
|
32
|
+
images,
|
|
33
|
+
stream: false,
|
|
34
|
+
options: {
|
|
35
|
+
temperature: 0.1,
|
|
36
|
+
},
|
|
37
|
+
}),
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
if (!response.ok) {
|
|
41
|
+
throw new Error(`Ollama request failed: ${response.status} ${response.statusText}`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const data = await response.json();
|
|
45
|
+
return this.parseResponse(data.response || '');
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
module.exports = OllamaProvider;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
const OpenAI = require('openai');
|
|
2
|
+
const BaseProvider = require('./base');
|
|
3
|
+
|
|
4
|
+
class OpenAIProvider extends BaseProvider {
|
|
5
|
+
constructor(apiKey, options = {}) {
|
|
6
|
+
super(apiKey, options);
|
|
7
|
+
this.client = new OpenAI({ apiKey });
|
|
8
|
+
this.model = options.model || 'gpt-4o';
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
async analyze(captureData, options = {}) {
|
|
12
|
+
const prompt = this.buildPrompt(captureData, options);
|
|
13
|
+
|
|
14
|
+
// Build content array with images
|
|
15
|
+
const content = [];
|
|
16
|
+
|
|
17
|
+
// Add screenshots as images
|
|
18
|
+
for (const screenshot of captureData.screenshots) {
|
|
19
|
+
content.push({
|
|
20
|
+
type: 'image_url',
|
|
21
|
+
image_url: {
|
|
22
|
+
url: `data:image/png;base64,${screenshot.buffer.toString('base64')}`,
|
|
23
|
+
detail: 'high',
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
content.push({
|
|
27
|
+
type: 'text',
|
|
28
|
+
text: `[Screenshot: ${screenshot.viewport} - ${screenshot.width}x${screenshot.height}]`,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Add the analysis prompt
|
|
33
|
+
content.push({
|
|
34
|
+
type: 'text',
|
|
35
|
+
text: prompt,
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const response = await this.client.chat.completions.create({
|
|
39
|
+
model: this.model,
|
|
40
|
+
max_tokens: 4096,
|
|
41
|
+
messages: [
|
|
42
|
+
{
|
|
43
|
+
role: 'user',
|
|
44
|
+
content,
|
|
45
|
+
},
|
|
46
|
+
],
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const responseText = response.choices[0]?.message?.content || '';
|
|
50
|
+
return this.parseResponse(responseText);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
module.exports = OpenAIProvider;
|
package/src/types.d.ts
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import type { Page, TestInfo } from '@playwright/test';
|
|
2
|
+
|
|
3
|
+
export interface AnalysisOptions {
|
|
4
|
+
/** Viewports to test (default: ['desktop', 'mobile']) */
|
|
5
|
+
viewports?: ('mobile' | 'tablet' | 'desktop')[];
|
|
6
|
+
/** Focus area for analysis */
|
|
7
|
+
focus?: 'all' | 'accessibility' | 'performance' | 'forms' | 'visual';
|
|
8
|
+
/** LLM provider to use */
|
|
9
|
+
provider?: 'anthropic' | 'openai' | 'gemini' | 'ollama' | 'codex';
|
|
10
|
+
/** API key (uses env var if not provided) */
|
|
11
|
+
apiKey?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface Bug {
|
|
15
|
+
/** Bug title */
|
|
16
|
+
title: string;
|
|
17
|
+
/** Detailed description */
|
|
18
|
+
description: string;
|
|
19
|
+
/** Severity level */
|
|
20
|
+
severity: 'critical' | 'high' | 'medium' | 'low';
|
|
21
|
+
/** Bug category */
|
|
22
|
+
category: string;
|
|
23
|
+
/** Viewport where bug was found */
|
|
24
|
+
viewport?: string;
|
|
25
|
+
/** How to fix the bug */
|
|
26
|
+
recommendation?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface Screenshot {
|
|
30
|
+
/** Screenshot name (e.g., "desktop-1920x1080") */
|
|
31
|
+
name: string;
|
|
32
|
+
/** Viewport name */
|
|
33
|
+
viewport: string;
|
|
34
|
+
/** Viewport width */
|
|
35
|
+
width: number;
|
|
36
|
+
/** Viewport height */
|
|
37
|
+
height: number;
|
|
38
|
+
/** Raw screenshot buffer */
|
|
39
|
+
buffer: Buffer;
|
|
40
|
+
/** Base64 encoded screenshot */
|
|
41
|
+
base64: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface NetworkError {
|
|
45
|
+
/** Request URL */
|
|
46
|
+
url: string;
|
|
47
|
+
/** HTTP method */
|
|
48
|
+
method: string;
|
|
49
|
+
/** HTTP status code (for response errors) */
|
|
50
|
+
status?: number;
|
|
51
|
+
/** Failure reason (for request failures) */
|
|
52
|
+
failure?: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface AnalysisReport {
|
|
56
|
+
/** Page URL */
|
|
57
|
+
url: string;
|
|
58
|
+
/** Page title */
|
|
59
|
+
title: string;
|
|
60
|
+
/** ISO timestamp */
|
|
61
|
+
timestamp: string;
|
|
62
|
+
/** Analysis duration */
|
|
63
|
+
duration: string;
|
|
64
|
+
/** QA score (0-100) */
|
|
65
|
+
score: number | null;
|
|
66
|
+
/** AI-generated summary */
|
|
67
|
+
summary: string;
|
|
68
|
+
/** All bugs found */
|
|
69
|
+
bugs: Bug[];
|
|
70
|
+
/** Only critical/high severity bugs (convenience property) */
|
|
71
|
+
criticalBugs: Bug[];
|
|
72
|
+
/** AI recommendations */
|
|
73
|
+
recommendations: string[];
|
|
74
|
+
/** Console errors captured */
|
|
75
|
+
consoleErrors: string[];
|
|
76
|
+
/** Network errors captured */
|
|
77
|
+
networkErrors: NetworkError[];
|
|
78
|
+
/** Screenshots taken */
|
|
79
|
+
screenshots: Screenshot[];
|
|
80
|
+
/** Viewports tested */
|
|
81
|
+
viewports: string[];
|
|
82
|
+
/** Focus area used */
|
|
83
|
+
focus: string;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export interface ViewportConfig {
|
|
87
|
+
width: number;
|
|
88
|
+
height: number;
|
|
89
|
+
name: string;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Analyze a page with AI
|
|
94
|
+
*
|
|
95
|
+
* @example
|
|
96
|
+
* ```typescript
|
|
97
|
+
* import { test, expect } from '@playwright/test';
|
|
98
|
+
* import { analyzeWithAI } from 'qaie';
|
|
99
|
+
*
|
|
100
|
+
* test('AI QA: homepage', async ({ page }) => {
|
|
101
|
+
* await page.goto('/');
|
|
102
|
+
* const report = await analyzeWithAI(page);
|
|
103
|
+
* expect(report.criticalBugs).toHaveLength(0);
|
|
104
|
+
* });
|
|
105
|
+
* ```
|
|
106
|
+
*/
|
|
107
|
+
export function analyzeWithAI(page: Page, options?: AnalysisOptions): Promise<AnalysisReport>;
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Create a configured analyzer with default options
|
|
111
|
+
*
|
|
112
|
+
* @example
|
|
113
|
+
* ```typescript
|
|
114
|
+
* const analyze = createAnalyzer({ viewports: ['desktop', 'mobile', 'tablet'] });
|
|
115
|
+
*
|
|
116
|
+
* test('homepage', async ({ page }) => {
|
|
117
|
+
* await page.goto('/');
|
|
118
|
+
* const report = await analyze(page);
|
|
119
|
+
* });
|
|
120
|
+
* ```
|
|
121
|
+
*/
|
|
122
|
+
export function createAnalyzer(
|
|
123
|
+
defaultOptions?: AnalysisOptions,
|
|
124
|
+
): (page: Page, options?: AnalysisOptions) => Promise<AnalysisReport>;
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Attach all screenshots from a report to the Playwright test
|
|
128
|
+
*
|
|
129
|
+
* @example
|
|
130
|
+
* ```typescript
|
|
131
|
+
* test('AI QA', async ({ page }, testInfo) => {
|
|
132
|
+
* await page.goto('/');
|
|
133
|
+
* const report = await analyzeWithAI(page);
|
|
134
|
+
* await attachScreenshots(testInfo, report);
|
|
135
|
+
* });
|
|
136
|
+
* ```
|
|
137
|
+
*/
|
|
138
|
+
export function attachScreenshots(testInfo: TestInfo, report: AnalysisReport): Promise<void>;
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Attach the bug report as a JSON attachment
|
|
142
|
+
*/
|
|
143
|
+
export function attachBugReport(testInfo: TestInfo, report: AnalysisReport): Promise<void>;
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Viewport configurations
|
|
147
|
+
*/
|
|
148
|
+
export const VIEWPORT_CONFIGS: Record<string, ViewportConfig>;
|