@fasttest-ai/qa-agent 0.4.3 → 1.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/README.md +1 -27
- 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/README.md
CHANGED
|
@@ -109,7 +109,7 @@ Higher-level tools that return structured prompts for your coding agent:
|
|
|
109
109
|
| ----------------- | ------------------------------------------------------------------- |
|
|
110
110
|
| `test` | Test a web app (navigates, snapshots, returns testing instructions) |
|
|
111
111
|
| `explore` | Discover pages, forms, and flows |
|
|
112
|
-
| `chaos` | Adversarial testing
|
|
112
|
+
| `chaos` | Adversarial testing for stability and edge cases |
|
|
113
113
|
| `vibe_shield` | One-command safety net: explore, generate tests, run regressions |
|
|
114
114
|
| `heal` | Fix broken CSS selectors using multiple strategies |
|
|
115
115
|
| `setup` | Authenticate with FastTest cloud (device auth flow) |
|
|
@@ -157,32 +157,6 @@ Use `{{VAR_NAME}}` placeholders in test steps for secrets (passwords, API keys).
|
|
|
157
157
|
--browser <type> chromium | firefox | webkit (default: chromium)
|
|
158
158
|
```
|
|
159
159
|
|
|
160
|
-
## Architecture
|
|
161
|
-
|
|
162
|
-
```
|
|
163
|
-
local-skill/
|
|
164
|
-
src/
|
|
165
|
-
index.ts MCP server — tool registration and routing
|
|
166
|
-
browser.ts Playwright lifecycle, sessions, dialogs
|
|
167
|
-
actions.ts Browser action wrappers (navigate, click, fill, etc.)
|
|
168
|
-
runner.ts Test execution orchestrator
|
|
169
|
-
healer.ts Selector healing cascade
|
|
170
|
-
cloud.ts HTTPS client to FastTest cloud API
|
|
171
|
-
config.ts Config file management (~/.fasttest/)
|
|
172
|
-
variables.ts {{VAR}} placeholder resolution
|
|
173
|
-
cli.ts CI runner (no MCP dependency)
|
|
174
|
-
```
|
|
175
|
-
|
|
176
|
-
Key design principle: **the MCP server never calls an LLM**. Your coding agent drives all browser interactions using its own reasoning. The server provides tools and structured prompts.
|
|
177
|
-
|
|
178
|
-
## Development
|
|
179
|
-
|
|
180
|
-
```bash
|
|
181
|
-
npm install
|
|
182
|
-
npm run build # Compile TypeScript
|
|
183
|
-
npm run dev # Watch mode
|
|
184
|
-
```
|
|
185
|
-
|
|
186
160
|
## License
|
|
187
161
|
|
|
188
162
|
Apache-2.0
|
package/bin/qa-agent.js
CHANGED
|
@@ -3,6 +3,10 @@
|
|
|
3
3
|
const sub = process.argv[2];
|
|
4
4
|
if (sub === "install" || sub === "uninstall") {
|
|
5
5
|
await import("../dist/install.js");
|
|
6
|
+
} else if (sub === "run") {
|
|
7
|
+
// Strip the "run" subcommand so cli.ts sees the flags directly
|
|
8
|
+
process.argv.splice(2, 1);
|
|
9
|
+
await import("../dist/cli.js");
|
|
6
10
|
} else {
|
|
7
11
|
await import("../dist/index.js");
|
|
8
12
|
}
|
package/dist/cli.js
CHANGED
|
@@ -1,195 +1,34 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
*
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
import {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
catch {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
})();
|
|
35
|
-
function parseCliArgs() {
|
|
36
|
-
const args = process.argv.slice(2);
|
|
37
|
-
let apiKey = "";
|
|
38
|
-
let suiteId = "";
|
|
39
|
-
let baseUrl = "https://api.fasttest.ai";
|
|
40
|
-
let appUrl;
|
|
41
|
-
let prUrl;
|
|
42
|
-
let browserType = "chromium";
|
|
43
|
-
let testCaseIds;
|
|
44
|
-
let json = false;
|
|
45
|
-
for (let i = 0; i < args.length; i++) {
|
|
46
|
-
switch (args[i]) {
|
|
47
|
-
case "--api-key":
|
|
48
|
-
apiKey = args[++i] ?? "";
|
|
49
|
-
break;
|
|
50
|
-
case "--suite-id":
|
|
51
|
-
suiteId = args[++i] ?? "";
|
|
52
|
-
break;
|
|
53
|
-
case "--base-url":
|
|
54
|
-
baseUrl = args[++i] ?? baseUrl;
|
|
55
|
-
break;
|
|
56
|
-
case "--app-url":
|
|
57
|
-
appUrl = args[++i];
|
|
58
|
-
break;
|
|
59
|
-
case "--pr-url":
|
|
60
|
-
prUrl = args[++i];
|
|
61
|
-
break;
|
|
62
|
-
case "--browser":
|
|
63
|
-
browserType = (args[++i] ?? "chromium");
|
|
64
|
-
break;
|
|
65
|
-
case "--test-case-ids":
|
|
66
|
-
testCaseIds = (args[++i] ?? "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
67
|
-
break;
|
|
68
|
-
case "--json":
|
|
69
|
-
json = true;
|
|
70
|
-
break;
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
if (!apiKey || !suiteId) {
|
|
74
|
-
console.error("Usage: fasttest-ci --api-key <key> --suite-id <id> [--base-url <url>] " +
|
|
75
|
-
"[--app-url <url>] [--pr-url <url>] [--browser chromium|firefox|webkit] " +
|
|
76
|
-
"[--test-case-ids id1,id2] [--json]");
|
|
77
|
-
process.exit(1);
|
|
78
|
-
}
|
|
79
|
-
return { apiKey, suiteId, baseUrl, appUrl, prUrl, browser: browserType, testCaseIds, json };
|
|
80
|
-
}
|
|
81
|
-
// ---------------------------------------------------------------------------
|
|
82
|
-
// Formatted output
|
|
83
|
-
// ---------------------------------------------------------------------------
|
|
84
|
-
function printFormattedResults(summary) {
|
|
85
|
-
const statusLabel = summary.status === "passed" ? "PASSED" : "FAILED";
|
|
86
|
-
console.log(`--- Results: ${statusLabel} ---`);
|
|
87
|
-
console.log(`Execution: ${summary.execution_id}`);
|
|
88
|
-
console.log(`Total: ${summary.total} | Passed: ${summary.passed} | Failed: ${summary.failed} | Skipped: ${summary.skipped}`);
|
|
89
|
-
console.log(`Duration: ${(summary.duration_ms / 1000).toFixed(1)}s`);
|
|
90
|
-
console.log("");
|
|
91
|
-
for (const r of summary.results) {
|
|
92
|
-
const icon = r.status === "passed" ? "PASS" : r.status === "failed" ? "FAIL" : "SKIP";
|
|
93
|
-
console.log(` [${icon}] ${r.name} (${r.duration_ms}ms)`);
|
|
94
|
-
if (r.error) {
|
|
95
|
-
console.log(` Error: ${r.error}`);
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
if (summary.healed.length > 0) {
|
|
99
|
-
console.log("");
|
|
100
|
-
console.log(`--- Self-Healed: ${summary.healed.length} selector(s) ---`);
|
|
101
|
-
for (const h of summary.healed) {
|
|
102
|
-
console.log(` "${h.test_case}" step ${h.step_index + 1}`);
|
|
103
|
-
console.log(` ${h.original_selector} -> ${h.new_selector}`);
|
|
104
|
-
console.log(` Strategy: ${h.strategy} (${Math.round(h.confidence * 100)}% confidence)`);
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
// ---------------------------------------------------------------------------
|
|
109
|
-
// Main
|
|
110
|
-
// ---------------------------------------------------------------------------
|
|
111
|
-
async function main() {
|
|
112
|
-
const config = parseCliArgs();
|
|
113
|
-
const orgSlug = sanitizePath(config.apiKey.split("_")[1] ?? "default");
|
|
114
|
-
const browserMgr = new BrowserManager({
|
|
115
|
-
browserType: config.browser,
|
|
116
|
-
headless: true,
|
|
117
|
-
orgSlug,
|
|
118
|
-
});
|
|
119
|
-
const cloud = new CloudClient({
|
|
120
|
-
apiKey: config.apiKey,
|
|
121
|
-
baseUrl: config.baseUrl,
|
|
122
|
-
});
|
|
123
|
-
const consoleLogs = [];
|
|
124
|
-
console.log(`FastTest CI Runner v${PKG_VERSION}`);
|
|
125
|
-
console.log(`Suite: ${config.suiteId}`);
|
|
126
|
-
console.log(`Browser: ${config.browser}`);
|
|
127
|
-
if (config.appUrl)
|
|
128
|
-
console.log(`App URL: ${config.appUrl}`);
|
|
129
|
-
console.log("");
|
|
130
|
-
let summary;
|
|
131
|
-
try {
|
|
132
|
-
summary = await executeRun(browserMgr, cloud, {
|
|
133
|
-
suiteId: config.suiteId,
|
|
134
|
-
testCaseIds: config.testCaseIds,
|
|
135
|
-
appUrlOverride: config.appUrl,
|
|
136
|
-
}, consoleLogs);
|
|
137
|
-
}
|
|
138
|
-
catch (err) {
|
|
139
|
-
console.error(`Fatal: ${err}`);
|
|
140
|
-
await safeClose(browserMgr);
|
|
141
|
-
process.exit(1);
|
|
142
|
-
}
|
|
143
|
-
// Output results
|
|
144
|
-
if (config.json) {
|
|
145
|
-
console.log(JSON.stringify(summary, null, 2));
|
|
146
|
-
}
|
|
147
|
-
else {
|
|
148
|
-
printFormattedResults(summary);
|
|
149
|
-
}
|
|
150
|
-
// Post PR comment if requested
|
|
151
|
-
if (config.prUrl) {
|
|
152
|
-
try {
|
|
153
|
-
const prResult = await cloud.postPrComment({
|
|
154
|
-
pr_url: config.prUrl,
|
|
155
|
-
execution_id: summary.execution_id,
|
|
156
|
-
status: summary.status,
|
|
157
|
-
total: summary.total,
|
|
158
|
-
passed: summary.passed,
|
|
159
|
-
failed: summary.failed,
|
|
160
|
-
skipped: summary.skipped,
|
|
161
|
-
duration_seconds: Math.round(summary.duration_ms / 1000),
|
|
162
|
-
test_results: summary.results.map((r) => ({
|
|
163
|
-
name: r.name,
|
|
164
|
-
status: r.status,
|
|
165
|
-
error: r.error,
|
|
166
|
-
})),
|
|
167
|
-
healed: summary.healed.map((h) => ({
|
|
168
|
-
original_selector: h.original_selector,
|
|
169
|
-
new_selector: h.new_selector,
|
|
170
|
-
strategy: h.strategy,
|
|
171
|
-
confidence: h.confidence,
|
|
172
|
-
})),
|
|
173
|
-
});
|
|
174
|
-
const commentUrl = prResult.comment_url;
|
|
175
|
-
console.log(`\nPR comment posted: ${commentUrl ?? config.prUrl}`);
|
|
176
|
-
}
|
|
177
|
-
catch (err) {
|
|
178
|
-
console.error(`\nFailed to post PR comment: ${err}`);
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
await safeClose(browserMgr);
|
|
182
|
-
process.exit(summary.status === "passed" ? 0 : 1);
|
|
183
|
-
}
|
|
184
|
-
/** Close browser with a timeout so CI doesn't hang. */
|
|
185
|
-
async function safeClose(browserMgr) {
|
|
186
|
-
await Promise.race([
|
|
187
|
-
browserMgr.close(),
|
|
188
|
-
new Promise((resolve) => setTimeout(resolve, 5000)),
|
|
189
|
-
]);
|
|
190
|
-
}
|
|
191
|
-
main().catch((err) => {
|
|
192
|
-
console.error("Fatal:", err);
|
|
193
|
-
process.exit(1);
|
|
194
|
-
});
|
|
195
|
-
//# sourceMappingURL=cli.js.map
|
|
2
|
+
import{readFileSync as We}from"node:fs";import{join as ze,dirname as Ge}from"node:path";import{fileURLToPath as Ze}from"node:url";import{chromium as ke,firefox as Re,webkit as Te,devices as Ce}from"playwright";import{execFileSync as Ae}from"node:child_process";import*as R from"node:fs";import*as D from"node:path";import*as J from"node:os";var j=D.join(J.homedir(),".fasttest","sessions"),Ee=/^(con|prn|aux|nul|com\d|lpt\d)$/i;function U(s){let t=s.replace(/[\/\\]/g,"_").replace(/\.\./g,"_").replace(/\0/g,"").replace(/^_+|_+$/g,"").replace(/^\./,"_")||"default";return Ee.test(t)?`_${t}`:t}var L=class s{browser=null;context=null;page=null;browserType;headless;orgSlug;deviceName;pendingDialogs=new WeakMap;networkEntries=[];constructor(e={}){this.browserType=e.browserType??"chromium",this.headless=e.headless??!0,this.orgSlug=U(e.orgSlug??"default"),this.deviceName=e.device}async setDevice(e){this.deviceName=e,this.page&&!this.page.isClosed()&&await this.page.close().catch(()=>{}),this.context&&await this.context.close().catch(()=>{}),this.page=null,this.context=null}getContextOptions(e){if(this.deviceName){let t=Ce[this.deviceName];if(!t)throw new Error(`Unknown Playwright device "${this.deviceName}". Use a name from Playwright's device registry (e.g. "iPhone 15", "Pixel 7").`);return{...t,ignoreHTTPSErrors:!0,...e}}return{viewport:{width:1280,height:720},ignoreHTTPSErrors:!0,...e}}async ensureBrowser(){if(this.page&&!this.page.isClosed())try{return await this.page.evaluate("1"),this.page}catch{}if(!this.browser||!this.browser.isConnected()){this.context=null,this.page=null;let e=this.browserType==="firefox"?Re:this.browserType==="webkit"?Te:ke;try{this.browser=await e.launch({headless:this.headless,args:this.browserType==="chromium"?["--disable-blink-features=AutomationControlled"]:[]})}catch(t){let r=t instanceof Error?t.message:String(t);if(r.includes("Executable doesn't exist")||r.includes("browserType.launch")){let n=process.platform==="win32"?"npx.cmd":"npx";Ae(n,["playwright","install","--with-deps",this.browserType],{stdio:"inherit"}),this.browser=await e.launch({headless:this.headless,args:this.browserType==="chromium"?["--disable-blink-features=AutomationControlled"]:[]})}else throw t}}return this.context||(this.context=await this.browser.newContext(this.getContextOptions())),this.page=await this.context.newPage(),this.attachDialogListener(this.page),this.attachNetworkListener(this.page),this.page}async getPage(){return this.ensureBrowser()}async newContext(){return(!this.browser||!this.browser.isConnected())&&await this.ensureBrowser(),this.page&&!this.page.isClosed()&&await this.page.close().catch(()=>{}),this.context&&await this.context.close().catch(()=>{}),this.context=await this.browser.newContext(this.getContextOptions()),this.page=await this.context.newPage(),this.attachDialogListener(this.page),this.attachNetworkListener(this.page),this.page}async saveSession(e){if(!this.context)throw new Error("No browser context \u2014 nothing to save");let t=U(e),r=D.join(j,this.orgSlug);R.mkdirSync(r,{recursive:!0,mode:448});let n=D.join(r,`${t}.json`),i=await this.context.storageState();return R.writeFileSync(n,JSON.stringify(i,null,2),{mode:384}),n}async restoreSession(e){let t=U(e),r=D.join(j,this.orgSlug,`${t}.json`);if(!R.existsSync(r))throw new Error(`Session "${e}" not found at ${r}`);let n=JSON.parse(R.readFileSync(r,"utf-8"));return(!this.browser||!this.browser.isConnected())&&await this.ensureBrowser(),this.page&&!this.page.isClosed()&&await this.page.close().catch(()=>{}),this.context&&await this.context.close().catch(()=>{}),this.context=await this.browser.newContext(this.getContextOptions({storageState:n})),this.page=await this.context.newPage(),this.attachDialogListener(this.page),this.attachNetworkListener(this.page),this.page}sessionExists(e){let t=U(e);return R.existsSync(D.join(j,this.orgSlug,`${t}.json`))}listSessions(){let e=D.join(j,this.orgSlug);return R.existsSync(e)?R.readdirSync(e).filter(t=>t.endsWith(".json")).map(t=>t.replace(/\.json$/,"")):[]}attachDialogListener(e){e.on("dialog",t=>{let r=this.pendingDialogs.get(e);r&&clearTimeout(r.dismissTimer);let n=setTimeout(()=>{this.pendingDialogs.get(e)?.dialog===t&&(t.dismiss().catch(()=>{}),this.pendingDialogs.delete(e))},3e4);this.pendingDialogs.set(e,{type:t.type(),message:t.message(),defaultValue:t.defaultValue(),dialog:t,dismissTimer:n})})}async handleDialog(e,t){let r=this.page,n=r?this.pendingDialogs.get(r):void 0;if(!n)throw new Error("No pending dialog to handle");return clearTimeout(n.dismissTimer),this.pendingDialogs.delete(r),e==="accept"?await n.dialog.accept(t):await n.dialog.dismiss(),{type:n.type,message:n.message}}static MAX_NETWORK_ENTRIES=1e3;attachNetworkListener(e){e.on("response",t=>{let r=t.request(),n=r.url();n.startsWith("http")&&(this.networkEntries.length>=s.MAX_NETWORK_ENTRIES&&this.networkEntries.shift(),this.networkEntries.push({url:n,method:r.method(),status:t.status(),duration:0,mimeType:t.headers()["content-type"]??"",responseSize:parseInt(t.headers()["content-length"]??"0",10)}))})}getNetworkSummary(){return[...this.networkEntries]}clearNetworkEntries(){this.networkEntries=[]}listPages(){return this.context?this.context.pages().map((e,t)=>({index:t,url:e.url(),title:""})):[]}async listPagesAsync(){if(!this.context)return[];let e=this.context.pages(),t=[];for(let r=0;r<e.length;r++)t.push({index:r,url:e[r].url(),title:await e[r].title().catch(()=>"")});return t}async createPage(e){this.context||await this.ensureBrowser();let t=await this.context.newPage();return this.attachDialogListener(t),this.attachNetworkListener(t),e&&await t.goto(e,{waitUntil:"domcontentloaded",timeout:3e4}),this.page=t,t}async switchToPage(e){if(!this.context)throw new Error("No browser context \u2014 no tabs to switch to");let t=this.context.pages();if(e<0||e>=t.length)throw new Error(`Tab index ${e} out of range (0-${t.length-1})`);return this.page=t[e],await this.page.bringToFront(),this.page}async closePage(e){if(!this.context)throw new Error("No browser context \u2014 no tabs to close");let t=this.context.pages();if(e<0||e>=t.length)throw new Error(`Tab index ${e} out of range (0-${t.length-1})`);await t[e].close();let n=this.context.pages();n.length>0?this.page=n[Math.min(e,n.length-1)]:this.page=null}async close(){this.page&&!this.page.isClosed()&&await this.page.close().catch(()=>{}),this.context&&await this.context.close().catch(()=>{}),this.browser&&await this.browser.close().catch(()=>{}),this.page=null,this.context=null,this.browser=null}};var K=class extends Error{constructor(t,r,n){super(`Monthly run limit reached (${r}/${n}). Current plan: ${t}. Upgrade at https://fasttest.ai to continue.`);this.plan=t;this.used=r;this.limit=n;this.name="QuotaExceededError"}},B=class{apiKey;baseUrl;constructor(e){this.apiKey=e.apiKey,this.baseUrl=(e.baseUrl??"https://api.fasttest.ai").replace(/\/$/,"")}get dashboardUrl(){try{let e=new URL(this.baseUrl);return e.hostname=e.hostname.replace(/^api\./,""),e.pathname="/",e.origin}catch{return"https://fasttest.ai"}}static async requestDeviceCode(e){let t=`${e.replace(/\/$/,"")}/api/v1/auth/device-code`,r=await fetch(t,{method:"POST"});if(!r.ok){let n=await r.text();throw new Error(`Device code request failed (${r.status}): ${n}`)}return await r.json()}static async fetchPrompts(e){let t=`${e.replace(/\/$/,"")}/api/v1/qa/prompts`,r=await fetch(t,{signal:AbortSignal.timeout(5e3)});if(!r.ok)throw new Error(`Prompt fetch failed (${r.status})`);return await r.json()}static async pollDeviceCode(e,t){let r=`${e.replace(/\/$/,"")}/api/v1/auth/device-code/status?poll_token=${encodeURIComponent(t)}`,n=await fetch(r);if(!n.ok){let i=await n.text();throw new Error(`Device code poll failed (${n.status}): ${i}`)}return await n.json()}async request(e,t,r){let n=`${this.baseUrl}/api/v1${t}`,i={"x-api-key":this.apiKey,"Content-Type":"application/json"},a=2,u=1e3;for(let c=0;c<=a;c++){let d=new AbortController,w=setTimeout(()=>d.abort(),3e4);try{let l={method:e,headers:i,signal:d.signal};r!==void 0&&(l.body=JSON.stringify(r));let m=await fetch(n,l);if(clearTimeout(w),!m.ok){let h=await m.text();if(m.status>=500&&c<a){await new Promise(p=>setTimeout(p,u*2**c));continue}if(m.status===402){let p=h.match(/\((\d+)\/(\d+)\).*plan:\s*(\w+)/i);throw new K(p?.[3]??"unknown",p?parseInt(p[1]):0,p?parseInt(p[2]):0)}throw new Error(`Cloud API ${e} ${t} \u2192 ${m.status}: ${h}`)}return await m.json()}catch(l){if(clearTimeout(w),l instanceof Error&&(l.name==="AbortError"||l.message.includes("fetch failed"))&&c<a){await new Promise(h=>setTimeout(h,u*2**c));continue}throw l}}throw new Error(`Cloud API ${e} ${t}: max retries exceeded`)}async get(e){return this.request("GET",e)}async post(e,t){return this.request("POST",e,t)}async health(){let e=`${this.baseUrl}/health`;return await(await fetch(e)).json()}async listProjects(){return this.get("/qa/projects/")}async resolveProject(e,t){let r={name:e};return t&&(r.base_url=t),this.post("/qa/projects/resolve",r)}async listSuites(e){let t=e?`?search=${encodeURIComponent(e)}`:"";return this.get(`/qa/projects/suites/all${t}`)}async resolveSuite(e,t){let r={name:e};return t&&(r.project_id=t),this.post("/qa/projects/suites/resolve",r)}async createSuite(e,t){return this.post(`/qa/projects/${e}/test-suites`,{...t,project_id:e})}async updateSuite(e,t){return this.request("PUT",`/qa/execution/suites/${e}`,t)}async createTestCase(e){return this.post("/qa/test-cases/",e)}async updateTestCase(e,t){return this.request("PUT",`/qa/test-cases/${e}`,t)}async applyHealing(e,t,r){return this.post(`/qa/test-cases/${e}/apply-healing`,{original_selector:t,healed_selector:r})}async detectSharedSteps(e,t){let r=new URLSearchParams;e&&r.set("project_id",e),t&&r.set("auto_create","true");let n=r.toString()?`?${r.toString()}`:"";return this.post(`/qa/shared-steps/detect${n}`,{})}async resolveEnvironment(e,t){return this.post("/qa/environments/resolve",{suite_id:e,name:t})}async startRun(e){return this.post("/qa/execution/run",e)}async reportResult(e,t){return this.post(`/qa/execution/executions/${e}/results`,t)}async completeExecution(e,t){return this.post(`/qa/execution/executions/${e}/complete`,{status:t})}async cancelExecution(e){return this.post(`/qa/execution/executions/${e}/cancel`,{})}async getExecutionStatus(e){return this.get(`/qa/execution/executions/${e}`)}async getExecutionDiff(e){return this.get(`/qa/execution/executions/${e}/diff`)}async notifyTestStarted(e,t,r){try{await this.post(`/qa/execution/executions/${e}/test-started`,{test_case_id:t,test_case_name:r})}catch{}}async notifyHealingStarted(e,t,r){try{await this.post(`/qa/execution/executions/${e}/healing-started`,{test_case_id:t,original_selector:r})}catch{}}async checkControlStatus(e){return(await this.get(`/qa/execution/executions/${e}/control-status`)).status}async setGithubToken(e){return this.request("PUT","/qa/github/token",{github_token:e})}async postPrComment(e){return this.post("/qa/github/pr-comment",e)}async createLiveSession(e){return this.post("/qa/live-sessions",e)}async updateLiveSession(e,t){return this.request("PATCH",`/qa/live-sessions/${e}`,t)}async saveChaosReport(e,t){let r=e?`?project_id=${e}`:"";return this.post(`/qa/chaos/reports${r}`,t)}};async function X(s,e){try{return await s.goto(e,{waitUntil:"domcontentloaded",timeout:3e4}),await s.waitForLoadState("networkidle",{timeout:5e3}).catch(()=>{}),{success:!0,data:{title:await s.title(),url:s.url()}}}catch(t){return{success:!1,error:String(t)}}}async function Q(s,e){try{return await s.click(e,{timeout:1e4}),await s.waitForLoadState("networkidle",{timeout:1e4}).catch(()=>{}),{success:!0}}catch(t){return{success:!1,error:String(t)}}}async function Y(s,e,t){try{return await s.fill(e,t,{timeout:1e4}),{success:!0}}catch(r){return{success:!1,error:String(r)}}}async function ee(s,e){try{return await s.hover(e,{timeout:1e4}),{success:!0}}catch(t){return{success:!1,error:String(t)}}}async function te(s,e,t){try{return await s.selectOption(e,t,{timeout:1e4}),{success:!0}}catch(r){return{success:!1,error:String(r)}}}async function se(s,e,t=1e4){try{return await s.waitForSelector(e,{timeout:t}),{success:!0}}catch(r){return{success:!1,error:String(r)}}}async function re(s,e=!1){return(await s.screenshot({type:"jpeg",quality:80,fullPage:e})).toString("base64")}async function ne(s){let e=await s.locator("body").ariaSnapshot().catch(()=>"");return{url:s.url(),title:await s.title(),accessibilityTree:e}}async function ie(s){try{return await s.goBack({waitUntil:"domcontentloaded",timeout:3e4})===null?{success:!1,error:"No previous page in history"}:{success:!0,data:{title:await s.title(),url:s.url()}}}catch(e){return{success:!1,error:String(e)}}}async function ae(s){try{return await s.goForward({waitUntil:"domcontentloaded",timeout:3e4})===null?{success:!1,error:"No next page in history"}:{success:!0,data:{title:await s.title(),url:s.url()}}}catch(e){return{success:!1,error:String(e)}}}async function oe(s,e){try{return await s.keyboard.press(e),{success:!0}}catch(t){return{success:!1,error:String(t)}}}async function ce(s,e,t){try{return await s.setInputFiles(e,t,{timeout:1e4}),{success:!0}}catch(r){return{success:!1,error:String(r)}}}async function ue(s,e){try{return{success:!0,data:{result:await s.evaluate(e)}}}catch(t){return{success:!1,error:String(t)}}}async function le(s,e,t){try{return await s.dragAndDrop(e,t,{timeout:1e4}),await s.waitForLoadState("networkidle",{timeout:5e3}).catch(()=>{}),{success:!0}}catch(r){return{success:!1,error:String(r)}}}async function de(s,e,t){try{return await s.setViewportSize({width:e,height:t}),{success:!0,data:{width:e,height:t}}}catch(r){return{success:!1,error:String(r)}}}async function ge(s,e){try{for(let[t,r]of Object.entries(e))await s.fill(t,r,{timeout:1e4});return{success:!0,data:{filled:Object.keys(e).length}}}catch(t){return{success:!1,error:String(t)}}}async function pe(s,e){try{switch(e.type){case"element_visible":{let t=await s.isVisible(e.selector,{timeout:5e3});return{pass:t,actual:t}}case"element_hidden":try{return await s.waitForSelector(e.selector,{state:"hidden",timeout:5e3}),{pass:!0,actual:!0}}catch{return{pass:!1,actual:!1,error:"Element is still visible"}}case"text_contains":{let t=s.locator(e.selector);if(await t.count()===0)return{pass:!1,error:"Element not found"};let n=await t.first().textContent();return{pass:n?.includes(e.text??"")??!1,actual:n??""}}case"text_equals":{let t=s.locator(e.selector);if(await t.count()===0)return{pass:!1,error:"Element not found"};let n=(await t.first().textContent())?.trim()??"";return{pass:n===e.text,actual:n}}case"url_contains":{let t=s.url(),r=e.url??e.text??"";return{pass:t.includes(r),actual:t}}case"url_equals":{let t=s.url();return{pass:t===e.url,actual:t}}case"element_count":{let r=await s.locator(e.selector).count();return{pass:r===(e.count??1),actual:r}}case"attribute_value":{let t=s.locator(e.selector);if(await t.count()===0)return{pass:!1,error:"Element not found"};let n=await t.first().getAttribute(e.attribute??"");return{pass:n===e.value,actual:n??""}}default:return{pass:!1,error:`Unknown assertion type: ${e.type}`}}}catch(t){return{pass:!1,error:String(t)}}}var fe={data_testid:.98,aria:.95,text:.9,structural:.85,ai:.75};async function he(s,e,t,r,n,i,a){if(e)try{let c=await e.post("/qa/healing/classify",{failure_type:r,selector:t,page_url:i,error_message:n});if(c.is_real_bug)return{healed:!1,error:c.reason??"Classified as real bug"};if(c.pattern){let d=await O(s,c.pattern.healed_value),w=d&&await me(s,c.pattern.healed_value,a);if(d&&w)return{healed:!0,newSelector:c.pattern.healed_value,strategy:c.pattern.strategy,confidence:c.pattern.confidence};c.pattern.id&&Fe(e,c.pattern.id,i)}}catch{}let u=[{name:"data_testid",fn:()=>De(s,t)},{name:"aria",fn:()=>Ie(s,t)},{name:"text",fn:()=>Ue(s,t)},{name:"structural",fn:()=>Oe(s,t)}];for(let c of u){let d=await c.fn();if(d){if(!await me(s,d,a))continue;return e&&await qe(e,r,t,d,c.name,fe[c.name]??.8,i),{healed:!0,newSelector:d,strategy:c.name,confidence:fe[c.name]}}}return{healed:!1,error:"Local healing strategies exhausted"}}async function O(s,e){try{return await s.locator(e).count()===1}catch{return!1}}async function me(s,e,t){if(!t)return!0;try{let r=await s.locator(e).evaluate(a=>({tag:a.tagName.toLowerCase(),role:a.getAttribute("role"),type:a.type??null,contentEditable:a.getAttribute("contenteditable"),text:(a.textContent??"").trim().slice(0,200),ariaLabel:a.getAttribute("aria-label")??""})),n=t.action;if(n==="click"||n==="hover"){let a=["button","a","input","select","summary","details","label","option"],u=["button","link","tab","menuitem","checkbox","radio","switch","option"];if(!(a.includes(r.tag)||r.role!=null&&u.includes(r.role)))return!1}if((n==="fill"||n==="type")&&!(r.tag==="input"||r.tag==="textarea"||r.contentEditable==="true"||r.contentEditable==="")||n==="select"&&r.tag!=="select"&&r.role!=="listbox"&&r.role!=="combobox")return!1;let i=[t.description,t.intent].filter(Boolean);for(let a of i){let u=a.match(/['"]([^'"]+)['"]/);if(u){let c=u[1].toLowerCase();if(!(r.text+" "+r.ariaLabel).toLowerCase().includes(c))return!1}}return!0}catch{return!0}}async function De(s,e){try{let t=H(e);if(!t)return null;let r=[`[data-testid="${t}"]`,`[data-test="${t}"]`,`[data-test-id="${t}"]`];for(let n of r)if(await O(s,n))return n;return null}catch{return null}}async function Ie(s,e){try{let t=H(e);if(!t)return null;let r=[`[aria-label="${t}"]`];for(let n of r)if(await O(s,n))return n;return null}catch{return null}}async function Ue(s,e){try{let t=H(e);if(!t)return null;let r=[`[aria-label="${t}"]`,`[title="${t}"]`,`[alt="${t}"]`,`[placeholder="${t}"]`,`role=button[name="${t}"]`,`role=link[name="${t}"]`];for(let n of r)if(await O(s,n))return n;return null}catch{return null}}async function Oe(s,e){try{let r=e.match(/^([a-z]+)/i)?.[1]??"",n=H(e);if(!r&&!n)return null;let i=[];r&&n&&(i.push(`${r}[name="${n}"]`),i.push(`${r}[id*="${n}"]`),i.push(`${r}[class*="${n}"]`));for(let a of i)if(await O(s,a))return a;return null}catch{return null}}function H(s){let e=s.match(/\[(?:data-testid|data-test|data-test-id|id|name|aria-label)\s*[~|^$*]?=\s*["']([^"']+)["']\]/);if(e)return e[1];let t=s.match(/#([\w-]+)/);if(t)return t[1];let r=[...s.matchAll(/\.([\w-]+)/g)];if(r.length>0)return r[r.length-1][1];let n=s.match(/\[name=["']([^"']+)["']\]/);return n?n[1]:s.match(/[a-zA-Z][\w-]{2,}/)?.[0]??null}async function qe(s,e,t,r,n,i,a){try{await s.post("/qa/healing/patterns",{failure_type:e,original_value:t,healed_value:r,strategy:n,confidence:i,page_url:a})}catch{}}async function Fe(s,e,t){try{await s.post(`/qa/healing/patterns/${e}/failed`,{page_url:t})}catch{}}var je=/\{\{([A-Z][A-Z0-9_]*)\}\}/g;function k(s,e=process.env){let t=[],r=s.replace(je,(n,i)=>{let a=e[i];return a===void 0?(t.push(i),n):a});if(t.length>0)throw new Error(`Missing environment variable(s): ${t.join(", ")}. Set these before running tests. In GitHub Actions, add them as repository secrets.`);return r}function V(s,e){let t={...s};if(t.value!==void 0&&(t.value=k(t.value,e)),t.url!==void 0&&(t.url=k(t.url,e)),t.expression!==void 0&&(t.expression=k(t.expression,e)),t.key!==void 0&&(t.key=k(t.key,e)),t.name!==void 0&&(t.name=k(t.name,e)),t.fields!==void 0){let r={};for(let[n,i]of Object.entries(t.fields))r[n]=k(i,e);t.fields=r}return t}function we(s,e){let t={...s};return t.text!==void 0&&(t.text=k(t.text,e)),t.url!==void 0&&(t.url=k(t.url,e)),t.value!==void 0&&(t.value=k(t.value,e)),t.expected_value!==void 0&&(t.expected_value=k(t.expected_value,e)),t}function W(s,e){let t=new Set;function r(n){if(!n)return;let i=/\{\{([A-Z][A-Z0-9_]*)\}\}/g,a;for(;(a=i.exec(n))!==null;)t.add(a[1])}for(let n of s)if(r(n.value),r(n.url),r(n.expression),r(n.key),r(n.name),n.fields)for(let i of Object.values(n.fields))r(i);for(let n of e)r(n.text),r(n.url),r(n.value),r(n.expected_value);return Array.from(t).sort()}async function ye(s,e,t,r){await s.setDevice(t.device);let n=await e.startRun({suite_id:t.suiteId,environment_id:t.environmentId,browser:"chromium",test_case_ids:t.testCaseIds,device:t.device}),i=n.execution_id,a=n.test_cases,u=n.default_session??void 0,c=t.appUrlOverride??n.base_url??"";if(c)try{c=k(c)}catch(o){try{await e.completeExecution(i)}catch{}return{execution_id:i,status:"failed",total:a.length,passed:0,failed:a.length,skipped:0,duration_ms:0,results:a.map(g=>({id:g.id,name:g.name,status:"failed",duration_ms:0,error:String(o),step_results:[]})),healed:[]}}let d=[];for(let o of a)for(let g of W(o.steps,o.assertions))d.includes(g)||d.push(g);if(n.setup){let o=Array.isArray(n.setup)?n.setup:Object.values(n.setup).flat();for(let g of W(o,[]))d.includes(g)||d.push(g)}let w=[u,...a.map(o=>o.session).filter(Boolean)].filter(Boolean);for(let o of w){let g=o.matchAll(/\{\{([A-Z][A-Z0-9_]*)\}\}/g);for(let f of g)d.includes(f[1])||d.push(f[1])}if(d.length>0){let o=[],g=[];for(let f of d)process.env[f]!==void 0?o.push(f):g.push(f);if(o.length>0&&process.stderr.write(`Environment variables resolved: ${o.join(", ")}
|
|
3
|
+
`),g.length>0){let f=`Missing environment variable(s): ${g.join(", ")}. Set these before running tests. In GitHub Actions, add them as repository secrets.`;process.stderr.write(`ERROR: ${f}
|
|
4
|
+
`);try{await e.completeExecution(i)}catch{}return{execution_id:i,status:"failed",total:a.length,passed:0,failed:a.length,skipped:0,duration_ms:0,results:a.map(P=>({id:P.id,name:P.name,status:"failed",duration_ms:0,error:f,step_results:[]})),healed:[]}}}let l=n.setup;if(l){let o;Array.isArray(l)?u?o={[u]:l}:(process.stderr.write(`Warning: suite has setup steps but no default_session set. Setup will be skipped. Set the suite's session field to enable CI login.
|
|
5
|
+
`),o={}):o=l;for(let[g,f]of Object.entries(o)){if(s.sessionExists(g)){process.stderr.write(`Session "${g}" found locally \u2014 skipping setup.
|
|
6
|
+
`);continue}if(f.length===0)continue;process.stderr.write(`Session "${g}" not found \u2014 running setup (${f.length} steps)...
|
|
7
|
+
`);let P=await s.newContext(),E=!1;for(let C=0;C<f.length;C++){let _;try{_=V(f[C])}catch(A){let T=`Setup "${g}" step ${C+1} failed to resolve variables: ${A}`;process.stderr.write(`ERROR: ${T}
|
|
8
|
+
`),E=!0;try{await e.completeExecution(i)}catch{}return{execution_id:i,status:"failed",total:a.length,passed:0,failed:a.length,skipped:0,duration_ms:0,results:a.map(N=>({id:N.id,name:N.name,status:"failed",duration_ms:0,error:T,step_results:[]})),healed:[]}}let $=await z(P,_,c,s);if($.page&&(P=$.page),!$.success){let A=`Setup "${g}" step ${C+1} (${_.action}) failed: ${$.error}`;process.stderr.write(`ERROR: ${A}
|
|
9
|
+
`),E=!0;try{await e.completeExecution(i)}catch{}return{execution_id:i,status:"failed",total:a.length,passed:0,failed:a.length,skipped:0,duration_ms:0,results:a.map(T=>({id:T.id,name:T.name,status:"failed",duration_ms:0,error:A,step_results:[]})),healed:[]}}}E||(await s.saveSession(g),process.stderr.write(`Setup complete \u2014 session "${g}" saved.
|
|
10
|
+
`))}}else u&&!s.sessionExists(u)&&process.stderr.write(`Warning: session "${u}" not found and no setup steps defined. Tests will run without auth. Add setup steps to the suite for CI support.
|
|
11
|
+
`);let m=Ve(a);n.previous_statuses&&(m=Ke(m,n.previous_statuses));let h=[],p=[],b=Date.now(),S=!1,x=0,y=new Set,I=new Set(m.map(o=>o.id));for(let o of m){if(o.depends_on&&o.depends_on.length>0){let _=o.depends_on.filter($=>I.has($)&&!y.has($));if(_.length>0){h.push({id:o.id,name:o.name,status:"skipped",duration_ms:0,error:`Skipped: dependency not met (${_.join(", ")})`,step_results:[]});continue}}try{let _=await e.checkControlStatus(i);if(_==="cancelled"){S=!0;break}if(_==="paused"){let $=!1,A=Date.now(),T=30*60*1e3;for(;!$;){if(Date.now()-A>T){process.stderr.write(`Pause exceeded 30-minute limit, auto-cancelling.
|
|
12
|
+
`),S=!0;break}await new Promise($e=>setTimeout($e,2e3));let N=await e.checkControlStatus(i);if(N==="running"&&($=!0),N==="cancelled"){S=!0;break}}if(S)break}}catch{}let g=o.retry_count??0,f,P=0;for(await e.notifyTestStarted(i,o.id,o.name);;){let _=(o.timeout_seconds||30)*1e3,$,A=new Promise((T,N)=>{$=setTimeout(()=>N(new Error(`Test case "${o.name}" timed out after ${o.timeout_seconds||30}s`)),_)});if(f=await Promise.race([Le(s,e,i,o,c,r,p,t.aiFallback,u),A]).finally(()=>clearTimeout($)).catch(T=>({id:o.id,name:o.name,status:"failed",duration_ms:_,error:String(T),step_results:[]})),f.status==="passed"||P>=g)break;P++,process.stderr.write(`Retrying ${o.name} (attempt ${P}/${g})...
|
|
13
|
+
`)}f.retry_attempts=P,f.status==="passed"&&y.add(o.id),h.push(f);let E=s.getNetworkSummary();s.clearNetworkEntries();let C=Me(E);try{await e.reportResult(i,{test_case_id:o.id,status:f.status,duration_ms:f.duration_ms,error_message:f.error,console_logs:r.slice(-50),retry_attempt:P,step_results:f.step_results.map(_=>({step_index:_.step_index,action:_.action,success:_.success,error:_.error,duration_ms:_.duration_ms,screenshot_url:_.screenshot_url,healed:_.healed,heal_details:_.heal_details})),network_summary:C.length>0?C:void 0})}catch(_){x++,process.stderr.write(`Failed to report result for ${o.name}: ${_}
|
|
14
|
+
`)}}let q=new Set(h.map(o=>o.id));for(let o of a)q.has(o.id)||h.push({id:o.id,name:o.name,status:"skipped",duration_ms:0,step_results:[]});let v=.9;if(p.length>0){let o=new Set;for(let g of p){if(g.confidence<v)continue;let f=`${g.test_case_id}:${g.original_selector}`;if(!o.has(f)){o.add(f);try{await e.applyHealing(g.test_case_id,g.original_selector,g.new_selector),process.stderr.write(`Auto-updated selector in "${g.test_case}": ${g.original_selector} \u2192 ${g.new_selector}
|
|
15
|
+
`)}catch{}}}}let M=h.filter(o=>o.status==="passed").length,F=h.filter(o=>o.status==="failed").length,Se=h.filter(o=>o.status==="skipped").length,Pe=Date.now()-b;try{await e.completeExecution(i,S?"cancelled":void 0)}catch(o){process.stderr.write(`Failed to complete execution: ${o}
|
|
16
|
+
`)}x>0&&process.stderr.write(`Warning: ${x} result report(s) failed to send to cloud.
|
|
17
|
+
`);let Z;if(t.aiFallback)for(let o of h){if(o.status!=="failed")continue;let g=o.step_results.find(f=>!f.success&&f.ai_context);if(g?.ai_context){let P=m.find(E=>E.id===o.id)?.steps[g.step_index]??{};Z={test_case_id:o.id,test_case_name:o.name,step_index:g.step_index,step:P,intent:g.ai_context.intent,error:g.error??o.error??"Unknown error",page_url:g.ai_context.page_url,snapshot:g.ai_context.snapshot};break}}return{execution_id:i,status:S?"cancelled":F===0?"passed":"failed",total:a.length,passed:M,failed:F,skipped:Se,duration_ms:Pe,results:h,healed:p,ai_fallback:Z}}async function Le(s,e,t,r,n,i,a,u,c){let d=[],w=Date.now();try{let l=r.session??c,m;if(l)try{m=k(l)}catch(p){if(/\{\{[A-Z_]+\}\}/.test(l))return{id:r.id,name:r.name,status:"failed",duration_ms:Date.now()-w,error:`Session name "${l}" contains unresolved variable: ${p}`,step_results:[]};m=l}let h;if(m)try{h=await s.restoreSession(m)}catch(p){process.stderr.write(`Warning: session "${m}" not found, using fresh context: ${p}
|
|
18
|
+
`),h=await s.newContext()}else h=await s.newContext();for(let p=0;p<r.steps.length;p++){let b=r.steps[p],S=Date.now(),x;try{x=V(b)}catch(v){return d.push({step_index:p,action:b.action,success:!1,error:String(v),duration_ms:Date.now()-S}),{id:r.id,name:r.name,status:"failed",duration_ms:Date.now()-w,error:`Step ${p+1} (${b.action}) failed: ${String(v)}`,step_results:d}}let y=await z(h,x,n,s);if(y.page&&(h=y.page),!y.success&&x.selector&&Be(y.error)){await e.notifyHealingStarted(t,r.id,x.selector);let v=await he(h,e,x.selector,He(y.error),y.error??"unknown",h.url(),{action:x.action,description:x.description,intent:x.intent});if(v.healed&&v.newSelector){let M={...x,selector:v.newSelector};if(y=await z(h,M,n,s),y.success){a.push({test_case_id:r.id,test_case:r.name,step_index:p,original_selector:b.selector,new_selector:v.newSelector,strategy:v.strategy??"unknown",confidence:v.confidence??0});let F=await _e(h);d.push({step_index:p,action:b.action,success:!0,duration_ms:Date.now()-S,screenshot_url:F?.dataUrl,healed:!0,heal_details:{original_selector:b.selector,new_selector:v.newSelector,strategy:v.strategy??"unknown",confidence:v.confidence??0}});continue}}}let I=await _e(h),q;if(!y.success&&u)try{let v=await ne(h);q={intent:x.intent??x.description,page_url:h.url(),snapshot:v}}catch{}if(d.push({step_index:p,action:b.action,success:y.success,error:y.error,duration_ms:Date.now()-S,screenshot_url:I?.dataUrl,ai_context:q}),!y.success)return{id:r.id,name:r.name,status:"failed",duration_ms:Date.now()-w,error:`Step ${p+1} (${b.action}) failed: ${y.error}`,step_results:d}}for(let p=0;p<r.assertions.length;p++){let b=r.assertions[p],S=Date.now(),x;try{x=we(b)}catch(I){return d.push({step_index:r.steps.length+p,action:`assert:${b.type}`,success:!1,error:String(I),duration_ms:Date.now()-S}),{id:r.id,name:r.name,status:"failed",duration_ms:Date.now()-w,error:`Assertion ${p+1} (${b.type}) failed: ${String(I)}`,step_results:d}}let y=await xe(h,x);if(d.push({step_index:r.steps.length+p,action:`assert:${b.type}`,success:y.pass,error:y.error,duration_ms:Date.now()-S}),!y.pass)return{id:r.id,name:r.name,status:"failed",duration_ms:Date.now()-w,error:`Assertion ${p+1} (${b.type}) failed: ${y.error??"expected value mismatch"}`,step_results:d}}return{id:r.id,name:r.name,status:"passed",duration_ms:Date.now()-w,step_results:d}}catch(l){return{id:r.id,name:r.name,status:"failed",duration_ms:Date.now()-w,error:String(l),step_results:d}}}async function _e(s){try{return{dataUrl:`data:image/jpeg;base64,${await re(s,!1)}`}}catch{return}}async function z(s,e,t,r){let n=e.action;try{switch(n){case"navigate":{let i=e.url??e.value??"";return i&&!i.startsWith("http")&&(i=t.replace(/\/$/,"")+i),await X(s,i)}case"click":return await Q(s,e.selector??"");case"type":case"fill":return await Y(s,e.selector??"",e.value??"");case"fill_form":{let i=e.fields??{};return await ge(s,i)}case"drag":return await le(s,e.selector??"",e.target??"");case"resize":return await de(s,e.width??1280,e.height??720);case"hover":return await ee(s,e.selector??"");case"select":return await te(s,e.selector??"",e.value??"");case"wait_for":return e.condition==="navigation"?(await s.waitForLoadState("domcontentloaded",{timeout:(e.timeout??10)*1e3}),await s.waitForLoadState("networkidle",{timeout:5e3}).catch(()=>{}),{success:!0}):await se(s,e.selector??"",(e.timeout??10)*1e3);case"scroll":return e.selector?await s.locator(e.selector).scrollIntoViewIfNeeded():await s.evaluate(()=>window.scrollTo(0,document.body.scrollHeight)),{success:!0};case"press_key":return await oe(s,e.key??e.value??"Enter");case"upload_file":{let i=e.file_paths??(e.value?[e.value]:null);return!i||i.length===0?{success:!1,error:"upload_file step missing file_paths"}:await ce(s,e.selector??"",i)}case"evaluate":return await ue(s,e.expression??e.value??"");case"go_back":return await ie(s);case"go_forward":return await ae(s);case"restore_session":{if(!r)return{success:!1,error:"restore_session requires browser manager"};let i=e.value??e.name??"";return i?{success:!0,page:await r.restoreSession(i)}:{success:!1,error:"restore_session step missing session name (set 'value' or 'name')"}}case"save_session":{if(!r)return{success:!1,error:"save_session requires browser manager"};let i=e.value??e.name??"";return i?(await r.saveSession(i),{success:!0}):{success:!1,error:"save_session step missing session name (set 'value' or 'name')"}}case"assert":return xe(s,e).then(i=>({success:i.pass,error:i.error}));default:return{success:!1,error:`Unknown action: ${n}`}}}catch(i){return{success:!1,error:String(i)}}}async function xe(s,e){return pe(s,{type:e.type,selector:e.selector,text:e.text??e.expected_value,url:e.url,count:e.count,attribute:e.attribute,value:e.value??e.expected_value})}function Be(s){if(!s)return!1;let e=s.toLowerCase();return e.includes("navigation")||e.includes("net::")||e.includes("page.goto")?!1:e.includes("selector")||e.includes("not found")||e.includes("waiting for selector")||e.includes("no element")||e.includes("waiting for locator")||e.includes("locator")}function He(s){if(!s)return"UNKNOWN";let e=s.toLowerCase();return e.includes("timeout")?"TIMEOUT":e.includes("not found")||e.includes("no element")||e.includes("selector")?"ELEMENT_NOT_FOUND":e.includes("navigation")||e.includes("net::")?"NAVIGATION_FAILED":"UNKNOWN"}function Me(s){return s.filter(e=>{let t=e.mimeType.toLowerCase();return!!(t.includes("json")||t.includes("text/html")||t.includes("text/plain")||e.status>=400)})}function Ke(s,e){let t=new Set(s.map(a=>a.id)),r=new Set;for(let a of s)if(a.depends_on)for(let u of a.depends_on)r.add(u);let n=[],i=[];for(let a of s){let u=e[a.id],c=a.depends_on?.some(d=>t.has(d))??!1;u==="failed"&&!r.has(a.id)&&!c?n.push(a):i.push(a)}return[...n,...i]}function Ve(s){let e=new Set(s.map(c=>c.id));if(!s.some(c=>c.depends_on&&c.depends_on.some(d=>e.has(d))))return s;let r=new Map(s.map(c=>[c.id,c])),n=new Set,i=new Set,a=[];function u(c){if(n.has(c))return!0;if(i.has(c))return!1;i.add(c);let d=r.get(c);if(d?.depends_on){for(let w of d.depends_on)if(e.has(w)&&!u(w))return!1}return i.delete(c),n.add(c),d&&a.push(d),!0}for(let c of s)if(!u(c.id))return process.stderr.write(`Warning: dependency cycle detected, using original test order.
|
|
19
|
+
`),s;return a}var Je=Ge(Ze(import.meta.url)),Xe=(()=>{try{return JSON.parse(We(ze(Je,"..","package.json"),"utf-8")).version??"0.0.0"}catch{return"0.0.0"}})();function Qe(){let s=process.argv.slice(2),e="",t="",r="",n="https://api.fasttest.ai",i,a,u,c="chromium",d,w=!1;for(let l=0;l<s.length;l++)switch(s[l]){case"--api-key":e=s[++l]??"";break;case"--suite":r=s[++l]??"";break;case"--suite-id":t=s[++l]??"";break;case"--base-url":n=s[++l]??n;break;case"--app-url":i=s[++l];break;case"--environment":a=s[++l];break;case"--pr-url":u=s[++l];break;case"--browser":c=s[++l]??"chromium";break;case"--test-case-ids":d=(s[++l]??"").split(",").map(m=>m.trim()).filter(Boolean);break;case"--json":w=!0;break}return(!e||!t&&!r)&&(console.error(`Usage: fasttest-ci --api-key <key> --suite "Suite Name" [options]
|
|
20
|
+
fasttest-ci --api-key <key> --suite-id <id> [options]
|
|
21
|
+
|
|
22
|
+
Options:
|
|
23
|
+
--suite Suite name (resolved via API)
|
|
24
|
+
--suite-id Suite ID (alternative to --suite)
|
|
25
|
+
--base-url Cloud API base URL
|
|
26
|
+
--app-url Override application URL
|
|
27
|
+
--environment Named environment (e.g. 'staging')
|
|
28
|
+
--pr-url GitHub PR URL for posting results
|
|
29
|
+
--browser chromium | firefox | webkit
|
|
30
|
+
--test-case-ids Comma-separated test case IDs
|
|
31
|
+
--json Output results as JSON`),process.exit(1)),{apiKey:e,suiteId:t,suiteName:r||void 0,baseUrl:n,appUrl:i,environment:a,prUrl:u,browser:c,testCaseIds:d,json:w}}function Ye(s){let e=s.status==="passed"?"PASSED":"FAILED";console.log(`--- Results: ${e} ---`),console.log(`Execution: ${s.execution_id}`),console.log(`Total: ${s.total} | Passed: ${s.passed} | Failed: ${s.failed} | Skipped: ${s.skipped}`),console.log(`Duration: ${(s.duration_ms/1e3).toFixed(1)}s`),console.log("");for(let t of s.results){let r=t.status==="passed"?"PASS":t.status==="failed"?"FAIL":"SKIP";console.log(` [${r}] ${t.name} (${t.duration_ms}ms)`),t.error&&console.log(` Error: ${t.error}`)}if(s.healed.length>0){console.log(""),console.log(`--- Self-Healed: ${s.healed.length} selector(s) ---`);for(let t of s.healed)console.log(` "${t.test_case}" step ${t.step_index+1}`),console.log(` ${t.original_selector} -> ${t.new_selector}`),console.log(` Strategy: ${t.strategy} (${Math.round(t.confidence*100)}% confidence)`)}}async function et(){let s=Qe(),e=U(s.apiKey.split("_")[1]??"default"),t=new L({browserType:s.browser,headless:!0,orgSlug:e});G=t;let r=new B({apiKey:s.apiKey,baseUrl:s.baseUrl}),n=[];if(console.log(`FastTest CI Runner v${Xe}`),!s.suiteId&&s.suiteName)try{let u=await r.resolveSuite(s.suiteName);s.suiteId=u.id,console.log(`Suite: "${u.name}" \u2192 ${u.id}`)}catch(u){console.error(`Failed to resolve suite "${s.suiteName}": ${u}`),process.exit(1)}else console.log(`Suite: ${s.suiteId}`);console.log(`Browser: ${s.browser}`),s.environment&&console.log(`Environment: ${s.environment}`),s.appUrl&&console.log(`App URL: ${s.appUrl}`),console.log("");let i;if(s.environment)try{let u=await r.resolveEnvironment(s.suiteId,s.environment);i=u.id,console.log(`Resolved environment "${s.environment}" \u2192 ${u.base_url}`)}catch(u){console.error(`Failed to resolve environment "${s.environment}": ${u}`),process.exit(1)}let a;try{a=await ye(t,r,{suiteId:s.suiteId,testCaseIds:s.testCaseIds,appUrlOverride:s.appUrl,environmentId:i},n)}catch(u){console.error(`Fatal: ${u}`),await be(t),process.exit(1)}if(s.json?console.log(JSON.stringify(a,null,2)):Ye(a),s.prUrl)try{let u,c;try{let l=await r.getExecutionDiff(a.execution_id);l.regressions?.length&&(u=l.regressions.map(m=>({name:m.name,previous_status:m.previous_status,current_status:m.current_status,error:m.error}))),l.fixes?.length&&(c=l.fixes.map(m=>({name:m.name,previous_status:m.previous_status,current_status:m.current_status})))}catch{}let w=(await r.postPrComment({pr_url:s.prUrl,execution_id:a.execution_id,status:a.status,total:a.total,passed:a.passed,failed:a.failed,skipped:a.skipped,duration_seconds:Math.round(a.duration_ms/1e3),test_results:a.results.map(l=>({name:l.name,status:l.status,error:l.error})),healed:a.healed.map(l=>({original_selector:l.original_selector,new_selector:l.new_selector,strategy:l.strategy,confidence:l.confidence})),regressions:u,fixes:c})).comment_url;console.log(`
|
|
32
|
+
PR comment posted: ${w??s.prUrl}`)}catch(u){console.error(`
|
|
33
|
+
Failed to post PR comment: ${u}`)}await be(t),process.exit(a.status==="passed"?0:1)}async function be(s){await Promise.race([s.close(),new Promise(e=>setTimeout(e,5e3))])}var G=null;async function ve(s){console.log(`
|
|
34
|
+
${s} received, shutting down\u2026`),G&&await Promise.race([G.close(),new Promise(e=>setTimeout(e,5e3))]),process.exit(0)}process.on("SIGTERM",()=>ve("SIGTERM"));process.on("SIGINT",()=>ve("SIGINT"));et().catch(s=>{console.error("Fatal:",s),process.exit(1)});
|