@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 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 on localhost:3000"
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
- ### Claude Code (recommended)
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
- This registers the MCP server and auto-approves its tools.
48
+ The installer auto-detects your IDE and configures the MCP server.
49
49
 
50
- ### Other IDEs
50
+ ### Manual setup
51
51
 
52
- Add to your MCP configuration:
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: Test, explore, or break a web app with FastTest
3
- argument-hint: "[url] description" (e.g. "localhost:3000 login flow")
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. **Otherwise** → use the `mcp__fasttest__test` tool (default)
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: Test, explore, or break a web app with FastTest
3
- argument-hint: "[url] description" (e.g. "localhost:3000 login flow")
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
- /ftest — FastTest quick command
19
+ /qa — FastTest quick command
20
20
 
21
21
  Usage:
22
- /ftest [url] [description] Test a web app (default)
23
- /ftest explore [url] Discover pages, forms, and flows
24
- /ftest chaos [url] Adversarial testing — try to break it
25
- /ftest shield [url] One-command regression safety net
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
- /ftest localhost:3000 login flow
29
- /ftest explore localhost:3000
30
- /ftest chaos localhost:5173 forms
31
- /ftest shield localhost:3000
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. **Otherwise** → use the `mcp__fasttest__test` tool (default)
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
- - `/ftest localhost:3000 login flow` → test tool, url=`http://localhost:3000`, description=`login flow`
54
- - `/ftest explore http://localhost:3000` → explore tool, url=`http://localhost:3000`
55
- - `/ftest chaos localhost:5173 forms` → chaos tool, url=`http://localhost:5173`, focus=`forms`
56
- - `/ftest shield localhost:3000` → vibe_shield tool, url=`http://localhost:3000`
57
- - `/ftest checkout flow` → test tool, description=`checkout flow` (no URL — the tool will handle it)
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 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]
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: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)});
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)});