@testrelic/playwright-analytics 2.14.0 → 2.16.0-next.121

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.
@@ -1,10 +1,10 @@
1
- 'use strict';var crypto=require('crypto'),fs$1=require('fs'),path=require('path'),os$1=require('os'),core=require('@testrelic/core'),child_process=require('child_process'),http=require('http'),readline=require('readline'),promises=require('fs/promises'),zlib=require('zlib'),util=require('util'),stream=require('stream');var pn=(t=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(t,{get:(e,r)=>(typeof require<"u"?require:e)[r]}):t)(function(t){if(typeof require<"u")return require.apply(this,arguments);throw Error('Dynamic require of "'+t+'" is not supported')});var hn=".testrelic-report";function Re(t,e,r,n){let a=`${t}|${e.join("|")}|${r}|${n}`;return crypto.createHash("sha256").update(a).digest("hex").substring(0,12)}var X=class{constructor(e){this.writeErrors=[];this.totalBytesWritten=0;this.reportDir=path.join(e,hn),this.testsDir=path.join(this.reportDir,"tests"),fs$1.mkdirSync(this.testsDir,{recursive:true});}getReportDir(){return this.reportDir}getWriteErrors(){return this.writeErrors}writeTestDetail(e,r,n){try{let a=path.join(this.testsDir,e);fs$1.mkdirSync(a,{recursive:!0});let s=path.join(a,"meta.json"),l=s+".tmp",i=JSON.stringify(r);return fs$1.writeFileSync(l,i,"utf-8"),fs$1.renameSync(l,s),this.totalBytesWritten+=Buffer.byteLength(i,"utf-8"),n?.networkRequestsFile&&this.moveFile(n.networkRequestsFile,path.join(a,"network.jsonl")),n?.consoleLogsFile&&this.moveFile(n.consoleLogsFile,path.join(a,"console.jsonl")),n?.apiCallsFile&&this.moveFile(n.apiCallsFile,path.join(a,"api-calls.jsonl")),!0}catch(a){let s=a instanceof Error?a.message:String(a);return this.writeErrors.push({testId:e,error:s,timestamp:new Date().toISOString()}),process.stderr.write(`[testrelic] Failed to write test detail for ${e}: ${s}
2
- `),false}}moveFile(e,r){try{fs$1.renameSync(e,r);}catch{try{let{copyFileSync:n}=pn("fs");n(e,r);try{fs$1.unlinkSync(e);}catch{}}catch(n){process.stderr.write(`[testrelic] Failed to move file ${e} \u2192 ${r}: ${n instanceof Error?n.message:String(n)}
3
- `);}}}writeIndex(e){let r=path.join(this.reportDir,"index.json"),n=r+".tmp",a=JSON.stringify(e);fs$1.writeFileSync(n,a,"utf-8"),fs$1.renameSync(n,r),this.totalBytesWritten+=Buffer.byteLength(a,"utf-8");}writeCompactIndex(e){let r=path.join(this.reportDir,"index-compact.json"),n=r+".tmp",a=e.map(({titlePath:l,project:i,...d})=>d),s=JSON.stringify(a);fs$1.writeFileSync(n,s,"utf-8"),fs$1.renameSync(n,r),this.totalBytesWritten+=Buffer.byteLength(s,"utf-8");}writeSummary(e){let r=path.join(this.reportDir,"summary.json"),n=r+".tmp",a=JSON.stringify(e);fs$1.writeFileSync(n,a,"utf-8"),fs$1.renameSync(n,r),this.totalBytesWritten+=Buffer.byteLength(a,"utf-8");}writeManifest(e){let r=path.join(this.reportDir,"manifest.json"),n=r+".tmp",a=JSON.stringify(e);fs$1.writeFileSync(n,a,"utf-8"),fs$1.renameSync(n,r),this.totalBytesWritten+=Buffer.byteLength(a,"utf-8");}computeTotalSize(){return this.totalBytesWritten}dispose(){}};var Dt=".testrelic",wn="testrelic-config.json",kn=".testrelic",Tn=5,Sn=new Set(["__proto__","constructor","prototype"]),Nt=Object.freeze({apiKey:null,endpoint:"https://platform.testrelic.ai/api/v1",uploadStrategy:"batch",timeout:3e4,uploadBudgetMs:12e4,projectName:null,queueMaxAge:6048e5,queueDirectory:`${Dt}/queue`,uploadArtifacts:true,artifactMaxSizeMb:50}),Cn={s:1e3,m:60*1e3,h:3600*1e3,d:1440*60*1e3};function Bt(t){let e=path.resolve(t);for(let r=0;r<=Tn;r++){let n=path.join(e,Dt,wn);if(fs$1.existsSync(n))try{if(fs$1.statSync(n).isFile())return n}catch{}let a=path.join(e,kn);if(fs$1.existsSync(a))try{if(fs$1.statSync(a).isFile())return process.stderr.write(`[testrelic] Deprecation: config file ".testrelic" has moved to ".testrelic/testrelic-config.json". Please migrate your config file to the new location.
4
- `),a}catch{}let s=path.dirname(e);if(s===e)break;e=s;}return null}function Ot(t){try{let e=fs$1.readFileSync(t,"utf-8"),r=JSON.parse(e);return typeof r!="object"||r===null||Array.isArray(r)||!Ut(r)?null:r}catch{return null}}function Ut(t){for(let e of Object.keys(t)){if(Sn.has(e))return false;let r=t[e];if(typeof r=="object"&&r!==null&&!Array.isArray(r)&&!Ut(r))return false}return true}function qt(t){let e=/^\$\{([A-Za-z_][A-Za-z0-9_]*)\}$/.exec(t);if(e)return process.env[e[1]]??null;let r=/^\$([A-Za-z_][A-Za-z0-9_]*)$/.exec(t);return r?process.env[r[1]]??null:t}function Ae(t){let e=Object.create(null);for(let r of Object.keys(t)){let n=t[r];typeof n=="string"&&n.startsWith("$")?e[r]=qt(n):typeof n=="object"&&n!==null&&!Array.isArray(n)?e[r]=Ae(n):e[r]=n;}return e}function Ft(t){let e=/^(\d+)\s*([smhd])$/.exec(t.trim());if(!e)return null;let r=parseInt(e[1],10),n=e[2],a=Cn[n];return !a||r<=0?null:r*a}function Ht(t,e){let r=Object.create(null);if(Object.assign(r,Nt),t){let d=t.cloud;d&&typeof d=="object"&&(typeof d.endpoint=="string"&&(r.endpoint=d.endpoint),typeof d.upload=="string"&&(r.uploadStrategy=d.upload),typeof d.timeout=="number"&&(r.timeout=d.timeout),typeof d.uploadBudgetMs=="number"&&(r.uploadBudgetMs=d.uploadBudgetMs),typeof d.apiKey=="string"&&d.apiKey.length>0&&(r.apiKey=d.apiKey));let o=t["testrelic-repo"]??t.project;o&&typeof o=="object"&&typeof o.name=="string"&&(r.projectName=o.name),t.project&&!t["testrelic-repo"]&&process.stderr.write(`[testrelic] Deprecation: "project" key in config is deprecated. Rename it to "testrelic-repo".
5
- `);let c=t.queue;if(c&&typeof c=="object"){if(typeof c.maxAge=="string"){let g=Ft(c.maxAge);g!==null&&(r.queueMaxAge=g);}typeof c.directory=="string"&&(r.queueDirectory=c.directory);}}if(e){if(typeof e.apiKey=="string"&&e.apiKey.length>0){let d=e.apiKey.startsWith("$")?qt(e.apiKey):e.apiKey;d&&(r.apiKey=d);}if(typeof e.endpoint=="string"&&(r.endpoint=e.endpoint),typeof e.upload=="string"&&(r.uploadStrategy=e.upload),typeof e.timeout=="number"&&(r.timeout=e.timeout),typeof e.uploadBudgetMs=="number"&&(r.uploadBudgetMs=e.uploadBudgetMs),typeof e.projectName=="string"&&(r.projectName=e.projectName),typeof e.queueMaxAge=="string"){let d=Ft(e.queueMaxAge);d!==null&&(r.queueMaxAge=d);}typeof e.queueDirectory=="string"&&(r.queueDirectory=e.queueDirectory),typeof e.uploadArtifacts=="boolean"&&(r.uploadArtifacts=e.uploadArtifacts),typeof e.artifactMaxSizeMb=="number"&&(r.artifactMaxSizeMb=e.artifactMaxSizeMb);}let n=process.env.TESTRELIC_API_KEY;n&&n.length>0&&(r.apiKey=n);let a=process.env.TESTRELIC_CLOUD_ENDPOINT;a&&core.isValidEndpointUrl(a)&&(r.endpoint=a);let s=process.env.TESTRELIC_UPLOAD_STRATEGY;s&&["batch","realtime","both"].includes(s)&&(r.uploadStrategy=s);let l=process.env.TESTRELIC_CLOUD_TIMEOUT;if(l){let d=parseInt(l,10);!isNaN(d)&&d>=1e3&&d<=12e4&&(r.timeout=d);}let i=Object.freeze(r);return core.isValidCloudConfig(i)?i:Nt}var In=[/AKIA[A-Z0-9]{16}/g,/Bearer\s+[A-Za-z0-9\-._~+/]+=*/g,/-----BEGIN\s+(RSA\s+)?PRIVATE\sKEY-----[\s\S]*?-----END/g,/\/\/[^:]+:[^@]+@/g],Mn=["authorization","cookie","set-cookie","x-api-key"],En=["password","secret","token","apiKey","api_key"];function zt(t){let e=Object.create(null);return e.trackApiCalls=t?.trackApiCalls??true,e.captureRequestHeaders=t?.captureRequestHeaders??true,e.captureResponseHeaders=t?.captureResponseHeaders??true,e.captureRequestBody=t?.captureRequestBody??true,e.captureResponseBody=t?.captureResponseBody??true,e.captureAssertions=t?.captureAssertions??true,e.redactHeaders=t?.redactHeaders??[...Mn],e.redactBodyFields=t?.redactBodyFields??[...En],e.apiIncludeUrls=t?.apiIncludeUrls??[],e.apiExcludeUrls=t?.apiExcludeUrls??[],Object.freeze(e)}function $t(t){if(t!==void 0&&!core.isValidConfig(t))throw core.createError(core.ErrorCode.CONFIG_INVALID,"Invalid reporter configuration");let e=Object.create(null);e.outputPath=t?.outputPath??"./test-results/analytics-timeline.json",e.includeStackTrace=t?.includeStackTrace??false,e.includeCodeSnippets=t?.includeCodeSnippets??true,e.codeContextLines=t?.codeContextLines??3,e.includeNetworkStats=t?.includeNetworkStats??true,e.captureNavigation=t?.captureNavigation??true,e.navigationTypes=t?.navigationTypes??null,e.redactPatterns=[...In,...t?.redactPatterns??[]],e.testRunId=t?.testRunId??null,e.metadata=t?.metadata??null;let r=e.outputPath;e.openReport=t?.openReport??true,e.htmlReportPath=t?.htmlReportPath??r.replace(/\.json$/,".html"),e.includeArtifacts=t?.includeArtifacts??true,e.trackApiCalls=t?.trackApiCalls??true,e.quiet=t?.quiet??false,e.includeActionSteps=t?.includeActionSteps??true,e.captureConsoleLogs=t?.captureConsoleLogs??true;let n=Bt(process.cwd()),a=n?Ot(n):null,s=a?Ae(a):null,l=Ht(s,t?.cloud);return e.cloud=l.apiKey?l:null,e.reportMode=t?.reportMode??"streaming",e.streamingThreshold=t?.streamingThreshold??0,Object.freeze(e)}var _e="1.3.0";function jt(t,e,r){try{let a=fs$1.readFileSync(t,"utf-8").split(`
6
- `);if(e<1||e>a.length)return null;let s=Math.max(1,e-r),l=Math.min(a.length,e+r),i=[];for(let d=s;d<=l;d++){let o=d===e?">":" ",c=String(d).padStart(String(l).length," ");i.push(`${o} ${c} | ${a[d-1]}`);}return i.join(`
7
- `)}catch{return null}}function Vt(t){return e=>{let r=e;for(let n of t)if(typeof n=="string"){let a=n.replace(/[.*+?^${}()|[\]\\]/g,"\\$&");r=r.replace(new RegExp(a,"g"),"[REDACTED]");}else {let a=n.flags.includes("g")?n.flags:n.flags+"g",s=new RegExp(n.source,a);r=r.replace(s,"[REDACTED]");}return r}}function Pn(t){return t.GITHUB_ACTIONS!=="true"?null:{provider:"github-actions",buildId:t.GITHUB_RUN_ID??null,commitSha:t.GITHUB_SHA??null,branch:t.GITHUB_REF_NAME??null,runUrl:t.GITHUB_SERVER_URL&&t.GITHUB_REPOSITORY&&t.GITHUB_RUN_ID?`${t.GITHUB_SERVER_URL}/${t.GITHUB_REPOSITORY}/actions/runs/${t.GITHUB_RUN_ID}`:null}}function Nn(t){return t.GITLAB_CI!=="true"?null:{provider:"gitlab-ci",buildId:t.CI_PIPELINE_ID??null,commitSha:t.CI_COMMIT_SHA??null,branch:t.CI_COMMIT_BRANCH??t.CI_COMMIT_REF_NAME??null,runUrl:t.CI_PIPELINE_URL??null}}function Fn(t){if(!t.JENKINS_URL)return null;let e=t.GIT_BRANCH??null;return e?.startsWith("origin/")&&(e=e.slice(7)),{provider:"jenkins",buildId:t.BUILD_ID??null,commitSha:t.GIT_COMMIT??null,branch:e,runUrl:t.BUILD_URL??null}}function Dn(t){return t.CIRCLECI!=="true"?null:{provider:"circleci",buildId:t.CIRCLE_BUILD_NUM??null,commitSha:t.CIRCLE_SHA1??null,branch:t.CIRCLE_BRANCH??null,runUrl:t.CIRCLE_BUILD_URL??null}}function Bn(t){if(!t.BITBUCKET_PIPELINE_UUID)return null;let e=t.BITBUCKET_WORKSPACE&&t.BITBUCKET_REPO_SLUG&&t.BITBUCKET_PIPELINE_UUID?`https://bitbucket.org/${t.BITBUCKET_WORKSPACE}/${t.BITBUCKET_REPO_SLUG}/pipelines/results/${t.BITBUCKET_PIPELINE_UUID}`:null;return {provider:"bitbucket-pipelines",buildId:t.BITBUCKET_BUILD_NUMBER??null,commitSha:t.BITBUCKET_COMMIT??null,branch:t.BITBUCKET_BRANCH??null,runUrl:e}}var On=[Pn,Nn,Fn,Dn,Bn];function P(t){let e=process.env;for(let r of On){let n=r(e);if(n)return n}return null}var Ie=`
1
+ 'use strict';var crypto=require('crypto'),fs$1=require('fs'),path=require('path'),os$1=require('os'),core=require('@testrelic/core'),child_process=require('child_process'),http=require('http'),readline=require('readline'),promises=require('fs/promises'),zlib=require('zlib'),util=require('util'),stream=require('stream');var nn=(e=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(e,{get:(t,r)=>(typeof require<"u"?require:t)[r]}):e)(function(e){if(typeof require<"u")return require.apply(this,arguments);throw Error('Dynamic require of "'+e+'" is not supported')});var on=".testrelic-report";function Be(e,t,r,a){let n=`${e}|${t.join("|")}|${r}|${a}`;return crypto.createHash("sha256").update(n).digest("hex").substring(0,12)}function Oe(e,t,r){let a=`${e}|${t.join("|")}|${r}`;return crypto.createHash("sha256").update(a).digest("hex").substring(0,12)}var ie=class{constructor(t){this.writeErrors=[];this.totalBytesWritten=0;this.reportDir=path.join(t,on),this.testsDir=path.join(this.reportDir,"tests"),fs$1.mkdirSync(this.testsDir,{recursive:true});}getReportDir(){return this.reportDir}getWriteErrors(){return this.writeErrors}writeTestDetail(t,r,a){try{let n=path.join(this.testsDir,t);fs$1.mkdirSync(n,{recursive:!0});let s=path.join(n,"meta.json"),l=s+".tmp",i=JSON.stringify(r);return fs$1.writeFileSync(l,i,"utf-8"),fs$1.renameSync(l,s),this.totalBytesWritten+=Buffer.byteLength(i,"utf-8"),a?.networkRequestsFile&&this.moveFile(a.networkRequestsFile,path.join(n,"network.jsonl")),a?.consoleLogsFile&&this.moveFile(a.consoleLogsFile,path.join(n,"console.jsonl")),a?.apiCallsFile&&this.moveFile(a.apiCallsFile,path.join(n,"api-calls.jsonl")),!0}catch(n){let s=n instanceof Error?n.message:String(n);return this.writeErrors.push({testId:t,error:s,timestamp:new Date().toISOString()}),process.stderr.write(`[testrelic] Failed to write test detail for ${t}: ${s}
2
+ `),false}}moveFile(t,r){try{fs$1.renameSync(t,r);}catch{try{let{copyFileSync:a}=nn("fs");a(t,r);try{fs$1.unlinkSync(t);}catch{}}catch(a){process.stderr.write(`[testrelic] Failed to move file ${t} \u2192 ${r}: ${a instanceof Error?a.message:String(a)}
3
+ `);}}}writeIndex(t){let r=path.join(this.reportDir,"index.json"),a=r+".tmp",n=JSON.stringify(t);fs$1.writeFileSync(a,n,"utf-8"),fs$1.renameSync(a,r),this.totalBytesWritten+=Buffer.byteLength(n,"utf-8");}writeCompactIndex(t){let r=path.join(this.reportDir,"index-compact.json"),a=r+".tmp",n=t.map(({titlePath:l,project:i,...d})=>d),s=JSON.stringify(n);fs$1.writeFileSync(a,s,"utf-8"),fs$1.renameSync(a,r),this.totalBytesWritten+=Buffer.byteLength(s,"utf-8");}writeSummary(t){let r=path.join(this.reportDir,"summary.json"),a=r+".tmp",n=JSON.stringify(t);fs$1.writeFileSync(a,n,"utf-8"),fs$1.renameSync(a,r),this.totalBytesWritten+=Buffer.byteLength(n,"utf-8");}writeManifest(t){let r=path.join(this.reportDir,"manifest.json"),a=r+".tmp",n=JSON.stringify(t);fs$1.writeFileSync(a,n,"utf-8"),fs$1.renameSync(a,r),this.totalBytesWritten+=Buffer.byteLength(n,"utf-8");}computeTotalSize(){return this.totalBytesWritten}dispose(){}};var or=".testrelic",fn="testrelic-config.json",gn=".testrelic",hn=5,mn=new Set(["__proto__","constructor","prototype"]),sr=Object.freeze({apiKey:null,endpoint:"https://platform.testrelic.ai/api/v1",uploadStrategy:"batch",timeout:3e4,uploadBudgetMs:12e4,projectName:null,queueMaxAge:6048e5,queueDirectory:`${or}/queue`,uploadArtifacts:true,artifactMaxSizeMb:50}),vn={s:1e3,m:60*1e3,h:3600*1e3,d:1440*60*1e3};function lr(e){let t=path.resolve(e);for(let r=0;r<=hn;r++){let a=path.join(t,or,fn);if(fs$1.existsSync(a))try{if(fs$1.statSync(a).isFile())return a}catch{}let n=path.join(t,gn);if(fs$1.existsSync(n))try{if(fs$1.statSync(n).isFile())return process.stderr.write(`[testrelic] Deprecation: config file ".testrelic" has moved to ".testrelic/testrelic-config.json". Please migrate your config file to the new location.
4
+ `),n}catch{}let s=path.dirname(t);if(s===t)break;t=s;}return null}function dr(e){try{let t=fs$1.readFileSync(e,"utf-8"),r=JSON.parse(t);return typeof r!="object"||r===null||Array.isArray(r)||!cr(r)?null:r}catch{return null}}function cr(e){for(let t of Object.keys(e)){if(mn.has(t))return false;let r=e[t];if(typeof r=="object"&&r!==null&&!Array.isArray(r)&&!cr(r))return false}return true}function ur(e){let t=/^\$\{([A-Za-z_][A-Za-z0-9_]*)\}$/.exec(e);if(t)return process.env[t[1]]??null;let r=/^\$([A-Za-z_][A-Za-z0-9_]*)$/.exec(e);return r?process.env[r[1]]??null:e}function Ue(e){let t=Object.create(null);for(let r of Object.keys(e)){let a=e[r];typeof a=="string"&&a.startsWith("$")?t[r]=ur(a):typeof a=="object"&&a!==null&&!Array.isArray(a)?t[r]=Ue(a):t[r]=a;}return t}function ir(e){let t=/^(\d+)\s*([smhd])$/.exec(e.trim());if(!t)return null;let r=parseInt(t[1],10),a=t[2],n=vn[a];return !n||r<=0?null:r*n}function pr(e,t){let r=Object.create(null);if(Object.assign(r,sr),e){let d=e.cloud;d&&typeof d=="object"&&(typeof d.endpoint=="string"&&(r.endpoint=d.endpoint),typeof d.upload=="string"&&(r.uploadStrategy=d.upload),typeof d.timeout=="number"&&(r.timeout=d.timeout),typeof d.uploadBudgetMs=="number"&&(r.uploadBudgetMs=d.uploadBudgetMs),typeof d.apiKey=="string"&&d.apiKey.length>0&&(r.apiKey=d.apiKey));let o=e["testrelic-repo"]??e.project;o&&typeof o=="object"&&typeof o.name=="string"&&(r.projectName=o.name),e.project&&!e["testrelic-repo"]&&process.stderr.write(`[testrelic] Deprecation: "project" key in config is deprecated. Rename it to "testrelic-repo".
5
+ `);let c=e.queue;if(c&&typeof c=="object"){if(typeof c.maxAge=="string"){let p=ir(c.maxAge);p!==null&&(r.queueMaxAge=p);}typeof c.directory=="string"&&(r.queueDirectory=c.directory);}}if(t){if(typeof t.apiKey=="string"&&t.apiKey.length>0){let d=t.apiKey.startsWith("$")?ur(t.apiKey):t.apiKey;d&&(r.apiKey=d);}if(typeof t.endpoint=="string"&&(r.endpoint=t.endpoint),typeof t.upload=="string"&&(r.uploadStrategy=t.upload),typeof t.timeout=="number"&&(r.timeout=t.timeout),typeof t.uploadBudgetMs=="number"&&(r.uploadBudgetMs=t.uploadBudgetMs),typeof t.projectName=="string"&&(r.projectName=t.projectName),typeof t.queueMaxAge=="string"){let d=ir(t.queueMaxAge);d!==null&&(r.queueMaxAge=d);}typeof t.queueDirectory=="string"&&(r.queueDirectory=t.queueDirectory),typeof t.uploadArtifacts=="boolean"&&(r.uploadArtifacts=t.uploadArtifacts),typeof t.artifactMaxSizeMb=="number"&&(r.artifactMaxSizeMb=t.artifactMaxSizeMb);}let a=process.env.TESTRELIC_API_KEY;a&&a.length>0&&(r.apiKey=a);let n=process.env.TESTRELIC_CLOUD_ENDPOINT;n&&core.isValidEndpointUrl(n)&&(r.endpoint=n);let s=process.env.TESTRELIC_UPLOAD_STRATEGY;s&&["batch","realtime","both"].includes(s)&&(r.uploadStrategy=s);let l=process.env.TESTRELIC_CLOUD_TIMEOUT;if(l){let d=parseInt(l,10);!isNaN(d)&&d>=1e3&&d<=12e4&&(r.timeout=d);}let i=Object.freeze(r);return core.isValidCloudConfig(i)?i:sr}var wn=[/AKIA[A-Z0-9]{16}/g,/Bearer\s+[A-Za-z0-9\-._~+/]+=*/g,/-----BEGIN\s+(RSA\s+)?PRIVATE\sKEY-----[\s\S]*?-----END/g,/\/\/[^:]+:[^@]+@/g],kn=["authorization","cookie","set-cookie","x-api-key"],Sn=["password","secret","token","apiKey","api_key"];function fr(e){let t=Object.create(null);return t.trackApiCalls=e?.trackApiCalls??true,t.captureRequestHeaders=e?.captureRequestHeaders??true,t.captureResponseHeaders=e?.captureResponseHeaders??true,t.captureRequestBody=e?.captureRequestBody??true,t.captureResponseBody=e?.captureResponseBody??true,t.captureAssertions=e?.captureAssertions??true,t.redactHeaders=e?.redactHeaders??[...kn],t.redactBodyFields=e?.redactBodyFields??[...Sn],t.apiIncludeUrls=e?.apiIncludeUrls??[],t.apiExcludeUrls=e?.apiExcludeUrls??[],Object.freeze(t)}function gr(e){if(e!==void 0&&!core.isValidConfig(e))throw core.createError(core.ErrorCode.CONFIG_INVALID,"Invalid reporter configuration");let t=Object.create(null);t.outputPath=e?.outputPath??"./test-results/analytics-timeline.json",t.includeStackTrace=e?.includeStackTrace??false,t.includeCodeSnippets=e?.includeCodeSnippets??true,t.codeContextLines=e?.codeContextLines??3,t.includeNetworkStats=e?.includeNetworkStats??true,t.captureNavigation=e?.captureNavigation??true,t.navigationTypes=e?.navigationTypes??null,t.redactPatterns=[...wn,...e?.redactPatterns??[]],t.testRunId=e?.testRunId??null,t.metadata=e?.metadata??null;let r=t.outputPath;t.openReport=e?.openReport??true,t.htmlReportPath=e?.htmlReportPath??r.replace(/\.json$/,".html"),t.includeArtifacts=e?.includeArtifacts??true,t.trackApiCalls=e?.trackApiCalls??true,t.quiet=e?.quiet??false,t.includeActionSteps=e?.includeActionSteps??true,t.captureConsoleLogs=e?.captureConsoleLogs??true;let a=lr(process.cwd()),n=a?dr(a):null,s=n?Ue(n):null,l=pr(s,e?.cloud);return t.cloud=l.apiKey?l:null,t.reportMode=e?.reportMode??"streaming",t.streamingThreshold=e?.streamingThreshold??0,Object.freeze(t)}var ze="1.6.0";function hr(e,t,r){try{let n=fs$1.readFileSync(e,"utf-8").split(`
6
+ `);if(t<1||t>n.length)return null;let s=Math.max(1,t-r),l=Math.min(n.length,t+r),i=[];for(let d=s;d<=l;d++){let o=d===t?">":" ",c=String(d).padStart(String(l).length," ");i.push(`${o} ${c} | ${n[d-1]}`);}return i.join(`
7
+ `)}catch{return null}}function mr(e){return t=>{let r=t;for(let a of e)if(typeof a=="string"){let n=a.replace(/[.*+?^${}()|[\]\\]/g,"\\$&");r=r.replace(new RegExp(n,"g"),"[REDACTED]");}else {let n=a.flags.includes("g")?a.flags:a.flags+"g",s=new RegExp(a.source,n);r=r.replace(s,"[REDACTED]");}return r}}function Tn(e){return e.GITHUB_ACTIONS!=="true"?null:{provider:"github-actions",buildId:e.GITHUB_RUN_ID??null,commitSha:e.GITHUB_SHA??null,branch:e.GITHUB_REF_NAME??null,runUrl:e.GITHUB_SERVER_URL&&e.GITHUB_REPOSITORY&&e.GITHUB_RUN_ID?`${e.GITHUB_SERVER_URL}/${e.GITHUB_REPOSITORY}/actions/runs/${e.GITHUB_RUN_ID}`:null}}function Rn(e){return e.GITLAB_CI!=="true"?null:{provider:"gitlab-ci",buildId:e.CI_PIPELINE_ID??null,commitSha:e.CI_COMMIT_SHA??null,branch:e.CI_COMMIT_BRANCH??e.CI_COMMIT_REF_NAME??null,runUrl:e.CI_PIPELINE_URL??null}}function An(e){if(!e.JENKINS_URL)return null;let t=e.GIT_BRANCH??null;return t?.startsWith("origin/")&&(t=t.slice(7)),{provider:"jenkins",buildId:e.BUILD_ID??null,commitSha:e.GIT_COMMIT??null,branch:t,runUrl:e.BUILD_URL??null}}function _n(e){return e.CIRCLECI!=="true"?null:{provider:"circleci",buildId:e.CIRCLE_BUILD_NUM??null,commitSha:e.CIRCLE_SHA1??null,branch:e.CIRCLE_BRANCH??null,runUrl:e.CIRCLE_BUILD_URL??null}}function In(e){if(!e.BITBUCKET_PIPELINE_UUID)return null;let t=e.BITBUCKET_WORKSPACE&&e.BITBUCKET_REPO_SLUG&&e.BITBUCKET_PIPELINE_UUID?`https://bitbucket.org/${e.BITBUCKET_WORKSPACE}/${e.BITBUCKET_REPO_SLUG}/pipelines/results/${e.BITBUCKET_PIPELINE_UUID}`:null;return {provider:"bitbucket-pipelines",buildId:e.BITBUCKET_BUILD_NUMBER??null,commitSha:e.BITBUCKET_COMMIT??null,branch:e.BITBUCKET_BRANCH??null,runUrl:t}}var Mn=[Tn,Rn,An,_n,In];function U(e){let t=process.env;for(let r of Mn){let a=r(t);if(a)return a}return null}var He=`
8
8
  /* Theme Variables */
9
9
  :root,[data-theme="dark"]{
10
10
  --bg:#0f1117;--bg-1:#161b22;--bg-2:#1c2128;--bg-3:#21262d;--bg-code:#13111c;
@@ -166,6 +166,12 @@ body{
166
166
  .test-row-arrow{color:var(--fg-2);flex-shrink:0;transition:color .15s}
167
167
  .test-row:hover .test-row-arrow{color:var(--fg-1)}
168
168
 
169
+ .test-row-retries{background:var(--bg-2);border-bottom:1px solid var(--bd-s)}
170
+ .test-row-retry-item{padding-left:42px;border-bottom:1px solid var(--bd-s)}
171
+ .test-row-retry-item:last-child{border-bottom:none}
172
+ .test-row-retry-item .test-row-title{font-size:13px;color:var(--fg-2);font-weight:400}
173
+ .test-row-retry-item .status-indicator{width:8px;height:8px}
174
+
169
175
  .no-tests{text-align:center;padding:48px 20px;color:var(--fg-2);font-size:14px}
170
176
 
171
177
  /* \u2500\u2500 Drawer Overlay \u2500\u2500 */
@@ -561,15 +567,15 @@ body{
561
567
  .detail-error .failure-message{margin-bottom:16px;padding:12px;background:rgba(239,68,68,0.06);border:1px solid rgba(239,68,68,0.15);border-radius:8px;font-size:12px;color:#f87171;text-align:left;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;white-space:pre-wrap;word-break:break-word}
562
568
  .detail-error code{font-size:11px;padding:2px 6px;background:var(--bg-2);border-radius:4px;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}
563
569
  .server-offline-banner{padding:8px 14px;background:rgba(251,191,36,0.08);border:1px solid rgba(251,191,36,0.2);border-radius:6px;font-size:11px;color:#fbbf24;text-align:center;margin-bottom:12px}
564
- `;var Me=`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 196 247" fill="none">
570
+ `;var qe=`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 196 247" fill="none">
565
571
  <path fill-rule="evenodd" clip-rule="evenodd" d="M4.75 1.08009C2.034 2.66209 -0.130999 7.63009 0.610001 10.5821C0.969001 12.0121 3.021 15.2131 5.17 17.6971C14.375 28.3321 18 39.7791 18 58.2101V71.0001H12.434C1.16501 71.0001 0 73.4641 0 97.3051C0 106.858 0.298003 128.922 0.662003 146.337L1.323 178H33.606C65.786 178 65.897 178.007 68.544 180.284C72.228 183.453 72.244 189.21 68.579 192.877L65.957 195.5L33.479 195.801L1 196.103V204.551V213H24.577H48.154L51.077 215.923C55.007 219.853 55.007 224.147 51.077 228.077L48.154 231H26.469H4.783L5.41901 233.534C5.76901 234.927 7.143 238.527 8.472 241.534L10.89 247H40.945H71L71.006 241.75C71.017 230.748 76.027 221.606 84.697 216.767C97.854 209.424 114.086 213.895 121.323 226.857C123.659 231.041 124.418 233.833 124.789 239.607L125.263 247H155.187H185.11L187.528 241.534C188.857 238.527 190.231 234.927 190.581 233.534L191.217 231H169.531H147.846L144.923 228.077C142.928 226.082 142 224.152 142 222C142 219.848 142.928 217.918 144.923 215.923L147.846 213H171.423H195V204.551V196.103L162.521 195.801L130.043 195.5L127.421 192.877C123.991 189.445 123.835 183.869 127.074 180.421L129.349 178H162.013H194.677L195.338 146.337C195.702 128.922 196 106.858 196 97.3051C196 73.4641 194.835 71.0001 183.566 71.0001H178V58.2101C178 39.6501 181.397 28.7731 190.538 18.0651C195.631 12.0971 196.572 9.00809 194.511 5.02109C192.672 1.46509 190.197 9.12233e-05 186.028 9.12233e-05C179.761 9.12233e-05 168.713 14.8831 163.388 30.5001C160.975 37.5771 160.608 40.3751 160.213 54.7501L159.765 71.0001H150.732H141.7L142.286 53.2501C142.904 34.5511 144.727 24.3761 148.938 16.1211C151.823 10.4671 151.628 5.90109 148.364 2.63609C145 -0.726907 140.105 -0.887909 136.596 2.25009C133.481 5.03609 128.686 17.0811 126.507 27.5921C125.569 32.1191 124.617 43.0901 124.28 53.2501L123.69 71.0001H115.345H107V38.4231V5.84609L104.077 2.92309C102.082 0.928088 100.152 9.12233e-05 98 9.12233e-05C95.848 9.12233e-05 93.918 0.928088 91.923 2.92309L89 5.84609V38.4231V71.0001H80.655H72.31L71.72 53.2501C71.383 43.0901 70.431 32.1191 69.493 27.5921C67.314 17.0811 62.519 5.03609 59.404 2.25009C55.998 -0.795909 51.059 -0.710905 47.646 2.45209C44.221 5.62609 44.191 9.92109 47.539 17.4911C51.71 26.9241 53.007 34.4791 53.676 53.2501L54.31 71.0001H45.272H36.235L35.787 54.7501C35.392 40.3751 35.025 37.5771 32.612 30.5001C27.194 14.6091 16.228 -0.02891 9.787 0.03009C7.979 0.04709 5.712 0.519093 4.75 1.08009ZM42.687 108.974C33.431 112.591 20.036 125.024 18.408 131.512C17.476 135.223 20.677 140.453 28.253 147.599C37.495 156.319 44.191 159.471 53.5 159.485C59.317 159.494 61.645 158.953 67.274 156.289C77.634 151.385 88.987 139.161 88.996 132.9C89.004 127.304 76.787 114.707 66.745 109.956C59.16 106.368 50.285 106.006 42.687 108.974ZM129.255 109.904C119.151 114.768 106.996 127.33 107.004 132.9C107.013 139.108 118.562 151.475 128.939 156.389C134.338 158.945 136.744 159.496 142.521 159.498C152.526 159.501 160.369 155.502 169.771 145.605C180.444 134.368 180.278 130.975 168.486 119.388C160.043 111.094 152.727 107.595 143 107.201C136.364 106.933 134.78 107.244 129.255 109.904ZM48.5 125.922C46.3 126.969 43.152 128.945 41.505 130.314L38.511 132.802L40.504 135.005C41.601 136.216 44.434 138.342 46.8 139.728C52.577 143.114 57.36 142.466 64.009 137.395L68.978 133.606L66.756 131.24C60.944 125.054 54.357 123.135 48.5 125.922ZM138.386 125.063C136.674 125.571 133.375 127.677 131.057 129.743L126.841 133.5L131.901 137.343C138.65 142.468 143.407 143.124 149.2 139.728C151.566 138.342 154.351 136.269 155.389 135.122C157.684 132.587 156.742 131.097 150.58 127.511C145.438 124.519 142.329 123.895 138.386 125.063ZM91.923 162.923C89.043 165.804 89 166.008 89 176.973C89 184.805 89.435 188.941 90.47 190.941C92.356 194.589 96.918 196.273 101.03 194.84C105.82 193.17 107 189.638 107 176.973C107 166.008 106.957 165.804 104.077 162.923C102.082 160.928 100.152 160 98 160C95.848 160 93.918 160.928 91.923 162.923ZM94.5 232.155C91.026 234.055 90 236.229 90 241.691V247H98H106V242.082C106 235.732 105.37 234.242 101.928 232.463C98.591 230.737 97.197 230.679 94.5 232.155Z" fill="url(#paint0_linear_55_11)"/>
566
572
  <defs><linearGradient id="paint0_linear_55_11" x1="98" y1="0.000244141" x2="98" y2="247" gradientUnits="userSpaceOnUse">
567
573
  <stop stop-color="#03B79C"/><stop offset="0.504808" stop-color="#4EDAA4"/><stop offset="0.865285" stop-color="#84F3AA"/>
568
- </linearGradient></defs></svg>`,Ee=`<svg width="32" height="40" viewBox="0 0 196 247" fill="none" xmlns="http://www.w3.org/2000/svg">
574
+ </linearGradient></defs></svg>`,$e=`<svg width="32" height="40" viewBox="0 0 196 247" fill="none" xmlns="http://www.w3.org/2000/svg">
569
575
  <path fill-rule="evenodd" clip-rule="evenodd" d="M4.75 1.08009C2.034 2.66209 -0.130999 7.63009 0.610001 10.5821C0.969001 12.0121 3.021 15.2131 5.17 17.6971C14.375 28.3321 18 39.7791 18 58.2101V71.0001H12.434C1.16501 71.0001 0 73.4641 0 97.3051C0 106.858 0.298003 128.922 0.662003 146.337L1.323 178H33.606C65.786 178 65.897 178.007 68.544 180.284C72.228 183.453 72.244 189.21 68.579 192.877L65.957 195.5L33.479 195.801L1 196.103V204.551V213H24.577H48.154L51.077 215.923C55.007 219.853 55.007 224.147 51.077 228.077L48.154 231H26.469H4.783L5.41901 233.534C5.76901 234.927 7.143 238.527 8.472 241.534L10.89 247H40.945H71L71.006 241.75C71.017 230.748 76.027 221.606 84.697 216.767C97.854 209.424 114.086 213.895 121.323 226.857C123.659 231.041 124.418 233.833 124.789 239.607L125.263 247H155.187H185.11L187.528 241.534C188.857 238.527 190.231 234.927 190.581 233.534L191.217 231H169.531H147.846L144.923 228.077C142.928 226.082 142 224.152 142 222C142 219.848 142.928 217.918 144.923 215.923L147.846 213H171.423H195V204.551V196.103L162.521 195.801L130.043 195.5L127.421 192.877C123.991 189.445 123.835 183.869 127.074 180.421L129.349 178H162.013H194.677L195.338 146.337C195.702 128.922 196 106.858 196 97.3051C196 73.4641 194.835 71.0001 183.566 71.0001H178V58.2101C178 39.6501 181.397 28.7731 190.538 18.0651C195.631 12.0971 196.572 9.00809 194.511 5.02109C192.672 1.46509 190.197 9.12233e-05 186.028 9.12233e-05C179.761 9.12233e-05 168.713 14.8831 163.388 30.5001C160.975 37.5771 160.608 40.3751 160.213 54.7501L159.765 71.0001H150.732H141.7L142.286 53.2501C142.904 34.5511 144.727 24.3761 148.938 16.1211C151.823 10.4671 151.628 5.90109 148.364 2.63609C145 -0.726907 140.105 -0.887909 136.596 2.25009C133.481 5.03609 128.686 17.0811 126.507 27.5921C125.569 32.1191 124.617 43.0901 124.28 53.2501L123.69 71.0001H115.345H107V38.4231V5.84609L104.077 2.92309C102.082 0.928088 100.152 9.12233e-05 98 9.12233e-05C95.848 9.12233e-05 93.918 0.928088 91.923 2.92309L89 5.84609V38.4231V71.0001H80.655H72.31L71.72 53.2501C71.383 43.0901 70.431 32.1191 69.493 27.5921C67.314 17.0811 62.519 5.03609 59.404 2.25009C55.998 -0.795909 51.059 -0.710905 47.646 2.45209C44.221 5.62609 44.191 9.92109 47.539 17.4911C51.71 26.9241 53.007 34.4791 53.676 53.2501L54.31 71.0001H45.272H36.235L35.787 54.7501C35.392 40.3751 35.025 37.5771 32.612 30.5001C27.194 14.6091 16.228 -0.02891 9.787 0.03009C7.979 0.04709 5.712 0.519093 4.75 1.08009ZM42.687 108.974C33.431 112.591 20.036 125.024 18.408 131.512C17.476 135.223 20.677 140.453 28.253 147.599C37.495 156.319 44.191 159.471 53.5 159.485C59.317 159.494 61.645 158.953 67.274 156.289C77.634 151.385 88.987 139.161 88.996 132.9C89.004 127.304 76.787 114.707 66.745 109.956C59.16 106.368 50.285 106.006 42.687 108.974ZM129.255 109.904C119.151 114.768 106.996 127.33 107.004 132.9C107.013 139.108 118.562 151.475 128.939 156.389C134.338 158.945 136.744 159.496 142.521 159.498C152.526 159.501 160.369 155.502 169.771 145.605C180.444 134.368 180.278 130.975 168.486 119.388C160.043 111.094 152.727 107.595 143 107.201C136.364 106.933 134.78 107.244 129.255 109.904ZM48.5 125.922C46.3 126.969 43.152 128.945 41.505 130.314L38.511 132.802L40.504 135.005C41.601 136.216 44.434 138.342 46.8 139.728C52.577 143.114 57.36 142.466 64.009 137.395L68.978 133.606L66.756 131.24C60.944 125.054 54.357 123.135 48.5 125.922ZM138.386 125.063C136.674 125.571 133.375 127.677 131.057 129.743L126.841 133.5L131.901 137.343C138.65 142.468 143.407 143.124 149.2 139.728C151.566 138.342 154.351 136.269 155.389 135.122C157.684 132.587 156.742 131.097 150.58 127.511C145.438 124.519 142.329 123.895 138.386 125.063ZM91.923 162.923C89.043 165.804 89 166.008 89 176.973C89 184.805 89.435 188.941 90.47 190.941C92.356 194.589 96.918 196.273 101.03 194.84C105.82 193.17 107 189.638 107 176.973C107 166.008 106.957 165.804 104.077 162.923C102.082 160.928 100.152 160 98 160C95.848 160 93.918 160.928 91.923 162.923ZM94.5 232.155C91.026 234.055 90 236.229 90 241.691V247H98H106V242.082C106 235.732 105.37 234.242 101.928 232.463C98.591 230.737 97.197 230.679 94.5 232.155Z" fill="url(#paint0_linear_55_11)"/>
570
576
  <defs><linearGradient id="paint0_linear_55_11" x1="98" y1="0.000244141" x2="98" y2="247" gradientUnits="userSpaceOnUse">
571
577
  <stop stop-color="#03B79C"/><stop offset="0.504808" stop-color="#4EDAA4"/><stop offset="0.865285" stop-color="#84F3AA"/>
572
- </linearGradient></defs></svg>`;var Le=`
578
+ </linearGradient></defs></svg>`;var Ve=`
573
579
  /* \u2500\u2500 Utilities \u2500\u2500 */
574
580
  function esc(s){if(!s)return '';return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;').replace(/'/g,'&#39;')}
575
581
  function stripAnsi(s){return s?s.replace(/\\u001b\\[[0-9;]*m/g,'').replace(/\\x1b\\[[0-9;]*m/g,''):''}
@@ -591,11 +597,25 @@ body{
591
597
  tags:t.tags,failure:t.failure,filePath:t.filePath,suiteName:t.suiteName,
592
598
  testType:t.testType,isFlaky:t.isFlaky,retryStatus:t.retryStatus,
593
599
  expectedStatus:t.expectedStatus,actualStatus:t.actualStatus,
594
- artifacts:t.artifacts,networkRequests:t.networkRequests||[],steps:[],apiCalls:[],actions:t.actions||[],consoleLogs:t.consoleLogs||[]
600
+ artifacts:t.artifacts,visual:t.visual||[],networkRequests:t.networkRequests||[],steps:[],apiCalls:[],actions:t.actions||[],consoleLogs:t.consoleLogs||[],
601
+ apiAssertions:t.apiAssertions||[],
602
+ apiCallCount:0,apiStatusMix:{'2xx':0,'3xx':0,'4xx':0,'5xx':0,'error':0},
603
+ assertionsPassed:0,assertionsFailed:0,
604
+ device:t.device||null,aiDefects:t.aiDefects||[]
595
605
  };
606
+ var _as=testMap[t.testId].apiAssertions;
607
+ for(var _ai=0;_ai<_as.length;_ai++){
608
+ if(_as[_ai].status==='failed')testMap[t.testId].assertionsFailed++;
609
+ else testMap[t.testId].assertionsPassed++;
610
+ }
596
611
  }
597
612
  if(entry.type==='api_call'){
598
- testMap[t.testId].apiCalls.push({
613
+ var _mt=testMap[t.testId];
614
+ _mt.apiCallCount++;
615
+ var _code=entry.response&&typeof entry.response.status==='number'?entry.response.status:null;
616
+ var _bucket=_code===null?'error':(_code>=500?'5xx':(_code>=400?'4xx':(_code>=300?'3xx':(_code>=200?'2xx':'error'))));
617
+ _mt.apiStatusMix[_bucket]++;
618
+ _mt.apiCalls.push({
599
619
  callId:entry.callId,method:entry.method,url:entry.url,
600
620
  responseTime:entry.responseTime,timestamp:entry.timestamp,
601
621
  request:entry.request,response:entry.response,assertions:entry.assertions
@@ -617,26 +637,73 @@ body{
617
637
  return a.startedAt<b.startedAt?-1:1;
618
638
  });
619
639
 
620
- /* \u2500\u2500 Streaming mode: build tests from compact index \u2500\u2500 */
640
+ /* \u2500\u2500 Streaming mode: build tests from compact index, grouping retries under the final attempt \u2500\u2500 */
621
641
  if(data.reportMode==='streaming'&&data.index&&data.index.length>0){
622
642
  tests=[];testMap={};
643
+ var _retryGroups={};
623
644
  for(var xi=0;xi<data.index.length;xi++){
624
645
  var ix=data.index[xi];
625
- if(ix.isRetry)continue;
646
+ var gk=ix.retryGroupKey||ix.id;
647
+ if(!_retryGroups[gk])_retryGroups[gk]={final:null,attempts:[]};
648
+ if(ix.isRetry){_retryGroups[gk].attempts.push(ix);}
649
+ else{_retryGroups[gk].final=ix;}
650
+ }
651
+ for(var gk2 in _retryGroups){
652
+ if(!_retryGroups.hasOwnProperty(gk2))continue;
653
+ var grp=_retryGroups[gk2];
654
+ // Defensive: if every attempt in a group was marked isRetry (shouldn't happen),
655
+ // fall back to the highest retryIndex so the test still surfaces.
656
+ var final=grp.final;
657
+ if(!final&&grp.attempts.length>0){
658
+ final=grp.attempts.reduce(function(a,b){return b.retryIndex>a.retryIndex?b:a;});
659
+ grp.attempts=grp.attempts.filter(function(a){return a!==final;});
660
+ }
661
+ if(!final)continue;
662
+ var ix2=final;
626
663
  var st={
627
- testId:ix.id,title:ix.title,status:ix.status,duration:ix.duration,
628
- filePath:ix.filePath,tags:ix.tags||[],isFlaky:ix.status==='flaky',
629
- testType:'e2e',retryCount:0,suiteName:'',
664
+ testId:ix2.id,title:ix2.title,status:ix2.status,duration:ix2.duration,
665
+ filePath:ix2.filePath,tags:ix2.tags||[],isFlaky:ix2.status==='flaky',
666
+ testType:ix2.testType||'e2e',retryCount:grp.attempts.length,suiteName:'',
630
667
  startedAt:'',completedAt:'',
631
668
  networkRequests:[],steps:[],apiCalls:[],actions:[],consoleLogs:[],
632
669
  artifacts:null,failure:null,retryStatus:null,expectedStatus:null,actualStatus:null,
633
- hasNetworkData:ix.hasNetworkData,hasConsoleData:ix.hasConsoleData,
634
- hasActionSteps:ix.hasActionSteps,hasArtifacts:ix.hasArtifacts,
635
- errorMessage:ix.errorMessage,networkCount:ix.networkCount||0,
636
- consoleCount:ix.consoleCount||0,actionCount:ix.actionCount||0
670
+ hasNetworkData:ix2.hasNetworkData,hasConsoleData:ix2.hasConsoleData,
671
+ hasActionSteps:ix2.hasActionSteps,hasArtifacts:ix2.hasArtifacts,
672
+ errorMessage:ix2.errorMessage,networkCount:ix2.networkCount||0,
673
+ consoleCount:ix2.consoleCount||0,actionCount:ix2.actionCount||0,
674
+ /* Carried from the index so the grid and the filter bar can count
675
+ visual comparisons without opening every test's detail file. */
676
+ visualCount:ix2.visualCount||0,visualFailedCount:ix2.visualFailedCount||0,
677
+ domChangeCount:ix2.domChangeCount||0,visualDiffPath:ix2.visualDiffPath||null,
678
+ /* API and mobile evidence, same rule: the grid must never open a
679
+ test's detail file to draw a row. */
680
+ apiCallCount:ix2.apiCallCount||0,apiStatusMix:ix2.apiStatusMix||null,
681
+ assertionsPassed:ix2.assertionsPassed||0,assertionsFailed:ix2.assertionsFailed||0,
682
+ firstFailedAssertion:ix2.firstFailedAssertion||null,
683
+ device:ix2.device||null,stepKinds:ix2.stepKinds||null,
684
+ deviceLogErrors:ix2.deviceLogErrors||0,hasCrashLog:!!ix2.hasCrashLog,
685
+ hasVideo:!!ix2.hasVideo,aiDefectCount:ix2.aiDefectCount||0,
686
+ retryAttempts:grp.attempts.slice().sort(function(a,b){return a.retryIndex-b.retryIndex;}).map(function(a){
687
+ return {testId:a.id,retryIndex:a.retryIndex,status:a.status,duration:a.duration,errorMessage:a.errorMessage};
688
+ })
637
689
  };
638
690
  testMap[st.testId]=st;
639
691
  tests.push(st);
692
+ // Register each retry attempt in testMap too (keyed by its own id) so opening
693
+ // its drawer has a title/meta to merge with the fetched detail \u2014 it never
694
+ // appears in the grid itself, only as a nested row under the final attempt.
695
+ for(var ai=0;ai<st.retryAttempts.length;ai++){
696
+ var ra=st.retryAttempts[ai];
697
+ testMap[ra.testId]={
698
+ testId:ra.testId,title:st.title+' (attempt '+(ra.retryIndex+1)+')',status:ra.status,
699
+ duration:ra.duration,filePath:st.filePath,tags:st.tags,isFlaky:false,
700
+ testType:st.testType,retryCount:0,suiteName:'',startedAt:'',completedAt:'',
701
+ networkRequests:[],steps:[],apiCalls:[],actions:[],consoleLogs:[],
702
+ artifacts:null,failure:null,retryStatus:'attempt '+(ra.retryIndex+1)+' of '+(st.retryAttempts.length+1),
703
+ expectedStatus:null,actualStatus:null,errorMessage:ra.errorMessage,
704
+ networkCount:0,consoleCount:0,actionCount:0
705
+ };
706
+ }
640
707
  }
641
708
  tests.sort(function(a,b){
642
709
  if(a.filePath!==b.filePath)return a.filePath<b.filePath?-1:1;
@@ -668,9 +735,7 @@ body{
668
735
  var summaryEl=document.getElementById('summary-strip');
669
736
  var filterEl=document.getElementById('filter-bar');
670
737
  var testGridEl=document.getElementById('test-grid');
671
- var drawerEl=document.getElementById('drawer');
672
738
  var drawerBodyEl=document.getElementById('drawer-body');
673
- var drawerBackdropEl=document.getElementById('drawer-backdrop');
674
739
 
675
740
  /* \u2500\u2500 Render: Run Meta \u2500\u2500 */
676
741
  function renderRunMeta(){
@@ -690,15 +755,81 @@ body{
690
755
  }
691
756
 
692
757
  /* \u2500\u2500 Render: Summary \u2500\u2500 */
693
- function renderSummary(){
694
- var s=data.summary;var h='';
695
- h+='<div class="summary-chip s-total"><div class="s-count">'+s.total+'</div><div class="s-label">Total</div></div>';
696
- h+='<div class="summary-chip s-passed"><div class="s-count">'+s.passed+'</div><div class="s-label">Passed</div></div>';
697
- h+='<div class="summary-chip s-failed"><div class="s-count">'+s.failed+'</div><div class="s-label">Failed</div></div>';
698
- h+='<div class="summary-chip s-flaky"><div class="s-count">'+s.flaky+'</div><div class="s-label">Flaky</div></div>';
699
- h+='<div class="summary-chip s-skipped"><div class="s-count">'+s.skipped+'</div><div class="s-label">Skipped</div></div>';
700
- if(s.timedout!==undefined)h+='<div class="summary-chip s-timedout"><div class="s-count">'+s.timedout+'</div><div class="s-label">Timeout</div></div>';
701
- summaryEl.innerHTML=h;
758
+ /* renderSummary lives in html-js-layout: the six equal stat chips were
759
+ replaced by the verdict band, which collapses empty statuses. */
760
+
761
+ /* Longest test in the run, so each bar reads as "relative to the slowest".
762
+ Recomputed per grid render because filtering changes the set. */
763
+ var _maxDur=1;
764
+ function rowBarPct(d){
765
+ var pct=Math.round(((d||0)/_maxDur)*100);
766
+ /* A bar too short to see says nothing; floor it at something visible. */
767
+ return Math.max(4,Math.min(100,pct));
768
+ }
769
+
770
+ /* Small square icons: what was captured, before you open the test. */
771
+
772
+ /* Both report modes reach these renderers with different shapes: the
773
+ streaming index carries counters, the embedded testMap carries the arrays.
774
+ Reading only the counters made the evidence column, the needs-attention
775
+ chips and the diff thumbnail vanish in embedded mode. */
776
+ function evVisualCount(t){return t.visual?t.visual.length:(t.visualCount||0);}
777
+ function evVisualFailed(t){
778
+ if(t.visual){var n=0;for(var i=0;i<t.visual.length;i++)if(t.visual[i].status==='failed')n++;return n;}
779
+ return t.visualFailedCount||0;
780
+ }
781
+ function evDomChanges(t){
782
+ if(t.visual){var n=0;for(var i=0;i<t.visual.length;i++)n+=(t.visual[i].domChangeCount||0);return n;}
783
+ return t.domChangeCount||0;
784
+ }
785
+ function evHasArtifacts(t){
786
+ if(t.artifacts)return !!(t.artifacts.screenshot||t.artifacts.video||t.artifacts.trace);
787
+ return !!t.hasArtifacts;
788
+ }
789
+ function evNetworkCount(t){return t.networkRequests?t.networkRequests.length:(t.networkCount||0);}
790
+ function evConsoleCount(t){return t.consoleLogs?t.consoleLogs.length:(t.consoleCount||0);}
791
+ function evDiffPath(t){
792
+ if(t.visualDiffPath)return t.visualDiffPath;
793
+ if(t.visual){for(var i=0;i<t.visual.length;i++){if(t.visual[i].diff)return t.visual[i].diff;}}
794
+ return null;
795
+ }
796
+
797
+ function rowEvidence(t){
798
+ var h='<div class="trow-evidence">';
799
+ var any=false;
800
+ if(typeof apiRowEvidence==='function'){
801
+ var _ae=apiRowEvidence(t);
802
+ if(_ae){h+=_ae;any=true;}
803
+ }
804
+ if(typeof mobileRowEvidence==='function'){
805
+ var _me=mobileRowEvidence(t);
806
+ if(_me){h+=_me;any=true;}
807
+ }
808
+ var _vis=evVisualCount(t),_visFail=evVisualFailed(t),_dom=evDomChanges(t);
809
+ var _net=evNetworkCount(t),_con=evConsoleCount(t);
810
+ if(_vis>0){
811
+ any=true;
812
+ h+='<span class="tev'+(_visFail>0?' tev-visual':'')+'" title="'+(_visFail>0?'Visual comparison failed':'Visual comparison')+'"><svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M12 3v18"/></svg></span>';
813
+ }
814
+ if(_dom>0){
815
+ any=true;
816
+ h+='<span class="tev tev-dom" title="'+_dom+' DOM elements changed"><svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M8 3H5a2 2 0 00-2 2v3M16 3h3a2 2 0 012 2v3M8 21H5a2 2 0 01-2-2v-3M16 21h3a2 2 0 002-2v-3"/></svg></span>';
817
+ }
818
+ if(evHasArtifacts(t)){
819
+ any=true;
820
+ h+='<span class="tev" title="Video and trace"><svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="5 3 19 12 5 21"/></svg></span>';
821
+ }
822
+ if(_net>0){
823
+ any=true;
824
+ h+='<span class="tev" title="'+_net+' network requests"><svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 12h4l3 8 4-16 3 8h4"/></svg></span>';
825
+ }
826
+ if(_con>0){
827
+ any=true;
828
+ h+='<span class="tev" title="'+_con+' console messages"><svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 17l6-6-6-6M12 19h8"/></svg></span>';
829
+ }
830
+ /* Keep the column even when empty so the rows stay aligned. */
831
+ if(!any)h+='<span style="width:18px"></span>';
832
+ return h+'</div>';
702
833
  }
703
834
 
704
835
  /* \u2500\u2500 Virtual Grid: progressive chunked rendering \u2500\u2500 */
@@ -730,7 +861,7 @@ body{
730
861
  var t=g.tests[i];
731
862
  h+='<div class="test-row" data-testid="'+esc(t.testId)+'">';
732
863
  h+='<div class="status-indicator si-'+t.status+'"></div>';
733
- h+='<div class="test-row-info">';
864
+ h+='<div class="test-row-info tri-inline">';
734
865
  h+='<div class="test-row-title" title="'+esc(t.title)+'">'+esc(shortTitle(t.title))+'</div>';
735
866
  var hasBadges=(t.testType&&t.testType!=='unknown')||t.isFlaky||(t.tags&&t.tags.length>0);
736
867
  if(hasBadges){
@@ -742,9 +873,24 @@ body{
742
873
  h+='</div>';
743
874
  }
744
875
  h+='</div>';
745
- h+='<span class="test-row-dur">'+fmtDur(t.duration)+'</span>';
876
+ h+=rowEvidence(t);
877
+ h+='<div class="trow-time"><div class="trow-bar"><span class="tb-'+t.status+'" style="width:'+rowBarPct(t.duration)+'%"></span></div>';
878
+ h+='<span class="test-row-dur">'+fmtDur(t.duration)+'</span></div>';
746
879
  h+='<svg class="test-row-arrow" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 18l6-6-6-6"/></svg>';
747
880
  h+='</div>';
881
+ if(t.retryAttempts&&t.retryAttempts.length>0){
882
+ h+='<div class="test-row-retries">';
883
+ for(var ri=0;ri<t.retryAttempts.length;ri++){
884
+ var ra=t.retryAttempts[ri];
885
+ h+='<div class="test-row test-row-retry-item" data-testid="'+esc(ra.testId)+'">';
886
+ h+='<div class="status-indicator si-'+ra.status+'"></div>';
887
+ h+='<div class="test-row-info"><div class="test-row-title">Attempt '+(ra.retryIndex+1)+' &mdash; '+esc(ra.status)+(ra.errorMessage?': '+esc(ra.errorMessage):'')+'</div></div>';
888
+ h+='<span class="test-row-dur">'+fmtDur(ra.duration)+'</span>';
889
+ h+='<svg class="test-row-arrow" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 18l6-6-6-6"/></svg>';
890
+ h+='</div>';
891
+ }
892
+ h+='</div>';
893
+ }
748
894
  }
749
895
  h+='</div></div>';
750
896
  return h;
@@ -790,6 +936,10 @@ body{
790
936
  function renderTestGrid(){
791
937
  if(_vgObserver){_vgObserver.disconnect();_vgObserver=null;}
792
938
  var filtered=getFilteredTests();
939
+ /* The bar on each row is relative to the slowest test CURRENTLY SHOWN, so
940
+ filtering to the slow ones still spreads them across the full width. */
941
+ _maxDur=1;
942
+ for(var md=0;md<filtered.length;md++){if(filtered[md].duration>_maxDur)_maxDur=filtered[md].duration;}
793
943
  if(filtered.length===0){testGridEl.innerHTML='<div class="no-tests">No tests match your filters</div>';_vgGroups=[];return;}
794
944
  _vgGroups=_vgBuildGroups(filtered);
795
945
  _vgRenderedTo=0;
@@ -818,17 +968,51 @@ body{
818
968
 
819
969
  function renderArtifactColumn(artifacts){
820
970
  if(!artifacts)return '<div class="artifact-empty">No artifacts captured</div>';
821
- var hasS=!!artifacts.screenshot,hasV=!!artifacts.video;
822
- if(!hasS&&!hasV)return '<div class="artifact-empty">No artifacts captured</div>';
971
+ var hasS=!!artifacts.screenshot,hasV=!!artifacts.video,hasT=!!artifacts.trace;
972
+ if(!hasS&&!hasV&&!hasT)return '<div class="artifact-empty">No artifacts captured</div>';
973
+ var tabs=[];
974
+ if(hasS)tabs.push('screenshot');
975
+ if(hasV)tabs.push('video');
976
+ if(hasT)tabs.push('trace');
977
+
823
978
  var h='';
824
- if(hasS&&hasV){
979
+ if(tabs.length>1){
825
980
  h+='<div class="seg-ctrl" data-seg-group="artifacts">';
826
- h+='<button class="seg-btn active" data-seg="screenshot"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8.5" cy="8.5" r="1.5"/><path d="M21 15l-5-5L5 21"/></svg>Screenshot</button>';
827
- h+='<button class="seg-btn" data-seg="video"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="5 3 19 12 5 21"/></svg>Video</button>';
981
+ for(var ti=0;ti<tabs.length;ti++){
982
+ var t=tabs[ti],ico='';
983
+ if(t==='screenshot')ico='<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8.5" cy="8.5" r="1.5"/><path d="M21 15l-5-5L5 21"/></svg>';
984
+ else if(t==='video')ico='<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="5 3 19 12 5 21"/></svg>';
985
+ else ico='<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 4h16v6H4zM4 14h16v6H4z"/></svg>';
986
+ var label=t==='screenshot'?'Screenshot':(t==='video'?'Video':'Trace');
987
+ h+='<button class="seg-btn'+(ti===0?' active':'')+'" data-seg="'+t+'">'+ico+label+'</button>';
988
+ }
828
989
  h+='</div>';
829
990
  }
830
- if(hasS){h+='<div class="seg-pane active" data-seg-pane="screenshot"><img class="artifact-col-img" src="'+esc(artifacts.screenshot)+'" loading="lazy" alt="Screenshot"><div class="artifact-col-caption"><svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M15 3h6v6M9 21H3v-6M21 3l-7 7M3 21l7-7"/></svg>Click to enlarge</div></div>';}
831
- if(hasV){h+='<div class="seg-pane'+(hasS?'':' active')+'" data-seg-pane="video"><div class="artifact-col-video"><video controls preload="metadata" src="'+esc(artifacts.video)+'"></video></div></div>';}
991
+
992
+ /* No lazy loading. A lazily-loaded image inside a display:none pane is
993
+ never fetched by Chrome, and every pane but the first starts hidden \u2014
994
+ the screenshot simply never appeared. The same bug was fixed for the
995
+ visual comparison images and left standing here. */
996
+ if(hasS){
997
+ h+='<div class="seg-pane'+(tabs[0]==='screenshot'?' active':'')+'" data-seg-pane="screenshot">';
998
+ h+='<img class="artifact-col-img" src="'+esc(artifacts.screenshot)+'" alt="Screenshot">';
999
+ h+='<div class="artifact-col-caption"><svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M15 3h6v6M9 21H3v-6M21 3l-7 7M3 21l7-7"/></svg>Click to enlarge</div></div>';
1000
+ }
1001
+ if(hasV){
1002
+ h+='<div class="seg-pane'+(tabs[0]==='video'?' active':'')+'" data-seg-pane="video">';
1003
+ h+='<div class="artifact-col-video"><video controls preload="metadata" src="'+esc(artifacts.video)+'"></video></div>';
1004
+ h+='<div class="artifact-col-caption">'+esc(artifacts.video.split('/').pop())+'</div></div>';
1005
+ }
1006
+ /* The trace is the thing a failure is actually debugged from, and the
1007
+ report had no way to reach it even though it was captured and copied. */
1008
+ if(hasT){
1009
+ h+='<div class="seg-pane'+(tabs[0]==='trace'?' active':'')+'" data-seg-pane="trace">';
1010
+ h+='<div class="trace-open"><a class="trace-link" href="'+esc(artifacts.trace)+'" download>';
1011
+ h+='<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 3v12M7 10l5 5 5-5M5 21h14"/></svg>';
1012
+ h+='Download '+esc(artifacts.trace.split('/').pop())+'</a>';
1013
+ h+='<div class="trace-hint">Open it with <code>npx playwright show-trace &lt;file&gt;</code>, or drop it on <code>trace.playwright.dev</code>.</div>';
1014
+ h+='</div></div>';
1015
+ }
832
1016
  return h;
833
1017
  }
834
1018
 
@@ -853,11 +1037,9 @@ body{
853
1037
  h+='</div><div class="detail-duration">'+fmtDur(t.duration)+'</div></div></div>';
854
1038
 
855
1039
  /* \u2500\u2500 Failure panel (full width) \u2500\u2500 */
856
- if((t.status==='failed'||t.status==='flaky')&&t.failure)h+=renderFailure(t.failure,t.filePath,t.title,t.tags);
857
- else if(t.status==='failed'&&!t.failure)h+='<div style="margin-bottom:20px;padding:14px;background:var(--bg-2);border:1px solid var(--bd);border-radius:10px;font-size:12px;color:var(--fg-2);font-style:italic">No diagnostic information available</div>';
858
-
859
1040
  /* \u2500\u2500 Stacked sections: 1) Artifacts 2) Timeline / Network 3) API Calls \u2500\u2500 */
860
- var hasArt=t.artifacts&&(t.artifacts.screenshot||t.artifacts.video);
1041
+ var hasArt=t.artifacts&&(t.artifacts.screenshot||t.artifacts.video||t.artifacts.trace);
1042
+ var hasVisual=t.visual&&t.visual.length>0;
861
1043
  var hasSteps=t.steps&&t.steps.length>0;
862
1044
  var hasApiCalls=t.apiCalls&&t.apiCalls.length>0;
863
1045
  var hasActions=t.actions&&t.actions.length>0;
@@ -868,30 +1050,69 @@ body{
868
1050
  var baseTime=0;
869
1051
  if(hasVideo&&t.startedAt){baseTime=Date.parse(t.startedAt);}
870
1052
  if(!baseTime&&hasVideo&&t.consoleLogs&&t.consoleLogs.length>0&&t.consoleLogs[0].timestamp){baseTime=Date.parse(t.consoleLogs[0].timestamp);}
871
- if(hasArt||hasSteps||hasApiCalls||hasActions||hasConsoleLogs||hasNetworkReqs){
1053
+
1054
+ var hasFailure=(t.status==='failed'||t.status==='flaky')&&t.failure;
1055
+ var hasDom=false;
1056
+ if(t.visual){for(var dv=0;dv<t.visual.length;dv++){if(t.visual[dv].domChangeCount)hasDom=true;}}
1057
+
1058
+ /* What the rail is allowed to offer \u2014 built from what actually rendered,
1059
+ so a jump can never land on a section that is not there. */
1060
+ var present={};
1061
+ if(hasFailure)present.failure={};
1062
+ if(hasVisual)present.visual={n:t.visual.length};
1063
+ if(hasDom)present.dom={n:(function(){var n=0;for(var q=0;q<t.visual.length;q++)n+=t.visual[q].domChangeCount||0;return n;})()};
1064
+ if(hasArt)present.artifacts={};
1065
+ if(hasActions)present.actions={n:t.actions.length};
1066
+ if(hasSteps)present.timeline={n:t.steps.length};
1067
+ var _asserts=t.apiAssertions||[];
1068
+ var hasAssertions=_asserts.length>0;
1069
+ if(hasAssertions){
1070
+ var _af=0;
1071
+ for(var _q=0;_q<_asserts.length;_q++)if(_asserts[_q].status==='failed')_af++;
1072
+ present.assertions={n:_asserts.length,bad:_af>0};
1073
+ }
1074
+ if(hasApiCalls)present.api={n:t.apiCalls.length};
1075
+ if(!hasSteps&&hasConsoleLogs)present.console={n:t.consoleLogs.length};
1076
+ if(!hasSteps&&hasNetworkReqs)present.network={n:t.networkRequests.length};
1077
+
1078
+ var anySection=hasArt||hasVisual||hasSteps||hasApiCalls||hasActions||hasConsoleLogs||hasNetworkReqs||hasAssertions;
1079
+
1080
+ h+='<div class="drawer-layout">';
1081
+ h+='<div class="drawer-rail">'+renderDrawerRail(present,t)+'</div>';
1082
+ h+='<div class="drawer-main">';
1083
+
1084
+ if(hasFailure)h+='<div class="drawer-section" data-section="failure">'+renderFailure(t.failure,t.filePath,t.title,t.tags)+'</div>';
1085
+ else if(t.status==='failed')h+='<div class="drawer-section" data-section="failure" style="padding:14px;background:var(--bg-2);border:1px solid var(--bd);border-radius:10px;font-size:12px;color:var(--fg-2);font-style:italic">No diagnostic information available</div>';
1086
+
1087
+ if(anySection){
872
1088
  h+='<div class="drawer-sections">';
873
- if(hasArt)h+='<div class="drawer-section">'+renderArtifactColumn(t.artifacts)+'</div>';
874
- if(hasActions)h+='<div class="drawer-section">'+renderActionsColumn(t.actions,hasVideo)+'</div>';
875
- if(hasSteps){var _pgMeta=t._networkTotal?{testId:t._testId,page:t._networkPage||1,total:t._networkTotal,pageSize:t._networkPageSize||500}:null;h+='<div class="drawer-section">'+renderTimelineColumn(t.steps,t.networkRequests,t.consoleLogs,t.testType,hasVideo,baseTime,_pgMeta)+'</div>';}
876
- if(!hasSteps&&hasNetworkReqs&&typeof renderNetworkDevTools==='function'){var _pgMeta2=t._networkTotal?{testId:t._testId,page:t._networkPage||1,total:t._networkTotal,pageSize:t._networkPageSize||500}:null;h+='<div class="drawer-section">'+renderNetworkDevTools(t.networkRequests,false,0,_pgMeta2)+'</div>';}
877
- if(!hasSteps&&hasConsoleLogs)h+='<div class="drawer-section">'+renderConsoleColumn(t.consoleLogs,t.testType,hasVideo,baseTime)+'</div>';
878
- if(hasApiCalls)h+='<div class="drawer-section">'+renderApiCallsColumn(t.apiCalls)+'</div>';
1089
+ if(hasVisual)h+='<div class="drawer-section" data-section="visual">'+renderVisualSection(t.visual)+'</div>';
1090
+ if(hasArt)h+='<div class="drawer-section" data-section="artifacts">'+renderArtifactColumn(t.artifacts)+'</div>';
1091
+ if(hasActions)h+='<div class="drawer-section" data-section="actions">'+renderActionsColumn(t.actions,hasVideo)+'</div>';
1092
+ if(hasSteps){var _pgMeta=t._networkTotal?{testId:t._testId,page:t._networkPage||1,total:t._networkTotal,pageSize:t._networkPageSize||500}:null;h+='<div class="drawer-section" data-section="timeline">'+renderTimelineColumn(t.steps,t.networkRequests,t.consoleLogs,t.testType,hasVideo,baseTime,_pgMeta)+'</div>';}
1093
+ if(!hasSteps&&hasNetworkReqs&&typeof renderNetworkDevTools==='function'){var _pgMeta2=t._networkTotal?{testId:t._testId,page:t._networkPage||1,total:t._networkTotal,pageSize:t._networkPageSize||500}:null;h+='<div class="drawer-section" data-section="network">'+renderNetworkDevTools(t.networkRequests,false,0,_pgMeta2)+'</div>';}
1094
+ if(!hasSteps&&hasConsoleLogs)h+='<div class="drawer-section" data-section="console">'+renderConsoleColumn(t.consoleLogs,t.testType,hasVideo,baseTime)+'</div>';
1095
+ /* Assertions before the calls: the assertion is what failed, the call
1096
+ is only where it happened. */
1097
+ if(hasAssertions&&typeof renderAssertionsSection==='function')
1098
+ h+='<div class="drawer-section" data-section="assertions">'+renderAssertionsSection(_asserts,t.apiCalls)+'</div>';
1099
+ if(hasApiCalls)h+='<div class="drawer-section" data-section="api">'+renderApiCallsColumn(t.apiCalls,_asserts)+'</div>';
879
1100
  h+='</div>';
880
1101
  }
881
1102
 
1103
+ h+='</div></div>';
1104
+
882
1105
  h+='</div>';
883
1106
  drawerBodyEl.innerHTML=h;
884
- drawerBodyEl.scrollTop=0;
885
1107
  /* Wire up video \u2194 action step sync if both are present */
886
1108
  if(typeof syncActionsWithVideo==='function')syncActionsWithVideo();
887
1109
  }
888
1110
 
889
1111
  /* \u2500\u2500 Drawer open / close \u2500\u2500 */
890
- function openDrawer(testId){
1112
+ /* Fetch (streaming) and render one test into the detail view. The routing
1113
+ around it lives in html-js-layout; this only fills the page. */
1114
+ function loadAndRenderTest(testId){
891
1115
  if(data.reportMode==='streaming'){
892
- drawerEl.classList.add('open');
893
- drawerBackdropEl.classList.add('open');
894
- document.body.style.overflow='hidden';
895
1116
  if(_detailCache[testId]){renderDrawer(testId,_detailCache[testId]);return;}
896
1117
  drawerBodyEl.innerHTML='<div class="detail-loading"><div class="loading-spinner"></div><div class="loading-text">Loading test details...</div></div>';
897
1118
  if(window.__trStreamingFetchDetail){
@@ -909,6 +1130,7 @@ body{
909
1130
  merged.actions=detail.actions||[];
910
1131
  merged.apiAssertions=detail.apiAssertions||[];
911
1132
  merged.artifacts=detail.artifacts||null;
1133
+ merged.visual=detail.visual||[];
912
1134
  merged.failure=detail.failureDiagnostic||detail.failure||null;
913
1135
  merged.steps=detail.navigations||[];
914
1136
  if(detail.startedAt)merged.startedAt=detail.startedAt;
@@ -923,7 +1145,7 @@ body{
923
1145
  merged.apiCalls=[];
924
1146
  cacheDetail(testId,merged);
925
1147
  renderDrawer(testId,merged);
926
- /* Lazy-load heavy data after drawer is visible */
1148
+ /* Lazy-load heavy data once the page is showing */
927
1149
  loadHeavyData(testId,merged);
928
1150
  });
929
1151
  }else{
@@ -934,9 +1156,6 @@ body{
934
1156
  return;
935
1157
  }
936
1158
  renderDrawer(testId);
937
- drawerEl.classList.add('open');
938
- drawerBackdropEl.classList.add('open');
939
- document.body.style.overflow='hidden';
940
1159
  }
941
1160
 
942
1161
  /* Lazy-load heavy data (network, console, API calls) after drawer renders */
@@ -985,14 +1204,6 @@ body{
985
1204
  });
986
1205
  }
987
1206
  }
988
- function closeDrawer(){
989
- drawerEl.classList.remove('open');
990
- drawerBackdropEl.classList.remove('open');
991
- document.body.style.overflow='';
992
- /* Pause any playing video */
993
- var v=drawerBodyEl.querySelector('video');
994
- if(v)v.pause();
995
- }
996
1207
 
997
1208
  /* \u2500\u2500 Search (debounced) \u2500\u2500 */
998
1209
  var _searchTimer=0;
@@ -1000,7 +1211,7 @@ body{
1000
1211
  clearTimeout(_searchTimer);
1001
1212
  _searchTimer=setTimeout(function(){searchQuery=q;applyFilters();},150);
1002
1213
  }
1003
- `;var Pe=`
1214
+ `;var je=`
1004
1215
  /* \u2500\u2500 Network Visualization Helpers \u2500\u2500 */
1005
1216
  var RES_COLORS={xhr:'#60a5fa',document:'#818cf8',script:'#fbbf24',stylesheet:'#a78bfa',image:'#34d399',font:'#f472b6',other:'#6b7280'};
1006
1217
  var RES_LABELS={xhr:'XHR',document:'Doc',script:'JS',stylesheet:'CSS',image:'Img',font:'Font',other:'Other'};
@@ -1464,7 +1675,20 @@ body{
1464
1675
  var sid='s-'+Math.random().toString(36).substr(2,8);
1465
1676
  var cpid='cp-'+Math.random().toString(36).substr(2,8);
1466
1677
  var h='<div class="failure-panel"><div class="failure-panel-header"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="M15 9l-6 6M9 9l6 6"/></svg>Failure Details<button class="copy-prompt-btn" id="'+cpid+'"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1"/></svg>Copy Prompt</button></div>';
1467
- h+='<div class="failure-message">'+esc(stripAnsi(failure.message))+'</div>';
1678
+ /* Split the call log off the message. It is the longest part by far and
1679
+ almost never what you want first \u2014 leaving it expanded is what pushed
1680
+ the visual comparison two screens down. */
1681
+ var fullMsg=stripAnsi(failure.message||'');
1682
+ var logAt=fullMsg.indexOf('Call log:');
1683
+ var headMsg=logAt>=0?fullMsg.slice(0,logAt).trim():fullMsg;
1684
+ var callLog=logAt>=0?fullMsg.slice(logAt).trim():'';
1685
+ h+='<div class="failure-message">'+esc(headMsg)+'</div>';
1686
+ if(callLog){
1687
+ var lid='cl-'+Math.random().toString(36).substr(2,8);
1688
+ var lines=callLog.split(String.fromCharCode(10)).length-1;
1689
+ h+='<button class="stack-toggle-btn" data-stack="'+lid+'">Show call log ('+lines+' steps)</button>';
1690
+ h+='<div class="stack-trace" id="'+lid+'">'+esc(callLog)+'</div>';
1691
+ }
1468
1692
  if(failure.code)h+=renderCodeSnippet(failure.code);
1469
1693
  if(failure.line!==null&&failure.line!==undefined)h+='<div class="line-marker">Line '+failure.line+'</div>';
1470
1694
  if(failure.stack){h+='<button class="stack-toggle-btn" data-stack="'+sid+'">Show stack trace</button><div class="stack-trace" id="'+sid+'">'+esc(stripAnsi(failure.stack))+'</div>';}
@@ -1525,8 +1749,44 @@ body{
1525
1749
  /* \u2500\u2500 API Calls Column (for API-only tests) \u2500\u2500 */
1526
1750
  var API_METHOD_COLORS={GET:'#60a5fa',POST:'#34d399',PUT:'#fbbf24',PATCH:'#a78bfa',DELETE:'#f87171',HEAD:'#6b7280',OPTIONS:'#6b7280'};
1527
1751
 
1528
- function renderApiCallDetail(ac,idx){
1752
+ /* Response size, derived. ApiCallRecord has no size field \u2014 the body is
1753
+ what we have, so measure it and say so rather than render an empty cell. */
1754
+ function apiBodyBytes(ac){
1755
+ var b=ac.responseBody;
1756
+ if(b===null||b===undefined)return null;
1757
+ var s=(typeof b==='string')?b:JSON.stringify(b);
1758
+ if(!s)return 0;
1759
+ /* base64 bodies decode to 3 bytes per 4 characters. */
1760
+ if(ac.isBinary)return Math.floor(s.length*3/4);
1761
+ return s.length;
1762
+ }
1763
+
1764
+ function renderCookieTable(cookies,emptyLabel){
1765
+ if(!cookies||cookies.length===0)return '<div class="ndt-body-empty">'+esc(emptyLabel)+'</div>';
1766
+ var h='<div class="ck-tbl">';
1767
+ for(var i=0;i<cookies.length;i++){
1768
+ var c=cookies[i];
1769
+ h+='<span class="ck-k">'+esc(c.name)+'</span>';
1770
+ h+='<span class="ck-v">'+esc(c.value);
1771
+ if(c.attributes)h+=' <span class="ck-attr">'+esc(c.attributes)+'</span>';
1772
+ h+='</span>';
1773
+ }
1774
+ return h+'</div>';
1775
+ }
1776
+
1777
+ /* The assertions made against one call. */
1778
+ function assertionsForCall(assertions,callId){
1779
+ var out=[];
1780
+ if(!assertions||!callId)return out;
1781
+ for(var i=0;i<assertions.length;i++){
1782
+ if(assertions[i].callId===callId)out.push(assertions[i]);
1783
+ }
1784
+ return out;
1785
+ }
1786
+
1787
+ function renderApiCallDetail(ac,idx,callAsserts){
1529
1788
  var did='ndt-d-api-'+idx;
1789
+ callAsserts=callAsserts||[];
1530
1790
  var reqHeaders=ac.requestHeaders||null;
1531
1791
  var reqBody=ac.requestBody||null;
1532
1792
  var resHeaders=ac.responseHeaders||null;
@@ -1542,6 +1802,9 @@ body{
1542
1802
  h+='<button class="ndt-detail-tab" data-ndt-tab="req-body" data-ndt-target="'+did+'">Payload</button>';
1543
1803
  h+='<button class="ndt-detail-tab" data-ndt-tab="res-headers" data-ndt-target="'+did+'">Response Headers</button>';
1544
1804
  h+='<button class="ndt-detail-tab" data-ndt-tab="res-body" data-ndt-target="'+did+'">Response</button>';
1805
+ var hasCookies=(ac.requestCookies&&ac.requestCookies.length)||(ac.responseCookies&&ac.responseCookies.length);
1806
+ if(hasCookies)h+='<button class="ndt-detail-tab" data-ndt-tab="cookies" data-ndt-target="'+did+'">Cookies</button>';
1807
+ if(callAsserts.length>0)h+='<button class="ndt-detail-tab" data-ndt-tab="asserts" data-ndt-target="'+did+'">Assertions <span style="opacity:.6">'+callAsserts.length+'</span></button>';
1545
1808
  h+='</div>';
1546
1809
 
1547
1810
  /* General tab */
@@ -1550,21 +1813,61 @@ body{
1550
1813
  h+='<span class="ndt-general-k">Method</span><span class="ndt-general-v">'+esc(ac.method)+'</span>';
1551
1814
  var sc=ac.responseStatusCode!=null?ac.responseStatusCode:0;
1552
1815
  var st=ac.responseStatusText||'';
1553
- h+='<span class="ndt-general-k">Status Code</span><span class="ndt-general-v'+(sc>=400?' ndt-err':'')+'">'+sc+(st?' '+esc(st):'')+'</span>';
1816
+ if(ac.error){
1817
+ /* The call never got a response. Rendering "0" here read as a status. */
1818
+ h+='<span class="ndt-general-k">Result</span><span class="ndt-general-v ndt-err-note">Request failed &mdash; '+esc(String(ac.error))+'</span>';
1819
+ }else{
1820
+ h+='<span class="ndt-general-k">Status Code</span><span class="ndt-general-v'+(sc>=400?' ndt-err':'')+'">'+sc+(st?' '+esc(st):'')+'</span>';
1821
+ }
1554
1822
  h+='<span class="ndt-general-k">Response Time</span><span class="ndt-general-v">'+(ac.responseTimeMs!=null?fmtDur(ac.responseTimeMs):'N/A')+'</span>';
1823
+ var _bz=apiBodyBytes(ac);
1824
+ if(_bz!==null)h+='<span class="ndt-general-k">Response Size</span><span class="ndt-general-v">'+fmtBytes(_bz)+(ac.isBinary?' (binary)':'')+'</span>';
1555
1825
  if(ac.timestamp)h+='<span class="ndt-general-k">Timestamp</span><span class="ndt-general-v">'+fmtDate(ac.timestamp)+'</span>';
1556
1826
  h+='</div></div>';
1557
1827
 
1558
1828
  h+='<div class="ndt-detail-pane" data-ndt-pane="req-headers">'+renderNdtHeaders(reqHeaders,'request','Request headers not available &mdash; only explicitly passed headers are captured for API tests')+'</div>';
1559
1829
  h+='<div class="ndt-detail-pane" data-ndt-pane="req-body">'+renderNdtBody(reqBodyStr,false,false,ct,'No request body')+'</div>';
1560
1830
  h+='<div class="ndt-detail-pane" data-ndt-pane="res-headers">'+renderNdtHeaders(resHeaders,'response')+'</div>';
1561
- h+='<div class="ndt-detail-pane" data-ndt-pane="res-body">'+renderNdtBody(resBodyStr,false,false,ct,'No response body captured')+'</div>';
1831
+ h+='<div class="ndt-detail-pane" data-ndt-pane="res-body">'+renderNdtBody(resBodyStr,false,ac.isBinary,ct,ac.error?'No response \u2014 the request failed':'No response body captured')+'</div>';
1832
+ if(hasCookies){
1833
+ h+='<div class="ndt-detail-pane" data-ndt-pane="cookies">';
1834
+ h+='<div class="ndt-headers-label">Request</div>'+renderCookieTable(ac.requestCookies,'No cookies sent');
1835
+ h+='<div class="ndt-headers-label">Response</div>'+renderCookieTable(ac.responseCookies,'No cookies set');
1836
+ h+='</div>';
1837
+ }
1838
+ if(callAsserts.length>0){
1839
+ h+='<div class="ndt-detail-pane" data-ndt-pane="asserts">'+renderCallAssertions(callAsserts)+'</div>';
1840
+ }
1562
1841
  h+='</div>';
1563
1842
  return h;
1564
1843
  }
1565
1844
 
1566
- function renderApiCallsColumn(apiCalls){
1845
+ /* The assertions on one call, inside that call's detail pane. The run-level
1846
+ section above lists them all; this answers "what did this call fail?". */
1847
+ function renderCallAssertions(list){
1848
+ var h='<div class="asec">';
1849
+ for(var i=0;i<list.length;i++){
1850
+ var a=list[i],failed=a.status==='failed';
1851
+ h+='<div class="arow-item'+(failed?' is-failed':'')+'">';
1852
+ h+=failed
1853
+ ? '<svg style="flex-shrink:0" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#ef4444" stroke-width="2.5"><path d="M18 6L6 18M6 6l12 12"/></svg>'
1854
+ : '<svg style="flex-shrink:0" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#22c55e" stroke-width="2.5"><path d="M20 6L9 17l-5-5"/></svg>';
1855
+ h+='<span class="atype">'+esc(String(a.type||'custom'))+'</span>';
1856
+ h+='<span class="aexpr">'+esc(typeof assertionExpression==='function'?assertionExpression(a):(a.expression||''))+'</span>';
1857
+ h+='</div>';
1858
+ if(failed&&typeof fmtAssertValue==='function'){
1859
+ h+='<div class="adiff">';
1860
+ h+='<span class="adiff-i"><span class="adiff-k">expected</span><span class="adiff-v adiff-exp">'+esc(fmtAssertValue(a.expected))+'</span></span>';
1861
+ h+='<span class="adiff-i"><span class="adiff-k">received</span><span class="adiff-v adiff-act">'+esc(fmtAssertValue(a.actual))+'</span></span>';
1862
+ h+='</div>';
1863
+ }
1864
+ }
1865
+ return h+'</div>';
1866
+ }
1867
+
1868
+ function renderApiCallsColumn(apiCalls,assertions){
1567
1869
  if(!apiCalls||apiCalls.length===0)return '<div class="artifact-empty">No API calls captured</div>';
1870
+ assertions=assertions||[];
1568
1871
  var uid='api-'+Math.random().toString(36).substr(2,6);
1569
1872
  var h='<div class="ndt" id="'+uid+'">';
1570
1873
 
@@ -1586,16 +1889,28 @@ body{
1586
1889
  for(var i=0;i<apiCalls.length;i++){
1587
1890
  var ac=apiCalls[i];
1588
1891
  var sc=ac.responseStatusCode!=null?ac.responseStatusCode:0;
1589
- var isFail=sc>=400||sc===0;
1892
+ var isFail=sc>=400||sc===0||!!ac.error;
1893
+ var callAsserts=assertionsForCall(assertions,ac.id||ac.callId);
1894
+ var aFail=0;
1895
+ for(var aj=0;aj<callAsserts.length;aj++)if(callAsserts[aj].status==='failed')aFail++;
1590
1896
  h+='<div class="ndt-row'+(isFail?' ndt-row--fail':'')+'" data-ndt-idx="api-'+i+'" data-ndt-type="'+esc(ac.method)+'" data-ndt-url="'+esc(ac.url.toLowerCase())+'">';
1591
- h+='<span class="ndt-status '+ndtStatusClass(sc)+'">'+sc+'</span>';
1897
+ h+='<span class="ndt-status '+ndtStatusClass(sc)+'" title="'+(ac.error?esc(String(ac.error)):'')+'">'+(ac.error?'ERR':sc)+'</span>';
1592
1898
  h+='<span class="ndt-method" style="color:'+(API_METHOD_COLORS[ac.method]||'#6b7280')+'">'+esc(ac.method)+'</span>';
1593
1899
  h+='<span class="ndt-url" title="'+esc(ac.url)+'">'+esc(urlPath(ac.url))+'</span>';
1594
- h+='<span class="ndt-type"></span>';
1595
- h+='<span class="ndt-size"></span>';
1900
+ /* Was: two permanently empty cells. Now the two things a reader of an
1901
+ API row actually wants \u2014 did its assertions hold, and how big was it. */
1902
+ if(callAsserts.length===0){
1903
+ h+='<span class="ndt-type"><span class="ndt-assert ndt-assert-none">&mdash;</span></span>';
1904
+ }else if(aFail>0){
1905
+ h+='<span class="ndt-type"><span class="ndt-assert ndt-assert-bad">'+aFail+' \u2717</span></span>';
1906
+ }else{
1907
+ h+='<span class="ndt-type"><span class="ndt-assert ndt-assert-ok">'+callAsserts.length+' \u2713</span></span>';
1908
+ }
1909
+ var bz=apiBodyBytes(ac);
1910
+ h+='<span class="ndt-size">'+(bz===null?'':fmtBytes(bz))+'</span>';
1596
1911
  h+='<span class="ndt-time">'+(ac.responseTimeMs!=null?fmtDur(ac.responseTimeMs):'')+'</span>';
1597
1912
  h+='</div>';
1598
- h+=renderApiCallDetail(ac,i);
1913
+ h+=renderApiCallDetail(ac,i,callAsserts);
1599
1914
  }
1600
1915
  h+='</div></div>';
1601
1916
  return h;
@@ -1631,8 +1946,44 @@ body{
1631
1946
  var countEl=ndt.querySelector('.ndt-count');
1632
1947
  if(countEl)countEl.textContent=visible+' / '+rows.length+' requests';
1633
1948
  }
1634
- `;var Ne=`
1635
- var _filterState={status:{},type:{},specFile:{}};
1949
+ `;var Ge=`
1950
+ var _filterState={status:{},type:{},specFile:{},visual:{},evidence:{}};
1951
+
1952
+ /* Evidence filters: one predicate per chip, so API and mobile runs share the
1953
+ mechanism and a chip only appears when something in the run matches it. */
1954
+ var EVIDENCE_FILTERS=[
1955
+ {v:'api-error',label:'4xx/5xx seen',test:function(t){
1956
+ var m=t.apiStatusMix;
1957
+ if(!m)return false;
1958
+ return (m['4xx']||0)+(m['5xx']||0)+(m['error']||0)>0;
1959
+ }},
1960
+ {v:'assert-failed',label:'Failed assertion',test:function(t){return (t.assertionsFailed||0)>0;}},
1961
+ {v:'crash',label:'Crash logged',test:function(t){return !!t.hasCrashLog;}},
1962
+ {v:'device-error',label:'Device log errors',test:function(t){return (t.deviceLogErrors||0)>0;}},
1963
+ {v:'video',label:'Has recording',test:function(t){return !!t.hasVideo||!!(t.artifacts&&t.artifacts.video);}},
1964
+ {v:'ai-defect',label:'AI defects',test:function(t){return (t.aiDefectCount||0)>0;}}
1965
+ ];
1966
+
1967
+ function _evidenceCounts(){
1968
+ var counts={};
1969
+ for(var f=0;f<EVIDENCE_FILTERS.length;f++)counts[EVIDENCE_FILTERS[f].v]=0;
1970
+ for(var i=0;i<tests.length;i++){
1971
+ for(var j=0;j<EVIDENCE_FILTERS.length;j++){
1972
+ if(EVIDENCE_FILTERS[j].test(tests[i]))counts[EVIDENCE_FILTERS[j].v]++;
1973
+ }
1974
+ }
1975
+ return counts;
1976
+ }
1977
+
1978
+ /* Visual counts reach the grid differently in the two report modes: the
1979
+ embedded report inlines the comparisons on each test, while the streaming
1980
+ index carries only counts so the grid never has to open a detail file.
1981
+ Both are read here so the filter works the same either way. */
1982
+ function _visCount(t){return t.visual?t.visual.length:(t.visualCount||0);}
1983
+ function _visFailed(t){
1984
+ if(t.visual){var n=0;for(var i=0;i<t.visual.length;i++)if(t.visual[i].status==='failed')n++;return n;}
1985
+ return t.visualFailedCount||0;
1986
+ }
1636
1987
  var _totalTests=0;
1637
1988
  var _originalSummary=null;
1638
1989
 
@@ -1641,18 +1992,22 @@ function renderFilterBar(){
1641
1992
  var drawerBodyEl=document.getElementById('filter-drawer-body');
1642
1993
  var statusCounts={passed:0,failed:0,flaky:0,skipped:0,timedout:0};
1643
1994
  var typeCounts={},specCounts={};
1995
+ var visualCounts={has:0,failed:0};
1644
1996
  for(var i=0;i<tests.length;i++){
1645
1997
  var t=tests[i];
1646
1998
  if(statusCounts[t.status]!==undefined)statusCounts[t.status]++;
1647
1999
  var tp=t.testType||'unknown';
1648
2000
  typeCounts[tp]=(typeCounts[tp]||0)+1;
1649
2001
  if(t.filePath)specCounts[t.filePath]=(specCounts[t.filePath]||0)+1;
2002
+ if(_visCount(t)>0){visualCounts.has++;if(_visFailed(t)>0)visualCounts.failed++;}
1650
2003
  }
1651
2004
  var hasStatus=Object.keys(_filterState.status).length>0;
1652
2005
  var hasType=Object.keys(_filterState.type).length>0;
1653
2006
  var hasSpec=Object.keys(_filterState.specFile).length>0;
1654
- var hasAny=hasStatus||hasType||hasSpec||!!searchQuery;
1655
- var hasAdvanced=hasType||hasSpec;
2007
+ var hasVisual=Object.keys(_filterState.visual).length>0;
2008
+ var hasEvidenceSel=Object.keys(_filterState.evidence).length>0;
2009
+ var hasAny=hasStatus||hasType||hasSpec||hasVisual||hasEvidenceSel||!!searchQuery;
2010
+ var hasAdvanced=hasType||hasSpec||hasVisual||hasEvidenceSel;
1656
2011
 
1657
2012
  /* \u2500\u2500 Filter Bar (search + status chips + filter icon) \u2500\u2500 */
1658
2013
  var h='';
@@ -1709,6 +2064,35 @@ function renderFilterBar(){
1709
2064
  d+='</div></div>';
1710
2065
  }
1711
2066
 
2067
+ /* Visual section \u2014 only when the run actually produced comparisons */
2068
+ if(visualCounts.has>0){
2069
+ d+='<div class="filter-drawer-section"><span class="filter-drawer-section-label">Visual</span>';
2070
+ d+='<div class="filter-chips">';
2071
+ var visOpts=[{v:'has',label:'Has visual',cnt:visualCounts.has},{v:'failed',label:'Visual failed',cnt:visualCounts.failed}];
2072
+ for(var vi=0;vi<visOpts.length;vi++){
2073
+ var vo=visOpts[vi];
2074
+ var vcls='filter-chip'+(!!_filterState.visual[vo.v]?' active':'')+(vo.cnt===0?' filter-chip--dimmed':'');
2075
+ d+='<button class="'+vcls+'" data-dimension="visual" data-value="'+vo.v+'">'+vo.label+' <span class="chip-count">'+vo.cnt+'</span></button>';
2076
+ }
2077
+ d+='</div></div>';
2078
+ }
2079
+
2080
+ /* Evidence section \u2014 a chip per predicate that anything in the run meets */
2081
+ var evCounts=_evidenceCounts();
2082
+ var evAny=false;
2083
+ for(var ek in evCounts){if(evCounts.hasOwnProperty(ek)&&evCounts[ek]>0)evAny=true;}
2084
+ if(evAny){
2085
+ d+='<div class="filter-drawer-section"><span class="filter-drawer-section-label">Evidence</span>';
2086
+ d+='<div class="filter-chips">';
2087
+ for(var ei=0;ei<EVIDENCE_FILTERS.length;ei++){
2088
+ var ef=EVIDENCE_FILTERS[ei],ecnt=evCounts[ef.v];
2089
+ if(ecnt===0)continue;
2090
+ var ecls='filter-chip'+(!!_filterState.evidence[ef.v]?' active':'');
2091
+ d+='<button class="'+ecls+'" data-dimension="evidence" data-value="'+ef.v+'">'+ef.label+' <span class="chip-count">'+ecnt+'</span></button>';
2092
+ }
2093
+ d+='</div></div>';
2094
+ }
2095
+
1712
2096
  /* Actions (Clear all + indicator) */
1713
2097
  d+='<div class="filter-drawer-actions">';
1714
2098
  d+='<button class="filter-clear" id="filter-clear" style="display:'+(hasAny?'inline-block':'none')+'">Clear all</button>';
@@ -1720,7 +2104,7 @@ function renderFilterBar(){
1720
2104
  }
1721
2105
 
1722
2106
  function initFilters(){
1723
- _filterState={status:{},type:{},specFile:{}};
2107
+ _filterState={status:{},type:{},specFile:{},visual:{},evidence:{}};
1724
2108
  _totalTests=tests.length;
1725
2109
  _originalSummary={total:data.summary.total,passed:data.summary.passed,
1726
2110
  failed:data.summary.failed,flaky:data.summary.flaky,
@@ -1731,12 +2115,27 @@ function getFilteredTests(){
1731
2115
  var hasStatus=Object.keys(_filterState.status).length>0;
1732
2116
  var hasType=Object.keys(_filterState.type).length>0;
1733
2117
  var hasSpec=Object.keys(_filterState.specFile).length>0;
2118
+ var hasVisual=Object.keys(_filterState.visual).length>0;
2119
+ var hasEvidence=Object.keys(_filterState.evidence).length>0;
1734
2120
  var out=[];
1735
2121
  for(var i=0;i<tests.length;i++){
1736
2122
  var t=tests[i];
1737
2123
  if(hasStatus&&!_filterState.status[t.status])continue;
1738
2124
  if(hasType&&!_filterState.type[t.testType||'unknown'])continue;
1739
2125
  if(hasSpec&&!_filterState.specFile[t.filePath])continue;
2126
+ if(hasVisual){
2127
+ /* OR within the dimension, matching every other filter here. */
2128
+ var visOk=(_filterState.visual.has&&_visCount(t)>0)||(_filterState.visual.failed&&_visFailed(t)>0);
2129
+ if(!visOk)continue;
2130
+ }
2131
+ if(hasEvidence){
2132
+ var evOk=false;
2133
+ for(var ef2=0;ef2<EVIDENCE_FILTERS.length;ef2++){
2134
+ var cand=EVIDENCE_FILTERS[ef2];
2135
+ if(_filterState.evidence[cand.v]&&cand.test(t)){evOk=true;break;}
2136
+ }
2137
+ if(!evOk)continue;
2138
+ }
1740
2139
  if(searchQuery){
1741
2140
  var q=searchQuery.toLowerCase();
1742
2141
  var match=t.title.toLowerCase().indexOf(q)>=0||t.filePath.toLowerCase().indexOf(q)>=0||(t.suiteName&&t.suiteName.toLowerCase().indexOf(q)>=0);
@@ -1759,7 +2158,7 @@ function toggleFilter(dim,val){
1759
2158
  }
1760
2159
 
1761
2160
  function clearAllFilters(){
1762
- _filterState={status:{},type:{},specFile:{}};
2161
+ for(var dim in _filterState){if(_filterState.hasOwnProperty(dim))_filterState[dim]={};}
1763
2162
  searchQuery='';
1764
2163
  var si=document.getElementById('search-input');
1765
2164
  if(si)si.value='';
@@ -1802,14 +2201,14 @@ function _updateCard(cls,val){
1802
2201
  var el=document.querySelector('.'+cls+' .s-count');
1803
2202
  if(el)el.textContent=''+val;
1804
2203
  }
1805
- `;var Fe=`
2204
+ `;var We=`
1806
2205
  /* \u2500\u2500 Event Delegation \u2500\u2500 */
1807
2206
  document.addEventListener('click',function(e){
1808
2207
  var target=e.target;
1809
2208
 
1810
2209
  /* Test row click \u2192 open drawer */
1811
2210
  var row=target.closest('.test-row');
1812
- if(row){openDrawer(row.getAttribute('data-testid'));return;}
2211
+ if(row){openTest(row.getAttribute('data-testid'));return;}
1813
2212
 
1814
2213
  /* Filter chip (multi-dimensional) */
1815
2214
  var chip=target.closest('.filter-chip');
@@ -1957,8 +2356,9 @@ function _updateCard(cls,val){
1957
2356
  if(themeBtn){setTheme(themeBtn.getAttribute('data-theme-val'));return;}
1958
2357
 
1959
2358
  /* Drawer close */
1960
- if(target.closest('#drawer-close')){closeDrawer();return;}
1961
- if(target.closest('#drawer-backdrop')){closeDrawer();return;}
2359
+ if(target.closest('#detail-back')){closeTest();return;}
2360
+ if(target.closest('#detail-prev')){stepTest(-1);return;}
2361
+ if(target.closest('#detail-next')){stepTest(1);return;}
1962
2362
  });
1963
2363
 
1964
2364
  /* Search input \u2014 main test search + network filter search */
@@ -1976,7 +2376,7 @@ function _updateCard(cls,val){
1976
2376
 
1977
2377
  /* Keyboard */
1978
2378
  document.addEventListener('keydown',function(e){
1979
- if(e.key==='Escape'){closeLightbox();closeDrawer();closeFilterDrawer();}
2379
+ if(e.key==='Escape'){closeLightbox();closeTest();closeFilterDrawer();}
1980
2380
  });
1981
2381
 
1982
2382
  /* \u2500\u2500 Theme Management \u2500\u2500 */
@@ -2072,7 +2472,7 @@ function _updateCard(cls,val){
2072
2472
  var d=document.getElementById('filter-drawer');if(d)d.classList.remove('open');
2073
2473
  var b=document.getElementById('filter-drawer-backdrop');if(b)b.classList.remove('open');
2074
2474
  }
2075
- `;var De=`
2475
+ `;var Ke=`
2076
2476
  /* \u2500\u2500 Category badge helpers \u2500\u2500 */
2077
2477
  function actionBadgeClass(cat){
2078
2478
  if(cat==='assertion')return 'action-badge-assertion';
@@ -2118,7 +2518,7 @@ function _updateCard(cls,val){
2118
2518
  h+='</div>';
2119
2519
  return h;
2120
2520
  }
2121
- `;var Be=`
2521
+ `;var Je=`
2122
2522
  /* \u2500\u2500 Console Log Level Colors & Labels \u2500\u2500 */
2123
2523
  var LOG_COLORS={log:'#9ca3af',warn:'#fbbf24',error:'#f87171',info:'#60a5fa',debug:'#a78bfa',stdout:'#9ca3af',stderr:'#f87171'};
2124
2524
  var LOG_LABELS={log:'LOG',warn:'WARN',error:'ERR',info:'INFO',debug:'DBG',stdout:'OUT',stderr:'ERR'};
@@ -2305,7 +2705,7 @@ function _updateCard(cls,val){
2305
2705
  }
2306
2706
  return h;
2307
2707
  }
2308
- `;var Oe=`
2708
+ `;var Ye=`
2309
2709
  /* \u2500\u2500 Artifact Utilities \u2500\u2500 */
2310
2710
  function formatBytes(bytes){
2311
2711
  if(bytes===0)return '0 B';
@@ -2314,41 +2714,1334 @@ function _updateCard(cls,val){
2314
2714
  if(i>=sizes.length)i=sizes.length-1;
2315
2715
  return parseFloat((bytes/Math.pow(k,i)).toFixed(1))+' '+sizes[i];
2316
2716
  }
2317
- `;var Un=`
2717
+ `;var Xe=`
2718
+ /* \u2500\u2500 Visual Regression \u2500\u2500 */
2719
+
2720
+ function visStatusLabel(status){
2721
+ if(status==='new')return 'new baseline';
2722
+ if(status==='updated')return 'baseline updated';
2723
+ return status;
2724
+ }
2725
+
2726
+ function visMetricsText(v){
2727
+ var parts=[];
2728
+ if(typeof v.diffPixels==='number')parts.push(v.diffPixels.toLocaleString()+' px');
2729
+ if(typeof v.diffPixelRatio==='number')parts.push((v.diffPixelRatio*100).toFixed(2)+'%');
2730
+ if(v.width&&v.height){
2731
+ var dims=v.width+'x'+v.height;
2732
+ /* Call out a size change explicitly: a comparison can fail purely
2733
+ because the render changed shape, and the pixel count alone reads
2734
+ like a content change when that happens. */
2735
+ if(v.actualWidth&&v.actualHeight&&(v.actualWidth!==v.width||v.actualHeight!==v.height))
2736
+ dims+=' -> '+v.actualWidth+'x'+v.actualHeight;
2737
+ parts.push(dims);
2738
+ }
2739
+ return parts.join(' \\u00b7 ');
2740
+ }
2741
+
2742
+ /* Deliberately no loading="lazy" on any image below. Every pane but the
2743
+ first is built inside a display:none container, and Chrome does not
2744
+ reliably start a lazy load when such an image is later revealed \u2014 the
2745
+ diff pane came up blank every time. A test has a handful of comparisons
2746
+ at most and they are only built when the drawer opens, so eager loading
2747
+ is by far the cheaper mistake. */
2748
+ function visImg(src,alt){
2749
+ return '<img class="artifact-col-img" src="'+esc(src)+'" alt="'+esc(alt)+'">';
2750
+ }
2751
+
2752
+ function visSideBySide(v){
2753
+ var h='<div class="vis-pair">';
2754
+ if(v.baseline)h+='<div><div class="vis-pane-label">Baseline</div>'+visImg(v.baseline,'Baseline')+'</div>';
2755
+ if(v.actual)h+='<div><div class="vis-pane-label">Actual</div>'+visImg(v.actual,'Actual')+'</div>';
2756
+ if(v.previous)h+='<div><div class="vis-pane-label">Previous attempt</div>'+visImg(v.previous,'Previous attempt')+'</div>';
2757
+ return h+'</div>';
2758
+ }
2759
+
2760
+ function visSlider(v,id){
2761
+ var h='<div class="vis-stack" data-vis-stack="'+id+'">';
2762
+ h+='<img src="'+esc(v.baseline)+'" alt="Baseline">';
2763
+ h+='<img class="vis-top" data-vis-wipe="'+id+'" src="'+esc(v.actual)+'" alt="Actual" style="clip-path:inset(0 0 0 50%)">';
2764
+ h+='<div class="vis-handle" data-vis-handle="'+id+'" style="left:50%"></div>';
2765
+ h+='</div>';
2766
+ h+='<div class="vis-range-row"><span>Baseline</span><input type="range" class="vis-range" min="0" max="100" value="50" data-vis-slider="'+id+'" aria-label="Wipe between baseline and actual"><span>Actual</span></div>';
2767
+ return h;
2768
+ }
2769
+
2770
+ function visOnion(v,id){
2771
+ var h='<div class="vis-stack">';
2772
+ h+='<img src="'+esc(v.baseline)+'" alt="Baseline">';
2773
+ h+='<img class="vis-top" data-vis-fade="'+id+'" src="'+esc(v.actual)+'" alt="Actual" style="opacity:0.5">';
2774
+ h+='</div>';
2775
+ h+='<div class="vis-range-row"><span>Baseline</span><input type="range" class="vis-range" min="0" max="100" value="50" data-vis-onion="'+id+'" aria-label="Blend between baseline and actual"><span>Actual</span></div>';
2776
+ return h;
2777
+ }
2778
+
2779
+ function visModes(v){
2780
+ var modes=[];
2781
+ if(v.baseline&&v.actual){
2782
+ modes.push({key:'side',label:'Side by side'});
2783
+ modes.push({key:'wipe',label:'Slider'});
2784
+ modes.push({key:'onion',label:'Onion skin'});
2785
+ }else if(v.baseline||v.actual){
2786
+ modes.push({key:'side',label:'Image'});
2787
+ }
2788
+ if(v.diff)modes.push({key:'diff',label:'Diff'});
2789
+ /* Only when a DOM baseline existed to compare against \u2014 an assertion
2790
+ written with Playwright's own expect compares pixels only. */
2791
+ if(v.domChangeCount!==undefined)modes.push({key:'dom',label:'Elements'});
2792
+ return modes;
2793
+ }
2794
+
2795
+ function visPaneBody(mode,v,id){
2796
+ if(mode==='side')return visSideBySide(v);
2797
+ if(mode==='wipe')return visSlider(v,id);
2798
+ if(mode==='onion')return visOnion(v,id);
2799
+ if(mode==='dom')return renderDomPane(v);
2800
+ return '<div class="vis-diff-wrap">'+visImg(v.diff,'Pixel diff')+'</div>';
2801
+ }
2802
+
2803
+ function renderVisualCard(v,idx){
2804
+ var id='v'+idx;
2805
+ var h='<div class="vis-card">';
2806
+ h+='<div class="vis-head"><span class="vis-name">'+esc(v.name)+'</span>';
2807
+ h+='<span class="vis-badge vb-'+esc(v.status)+'">'+esc(visStatusLabel(v.status))+'</span>';
2808
+ if(v.domChangeCount)h+='<span class="vis-badge vb-dom">'+v.domChangeCount+' element'+(v.domChangeCount===1?'':'s')+'</span>';
2809
+ var metrics=visMetricsText(v);
2810
+ if(metrics)h+='<span class="vis-metrics">'+esc(metrics)+'</span>';
2811
+ h+='</div>';
2812
+
2813
+ var modes=visModes(v);
2814
+ if(modes.length===0){
2815
+ h+='<div class="vis-note">No images were captured for this comparison.</div></div>';
2816
+ return h;
2817
+ }
2818
+
2819
+ if(v.status==='new')h+='<div class="vis-note">No baseline existed, so this render was written as one. Nothing was compared.</div>';
2820
+ else if(v.status==='updated')h+='<div class="vis-note">Run with --update-snapshots: the baseline was rewritten rather than compared.</div>';
2821
+
2822
+ if(modes.length>1){
2823
+ h+='<div class="seg-ctrl" data-seg-group="visual-'+id+'">';
2824
+ for(var m=0;m<modes.length;m++){
2825
+ h+='<button class="seg-btn'+(m===0?' active':'')+'" data-seg="'+id+'-'+modes[m].key+'">'+esc(modes[m].label)+'</button>';
2826
+ }
2827
+ h+='</div>';
2828
+ }
2829
+ for(var p=0;p<modes.length;p++){
2830
+ h+='<div class="seg-pane'+(p===0?' active':'')+'" data-seg-pane="'+id+'-'+modes[p].key+'">'+visPaneBody(modes[p].key,v,id)+'</div>';
2831
+ }
2832
+ return h+'</div>';
2833
+ }
2834
+
2835
+ function renderVisualSection(visual){
2836
+ if(!visual||visual.length===0)return '';
2837
+ var failed=0;
2838
+ for(var f=0;f<visual.length;f++)if(visual[f].status==='failed')failed++;
2839
+ var h='<div class="vis-header"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M12 3v18"/></svg>Visual comparisons <span class="vis-count">'+visual.length+'</span>';
2840
+ /* A separator, because two bare adjacent numbers read as one. */
2841
+ if(failed>0)h+='<span class="vis-count vis-count-fail">\xB7 '+failed+' failed</span>';
2842
+ h+='</div><div class="vis-list">';
2843
+ for(var i=0;i<visual.length;i++)h+=renderVisualCard(visual[i],i);
2844
+ return h+'</div>';
2845
+ }
2846
+
2847
+ /* Slider and onion-skin are driven by range inputs, which fire 'input'
2848
+ rather than 'click' \u2014 the delegated click handler cannot see them. One
2849
+ document-level listener covers every card, including cards rendered into
2850
+ the drawer long after this runs. */
2851
+ document.addEventListener('input',function(e){
2852
+ var el=e.target;
2853
+ if(!el||!el.getAttribute)return;
2854
+
2855
+ var wipeId=el.getAttribute('data-vis-slider');
2856
+ if(wipeId){
2857
+ var card=el.closest('.vis-card');
2858
+ if(!card)return;
2859
+ var top=card.querySelector('[data-vis-wipe="'+wipeId+'"]');
2860
+ var handle=card.querySelector('[data-vis-handle="'+wipeId+'"]');
2861
+ if(top)top.style.clipPath='inset(0 0 0 '+el.value+'%)';
2862
+ if(handle)handle.style.left=el.value+'%';
2863
+ return;
2864
+ }
2865
+
2866
+ var fadeId=el.getAttribute('data-vis-onion');
2867
+ if(fadeId){
2868
+ var ocard=el.closest('.vis-card');
2869
+ if(!ocard)return;
2870
+ var otop=ocard.querySelector('[data-vis-fade="'+fadeId+'"]');
2871
+ if(otop)otop.style.opacity=String(el.value/100);
2872
+ }
2873
+ });
2874
+ `;var Ze=`
2875
+ /* \u2500\u2500 DOM comparison \u2500\u2500 */
2876
+
2877
+ /* Rank-cycled so a row and its box always share a colour. */
2878
+ var DOM_TONES = ['#03b79c','#f59e0b','#60a5fa','#e879f9','#ef4444','#a3e635'];
2879
+ function domTone(rank){return DOM_TONES[(rank-1)%DOM_TONES.length];}
2880
+
2881
+ function domKindLabel(kind){
2882
+ if(kind==='restyled')return 'restyled';
2883
+ if(kind==='text')return 'text';
2884
+ if(kind==='resized')return 'resized';
2885
+ return kind;
2886
+ }
2887
+
2888
+ /* Percentages of the capture box: the image may be rendered at any size. */
2889
+ function domBoxStyle(rect,box){
2890
+ if(!rect||!box||!box[2]||!box[3])return null;
2891
+ var left=((rect[0]-box[0])/box[2])*100;
2892
+ var top=((rect[1]-box[1])/box[3])*100;
2893
+ var width=(rect[2]/box[2])*100;
2894
+ var height=(rect[3]/box[3])*100;
2895
+ /* An element scrolled out of the captured region has nothing to point at. */
2896
+ if(left>100||top>100||left+width<0||top+height<0)return null;
2897
+ return 'left:'+left.toFixed(3)+'%;top:'+top.toFixed(3)+'%;width:'+width.toFixed(3)+'%;height:'+height.toFixed(3)+'%';
2898
+ }
2899
+
2900
+ function renderDomOverlay(v){
2901
+ var h='<div class="dom-ov">';
2902
+ for(var i=0;i<v.domChanges.length;i++){
2903
+ var c=v.domChanges[i];
2904
+ /* A removed element has no box in this render; show where it used to be. */
2905
+ var style=domBoxStyle(c.rect||c.baselineRect,v.domCaptureBox);
2906
+ if(!style)continue;
2907
+ h+='<div class="dom-box" data-dom-box="'+c.rank+'" style="'+style+';border-color:'+domTone(c.rank)+'"></div>';
2908
+ }
2909
+ return h+'</div>';
2910
+ }
2911
+
2912
+ function renderDomRow(c){
2913
+ var h='<div class="dom-row" data-dom-row="'+c.rank+'">';
2914
+ h+='<span class="dom-swatch" style="background:'+domTone(c.rank)+'"></span>';
2915
+ h+='<span class="dom-label">'+esc(c.label)+'</span>';
2916
+ for(var k=0;k<c.kinds.length;k++){
2917
+ h+='<span class="dom-kind dk-'+esc(c.kinds[k])+'">'+esc(domKindLabel(c.kinds[k]))+'</span>';
2918
+ }
2919
+ h+='<span class="dom-area">'+(c.areaPct*100).toFixed(1)+'%</span>';
2920
+ h+='</div>';
2921
+
2922
+ var detail='';
2923
+ for(var a=0;a<(c.attrs||[]).length;a++){
2924
+ var at=c.attrs[a];
2925
+ detail+='<div class="dom-prop dom-prop-attr"><span class="dom-prop-name">'+esc(at.prop)+'</span>'+
2926
+ '<span class="dom-from">'+esc(at.from)+'</span>'+
2927
+ '<span class="dom-arrow">&rarr;</span>'+
2928
+ '<span class="dom-to">'+esc(at.to)+'</span></div>';
2929
+ }
2930
+ for(var s=0;s<(c.styles||[]).length;s++){
2931
+ var st=c.styles[s];
2932
+ detail+='<div class="dom-prop"><span class="dom-prop-name">'+esc(st.prop)+'</span>'+
2933
+ '<span class="dom-from">'+esc(st.from)+'</span>'+
2934
+ '<span class="dom-arrow">&rarr;</span>'+
2935
+ '<span class="dom-to">'+esc(st.to)+'</span></div>';
2936
+ }
2937
+ if(c.text){
2938
+ detail+='<div class="dom-prop"><span class="dom-prop-name">text</span>'+
2939
+ '<span class="dom-from">'+esc(c.text.from)+'</span>'+
2940
+ '<span class="dom-arrow">&rarr;</span>'+
2941
+ '<span class="dom-to">'+esc(c.text.to)+'</span></div>';
2942
+ }
2943
+ if(c.baselineRect&&c.rect){
2944
+ var moved=(c.baselineRect[0]!==c.rect[0]||c.baselineRect[1]!==c.rect[1]);
2945
+ var sized=(c.baselineRect[2]!==c.rect[2]||c.baselineRect[3]!==c.rect[3]);
2946
+ if(moved||sized){
2947
+ detail+='<div class="dom-prop"><span class="dom-prop-name">rect</span>'+
2948
+ '<span class="dom-from">'+c.baselineRect.join(', ')+'</span>'+
2949
+ '<span class="dom-arrow">&rarr;</span>'+
2950
+ '<span class="dom-to">'+c.rect.join(', ')+'</span></div>';
2951
+ }
2952
+ }
2953
+ if(detail)h+='<div class="dom-detail">'+detail+'</div>';
2954
+ return h;
2955
+ }
2956
+
2957
+ function renderDomPane(v){
2958
+ var changes=v.domChanges||[];
2959
+ if(changes.length===0){
2960
+ return '<div class="vis-note">The DOM matched its baseline exactly \\u2014 every element in the same place, with the same styles and text.</div>';
2961
+ }
2962
+
2963
+ var roots=[],shifted=[];
2964
+ for(var i=0;i<changes.length;i++){
2965
+ if(changes[i].consequence)shifted.push(changes[i]);else roots.push(changes[i]);
2966
+ }
2967
+
2968
+ var h='';
2969
+ var image=v.actual||v.baseline;
2970
+ if(image){
2971
+ h+='<div class="dom-figure">';
2972
+ h+='<img class="dom-img" src="'+esc(image)+'" alt="Changed elements">';
2973
+ h+=renderDomOverlay(v);
2974
+ h+='</div>';
2975
+ }
2976
+
2977
+ h+='<div class="dom-summary">';
2978
+ h+='<strong>'+(v.domChangeCount||changes.length)+'</strong> element'+((v.domChangeCount||changes.length)===1?'':'s')+' differ';
2979
+ if(shifted.length>0){
2980
+ /* Say what we actually know: these only moved. Not that the one above
2981
+ moved them \u2014 the diff does not prove causation and must not imply it. */
2982
+ h+=' \\u00b7 <strong>'+roots.length+'</strong> changed, <strong>'+shifted.length+'</strong> only shifted position';
2983
+ }
2984
+ if(v.domChangeCount&&v.domChangeCount>changes.length){
2985
+ h+=' \\u00b7 showing the top '+changes.length;
2986
+ }
2987
+ if(v.domTruncated)h+=' <span class="dom-truncated">capture truncated</span>';
2988
+ h+='</div>';
2989
+
2990
+ h+='<div class="dom-list">';
2991
+ for(var r=0;r<roots.length;r++)h+=renderDomRow(roots[r]);
2992
+ if(shifted.length>0){
2993
+ h+='<div class="dom-group-label">Moved or resized only</div>';
2994
+ for(var m=0;m<shifted.length;m++)h+=renderDomRow(shifted[m]);
2995
+ }
2996
+ h+='</div>';
2997
+ return h;
2998
+ }
2999
+
3000
+ /* Hover a row, highlight its box. Delegated so it covers cards that are
3001
+ rendered into the drawer long after this runs. */
3002
+ function domSetActive(card,rank,on){
3003
+ var box=card.querySelector('[data-dom-box="'+rank+'"]');
3004
+ if(box)box.classList.toggle('hi',on);
3005
+ var row=card.querySelector('[data-dom-row="'+rank+'"]');
3006
+ if(row)row.classList.toggle('hi',on);
3007
+ }
3008
+
3009
+ document.addEventListener('mouseover',function(e){
3010
+ var row=e.target.closest?e.target.closest('.dom-row'):null;
3011
+ if(!row)return;
3012
+ var card=row.closest('.vis-card');
3013
+ if(card)domSetActive(card,row.getAttribute('data-dom-row'),true);
3014
+ });
3015
+
3016
+ document.addEventListener('mouseout',function(e){
3017
+ var row=e.target.closest?e.target.closest('.dom-row'):null;
3018
+ if(!row)return;
3019
+ var card=row.closest('.vis-card');
3020
+ if(card&&!row.classList.contains('pinned'))domSetActive(card,row.getAttribute('data-dom-row'),false);
3021
+ });
3022
+ `;var Qe=`
3023
+ /* \u2500\u2500 Verdict band \u2500\u2500 */
3024
+
3025
+ /* Both report modes hand us a summary; streaming's carries fewer fields. */
3026
+ function vNum(v){return typeof v==='number'?v:0;}
3027
+
3028
+ function renderVerdictBar(s){
3029
+ var order=[
3030
+ {k:'passed',n:vNum(s.passed)},{k:'failed',n:vNum(s.failed)},
3031
+ {k:'flaky',n:vNum(s.flaky)},{k:'timedout',n:vNum(s.timedout)},
3032
+ {k:'skipped',n:vNum(s.skipped)}
3033
+ ];
3034
+ var total=0;for(var i=0;i<order.length;i++)total+=order[i].n;
3035
+ if(total===0)return '<div class="verdict-bar"><span class="verdict-bar-empty"></span></div>';
3036
+ var h='<div class="verdict-bar">';
3037
+ for(var j=0;j<order.length;j++){
3038
+ if(order[j].n===0)continue;
3039
+ h+='<span class="vb-'+order[j].k+'" style="flex:'+order[j].n+'"></span>';
3040
+ }
3041
+ return h+'</div>';
3042
+ }
3043
+
3044
+ function renderVerdictHeadline(s){
3045
+ var failed=vNum(s.failed),total=vNum(s.total),flaky=vNum(s.flaky);
3046
+ if(failed>0)return failed+' of '+total+' test'+(total===1?'':'s')+' failed';
3047
+ if(flaky>0)return flaky+' of '+total+' test'+(total===1?'':'s')+' passed on retry';
3048
+ if(total===0)return 'No tests ran';
3049
+ return 'All '+total+' test'+(total===1?'':'s')+' passed';
3050
+ }
3051
+
3052
+ /* Where the failures are, when they are all in one place \u2014 saves a scan. */
3053
+ function failureLocation(){
3054
+ var files={};var n=0;
3055
+ for(var i=0;i<tests.length;i++){
3056
+ var t=tests[i];
3057
+ if(t.status!=='failed'&&t.status!=='timedout')continue;
3058
+ if(!files[t.filePath]){files[t.filePath]=1;n++;}
3059
+ }
3060
+ if(n!==1)return null;
3061
+ for(var f in files){if(files.hasOwnProperty(f))return f;}
3062
+ return null;
3063
+ }
3064
+
3065
+ function renderSummary(){
3066
+ var s=data.summary||{};
3067
+ var failed=vNum(s.failed),flaky=vNum(s.flaky);
3068
+ var cls=failed>0?'v-failed':(flaky>0?'':'v-passed');
3069
+
3070
+ var h='<div class="verdict"><div class="verdict-main '+cls+'">';
3071
+
3072
+ h+='<div class="verdict-head"><div class="verdict-title">'+esc(renderVerdictHeadline(s))+'</div>';
3073
+ var where=failed>0?failureLocation():null;
3074
+ if(where)h+='<div class="verdict-where">all in <code>'+esc(where.split('/').pop())+'</code></div>';
3075
+ h+='<div class="verdict-dur"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 2"/></svg>'+fmtDur(data.totalDuration||0)+'</div>';
3076
+ h+='</div>';
3077
+
3078
+ h+=renderVerdictBar(s);
3079
+
3080
+ /* Non-zero statuses get a real count; the rest collapse to one grey line
3081
+ rather than each taking a sixth of the width. */
3082
+ var kinds=[
3083
+ {k:'passed',l:'passed',c:'#22c55e'},{k:'failed',l:'failed',c:'#ef4444'},
3084
+ {k:'flaky',l:'flaky',c:'#f59e0b'},{k:'timedout',l:'timed out',c:'#f97316'},
3085
+ {k:'skipped',l:'skipped',c:'#6b7280'}
3086
+ ];
3087
+ var counts='',zeros=[];
3088
+ for(var m=0;m<kinds.length;m++){
3089
+ var kk=kinds[m],n=vNum(s[kk.k]);
3090
+ if(n>0){
3091
+ counts+='<div class="vc"><span class="vc-dot" style="background:'+kk.c+'"></span><span class="vc-n">'+n+'</span><span class="vc-l">'+kk.l+'</span></div>';
3092
+ }else{
3093
+ zeros.push('0 '+kk.l);
3094
+ }
3095
+ }
3096
+ h+='<div class="verdict-counts">'+counts;
3097
+ if(zeros.length>0)h+='<div class="verdict-zeros">'+esc(zeros.join(' \\u00b7 '))+'</div>';
3098
+ h+='</div>';
3099
+
3100
+ h+='</div>';
3101
+
3102
+ /* What this run actually captured \u2014 only tiles with something in them. */
3103
+ var tiles=[];
3104
+ if(vNum(s.visualChecks)>0)tiles.push({n:vNum(s.visualChecks),l:'visual checks',cls:''});
3105
+ if(vNum(s.visualDomChanges)>0)tiles.push({n:vNum(s.visualDomChanges),l:'DOM changes',cls:' vs-dom'});
3106
+ if(vNum(s.totalActionSteps)>0)tiles.push({n:vNum(s.totalActionSteps),l:'actions',cls:''});
3107
+ if(vNum(s.totalApiCalls)>0)tiles.push({n:vNum(s.totalApiCalls),l:'API calls',cls:''});
3108
+ if(vNum(s.totalNavigations)>0)tiles.push({n:vNum(s.totalNavigations),l:'navigations',cls:''});
3109
+
3110
+ if(tiles.length>0){
3111
+ h+='<div class="verdict-side"><div class="verdict-side-label">Captured this run</div><div class="verdict-side-grid">';
3112
+ for(var q=0;q<Math.min(tiles.length,4);q++){
3113
+ h+='<div><div class="vs-n'+tiles[q].cls+'">'+tiles[q].n.toLocaleString()+'</div><div class="vs-l">'+esc(tiles[q].l)+'</div></div>';
3114
+ }
3115
+ h+='</div></div>';
3116
+ }
3117
+
3118
+ h+='</div>';
3119
+ summaryEl.innerHTML=h;
3120
+ }
3121
+
3122
+ /* \u2500\u2500 Needs attention \u2500\u2500 */
3123
+
3124
+ /* The first line of the error, which is the part that says what happened.
3125
+ Playwright's colour codes are stripped by the reporter before this. */
3126
+ function causeLine(t){
3127
+ if(t.firstFailedAssertion&&typeof assertionSentence==='function'){
3128
+ return '<em>'+assertionSentence(t.firstFailedAssertion)+'</em>';
3129
+ }
3130
+ if(t.assertionsFailed>0&&typeof apiFailureSentence==='function'&&t.apiAssertions){
3131
+ var sentence=apiFailureSentence(t.apiAssertions);
3132
+ if(sentence)return '<em>'+sentence+'</em>';
3133
+ }
3134
+ if(evVisualFailed(t)>0&&t.errorMessage){
3135
+ var px=t.errorMessage.match(/([\\d,]+) pixels \\(ratio ([\\d.]+)/);
3136
+ if(px)return '<em>'+esc(px[1])+' px differ</em>';
3137
+ }
3138
+ if(!t.errorMessage)return '';
3139
+ var first=String(t.errorMessage).split('\\n')[0].trim();
3140
+ if(first.length>150)first=first.slice(0,150)+'\\u2026';
3141
+ return '<em>'+esc(first)+'</em>';
3142
+ }
3143
+
3144
+ function evidenceChips(t){
3145
+ var h='';
3146
+ /* These accessors read both report shapes \u2014 see html-js-render. */
3147
+ var _vis=evVisualCount(t),_visFail=evVisualFailed(t),_dom=evDomChanges(t),_net=evNetworkCount(t);
3148
+ if(_vis>0){
3149
+ h+='<span class="nchip'+(_visFail>0?' nchip-visual':'')+'"><svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M12 3v18"/></svg>'+(_visFail>0?'Visual diff':'Visual')+'</span>';
3150
+ }
3151
+ if(_dom>0){
3152
+ h+='<span class="nchip nchip-dom"><svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M8 3H5a2 2 0 00-2 2v3M16 3h3a2 2 0 012 2v3M8 21H5a2 2 0 01-2-2v-3M16 21h3a2 2 0 002-2v-3"/></svg>'+_dom+' element'+(_dom===1?'':'s')+'</span>';
3153
+ }
3154
+ if(evHasArtifacts(t)){
3155
+ h+='<span class="nchip"><svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="5 3 19 12 5 21"/></svg>Video</span>';
3156
+ }
3157
+ if(_net>0){
3158
+ h+='<span class="nchip"><svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 12h4l3 8 4-16 3 8h4"/></svg>'+_net+' requests</span>';
3159
+ }
3160
+ if(t.apiCallCount>0){
3161
+ h+='<span class="nchip">'+t.apiCallCount+' API call'+(t.apiCallCount===1?'':'s')+'</span>';
3162
+ }
3163
+ if(t.assertionsFailed>0){
3164
+ h+='<span class="nchip nchip-visual">'+t.assertionsFailed+' failed assertion'+(t.assertionsFailed===1?'':'s')+'</span>';
3165
+ }else if(t.assertionsPassed>0){
3166
+ h+='<span class="nchip">'+t.assertionsPassed+' assertion'+(t.assertionsPassed===1?'':'s')+'</span>';
3167
+ }
3168
+ if(typeof mobileEvidenceChips==='function')h+=mobileEvidenceChips(t);
3169
+ return h;
3170
+ }
3171
+
3172
+ function renderNeedsAttention(){
3173
+ var el=document.getElementById('needs-attention');
3174
+ if(!el)return;
3175
+
3176
+ var bad=[];
3177
+ for(var i=0;i<tests.length;i++){
3178
+ var st=tests[i].status;
3179
+ if(st==='failed'||st==='timedout'||st==='flaky')bad.push(tests[i]);
3180
+ }
3181
+ if(bad.length===0){el.innerHTML='';return;}
3182
+
3183
+ /* Hard failures first, then flakes; biggest problems at the top. */
3184
+ bad.sort(function(a,b){
3185
+ var rank=function(x){return x.status==='flaky'?1:0;};
3186
+ return rank(a)-rank(b);
3187
+ });
3188
+
3189
+ var h='<div class="needs"><div class="needs-head"><span class="needs-title">Needs attention</span><span class="needs-count">'+bad.length+'</span></div><div class="needs-list">';
3190
+
3191
+ for(var j=0;j<Math.min(bad.length,8);j++){
3192
+ var t=bad[j];
3193
+ h+='<div class="needs-card'+(t.status==='flaky'?' nc-flaky':'')+'" data-testid="'+esc(t.testId)+'">';
3194
+
3195
+ h+='<div class="needs-body">';
3196
+ h+='<div class="needs-row1"><span class="needs-name">'+esc(t.title)+'</span>';
3197
+ if(t.testType&&t.testType!=='unknown')h+='<span class="trb trb-type">'+esc(t.testType)+'</span>';
3198
+ if(t.status==='flaky')h+='<span class="trb trb-flaky">passed on retry</span>';
3199
+ h+='<span class="needs-file">'+esc(t.filePath)+'</span></div>';
3200
+
3201
+ var cause=causeLine(t);
3202
+ if(cause)h+='<div class="needs-cause">'+cause+'</div>';
3203
+
3204
+ var chips=evidenceChips(t);
3205
+ if(chips)h+='<div class="needs-chips">'+chips+'</div>';
3206
+ h+='</div>';
3207
+
3208
+ /* Show the failure, do not only describe it. */
3209
+ var _diff=evDiffPath(t);
3210
+ if(_diff){
3211
+ h+='<div class="needs-thumb"><img src="'+esc(_diff)+'" alt="Pixel diff"><div class="needs-thumb-cap">pixel diff</div></div>';
3212
+ }
3213
+
3214
+ h+='<div class="needs-right"><div class="needs-dur">'+fmtDur(t.duration)+'</div>';
3215
+ h+='<div class="needs-open">Open<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2"><path d="M9 18l6-6-6-6"/></svg></div></div>';
3216
+
3217
+ h+='</div>';
3218
+ }
3219
+
3220
+ if(bad.length>8)h+='<div class="needs-thumb-cap" style="text-align:left;padding:4px 2px">and '+(bad.length-8)+' more below</div>';
3221
+
3222
+ el.innerHTML=h+'</div></div>';
3223
+ }
3224
+
3225
+ /* \u2500\u2500 Detail section rail \u2500\u2500 */
3226
+
3227
+ var RAIL_SECTIONS=[
3228
+ {id:'failure',label:'Why it failed',fail:true},
3229
+ {id:'visual',label:'Visual'},
3230
+ {id:'dom',label:'Elements',dom:true},
3231
+ {id:'assertions',label:'Assertions'},
3232
+ {id:'artifacts',label:'Video & trace'},
3233
+ {id:'actions',label:'Actions'},
3234
+ {id:'timeline',label:'Timeline'},
3235
+ {id:'api',label:'API calls'},
3236
+ {id:'console',label:'Console'},
3237
+ {id:'network',label:'Network'}
3238
+ ];
3239
+
3240
+ function railIcon(id){
3241
+ if(id==='failure')return '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="9"/><path d="M12 8v4M12 16h.01"/></svg>';
3242
+ if(id==='visual')return '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M12 3v18"/></svg>';
3243
+ if(id==='dom')return '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M8 3H5a2 2 0 00-2 2v3M16 3h3a2 2 0 012 2v3M8 21H5a2 2 0 01-2-2v-3M16 21h3a2 2 0 002-2v-3"/></svg>';
3244
+ if(id==='artifacts')return '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="5 3 19 12 5 21"/></svg>';
3245
+ if(id==='actions')return '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 20V10M18 20V4M6 20v-4"/></svg>';
3246
+ if(id==='assertions')return '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20 6L9 17l-5-5"/></svg>';
3247
+ if(id==='console')return '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 17l6-6-6-6M12 19h8"/></svg>';
3248
+ return '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 12h4l3 8 4-16 3 8h4"/></svg>';
3249
+ }
3250
+
3251
+ /* Built from what the drawer actually rendered, so the rail can never
3252
+ offer a jump to a section that is not there. */
3253
+ function renderDrawerRail(present,t){
3254
+ var h='<div class="rail-group"><div class="rail-label">Evidence</div>';
3255
+ var first=true;
3256
+ for(var i=0;i<RAIL_SECTIONS.length;i++){
3257
+ var sec=RAIL_SECTIONS[i];
3258
+ var info=present[sec.id];
3259
+ if(!info)continue;
3260
+ h+='<button class="rail-item'+(sec.fail?' rail-fail':'')+(first?' active':'')+'" data-rail="'+sec.id+'">';
3261
+ h+=railIcon(sec.id);
3262
+ h+='<span class="rail-name">'+esc(sec.label)+'</span>';
3263
+ if(info.n)h+='<span class="rail-n'+(sec.dom?' rail-n-dom':'')+'">'+info.n+'</span>';
3264
+ h+='</button>';
3265
+ first=false;
3266
+ }
3267
+ h+='</div>';
3268
+
3269
+ h+='<div class="rail-meta"><div class="rail-label">Test</div><div class="rail-meta-rows">';
3270
+ if(t.testId)h+='<div class="rail-meta-row"><span class="rail-meta-k">ID</span><span class="rail-meta-v mono">'+esc(String(t.testId).substring(0,12))+'</span></div>';
3271
+ if(t.suiteName)h+='<div class="rail-meta-row"><span class="rail-meta-k">Suite</span><span class="rail-meta-v">'+esc(t.suiteName)+'</span></div>';
3272
+ h+='<div class="rail-meta-row"><span class="rail-meta-k">Retry</span><span class="rail-meta-v">'+(t.retryCount>0?t.retryCount+'':'none')+'</span></div>';
3273
+ h+='</div></div>';
3274
+ return h;
3275
+ }
3276
+
3277
+ /* Scroll the page so a section's top is just under the viewport top.
3278
+ A smooth scrollIntoView was silently dropped here: the rail
3279
+ item is a <button>, and a smooth scroll requested from inside its click
3280
+ handler never started \u2014 the section stayed off-screen and the rail entry
3281
+ did nothing. Instant scrolling from a computed offset always lands, and
3282
+ smooth is attempted first with a check that it actually moved. */
3283
+ function jumpToSection(el){
3284
+ if(!el)return;
3285
+ var pageY=window.scrollY!==undefined?window.scrollY:(window.pageYOffset||0);
3286
+ var y=Math.max(0,el.getBoundingClientRect().top+pageY-12);
3287
+ try{
3288
+ window.scrollTo({top:y,behavior:'smooth'});
3289
+ }catch(e){
3290
+ window.scrollTo(0,y);
3291
+ }
3292
+ setTimeout(function(){
3293
+ var now=window.scrollY!==undefined?window.scrollY:(window.pageYOffset||0);
3294
+ if(Math.abs(now-y)>40)window.scrollTo(0,y);
3295
+ },350);
3296
+ }
3297
+
3298
+ /* Jump, rather than scroll past everything above it. */
3299
+ document.addEventListener('click',function(e){
3300
+ var btn=e.target.closest?e.target.closest('.rail-item'):null;
3301
+ if(!btn)return;
3302
+ var id=btn.getAttribute('data-rail');
3303
+
3304
+ /* Elements is not a section of its own \u2014 it is a pane inside Visual. Jump
3305
+ to Visual and switch the pane, so the rail entry lands where it says. */
3306
+ var sectionId=id==='dom'?'visual':id;
3307
+ var target=document.querySelector('[data-section="'+sectionId+'"]');
3308
+ if(id==='dom'&&target){
3309
+ var btns=target.querySelectorAll('.seg-btn');
3310
+ for(var b2=0;b2<btns.length;b2++){
3311
+ if(btns[b2].textContent==='Elements'){btns[b2].click();break;}
3312
+ }
3313
+ }
3314
+ jumpToSection(target);
3315
+ var all=document.querySelectorAll('.rail-item');
3316
+ for(var i=0;i<all.length;i++)all[i].classList.remove('active');
3317
+ btn.classList.add('active');
3318
+ });
3319
+
3320
+ /* A failing test opens on its failure, not on the top of a long page. */
3321
+ document.addEventListener('click',function(e){
3322
+ var card=e.target.closest?e.target.closest('.needs-card'):null;
3323
+ if(!card)return;
3324
+ openTest(card.getAttribute('data-testid'));
3325
+ });
3326
+ `,et=`
3327
+ var overviewViewEl=document.getElementById('view-overview');
3328
+ var detailViewEl=document.getElementById('view-detail');
3329
+ var detailNavEl=document.getElementById('detail-nav');
3330
+ var _baseTitle=document.title;
3331
+ var _overviewScroll=0;
3332
+ var _currentTestId=null;
3333
+ /* Set while we are the ones changing the URL, so our own write does not
3334
+ bounce back through the router as if the user had pressed Back. */
3335
+ var _suppressRoute=false;
3336
+
3337
+ function testHash(id){return '#test/'+encodeURIComponent(id);}
3338
+
3339
+ function parseTestHash(){
3340
+ var h=String(location.hash||'');
3341
+ if(h.indexOf('#test/')!==0)return null;
3342
+ try{return decodeURIComponent(h.slice(6));}catch(e){return h.slice(6);}
3343
+ }
3344
+
3345
+ /* Switch the view FIRST, then record it in the URL.
3346
+ Driving the switch off the hashchange event made the whole navigation
3347
+ hostage to an event that is not always delivered \u2014 the view simply never
3348
+ changed. The URL still updates, so deep links and Back keep working, but
3349
+ nothing user-visible depends on the event arriving. */
3350
+ function setHashQuietly(value){
3351
+ try{
3352
+ _suppressRoute=true;
3353
+ if(history&&history.replaceState){
3354
+ history.pushState(null,'',value||location.pathname+location.search);
3355
+ }else{
3356
+ location.hash=value||'';
3357
+ }
3358
+ }catch(e){
3359
+ /* pushState is refused on file:// \u2014 the hash still works there. */
3360
+ try{location.hash=value||'';}catch(e2){/* no URL update; the view is right */}
3361
+ }
3362
+ setTimeout(function(){_suppressRoute=false;},0);
3363
+ }
3364
+
3365
+ /* Whether the detail view we are showing came from a push of ours, and so
3366
+ can be closed by unwinding history instead of pushing again. */
3367
+ var _pushedDetailEntry=false;
3368
+
3369
+ function openTest(testId){
3370
+ if(!testId||!testMap[testId])return;
3371
+ showTest(testId);
3372
+ setHashQuietly(testHash(testId));
3373
+ _pushedDetailEntry=true;
3374
+ }
3375
+
3376
+ function closeTest(){
3377
+ if(!_currentTestId)return;
3378
+ /* Leave the detail entry behind rather than stacking another one on top of
3379
+ it. setHashQuietly pushes, so closing this way left #test/<id> in
3380
+ history *behind* the overview \u2014 the browser Back button then replayed it
3381
+ and re-opened the very test the reader had just closed.
3382
+ history.back() unwinds our own push; routeFromHash handles the
3383
+ resulting popstate and switches the view. */
3384
+ if(_pushedDetailEntry&&history&&history.back){
3385
+ _pushedDetailEntry=false;
3386
+ history.back();
3387
+ return;
3388
+ }
3389
+ showOverview();
3390
+ setHashQuietly('');
3391
+ }
3392
+
3393
+ /* Prev/next walk the CURRENTLY FILTERED list, so stepping through a
3394
+ "Failed" filter visits the failures and nothing else. */
3395
+ function testNeighbours(testId){
3396
+ var list=getFilteredTests();
3397
+ for(var i=0;i<list.length;i++){
3398
+ if(list[i].testId===testId)return {list:list,idx:i};
3399
+ }
3400
+ return {list:list,idx:-1};
3401
+ }
3402
+
3403
+ function stepTest(delta){
3404
+ if(!_currentTestId)return;
3405
+ var n=testNeighbours(_currentTestId);
3406
+ if(n.idx<0)return;
3407
+ var next=n.list[n.idx+delta];
3408
+ if(next)openTest(next.testId);
3409
+ }
3410
+
3411
+ function renderDetailNav(testId){
3412
+ var t=testMap[testId]||{};
3413
+ var n=testNeighbours(testId);
3414
+ var pos=n.idx>=0?(n.idx+1)+' of '+n.list.length:'';
3415
+
3416
+ var h='<button class="detail-back" id="detail-back"><svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M15 18l-6-6 6-6"/></svg>Back to results</button>';
3417
+ if(t.filePath)h+='<span class="detail-crumb">'+esc(t.filePath)+'</span>';
3418
+
3419
+ h+='<div class="detail-steps">';
3420
+ var noPrev=n.idx<=0;
3421
+ var noNext=n.idx<0||n.idx>=n.list.length-1;
3422
+ h+='<button class="detail-step'+(noPrev?' is-off':'')+'" id="detail-prev" title="Previous test"'+(noPrev?' disabled':'')+'><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M15 18l-6-6 6-6"/></svg></button>';
3423
+ if(pos)h+='<span class="detail-pos">'+esc(pos)+'</span>';
3424
+ h+='<button class="detail-step'+(noNext?' is-off':'')+'" id="detail-next" title="Next test"'+(noNext?' disabled':'')+'><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 18l6-6-6-6"/></svg></button>';
3425
+ h+='</div>';
3426
+
3427
+ detailNavEl.innerHTML=h;
3428
+ }
3429
+
3430
+ function showOverview(){
3431
+ _currentTestId=null;
3432
+ var v=drawerBodyEl.querySelector('video');
3433
+ if(v)v.pause();
3434
+ detailViewEl.hidden=true;
3435
+ overviewViewEl.hidden=false;
3436
+ document.title=_baseTitle;
3437
+ /* Put the reader back where they were in the list, not at the top. */
3438
+ window.scrollTo(0,_overviewScroll);
3439
+ }
3440
+
3441
+ function showTest(testId){
3442
+ if(_currentTestId===null)_overviewScroll=window.scrollY||window.pageYOffset||0;
3443
+ _currentTestId=testId;
3444
+ overviewViewEl.hidden=true;
3445
+ detailViewEl.hidden=false;
3446
+ window.scrollTo(0,0);
3447
+ var t=testMap[testId];
3448
+ document.title=(t&&t.title?t.title+' \u2014 ':'')+_baseTitle;
3449
+ renderDetailNav(testId);
3450
+ loadAndRenderTest(testId);
3451
+ }
3452
+
3453
+ /* Back/forward, and the initial load of a pasted link. Idempotent: a
3454
+ re-entrant call for the view already showing does nothing. */
3455
+ function routeFromHash(){
3456
+ if(_suppressRoute)return;
3457
+ var id=parseTestHash();
3458
+ if(id&&testMap[id]){
3459
+ if(_currentTestId!==id)showTest(id);
3460
+ }else if(_currentTestId!==null){
3461
+ showOverview();
3462
+ }
3463
+ }
3464
+
3465
+ window.addEventListener('hashchange',routeFromHash);
3466
+ window.addEventListener('popstate',routeFromHash);
3467
+ `;var tt=`
3468
+ var API_METHOD_COLOR={GET:'#60a5fa',POST:'#34d399',PUT:'#fbbf24',PATCH:'#a78bfa',DELETE:'#f87171',HEAD:'#6b7280',OPTIONS:'#6b7280'};
3469
+ var API_STATUS_ORDER=['2xx','3xx','4xx','5xx','error'];
3470
+ var API_STATUS_COLOR={'2xx':'#22c55e','3xx':'#60a5fa','4xx':'#fb923c','5xx':'#ef4444','error':'#ef4444'};
3471
+
3472
+ function apiMethodColor(m){return API_METHOD_COLOR[String(m||'').toUpperCase()]||'#8b949e';}
3473
+
3474
+ /* A value from an assertion. These are arbitrary JSON, so keep them short
3475
+ enough to sit on one line and let the reader open the call for the rest. */
3476
+ function fmtAssertValue(v){
3477
+ if(v===undefined)return 'undefined';
3478
+ if(v===null)return 'null';
3479
+ var s;
3480
+ if(typeof v==='string'){s=v;}
3481
+ else{try{s=JSON.stringify(v);}catch(e){s=String(v);}}
3482
+ if(s===undefined||s===null)s=String(v);
3483
+ if(s.length>120)s=s.slice(0,120)+'\\u2026';
3484
+ return s;
3485
+ }
3486
+
3487
+ function statusMixTotal(mix){
3488
+ if(!mix)return 0;
3489
+ var n=0;
3490
+ for(var i=0;i<API_STATUS_ORDER.length;i++)n+=(mix[API_STATUS_ORDER[i]]||0);
3491
+ return n;
3492
+ }
3493
+
3494
+ /* \u2500\u2500 Run-level API band \u2500\u2500 */
3495
+
3496
+ function renderApiMethodCard(byMethod,total){
3497
+ var keys=[];
3498
+ for(var k in byMethod){if(byMethod.hasOwnProperty(k)&&byMethod[k]>0)keys.push(k);}
3499
+ if(keys.length===0)return '';
3500
+ keys.sort(function(a,b){return byMethod[b]-byMethod[a];});
3501
+ var max=byMethod[keys[0]]||1;
3502
+ var h='<div class="tcard"><div class="tcard-h">By method<span class="tcard-sub">'+total.toLocaleString()+' calls</span></div><div style="display:flex;flex-direction:column;gap:8px">';
3503
+ for(var i=0;i<keys.length;i++){
3504
+ var m=keys[i],c=apiMethodColor(m),pct=Math.max(2,Math.round((byMethod[m]/max)*100));
3505
+ h+='<div class="mrow"><span class="mrow-k" style="color:'+c+'">'+esc(m)+'</span>';
3506
+ h+='<span class="mrow-track"><span class="mrow-fill" style="width:'+pct+'%;background:'+c+'"></span></span>';
3507
+ h+='<span class="mrow-n">'+byMethod[m].toLocaleString()+'</span></div>';
3508
+ }
3509
+ return h+'</div></div>';
3510
+ }
3511
+
3512
+ function renderApiStatusCard(mix){
3513
+ var total=statusMixTotal(mix);
3514
+ if(total===0)return '';
3515
+ var h='<div class="tcard"><div class="tcard-h">By status</div><div class="sbar">';
3516
+ for(var i=0;i<API_STATUS_ORDER.length;i++){
3517
+ var k=API_STATUS_ORDER[i],n=mix[k]||0;
3518
+ if(n===0)continue;
3519
+ h+='<span style="flex:'+n+';background:'+API_STATUS_COLOR[k]+'"></span>';
3520
+ }
3521
+ h+='</div><div class="sgrid">';
3522
+ for(var j=0;j<API_STATUS_ORDER.length;j++){
3523
+ var kk=API_STATUS_ORDER[j],nn=mix[kk]||0;
3524
+ h+='<div class="scell"><span class="spill sp-'+kk+'">'+esc(kk)+'</span>';
3525
+ h+='<span class="scell-n'+(nn===0?' is-zero':'')+'">'+nn.toLocaleString()+'</span></div>';
3526
+ }
3527
+ return h+'</div></div>';
3528
+ }
3529
+
3530
+ function renderAssertionsCard(s){
3531
+ var total=vNum(s.totalAssertions);
3532
+ if(total===0)return '';
3533
+ var pass=vNum(s.passedAssertions),fail=vNum(s.failedAssertions);
3534
+ var h='<div class="tcard"><div class="tcard-h">Assertions</div>';
3535
+ h+='<div class="tbig"><span class="tbig-n">'+total.toLocaleString()+'</span><span class="tbig-l">total</span></div>';
3536
+ h+='<div class="tsplit">';
3537
+ h+='<span class="tsplit-i tsplit-ok"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M20 6L9 17l-5-5"/></svg>'+pass.toLocaleString()+'</span>';
3538
+ h+='<span class="tsplit-i '+(fail>0?'tsplit-bad':'')+'" style="'+(fail>0?'':'color:var(--fg-2)')+'"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M18 6L6 18M6 6l12 12"/></svg>'+fail.toLocaleString()+'</span>';
3539
+ h+='</div></div>';
3540
+ return h;
3541
+ }
3542
+
3543
+ function renderApiTimingCard(rt,uniqueUrls){
3544
+ if(!rt)return '';
3545
+ var h='<div class="tcard"><div class="tcard-h">Response time</div><div class="tpair">';
3546
+ h+='<div class="tpair-i"><span class="tpair-n">'+Math.round(rt.avg)+'<small>ms</small></span><span class="tpair-l">average</span></div>';
3547
+ h+='<div class="tpair-i"><span class="tpair-n">'+Math.round(rt.p95)+'<small>ms</small></span><span class="tpair-l">p95</span></div>';
3548
+ h+='<div class="tpair-i"><span class="tpair-n">'+Math.round(rt.max)+'<small>ms</small></span><span class="tpair-l">slowest</span></div>';
3549
+ if(uniqueUrls>0)h+='<div class="tpair-i"><span class="tpair-n">'+uniqueUrls.toLocaleString()+'</span><span class="tpair-l">endpoints</span></div>';
3550
+ return h+'</div></div>';
3551
+ }
3552
+
3553
+ /* The band only exists when the run actually produced the evidence it
3554
+ describes \u2014 a pure-UI run renders nothing here rather than four zeroes. */
3555
+ function renderTypeBand(){
3556
+ var el=document.getElementById('type-band');
3557
+ if(!el)return;
3558
+ var s=data.summary||{};
3559
+ var cards=[];
3560
+
3561
+ if(vNum(s.totalApiCalls)>0){
3562
+ var mc=renderApiMethodCard(s.apiCallsByMethod||{},vNum(s.totalApiCalls));
3563
+ if(mc)cards.push(mc);
3564
+ var sc=renderApiStatusCard(s.apiCallsByStatusRange||{});
3565
+ if(sc)cards.push(sc);
3566
+ var tc=renderApiTimingCard(s.apiResponseTime,vNum(s.uniqueApiUrls));
3567
+ if(tc)cards.push(tc);
3568
+ }
3569
+ var ac=renderAssertionsCard(s);
3570
+ if(ac)cards.push(ac);
3571
+
3572
+ if(typeof renderMobileCards==='function'){
3573
+ var mob=renderMobileCards(s);
3574
+ for(var mi=0;mi<mob.length;mi++)cards.push(mob[mi]);
3575
+ }
3576
+
3577
+ if(cards.length===0){el.innerHTML='';return;}
3578
+ var cls=cards.length>=3?'tband-3':'tband-2';
3579
+ /* Four cards wrap to two rows of two rather than shrinking to slivers. */
3580
+ if(cards.length===4)cls='tband-2';
3581
+ el.innerHTML='<div class="tband '+cls+'">'+cards.join('')+'</div>';
3582
+ }
3583
+
3584
+ /* \u2500\u2500 Per-row evidence \u2500\u2500 */
3585
+
3586
+ /* Status chips plus a single pass/fail mark. A row has roughly 110px for
3587
+ this, so it shows the shape of the calls, not the calls themselves. */
3588
+ function apiRowEvidence(t){
3589
+ if(!t)return '';
3590
+ var mix=t.apiStatusMix,h='';
3591
+ if(mix&&statusMixTotal(mix)>0){
3592
+ for(var i=0;i<API_STATUS_ORDER.length;i++){
3593
+ var k=API_STATUS_ORDER[i],n=mix[k]||0;
3594
+ if(n===0)continue;
3595
+ h+='<span class="achip sp-'+k+'" title="'+n+' '+esc(k)+' response'+(n===1?'':'s')+'">'+n+'</span>';
3596
+ }
3597
+ }else if(t.apiCallCount>0){
3598
+ h+='<span class="achip" style="background:rgba(255,255,255,.04);color:var(--fg-1)">'+t.apiCallCount+'</span>';
3599
+ }
3600
+ var pass=t.assertionsPassed||0,fail=t.assertionsFailed||0;
3601
+ if(pass>0||fail>0){
3602
+ if(fail>0){
3603
+ h+='<span class="amark amark-bad" title="'+fail+' assertion'+(fail===1?'':'s')+' failed"><svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M18 6L6 18M6 6l12 12"/></svg></span>';
3604
+ }else{
3605
+ h+='<span class="amark amark-ok" title="'+pass+' assertion'+(pass===1?'':'s')+' passed"><svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M20 6L9 17l-5-5"/></svg></span>';
3606
+ }
3607
+ }
3608
+ return h?'<span class="arow">'+h+'</span>':'';
3609
+ }
3610
+
3611
+ /* \u2500\u2500 Detail: the assertions section \u2500\u2500 */
3612
+
3613
+ function findApiCall(apiCalls,callId){
3614
+ if(!apiCalls||!callId)return null;
3615
+ for(var i=0;i<apiCalls.length;i++){
3616
+ var c=apiCalls[i];
3617
+ if(c.id===callId||c.callId===callId)return c;
3618
+ }
3619
+ return null;
3620
+ }
3621
+
3622
+ function assertionExpression(a){
3623
+ if(a.expression)return a.expression;
3624
+ /* No expression was captured; say what the assertion checked instead of
3625
+ leaving the row blank. */
3626
+ return 'expect('+String(a.type||'value')+') \\u2192 '+fmtAssertValue(a.expected);
3627
+ }
3628
+
3629
+ function renderAssertionsSection(assertions,apiCalls){
3630
+ if(!assertions||assertions.length===0)return '';
3631
+ var pass=0,fail=0,i;
3632
+ for(i=0;i<assertions.length;i++){
3633
+ if(assertions[i].status==='failed')fail++;else pass++;
3634
+ }
3635
+
3636
+ var h='<div class="apisec"><div class="apisec-head">';
3637
+ h+='<span class="apisec-title">Assertions</span>';
3638
+ if(pass>0)h+='<span class="apisec-badge ab-ok">'+pass+' passed</span>';
3639
+ if(fail>0)h+='<span class="apisec-badge ab-bad">'+fail+' failed</span>';
3640
+ h+='</div><div class="asec">';
3641
+
3642
+ /* Failures first \u2014 the reason anyone opened this page. */
3643
+ var ordered=assertions.slice().sort(function(a,b){
3644
+ return (a.status==='failed'?0:1)-(b.status==='failed'?0:1);
3645
+ });
3646
+
3647
+ for(i=0;i<ordered.length;i++){
3648
+ var a=ordered[i],failed=a.status==='failed';
3649
+ h+='<div class="arow-item'+(failed?' is-failed':'')+'">';
3650
+ h+=failed
3651
+ ? '<svg style="flex-shrink:0" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#ef4444" stroke-width="2.5"><path d="M18 6L6 18M6 6l12 12"/></svg>'
3652
+ : '<svg style="flex-shrink:0" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#22c55e" stroke-width="2.5"><path d="M20 6L9 17l-5-5"/></svg>';
3653
+ h+='<span class="atype">'+esc(String(a.type||'custom'))+'</span>';
3654
+ h+='<span class="aexpr">'+esc(assertionExpression(a))+'</span>';
3655
+ if(a.location&&a.location.line)h+='<span class="aloc">:'+a.location.line+'</span>';
3656
+ h+='</div>';
3657
+
3658
+ if(failed){
3659
+ h+='<div class="adiff">';
3660
+ h+='<span class="adiff-i"><span class="adiff-k">expected</span><span class="adiff-v adiff-exp">'+esc(fmtAssertValue(a.expected))+'</span></span>';
3661
+ h+='<span class="adiff-i"><span class="adiff-k">received</span><span class="adiff-v adiff-act">'+esc(fmtAssertValue(a.actual))+'</span></span>';
3662
+ h+='</div>';
3663
+ var call=findApiCall(apiCalls,a.callId);
3664
+ if(call){
3665
+ var code=call.responseStatusCode!=null?call.responseStatusCode:(call.response&&call.response.status);
3666
+ h+='<div class="acall">on '+esc(String(call.method||''))+' '+esc(urlPath(String(call.url||'')))+(code?' \\u2192 '+code:'')+'</div>';
3667
+ }
3668
+ }
3669
+ }
3670
+ return h+'</div></div>';
3671
+ }
3672
+
3673
+ /* The same sentence, built from the flattened summary the index carries \u2014
3674
+ so the run overview reads identically in embedded and streaming mode. */
3675
+ function assertionSentence(fa){
3676
+ if(!fa)return '';
3677
+ return 'A <code>'+esc(String(fa.type||'custom'))+'</code> assertion expected <code>'+
3678
+ esc(String(fa.expected))+'</code> but received <code>'+esc(String(fa.actual))+'</code>.';
3679
+ }
3680
+
3681
+ /* The headline for an API failure: the assertion, not the matcher wrapper. */
3682
+ function apiFailureSentence(assertions){
3683
+ if(!assertions||assertions.length===0)return '';
3684
+ for(var i=0;i<assertions.length;i++){
3685
+ var a=assertions[i];
3686
+ if(a.status!=='failed')continue;
3687
+ return 'A <code>'+esc(String(a.type||'custom'))+'</code> assertion expected <code>'+
3688
+ esc(fmtAssertValue(a.expected))+'</code> but received <code>'+
3689
+ esc(fmtAssertValue(a.actual))+'</code>.';
3690
+ }
3691
+ return '';
3692
+ }
3693
+ `;var rt=`
3694
+ .vis-header{font-size:11px;font-weight:700;color:var(--fg-1);text-transform:uppercase;letter-spacing:.06em;margin-bottom:12px;display:flex;align-items:center;gap:6px}
3695
+ .vis-header svg{opacity:.6}
3696
+ .vis-count{font-weight:400;color:var(--fg-2);font-size:10px}
3697
+ .vis-count-fail{color:#ef4444}
3698
+ .vis-list{display:flex;flex-direction:column;gap:20px}
3699
+ .vis-card{border:1px solid var(--bd);border-radius:10px;padding:14px;background:var(--bg-2);min-width:0}
3700
+ .vis-head{display:flex;align-items:center;gap:10px;flex-wrap:wrap;margin-bottom:10px}
3701
+ .vis-name{font-size:12px;font-weight:600;color:var(--fg-1);word-break:break-all}
3702
+ .vis-badge{font-size:10px;font-weight:600;padding:2px 8px;border-radius:999px;text-transform:uppercase;letter-spacing:.04em;white-space:nowrap}
3703
+ .vis-badge.vb-passed{background:rgba(3,183,156,0.14);color:#2dd4a8}
3704
+ .vis-badge.vb-failed{background:rgba(239,68,68,0.14);color:#ef4444}
3705
+ .vis-badge.vb-new{background:rgba(59,130,246,0.14);color:#60a5fa}
3706
+ .vis-badge.vb-updated{background:rgba(234,179,8,0.14);color:#eab308}
3707
+ .vis-badge.vb-dom{background:rgba(232,121,249,0.14);color:#e879f9}
3708
+ .vis-metrics{font-size:11px;color:var(--fg-2);margin-left:auto;white-space:nowrap;font-variant-numeric:tabular-nums}
3709
+ .vis-note{font-size:11px;color:var(--fg-2);font-style:italic;padding:6px 0}
3710
+
3711
+ /* Side-by-side: wraps to a single column when the drawer is narrow. */
3712
+ .vis-pair{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:12px}
3713
+ .vis-pane-label{font-size:10px;font-weight:600;color:var(--fg-2);text-transform:uppercase;letter-spacing:.04em;margin-bottom:6px}
3714
+
3715
+ /* Layered comparison surface shared by the slider and onion-skin modes. */
3716
+ .vis-stack{position:relative;line-height:0;border:1px solid var(--bd);border-radius:8px;overflow:hidden;background:var(--bg)}
3717
+ .vis-stack img{display:block;width:100%;height:auto}
3718
+ .vis-stack .vis-top{position:absolute;top:0;left:0;width:100%;height:100%;object-fit:contain}
3719
+ .vis-handle{position:absolute;top:0;bottom:0;width:2px;background:#03b79c;pointer-events:none;box-shadow:0 0 0 1px rgba(0,0,0,0.35)}
3720
+ .vis-range{width:100%;margin-top:10px;accent-color:#03b79c;cursor:ew-resize}
3721
+ .vis-range-row{display:flex;align-items:center;gap:10px;font-size:10px;color:var(--fg-2);text-transform:uppercase;letter-spacing:.04em}
3722
+ .vis-range-row .vis-range{margin-top:0}
3723
+
3724
+ /* A checkerboard behind the diff so fully transparent regions stay legible. */
3725
+ .vis-diff-wrap{background-image:linear-gradient(45deg,var(--bd-xs) 25%,transparent 25%),linear-gradient(-45deg,var(--bd-xs) 25%,transparent 25%),linear-gradient(45deg,transparent 75%,var(--bd-xs) 75%),linear-gradient(-45deg,transparent 75%,var(--bd-xs) 75%);background-size:16px 16px;background-position:0 0,0 8px,8px -8px,-8px 0;border-radius:8px}
3726
+ `,at=`
3727
+ .dom-figure{position:relative;line-height:0;border:1px solid var(--bd);border-radius:8px;overflow:hidden;margin-bottom:12px}
3728
+ .dom-img{display:block;width:100%;height:auto}
3729
+ .dom-ov{position:absolute;top:0;left:0;right:0;bottom:0;pointer-events:none}
3730
+ .dom-box{position:absolute;border:2px solid #03b79c;border-radius:2px;opacity:.55;transition:opacity .12s,box-shadow .12s}
3731
+ .dom-box.hi{opacity:1;box-shadow:0 0 0 2px rgba(0,0,0,.45),0 0 12px rgba(255,255,255,.25)}
3732
+
3733
+ .dom-summary{font-size:11px;color:var(--fg-2);margin-bottom:10px}
3734
+ .dom-summary strong{color:var(--fg-1);font-variant-numeric:tabular-nums}
3735
+ .dom-truncated{display:inline-block;margin-left:6px;padding:1px 6px;border-radius:999px;background:rgba(234,179,8,.14);color:#eab308;font-size:10px;font-weight:600}
3736
+
3737
+ .dom-list{display:flex;flex-direction:column;gap:2px}
3738
+ .dom-group-label{font-size:10px;font-weight:600;color:var(--fg-2);text-transform:uppercase;letter-spacing:.04em;margin:12px 0 4px}
3739
+ .dom-row{display:flex;align-items:center;gap:8px;padding:6px 8px;border-radius:6px;cursor:default;flex-wrap:wrap}
3740
+ .dom-row:hover,.dom-row.hi{background:var(--bd-xs)}
3741
+ .dom-swatch{width:8px;height:8px;border-radius:2px;flex:none}
3742
+ .dom-label{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:11px;color:var(--fg-1);word-break:break-all}
3743
+ .dom-kind{font-size:9px;font-weight:600;padding:1px 6px;border-radius:999px;text-transform:uppercase;letter-spacing:.04em;background:var(--bd-xs);color:var(--fg-2)}
3744
+ .dom-kind.dk-restyled{background:rgba(232,121,249,.14);color:#e879f9}
3745
+ .dom-kind.dk-text{background:rgba(96,165,250,.14);color:#60a5fa}
3746
+ .dom-kind.dk-attrs{background:rgba(251,146,60,.14);color:#fb923c}
3747
+ .dom-kind.dk-added{background:rgba(3,183,156,.14);color:#2dd4a8}
3748
+ .dom-kind.dk-removed{background:rgba(239,68,68,.14);color:#ef4444}
3749
+ .dom-area{margin-left:auto;font-size:10px;color:var(--fg-2);font-variant-numeric:tabular-nums}
3750
+
3751
+ .dom-detail{padding:0 8px 8px 24px;display:flex;flex-direction:column;gap:3px}
3752
+ .dom-prop{display:flex;align-items:baseline;gap:8px;font-size:10px;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;flex-wrap:wrap}
3753
+ .dom-prop-name{color:var(--fg-2);min-width:130px}
3754
+ .dom-prop-attr .dom-prop-name{color:#fb923c;font-weight:600}
3755
+ .dom-from{color:#ef4444;word-break:break-all}
3756
+ .dom-arrow{color:var(--fg-2)}
3757
+ .dom-to{color:#2dd4a8;word-break:break-all}
3758
+ `;var nt=`
3759
+ /* \u2500\u2500 Wider shell \u2500\u2500
3760
+ 960px was sized for a list. The row now carries evidence and timing, and
3761
+ the detail panel shows two screenshots side by side; both were cramped. */
3762
+ .wrap{max-width:1400px;padding:28px 40px 64px}
3763
+
3764
+ /* \u2500\u2500 Verdict band (replaces the six equal stat chips) \u2500\u2500 */
3765
+ .verdict{display:flex;gap:16px;align-items:stretch;margin-bottom:22px}
3766
+ .verdict-main{flex:1;min-width:0;background:var(--bg-1);border:1px solid var(--bd);border-radius:12px;padding:18px 22px;display:flex;flex-direction:column;gap:14px}
3767
+ .verdict-main.v-failed{border-color:rgba(239,68,68,0.22)}
3768
+ .verdict-main.v-passed{border-color:rgba(34,197,94,0.2)}
3769
+ .verdict-head{display:flex;align-items:baseline;gap:12px;flex-wrap:wrap}
3770
+ .verdict-title{font-size:21px;font-weight:700;letter-spacing:-0.01em;color:var(--fg)}
3771
+ .verdict-where{font-size:12px;color:var(--fg-1)}
3772
+ .verdict-where code{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;color:var(--fg)}
3773
+ .verdict-dur{margin-left:auto;display:flex;align-items:center;gap:6px;font-size:12px;color:var(--fg-1);white-space:nowrap}
3774
+
3775
+ /* Proportion bar: one glance at the shape of the run. */
3776
+ .verdict-bar{display:flex;height:8px;gap:2px}
3777
+ .verdict-bar span{border-radius:999px;min-width:3px}
3778
+ .vb-passed{background:#22c55e}.vb-failed{background:#ef4444}
3779
+ .vb-flaky{background:#f59e0b}.vb-skipped{background:#6b7280}.vb-timedout{background:#f97316}
3780
+ .verdict-bar-empty{flex:1;background:var(--bd-m);border-radius:999px}
3781
+
3782
+ .verdict-counts{display:flex;align-items:center;gap:20px;flex-wrap:wrap}
3783
+ .vc{display:flex;align-items:center;gap:7px}
3784
+ .vc-dot{width:9px;height:9px;border-radius:50%;flex-shrink:0}
3785
+ .vc-n{font-size:14px;font-weight:700;color:var(--fg);font-variant-numeric:tabular-nums}
3786
+ .vc-l{font-size:12px;color:var(--fg-1)}
3787
+ /* Statuses with nothing in them stop taking a sixth of the width. */
3788
+ .verdict-zeros{margin-left:auto;font-size:11px;color:var(--fg-2)}
3789
+
3790
+ .verdict-side{width:268px;flex-shrink:0;background:var(--bg-1);border:1px solid var(--bd);border-radius:12px;padding:18px 20px;display:flex;flex-direction:column;gap:12px}
3791
+ .verdict-side-label{font-size:10px;font-weight:600;color:var(--fg-2);letter-spacing:.06em;text-transform:uppercase}
3792
+ .verdict-side-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px 10px}
3793
+ .vs-n{font-size:17px;font-weight:700;color:var(--fg);font-variant-numeric:tabular-nums}
3794
+ .vs-n.vs-dom{color:#e879f9}
3795
+ .vs-l{font-size:10px;color:var(--fg-1)}
3796
+
3797
+ /* \u2500\u2500 Needs attention \u2500\u2500 */
3798
+ .needs{display:flex;flex-direction:column;gap:10px;margin-bottom:22px}
3799
+ .needs-head{display:flex;align-items:center;gap:8px;padding:0 2px}
3800
+ .needs-title{font-size:11px;font-weight:700;color:var(--fg);letter-spacing:.06em;text-transform:uppercase}
3801
+ .needs-count{font-size:10px;font-weight:700;color:#ef4444;background:rgba(239,68,68,0.12);padding:1px 7px;border-radius:4px}
3802
+ .needs-list{display:flex;flex-direction:column;gap:8px}
3803
+ .needs-card{background:var(--bg-1);border:1px solid rgba(239,68,68,0.2);border-left:3px solid #ef4444;border-radius:10px;padding:14px 18px;display:flex;gap:16px;align-items:flex-start;cursor:pointer;transition:border-color .15s,background .1s}
3804
+ .needs-card:hover{background:var(--hvr);border-color:rgba(239,68,68,0.4)}
3805
+ .needs-card.nc-flaky{border-color:rgba(245,158,11,0.2);border-left-color:#f59e0b}
3806
+ .needs-card.nc-flaky:hover{border-color:rgba(245,158,11,0.4)}
3807
+ .needs-body{flex:1;min-width:0;display:flex;flex-direction:column;gap:8px}
3808
+ .needs-row1{display:flex;align-items:center;gap:10px;flex-wrap:wrap}
3809
+ .needs-name{font-size:14px;font-weight:600;color:var(--fg)}
3810
+ .needs-file{font-size:11px;color:var(--fg-2);font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}
3811
+ .needs-cause{font-size:12px;color:var(--fg-1);font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;line-height:1.6;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
3812
+ .needs-cause em{font-style:normal;color:var(--fg-err)}
3813
+ .needs-chips{display:flex;gap:6px;flex-wrap:wrap;align-items:center}
3814
+ .needs-thumb{width:148px;flex-shrink:0;display:flex;flex-direction:column;gap:5px}
3815
+ .needs-thumb img{width:100%;height:92px;object-fit:cover;object-position:top center;border-radius:7px;border:1px solid rgba(239,68,68,0.35);background:var(--bg)}
3816
+ .needs-thumb-cap{font-size:9px;color:var(--fg-2);text-align:center}
3817
+ .needs-right{display:flex;flex-direction:column;align-items:flex-end;gap:8px;flex-shrink:0}
3818
+ .needs-dur{font-size:13px;font-weight:700;color:var(--fg-1);font-variant-numeric:tabular-nums}
3819
+ .needs-open{display:flex;align-items:center;gap:4px;font-size:11px;font-weight:600;color:#2dd4a8}
3820
+
3821
+ /* Pill chips used in the needs cards. */
3822
+ .nchip{display:flex;align-items:center;gap:4px;font-size:10px;font-weight:500;color:var(--fg-1);background:var(--bg-2);border:1px solid var(--bd);padding:2px 8px;border-radius:999px}
3823
+ .nchip-visual{color:#2dd4a8;background:rgba(3,183,156,0.1);border-color:rgba(3,183,156,0.22);font-weight:600}
3824
+ .nchip-dom{color:#e879f9;background:rgba(232,121,249,0.1);border-color:rgba(232,121,249,0.2);font-weight:600}
3825
+
3826
+ /* \u2500\u2500 Enriched test row \u2500\u2500
3827
+ Same height as before. Two regions added: what was captured, and how long
3828
+ it took relative to the slowest test in the run. */
3829
+ .trow-evidence{display:flex;gap:4px;align-items:center;flex-shrink:0}
3830
+ .tev{width:18px;height:18px;border-radius:4px;background:var(--bg-2);display:flex;align-items:center;justify-content:center;color:var(--fg-1);flex-shrink:0}
3831
+ .tev-visual{background:rgba(3,183,156,0.12);color:#2dd4a8}
3832
+ .tev-dom{background:rgba(232,121,249,0.12);color:#e879f9}
3833
+ .tev-err{background:rgba(239,68,68,0.12);color:#ef4444}
3834
+ .trow-time{width:120px;flex-shrink:0;display:flex;align-items:center;gap:8px}
3835
+ .trow-bar{flex:1;height:4px;border-radius:999px;background:var(--bd-m);overflow:hidden}
3836
+ .trow-bar span{display:block;height:100%;border-radius:999px}
3837
+ .tb-passed{background:#22c55e}.tb-failed{background:#ef4444}
3838
+ .tb-flaky{background:#f59e0b}.tb-skipped{background:#6b7280}.tb-timedout{background:#f97316}
3839
+ .test-row-dur{font-variant-numeric:tabular-nums}
3840
+ /* The title and its badges sit on one line now that the row has columns. */
3841
+ .test-row-info.tri-inline{flex-direction:row;align-items:center;gap:10px}
3842
+ .test-row-info.tri-inline .test-row-badges{flex-shrink:0}
3843
+
3844
+ /* \u2500\u2500 Detail panel \u2500\u2500
3845
+ Was 50vw, which left the comparison about 330px a side and a dead half-
3846
+ screen of grid behind it. */
3847
+ .drawer{width:min(1240px,94vw);max-width:94vw}
3848
+ .drawer-layout{display:flex;gap:28px;align-items:flex-start}
3849
+ .drawer-rail{width:206px;flex-shrink:0;position:sticky;top:0;display:flex;flex-direction:column;gap:18px}
3850
+ .drawer-main{flex:1;min-width:0;display:flex;flex-direction:column;gap:24px}
3851
+ .rail-group{display:flex;flex-direction:column;gap:2px}
3852
+ .rail-label{font-size:10px;font-weight:600;color:var(--fg-2);letter-spacing:.06em;text-transform:uppercase;padding:0 10px 7px}
3853
+ .rail-item{display:flex;align-items:center;gap:9px;padding:8px 10px;border-radius:7px;color:var(--fg-1);cursor:pointer;transition:background .12s,color .12s;border:none;background:transparent;font-family:inherit;text-align:left;width:100%}
3854
+ .rail-item:hover{background:var(--bd-xs);color:var(--fg)}
3855
+ .rail-item.active{background:rgba(3,183,156,0.12);color:#2dd4a8}
3856
+ .rail-item.active .rail-name{font-weight:600}
3857
+ .rail-item.rail-fail{color:var(--fg-err)}
3858
+ .rail-item.rail-fail.active{background:rgba(239,68,68,0.1);color:var(--fg-err)}
3859
+ .rail-name{font-size:12.5px;font-weight:500;flex:1;min-width:0}
3860
+ .rail-n{font-size:10px;font-weight:700;background:var(--bd-xs);padding:1px 6px;border-radius:4px;font-variant-numeric:tabular-nums}
3861
+ .rail-item.active .rail-n{background:rgba(3,183,156,0.18)}
3862
+ .rail-n.rail-n-dom{color:#e879f9;background:rgba(232,121,249,0.12)}
3863
+ .rail-meta{border-top:1px solid var(--bd);padding-top:16px;display:flex;flex-direction:column;gap:9px}
3864
+ .rail-meta-rows{padding:0 10px;display:flex;flex-direction:column;gap:7px;font-size:11px}
3865
+ .rail-meta-row{display:flex;gap:8px}
3866
+ .rail-meta-k{color:var(--fg-2);width:44px;flex-shrink:0}
3867
+ .rail-meta-v{color:var(--fg-1);min-width:0;overflow:hidden;text-overflow:ellipsis}
3868
+ .rail-meta-v.mono{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}
3869
+
3870
+ /* Scroll-margin so a rail jump does not tuck the heading under the bar. */
3871
+ .drawer-section{scroll-margin-top:12px}
3872
+
3873
+ @media (max-width:1100px){
3874
+ .drawer{width:96vw;max-width:96vw}
3875
+ .drawer-layout{flex-direction:column;gap:18px}
3876
+ .drawer-rail{width:100%;position:static;flex-direction:row;flex-wrap:wrap;gap:6px}
3877
+ .rail-group{flex-direction:row;flex-wrap:wrap;gap:4px}
3878
+ .rail-label,.rail-meta{display:none}
3879
+ .rail-item{width:auto}
3880
+ .verdict{flex-direction:column}
3881
+ .verdict-side{width:100%}
3882
+ }
3883
+ `,st=`
3884
+ /* The overlay is gone. Anything still carrying .drawer must not float. */
3885
+ .drawer,.drawer-backdrop{display:none}
3886
+
3887
+ .detail-page{display:flex;flex-direction:column;gap:20px}
3888
+ #view-detail[hidden],#view-overview[hidden]{display:none}
3889
+
3890
+ .detail-nav{display:flex;align-items:center;gap:14px;flex-wrap:wrap;padding-bottom:14px;border-bottom:1px solid var(--bd)}
3891
+ .detail-back{display:inline-flex;align-items:center;gap:6px;font-size:12px;font-weight:600;color:var(--fg-1);background:transparent;border:1px solid var(--bd-m);border-radius:8px;padding:6px 12px 6px 9px;cursor:pointer;font-family:inherit;transition:background .15s,color .15s,border-color .15s}
3892
+ .detail-back:hover{background:var(--hvr);color:var(--fg);border-color:var(--bd-l)}
3893
+ .detail-crumb{font-size:11px;color:var(--fg-2);font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}
3894
+
3895
+ .detail-steps{margin-left:auto;display:inline-flex;align-items:center;border:1px solid var(--bd-m);border-radius:8px;overflow:hidden}
3896
+ .detail-step{display:flex;align-items:center;justify-content:center;width:32px;height:30px;background:transparent;border:none;color:var(--fg-1);cursor:pointer;font-family:inherit;transition:background .15s,color .15s}
3897
+ .detail-step:hover:not(.is-off){background:var(--hvr);color:var(--fg)}
3898
+ .detail-step.is-off{color:var(--fg-2);cursor:default}
3899
+ .detail-pos{font-size:11px;font-weight:600;color:var(--fg-1);padding:0 10px;border-left:1px solid var(--bd-m);border-right:1px solid var(--bd-m);line-height:30px;font-variant-numeric:tabular-nums}
3900
+
3901
+ /* The page scrolls now, so the old panel body must not scroll inside it. */
3902
+ #view-detail .drawer-body{flex:none;overflow:visible;padding:0}
3903
+ .detail-page .detail-rail,.detail-page .drawer-rail{top:16px}
3904
+ `;var it=`
3905
+ /* \u2500\u2500 Run-level type band \u2500\u2500 */
3906
+ .tband{display:grid;gap:16px;margin-bottom:20px}
3907
+ .tband-3{grid-template-columns:1.1fr 1.1fr .9fr}
3908
+ .tband-2{grid-template-columns:1fr 1fr}
3909
+ .tcard{background:var(--bg-1);border:1px solid rgba(255,255,255,.06);border-radius:12px;padding:16px 20px;display:flex;flex-direction:column;gap:12px;min-width:0}
3910
+ .tcard-h{font-size:10px;font-weight:600;color:var(--fg-2);letter-spacing:.06em;text-transform:uppercase;display:flex;align-items:center;gap:8px}
3911
+ .tcard-h .tcard-sub{margin-left:auto;font-weight:500;letter-spacing:0;text-transform:none;font-size:10px}
3912
+
3913
+ /* Method distribution */
3914
+ .mrow{display:flex;align-items:center;gap:10px}
3915
+ .mrow-k{width:54px;font-size:11px;font-weight:700;font-family:var(--mono,ui-monospace,SFMono-Regular,Menlo,Consolas,monospace)}
3916
+ .mrow-track{display:block;flex:1;height:6px;border-radius:999px;background:rgba(255,255,255,.06);overflow:hidden}
3917
+ .mrow-fill{display:block;height:100%;border-radius:999px;min-width:3px}
3918
+ .mrow-n{font-size:11px;color:var(--fg-1);width:34px;text-align:right}
3919
+
3920
+ /* Status distribution */
3921
+ .sbar{display:flex;height:8px;gap:2px}
3922
+ .sbar>span{border-radius:999px}
3923
+ .sgrid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px 12px}
3924
+ .scell{display:flex;align-items:center;gap:8px}
3925
+ .spill{font-size:10px;font-weight:700;padding:1px 7px;border-radius:4px}
3926
+ .scell-n{font-size:12px;font-weight:700}
3927
+ .scell-n.is-zero{font-weight:400;color:var(--fg-2)}
3928
+ .sp-2xx{background:rgba(34,197,94,.12);color:#22c55e}
3929
+ .sp-3xx{background:rgba(96,165,250,.12);color:#60a5fa}
3930
+ .sp-4xx{background:rgba(251,146,60,.12);color:#fb923c}
3931
+ .sp-5xx{background:rgba(239,68,68,.12);color:#ef4444}
3932
+ .sp-error{background:rgba(239,68,68,.12);color:#ef4444}
3933
+
3934
+ /* Big-number card (assertions, timings) */
3935
+ .tbig{display:flex;align-items:baseline;gap:8px}
3936
+ .tbig-n{font-size:28px;font-weight:800;line-height:1}
3937
+ .tbig-l{font-size:11px;color:var(--fg-1)}
3938
+ .tsplit{display:flex;gap:14px;flex-wrap:wrap}
3939
+ .tsplit-i{display:flex;align-items:center;gap:6px;font-size:13px;font-weight:700}
3940
+ .tsplit-ok{color:#22c55e}
3941
+ .tsplit-bad{color:#ef4444}
3942
+ .tpair{display:flex;gap:18px;flex-wrap:wrap}
3943
+ .tpair-i{display:flex;flex-direction:column;gap:2px}
3944
+ .tpair-n{font-size:17px;font-weight:700}
3945
+ .tpair-n small{font-size:11px;font-weight:500;color:var(--fg-1)}
3946
+ .tpair-l{font-size:10px;color:var(--fg-1)}
3947
+
3948
+ /* \u2500\u2500 Row evidence in the grid \u2500\u2500 */
3949
+ .arow{display:flex;gap:5px;align-items:center;flex-shrink:0}
3950
+ .achip{font-size:10px;font-weight:700;padding:1px 6px;border-radius:4px;font-family:var(--mono,ui-monospace,SFMono-Regular,Menlo,Consolas,monospace)}
3951
+ .amark{width:18px;height:18px;border-radius:4px;display:flex;align-items:center;justify-content:center;flex-shrink:0}
3952
+ .amark-ok{background:rgba(34,197,94,.12);color:#22c55e}
3953
+ .amark-bad{background:rgba(239,68,68,.12);color:#ef4444}
3954
+
3955
+ /* \u2500\u2500 Assertions section (detail page) \u2500\u2500 */
3956
+ .apisec{border:1px solid var(--bd);border-radius:11px;background:var(--bg-1);overflow:hidden}
3957
+ .apisec-head{display:flex;align-items:center;gap:10px;padding:12px 20px;border-bottom:1px solid rgba(255,255,255,.06)}
3958
+ .apisec-title{font-size:12px;font-weight:700;letter-spacing:.04em;text-transform:uppercase}
3959
+ .apisec-badge{font-size:10px;font-weight:700;padding:1px 7px;border-radius:4px}
3960
+ .apisec-badge.ab-ok{background:rgba(34,197,94,.12);color:#22c55e}
3961
+ .apisec-badge.ab-bad{background:rgba(239,68,68,.12);color:#ef4444}
3962
+ .asec{display:flex;flex-direction:column}
3963
+ .arow-item{display:flex;align-items:center;gap:12px;padding:10px 20px;border-bottom:1px solid rgba(255,255,255,.04)}
3964
+ .arow-item:last-child{border-bottom:none}
3965
+ .arow-item.is-failed{background:rgba(239,68,68,.04)}
3966
+ .atype{font-size:9px;font-weight:700;padding:1px 6px;border-radius:999px;letter-spacing:.04em;text-transform:uppercase;flex-shrink:0;background:rgba(255,255,255,.04);color:var(--fg-1)}
3967
+ .arow-item.is-failed .atype{background:rgba(239,68,68,.14);color:#ef4444}
3968
+ .aexpr{font-size:11.5px;flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-family:var(--mono,ui-monospace,SFMono-Regular,Menlo,Consolas,monospace)}
3969
+ .aloc{font-size:10px;color:var(--fg-2);flex-shrink:0;font-family:var(--mono,ui-monospace,SFMono-Regular,Menlo,Consolas,monospace)}
3970
+ .adiff{display:flex;gap:22px;flex-wrap:wrap;padding:0 20px 12px 52px}
3971
+ .adiff-i{display:flex;gap:8px;align-items:baseline}
3972
+ .adiff-k{font-size:10px;color:var(--fg-2);width:58px}
3973
+ .adiff-v{font-size:11px;font-family:var(--mono,ui-monospace,SFMono-Regular,Menlo,Consolas,monospace);word-break:break-all}
3974
+ .adiff-exp{color:#2dd4a8}
3975
+ .adiff-act{color:#ef4444}
3976
+ .acall{font-size:10px;color:var(--fg-2);padding:0 20px 10px 52px;font-family:var(--mono,ui-monospace,SFMono-Regular,Menlo,Consolas,monospace)}
3977
+
3978
+ /* \u2500\u2500 API call row: the two columns that used to render empty \u2500\u2500 */
3979
+ .ndt-assert{display:inline-flex;align-items:center;justify-content:center;width:100%;font-size:10px;font-weight:700}
3980
+ .ndt-assert-ok{color:#22c55e}
3981
+ .ndt-assert-bad{color:#ef4444}
3982
+ .ndt-assert-none{color:var(--fg-2);font-weight:400}
3983
+ .ndt-err-note{color:#ef4444;font-size:11px}
3984
+
3985
+ /* Trace pane \u2014 a link out, plus how to open the thing */
3986
+ .trace-open{display:flex;flex-direction:column;gap:10px;padding:18px;background:var(--bg-2);border:1px solid var(--bd);border-radius:10px}
3987
+ .trace-link{display:inline-flex;align-items:center;gap:8px;align-self:flex-start;font-size:12.5px;font-weight:600;color:#2dd4a8;text-decoration:none;border:1px solid rgba(3,183,156,.3);background:rgba(3,183,156,.08);padding:8px 14px;border-radius:8px}
3988
+ .trace-link:hover{background:rgba(3,183,156,.14)}
3989
+ .trace-hint{font-size:11.5px;color:var(--fg-1);line-height:1.6}
3990
+ .trace-hint code{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:11px;background:var(--bg-code);padding:1px 5px;border-radius:4px;color:var(--fg)}
3991
+
3992
+ /* Cookie table */
3993
+ .ndt-headers-label{font-size:9px;font-weight:700;color:var(--fg-2);letter-spacing:.06em;text-transform:uppercase;padding:10px 14px 0}
3994
+ .ck-tbl{display:grid;grid-template-columns:auto 1fr;gap:6px 16px;padding:12px 14px;font-size:11.5px}
3995
+ .ck-k{color:var(--fg-1);font-family:var(--mono,ui-monospace,SFMono-Regular,Menlo,Consolas,monospace)}
3996
+ .ck-v{color:var(--fg);word-break:break-all;font-family:var(--mono,ui-monospace,SFMono-Regular,Menlo,Consolas,monospace)}
3997
+ .ck-attr{color:var(--fg-2);font-size:10px}
3998
+
3999
+ @media (max-width:1100px){
4000
+ .tband-3,.tband-2{grid-template-columns:1fr}
4001
+ }
4002
+ `;var En=`
2318
4003
  (function(){
2319
4004
  var data=JSON.parse(document.getElementById('report-data').textContent);
2320
- ${Le}
2321
- ${De}
2322
- ${Be}
2323
- ${Pe}
2324
- ${Ne}
2325
- ${Oe}
2326
- ${Fe}
4005
+ ${Ve}
4006
+ ${Ke}
4007
+ ${Je}
4008
+ ${je}
4009
+ ${Ge}
4010
+ ${Ye}
4011
+ ${Ze}
4012
+ ${Xe}
4013
+ ${We}
4014
+ ${Qe}
4015
+ ${tt}
4016
+ ${et}
2327
4017
  applyTheme();
2328
4018
  renderRunMeta();
2329
4019
  renderSummary();
4020
+ renderTypeBand();
2330
4021
  initFilters();
2331
4022
  renderFilterBar();
2332
4023
  renderTestGrid();
4024
+ renderNeedsAttention();
4025
+ routeFromHash();
2333
4026
  var _lo=document.getElementById('loading-overlay');if(_lo)_lo.remove();
2334
- })();`;function Gt(t,e,r){let n=t.replace(/<\//g,"<\\/"),a=r?r.replace(/<\//g,"<\\/"):null;return `<!-- TestRelic AI Analytics Report \u2014 self-contained, no external dependencies -->
4027
+ })();`;function vr(e,t,r){let a=e.replace(/<\//g,"<\\/"),n=r?r.replace(/<\//g,"<\\/"):null;return `<!-- TestRelic AI Analytics Report \u2014 self-contained, no external dependencies -->
2335
4028
  <!DOCTYPE html>
2336
4029
  <html lang="en" data-theme="">
2337
4030
  <head>
2338
4031
  <meta charset="UTF-8">
2339
4032
  <meta name="viewport" content="width=device-width,initial-scale=1">
2340
- <meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'unsafe-inline'; script-src 'unsafe-inline'; img-src data: blob: 'self'; media-src blob: 'self'; connect-src 'self' http://127.0.0.1:*;">
4033
+ <meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'unsafe-inline'; script-src 'unsafe-inline'; img-src data: blob: file: 'self'; media-src blob: data: file: 'self'; connect-src 'self' http://127.0.0.1:*;">
2341
4034
  <title>TestRelic AI Analytics Report</title>
2342
4035
  ${""}
2343
- <link rel="icon" type="image/svg+xml" href="data:image/svg+xml;base64,${Buffer.from(Me).toString("base64")}">
2344
- <style>${Ie}</style>
4036
+ <link rel="icon" type="image/svg+xml" href="data:image/svg+xml;base64,${Buffer.from(qe).toString("base64")}">
4037
+ <style>${He}${rt}${at}${nt}${st}${it}</style>
2345
4038
  <script>(function(){var p=localStorage.getItem('tr-theme')||'system';var t=p==='system'?(window.matchMedia('(prefers-color-scheme:light)').matches?'light':'dark'):p;document.documentElement.setAttribute('data-theme',t);})()</script>
2346
4039
  </head>
2347
4040
  <body>
2348
4041
  <div class="wrap">
2349
4042
  <div class="top-bar">
2350
4043
  <div class="brand">
2351
- ${Ee}
4044
+ ${$e}
2352
4045
  <div class="brand-text">
2353
4046
  <span class="brand-title">TestRelic AI</span>
2354
4047
  <span class="brand-sub">Analytics Report</span>
@@ -2362,19 +4055,19 @@ ${""}
2362
4055
  </div>
2363
4056
  <a class="cta-btn" href="https://testrelic.ai" target="_blank" rel="noopener noreferrer"><svg class="cta-icon" viewBox="0 0 16 16" fill="none"><path d="M8 1l1.545 4.955L14.5 7.5l-4.955 1.545L8 14l-1.545-4.955L1.5 7.5l4.955-1.545L8 1z" fill="currentColor"/></svg>Advanced Insights with AI<span class="cta-sep">\u2013</span><strong>Get Started</strong><svg class="cta-arrow" viewBox="0 0 12 12" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M2.5 6h7M6.5 3l3 3-3 3"/></svg></a>
2364
4057
  </div>
2365
- <div id="summary-strip" class="summary-strip"></div>
2366
- <div id="filter-bar" class="filter-bar"></div>
2367
- <div id="test-grid"></div>
4058
+ <div id="view-overview">
4059
+ <div id="summary-strip"></div>
4060
+ <div id="type-band"></div>
4061
+ <div id="needs-attention"></div>
4062
+ <div id="filter-bar" class="filter-bar"></div>
4063
+ <div id="test-grid"></div>
4064
+ </div>
4065
+ <div id="view-detail" class="detail-page" hidden>
4066
+ <div class="detail-nav" id="detail-nav"></div>
4067
+ <div class="drawer-body" id="drawer-body"></div>
4068
+ </div>
2368
4069
  </div>
2369
4070
  <div class="loading-overlay" id="loading-overlay"><div class="loading-spinner"></div><div class="loading-text">Loading report...</div></div>
2370
- <div class="drawer-backdrop" id="drawer-backdrop"></div>
2371
- <aside class="drawer" id="drawer">
2372
- <div class="drawer-bar">
2373
- <span class="drawer-bar-title">Test Details</span>
2374
- <button class="drawer-close" id="drawer-close">&times;</button>
2375
- </div>
2376
- <div class="drawer-body" id="drawer-body"></div>
2377
- </aside>
2378
4071
  <div class="filter-drawer-backdrop" id="filter-drawer-backdrop"></div>
2379
4072
  <aside class="filter-drawer" id="filter-drawer">
2380
4073
  <div class="filter-drawer-bar">
@@ -2383,15 +4076,15 @@ ${""}
2383
4076
  </div>
2384
4077
  <div class="filter-drawer-body" id="filter-drawer-body"></div>
2385
4078
  </aside>
2386
- <script id="report-data" type="application/json">${n}</script>
2387
- ${a?`<script id="artifact-manifest-data" type="application/json">${a}</script>`:""}
2388
- <script>${Un}</script>
4079
+ <script id="report-data" type="application/json">${a}</script>
4080
+ ${n?`<script id="artifact-manifest-data" type="application/json">${n}</script>`:""}
4081
+ <script>${En}</script>
2389
4082
  </body>
2390
- </html>`}function Ue(t,e,r,n){let a=t.replace(/<\//g,"<\\/"),s=e.replace(/<\//g,"<\\/"),l=n?n.replace(/<\//g,"<\\/"):null,i={};try{i=JSON.parse(t);}catch{}let d=`{
4083
+ </html>`}function ot(e,t,r,a){let n=e.replace(/<\//g,"<\\/"),s=t.replace(/<\//g,"<\\/"),l=a?a.replace(/<\//g,"<\\/"):null,i={};try{i=JSON.parse(e);}catch{}let d=`{
2391
4084
  "schemaVersion":"2.0",
2392
4085
  "reportMode":"streaming",
2393
4086
  "serverPort":${r??"null"},
2394
- "summary":${a},
4087
+ "summary":${n},
2395
4088
  "index":${s},
2396
4089
  "timeline":[],
2397
4090
  "testRunId":${JSON.stringify(String(i.testRunId??""))},
@@ -2471,20 +4164,30 @@ ${a?`<script id="artifact-manifest-data" type="application/json">${a}</script>`:
2471
4164
  window.__trServerPort=_serverPort;
2472
4165
  window.__trIsStreaming=true;
2473
4166
 
2474
- ${Le}
2475
- ${De}
2476
- ${Be}
2477
- ${Pe}
2478
- ${Ne}
2479
- ${Oe}
2480
- ${Fe}
4167
+ ${Ve}
4168
+ ${Ke}
4169
+ ${Je}
4170
+ ${je}
4171
+ ${Ge}
4172
+ ${Ye}
4173
+ ${Ze}
4174
+ ${Xe}
4175
+ ${We}
4176
+ ${Qe}
4177
+ ${tt}
4178
+ ${et}
2481
4179
  applyTheme();
2482
4180
  renderRunMeta();
2483
4181
  renderSummary();
4182
+ renderTypeBand();
2484
4183
  initFilters();
2485
4184
  renderFilterBar();
2486
4185
  renderTestGrid();
2487
- checkServer(function(ok){});
4186
+ renderNeedsAttention();
4187
+ /* Route only once the health check has answered. Routing first meant a
4188
+ pasted link straight to a test always rendered "Server unavailable" \u2014
4189
+ the fetch ran while _serverAvailable was still false. */
4190
+ checkServer(function(ok){routeFromHash();});
2488
4191
  var _lo=document.getElementById('loading-overlay');if(_lo)_lo.remove();
2489
4192
  })();`;return `<!-- TestRelic AI Analytics Report \u2014 streaming mode -->
2490
4193
  <!DOCTYPE html>
@@ -2492,19 +4195,19 @@ ${a?`<script id="artifact-manifest-data" type="application/json">${a}</script>`:
2492
4195
  <head>
2493
4196
  <meta charset="UTF-8">
2494
4197
  <meta name="viewport" content="width=device-width,initial-scale=1">
2495
- <meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'unsafe-inline'; script-src 'unsafe-inline'; img-src data: blob: 'self'; media-src blob: 'self'; connect-src 'self' http://127.0.0.1:*;">
4198
+ <meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'unsafe-inline'; script-src 'unsafe-inline'; img-src data: blob: file: 'self'; media-src blob: data: file: 'self'; connect-src 'self' http://127.0.0.1:*;">
2496
4199
  <title>TestRelic AI Analytics Report</title>
2497
4200
  ${r?`<meta name="report-server-port" content="${r}">`:""}
2498
4201
  ${r?`<meta name="artifact-server-port" content="${r}">`:""}
2499
- <link rel="icon" type="image/svg+xml" href="data:image/svg+xml;base64,${Buffer.from(Me).toString("base64")}">
2500
- <style>${Ie}</style>
4202
+ <link rel="icon" type="image/svg+xml" href="data:image/svg+xml;base64,${Buffer.from(qe).toString("base64")}">
4203
+ <style>${He}${rt}${at}${nt}${st}${it}</style>
2501
4204
  <script>(function(){var p=localStorage.getItem('tr-theme')||'system';var t=p==='system'?(window.matchMedia('(prefers-color-scheme:light)').matches?'light':'dark'):p;document.documentElement.setAttribute('data-theme',t);})()</script>
2502
4205
  </head>
2503
4206
  <body>
2504
4207
  <div class="wrap">
2505
4208
  <div class="top-bar">
2506
4209
  <div class="brand">
2507
- ${Ee}
4210
+ ${$e}
2508
4211
  <div class="brand-text">
2509
4212
  <span class="brand-title">TestRelic AI</span>
2510
4213
  <span class="brand-sub">Analytics Report</span>
@@ -2518,19 +4221,19 @@ ${r?`<meta name="artifact-server-port" content="${r}">`:""}
2518
4221
  </div>
2519
4222
  <a class="cta-btn" href="https://testrelic.ai" target="_blank" rel="noopener noreferrer"><svg class="cta-icon" viewBox="0 0 16 16" fill="none"><path d="M8 1l1.545 4.955L14.5 7.5l-4.955 1.545L8 14l-1.545-4.955L1.5 7.5l4.955-1.545L8 1z" fill="currentColor"/></svg>Advanced Insights with AI<span class="cta-sep">\u2013</span><strong>Get Started</strong><svg class="cta-arrow" viewBox="0 0 12 12" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M2.5 6h7M6.5 3l3 3-3 3"/></svg></a>
2520
4223
  </div>
2521
- <div id="summary-strip" class="summary-strip"></div>
2522
- <div id="filter-bar" class="filter-bar"></div>
2523
- <div id="test-grid"></div>
4224
+ <div id="view-overview">
4225
+ <div id="summary-strip"></div>
4226
+ <div id="type-band"></div>
4227
+ <div id="needs-attention"></div>
4228
+ <div id="filter-bar" class="filter-bar"></div>
4229
+ <div id="test-grid"></div>
4230
+ </div>
4231
+ <div id="view-detail" class="detail-page" hidden>
4232
+ <div class="detail-nav" id="detail-nav"></div>
4233
+ <div class="drawer-body" id="drawer-body"></div>
4234
+ </div>
2524
4235
  </div>
2525
4236
  <div class="loading-overlay" id="loading-overlay"><div class="loading-spinner"></div><div class="loading-text">Loading report...</div></div>
2526
- <div class="drawer-backdrop" id="drawer-backdrop"></div>
2527
- <aside class="drawer" id="drawer">
2528
- <div class="drawer-bar">
2529
- <span class="drawer-bar-title">Test Details</span>
2530
- <button class="drawer-close" id="drawer-close">&times;</button>
2531
- </div>
2532
- <div class="drawer-body" id="drawer-body"></div>
2533
- </aside>
2534
4237
  <div class="filter-drawer-backdrop" id="filter-drawer-backdrop"></div>
2535
4238
  <aside class="filter-drawer" id="filter-drawer">
2536
4239
  <div class="filter-drawer-bar">
@@ -2543,55 +4246,56 @@ ${r?`<meta name="artifact-server-port" content="${r}">`:""}
2543
4246
  ${l?`<script id="artifact-manifest-data" type="application/json">${l}</script>`:""}
2544
4247
  <script>${o}</script>
2545
4248
  </body>
2546
- </html>`}function qe(t){try{let e=process.platform,r,n;e==="darwin"?(r="open",n=[t]):e==="win32"?(r="cmd",n=["/c","start","",t]):(r="xdg-open",n=[t]),child_process.execFile(r,n,a=>{a&&process.stderr.write(`[testrelic] Failed to open browser: ${a.message}
2547
- `);});}catch{}}path.join(os$1.tmpdir(),"testrelic-data");async function Wt(t,e,r,n){let a=(e-1)*r,s=[],l=0,i=readline.createInterface({input:fs$1.createReadStream(t,{encoding:"utf-8"}),crlfDelay:1/0});for await(let c of i)if(c.length!==0){if(l>=a&&s.length<r)try{s.push(JSON.parse(c));}catch{}if(l++,s.length>=r&&n!==void 0)break}let d=n??l,o=Math.max(1,Math.ceil(d/r));return {items:s,total:d,page:e,pageSize:r,totalPages:o}}var Yt=/^[a-f0-9]{12}$/,He=/^\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}(-\d+)?$/,Zt=500;function x(t,e,r){t.writeHead(e,{"Content-Type":"application/json"}),t.end(JSON.stringify(r));}function Qt(t){t.setHeader("Access-Control-Allow-Origin","*"),t.setHeader("Access-Control-Allow-Methods","GET, DELETE, OPTIONS"),t.setHeader("Access-Control-Allow-Headers","Content-Type");}function de(t){try{return fs$1.existsSync(t)?JSON.parse(fs$1.readFileSync(t,"utf-8")):null}catch{return null}}function ce(t){let e=0;try{let r=fs$1.readdirSync(t,{withFileTypes:!0});for(let n of r){let a=path.join(t,n.name);n.isFile()?e+=fs$1.statSync(a).size:n.isDirectory()&&(e+=ce(a));}}catch{}return e}function Xt(t,e,r,n){let a=de(path.join(r,"index.json"));x(e,200,{status:"ok",reportMode:"streaming",testCount:a?.length??0,uptime:Math.floor((Date.now()-n)/1e3)});}function er(t,e,r){let n=de(path.join(r,"summary.json"));if(!n){x(e,404,{error:"Summary not found"});return}x(e,200,n);}function tr(t,e,r){let n=de(path.join(r,"index.json"));if(!n){x(e,404,{error:"Test index not found"});return}let s=new URL(t.url??"/",`http://${t.headers.host}`).searchParams,l=Math.max(1,parseInt(s.get("page")??"1",10)||1),i=Math.min(Zt,Math.max(1,parseInt(s.get("pageSize")??"100",10)||100)),d=s.get("status")?.split(",").filter(Boolean)??null,o=s.get("file")??null,c=s.get("search")?.toLowerCase()??null,g=s.get("tag")?.split(",").filter(Boolean)??null,m=s.get("sort")??"file",h=s.get("order")==="desc"?-1:1,u=n;d&&d.length>0&&(u=u.filter(y=>d.includes(y.status))),o&&(u=u.filter(y=>y.filePath===o||y.filePath.startsWith(o+"/"))),c&&(u=u.filter(y=>y.title.toLowerCase().includes(c)||y.filePath.toLowerCase().includes(c))),g&&g.length>0&&(u=u.filter(y=>g.some(k=>y.tags.includes(k)))),u=[...u].sort((y,k)=>{let C=0;switch(m){case "duration":C=y.duration-k.duration;break;case "status":C=y.status.localeCompare(k.status);break;case "title":C=y.title.localeCompare(k.title);break;default:C=y.filePath.localeCompare(k.filePath);break}return C*h});let p=u.length,f=Math.max(1,Math.ceil(p/i)),v=(l-1)*i,w=u.slice(v,v+i);x(e,200,{tests:w,pagination:{page:l,pageSize:i,totalItems:p,totalPages:f},filters:{status:d,file:o,search:c,tag:g}});}function rr(t,e,r,n){if(!Yt.test(n)){x(e,400,{error:"Invalid test ID format"});return}let a=path.join(r,"tests",n,"meta.json"),s=path.join(r,"tests",`${n}.json`),l=fs$1.existsSync(a)?a:fs$1.existsSync(s)?s:null;if(!l){x(e,404,{error:`Test not found: ${n}`});return}try{let i=fs$1.readFileSync(l,"utf-8");e.writeHead(200,{"Content-Type":"application/json"}),e.end(i);}catch(i){x(e,500,{error:i instanceof Error?i.message:String(i)});}}async function nr(t,e,r,n,a){if(!Yt.test(n)){x(e,400,{error:"Invalid test ID format"});return}let l=path.join(r,"tests",n,{network:"network.jsonl",console:"console.jsonl","api-calls":"api-calls.jsonl"}[a]);if(!fs$1.existsSync(l)){x(e,200,{items:[],total:0,page:1,pageSize:50,totalPages:0});return}try{let d=new URL(t.url??"/",`http://${t.headers.host}`).searchParams,o=Math.max(1,parseInt(d.get("page")??"1",10)||1),c=Math.min(Zt,Math.max(1,parseInt(d.get("pageSize")??"50",10)||50)),g,m=path.join(r,"tests",n,"meta.json");if(fs$1.existsSync(m))try{let u=JSON.parse(fs$1.readFileSync(m,"utf-8"));switch(a){case "network":g=u.networkRequestsCount;break;case "console":g=u.consoleLogsCount;break;case "api-calls":g=u.apiCallsCount;break}}catch{}let h=await Wt(l,o,c,g);x(e,200,h);}catch(i){x(e,500,{error:i instanceof Error?i.message:String(i)});}}function sr(t,e,r){let n=de(path.join(r,"index.json"));if(!n){x(e,404,{error:"Test index not found"});return}let a=new Map;for(let l of n){if(l.isRetry)continue;let i=a.get(l.filePath);switch(i||(i={total:0,passed:0,failed:0,flaky:0,skipped:0,timedOut:0},a.set(l.filePath,i)),i.total++,l.status){case "passed":i.passed++;break;case "failed":i.failed++;break;case "flaky":i.flaky++;break;case "skipped":i.skipped++;break;case "timedout":i.timedOut++;break}}let s=Array.from(a.entries()).map(([l,i])=>({filePath:l,...i})).sort((l,i)=>l.filePath.localeCompare(i.filePath));x(e,200,{files:s});}function ar(t,e,r){if(!fs$1.existsSync(r)){x(e,200,{runs:[],totalSizeBytes:0});return}try{let n=[],a=0,s=fs$1.readdirSync(r,{withFileTypes:!0});for(let l of s){if(!l.isDirectory()||!He.test(l.name))continue;let i=path.join(r,l.name),d=ce(i),o=fs$1.readdirSync(i,{withFileTypes:!0}).filter(c=>c.isDirectory());n.push({folderName:l.name,totalSizeBytes:d,testCount:o.length}),a+=d;}n.sort((l,i)=>i.folderName.localeCompare(l.folderName)),x(e,200,{runs:n,totalSizeBytes:a});}catch(n){x(e,500,{error:n instanceof Error?n.message:String(n)});}}function ir(t,e,r){try{let n=0,a=0;if(fs$1.existsSync(r)){let s=fs$1.readdirSync(r,{withFileTypes:!0});for(let l of s){if(!l.isDirectory()||!He.test(l.name))continue;let i=path.join(r,l.name),d=ce(i);fs$1.rmSync(i,{recursive:!0,force:!0}),a+=d,n++;}}x(e,200,{deletedCount:n,freedBytes:a});}catch(n){x(e,500,{error:n instanceof Error?n.message:String(n)});}}function or(t,e,r,n){if(!He.test(n)){x(e,400,{error:"Invalid folder name"});return}let a=path.join(r,n);try{if(!fs$1.statSync(a).isDirectory()){x(e,404,{error:"Not found"});return}}catch{x(e,404,{error:"Not found"});return}try{let s=ce(a);fs$1.rmSync(a,{recursive:!0,force:!0}),x(e,200,{deleted:n,freedBytes:s});}catch(s){x(e,500,{error:s instanceof Error?s.message:String(s)});}}function lr(t,e,r){x(e,200,{status:"shutting_down"}),r.close();}function ze(t,e,r){if(!fs$1.existsSync(r)){x(e,404,{error:"File not found"});return}try{let n=fs$1.readFileSync(r),a=path.extname(r).toLowerCase(),s=Gn[a]??"application/octet-stream";e.writeHead(200,{"Content-Type":s}),e.end(n);}catch(n){x(e,500,{error:n instanceof Error?n.message:String(n)});}}var Gn={".png":"image/png",".jpg":"image/jpeg",".jpeg":"image/jpeg",".gif":"image/gif",".webp":"image/webp",".webm":"video/webm",".mp4":"video/mp4",".json":"application/json",".html":"text/html",".css":"text/css",".js":"text/javascript",".svg":"image/svg+xml"};var Yn=9323,Zn=10,Qn=1800*1e3;function ee(t,e){return new Promise((r,n)=>{let a=e?.port??Yn,s=Date.now(),l,i=0,d=fs$1.existsSync(path.join(t,"artifacts"))?path.join(t,"artifacts"):fs$1.existsSync(path.join(t,"..","artifacts"))?path.join(t,"..","artifacts"):path.join(t,"artifacts");function o(){clearTimeout(l),l=setTimeout(()=>{g.close();},Qn);}let c=e?.htmlPath??null;if(!c){let h=path.dirname(t);try{let u=fs$1.readdirSync(h).find(p=>p.endsWith(".html"));u&&(c=path.join(h,u));}catch{}}let g=http.createServer((h,u)=>{if(o(),Qt(u),h.method==="OPTIONS"){u.writeHead(204),u.end();return}let p;try{p=new URL(h.url??"/",`http://${h.headers.host??"localhost"}`).pathname;}catch{x(u,400,{error:"Invalid URL"});return}if(h.method==="GET"&&(p==="/"||p==="/index.html")){if(c&&fs$1.existsSync(c)){ze(h,u,c);return}x(u,404,{error:"HTML report not found"});return}if(h.method==="GET"&&p==="/api/health"){Xt(h,u,t,s);return}if(h.method==="GET"&&p==="/api/summary"){er(h,u,t);return}if(h.method==="GET"&&p==="/api/tests"){tr(h,u,t);return}if(h.method==="GET"&&p==="/api/files"){sr(h,u,t);return}let f=p.match(/^\/api\/tests\/([a-f0-9]+)\/(network|console|api-calls)$/);if(h.method==="GET"&&f){nr(h,u,t,f[1],f[2]);return}let v=p.match(/^\/api\/tests\/([a-f0-9]+)$/);if(h.method==="GET"&&v){rr(h,u,t,v[1]);return}if(h.method==="GET"&&p==="/api/artifacts"){ar(h,u,d);return}if(h.method==="DELETE"&&p==="/api/artifacts"){ir(h,u,d);return}let w=p.match(/^\/api\/artifacts\/(.+)$/);if(h.method==="DELETE"&&w){or(h,u,d,decodeURIComponent(w[1]));return}if(h.method==="GET"&&p.startsWith("/artifacts/")){let y=decodeURIComponent(p.slice(11));if(y.includes("..")||y.includes("\0")){x(u,400,{error:"Invalid path"});return}ze(h,u,path.join(d,y));return}if(h.method==="POST"&&p==="/api/shutdown"){lr(h,u,g);return}x(u,404,{error:"Not found"});});function m(h){let u=p=>{p.code==="EADDRINUSE"&&i<Zn?(i++,m(h+1)):n(p);};g.once("error",u),g.listen(h,"127.0.0.1",()=>{g.removeListener("error",u);let p=g.address();if(!p||typeof p=="string"){n(new Error("Failed to get server address"));return}o(),r({port:p.port,dispose:()=>new Promise(f=>{clearTimeout(l),g.close(()=>f());})});});}m(a);})}async function dr(t){let e=await ee(t);return {port:e.port,dispose:e.dispose}}function ts(t,e,r){let n=JSON.stringify(t),a=r?JSON.stringify(r):null;return Gt(n,e,a)}async function je(t,e,r){try{let n=null,a=null,s=e.reportMode==="streaming"||e.reportMode==="auto"&&t.timeline.length===0&&t.summary.total>=e.streamingThreshold,l,i=e.htmlReportPath,d=path.dirname(i);fs$1.mkdirSync(d,{recursive:!0});let o="",c="[]",g=null;if(s){o=JSON.stringify({...t.summary,testRunId:t.testRunId,startedAt:t.startedAt,completedAt:t.completedAt,totalDuration:t.totalDuration,ci:t.ci,metadata:t.metadata,shardRunIds:t.shardRunIds});let h=path.dirname(e.outputPath),u=path.join(h,".testrelic-report");try{let p=path.join(u,"index-compact.json"),f=path.join(u,"index.json");fs$1.existsSync(p)?c=fs$1.readFileSync(p,"utf-8"):fs$1.existsSync(f)&&(c=fs$1.readFileSync(f,"utf-8"));}catch{}g=r?JSON.stringify(r):null,l=Ue(o,c,null,g);}else l=ts(t,null,r);let m=i+".tmp";if(fs$1.writeFileSync(m,l,"utf-8"),fs$1.renameSync(m,i),s){if(t.ci===null){let h=path.dirname(e.outputPath),u=path.join(h,".testrelic-report"),p=path.resolve(i);try{if(await ns(),n=await ss(u),!n){a=await ee(u,{htmlPath:p}),n=a.port;let f=setInterval(()=>{},6e4),v=()=>{clearInterval(f),a?.dispose();};process.on("SIGINT",v),process.on("SIGTERM",v),setTimeout(v,1800*1e3).unref();}if(n){process.stderr.write(`
2548
- Report server: http://127.0.0.1:${n}
2549
- `);try{let f=Ue(o,c,n,g),v=i+".tmp";fs$1.writeFileSync(v,f,"utf-8"),fs$1.renameSync(v,i);}catch{}}}catch{}}}else if(e.openReport&&t.ci===null&&e.includeArtifacts){let h=path.dirname(e.outputPath),u=path.join(h,"artifacts");if(fs$1.existsSync(u))try{let p=await dr(u);n=p.port,process.on("exit",()=>{p.dispose();});}catch{}}if(e.openReport&&t.ci===null)if(s&&n)qe(`http://127.0.0.1:${n}`);else {let h=path.resolve(i);qe(h);}}catch(n){process.stderr.write(`[testrelic] Failed to write HTML report: ${n instanceof Error?n.message:String(n)}
2550
- `);}}var fe=9323,gr=10;async function rs(){for(let t=fe;t<fe+gr;t++)try{let e=new AbortController,r=setTimeout(()=>e.abort(),500),n=await fetch(`http://127.0.0.1:${t}/api/health`,{signal:e.signal});if(clearTimeout(r),n.ok)return t}catch{}return null}async function ns(){for(let t=fe;t<fe+gr;t++)try{let e=new AbortController,r=setTimeout(()=>e.abort(),1e3);await fetch(`http://127.0.0.1:${t}/api/shutdown`,{method:"POST",signal:e.signal}),clearTimeout(r);}catch{}await new Promise(t=>setTimeout(t,300));}async function ss(t){let r=[path.join(__dirname,"cli.cjs"),path.join(__dirname,"cli.js"),path.join(__dirname,"..","dist","cli.cjs"),path.join(__dirname,"..","dist","cli.js")].find(n=>fs$1.existsSync(n));if(!r){let n=await ee(t);return process.on("exit",()=>{n?.dispose();}),n.port}return new Promise(n=>{let a=child_process.spawn(process.execPath,[r,"serve",t],{detached:true,stdio:["ignore","ignore","pipe"],env:{...process.env}}),s="",l=false,i=setTimeout(()=>{l||(l=true,a.stderr?.removeAllListeners(),rs().then(n));},5e3);a.stderr?.on("data",d=>{s+=d.toString();let o=s.match(/127\.0\.0\.1:(\d+)/);o&&!l&&(l=true,clearTimeout(i),a.stderr?.removeAllListeners(),a.unref(),n(Number(o[1])));}),a.on("error",()=>{l||(l=true,clearTimeout(i),n(null));}),a.on("exit",()=>{l||(l=true,clearTimeout(i),n(null));}),a.unref();})}var os=20,Ge=0,We=[];async function Ke(t,e){Ge>=os&&await new Promise(r=>We.push(r)),Ge++;try{return await promises.copyFile(t,e),!0}catch{return false}finally{Ge--,We.length>0&&We.shift()();}}var G=[];async function mr(){G.length!==0&&(await Promise.allSettled(G),G.length=0);}function vr(t){let e=new Date,r=s=>String(s).padStart(2,"0"),n=`${e.getFullYear()}-${r(e.getMonth()+1)}-${r(e.getDate())}T${r(e.getHours())}-${r(e.getMinutes())}-${r(e.getSeconds())}`;if(!fs$1.existsSync(path.join(t,n)))return n;let a=1;for(;fs$1.existsSync(path.join(t,`${n}-${a}`));)a++;return `${n}-${a}`}function hr(t){let e=t.replace(/[^a-zA-Z0-9\-_ ]/g,"-").replace(/\s+/g,"-").replace(/-{2,}/g,"-").replace(/^-+|-+$/g,"");return e.length>100&&(e=e.substring(0,100).replace(/-+$/,"")),e||"unnamed-test"}var ls=8;function ds(t,e,r){let n=hr(t),a=r?hr(r).substring(0,ls):"";return a&&(n+=`--${a}`),e>0&&(n+=`--retry-${e}`),n}function yr(t,e,r,n,a,s){let l=t.find(u=>u.name==="screenshot"&&u.path),i=t.find(u=>u.name==="video"&&u.path),d=t.find(u=>u.name==="trace"&&u.path);if(!l&&!i&&!d)return null;let o=ds(e,r,s),c=a?["artifacts",a,o]:["artifacts",o],g=path.join(n,...c),m=c,h={};try{fs$1.mkdirSync(g,{recursive:!0});}catch{return null}if(l?.path&&fs$1.existsSync(l.path)){let p=`screenshot${path.extname(l.path)||".png"}`,f=path.join(g,p);G.push(Ke(l.path,f).then(()=>{})),h.screenshot=`${m.join("/")}/${p}`;}if(i?.path&&fs$1.existsSync(i.path)){let p=`video${path.extname(i.path)||".webm"}`,f=path.join(g,p);G.push(Ke(i.path,f).then(()=>{})),h.video=`${m.join("/")}/${p}`;}if(d?.path&&fs$1.existsSync(d.path)){let p=`trace${path.extname(d.path)||".zip"}`,f=path.join(g,p);G.push(Ke(d.path,f).then(()=>{})),h.trace=`${m.join("/")}/${p}`;}return !h.screenshot&&!h.video&&!h.trace?null:h}var fs=/^\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}(-\d+)?$/,gs="1.0";function hs(t){let e=path.extname(t).toLowerCase();return [".png",".jpg",".jpeg",".gif",".bmp",".webp"].includes(e)?"screenshot":[".webm",".mp4",".avi",".mov"].includes(e)?"video":e===".zip"&&path.basename(t,e)==="trace"?"trace":"other"}function br(t,e,r){let n=[];try{let a=fs$1.readdirSync(t,{withFileTypes:!0});for(let s of a){if(!s.isFile())continue;let l=path.join(t,s.name),i=fs$1.statSync(l);n.push({name:s.name,type:hs(s.name),relativePath:`artifacts/${e}/${r}/${s.name}`,sizeBytes:i.size});}}catch{}return {testName:r,files:n}}function ms(t,e){let r=path.join(t,e),n=[],a=0;try{let l=fs$1.readdirSync(r,{withFileTypes:!0});for(let i of l){if(!i.isDirectory())continue;let d=br(path.join(r,i.name),e,i.name);n.push(d);for(let o of d.files)a+=o.sizeBytes;}}catch{}let s=e.replace(/^(\d{4}-\d{2}-\d{2})T(\d{2})-(\d{2})-(\d{2})/,"$1T$2:$3:$4").replace(/-\d+$/,"");return n.sort((l,i)=>l.testName.localeCompare(i.testName)),{folderName:e,timestamp:s,totalSizeBytes:a,testCount:n.length,tests:n,isCurrentRun:false}}function xr(t,e){let r=path.join(t,"artifacts"),n=[],a=[],s=0;try{let i=fs$1.readdirSync(r,{withFileTypes:!0});for(let d of i)if(d.isDirectory())if(fs.test(d.name)){let o=ms(r,d.name);n.push({...o,isCurrentRun:d.name===e});}else {let o=br(path.join(r,d.name),d.name,d.name);a.push(o);for(let c of o.files)s+=c.sizeBytes;}}catch{}n.sort((i,d)=>d.timestamp.localeCompare(i.timestamp)),a.length>0&&(a.sort((i,d)=>i.testName.localeCompare(d.testName)),n.push({folderName:"__legacy__",timestamp:"1970-01-01T00:00:00",totalSizeBytes:s,testCount:a.length,tests:a,isCurrentRun:false}));let l=n.reduce((i,d)=>i+d.totalSizeBytes,0);return {schemaVersion:gs,generatedAt:new Date().toISOString(),artifactBaseDir:"artifacts",totalSizeBytes:l,runs:n,serverPort:null}}var ks=256;function Ts(t){let e=path.resolve(t);for(let r=0;r<ks;r++){if(fs$1.existsSync(path.join(e,".git")))return e;let n=path.dirname(e);if(n===e)return null;e=n;}return null}function Tr(t){try{let e=Ts(t);if(!e)return;let r=path.join(e,".gitignore"),n=path.relative(e,t).replace(/\\/g,"/"),a=n.endsWith("/")?n:`${n}/`;if(fs$1.existsSync(r)&&fs$1.readFileSync(r,"utf-8").split(`
2551
- `).map(d=>d.trim()).some(d=>d===a||d===n))return;let s=`
4249
+ </html>`}function lt(e){try{let t=process.platform,r,a;t==="darwin"?(r="open",a=[e]):t==="win32"?(r="cmd",a=["/c","start","",e]):(r="xdg-open",a=[e]),child_process.execFile(r,a,n=>{n&&process.stderr.write(`[testrelic] Failed to open browser: ${n.message}
4250
+ `);});}catch{}}path.join(os$1.tmpdir(),"testrelic-data");async function br(e,t,r,a){let n=(t-1)*r,s=[],l=0,i=readline.createInterface({input:fs$1.createReadStream(e,{encoding:"utf-8"}),crlfDelay:1/0});for await(let c of i)if(c.length!==0){if(l>=n&&s.length<r)try{s.push(JSON.parse(c));}catch{}if(l++,s.length>=r&&a!==void 0)break}let d=a??l,o=Math.max(1,Math.ceil(d/r));return {items:s,total:d,page:t,pageSize:r,totalPages:o}}var kr=/^[a-f0-9]{12}$/,ut=/^\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}(-\d+)?$/,Sr=500;function k(e,t,r){e.writeHead(t,{"Content-Type":"application/json"}),e.end(JSON.stringify(r));}function Cr(e){e.setHeader("Access-Control-Allow-Origin","*"),e.setHeader("Access-Control-Allow-Methods","GET, DELETE, OPTIONS"),e.setHeader("Access-Control-Allow-Headers","Content-Type");}function he(e){try{return fs$1.existsSync(e)?JSON.parse(fs$1.readFileSync(e,"utf-8")):null}catch{return null}}function me(e){let t=0;try{let r=fs$1.readdirSync(e,{withFileTypes:!0});for(let a of r){let n=path.join(e,a.name);a.isFile()?t+=fs$1.statSync(n).size:a.isDirectory()&&(t+=me(n));}}catch{}return t}function Tr(e,t,r,a){let n=he(path.join(r,"index.json"));k(t,200,{status:"ok",reportMode:"streaming",testCount:n?.length??0,uptime:Math.floor((Date.now()-a)/1e3)});}function Rr(e,t,r){let a=he(path.join(r,"summary.json"));if(!a){k(t,404,{error:"Summary not found"});return}k(t,200,a);}function Ar(e,t,r){let a=he(path.join(r,"index.json"));if(!a){k(t,404,{error:"Test index not found"});return}let s=new URL(e.url??"/",`http://${e.headers.host}`).searchParams,l=Math.max(1,parseInt(s.get("page")??"1",10)||1),i=Math.min(Sr,Math.max(1,parseInt(s.get("pageSize")??"100",10)||100)),d=s.get("status")?.split(",").filter(Boolean)??null,o=s.get("file")??null,c=s.get("search")?.toLowerCase()??null,p=s.get("tag")?.split(",").filter(Boolean)??null,v=s.get("sort")??"file",g=s.get("order")==="desc"?-1:1,f=a;d&&d.length>0&&(f=f.filter(y=>d.includes(y.status))),o&&(f=f.filter(y=>y.filePath===o||y.filePath.startsWith(o+"/"))),c&&(f=f.filter(y=>y.title.toLowerCase().includes(c)||y.filePath.toLowerCase().includes(c))),p&&p.length>0&&(f=f.filter(y=>p.some(w=>y.tags.includes(w)))),f=[...f].sort((y,w)=>{let C=0;switch(v){case "duration":C=y.duration-w.duration;break;case "status":C=y.status.localeCompare(w.status);break;case "title":C=y.title.localeCompare(w.title);break;default:C=y.filePath.localeCompare(w.filePath);break}return C*g});let m=f.length,u=Math.max(1,Math.ceil(m/i)),h=(l-1)*i,b=f.slice(h,h+i);k(t,200,{tests:b,pagination:{page:l,pageSize:i,totalItems:m,totalPages:u},filters:{status:d,file:o,search:c,tag:p}});}function _r(e,t,r,a){if(!kr.test(a)){k(t,400,{error:"Invalid test ID format"});return}let n=path.join(r,"tests",a,"meta.json"),s=path.join(r,"tests",`${a}.json`),l=fs$1.existsSync(n)?n:fs$1.existsSync(s)?s:null;if(!l){k(t,404,{error:`Test not found: ${a}`});return}try{let i=fs$1.readFileSync(l,"utf-8");t.writeHead(200,{"Content-Type":"application/json"}),t.end(i);}catch(i){k(t,500,{error:i instanceof Error?i.message:String(i)});}}async function Ir(e,t,r,a,n){if(!kr.test(a)){k(t,400,{error:"Invalid test ID format"});return}let l=path.join(r,"tests",a,{network:"network.jsonl",console:"console.jsonl","api-calls":"api-calls.jsonl"}[n]);if(!fs$1.existsSync(l)){k(t,200,{items:[],total:0,page:1,pageSize:50,totalPages:0});return}try{let d=new URL(e.url??"/",`http://${e.headers.host}`).searchParams,o=Math.max(1,parseInt(d.get("page")??"1",10)||1),c=Math.min(Sr,Math.max(1,parseInt(d.get("pageSize")??"50",10)||50)),p,v=path.join(r,"tests",a,"meta.json");if(fs$1.existsSync(v))try{let f=JSON.parse(fs$1.readFileSync(v,"utf-8"));switch(n){case "network":p=f.networkRequestsCount;break;case "console":p=f.consoleLogsCount;break;case "api-calls":p=f.apiCallsCount;break}}catch{}let g=await br(l,o,c,p);k(t,200,g);}catch(i){k(t,500,{error:i instanceof Error?i.message:String(i)});}}function Mr(e,t,r){let a=he(path.join(r,"index.json"));if(!a){k(t,404,{error:"Test index not found"});return}let n=new Map;for(let l of a){if(l.isRetry)continue;let i=n.get(l.filePath);switch(i||(i={total:0,passed:0,failed:0,flaky:0,skipped:0,timedOut:0},n.set(l.filePath,i)),i.total++,l.status){case "passed":i.passed++;break;case "failed":i.failed++;break;case "flaky":i.flaky++;break;case "skipped":i.skipped++;break;case "timedout":i.timedOut++;break}}let s=Array.from(n.entries()).map(([l,i])=>({filePath:l,...i})).sort((l,i)=>l.filePath.localeCompare(i.filePath));k(t,200,{files:s});}function Er(e,t,r){if(!fs$1.existsSync(r)){k(t,200,{runs:[],totalSizeBytes:0});return}try{let a=[],n=0,s=fs$1.readdirSync(r,{withFileTypes:!0});for(let l of s){if(!l.isDirectory()||!ut.test(l.name))continue;let i=path.join(r,l.name),d=me(i),o=fs$1.readdirSync(i,{withFileTypes:!0}).filter(c=>c.isDirectory());a.push({folderName:l.name,totalSizeBytes:d,testCount:o.length}),n+=d;}a.sort((l,i)=>i.folderName.localeCompare(l.folderName)),k(t,200,{runs:a,totalSizeBytes:n});}catch(a){k(t,500,{error:a instanceof Error?a.message:String(a)});}}function Pr(e,t,r){try{let a=0,n=0;if(fs$1.existsSync(r)){let s=fs$1.readdirSync(r,{withFileTypes:!0});for(let l of s){if(!l.isDirectory()||!ut.test(l.name))continue;let i=path.join(r,l.name),d=me(i);fs$1.rmSync(i,{recursive:!0,force:!0}),n+=d,a++;}}k(t,200,{deletedCount:a,freedBytes:n});}catch(a){k(t,500,{error:a instanceof Error?a.message:String(a)});}}function Lr(e,t,r,a){if(!ut.test(a)){k(t,400,{error:"Invalid folder name"});return}let n=path.join(r,a);try{if(!fs$1.statSync(n).isDirectory()){k(t,404,{error:"Not found"});return}}catch{k(t,404,{error:"Not found"});return}try{let s=me(n);fs$1.rmSync(n,{recursive:!0,force:!0}),k(t,200,{deleted:a,freedBytes:s});}catch(s){k(t,500,{error:s instanceof Error?s.message:String(s)});}}function Dr(e,t,r){k(t,200,{status:"shutting_down"}),r.close();}function pt(e,t,r){if(!fs$1.existsSync(r)){k(t,404,{error:"File not found"});return}try{let a=fs$1.statSync(r).size,n=path.extname(r).toLowerCase(),s=Un[n]??"application/octet-stream",l=On(e.headers.range,a);if(l==="unsatisfiable"){t.writeHead(416,{"Content-Range":`bytes */${a}`,"Accept-Ranges":"bytes"}),t.end();return}if(l){t.writeHead(206,{"Content-Type":s,"Content-Length":l.end-l.start+1,"Content-Range":`bytes ${l.start}-${l.end}/${a}`,"Accept-Ranges":"bytes"}),xr(fs$1.createReadStream(r,{start:l.start,end:l.end}),t);return}t.writeHead(200,{"Content-Type":s,"Content-Length":a,"Accept-Ranges":"bytes"}),xr(fs$1.createReadStream(r),t);}catch(a){k(t,500,{error:a instanceof Error?a.message:String(a)});}}function xr(e,t){e.on("error",()=>{e.destroy(),t.destroy();}),t.on("close",()=>{e.destroy();}),e.pipe(t);}function On(e,t){if(!e)return null;let r=/^bytes=(\d*)-(\d*)$/.exec(e.trim());if(!r)return null;let[,a,n]=r;if(a===""&&n==="")return null;let s,l;if(a===""){let i=Number(n);if(i<=0)return "unsatisfiable";s=Math.max(0,t-i),l=t-1;}else s=Number(a),l=n===""?t-1:Math.min(Number(n),t-1);return !Number.isFinite(s)||!Number.isFinite(l)?null:t===0||s>=t||s>l?"unsatisfiable":{start:s,end:l}}var Un={".png":"image/png",".jpg":"image/jpeg",".jpeg":"image/jpeg",".gif":"image/gif",".webp":"image/webp",".webm":"video/webm",".mp4":"video/mp4",".json":"application/json",".html":"text/html",".css":"text/css",".js":"text/javascript",".svg":"image/svg+xml"};var $n=9323,Vn=10,jn=1800*1e3;function oe(e,t){return new Promise((r,a)=>{let n=t?.port??$n,s=Date.now(),l,i=0,d=fs$1.existsSync(path.join(e,"artifacts"))?path.join(e,"artifacts"):fs$1.existsSync(path.join(e,"..","artifacts"))?path.join(e,"..","artifacts"):path.join(e,"artifacts");function o(){clearTimeout(l),l=setTimeout(()=>{p.close();},jn);}let c=t?.htmlPath??null;if(!c){let g=path.dirname(e);try{let f=fs$1.readdirSync(g).find(m=>m.endsWith(".html"));f&&(c=path.join(g,f));}catch{}}let p=http.createServer((g,f)=>{if(o(),Cr(f),g.method==="OPTIONS"){f.writeHead(204),f.end();return}let m;try{m=new URL(g.url??"/",`http://${g.headers.host??"localhost"}`).pathname;}catch{k(f,400,{error:"Invalid URL"});return}if(g.method==="GET"&&(m==="/"||m==="/index.html")){if(c&&fs$1.existsSync(c)){pt(g,f,c);return}k(f,404,{error:"HTML report not found"});return}if(g.method==="GET"&&m==="/api/health"){Tr(g,f,e,s);return}if(g.method==="GET"&&m==="/api/summary"){Rr(g,f,e);return}if(g.method==="GET"&&m==="/api/tests"){Ar(g,f,e);return}if(g.method==="GET"&&m==="/api/files"){Mr(g,f,e);return}let u=m.match(/^\/api\/tests\/([a-f0-9]+)\/(network|console|api-calls)$/);if(g.method==="GET"&&u){Ir(g,f,e,u[1],u[2]);return}let h=m.match(/^\/api\/tests\/([a-f0-9]+)$/);if(g.method==="GET"&&h){_r(g,f,e,h[1]);return}if(g.method==="GET"&&m==="/api/artifacts"){Er(g,f,d);return}if(g.method==="DELETE"&&m==="/api/artifacts"){Pr(g,f,d);return}let b=m.match(/^\/api\/artifacts\/(.+)$/);if(g.method==="DELETE"&&b){Lr(g,f,d,decodeURIComponent(b[1]));return}if(g.method==="GET"&&m.startsWith("/artifacts/")){let y=decodeURIComponent(m.slice(11));if(y.includes("..")||y.includes("\0")){k(f,400,{error:"Invalid path"});return}pt(g,f,path.join(d,y));return}if(g.method==="POST"&&m==="/api/shutdown"){Dr(g,f,p);return}k(f,404,{error:"Not found"});});function v(g){let f=m=>{m.code==="EADDRINUSE"&&i<Vn?(i++,v(g+1)):a(m);};p.once("error",f),p.listen(g,"127.0.0.1",()=>{p.removeListener("error",f);let m=p.address();if(!m||typeof m=="string"){a(new Error("Failed to get server address"));return}o(),r({port:m.port,dispose:()=>new Promise(u=>{clearTimeout(l),p.close(()=>u());})});});}v(n);})}async function Nr(e){let t=await oe(e);return {port:t.port,dispose:t.dispose}}function Kn(e,t,r){let a=JSON.stringify(e),n=r?JSON.stringify(r):null;return vr(a,t,n)}async function gt(e,t,r){try{let a=null,n=null,s=t.reportMode==="streaming"||t.reportMode==="auto"&&e.timeline.length===0&&e.summary.total>=t.streamingThreshold,l,i=t.htmlReportPath,d=path.dirname(i);fs$1.mkdirSync(d,{recursive:!0});let o="",c="[]",p=null;if(s){o=JSON.stringify({...e.summary,testRunId:e.testRunId,startedAt:e.startedAt,completedAt:e.completedAt,totalDuration:e.totalDuration,ci:e.ci,metadata:e.metadata,shardRunIds:e.shardRunIds});let g=path.dirname(t.outputPath),f=path.join(g,".testrelic-report");try{let m=path.join(f,"index-compact.json"),u=path.join(f,"index.json");fs$1.existsSync(m)?c=fs$1.readFileSync(m,"utf-8"):fs$1.existsSync(u)&&(c=fs$1.readFileSync(u,"utf-8"));}catch{}p=r?JSON.stringify(r):null,l=ot(o,c,null,p);}else l=Kn(e,null,r);let v=i+".tmp";if(fs$1.writeFileSync(v,l,"utf-8"),fs$1.renameSync(v,i),s){if(e.ci===null){let g=path.dirname(t.outputPath),f=path.join(g,".testrelic-report"),m=path.resolve(i);try{if(await Yn(),a=await Xn(f),!a){n=await oe(f,{htmlPath:m}),a=n.port;let u=setInterval(()=>{},6e4),h=()=>{clearInterval(u),n?.dispose();};process.on("SIGINT",h),process.on("SIGTERM",h),setTimeout(h,1800*1e3).unref();}if(a){process.stderr.write(`
4251
+ Report server: http://127.0.0.1:${a}
4252
+ `);try{let u=ot(o,c,a,p),h=i+".tmp";fs$1.writeFileSync(h,u,"utf-8"),fs$1.renameSync(h,i);}catch{}}}catch{}}}else if(t.openReport&&e.ci===null&&t.includeArtifacts){let g=path.dirname(t.outputPath),f=path.join(g,"artifacts");if(fs$1.existsSync(f))try{let m=await Nr(f);a=m.port,process.on("exit",()=>{m.dispose();});}catch{}}if(t.openReport&&e.ci===null)if(s&&a)lt(`http://127.0.0.1:${a}`);else {let g=path.resolve(i);lt(g);}}catch(a){process.stderr.write(`[testrelic] Failed to write HTML report: ${a instanceof Error?a.message:String(a)}
4253
+ `);}}var ye=9323,zr=10;async function Jn(){for(let e=ye;e<ye+zr;e++)try{let t=new AbortController,r=setTimeout(()=>t.abort(),500),a=await fetch(`http://127.0.0.1:${e}/api/health`,{signal:t.signal});if(clearTimeout(r),a.ok)return e}catch{}return null}async function Yn(){for(let e=ye;e<ye+zr;e++)try{let t=new AbortController,r=setTimeout(()=>t.abort(),1e3);await fetch(`http://127.0.0.1:${e}/api/shutdown`,{method:"POST",signal:t.signal}),clearTimeout(r);}catch{}await new Promise(e=>setTimeout(e,300));}async function Xn(e){let r=[path.join(__dirname,"cli.cjs"),path.join(__dirname,"cli.js"),path.join(__dirname,"..","dist","cli.cjs"),path.join(__dirname,"..","dist","cli.js")].find(a=>fs$1.existsSync(a));if(!r){let a=await oe(e);return process.on("exit",()=>{a?.dispose();}),a.port}return new Promise(a=>{let n=child_process.spawn(process.execPath,[r,"serve",e],{detached:true,stdio:["ignore","ignore","pipe"],env:{...process.env}}),s="",l=false,i=setTimeout(()=>{l||(l=true,n.stderr?.removeAllListeners(),Jn().then(a));},5e3);n.stderr?.on("data",d=>{s+=d.toString();let o=s.match(/127\.0\.0\.1:(\d+)/);o&&!l&&(l=true,clearTimeout(i),n.stderr?.removeAllListeners(),n.unref(),a(Number(o[1])));}),n.on("error",()=>{l||(l=true,clearTimeout(i),a(null));}),n.on("exit",()=>{l||(l=true,clearTimeout(i),a(null));}),n.unref();})}var Hr=Buffer.from([137,80,78,71,13,10,26,10]),Zn=16,Qn=20,xe=24;function qr(e){if(e.length<xe||!e.subarray(0,Hr.length).equals(Hr))return null;let t=e.readUInt32BE(Zn),r=e.readUInt32BE(Qn);return t===0||r===0?null:{width:t,height:r}}var es=/(\d+) pixels \(ratio [\d.]+ of all image pixels\) are different/,$r=/Expected an image (\d+)px by (\d+)px, received (\d+)px by (\d+)px/;function Vr(e){return e?$r.test(e):false}var ts=/^\s*Snapshot:\s*(.+?)\s*$/m;function jr(e){if(!e)return {};let t={},r=es.exec(e);if(r){let s=Number.parseInt(r[1],10);Number.isFinite(s)&&(t.diffPixels=s);}let a=$r.exec(e);if(a){let[,s,l,i,d]=a,o=[s,l,i,d].map(c=>Number.parseInt(c,10));o.every(c=>Number.isFinite(c))&&(t.expectedWidth=o[0],t.expectedHeight=o[1],t.actualWidth=o[2],t.actualHeight=o[3]);}let n=ts.exec(e);return n&&(t.snapshotName=n[1]),t}function Gr(e,t){if(e===void 0||!t)return;let r=t.width*t.height;if(!(r<=0))return e/r}var os=20,ht=0,mt=[];async function le(e,t){ht>=os&&await new Promise(r=>mt.push(r)),ht++;try{return await promises.copyFile(e,t),!0}catch{return false}finally{ht--,mt.length>0&&mt.shift()();}}var H=[];async function Kr(){H.length!==0&&(await Promise.allSettled(H),H.length=0);}function Jr(e){let t=new Date,r=s=>String(s).padStart(2,"0"),a=`${t.getFullYear()}-${r(t.getMonth()+1)}-${r(t.getDate())}T${r(t.getHours())}-${r(t.getMinutes())}-${r(t.getSeconds())}`;if(!fs$1.existsSync(path.join(e,a)))return a;let n=1;for(;fs$1.existsSync(path.join(e,`${a}-${n}`));)n++;return `${a}-${n}`}function vt(e){let t=e.replace(/[^a-zA-Z0-9\-_ ]/g,"-").replace(/\s+/g,"-").replace(/-{2,}/g,"-").replace(/^-+|-+$/g,"");return t.length>100&&(t=t.substring(0,100).replace(/-+$/,"")),t||"unnamed-test"}var ls=8;function Yr(e,t,r){let a=vt(e),n=r?vt(r).substring(0,ls):"";return n&&(a+=`--${n}`),t>0&&(a+=`--retry-${t}`),a}function Xr(e,t,r,a,n,s){let l=e.find(f=>f.name==="screenshot"&&f.path),i=e.find(f=>f.name==="video"&&f.path),d=e.find(f=>f.name==="trace"&&f.path);if(!l&&!i&&!d)return null;let o=Yr(t,r,s),c=n?["artifacts",n,o]:["artifacts",o],p=path.join(a,...c),v=c,g={};try{fs$1.mkdirSync(p,{recursive:!0});}catch{return null}if(l?.path&&fs$1.existsSync(l.path)){let m=`screenshot${path.extname(l.path)||".png"}`,u=path.join(p,m);H.push(le(l.path,u).then(()=>{})),g.screenshot=`${v.join("/")}/${m}`;}if(i?.path&&fs$1.existsSync(i.path)){let m=`video${path.extname(i.path)||".webm"}`,u=path.join(p,m);H.push(le(i.path,u).then(()=>{})),g.video=`${v.join("/")}/${m}`;}if(d?.path&&fs$1.existsSync(d.path)){let m=`trace${path.extname(d.path)||".zip"}`,u=path.join(p,m);H.push(le(d.path,u).then(()=>{})),g.trace=`${v.join("/")}/${m}`;}return !g.screenshot&&!g.video&&!g.trace?null:g}var bt="visual",ds=6;function cs(e){let t=crypto.createHash("sha256").update(e).digest("hex").substring(0,ds);return `${vt(e)}--${t}`}function Wr(e){let t=null;try{t=fs$1.openSync(e,"r");let r=Buffer.alloc(xe),a=fs$1.readSync(t,r,0,xe,0);return qr(r.subarray(0,a))}catch{return null}finally{if(t!==null)try{fs$1.closeSync(t);}catch{}}}var us=[{key:"expectedPath",dest:"baseline",field:"baseline"},{key:"actualPath",dest:"actual",field:"actual"},{key:"diffPath",dest:"diff",field:"diff"},{key:"previousPath",dest:"previous",field:"previous"}],ps=[{key:"baselineDomPath",dest:"baseline.dom.json",field:"baselineDom"},{key:"actualDomPath",dest:"actual.dom.json",field:"actualDom"}];function fs(e,t,r){let a=cs(e.name),n=path.join(t,a);try{fs$1.mkdirSync(n,{recursive:!0});}catch{return null}let s={},l=false;for(let c of us){let p=e[c.key];if(!p||!fs$1.existsSync(p))continue;let v=path.extname(p)||".png",g=`${c.dest}${v}`;H.push(le(p,path.join(n,g)).then(()=>{})),s[c.field]=`${r}/${bt}/${a}/${g}`,l=true;}let i=e.dom;if(i)for(let c of ps){let p=i[c.key];!p||!fs$1.existsSync(p)||(H.push(le(p,path.join(n,c.dest)).then(()=>{})),s[c.field]=`${r}/${bt}/${a}/${c.dest}`);}if(!l)return null;let d=e.expectedPath?Wr(e.expectedPath):e.actualPath?Wr(e.actualPath):null,o=Gr(e.diffPixels,d);return {name:e.name,status:e.status,source:e.source,...s,...d?{width:d.width,height:d.height}:{},...e.actualWidth!==void 0?{actualWidth:e.actualWidth}:{},...e.actualHeight!==void 0?{actualHeight:e.actualHeight}:{},...e.diffPixels!==void 0?{diffPixels:e.diffPixels}:{},...o!==void 0?{diffPixelRatio:o}:{},...i?{domChanges:i.changes,domChangeCount:i.total,domRootCauses:i.rootCauses,domTruncated:i.truncated,domCaptureBox:i.captureBox}:{}}}function Zr(e,t,r,a,n,s){if(e.length===0)return null;let l=Yr(t,r,s),i=n?["artifacts",n,l]:["artifacts",l],d=path.join(a,...i,bt),o=i.join("/");try{fs$1.mkdirSync(d,{recursive:!0});}catch{return null}let c=[];for(let p of e){let v=fs(p,d,o);v&&c.push(v);}return c.length>0?c:null}var Qr="testrelic-visual-dom";var gs="testrelic-visual",hs=/^(.+)-(expected|actual|diff|previous)(\.[A-Za-z0-9]+)$/;function ms(e){let t=new Map;for(let r of e)if(!(r.name!==gs||!r.body))try{let a=JSON.parse(r.body.toString("utf-8"));if(!Array.isArray(a))continue;for(let n of a){let s=bs(n);s&&t.set(s.stem,s);}}catch{}return t}var vs=["passed","failed","new","updated"];function bs(e){if(typeof e!="object"||e===null)return null;let t=e;return typeof t.stem!="string"||!t.stem||typeof t.name!="string"||!t.name||typeof t.status!="string"||!vs.includes(t.status)?null:{stem:t.stem,name:t.name,status:t.status}}function ys(e){let t=new Map;for(let r of e)if(!(r.name!==Qr||!r.body))try{let a=JSON.parse(r.body.toString("utf-8"));if(!Array.isArray(a))continue;for(let n of a){if(typeof n!="object"||n===null)continue;let s=n;typeof s.stem!="string"||!Array.isArray(s.changes)||t.set(s.stem,n);}}catch{}return t}function xs(e){let t=new Map;for(let r of e){if(!r.path)continue;let a=hs.exec(r.name);if(!a)continue;let[,n,s]=a,l=t.get(n)??{};l[s]=r.path,t.set(n,l);}return t}function ws(e){return !!(e.actual&&(e.expected||e.diff))}function ks(e,t,r){return e.diff||Vr(r)?"failed":t?"updated":"new"}function Ss(e,t){if(e.length===0)return null;if(e.length===1)return e[0];if(!t)return null;let r=t.replace(/\.[A-Za-z0-9]+$/,""),a=e.filter(n=>n===r||n.endsWith(`/${r}`));return a.length===1?a[0]:null}function ta(e,t={}){if(e.length===0)return [];let r=ms(e),a=ys(e),n=xs(e);if(n.size===0)return [];let s=t.updatingSnapshots===true,l=[];for(let[i,d]of n){let o=r.get(i);!o&&!ws(d)||l.push({name:o?.name??i,status:o?.status??ks(d,s,t.failureMessage),source:o?"toMatchVisualBaseline":"toHaveScreenshot",...d.expected?{expectedPath:d.expected}:{},...d.actual?{actualPath:d.actual}:{},...d.diff?{diffPath:d.diff}:{},...d.previous?{previousPath:d.previous}:{},...a.has(i)?{dom:a.get(i)}:{}});}return Cs(l,n,t.failureMessage)}function Cs(e,t,r){let a=jr(r);if(a.diffPixels===void 0&&a.actualWidth===void 0)return [...e];let n=e.filter(l=>l.status==="failed").map(l=>ea(l,t)),s=Ss(n,a.snapshotName);return s===null?[...e]:e.map(l=>ea(l,t)===s?{...l,...a.diffPixels!==void 0?{diffPixels:a.diffPixels}:{},...a.actualWidth!==void 0?{actualWidth:a.actualWidth}:{},...a.actualHeight!==void 0?{actualHeight:a.actualHeight}:{}}:l)}function ea(e,t){if(t.has(e.name))return e.name;for(let[r,a]of t)if(a.actual===e.actualPath&&a.expected===e.expectedPath&&a.diff===e.diffPath)return r;return e.name}var Ts=new RegExp("[\x1B\x9B][[\\]()#;?]*(?:\\d{1,4}(?:;\\d{0,4})*)?[0-9A-PR-TZcf-nqry=><]","g");function ke(e){return !e||e.indexOf("\x1B")===-1&&e.indexOf("\x9B")===-1?e:e.replace(Ts,"")}function ra(e){return e==null?null:ke(e)}var Rs=/^\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}(-\d+)?$/,As="1.0",xt="visual";function ia(e){let t=path.extname(e).toLowerCase();return [".png",".jpg",".jpeg",".gif",".bmp",".webp"].includes(t)?"screenshot":[".webm",".mp4",".avi",".mov"].includes(t)?"video":t===".zip"&&path.basename(e,t)==="trace"?"trace":"other"}function _s(e){if(e.endsWith(".dom.json"))return "dom";let t=path.basename(e,path.extname(e));return t==="baseline"?"baseline":t==="diff"?"diff":ia(e)}function Is(e,t,r){let a=[];try{for(let n of fs$1.readdirSync(e,{withFileTypes:!0})){if(!n.isFile())continue;let s=fs$1.statSync(path.join(e,n.name));a.push({name:`${xt}/${r}/${n.name}`,type:_s(n.name),relativePath:`${t}/${xt}/${r}/${n.name}`,sizeBytes:s.size});}}catch{}return a}function oa(e,t,r){let a=[],n=`artifacts/${t}/${r}`;try{let s=fs$1.readdirSync(e,{withFileTypes:!0});for(let l of s){if(l.isDirectory()&&l.name===xt){let o=path.join(e,l.name);for(let c of fs$1.readdirSync(o,{withFileTypes:!0}))c.isDirectory()&&a.push(...Is(path.join(o,c.name),n,c.name));continue}if(!l.isFile())continue;let i=path.join(e,l.name),d=fs$1.statSync(i);a.push({name:l.name,type:ia(l.name),relativePath:`${n}/${l.name}`,sizeBytes:d.size});}}catch{}return {testName:r,files:a}}function Ms(e,t){let r=path.join(e,t),a=[],n=0;try{let l=fs$1.readdirSync(r,{withFileTypes:!0});for(let i of l){if(!i.isDirectory())continue;let d=oa(path.join(r,i.name),t,i.name);a.push(d);for(let o of d.files)n+=o.sizeBytes;}}catch{}let s=t.replace(/^(\d{4}-\d{2}-\d{2})T(\d{2})-(\d{2})-(\d{2})/,"$1T$2:$3:$4").replace(/-\d+$/,"");return a.sort((l,i)=>l.testName.localeCompare(i.testName)),{folderName:t,timestamp:s,totalSizeBytes:n,testCount:a.length,tests:a,isCurrentRun:false}}function la(e,t){let r=path.join(e,"artifacts"),a=[],n=[],s=0;try{let i=fs$1.readdirSync(r,{withFileTypes:!0});for(let d of i)if(d.isDirectory())if(Rs.test(d.name)){let o=Ms(r,d.name);a.push({...o,isCurrentRun:d.name===t});}else {let o=oa(path.join(r,d.name),d.name,d.name);n.push(o);for(let c of o.files)s+=c.sizeBytes;}}catch{}a.sort((i,d)=>d.timestamp.localeCompare(i.timestamp)),n.length>0&&(n.sort((i,d)=>i.testName.localeCompare(d.testName)),a.push({folderName:"__legacy__",timestamp:"1970-01-01T00:00:00",totalSizeBytes:s,testCount:n.length,tests:n,isCurrentRun:false}));let l=a.reduce((i,d)=>i+d.totalSizeBytes,0);return {schemaVersion:As,generatedAt:new Date().toISOString(),artifactBaseDir:"artifacts",totalSizeBytes:l,runs:a,serverPort:null}}var Fs=256;function Bs(e){let t=path.resolve(e);for(let r=0;r<Fs;r++){if(fs$1.existsSync(path.join(t,".git")))return t;let a=path.dirname(t);if(a===t)return null;t=a;}return null}function ua(e){try{let t=Bs(e);if(!t)return;let r=path.join(t,".gitignore"),a=path.relative(t,e).replace(/\\/g,"/"),n=a.endsWith("/")?a:`${a}/`;if(fs$1.existsSync(r)&&fs$1.readFileSync(r,"utf-8").split(`
4254
+ `).map(d=>d.trim()).some(d=>d===n||d===a))return;let s=`
2552
4255
  # TestRelic test artifacts
2553
- ${a}
2554
- `;fs$1.appendFileSync(r,s,"utf-8");}catch{}}var Sr=Symbol("testrelic.timeout"),Cr=3e4,Ye=12e4,Ze=6e4,Rr=15e3;async function H(t,e,r){let n,a=new Promise(s=>{n=setTimeout(()=>s(Sr),e),typeof n?.unref=="function"&&n.unref();});try{let s=await Promise.race([t(),a]);if(s===Sr){process.stderr.write(`[testrelic] ${r} timed out after ${Math.round(e/1e3)}s \u2014 continuing without it. Your tests are unaffected.
4256
+ ${n}
4257
+ `;fs$1.appendFileSync(r,s,"utf-8");}catch{}}var pa=Symbol("testrelic.timeout"),fa=3e4,wt=12e4,kt=6e4,ga=15e3;async function J(e,t,r){let a,n=new Promise(s=>{a=setTimeout(()=>s(pa),t),typeof a?.unref=="function"&&a.unref();});try{let s=await Promise.race([e(),n]);if(s===pa){process.stderr.write(`[testrelic] ${r} timed out after ${Math.round(t/1e3)}s \u2014 continuing without it. Your tests are unaffected.
2555
4258
  `);return}return s}catch(s){process.stderr.write(`[testrelic] ${r} failed \u2014 continuing without it. Your tests are unaffected. (${s instanceof Error?s.message:String(s)})
2556
- `);return}finally{n&&clearTimeout(n);}}function Ir(t,e){let r=[];for(let n of t){let a=Ss(n),s=Cs(n),l=Rs(n,a,s,e),i=As(n,a,s);if(l.length===0&&i.length===0){r.push({type:"navigation",url:"about:blank",timestamp:n.startedAt,durationOnUrl:n.duration,navigationType:"dummy",domContentLoadedAt:null,networkIdleAt:null,networkStats:null,specFile:n.specFile,test:a,tests:[s],_testTitle:n.title});continue}r.push(...l,...i);}return r.sort(_s),Is(r,t),r.map((n,a)=>n.type==="navigation"?{...{index:a,type:"navigation",url:n.url,timestamp:n.timestamp,durationOnUrl:n.durationOnUrl??0,navigationType:n.navigationType,domContentLoadedAt:n.domContentLoadedAt??null,networkIdleAt:n.networkIdleAt??null,networkStats:n.networkStats??null,specFile:n.specFile,test:n.test},tests:n.tests}:{...{index:a,type:"api_call",callId:n.callId,method:n.method,url:n.url,timestamp:n.timestamp,responseTime:n.responseTime??null,request:n.request,response:n.response??null,...n.error?{error:n.error}:{},assertions:n.assertions??[],specFile:n.specFile,test:n.test},tests:n.tests})}function Ss(t){return {title:t.title,fullTitle:t.titlePath,status:t.status,duration:t.duration,retries:t.retryCount,retry:t.retry,tags:t.tags,failure:t.failure}}function Cs(t){return {title:t.title,status:t.status,duration:t.duration,startedAt:t.startedAt,completedAt:t.completedAt,retryCount:t.retryCount,tags:t.tags,failure:t.failure,testId:t.testId,filePath:t.filePath,suiteName:t.suiteName,testType:t.testType,isFlaky:t.isFlaky,retryStatus:t.retryStatus,expectedStatus:t.expectedStatus,actualStatus:t.actualStatus,artifacts:t.artifacts,networkRequests:t.networkRequests,apiCalls:t.apiCalls,apiAssertions:t.apiAssertions,actions:t.actions,consoleLogs:t.consoleLogs}}function Rs(t,e,r,n){let a=[];for(let s of t.navigations)n.navigationTypes!==null&&!n.navigationTypes.includes(s.navigationType)||a.push({type:"navigation",url:s.url,timestamp:s.timestamp,durationOnUrl:0,navigationType:s.navigationType,domContentLoadedAt:s.domContentLoadedAt??null,networkIdleAt:s.networkIdleAt??null,networkStats:s.networkStats??null,specFile:t.specFile,test:e,tests:[r],_testTitle:t.title});return a}function As(t,e,r){if(!t.apiCalls||t.apiCalls.length===0)return [];let n=t.apiAssertions??[];return t.apiCalls.map(a=>{let s=n.filter(i=>i.callId===a.id).map(i=>({type:i.type,expected:i.expected,actual:i.actual,status:i.status,location:i.location,...i.expression!==void 0?{expression:i.expression}:{}})),l=null;return a.responseStatusCode!==null&&a.responseStatusText!==null&&(l={statusCode:a.responseStatusCode,statusText:a.responseStatusText,headers:a.responseHeaders,body:_r(a.responseBody)}),{type:"api_call",callId:a.id,method:a.method,url:a.url,timestamp:a.timestamp,responseTime:a.error?null:a.responseTimeMs,request:{headers:a.requestHeaders,body:_r(a.requestBody)},response:l,...a.error?{error:a.error}:{},assertions:s,specFile:t.specFile,test:e,tests:[r],_testTitle:t.title}})}function _s(t,e){let r=new Date(t.timestamp).getTime(),n=new Date(e.timestamp).getTime();if(r!==n)return r-n;let a={navigation:0,api_call:1},s=a[t.type],l=a[e.type];return s!==l?s-l:t.type==="api_call"&&e.type==="api_call"&&t.callId&&e.callId?Ar(t.callId)-Ar(e.callId):0}function Ar(t){let e=t.match(/(\d+)$/);return e?parseInt(e[1],10):0}function Is(t,e){for(let r=0;r<t.length;r++){let n=t[r];if(n.type!=="navigation")continue;let a=new Date(n.timestamp).getTime(),s=null;for(let l=r+1;l<t.length;l++)if(t[l]._testTitle===n._testTitle){s=new Date(t[l].timestamp).getTime();break}if(s!==null)n.durationOnUrl=Math.max(0,s-a);else {let l=e.find(i=>i.title===n._testTitle);if(l){let i=new Date(l.completedAt).getTime();n.durationOnUrl=Math.max(0,i-a);}}}}function _r(t){if(t==null)return null;try{return JSON.parse(t)}catch{return t}}function z(t,e){let r=t.length,n=Math.min(Math.ceil(e/100*r)-1,r-1);return t[Math.max(0,n)]}function Qe(t){try{let e=new URL(t);return e.origin+e.pathname}catch{return t}}function Ms(t){return t==null?"error":t>=200&&t<300?"2xx":t>=300&&t<400?"3xx":t>=400&&t<500?"4xx":t>=500&&t<600?"5xx":"error"}function Mr(t,e){let r=0,n=0,a=0,s=0,l=0;for(let b of t)switch(b.status){case "passed":r++;break;case "failed":n++;break;case "flaky":a++;break;case "skipped":s++;break;case "timedout":l++;break}let i=[];for(let b of t)b.apiCalls&&i.push(...b.apiCalls);let d=i.length,o=new Set,c={},g={"2xx":0,"3xx":0,"4xx":0,"5xx":0,error:0},m=[];for(let b of i)o.add(Qe(b.url)),c[b.method]=(c[b.method]??0)+1,g[Ms(b.responseStatusCode)]+=1,m.push(b.responseTimeMs);let h=null;if(m.length>0){let b=[...m].sort((A,$)=>A-$),T=b.reduce((A,$)=>A+$,0);h={avg:Math.round(T/b.length),min:b[0],max:b[b.length-1],p50:z(b,50),p95:z(b,95),p99:z(b,99)};}let u=0,p=0,f=0;for(let b of t)if(b.apiAssertions)for(let T of b.apiAssertions)u++,T.status==="passed"?p++:f++;let v=0,w=new Set;for(let b of t){v+=b.navigations.length;for(let T of b.navigations)w.add(T.url);}let y=0,k={};function C(b){for(let T of b)y++,k[T.category]=(k[T.category]??0)+1,T.children.length>0&&C(T.children);}for(let b of t)b.actions&&C(b.actions);return {total:t.length,passed:r,failed:n,flaky:a,skipped:s,timedout:l,totalApiCalls:d,uniqueApiUrls:o.size,apiCallsByMethod:c,apiCallsByStatusRange:g,apiResponseTime:h,totalAssertions:u,passedAssertions:p,failedAssertions:f,totalNavigations:v,uniqueNavigationUrls:w.size,totalTimelineSteps:e,totalActionSteps:y,actionStepsByCategory:k}}function Es(t){let e=56-t.length;return e>0?t+" ".repeat(e):t}function I(t){return `\u2502 ${Es(t)} \u2502
2557
- `}function Xe(t,e,r,n){if(n||t.total===0)return;let a=`\u250C${"\u2500".repeat(58)}\u2510
4259
+ `);return}finally{a&&clearTimeout(a);}}function va(e,t){let r=[];for(let a of e){let n=Os(a),s=Us(a),l=zs(a,n,s,t),i=Hs(a,n,s);if(l.length===0&&i.length===0){r.push({type:"navigation",url:"about:blank",timestamp:a.startedAt,durationOnUrl:a.duration,navigationType:"dummy",domContentLoadedAt:null,networkIdleAt:null,networkStats:null,specFile:a.specFile,test:n,tests:[s],_testTitle:a.title});continue}r.push(...l,...i);}return r.sort(qs),$s(r,e),r.map((a,n)=>a.type==="navigation"?{...{index:n,type:"navigation",url:a.url,timestamp:a.timestamp,durationOnUrl:a.durationOnUrl??0,navigationType:a.navigationType,domContentLoadedAt:a.domContentLoadedAt??null,networkIdleAt:a.networkIdleAt??null,networkStats:a.networkStats??null,specFile:a.specFile,test:a.test},tests:a.tests}:{...{index:n,type:"api_call",callId:a.callId,method:a.method,url:a.url,timestamp:a.timestamp,responseTime:a.responseTime??null,request:a.request,response:a.response??null,...a.error?{error:a.error}:{},assertions:a.assertions??[],specFile:a.specFile,test:a.test},tests:a.tests})}function Os(e){return {title:e.title,fullTitle:e.titlePath,status:e.status,duration:e.duration,retries:e.retryCount,retry:e.retry,tags:e.tags,failure:e.failure}}function Us(e){return {title:e.title,status:e.status,duration:e.duration,startedAt:e.startedAt,completedAt:e.completedAt,retryCount:e.retryCount,tags:e.tags,failure:e.failure,testId:e.testId,filePath:e.filePath,suiteName:e.suiteName,testType:e.testType,isFlaky:e.isFlaky,retryStatus:e.retryStatus,expectedStatus:e.expectedStatus,actualStatus:e.actualStatus,artifacts:e.artifacts,networkRequests:e.networkRequests,apiCalls:e.apiCalls,apiAssertions:e.apiAssertions,actions:e.actions,consoleLogs:e.consoleLogs}}function zs(e,t,r,a){let n=[];for(let s of e.navigations)a.navigationTypes!==null&&!a.navigationTypes.includes(s.navigationType)||n.push({type:"navigation",url:s.url,timestamp:s.timestamp,durationOnUrl:0,navigationType:s.navigationType,domContentLoadedAt:s.domContentLoadedAt??null,networkIdleAt:s.networkIdleAt??null,networkStats:s.networkStats??null,specFile:e.specFile,test:t,tests:[r],_testTitle:e.title});return n}function Hs(e,t,r){if(!e.apiCalls||e.apiCalls.length===0)return [];let a=e.apiAssertions??[];return e.apiCalls.map(n=>{let s=a.filter(i=>i.callId===n.id).map(i=>({type:i.type,expected:i.expected,actual:i.actual,status:i.status,location:i.location,...i.expression!==void 0?{expression:i.expression}:{}})),l=null;return n.responseStatusCode!==null&&n.responseStatusText!==null&&(l={statusCode:n.responseStatusCode,statusText:n.responseStatusText,headers:n.responseHeaders,body:ma(n.responseBody)}),{type:"api_call",callId:n.id,method:n.method,url:n.url,timestamp:n.timestamp,responseTime:n.error?null:n.responseTimeMs,request:{headers:n.requestHeaders,body:ma(n.requestBody)},response:l,...n.error?{error:n.error}:{},assertions:s,specFile:e.specFile,test:t,tests:[r],_testTitle:e.title}})}function qs(e,t){let r=new Date(e.timestamp).getTime(),a=new Date(t.timestamp).getTime();if(r!==a)return r-a;let n={navigation:0,api_call:1},s=n[e.type],l=n[t.type];return s!==l?s-l:e.type==="api_call"&&t.type==="api_call"&&e.callId&&t.callId?ha(e.callId)-ha(t.callId):0}function ha(e){let t=e.match(/(\d+)$/);return t?parseInt(t[1],10):0}function $s(e,t){for(let r=0;r<e.length;r++){let a=e[r];if(a.type!=="navigation")continue;let n=new Date(a.timestamp).getTime(),s=null;for(let l=r+1;l<e.length;l++)if(e[l]._testTitle===a._testTitle){s=new Date(e[l].timestamp).getTime();break}if(s!==null)a.durationOnUrl=Math.max(0,s-n);else {let l=t.find(i=>i.title===a._testTitle);if(l){let i=new Date(l.completedAt).getTime();a.durationOnUrl=Math.max(0,i-n);}}}}function ma(e){if(e==null)return null;try{return JSON.parse(e)}catch{return e}}function Y(e,t){let r=e.length,a=Math.min(Math.ceil(t/100*r)-1,r-1);return e[Math.max(0,a)]}function St(e){try{let t=new URL(e);return t.origin+t.pathname}catch{return e}}function Vs(e){return e==null?"error":e>=200&&e<300?"2xx":e>=300&&e<400?"3xx":e>=400&&e<500?"4xx":e>=500&&e<600?"5xx":"error"}function ba(e,t){let r=0,a=0,n=0,s=0,l=0;for(let x of e)switch(x.status){case "passed":r++;break;case "failed":a++;break;case "flaky":n++;break;case "skipped":s++;break;case "timedout":l++;break}let i=[];for(let x of e)x.apiCalls&&i.push(...x.apiCalls);let d=i.length,o=new Set,c={},p={"2xx":0,"3xx":0,"4xx":0,"5xx":0,error:0},v=[];for(let x of i)o.add(St(x.url)),c[x.method]=(c[x.method]??0)+1,p[Vs(x.responseStatusCode)]+=1,v.push(x.responseTimeMs);let g=null;if(v.length>0){let x=[...v].sort((X,G)=>X-G),S=x.reduce((X,G)=>X+G,0);g={avg:Math.round(S/x.length),min:x[0],max:x[x.length-1],p50:Y(x,50),p95:Y(x,95),p99:Y(x,99)};}let f=0,m=0,u=0;for(let x of e)if(x.apiAssertions)for(let S of x.apiAssertions)f++,S.status==="passed"?m++:u++;let h=0,b=new Set;for(let x of e){h+=x.navigations.length;for(let S of x.navigations)b.add(S.url);}let y=0,w={};function C(x){for(let S of x)y++,w[S.category]=(w[S.category]??0)+1,S.children.length>0&&C(S.children);}for(let x of e)x.actions&&C(x.actions);let P=0,R=0,_=0,L=0;for(let x of e)if(x.visual)for(let S of x.visual)P++,S.status==="failed"?R++:(S.status==="new"||S.status==="updated")&&_++,L+=S.domChangeCount??0;return {total:e.length,passed:r,failed:a,flaky:n,skipped:s,timedout:l,totalApiCalls:d,uniqueApiUrls:o.size,apiCallsByMethod:c,apiCallsByStatusRange:p,apiResponseTime:g,totalAssertions:f,passedAssertions:m,failedAssertions:u,totalNavigations:h,uniqueNavigationUrls:b.size,totalTimelineSteps:t,totalActionSteps:y,actionStepsByCategory:w,visualChecks:P,visualFailures:R,visualNew:_,visualDomChanges:L}}function js(e){let t=56-e.length;return t>0?e+" ".repeat(t):e}function E(e){return `\u2502 ${js(e)} \u2502
4260
+ `}function Ct(e,t,r,a){if(a||e.total===0)return;let n=`\u250C${"\u2500".repeat(58)}\u2510
2558
4261
  `,s=`\u2514${"\u2500".repeat(58)}\u2518
2559
- `,l=I(""),i=a;i+=I("TestRelic AI - Playwright Test Report"),i+=l;let d=[];if(t.passed>0&&d.push(`${t.passed} \u2713`),t.failed>0&&d.push(`${t.failed} \u2717`),t.flaky>0&&d.push(`${t.flaky} \u26A0`),t.skipped>0&&d.push(`${t.skipped} skipped`),t.timedout>0&&d.push(`${t.timedout} timedout`),i+=I(`Tests: ${t.total} total (${d.join(" ")})`),t.totalNavigations>0&&(i+=I(`Navigations: ${t.totalNavigations} visits across ${t.uniqueNavigationUrls} unique URLs`)),t.totalApiCalls>0){i+=I(`API Calls: ${t.totalApiCalls} calls across ${t.uniqueApiUrls} unique endpoints`);let o=Object.entries(t.apiCallsByMethod).filter(([,g])=>g>0).map(([g,m])=>`${g}: ${m}`);o.length>0&&(i+=I(` ${o.join(" ")}`));let c=Object.entries(t.apiCallsByStatusRange).filter(([,g])=>g>0).map(([g,m])=>`${g}: ${m}`);c.length>0&&(i+=I(` ${c.join(" ")}`)),t.apiResponseTime&&(i+=I(` Avg response: ${t.apiResponseTime.avg}ms P95: ${t.apiResponseTime.p95}ms`));}if(t.totalAssertions>0&&(i+=I(`Assertions: ${t.totalAssertions} total (${t.passedAssertions} \u2713 ${t.failedAssertions} \u2717)`)),t.totalActionSteps>0){let o=[],c=t.actionStepsByCategory;c.ui_action&&o.push(`${c.ui_action} UI`),c.assertion&&o.push(`${c.assertion} assertions`),c.custom_step&&o.push(`${c.custom_step} custom`);let g=o.length>0?` (${o.join(" ")})`:"";i+=I(`Actions: ${t.totalActionSteps} steps${g}`);}i+=I(`Report: ${r}`),i+=I(`Data: ${e}`),i+=s,i+=`For more information visit us at https://docs.testrelic.ai
2560
- `,process.stderr.write(i);}var Ls=new Set(["pw:api","expect","test.step"]);function Ps(t){return t==="expect"?"assertion":t==="test.step"?"custom_step":"ui_action"}function et(t,e,r){if(Ls.has(t.category)){let n=[];for(let s of t.steps)et(s,e,n);let a=t.startTime.getTime()-e.getTime();r.push({title:t.title,category:Ps(t.category),timestamp:t.startTime.toISOString(),duration:t.duration,videoOffset:a>=0?a/1e3:null,status:t.error?"failed":"passed",error:t.error?.message??null,children:n});}else for(let n of t.steps)et(n,e,r);}function Er(t,e){let r=[];for(let n of t)et(n,e,r);return r}var Ds=new Set(["localhost","127.0.0.1","0.0.0.0"]);function Pr(t){let e=new URL(t);if(e.protocol!=="https:"&&!(e.protocol==="http:"&&Ds.has(e.hostname)))throw core.createError(core.ErrorCode.CLOUD_CONFIG_INVALID,`HTTPS is required for cloud communication. Got: ${e.protocol}//${e.hostname}`)}var Bs=3e3;async function tt(t){let e=new AbortController,r=setTimeout(()=>e.abort(),Bs);try{return (await fetch(`${t}/health`,{method:"GET",signal:e.signal})).ok}catch{return false}finally{clearTimeout(r);}}async function Nr(t){return new Promise(e=>setTimeout(e,t))}async function Lr(t){try{let r=(await t.json()).error;if(r&&typeof r.message=="string"){let n=r.details;if(Array.isArray(n)&&n.length>0){let a=n.map(s=>`${s.field}: ${s.message}`).join(", ");return `${r.message} (${a})`}return r.message}}catch{}return `HTTP ${t.status}`}async function rt(t,e,r){let n=new AbortController,a=setTimeout(()=>n.abort(),r);try{let s=await fetch(`${t}/sdk/auth/token`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({apiKey:e}),signal:n.signal});if(s.ok){let i=await s.json();return {accessToken:i.accessToken,refreshToken:i.refreshToken,expiresIn:i.expiresIn,orgId:i.orgId,orgName:i.orgName,userId:i.userId,userName:i.userName}}if(s.status===429){let i=s.headers.get("Retry-After"),d=i?parseInt(i,10)*1e3:5e3;if(!isNaN(d)&&d>0){await Nr(d);let o=await fetch(`${t}/sdk/auth/token`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({apiKey:e}),signal:n.signal});if(o.ok){let c=await o.json();return {accessToken:c.accessToken,refreshToken:c.refreshToken,expiresIn:c.expiresIn,orgId:c.orgId,orgName:c.orgName,userId:c.userId,userName:c.userName}}}return {code:"rate_limited",message:"Rate limited during token exchange.",statusCode:429}}return s.status===400?{code:"validation_error",message:await Lr(s),statusCode:400}:s.status===401?{code:"invalid_key",message:"API key is invalid or has been revoked.",statusCode:401}:s.status===403?{code:"expired_key",message:"API key has expired.",statusCode:403}:{code:"server_error",message:await Lr(s),statusCode:s.status}}catch(s){return s instanceof DOMException&&s.name==="AbortError"?{code:"timeout",message:"Token exchange timed out.",statusCode:null}:{code:"network_error",message:"Failed to reach cloud for token exchange.",statusCode:null}}finally{clearTimeout(a);}}function nt(t){return "code"in t&&typeof t.code=="string"&&["invalid_key","expired_key","validation_error","rate_limited","server_error","network_error","timeout"].includes(t.code)}async function Fr(t,e,r){let n=new AbortController,a=setTimeout(()=>n.abort(),r);try{let s=await fetch(`${t}/sdk/auth/refresh`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({refreshToken:e}),signal:n.signal});if(s.status===429){let i=s.headers.get("Retry-After"),d=i?parseInt(i,10)*1e3:5e3;!isNaN(d)&&d>0&&(await Nr(d),s=await fetch(`${t}/sdk/auth/refresh`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({refreshToken:e}),signal:n.signal}));}if(!s.ok)return null;let l=await s.json();return {accessToken:l.accessToken,refreshToken:l.refreshToken,expiresIn:l.expiresIn}}catch{return null}finally{clearTimeout(a);}}async function Dr(t,e,r,n,a,s){let l=new AbortController,i=setTimeout(()=>l.abort(),a);try{let d={gitId:r,displayName:n};s&&(d.branch=s);let o=await fetch(`${t}/repos/resolve`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${e}`},body:JSON.stringify(d),signal:l.signal});if(!o.ok)return null;let c=await o.json();return {repoId:c.repoId,displayName:c.displayName}}catch{return null}finally{clearTimeout(i);}}var zs=5e3;function F(t,e){try{return child_process.execSync(t,{cwd:e,timeout:zs,encoding:"utf-8",stdio:["pipe","pipe","pipe"]}).trim()||null}catch{return null}}function Br(t){let e=F("git rev-parse --abbrev-ref HEAD",t),r=F("git rev-parse --short HEAD",t),n=F("git log -1 --pretty=%s",t),a=F("git log -1 --format=%an",t)??F("git config user.name",t),s=$s(t),l=s?ne(s):null;return {branch:e,commitSha:r,commitMessage:n,commitAuthor:a,remoteUrl:l}}function $s(t){let e=F("git remote get-url origin",t);if(e)return e;let r=F("git remote",t);if(!r)return null;let n=r.split(`
2561
- `)[0]?.trim();return n?F(`git remote get-url ${n}`,t):null}function ne(t){let e=t.trim();return e=e.replace(/^[a-z+]+:\/\//,""),e=e.replace(/^[^@]+@/,""),e=e.replace(/:(?!\d)/,"/"),e=e.replace(/\.git$/,""),e=e.replace(/\/+$/,""),e=e.replace(/^[^@/]+@/,""),e.toLowerCase()}function Or(t){let e=t.split("/").filter(Boolean);return e[e.length-1]??t}function st(t){try{let e=fs$1.readFileSync(path.join(t,"package.json"),"utf-8"),r=JSON.parse(e);return typeof r.name=="string"&&r.name.length>0?r.name:null}catch{return null}}function se(t){let e=st(t);return e||`local/${path.basename(t)}`}var js=new Set(["smoke","regression","nightly","ci"]);function ge(){let t=process.env.TESTRELIC_RUN_TYPE?.trim().toLowerCase();if(t&&js.has(t))return t}async function D(t,e,r){let n=new AbortController,a=setTimeout(()=>n.abort(),Math.max(1,r));try{return await fetch(t,{...e,signal:n.signal})}finally{clearTimeout(a);}}function Ur(t,e){let r=e+Math.ceil(Math.max(0,t)/1e3);return Math.min(6e5,Math.max(3e4,r))}var he=class{constructor(e=12e4,r=Date.now()){this.deadline=r+Math.max(0,e);}remaining(e=Date.now()){return Math.max(0,this.deadline-e)}expired(e=Date.now()){return this.remaining(e)===0}slice(e,r=Date.now()){return Math.min(e,this.remaining(r))}};var Ws=1048576,me=[1e3,3e3,9e3],ve=3,Hr=3e4,Ks=90*1048576,Js=new Set(["e2e","mobile","unit"]);function it(t){if(t&&t.length>0&&t.every(a=>a.testType==="api")){let a=new Map;for(let i of t){let d=i.apiProtocol??"rest";a.set(d,(a.get(d)??0)+1);}let s="rest",l=0;for(let[i,d]of a)d>l&&(l=d,s=i);return {testType:"api",apiProtocol:s,metadata:{testType:"api",primaryProtocol:s}}}let e=new Map;for(let a of t??[])Js.has(a.testType)&&e.set(a.testType,(e.get(a.testType)??0)+1);let r=null,n=0;for(let[a,s]of e)s>n&&(n=s,r=a);return {testType:r??"e2e"}}function ot(t,e,r,n,a,s){let l=s?.cloudPlatform?s.cloudPlatform:n?.provider&&n.provider!=="unknown"?n.provider:"local",i=ge(),d=it(a),o=d.testType==="api"?d:null,c=o&&a?a.map(m=>m.apiProtocol?m:{...m,apiProtocol:o.apiProtocol}):a;return {runId:t.testRunId,repoGitId:e,startedAt:t.startedAt,summary:t.summary,timeline:t.timeline,environment:l,...i?{runType:i}:{},...o?{testType:o.testType,apiProtocol:o.apiProtocol,metadata:o.metadata}:{testType:d.testType},...c&&c.length>0?{tests:c}:{},...r?.branch?{branch:r.branch}:{},...r?.commitSha?{commit:r.commitSha}:{},...r?.commitMessage?{commitMessage:r.commitMessage}:{},...r?.commitAuthor?{commitAuthor:r.commitAuthor}:{},...t.completedAt?{finishedAt:t.completedAt}:{},...t.totalDuration?{duration:t.totalDuration}:{},...n?.provider?{ciProvider:n.provider}:{},...n?.runUrl?{ciRunUrl:n.runUrl}:{},...s?.cloudPlatform?{cloudPlatform:s.cloudPlatform}:{},...s?.cloudBuildId?{cloudBuildId:s.cloudBuildId}:{},...s?.cloudSessionId?{cloudSessionId:s.cloudSessionId}:{}}}function at(t){return typeof t=="string"?Buffer.byteLength(t,"utf-8"):0}function lt(t){return Array.isArray(t)?t.map(e=>{let r=e.statusCode??e.responseStatusCode??null;return {...e.id!=null?{id:e.id}:{},...e.protocol!=null?{protocol:e.protocol}:{},method:e.method??null,url:e.url??null,statusCode:r,statusText:e.statusText??e.responseStatusText??null,ok:typeof r=="number"?r<400:e.ok??null,durationMs:e.durationMs??e.responseTimeMs??null,bytesIn:typeof e.bytesIn=="number"?e.bytesIn:at(e.responseBody),bytesOut:typeof e.bytesOut=="number"?e.bytesOut:at(e.requestBody),requestHeaders:e.requestHeaders??null,responseHeaders:e.responseHeaders??null,requestBody:e.requestBody??null,responseBody:e.responseBody??null,requestCookies:e.requestCookies??null,responseCookies:e.responseCookies??null}}):[]}function dt(t){return Array.isArray(t)?t.map(e=>{let r=e.responseStatusCode??e.statusCode??0,n=e.timestamp??null;return {method:e.method??"GET",url:e.url??"",status:r,statusCode:r,type:"xhr",size:at(e.responseBody),duration:e.responseTimeMs??e.durationMs??0,responseTimeMs:e.responseTimeMs??e.durationMs??0,requestHeaders:e.requestHeaders??null,responseHeaders:e.responseHeaders??null,requestBody:e.requestBody??null,responseBody:e.responseBody??null,timestamp:n,startedAt:n}}):[]}function ct(t){if(!Array.isArray(t))return [];let e=r=>{if(r==null)return String(r);if(typeof r=="string")return r;try{return JSON.stringify(r)}catch{return String(r)}};return t.map(r=>{let n=r.status==="failed"?"failed":"passed",a=typeof r.expression=="string"&&r.expression.trim()?r.expression.trim():null,s=r.kind??r.type??"assertion",l=n==="failed"?`expected ${e(r.expected)}, received ${e(r.actual)}`:r.expected!==void 0?`expected ${e(r.expected)}`:null;return {...r.callId!=null?{callId:r.callId}:{},kind:s,description:a??r.description??`${s} assertion`,status:n,detail:l}})}async function ye(t){return new Promise(e=>setTimeout(e,t))}function Ys(t){let e={};for(let[r,n]of Object.entries(t))n!=null&&(e[r]=n);return e}async function zr(t,e,r,n=Hr,a){for(let s=0;s<ve;s++){if(a?.expired())return null;let l=a?a.slice(n):n;if(l<=0)return null;try{let i=await D(t,e,l);if(i.ok)return i;if(i.status===401&&r&&s===0){let d=await r();if(d){let o={...e,headers:{...e.headers,Authorization:`Bearer ${d}`}},c=await D(t,o,l);if(c.ok)return c}return null}if(i.status===429&&s<ve-1){let d=i.headers.get("Retry-After"),o=d?parseInt(d,10)*1e3:me[s];!isNaN(o)&&o>0?await ye(o):await ye(me[s]);continue}if(i.status>=500&&s<ve-1){await ye(me[s]);continue}return i}catch{if(s<ve-1){await ye(me[s]);continue}return null}}return null}var Zs=util.promisify(zlib.gzip);function qr(t,e){if(!Array.isArray(t.tests))return t;let r=t.tests.map(n=>{let{consoleLogs:a,networkRequests:s,apiCalls:l,...i}=n;return e?i:{...i,...l!==void 0?{apiCalls:l}:{}}});return {...t,tests:r}}var Qs=["consoleLogs","networkRequests","apiCalls","actions","navigations","protocolAssertions"];function Xs(t){if(!Array.isArray(t.tests))return t;let e=new Set(Qs),r=t.tests.map(n=>{let a={};for(let s in n)if(!e.has(s))if(s==="failure"&&n[s]&&typeof n[s]=="object"){let l=n[s];a.failure={message:l.message??null,line:l.line??null};}else a[s]=n[s];return a});return {...t,tests:r}}function ea(t){let e=new Set(["tests","timeline","unifiedTimeline","navigations"]),r={};for(let n in t)e.has(n)||(r[n]=t[n]);return r.tests=[],r}async function ut(t,e=Ks){let r={"Content-Type":"application/json"},n=JSON.stringify(t),a=t,s=Array.isArray(a?.tests)?a.tests.length:0,l=i=>Buffer.byteLength(i,"utf-8")>e;if(s>0&&l(n)){let i=Math.max(1,Math.round(e/1048576));process.stderr.write(`[testrelic] WARNING: batch payload exceeds ${i}MB \u2014 dropping consoleLogs/networkRequests from ${s} test(s) to avoid HTTP 413. Console/network detail will be missing in the cloud.
2562
- `),n=JSON.stringify(qr(a,false)),l(n)&&(process.stderr.write(`[testrelic] WARNING: batch still exceeds ${i}MB after dropping logs \u2014 also dropping apiCalls from ${s} test(s). API request/response detail will be missing in the cloud.
2563
- `),n=JSON.stringify(qr(a,true))),l(n)&&(process.stderr.write(`[testrelic] WARNING: batch still exceeds ${i}MB \u2014 shedding remaining per-test detail (actions/navigations/assertions) from ${s} test(s). Only test results + summary will reach the cloud.
2564
- `),n=JSON.stringify(Xs(a))),l(n)&&(process.stderr.write(`[testrelic] ERROR: batch still exceeds ${i}MB after shedding all per-test detail \u2014 uploading run summary only (${s} per-test result(s) omitted) to avoid losing the run to HTTP 413.
2565
- `),n=JSON.stringify(ea(a)));}return Buffer.byteLength(n,"utf-8")>Ws?(r["Content-Encoding"]="gzip",{body:await Zs(Buffer.from(n,"utf-8")),headers:r}):{body:n,headers:r}}async function $r(t,e,r,n,a,s){let l=`${t}/runs`,{body:i,headers:d}=await ut(r);d.Authorization=`Bearer ${e}`;let o=await zr(l,{method:"POST",headers:d,body:i},n,a,s);return o?.ok?{success:true,statusCode:o.status,error:null}:o?.status===409?{success:true,statusCode:409,error:null}:{success:false,reason:o?`Upload failed with status ${o.status}`:"Upload failed after retries",statusCode:o?.status??null,payload:r,targetEndpoint:l,method:"POST"}}async function jr(t,e,r,n){let a=`${t}/runs/init`;try{let s=await D(a,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${e}`},body:JSON.stringify(Ys(r))},n??Hr);return s.ok?{runId:(await s.json()).runId}:null}catch{return null}}function Vr(t,e,r,n){let a=`${t}/runs/${r}/tests`;fetch(a,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${e}`},body:JSON.stringify(n)}).catch(()=>{});}async function Gr(t,e,r,n){let a=`${t}/runs/${r}/finalize`;return (await zr(a,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${e}`},body:JSON.stringify(n)}))?.ok??false}function pt(t,e,r,n){let a=`${t}/runs/${r}/finalize`,s=new Date,l={finishedAt:s.toISOString(),duration:s.getTime()-new Date(n).getTime(),summary:{}};fetch(a,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${e}`},body:JSON.stringify(l)}).catch(()=>{});}var aa="1.0.0",Wr=10;function Kr(t){return t instanceof Error?t.message:String(t)}function ia(t){return t>=400&&t<500&&t!==408&&t!==429}function ae(t,e,r,n,a,s,l,i){try{fs$1.mkdirSync(t,{recursive:!0});let d=Date.now(),o=`${d}-${e}-${r}.json`,c=path.join(t,o),g={version:aa,queuedAt:new Date(d).toISOString(),reason:n,retryCount:0,targetEndpoint:a,method:s,payload:l,headers:i},m=c+".tmp";fs$1.writeFileSync(m,JSON.stringify(g,null,2),"utf-8"),fs$1.renameSync(m,c);}catch(d){d.code==="ENOSPC"?process.stderr.write(`\u26A0 TestRelic: Disk full \u2014 unable to queue upload data.
4262
+ `,l=E(""),i=n;i+=E("TestRelic AI - Playwright Test Report"),i+=l;let d=[];if(e.passed>0&&d.push(`${e.passed} \u2713`),e.failed>0&&d.push(`${e.failed} \u2717`),e.flaky>0&&d.push(`${e.flaky} \u26A0`),e.skipped>0&&d.push(`${e.skipped} skipped`),e.timedout>0&&d.push(`${e.timedout} timedout`),i+=E(`Tests: ${e.total} total (${d.join(" ")})`),e.totalNavigations>0&&(i+=E(`Navigations: ${e.totalNavigations} visits across ${e.uniqueNavigationUrls} unique URLs`)),e.totalApiCalls>0){i+=E(`API Calls: ${e.totalApiCalls} calls across ${e.uniqueApiUrls} unique endpoints`);let o=Object.entries(e.apiCallsByMethod).filter(([,p])=>p>0).map(([p,v])=>`${p}: ${v}`);o.length>0&&(i+=E(` ${o.join(" ")}`));let c=Object.entries(e.apiCallsByStatusRange).filter(([,p])=>p>0).map(([p,v])=>`${p}: ${v}`);c.length>0&&(i+=E(` ${c.join(" ")}`)),e.apiResponseTime&&(i+=E(` Avg response: ${e.apiResponseTime.avg}ms P95: ${e.apiResponseTime.p95}ms`));}if(e.totalAssertions>0&&(i+=E(`Assertions: ${e.totalAssertions} total (${e.passedAssertions} \u2713 ${e.failedAssertions} \u2717)`)),e.totalActionSteps>0){let o=[],c=e.actionStepsByCategory;c.ui_action&&o.push(`${c.ui_action} UI`),c.assertion&&o.push(`${c.assertion} assertions`),c.custom_step&&o.push(`${c.custom_step} custom`);let p=o.length>0?` (${o.join(" ")})`:"";i+=E(`Actions: ${e.totalActionSteps} steps${p}`);}i+=E(`Report: ${r}`),i+=E(`Data: ${t}`),i+=s,i+=`For more information visit us at https://docs.testrelic.ai
4263
+ `,process.stderr.write(i);}var Gs=new Set(["pw:api","expect","test.step"]);function Ws(e){return e==="expect"?"assertion":e==="test.step"?"custom_step":"ui_action"}function Tt(e,t,r){if(Gs.has(e.category)){let a=[];for(let s of e.steps)Tt(s,t,a);let n=e.startTime.getTime()-t.getTime();r.push({title:e.title,category:Ws(e.category),timestamp:e.startTime.toISOString(),duration:e.duration,videoOffset:n>=0?n/1e3:null,status:e.error?"failed":"passed",error:ra(e.error?.message),children:a});}else for(let a of e.steps)Tt(a,t,r);}function ya(e,t){let r=[];for(let a of e)Tt(a,t,r);return r}var Ys=new Set(["localhost","127.0.0.1","0.0.0.0"]);function wa(e){let t=new URL(e);if(t.protocol!=="https:"&&!(t.protocol==="http:"&&Ys.has(t.hostname)))throw core.createError(core.ErrorCode.CLOUD_CONFIG_INVALID,`HTTPS is required for cloud communication. Got: ${t.protocol}//${t.hostname}`)}var Xs=3e3;async function Rt(e){let t=new AbortController,r=setTimeout(()=>t.abort(),Xs);try{return (await fetch(`${e}/health`,{method:"GET",signal:t.signal})).ok}catch{return false}finally{clearTimeout(r);}}async function ka(e){return new Promise(t=>setTimeout(t,e))}async function xa(e){try{let r=(await e.json()).error;if(r&&typeof r.message=="string"){let a=r.details;if(Array.isArray(a)&&a.length>0){let n=a.map(s=>`${s.field}: ${s.message}`).join(", ");return `${r.message} (${n})`}return r.message}}catch{}return `HTTP ${e.status}`}async function At(e,t,r){let a=new AbortController,n=setTimeout(()=>a.abort(),r);try{let s=await fetch(`${e}/sdk/auth/token`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({apiKey:t}),signal:a.signal});if(s.ok){let i=await s.json();return {accessToken:i.accessToken,refreshToken:i.refreshToken,expiresIn:i.expiresIn,orgId:i.orgId,orgName:i.orgName,userId:i.userId,userName:i.userName}}if(s.status===429){let i=s.headers.get("Retry-After"),d=i?parseInt(i,10)*1e3:5e3;if(!isNaN(d)&&d>0){await ka(d);let o=await fetch(`${e}/sdk/auth/token`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({apiKey:t}),signal:a.signal});if(o.ok){let c=await o.json();return {accessToken:c.accessToken,refreshToken:c.refreshToken,expiresIn:c.expiresIn,orgId:c.orgId,orgName:c.orgName,userId:c.userId,userName:c.userName}}}return {code:"rate_limited",message:"Rate limited during token exchange.",statusCode:429}}return s.status===400?{code:"validation_error",message:await xa(s),statusCode:400}:s.status===401?{code:"invalid_key",message:"API key is invalid or has been revoked.",statusCode:401}:s.status===403?{code:"expired_key",message:"API key has expired.",statusCode:403}:{code:"server_error",message:await xa(s),statusCode:s.status}}catch(s){return s instanceof DOMException&&s.name==="AbortError"?{code:"timeout",message:"Token exchange timed out.",statusCode:null}:{code:"network_error",message:"Failed to reach cloud for token exchange.",statusCode:null}}finally{clearTimeout(n);}}function _t(e){return "code"in e&&typeof e.code=="string"&&["invalid_key","expired_key","validation_error","rate_limited","server_error","network_error","timeout"].includes(e.code)}async function Sa(e,t,r){let a=new AbortController,n=setTimeout(()=>a.abort(),r);try{let s=await fetch(`${e}/sdk/auth/refresh`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({refreshToken:t}),signal:a.signal});if(s.status===429){let i=s.headers.get("Retry-After"),d=i?parseInt(i,10)*1e3:5e3;!isNaN(d)&&d>0&&(await ka(d),s=await fetch(`${e}/sdk/auth/refresh`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({refreshToken:t}),signal:a.signal}));}if(!s.ok)return null;let l=await s.json();return {accessToken:l.accessToken,refreshToken:l.refreshToken,expiresIn:l.expiresIn}}catch{return null}finally{clearTimeout(n);}}async function Ca(e,t,r,a,n,s){let l=new AbortController,i=setTimeout(()=>l.abort(),n);try{let d={gitId:r,displayName:a};s&&(d.branch=s);let o=await fetch(`${e}/repos/resolve`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${t}`},body:JSON.stringify(d),signal:l.signal});if(!o.ok)return null;let c=await o.json();return {repoId:c.repoId,displayName:c.displayName}}catch{return null}finally{clearTimeout(i);}}var ri=5e3;function $(e,t){try{return child_process.execSync(e,{cwd:t,timeout:ri,encoding:"utf-8",stdio:["pipe","pipe","pipe"]}).trim()||null}catch{return null}}function Ta(e){let t=$("git rev-parse --abbrev-ref HEAD",e),r=$("git rev-parse --short HEAD",e),a=$("git log -1 --pretty=%s",e),n=$("git log -1 --format=%an",e)??$("git config user.name",e),s=ai(e),l=s?ce(s):null;return {branch:t,commitSha:r,commitMessage:a,commitAuthor:n,remoteUrl:l}}function ai(e){let t=$("git remote get-url origin",e);if(t)return t;let r=$("git remote",e);if(!r)return null;let a=r.split(`
4264
+ `)[0]?.trim();return a?$(`git remote get-url ${a}`,e):null}function ce(e){let t=e.trim();return t=t.replace(/^[a-z+]+:\/\//,""),t=t.replace(/^[^@]+@/,""),t=t.replace(/:(?!\d)/,"/"),t=t.replace(/\.git$/,""),t=t.replace(/\/+$/,""),t=t.replace(/^[^@/]+@/,""),t.toLowerCase()}function Ra(e){let t=e.split("/").filter(Boolean);return t[t.length-1]??e}function It(e){try{let t=fs$1.readFileSync(path.join(e,"package.json"),"utf-8"),r=JSON.parse(t);return typeof r.name=="string"&&r.name.length>0?r.name:null}catch{return null}}function ue(e){let t=It(e);return t||`local/${path.basename(e)}`}var ni=new Set(["smoke","regression","nightly","ci"]);function Se(){let e=process.env.TESTRELIC_RUN_TYPE?.trim().toLowerCase();if(e&&ni.has(e))return e}async function V(e,t,r){let a=new AbortController,n=setTimeout(()=>a.abort(),Math.max(1,r));try{return await fetch(e,{...t,signal:a.signal})}finally{clearTimeout(n);}}function Aa(e,t){let r=t+Math.ceil(Math.max(0,e)/1e3);return Math.min(6e5,Math.max(3e4,r))}var Ce=class{constructor(t=12e4,r=Date.now()){this.deadline=r+Math.max(0,t);}remaining(t=Date.now()){return Math.max(0,this.deadline-t)}expired(t=Date.now()){return this.remaining(t)===0}slice(t,r=Date.now()){return Math.min(t,this.remaining(r))}};var oi=1048576,Te=[1e3,3e3,9e3],Re=3,Ia=3e4,li=90*1048576,di=new Set(["e2e","mobile","unit"]);function Et(e){if(e&&e.length>0&&e.every(n=>n.testType==="api")){let n=new Map;for(let i of e){let d=i.apiProtocol??"rest";n.set(d,(n.get(d)??0)+1);}let s="rest",l=0;for(let[i,d]of n)d>l&&(l=d,s=i);return {testType:"api",apiProtocol:s,metadata:{testType:"api",primaryProtocol:s}}}let t=new Map;for(let n of e??[])di.has(n.testType)&&t.set(n.testType,(t.get(n.testType)??0)+1);let r=null,a=0;for(let[n,s]of t)s>a&&(a=s,r=n);return {testType:r??"e2e"}}function Pt(e,t,r,a,n,s){let l=s?.cloudPlatform?s.cloudPlatform:a?.provider&&a.provider!=="unknown"?a.provider:"local",i=Se(),d=Et(n),o=d.testType==="api"?d:null,c=o&&n?n.map(v=>v.apiProtocol?v:{...v,apiProtocol:o.apiProtocol}):n;return {runId:e.testRunId,repoGitId:t,startedAt:e.startedAt,summary:e.summary,timeline:e.timeline,environment:l,...i?{runType:i}:{},...o?{testType:o.testType,apiProtocol:o.apiProtocol,metadata:o.metadata}:{testType:d.testType},...c&&c.length>0?{tests:c}:{},...r?.branch?{branch:r.branch}:{},...r?.commitSha?{commit:r.commitSha}:{},...r?.commitMessage?{commitMessage:r.commitMessage}:{},...r?.commitAuthor?{commitAuthor:r.commitAuthor}:{},...e.completedAt?{finishedAt:e.completedAt}:{},...e.totalDuration?{duration:e.totalDuration}:{},...a?.provider?{ciProvider:a.provider}:{},...a?.runUrl?{ciRunUrl:a.runUrl}:{},...s?.cloudPlatform?{cloudPlatform:s.cloudPlatform}:{},...s?.cloudBuildId?{cloudBuildId:s.cloudBuildId}:{},...s?.cloudSessionId?{cloudSessionId:s.cloudSessionId}:{}}}function Mt(e){return typeof e=="string"?Buffer.byteLength(e,"utf-8"):0}function Lt(e){return Array.isArray(e)?e.map(t=>{let r=t.statusCode??t.responseStatusCode??null;return {...t.id!=null?{id:t.id}:{},...t.protocol!=null?{protocol:t.protocol}:{},method:t.method??null,url:t.url??null,statusCode:r,statusText:t.statusText??t.responseStatusText??null,ok:typeof r=="number"?r<400:t.ok??null,durationMs:t.durationMs??t.responseTimeMs??null,bytesIn:typeof t.bytesIn=="number"?t.bytesIn:Mt(t.responseBody),bytesOut:typeof t.bytesOut=="number"?t.bytesOut:Mt(t.requestBody),requestHeaders:t.requestHeaders??null,responseHeaders:t.responseHeaders??null,requestBody:t.requestBody??null,responseBody:t.responseBody??null,requestCookies:t.requestCookies??null,responseCookies:t.responseCookies??null}}):[]}function Dt(e){return Array.isArray(e)?e.map(t=>{let r=t.responseStatusCode??t.statusCode??0,a=t.timestamp??null;return {method:t.method??"GET",url:t.url??"",status:r,statusCode:r,type:"xhr",size:Mt(t.responseBody),duration:t.responseTimeMs??t.durationMs??0,responseTimeMs:t.responseTimeMs??t.durationMs??0,requestHeaders:t.requestHeaders??null,responseHeaders:t.responseHeaders??null,requestBody:t.requestBody??null,responseBody:t.responseBody??null,timestamp:a,startedAt:a}}):[]}function Nt(e){if(!Array.isArray(e))return [];let t=r=>{if(r==null)return String(r);if(typeof r=="string")return r;try{return JSON.stringify(r)}catch{return String(r)}};return e.map(r=>{let a=r.status==="failed"?"failed":"passed",n=typeof r.expression=="string"&&r.expression.trim()?r.expression.trim():null,s=r.kind??r.type??"assertion",l=a==="failed"?`expected ${t(r.expected)}, received ${t(r.actual)}`:r.expected!==void 0?`expected ${t(r.expected)}`:null;return {...r.callId!=null?{callId:r.callId}:{},kind:s,description:n??r.description??`${s} assertion`,status:a,detail:l}})}async function Ae(e){return new Promise(t=>setTimeout(t,e))}function ci(e){let t={};for(let[r,a]of Object.entries(e))a!=null&&(t[r]=a);return t}async function Ma(e,t,r,a=Ia,n){for(let s=0;s<Re;s++){if(n?.expired())return null;let l=n?n.slice(a):a;if(l<=0)return null;try{let i=await V(e,t,l);if(i.ok)return i;if(i.status===401&&r&&s===0){let d=await r();if(d){let o={...t,headers:{...t.headers,Authorization:`Bearer ${d}`}},c=await V(e,o,l);if(c.ok)return c}return null}if(i.status===429&&s<Re-1){let d=i.headers.get("Retry-After"),o=d?parseInt(d,10)*1e3:Te[s];!isNaN(o)&&o>0?await Ae(o):await Ae(Te[s]);continue}if(i.status>=500&&s<Re-1){await Ae(Te[s]);continue}return i}catch{if(s<Re-1){await Ae(Te[s]);continue}return null}}return null}var ui=util.promisify(zlib.gzip);function _a(e,t){if(!Array.isArray(e.tests))return e;let r=e.tests.map(a=>{let{consoleLogs:n,networkRequests:s,apiCalls:l,...i}=a;return t?i:{...i,...l!==void 0?{apiCalls:l}:{}}});return {...e,tests:r}}var pi=["consoleLogs","networkRequests","apiCalls","actions","navigations","protocolAssertions"];function fi(e){if(!Array.isArray(e.tests))return e;let t=new Set(pi),r=e.tests.map(a=>{let n={};for(let s in a)if(!t.has(s))if(s==="failure"&&a[s]&&typeof a[s]=="object"){let l=a[s];n.failure={message:l.message??null,line:l.line??null};}else n[s]=a[s];return n});return {...e,tests:r}}function gi(e){let t=new Set(["tests","timeline","unifiedTimeline","navigations"]),r={};for(let a in e)t.has(a)||(r[a]=e[a]);return r.tests=[],r}async function Ft(e,t=li){let r={"Content-Type":"application/json"},a=JSON.stringify(e),n=e,s=Array.isArray(n?.tests)?n.tests.length:0,l=i=>Buffer.byteLength(i,"utf-8")>t;if(s>0&&l(a)){let i=Math.max(1,Math.round(t/1048576));process.stderr.write(`[testrelic] WARNING: batch payload exceeds ${i}MB \u2014 dropping consoleLogs/networkRequests from ${s} test(s) to avoid HTTP 413. Console/network detail will be missing in the cloud.
4265
+ `),a=JSON.stringify(_a(n,false)),l(a)&&(process.stderr.write(`[testrelic] WARNING: batch still exceeds ${i}MB after dropping logs \u2014 also dropping apiCalls from ${s} test(s). API request/response detail will be missing in the cloud.
4266
+ `),a=JSON.stringify(_a(n,true))),l(a)&&(process.stderr.write(`[testrelic] WARNING: batch still exceeds ${i}MB \u2014 shedding remaining per-test detail (actions/navigations/assertions) from ${s} test(s). Only test results + summary will reach the cloud.
4267
+ `),a=JSON.stringify(fi(n))),l(a)&&(process.stderr.write(`[testrelic] ERROR: batch still exceeds ${i}MB after shedding all per-test detail \u2014 uploading run summary only (${s} per-test result(s) omitted) to avoid losing the run to HTTP 413.
4268
+ `),a=JSON.stringify(gi(n)));}if(Buffer.byteLength(a,"utf-8")>oi){r["Content-Encoding"]="gzip";let i=await ui(Buffer.from(a,"utf-8")),d=i.buffer;return {body:new Uint8Array(d,i.byteOffset,i.byteLength),headers:r}}return {body:a,headers:r}}async function Ea(e,t,r,a,n,s){let l=`${e}/runs`,{body:i,headers:d}=await Ft(r);d.Authorization=`Bearer ${t}`;let o=await Ma(l,{method:"POST",headers:d,body:i},a,n,s);return o?.ok?{success:true,statusCode:o.status,error:null}:o?.status===409?{success:true,statusCode:409,error:null}:{success:false,reason:o?`Upload failed with status ${o.status}`:"Upload failed after retries",statusCode:o?.status??null,payload:r,targetEndpoint:l,method:"POST"}}async function Pa(e,t,r,a){let n=`${e}/runs/init`;try{let s=await V(n,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${t}`},body:JSON.stringify(ci(r))},a??Ia);return s.ok?{runId:(await s.json()).runId}:null}catch{return null}}function La(e,t,r,a){let n=`${e}/runs/${r}/tests`;fetch(n,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${t}`},body:JSON.stringify(a)}).catch(()=>{});}async function Da(e,t,r,a){let n=`${e}/runs/${r}/finalize`;return (await Ma(n,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${t}`},body:JSON.stringify(a)}))?.ok??false}function Bt(e,t,r,a){let n=`${e}/runs/${r}/finalize`,s=new Date,l={finishedAt:s.toISOString(),duration:s.getTime()-new Date(a).getTime(),summary:{}};fetch(n,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${t}`},body:JSON.stringify(l)}).catch(()=>{});}var yi="1.0.0",Na=10;function Fa(e){return e instanceof Error?e.message:String(e)}function xi(e){return e>=400&&e<500&&e!==408&&e!==429}function pe(e,t,r,a,n,s,l,i){try{fs$1.mkdirSync(e,{recursive:!0});let d=Date.now(),o=`${d}-${t}-${r}.json`,c=path.join(e,o),p={version:yi,queuedAt:new Date(d).toISOString(),reason:a,retryCount:0,targetEndpoint:n,method:s,payload:l,headers:i},v=c+".tmp";fs$1.writeFileSync(v,JSON.stringify(p,null,2),"utf-8"),fs$1.renameSync(v,c);}catch(d){d.code==="ENOSPC"?process.stderr.write(`\u26A0 TestRelic: Disk full \u2014 unable to queue upload data.
2566
4269
  `):process.stderr.write(`\u26A0 TestRelic: Unable to queue upload data.
2567
- `);}}async function Qr(t,e,r){let n;try{n=fs$1.readdirSync(t).filter(s=>s.endsWith(".json")&&!s.endsWith(".tmp")).sort();}catch{return}if(n.length===0)return;let a=[];for(let s=0;s<n.length;s+=Wr)a.push(n.slice(s,s+Wr));for(let s of a)for(let l of s){let i=path.join(t,l),d;try{if(!fs$1.statSync(i).isFile())continue;let c=JSON.parse(fs$1.readFileSync(i,"utf-8"));if(!core.isValidQueueEntry(c)){process.stderr.write(`\u26A0 TestRelic: Corrupted queue file deleted: ${l}
2568
- `),fs$1.unlinkSync(i);continue}d=c;}catch(o){process.stderr.write(`\u26A0 TestRelic: Skipping unreadable queue file ${l}: ${Kr(o)}
2569
- `);try{fs$1.unlinkSync(i);}catch{}continue}try{let{body:o,headers:c}=await ut(d.payload),g={...d.headers,...c,Authorization:`Bearer ${r}`},m=await fetch(d.targetEndpoint,{method:d.method,headers:g,body:o});if(m.ok)fs$1.unlinkSync(i);else if(ia(m.status)){process.stderr.write(`\u26A0 TestRelic: Queue entry ${l} rejected (HTTP ${m.status}) \u2014 moving aside; it will not be retried.
2570
- `);try{fs$1.renameSync(i,`${i}.failed`);}catch{try{fs$1.unlinkSync(i);}catch{}}}else {process.stderr.write(`\u26A0 TestRelic: Queue flush paused \u2014 server returned HTTP ${m.status} for ${l}; will retry later.
2571
- `);return}}catch(o){process.stderr.write(`\u26A0 TestRelic: Queue flush paused \u2014 ${Kr(o)}; will retry later.
2572
- `);return}}}function Xr(t,e){try{let r=fs$1.readdirSync(t).filter(a=>(a.endsWith(".json")||a.endsWith(".failed"))&&!a.endsWith(".tmp")),n=Date.now();for(let a of r){let s=path.join(t,a);try{let l=fs$1.readFileSync(s,"utf-8"),d=JSON.parse(l).queuedAt;if(typeof d=="string"){let o=new Date(d).getTime();n-o>e&&fs$1.unlinkSync(s);}}catch{try{fs$1.unlinkSync(s);}catch{}}}}catch{}}var fa=3e5,ga=864e5,ha=6e4,en=3e4,ht="https://platform.testrelic.ai/settings/api-keys";function tn(t){return new Promise(e=>{let r=setTimeout(e,t);typeof r?.unref=="function"&&r.unref();})}var be=class{constructor(e){this.gitMetadata=null;this.repoId=null;this.failureReason=null;this.flushPromise=null;this.healthCheckTimer=null;this.config=e,this.authState={mode:"pending",accessToken:null,refreshToken:null,expiresAt:null,orgId:null,orgName:null,userId:null,userName:null};}async initialize(){if(!this.config||!this.config.apiKey){this.setLocalMode("no_api_key"),process.stderr.write(`\u2139 TestRelic: No API key configured. Running in local mode.
2573
- `);return}try{if(Pr(this.config.endpoint),this.gitMetadata=Br(),!await tt(this.config.endpoint)){this.setLocalMode("cloud_unreachable"),process.stderr.write(`\u26A0 TestRelic: Cloud unreachable. Switching to local mode. Data will be queued for later upload.
2574
- `);return}let r=await rt(this.config.endpoint,this.config.apiKey,this.config.timeout);if(nt(r)){this.handleAuthError(r.code,r.statusCode);return}let n=r;this.authState={mode:"cloud",accessToken:n.accessToken,refreshToken:n.refreshToken,expiresAt:Date.now()+n.expiresIn*1e3,orgId:n.orgId,orgName:n.orgName,userId:n.userId,userName:n.userName},process.stderr.write(`\u2713 TestRelic: Connected to cloud (${n.orgName} / ${n.userName})
2575
- `),await this.resolveRepoId(),this.config.queueDirectory&&(Xr(this.config.queueDirectory,this.config.queueMaxAge),this.startBackgroundFlush()),this.startHealthCheck();}catch(e){this.setLocalMode("unexpected_error"),process.stderr.write(`\u26A0 TestRelic: Unexpected error during cloud initialization. Running in local mode. ${e instanceof Error?e.message:String(e)}
2576
- `);}}getMode(){return this.authState.mode}isCloudMode(){return this.authState.mode==="cloud"}isLocalMode(){return this.authState.mode==="local"}getAccessToken(){return this.authState.accessToken}getRepoId(){return this.repoId}getGitMetadata(){return this.gitMetadata}getConfig(){return this.config}getFailureReason(){return this.failureReason}getEndpoint(){return this.config?.endpoint??"https://platform.testrelic.ai/api/v1"}async ensureValidToken(){if(!this.isCloudMode()||!this.authState.accessToken)return false;let e=this.authState.expiresAt??0;if(Date.now()+fa<e)return true;if(!this.config||!this.authState.refreshToken)return this.switchToLocalMode("token_expired_no_refresh"),false;try{let r=await Fr(this.config.endpoint,this.authState.refreshToken,this.config.timeout);return r?(this.authState.accessToken=r.accessToken,this.authState.refreshToken=r.refreshToken,this.authState.expiresAt=Date.now()+r.expiresIn*1e3,!0):(this.switchToLocalMode("token_refresh_failed"),!1)}catch{return this.switchToLocalMode("token_refresh_error"),false}}switchToLocalMode(e){this.authState.mode!=="local"&&(this.authState.mode="local",this.failureReason=e,process.stderr.write(`\u26A0 TestRelic: Switched to local mode (${e}).
2577
- `));}async dispose(){if(this.healthCheckTimer&&(clearInterval(this.healthCheckTimer),this.healthCheckTimer=null),this.flushPromise){try{await Promise.race([this.flushPromise,tn(en)]);}catch{}this.flushPromise=null;}this.authState={mode:"local",accessToken:null,refreshToken:null,expiresAt:null,orgId:null,orgName:null,userId:null,userName:null},this.repoId=null,this.gitMetadata=null;}setLocalMode(e){this.authState.mode="local",this.failureReason=e;}handleAuthError(e,r){switch(e){case "invalid_key":this.setLocalMode("invalid_api_key"),process.stderr.write(`\u26A0 TestRelic: API key is invalid or revoked. Running in local mode.
2578
- \u2192 Manage keys: ${ht}
4270
+ `);}}async function za(e,t,r){let a;try{a=fs$1.readdirSync(e).filter(s=>s.endsWith(".json")&&!s.endsWith(".tmp")).sort();}catch{return}if(a.length===0)return;let n=[];for(let s=0;s<a.length;s+=Na)n.push(a.slice(s,s+Na));for(let s of n)for(let l of s){let i=path.join(e,l),d;try{if(!fs$1.statSync(i).isFile())continue;let c=JSON.parse(fs$1.readFileSync(i,"utf-8"));if(!core.isValidQueueEntry(c)){process.stderr.write(`\u26A0 TestRelic: Corrupted queue file deleted: ${l}
4271
+ `),fs$1.unlinkSync(i);continue}d=c;}catch(o){process.stderr.write(`\u26A0 TestRelic: Skipping unreadable queue file ${l}: ${Fa(o)}
4272
+ `);try{fs$1.unlinkSync(i);}catch{}continue}try{let{body:o,headers:c}=await Ft(d.payload),p={...d.headers,...c,Authorization:`Bearer ${r}`},v=await fetch(d.targetEndpoint,{method:d.method,headers:p,body:o});if(v.ok)fs$1.unlinkSync(i);else if(xi(v.status)){process.stderr.write(`\u26A0 TestRelic: Queue entry ${l} rejected (HTTP ${v.status}) \u2014 moving aside; it will not be retried.
4273
+ `);try{fs$1.renameSync(i,`${i}.failed`);}catch{try{fs$1.unlinkSync(i);}catch{}}}else {process.stderr.write(`\u26A0 TestRelic: Queue flush paused \u2014 server returned HTTP ${v.status} for ${l}; will retry later.
4274
+ `);return}}catch(o){process.stderr.write(`\u26A0 TestRelic: Queue flush paused \u2014 ${Fa(o)}; will retry later.
4275
+ `);return}}}function Ha(e,t){try{let r=fs$1.readdirSync(e).filter(n=>(n.endsWith(".json")||n.endsWith(".failed"))&&!n.endsWith(".tmp")),a=Date.now();for(let n of r){let s=path.join(e,n);try{let l=fs$1.readFileSync(s,"utf-8"),d=JSON.parse(l).queuedAt;if(typeof d=="string"){let o=new Date(d).getTime();a-o>t&&fs$1.unlinkSync(s);}}catch{try{fs$1.unlinkSync(s);}catch{}}}}catch{}}var Ai=3e5,_i=864e5,Ii=6e4,qa=3e4,zt="https://platform.testrelic.ai/settings/api-keys";function $a(e){return new Promise(t=>{let r=setTimeout(t,e);typeof r?.unref=="function"&&r.unref();})}var _e=class{constructor(t){this.gitMetadata=null;this.repoId=null;this.failureReason=null;this.flushPromise=null;this.healthCheckTimer=null;this.config=t,this.authState={mode:"pending",accessToken:null,refreshToken:null,expiresAt:null,orgId:null,orgName:null,userId:null,userName:null};}async initialize(){if(!this.config||!this.config.apiKey){this.setLocalMode("no_api_key"),process.stderr.write(`\u2139 TestRelic: No API key configured. Running in local mode.
4276
+ `);return}try{if(wa(this.config.endpoint),this.gitMetadata=Ta(),!await Rt(this.config.endpoint)){this.setLocalMode("cloud_unreachable"),process.stderr.write(`\u26A0 TestRelic: Cloud unreachable. Switching to local mode. Data will be queued for later upload.
4277
+ `);return}let r=await At(this.config.endpoint,this.config.apiKey,this.config.timeout);if(_t(r)){this.handleAuthError(r.code,r.statusCode);return}let a=r;this.authState={mode:"cloud",accessToken:a.accessToken,refreshToken:a.refreshToken,expiresAt:Date.now()+a.expiresIn*1e3,orgId:a.orgId,orgName:a.orgName,userId:a.userId,userName:a.userName},process.stderr.write(`\u2713 TestRelic: Connected to cloud (${a.orgName} / ${a.userName})
4278
+ `),await this.resolveRepoId(),this.config.queueDirectory&&(Ha(this.config.queueDirectory,this.config.queueMaxAge),this.startBackgroundFlush()),this.startHealthCheck();}catch(t){this.setLocalMode("unexpected_error"),process.stderr.write(`\u26A0 TestRelic: Unexpected error during cloud initialization. Running in local mode. ${t instanceof Error?t.message:String(t)}
4279
+ `);}}getMode(){return this.authState.mode}isCloudMode(){return this.authState.mode==="cloud"}isLocalMode(){return this.authState.mode==="local"}getAccessToken(){return this.authState.accessToken}getRepoId(){return this.repoId}getGitMetadata(){return this.gitMetadata}getConfig(){return this.config}getFailureReason(){return this.failureReason}getEndpoint(){return this.config?.endpoint??"https://platform.testrelic.ai/api/v1"}async ensureValidToken(){if(!this.isCloudMode()||!this.authState.accessToken)return false;let t=this.authState.expiresAt??0;if(Date.now()+Ai<t)return true;if(!this.config||!this.authState.refreshToken)return this.switchToLocalMode("token_expired_no_refresh"),false;try{let r=await Sa(this.config.endpoint,this.authState.refreshToken,this.config.timeout);return r?(this.authState.accessToken=r.accessToken,this.authState.refreshToken=r.refreshToken,this.authState.expiresAt=Date.now()+r.expiresIn*1e3,!0):(this.switchToLocalMode("token_refresh_failed"),!1)}catch{return this.switchToLocalMode("token_refresh_error"),false}}switchToLocalMode(t){this.authState.mode!=="local"&&(this.authState.mode="local",this.failureReason=t,process.stderr.write(`\u26A0 TestRelic: Switched to local mode (${t}).
4280
+ `));}async dispose(){if(this.healthCheckTimer&&(clearInterval(this.healthCheckTimer),this.healthCheckTimer=null),this.flushPromise){try{await Promise.race([this.flushPromise,$a(qa)]);}catch{}this.flushPromise=null;}this.authState={mode:"local",accessToken:null,refreshToken:null,expiresAt:null,orgId:null,orgName:null,userId:null,userName:null},this.repoId=null,this.gitMetadata=null;}setLocalMode(t){this.authState.mode="local",this.failureReason=t;}handleAuthError(t,r){switch(t){case "invalid_key":this.setLocalMode("invalid_api_key"),process.stderr.write(`\u26A0 TestRelic: API key is invalid or revoked. Running in local mode.
4281
+ \u2192 Manage keys: ${zt}
2579
4282
  `);break;case "expired_key":this.setLocalMode("expired_api_key"),process.stderr.write(`\u26A0 TestRelic: API key has expired. Running in local mode.
2580
- \u2192 Generate a new key: ${ht}
4283
+ \u2192 Generate a new key: ${zt}
2581
4284
  `);break;case "validation_error":this.setLocalMode("validation_error"),process.stderr.write(`\u26A0 TestRelic: API key format invalid. Running in local mode.
2582
- \u2192 Manage keys: ${ht}
4285
+ \u2192 Manage keys: ${zt}
2583
4286
  `);break;case "rate_limited":this.setLocalMode("rate_limited"),process.stderr.write(`\u26A0 TestRelic: Rate limited during authentication. Running in local mode.
2584
4287
  `);break;case "timeout":this.setLocalMode("auth_timeout"),process.stderr.write(`\u26A0 TestRelic: Authentication timed out. Running in local mode.
2585
4288
  `);break;default:this.setLocalMode(`auth_error_${r??"unknown"}`),process.stderr.write(`\u26A0 TestRelic: Cloud authentication failed. Running in local mode.
2586
- `);break}}async resolveRepoId(){if(!this.config||!this.authState.accessToken)return;let e=this.gitMetadata?.remoteUrl?ne(this.gitMetadata.remoteUrl):null,r=e?Or(e):this.config.projectName??se(process.cwd()),n=e??this.config.projectName??se(process.cwd()),a=this.readRepoCache(n);if(a){this.repoId=a.repoId;return}try{let s=await Dr(this.config.endpoint,this.authState.accessToken,n,r,this.config.timeout,this.gitMetadata?.branch);s&&(this.repoId=s.repoId,this.writeRepoCache(n,s.repoId,s.displayName));}catch{}!e&&!this.config.projectName&&!st(process.cwd())&&process.stderr.write(`\u2139 TestRelic: No git remote or package.json detected. Set project.name in .testrelic for stable project identity.
2587
- `);}readRepoCache(e){if(!this.config)return null;let r=path.join(this.config.queueDirectory,"..","cache","repo.json");try{if(!fs$1.existsSync(r))return null;let n=fs$1.readFileSync(r,"utf-8"),a=JSON.parse(n);if(a.gitId!==e||Date.now()-a.resolvedAt>ga)return null;let s=this.hashApiKey();return s&&a.apiKeyHash!==s?null:a}catch{return null}}writeRepoCache(e,r,n){if(!this.config)return;let a=path.join(this.config.queueDirectory,"..","cache"),s=path.join(a,"repo.json");try{fs$1.mkdirSync(a,{recursive:!0});let l={repoId:r,gitId:e,displayName:n,resolvedAt:Date.now(),apiKeyHash:this.hashApiKey()??""},i=s+".tmp";fs$1.writeFileSync(i,JSON.stringify(l,null,2),"utf-8"),fs$1.renameSync(i,s);}catch{}}hashApiKey(){return this.config?.apiKey?crypto.createHash("sha256").update(this.config.apiKey).digest("hex").substring(0,16):null}startBackgroundFlush(){if(!this.config||!this.authState.accessToken)return;let e=this.authState.accessToken,r=this.config.queueDirectory,n=this.config.endpoint;this.flushPromise=Promise.race([Qr(r,n,e),tn(en)]).catch(()=>{});}startHealthCheck(){if(!this.config)return;let e=this.config.endpoint;this.healthCheckTimer=setInterval(async()=>{if(!this.isCloudMode()&&!(!this.failureReason||!["cloud_unreachable","auth_timeout","network_error"].includes(this.failureReason)))try{if(!await tt(e))return;if(this.config?.apiKey){let n=await rt(this.config.endpoint,this.config.apiKey,this.config.timeout);if(!nt(n)){let a=n;this.authState={mode:"cloud",accessToken:a.accessToken,refreshToken:a.refreshToken,expiresAt:Date.now()+a.expiresIn*1e3,orgId:a.orgId,orgName:a.orgName,userId:a.userId,userName:a.userName},this.failureReason=null,process.stderr.write(`\u2713 TestRelic: Cloud connectivity restored.
2588
- `),this.startBackgroundFlush();}}}catch{}},ha),typeof this.healthCheckTimer?.unref=="function"&&this.healthCheckTimer.unref();}};function rn(t,e,r,n){let a=[],s=[],l=null,i=0,d=null,o=0,c=null,g=0,m=t.filter(p=>p.name===core.ATTACHMENT_NAME&&p.body),h=false;if(m.length>0)for(let p of m)try{let f=JSON.parse(p.body.toString());if(core.isTestRelicFilePayload(f)){a=a.concat(f.navigations),s=s.concat(f.apiAssertions),f.networkRequestsFile&&(l=f.networkRequestsFile,i+=f.networkRequestsCount),f.consoleLogsFile&&(d=f.consoleLogsFile,o+=f.consoleLogsCount),f.apiCallsFile&&(c=f.apiCallsFile,g+=f.apiCallsCount),h=!0;continue}if(core.isTestRelicDataPayload(f)){a=a.concat(f.navigations),Array.isArray(f.apiAssertions)&&(s=s.concat(f.apiAssertions)),i+=f.networkRequests?.length??0,g+=f.apiCalls?.length??0;let v=f.consoleLogs;Array.isArray(v)&&(o+=v.length),h=!0;}}catch{process.stderr.write(`[testrelic] Warning: Corrupt attachment for test "${r}"
2589
- `);}return h||(a=e.filter(p=>p.type==="lambdatest-navigation"&&p.description).map(p=>{try{return JSON.parse(p.description)}catch{return null}}).filter(p=>p!==null)),a.length===0&&i===0&&g===0&&!n&&!h&&process.stderr.write(`[testrelic] Warning: No data found for test "${r}". Make sure your tests import { test, expect } from '@testrelic/playwright-analytics/fixture' instead of '@playwright/test'. Without the TestRelic fixture, network logs, video sync, and Test Navigation data cannot be captured.
2590
- `),{navigations:a,apiAssertions:s,networkRequestsFile:l,networkRequestsCount:i,consoleLogsFile:d,consoleLogsCount:o,apiCallsFile:c,apiCallsCount:g}}function xe(t=process.env){let e=ba(t.TESTRELIC_CLOUD_PLATFORM);if(e)return {cloudPlatform:e,cloudBuildId:t.TESTRELIC_CLOUD_BUILD_ID||void 0,cloudSessionId:t.TESTRELIC_CLOUD_SESSION_ID||void 0};if(t.BROWSERSTACK_USERNAME||t.BROWSERSTACK_ACCESS_KEY)return {cloudPlatform:"browserstack",cloudBuildId:t.BROWSERSTACK_BUILD_NAME||t.BROWSERSTACK_BUILD_ID||void 0,cloudSessionId:t.BROWSERSTACK_SESSION_ID||void 0};if(t.LT_USERNAME||t.LAMBDATEST_USERNAME||t.LT_ACCESS_KEY||t.LAMBDATEST_ACCESS_KEY)return {cloudPlatform:"lambdatest",cloudBuildId:t.LT_BUILD||t.LAMBDATEST_BUILD||void 0,cloudSessionId:t.LT_SESSION_ID||t.LAMBDATEST_SESSION_ID||void 0};let r=t.PLAYWRIGHT_WS_ENDPOINT;if(r){if(/cdp\.lambdatest\.com/i.test(r))return {cloudPlatform:"lambdatest"};if(/cdp\.browserstack\.com/i.test(r))return {cloudPlatform:"browserstack"}}return {}}function ba(t){if(!t)return;let e=t.toLowerCase().trim();if(e==="browserstack"||e==="bs")return "browserstack";if(e==="lambdatest"||e==="lt")return "lambdatest"}var Sa=5,we=[1e3,3e3,9e3],K=3,yt=3e4,Ca={".png":"image/png",".jpg":"image/jpeg",".jpeg":"image/jpeg",".webp":"image/webp",".webm":"video/webm",".mp4":"video/mp4",".zip":"application/zip"},mt=0,vt=[];async function Ra(){mt>=Sa&&await new Promise(t=>vt.push(t)),mt++;}function Aa(){mt--,vt.length>0&&vt.shift()();}async function ke(t){return new Promise(e=>setTimeout(e,t))}function _a(t){let e=t.substring(t.lastIndexOf(".")).toLowerCase();return Ca[e]??"application/octet-stream"}function Ia(t){try{return fs$1.statSync(t).size}catch{return 0}}async function Ma(t,e,r,n,a){let s=`${t}/artifacts/upload-url`,l=JSON.stringify({runId:r.runId,testId:r.testId,fileName:path.basename(r.filePath),contentType:a,type:r.type,sizeBytes:n});for(let i=0;i<K;i++)try{let d=await D(s,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${e}`},body:l},yt);if(d.ok)return await d.json();if(d.status>=500&&i<K-1){await ke(we[i]);continue}return null}catch{if(i<K-1){await ke(we[i]);continue}return null}return null}async function Ea(t,e,r,n){let a=Ur(n,yt);for(let s=0;s<K;s++)try{let l=fs$1.createReadStream(e),i=stream.Readable.toWeb(l),d=await D(t,{method:"PUT",headers:{"Content-Type":r,"Content-Length":String(n)},body:i,duplex:"half"},a);if(d.ok)return !0;if(d.status>=500&&s<K-1){await ke(we[s]);continue}return !1}catch{if(s<K-1){await ke(we[s]);continue}return false}return false}async function La(t,e,r){let n=`${t}/artifacts/confirm`;try{return (await D(n,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${e}`},body:JSON.stringify({artifactId:r})},yt)).ok}catch{return false}}async function Pa(t,e,r,n){let a=Ia(r.filePath);if(a===0)return {success:false,storageKey:null,artifactId:null,error:"file_not_found_or_empty"};if(a>n*1024*1024)return {success:false,storageKey:null,artifactId:null,error:"file_too_large"};let s=_a(r.filePath);await Ra();try{let l=await Ma(t,e,r,a,s);if(!l)return {success:!1,storageKey:null,artifactId:null,error:"upload_url_request_failed"};if(!await Ea(l.uploadUrl,r.filePath,s,a))return {success:!1,storageKey:l.storageKey,artifactId:l.artifactId,error:"presigned_put_failed"};let d=await La(t,e,l.artifactId);return {success:d,storageKey:l.storageKey,artifactId:l.artifactId,error:d?null:"confirm_failed"}}finally{Aa();}}async function nn(t,e,r,n,a){let s=new Map,l=r.map(async i=>{if(a?.expired()){s.set(i.filePath,{success:false,storageKey:null,artifactId:null,error:"upload_budget_exhausted"});return}let d=await Pa(t,e,i,n);s.set(i.filePath,d);});return await Promise.allSettled(l),s}function wt(t){let r=t.getGitMetadata()?.remoteUrl;return r?ne(r):t.getConfig()?.projectName??se(process.cwd())}async function sn(t,e,r,n,a){if(!t.isCloudMode()||e!=="realtime"&&e!=="both"||!await t.ensureValidToken())return null;let l=t.getGitMetadata(),i=P(),d=xe(),o=ge();return (await jr(t.getEndpoint(),t.getAccessToken(),{runId:r,repoGitId:wt(t),branch:l?.branch??null,commit:l?.commitSha??null,commitMessage:l?.commitMessage??null,commitAuthor:l?.commitAuthor??null,startedAt:n,totalTests:null,ciProvider:i?.provider??null,ciRunUrl:i?.runUrl??null,environment:a??d.cloudPlatform??null,cloudPlatform:d.cloudPlatform??null,cloudBuildId:d.cloudBuildId??null,cloudSessionId:d.cloudSessionId??null,...o?{runType:o}:{},testType:"e2e"}))?.runId??null}var Fa=true,Da=50,xt={keyMap:new Map,total:0,uploaded:0};async function Ba(t,e,r,n,a){let s=new Map;if(!(e?.uploadArtifacts??Fa)||!await t.ensureValidToken())return xt;let d=[],o=new Map;for(let u of n){if(u.artifacts.screenshot){let p=path.join(u.outputDir,u.artifacts.screenshot);d.push({filePath:p,runId:r,testId:u.testId,type:"screenshot"}),o.set(p,{testId:u.testId,field:"screenshot"});}if(u.artifacts.video){let p=path.join(u.outputDir,u.artifacts.video);d.push({filePath:p,runId:r,testId:u.testId,type:"video"}),o.set(p,{testId:u.testId,field:"video"});}if(u.artifacts.trace){let p=path.join(u.outputDir,u.artifacts.trace);d.push({filePath:p,runId:r,testId:u.testId,type:"trace"}),o.set(p,{testId:u.testId,field:"trace"});}}if(d.length===0)return xt;let c=e?.artifactMaxSizeMb??Da,g=await nn(t.getEndpoint(),t.getAccessToken(),d,c,a),m=new Map;for(let[u,p]of g){if(!p.success||!p.storageKey)continue;let f=o.get(u);if(!f)continue;let v=m.get(f.testId)??{};f.field==="screenshot"&&(v.screenshotKey=p.storageKey),f.field==="video"&&(v.videoKey=p.storageKey),f.field==="trace"&&(v.traceKey=p.storageKey),m.set(f.testId,v);}for(let u of n){let p=m.get(u.testId);s.set(u.testId,{...u.artifacts,...p?.screenshotKey?{screenshotKey:p.screenshotKey}:{},...p?.videoKey?{videoKey:p.videoKey}:{},...p?.traceKey?{traceKey:p.traceKey}:{}});}let h=Array.from(g.values()).filter(u=>u.success).length;return h>0&&process.stderr.write(`\u2713 TestRelic: Uploaded ${h} artifact(s) to cloud storage.
2591
- `),{keyMap:s,total:d.length,uploaded:h}}async function kt(t,e,r,n,a,s,l,i,d,o,c){let g={realtimeFinalizeAttempted:false,realtimeFinalized:false,batchAttempted:false,batchAcknowledged:false,artifactsTotal:0,artifactsUploaded:0,failureReason:null},m=new he(e?.uploadBudgetMs??12e4),h=u=>{g.failureReason||(g.failureReason=u);};try{if(t.isCloudMode()&&a&&(r==="realtime"||r==="both")){g.realtimeFinalizeAttempted=!0;let p=await t.ensureValidToken(),f=t.getGitMetadata(),v=it(c),w={finishedAt:l,duration:i,summary:d,...f?.commitMessage?{commitMessage:f.commitMessage}:{},testType:v.testType,...v.testType==="api"?{apiProtocol:v.apiProtocol,metadata:v.metadata}:{}};if(p){let y=await Gr(t.getEndpoint(),t.getAccessToken(),a,w);if(g.realtimeFinalized=y,!y){h("finalize_failed_after_retries");let k=e?.queueDirectory??".testrelic/queue";ae(k,n,"finalize","finalize_failed_after_retries",`${t.getEndpoint()}/runs/${a}/finalize`,"POST",w,{"Content-Type":"application/json"});}}else {h(t.getFailureReason()??"token_invalid");let y=e?.queueDirectory??".testrelic/queue";ae(y,n,"finalize",t.getFailureReason()??"token_invalid",`${t.getEndpoint()}/runs/${a}/finalize`,"POST",w,{"Content-Type":"application/json"});}}let u=new Map;if(t.isCloudMode()&&o&&o.length>0){let p=await Ba(t,e,n,o,m);u=p.keyMap,g.artifactsTotal=p.total,g.artifactsUploaded=p.uploaded;}if(t.isCloudMode()&&(!r||r==="batch"||r==="both"))if(g.batchAttempted=!0,await t.ensureValidToken()){let f=t.getGitMetadata(),v=P(),w=wt(t),y=Oa(s,u),k=ot(y,w,f,v,c,xe()),C=await $r(t.getEndpoint(),t.getAccessToken(),k,async()=>await t.ensureValidToken()?t.getAccessToken():null,e?.timeout,m);if(g.batchAcknowledged=C.success,!C.success){let b=C;h(b.reason);let T=e?.queueDirectory??".testrelic/queue";ae(T,n,"batch",b.reason,b.targetEndpoint,b.method,b.payload,{"Content-Type":"application/json"});}}else {h(t.getFailureReason()??"token_invalid");let f=e?.queueDirectory??".testrelic/queue",v=t.getGitMetadata(),w=P(),y=wt(t),k=ot(s,y,v,w,void 0,xe());ae(f,n,"batch",t.getFailureReason()??"token_invalid",`${t.getEndpoint()}/runs`,"POST",k,{"Content-Type":"application/json"});}await t.dispose();}catch(u){h(u instanceof Error?u.message:"unexpected_upload_error");try{await t.dispose();}catch{}}return g}function Oa(t,e){if(e.size===0)return t;let r=t.timeline.map(n=>{let a=n.testId;if(!a)return n;let s=e.get(a);return s?{...n,...s.screenshotKey?{screenshotKey:s.screenshotKey}:{},...s.videoKey?{videoKey:s.videoKey}:{}}:n});return {...t,timeline:r}}var Ua=new Set(["batch","realtime","both"]),ie=Object.freeze({attempted:false,acknowledged:false,runId:null,endpoint:null,finalized:false,artifactsUploaded:0,artifactsTotal:0,detail:"cloud reporting was never initialised"});function qa(t){switch(t){case "no_api_key":return "cloud mode off \u2014 no API key configured";case "cloud_unreachable":case "network_error":return "cloud endpoint unreachable \u2014 the run stayed local";case "invalid_api_key":return "API key rejected as invalid or revoked \u2014 the run stayed local";case "expired_api_key":return "API key has expired \u2014 the run stayed local";case "validation_error":return "API key format rejected \u2014 the run stayed local";case "rate_limited":return "rate limited during authentication \u2014 the run stayed local";case "auth_timeout":return "authentication timed out \u2014 the run stayed local";case "token_expired_no_refresh":case "token_refresh_failed":case "token_refresh_error":return "access token could not be refreshed \u2014 the run stayed local";case "init_error":return "cloud initialisation timed out \u2014 the run stayed local";case "unexpected_error":return "cloud initialisation failed \u2014 the run stayed local";default:return t?`cloud mode off (${t}) \u2014 the run stayed local`:"cloud mode off \u2014 the run stayed local"}}function an(t,e,r){if(!t)return ie;if(!t.isCloudMode())return {...ie,detail:qa(t.getFailureReason())};let n=e??"batch";if(!Ua.has(n))return {...ie,detail:`upload strategy '${n}' is not recognised \u2014 nothing was sent`};let a=t.getEndpoint(),s=n==="realtime"||n==="both";return s&&r?{attempted:true,acknowledged:true,runId:r,endpoint:a,finalized:false,artifactsUploaded:0,artifactsTotal:0,detail:null}:{attempted:true,acknowledged:false,runId:null,endpoint:a,finalized:false,artifactsUploaded:0,artifactsTotal:0,detail:s?"run init failed \u2014 the platform did not acknowledge this run":`upload runs at the end of the run under the '${n}' strategy \u2014 not sent yet`}}function Tt(t,e,r){if(!t.attempted)return t;if(!e)return {...t,detail:t.acknowledged?"upload timed out before it completed \u2014 the run may be incomplete on the platform":"upload timed out before the platform acknowledged this run",acknowledged:t.acknowledged};let n=t.acknowledged||e.batchAcknowledged,a=t.runId??(e.batchAcknowledged?r:null),s=e.realtimeFinalizeAttempted?e.realtimeFinalized:e.batchAcknowledged,l=null;return n?s||(l="run acknowledged but not closed out \u2014 finalize failed and was queued for retry"):l=e.failureReason?`upload failed (${e.failureReason}) \u2014 queued locally for retry`:"upload failed \u2014 the platform did not acknowledge this run",{attempted:true,acknowledged:n,runId:a,endpoint:t.endpoint,finalized:s,artifactsUploaded:e.artifactsUploaded,artifactsTotal:e.artifactsTotal,detail:l}}function O(t){try{return fs$1.readFileSync(t,"utf-8").split(`
2592
- `).filter(r=>r.length>0).map(r=>{try{return JSON.parse(r)}catch{return null}}).filter(r=>r!==null)}catch{return []}}function Ct(t){switch(t){case "passed":return "passed";case "failed":return "failed";case "timedOut":return "timedout";case "skipped":return "skipped";case "interrupted":return "failed";default:return "failed"}}function Wa(t,e,r){let n=`${t}::${e}::${r}`;return crypto.createHash("sha256").update(n).digest("hex").substring(0,16)}function Ka(t){return t.length<=3?"":t[t.length-2]}function Ja(t){let e=t.findIndex(r=>r.status==="passed");return e>0?`passed on retry ${e}`:null}function Ya(t){return t===null?"error":t>=200&&t<300?"2xx":t>=300&&t<400?"3xx":t>=400&&t<500?"4xx":t>=500&&t<600?"5xx":"error"}function cn(t,e){for(let r of t)e[r.category]=(e[r.category]??0)+1,r.children.length>0&&cn(r.children,e);}function Za(t){if(t.length===0)return null;let e=[...t].sort((n,a)=>n-a),r=e.reduce((n,a)=>n+a,0);return {avg:Math.round(r/e.length),min:e[0],max:e[e.length-1],p50:z(e,50),p95:z(e,95),p99:z(e,99)}}var un=2e5,Te=1e5;function dn(t,e,r,n,a,s,l){let i={retryIndex:t,apiCalls:e?.length??0,apiUrls:[],methodCounts:{},statusRanges:{"2xx":0,"3xx":0,"4xx":0,"5xx":0,error:0},responseTimes:[],passedAssertions:0,failedAssertions:0,assertions:r.length,navigations:n.length,navUrls:[],networkRequests:a,consoleLogs:s,actionSteps:l?.length??0,actionCategories:{}};if(e){let o=new Set;for(let c of e){let g=typeof c.method=="string"?c.method:"OTHER";i.methodCounts[g]=(i.methodCounts[g]??0)+1;let m=typeof c.url=="string"?c.url:null;m&&o.size<Te&&o.add(Qe(m));let h=typeof c.responseStatusCode=="number"?c.responseStatusCode:typeof c.statusCode=="number"?c.statusCode:null,u=h!==null&&Number.isFinite(h)?h:null;i.statusRanges[Ya(u)]++;let p=typeof c.responseTimeMs=="number"?c.responseTimeMs:typeof c.durationMs=="number"?c.durationMs:null;p!==null&&Number.isFinite(p)&&i.responseTimes.length<un&&i.responseTimes.push(p);}i.apiUrls=Array.from(o);}for(let o of r)o.status==="passed"?i.passedAssertions++:i.failedAssertions++;let d=new Set;for(let o of n)o.url&&d.size<Te&&d.add(o.url);return i.navUrls=Array.from(d),l&&cn(l,i.actionCategories),i}function Qa(){return {apiUrlSet:new Set,methodCounts:{},statusRanges:{"2xx":0,"3xx":0,"4xx":0,"5xx":0,error:0},responseTimes:[],passedAssertions:0,failedAssertions:0,navUrlSet:new Set,actionCategoryCounts:{},volApiCalls:0,volAssertions:0,volNavigations:0,volNetworkRequests:0,volConsoleLogs:0,volActionSteps:0}}function Xa(t){return process.env.BROWSERSTACK_USERNAME?"BrowserStack":t?{"github-actions":"GitHub Actions","gitlab-ci":"GitLab CI",jenkins:"Jenkins",circleci:"CircleCI","bitbucket-pipelines":"Bitbucket Pipelines"}[t.provider]??"CI":"Local"}function ei(){let t=os$1.platform();return t==="win32"?"Windows":t==="darwin"?"macOS":"Linux"}function ti(t){let e=t.parent?.project?.()?.use;if(!e)return;let{browserName:r,channel:n}=e;if(n){let s=n.toLowerCase();return s==="chrome"||s==="chrome-stable"?"Chrome":s.startsWith("chrome-beta")?"Chrome Beta":s.startsWith("chrome-dev")?"Chrome Dev":s.startsWith("chrome-canary")?"Chrome Canary":s==="msedge"||s==="msedge-stable"?"Microsoft Edge":s.startsWith("msedge-beta")?"Edge Beta":s.startsWith("msedge-dev")?"Edge Dev":s.startsWith("msedge-canary")?"Edge Canary":n}if(r){let s=r.toLowerCase();return s==="chromium"?"Chromium":s==="firefox"?"Firefox":s==="webkit"?"WebKit":r}let a=e.defaultBrowserType;if(a){let s=a.toLowerCase();return s==="chromium"?"Chromium":s==="firefox"?"Firefox":s==="webkit"?"WebKit":a}}function ri(t,e,r){let n=r.parent?.project?.(),a="unknown";n!==void 0&&(n.use.isMobile===true?a="mobile":a="e2e");let s=["e2e","api","unit","mobile"];for(let d of s)if(t.some(o=>o===`@${d}`||o===d))return d;let l=e.replace(/\\/g,"/"),i=["e2e","api","unit"];for(let d of i)if(l.includes(`/${d}/`))return d;return a}var ni="__testrelic_api_config";function si(t){return JSON.stringify(t,(e,r)=>r instanceof RegExp?{__regexp:true,source:r.source,flags:r.flags}:r)}var Se=class{constructor(e){this.rootDir="";this.startedAt="";this.testRunId="";this.collectedTests=[];this.fixtureDataReceived=false;this.navigationsCaptured=false;this.testCount=0;this.cloudClient=null;this.cloudRunId=null;this.runTimestamp="";this.cloudReceipt=ie;this.pendingArtifactEntries=[];this.detectedOs="";this.detectedSource="";this.cloudTestsBuffer=[];this.activeReportMode="embedded";this.streamingWriter=null;this.testIndex=[];this.summaryCounters={total:0,passed:0,failed:0,flaky:0,skipped:0,timedOut:0,interrupted:0,totalApiCalls:0,totalAssertions:0,totalNavigations:0,totalNetworkRequests:0,totalConsoleLogs:0,totalActionSteps:0};this.streamingTestAgg=new Map;this.config=$t(e),this.apiConfig=zt(e);}upsertAggContribution(e,r){let n=this.streamingTestAgg.get(e);(!n||r.retryIndex>n.retryIndex)&&this.streamingTestAgg.set(e,r);}async onBegin(e,r){try{if(this.rootDir=e.rootDir,this.startedAt=new Date().toISOString(),this.testRunId=this.config.testRunId??crypto.randomUUID(),this.config.reportMode==="auto"){let l=r.allTests().length;this.activeReportMode=l>=this.config.streamingThreshold?"streaming":"embedded";}else this.activeReportMode=this.config.reportMode;let n=r.allTests().length;if(process.stderr.write(`[testrelic] Report mode: ${this.activeReportMode} (${n} tests)
2593
- `),this.activeReportMode==="streaming"){let l=path.dirname(this.config.outputPath);this.streamingWriter=new X(l);let i=()=>{if(this.streamingWriter){try{this.streamingWriter.writeIndex(this.testIndex);let d={testRunId:this.testRunId??"",startedAt:this.startedAt,completedAt:new Date().toISOString(),totalDuration:Date.now()-new Date(this.startedAt).getTime(),...this.summaryCounters,metadata:null,reportMode:"streaming",files:[],enrichedSummary:null,validation:null,writeErrors:this.streamingWriter.getWriteErrors(),cloudUpload:this.cloudReceipt};this.streamingWriter.writeSummary(d);}catch{}this.streamingWriter.dispose();}try{if(this.cloudClient?.isCloudMode()&&this.cloudRunId){let d=this.cloudClient.getAccessToken();d&&pt(this.cloudClient.getEndpoint(),d,this.cloudRunId,this.startedAt);}}catch{}};process.on("SIGTERM",i),process.on("SIGINT",i);}if(this.config.includeArtifacts){let l=path.dirname(this.config.outputPath);this.runTimestamp=vr(path.join(l,"artifacts"));try{Tr(path.join(l,"artifacts"));}catch{}}let a=P();this.detectedOs=ei(),this.detectedSource=Xa(a);let s=await H(async()=>(this.cloudClient=new be(this.config.cloud),await this.cloudClient.initialize(),sn(this.cloudClient,this.config.cloud?.uploadStrategy,this.testRunId,this.startedAt,this.detectedSource)),Cr,"Cloud initialization");if(this.cloudRunId=s??null,s===void 0&&this.cloudClient?.switchToLocalMode("init_error"),this.cloudReceipt=an(this.cloudClient,this.config.cloud?.uploadStrategy,this.cloudRunId),this.activeReportMode!=="streaming"){let l=()=>{try{if(this.cloudClient?.isCloudMode()&&this.cloudRunId){let i=this.cloudClient.getAccessToken();i&&pt(this.cloudClient.getEndpoint(),i,this.cloudRunId,this.startedAt);}}catch{}};process.on("SIGTERM",l),process.on("SIGINT",l);}}catch{}}onTestBegin(e,r){try{e.annotations.push({type:ni,description:si(this.apiConfig)});}catch{}}onTestEnd(e,r){try{let n=r,a=e.outcome(),s;a==="flaky"?s="flaky":a==="skipped"?s="skipped":s=Ct(n.status);let l=n.startTime.toISOString(),i=new Date(n.startTime.getTime()+n.duration).toISOString(),d=e.tags?[...e.tags]:e.annotations.filter(R=>R.type==="tag").map(R=>R.description??""),o=rn(n.attachments,e.annotations,e.title,s==="skipped");(o.navigations.length>0||o.networkRequestsCount>0||o.apiCallsCount>0)&&(this.fixtureDataReceived=!0),o.navigations.length>0&&(this.navigationsCaptured=!0),!this.config.captureNavigation&&o.navigations.length>0&&(o={...o,navigations:[]});let c=null;if(s==="failed"||s==="flaky"){let S=(s==="flaky"?e.results.find(U=>U.status!=="passed")?.errors??[]:n.errors)[0];if(S){let U=Vt(this.config.redactPatterns),Y=S.location?.line??null,Z=null;this.config.includeCodeSnippets&&Y!==null&&S.location?.file&&(Z=jt(S.location.file,Y,this.config.codeContextLines),Z&&(Z=U(Z))),c={message:U(S.message??"Unknown error"),line:Y,code:Z,stack:this.config.includeStackTrace&&S.stack?U(S.stack):null};}}let g=e.titlePath().filter(Boolean),m=path.relative(this.rootDir||".",e.location.file),h=Ka(g),u=g.join(" > "),p=m,f=Wa(p,h,u),v=ri(d,p,e),w=v!=="mobile"&&o.apiCallsCount>0?"api":v,y=a==="flaky",k=Ja(e.results),C=Ct(e.expectedStatus),b=Ct(n.status),T=null;if(this.config.includeArtifacts&&s!=="skipped"&&n.attachments){let R=path.dirname(this.config.outputPath),S=g[g.length-1]??e.title;T=yr(n.attachments,S,n.retry,R,this.runTimestamp||void 0,f);}let A=this.config.includeActionSteps&&n.steps?Er(n.steps,n.startTime):null,$=ti(e),E={titlePath:g,title:u,status:s,duration:n.duration,startedAt:l,completedAt:i,retryCount:e.results.length-1,retry:n.retry,tags:d,failure:c,specFile:m,navigations:o.navigations,testId:f,filePath:p,suiteName:h,testType:w,isFlaky:y,retryStatus:k,expectedStatus:C,actualStatus:b,artifacts:T,apiAssertions:o.apiAssertions,actions:A,networkRequestsFile:o.networkRequestsFile,networkRequestsCount:o.networkRequestsCount,consoleLogsFile:o.consoleLogsFile,consoleLogsCount:o.consoleLogsCount,apiCallsFile:o.apiCallsFile,apiCallsCount:o.apiCallsCount,browser:$,os:this.detectedOs||void 0,source:this.detectedSource||void 0},Rt=o.consoleLogsFile&&o.consoleLogsCount>0?O(o.consoleLogsFile):void 0,Ce=o.networkRequestsFile&&o.networkRequestsCount>0?O(o.networkRequestsFile):void 0,J=o.apiCallsFile&&o.apiCallsCount>0?O(o.apiCallsFile):void 0,At=J?lt(J):void 0,_t=!Ce&&J?dt(J):void 0,It=o.apiAssertions&&o.apiAssertions.length>0?ct(o.apiAssertions):void 0;this.cloudTestsBuffer.push({testId:f,title:u,status:s,duration:n.duration,suiteName:h,filePath:p,testType:w,isFlaky:y,startedAt:l,completedAt:i,tags:d,failure:c,retry:n.retry,retryCount:e.results.length-1,retryStatus:k,browser:$,os:this.detectedOs||void 0,source:this.detectedSource||void 0,...A&&A.length>0?{actions:A}:{},...o.navigations&&o.navigations.length>0?{navigations:o.navigations}:{},...Rt?{consoleLogs:Rt}:{},...Ce?{networkRequests:Ce}:_t?{networkRequests:_t}:{},...At?{apiCalls:At}:{},...It?{protocolAssertions:It}:{}});try{let R=this.config.cloud?.uploadStrategy;if(this.cloudClient?.isCloudMode()&&this.cloudRunId&&(R==="realtime"||R==="both")){let S=this.cloudClient.getAccessToken();S&&Vr(this.cloudClient.getEndpoint(),S,this.cloudRunId,this.buildCloudTestPayload(E));}}catch{}if(this.activeReportMode==="streaming"&&this.streamingWriter){let R=Re(p,g,e.id??"",n.retry),S=n.retry===e.results.length-1,U={id:R,navigations:o.navigations,apiAssertions:o.apiAssertions,actions:A??[],artifacts:T,failureDiagnostic:c,hasNetworkFile:o.networkRequestsFile!==null,networkRequestsCount:o.networkRequestsCount,hasConsoleFile:o.consoleLogsFile!==null,consoleLogsCount:o.consoleLogsCount,hasApiCallsFile:o.apiCallsFile!==null,apiCallsCount:o.apiCallsCount,startedAt:l};this.streamingWriter.writeTestDetail(R,U,{networkRequestsFile:o.networkRequestsFile,consoleLogsFile:o.consoleLogsFile,apiCallsFile:o.apiCallsFile});let Y={id:R,title:g[g.length-1]??e.title,titlePath:g,filePath:p,status:s,duration:n.duration,project:g[1]??"",retryIndex:n.retry,tags:d,hasNetworkData:o.networkRequestsCount>0,hasConsoleData:o.consoleLogsCount>0,hasArtifacts:T!==null,hasActionSteps:(A?.length??0)>0,errorMessage:c?.message?.split(`
2594
- `)[0]??null,networkCount:o.networkRequestsCount,consoleCount:o.consoleLogsCount,actionCount:A?.length??0,isRetry:!S};if(this.testIndex.push(Y),S)switch(this.summaryCounters.total++,s){case "passed":this.summaryCounters.passed++;break;case "failed":this.summaryCounters.failed++;break;case "flaky":this.summaryCounters.flaky++;break;case "skipped":this.summaryCounters.skipped++;break;case "timedout":this.summaryCounters.timedOut++;break;default:this.summaryCounters.interrupted++;break}this.upsertAggContribution(`${p}::${g.join(" > ")}`,dn(n.retry,J,o.apiAssertions,o.navigations,o.networkRequestsCount,o.consoleLogsCount,A)),T&&this.pendingArtifactEntries.push({testId:f,artifacts:T,outputDir:path.dirname(this.config.outputPath)}),E.actions=null,E.apiAssertions=null,E.navigations=[],E.artifacts=null,E.failure=null,E.networkRequestsFile=null,E.consoleLogsFile=null,E.apiCallsFile=null;}this.activeReportMode!=="streaming"&&this.collectedTests.push(E),this.testCount++,this.checkMemoryPressure();}catch{}}async onEnd(e){try{let r=new Date().toISOString(),n=new Date(this.startedAt).getTime(),s=new Date(r).getTime()-n,l=this.activeReportMode==="streaming"?this.testIndex.some(d=>d.status!=="skipped"):this.collectedTests.some(d=>d.status!=="skipped");!this.fixtureDataReceived&&l&&process.stderr.write(`
4289
+ `);break}}async resolveRepoId(){if(!this.config||!this.authState.accessToken)return;let t=this.gitMetadata?.remoteUrl?ce(this.gitMetadata.remoteUrl):null,r=t?Ra(t):this.config.projectName??ue(process.cwd()),a=t??this.config.projectName??ue(process.cwd()),n=this.readRepoCache(a);if(n){this.repoId=n.repoId;return}try{let s=await Ca(this.config.endpoint,this.authState.accessToken,a,r,this.config.timeout,this.gitMetadata?.branch);s&&(this.repoId=s.repoId,this.writeRepoCache(a,s.repoId,s.displayName));}catch{}!t&&!this.config.projectName&&!It(process.cwd())&&process.stderr.write(`\u2139 TestRelic: No git remote or package.json detected. Set project.name in .testrelic for stable project identity.
4290
+ `);}readRepoCache(t){if(!this.config)return null;let r=path.join(this.config.queueDirectory,"..","cache","repo.json");try{if(!fs$1.existsSync(r))return null;let a=fs$1.readFileSync(r,"utf-8"),n=JSON.parse(a);if(n.gitId!==t||Date.now()-n.resolvedAt>_i)return null;let s=this.hashApiKey();return s&&n.apiKeyHash!==s?null:n}catch{return null}}writeRepoCache(t,r,a){if(!this.config)return;let n=path.join(this.config.queueDirectory,"..","cache"),s=path.join(n,"repo.json");try{fs$1.mkdirSync(n,{recursive:!0});let l={repoId:r,gitId:t,displayName:a,resolvedAt:Date.now(),apiKeyHash:this.hashApiKey()??""},i=s+".tmp";fs$1.writeFileSync(i,JSON.stringify(l,null,2),"utf-8"),fs$1.renameSync(i,s);}catch{}}hashApiKey(){return this.config?.apiKey?crypto.createHash("sha256").update(this.config.apiKey).digest("hex").substring(0,16):null}startBackgroundFlush(){if(!this.config||!this.authState.accessToken)return;let t=this.authState.accessToken,r=this.config.queueDirectory,a=this.config.endpoint;this.flushPromise=Promise.race([za(r,a,t),$a(qa)]).catch(()=>{});}startHealthCheck(){if(!this.config)return;let t=this.config.endpoint;this.healthCheckTimer=setInterval(async()=>{if(!this.isCloudMode()&&!(!this.failureReason||!["cloud_unreachable","auth_timeout","network_error"].includes(this.failureReason)))try{if(!await Rt(t))return;if(this.config?.apiKey){let a=await At(this.config.endpoint,this.config.apiKey,this.config.timeout);if(!_t(a)){let n=a;this.authState={mode:"cloud",accessToken:n.accessToken,refreshToken:n.refreshToken,expiresAt:Date.now()+n.expiresIn*1e3,orgId:n.orgId,orgName:n.orgName,userId:n.userId,userName:n.userName},this.failureReason=null,process.stderr.write(`\u2713 TestRelic: Cloud connectivity restored.
4291
+ `),this.startBackgroundFlush();}}}catch{}},Ii),typeof this.healthCheckTimer?.unref=="function"&&this.healthCheckTimer.unref();}};function Li(e,t){return t?e?{"2xx":e["2xx"]+t["2xx"],"3xx":e["3xx"]+t["3xx"],"4xx":e["4xx"]+t["4xx"],"5xx":e["5xx"]+t["5xx"],error:e.error+t.error}:{...t}:e}function Va(e,t,r,a){let n=[],s=[],l=null,i=0,d=null,o=0,c=null,p=0,v=null,g=e.filter(u=>u.name===core.ATTACHMENT_NAME&&u.body),f=false;if(g.length>0)for(let u of g)try{let h=JSON.parse(u.body.toString());if(core.isTestRelicFilePayload(h)){n=n.concat(h.navigations),s=s.concat(h.apiAssertions),h.networkRequestsFile&&(l=h.networkRequestsFile,i+=h.networkRequestsCount),h.consoleLogsFile&&(d=h.consoleLogsFile,o+=h.consoleLogsCount),h.apiCallsFile&&(c=h.apiCallsFile,p+=h.apiCallsCount),v=Li(v,h.apiStatusMix),f=!0;continue}if(core.isTestRelicDataPayload(h)){n=n.concat(h.navigations),Array.isArray(h.apiAssertions)&&(s=s.concat(h.apiAssertions)),i+=h.networkRequests?.length??0,p+=h.apiCalls?.length??0;let b=h.consoleLogs;Array.isArray(b)&&(o+=b.length),f=!0;}}catch{process.stderr.write(`[testrelic] Warning: Corrupt attachment for test "${r}"
4292
+ `);}return f||(n=t.filter(u=>u.type==="lambdatest-navigation"&&u.description).map(u=>{try{return JSON.parse(u.description)}catch{return null}}).filter(u=>u!==null)),n.length===0&&i===0&&p===0&&!a&&!f&&process.stderr.write(`[testrelic] Warning: No data found for test "${r}". Make sure your tests import { test, expect } from '@testrelic/playwright-analytics/fixture' instead of '@playwright/test'. Without the TestRelic fixture, network logs, video sync, and Test Navigation data cannot be captured.
4293
+ `),{navigations:n,apiAssertions:s,networkRequestsFile:l,networkRequestsCount:i,consoleLogsFile:d,consoleLogsCount:o,apiCallsFile:c,apiCallsCount:p,apiStatusMix:v}}function Ie(e=process.env){let t=Di(e.TESTRELIC_CLOUD_PLATFORM);if(t)return {cloudPlatform:t,cloudBuildId:e.TESTRELIC_CLOUD_BUILD_ID||void 0,cloudSessionId:e.TESTRELIC_CLOUD_SESSION_ID||void 0};if(e.BROWSERSTACK_USERNAME||e.BROWSERSTACK_ACCESS_KEY)return {cloudPlatform:"browserstack",cloudBuildId:e.BROWSERSTACK_BUILD_NAME||e.BROWSERSTACK_BUILD_ID||void 0,cloudSessionId:e.BROWSERSTACK_SESSION_ID||void 0};if(e.LT_USERNAME||e.LAMBDATEST_USERNAME||e.LT_ACCESS_KEY||e.LAMBDATEST_ACCESS_KEY)return {cloudPlatform:"lambdatest",cloudBuildId:e.LT_BUILD||e.LAMBDATEST_BUILD||void 0,cloudSessionId:e.LT_SESSION_ID||e.LAMBDATEST_SESSION_ID||void 0};let r=e.PLAYWRIGHT_WS_ENDPOINT;if(r){if(/cdp\.lambdatest\.com/i.test(r))return {cloudPlatform:"lambdatest"};if(/cdp\.browserstack\.com/i.test(r))return {cloudPlatform:"browserstack"}}return {}}function Di(e){if(!e)return;let t=e.toLowerCase().trim();if(t==="browserstack"||t==="bs")return "browserstack";if(t==="lambdatest"||t==="lt")return "lambdatest"}var Ui=5,Me=[1e3,3e3,9e3],te=3,$t=3e4,zi={".png":"image/png",".jpg":"image/jpeg",".jpeg":"image/jpeg",".webp":"image/webp",".webm":"video/webm",".mp4":"video/mp4",".zip":"application/zip",".json":"application/json"},Ht=0,qt=[];async function Hi(){Ht>=Ui&&await new Promise(e=>qt.push(e)),Ht++;}function qi(){Ht--,qt.length>0&&qt.shift()();}async function Ee(e){return new Promise(t=>setTimeout(t,e))}function $i(e){let t=e.substring(e.lastIndexOf(".")).toLowerCase();return zi[t]??"application/octet-stream"}function Vi(e){try{return fs$1.statSync(e).size}catch{return 0}}async function ji(e,t,r,a,n){let s=`${e}/artifacts/upload-url`,l=JSON.stringify({runId:r.runId,testId:r.testId,fileName:path.basename(r.filePath),contentType:n,type:r.type,sizeBytes:a});for(let i=0;i<te;i++)try{let d=await V(s,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${t}`},body:l},$t);if(d.ok)return await d.json();if(d.status>=500&&i<te-1){await Ee(Me[i]);continue}return null}catch{if(i<te-1){await Ee(Me[i]);continue}return null}return null}async function Gi(e,t,r,a){let n=Aa(a,$t);for(let s=0;s<te;s++)try{let l=fs$1.createReadStream(t),i=stream.Readable.toWeb(l),d=await V(e,{method:"PUT",headers:{"Content-Type":r,"Content-Length":String(a)},body:i,duplex:"half"},n);if(d.ok)return !0;if(d.status>=500&&s<te-1){await Ee(Me[s]);continue}return !1}catch{if(s<te-1){await Ee(Me[s]);continue}return false}return false}async function Wi(e,t,r){let a=`${e}/artifacts/confirm`;try{return (await V(a,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${t}`},body:JSON.stringify({artifactId:r})},$t)).ok}catch{return false}}async function Ki(e,t,r,a){let n=Vi(r.filePath);if(n===0)return {success:false,storageKey:null,artifactId:null,error:"file_not_found_or_empty"};if(n>a*1024*1024)return {success:false,storageKey:null,artifactId:null,error:"file_too_large"};let s=$i(r.filePath);await Hi();try{let l=await ji(e,t,r,n,s);if(!l)return {success:!1,storageKey:null,artifactId:null,error:"upload_url_request_failed"};if(!await Gi(l.uploadUrl,r.filePath,s,n))return {success:!1,storageKey:l.storageKey,artifactId:l.artifactId,error:"presigned_put_failed"};let d=await Wi(e,t,l.artifactId);return {success:d,storageKey:l.storageKey,artifactId:l.artifactId,error:d?null:"confirm_failed"}}finally{qi();}}async function ja(e,t,r,a,n){let s=new Map,l=r.map(async i=>{if(n?.expired()){s.set(i.filePath,{success:false,storageKey:null,artifactId:null,error:"upload_budget_exhausted"});return}let d=await Ki(e,t,i,a);s.set(i.filePath,d);});return await Promise.allSettled(l),s}function jt(e){let r=e.getGitMetadata()?.remoteUrl;return r?ce(r):e.getConfig()?.projectName??ue(process.cwd())}async function Ga(e,t,r,a,n){if(!e.isCloudMode()||t!=="realtime"&&t!=="both"||!await e.ensureValidToken())return null;let l=e.getGitMetadata(),i=U(),d=Ie(),o=Se();return (await Pa(e.getEndpoint(),e.getAccessToken(),{runId:r,repoGitId:jt(e),branch:l?.branch??null,commit:l?.commitSha??null,commitMessage:l?.commitMessage??null,commitAuthor:l?.commitAuthor??null,startedAt:a,totalTests:null,ciProvider:i?.provider??null,ciRunUrl:i?.runUrl??null,environment:n??d.cloudPlatform??null,cloudPlatform:d.cloudPlatform??null,cloudBuildId:d.cloudBuildId??null,cloudSessionId:d.cloudSessionId??null,...o?{runType:o}:{},testType:"e2e"}))?.runId??null}var Yi=[{field:"baseline",keyField:"baselineKey",type:"baseline"},{field:"actual",keyField:"actualKey",type:"screenshot"},{field:"diff",keyField:"diffKey",type:"diff"},{field:"baselineDom",keyField:"baselineDomKey",type:"dom"},{field:"actualDom",keyField:"actualDomKey",type:"dom"}],Xi=true,Zi=50,Vt={keyMap:new Map,visualKeyMap:new Map,total:0,uploaded:0};async function Qi(e,t,r,a,n){let s=new Map,l=new Map;if(!(t?.uploadArtifacts??Xi)||!await e.ensureValidToken())return Vt;let o=[],c=new Map,p=new Map;for(let u of a){if(u.artifacts.screenshot){let b=path.join(u.outputDir,u.artifacts.screenshot);o.push({filePath:b,runId:r,testId:u.testId,type:"screenshot"}),c.set(b,{testId:u.testId,field:"screenshot"});}if(u.artifacts.video){let b=path.join(u.outputDir,u.artifacts.video);o.push({filePath:b,runId:r,testId:u.testId,type:"video"}),c.set(b,{testId:u.testId,field:"video"});}if(u.artifacts.trace){let b=path.join(u.outputDir,u.artifacts.trace);o.push({filePath:b,runId:r,testId:u.testId,type:"trace"}),c.set(b,{testId:u.testId,field:"trace"});}let h=u.visual??[];for(let b=0;b<h.length;b++){let y=h[b];if(y)for(let w of Yi){let C=y[w.field];if(!C)continue;let P=path.join(u.outputDir,C);o.push({filePath:P,runId:r,testId:u.testId,type:w.type}),c.set(P,{testId:u.testId,field:"visual",visualIndex:b,visualKey:w.keyField});}}}if(o.length===0)return Vt;let v=t?.artifactMaxSizeMb??Zi,g=await ja(e.getEndpoint(),e.getAccessToken(),o,v,n),f=new Map;for(let[u,h]of g){if(!h.success||!h.storageKey)continue;let b=c.get(u);if(!b)continue;let y=f.get(b.testId)??{};if(b.field==="screenshot"&&(y.screenshotKey=h.storageKey),b.field==="video"&&(y.videoKey=h.storageKey),b.field==="trace"&&(y.traceKey=h.storageKey),b.field==="visual"&&b.visualIndex!==void 0&&b.visualKey){let w=p.get(b.testId)??new Map,C=w.get(b.visualIndex)??{};C[b.visualKey]=h.storageKey,w.set(b.visualIndex,C),p.set(b.testId,w);}f.set(b.testId,y);}for(let u of a){if(u.visual&&u.visual.length>0){let b=p.get(u.testId);l.set(u.testId,u.visual.map((y,w)=>({...y,...b?.get(w)??{}})));}let h=f.get(u.testId);s.set(u.testId,{...u.artifacts,...h?.screenshotKey?{screenshotKey:h.screenshotKey}:{},...h?.videoKey?{videoKey:h.videoKey}:{},...h?.traceKey?{traceKey:h.traceKey}:{}});}let m=Array.from(g.values()).filter(u=>u.success).length;return m>0&&process.stderr.write(`\u2713 TestRelic: Uploaded ${m} artifact(s) to cloud storage.
4294
+ `),{keyMap:s,visualKeyMap:l,total:o.length,uploaded:m}}async function Gt(e,t,r,a,n,s,l,i,d,o,c){let p={realtimeFinalizeAttempted:false,realtimeFinalized:false,batchAttempted:false,batchAcknowledged:false,artifactsTotal:0,artifactsUploaded:0,failureReason:null},v=new Ce(t?.uploadBudgetMs??12e4),g=f=>{p.failureReason||(p.failureReason=f);};try{if(e.isCloudMode()&&n&&(r==="realtime"||r==="both")){p.realtimeFinalizeAttempted=!0;let u=await e.ensureValidToken(),h=e.getGitMetadata(),b=Et(c),y={finishedAt:l,duration:i,summary:d,...h?.commitMessage?{commitMessage:h.commitMessage}:{},testType:b.testType,...b.testType==="api"?{apiProtocol:b.apiProtocol,metadata:b.metadata}:{}};if(u){let w=await Da(e.getEndpoint(),e.getAccessToken(),n,y);if(p.realtimeFinalized=w,!w){g("finalize_failed_after_retries");let C=t?.queueDirectory??".testrelic/queue";pe(C,a,"finalize","finalize_failed_after_retries",`${e.getEndpoint()}/runs/${n}/finalize`,"POST",y,{"Content-Type":"application/json"});}}else {g(e.getFailureReason()??"token_invalid");let w=t?.queueDirectory??".testrelic/queue";pe(w,a,"finalize",e.getFailureReason()??"token_invalid",`${e.getEndpoint()}/runs/${n}/finalize`,"POST",y,{"Content-Type":"application/json"});}}let f=new Map,m=new Map;if(e.isCloudMode()&&o&&o.length>0){let u=await Qi(e,t,a,o,v);f=u.keyMap,m=u.visualKeyMap,p.artifactsTotal=u.total,p.artifactsUploaded=u.uploaded;}if(e.isCloudMode()&&(!r||r==="batch"||r==="both"))if(p.batchAttempted=!0,await e.ensureValidToken()){let h=e.getGitMetadata(),b=U(),y=jt(e),w=eo(to(s,f),m),C=Pt(w,y,h,b,c,Ie()),P=await Ea(e.getEndpoint(),e.getAccessToken(),C,async()=>await e.ensureValidToken()?e.getAccessToken():null,t?.timeout,v);if(p.batchAcknowledged=P.success,!P.success){let R=P;g(R.reason);let _=t?.queueDirectory??".testrelic/queue";pe(_,a,"batch",R.reason,R.targetEndpoint,R.method,R.payload,{"Content-Type":"application/json"});}}else {g(e.getFailureReason()??"token_invalid");let h=t?.queueDirectory??".testrelic/queue",b=e.getGitMetadata(),y=U(),w=jt(e),C=Pt(s,w,b,y,void 0,Ie());pe(h,a,"batch",e.getFailureReason()??"token_invalid",`${e.getEndpoint()}/runs`,"POST",C,{"Content-Type":"application/json"});}await e.dispose();}catch(f){g(f instanceof Error?f.message:"unexpected_upload_error");try{await e.dispose();}catch{}}return p}function eo(e,t){if(t.size===0)return e;let r=e.timeline.map(a=>{let n=a.tests;if(!Array.isArray(n))return a;let s=false,l=n.map(i=>{let d=i.testId;if(!d)return i;let o=t.get(d);return o?(s=true,{...i,visual:o}):i});return s?{...a,tests:l}:a});return {...e,timeline:r}}function to(e,t){if(t.size===0)return e;let r=e.timeline.map(a=>{let n=a.testId;if(!n)return a;let s=t.get(n);return s?{...a,...s.screenshotKey?{screenshotKey:s.screenshotKey}:{},...s.videoKey?{videoKey:s.videoKey}:{}}:a});return {...e,timeline:r}}var ro=new Set(["batch","realtime","both"]),fe=Object.freeze({attempted:false,acknowledged:false,runId:null,endpoint:null,finalized:false,artifactsUploaded:0,artifactsTotal:0,detail:"cloud reporting was never initialised"});function ao(e){switch(e){case "no_api_key":return "cloud mode off \u2014 no API key configured";case "cloud_unreachable":case "network_error":return "cloud endpoint unreachable \u2014 the run stayed local";case "invalid_api_key":return "API key rejected as invalid or revoked \u2014 the run stayed local";case "expired_api_key":return "API key has expired \u2014 the run stayed local";case "validation_error":return "API key format rejected \u2014 the run stayed local";case "rate_limited":return "rate limited during authentication \u2014 the run stayed local";case "auth_timeout":return "authentication timed out \u2014 the run stayed local";case "token_expired_no_refresh":case "token_refresh_failed":case "token_refresh_error":return "access token could not be refreshed \u2014 the run stayed local";case "init_error":return "cloud initialisation timed out \u2014 the run stayed local";case "unexpected_error":return "cloud initialisation failed \u2014 the run stayed local";default:return e?`cloud mode off (${e}) \u2014 the run stayed local`:"cloud mode off \u2014 the run stayed local"}}function Wa(e,t,r){if(!e)return fe;if(!e.isCloudMode())return {...fe,detail:ao(e.getFailureReason())};let a=t??"batch";if(!ro.has(a))return {...fe,detail:`upload strategy '${a}' is not recognised \u2014 nothing was sent`};let n=e.getEndpoint(),s=a==="realtime"||a==="both";return s&&r?{attempted:true,acknowledged:true,runId:r,endpoint:n,finalized:false,artifactsUploaded:0,artifactsTotal:0,detail:null}:{attempted:true,acknowledged:false,runId:null,endpoint:n,finalized:false,artifactsUploaded:0,artifactsTotal:0,detail:s?"run init failed \u2014 the platform did not acknowledge this run":`upload runs at the end of the run under the '${a}' strategy \u2014 not sent yet`}}function Wt(e,t,r){if(!e.attempted)return e;if(!t)return {...e,detail:e.acknowledged?"upload timed out before it completed \u2014 the run may be incomplete on the platform":"upload timed out before the platform acknowledged this run",acknowledged:e.acknowledged};let a=e.acknowledged||t.batchAcknowledged,n=e.runId??(t.batchAcknowledged?r:null),s=t.realtimeFinalizeAttempted?t.realtimeFinalized:t.batchAcknowledged,l=null;return a?s||(l="run acknowledged but not closed out \u2014 finalize failed and was queued for retry"):l=t.failureReason?`upload failed (${t.failureReason}) \u2014 queued locally for retry`:"upload failed \u2014 the platform did not acknowledge this run",{attempted:true,acknowledged:a,runId:n,endpoint:e.endpoint,finalized:s,artifactsUploaded:t.artifactsUploaded,artifactsTotal:t.artifactsTotal,detail:l}}function j(e){try{return fs$1.readFileSync(e,"utf-8").split(`
4295
+ `).filter(r=>r.length>0).map(r=>{try{return JSON.parse(r)}catch{return null}}).filter(r=>r!==null)}catch{return []}}function uo(e){if(!e)return null;let t=e.split(`
4296
+ `).map(s=>s.trim()).filter(Boolean);if(t.length===0)return null;let r=t.filter(s=>!/^Error:\s*expect\(/.test(s));if(r.length===0)return t[0]??null;let a=r[0],n=r[1];return n!==void 0&&/^Received:/.test(n)&&/^Expected:/.test(a)?`${a} \xB7 ${n}`:a}function Jt(e){switch(e){case "passed":return "passed";case "failed":return "failed";case "timedOut":return "timedout";case "skipped":return "skipped";case "interrupted":return "failed";default:return "failed"}}function po(e,t,r){let a=`${e}::${t}::${r}`;return crypto.createHash("sha256").update(a).digest("hex").substring(0,16)}function fo(e){return e.length<=3?"":e[e.length-2]}function go(e){let t=e.findIndex(r=>r.status==="passed");return t>0?`passed on retry ${t}`:null}function en(e){return e===null?"error":e>=200&&e<300?"2xx":e>=300&&e<400?"3xx":e>=400&&e<500?"4xx":e>=500&&e<600?"5xx":"error"}function ho(e){let t={"2xx":0,"3xx":0,"4xx":0,"5xx":0,error:0},r=t;for(let a of e){let n=typeof a.responseStatusCode=="number"?a.responseStatusCode:null;r[en(n)]++;}return t}function Xa(e){if(e){for(let t of e)if(t.status==="failed")return {type:t.type,expected:Za(t.expected),actual:Za(t.actual),...t.expression?{expression:t.expression}:{}}}}function Za(e){if(e===void 0)return "undefined";if(e===null)return "null";let t;if(typeof e=="string")t=e;else try{t=JSON.stringify(e)??String(e);}catch{t=String(e);}return t.length>200?t.slice(0,200)+"\u2026":t}function Le(e,t){if(!e)return 0;let r=0;for(let a of e)a.status===t&&r++;return r}function tn(e,t){for(let r of e)t[r.category]=(t[r.category]??0)+1,r.children.length>0&&tn(r.children,t);}function mo(e){if(e.length===0)return null;let t=[...e].sort((a,n)=>a-n),r=t.reduce((a,n)=>a+n,0);return {avg:Math.round(r/t.length),min:t[0],max:t[t.length-1],p50:Y(t,50),p95:Y(t,95),p99:Y(t,99)}}var rn=2e5,De=1e5;function Qa(e,t,r,a,n,s,l){let i={retryIndex:e,apiCalls:t?.length??0,apiUrls:[],methodCounts:{},statusRanges:{"2xx":0,"3xx":0,"4xx":0,"5xx":0,error:0},responseTimes:[],passedAssertions:0,failedAssertions:0,assertions:r.length,navigations:a.length,navUrls:[],networkRequests:n,consoleLogs:s,actionSteps:l?.length??0,actionCategories:{}};if(t){let o=new Set;for(let c of t){let p=typeof c.method=="string"?c.method:"OTHER";i.methodCounts[p]=(i.methodCounts[p]??0)+1;let v=typeof c.url=="string"?c.url:null;v&&o.size<De&&o.add(St(v));let g=typeof c.responseStatusCode=="number"?c.responseStatusCode:typeof c.statusCode=="number"?c.statusCode:null,f=g!==null&&Number.isFinite(g)?g:null;i.statusRanges[en(f)]++;let m=typeof c.responseTimeMs=="number"?c.responseTimeMs:typeof c.durationMs=="number"?c.durationMs:null;m!==null&&Number.isFinite(m)&&i.responseTimes.length<rn&&i.responseTimes.push(m);}i.apiUrls=Array.from(o);}for(let o of r)o.status==="passed"?i.passedAssertions++:i.failedAssertions++;let d=new Set;for(let o of a)o.url&&d.size<De&&d.add(o.url);return i.navUrls=Array.from(d),l&&tn(l,i.actionCategories),i}function vo(){return {apiUrlSet:new Set,methodCounts:{},statusRanges:{"2xx":0,"3xx":0,"4xx":0,"5xx":0,error:0},responseTimes:[],passedAssertions:0,failedAssertions:0,navUrlSet:new Set,actionCategoryCounts:{},volApiCalls:0,volAssertions:0,volNavigations:0,volNetworkRequests:0,volConsoleLogs:0,volActionSteps:0}}function bo(e){return process.env.BROWSERSTACK_USERNAME?"BrowserStack":e?{"github-actions":"GitHub Actions","gitlab-ci":"GitLab CI",jenkins:"Jenkins",circleci:"CircleCI","bitbucket-pipelines":"Bitbucket Pipelines"}[e.provider]??"CI":"Local"}function yo(){let e=os$1.platform();return e==="win32"?"Windows":e==="darwin"?"macOS":"Linux"}function xo(e){let t=e.parent?.project?.()?.use;if(!t)return;let{browserName:r,channel:a}=t;if(a){let s=a.toLowerCase();return s==="chrome"||s==="chrome-stable"?"Chrome":s.startsWith("chrome-beta")?"Chrome Beta":s.startsWith("chrome-dev")?"Chrome Dev":s.startsWith("chrome-canary")?"Chrome Canary":s==="msedge"||s==="msedge-stable"?"Microsoft Edge":s.startsWith("msedge-beta")?"Edge Beta":s.startsWith("msedge-dev")?"Edge Dev":s.startsWith("msedge-canary")?"Edge Canary":a}if(r){let s=r.toLowerCase();return s==="chromium"?"Chromium":s==="firefox"?"Firefox":s==="webkit"?"WebKit":r}let n=t.defaultBrowserType;if(n){let s=n.toLowerCase();return s==="chromium"?"Chromium":s==="firefox"?"Firefox":s==="webkit"?"WebKit":n}}function wo(e,t,r){let a=r.parent?.project?.(),n="unknown";a!==void 0&&(a.use.isMobile===true?n="mobile":n="e2e");let s=["e2e","api","unit","mobile"];for(let d of s)if(e.some(o=>o===`@${d}`||o===d))return d;let l=t.replace(/\\/g,"/"),i=["e2e","api","unit"];for(let d of i)if(l.includes(`/${d}/`))return d;return n}var ko="__testrelic_api_config";function So(e){return JSON.stringify(e,(t,r)=>r instanceof RegExp?{__regexp:true,source:r.source,flags:r.flags}:r)}var Ne=class{constructor(t){this.rootDir="";this.startedAt="";this.testRunId="";this.collectedTests=[];this.fixtureDataReceived=false;this.navigationsCaptured=false;this.memoryPressureWarned=false;this.testCount=0;this.cloudClient=null;this.cloudRunId=null;this.runTimestamp="";this.updatingSnapshots=false;this.cloudReceipt=fe;this.pendingArtifactEntries=[];this.detectedOs="";this.detectedSource="";this.cloudTestsBuffer=[];this.activeReportMode="embedded";this.streamingWriter=null;this.testIndex=[];this.summaryCounters={total:0,passed:0,failed:0,flaky:0,skipped:0,timedOut:0,interrupted:0,totalApiCalls:0,totalAssertions:0,totalNavigations:0,totalNetworkRequests:0,totalConsoleLogs:0,totalActionSteps:0,visualChecks:0,visualFailures:0,visualDomChanges:0};this.streamingTestAgg=new Map;this.config=gr(t),this.apiConfig=fr(t);}upsertAggContribution(t,r){let a=this.streamingTestAgg.get(t);(!a||r.retryIndex>a.retryIndex)&&this.streamingTestAgg.set(t,r);}async onBegin(t,r){try{this.rootDir=t.rootDir;let a=t.updateSnapshots;if(this.updatingSnapshots=a==="all"||a==="changed",this.startedAt=new Date().toISOString(),this.testRunId=this.config.testRunId??crypto.randomUUID(),this.config.reportMode==="auto"){let i=r.allTests().length;this.activeReportMode=i>=this.config.streamingThreshold?"streaming":"embedded";}else this.activeReportMode=this.config.reportMode;let n=r.allTests().length;if(process.stderr.write(`[testrelic] Report mode: ${this.activeReportMode} (${n} tests)
4297
+ `),this.activeReportMode==="streaming"){let i=path.dirname(this.config.outputPath);this.streamingWriter=new ie(i);let d=()=>{if(this.streamingWriter){try{this.streamingWriter.writeIndex(this.testIndex);let o={testRunId:this.testRunId??"",startedAt:this.startedAt,completedAt:new Date().toISOString(),totalDuration:Date.now()-new Date(this.startedAt).getTime(),...this.summaryCounters,metadata:null,reportMode:"streaming",files:[],enrichedSummary:null,validation:null,writeErrors:this.streamingWriter.getWriteErrors(),cloudUpload:this.cloudReceipt};this.streamingWriter.writeSummary(o);}catch{}this.streamingWriter.dispose();}try{if(this.cloudClient?.isCloudMode()&&this.cloudRunId){let o=this.cloudClient.getAccessToken();o&&Bt(this.cloudClient.getEndpoint(),o,this.cloudRunId,this.startedAt);}}catch{}};process.on("SIGTERM",d),process.on("SIGINT",d);}if(this.config.includeArtifacts){let i=path.dirname(this.config.outputPath);this.runTimestamp=Jr(path.join(i,"artifacts"));try{ua(path.join(i,"artifacts"));}catch{}}let s=U();this.detectedOs=yo(),this.detectedSource=bo(s);let l=await J(async()=>(this.cloudClient=new _e(this.config.cloud),await this.cloudClient.initialize(),Ga(this.cloudClient,this.config.cloud?.uploadStrategy,this.testRunId,this.startedAt,this.detectedSource)),fa,"Cloud initialization");if(this.cloudRunId=l??null,l===void 0&&this.cloudClient?.switchToLocalMode("init_error"),this.cloudReceipt=Wa(this.cloudClient,this.config.cloud?.uploadStrategy,this.cloudRunId),this.activeReportMode!=="streaming"){let i=()=>{try{if(this.cloudClient?.isCloudMode()&&this.cloudRunId){let d=this.cloudClient.getAccessToken();d&&Bt(this.cloudClient.getEndpoint(),d,this.cloudRunId,this.startedAt);}}catch{}};process.on("SIGTERM",i),process.on("SIGINT",i);}}catch{}}onTestBegin(t,r){try{t.annotations.push({type:ko,description:So(this.apiConfig)});}catch{}}onTestEnd(t,r){try{let a=r,n=t.outcome(),s;n==="flaky"?s="flaky":n==="skipped"?s="skipped":s=Jt(a.status);let l=a.startTime.toISOString(),i=new Date(a.startTime.getTime()+a.duration).toISOString(),d=t.tags?[...t.tags]:t.annotations.filter(A=>A.type==="tag").map(A=>A.description??""),o=Va(a.attachments,t.annotations,t.title,s==="skipped");(o.navigations.length>0||o.networkRequestsCount>0||o.apiCallsCount>0)&&(this.fixtureDataReceived=!0),o.navigations.length>0&&(this.navigationsCaptured=!0),!this.config.captureNavigation&&o.navigations.length>0&&(o={...o,navigations:[]});let c=null;if(s==="failed"||s==="flaky"){let T=(s==="flaky"?t.results.find(F=>F.status!=="passed")?.errors??[]:a.errors)[0];if(T){let F=mr(this.config.redactPatterns),ae=T.location?.file??null,I=o.apiAssertions.find(an=>an.status==="failed")?.location??null,Z=I!==null&&(ae===null||path.resolve(ae)!==path.resolve(t.location.file)),Qt=Z?I.file:ae,Fe=Z?I.line:T.location?.line??null,ne=null;this.config.includeCodeSnippets&&Fe!==null&&Qt&&(ne=hr(Qt,Fe,this.config.codeContextLines),ne&&(ne=F(ne))),c={message:F(ke(T.message??"Unknown error")),line:Fe,code:ne,stack:this.config.includeStackTrace&&T.stack?F(ke(T.stack)):null};}}let p=t.titlePath().filter(Boolean),v=path.relative(this.rootDir||".",t.location.file).replace(/\\/g,"/"),g=fo(p),f=p.join(" > "),m=v,u=po(m,g,f),h=wo(d,m,t),b=h!=="mobile"&&o.apiCallsCount>0?"api":h,y=n==="flaky",w=go(t.results),C=Jt(t.expectedStatus),P=Jt(a.status),R=null;if(this.config.includeArtifacts&&s!=="skipped"&&a.attachments){let A=path.dirname(this.config.outputPath),T=p[p.length-1]??t.title;R=Xr(a.attachments,T,a.retry,A,this.runTimestamp||void 0,u);}let _=null;if(this.config.includeArtifacts&&s!=="skipped"&&a.attachments)try{let A=ta(a.attachments,{updatingSnapshots:this.updatingSnapshots,failureMessage:a.errors.map(T=>T.message??"").join(`
4298
+ `)});if(A.length>0){let T=path.dirname(this.config.outputPath),F=p[p.length-1]??t.title;_=Zr(A,F,a.retry,T,this.runTimestamp||void 0,u);}}catch{}let L=this.config.includeActionSteps&&a.steps?ya(a.steps,a.startTime):null,x=xo(t),S={titlePath:p,title:f,status:s,duration:a.duration,startedAt:l,completedAt:i,retryCount:t.results.length-1,retry:a.retry,tags:d,failure:c,specFile:v,navigations:o.navigations,testId:u,filePath:m,suiteName:g,testType:b,isFlaky:y,retryStatus:w,expectedStatus:C,actualStatus:P,artifacts:R,visual:_,apiAssertions:o.apiAssertions,actions:L,networkRequestsFile:o.networkRequestsFile,networkRequestsCount:o.networkRequestsCount,consoleLogsFile:o.consoleLogsFile,consoleLogsCount:o.consoleLogsCount,apiCallsFile:o.apiCallsFile,apiCallsCount:o.apiCallsCount,apiStatusMix:o.apiStatusMix,browser:x,os:this.detectedOs||void 0,source:this.detectedSource||void 0},X=o.consoleLogsFile&&o.consoleLogsCount>0?j(o.consoleLogsFile):void 0,G=o.networkRequestsFile&&o.networkRequestsCount>0?j(o.networkRequestsFile):void 0,re=o.apiCallsFile&&o.apiCallsCount>0?j(o.apiCallsFile):void 0,Yt=re?Lt(re):void 0,Xt=!G&&re?Dt(re):void 0,Zt=o.apiAssertions&&o.apiAssertions.length>0?Nt(o.apiAssertions):void 0;this.cloudTestsBuffer.push({testId:u,title:f,status:s,duration:a.duration,suiteName:g,filePath:m,testType:b,isFlaky:y,startedAt:l,completedAt:i,tags:d,failure:c,retry:a.retry,retryCount:t.results.length-1,retryStatus:w,browser:x,os:this.detectedOs||void 0,source:this.detectedSource||void 0,...L&&L.length>0?{actions:L}:{},...o.navigations&&o.navigations.length>0?{navigations:o.navigations}:{},...X?{consoleLogs:X}:{},...G?{networkRequests:G}:Xt?{networkRequests:Xt}:{},...Yt?{apiCalls:Yt}:{},...Zt?{protocolAssertions:Zt}:{}});try{let A=this.config.cloud?.uploadStrategy;if(this.cloudClient?.isCloudMode()&&this.cloudRunId&&(A==="realtime"||A==="both")){let T=this.cloudClient.getAccessToken();T&&La(this.cloudClient.getEndpoint(),T,this.cloudRunId,this.buildCloudTestPayload(S));}}catch{}if(this.activeReportMode==="streaming"&&this.streamingWriter){let A=Be(m,p,t.id??"",a.retry),T=a.retry===t.results.length-1,F={id:A,navigations:o.navigations,apiAssertions:o.apiAssertions,actions:L??[],artifacts:R,visual:_,failureDiagnostic:c,hasNetworkFile:o.networkRequestsFile!==null,networkRequestsCount:o.networkRequestsCount,hasConsoleFile:o.consoleLogsFile!==null,consoleLogsCount:o.consoleLogsCount,hasApiCallsFile:o.apiCallsFile!==null,apiCallsCount:o.apiCallsCount,startedAt:l};this.streamingWriter.writeTestDetail(A,F,{networkRequestsFile:o.networkRequestsFile,consoleLogsFile:o.consoleLogsFile,apiCallsFile:o.apiCallsFile});let ae={id:A,title:p[p.length-1]??t.title,titlePath:p,filePath:m,status:s,duration:a.duration,project:p[1]??"",retryIndex:a.retry,tags:d,hasNetworkData:o.networkRequestsCount>0,hasConsoleData:o.consoleLogsCount>0,hasArtifacts:R!==null,hasActionSteps:(L?.length??0)>0,visualCount:_?.length??0,visualFailedCount:_?.filter(I=>I.status==="failed").length??0,domChangeCount:_?.reduce((I,Z)=>I+(Z.domChangeCount??0),0)??0,...(()=>{let I=_?.find(Z=>Z.diff);return I?.diff?{visualDiffPath:I.diff}:{}})(),errorMessage:uo(c?.message),networkCount:o.networkRequestsCount,consoleCount:o.consoleLogsCount,actionCount:L?.length??0,testType:b,apiCallCount:o.apiCallsCount,...o.apiStatusMix?{apiStatusMix:o.apiStatusMix}:{},assertionsPassed:Le(o.apiAssertions,"passed"),assertionsFailed:Le(o.apiAssertions,"failed"),...(()=>{let I=Xa(o.apiAssertions);return I?{firstFailedAssertion:I}:{}})(),isRetry:!T,retryGroupKey:Oe(m,p,t.id??"")};if(this.testIndex.push(ae),T)switch(this.summaryCounters.total++,s){case "passed":this.summaryCounters.passed++;break;case "failed":this.summaryCounters.failed++;break;case "flaky":this.summaryCounters.flaky++;break;case "skipped":this.summaryCounters.skipped++;break;case "timedout":this.summaryCounters.timedOut++;break;default:this.summaryCounters.interrupted++;break}this.upsertAggContribution(`${m}::${p.join(" > ")}`,Qa(a.retry,re,o.apiAssertions,o.navigations,o.networkRequestsCount,o.consoleLogsCount,L)),(R||_)&&this.pendingArtifactEntries.push({testId:u,artifacts:R??{},visual:_,outputDir:path.dirname(this.config.outputPath)}),S.actions=null,S.apiAssertions=null,S.navigations=[],S.artifacts=null,S.failure=null,S.networkRequestsFile=null,S.consoleLogsFile=null,S.apiCallsFile=null;}this.activeReportMode!=="streaming"&&this.collectedTests.push(S),this.testCount++,this.checkMemoryPressure();}catch{}}async onEnd(t){try{let r=new Date().toISOString(),a=new Date(this.startedAt).getTime(),s=new Date(r).getTime()-a,l=this.activeReportMode==="streaming"?this.testIndex.some(d=>d.status!=="skipped"):this.collectedTests.some(d=>d.status!=="skipped");!this.fixtureDataReceived&&l&&process.stderr.write(`
2595
4299
  \u26A0 TestRelic: No fixture data received from any test.
2596
4300
  To enable network logs, video sync, and Test Navigation tracking, import the TestRelic fixture:
2597
4301
  import { test, expect } from '@testrelic/playwright-analytics/fixture';
@@ -2600,14 +4304,14 @@ ${a}
2600
4304
 
2601
4305
  `),this.config.captureNavigation?this.fixtureDataReceived&&!this.navigationsCaptured&&l&&process.stderr.write(`[testrelic] Test Navigation: 0 navigation events captured this run. Navigation is recorded automatically on page.goto()/clicks; if you expected a navigation graph, ensure your tests drive page navigations (and use the '@testrelic/playwright-analytics/fixture' import).
2602
4306
  `):process.stderr.write(`[testrelic] Test Navigation capture is disabled (captureNavigation: false). Remove the option or set it to true to capture the navigation graph.
2603
- `),await H(()=>mr(),Ze,"Artifact flush");let i=null;if(this.config.includeArtifacts&&this.runTimestamp)try{let d=path.dirname(this.config.outputPath);i=xr(d,this.runTimestamp);let o=path.join(d,"artifact-manifest.json"),c=JSON.stringify(i),g=o+".tmp";fs$1.writeFileSync(g,c,"utf-8"),fs$1.renameSync(g,o);}catch{}if(this.activeReportMode==="streaming"&&this.streamingWriter)await H(()=>this.finalizeStreamingReport(r,s,e,i),Ye,"Report finalization");else {let d=this.buildTimeline(),o=new Map;for(let h of this.collectedTests){let u=o.get(h.testId);(!u||h.retry>u.retry)&&o.set(h.testId,h);}let c=Array.from(o.values()),g=Mr(c,d.length),m={schemaVersion:_e,testRunId:this.testRunId,startedAt:this.startedAt,completedAt:r,totalDuration:s,summary:g,ci:P(),metadata:this.config.metadata,timeline:d,shardRunIds:null};if(this.writeReport(m),await H(()=>je(m,this.config,i),Ze,"HTML report write"),Xe(g,this.config.outputPath,this.config.htmlReportPath,this.config.quiet),this.cloudClient){let h=this.collectArtifactEntries(),u=this.cloudTestsBuffer.slice();this.cloudTestsBuffer.length=0;let p=await H(()=>kt(this.cloudClient,this.config.cloud,this.config.cloud?.uploadStrategy,this.testRunId,this.cloudRunId,m,r,s,g,h,u),Ye,"Cloud upload");this.cloudReceipt=Tt(this.cloudReceipt,p,this.testRunId);}}}catch{}finally{await this.disposeCloudClient();}}async disposeCloudClient(){let e=this.cloudClient;e&&(this.cloudClient=null,await H(()=>e.dispose(),Rr,"Cloud client shutdown"));}collectArtifactEntries(){if(this.activeReportMode==="streaming")return this.pendingArtifactEntries;let e=path.dirname(this.config.outputPath),r=[];for(let n of this.collectedTests)n.artifacts&&r.push({testId:n.testId,artifacts:n.artifacts,outputDir:e});return r}async finalizeStreamingReport(e,r,n,a){if(!this.streamingWriter)return;this.summaryCounters={total:0,passed:0,failed:0,flaky:0,skipped:0,timedOut:0,interrupted:0,totalApiCalls:0,totalAssertions:0,totalNavigations:0,totalNetworkRequests:0,totalConsoleLogs:0,totalActionSteps:0};let s=new Map,l=new Map;for(let f of this.testIndex){let v=`${f.filePath}::${f.titlePath.join(" > ")}`,w=s.get(v);(w===void 0||f.retryIndex>w)&&s.set(v,f.retryIndex);}let i=0;for(let f of this.testIndex){let v=`${f.filePath}::${f.titlePath.join(" > ")}`,w=s.get(v);if(f.isRetry=f.retryIndex<w,f.isRetry)continue;switch(i++,this.summaryCounters.total++,f.status){case "passed":this.summaryCounters.passed++;break;case "failed":this.summaryCounters.failed++;break;case "flaky":this.summaryCounters.flaky++;break;case "skipped":this.summaryCounters.skipped++;break;case "timedout":this.summaryCounters.timedOut++;break;default:this.summaryCounters.interrupted++;break}let y=l.get(f.filePath);switch(y||(y={filePath:f.filePath,total:0,passed:0,failed:0,flaky:0,skipped:0,timedOut:0},l.set(f.filePath,y)),y.total++,f.status){case "passed":y.passed++;break;case "failed":y.failed++;break;case "flaky":y.flaky++;break;case "skipped":y.skipped++;break;case "timedout":y.timedOut++;break}}let d=Array.from(l.values()),o=Qa();for(let f of this.streamingTestAgg.values()){o.volApiCalls+=f.apiCalls,o.volAssertions+=f.assertions,o.volNavigations+=f.navigations,o.volNetworkRequests+=f.networkRequests,o.volConsoleLogs+=f.consoleLogs,o.volActionSteps+=f.actionSteps;for(let v in f.methodCounts)o.methodCounts[v]=(o.methodCounts[v]??0)+f.methodCounts[v];o.statusRanges["2xx"]+=f.statusRanges["2xx"],o.statusRanges["3xx"]+=f.statusRanges["3xx"],o.statusRanges["4xx"]+=f.statusRanges["4xx"],o.statusRanges["5xx"]+=f.statusRanges["5xx"],o.statusRanges.error+=f.statusRanges.error,o.passedAssertions+=f.passedAssertions,o.failedAssertions+=f.failedAssertions;for(let v in f.actionCategories)o.actionCategoryCounts[v]=(o.actionCategoryCounts[v]??0)+f.actionCategories[v];for(let v of f.apiUrls){if(o.apiUrlSet.size>=Te)break;o.apiUrlSet.add(v);}for(let v of f.navUrls){if(o.navUrlSet.size>=Te)break;o.navUrlSet.add(v);}for(let v of f.responseTimes){if(o.responseTimes.length>=un)break;o.responseTimes.push(v);}}this.summaryCounters.totalApiCalls=o.volApiCalls,this.summaryCounters.totalAssertions=o.volAssertions,this.summaryCounters.totalNavigations=o.volNavigations,this.summaryCounters.totalNetworkRequests=o.volNetworkRequests,this.summaryCounters.totalConsoleLogs=o.volConsoleLogs,this.summaryCounters.totalActionSteps=o.volActionSteps,this.streamingWriter.writeIndex(this.testIndex),this.streamingWriter.writeCompactIndex(this.testIndex);let c={reporterTotal:this.summaryCounters.total,indexTotal:i,playwrightStatus:n.status,isValid:this.summaryCounters.total===i};c.isValid||process.stderr.write(`[testrelic] WARNING: Summary count mismatch \u2014 reporter: ${c.reporterTotal}, index: ${c.indexTotal}
2604
- `);let g={total:this.summaryCounters.total,passed:this.summaryCounters.passed,failed:this.summaryCounters.failed,flaky:this.summaryCounters.flaky,skipped:this.summaryCounters.skipped,timedout:this.summaryCounters.timedOut,totalApiCalls:this.summaryCounters.totalApiCalls,uniqueApiUrls:o.apiUrlSet.size,apiCallsByMethod:{...o.methodCounts},apiCallsByStatusRange:{"2xx":o.statusRanges["2xx"],"3xx":o.statusRanges["3xx"],"4xx":o.statusRanges["4xx"],"5xx":o.statusRanges["5xx"],error:o.statusRanges.error},apiResponseTime:Za(o.responseTimes),totalAssertions:this.summaryCounters.totalAssertions,passedAssertions:o.passedAssertions,failedAssertions:o.failedAssertions,totalNavigations:this.summaryCounters.totalNavigations,uniqueNavigationUrls:o.navUrlSet.size,totalTimelineSteps:i,totalActionSteps:this.summaryCounters.totalActionSteps,actionStepsByCategory:{...o.actionCategoryCounts}},m={testRunId:this.testRunId,startedAt:this.startedAt,completedAt:e,totalDuration:r,...this.summaryCounters,metadata:this.config.metadata,reportMode:"streaming",files:d,enrichedSummary:g,validation:c,writeErrors:this.streamingWriter.getWriteErrors(),cloudUpload:this.cloudReceipt};this.streamingWriter.writeSummary(m);let h={schemaVersion:"2.0",generatedAt:new Date().toISOString(),reportMode:"streaming",summaryFile:"summary.json",indexFile:"index.json",testsDir:"tests",artifactsDir:"artifacts",testCount:this.testIndex.length,totalSizeBytes:this.streamingWriter.computeTotalSize()};this.streamingWriter.writeManifest(h),await je(this.buildStreamingReport(m,g,e,r),this.config,a),Xe(g,this.config.outputPath,this.config.htmlReportPath,this.config.quiet);let u=this.streamingWriter.getReportDir();process.stderr.write(`[testrelic] Streaming report written to ${u}
4307
+ `),await J(()=>Kr(),kt,"Artifact flush");let i=null;if(this.config.includeArtifacts&&this.runTimestamp)try{let d=path.dirname(this.config.outputPath);i=la(d,this.runTimestamp);let o=path.join(d,"artifact-manifest.json"),c=JSON.stringify(i),p=o+".tmp";fs$1.writeFileSync(p,c,"utf-8"),fs$1.renameSync(p,o);}catch{}if(this.activeReportMode==="streaming"&&this.streamingWriter)await J(()=>this.finalizeStreamingReport(r,s,t,i),wt,"Report finalization");else {let d=this.buildTimeline(),o=new Map;for(let g of this.collectedTests){let f=o.get(g.testId);(!f||g.retry>f.retry)&&o.set(g.testId,g);}let c=Array.from(o.values()),p=ba(c,d.length),v={schemaVersion:ze,testRunId:this.testRunId,startedAt:this.startedAt,completedAt:r,totalDuration:s,summary:p,ci:U(),metadata:this.config.metadata,timeline:d,shardRunIds:null};if(this.writeReport(v),await J(()=>gt(v,this.config,i),kt,"HTML report write"),Ct(p,this.config.outputPath,this.config.htmlReportPath,this.config.quiet),this.cloudClient){let g=this.collectArtifactEntries(),f=this.cloudTestsBuffer.slice();this.cloudTestsBuffer.length=0;let m=await J(()=>Gt(this.cloudClient,this.config.cloud,this.config.cloud?.uploadStrategy,this.testRunId,this.cloudRunId,v,r,s,p,g,f),wt,"Cloud upload");this.cloudReceipt=Wt(this.cloudReceipt,m,this.testRunId);}}}catch{}finally{await this.disposeCloudClient();}}async disposeCloudClient(){let t=this.cloudClient;t&&(this.cloudClient=null,await J(()=>t.dispose(),ga,"Cloud client shutdown"));}collectArtifactEntries(){if(this.activeReportMode==="streaming")return this.pendingArtifactEntries;let t=path.dirname(this.config.outputPath),r=[];for(let a of this.collectedTests)(a.artifacts||a.visual)&&r.push({testId:a.testId,artifacts:a.artifacts??{},visual:a.visual,outputDir:t});return r}async finalizeStreamingReport(t,r,a,n){if(!this.streamingWriter)return;this.summaryCounters={total:0,passed:0,failed:0,flaky:0,skipped:0,timedOut:0,interrupted:0,totalApiCalls:0,totalAssertions:0,totalNavigations:0,totalNetworkRequests:0,totalConsoleLogs:0,totalActionSteps:0,visualChecks:0,visualFailures:0,visualDomChanges:0};let s=new Map,l=new Map;for(let u of this.testIndex){let h=`${u.filePath}::${u.titlePath.join(" > ")}`,b=s.get(h);(b===void 0||u.retryIndex>b)&&s.set(h,u.retryIndex);}let i=0;for(let u of this.testIndex){let h=`${u.filePath}::${u.titlePath.join(" > ")}`,b=s.get(h);if(u.isRetry=u.retryIndex<b,u.isRetry)continue;switch(i++,this.summaryCounters.total++,u.status){case "passed":this.summaryCounters.passed++;break;case "failed":this.summaryCounters.failed++;break;case "flaky":this.summaryCounters.flaky++;break;case "skipped":this.summaryCounters.skipped++;break;case "timedout":this.summaryCounters.timedOut++;break;default:this.summaryCounters.interrupted++;break}this.summaryCounters.visualChecks+=u.visualCount??0,this.summaryCounters.visualFailures+=u.visualFailedCount??0,this.summaryCounters.visualDomChanges+=u.domChangeCount??0;let y=l.get(u.filePath);switch(y||(y={filePath:u.filePath,total:0,passed:0,failed:0,flaky:0,skipped:0,timedOut:0},l.set(u.filePath,y)),y.total++,u.status){case "passed":y.passed++;break;case "failed":y.failed++;break;case "flaky":y.flaky++;break;case "skipped":y.skipped++;break;case "timedout":y.timedOut++;break}}let d=Array.from(l.values()),o=vo();for(let u of this.streamingTestAgg.values()){o.volApiCalls+=u.apiCalls,o.volAssertions+=u.assertions,o.volNavigations+=u.navigations,o.volNetworkRequests+=u.networkRequests,o.volConsoleLogs+=u.consoleLogs,o.volActionSteps+=u.actionSteps;for(let h in u.methodCounts)o.methodCounts[h]=(o.methodCounts[h]??0)+u.methodCounts[h];o.statusRanges["2xx"]+=u.statusRanges["2xx"],o.statusRanges["3xx"]+=u.statusRanges["3xx"],o.statusRanges["4xx"]+=u.statusRanges["4xx"],o.statusRanges["5xx"]+=u.statusRanges["5xx"],o.statusRanges.error+=u.statusRanges.error,o.passedAssertions+=u.passedAssertions,o.failedAssertions+=u.failedAssertions;for(let h in u.actionCategories)o.actionCategoryCounts[h]=(o.actionCategoryCounts[h]??0)+u.actionCategories[h];for(let h of u.apiUrls){if(o.apiUrlSet.size>=De)break;o.apiUrlSet.add(h);}for(let h of u.navUrls){if(o.navUrlSet.size>=De)break;o.navUrlSet.add(h);}for(let h of u.responseTimes){if(o.responseTimes.length>=rn)break;o.responseTimes.push(h);}}this.summaryCounters.totalApiCalls=o.volApiCalls,this.summaryCounters.totalAssertions=o.volAssertions,this.summaryCounters.totalNavigations=o.volNavigations,this.summaryCounters.totalNetworkRequests=o.volNetworkRequests,this.summaryCounters.totalConsoleLogs=o.volConsoleLogs,this.summaryCounters.totalActionSteps=o.volActionSteps,this.streamingWriter.writeIndex(this.testIndex),this.streamingWriter.writeCompactIndex(this.testIndex);let c={reporterTotal:this.summaryCounters.total,indexTotal:i,playwrightStatus:a.status,isValid:this.summaryCounters.total===i};c.isValid||process.stderr.write(`[testrelic] WARNING: Summary count mismatch \u2014 reporter: ${c.reporterTotal}, index: ${c.indexTotal}
4308
+ `);let p={total:this.summaryCounters.total,passed:this.summaryCounters.passed,failed:this.summaryCounters.failed,flaky:this.summaryCounters.flaky,skipped:this.summaryCounters.skipped,timedout:this.summaryCounters.timedOut,totalApiCalls:this.summaryCounters.totalApiCalls,uniqueApiUrls:o.apiUrlSet.size,apiCallsByMethod:{...o.methodCounts},apiCallsByStatusRange:{"2xx":o.statusRanges["2xx"],"3xx":o.statusRanges["3xx"],"4xx":o.statusRanges["4xx"],"5xx":o.statusRanges["5xx"],error:o.statusRanges.error},apiResponseTime:mo(o.responseTimes),totalAssertions:this.summaryCounters.totalAssertions,passedAssertions:o.passedAssertions,failedAssertions:o.failedAssertions,totalNavigations:this.summaryCounters.totalNavigations,uniqueNavigationUrls:o.navUrlSet.size,totalTimelineSteps:i,totalActionSteps:this.summaryCounters.totalActionSteps,actionStepsByCategory:{...o.actionCategoryCounts},visualChecks:this.summaryCounters.visualChecks,visualFailures:this.summaryCounters.visualFailures,visualDomChanges:this.summaryCounters.visualDomChanges},v={testRunId:this.testRunId,startedAt:this.startedAt,completedAt:t,totalDuration:r,...this.summaryCounters,metadata:this.config.metadata,reportMode:"streaming",files:d,enrichedSummary:p,validation:c,writeErrors:this.streamingWriter.getWriteErrors(),cloudUpload:this.cloudReceipt};this.streamingWriter.writeSummary(v);let g={schemaVersion:"2.0",generatedAt:new Date().toISOString(),reportMode:"streaming",summaryFile:"summary.json",indexFile:"index.json",testsDir:"tests",artifactsDir:"artifacts",testCount:this.testIndex.length,totalSizeBytes:this.streamingWriter.computeTotalSize()};this.streamingWriter.writeManifest(g),await gt(this.buildStreamingReport(v,p,t,r),this.config,n),Ct(p,this.config.outputPath,this.config.htmlReportPath,this.config.quiet);let f=this.streamingWriter.getReportDir();process.stderr.write(`[testrelic] Streaming report written to ${f}
2605
4309
  [testrelic] ${this.testIndex.length} test detail files on disk
2606
- [testrelic] View report: npx testrelic serve ${u}
4310
+ [testrelic] View report: npx testrelic serve ${f}
2607
4311
  `),this.streamingWriter.getWriteErrors().length>0&&process.stderr.write(`[testrelic] WARNING: ${this.streamingWriter.getWriteErrors().length} write error(s) during streaming
2608
- `);let p=this.streamingWriter;try{if(this.cloudClient){let f=this.buildStreamingReport(m,g,e,r),v=this.collectArtifactEntries(),w=this.cloudTestsBuffer.slice();this.cloudTestsBuffer.length=0;let y=await kt(this.cloudClient,this.config.cloud,this.config.cloud?.uploadStrategy,this.testRunId,this.cloudRunId,f,e,r,g,v,w);this.cloudReceipt=Tt(this.cloudReceipt,y,this.testRunId);let k={...m,cloudUpload:this.cloudReceipt};try{p.writeSummary(k);}catch{}}}finally{p.dispose();}this.testIndex.length=0,this.collectedTests.length=0,this.pendingArtifactEntries.length=0,this.cloudTestsBuffer.length=0,this.streamingWriter=null,this.cloudClient=null;}buildStreamingReport(e,r,n,a){return {schemaVersion:_e,testRunId:this.testRunId,startedAt:this.startedAt,completedAt:n,totalDuration:a,summary:r,ci:P(),metadata:this.config.metadata,timeline:[],shardRunIds:null}}printsToStdio(){return false}checkMemoryPressure(){try{let e=process.memoryUsage(),r=e.heapUsed/1024/1024;if(r<200)return;let n=e.heapUsed/e.heapTotal;n>.9&&this.activeReportMode==="embedded"?(process.stderr.write(`[testrelic] WARNING: High memory (${Math.round(r)} MB). Auto-switching to streaming mode.
2609
- `),this.switchToStreamingMode()):n>.8&&process.stderr.write(`[testrelic] WARNING: Memory pressure detected (${Math.round(r)} MB used).
2610
- `);}catch{}}switchToStreamingMode(){try{if(this.activeReportMode==="streaming")return;this.activeReportMode="streaming";let e=path.dirname(this.config.outputPath);this.streamingWriter=new X(e);for(let r of this.collectedTests){let n=Re(r.filePath,r.titlePath,r.testId,r.retry),a=r.apiCallsFile&&r.apiCallsCount>0?O(r.apiCallsFile):void 0,s={id:n,navigations:r.navigations??[],apiAssertions:r.apiAssertions??[],actions:r.actions??[],artifacts:r.artifacts,failureDiagnostic:r.failure,hasNetworkFile:r.networkRequestsFile!==null,networkRequestsCount:r.networkRequestsCount,hasConsoleFile:r.consoleLogsFile!==null,consoleLogsCount:r.consoleLogsCount,hasApiCallsFile:r.apiCallsFile!==null,apiCallsCount:r.apiCallsCount,startedAt:r.startedAt};this.streamingWriter.writeTestDetail(n,s,{networkRequestsFile:r.networkRequestsFile,consoleLogsFile:r.consoleLogsFile,apiCallsFile:r.apiCallsFile});let l={id:n,title:r.titlePath[r.titlePath.length-1]??r.title,titlePath:r.titlePath,filePath:r.filePath,status:r.status,duration:r.duration,project:r.titlePath[1]??"",retryIndex:r.retry,tags:r.tags,hasNetworkData:r.networkRequestsCount>0,hasConsoleData:r.consoleLogsCount>0,hasArtifacts:r.artifacts!==null,hasActionSteps:(r.actions?.length??0)>0,errorMessage:r.failure?.message?.split(`
2611
- `)[0]??null,networkCount:r.networkRequestsCount,consoleCount:r.consoleLogsCount,actionCount:r.actions?.length??0,isRetry:!1};switch(this.testIndex.push(l),this.summaryCounters.total++,r.status){case "passed":this.summaryCounters.passed++;break;case "failed":this.summaryCounters.failed++;break;case "flaky":this.summaryCounters.flaky++;break;case "skipped":this.summaryCounters.skipped++;break;case "timedout":this.summaryCounters.timedOut++;break;default:this.summaryCounters.interrupted++;break}this.upsertAggContribution(`${r.filePath}::${r.titlePath.join(" > ")}`,dn(r.retry,a,r.apiAssertions??[],r.navigations??[],r.networkRequestsCount,r.consoleLogsCount,r.actions??null));}this.collectedTests.length=0;}catch{}}buildTimeline(){let e=this.collectedTests.map(r=>({titlePath:r.titlePath,title:r.title,status:r.status,duration:r.duration,startedAt:r.startedAt,completedAt:r.completedAt,retryCount:r.retryCount,retry:r.retry,tags:r.tags,failure:r.failure,specFile:r.specFile,navigations:r.navigations,apiCalls:null,apiAssertions:r.apiAssertions,testId:r.testId,filePath:r.filePath,suiteName:r.suiteName,testType:r.testType,isFlaky:r.isFlaky,retryStatus:r.retryStatus,expectedStatus:r.expectedStatus,actualStatus:r.actualStatus,artifacts:r.artifacts,networkRequests:null,actions:r.actions,consoleLogs:null}));return Ir(e,{navigationTypes:this.config.navigationTypes})}readJsonlFile(e){if(!e)return null;let r=O(e);return r.length>0?r:null}toTestResult(e,r){let n=null,a=null,s=null;return r?.loadFiles&&(n=this.readJsonlFile(e.networkRequestsFile),a=this.readJsonlFile(e.consoleLogsFile),s=this.readJsonlFile(e.apiCallsFile)),{title:e.title,status:e.status,duration:e.duration,startedAt:e.startedAt,completedAt:e.completedAt,retryCount:e.retryCount,tags:e.tags,failure:e.failure,testId:e.testId,filePath:e.filePath,suiteName:e.suiteName,testType:e.testType,isFlaky:e.isFlaky,retryStatus:e.retryStatus,expectedStatus:e.expectedStatus,actualStatus:e.actualStatus,artifacts:e.artifacts,networkRequests:n,apiCalls:s,apiAssertions:e.apiAssertions,actions:e.actions??null,consoleLogs:a}}buildCloudTestPayload(e){let r={title:e.title,status:e.status,duration:e.duration,startedAt:e.startedAt,completedAt:e.completedAt,retry:e.retry,retryCount:e.retryCount,retryStatus:e.retryStatus,tags:e.tags,failure:e.failure,testId:e.testId,filePath:e.filePath,suiteName:e.suiteName,testType:e.testType,isFlaky:e.isFlaky,expectedStatus:e.expectedStatus,actualStatus:e.actualStatus,artifacts:e.artifacts,actions:e.actions??[],navigations:e.navigations??[],steps:e.actions??[],browser:e.browser,os:e.os,source:e.source};if(e.apiAssertions&&e.apiAssertions.length>0&&(r.protocolAssertions=ct(e.apiAssertions)),e.consoleLogsFile&&(r.consoleLogs=O(e.consoleLogsFile)),e.networkRequestsFile&&(r.networkRequests=O(e.networkRequestsFile)),e.apiCallsFile){let n=O(e.apiCallsFile);r.apiCalls=lt(n),(!r.networkRequests||r.networkRequests.length===0)&&(r.networkRequests=dt(n));}return r}writeReport(e){try{let r=JSON.stringify(e),n=this.config.outputPath,a=path.dirname(n);fs$1.mkdirSync(a,{recursive:!0});let s=n+".tmp";fs$1.writeFileSync(s,r,"utf-8"),fs$1.renameSync(s,n);}catch(r){process.stderr.write(`[testrelic] Failed to write report: ${r instanceof Error?r.message:String(r)}
2612
- `);}}};module.exports=Se;//# sourceMappingURL=reporter-entry.cjs.map
4312
+ `);let m=this.streamingWriter;try{if(this.cloudClient){let u=this.buildStreamingReport(v,p,t,r),h=this.collectArtifactEntries(),b=this.cloudTestsBuffer.slice();this.cloudTestsBuffer.length=0;let y=await Gt(this.cloudClient,this.config.cloud,this.config.cloud?.uploadStrategy,this.testRunId,this.cloudRunId,u,t,r,p,h,b);this.cloudReceipt=Wt(this.cloudReceipt,y,this.testRunId);let w={...v,cloudUpload:this.cloudReceipt};try{m.writeSummary(w);}catch{}}}finally{m.dispose();}this.testIndex.length=0,this.collectedTests.length=0,this.pendingArtifactEntries.length=0,this.cloudTestsBuffer.length=0,this.streamingWriter=null,this.cloudClient=null;}buildStreamingReport(t,r,a,n){return {schemaVersion:ze,testRunId:this.testRunId,startedAt:this.startedAt,completedAt:a,totalDuration:n,summary:r,ci:U(),metadata:this.config.metadata,timeline:[],shardRunIds:null}}printsToStdio(){return false}checkMemoryPressure(){try{let t=process.memoryUsage(),r=t.heapUsed/1024/1024;if(r<200)return;let a=t.heapUsed/t.heapTotal;a>.9&&this.activeReportMode==="embedded"?(process.stderr.write(`[testrelic] WARNING: High memory (${Math.round(r)} MB). Auto-switching to streaming mode.
4313
+ `),this.switchToStreamingMode()):a>.8&&r>=500&&!this.memoryPressureWarned&&(this.memoryPressureWarned=!0,process.stderr.write(`[testrelic] WARNING: Memory pressure detected (${Math.round(r)} MB used).
4314
+ `));}catch{}}switchToStreamingMode(){try{if(this.activeReportMode==="streaming")return;this.activeReportMode="streaming";let t=path.dirname(this.config.outputPath);this.streamingWriter=new ie(t);for(let r of this.collectedTests){let a=Be(r.filePath,r.titlePath,r.testId,r.retry),n=r.apiCallsFile&&r.apiCallsCount>0?j(r.apiCallsFile):void 0,s={id:a,navigations:r.navigations??[],apiAssertions:r.apiAssertions??[],actions:r.actions??[],artifacts:r.artifacts,failureDiagnostic:r.failure,hasNetworkFile:r.networkRequestsFile!==null,networkRequestsCount:r.networkRequestsCount,hasConsoleFile:r.consoleLogsFile!==null,consoleLogsCount:r.consoleLogsCount,hasApiCallsFile:r.apiCallsFile!==null,apiCallsCount:r.apiCallsCount,startedAt:r.startedAt};this.streamingWriter.writeTestDetail(a,s,{networkRequestsFile:r.networkRequestsFile,consoleLogsFile:r.consoleLogsFile,apiCallsFile:r.apiCallsFile});let l={id:a,title:r.titlePath[r.titlePath.length-1]??r.title,titlePath:r.titlePath,filePath:r.filePath,status:r.status,duration:r.duration,project:r.titlePath[1]??"",retryIndex:r.retry,tags:r.tags,hasNetworkData:r.networkRequestsCount>0,hasConsoleData:r.consoleLogsCount>0,hasArtifacts:r.artifacts!==null,hasActionSteps:(r.actions?.length??0)>0,errorMessage:r.failure?.message?.split(`
4315
+ `)[0]??null,networkCount:r.networkRequestsCount,consoleCount:r.consoleLogsCount,actionCount:r.actions?.length??0,testType:r.testType,apiCallCount:r.apiCallsCount,...r.apiStatusMix?{apiStatusMix:r.apiStatusMix}:n?{apiStatusMix:ho(n)}:{},assertionsPassed:Le(r.apiAssertions,"passed"),assertionsFailed:Le(r.apiAssertions,"failed"),...(()=>{let i=Xa(r.apiAssertions);return i?{firstFailedAssertion:i}:{}})(),isRetry:!1,retryGroupKey:Oe(r.filePath,r.titlePath,r.testId)};switch(this.testIndex.push(l),this.summaryCounters.total++,r.status){case "passed":this.summaryCounters.passed++;break;case "failed":this.summaryCounters.failed++;break;case "flaky":this.summaryCounters.flaky++;break;case "skipped":this.summaryCounters.skipped++;break;case "timedout":this.summaryCounters.timedOut++;break;default:this.summaryCounters.interrupted++;break}this.upsertAggContribution(`${r.filePath}::${r.titlePath.join(" > ")}`,Qa(r.retry,n,r.apiAssertions??[],r.navigations??[],r.networkRequestsCount,r.consoleLogsCount,r.actions??null));}this.collectedTests.length=0;}catch{}}buildTimeline(){let t=this.collectedTests.map(r=>({titlePath:r.titlePath,title:r.title,status:r.status,duration:r.duration,startedAt:r.startedAt,completedAt:r.completedAt,retryCount:r.retryCount,retry:r.retry,tags:r.tags,failure:r.failure,specFile:r.specFile,navigations:r.navigations,apiCalls:null,apiAssertions:r.apiAssertions,testId:r.testId,filePath:r.filePath,suiteName:r.suiteName,testType:r.testType,isFlaky:r.isFlaky,retryStatus:r.retryStatus,expectedStatus:r.expectedStatus,actualStatus:r.actualStatus,artifacts:r.artifacts,networkRequests:null,actions:r.actions,consoleLogs:null}));return va(t,{navigationTypes:this.config.navigationTypes})}readJsonlFile(t){if(!t)return null;let r=j(t);return r.length>0?r:null}toTestResult(t,r){let a=null,n=null,s=null;return r?.loadFiles&&(a=this.readJsonlFile(t.networkRequestsFile),n=this.readJsonlFile(t.consoleLogsFile),s=this.readJsonlFile(t.apiCallsFile)),{title:t.title,status:t.status,duration:t.duration,startedAt:t.startedAt,completedAt:t.completedAt,retryCount:t.retryCount,tags:t.tags,failure:t.failure,testId:t.testId,filePath:t.filePath,suiteName:t.suiteName,testType:t.testType,isFlaky:t.isFlaky,retryStatus:t.retryStatus,expectedStatus:t.expectedStatus,actualStatus:t.actualStatus,artifacts:t.artifacts,visual:t.visual,networkRequests:a,apiCalls:s,apiAssertions:t.apiAssertions,actions:t.actions??null,consoleLogs:n}}buildCloudTestPayload(t){let r={title:t.title,status:t.status,duration:t.duration,startedAt:t.startedAt,completedAt:t.completedAt,retry:t.retry,retryCount:t.retryCount,retryStatus:t.retryStatus,tags:t.tags,failure:t.failure,testId:t.testId,filePath:t.filePath,suiteName:t.suiteName,testType:t.testType,isFlaky:t.isFlaky,expectedStatus:t.expectedStatus,actualStatus:t.actualStatus,artifacts:t.artifacts,visual:t.visual??[],actions:t.actions??[],navigations:t.navigations??[],steps:t.actions??[],browser:t.browser,os:t.os,source:t.source};if(t.apiAssertions&&t.apiAssertions.length>0&&(r.protocolAssertions=Nt(t.apiAssertions)),t.consoleLogsFile&&(r.consoleLogs=j(t.consoleLogsFile)),t.networkRequestsFile&&(r.networkRequests=j(t.networkRequestsFile)),t.apiCallsFile){let a=j(t.apiCallsFile);r.apiCalls=Lt(a),(!r.networkRequests||r.networkRequests.length===0)&&(r.networkRequests=Dt(a));}return r}writeReport(t){try{let r=JSON.stringify(t),a=this.config.outputPath,n=path.dirname(a);fs$1.mkdirSync(n,{recursive:!0});let s=a+".tmp";fs$1.writeFileSync(s,r,"utf-8"),fs$1.renameSync(s,a);}catch(r){process.stderr.write(`[testrelic] Failed to write report: ${r instanceof Error?r.message:String(r)}
4316
+ `);}}};module.exports=Ne;//# sourceMappingURL=reporter-entry.cjs.map
2613
4317
  //# sourceMappingURL=reporter-entry.cjs.map