@fasttest-ai/qa-agent 1.0.1 → 1.0.3
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 +5 -5
- package/commands/ftest.md +19 -4
- package/commands/qa.md +33 -18
- package/dist/cli.js +22 -22
- package/dist/index.js +55 -41
- package/dist/install.js +31 -33
- package/package.json +6 -8
package/README.md
CHANGED
|
@@ -32,24 +32,24 @@ Your IDE (Claude Code, Cursor, etc.)
|
|
|
32
32
|
└── save_suite, run (cloud features)
|
|
33
33
|
```
|
|
34
34
|
|
|
35
|
-
1. You say "test the login flow
|
|
35
|
+
1. You say "test the login flow"
|
|
36
36
|
2. The `test` tool navigates to the URL, captures a page snapshot, and returns testing instructions
|
|
37
37
|
3. Your coding agent reads the snapshot and drives browser tools to execute tests
|
|
38
38
|
4. Results are reported inline. Optionally, save as a suite for CI/CD.
|
|
39
39
|
|
|
40
40
|
## Installation
|
|
41
41
|
|
|
42
|
-
|
|
42
|
+
Works with Claude Code, Cursor, Windsurf, VS Code Copilot, Codex, and Antigravity:
|
|
43
43
|
|
|
44
44
|
```bash
|
|
45
45
|
npx -y @fasttest-ai/qa-agent install
|
|
46
46
|
```
|
|
47
47
|
|
|
48
|
-
|
|
48
|
+
The installer auto-detects your IDE and configures the MCP server.
|
|
49
49
|
|
|
50
|
-
###
|
|
50
|
+
### Manual setup
|
|
51
51
|
|
|
52
|
-
|
|
52
|
+
If your IDE isn't detected, add this to your MCP configuration:
|
|
53
53
|
|
|
54
54
|
```json
|
|
55
55
|
{
|
package/commands/ftest.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
|
-
description:
|
|
3
|
-
argument-hint: "[url] description"
|
|
2
|
+
description: QA your web app — test flows, find bugs, save & run test suites
|
|
3
|
+
argument-hint: "[test|explore|chaos|shield|run|save|suites|setup] [url] [description]"
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# FastTest Quick Command
|
|
@@ -23,12 +23,19 @@ Usage:
|
|
|
23
23
|
/ftest explore [url] Discover pages, forms, and flows
|
|
24
24
|
/ftest chaos [url] Adversarial testing — try to break it
|
|
25
25
|
/ftest shield [url] One-command regression safety net
|
|
26
|
+
/ftest run [suite name] Run a saved test suite
|
|
27
|
+
/ftest save [suite name] Save tests as a reusable suite
|
|
28
|
+
/ftest suites List saved test suites
|
|
29
|
+
/ftest setup Authenticate with FastTest cloud
|
|
26
30
|
|
|
27
31
|
Examples:
|
|
28
32
|
/ftest localhost:3000 login flow
|
|
29
33
|
/ftest explore localhost:3000
|
|
30
34
|
/ftest chaos localhost:5173 forms
|
|
31
35
|
/ftest shield localhost:3000
|
|
36
|
+
/ftest run checkout flow
|
|
37
|
+
/ftest save login tests
|
|
38
|
+
/ftest suites
|
|
32
39
|
```
|
|
33
40
|
|
|
34
41
|
Do not call any tool. Just print the help text above and stop.
|
|
@@ -40,20 +47,28 @@ Examine the input and determine which mode to use:
|
|
|
40
47
|
1. **If input starts with `explore`** → use the `mcp__fasttest__explore` tool
|
|
41
48
|
2. **If input starts with `chaos` or `break`** → use the `mcp__fasttest__chaos` tool
|
|
42
49
|
3. **If input starts with `shield`** → use the `mcp__fasttest__vibe_shield` tool
|
|
43
|
-
4. **
|
|
50
|
+
4. **If input starts with `run`** → use the `mcp__fasttest__run` tool
|
|
51
|
+
5. **If input starts with `save`** → use the `mcp__fasttest__save_suite` tool
|
|
52
|
+
6. **If input starts with `suites` or `list`** → use the `mcp__fasttest__list_suites` tool
|
|
53
|
+
7. **If input starts with `setup`** → use the `mcp__fasttest__setup` tool
|
|
54
|
+
8. **Otherwise** → use the `mcp__fasttest__test` tool (default)
|
|
44
55
|
|
|
45
56
|
## Parsing the Arguments
|
|
46
57
|
|
|
47
58
|
After removing the mode keyword (if any), the remaining text contains:
|
|
48
59
|
|
|
49
60
|
- **URL** (optional): anything that looks like a URL or `localhost:PORT` pattern. If no protocol, prepend `http://`.
|
|
50
|
-
- **Description** (optional): everything else is the test description.
|
|
61
|
+
- **Description / name** (optional): everything else is the test description or suite name.
|
|
51
62
|
|
|
52
63
|
Examples:
|
|
53
64
|
- `/ftest localhost:3000 login flow` → test tool, url=`http://localhost:3000`, description=`login flow`
|
|
54
65
|
- `/ftest explore http://localhost:3000` → explore tool, url=`http://localhost:3000`
|
|
55
66
|
- `/ftest chaos localhost:5173 forms` → chaos tool, url=`http://localhost:5173`, focus=`forms`
|
|
56
67
|
- `/ftest shield localhost:3000` → vibe_shield tool, url=`http://localhost:3000`
|
|
68
|
+
- `/ftest run checkout flow` → run tool, suite_name=`checkout flow`
|
|
69
|
+
- `/ftest save login tests` → save_suite tool, suite_name=`login tests`
|
|
70
|
+
- `/ftest suites` → list_suites tool (no parameters needed)
|
|
71
|
+
- `/ftest setup` → setup tool (no parameters needed)
|
|
57
72
|
- `/ftest checkout flow` → test tool, description=`checkout flow` (no URL — the tool will handle it)
|
|
58
73
|
|
|
59
74
|
## Execution
|
package/commands/qa.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
|
-
description:
|
|
3
|
-
argument-hint: "[url] description"
|
|
2
|
+
description: QA your web app — test flows, find bugs, save & run test suites
|
|
3
|
+
argument-hint: "[test|explore|chaos|shield|run|save|suites|setup] [url] [description]"
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# FastTest Quick Command
|
|
@@ -16,19 +16,26 @@ $ARGUMENTS
|
|
|
16
16
|
If `$ARGUMENTS` is blank or missing, show this help message and stop (do NOT call any tool):
|
|
17
17
|
|
|
18
18
|
```
|
|
19
|
-
/
|
|
19
|
+
/qa — FastTest quick command
|
|
20
20
|
|
|
21
21
|
Usage:
|
|
22
|
-
/
|
|
23
|
-
/
|
|
24
|
-
/
|
|
25
|
-
/
|
|
22
|
+
/qa [url] [description] Test a web app (default)
|
|
23
|
+
/qa explore [url] Discover pages, forms, and flows
|
|
24
|
+
/qa chaos [url] Adversarial testing — try to break it
|
|
25
|
+
/qa shield [url] One-command regression safety net
|
|
26
|
+
/qa run [suite name] Run a saved test suite
|
|
27
|
+
/qa save [suite name] Save tests as a reusable suite
|
|
28
|
+
/qa suites List saved test suites
|
|
29
|
+
/qa setup Authenticate with FastTest cloud
|
|
26
30
|
|
|
27
31
|
Examples:
|
|
28
|
-
/
|
|
29
|
-
/
|
|
30
|
-
/
|
|
31
|
-
/
|
|
32
|
+
/qa localhost:3000 login flow
|
|
33
|
+
/qa explore localhost:3000
|
|
34
|
+
/qa chaos localhost:5173 forms
|
|
35
|
+
/qa shield localhost:3000
|
|
36
|
+
/qa run checkout flow
|
|
37
|
+
/qa save login tests
|
|
38
|
+
/qa suites
|
|
32
39
|
```
|
|
33
40
|
|
|
34
41
|
Do not call any tool. Just print the help text above and stop.
|
|
@@ -40,21 +47,29 @@ Examine the input and determine which mode to use:
|
|
|
40
47
|
1. **If input starts with `explore`** → use the `mcp__fasttest__explore` tool
|
|
41
48
|
2. **If input starts with `chaos` or `break`** → use the `mcp__fasttest__chaos` tool
|
|
42
49
|
3. **If input starts with `shield`** → use the `mcp__fasttest__vibe_shield` tool
|
|
43
|
-
4. **
|
|
50
|
+
4. **If input starts with `run`** → use the `mcp__fasttest__run` tool
|
|
51
|
+
5. **If input starts with `save`** → use the `mcp__fasttest__save_suite` tool
|
|
52
|
+
6. **If input starts with `suites` or `list`** → use the `mcp__fasttest__list_suites` tool
|
|
53
|
+
7. **If input starts with `setup`** → use the `mcp__fasttest__setup` tool
|
|
54
|
+
8. **Otherwise** → use the `mcp__fasttest__test` tool (default)
|
|
44
55
|
|
|
45
56
|
## Parsing the Arguments
|
|
46
57
|
|
|
47
58
|
After removing the mode keyword (if any), the remaining text contains:
|
|
48
59
|
|
|
49
60
|
- **URL** (optional): anything that looks like a URL or `localhost:PORT` pattern. If no protocol, prepend `http://`.
|
|
50
|
-
- **Description** (optional): everything else is the test description.
|
|
61
|
+
- **Description / name** (optional): everything else is the test description or suite name.
|
|
51
62
|
|
|
52
63
|
Examples:
|
|
53
|
-
- `/
|
|
54
|
-
- `/
|
|
55
|
-
- `/
|
|
56
|
-
- `/
|
|
57
|
-
- `/
|
|
64
|
+
- `/qa localhost:3000 login flow` → test tool, url=`http://localhost:3000`, description=`login flow`
|
|
65
|
+
- `/qa explore http://localhost:3000` → explore tool, url=`http://localhost:3000`
|
|
66
|
+
- `/qa chaos localhost:5173 forms` → chaos tool, url=`http://localhost:5173`, focus=`forms`
|
|
67
|
+
- `/qa shield localhost:3000` → vibe_shield tool, url=`http://localhost:3000`
|
|
68
|
+
- `/qa run checkout flow` → run tool, suite_name=`checkout flow`
|
|
69
|
+
- `/qa save login tests` → save_suite tool, suite_name=`login tests`
|
|
70
|
+
- `/qa suites` → list_suites tool (no parameters needed)
|
|
71
|
+
- `/qa setup` → setup tool (no parameters needed)
|
|
72
|
+
- `/qa checkout flow` → test tool, description=`checkout flow` (no URL — the tool will handle it)
|
|
58
73
|
|
|
59
74
|
## Execution
|
|
60
75
|
|
package/dist/cli.js
CHANGED
|
@@ -1,22 +1,22 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
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
|
|
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(
|
|
5
|
-
`),o={}):o=
|
|
6
|
-
`);continue}if(
|
|
7
|
-
`);let
|
|
8
|
-
`),
|
|
9
|
-
`),
|
|
10
|
-
`))}}else
|
|
11
|
-
`);let
|
|
12
|
-
`),
|
|
13
|
-
`)}
|
|
14
|
-
`)}}let
|
|
15
|
-
`)}catch{}}}}let
|
|
16
|
-
`)}
|
|
17
|
-
`);let
|
|
18
|
-
`),
|
|
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,
|
|
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 Re,firefox as ke,webkit as Te,devices as Ce}from"playwright";import{execFileSync as Ae}from"node:child_process";import*as S from"node:fs";import*as T from"node:path";import*as X from"node:os";var j=T.join(X.homedir(),".fasttest","sessions"),Ee=/^(con|prn|aux|nul|com\d|lpt\d)$/i;function N(s){let t=s.replace(/[\/\\]/g,"_").replace(/\.\./g,"_").replace(/\0/g,"").replace(/^_+|_+$/g,"").replace(/^\./,"_")||"default";return Ee.test(t)?`_${t}`:t}var B=class s{browser=null;context=null;page=null;browserType;headless;orgSlug;deviceName;pendingDialogs=new WeakMap;networkEntries=[];environmentScope=null;constructor(e={}){this.browserType=e.browserType??"chromium",this.headless=e.headless??!0,this.orgSlug=N(e.orgSlug??"default"),this.deviceName=e.device}setOrgSlug(e){this.orgSlug=N(e)}setEnvironmentScope(e){this.environmentScope=e?N(e):null}getEnvironmentScope(){return this.environmentScope}sessionDir(){return this.environmentScope?T.join(j,this.orgSlug,this.environmentScope):T.join(j,this.orgSlug)}resolveSessionPath(e){if(this.environmentScope){let r=T.join(j,this.orgSlug,this.environmentScope,`${e}.json`);if(S.existsSync(r))return r}let t=T.join(j,this.orgSlug,`${e}.json`);return S.existsSync(t)?t:null}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"?ke:this.browserType==="webkit"?Te:Re;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=N(e),r=this.sessionDir();S.mkdirSync(r,{recursive:!0,mode:448});let n=T.join(r,`${t}.json`),i=await this.context.storageState();return S.writeFileSync(n,JSON.stringify(i,null,2),{mode:384}),n}async restoreSession(e){let t=N(e),r=this.resolveSessionPath(t);if(!r){let i=T.join(this.sessionDir(),`${t}.json`);throw new Error(`Session "${e}" not found at ${i}`)}let n=JSON.parse(S.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=N(e);return this.resolveSessionPath(t)!==null}listSessions(){let e=new Set;if(this.environmentScope){let r=T.join(j,this.orgSlug,this.environmentScope);if(S.existsSync(r))for(let n of S.readdirSync(r))n.endsWith(".json")&&e.add(n.replace(/\.json$/,""))}let t=T.join(j,this.orgSlug);if(S.existsSync(t))for(let r of S.readdirSync(t))r.endsWith(".json")&&S.statSync(T.join(t,r)).isFile()&&e.add(r.replace(/\.json$/,""));return[...e]}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;requestStartTimes=new Map;attachNetworkListener(e){e.on("request",t=>{this.requestStartTimes.set(t,Date.now())}),e.on("response",t=>{let r=t.request(),n=r.url();if(!n.startsWith("http"))return;this.networkEntries.length>=s.MAX_NETWORK_ENTRIES&&this.networkEntries.shift();let i=this.requestStartTimes.get(r),a=i?Date.now()-i:0;this.requestStartTimes.delete(r),this.networkEntries.push({url:n,method:r.method(),status:t.status(),duration:a,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 V=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"}},H=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,l=1e3;for(let d=0;d<=a;d++){let c=new AbortController,h=setTimeout(()=>c.abort(),3e4);try{let u={method:e,headers:i,signal:c.signal};r!==void 0&&(u.body=JSON.stringify(r));let f=await fetch(n,u);if(clearTimeout(h),!f.ok){let m=await f.text();if(f.status>=500&&d<a){await new Promise(P=>setTimeout(P,l*2**d));continue}if(f.status===402){let P=m.match(/\((\d+)\/(\d+)\).*plan:\s*(\w+)/i);throw new V(P?.[3]??"unknown",P?parseInt(P[1]):0,P?parseInt(P[2]):0)}throw new Error(`Cloud API ${e} ${t} \u2192 ${f.status}: ${m}`)}return await f.json()}catch(u){if(clearTimeout(h),u instanceof Error&&(u.name==="AbortError"||u.message.includes("fetch failed"))&&d<a){await new Promise(m=>setTimeout(m,l*2**d));continue}throw u}}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,r){let n={name:e};return t&&(n.project_id=t),r&&(n.exact=!0),this.post("/qa/projects/suites/resolve",n)}async getSuiteTestCases(e){return this.get(`/qa/execution/suites/${e}/test-cases`)}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 recordInitialResults(e,t){return this.post("/qa/execution/record-initial",{suite_id:e,results:t})}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 startChaosSession(){return this.post("/qa/chaos/start",{})}async saveChaosReport(e,t){let r=e?`?project_id=${e}`:"";return this.post(`/qa/chaos/reports${r}`,t)}};async function Q(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 Y(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 ee(s,e,t){try{return await s.fill(e,t,{timeout:1e4}),{success:!0}}catch(r){return{success:!1,error:String(r)}}}async function te(s,e){try{return await s.hover(e,{timeout:1e4}),{success:!0}}catch(t){return{success:!1,error:String(t)}}}async function se(s,e,t){try{return await s.selectOption(e,t,{timeout:1e4}),{success:!0}}catch(r){return{success:!1,error:String(r)}}}async function re(s,e,t=1e4){try{return await s.waitForSelector(e,{timeout:t}),{success:!0}}catch(r){return{success:!1,error:String(r)}}}async function ne(s,e=!1){return(await s.screenshot({type:"jpeg",quality:80,fullPage:e})).toString("base64")}async function ie(s){let e=await s.locator("body").ariaSnapshot().catch(()=>"");return{url:s.url(),title:await s.title(),accessibilityTree:e}}async function ae(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 oe(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 ce(s,e){try{return await s.keyboard.press(e),{success:!0}}catch(t){return{success:!1,error:String(t)}}}async function ue(s,e,t){try{return await s.setInputFiles(e,t,{timeout:1e4}),{success:!0}}catch(r){return{success:!1,error:String(r)}}}async function le(s,e){try{return{success:!0,data:{result:await s.evaluate(e)}}}catch(t){return{success:!1,error:String(t)}}}async function de(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 ge(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 pe(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 fe(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(),r=e.url??e.text??"";return{pass:t===r,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??""}}case"evaluate_truthy":{if(!e.expression)return{pass:!1,error:"evaluate_truthy requires 'expression'"};try{let t=await s.evaluate(e.expression);return{pass:!!t,actual:String(t)}}catch(t){return{pass:!1,error:`Evaluation failed: ${String(t)}`}}}default:return{pass:!1,error:`Unknown assertion type: ${e.type}`}}}catch(t){return{pass:!1,error:String(t)}}}var me={data_testid:.98,aria:.95,text:.9,structural:.85,ai:.75};async function we(s,e,t,r,n,i,a,l){if(e)try{let c=await e.post("/qa/healing/classify",{failure_type:r,selector:t,page_url:i,error_message:n,...l?{test_case_id:l}:{}});if(c.is_real_bug)return{healed:!1,error:c.reason??"Classified as real bug"};if(c.pattern){let h=await O(s,c.pattern.healed_value),u=h&&await he(s,c.pattern.healed_value,a);if(h&&u)return{healed:!0,newSelector:c.pattern.healed_value,strategy:c.pattern.strategy,confidence:c.pattern.confidence};c.pattern.id&&Oe(e,c.pattern.id,i)}}catch{}let d=[{name:"data_testid",fn:()=>Ne(s,t)},{name:"aria",fn:()=>Ie(s,t)},{name:"text",fn:()=>qe(s,t)},{name:"structural",fn:()=>Ue(s,t)}];for(let c of d){let h=await c.fn();if(h){if(!await he(s,h,a))continue;return e&&await je(e,r,t,h,c.name,me[c.name]??.8,i),{healed:!0,newSelector:h,strategy:c.name,confidence:me[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 he(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"],l=["button","link","tab","menuitem","checkbox","radio","switch","option"];if(!(a.includes(r.tag)||r.role!=null&&l.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 l=a.match(/['"]([^'"]+)['"]/);if(l){let d=l[1].toLowerCase();if(!(r.text+" "+r.ariaLabel).toLowerCase().includes(d))return!1}}return!0}catch{return!0}}async function Ne(s,e){try{let t=M(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=M(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 qe(s,e){try{let t=M(e);if(!t)return null;let r=[`[title="${t}"]`,`[alt="${t}"]`,`[placeholder="${t}"]`,`role=button[name="${t}"]`,`role=link[name="${t}"]`,`text="${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 r=e.match(/^([a-z]+)/i)?.[1]??"",n=M(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 M(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 je(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 Oe(s,e,t){try{await s.post(`/qa/healing/patterns/${e}/failed`,{page_url:t})}catch{}}var Fe=/\{\{([A-Z][A-Z0-9_]*)\}\}/g;function k(s,e=process.env){let t=[],r=s.replace(Fe,(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 W(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 _e(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 z(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 xe(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,l=n.default_session??void 0,d=t.appUrlOverride??n.base_url??"";if(d)try{d=k(d)}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:[]}}if(n.environment_name)s.setEnvironmentScope(n.environment_name);else if(d)try{let o=new URL(d),g=o.port&&o.port!=="80"&&o.port!=="443"?`${o.hostname}-${o.port}`:o.hostname;s.setEnvironmentScope(g)}catch{}let c=[];for(let o of a)for(let g of z(o.steps,o.assertions))c.includes(g)||c.push(g);if(n.setup){let o=Array.isArray(n.setup)?n.setup:Object.values(n.setup).flat();for(let g of z(o,[]))c.includes(g)||c.push(g)}let h=[l,...a.map(o=>o.session).filter(Boolean)].filter(Boolean);for(let o of h){let g=o.matchAll(/\{\{([A-Z][A-Z0-9_]*)\}\}/g);for(let p of g)c.includes(p[1])||c.push(p[1])}if(c.length>0){let o=[],g=[];for(let p of c)process.env[p]!==void 0?o.push(p):g.push(p);if(o.length>0&&process.stderr.write(`Environment variables resolved: ${o.join(", ")}
|
|
3
|
+
`),g.length>0){let p=`Missing environment variable(s): ${g.join(", ")}. Set these before running tests. In GitHub Actions, add them as repository secrets.`;process.stderr.write(`ERROR: ${p}
|
|
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($=>({id:$.id,name:$.name,status:"failed",duration_ms:0,error:p,step_results:[]})),healed:[]}}}let u=n.setup;if(u){let o;Array.isArray(u)?l?o={[l]:u}:(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=u;for(let[g,p]of Object.entries(o)){if(s.sessionExists(g)){process.stderr.write(`Session "${g}" found locally \u2014 skipping setup.
|
|
6
|
+
`);continue}if(p.length===0)continue;process.stderr.write(`Session "${g}" not found \u2014 running setup (${p.length} steps)...
|
|
7
|
+
`);let $=await s.newContext(),I=!1;for(let E=0;E<p.length;E++){let _;try{_=W(p[E])}catch(D){let A=`Setup "${g}" step ${E+1} failed to resolve variables: ${D}`;process.stderr.write(`ERROR: ${A}
|
|
8
|
+
`),I=!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(q=>({id:q.id,name:q.name,status:"failed",duration_ms:0,error:A,step_results:[]})),healed:[]}}let R=await G($,_,d,s);if(R.page&&($=R.page),!R.success){let D=`Setup "${g}" step ${E+1} (${_.action}) failed: ${R.error}`;process.stderr.write(`ERROR: ${D}
|
|
9
|
+
`),I=!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(A=>({id:A.id,name:A.name,status:"failed",duration_ms:0,error:D,step_results:[]})),healed:[]}}}I||(await s.saveSession(g),process.stderr.write(`Setup complete \u2014 session "${g}" saved.
|
|
10
|
+
`))}}else l&&!s.sessionExists(l)&&process.stderr.write(`Warning: session "${l}" not found and no setup steps defined. Tests will run without auth. Add setup steps to the suite for CI support.
|
|
11
|
+
`);let f=Ve(a);n.previous_statuses&&(f=Ke(f,n.previous_statuses));let m=[],P=[],w=Date.now(),y=!1,C=0,v=new Set,x=new Set(f.map(o=>o.id));for(let o of f){if(o.depends_on&&o.depends_on.length>0){let _=o.depends_on.filter(R=>x.has(R)&&!v.has(R));if(_.length>0){m.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"){y=!0;break}if(_==="paused"){let R=!1,D=Date.now(),A=30*60*1e3;for(;!R;){if(Date.now()-D>A){process.stderr.write(`Pause exceeded 30-minute limit, auto-cancelling.
|
|
12
|
+
`),y=!0;break}await new Promise($e=>setTimeout($e,2e3));let q=await e.checkControlStatus(i);if(q==="running"&&(R=!0),q==="cancelled"){y=!0;break}}if(y)break}}catch{}let g=o.retry_count??0,p,$=0;for(await e.notifyTestStarted(i,o.id,o.name);;){let _=(o.timeout_seconds||30)*1e3,R,D=new Promise((A,q)=>{R=setTimeout(()=>q(new Error(`Test case "${o.name}" timed out after ${o.timeout_seconds||30}s`)),_)});if(p=await Promise.race([Le(s,e,i,o,d,r,P,t.aiFallback,l),D]).finally(()=>clearTimeout(R)).catch(A=>({id:o.id,name:o.name,status:"failed",duration_ms:_,error:String(A),step_results:[]})),p.status==="passed"||$>=g)break;$++,process.stderr.write(`Retrying ${o.name} (attempt ${$}/${g})...
|
|
13
|
+
`)}p.retry_attempts=$,p.status==="passed"&&v.add(o.id),m.push(p);let I=s.getNetworkSummary();s.clearNetworkEntries();let E=Me(I);try{await e.reportResult(i,{test_case_id:o.id,status:p.status,duration_ms:p.duration_ms,error_message:p.error,console_logs:r.slice(-50),retry_attempt:$,step_results:p.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:E.length>0?E:void 0})}catch(_){C++,process.stderr.write(`Failed to report result for ${o.name}: ${_}
|
|
14
|
+
`)}}let U=new Set(m.map(o=>o.id));for(let o of a)U.has(o.id)||m.push({id:o.id,name:o.name,status:"skipped",duration_ms:0,step_results:[]});let F=.9;if(P.length>0){let o=new Set;for(let g of P){if(g.confidence<F)continue;let p=`${g.test_case_id}:${g.original_selector}`;if(!o.has(p)){o.add(p);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 b=m.filter(o=>o.status==="passed").length,L=m.filter(o=>o.status==="failed").length,K=m.filter(o=>o.status==="skipped").length,Pe=Date.now()-w;try{await e.completeExecution(i,y?"cancelled":void 0)}catch(o){process.stderr.write(`Failed to complete execution: ${o}
|
|
16
|
+
`)}C>0&&process.stderr.write(`Warning: ${C} result report(s) failed to send to cloud.
|
|
17
|
+
`);let J;if(t.aiFallback)for(let o of m){if(o.status!=="failed")continue;let g=o.step_results.find(p=>!p.success&&p.ai_context);if(g?.ai_context){let $=f.find(I=>I.id===o.id)?.steps[g.step_index]??{};J={test_case_id:o.id,test_case_name:o.name,step_index:g.step_index,step:$,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:y?"cancelled":L===0?"passed":"failed",total:a.length,passed:b,failed:L,skipped:K,duration_ms:Pe,results:m,healed:P,ai_fallback:J}}async function Le(s,e,t,r,n,i,a,l,d){let c=[],h=Date.now();try{let u=r.session??d,f;if(u)try{f=k(u)}catch(w){if(/\{\{[A-Z_]+\}\}/.test(u))return{id:r.id,name:r.name,status:"failed",duration_ms:Date.now()-h,error:`Session name "${u}" contains unresolved variable: ${w}`,step_results:[]};f=u}let m;if(f)try{m=await s.restoreSession(f)}catch(w){process.stderr.write(`Warning: session "${f}" not found, using fresh context: ${w}
|
|
18
|
+
`),m=await s.newContext()}else m=await s.newContext();let P=w=>{i.push(`[${w.type()}] ${w.text()}`)};m.on("console",P);for(let w=0;w<r.steps.length;w++){let y=r.steps[w],C=Date.now(),v;try{v=W(y)}catch(b){return c.push({step_index:w,action:y.action,success:!1,error:String(b),duration_ms:Date.now()-C}),{id:r.id,name:r.name,status:"failed",duration_ms:Date.now()-h,error:`Step ${w+1} (${y.action}) failed: ${String(b)}`,step_results:c}}let x=await G(m,v,n,s);if(x.page&&(m=x.page),!x.success&&v.selector&&Be(x.error)){await e.notifyHealingStarted(t,r.id,v.selector);let b=await we(m,e,v.selector,He(x.error),x.error??"unknown",m.url(),{action:v.action,description:v.description,intent:v.intent},r.id);if(b.healed&&b.newSelector){let L={...v,selector:b.newSelector};if(x=await G(m,L,n,s),x.success){a.push({test_case_id:r.id,test_case:r.name,step_index:w,original_selector:y.selector,new_selector:b.newSelector,strategy:b.strategy??"unknown",confidence:b.confidence??0});let K=await ye(m);c.push({step_index:w,action:y.action,success:!0,duration_ms:Date.now()-C,screenshot_url:K?.dataUrl,healed:!0,heal_details:{original_selector:y.selector,new_selector:b.newSelector,strategy:b.strategy??"unknown",confidence:b.confidence??0}});continue}}}let U=await ye(m),F;if(!x.success&&l)try{let b=await ie(m);F={intent:v.intent??v.description,page_url:m.url(),snapshot:b}}catch{}if(c.push({step_index:w,action:y.action,success:x.success,error:x.error,duration_ms:Date.now()-C,screenshot_url:U?.dataUrl,ai_context:F}),!x.success)return{id:r.id,name:r.name,status:"failed",duration_ms:Date.now()-h,error:`Step ${w+1} (${y.action}) failed: ${x.error}`,step_results:c}}for(let w=0;w<r.assertions.length;w++){let y=r.assertions[w],C=Date.now(),v;try{v=_e(y)}catch(U){return c.push({step_index:r.steps.length+w,action:`assert:${y.type}`,success:!1,error:String(U),duration_ms:Date.now()-C}),{id:r.id,name:r.name,status:"failed",duration_ms:Date.now()-h,error:`Assertion ${w+1} (${y.type}) failed: ${String(U)}`,step_results:c}}let x=await ve(m,v);if(c.push({step_index:r.steps.length+w,action:`assert:${y.type}`,success:x.pass,error:x.error,duration_ms:Date.now()-C}),!x.pass)return{id:r.id,name:r.name,status:"failed",duration_ms:Date.now()-h,error:`Assertion ${w+1} (${y.type}) failed: ${x.error??"expected value mismatch"}`,step_results:c}}return{id:r.id,name:r.name,status:"passed",duration_ms:Date.now()-h,step_results:c}}catch(u){return{id:r.id,name:r.name,status:"failed",duration_ms:Date.now()-h,error:String(u),step_results:c}}}async function ye(s){try{return{dataUrl:`data:image/jpeg;base64,${await ne(s,!1)}`}}catch{return}}async function G(s,e,t,r){let n=e.action;try{switch(n){case"navigate":{let i=e.url??e.value??"";if(i&&!i.startsWith("http")){if(!t)return{success:!1,error:`Navigate step has a relative URL "${i}" but no base URL is configured. Set a base URL on your project or environment.`};i=t.replace(/\/$/,"")+i}return await Q(s,i)}case"click":return await Y(s,e.selector??"");case"type":case"fill":return await ee(s,e.selector??"",e.value??"");case"fill_form":{let i=e.fields??{};return await pe(s,i)}case"drag":return await de(s,e.selector??"",e.target??"");case"resize":return await ge(s,e.width??1280,e.height??720);case"hover":return await te(s,e.selector??"");case"select":return await se(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 re(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 ce(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 ue(s,e.selector??"",i)}case"evaluate":return await le(s,e.expression??e.value??"");case"go_back":return await ae(s);case"go_forward":return await oe(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 ve(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 ve(s,e){return fe(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,expression:e.expression,description:e.description})}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 l of a.depends_on)r.add(l);let n=[],i=[];for(let a of s){let l=e[a.id],d=a.depends_on?.some(c=>t.has(c))??!1;l==="failed"&&!r.has(a.id)&&!d?n.push(a):i.push(a)}return[...n,...i]}function Ve(s){let e=new Set(s.map(d=>d.id));if(!s.some(d=>d.depends_on&&d.depends_on.some(c=>e.has(c))))return s;let r=new Map(s.map(d=>[d.id,d])),n=new Set,i=new Set,a=[];function l(d){if(n.has(d))return!0;if(i.has(d))return!1;i.add(d);let c=r.get(d);if(c?.depends_on){for(let h of c.depends_on)if(e.has(h)&&!l(h))return!1}return i.delete(d),n.add(d),c&&a.push(c),!0}for(let d of s)if(!l(d.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,l,d="chromium",c,h=!1;for(let u=0;u<s.length;u++)switch(s[u]){case"--api-key":e=s[++u]??"";break;case"--suite":r=s[++u]??"";break;case"--suite-id":t=s[++u]??"";break;case"--base-url":n=s[++u]??n;break;case"--app-url":i=s[++u];break;case"--environment":a=s[++u];break;case"--pr-url":l=s[++u];break;case"--browser":d=s[++u]??"chromium";break;case"--test-case-ids":c=(s[++u]??"").split(",").map(f=>f.trim()).filter(Boolean);break;case"--json":h=!0;break}return(!e||!t&&!r)&&(console.error(`Usage: fasttest-ci --api-key <key> --suite "Suite Name" [options]
|
|
20
20
|
fasttest-ci --api-key <key> --suite-id <id> [options]
|
|
21
21
|
|
|
22
22
|
Options:
|
|
@@ -28,7 +28,7 @@ Options:
|
|
|
28
28
|
--pr-url GitHub PR URL for posting results
|
|
29
29
|
--browser chromium | firefox | webkit
|
|
30
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:
|
|
32
|
-
PR comment posted: ${
|
|
33
|
-
Failed to post PR comment: ${
|
|
34
|
-
${s} received, shutting down\u2026`),
|
|
31
|
+
--json Output results as JSON`),process.exit(1)),{apiKey:e,suiteId:t,suiteName:r||void 0,baseUrl:n,appUrl:i,environment:a,prUrl:l,browser:d,testCaseIds:c,json:h}}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=N(s.apiKey.split("_")[1]??"default"),t=new B({browserType:s.browser,headless:!0,orgSlug:e});Z=t;let r=new H({apiKey:s.apiKey,baseUrl:s.baseUrl}),n=[];if(console.log(`FastTest CI Runner v${Xe}`),!s.suiteId&&s.suiteName)try{let l=await r.resolveSuite(s.suiteName);s.suiteId=l.id,console.log(`Suite: "${l.name}" \u2192 ${l.id}`)}catch(l){console.error(`Failed to resolve suite "${s.suiteName}": ${l}`),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 l=await r.resolveEnvironment(s.suiteId,s.environment);i=l.id,console.log(`Resolved environment "${s.environment}" \u2192 ${l.base_url}`)}catch(l){console.error(`Failed to resolve environment "${s.environment}": ${l}`),process.exit(1)}let a;try{a=await xe(t,r,{suiteId:s.suiteId,testCaseIds:s.testCaseIds,appUrlOverride:s.appUrl,environmentId:i},n)}catch(l){console.error(`Fatal: ${l}`),await be(t),process.exit(1)}if(s.json?console.log(JSON.stringify(a,null,2)):Ye(a),s.prUrl)try{let l,d;try{let u=await r.getExecutionDiff(a.execution_id);u.regressions?.length&&(l=u.regressions.map(f=>({name:f.name,previous_status:f.previous_status,current_status:f.current_status,error:f.error}))),u.fixes?.length&&(d=u.fixes.map(f=>({name:f.name,previous_status:f.previous_status,current_status:f.current_status})))}catch{}let h=(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(u=>({name:u.name,status:u.status,error:u.error})),healed:a.healed.map(u=>({original_selector:u.original_selector,new_selector:u.new_selector,strategy:u.strategy,confidence:u.confidence})),regressions:l,fixes:d})).comment_url;console.log(`
|
|
32
|
+
PR comment posted: ${h??s.prUrl}`)}catch(l){console.error(`
|
|
33
|
+
Failed to post PR comment: ${l}`)}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 Z=null;async function Se(s){console.log(`
|
|
34
|
+
${s} received, shutting down\u2026`),Z&&await Promise.race([Z.close(),new Promise(e=>setTimeout(e,5e3))]),process.exit(0)}process.on("SIGTERM",()=>Se("SIGTERM"));process.on("SIGINT",()=>Se("SIGINT"));et().catch(s=>{console.error("Fatal:",s),process.exit(1)});
|