@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.
- package/README.md +82 -0
- package/dist/api-fixture.cjs +6 -4
- package/dist/api-fixture.cjs.map +1 -1
- package/dist/api-fixture.js +6 -4
- package/dist/api-fixture.js.map +1 -1
- package/dist/cli.cjs +55 -4
- package/dist/fixture.cjs +6 -4
- package/dist/fixture.cjs.map +1 -1
- package/dist/fixture.d.cts +7 -5
- package/dist/fixture.d.ts +7 -5
- package/dist/fixture.js +6 -4
- package/dist/fixture.js.map +1 -1
- package/dist/index.cjs +1902 -196
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1902 -196
- package/dist/index.js.map +1 -1
- package/dist/reporter-entry.cjs +1898 -194
- package/dist/reporter-entry.cjs.map +1 -1
- package/dist/reporter-entry.d.cts +11 -0
- package/dist/reporter-entry.d.ts +11 -0
- package/dist/reporter-entry.js +1898 -194
- package/dist/reporter-entry.js.map +1 -1
- package/dist/visual.cjs +3 -0
- package/dist/visual.cjs.map +1 -0
- package/dist/visual.d.cts +68 -0
- package/dist/visual.d.ts +68 -0
- package/dist/visual.js +3 -0
- package/dist/visual.js.map +1 -0
- package/package.json +17 -3
package/dist/reporter-entry.cjs
CHANGED
|
@@ -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
|
|
2
|
-
`),false}}moveFile(
|
|
3
|
-
`);}}}writeIndex(
|
|
4
|
-
`),
|
|
5
|
-
`);let c=
|
|
6
|
-
`);if(
|
|
7
|
-
`)}catch{return null}}function
|
|
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
|
|
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
|
|
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
|
|
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,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"').replace(/'/g,''')}
|
|
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]
|
|
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
|
-
|
|
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:
|
|
628
|
-
filePath:
|
|
629
|
-
testType:'e2e',retryCount:
|
|
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:
|
|
634
|
-
hasActionSteps:
|
|
635
|
-
errorMessage:
|
|
636
|
-
consoleCount:
|
|
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
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
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+=
|
|
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)+' — '+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(
|
|
979
|
+
if(tabs.length>1){
|
|
825
980
|
h+='<div class="seg-ctrl" data-seg-group="artifacts">';
|
|
826
|
-
|
|
827
|
-
|
|
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
|
-
|
|
831
|
-
|
|
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 <file></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
|
-
|
|
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(
|
|
874
|
-
if(
|
|
875
|
-
if(
|
|
876
|
-
if(
|
|
877
|
-
if(!hasSteps&&
|
|
878
|
-
if(
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 — '+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 — 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,
|
|
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
|
-
|
|
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
|
-
|
|
1595
|
-
|
|
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">—</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
|
|
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
|
|
1655
|
-
var
|
|
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={
|
|
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
|
|
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){
|
|
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('#
|
|
1961
|
-
if(target.closest('#
|
|
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();
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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">→</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">→</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">→</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">→</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
|
-
${
|
|
2321
|
-
${
|
|
2322
|
-
${
|
|
2323
|
-
${
|
|
2324
|
-
${
|
|
2325
|
-
${
|
|
2326
|
-
${
|
|
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
|
|
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(
|
|
2344
|
-
<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
|
-
${
|
|
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="
|
|
2366
|
-
|
|
2367
|
-
|
|
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">×</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">${
|
|
2387
|
-
${
|
|
2388
|
-
<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
|
|
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":${
|
|
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
|
-
${
|
|
2475
|
-
${
|
|
2476
|
-
${
|
|
2477
|
-
${
|
|
2478
|
-
${
|
|
2479
|
-
${
|
|
2480
|
-
${
|
|
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
|
-
|
|
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(
|
|
2500
|
-
<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
|
-
${
|
|
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="
|
|
2522
|
-
|
|
2523
|
-
|
|
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">×</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
|
|
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:${
|
|
2549
|
-
`);try{let
|
|
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===
|
|
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
|
-
${
|
|
2554
|
-
`;fs$1.appendFileSync(r,s,"utf-8");}catch{}}var
|
|
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{
|
|
2557
|
-
`}function
|
|
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=
|
|
2560
|
-
`,process.stderr.write(i);}var
|
|
2561
|
-
`)[0]?.trim();return
|
|
2562
|
-
`),
|
|
2563
|
-
`),
|
|
2564
|
-
`),
|
|
2565
|
-
`),
|
|
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
|
|
2568
|
-
`),fs$1.unlinkSync(i);continue}d=c;}catch(o){process.stderr.write(`\u26A0 TestRelic: Skipping unreadable queue file ${l}: ${
|
|
2569
|
-
`);try{fs$1.unlinkSync(i);}catch{}continue}try{let{body:o,headers:c}=await
|
|
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 ${
|
|
2571
|
-
`);return}}catch(o){process.stderr.write(`\u26A0 TestRelic: Queue flush paused \u2014 ${
|
|
2572
|
-
`);return}}}function
|
|
2573
|
-
`);return}try{if(
|
|
2574
|
-
`);return}let r=await
|
|
2575
|
-
`),await this.resolveRepoId(),this.config.queueDirectory&&(
|
|
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
|
|
2577
|
-
`));}async dispose(){if(this.healthCheckTimer&&(clearInterval(this.healthCheckTimer),this.healthCheckTimer=null),this.flushPromise){try{await Promise.race([this.flushPromise
|
|
2578
|
-
\u2192 Manage keys: ${
|
|
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: ${
|
|
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: ${
|
|
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
|
|
2587
|
-
`);}readRepoCache(
|
|
2588
|
-
`),this.startBackgroundFlush();}}}catch{}},
|
|
2589
|
-
`);}return
|
|
2590
|
-
`),{navigations:
|
|
2591
|
-
`),{keyMap:s,total:
|
|
2592
|
-
`).filter(r=>r.length>0).map(r=>{try{return JSON.parse(r)}catch{return null}}).filter(r=>r!==null)}catch{return []}}function
|
|
2593
|
-
`)
|
|
2594
|
-
`)
|
|
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
|
|
2604
|
-
`);let
|
|
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 ${
|
|
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
|
|
2609
|
-
`),this.switchToStreamingMode()):
|
|
2610
|
-
`);}catch{}}switchToStreamingMode(){try{if(this.activeReportMode==="streaming")return;this.activeReportMode="streaming";let
|
|
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(" > ")}`,
|
|
2612
|
-
`);}}};module.exports=
|
|
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
|