@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.
Files changed (67) hide show
  1. package/dist/agent/context/codecs/browser-state.codec.d.ts +33 -0
  2. package/dist/agent/context/codecs/browser-state.codec.d.ts.map +1 -0
  3. package/dist/agent/context/codecs/browser-state.codec.js +46 -0
  4. package/dist/agent/context/codecs/index.d.ts +3 -0
  5. package/dist/agent/context/codecs/index.d.ts.map +1 -0
  6. package/dist/agent/context/codecs/index.js +2 -0
  7. package/dist/agent/context/codecs/qa-scenario.codec.d.ts +21 -0
  8. package/dist/agent/context/codecs/qa-scenario.codec.d.ts.map +1 -0
  9. package/dist/agent/context/codecs/qa-scenario.codec.js +44 -0
  10. package/dist/agent/context/index.d.ts +4 -0
  11. package/dist/agent/context/index.d.ts.map +1 -0
  12. package/dist/agent/context/index.js +3 -0
  13. package/dist/agent/context/qa-context.service.d.ts +24 -0
  14. package/dist/agent/context/qa-context.service.d.ts.map +1 -0
  15. package/dist/agent/context/qa-context.service.js +53 -0
  16. package/dist/agent/context/qa-policy.d.ts +24 -0
  17. package/dist/agent/context/qa-policy.d.ts.map +1 -0
  18. package/dist/agent/context/qa-policy.js +47 -0
  19. package/dist/agent/loop.d.ts +28 -0
  20. package/dist/agent/loop.d.ts.map +1 -0
  21. package/dist/agent/loop.js +111 -0
  22. package/dist/agent/system-prompt.d.ts +2 -0
  23. package/dist/agent/system-prompt.d.ts.map +1 -0
  24. package/dist/agent/system-prompt.js +39 -0
  25. package/dist/agent/tools/index.d.ts +5 -0
  26. package/dist/agent/tools/index.d.ts.map +1 -0
  27. package/dist/agent/tools/index.js +152 -0
  28. package/dist/browser/controller.d.ts +82 -0
  29. package/dist/browser/controller.d.ts.map +1 -0
  30. package/dist/browser/controller.js +289 -0
  31. package/dist/index.d.ts +7 -0
  32. package/dist/index.d.ts.map +1 -0
  33. package/dist/index.js +7 -0
  34. package/dist/report/builder.d.ts +23 -0
  35. package/dist/report/builder.d.ts.map +1 -0
  36. package/dist/report/builder.js +69 -0
  37. package/dist/runner.d.ts +10 -0
  38. package/dist/runner.d.ts.map +1 -0
  39. package/dist/runner.js +143 -0
  40. package/dist/types.d.ts +69 -0
  41. package/dist/types.d.ts.map +1 -0
  42. package/dist/types.js +1 -0
  43. package/dist/vision/analyzer.d.ts +8 -0
  44. package/dist/vision/analyzer.d.ts.map +1 -0
  45. package/dist/vision/analyzer.js +49 -0
  46. package/dist/vision/prompts.d.ts +2 -0
  47. package/dist/vision/prompts.d.ts.map +1 -0
  48. package/dist/vision/prompts.js +28 -0
  49. package/package.json +49 -0
  50. package/src/agent/context/codecs/browser-state.codec.ts +57 -0
  51. package/src/agent/context/codecs/index.ts +2 -0
  52. package/src/agent/context/codecs/qa-scenario.codec.ts +55 -0
  53. package/src/agent/context/index.ts +3 -0
  54. package/src/agent/context/qa-context.service.ts +90 -0
  55. package/src/agent/context/qa-policy.ts +54 -0
  56. package/src/agent/loop.ts +147 -0
  57. package/src/agent/system-prompt.ts +39 -0
  58. package/src/agent/tools/index.ts +162 -0
  59. package/src/browser/controller.ts +321 -0
  60. package/src/index.ts +19 -0
  61. package/src/report/builder.ts +94 -0
  62. package/src/runner.ts +166 -0
  63. package/src/types.ts +68 -0
  64. package/src/vision/analyzer.ts +61 -0
  65. package/src/vision/prompts.ts +28 -0
  66. package/tsconfig.json +20 -0
  67. 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
+ }
@@ -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
+ }
@@ -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"}