@diyor28/qa-core 0.1.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/dist/agent/context/codecs/browser-state.codec.d.ts +33 -0
- package/dist/agent/context/codecs/browser-state.codec.d.ts.map +1 -0
- package/dist/agent/context/codecs/browser-state.codec.js +46 -0
- package/dist/agent/context/codecs/index.d.ts +3 -0
- package/dist/agent/context/codecs/index.d.ts.map +1 -0
- package/dist/agent/context/codecs/index.js +2 -0
- package/dist/agent/context/codecs/qa-scenario.codec.d.ts +21 -0
- package/dist/agent/context/codecs/qa-scenario.codec.d.ts.map +1 -0
- package/dist/agent/context/codecs/qa-scenario.codec.js +44 -0
- package/dist/agent/context/index.d.ts +4 -0
- package/dist/agent/context/index.d.ts.map +1 -0
- package/dist/agent/context/index.js +3 -0
- package/dist/agent/context/qa-context.service.d.ts +24 -0
- package/dist/agent/context/qa-context.service.d.ts.map +1 -0
- package/dist/agent/context/qa-context.service.js +53 -0
- package/dist/agent/context/qa-policy.d.ts +24 -0
- package/dist/agent/context/qa-policy.d.ts.map +1 -0
- package/dist/agent/context/qa-policy.js +47 -0
- package/dist/agent/loop.d.ts +28 -0
- package/dist/agent/loop.d.ts.map +1 -0
- package/dist/agent/loop.js +111 -0
- package/dist/agent/system-prompt.d.ts +2 -0
- package/dist/agent/system-prompt.d.ts.map +1 -0
- package/dist/agent/system-prompt.js +39 -0
- package/dist/agent/tools/index.d.ts +5 -0
- package/dist/agent/tools/index.d.ts.map +1 -0
- package/dist/agent/tools/index.js +152 -0
- package/dist/browser/controller.d.ts +82 -0
- package/dist/browser/controller.d.ts.map +1 -0
- package/dist/browser/controller.js +289 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/dist/report/builder.d.ts +23 -0
- package/dist/report/builder.d.ts.map +1 -0
- package/dist/report/builder.js +69 -0
- package/dist/runner.d.ts +10 -0
- package/dist/runner.d.ts.map +1 -0
- package/dist/runner.js +143 -0
- package/dist/types.d.ts +69 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +1 -0
- package/dist/vision/analyzer.d.ts +8 -0
- package/dist/vision/analyzer.d.ts.map +1 -0
- package/dist/vision/analyzer.js +49 -0
- package/dist/vision/prompts.d.ts +2 -0
- package/dist/vision/prompts.d.ts.map +1 -0
- package/dist/vision/prompts.js +28 -0
- package/package.json +49 -0
- package/src/agent/context/codecs/browser-state.codec.ts +57 -0
- package/src/agent/context/codecs/index.ts +2 -0
- package/src/agent/context/codecs/qa-scenario.codec.ts +55 -0
- package/src/agent/context/index.ts +3 -0
- package/src/agent/context/qa-context.service.ts +90 -0
- package/src/agent/context/qa-policy.ts +54 -0
- package/src/agent/loop.ts +147 -0
- package/src/agent/system-prompt.ts +39 -0
- package/src/agent/tools/index.ts +162 -0
- package/src/browser/controller.ts +321 -0
- package/src/index.ts +19 -0
- package/src/report/builder.ts +94 -0
- package/src/runner.ts +166 -0
- package/src/types.ts +68 -0
- package/src/vision/analyzer.ts +61 -0
- package/src/vision/prompts.ts +28 -0
- package/tsconfig.json +20 -0
- package/tsconfig.tsbuildinfo +1 -0
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export function createAgentTools(browserController, visionAnalyzer) {
|
|
3
|
+
return {
|
|
4
|
+
navigate: {
|
|
5
|
+
description: 'Navigate to a URL',
|
|
6
|
+
parameters: z.object({
|
|
7
|
+
url: z.string().describe('The URL to navigate to'),
|
|
8
|
+
}),
|
|
9
|
+
execute: async ({ url }) => {
|
|
10
|
+
return await browserController.navigate(url);
|
|
11
|
+
},
|
|
12
|
+
},
|
|
13
|
+
click: {
|
|
14
|
+
description: 'Click an element by selector or text. Use role:button, text:Login, or CSS selectors.',
|
|
15
|
+
parameters: z.object({
|
|
16
|
+
selector: z.string().describe('CSS selector, role:type, or text:content to find the element'),
|
|
17
|
+
}),
|
|
18
|
+
execute: async ({ selector }) => {
|
|
19
|
+
return await browserController.click(selector);
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
fill: {
|
|
23
|
+
description: 'Fill an input field with a value',
|
|
24
|
+
parameters: z.object({
|
|
25
|
+
selector: z.string().describe('Selector for the input field'),
|
|
26
|
+
value: z.string().describe('Value to fill'),
|
|
27
|
+
}),
|
|
28
|
+
execute: async ({ selector, value }) => {
|
|
29
|
+
return await browserController.fill(selector, value);
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
type: {
|
|
33
|
+
description: 'Type text into an element with key events (simulates typing)',
|
|
34
|
+
parameters: z.object({
|
|
35
|
+
selector: z.string().describe('Selector for the element'),
|
|
36
|
+
text: z.string().describe('Text to type'),
|
|
37
|
+
}),
|
|
38
|
+
execute: async ({ selector, text }) => {
|
|
39
|
+
return await browserController.type(selector, text);
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
screenshot: {
|
|
43
|
+
description: 'Take a screenshot with a descriptive name',
|
|
44
|
+
parameters: z.object({
|
|
45
|
+
name: z.string().describe('Descriptive name for the screenshot (e.g., "login-page", "after-submit")'),
|
|
46
|
+
context: z.string().optional().describe('What is happening in this screenshot'),
|
|
47
|
+
}),
|
|
48
|
+
execute: async ({ name, context }) => {
|
|
49
|
+
return await browserController.screenshot(name, context);
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
analyze_ui_ux: {
|
|
53
|
+
description: 'Analyze the current or a specific screenshot for UX/UI issues using AI vision. Returns findings about visual overflow, poor contrast, bad UX, and accessibility.',
|
|
54
|
+
parameters: z.object({
|
|
55
|
+
screenshotName: z
|
|
56
|
+
.string()
|
|
57
|
+
.optional()
|
|
58
|
+
.describe('Name of specific screenshot to analyze. If omitted, analyzes the most recent screenshot.'),
|
|
59
|
+
focus: z
|
|
60
|
+
.enum(['visual', 'accessibility', 'ux', 'all'])
|
|
61
|
+
.optional()
|
|
62
|
+
.default('all')
|
|
63
|
+
.describe('What aspect to focus the analysis on'),
|
|
64
|
+
}),
|
|
65
|
+
execute: async ({ screenshotName, focus }) => {
|
|
66
|
+
// Get screenshot from history
|
|
67
|
+
let screenshot = screenshotName
|
|
68
|
+
? browserController.getScreenshotByName(screenshotName)
|
|
69
|
+
: browserController.getLatestScreenshot();
|
|
70
|
+
if (!screenshot) {
|
|
71
|
+
// No screenshot available, take one automatically
|
|
72
|
+
await browserController.screenshot('auto-capture', 'Auto-captured for UX analysis');
|
|
73
|
+
screenshot = browserController.getLatestScreenshot();
|
|
74
|
+
if (!screenshot) {
|
|
75
|
+
return { error: 'Failed to capture screenshot' };
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
// Analyze with Gemini
|
|
79
|
+
const findings = await visionAnalyzer.analyzeScreenshot(screenshot.buffer, screenshot.context, screenshot.url);
|
|
80
|
+
return {
|
|
81
|
+
findings,
|
|
82
|
+
analyzed_screenshot: screenshot.name,
|
|
83
|
+
analyzed_url: screenshot.url,
|
|
84
|
+
};
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
assert_visible: {
|
|
88
|
+
description: 'Assert that an element is visible on the page',
|
|
89
|
+
parameters: z.object({
|
|
90
|
+
selector: z.string().describe('Selector for the element'),
|
|
91
|
+
}),
|
|
92
|
+
execute: async ({ selector }) => {
|
|
93
|
+
return await browserController.assertVisible(selector);
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
assert_text: {
|
|
97
|
+
description: 'Assert that an element contains specific text',
|
|
98
|
+
parameters: z.object({
|
|
99
|
+
selector: z.string().describe('Selector for the element'),
|
|
100
|
+
expected: z.string().describe('Expected text content'),
|
|
101
|
+
}),
|
|
102
|
+
execute: async ({ selector, expected }) => {
|
|
103
|
+
return await browserController.assertText(selector, expected);
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
get_text: {
|
|
107
|
+
description: 'Get text content from an element',
|
|
108
|
+
parameters: z.object({
|
|
109
|
+
selector: z.string().describe('Selector for the element'),
|
|
110
|
+
}),
|
|
111
|
+
execute: async ({ selector }) => {
|
|
112
|
+
return await browserController.getText(selector);
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
wait_for: {
|
|
116
|
+
description: 'Wait for an element to appear on the page',
|
|
117
|
+
parameters: z.object({
|
|
118
|
+
selector: z.string().describe('Selector for the element'),
|
|
119
|
+
timeout: z.number().optional().default(5000).describe('Timeout in milliseconds'),
|
|
120
|
+
}),
|
|
121
|
+
execute: async ({ selector, timeout }) => {
|
|
122
|
+
return await browserController.waitFor(selector, timeout);
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
scroll: {
|
|
126
|
+
description: 'Scroll the page up or down',
|
|
127
|
+
parameters: z.object({
|
|
128
|
+
direction: z.enum(['up', 'down']).describe('Scroll direction'),
|
|
129
|
+
amount: z.number().optional().describe('Scroll amount in pixels (default: 500)'),
|
|
130
|
+
}),
|
|
131
|
+
execute: async ({ direction, amount }) => {
|
|
132
|
+
return await browserController.scroll(direction, amount);
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
evaluate_js: {
|
|
136
|
+
description: 'Execute JavaScript code in the browser context',
|
|
137
|
+
parameters: z.object({
|
|
138
|
+
code: z.string().describe('JavaScript code to execute'),
|
|
139
|
+
}),
|
|
140
|
+
execute: async ({ code }) => {
|
|
141
|
+
return await browserController.evaluateJs(code);
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
accessibility_snapshot: {
|
|
145
|
+
description: 'Capture an accessibility tree snapshot for analysis',
|
|
146
|
+
parameters: z.object({}),
|
|
147
|
+
execute: async () => {
|
|
148
|
+
return await browserController.accessibilitySnapshot();
|
|
149
|
+
},
|
|
150
|
+
},
|
|
151
|
+
};
|
|
152
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import type { ScreenshotEntry } from '../types.js';
|
|
2
|
+
export interface BrowserControllerConfig {
|
|
3
|
+
headless?: boolean;
|
|
4
|
+
viewport: {
|
|
5
|
+
width: number;
|
|
6
|
+
height: number;
|
|
7
|
+
};
|
|
8
|
+
enableTrace?: boolean;
|
|
9
|
+
traceDir?: string;
|
|
10
|
+
}
|
|
11
|
+
export declare class BrowserController {
|
|
12
|
+
private browser;
|
|
13
|
+
private context;
|
|
14
|
+
private page;
|
|
15
|
+
private screenshotHistory;
|
|
16
|
+
private config;
|
|
17
|
+
constructor(config: BrowserControllerConfig);
|
|
18
|
+
launch(): Promise<void>;
|
|
19
|
+
close(): Promise<void>;
|
|
20
|
+
navigate(url: string): Promise<{
|
|
21
|
+
success: boolean;
|
|
22
|
+
message: string;
|
|
23
|
+
}>;
|
|
24
|
+
click(selector: string): Promise<{
|
|
25
|
+
success: boolean;
|
|
26
|
+
message: string;
|
|
27
|
+
}>;
|
|
28
|
+
fill(selector: string, value: string): Promise<{
|
|
29
|
+
success: boolean;
|
|
30
|
+
message: string;
|
|
31
|
+
}>;
|
|
32
|
+
type(selector: string, text: string): Promise<{
|
|
33
|
+
success: boolean;
|
|
34
|
+
message: string;
|
|
35
|
+
}>;
|
|
36
|
+
screenshot(name: string, context?: string): Promise<{
|
|
37
|
+
success: boolean;
|
|
38
|
+
message: string;
|
|
39
|
+
screenshotName: string;
|
|
40
|
+
}>;
|
|
41
|
+
assertVisible(selector: string): Promise<{
|
|
42
|
+
success: boolean;
|
|
43
|
+
message: string;
|
|
44
|
+
}>;
|
|
45
|
+
assertText(selector: string, expected: string): Promise<{
|
|
46
|
+
success: boolean;
|
|
47
|
+
message: string;
|
|
48
|
+
}>;
|
|
49
|
+
waitFor(selector: string, timeout?: number): Promise<{
|
|
50
|
+
success: boolean;
|
|
51
|
+
message: string;
|
|
52
|
+
}>;
|
|
53
|
+
getText(selector: string): Promise<{
|
|
54
|
+
success: boolean;
|
|
55
|
+
message: string;
|
|
56
|
+
text?: string;
|
|
57
|
+
}>;
|
|
58
|
+
scroll(direction: 'up' | 'down', amount?: number): Promise<{
|
|
59
|
+
success: boolean;
|
|
60
|
+
message: string;
|
|
61
|
+
}>;
|
|
62
|
+
evaluateJs(code: string): Promise<{
|
|
63
|
+
success: boolean;
|
|
64
|
+
message: string;
|
|
65
|
+
result?: any;
|
|
66
|
+
}>;
|
|
67
|
+
accessibilitySnapshot(): Promise<{
|
|
68
|
+
success: boolean;
|
|
69
|
+
message: string;
|
|
70
|
+
snapshot?: any;
|
|
71
|
+
}>;
|
|
72
|
+
getLatestScreenshot(): ScreenshotEntry | null;
|
|
73
|
+
getScreenshotByName(name: string): ScreenshotEntry | null;
|
|
74
|
+
getAllScreenshots(): ScreenshotEntry[];
|
|
75
|
+
getCurrentUrl(): string;
|
|
76
|
+
getViewport(): {
|
|
77
|
+
width: number;
|
|
78
|
+
height: number;
|
|
79
|
+
};
|
|
80
|
+
getScreenshotCount(): number;
|
|
81
|
+
}
|
|
82
|
+
//# sourceMappingURL=controller.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"controller.d.ts","sourceRoot":"","sources":["../../src/browser/controller.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
import { chromium } from 'playwright';
|
|
2
|
+
export class BrowserController {
|
|
3
|
+
browser = null;
|
|
4
|
+
context = null;
|
|
5
|
+
page = null;
|
|
6
|
+
screenshotHistory = [];
|
|
7
|
+
config;
|
|
8
|
+
constructor(config) {
|
|
9
|
+
this.config = config;
|
|
10
|
+
}
|
|
11
|
+
async launch() {
|
|
12
|
+
this.browser = await chromium.launch({
|
|
13
|
+
headless: this.config.headless ?? true,
|
|
14
|
+
});
|
|
15
|
+
this.context = await this.browser.newContext({
|
|
16
|
+
viewport: this.config.viewport,
|
|
17
|
+
});
|
|
18
|
+
if (this.config.enableTrace) {
|
|
19
|
+
await this.context.tracing.start({
|
|
20
|
+
screenshots: true,
|
|
21
|
+
snapshots: true,
|
|
22
|
+
sources: true,
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
this.page = await this.context.newPage();
|
|
26
|
+
}
|
|
27
|
+
async close() {
|
|
28
|
+
if (this.config.enableTrace && this.context) {
|
|
29
|
+
const tracePath = `${this.config.traceDir}/trace-${Date.now()}.zip`;
|
|
30
|
+
await this.context.tracing.stop({ path: tracePath });
|
|
31
|
+
}
|
|
32
|
+
await this.context?.close();
|
|
33
|
+
await this.browser?.close();
|
|
34
|
+
}
|
|
35
|
+
async navigate(url) {
|
|
36
|
+
if (!this.page)
|
|
37
|
+
throw new Error('Browser not launched');
|
|
38
|
+
try {
|
|
39
|
+
await this.page.goto(url, { waitUntil: 'domcontentloaded', timeout: 30000 });
|
|
40
|
+
return { success: true, message: `Navigated to ${url}` };
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
return {
|
|
44
|
+
success: false,
|
|
45
|
+
message: `Failed to navigate to ${url}: ${error instanceof Error ? error.message : String(error)}`,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
async click(selector) {
|
|
50
|
+
if (!this.page)
|
|
51
|
+
throw new Error('Browser not launched');
|
|
52
|
+
try {
|
|
53
|
+
// Try intelligent locator strategies
|
|
54
|
+
let element = null;
|
|
55
|
+
// Strategy 1: getByRole
|
|
56
|
+
if (selector.includes('role:')) {
|
|
57
|
+
const role = selector.replace('role:', '');
|
|
58
|
+
element = this.page.getByRole(role);
|
|
59
|
+
}
|
|
60
|
+
// Strategy 2: getByText
|
|
61
|
+
else if (selector.includes('text:')) {
|
|
62
|
+
const text = selector.replace('text:', '');
|
|
63
|
+
element = this.page.getByText(text);
|
|
64
|
+
}
|
|
65
|
+
// Strategy 3: CSS/XPath
|
|
66
|
+
else {
|
|
67
|
+
element = this.page.locator(selector);
|
|
68
|
+
}
|
|
69
|
+
await element.click({ timeout: 10000 });
|
|
70
|
+
return { success: true, message: `Clicked ${selector}` };
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
// Retry with alternative strategies
|
|
74
|
+
try {
|
|
75
|
+
await this.page.locator(selector).first().click({ timeout: 5000 });
|
|
76
|
+
return { success: true, message: `Clicked ${selector} (fallback)` };
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
return {
|
|
80
|
+
success: false,
|
|
81
|
+
message: `Failed to click ${selector}: ${error instanceof Error ? error.message : String(error)}`,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
async fill(selector, value) {
|
|
87
|
+
if (!this.page)
|
|
88
|
+
throw new Error('Browser not launched');
|
|
89
|
+
try {
|
|
90
|
+
await this.page.locator(selector).fill(value, { timeout: 10000 });
|
|
91
|
+
return { success: true, message: `Filled ${selector} with "${value}"` };
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
return {
|
|
95
|
+
success: false,
|
|
96
|
+
message: `Failed to fill ${selector}: ${error instanceof Error ? error.message : String(error)}`,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
async type(selector, text) {
|
|
101
|
+
if (!this.page)
|
|
102
|
+
throw new Error('Browser not launched');
|
|
103
|
+
try {
|
|
104
|
+
await this.page.locator(selector).pressSequentially(text, { timeout: 10000 });
|
|
105
|
+
return { success: true, message: `Typed "${text}" into ${selector}` };
|
|
106
|
+
}
|
|
107
|
+
catch (error) {
|
|
108
|
+
return {
|
|
109
|
+
success: false,
|
|
110
|
+
message: `Failed to type into ${selector}: ${error instanceof Error ? error.message : String(error)}`,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
async screenshot(name, context) {
|
|
115
|
+
if (!this.page)
|
|
116
|
+
throw new Error('Browser not launched');
|
|
117
|
+
try {
|
|
118
|
+
const buffer = await this.page.screenshot({ fullPage: false });
|
|
119
|
+
const url = this.page.url();
|
|
120
|
+
this.screenshotHistory.push({
|
|
121
|
+
timestamp: Date.now(),
|
|
122
|
+
name,
|
|
123
|
+
buffer,
|
|
124
|
+
context: context || `Screenshot: ${name}`,
|
|
125
|
+
url,
|
|
126
|
+
});
|
|
127
|
+
return { success: true, message: `Captured screenshot: ${name}`, screenshotName: name };
|
|
128
|
+
}
|
|
129
|
+
catch (error) {
|
|
130
|
+
return {
|
|
131
|
+
success: false,
|
|
132
|
+
message: `Failed to capture screenshot: ${error instanceof Error ? error.message : String(error)}`,
|
|
133
|
+
screenshotName: name,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
async assertVisible(selector) {
|
|
138
|
+
if (!this.page)
|
|
139
|
+
throw new Error('Browser not launched');
|
|
140
|
+
try {
|
|
141
|
+
const element = this.page.locator(selector);
|
|
142
|
+
await element.waitFor({ state: 'visible', timeout: 10000 });
|
|
143
|
+
const isVisible = await element.isVisible();
|
|
144
|
+
if (isVisible) {
|
|
145
|
+
return { success: true, message: `Element ${selector} is visible` };
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
return { success: false, message: `Element ${selector} is not visible` };
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
catch (error) {
|
|
152
|
+
return {
|
|
153
|
+
success: false,
|
|
154
|
+
message: `Element ${selector} not found or not visible: ${error instanceof Error ? error.message : String(error)}`,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
async assertText(selector, expected) {
|
|
159
|
+
if (!this.page)
|
|
160
|
+
throw new Error('Browser not launched');
|
|
161
|
+
try {
|
|
162
|
+
const element = this.page.locator(selector);
|
|
163
|
+
const actual = await element.textContent();
|
|
164
|
+
if (actual?.includes(expected)) {
|
|
165
|
+
return { success: true, message: `Text "${expected}" found in ${selector}` };
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
return {
|
|
169
|
+
success: false,
|
|
170
|
+
message: `Expected "${expected}" but got "${actual}" in ${selector}`,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
catch (error) {
|
|
175
|
+
return {
|
|
176
|
+
success: false,
|
|
177
|
+
message: `Failed to assert text in ${selector}: ${error instanceof Error ? error.message : String(error)}`,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
async waitFor(selector, timeout = 5000) {
|
|
182
|
+
if (!this.page)
|
|
183
|
+
throw new Error('Browser not launched');
|
|
184
|
+
try {
|
|
185
|
+
await this.page.locator(selector).waitFor({ state: 'visible', timeout });
|
|
186
|
+
return { success: true, message: `Element ${selector} appeared` };
|
|
187
|
+
}
|
|
188
|
+
catch (error) {
|
|
189
|
+
return {
|
|
190
|
+
success: false,
|
|
191
|
+
message: `Element ${selector} did not appear within ${timeout}ms`,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
async getText(selector) {
|
|
196
|
+
if (!this.page)
|
|
197
|
+
throw new Error('Browser not launched');
|
|
198
|
+
try {
|
|
199
|
+
const text = await this.page.locator(selector).textContent();
|
|
200
|
+
return { success: true, message: `Got text from ${selector}`, text: text || '' };
|
|
201
|
+
}
|
|
202
|
+
catch (error) {
|
|
203
|
+
return {
|
|
204
|
+
success: false,
|
|
205
|
+
message: `Failed to get text from ${selector}: ${error instanceof Error ? error.message : String(error)}`,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
async scroll(direction, amount) {
|
|
210
|
+
if (!this.page)
|
|
211
|
+
throw new Error('Browser not launched');
|
|
212
|
+
try {
|
|
213
|
+
const delta = amount || 500;
|
|
214
|
+
const scrollAmount = direction === 'down' ? delta : -delta;
|
|
215
|
+
await this.page.evaluate((scroll) => {
|
|
216
|
+
window.scrollBy(0, scroll);
|
|
217
|
+
}, scrollAmount);
|
|
218
|
+
return { success: true, message: `Scrolled ${direction} by ${Math.abs(scrollAmount)}px` };
|
|
219
|
+
}
|
|
220
|
+
catch (error) {
|
|
221
|
+
return {
|
|
222
|
+
success: false,
|
|
223
|
+
message: `Failed to scroll: ${error instanceof Error ? error.message : String(error)}`,
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
async evaluateJs(code) {
|
|
228
|
+
if (!this.page)
|
|
229
|
+
throw new Error('Browser not launched');
|
|
230
|
+
try {
|
|
231
|
+
const result = await this.page.evaluate((jsCode) => {
|
|
232
|
+
return eval(jsCode);
|
|
233
|
+
}, code);
|
|
234
|
+
return { success: true, message: 'JavaScript executed', result };
|
|
235
|
+
}
|
|
236
|
+
catch (error) {
|
|
237
|
+
return {
|
|
238
|
+
success: false,
|
|
239
|
+
message: `Failed to execute JavaScript: ${error instanceof Error ? error.message : String(error)}`,
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
async accessibilitySnapshot() {
|
|
244
|
+
if (!this.page)
|
|
245
|
+
throw new Error('Browser not launched');
|
|
246
|
+
try {
|
|
247
|
+
// Get ARIA tree via evaluate
|
|
248
|
+
const snapshot = await this.page.evaluate(() => {
|
|
249
|
+
const getAriaTree = (element) => {
|
|
250
|
+
const role = element.getAttribute('role');
|
|
251
|
+
const ariaLabel = element.getAttribute('aria-label');
|
|
252
|
+
const ariaLabelledby = element.getAttribute('aria-labelledby');
|
|
253
|
+
return {
|
|
254
|
+
role: role || element.tagName.toLowerCase(),
|
|
255
|
+
name: ariaLabel || ariaLabelledby || element.textContent?.substring(0, 50),
|
|
256
|
+
children: Array.from(element.children).map((child) => getAriaTree(child)),
|
|
257
|
+
};
|
|
258
|
+
};
|
|
259
|
+
return getAriaTree(document.body);
|
|
260
|
+
});
|
|
261
|
+
return { success: true, message: 'Accessibility snapshot captured', snapshot };
|
|
262
|
+
}
|
|
263
|
+
catch (error) {
|
|
264
|
+
return {
|
|
265
|
+
success: false,
|
|
266
|
+
message: `Failed to capture accessibility snapshot: ${error instanceof Error ? error.message : String(error)}`,
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
// Screenshot history access methods
|
|
271
|
+
getLatestScreenshot() {
|
|
272
|
+
return this.screenshotHistory[this.screenshotHistory.length - 1] || null;
|
|
273
|
+
}
|
|
274
|
+
getScreenshotByName(name) {
|
|
275
|
+
return this.screenshotHistory.find((s) => s.name === name) || null;
|
|
276
|
+
}
|
|
277
|
+
getAllScreenshots() {
|
|
278
|
+
return [...this.screenshotHistory];
|
|
279
|
+
}
|
|
280
|
+
getCurrentUrl() {
|
|
281
|
+
return this.page?.url() || '';
|
|
282
|
+
}
|
|
283
|
+
getViewport() {
|
|
284
|
+
return this.config.viewport;
|
|
285
|
+
}
|
|
286
|
+
getScreenshotCount() {
|
|
287
|
+
return this.screenshotHistory.length;
|
|
288
|
+
}
|
|
289
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { QaRunner } from './runner.js';
|
|
2
|
+
export type { QaScenario, QaFinding, QaArtifact, QaReport, QaProgressEvent, QaRunnerConfig, ScreenshotEntry, } from './types.js';
|
|
3
|
+
export { BrowserController } from './browser/controller.js';
|
|
4
|
+
export { VisionAnalyzer } from './vision/analyzer.js';
|
|
5
|
+
export { AgentLoop } from './agent/loop.js';
|
|
6
|
+
export { ReportBuilder } from './report/builder.js';
|
|
7
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAGvC,YAAY,EACV,UAAU,EACV,SAAS,EACT,UAAU,EACV,QAAQ,EACR,eAAe,EACf,cAAc,EACd,eAAe,GAChB,MAAM,YAAY,CAAC;AAGpB,OAAO,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAC5D,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAC5C,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// Main exports
|
|
2
|
+
export { QaRunner } from './runner.js';
|
|
3
|
+
// Component exports (for advanced usage)
|
|
4
|
+
export { BrowserController } from './browser/controller.js';
|
|
5
|
+
export { VisionAnalyzer } from './vision/analyzer.js';
|
|
6
|
+
export { AgentLoop } from './agent/loop.js';
|
|
7
|
+
export { ReportBuilder } from './report/builder.js';
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { QaReport, QaFinding, QaArtifact } from '../types.js';
|
|
2
|
+
export interface BuildReportOptions {
|
|
3
|
+
findings: QaFinding[];
|
|
4
|
+
artifacts: QaArtifact[];
|
|
5
|
+
metadata: {
|
|
6
|
+
scenarioName: string;
|
|
7
|
+
baseUrl: string;
|
|
8
|
+
viewport: {
|
|
9
|
+
width: number;
|
|
10
|
+
height: number;
|
|
11
|
+
};
|
|
12
|
+
timestamp: string;
|
|
13
|
+
};
|
|
14
|
+
duration: number;
|
|
15
|
+
summary: string;
|
|
16
|
+
}
|
|
17
|
+
export declare class ReportBuilder {
|
|
18
|
+
static buildReport(options: BuildReportOptions): QaReport;
|
|
19
|
+
private static generateSummary;
|
|
20
|
+
static aggregateFindingsByCategory(findings: QaFinding[]): Record<string, QaFinding[]>;
|
|
21
|
+
static aggregateFindingsBySeverity(findings: QaFinding[]): Record<string, QaFinding[]>;
|
|
22
|
+
}
|
|
23
|
+
//# sourceMappingURL=builder.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"builder.d.ts","sourceRoot":"","sources":["../../src/report/builder.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAEnE,MAAM,WAAW,kBAAkB;IACjC,QAAQ,EAAE,SAAS,EAAE,CAAC;IACtB,SAAS,EAAE,UAAU,EAAE,CAAC;IACxB,QAAQ,EAAE;QACR,YAAY,EAAE,MAAM,CAAC;QACrB,OAAO,EAAE,MAAM,CAAC;QAChB,QAAQ,EAAE;YAAE,KAAK,EAAE,MAAM,CAAC;YAAC,MAAM,EAAE,MAAM,CAAA;SAAE,CAAC;QAC5C,SAAS,EAAE,MAAM,CAAC;KACnB,CAAC;IACF,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,qBAAa,aAAa;IACxB,MAAM,CAAC,WAAW,CAAC,OAAO,EAAE,kBAAkB,GAAG,QAAQ;IA4BzD,OAAO,CAAC,MAAM,CAAC,eAAe;IAoB9B,MAAM,CAAC,2BAA2B,CAAC,QAAQ,EAAE,SAAS,EAAE,GAAG,MAAM,CAAC,MAAM,EAAE,SAAS,EAAE,CAAC;IAetF,MAAM,CAAC,2BAA2B,CAAC,QAAQ,EAAE,SAAS,EAAE,GAAG,MAAM,CAAC,MAAM,EAAE,SAAS,EAAE,CAAC;CAcvF"}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
export class ReportBuilder {
|
|
2
|
+
static buildReport(options) {
|
|
3
|
+
const { findings, artifacts, metadata, duration, summary } = options;
|
|
4
|
+
// Determine overall status
|
|
5
|
+
let status = 'passed';
|
|
6
|
+
if (findings.some((f) => f.severity === 'critical' || f.severity === 'high')) {
|
|
7
|
+
status = 'failed';
|
|
8
|
+
}
|
|
9
|
+
else if (findings.length > 0) {
|
|
10
|
+
status = 'issues_found';
|
|
11
|
+
}
|
|
12
|
+
// Generate summary if not provided
|
|
13
|
+
let finalSummary = summary;
|
|
14
|
+
if (!finalSummary) {
|
|
15
|
+
finalSummary = this.generateSummary(findings, status);
|
|
16
|
+
}
|
|
17
|
+
return {
|
|
18
|
+
summary: finalSummary,
|
|
19
|
+
status,
|
|
20
|
+
duration,
|
|
21
|
+
findings,
|
|
22
|
+
artifacts,
|
|
23
|
+
metadata,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
static generateSummary(findings, status) {
|
|
27
|
+
if (status === 'passed') {
|
|
28
|
+
return 'Test completed successfully with no issues found.';
|
|
29
|
+
}
|
|
30
|
+
const critical = findings.filter((f) => f.severity === 'critical').length;
|
|
31
|
+
const high = findings.filter((f) => f.severity === 'high').length;
|
|
32
|
+
const medium = findings.filter((f) => f.severity === 'medium').length;
|
|
33
|
+
const low = findings.filter((f) => f.severity === 'low').length;
|
|
34
|
+
const parts = [`Test completed with ${findings.length} issue(s) found:`];
|
|
35
|
+
if (critical > 0)
|
|
36
|
+
parts.push(`${critical} critical`);
|
|
37
|
+
if (high > 0)
|
|
38
|
+
parts.push(`${high} high`);
|
|
39
|
+
if (medium > 0)
|
|
40
|
+
parts.push(`${medium} medium`);
|
|
41
|
+
if (low > 0)
|
|
42
|
+
parts.push(`${low} low`);
|
|
43
|
+
return parts.join(', ') + '.';
|
|
44
|
+
}
|
|
45
|
+
static aggregateFindingsByCategory(findings) {
|
|
46
|
+
const byCategory = {
|
|
47
|
+
functional: [],
|
|
48
|
+
ux: [],
|
|
49
|
+
ui: [],
|
|
50
|
+
accessibility: [],
|
|
51
|
+
};
|
|
52
|
+
for (const finding of findings) {
|
|
53
|
+
byCategory[finding.category].push(finding);
|
|
54
|
+
}
|
|
55
|
+
return byCategory;
|
|
56
|
+
}
|
|
57
|
+
static aggregateFindingsBySeverity(findings) {
|
|
58
|
+
const bySeverity = {
|
|
59
|
+
critical: [],
|
|
60
|
+
high: [],
|
|
61
|
+
medium: [],
|
|
62
|
+
low: [],
|
|
63
|
+
};
|
|
64
|
+
for (const finding of findings) {
|
|
65
|
+
bySeverity[finding.severity].push(finding);
|
|
66
|
+
}
|
|
67
|
+
return bySeverity;
|
|
68
|
+
}
|
|
69
|
+
}
|
package/dist/runner.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { QaReport, QaRunnerConfig } from './types.js';
|
|
2
|
+
export declare class QaRunner {
|
|
3
|
+
private config;
|
|
4
|
+
private browserController;
|
|
5
|
+
private visionAnalyzer;
|
|
6
|
+
constructor(config: QaRunnerConfig);
|
|
7
|
+
run(): Promise<QaReport>;
|
|
8
|
+
private emitProgress;
|
|
9
|
+
}
|
|
10
|
+
//# sourceMappingURL=runner.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"runner.d.ts","sourceRoot":"","sources":["../src/runner.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,QAAQ,EAAE,cAAc,EAA+B,MAAM,YAAY,CAAC;AAIxF,qBAAa,QAAQ;IACnB,OAAO,CAAC,MAAM,CAAiB;IAC/B,OAAO,CAAC,iBAAiB,CAAkC;IAC3D,OAAO,CAAC,cAAc,CAA+B;gBAEzC,MAAM,EAAE,cAAc;IAI5B,GAAG,IAAI,OAAO,CAAC,QAAQ,CAAC;IA2I9B,OAAO,CAAC,YAAY;CASrB"}
|