@fasttest-ai/qa-agent 0.4.3 → 1.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 +4 -30
- package/bin/qa-agent.js +4 -0
- package/dist/cli.js +33 -194
- package/dist/index.js +64 -1906
- package/dist/install.js +39 -570
- package/package.json +5 -2
- package/dist/actions.d.ts +0 -41
- package/dist/actions.js +0 -224
- package/dist/actions.js.map +0 -1
- package/dist/browser.d.ts +0 -77
- package/dist/browser.js +0 -312
- package/dist/browser.js.map +0 -1
- package/dist/cli.d.ts +0 -19
- package/dist/cli.js.map +0 -1
- package/dist/cloud.d.ts +0 -302
- package/dist/cloud.js +0 -261
- package/dist/cloud.js.map +0 -1
- package/dist/config.d.ts +0 -21
- package/dist/config.js +0 -49
- package/dist/config.js.map +0 -1
- package/dist/healer.d.ts +0 -32
- package/dist/healer.js +0 -316
- package/dist/healer.js.map +0 -1
- package/dist/index.d.ts +0 -13
- package/dist/index.js.map +0 -1
- package/dist/install.d.ts +0 -11
- package/dist/install.js.map +0 -1
- package/dist/runner.d.ts +0 -90
- package/dist/runner.js +0 -700
- package/dist/runner.js.map +0 -1
- package/dist/variables.d.ts +0 -30
- package/dist/variables.js +0 -104
- package/dist/variables.js.map +0 -1
package/dist/actions.d.ts
DELETED
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Browser action executor — navigate, click, fill, screenshot, etc.
|
|
3
|
-
*/
|
|
4
|
-
import type { Page } from "playwright";
|
|
5
|
-
export interface ActionResult {
|
|
6
|
-
success: boolean;
|
|
7
|
-
error?: string;
|
|
8
|
-
data?: Record<string, unknown>;
|
|
9
|
-
}
|
|
10
|
-
export declare function navigate(page: Page, url: string): Promise<ActionResult>;
|
|
11
|
-
export declare function click(page: Page, selector: string): Promise<ActionResult>;
|
|
12
|
-
export declare function fill(page: Page, selector: string, value: string): Promise<ActionResult>;
|
|
13
|
-
export declare function hover(page: Page, selector: string): Promise<ActionResult>;
|
|
14
|
-
export declare function selectOption(page: Page, selector: string, value: string): Promise<ActionResult>;
|
|
15
|
-
export declare function waitFor(page: Page, selector: string, timeoutMs?: number): Promise<ActionResult>;
|
|
16
|
-
export declare function screenshot(page: Page, fullPage?: boolean): Promise<string>;
|
|
17
|
-
export declare function getSnapshot(page: Page): Promise<Record<string, unknown>>;
|
|
18
|
-
export declare function goBack(page: Page): Promise<ActionResult>;
|
|
19
|
-
export declare function goForward(page: Page): Promise<ActionResult>;
|
|
20
|
-
export declare function pressKey(page: Page, key: string): Promise<ActionResult>;
|
|
21
|
-
export declare function uploadFile(page: Page, selector: string, filePaths: string[]): Promise<ActionResult>;
|
|
22
|
-
export declare function evaluate(page: Page, expression: string): Promise<ActionResult>;
|
|
23
|
-
export declare function drag(page: Page, sourceSelector: string, targetSelector: string): Promise<ActionResult>;
|
|
24
|
-
export declare function resize(page: Page, width: number, height: number): Promise<ActionResult>;
|
|
25
|
-
export declare function fillForm(page: Page, fields: Record<string, string>): Promise<ActionResult>;
|
|
26
|
-
export type AssertionType = "element_visible" | "element_hidden" | "text_contains" | "text_equals" | "url_contains" | "url_equals" | "element_count" | "attribute_value";
|
|
27
|
-
export interface AssertionParams {
|
|
28
|
-
type: AssertionType;
|
|
29
|
-
selector?: string;
|
|
30
|
-
text?: string;
|
|
31
|
-
url?: string;
|
|
32
|
-
count?: number;
|
|
33
|
-
attribute?: string;
|
|
34
|
-
value?: string;
|
|
35
|
-
}
|
|
36
|
-
export interface AssertionResult {
|
|
37
|
-
pass: boolean;
|
|
38
|
-
actual?: string | number | boolean;
|
|
39
|
-
error?: string;
|
|
40
|
-
}
|
|
41
|
-
export declare function assertPage(page: Page, params: AssertionParams): Promise<AssertionResult>;
|
package/dist/actions.js
DELETED
|
@@ -1,224 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Browser action executor — navigate, click, fill, screenshot, etc.
|
|
3
|
-
*/
|
|
4
|
-
export async function navigate(page, url) {
|
|
5
|
-
try {
|
|
6
|
-
await page.goto(url, { waitUntil: "domcontentloaded", timeout: 30_000 });
|
|
7
|
-
// Brief settle time for JS frameworks to hydrate
|
|
8
|
-
await page.waitForLoadState("networkidle", { timeout: 5_000 }).catch(() => { });
|
|
9
|
-
return {
|
|
10
|
-
success: true,
|
|
11
|
-
data: {
|
|
12
|
-
title: await page.title(),
|
|
13
|
-
url: page.url(),
|
|
14
|
-
},
|
|
15
|
-
};
|
|
16
|
-
}
|
|
17
|
-
catch (err) {
|
|
18
|
-
return { success: false, error: String(err) };
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
export async function click(page, selector) {
|
|
22
|
-
try {
|
|
23
|
-
await page.click(selector, { timeout: 10_000 });
|
|
24
|
-
await page.waitForLoadState("networkidle", { timeout: 10_000 }).catch(() => { });
|
|
25
|
-
return { success: true };
|
|
26
|
-
}
|
|
27
|
-
catch (err) {
|
|
28
|
-
return { success: false, error: String(err) };
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
export async function fill(page, selector, value) {
|
|
32
|
-
try {
|
|
33
|
-
await page.fill(selector, value, { timeout: 10_000 });
|
|
34
|
-
return { success: true };
|
|
35
|
-
}
|
|
36
|
-
catch (err) {
|
|
37
|
-
return { success: false, error: String(err) };
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
export async function hover(page, selector) {
|
|
41
|
-
try {
|
|
42
|
-
await page.hover(selector, { timeout: 10_000 });
|
|
43
|
-
return { success: true };
|
|
44
|
-
}
|
|
45
|
-
catch (err) {
|
|
46
|
-
return { success: false, error: String(err) };
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
export async function selectOption(page, selector, value) {
|
|
50
|
-
try {
|
|
51
|
-
await page.selectOption(selector, value, { timeout: 10_000 });
|
|
52
|
-
return { success: true };
|
|
53
|
-
}
|
|
54
|
-
catch (err) {
|
|
55
|
-
return { success: false, error: String(err) };
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
export async function waitFor(page, selector, timeoutMs = 10_000) {
|
|
59
|
-
try {
|
|
60
|
-
await page.waitForSelector(selector, { timeout: timeoutMs });
|
|
61
|
-
return { success: true };
|
|
62
|
-
}
|
|
63
|
-
catch (err) {
|
|
64
|
-
return { success: false, error: String(err) };
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
export async function screenshot(page, fullPage = false) {
|
|
68
|
-
const buffer = await page.screenshot({
|
|
69
|
-
type: "jpeg",
|
|
70
|
-
quality: 80,
|
|
71
|
-
fullPage,
|
|
72
|
-
});
|
|
73
|
-
return buffer.toString("base64");
|
|
74
|
-
}
|
|
75
|
-
export async function getSnapshot(page) {
|
|
76
|
-
// Use Playwright's built-in aria snapshot for accessibility tree
|
|
77
|
-
const ariaSnapshot = await page.locator("body").ariaSnapshot().catch(() => "");
|
|
78
|
-
return {
|
|
79
|
-
url: page.url(),
|
|
80
|
-
title: await page.title(),
|
|
81
|
-
accessibilityTree: ariaSnapshot,
|
|
82
|
-
};
|
|
83
|
-
}
|
|
84
|
-
export async function goBack(page) {
|
|
85
|
-
try {
|
|
86
|
-
const response = await page.goBack({ waitUntil: "domcontentloaded", timeout: 30_000 });
|
|
87
|
-
if (response === null) {
|
|
88
|
-
return { success: false, error: "No previous page in history" };
|
|
89
|
-
}
|
|
90
|
-
return { success: true, data: { title: await page.title(), url: page.url() } };
|
|
91
|
-
}
|
|
92
|
-
catch (err) {
|
|
93
|
-
return { success: false, error: String(err) };
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
export async function goForward(page) {
|
|
97
|
-
try {
|
|
98
|
-
const response = await page.goForward({ waitUntil: "domcontentloaded", timeout: 30_000 });
|
|
99
|
-
if (response === null) {
|
|
100
|
-
return { success: false, error: "No next page in history" };
|
|
101
|
-
}
|
|
102
|
-
return { success: true, data: { title: await page.title(), url: page.url() } };
|
|
103
|
-
}
|
|
104
|
-
catch (err) {
|
|
105
|
-
return { success: false, error: String(err) };
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
export async function pressKey(page, key) {
|
|
109
|
-
try {
|
|
110
|
-
await page.keyboard.press(key);
|
|
111
|
-
return { success: true };
|
|
112
|
-
}
|
|
113
|
-
catch (err) {
|
|
114
|
-
return { success: false, error: String(err) };
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
export async function uploadFile(page, selector, filePaths) {
|
|
118
|
-
try {
|
|
119
|
-
await page.setInputFiles(selector, filePaths, { timeout: 10_000 });
|
|
120
|
-
return { success: true };
|
|
121
|
-
}
|
|
122
|
-
catch (err) {
|
|
123
|
-
return { success: false, error: String(err) };
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
export async function evaluate(page, expression) {
|
|
127
|
-
try {
|
|
128
|
-
const result = await page.evaluate(expression);
|
|
129
|
-
return { success: true, data: { result } };
|
|
130
|
-
}
|
|
131
|
-
catch (err) {
|
|
132
|
-
return { success: false, error: String(err) };
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
export async function drag(page, sourceSelector, targetSelector) {
|
|
136
|
-
try {
|
|
137
|
-
await page.dragAndDrop(sourceSelector, targetSelector, { timeout: 10_000 });
|
|
138
|
-
// Wait for network settle after drag (may trigger state changes)
|
|
139
|
-
await page.waitForLoadState("networkidle", { timeout: 5_000 }).catch(() => { });
|
|
140
|
-
return { success: true };
|
|
141
|
-
}
|
|
142
|
-
catch (err) {
|
|
143
|
-
return { success: false, error: String(err) };
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
export async function resize(page, width, height) {
|
|
147
|
-
try {
|
|
148
|
-
await page.setViewportSize({ width, height });
|
|
149
|
-
return { success: true, data: { width, height } };
|
|
150
|
-
}
|
|
151
|
-
catch (err) {
|
|
152
|
-
return { success: false, error: String(err) };
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
export async function fillForm(page, fields) {
|
|
156
|
-
try {
|
|
157
|
-
for (const [selector, value] of Object.entries(fields)) {
|
|
158
|
-
await page.fill(selector, value, { timeout: 10_000 });
|
|
159
|
-
}
|
|
160
|
-
return { success: true, data: { filled: Object.keys(fields).length } };
|
|
161
|
-
}
|
|
162
|
-
catch (err) {
|
|
163
|
-
return { success: false, error: String(err) };
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
export async function assertPage(page, params) {
|
|
167
|
-
try {
|
|
168
|
-
switch (params.type) {
|
|
169
|
-
case "element_visible": {
|
|
170
|
-
const visible = await page.isVisible(params.selector, { timeout: 5000 });
|
|
171
|
-
return { pass: visible, actual: visible };
|
|
172
|
-
}
|
|
173
|
-
case "element_hidden": {
|
|
174
|
-
const visible = await page.isVisible(params.selector);
|
|
175
|
-
return { pass: !visible, actual: !visible };
|
|
176
|
-
}
|
|
177
|
-
case "text_contains": {
|
|
178
|
-
const loc = page.locator(params.selector);
|
|
179
|
-
const count = await loc.count();
|
|
180
|
-
if (count === 0)
|
|
181
|
-
return { pass: false, error: "Element not found" };
|
|
182
|
-
const text = await loc.first().textContent();
|
|
183
|
-
const contains = text?.includes(params.text ?? "") ?? false;
|
|
184
|
-
return { pass: contains, actual: text ?? "" };
|
|
185
|
-
}
|
|
186
|
-
case "text_equals": {
|
|
187
|
-
const loc = page.locator(params.selector);
|
|
188
|
-
const count = await loc.count();
|
|
189
|
-
if (count === 0)
|
|
190
|
-
return { pass: false, error: "Element not found" };
|
|
191
|
-
const text = (await loc.first().textContent())?.trim() ?? "";
|
|
192
|
-
return { pass: text === params.text, actual: text };
|
|
193
|
-
}
|
|
194
|
-
case "url_contains": {
|
|
195
|
-
const currentUrl = page.url();
|
|
196
|
-
const expected = params.url ?? params.text ?? "";
|
|
197
|
-
return { pass: currentUrl.includes(expected), actual: currentUrl };
|
|
198
|
-
}
|
|
199
|
-
case "url_equals": {
|
|
200
|
-
const currentUrl = page.url();
|
|
201
|
-
return { pass: currentUrl === params.url, actual: currentUrl };
|
|
202
|
-
}
|
|
203
|
-
case "element_count": {
|
|
204
|
-
const loc = page.locator(params.selector);
|
|
205
|
-
const elCount = await loc.count();
|
|
206
|
-
return { pass: elCount === (params.count ?? 1), actual: elCount };
|
|
207
|
-
}
|
|
208
|
-
case "attribute_value": {
|
|
209
|
-
const loc = page.locator(params.selector);
|
|
210
|
-
const elCount = await loc.count();
|
|
211
|
-
if (elCount === 0)
|
|
212
|
-
return { pass: false, error: "Element not found" };
|
|
213
|
-
const attrVal = await loc.first().getAttribute(params.attribute ?? "");
|
|
214
|
-
return { pass: attrVal === params.value, actual: attrVal ?? "" };
|
|
215
|
-
}
|
|
216
|
-
default:
|
|
217
|
-
return { pass: false, error: `Unknown assertion type: ${params.type}` };
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
catch (err) {
|
|
221
|
-
return { pass: false, error: String(err) };
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
//# sourceMappingURL=actions.js.map
|
package/dist/actions.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"actions.js","sourceRoot":"","sources":["../src/actions.ts"],"names":[],"mappings":"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"}
|
package/dist/browser.d.ts
DELETED
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Playwright browser manager — launch, reuse, and close browser instances.
|
|
3
|
-
* Manages browser contexts and pages with session persistence.
|
|
4
|
-
*/
|
|
5
|
-
import { type Page, type Dialog } from "playwright";
|
|
6
|
-
export type BrowserType = "chromium" | "firefox" | "webkit";
|
|
7
|
-
export interface HarEntry {
|
|
8
|
-
url: string;
|
|
9
|
-
method: string;
|
|
10
|
-
status: number;
|
|
11
|
-
duration: number;
|
|
12
|
-
mimeType: string;
|
|
13
|
-
responseSize: number;
|
|
14
|
-
}
|
|
15
|
-
export interface BrowserManagerOptions {
|
|
16
|
-
browserType?: BrowserType;
|
|
17
|
-
headless?: boolean;
|
|
18
|
-
orgSlug?: string;
|
|
19
|
-
device?: string;
|
|
20
|
-
}
|
|
21
|
-
/** Sanitize a name to prevent path traversal (used for session names and org slugs). */
|
|
22
|
-
export declare function sanitizePath(name: string): string;
|
|
23
|
-
export interface PendingDialog {
|
|
24
|
-
type: string;
|
|
25
|
-
message: string;
|
|
26
|
-
defaultValue?: string;
|
|
27
|
-
dialog: Dialog;
|
|
28
|
-
dismissTimer: ReturnType<typeof setTimeout>;
|
|
29
|
-
}
|
|
30
|
-
export declare class BrowserManager {
|
|
31
|
-
private browser;
|
|
32
|
-
private context;
|
|
33
|
-
private page;
|
|
34
|
-
private browserType;
|
|
35
|
-
private headless;
|
|
36
|
-
private orgSlug;
|
|
37
|
-
private deviceName;
|
|
38
|
-
private pendingDialog;
|
|
39
|
-
private networkEntries;
|
|
40
|
-
constructor(options?: BrowserManagerOptions);
|
|
41
|
-
/** Set (or clear) the device to emulate. Closes current context so the next one picks up new config. */
|
|
42
|
-
setDevice(device: string | undefined): Promise<void>;
|
|
43
|
-
/** Build context options based on current device setting. */
|
|
44
|
-
private getContextOptions;
|
|
45
|
-
ensureBrowser(): Promise<Page>;
|
|
46
|
-
getPage(): Promise<Page>;
|
|
47
|
-
newContext(): Promise<Page>;
|
|
48
|
-
saveSession(name: string): Promise<string>;
|
|
49
|
-
restoreSession(name: string): Promise<Page>;
|
|
50
|
-
private attachDialogListener;
|
|
51
|
-
handleDialog(action: "accept" | "dismiss", promptText?: string): Promise<{
|
|
52
|
-
type: string;
|
|
53
|
-
message: string;
|
|
54
|
-
}>;
|
|
55
|
-
private attachNetworkListener;
|
|
56
|
-
getNetworkSummary(): HarEntry[];
|
|
57
|
-
clearNetworkEntries(): void;
|
|
58
|
-
/** List all open pages (tabs) in the current context. */
|
|
59
|
-
listPages(): {
|
|
60
|
-
index: number;
|
|
61
|
-
url: string;
|
|
62
|
-
title: string;
|
|
63
|
-
}[];
|
|
64
|
-
/** List all open pages with titles (async). */
|
|
65
|
-
listPagesAsync(): Promise<{
|
|
66
|
-
index: number;
|
|
67
|
-
url: string;
|
|
68
|
-
title: string;
|
|
69
|
-
}[]>;
|
|
70
|
-
/** Create a new tab and optionally navigate to a URL. */
|
|
71
|
-
createPage(url?: string): Promise<Page>;
|
|
72
|
-
/** Switch to a tab by index. */
|
|
73
|
-
switchToPage(index: number): Promise<Page>;
|
|
74
|
-
/** Close a tab by index. Falls back to another tab if available. */
|
|
75
|
-
closePage(index: number): Promise<void>;
|
|
76
|
-
close(): Promise<void>;
|
|
77
|
-
}
|
package/dist/browser.js
DELETED
|
@@ -1,312 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Playwright browser manager — launch, reuse, and close browser instances.
|
|
3
|
-
* Manages browser contexts and pages with session persistence.
|
|
4
|
-
*/
|
|
5
|
-
import { chromium, firefox, webkit, devices } from "playwright";
|
|
6
|
-
import { execFileSync } from "node:child_process";
|
|
7
|
-
import * as fs from "node:fs";
|
|
8
|
-
import * as path from "node:path";
|
|
9
|
-
import * as os from "node:os";
|
|
10
|
-
const SESSION_DIR = path.join(os.homedir(), ".fasttest", "sessions");
|
|
11
|
-
/** Windows reserved device names that cannot be used as file/directory names. */
|
|
12
|
-
const WINDOWS_RESERVED = /^(con|prn|aux|nul|com\d|lpt\d)$/i;
|
|
13
|
-
/** Sanitize a name to prevent path traversal (used for session names and org slugs). */
|
|
14
|
-
export function sanitizePath(name) {
|
|
15
|
-
const cleaned = name.replace(/[\/\\]/g, "_").replace(/\.\./g, "_").replace(/\0/g, "").replace(/^_+|_+$/g, "") || "default";
|
|
16
|
-
// Guard against Windows reserved device names (CON, PRN, AUX, NUL, COM1-9, LPT1-9)
|
|
17
|
-
return WINDOWS_RESERVED.test(cleaned) ? `_${cleaned}` : cleaned;
|
|
18
|
-
}
|
|
19
|
-
export class BrowserManager {
|
|
20
|
-
browser = null;
|
|
21
|
-
context = null;
|
|
22
|
-
page = null;
|
|
23
|
-
browserType;
|
|
24
|
-
headless;
|
|
25
|
-
orgSlug;
|
|
26
|
-
deviceName;
|
|
27
|
-
pendingDialog = null;
|
|
28
|
-
networkEntries = [];
|
|
29
|
-
constructor(options = {}) {
|
|
30
|
-
this.browserType = options.browserType ?? "chromium";
|
|
31
|
-
this.headless = options.headless ?? true;
|
|
32
|
-
this.orgSlug = sanitizePath(options.orgSlug ?? "default");
|
|
33
|
-
this.deviceName = options.device;
|
|
34
|
-
}
|
|
35
|
-
/** Set (or clear) the device to emulate. Closes current context so the next one picks up new config. */
|
|
36
|
-
async setDevice(device) {
|
|
37
|
-
this.deviceName = device;
|
|
38
|
-
if (this.page && !this.page.isClosed()) {
|
|
39
|
-
await this.page.close().catch(() => { });
|
|
40
|
-
}
|
|
41
|
-
if (this.context) {
|
|
42
|
-
await this.context.close().catch(() => { });
|
|
43
|
-
}
|
|
44
|
-
this.page = null;
|
|
45
|
-
this.context = null;
|
|
46
|
-
}
|
|
47
|
-
/** Build context options based on current device setting. */
|
|
48
|
-
getContextOptions(extra) {
|
|
49
|
-
if (this.deviceName) {
|
|
50
|
-
const preset = devices[this.deviceName];
|
|
51
|
-
if (!preset) {
|
|
52
|
-
throw new Error(`Unknown Playwright device "${this.deviceName}". Use a name from Playwright's device registry (e.g. "iPhone 15", "Pixel 7").`);
|
|
53
|
-
}
|
|
54
|
-
return { ...preset, ignoreHTTPSErrors: true, ...extra };
|
|
55
|
-
}
|
|
56
|
-
return { viewport: { width: 1280, height: 720 }, ignoreHTTPSErrors: true, ...extra };
|
|
57
|
-
}
|
|
58
|
-
async ensureBrowser() {
|
|
59
|
-
// Quick health check: verify existing page is actually alive
|
|
60
|
-
if (this.page && !this.page.isClosed()) {
|
|
61
|
-
try {
|
|
62
|
-
await this.page.evaluate("1");
|
|
63
|
-
return this.page;
|
|
64
|
-
}
|
|
65
|
-
catch {
|
|
66
|
-
// Page is dead, fall through to recovery
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
if (!this.browser || !this.browser.isConnected()) {
|
|
70
|
-
// Browser died or never launched — reset all state
|
|
71
|
-
this.context = null;
|
|
72
|
-
this.page = null;
|
|
73
|
-
const launcher = this.browserType === "firefox"
|
|
74
|
-
? firefox
|
|
75
|
-
: this.browserType === "webkit"
|
|
76
|
-
? webkit
|
|
77
|
-
: chromium;
|
|
78
|
-
try {
|
|
79
|
-
this.browser = await launcher.launch({
|
|
80
|
-
headless: this.headless,
|
|
81
|
-
args: this.browserType === "chromium" ? ["--disable-blink-features=AutomationControlled"] : [],
|
|
82
|
-
});
|
|
83
|
-
}
|
|
84
|
-
catch (err) {
|
|
85
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
86
|
-
if (msg.includes("Executable doesn't exist") || msg.includes("browserType.launch")) {
|
|
87
|
-
const npx = process.platform === "win32" ? "npx.cmd" : "npx";
|
|
88
|
-
execFileSync(npx, ["playwright", "install", "--with-deps", this.browserType], {
|
|
89
|
-
stdio: "inherit",
|
|
90
|
-
});
|
|
91
|
-
this.browser = await launcher.launch({
|
|
92
|
-
headless: this.headless,
|
|
93
|
-
args: this.browserType === "chromium" ? ["--disable-blink-features=AutomationControlled"] : [],
|
|
94
|
-
});
|
|
95
|
-
}
|
|
96
|
-
else {
|
|
97
|
-
throw err;
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
if (!this.context) {
|
|
102
|
-
this.context = await this.browser.newContext(this.getContextOptions());
|
|
103
|
-
}
|
|
104
|
-
this.page = await this.context.newPage();
|
|
105
|
-
this.attachDialogListener(this.page);
|
|
106
|
-
this.attachNetworkListener(this.page);
|
|
107
|
-
return this.page;
|
|
108
|
-
}
|
|
109
|
-
async getPage() {
|
|
110
|
-
return this.ensureBrowser();
|
|
111
|
-
}
|
|
112
|
-
async newContext() {
|
|
113
|
-
if (!this.browser || !this.browser.isConnected()) {
|
|
114
|
-
await this.ensureBrowser();
|
|
115
|
-
}
|
|
116
|
-
// Close old context/page if open
|
|
117
|
-
if (this.page && !this.page.isClosed()) {
|
|
118
|
-
await this.page.close().catch(() => { });
|
|
119
|
-
}
|
|
120
|
-
if (this.context) {
|
|
121
|
-
await this.context.close().catch(() => { });
|
|
122
|
-
}
|
|
123
|
-
this.context = await this.browser.newContext(this.getContextOptions());
|
|
124
|
-
this.page = await this.context.newPage();
|
|
125
|
-
this.attachDialogListener(this.page);
|
|
126
|
-
this.attachNetworkListener(this.page);
|
|
127
|
-
return this.page;
|
|
128
|
-
}
|
|
129
|
-
async saveSession(name) {
|
|
130
|
-
if (!this.context) {
|
|
131
|
-
throw new Error("No browser context — nothing to save");
|
|
132
|
-
}
|
|
133
|
-
const safeName = sanitizePath(name);
|
|
134
|
-
const dir = path.join(SESSION_DIR, this.orgSlug);
|
|
135
|
-
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
136
|
-
const filePath = path.join(dir, `${safeName}.json`);
|
|
137
|
-
const state = await this.context.storageState();
|
|
138
|
-
fs.writeFileSync(filePath, JSON.stringify(state, null, 2), { mode: 0o600 });
|
|
139
|
-
return filePath;
|
|
140
|
-
}
|
|
141
|
-
async restoreSession(name) {
|
|
142
|
-
const safeName = sanitizePath(name);
|
|
143
|
-
const filePath = path.join(SESSION_DIR, this.orgSlug, `${safeName}.json`);
|
|
144
|
-
if (!fs.existsSync(filePath)) {
|
|
145
|
-
throw new Error(`Session "${name}" not found at ${filePath}`);
|
|
146
|
-
}
|
|
147
|
-
const state = JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
148
|
-
if (!this.browser || !this.browser.isConnected()) {
|
|
149
|
-
await this.ensureBrowser();
|
|
150
|
-
}
|
|
151
|
-
if (this.page && !this.page.isClosed()) {
|
|
152
|
-
await this.page.close().catch(() => { });
|
|
153
|
-
}
|
|
154
|
-
if (this.context) {
|
|
155
|
-
await this.context.close().catch(() => { });
|
|
156
|
-
}
|
|
157
|
-
this.context = await this.browser.newContext(this.getContextOptions({ storageState: state }));
|
|
158
|
-
this.page = await this.context.newPage();
|
|
159
|
-
this.attachDialogListener(this.page);
|
|
160
|
-
this.attachNetworkListener(this.page);
|
|
161
|
-
return this.page;
|
|
162
|
-
}
|
|
163
|
-
attachDialogListener(page) {
|
|
164
|
-
page.on("dialog", (dialog) => {
|
|
165
|
-
// Clear any previous timer to avoid double-dismiss races
|
|
166
|
-
if (this.pendingDialog) {
|
|
167
|
-
clearTimeout(this.pendingDialog.dismissTimer);
|
|
168
|
-
}
|
|
169
|
-
const dismissTimer = setTimeout(() => {
|
|
170
|
-
if (this.pendingDialog?.dialog === dialog) {
|
|
171
|
-
dialog.dismiss().catch(() => { });
|
|
172
|
-
this.pendingDialog = null;
|
|
173
|
-
}
|
|
174
|
-
}, 30_000);
|
|
175
|
-
this.pendingDialog = {
|
|
176
|
-
type: dialog.type(),
|
|
177
|
-
message: dialog.message(),
|
|
178
|
-
defaultValue: dialog.defaultValue(),
|
|
179
|
-
dialog,
|
|
180
|
-
dismissTimer,
|
|
181
|
-
};
|
|
182
|
-
});
|
|
183
|
-
}
|
|
184
|
-
async handleDialog(action, promptText) {
|
|
185
|
-
const pending = this.pendingDialog;
|
|
186
|
-
if (!pending) {
|
|
187
|
-
throw new Error("No pending dialog to handle");
|
|
188
|
-
}
|
|
189
|
-
clearTimeout(pending.dismissTimer);
|
|
190
|
-
this.pendingDialog = null;
|
|
191
|
-
if (action === "accept") {
|
|
192
|
-
await pending.dialog.accept(promptText);
|
|
193
|
-
}
|
|
194
|
-
else {
|
|
195
|
-
await pending.dialog.dismiss();
|
|
196
|
-
}
|
|
197
|
-
return { type: pending.type, message: pending.message };
|
|
198
|
-
}
|
|
199
|
-
attachNetworkListener(page) {
|
|
200
|
-
page.on("response", (response) => {
|
|
201
|
-
const req = response.request();
|
|
202
|
-
const url = req.url();
|
|
203
|
-
if (!url.startsWith("http"))
|
|
204
|
-
return;
|
|
205
|
-
this.networkEntries.push({
|
|
206
|
-
url,
|
|
207
|
-
method: req.method(),
|
|
208
|
-
status: response.status(),
|
|
209
|
-
duration: 0,
|
|
210
|
-
mimeType: response.headers()["content-type"] ?? "",
|
|
211
|
-
responseSize: parseInt(response.headers()["content-length"] ?? "0", 10),
|
|
212
|
-
});
|
|
213
|
-
});
|
|
214
|
-
}
|
|
215
|
-
getNetworkSummary() {
|
|
216
|
-
return [...this.networkEntries];
|
|
217
|
-
}
|
|
218
|
-
clearNetworkEntries() {
|
|
219
|
-
this.networkEntries = [];
|
|
220
|
-
}
|
|
221
|
-
/** List all open pages (tabs) in the current context. */
|
|
222
|
-
listPages() {
|
|
223
|
-
if (!this.context)
|
|
224
|
-
return [];
|
|
225
|
-
return this.context.pages().map((p, i) => ({
|
|
226
|
-
index: i,
|
|
227
|
-
url: p.url(),
|
|
228
|
-
title: "", // title requires async, populated below
|
|
229
|
-
}));
|
|
230
|
-
}
|
|
231
|
-
/** List all open pages with titles (async). */
|
|
232
|
-
async listPagesAsync() {
|
|
233
|
-
if (!this.context)
|
|
234
|
-
return [];
|
|
235
|
-
const pages = this.context.pages();
|
|
236
|
-
const result = [];
|
|
237
|
-
for (let i = 0; i < pages.length; i++) {
|
|
238
|
-
result.push({
|
|
239
|
-
index: i,
|
|
240
|
-
url: pages[i].url(),
|
|
241
|
-
title: await pages[i].title().catch(() => ""),
|
|
242
|
-
});
|
|
243
|
-
}
|
|
244
|
-
return result;
|
|
245
|
-
}
|
|
246
|
-
/** Create a new tab and optionally navigate to a URL. */
|
|
247
|
-
async createPage(url) {
|
|
248
|
-
if (!this.context) {
|
|
249
|
-
await this.ensureBrowser();
|
|
250
|
-
}
|
|
251
|
-
const newPage = await this.context.newPage();
|
|
252
|
-
this.attachDialogListener(newPage);
|
|
253
|
-
this.attachNetworkListener(newPage);
|
|
254
|
-
if (url) {
|
|
255
|
-
await newPage.goto(url, { waitUntil: "domcontentloaded", timeout: 30_000 });
|
|
256
|
-
}
|
|
257
|
-
this.page = newPage;
|
|
258
|
-
return newPage;
|
|
259
|
-
}
|
|
260
|
-
/** Switch to a tab by index. */
|
|
261
|
-
async switchToPage(index) {
|
|
262
|
-
if (!this.context) {
|
|
263
|
-
throw new Error("No browser context — no tabs to switch to");
|
|
264
|
-
}
|
|
265
|
-
const pages = this.context.pages();
|
|
266
|
-
if (index < 0 || index >= pages.length) {
|
|
267
|
-
throw new Error(`Tab index ${index} out of range (0-${pages.length - 1})`);
|
|
268
|
-
}
|
|
269
|
-
this.page = pages[index];
|
|
270
|
-
await this.page.bringToFront();
|
|
271
|
-
return this.page;
|
|
272
|
-
}
|
|
273
|
-
/** Close a tab by index. Falls back to another tab if available. */
|
|
274
|
-
async closePage(index) {
|
|
275
|
-
if (!this.context) {
|
|
276
|
-
throw new Error("No browser context — no tabs to close");
|
|
277
|
-
}
|
|
278
|
-
const pages = this.context.pages();
|
|
279
|
-
if (index < 0 || index >= pages.length) {
|
|
280
|
-
throw new Error(`Tab index ${index} out of range (0-${pages.length - 1})`);
|
|
281
|
-
}
|
|
282
|
-
const target = pages[index];
|
|
283
|
-
await target.close();
|
|
284
|
-
// Switch to another tab if the closed tab was the active one
|
|
285
|
-
const remaining = this.context.pages();
|
|
286
|
-
if (remaining.length > 0) {
|
|
287
|
-
this.page = remaining[Math.min(index, remaining.length - 1)];
|
|
288
|
-
}
|
|
289
|
-
else {
|
|
290
|
-
this.page = null;
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
async close() {
|
|
294
|
-
if (this.pendingDialog) {
|
|
295
|
-
clearTimeout(this.pendingDialog.dismissTimer);
|
|
296
|
-
this.pendingDialog = null;
|
|
297
|
-
}
|
|
298
|
-
if (this.page && !this.page.isClosed()) {
|
|
299
|
-
await this.page.close().catch(() => { });
|
|
300
|
-
}
|
|
301
|
-
if (this.context) {
|
|
302
|
-
await this.context.close().catch(() => { });
|
|
303
|
-
}
|
|
304
|
-
if (this.browser) {
|
|
305
|
-
await this.browser.close().catch(() => { });
|
|
306
|
-
}
|
|
307
|
-
this.page = null;
|
|
308
|
-
this.context = null;
|
|
309
|
-
this.browser = null;
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
//# sourceMappingURL=browser.js.map
|