@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/index.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import {randomUUID,createHash}from'crypto';import {mkdirSync,openSync,writeSync,closeSync,unlinkSync,readFileSync,
|
|
2
|
-
`),false}}moveFile(e,r){try{renameSync(e,r);}catch{try{let{copyFileSync:n}=
|
|
3
|
-
`);}}}writeIndex(e){let r=join(this.reportDir,"index.json"),n=r+".tmp",s=JSON.stringify(e);writeFileSync(n,s,"utf-8"),renameSync(n,r),this.totalBytesWritten+=Buffer.byteLength(s,"utf-8");}writeCompactIndex(e){let r=join(this.reportDir,"index-compact.json"),n=r+".tmp",s=e.map(({titlePath:
|
|
4
|
-
`),s}catch{}let a=dirname(e);if(a===e)break;e=a;}return null}function
|
|
5
|
-
`);let c=t.queue;if(c&&typeof c=="object"){if(typeof c.maxAge=="string"){let
|
|
6
|
-
`);if(e<1||e>s.length)return null;let a=Math.max(1,e-r),
|
|
7
|
-
`)}catch{return null}}function
|
|
1
|
+
import {randomUUID,createHash}from'crypto';import {writeFileSync,existsSync,mkdirSync,openSync,writeSync,closeSync,unlinkSync,readFileSync,renameSync,appendFileSync,readdirSync,statSync,readSync,createReadStream,rmSync}from'fs';import {join,extname,basename,dirname,resolve,relative}from'path';import {tmpdir,platform}from'os';import {PAYLOAD_VERSION,ATTACHMENT_NAME,ATTACHMENT_CONTENT_TYPE,isValidConfig,createError,ErrorCode,isTestRelicFilePayload,isTestRelicDataPayload,isValidEndpointUrl,isValidCloudConfig,isValidQueueEntry}from'@testrelic/core';export{ATTACHMENT_CONTENT_TYPE,ATTACHMENT_NAME,PAYLOAD_VERSION,isTestRelicDataPayload}from'@testrelic/core';import {spawn,execFile,execSync}from'child_process';import {createServer}from'http';import {createInterface}from'readline';import {copyFile}from'fs/promises';import {gzip}from'zlib';import {promisify}from'util';import {Readable}from'stream';import {expect,test,defineConfig}from'@playwright/test';import {performance}from'perf_hooks';import {fileURLToPath}from'url';var oa=(t=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(t,{get:(e,r)=>(typeof require<"u"?require:e)[r]}):t)(function(t){if(typeof require<"u")return require.apply(this,arguments);throw Error('Dynamic require of "'+t+'" is not supported')});var da=".testrelic-report";function et(t,e,r,n){let s=`${t}|${e.join("|")}|${r}|${n}`;return createHash("sha256").update(s).digest("hex").substring(0,12)}function tt(t,e,r){let n=`${t}|${e.join("|")}|${r}`;return createHash("sha256").update(n).digest("hex").substring(0,12)}var he=class{constructor(e){this.writeErrors=[];this.totalBytesWritten=0;this.reportDir=join(e,da),this.testsDir=join(this.reportDir,"tests"),mkdirSync(this.testsDir,{recursive:true});}getReportDir(){return this.reportDir}getWriteErrors(){return this.writeErrors}writeTestDetail(e,r,n){try{let s=join(this.testsDir,e);mkdirSync(s,{recursive:!0});let a=join(s,"meta.json"),o=a+".tmp",i=JSON.stringify(r);return writeFileSync(o,i,"utf-8"),renameSync(o,a),this.totalBytesWritten+=Buffer.byteLength(i,"utf-8"),n?.networkRequestsFile&&this.moveFile(n.networkRequestsFile,join(s,"network.jsonl")),n?.consoleLogsFile&&this.moveFile(n.consoleLogsFile,join(s,"console.jsonl")),n?.apiCallsFile&&this.moveFile(n.apiCallsFile,join(s,"api-calls.jsonl")),!0}catch(s){let a=s instanceof Error?s.message:String(s);return this.writeErrors.push({testId:e,error:a,timestamp:new Date().toISOString()}),process.stderr.write(`[testrelic] Failed to write test detail for ${e}: ${a}
|
|
2
|
+
`),false}}moveFile(e,r){try{renameSync(e,r);}catch{try{let{copyFileSync:n}=oa("fs");n(e,r);try{unlinkSync(e);}catch{}}catch(n){process.stderr.write(`[testrelic] Failed to move file ${e} \u2192 ${r}: ${n instanceof Error?n.message:String(n)}
|
|
3
|
+
`);}}}writeIndex(e){let r=join(this.reportDir,"index.json"),n=r+".tmp",s=JSON.stringify(e);writeFileSync(n,s,"utf-8"),renameSync(n,r),this.totalBytesWritten+=Buffer.byteLength(s,"utf-8");}writeCompactIndex(e){let r=join(this.reportDir,"index-compact.json"),n=r+".tmp",s=e.map(({titlePath:o,project:i,...d})=>d),a=JSON.stringify(s);writeFileSync(n,a,"utf-8"),renameSync(n,r),this.totalBytesWritten+=Buffer.byteLength(a,"utf-8");}writeSummary(e){let r=join(this.reportDir,"summary.json"),n=r+".tmp",s=JSON.stringify(e);writeFileSync(n,s,"utf-8"),renameSync(n,r),this.totalBytesWritten+=Buffer.byteLength(s,"utf-8");}writeManifest(e){let r=join(this.reportDir,"manifest.json"),n=r+".tmp",s=JSON.stringify(e);writeFileSync(n,s,"utf-8"),renameSync(n,r),this.totalBytesWritten+=Buffer.byteLength(s,"utf-8");}computeTotalSize(){return this.totalBytesWritten}dispose(){}};var Lr=".testrelic",ha="testrelic-config.json",ma=".testrelic",va=5,ya=new Set(["__proto__","constructor","prototype"]),Er=Object.freeze({apiKey:null,endpoint:"https://platform.testrelic.ai/api/v1",uploadStrategy:"batch",timeout:3e4,uploadBudgetMs:12e4,projectName:null,queueMaxAge:6048e5,queueDirectory:`${Lr}/queue`,uploadArtifacts:true,artifactMaxSizeMb:50}),ba={s:1e3,m:60*1e3,h:3600*1e3,d:1440*60*1e3};function Nr(t){let e=resolve(t);for(let r=0;r<=va;r++){let n=join(e,Lr,ha);if(existsSync(n))try{if(statSync(n).isFile())return n}catch{}let s=join(e,ma);if(existsSync(s))try{if(statSync(s).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
|
+
`),s}catch{}let a=dirname(e);if(a===e)break;e=a;}return null}function Dr(t){try{let e=readFileSync(t,"utf-8"),r=JSON.parse(e);return typeof r!="object"||r===null||Array.isArray(r)||!Fr(r)?null:r}catch{return null}}function Fr(t){for(let e of Object.keys(t)){if(ya.has(e))return false;let r=t[e];if(typeof r=="object"&&r!==null&&!Array.isArray(r)&&!Fr(r))return false}return true}function Br(t){let e=/^\$\{([A-Za-z_][A-Za-z0-9_]*)\}$/.exec(t);if(e)return process.env[e[1]]??null;let r=/^\$([A-Za-z_][A-Za-z0-9_]*)$/.exec(t);return r?process.env[r[1]]??null:t}function rt(t){let e=Object.create(null);for(let r of Object.keys(t)){let n=t[r];typeof n=="string"&&n.startsWith("$")?e[r]=Br(n):typeof n=="object"&&n!==null&&!Array.isArray(n)?e[r]=rt(n):e[r]=n;}return e}function Pr(t){let e=/^(\d+)\s*([smhd])$/.exec(t.trim());if(!e)return null;let r=parseInt(e[1],10),n=e[2],s=ba[n];return !s||r<=0?null:r*s}function Or(t,e){let r=Object.create(null);if(Object.assign(r,Er),t){let d=t.cloud;d&&typeof d=="object"&&(typeof d.endpoint=="string"&&(r.endpoint=d.endpoint),typeof d.upload=="string"&&(r.uploadStrategy=d.upload),typeof d.timeout=="number"&&(r.timeout=d.timeout),typeof d.uploadBudgetMs=="number"&&(r.uploadBudgetMs=d.uploadBudgetMs),typeof d.apiKey=="string"&&d.apiKey.length>0&&(r.apiKey=d.apiKey));let l=t["testrelic-repo"]??t.project;l&&typeof l=="object"&&typeof l.name=="string"&&(r.projectName=l.name),t.project&&!t["testrelic-repo"]&&process.stderr.write(`[testrelic] Deprecation: "project" key in config is deprecated. Rename it to "testrelic-repo".
|
|
5
|
+
`);let c=t.queue;if(c&&typeof c=="object"){if(typeof c.maxAge=="string"){let u=Pr(c.maxAge);u!==null&&(r.queueMaxAge=u);}typeof c.directory=="string"&&(r.queueDirectory=c.directory);}}if(e){if(typeof e.apiKey=="string"&&e.apiKey.length>0){let d=e.apiKey.startsWith("$")?Br(e.apiKey):e.apiKey;d&&(r.apiKey=d);}if(typeof e.endpoint=="string"&&(r.endpoint=e.endpoint),typeof e.upload=="string"&&(r.uploadStrategy=e.upload),typeof e.timeout=="number"&&(r.timeout=e.timeout),typeof e.uploadBudgetMs=="number"&&(r.uploadBudgetMs=e.uploadBudgetMs),typeof e.projectName=="string"&&(r.projectName=e.projectName),typeof e.queueMaxAge=="string"){let d=Pr(e.queueMaxAge);d!==null&&(r.queueMaxAge=d);}typeof e.queueDirectory=="string"&&(r.queueDirectory=e.queueDirectory),typeof e.uploadArtifacts=="boolean"&&(r.uploadArtifacts=e.uploadArtifacts),typeof e.artifactMaxSizeMb=="number"&&(r.artifactMaxSizeMb=e.artifactMaxSizeMb);}let n=process.env.TESTRELIC_API_KEY;n&&n.length>0&&(r.apiKey=n);let s=process.env.TESTRELIC_CLOUD_ENDPOINT;s&&isValidEndpointUrl(s)&&(r.endpoint=s);let a=process.env.TESTRELIC_UPLOAD_STRATEGY;a&&["batch","realtime","both"].includes(a)&&(r.uploadStrategy=a);let o=process.env.TESTRELIC_CLOUD_TIMEOUT;if(o){let d=parseInt(o,10);!isNaN(d)&&d>=1e3&&d<=12e4&&(r.timeout=d);}let i=Object.freeze(r);return isValidCloudConfig(i)?i:Er}var Ca=[/AKIA[A-Z0-9]{16}/g,/Bearer\s+[A-Za-z0-9\-._~+/]+=*/g,/-----BEGIN\s+(RSA\s+)?PRIVATE\sKEY-----[\s\S]*?-----END/g,/\/\/[^:]+:[^@]+@/g],Ta=["authorization","cookie","set-cookie","x-api-key"],Sa=["password","secret","token","apiKey","api_key"];function qr(t){let e=Object.create(null);return e.trackApiCalls=t?.trackApiCalls??true,e.captureRequestHeaders=t?.captureRequestHeaders??true,e.captureResponseHeaders=t?.captureResponseHeaders??true,e.captureRequestBody=t?.captureRequestBody??true,e.captureResponseBody=t?.captureResponseBody??true,e.captureAssertions=t?.captureAssertions??true,e.redactHeaders=t?.redactHeaders??[...Ta],e.redactBodyFields=t?.redactBodyFields??[...Sa],e.apiIncludeUrls=t?.apiIncludeUrls??[],e.apiExcludeUrls=t?.apiExcludeUrls??[],Object.freeze(e)}function Hr(t){if(t!==void 0&&!isValidConfig(t))throw createError(ErrorCode.CONFIG_INVALID,"Invalid reporter configuration");let e=Object.create(null);e.outputPath=t?.outputPath??"./test-results/analytics-timeline.json",e.includeStackTrace=t?.includeStackTrace??false,e.includeCodeSnippets=t?.includeCodeSnippets??true,e.codeContextLines=t?.codeContextLines??3,e.includeNetworkStats=t?.includeNetworkStats??true,e.captureNavigation=t?.captureNavigation??true,e.navigationTypes=t?.navigationTypes??null,e.redactPatterns=[...Ca,...t?.redactPatterns??[]],e.testRunId=t?.testRunId??null,e.metadata=t?.metadata??null;let r=e.outputPath;e.openReport=t?.openReport??true,e.htmlReportPath=t?.htmlReportPath??r.replace(/\.json$/,".html"),e.includeArtifacts=t?.includeArtifacts??true,e.trackApiCalls=t?.trackApiCalls??true,e.quiet=t?.quiet??false,e.includeActionSteps=t?.includeActionSteps??true,e.captureConsoleLogs=t?.captureConsoleLogs??true;let n=Nr(process.cwd()),s=n?Dr(n):null,a=s?rt(s):null,o=Or(a,t?.cloud);return e.cloud=o.apiKey?o:null,e.reportMode=t?.reportMode??"streaming",e.streamingThreshold=t?.streamingThreshold??0,Object.freeze(e)}var Ce="1.6.0";function Ur(t,e,r){try{let s=readFileSync(t,"utf-8").split(`
|
|
6
|
+
`);if(e<1||e>s.length)return null;let a=Math.max(1,e-r),o=Math.min(s.length,e+r),i=[];for(let d=a;d<=o;d++){let l=d===e?">":" ",c=String(d).padStart(String(o).length," ");i.push(`${l} ${c} | ${s[d-1]}`);}return i.join(`
|
|
7
|
+
`)}catch{return null}}function zr(t){return e=>{let r=e;for(let n of t)if(typeof n=="string"){let s=n.replace(/[.*+?^${}()|[\]\\]/g,"\\$&");r=r.replace(new RegExp(s,"g"),"[REDACTED]");}else {let s=n.flags.includes("g")?n.flags:n.flags+"g",a=new RegExp(n.source,s);r=r.replace(a,"[REDACTED]");}return r}}function Aa(t){return t.GITHUB_ACTIONS!=="true"?null:{provider:"github-actions",buildId:t.GITHUB_RUN_ID??null,commitSha:t.GITHUB_SHA??null,branch:t.GITHUB_REF_NAME??null,runUrl:t.GITHUB_SERVER_URL&&t.GITHUB_REPOSITORY&&t.GITHUB_RUN_ID?`${t.GITHUB_SERVER_URL}/${t.GITHUB_REPOSITORY}/actions/runs/${t.GITHUB_RUN_ID}`:null}}function _a(t){return t.GITLAB_CI!=="true"?null:{provider:"gitlab-ci",buildId:t.CI_PIPELINE_ID??null,commitSha:t.CI_COMMIT_SHA??null,branch:t.CI_COMMIT_BRANCH??t.CI_COMMIT_REF_NAME??null,runUrl:t.CI_PIPELINE_URL??null}}function Ia(t){if(!t.JENKINS_URL)return null;let e=t.GIT_BRANCH??null;return e?.startsWith("origin/")&&(e=e.slice(7)),{provider:"jenkins",buildId:t.BUILD_ID??null,commitSha:t.GIT_COMMIT??null,branch:e,runUrl:t.BUILD_URL??null}}function Ma(t){return t.CIRCLECI!=="true"?null:{provider:"circleci",buildId:t.CIRCLE_BUILD_NUM??null,commitSha:t.CIRCLE_SHA1??null,branch:t.CIRCLE_BRANCH??null,runUrl:t.CIRCLE_BUILD_URL??null}}function Ea(t){if(!t.BITBUCKET_PIPELINE_UUID)return null;let e=t.BITBUCKET_WORKSPACE&&t.BITBUCKET_REPO_SLUG&&t.BITBUCKET_PIPELINE_UUID?`https://bitbucket.org/${t.BITBUCKET_WORKSPACE}/${t.BITBUCKET_REPO_SLUG}/pipelines/results/${t.BITBUCKET_PIPELINE_UUID}`:null;return {provider:"bitbucket-pipelines",buildId:t.BITBUCKET_BUILD_NUMBER??null,commitSha:t.BITBUCKET_COMMIT??null,branch:t.BITBUCKET_BRANCH??null,runUrl:e}}var Pa=[Aa,_a,Ia,Ma,Ea];function V(t){let e=process.env;for(let r of Pa){let n=r(e);if(n)return n}return null}var nt=`
|
|
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 st=`<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>`,at=`<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 it=`
|
|
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 ot=`
|
|
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 lt=`
|
|
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 dt=`
|
|
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 ct=`
|
|
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 ut=`
|
|
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 pt=`
|
|
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 ft=`
|
|
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 gt=`
|
|
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 ht=`
|
|
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
|
+
`,mt=`
|
|
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 vt=`
|
|
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 yt=`
|
|
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
|
+
`,bt=`
|
|
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 xt=`
|
|
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
|
+
`,wt=`
|
|
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 kt=`
|
|
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 La=`
|
|
2318
4003
|
(function(){
|
|
2319
4004
|
var data=JSON.parse(document.getElementById('report-data').textContent);
|
|
2320
|
-
${
|
|
2321
|
-
${
|
|
2322
|
-
${
|
|
2323
|
-
${
|
|
2324
|
-
${
|
|
2325
|
-
${
|
|
2326
|
-
${
|
|
4005
|
+
${it}
|
|
4006
|
+
${ct}
|
|
4007
|
+
${ut}
|
|
4008
|
+
${ot}
|
|
4009
|
+
${lt}
|
|
4010
|
+
${pt}
|
|
4011
|
+
${gt}
|
|
4012
|
+
${ft}
|
|
4013
|
+
${dt}
|
|
4014
|
+
${ht}
|
|
4015
|
+
${vt}
|
|
4016
|
+
${mt}
|
|
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 jr(t,e,r){let n=t.replace(/<\//g,"<\\/"),s=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(st).toString("base64")}">
|
|
4037
|
+
<style>${nt}${yt}${bt}${xt}${wt}${kt}</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
|
+
${at}
|
|
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">
|
|
@@ -2385,9 +4078,9 @@ ${""}
|
|
|
2385
4078
|
</aside>
|
|
2386
4079
|
<script id="report-data" type="application/json">${n}</script>
|
|
2387
4080
|
${s?`<script id="artifact-manifest-data" type="application/json">${s}</script>`:""}
|
|
2388
|
-
<script>${
|
|
4081
|
+
<script>${La}</script>
|
|
2389
4082
|
</body>
|
|
2390
|
-
</html>`}function
|
|
4083
|
+
</html>`}function Ct(t,e,r,n){let s=t.replace(/<\//g,"<\\/"),a=e.replace(/<\//g,"<\\/"),o=n?n.replace(/<\//g,"<\\/"):null,i={};try{i=JSON.parse(t);}catch{}let d=`{
|
|
2391
4084
|
"schemaVersion":"2.0",
|
|
2392
4085
|
"reportMode":"streaming",
|
|
2393
4086
|
"serverPort":${r??"null"},
|
|
@@ -2401,7 +4094,7 @@ ${s?`<script id="artifact-manifest-data" type="application/json">${s}</script>`:
|
|
|
2401
4094
|
"ci":${i.ci?JSON.stringify(i.ci).replace(/<\//g,"<\\/"):"null"},
|
|
2402
4095
|
"metadata":${i.metadata?JSON.stringify(i.metadata).replace(/<\//g,"<\\/"):"null"},
|
|
2403
4096
|
"shardRunIds":${i.shardRunIds?JSON.stringify(i.shardRunIds).replace(/<\//g,"<\\/"):"null"}
|
|
2404
|
-
}`,
|
|
4097
|
+
}`,l=`
|
|
2405
4098
|
(function(){
|
|
2406
4099
|
var data=JSON.parse(document.getElementById('report-data').textContent);
|
|
2407
4100
|
var _serverPort=data.serverPort||null;
|
|
@@ -2471,20 +4164,30 @@ ${s?`<script id="artifact-manifest-data" type="application/json">${s}</script>`:
|
|
|
2471
4164
|
window.__trServerPort=_serverPort;
|
|
2472
4165
|
window.__trIsStreaming=true;
|
|
2473
4166
|
|
|
2474
|
-
${
|
|
2475
|
-
${
|
|
2476
|
-
${
|
|
2477
|
-
${
|
|
2478
|
-
${
|
|
2479
|
-
${
|
|
2480
|
-
${
|
|
4167
|
+
${it}
|
|
4168
|
+
${ct}
|
|
4169
|
+
${ut}
|
|
4170
|
+
${ot}
|
|
4171
|
+
${lt}
|
|
4172
|
+
${pt}
|
|
4173
|
+
${gt}
|
|
4174
|
+
${ft}
|
|
4175
|
+
${dt}
|
|
4176
|
+
${ht}
|
|
4177
|
+
${vt}
|
|
4178
|
+
${mt}
|
|
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 @@ ${s?`<script id="artifact-manifest-data" type="application/json">${s}</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(st).toString("base64")}">
|
|
4203
|
+
<style>${nt}${yt}${bt}${xt}${wt}${kt}</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
|
+
${at}
|
|
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">
|
|
@@ -2540,78 +4243,81 @@ ${r?`<meta name="artifact-server-port" content="${r}">`:""}
|
|
|
2540
4243
|
<div class="filter-drawer-body" id="filter-drawer-body"></div>
|
|
2541
4244
|
</aside>
|
|
2542
4245
|
<script id="report-data" type="application/json">${d}</script>
|
|
2543
|
-
${
|
|
2544
|
-
<script>${
|
|
4246
|
+
${o?`<script id="artifact-manifest-data" type="application/json">${o}</script>`:""}
|
|
4247
|
+
<script>${l}</script>
|
|
2545
4248
|
</body>
|
|
2546
|
-
</html>`}function
|
|
2547
|
-
`);});}catch{}}var
|
|
2548
|
-
`;writeSync(this.fd,r),this.count++;}getPath(){return this.filePath}getCount(){return this.count}close(){if(!this.closed){this.closed=true;try{closeSync(this.fd);}catch{}}}cleanup(){try{unlinkSync(this.filePath);}catch{}}};async function ur(t,e,r,n){let s=(e-1)*r,a=[],l=0,i=createInterface({input:createReadStream(t,{encoding:"utf-8"}),crlfDelay:1/0});for await(let c of i)if(c.length!==0){if(l>=s&&a.length<r)try{a.push(JSON.parse(c));}catch{}if(l++,a.length>=r&&n!==void 0)break}let d=n??l,o=Math.max(1,Math.ceil(d/r));return {items:a,total:d,page:e,pageSize:r,totalPages:o}}var gr=/^[a-f0-9]{12}$/,Ye=/^\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}(-\d+)?$/,hr=500;function x(t,e,r){t.writeHead(e,{"Content-Type":"application/json"}),t.end(JSON.stringify(r));}function mr(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 he(t){try{return existsSync(t)?JSON.parse(readFileSync(t,"utf-8")):null}catch{return null}}function me(t){let e=0;try{let r=readdirSync(t,{withFileTypes:!0});for(let n of r){let s=join(t,n.name);n.isFile()?e+=statSync(s).size:n.isDirectory()&&(e+=me(s));}}catch{}return e}function yr(t,e,r,n){let s=he(join(r,"index.json"));x(e,200,{status:"ok",reportMode:"streaming",testCount:s?.length??0,uptime:Math.floor((Date.now()-n)/1e3)});}function vr(t,e,r){let n=he(join(r,"summary.json"));if(!n){x(e,404,{error:"Summary not found"});return}x(e,200,n);}function br(t,e,r){let n=he(join(r,"index.json"));if(!n){x(e,404,{error:"Test index not found"});return}let a=new URL(t.url??"/",`http://${t.headers.host}`).searchParams,l=Math.max(1,parseInt(a.get("page")??"1",10)||1),i=Math.min(hr,Math.max(1,parseInt(a.get("pageSize")??"100",10)||100)),d=a.get("status")?.split(",").filter(Boolean)??null,o=a.get("file")??null,c=a.get("search")?.toLowerCase()??null,p=a.get("tag")?.split(",").filter(Boolean)??null,g=a.get("sort")??"file",u=a.get("order")==="desc"?-1:1,f=n;d&&d.length>0&&(f=f.filter(v=>d.includes(v.status))),o&&(f=f.filter(v=>v.filePath===o||v.filePath.startsWith(o+"/"))),c&&(f=f.filter(v=>v.title.toLowerCase().includes(c)||v.filePath.toLowerCase().includes(c))),p&&p.length>0&&(f=f.filter(v=>p.some(k=>v.tags.includes(k)))),f=[...f].sort((v,k)=>{let C=0;switch(g){case "duration":C=v.duration-k.duration;break;case "status":C=v.status.localeCompare(k.status);break;case "title":C=v.title.localeCompare(k.title);break;default:C=v.filePath.localeCompare(k.filePath);break}return C*u});let h=f.length,m=Math.max(1,Math.ceil(h/i)),y=(l-1)*i,w=f.slice(y,y+i);x(e,200,{tests:w,pagination:{page:l,pageSize:i,totalItems:h,totalPages:m},filters:{status:d,file:o,search:c,tag:p}});}function wr(t,e,r,n){if(!gr.test(n)){x(e,400,{error:"Invalid test ID format"});return}let s=join(r,"tests",n,"meta.json"),a=join(r,"tests",`${n}.json`),l=existsSync(s)?s:existsSync(a)?a:null;if(!l){x(e,404,{error:`Test not found: ${n}`});return}try{let i=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 xr(t,e,r,n,s){if(!gr.test(n)){x(e,400,{error:"Invalid test ID format"});return}let l=join(r,"tests",n,{network:"network.jsonl",console:"console.jsonl","api-calls":"api-calls.jsonl"}[s]);if(!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(hr,Math.max(1,parseInt(d.get("pageSize")??"50",10)||50)),p,g=join(r,"tests",n,"meta.json");if(existsSync(g))try{let f=JSON.parse(readFileSync(g,"utf-8"));switch(s){case "network":p=f.networkRequestsCount;break;case "console":p=f.consoleLogsCount;break;case "api-calls":p=f.apiCallsCount;break}}catch{}let u=await ur(l,o,c,p);x(e,200,u);}catch(i){x(e,500,{error:i instanceof Error?i.message:String(i)});}}function kr(t,e,r){let n=he(join(r,"index.json"));if(!n){x(e,404,{error:"Test index not found"});return}let s=new Map;for(let l of n){if(l.isRetry)continue;let i=s.get(l.filePath);switch(i||(i={total:0,passed:0,failed:0,flaky:0,skipped:0,timedOut:0},s.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 a=Array.from(s.entries()).map(([l,i])=>({filePath:l,...i})).sort((l,i)=>l.filePath.localeCompare(i.filePath));x(e,200,{files:a});}function Tr(t,e,r){if(!existsSync(r)){x(e,200,{runs:[],totalSizeBytes:0});return}try{let n=[],s=0,a=readdirSync(r,{withFileTypes:!0});for(let l of a){if(!l.isDirectory()||!Ye.test(l.name))continue;let i=join(r,l.name),d=me(i),o=readdirSync(i,{withFileTypes:!0}).filter(c=>c.isDirectory());n.push({folderName:l.name,totalSizeBytes:d,testCount:o.length}),s+=d;}n.sort((l,i)=>i.folderName.localeCompare(l.folderName)),x(e,200,{runs:n,totalSizeBytes:s});}catch(n){x(e,500,{error:n instanceof Error?n.message:String(n)});}}function Cr(t,e,r){try{let n=0,s=0;if(existsSync(r)){let a=readdirSync(r,{withFileTypes:!0});for(let l of a){if(!l.isDirectory()||!Ye.test(l.name))continue;let i=join(r,l.name),d=me(i);rmSync(i,{recursive:!0,force:!0}),s+=d,n++;}}x(e,200,{deletedCount:n,freedBytes:s});}catch(n){x(e,500,{error:n instanceof Error?n.message:String(n)});}}function Sr(t,e,r,n){if(!Ye.test(n)){x(e,400,{error:"Invalid folder name"});return}let s=join(r,n);try{if(!statSync(s).isDirectory()){x(e,404,{error:"Not found"});return}}catch{x(e,404,{error:"Not found"});return}try{let a=me(s);rmSync(s,{recursive:!0,force:!0}),x(e,200,{deleted:n,freedBytes:a});}catch(a){x(e,500,{error:a instanceof Error?a.message:String(a)});}}function Rr(t,e,r){x(e,200,{status:"shutting_down"}),r.close();}function Qe(t,e,r){if(!existsSync(r)){x(e,404,{error:"File not found"});return}try{let n=readFileSync(r),s=extname(r).toLowerCase(),a=Ms[s]??"application/octet-stream";e.writeHead(200,{"Content-Type":a}),e.end(n);}catch(n){x(e,500,{error:n instanceof Error?n.message:String(n)});}}var Ms={".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 Ns=9323,Fs=10,Ds=1800*1e3;function ae(t,e){return new Promise((r,n)=>{let s=e?.port??Ns,a=Date.now(),l,i=0,d=existsSync(join(t,"artifacts"))?join(t,"artifacts"):existsSync(join(t,"..","artifacts"))?join(t,"..","artifacts"):join(t,"artifacts");function o(){clearTimeout(l),l=setTimeout(()=>{p.close();},Ds);}let c=e?.htmlPath??null;if(!c){let u=dirname(t);try{let f=readdirSync(u).find(h=>h.endsWith(".html"));f&&(c=join(u,f));}catch{}}let p=createServer((u,f)=>{if(o(),mr(f),u.method==="OPTIONS"){f.writeHead(204),f.end();return}let h;try{h=new URL(u.url??"/",`http://${u.headers.host??"localhost"}`).pathname;}catch{x(f,400,{error:"Invalid URL"});return}if(u.method==="GET"&&(h==="/"||h==="/index.html")){if(c&&existsSync(c)){Qe(u,f,c);return}x(f,404,{error:"HTML report not found"});return}if(u.method==="GET"&&h==="/api/health"){yr(u,f,t,a);return}if(u.method==="GET"&&h==="/api/summary"){vr(u,f,t);return}if(u.method==="GET"&&h==="/api/tests"){br(u,f,t);return}if(u.method==="GET"&&h==="/api/files"){kr(u,f,t);return}let m=h.match(/^\/api\/tests\/([a-f0-9]+)\/(network|console|api-calls)$/);if(u.method==="GET"&&m){xr(u,f,t,m[1],m[2]);return}let y=h.match(/^\/api\/tests\/([a-f0-9]+)$/);if(u.method==="GET"&&y){wr(u,f,t,y[1]);return}if(u.method==="GET"&&h==="/api/artifacts"){Tr(u,f,d);return}if(u.method==="DELETE"&&h==="/api/artifacts"){Cr(u,f,d);return}let w=h.match(/^\/api\/artifacts\/(.+)$/);if(u.method==="DELETE"&&w){Sr(u,f,d,decodeURIComponent(w[1]));return}if(u.method==="GET"&&h.startsWith("/artifacts/")){let v=decodeURIComponent(h.slice(11));if(v.includes("..")||v.includes("\0")){x(f,400,{error:"Invalid path"});return}Qe(u,f,join(d,v));return}if(u.method==="POST"&&h==="/api/shutdown"){Rr(u,f,p);return}x(f,404,{error:"Not found"});});function g(u){let f=h=>{h.code==="EADDRINUSE"&&i<Fs?(i++,g(u+1)):n(h);};p.once("error",f),p.listen(u,"127.0.0.1",()=>{p.removeListener("error",f);let h=p.address();if(!h||typeof h=="string"){n(new Error("Failed to get server address"));return}o(),r({port:h.port,dispose:()=>new Promise(m=>{clearTimeout(l),p.close(()=>m());})});});}g(s);})}async function Ar(t){let e=await ae(t);return {port:e.port,dispose:e.dispose}}function qs(t,e,r){let n=JSON.stringify(t),s=r?JSON.stringify(r):null;return lr(n,e,s)}async function Xe(t,e,r){try{let n=null,s=null,a=e.reportMode==="streaming"||e.reportMode==="auto"&&t.timeline.length===0&&t.summary.total>=e.streamingThreshold,l,i=e.htmlReportPath,d=dirname(i);mkdirSync(d,{recursive:!0});let o="",c="[]",p=null;if(a){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 u=dirname(e.outputPath),f=join(u,".testrelic-report");try{let h=join(f,"index-compact.json"),m=join(f,"index.json");existsSync(h)?c=readFileSync(h,"utf-8"):existsSync(m)&&(c=readFileSync(m,"utf-8"));}catch{}p=r?JSON.stringify(r):null,l=Ke(o,c,null,p);}else l=qs(t,null,r);let g=i+".tmp";if(writeFileSync(g,l,"utf-8"),renameSync(g,i),a){if(t.ci===null){let u=dirname(e.outputPath),f=join(u,".testrelic-report"),h=resolve(i);try{if(await Hs(),n=await zs(f),!n){s=await ae(f,{htmlPath:h}),n=s.port;let m=setInterval(()=>{},6e4),y=()=>{clearInterval(m),s?.dispose();};process.on("SIGINT",y),process.on("SIGTERM",y),setTimeout(y,1800*1e3).unref();}if(n){process.stderr.write(`
|
|
4249
|
+
</html>`}function Tt(t){try{let e=process.platform,r,n;e==="darwin"?(r="open",n=[t]):e==="win32"?(r="cmd",n=["/c","start","",t]):(r="xdg-open",n=[t]),execFile(r,n,s=>{s&&process.stderr.write(`[testrelic] Failed to open browser: ${s.message}
|
|
4250
|
+
`);});}catch{}}var Vr=join(tmpdir(),"testrelic-data"),q=class{constructor(e){this.count=0;this.closed=false;mkdirSync(Vr,{recursive:true}),this.filePath=join(Vr,`${e}-${randomUUID().substring(0,8)}.jsonl`),this.fd=openSync(this.filePath,"w");}append(e){if(this.closed)return;let r=JSON.stringify(e)+`
|
|
4251
|
+
`;writeSync(this.fd,r),this.count++;}getPath(){return this.filePath}getCount(){return this.count}close(){if(!this.closed){this.closed=true;try{closeSync(this.fd);}catch{}}}cleanup(){try{unlinkSync(this.filePath);}catch{}}};async function Wr(t,e,r,n){let s=(e-1)*r,a=[],o=0,i=createInterface({input:createReadStream(t,{encoding:"utf-8"}),crlfDelay:1/0});for await(let c of i)if(c.length!==0){if(o>=s&&a.length<r)try{a.push(JSON.parse(c));}catch{}if(o++,a.length>=r&&n!==void 0)break}let d=n??o,l=Math.max(1,Math.ceil(d/r));return {items:a,total:d,page:e,pageSize:r,totalPages:l}}var Yr=/^[a-f0-9]{12}$/,At=/^\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}(-\d+)?$/,Xr=500;function R(t,e,r){t.writeHead(e,{"Content-Type":"application/json"}),t.end(JSON.stringify(r));}function Zr(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 Se(t){try{return existsSync(t)?JSON.parse(readFileSync(t,"utf-8")):null}catch{return null}}function Re(t){let e=0;try{let r=readdirSync(t,{withFileTypes:!0});for(let n of r){let s=join(t,n.name);n.isFile()?e+=statSync(s).size:n.isDirectory()&&(e+=Re(s));}}catch{}return e}function Qr(t,e,r,n){let s=Se(join(r,"index.json"));R(e,200,{status:"ok",reportMode:"streaming",testCount:s?.length??0,uptime:Math.floor((Date.now()-n)/1e3)});}function en(t,e,r){let n=Se(join(r,"summary.json"));if(!n){R(e,404,{error:"Summary not found"});return}R(e,200,n);}function tn(t,e,r){let n=Se(join(r,"index.json"));if(!n){R(e,404,{error:"Test index not found"});return}let a=new URL(t.url??"/",`http://${t.headers.host}`).searchParams,o=Math.max(1,parseInt(a.get("page")??"1",10)||1),i=Math.min(Xr,Math.max(1,parseInt(a.get("pageSize")??"100",10)||100)),d=a.get("status")?.split(",").filter(Boolean)??null,l=a.get("file")??null,c=a.get("search")?.toLowerCase()??null,u=a.get("tag")?.split(",").filter(Boolean)??null,f=a.get("sort")??"file",p=a.get("order")==="desc"?-1:1,g=n;d&&d.length>0&&(g=g.filter(b=>d.includes(b.status))),l&&(g=g.filter(b=>b.filePath===l||b.filePath.startsWith(l+"/"))),c&&(g=g.filter(b=>b.title.toLowerCase().includes(c)||b.filePath.toLowerCase().includes(c))),u&&u.length>0&&(g=g.filter(b=>u.some(k=>b.tags.includes(k)))),g=[...g].sort((b,k)=>{let A=0;switch(f){case "duration":A=b.duration-k.duration;break;case "status":A=b.status.localeCompare(k.status);break;case "title":A=b.title.localeCompare(k.title);break;default:A=b.filePath.localeCompare(k.filePath);break}return A*p});let m=g.length,h=Math.max(1,Math.ceil(m/i)),v=(o-1)*i,y=g.slice(v,v+i);R(e,200,{tests:y,pagination:{page:o,pageSize:i,totalItems:m,totalPages:h},filters:{status:d,file:l,search:c,tag:u}});}function rn(t,e,r,n){if(!Yr.test(n)){R(e,400,{error:"Invalid test ID format"});return}let s=join(r,"tests",n,"meta.json"),a=join(r,"tests",`${n}.json`),o=existsSync(s)?s:existsSync(a)?a:null;if(!o){R(e,404,{error:`Test not found: ${n}`});return}try{let i=readFileSync(o,"utf-8");e.writeHead(200,{"Content-Type":"application/json"}),e.end(i);}catch(i){R(e,500,{error:i instanceof Error?i.message:String(i)});}}async function nn(t,e,r,n,s){if(!Yr.test(n)){R(e,400,{error:"Invalid test ID format"});return}let o=join(r,"tests",n,{network:"network.jsonl",console:"console.jsonl","api-calls":"api-calls.jsonl"}[s]);if(!existsSync(o)){R(e,200,{items:[],total:0,page:1,pageSize:50,totalPages:0});return}try{let d=new URL(t.url??"/",`http://${t.headers.host}`).searchParams,l=Math.max(1,parseInt(d.get("page")??"1",10)||1),c=Math.min(Xr,Math.max(1,parseInt(d.get("pageSize")??"50",10)||50)),u,f=join(r,"tests",n,"meta.json");if(existsSync(f))try{let g=JSON.parse(readFileSync(f,"utf-8"));switch(s){case "network":u=g.networkRequestsCount;break;case "console":u=g.consoleLogsCount;break;case "api-calls":u=g.apiCallsCount;break}}catch{}let p=await Wr(o,l,c,u);R(e,200,p);}catch(i){R(e,500,{error:i instanceof Error?i.message:String(i)});}}function sn(t,e,r){let n=Se(join(r,"index.json"));if(!n){R(e,404,{error:"Test index not found"});return}let s=new Map;for(let o of n){if(o.isRetry)continue;let i=s.get(o.filePath);switch(i||(i={total:0,passed:0,failed:0,flaky:0,skipped:0,timedOut:0},s.set(o.filePath,i)),i.total++,o.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 a=Array.from(s.entries()).map(([o,i])=>({filePath:o,...i})).sort((o,i)=>o.filePath.localeCompare(i.filePath));R(e,200,{files:a});}function an(t,e,r){if(!existsSync(r)){R(e,200,{runs:[],totalSizeBytes:0});return}try{let n=[],s=0,a=readdirSync(r,{withFileTypes:!0});for(let o of a){if(!o.isDirectory()||!At.test(o.name))continue;let i=join(r,o.name),d=Re(i),l=readdirSync(i,{withFileTypes:!0}).filter(c=>c.isDirectory());n.push({folderName:o.name,totalSizeBytes:d,testCount:l.length}),s+=d;}n.sort((o,i)=>i.folderName.localeCompare(o.folderName)),R(e,200,{runs:n,totalSizeBytes:s});}catch(n){R(e,500,{error:n instanceof Error?n.message:String(n)});}}function on(t,e,r){try{let n=0,s=0;if(existsSync(r)){let a=readdirSync(r,{withFileTypes:!0});for(let o of a){if(!o.isDirectory()||!At.test(o.name))continue;let i=join(r,o.name),d=Re(i);rmSync(i,{recursive:!0,force:!0}),s+=d,n++;}}R(e,200,{deletedCount:n,freedBytes:s});}catch(n){R(e,500,{error:n instanceof Error?n.message:String(n)});}}function ln(t,e,r,n){if(!At.test(n)){R(e,400,{error:"Invalid folder name"});return}let s=join(r,n);try{if(!statSync(s).isDirectory()){R(e,404,{error:"Not found"});return}}catch{R(e,404,{error:"Not found"});return}try{let a=Re(s);rmSync(s,{recursive:!0,force:!0}),R(e,200,{deleted:n,freedBytes:a});}catch(a){R(e,500,{error:a instanceof Error?a.message:String(a)});}}function dn(t,e,r){R(e,200,{status:"shutting_down"}),r.close();}function _t(t,e,r){if(!existsSync(r)){R(e,404,{error:"File not found"});return}try{let n=statSync(r).size,s=extname(r).toLowerCase(),a=Wa[s]??"application/octet-stream",o=$a(t.headers.range,n);if(o==="unsatisfiable"){e.writeHead(416,{"Content-Range":`bytes */${n}`,"Accept-Ranges":"bytes"}),e.end();return}if(o){e.writeHead(206,{"Content-Type":a,"Content-Length":o.end-o.start+1,"Content-Range":`bytes ${o.start}-${o.end}/${n}`,"Accept-Ranges":"bytes"}),Kr(createReadStream(r,{start:o.start,end:o.end}),e);return}e.writeHead(200,{"Content-Type":a,"Content-Length":n,"Accept-Ranges":"bytes"}),Kr(createReadStream(r),e);}catch(n){R(e,500,{error:n instanceof Error?n.message:String(n)});}}function Kr(t,e){t.on("error",()=>{t.destroy(),e.destroy();}),e.on("close",()=>{t.destroy();}),t.pipe(e);}function $a(t,e){if(!t)return null;let r=/^bytes=(\d*)-(\d*)$/.exec(t.trim());if(!r)return null;let[,n,s]=r;if(n===""&&s==="")return null;let a,o;if(n===""){let i=Number(s);if(i<=0)return "unsatisfiable";a=Math.max(0,e-i),o=e-1;}else a=Number(n),o=s===""?e-1:Math.min(Number(s),e-1);return !Number.isFinite(a)||!Number.isFinite(o)?null:e===0||a>=e||a>o?"unsatisfiable":{start:a,end:o}}var Wa={".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 Ya=9323,Xa=10,Za=1800*1e3;function me(t,e){return new Promise((r,n)=>{let s=e?.port??Ya,a=Date.now(),o,i=0,d=existsSync(join(t,"artifacts"))?join(t,"artifacts"):existsSync(join(t,"..","artifacts"))?join(t,"..","artifacts"):join(t,"artifacts");function l(){clearTimeout(o),o=setTimeout(()=>{u.close();},Za);}let c=e?.htmlPath??null;if(!c){let p=dirname(t);try{let g=readdirSync(p).find(m=>m.endsWith(".html"));g&&(c=join(p,g));}catch{}}let u=createServer((p,g)=>{if(l(),Zr(g),p.method==="OPTIONS"){g.writeHead(204),g.end();return}let m;try{m=new URL(p.url??"/",`http://${p.headers.host??"localhost"}`).pathname;}catch{R(g,400,{error:"Invalid URL"});return}if(p.method==="GET"&&(m==="/"||m==="/index.html")){if(c&&existsSync(c)){_t(p,g,c);return}R(g,404,{error:"HTML report not found"});return}if(p.method==="GET"&&m==="/api/health"){Qr(p,g,t,a);return}if(p.method==="GET"&&m==="/api/summary"){en(p,g,t);return}if(p.method==="GET"&&m==="/api/tests"){tn(p,g,t);return}if(p.method==="GET"&&m==="/api/files"){sn(p,g,t);return}let h=m.match(/^\/api\/tests\/([a-f0-9]+)\/(network|console|api-calls)$/);if(p.method==="GET"&&h){nn(p,g,t,h[1],h[2]);return}let v=m.match(/^\/api\/tests\/([a-f0-9]+)$/);if(p.method==="GET"&&v){rn(p,g,t,v[1]);return}if(p.method==="GET"&&m==="/api/artifacts"){an(p,g,d);return}if(p.method==="DELETE"&&m==="/api/artifacts"){on(p,g,d);return}let y=m.match(/^\/api\/artifacts\/(.+)$/);if(p.method==="DELETE"&&y){ln(p,g,d,decodeURIComponent(y[1]));return}if(p.method==="GET"&&m.startsWith("/artifacts/")){let b=decodeURIComponent(m.slice(11));if(b.includes("..")||b.includes("\0")){R(g,400,{error:"Invalid path"});return}_t(p,g,join(d,b));return}if(p.method==="POST"&&m==="/api/shutdown"){dn(p,g,u);return}R(g,404,{error:"Not found"});});function f(p){let g=m=>{m.code==="EADDRINUSE"&&i<Xa?(i++,f(p+1)):n(m);};u.once("error",g),u.listen(p,"127.0.0.1",()=>{u.removeListener("error",g);let m=u.address();if(!m||typeof m=="string"){n(new Error("Failed to get server address"));return}l(),r({port:m.port,dispose:()=>new Promise(h=>{clearTimeout(o),u.close(()=>h());})});});}f(s);})}async function cn(t){let e=await me(t);return {port:e.port,dispose:e.dispose}}function ti(t,e,r){let n=JSON.stringify(t),s=r?JSON.stringify(r):null;return jr(n,e,s)}async function Mt(t,e,r){try{let n=null,s=null,a=e.reportMode==="streaming"||e.reportMode==="auto"&&t.timeline.length===0&&t.summary.total>=e.streamingThreshold,o,i=e.htmlReportPath,d=dirname(i);mkdirSync(d,{recursive:!0});let l="",c="[]",u=null;if(a){l=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 p=dirname(e.outputPath),g=join(p,".testrelic-report");try{let m=join(g,"index-compact.json"),h=join(g,"index.json");existsSync(m)?c=readFileSync(m,"utf-8"):existsSync(h)&&(c=readFileSync(h,"utf-8"));}catch{}u=r?JSON.stringify(r):null,o=Ct(l,c,null,u);}else o=ti(t,null,r);let f=i+".tmp";if(writeFileSync(f,o,"utf-8"),renameSync(f,i),a){if(t.ci===null){let p=dirname(e.outputPath),g=join(p,".testrelic-report"),m=resolve(i);try{if(await ni(),n=await si(g),!n){s=await me(g,{htmlPath:m}),n=s.port;let h=setInterval(()=>{},6e4),v=()=>{clearInterval(h),s?.dispose();};process.on("SIGINT",v),process.on("SIGTERM",v),setTimeout(v,1800*1e3).unref();}if(n){process.stderr.write(`
|
|
2549
4252
|
Report server: http://127.0.0.1:${n}
|
|
2550
|
-
`);try{let
|
|
2551
|
-
`);}}var be=9323,Er=10;async function Us(){for(let t=be;t<be+Er;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 Hs(){for(let t=be;t<be+Er;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 zs(t){let r=[join(__dirname,"cli.cjs"),join(__dirname,"cli.js"),join(__dirname,"..","dist","cli.cjs"),join(__dirname,"..","dist","cli.js")].find(n=>existsSync(n));if(!r){let n=await ae(t);return process.on("exit",()=>{n?.dispose();}),n.port}return new Promise(n=>{let s=spawn(process.execPath,[r,"serve",t],{detached:true,stdio:["ignore","ignore","pipe"],env:{...process.env}}),a="",l=false,i=setTimeout(()=>{l||(l=true,s.stderr?.removeAllListeners(),Us().then(n));},5e3);s.stderr?.on("data",d=>{a+=d.toString();let o=a.match(/127\.0\.0\.1:(\d+)/);o&&!l&&(l=true,clearTimeout(i),s.stderr?.removeAllListeners(),s.unref(),n(Number(o[1])));}),s.on("error",()=>{l||(l=true,clearTimeout(i),n(null));}),s.on("exit",()=>{l||(l=true,clearTimeout(i),n(null));}),s.unref();})}var Ws=20,tt=0,rt=[];async function nt(t,e){tt>=Ws&&await new Promise(r=>rt.push(r)),tt++;try{return await copyFile(t,e),!0}catch{return false}finally{tt--,rt.length>0&&rt.shift()();}}var K=[];async function Nr(){K.length!==0&&(await Promise.allSettled(K),K.length=0);}function Fr(t){let e=new Date,r=a=>String(a).padStart(2,"0"),n=`${e.getFullYear()}-${r(e.getMonth()+1)}-${r(e.getDate())}T${r(e.getHours())}-${r(e.getMinutes())}-${r(e.getSeconds())}`;if(!existsSync(join(t,n)))return n;let s=1;for(;existsSync(join(t,`${n}-${s}`));)s++;return `${n}-${s}`}function Pr(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 Vs=8;function Gs(t,e,r){let n=Pr(t),s=r?Pr(r).substring(0,Vs):"";return s&&(n+=`--${s}`),e>0&&(n+=`--retry-${e}`),n}function Dr(t,e,r,n,s,a){let l=t.find(f=>f.name==="screenshot"&&f.path),i=t.find(f=>f.name==="video"&&f.path),d=t.find(f=>f.name==="trace"&&f.path);if(!l&&!i&&!d)return null;let o=Gs(e,r,a),c=s?["artifacts",s,o]:["artifacts",o],p=join(n,...c),g=c,u={};try{mkdirSync(p,{recursive:!0});}catch{return null}if(l?.path&&existsSync(l.path)){let h=`screenshot${extname(l.path)||".png"}`,m=join(p,h);K.push(nt(l.path,m).then(()=>{})),u.screenshot=`${g.join("/")}/${h}`;}if(i?.path&&existsSync(i.path)){let h=`video${extname(i.path)||".webm"}`,m=join(p,h);K.push(nt(i.path,m).then(()=>{})),u.video=`${g.join("/")}/${h}`;}if(d?.path&&existsSync(d.path)){let h=`trace${extname(d.path)||".zip"}`,m=join(p,h);K.push(nt(d.path,m).then(()=>{})),u.trace=`${g.join("/")}/${h}`;}return !u.screenshot&&!u.video&&!u.trace?null:u}var Qs=/^\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}(-\d+)?$/,Zs="1.0";function Xs(t){let e=extname(t).toLowerCase();return [".png",".jpg",".jpeg",".gif",".bmp",".webp"].includes(e)?"screenshot":[".webm",".mp4",".avi",".mov"].includes(e)?"video":e===".zip"&&basename(t,e)==="trace"?"trace":"other"}function Br(t,e,r){let n=[];try{let s=readdirSync(t,{withFileTypes:!0});for(let a of s){if(!a.isFile())continue;let l=join(t,a.name),i=statSync(l);n.push({name:a.name,type:Xs(a.name),relativePath:`artifacts/${e}/${r}/${a.name}`,sizeBytes:i.size});}}catch{}return {testName:r,files:n}}function ea(t,e){let r=join(t,e),n=[],s=0;try{let l=readdirSync(r,{withFileTypes:!0});for(let i of l){if(!i.isDirectory())continue;let d=Br(join(r,i.name),e,i.name);n.push(d);for(let o of d.files)s+=o.sizeBytes;}}catch{}let a=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:a,totalSizeBytes:s,testCount:n.length,tests:n,isCurrentRun:false}}function Or(t,e){let r=join(t,"artifacts"),n=[],s=[],a=0;try{let i=readdirSync(r,{withFileTypes:!0});for(let d of i)if(d.isDirectory())if(Qs.test(d.name)){let o=ea(r,d.name);n.push({...o,isCurrentRun:d.name===e});}else {let o=Br(join(r,d.name),d.name,d.name);s.push(o);for(let c of o.files)a+=c.sizeBytes;}}catch{}n.sort((i,d)=>d.timestamp.localeCompare(i.timestamp)),s.length>0&&(s.sort((i,d)=>i.testName.localeCompare(d.testName)),n.push({folderName:"__legacy__",timestamp:"1970-01-01T00:00:00",totalSizeBytes:a,testCount:s.length,tests:s,isCurrentRun:false}));let l=n.reduce((i,d)=>i+d.totalSizeBytes,0);return {schemaVersion:Zs,generatedAt:new Date().toISOString(),artifactBaseDir:"artifacts",totalSizeBytes:l,runs:n,serverPort:null}}var ia=256;function oa(t){let e=resolve(t);for(let r=0;r<ia;r++){if(existsSync(join(e,".git")))return e;let n=dirname(e);if(n===e)return null;e=n;}return null}function Hr(t){try{let e=oa(t);if(!e)return;let r=join(e,".gitignore"),n=relative(e,t).replace(/\\/g,"/"),s=n.endsWith("/")?n:`${n}/`;if(existsSync(r)&&readFileSync(r,"utf-8").split(`
|
|
4253
|
+
`);try{let h=Ct(l,c,n,u),v=i+".tmp";writeFileSync(v,h,"utf-8"),renameSync(v,i);}catch{}}}catch{}}}else if(e.openReport&&t.ci===null&&e.includeArtifacts){let p=dirname(e.outputPath),g=join(p,"artifacts");if(existsSync(g))try{let m=await cn(g);n=m.port,process.on("exit",()=>{m.dispose();});}catch{}}if(e.openReport&&t.ci===null)if(a&&n)Tt(`http://127.0.0.1:${n}`);else {let p=resolve(i);Tt(p);}}catch(n){process.stderr.write(`[testrelic] Failed to write HTML report: ${n instanceof Error?n.message:String(n)}
|
|
4254
|
+
`);}}var Ie=9323,hn=10;async function ri(){for(let t=Ie;t<Ie+hn;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 ni(){for(let t=Ie;t<Ie+hn;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 si(t){let r=[join(__dirname,"cli.cjs"),join(__dirname,"cli.js"),join(__dirname,"..","dist","cli.cjs"),join(__dirname,"..","dist","cli.js")].find(n=>existsSync(n));if(!r){let n=await me(t);return process.on("exit",()=>{n?.dispose();}),n.port}return new Promise(n=>{let s=spawn(process.execPath,[r,"serve",t],{detached:true,stdio:["ignore","ignore","pipe"],env:{...process.env}}),a="",o=false,i=setTimeout(()=>{o||(o=true,s.stderr?.removeAllListeners(),ri().then(n));},5e3);s.stderr?.on("data",d=>{a+=d.toString();let l=a.match(/127\.0\.0\.1:(\d+)/);l&&!o&&(o=true,clearTimeout(i),s.stderr?.removeAllListeners(),s.unref(),n(Number(l[1])));}),s.on("error",()=>{o||(o=true,clearTimeout(i),n(null));}),s.on("exit",()=>{o||(o=true,clearTimeout(i),n(null));}),s.unref();})}var mn=Buffer.from([137,80,78,71,13,10,26,10]),ai=16,ii=20,Me=24;function vn(t){if(t.length<Me||!t.subarray(0,mn.length).equals(mn))return null;let e=t.readUInt32BE(ai),r=t.readUInt32BE(ii);return e===0||r===0?null:{width:e,height:r}}var oi=/(\d+) pixels \(ratio [\d.]+ of all image pixels\) are different/,yn=/Expected an image (\d+)px by (\d+)px, received (\d+)px by (\d+)px/;function Ee(t){return t?yn.test(t):false}var li=/^\s*Snapshot:\s*(.+?)\s*$/m;function bn(t){if(!t)return {};let e={},r=oi.exec(t);if(r){let a=Number.parseInt(r[1],10);Number.isFinite(a)&&(e.diffPixels=a);}let n=yn.exec(t);if(n){let[,a,o,i,d]=n,l=[a,o,i,d].map(c=>Number.parseInt(c,10));l.every(c=>Number.isFinite(c))&&(e.expectedWidth=l[0],e.expectedHeight=l[1],e.actualWidth=l[2],e.actualHeight=l[3]);}let s=li.exec(t);return s&&(e.snapshotName=s[1]),e}function xn(t,e){if(t===void 0||!e)return;let r=e.width*e.height;if(!(r<=0))return t/r}var gi=20,Et=0,Pt=[];async function ve(t,e){Et>=gi&&await new Promise(r=>Pt.push(r)),Et++;try{return await copyFile(t,e),!0}catch{return false}finally{Et--,Pt.length>0&&Pt.shift()();}}var W=[];async function kn(){W.length!==0&&(await Promise.allSettled(W),W.length=0);}function Cn(t){let e=new Date,r=a=>String(a).padStart(2,"0"),n=`${e.getFullYear()}-${r(e.getMonth()+1)}-${r(e.getDate())}T${r(e.getHours())}-${r(e.getMinutes())}-${r(e.getSeconds())}`;if(!existsSync(join(t,n)))return n;let s=1;for(;existsSync(join(t,`${n}-${s}`));)s++;return `${n}-${s}`}function Lt(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 hi=8;function Tn(t,e,r){let n=Lt(t),s=r?Lt(r).substring(0,hi):"";return s&&(n+=`--${s}`),e>0&&(n+=`--retry-${e}`),n}function Sn(t,e,r,n,s,a){let o=t.find(g=>g.name==="screenshot"&&g.path),i=t.find(g=>g.name==="video"&&g.path),d=t.find(g=>g.name==="trace"&&g.path);if(!o&&!i&&!d)return null;let l=Tn(e,r,a),c=s?["artifacts",s,l]:["artifacts",l],u=join(n,...c),f=c,p={};try{mkdirSync(u,{recursive:!0});}catch{return null}if(o?.path&&existsSync(o.path)){let m=`screenshot${extname(o.path)||".png"}`,h=join(u,m);W.push(ve(o.path,h).then(()=>{})),p.screenshot=`${f.join("/")}/${m}`;}if(i?.path&&existsSync(i.path)){let m=`video${extname(i.path)||".webm"}`,h=join(u,m);W.push(ve(i.path,h).then(()=>{})),p.video=`${f.join("/")}/${m}`;}if(d?.path&&existsSync(d.path)){let m=`trace${extname(d.path)||".zip"}`,h=join(u,m);W.push(ve(d.path,h).then(()=>{})),p.trace=`${f.join("/")}/${m}`;}return !p.screenshot&&!p.video&&!p.trace?null:p}var Nt="visual",mi=6;function vi(t){let e=createHash("sha256").update(t).digest("hex").substring(0,mi);return `${Lt(t)}--${e}`}function wn(t){let e=null;try{e=openSync(t,"r");let r=Buffer.alloc(Me),n=readSync(e,r,0,Me,0);return vn(r.subarray(0,n))}catch{return null}finally{if(e!==null)try{closeSync(e);}catch{}}}var yi=[{key:"expectedPath",dest:"baseline",field:"baseline"},{key:"actualPath",dest:"actual",field:"actual"},{key:"diffPath",dest:"diff",field:"diff"},{key:"previousPath",dest:"previous",field:"previous"}],bi=[{key:"baselineDomPath",dest:"baseline.dom.json",field:"baselineDom"},{key:"actualDomPath",dest:"actual.dom.json",field:"actualDom"}];function xi(t,e,r){let n=vi(t.name),s=join(e,n);try{mkdirSync(s,{recursive:!0});}catch{return null}let a={},o=false;for(let c of yi){let u=t[c.key];if(!u||!existsSync(u))continue;let f=extname(u)||".png",p=`${c.dest}${f}`;W.push(ve(u,join(s,p)).then(()=>{})),a[c.field]=`${r}/${Nt}/${n}/${p}`,o=true;}let i=t.dom;if(i)for(let c of bi){let u=i[c.key];!u||!existsSync(u)||(W.push(ve(u,join(s,c.dest)).then(()=>{})),a[c.field]=`${r}/${Nt}/${n}/${c.dest}`);}if(!o)return null;let d=t.expectedPath?wn(t.expectedPath):t.actualPath?wn(t.actualPath):null,l=xn(t.diffPixels,d);return {name:t.name,status:t.status,source:t.source,...a,...d?{width:d.width,height:d.height}:{},...t.actualWidth!==void 0?{actualWidth:t.actualWidth}:{},...t.actualHeight!==void 0?{actualHeight:t.actualHeight}:{},...t.diffPixels!==void 0?{diffPixels:t.diffPixels}:{},...l!==void 0?{diffPixelRatio:l}:{},...i?{domChanges:i.changes,domChangeCount:i.total,domRootCauses:i.rootCauses,domTruncated:i.truncated,domCaptureBox:i.captureBox}:{}}}function Rn(t,e,r,n,s,a){if(t.length===0)return null;let o=Tn(e,r,a),i=s?["artifacts",s,o]:["artifacts",o],d=join(n,...i,Nt),l=i.join("/");try{mkdirSync(d,{recursive:!0});}catch{return null}let c=[];for(let u of t){let f=xi(u,d,l);f&&c.push(f);}return c.length>0?c:null}function An(t){let{styleProps:e,defaults:r,inherited:n,maxNodes:s,maxText:a,maskBoxes:o,viewportBox:i,fullPage:d}=t,l={};for(let T=0;T<n.length;T++)l[n[T]]=true;let c=window.scrollX||window.pageXOffset||0,u=window.scrollY||window.pageYOffset||0,f=[],p=0,g=[];for(let T=0;T<o.length;T++){let w=o[T];g.push([w[0]+c,w[1]+u,w[2],w[3]]);}function m(T,w,S,x){for(let C=0;C<g.length;C++){let _=g[C];if(T<_[0]+_[2]&&T+S>_[0]&&w<_[1]+_[3]&&w+x>_[1])return true}return false}function h(T){let w=T.parentElement;if(!w)return 0;let S=w.children,x=0;for(let C=0;C<S.length;C++){let _=S[C];if(_===T)break;_.tagName===T.tagName&&x++;}return x}function v(T){let w="",S=T.childNodes;for(let x=0;x<S.length;x++){let C=S[x];C.nodeType===3&&(w+=C.nodeValue??"");}return w=w.replace(/\s+/g," ").trim(),w.length>a?w.slice(0,a):w}function y(T,w){let S;for(let x=0;x<e.length;x++){let C=e[x],_=T.getPropertyValue(C);if(_){if(l[C]){if(w&&w.getPropertyValue(C)===_)continue}else if(r[C]===_)continue;S||(S={}),S[C]=_;}}return S}let b=[{el:document.body,parentPath:"",parentComputed:null}];for(;b.length>0;){let T=b.pop(),w=T.el,S=window.getComputedStyle(w);if(S.display==="none")continue;let x=w.tagName.toLowerCase(),C=w.id?"#"+w.id:T.parentPath?T.parentPath+">"+x+"["+h(w)+"]":x,_=w.children;for(let P=_.length-1;P>=0;P--)b.push({el:_[P],parentPath:C,parentComputed:S});if(S.visibility==="hidden")continue;let L=w.getBoundingClientRect();if(L.width<=0||L.height<=0)continue;if(f.length>=s){p++;continue}let z=Math.round(L.left+c),ce=Math.round(L.top+u),ue=Math.round(L.width),pe=Math.round(L.height),I={p:C,t:x,r:[z,ce,ue,pe]};w.id&&(I.i=w.id);let M=w.getAttribute("class");M&&(I.c=M.replace(/\s+/g," ").trim());let N=v(w);N&&(I.x=N);let j=y(S,T.parentComputed);j&&(I.s=j),m(z,ce,ue,pe)&&(I.m=1),f.push(I);}let k=Math.max(document.documentElement.scrollWidth,window.innerWidth),A=Math.max(document.documentElement.scrollHeight,window.innerHeight),E;return i?E=[Math.round(i[0]+c),Math.round(i[1]+u),Math.round(i[2]),Math.round(i[3])]:d?E=[0,0,k,A]:E=[Math.round(c),Math.round(u),window.innerWidth,window.innerHeight],{version:"1.0",url:location.href,viewport:{width:window.innerWidth,height:window.innerHeight},content:{width:k,height:A},captureBox:E,nodes:f,truncatedNodes:p}}var Ri=["display","position","color","background-color","background-image","border-top-width","border-right-width","border-bottom-width","border-left-width","border-color","border-style","border-radius","box-shadow","opacity","transform","margin-top","margin-right","margin-bottom","margin-left","padding-top","padding-right","padding-bottom","padding-left","font-family","font-size","font-weight","line-height","letter-spacing","text-align","text-decoration-line","text-transform","flex-direction","justify-content","align-items","gap","z-index","overflow"],Ai={position:"static","background-color":"rgba(0, 0, 0, 0)","background-image":"none","border-top-width":"0px","border-right-width":"0px","border-bottom-width":"0px","border-left-width":"0px","border-style":"none","border-radius":"0px","box-shadow":"none",opacity:"1",transform:"none","margin-top":"0px","margin-right":"0px","margin-bottom":"0px","margin-left":"0px","padding-top":"0px","padding-right":"0px","padding-bottom":"0px","padding-left":"0px","text-decoration-line":"none","flex-direction":"row","justify-content":"normal","align-items":"normal",gap:"normal","z-index":"auto",overflow:"visible"},_i=["color","font-family","font-size","font-weight","line-height","letter-spacing","text-align","text-transform"],Ii=1024*1024,Mi=1500,Ei=200;async function Pi(t){let e=[];for(let r of t)try{let n=await r.boundingBox();n&&e.push([n.x,n.y,n.width,n.height]);}catch{}return e}async function _n(t){let e=t.config??{};try{let r=t.maskLocators?.length?await Pi(t.maskLocators):[],n=null;if(t.bounded){let a=await t.bounded.boundingBox();a&&(n=[a.x,a.y,a.width,a.height]);}let s={styleProps:Ri,defaults:Ai,inherited:_i,maxNodes:e.maxNodes??Mi,maxText:e.maxText??Ei,maskBoxes:r,viewportBox:n,fullPage:t.fullPage===!0};return await t.target.evaluate(An,s)}catch{return null}}function In(t,e=Ii){let r=JSON.stringify(t);if(Buffer.byteLength(r,"utf-8")<=e)return {json:r,truncatedBytes:false};let n=t.nodes.slice(),s=0;for(;n.length>0&&Buffer.byteLength(r,"utf-8")>e;){let a=Math.max(1,Math.floor(n.length*.1));n.splice(-a,a),s+=a,r=JSON.stringify({...t,nodes:n,truncatedNodes:t.truncatedNodes+s});}return {json:r,truncatedBytes:true}}function Mn(t){return t.replace(/\.[A-Za-z0-9]+$/,"")+".dom.json"}function En(t){try{if(!existsSync(t))return null;let e=JSON.parse(readFileSync(t,"utf-8"));return Li(e)?e:null}catch{return null}}function Pn(t,e){try{return mkdirSync(dirname(t),{recursive:!0}),writeFileSync(t,e,"utf-8"),!0}catch{return false}}function Li(t){if(typeof t!="object"||t===null)return false;let e=t;return typeof e.version=="string"&&Array.isArray(e.nodes)&&Array.isArray(e.captureBox)&&e.captureBox.length===4}var Ni=new Set(["moved","resized"]);function Le(t){return t.some(e=>!Ni.has(e))}function Ft(t){let e=t.t;if(t.i&&(e+="#"+t.i),t.c){let r=t.c.split(" ").filter(Boolean).slice(0,3);r.length>0&&(e+="."+r.join("."));}return e}function Di(t,e){let r=[],n=Math.abs(t.r[0]-e.r[0]),s=Math.abs(t.r[1]-e.r[1]),a=Math.abs(t.r[2]-e.r[2]),o=Math.abs(t.r[3]-e.r[3]);return (n>1||s>1)&&r.push("moved"),(a>1||o>1)&&r.push("resized"),r}function Fi(t,e){let r=t.s??{},n=e.s??{},s=new Set([...Object.keys(r),...Object.keys(n)]),a=[];for(let o of s){let i=r[o],d=n[o];i!==d&&a.push({prop:o,from:i??"(initial)",to:d??"(initial)"});}return a.sort((o,i)=>o.prop.localeCompare(i.prop)),a}function Ln(t){return t.i?"i:"+t.t+"#"+t.i:t.c?"c:"+t.t+"."+t.c:null}function Bi(t,e){let r=new Map;for(let u of t)r.set(u.p,u);let n=[],s=[],a=new Set;for(let u of e){let f=r.get(u.p);if(f&&f.t!==u.t){s.push(u);continue}f&&!a.has(f)?(a.add(f),n.push({before:f,after:u})):s.push(u);}let o=t.filter(u=>!a.has(u)),i=new Map;for(let u of o){let f=Ln(u);if(!f)continue;let p=i.get(f);p?p.push(u):i.set(f,[u]);}let d=[],l=new Set;for(let u of s){let f=Ln(u),g=(f?i.get(f):void 0)?.find(m=>!l.has(m));g?(l.add(g),n.push({before:g,after:u})):d.push(u);}let c=o.filter(u=>!l.has(u));return {pairs:n,removed:c,added:d}}function Oi(t,e){let r=[];return (t.i??"")!==(e.i??"")&&r.push({prop:"id",from:t.i??"(none)",to:e.i??"(none)"}),(t.c??"")!==(e.c??"")&&r.push({prop:"class",from:t.c??"(none)",to:e.c??"(none)"}),r}function Bt(t,e){let r=(e[2]??0)*(e[3]??0);return r<=0?0:Math.min(1,t.r[2]*t.r[3]/r)}function Nn(t,e,r={}){let n=r.maxChanges??50,s=r.includeText!==false,a=r.includeStyles!==false,o=t.nodes.filter(g=>g.m!==1),i=e.nodes.filter(g=>g.m!==1),{pairs:d,removed:l,added:c}=Bi(o,i),u=[];for(let{before:g,after:m}of d){let h=Di(g,m),v=a?Fi(g,m):[];v.length>0&&h.push("restyled");let y=Oi(g,m);y.length>0&&h.push("attrs");let b=s&&(g.x??"")!==(m.x??"");if(b&&h.push("text"),h.length===0)continue;let k=Bt(m,e.captureBox);u.push({score:k,change:{path:m.p,label:Ft(m),kinds:h,rect:m.r,baselineRect:g.r,areaPct:k,...y.length>0?{attrs:y}:{},...v.length>0?{styles:v}:{},...b?{text:{from:g.x??"",to:m.x??""}}:{}}});}for(let g of c){let m=Bt(g,e.captureBox);u.push({score:m,change:{path:g.p,label:Ft(g),kinds:["added"],rect:g.r,areaPct:m}});}for(let g of l){let m=Bt(g,t.captureBox);u.push({score:m,change:{path:g.p,label:Ft(g),kinds:["removed"],baselineRect:g.r,areaPct:m}});}let f=u.filter(g=>Le(g.change.kinds)).length;return u.sort((g,m)=>{if(f>0){let h=Le(g.change.kinds)?1:0,v=Le(m.change.kinds)?1:0;if(h!==v)return v-h}return m.score-g.score}),{changes:u.slice(0,n).map((g,m)=>({rank:m+1,...g.change,...f>0&&!Le(g.change.kinds)?{consequence:true}:{}})),total:u.length,rootCauses:f}}var Dn="__testrelic_snapshot_path_probe__.png",Fn=Symbol.for("testrelic.snapshotPathWarned");function qi(){let t=globalThis,e=t[Fn];return e||(e={warned:false},t[Fn]=e),e}function Ne(t){try{let e=t.snapshotPath(Dn,{kind:"screenshot"});return typeof e=="string"&&e.endsWith(Dn)}catch{return false}}function De(t){let e=qi();e.warned||(e.warned=true,process.stderr.write(`[testrelic] ${t} needs Playwright >= 1.51 for testInfo.snapshotPath(name, { kind }); this project's Playwright resolves that call the older way. Everything else in the reporter is unaffected \u2014 upgrade Playwright to enable it.
|
|
4255
|
+
`));}var Ot="testrelic-visual-dom",Vi=new Set(["toHaveScreenshot","toMatchVisualBaseline"]);function Bn(t){if(typeof t!="object"||t===null)return false;let e=t;return typeof e.boundingBox=="function"&&typeof e.page=="function"}function $i(t){if(typeof t!="object"||t===null)return false;let e=t;return typeof e.evaluate=="function"&&typeof e.screenshot=="function"}function Wi(t){let e=t[0];if(typeof e=="string")return e;if(Array.isArray(e))return e.join("/");if(typeof e=="object"&&e!==null){let r=e.name;if(typeof r=="string")return r}return null}function Gi(t){for(let e of t)if(typeof e=="object"&&e!==null&&!Array.isArray(e))return e;return {}}function Ki(t){let e=t.mask;return Array.isArray(e)?e.filter(r=>typeof r=="object"&&r!==null&&typeof r.boundingBox=="function"):[]}function Ji(t){return /\.(png|jpg|jpeg)$/i.test(t)?t:`${t}.png`}var Yi=/^(.+)-(expected|actual|diff|previous)\.[A-Za-z0-9]+$/;function Xi(t,e,r){let n=t.attachments;for(let s=e;s<n.length;s++){let a=Yi.exec(n[s]?.name??"");if(a)return a[1]}return r}async function Zi(t){let e=Gi(t.args),r=Bn(t.received)?t.received:null,n=r?r.page():t.received;return _n({target:n,bounded:r,maskLocators:Ki(e),fullPage:e.fullPage===true})}function Qi(t){let e=t.config?.updateSnapshots;return e==="all"||e==="changed"}async function eo(t,e){let r=Wi(t.args);if(!r)return;if(!Ne(t.testInfo)){De("DOM comparison");return}let n=t.testInfo.snapshotPath(Ji(r),{kind:"screenshot"}),s=Mn(n),a=Xi(t.testInfo,t.attachedBefore,basename(n,extname(n))),o=In(e),d=Qi(t.testInfo)?null:En(s);if(!d){Pn(s,o.json);return}let{changes:l,total:c,rootCauses:u}=Nn(d,e),f;try{f=t.testInfo.outputPath(`${a}.actual.dom.json`),writeFileSync(f,o.json,"utf-8");}catch{f=void 0;}let p={stem:a,changes:l,total:c,rootCauses:u,truncated:o.truncatedBytes||e.truncatedNodes>0,captureBox:e.captureBox,...existsSync(s)?{baselineDomPath:s}:{},...f?{actualDomPath:f}:{}};await t.testInfo.attach(Ot,{body:Buffer.from(JSON.stringify([p])),contentType:"application/json"});}function On(t,e){let r=function(o,...i){let d=t(o,...i);return !(Bn(o)||$i(o))||typeof d!="object"||d===null?d:new Proxy(d,{get(c,u,f){let p=Reflect.get(c,u,f);if(typeof u!="string"||!Vi.has(u)||typeof p!="function")return p;let g=p;return async function(...h){let v=e(),y=null,b=v?.attachments.length??0;if(v)try{y=await Zi({received:o,args:h,testInfo:v,attachedBefore:b});}catch{}try{return await g.apply(c,h)}finally{if(v&&y)try{await eo({received:o,args:h,testInfo:v,attachedBefore:b},y);}catch{}}}}})},n=t,s=r;for(let a of Object.keys(n))s[a]=n[a];return r}var qt="testrelic-visual",to=/^(.+)-(expected|actual|diff|previous)(\.[A-Za-z0-9]+)$/;function ro(t){let e=new Map;for(let r of t)if(!(r.name!==qt||!r.body))try{let n=JSON.parse(r.body.toString("utf-8"));if(!Array.isArray(n))continue;for(let s of n){let a=so(s);a&&e.set(a.stem,a);}}catch{}return e}var no=["passed","failed","new","updated"];function so(t){if(typeof t!="object"||t===null)return null;let e=t;return typeof e.stem!="string"||!e.stem||typeof e.name!="string"||!e.name||typeof e.status!="string"||!no.includes(e.status)?null:{stem:e.stem,name:e.name,status:e.status}}function ao(t){let e=new Map;for(let r of t)if(!(r.name!==Ot||!r.body))try{let n=JSON.parse(r.body.toString("utf-8"));if(!Array.isArray(n))continue;for(let s of n){if(typeof s!="object"||s===null)continue;let a=s;typeof a.stem!="string"||!Array.isArray(a.changes)||e.set(a.stem,s);}}catch{}return e}function io(t){let e=new Map;for(let r of t){if(!r.path)continue;let n=to.exec(r.name);if(!n)continue;let[,s,a]=n,o=e.get(s)??{};o[a]=r.path,e.set(s,o);}return e}function oo(t){return !!(t.actual&&(t.expected||t.diff))}function lo(t,e,r){return t.diff||Ee(r)?"failed":e?"updated":"new"}function co(t,e){if(t.length===0)return null;if(t.length===1)return t[0];if(!e)return null;let r=e.replace(/\.[A-Za-z0-9]+$/,""),n=t.filter(s=>s===r||s.endsWith(`/${r}`));return n.length===1?n[0]:null}function Hn(t,e={}){if(t.length===0)return [];let r=ro(t),n=ao(t),s=io(t);if(s.size===0)return [];let a=e.updatingSnapshots===true,o=[];for(let[i,d]of s){let l=r.get(i);!l&&!oo(d)||o.push({name:l?.name??i,status:l?.status??lo(d,a,e.failureMessage),source:l?"toMatchVisualBaseline":"toHaveScreenshot",...d.expected?{expectedPath:d.expected}:{},...d.actual?{actualPath:d.actual}:{},...d.diff?{diffPath:d.diff}:{},...d.previous?{previousPath:d.previous}:{},...n.has(i)?{dom:n.get(i)}:{}});}return uo(o,s,e.failureMessage)}function uo(t,e,r){let n=bn(r);if(n.diffPixels===void 0&&n.actualWidth===void 0)return [...t];let s=t.filter(o=>o.status==="failed").map(o=>qn(o,e)),a=co(s,n.snapshotName);return a===null?[...t]:t.map(o=>qn(o,e)===a?{...o,...n.diffPixels!==void 0?{diffPixels:n.diffPixels}:{},...n.actualWidth!==void 0?{actualWidth:n.actualWidth}:{},...n.actualHeight!==void 0?{actualHeight:n.actualHeight}:{}}:o)}function qn(t,e){if(e.has(t.name))return t.name;for(let[r,n]of e)if(n.actual===t.actualPath&&n.expected===t.expectedPath&&n.diff===t.diffPath)return r;return t.name}var po=new RegExp("[\x1B\x9B][[\\]()#;?]*(?:\\d{1,4}(?:;\\d{0,4})*)?[0-9A-PR-TZcf-nqry=><]","g");function se(t){return !t||t.indexOf("\x1B")===-1&&t.indexOf("\x9B")===-1?t:t.replace(po,"")}function Un(t){return t==null?null:se(t)}var fo=/^\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}(-\d+)?$/,go="1.0",Ht="visual";function $n(t){let e=extname(t).toLowerCase();return [".png",".jpg",".jpeg",".gif",".bmp",".webp"].includes(e)?"screenshot":[".webm",".mp4",".avi",".mov"].includes(e)?"video":e===".zip"&&basename(t,e)==="trace"?"trace":"other"}function ho(t){if(t.endsWith(".dom.json"))return "dom";let e=basename(t,extname(t));return e==="baseline"?"baseline":e==="diff"?"diff":$n(t)}function mo(t,e,r){let n=[];try{for(let s of readdirSync(t,{withFileTypes:!0})){if(!s.isFile())continue;let a=statSync(join(t,s.name));n.push({name:`${Ht}/${r}/${s.name}`,type:ho(s.name),relativePath:`${e}/${Ht}/${r}/${s.name}`,sizeBytes:a.size});}}catch{}return n}function Wn(t,e,r){let n=[],s=`artifacts/${e}/${r}`;try{let a=readdirSync(t,{withFileTypes:!0});for(let o of a){if(o.isDirectory()&&o.name===Ht){let l=join(t,o.name);for(let c of readdirSync(l,{withFileTypes:!0}))c.isDirectory()&&n.push(...mo(join(l,c.name),s,c.name));continue}if(!o.isFile())continue;let i=join(t,o.name),d=statSync(i);n.push({name:o.name,type:$n(o.name),relativePath:`${s}/${o.name}`,sizeBytes:d.size});}}catch{}return {testName:r,files:n}}function vo(t,e){let r=join(t,e),n=[],s=0;try{let o=readdirSync(r,{withFileTypes:!0});for(let i of o){if(!i.isDirectory())continue;let d=Wn(join(r,i.name),e,i.name);n.push(d);for(let l of d.files)s+=l.sizeBytes;}}catch{}let a=e.replace(/^(\d{4}-\d{2}-\d{2})T(\d{2})-(\d{2})-(\d{2})/,"$1T$2:$3:$4").replace(/-\d+$/,"");return n.sort((o,i)=>o.testName.localeCompare(i.testName)),{folderName:e,timestamp:a,totalSizeBytes:s,testCount:n.length,tests:n,isCurrentRun:false}}function Gn(t,e){let r=join(t,"artifacts"),n=[],s=[],a=0;try{let i=readdirSync(r,{withFileTypes:!0});for(let d of i)if(d.isDirectory())if(fo.test(d.name)){let l=vo(r,d.name);n.push({...l,isCurrentRun:d.name===e});}else {let l=Wn(join(r,d.name),d.name,d.name);s.push(l);for(let c of l.files)a+=c.sizeBytes;}}catch{}n.sort((i,d)=>d.timestamp.localeCompare(i.timestamp)),s.length>0&&(s.sort((i,d)=>i.testName.localeCompare(d.testName)),n.push({folderName:"__legacy__",timestamp:"1970-01-01T00:00:00",totalSizeBytes:a,testCount:s.length,tests:s,isCurrentRun:false}));let o=n.reduce((i,d)=>i+d.totalSizeBytes,0);return {schemaVersion:go,generatedAt:new Date().toISOString(),artifactBaseDir:"artifacts",totalSizeBytes:o,runs:n,serverPort:null}}var Co=256;function To(t){let e=resolve(t);for(let r=0;r<Co;r++){if(existsSync(join(e,".git")))return e;let n=dirname(e);if(n===e)return null;e=n;}return null}function Yn(t){try{let e=To(t);if(!e)return;let r=join(e,".gitignore"),n=relative(e,t).replace(/\\/g,"/"),s=n.endsWith("/")?n:`${n}/`;if(existsSync(r)&&readFileSync(r,"utf-8").split(`
|
|
2552
4256
|
`).map(d=>d.trim()).some(d=>d===s||d===n))return;let a=`
|
|
2553
4257
|
# TestRelic test artifacts
|
|
2554
4258
|
${s}
|
|
2555
|
-
`;appendFileSync(r,a,"utf-8");}catch{}}var
|
|
4259
|
+
`;appendFileSync(r,a,"utf-8");}catch{}}var Xn=Symbol("testrelic.timeout"),Zn=3e4,Ut=12e4,zt=6e4,Qn=15e3;async function Q(t,e,r){let n,s=new Promise(a=>{n=setTimeout(()=>a(Xn),e),typeof n?.unref=="function"&&n.unref();});try{let a=await Promise.race([t(),s]);if(a===Xn){process.stderr.write(`[testrelic] ${r} timed out after ${Math.round(e/1e3)}s \u2014 continuing without it. Your tests are unaffected.
|
|
2556
4260
|
`);return}return a}catch(a){process.stderr.write(`[testrelic] ${r} failed \u2014 continuing without it. Your tests are unaffected. (${a instanceof Error?a.message:String(a)})
|
|
2557
|
-
`);return}finally{n&&clearTimeout(n);}}function
|
|
2558
|
-
`}function
|
|
4261
|
+
`);return}finally{n&&clearTimeout(n);}}function rs(t,e){let r=[];for(let n of t){let s=So(n),a=Ro(n),o=Ao(n,s,a,e),i=_o(n,s,a);if(o.length===0&&i.length===0){r.push({type:"navigation",url:"about:blank",timestamp:n.startedAt,durationOnUrl:n.duration,navigationType:"dummy",domContentLoadedAt:null,networkIdleAt:null,networkStats:null,specFile:n.specFile,test:s,tests:[a],_testTitle:n.title});continue}r.push(...o,...i);}return r.sort(Io),Mo(r,t),r.map((n,s)=>n.type==="navigation"?{...{index:s,type:"navigation",url:n.url,timestamp:n.timestamp,durationOnUrl:n.durationOnUrl??0,navigationType:n.navigationType,domContentLoadedAt:n.domContentLoadedAt??null,networkIdleAt:n.networkIdleAt??null,networkStats:n.networkStats??null,specFile:n.specFile,test:n.test},tests:n.tests}:{...{index:s,type:"api_call",callId:n.callId,method:n.method,url:n.url,timestamp:n.timestamp,responseTime:n.responseTime??null,request:n.request,response:n.response??null,...n.error?{error:n.error}:{},assertions:n.assertions??[],specFile:n.specFile,test:n.test},tests:n.tests})}function So(t){return {title:t.title,fullTitle:t.titlePath,status:t.status,duration:t.duration,retries:t.retryCount,retry:t.retry,tags:t.tags,failure:t.failure}}function Ro(t){return {title:t.title,status:t.status,duration:t.duration,startedAt:t.startedAt,completedAt:t.completedAt,retryCount:t.retryCount,tags:t.tags,failure:t.failure,testId:t.testId,filePath:t.filePath,suiteName:t.suiteName,testType:t.testType,isFlaky:t.isFlaky,retryStatus:t.retryStatus,expectedStatus:t.expectedStatus,actualStatus:t.actualStatus,artifacts:t.artifacts,networkRequests:t.networkRequests,apiCalls:t.apiCalls,apiAssertions:t.apiAssertions,actions:t.actions,consoleLogs:t.consoleLogs}}function Ao(t,e,r,n){let s=[];for(let a of t.navigations)n.navigationTypes!==null&&!n.navigationTypes.includes(a.navigationType)||s.push({type:"navigation",url:a.url,timestamp:a.timestamp,durationOnUrl:0,navigationType:a.navigationType,domContentLoadedAt:a.domContentLoadedAt??null,networkIdleAt:a.networkIdleAt??null,networkStats:a.networkStats??null,specFile:t.specFile,test:e,tests:[r],_testTitle:t.title});return s}function _o(t,e,r){if(!t.apiCalls||t.apiCalls.length===0)return [];let n=t.apiAssertions??[];return t.apiCalls.map(s=>{let a=n.filter(i=>i.callId===s.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}:{}})),o=null;return s.responseStatusCode!==null&&s.responseStatusText!==null&&(o={statusCode:s.responseStatusCode,statusText:s.responseStatusText,headers:s.responseHeaders,body:ts(s.responseBody)}),{type:"api_call",callId:s.id,method:s.method,url:s.url,timestamp:s.timestamp,responseTime:s.error?null:s.responseTimeMs,request:{headers:s.requestHeaders,body:ts(s.requestBody)},response:o,...s.error?{error:s.error}:{},assertions:a,specFile:t.specFile,test:e,tests:[r],_testTitle:t.title}})}function Io(t,e){let r=new Date(t.timestamp).getTime(),n=new Date(e.timestamp).getTime();if(r!==n)return r-n;let s={navigation:0,api_call:1},a=s[t.type],o=s[e.type];return a!==o?a-o:t.type==="api_call"&&e.type==="api_call"&&t.callId&&e.callId?es(t.callId)-es(e.callId):0}function es(t){let e=t.match(/(\d+)$/);return e?parseInt(e[1],10):0}function Mo(t,e){for(let r=0;r<t.length;r++){let n=t[r];if(n.type!=="navigation")continue;let s=new Date(n.timestamp).getTime(),a=null;for(let o=r+1;o<t.length;o++)if(t[o]._testTitle===n._testTitle){a=new Date(t[o].timestamp).getTime();break}if(a!==null)n.durationOnUrl=Math.max(0,a-s);else {let o=e.find(i=>i.title===n._testTitle);if(o){let i=new Date(o.completedAt).getTime();n.durationOnUrl=Math.max(0,i-s);}}}}function ts(t){if(t==null)return null;try{return JSON.parse(t)}catch{return t}}function ee(t,e){let r=t.length,n=Math.min(Math.ceil(e/100*r)-1,r-1);return t[Math.max(0,n)]}function jt(t){try{let e=new URL(t);return e.origin+e.pathname}catch{return t}}function Eo(t){return t==null?"error":t>=200&&t<300?"2xx":t>=300&&t<400?"3xx":t>=400&&t<500?"4xx":t>=500&&t<600?"5xx":"error"}function ns(t,e){let r=0,n=0,s=0,a=0,o=0;for(let x of t)switch(x.status){case "passed":r++;break;case "failed":n++;break;case "flaky":s++;break;case "skipped":a++;break;case "timedout":o++;break}let i=[];for(let x of t)x.apiCalls&&i.push(...x.apiCalls);let d=i.length,l=new Set,c={},u={"2xx":0,"3xx":0,"4xx":0,"5xx":0,error:0},f=[];for(let x of i)l.add(jt(x.url)),c[x.method]=(c[x.method]??0)+1,u[Eo(x.responseStatusCode)]+=1,f.push(x.responseTimeMs);let p=null;if(f.length>0){let x=[...f].sort((_,L)=>_-L),C=x.reduce((_,L)=>_+L,0);p={avg:Math.round(C/x.length),min:x[0],max:x[x.length-1],p50:ee(x,50),p95:ee(x,95),p99:ee(x,99)};}let g=0,m=0,h=0;for(let x of t)if(x.apiAssertions)for(let C of x.apiAssertions)g++,C.status==="passed"?m++:h++;let v=0,y=new Set;for(let x of t){v+=x.navigations.length;for(let C of x.navigations)y.add(C.url);}let b=0,k={};function A(x){for(let C of x)b++,k[C.category]=(k[C.category]??0)+1,C.children.length>0&&A(C.children);}for(let x of t)x.actions&&A(x.actions);let E=0,T=0,w=0,S=0;for(let x of t)if(x.visual)for(let C of x.visual)E++,C.status==="failed"?T++:(C.status==="new"||C.status==="updated")&&w++,S+=C.domChangeCount??0;return {total:t.length,passed:r,failed:n,flaky:s,skipped:a,timedout:o,totalApiCalls:d,uniqueApiUrls:l.size,apiCallsByMethod:c,apiCallsByStatusRange:u,apiResponseTime:p,totalAssertions:g,passedAssertions:m,failedAssertions:h,totalNavigations:v,uniqueNavigationUrls:y.size,totalTimelineSteps:e,totalActionSteps:b,actionStepsByCategory:k,visualChecks:E,visualFailures:T,visualNew:w,visualDomChanges:S}}function Po(t){let e=56-t.length;return e>0?t+" ".repeat(e):t}function F(t){return `\u2502 ${Po(t)} \u2502
|
|
4262
|
+
`}function Vt(t,e,r,n){if(n||t.total===0)return;let s=`\u250C${"\u2500".repeat(58)}\u2510
|
|
2559
4263
|
`,a=`\u2514${"\u2500".repeat(58)}\u2518
|
|
2560
|
-
`,
|
|
2561
|
-
`,process.stderr.write(i);}var
|
|
2562
|
-
`)[0]?.trim();return n?
|
|
2563
|
-
`),n=JSON.stringify(
|
|
2564
|
-
`),n=JSON.stringify(
|
|
2565
|
-
`),n=JSON.stringify(
|
|
2566
|
-
`),n=JSON.stringify(
|
|
4264
|
+
`,o=F(""),i=s;i+=F("TestRelic AI - Playwright Test Report"),i+=o;let d=[];if(t.passed>0&&d.push(`${t.passed} \u2713`),t.failed>0&&d.push(`${t.failed} \u2717`),t.flaky>0&&d.push(`${t.flaky} \u26A0`),t.skipped>0&&d.push(`${t.skipped} skipped`),t.timedout>0&&d.push(`${t.timedout} timedout`),i+=F(`Tests: ${t.total} total (${d.join(" ")})`),t.totalNavigations>0&&(i+=F(`Navigations: ${t.totalNavigations} visits across ${t.uniqueNavigationUrls} unique URLs`)),t.totalApiCalls>0){i+=F(`API Calls: ${t.totalApiCalls} calls across ${t.uniqueApiUrls} unique endpoints`);let l=Object.entries(t.apiCallsByMethod).filter(([,u])=>u>0).map(([u,f])=>`${u}: ${f}`);l.length>0&&(i+=F(` ${l.join(" ")}`));let c=Object.entries(t.apiCallsByStatusRange).filter(([,u])=>u>0).map(([u,f])=>`${u}: ${f}`);c.length>0&&(i+=F(` ${c.join(" ")}`)),t.apiResponseTime&&(i+=F(` Avg response: ${t.apiResponseTime.avg}ms P95: ${t.apiResponseTime.p95}ms`));}if(t.totalAssertions>0&&(i+=F(`Assertions: ${t.totalAssertions} total (${t.passedAssertions} \u2713 ${t.failedAssertions} \u2717)`)),t.totalActionSteps>0){let l=[],c=t.actionStepsByCategory;c.ui_action&&l.push(`${c.ui_action} UI`),c.assertion&&l.push(`${c.assertion} assertions`),c.custom_step&&l.push(`${c.custom_step} custom`);let u=l.length>0?` (${l.join(" ")})`:"";i+=F(`Actions: ${t.totalActionSteps} steps${u}`);}i+=F(`Report: ${r}`),i+=F(`Data: ${e}`),i+=a,i+=`For more information visit us at https://docs.testrelic.ai
|
|
4265
|
+
`,process.stderr.write(i);}var Lo=new Set(["pw:api","expect","test.step"]);function No(t){return t==="expect"?"assertion":t==="test.step"?"custom_step":"ui_action"}function $t(t,e,r){if(Lo.has(t.category)){let n=[];for(let a of t.steps)$t(a,e,n);let s=t.startTime.getTime()-e.getTime();r.push({title:t.title,category:No(t.category),timestamp:t.startTime.toISOString(),duration:t.duration,videoOffset:s>=0?s/1e3:null,status:t.error?"failed":"passed",error:Un(t.error?.message),children:n});}else for(let n of t.steps)$t(n,e,r);}function ss(t,e){let r=[];for(let n of t)$t(n,e,r);return r}var Bo=new Set(["localhost","127.0.0.1","0.0.0.0"]);function is(t){let e=new URL(t);if(e.protocol!=="https:"&&!(e.protocol==="http:"&&Bo.has(e.hostname)))throw createError(ErrorCode.CLOUD_CONFIG_INVALID,`HTTPS is required for cloud communication. Got: ${e.protocol}//${e.hostname}`)}var Oo=3e3;async function Wt(t){let e=new AbortController,r=setTimeout(()=>e.abort(),Oo);try{return (await fetch(`${t}/health`,{method:"GET",signal:e.signal})).ok}catch{return false}finally{clearTimeout(r);}}async function os(t){return new Promise(e=>setTimeout(e,t))}async function as(t){try{let r=(await t.json()).error;if(r&&typeof r.message=="string"){let n=r.details;if(Array.isArray(n)&&n.length>0){let s=n.map(a=>`${a.field}: ${a.message}`).join(", ");return `${r.message} (${s})`}return r.message}}catch{}return `HTTP ${t.status}`}async function Gt(t,e,r){let n=new AbortController,s=setTimeout(()=>n.abort(),r);try{let a=await fetch(`${t}/sdk/auth/token`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({apiKey:e}),signal:n.signal});if(a.ok){let i=await a.json();return {accessToken:i.accessToken,refreshToken:i.refreshToken,expiresIn:i.expiresIn,orgId:i.orgId,orgName:i.orgName,userId:i.userId,userName:i.userName}}if(a.status===429){let i=a.headers.get("Retry-After"),d=i?parseInt(i,10)*1e3:5e3;if(!isNaN(d)&&d>0){await os(d);let l=await fetch(`${t}/sdk/auth/token`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({apiKey:e}),signal:n.signal});if(l.ok){let c=await l.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 a.status===400?{code:"validation_error",message:await as(a),statusCode:400}:a.status===401?{code:"invalid_key",message:"API key is invalid or has been revoked.",statusCode:401}:a.status===403?{code:"expired_key",message:"API key has expired.",statusCode:403}:{code:"server_error",message:await as(a),statusCode:a.status}}catch(a){return a instanceof DOMException&&a.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(s);}}function Kt(t){return "code"in t&&typeof t.code=="string"&&["invalid_key","expired_key","validation_error","rate_limited","server_error","network_error","timeout"].includes(t.code)}async function ls(t,e,r){let n=new AbortController,s=setTimeout(()=>n.abort(),r);try{let a=await fetch(`${t}/sdk/auth/refresh`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({refreshToken:e}),signal:n.signal});if(a.status===429){let i=a.headers.get("Retry-After"),d=i?parseInt(i,10)*1e3:5e3;!isNaN(d)&&d>0&&(await os(d),a=await fetch(`${t}/sdk/auth/refresh`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({refreshToken:e}),signal:n.signal}));}if(!a.ok)return null;let o=await a.json();return {accessToken:o.accessToken,refreshToken:o.refreshToken,expiresIn:o.expiresIn}}catch{return null}finally{clearTimeout(s);}}async function ds(t,e,r,n,s,a){let o=new AbortController,i=setTimeout(()=>o.abort(),s);try{let d={gitId:r,displayName:n};a&&(d.branch=a);let l=await fetch(`${t}/repos/resolve`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${e}`},body:JSON.stringify(d),signal:o.signal});if(!l.ok)return null;let c=await l.json();return {repoId:c.repoId,displayName:c.displayName}}catch{return null}finally{clearTimeout(i);}}var jo=5e3;function K(t,e){try{return execSync(t,{cwd:e,timeout:jo,encoding:"utf-8",stdio:["pipe","pipe","pipe"]}).trim()||null}catch{return null}}function cs(t){let e=K("git rev-parse --abbrev-ref HEAD",t),r=K("git rev-parse --short HEAD",t),n=K("git log -1 --pretty=%s",t),s=K("git log -1 --format=%an",t)??K("git config user.name",t),a=Vo(t),o=a?be(a):null;return {branch:e,commitSha:r,commitMessage:n,commitAuthor:s,remoteUrl:o}}function Vo(t){let e=K("git remote get-url origin",t);if(e)return e;let r=K("git remote",t);if(!r)return null;let n=r.split(`
|
|
4266
|
+
`)[0]?.trim();return n?K(`git remote get-url ${n}`,t):null}function be(t){let e=t.trim();return e=e.replace(/^[a-z+]+:\/\//,""),e=e.replace(/^[^@]+@/,""),e=e.replace(/:(?!\d)/,"/"),e=e.replace(/\.git$/,""),e=e.replace(/\/+$/,""),e=e.replace(/^[^@/]+@/,""),e.toLowerCase()}function us(t){let e=t.split("/").filter(Boolean);return e[e.length-1]??t}function Jt(t){try{let e=readFileSync(join(t,"package.json"),"utf-8"),r=JSON.parse(e);return typeof r.name=="string"&&r.name.length>0?r.name:null}catch{return null}}function xe(t){let e=Jt(t);return e||`local/${basename(t)}`}var $o=new Set(["smoke","regression","nightly","ci"]);function Fe(){let t=process.env.TESTRELIC_RUN_TYPE?.trim().toLowerCase();if(t&&$o.has(t))return t}async function J(t,e,r){let n=new AbortController,s=setTimeout(()=>n.abort(),Math.max(1,r));try{return await fetch(t,{...e,signal:n.signal})}finally{clearTimeout(s);}}function ps(t,e){let r=e+Math.ceil(Math.max(0,t)/1e3);return Math.min(6e5,Math.max(3e4,r))}var Be=class{constructor(e=12e4,r=Date.now()){this.deadline=r+Math.max(0,e);}remaining(e=Date.now()){return Math.max(0,this.deadline-e)}expired(e=Date.now()){return this.remaining(e)===0}slice(e,r=Date.now()){return Math.min(e,this.remaining(r))}};var Ko=1048576,Oe=[1e3,3e3,9e3],qe=3,gs=3e4,Jo=90*1048576,Yo=new Set(["e2e","mobile","unit"]);function Xt(t){if(t&&t.length>0&&t.every(s=>s.testType==="api")){let s=new Map;for(let i of t){let d=i.apiProtocol??"rest";s.set(d,(s.get(d)??0)+1);}let a="rest",o=0;for(let[i,d]of s)d>o&&(o=d,a=i);return {testType:"api",apiProtocol:a,metadata:{testType:"api",primaryProtocol:a}}}let e=new Map;for(let s of t??[])Yo.has(s.testType)&&e.set(s.testType,(e.get(s.testType)??0)+1);let r=null,n=0;for(let[s,a]of e)a>n&&(n=a,r=s);return {testType:r??"e2e"}}function Zt(t,e,r,n,s,a){let o=a?.cloudPlatform?a.cloudPlatform:n?.provider&&n.provider!=="unknown"?n.provider:"local",i=Fe(),d=Xt(s),l=d.testType==="api"?d:null,c=l&&s?s.map(f=>f.apiProtocol?f:{...f,apiProtocol:l.apiProtocol}):s;return {runId:t.testRunId,repoGitId:e,startedAt:t.startedAt,summary:t.summary,timeline:t.timeline,environment:o,...i?{runType:i}:{},...l?{testType:l.testType,apiProtocol:l.apiProtocol,metadata:l.metadata}:{testType:d.testType},...c&&c.length>0?{tests:c}:{},...r?.branch?{branch:r.branch}:{},...r?.commitSha?{commit:r.commitSha}:{},...r?.commitMessage?{commitMessage:r.commitMessage}:{},...r?.commitAuthor?{commitAuthor:r.commitAuthor}:{},...t.completedAt?{finishedAt:t.completedAt}:{},...t.totalDuration?{duration:t.totalDuration}:{},...n?.provider?{ciProvider:n.provider}:{},...n?.runUrl?{ciRunUrl:n.runUrl}:{},...a?.cloudPlatform?{cloudPlatform:a.cloudPlatform}:{},...a?.cloudBuildId?{cloudBuildId:a.cloudBuildId}:{},...a?.cloudSessionId?{cloudSessionId:a.cloudSessionId}:{}}}function Yt(t){return typeof t=="string"?Buffer.byteLength(t,"utf-8"):0}function Qt(t){return Array.isArray(t)?t.map(e=>{let r=e.statusCode??e.responseStatusCode??null;return {...e.id!=null?{id:e.id}:{},...e.protocol!=null?{protocol:e.protocol}:{},method:e.method??null,url:e.url??null,statusCode:r,statusText:e.statusText??e.responseStatusText??null,ok:typeof r=="number"?r<400:e.ok??null,durationMs:e.durationMs??e.responseTimeMs??null,bytesIn:typeof e.bytesIn=="number"?e.bytesIn:Yt(e.responseBody),bytesOut:typeof e.bytesOut=="number"?e.bytesOut:Yt(e.requestBody),requestHeaders:e.requestHeaders??null,responseHeaders:e.responseHeaders??null,requestBody:e.requestBody??null,responseBody:e.responseBody??null,requestCookies:e.requestCookies??null,responseCookies:e.responseCookies??null}}):[]}function er(t){return Array.isArray(t)?t.map(e=>{let r=e.responseStatusCode??e.statusCode??0,n=e.timestamp??null;return {method:e.method??"GET",url:e.url??"",status:r,statusCode:r,type:"xhr",size:Yt(e.responseBody),duration:e.responseTimeMs??e.durationMs??0,responseTimeMs:e.responseTimeMs??e.durationMs??0,requestHeaders:e.requestHeaders??null,responseHeaders:e.responseHeaders??null,requestBody:e.requestBody??null,responseBody:e.responseBody??null,timestamp:n,startedAt:n}}):[]}function tr(t){if(!Array.isArray(t))return [];let e=r=>{if(r==null)return String(r);if(typeof r=="string")return r;try{return JSON.stringify(r)}catch{return String(r)}};return t.map(r=>{let n=r.status==="failed"?"failed":"passed",s=typeof r.expression=="string"&&r.expression.trim()?r.expression.trim():null,a=r.kind??r.type??"assertion",o=n==="failed"?`expected ${e(r.expected)}, received ${e(r.actual)}`:r.expected!==void 0?`expected ${e(r.expected)}`:null;return {...r.callId!=null?{callId:r.callId}:{},kind:a,description:s??r.description??`${a} assertion`,status:n,detail:o}})}async function He(t){return new Promise(e=>setTimeout(e,t))}function Xo(t){let e={};for(let[r,n]of Object.entries(t))n!=null&&(e[r]=n);return e}async function hs(t,e,r,n=gs,s){for(let a=0;a<qe;a++){if(s?.expired())return null;let o=s?s.slice(n):n;if(o<=0)return null;try{let i=await J(t,e,o);if(i.ok)return i;if(i.status===401&&r&&a===0){let d=await r();if(d){let l={...e,headers:{...e.headers,Authorization:`Bearer ${d}`}},c=await J(t,l,o);if(c.ok)return c}return null}if(i.status===429&&a<qe-1){let d=i.headers.get("Retry-After"),l=d?parseInt(d,10)*1e3:Oe[a];!isNaN(l)&&l>0?await He(l):await He(Oe[a]);continue}if(i.status>=500&&a<qe-1){await He(Oe[a]);continue}return i}catch{if(a<qe-1){await He(Oe[a]);continue}return null}}return null}var Zo=promisify(gzip);function fs(t,e){if(!Array.isArray(t.tests))return t;let r=t.tests.map(n=>{let{consoleLogs:s,networkRequests:a,apiCalls:o,...i}=n;return e?i:{...i,...o!==void 0?{apiCalls:o}:{}}});return {...t,tests:r}}var Qo=["consoleLogs","networkRequests","apiCalls","actions","navigations","protocolAssertions"];function el(t){if(!Array.isArray(t.tests))return t;let e=new Set(Qo),r=t.tests.map(n=>{let s={};for(let a in n)if(!e.has(a))if(a==="failure"&&n[a]&&typeof n[a]=="object"){let o=n[a];s.failure={message:o.message??null,line:o.line??null};}else s[a]=n[a];return s});return {...t,tests:r}}function tl(t){let e=new Set(["tests","timeline","unifiedTimeline","navigations"]),r={};for(let n in t)e.has(n)||(r[n]=t[n]);return r.tests=[],r}async function rr(t,e=Jo){let r={"Content-Type":"application/json"},n=JSON.stringify(t),s=t,a=Array.isArray(s?.tests)?s.tests.length:0,o=i=>Buffer.byteLength(i,"utf-8")>e;if(a>0&&o(n)){let i=Math.max(1,Math.round(e/1048576));process.stderr.write(`[testrelic] WARNING: batch payload exceeds ${i}MB \u2014 dropping consoleLogs/networkRequests from ${a} test(s) to avoid HTTP 413. Console/network detail will be missing in the cloud.
|
|
4267
|
+
`),n=JSON.stringify(fs(s,false)),o(n)&&(process.stderr.write(`[testrelic] WARNING: batch still exceeds ${i}MB after dropping logs \u2014 also dropping apiCalls from ${a} test(s). API request/response detail will be missing in the cloud.
|
|
4268
|
+
`),n=JSON.stringify(fs(s,true))),o(n)&&(process.stderr.write(`[testrelic] WARNING: batch still exceeds ${i}MB \u2014 shedding remaining per-test detail (actions/navigations/assertions) from ${a} test(s). Only test results + summary will reach the cloud.
|
|
4269
|
+
`),n=JSON.stringify(el(s))),o(n)&&(process.stderr.write(`[testrelic] ERROR: batch still exceeds ${i}MB after shedding all per-test detail \u2014 uploading run summary only (${a} per-test result(s) omitted) to avoid losing the run to HTTP 413.
|
|
4270
|
+
`),n=JSON.stringify(tl(s)));}if(Buffer.byteLength(n,"utf-8")>Ko){r["Content-Encoding"]="gzip";let i=await Zo(Buffer.from(n,"utf-8")),d=i.buffer;return {body:new Uint8Array(d,i.byteOffset,i.byteLength),headers:r}}return {body:n,headers:r}}async function ms(t,e,r,n,s,a){let o=`${t}/runs`,{body:i,headers:d}=await rr(r);d.Authorization=`Bearer ${e}`;let l=await hs(o,{method:"POST",headers:d,body:i},n,s,a);return l?.ok?{success:true,statusCode:l.status,error:null}:l?.status===409?{success:true,statusCode:409,error:null}:{success:false,reason:l?`Upload failed with status ${l.status}`:"Upload failed after retries",statusCode:l?.status??null,payload:r,targetEndpoint:o,method:"POST"}}async function vs(t,e,r,n){let s=`${t}/runs/init`;try{let a=await J(s,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${e}`},body:JSON.stringify(Xo(r))},n??gs);return a.ok?{runId:(await a.json()).runId}:null}catch{return null}}function ys(t,e,r,n){let s=`${t}/runs/${r}/tests`;fetch(s,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${e}`},body:JSON.stringify(n)}).catch(()=>{});}async function bs(t,e,r,n){let s=`${t}/runs/${r}/finalize`;return (await hs(s,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${e}`},body:JSON.stringify(n)}))?.ok??false}function nr(t,e,r,n){let s=`${t}/runs/${r}/finalize`,a=new Date,o={finishedAt:a.toISOString(),duration:a.getTime()-new Date(n).getTime(),summary:{}};fetch(s,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${e}`},body:JSON.stringify(o)}).catch(()=>{});}var il="1.0.0",xs=10;function ws(t){return t instanceof Error?t.message:String(t)}function ol(t){return t>=400&&t<500&&t!==408&&t!==429}function we(t,e,r,n,s,a,o,i){try{mkdirSync(t,{recursive:!0});let d=Date.now(),l=`${d}-${e}-${r}.json`,c=join(t,l),u={version:il,queuedAt:new Date(d).toISOString(),reason:n,retryCount:0,targetEndpoint:s,method:a,payload:o,headers:i},f=c+".tmp";writeFileSync(f,JSON.stringify(u,null,2),"utf-8"),renameSync(f,c);}catch(d){d.code==="ENOSPC"?process.stderr.write(`\u26A0 TestRelic: Disk full \u2014 unable to queue upload data.
|
|
2567
4271
|
`):process.stderr.write(`\u26A0 TestRelic: Unable to queue upload data.
|
|
2568
|
-
`);}}async function
|
|
2569
|
-
`),unlinkSync(i);continue}d=c;}catch(
|
|
2570
|
-
`);try{unlinkSync(i);}catch{}continue}try{let{body:
|
|
2571
|
-
`);try{renameSync(i,`${i}.failed`);}catch{try{unlinkSync(i);}catch{}}}else {process.stderr.write(`\u26A0 TestRelic: Queue flush paused \u2014 server returned HTTP ${
|
|
2572
|
-
`);return}}catch(
|
|
2573
|
-
`);return}}}function
|
|
2574
|
-
`);return}try{if(
|
|
2575
|
-
`);return}let r=await
|
|
2576
|
-
`),await this.resolveRepoId(),this.config.queueDirectory&&(
|
|
2577
|
-
`);}}getMode(){return this.authState.mode}isCloudMode(){return this.authState.mode==="cloud"}isLocalMode(){return this.authState.mode==="local"}getAccessToken(){return this.authState.accessToken}getRepoId(){return this.repoId}getGitMetadata(){return this.gitMetadata}getConfig(){return this.config}getFailureReason(){return this.failureReason}getEndpoint(){return this.config?.endpoint??"https://platform.testrelic.ai/api/v1"}async ensureValidToken(){if(!this.isCloudMode()||!this.authState.accessToken)return false;let e=this.authState.expiresAt??0;if(Date.now()+
|
|
2578
|
-
`));}async dispose(){if(this.healthCheckTimer&&(clearInterval(this.healthCheckTimer),this.healthCheckTimer=null),this.flushPromise){try{await Promise.race([this.flushPromise,
|
|
2579
|
-
\u2192 Manage keys: ${
|
|
4272
|
+
`);}}async function Ss(t,e,r){let n;try{n=readdirSync(t).filter(a=>a.endsWith(".json")&&!a.endsWith(".tmp")).sort();}catch{return}if(n.length===0)return;let s=[];for(let a=0;a<n.length;a+=xs)s.push(n.slice(a,a+xs));for(let a of s)for(let o of a){let i=join(t,o),d;try{if(!statSync(i).isFile())continue;let c=JSON.parse(readFileSync(i,"utf-8"));if(!isValidQueueEntry(c)){process.stderr.write(`\u26A0 TestRelic: Corrupted queue file deleted: ${o}
|
|
4273
|
+
`),unlinkSync(i);continue}d=c;}catch(l){process.stderr.write(`\u26A0 TestRelic: Skipping unreadable queue file ${o}: ${ws(l)}
|
|
4274
|
+
`);try{unlinkSync(i);}catch{}continue}try{let{body:l,headers:c}=await rr(d.payload),u={...d.headers,...c,Authorization:`Bearer ${r}`},f=await fetch(d.targetEndpoint,{method:d.method,headers:u,body:l});if(f.ok)unlinkSync(i);else if(ol(f.status)){process.stderr.write(`\u26A0 TestRelic: Queue entry ${o} rejected (HTTP ${f.status}) \u2014 moving aside; it will not be retried.
|
|
4275
|
+
`);try{renameSync(i,`${i}.failed`);}catch{try{unlinkSync(i);}catch{}}}else {process.stderr.write(`\u26A0 TestRelic: Queue flush paused \u2014 server returned HTTP ${f.status} for ${o}; will retry later.
|
|
4276
|
+
`);return}}catch(l){process.stderr.write(`\u26A0 TestRelic: Queue flush paused \u2014 ${ws(l)}; will retry later.
|
|
4277
|
+
`);return}}}function Rs(t,e){try{let r=readdirSync(t).filter(s=>(s.endsWith(".json")||s.endsWith(".failed"))&&!s.endsWith(".tmp")),n=Date.now();for(let s of r){let a=join(t,s);try{let o=readFileSync(a,"utf-8"),d=JSON.parse(o).queuedAt;if(typeof d=="string"){let l=new Date(d).getTime();n-l>e&&unlinkSync(a);}}catch{try{unlinkSync(a);}catch{}}}}catch{}}var gl=3e5,hl=864e5,ml=6e4,As=3e4,ir="https://platform.testrelic.ai/settings/api-keys";function _s(t){return new Promise(e=>{let r=setTimeout(e,t);typeof r?.unref=="function"&&r.unref();})}var Ue=class{constructor(e){this.gitMetadata=null;this.repoId=null;this.failureReason=null;this.flushPromise=null;this.healthCheckTimer=null;this.config=e,this.authState={mode:"pending",accessToken:null,refreshToken:null,expiresAt:null,orgId:null,orgName:null,userId:null,userName:null};}async initialize(){if(!this.config||!this.config.apiKey){this.setLocalMode("no_api_key"),process.stderr.write(`\u2139 TestRelic: No API key configured. Running in local mode.
|
|
4278
|
+
`);return}try{if(is(this.config.endpoint),this.gitMetadata=cs(),!await Wt(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.
|
|
4279
|
+
`);return}let r=await Gt(this.config.endpoint,this.config.apiKey,this.config.timeout);if(Kt(r)){this.handleAuthError(r.code,r.statusCode);return}let n=r;this.authState={mode:"cloud",accessToken:n.accessToken,refreshToken:n.refreshToken,expiresAt:Date.now()+n.expiresIn*1e3,orgId:n.orgId,orgName:n.orgName,userId:n.userId,userName:n.userName},process.stderr.write(`\u2713 TestRelic: Connected to cloud (${n.orgName} / ${n.userName})
|
|
4280
|
+
`),await this.resolveRepoId(),this.config.queueDirectory&&(Rs(this.config.queueDirectory,this.config.queueMaxAge),this.startBackgroundFlush()),this.startHealthCheck();}catch(e){this.setLocalMode("unexpected_error"),process.stderr.write(`\u26A0 TestRelic: Unexpected error during cloud initialization. Running in local mode. ${e instanceof Error?e.message:String(e)}
|
|
4281
|
+
`);}}getMode(){return this.authState.mode}isCloudMode(){return this.authState.mode==="cloud"}isLocalMode(){return this.authState.mode==="local"}getAccessToken(){return this.authState.accessToken}getRepoId(){return this.repoId}getGitMetadata(){return this.gitMetadata}getConfig(){return this.config}getFailureReason(){return this.failureReason}getEndpoint(){return this.config?.endpoint??"https://platform.testrelic.ai/api/v1"}async ensureValidToken(){if(!this.isCloudMode()||!this.authState.accessToken)return false;let e=this.authState.expiresAt??0;if(Date.now()+gl<e)return true;if(!this.config||!this.authState.refreshToken)return this.switchToLocalMode("token_expired_no_refresh"),false;try{let r=await ls(this.config.endpoint,this.authState.refreshToken,this.config.timeout);return r?(this.authState.accessToken=r.accessToken,this.authState.refreshToken=r.refreshToken,this.authState.expiresAt=Date.now()+r.expiresIn*1e3,!0):(this.switchToLocalMode("token_refresh_failed"),!1)}catch{return this.switchToLocalMode("token_refresh_error"),false}}switchToLocalMode(e){this.authState.mode!=="local"&&(this.authState.mode="local",this.failureReason=e,process.stderr.write(`\u26A0 TestRelic: Switched to local mode (${e}).
|
|
4282
|
+
`));}async dispose(){if(this.healthCheckTimer&&(clearInterval(this.healthCheckTimer),this.healthCheckTimer=null),this.flushPromise){try{await Promise.race([this.flushPromise,_s(As)]);}catch{}this.flushPromise=null;}this.authState={mode:"local",accessToken:null,refreshToken:null,expiresAt:null,orgId:null,orgName:null,userId:null,userName:null},this.repoId=null,this.gitMetadata=null;}setLocalMode(e){this.authState.mode="local",this.failureReason=e;}handleAuthError(e,r){switch(e){case "invalid_key":this.setLocalMode("invalid_api_key"),process.stderr.write(`\u26A0 TestRelic: API key is invalid or revoked. Running in local mode.
|
|
4283
|
+
\u2192 Manage keys: ${ir}
|
|
2580
4284
|
`);break;case "expired_key":this.setLocalMode("expired_api_key"),process.stderr.write(`\u26A0 TestRelic: API key has expired. Running in local mode.
|
|
2581
|
-
\u2192 Generate a new key: ${
|
|
4285
|
+
\u2192 Generate a new key: ${ir}
|
|
2582
4286
|
`);break;case "validation_error":this.setLocalMode("validation_error"),process.stderr.write(`\u26A0 TestRelic: API key format invalid. Running in local mode.
|
|
2583
|
-
\u2192 Manage keys: ${
|
|
4287
|
+
\u2192 Manage keys: ${ir}
|
|
2584
4288
|
`);break;case "rate_limited":this.setLocalMode("rate_limited"),process.stderr.write(`\u26A0 TestRelic: Rate limited during authentication. Running in local mode.
|
|
2585
4289
|
`);break;case "timeout":this.setLocalMode("auth_timeout"),process.stderr.write(`\u26A0 TestRelic: Authentication timed out. Running in local mode.
|
|
2586
4290
|
`);break;default:this.setLocalMode(`auth_error_${r??"unknown"}`),process.stderr.write(`\u26A0 TestRelic: Cloud authentication failed. Running in local mode.
|
|
2587
|
-
`);break}}async resolveRepoId(){if(!this.config||!this.authState.accessToken)return;let e=this.gitMetadata?.remoteUrl?
|
|
2588
|
-
`);}readRepoCache(e){if(!this.config)return null;let r=join(this.config.queueDirectory,"..","cache","repo.json");try{if(!existsSync(r))return null;let n=readFileSync(r,"utf-8"),s=JSON.parse(n);if(s.gitId!==e||Date.now()-s.resolvedAt>
|
|
2589
|
-
`),this.startBackgroundFlush();}}}catch{}},
|
|
2590
|
-
`);}return
|
|
2591
|
-
`),{navigations:s,apiAssertions:a,networkRequestsFile:
|
|
2592
|
-
`),{keyMap:a,total:
|
|
2593
|
-
`).filter(r=>r.length>0).map(r=>{try{return JSON.parse(r)}catch{return null}}).filter(r=>r!==null)}catch{return []}}function
|
|
2594
|
-
`)
|
|
2595
|
-
`)
|
|
4291
|
+
`);break}}async resolveRepoId(){if(!this.config||!this.authState.accessToken)return;let e=this.gitMetadata?.remoteUrl?be(this.gitMetadata.remoteUrl):null,r=e?us(e):this.config.projectName??xe(process.cwd()),n=e??this.config.projectName??xe(process.cwd()),s=this.readRepoCache(n);if(s){this.repoId=s.repoId;return}try{let a=await ds(this.config.endpoint,this.authState.accessToken,n,r,this.config.timeout,this.gitMetadata?.branch);a&&(this.repoId=a.repoId,this.writeRepoCache(n,a.repoId,a.displayName));}catch{}!e&&!this.config.projectName&&!Jt(process.cwd())&&process.stderr.write(`\u2139 TestRelic: No git remote or package.json detected. Set project.name in .testrelic for stable project identity.
|
|
4292
|
+
`);}readRepoCache(e){if(!this.config)return null;let r=join(this.config.queueDirectory,"..","cache","repo.json");try{if(!existsSync(r))return null;let n=readFileSync(r,"utf-8"),s=JSON.parse(n);if(s.gitId!==e||Date.now()-s.resolvedAt>hl)return null;let a=this.hashApiKey();return a&&s.apiKeyHash!==a?null:s}catch{return null}}writeRepoCache(e,r,n){if(!this.config)return;let s=join(this.config.queueDirectory,"..","cache"),a=join(s,"repo.json");try{mkdirSync(s,{recursive:!0});let o={repoId:r,gitId:e,displayName:n,resolvedAt:Date.now(),apiKeyHash:this.hashApiKey()??""},i=a+".tmp";writeFileSync(i,JSON.stringify(o,null,2),"utf-8"),renameSync(i,a);}catch{}}hashApiKey(){return this.config?.apiKey?createHash("sha256").update(this.config.apiKey).digest("hex").substring(0,16):null}startBackgroundFlush(){if(!this.config||!this.authState.accessToken)return;let e=this.authState.accessToken,r=this.config.queueDirectory,n=this.config.endpoint;this.flushPromise=Promise.race([Ss(r,n,e),_s(As)]).catch(()=>{});}startHealthCheck(){if(!this.config)return;let e=this.config.endpoint;this.healthCheckTimer=setInterval(async()=>{if(!this.isCloudMode()&&!(!this.failureReason||!["cloud_unreachable","auth_timeout","network_error"].includes(this.failureReason)))try{if(!await Wt(e))return;if(this.config?.apiKey){let n=await Gt(this.config.endpoint,this.config.apiKey,this.config.timeout);if(!Kt(n)){let s=n;this.authState={mode:"cloud",accessToken:s.accessToken,refreshToken:s.refreshToken,expiresAt:Date.now()+s.expiresIn*1e3,orgId:s.orgId,orgName:s.orgName,userId:s.userId,userName:s.userName},this.failureReason=null,process.stderr.write(`\u2713 TestRelic: Cloud connectivity restored.
|
|
4293
|
+
`),this.startBackgroundFlush();}}}catch{}},ml),typeof this.healthCheckTimer?.unref=="function"&&this.healthCheckTimer.unref();}};function xl(t,e){return e?t?{"2xx":t["2xx"]+e["2xx"],"3xx":t["3xx"]+e["3xx"],"4xx":t["4xx"]+e["4xx"],"5xx":t["5xx"]+e["5xx"],error:t.error+e.error}:{...e}:t}function Is(t,e,r,n){let s=[],a=[],o=null,i=0,d=null,l=0,c=null,u=0,f=null,p=t.filter(h=>h.name===ATTACHMENT_NAME&&h.body),g=false;if(p.length>0)for(let h of p)try{let v=JSON.parse(h.body.toString());if(isTestRelicFilePayload(v)){s=s.concat(v.navigations),a=a.concat(v.apiAssertions),v.networkRequestsFile&&(o=v.networkRequestsFile,i+=v.networkRequestsCount),v.consoleLogsFile&&(d=v.consoleLogsFile,l+=v.consoleLogsCount),v.apiCallsFile&&(c=v.apiCallsFile,u+=v.apiCallsCount),f=xl(f,v.apiStatusMix),g=!0;continue}if(isTestRelicDataPayload(v)){s=s.concat(v.navigations),Array.isArray(v.apiAssertions)&&(a=a.concat(v.apiAssertions)),i+=v.networkRequests?.length??0,u+=v.apiCalls?.length??0;let y=v.consoleLogs;Array.isArray(y)&&(l+=y.length),g=!0;}}catch{process.stderr.write(`[testrelic] Warning: Corrupt attachment for test "${r}"
|
|
4294
|
+
`);}return g||(s=e.filter(h=>h.type==="lambdatest-navigation"&&h.description).map(h=>{try{return JSON.parse(h.description)}catch{return null}}).filter(h=>h!==null)),s.length===0&&i===0&&u===0&&!n&&!g&&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.
|
|
4295
|
+
`),{navigations:s,apiAssertions:a,networkRequestsFile:o,networkRequestsCount:i,consoleLogsFile:d,consoleLogsCount:l,apiCallsFile:c,apiCallsCount:u,apiStatusMix:f}}function ze(t=process.env){let e=wl(t.TESTRELIC_CLOUD_PLATFORM);if(e)return {cloudPlatform:e,cloudBuildId:t.TESTRELIC_CLOUD_BUILD_ID||void 0,cloudSessionId:t.TESTRELIC_CLOUD_SESSION_ID||void 0};if(t.BROWSERSTACK_USERNAME||t.BROWSERSTACK_ACCESS_KEY)return {cloudPlatform:"browserstack",cloudBuildId:t.BROWSERSTACK_BUILD_NAME||t.BROWSERSTACK_BUILD_ID||void 0,cloudSessionId:t.BROWSERSTACK_SESSION_ID||void 0};if(t.LT_USERNAME||t.LAMBDATEST_USERNAME||t.LT_ACCESS_KEY||t.LAMBDATEST_ACCESS_KEY)return {cloudPlatform:"lambdatest",cloudBuildId:t.LT_BUILD||t.LAMBDATEST_BUILD||void 0,cloudSessionId:t.LT_SESSION_ID||t.LAMBDATEST_SESSION_ID||void 0};let r=t.PLAYWRIGHT_WS_ENDPOINT;if(r){if(/cdp\.lambdatest\.com/i.test(r))return {cloudPlatform:"lambdatest"};if(/cdp\.browserstack\.com/i.test(r))return {cloudPlatform:"browserstack"}}return {}}function wl(t){if(!t)return;let e=t.toLowerCase().trim();if(e==="browserstack"||e==="bs")return "browserstack";if(e==="lambdatest"||e==="lt")return "lambdatest"}var Rl=5,je=[1e3,3e3,9e3],ie=3,dr=3e4,Al={".png":"image/png",".jpg":"image/jpeg",".jpeg":"image/jpeg",".webp":"image/webp",".webm":"video/webm",".mp4":"video/mp4",".zip":"application/zip",".json":"application/json"},or=0,lr=[];async function _l(){or>=Rl&&await new Promise(t=>lr.push(t)),or++;}function Il(){or--,lr.length>0&&lr.shift()();}async function Ve(t){return new Promise(e=>setTimeout(e,t))}function Ml(t){let e=t.substring(t.lastIndexOf(".")).toLowerCase();return Al[e]??"application/octet-stream"}function El(t){try{return statSync(t).size}catch{return 0}}async function Pl(t,e,r,n,s){let a=`${t}/artifacts/upload-url`,o=JSON.stringify({runId:r.runId,testId:r.testId,fileName:basename(r.filePath),contentType:s,type:r.type,sizeBytes:n});for(let i=0;i<ie;i++)try{let d=await J(a,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${e}`},body:o},dr);if(d.ok)return await d.json();if(d.status>=500&&i<ie-1){await Ve(je[i]);continue}return null}catch{if(i<ie-1){await Ve(je[i]);continue}return null}return null}async function Ll(t,e,r,n){let s=ps(n,dr);for(let a=0;a<ie;a++)try{let o=createReadStream(e),i=Readable.toWeb(o),d=await J(t,{method:"PUT",headers:{"Content-Type":r,"Content-Length":String(n)},body:i,duplex:"half"},s);if(d.ok)return !0;if(d.status>=500&&a<ie-1){await Ve(je[a]);continue}return !1}catch{if(a<ie-1){await Ve(je[a]);continue}return false}return false}async function Nl(t,e,r){let n=`${t}/artifacts/confirm`;try{return (await J(n,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${e}`},body:JSON.stringify({artifactId:r})},dr)).ok}catch{return false}}async function Dl(t,e,r,n){let s=El(r.filePath);if(s===0)return {success:false,storageKey:null,artifactId:null,error:"file_not_found_or_empty"};if(s>n*1024*1024)return {success:false,storageKey:null,artifactId:null,error:"file_too_large"};let a=Ml(r.filePath);await _l();try{let o=await Pl(t,e,r,s,a);if(!o)return {success:!1,storageKey:null,artifactId:null,error:"upload_url_request_failed"};if(!await Ll(o.uploadUrl,r.filePath,a,s))return {success:!1,storageKey:o.storageKey,artifactId:o.artifactId,error:"presigned_put_failed"};let d=await Nl(t,e,o.artifactId);return {success:d,storageKey:o.storageKey,artifactId:o.artifactId,error:d?null:"confirm_failed"}}finally{Il();}}async function Ms(t,e,r,n,s){let a=new Map,o=r.map(async i=>{if(s?.expired()){a.set(i.filePath,{success:false,storageKey:null,artifactId:null,error:"upload_budget_exhausted"});return}let d=await Dl(t,e,i,n);a.set(i.filePath,d);});return await Promise.allSettled(o),a}function ur(t){let r=t.getGitMetadata()?.remoteUrl;return r?be(r):t.getConfig()?.projectName??xe(process.cwd())}async function Es(t,e,r,n,s){if(!t.isCloudMode()||e!=="realtime"&&e!=="both"||!await t.ensureValidToken())return null;let o=t.getGitMetadata(),i=V(),d=ze(),l=Fe();return (await vs(t.getEndpoint(),t.getAccessToken(),{runId:r,repoGitId:ur(t),branch:o?.branch??null,commit:o?.commitSha??null,commitMessage:o?.commitMessage??null,commitAuthor:o?.commitAuthor??null,startedAt:n,totalTests:null,ciProvider:i?.provider??null,ciRunUrl:i?.runUrl??null,environment:s??d.cloudPlatform??null,cloudPlatform:d.cloudPlatform??null,cloudBuildId:d.cloudBuildId??null,cloudSessionId:d.cloudSessionId??null,...l?{runType:l}:{},testType:"e2e"}))?.runId??null}var Bl=[{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"}],Ol=true,ql=50,cr={keyMap:new Map,visualKeyMap:new Map,total:0,uploaded:0};async function Hl(t,e,r,n,s){let a=new Map,o=new Map;if(!(e?.uploadArtifacts??Ol)||!await t.ensureValidToken())return cr;let l=[],c=new Map,u=new Map;for(let h of n){if(h.artifacts.screenshot){let y=join(h.outputDir,h.artifacts.screenshot);l.push({filePath:y,runId:r,testId:h.testId,type:"screenshot"}),c.set(y,{testId:h.testId,field:"screenshot"});}if(h.artifacts.video){let y=join(h.outputDir,h.artifacts.video);l.push({filePath:y,runId:r,testId:h.testId,type:"video"}),c.set(y,{testId:h.testId,field:"video"});}if(h.artifacts.trace){let y=join(h.outputDir,h.artifacts.trace);l.push({filePath:y,runId:r,testId:h.testId,type:"trace"}),c.set(y,{testId:h.testId,field:"trace"});}let v=h.visual??[];for(let y=0;y<v.length;y++){let b=v[y];if(b)for(let k of Bl){let A=b[k.field];if(!A)continue;let E=join(h.outputDir,A);l.push({filePath:E,runId:r,testId:h.testId,type:k.type}),c.set(E,{testId:h.testId,field:"visual",visualIndex:y,visualKey:k.keyField});}}}if(l.length===0)return cr;let f=e?.artifactMaxSizeMb??ql,p=await Ms(t.getEndpoint(),t.getAccessToken(),l,f,s),g=new Map;for(let[h,v]of p){if(!v.success||!v.storageKey)continue;let y=c.get(h);if(!y)continue;let b=g.get(y.testId)??{};if(y.field==="screenshot"&&(b.screenshotKey=v.storageKey),y.field==="video"&&(b.videoKey=v.storageKey),y.field==="trace"&&(b.traceKey=v.storageKey),y.field==="visual"&&y.visualIndex!==void 0&&y.visualKey){let k=u.get(y.testId)??new Map,A=k.get(y.visualIndex)??{};A[y.visualKey]=v.storageKey,k.set(y.visualIndex,A),u.set(y.testId,k);}g.set(y.testId,b);}for(let h of n){if(h.visual&&h.visual.length>0){let y=u.get(h.testId);o.set(h.testId,h.visual.map((b,k)=>({...b,...y?.get(k)??{}})));}let v=g.get(h.testId);a.set(h.testId,{...h.artifacts,...v?.screenshotKey?{screenshotKey:v.screenshotKey}:{},...v?.videoKey?{videoKey:v.videoKey}:{},...v?.traceKey?{traceKey:v.traceKey}:{}});}let m=Array.from(p.values()).filter(h=>h.success).length;return m>0&&process.stderr.write(`\u2713 TestRelic: Uploaded ${m} artifact(s) to cloud storage.
|
|
4296
|
+
`),{keyMap:a,visualKeyMap:o,total:l.length,uploaded:m}}async function pr(t,e,r,n,s,a,o,i,d,l,c){let u={realtimeFinalizeAttempted:false,realtimeFinalized:false,batchAttempted:false,batchAcknowledged:false,artifactsTotal:0,artifactsUploaded:0,failureReason:null},f=new Be(e?.uploadBudgetMs??12e4),p=g=>{u.failureReason||(u.failureReason=g);};try{if(t.isCloudMode()&&s&&(r==="realtime"||r==="both")){u.realtimeFinalizeAttempted=!0;let h=await t.ensureValidToken(),v=t.getGitMetadata(),y=Xt(c),b={finishedAt:o,duration:i,summary:d,...v?.commitMessage?{commitMessage:v.commitMessage}:{},testType:y.testType,...y.testType==="api"?{apiProtocol:y.apiProtocol,metadata:y.metadata}:{}};if(h){let k=await bs(t.getEndpoint(),t.getAccessToken(),s,b);if(u.realtimeFinalized=k,!k){p("finalize_failed_after_retries");let A=e?.queueDirectory??".testrelic/queue";we(A,n,"finalize","finalize_failed_after_retries",`${t.getEndpoint()}/runs/${s}/finalize`,"POST",b,{"Content-Type":"application/json"});}}else {p(t.getFailureReason()??"token_invalid");let k=e?.queueDirectory??".testrelic/queue";we(k,n,"finalize",t.getFailureReason()??"token_invalid",`${t.getEndpoint()}/runs/${s}/finalize`,"POST",b,{"Content-Type":"application/json"});}}let g=new Map,m=new Map;if(t.isCloudMode()&&l&&l.length>0){let h=await Hl(t,e,n,l,f);g=h.keyMap,m=h.visualKeyMap,u.artifactsTotal=h.total,u.artifactsUploaded=h.uploaded;}if(t.isCloudMode()&&(!r||r==="batch"||r==="both"))if(u.batchAttempted=!0,await t.ensureValidToken()){let v=t.getGitMetadata(),y=V(),b=ur(t),k=Ul(zl(a,g),m),A=Zt(k,b,v,y,c,ze()),E=await ms(t.getEndpoint(),t.getAccessToken(),A,async()=>await t.ensureValidToken()?t.getAccessToken():null,e?.timeout,f);if(u.batchAcknowledged=E.success,!E.success){let T=E;p(T.reason);let w=e?.queueDirectory??".testrelic/queue";we(w,n,"batch",T.reason,T.targetEndpoint,T.method,T.payload,{"Content-Type":"application/json"});}}else {p(t.getFailureReason()??"token_invalid");let v=e?.queueDirectory??".testrelic/queue",y=t.getGitMetadata(),b=V(),k=ur(t),A=Zt(a,k,y,b,void 0,ze());we(v,n,"batch",t.getFailureReason()??"token_invalid",`${t.getEndpoint()}/runs`,"POST",A,{"Content-Type":"application/json"});}await t.dispose();}catch(g){p(g instanceof Error?g.message:"unexpected_upload_error");try{await t.dispose();}catch{}}return u}function Ul(t,e){if(e.size===0)return t;let r=t.timeline.map(n=>{let s=n.tests;if(!Array.isArray(s))return n;let a=false,o=s.map(i=>{let d=i.testId;if(!d)return i;let l=e.get(d);return l?(a=true,{...i,visual:l}):i});return a?{...n,tests:o}:n});return {...t,timeline:r}}function zl(t,e){if(e.size===0)return t;let r=t.timeline.map(n=>{let s=n.testId;if(!s)return n;let a=e.get(s);return a?{...n,...a.screenshotKey?{screenshotKey:a.screenshotKey}:{},...a.videoKey?{videoKey:a.videoKey}:{}}:n});return {...t,timeline:r}}var jl=new Set(["batch","realtime","both"]),ke=Object.freeze({attempted:false,acknowledged:false,runId:null,endpoint:null,finalized:false,artifactsUploaded:0,artifactsTotal:0,detail:"cloud reporting was never initialised"});function Vl(t){switch(t){case "no_api_key":return "cloud mode off \u2014 no API key configured";case "cloud_unreachable":case "network_error":return "cloud endpoint unreachable \u2014 the run stayed local";case "invalid_api_key":return "API key rejected as invalid or revoked \u2014 the run stayed local";case "expired_api_key":return "API key has expired \u2014 the run stayed local";case "validation_error":return "API key format rejected \u2014 the run stayed local";case "rate_limited":return "rate limited during authentication \u2014 the run stayed local";case "auth_timeout":return "authentication timed out \u2014 the run stayed local";case "token_expired_no_refresh":case "token_refresh_failed":case "token_refresh_error":return "access token could not be refreshed \u2014 the run stayed local";case "init_error":return "cloud initialisation timed out \u2014 the run stayed local";case "unexpected_error":return "cloud initialisation failed \u2014 the run stayed local";default:return t?`cloud mode off (${t}) \u2014 the run stayed local`:"cloud mode off \u2014 the run stayed local"}}function Ps(t,e,r){if(!t)return ke;if(!t.isCloudMode())return {...ke,detail:Vl(t.getFailureReason())};let n=e??"batch";if(!jl.has(n))return {...ke,detail:`upload strategy '${n}' is not recognised \u2014 nothing was sent`};let s=t.getEndpoint(),a=n==="realtime"||n==="both";return a&&r?{attempted:true,acknowledged:true,runId:r,endpoint:s,finalized:false,artifactsUploaded:0,artifactsTotal:0,detail:null}:{attempted:true,acknowledged:false,runId:null,endpoint:s,finalized:false,artifactsUploaded:0,artifactsTotal:0,detail:a?"run init failed \u2014 the platform did not acknowledge this run":`upload runs at the end of the run under the '${n}' strategy \u2014 not sent yet`}}function fr(t,e,r){if(!t.attempted)return t;if(!e)return {...t,detail:t.acknowledged?"upload timed out before it completed \u2014 the run may be incomplete on the platform":"upload timed out before the platform acknowledged this run",acknowledged:t.acknowledged};let n=t.acknowledged||e.batchAcknowledged,s=t.runId??(e.batchAcknowledged?r:null),a=e.realtimeFinalizeAttempted?e.realtimeFinalized:e.batchAcknowledged,o=null;return n?a||(o="run acknowledged but not closed out \u2014 finalize failed and was queued for retry"):o=e.failureReason?`upload failed (${e.failureReason}) \u2014 queued locally for retry`:"upload failed \u2014 the platform did not acknowledge this run",{attempted:true,acknowledged:n,runId:s,endpoint:t.endpoint,finalized:a,artifactsUploaded:e.artifactsUploaded,artifactsTotal:e.artifactsTotal,detail:o}}function Y(t){try{return readFileSync(t,"utf-8").split(`
|
|
4297
|
+
`).filter(r=>r.length>0).map(r=>{try{return JSON.parse(r)}catch{return null}}).filter(r=>r!==null)}catch{return []}}function Xl(t){if(!t)return null;let e=t.split(`
|
|
4298
|
+
`).map(a=>a.trim()).filter(Boolean);if(e.length===0)return null;let r=e.filter(a=>!/^Error:\s*expect\(/.test(a));if(r.length===0)return e[0]??null;let n=r[0],s=r[1];return s!==void 0&&/^Received:/.test(s)&&/^Expected:/.test(n)?`${n} \xB7 ${s}`:n}function hr(t){switch(t){case "passed":return "passed";case "failed":return "failed";case "timedOut":return "timedout";case "skipped":return "skipped";case "interrupted":return "failed";default:return "failed"}}function Zl(t,e,r){let n=`${t}::${e}::${r}`;return createHash("sha256").update(n).digest("hex").substring(0,16)}function Ql(t){return t.length<=3?"":t[t.length-2]}function ed(t){let e=t.findIndex(r=>r.status==="passed");return e>0?`passed on retry ${e}`:null}function qs(t){return t===null?"error":t>=200&&t<300?"2xx":t>=300&&t<400?"3xx":t>=400&&t<500?"4xx":t>=500&&t<600?"5xx":"error"}function td(t){let e={"2xx":0,"3xx":0,"4xx":0,"5xx":0,error:0},r=e;for(let n of t){let s=typeof n.responseStatusCode=="number"?n.responseStatusCode:null;r[qs(s)]++;}return e}function Fs(t){if(t){for(let e of t)if(e.status==="failed")return {type:e.type,expected:Bs(e.expected),actual:Bs(e.actual),...e.expression?{expression:e.expression}:{}}}}function Bs(t){if(t===void 0)return "undefined";if(t===null)return "null";let e;if(typeof t=="string")e=t;else try{e=JSON.stringify(t)??String(t);}catch{e=String(t);}return e.length>200?e.slice(0,200)+"\u2026":e}function We(t,e){if(!t)return 0;let r=0;for(let n of t)n.status===e&&r++;return r}function Hs(t,e){for(let r of t)e[r.category]=(e[r.category]??0)+1,r.children.length>0&&Hs(r.children,e);}function rd(t){if(t.length===0)return null;let e=[...t].sort((n,s)=>n-s),r=e.reduce((n,s)=>n+s,0);return {avg:Math.round(r/e.length),min:e[0],max:e[e.length-1],p50:ee(e,50),p95:ee(e,95),p99:ee(e,99)}}var Us=2e5,Ge=1e5;function Os(t,e,r,n,s,a,o){let i={retryIndex:t,apiCalls:e?.length??0,apiUrls:[],methodCounts:{},statusRanges:{"2xx":0,"3xx":0,"4xx":0,"5xx":0,error:0},responseTimes:[],passedAssertions:0,failedAssertions:0,assertions:r.length,navigations:n.length,navUrls:[],networkRequests:s,consoleLogs:a,actionSteps:o?.length??0,actionCategories:{}};if(e){let l=new Set;for(let c of e){let u=typeof c.method=="string"?c.method:"OTHER";i.methodCounts[u]=(i.methodCounts[u]??0)+1;let f=typeof c.url=="string"?c.url:null;f&&l.size<Ge&&l.add(jt(f));let p=typeof c.responseStatusCode=="number"?c.responseStatusCode:typeof c.statusCode=="number"?c.statusCode:null,g=p!==null&&Number.isFinite(p)?p:null;i.statusRanges[qs(g)]++;let m=typeof c.responseTimeMs=="number"?c.responseTimeMs:typeof c.durationMs=="number"?c.durationMs:null;m!==null&&Number.isFinite(m)&&i.responseTimes.length<Us&&i.responseTimes.push(m);}i.apiUrls=Array.from(l);}for(let l of r)l.status==="passed"?i.passedAssertions++:i.failedAssertions++;let d=new Set;for(let l of n)l.url&&d.size<Ge&&d.add(l.url);return i.navUrls=Array.from(d),o&&Hs(o,i.actionCategories),i}function nd(){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 sd(t){return process.env.BROWSERSTACK_USERNAME?"BrowserStack":t?{"github-actions":"GitHub Actions","gitlab-ci":"GitLab CI",jenkins:"Jenkins",circleci:"CircleCI","bitbucket-pipelines":"Bitbucket Pipelines"}[t.provider]??"CI":"Local"}function ad(){let t=platform();return t==="win32"?"Windows":t==="darwin"?"macOS":"Linux"}function id(t){let e=t.parent?.project?.()?.use;if(!e)return;let{browserName:r,channel:n}=e;if(n){let a=n.toLowerCase();return a==="chrome"||a==="chrome-stable"?"Chrome":a.startsWith("chrome-beta")?"Chrome Beta":a.startsWith("chrome-dev")?"Chrome Dev":a.startsWith("chrome-canary")?"Chrome Canary":a==="msedge"||a==="msedge-stable"?"Microsoft Edge":a.startsWith("msedge-beta")?"Edge Beta":a.startsWith("msedge-dev")?"Edge Dev":a.startsWith("msedge-canary")?"Edge Canary":n}if(r){let a=r.toLowerCase();return a==="chromium"?"Chromium":a==="firefox"?"Firefox":a==="webkit"?"WebKit":r}let s=e.defaultBrowserType;if(s){let a=s.toLowerCase();return a==="chromium"?"Chromium":a==="firefox"?"Firefox":a==="webkit"?"WebKit":s}}function od(t,e,r){let n=r.parent?.project?.(),s="unknown";n!==void 0&&(n.use.isMobile===true?s="mobile":s="e2e");let a=["e2e","api","unit","mobile"];for(let d of a)if(t.some(l=>l===`@${d}`||l===d))return d;let o=e.replace(/\\/g,"/"),i=["e2e","api","unit"];for(let d of i)if(o.includes(`/${d}/`))return d;return s}var ld="__testrelic_api_config";function dd(t){return JSON.stringify(t,(e,r)=>r instanceof RegExp?{__regexp:true,source:r.source,flags:r.flags}:r)}var Ke=class{constructor(e){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=ke;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=Hr(e),this.apiConfig=qr(e);}upsertAggContribution(e,r){let n=this.streamingTestAgg.get(e);(!n||r.retryIndex>n.retryIndex)&&this.streamingTestAgg.set(e,r);}async onBegin(e,r){try{this.rootDir=e.rootDir;let n=e.updateSnapshots;if(this.updatingSnapshots=n==="all"||n==="changed",this.startedAt=new Date().toISOString(),this.testRunId=this.config.testRunId??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 s=r.allTests().length;if(process.stderr.write(`[testrelic] Report mode: ${this.activeReportMode} (${s} tests)
|
|
4299
|
+
`),this.activeReportMode==="streaming"){let i=dirname(this.config.outputPath);this.streamingWriter=new he(i);let d=()=>{if(this.streamingWriter){try{this.streamingWriter.writeIndex(this.testIndex);let l={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(l);}catch{}this.streamingWriter.dispose();}try{if(this.cloudClient?.isCloudMode()&&this.cloudRunId){let l=this.cloudClient.getAccessToken();l&&nr(this.cloudClient.getEndpoint(),l,this.cloudRunId,this.startedAt);}}catch{}};process.on("SIGTERM",d),process.on("SIGINT",d);}if(this.config.includeArtifacts){let i=dirname(this.config.outputPath);this.runTimestamp=Cn(join(i,"artifacts"));try{Yn(join(i,"artifacts"));}catch{}}let a=V();this.detectedOs=ad(),this.detectedSource=sd(a);let o=await Q(async()=>(this.cloudClient=new Ue(this.config.cloud),await this.cloudClient.initialize(),Es(this.cloudClient,this.config.cloud?.uploadStrategy,this.testRunId,this.startedAt,this.detectedSource)),Zn,"Cloud initialization");if(this.cloudRunId=o??null,o===void 0&&this.cloudClient?.switchToLocalMode("init_error"),this.cloudReceipt=Ps(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&&nr(this.cloudClient.getEndpoint(),d,this.cloudRunId,this.startedAt);}}catch{}};process.on("SIGTERM",i),process.on("SIGINT",i);}}catch{}}onTestBegin(e,r){try{e.annotations.push({type:ld,description:dd(this.apiConfig)});}catch{}}onTestEnd(e,r){try{let n=r,s=e.outcome(),a;s==="flaky"?a="flaky":s==="skipped"?a="skipped":a=hr(n.status);let o=n.startTime.toISOString(),i=new Date(n.startTime.getTime()+n.duration).toISOString(),d=e.tags?[...e.tags]:e.annotations.filter(I=>I.type==="tag").map(I=>I.description??""),l=Is(n.attachments,e.annotations,e.title,a==="skipped");(l.navigations.length>0||l.networkRequestsCount>0||l.apiCallsCount>0)&&(this.fixtureDataReceived=!0),l.navigations.length>0&&(this.navigationsCaptured=!0),!this.config.captureNavigation&&l.navigations.length>0&&(l={...l,navigations:[]});let c=null;if(a==="failed"||a==="flaky"){let M=(a==="flaky"?e.results.find(N=>N.status!=="passed")?.errors??[]:n.errors)[0];if(M){let N=zr(this.config.redactPatterns),j=M.location?.file??null,P=l.apiAssertions.find(ia=>ia.status==="failed")?.location??null,re=P!==null&&(j===null||resolve(j)!==resolve(e.location.file)),Sr=re?P.file:j,Qe=re?P.line:M.location?.line??null,fe=null;this.config.includeCodeSnippets&&Qe!==null&&Sr&&(fe=Ur(Sr,Qe,this.config.codeContextLines),fe&&(fe=N(fe))),c={message:N(se(M.message??"Unknown error")),line:Qe,code:fe,stack:this.config.includeStackTrace&&M.stack?N(se(M.stack)):null};}}let u=e.titlePath().filter(Boolean),f=relative(this.rootDir||".",e.location.file).replace(/\\/g,"/"),p=Ql(u),g=u.join(" > "),m=f,h=Zl(m,p,g),v=od(d,m,e),y=v!=="mobile"&&l.apiCallsCount>0?"api":v,b=s==="flaky",k=ed(e.results),A=hr(e.expectedStatus),E=hr(n.status),T=null;if(this.config.includeArtifacts&&a!=="skipped"&&n.attachments){let I=dirname(this.config.outputPath),M=u[u.length-1]??e.title;T=Sn(n.attachments,M,n.retry,I,this.runTimestamp||void 0,h);}let w=null;if(this.config.includeArtifacts&&a!=="skipped"&&n.attachments)try{let I=Hn(n.attachments,{updatingSnapshots:this.updatingSnapshots,failureMessage:n.errors.map(M=>M.message??"").join(`
|
|
4300
|
+
`)});if(I.length>0){let M=dirname(this.config.outputPath),N=u[u.length-1]??e.title;w=Rn(I,N,n.retry,M,this.runTimestamp||void 0,h);}}catch{}let S=this.config.includeActionSteps&&n.steps?ss(n.steps,n.startTime):null,x=id(e),C={titlePath:u,title:g,status:a,duration:n.duration,startedAt:o,completedAt:i,retryCount:e.results.length-1,retry:n.retry,tags:d,failure:c,specFile:f,navigations:l.navigations,testId:h,filePath:m,suiteName:p,testType:y,isFlaky:b,retryStatus:k,expectedStatus:A,actualStatus:E,artifacts:T,visual:w,apiAssertions:l.apiAssertions,actions:S,networkRequestsFile:l.networkRequestsFile,networkRequestsCount:l.networkRequestsCount,consoleLogsFile:l.consoleLogsFile,consoleLogsCount:l.consoleLogsCount,apiCallsFile:l.apiCallsFile,apiCallsCount:l.apiCallsCount,apiStatusMix:l.apiStatusMix,browser:x,os:this.detectedOs||void 0,source:this.detectedSource||void 0},_=l.consoleLogsFile&&l.consoleLogsCount>0?Y(l.consoleLogsFile):void 0,L=l.networkRequestsFile&&l.networkRequestsCount>0?Y(l.networkRequestsFile):void 0,z=l.apiCallsFile&&l.apiCallsCount>0?Y(l.apiCallsFile):void 0,ce=z?Qt(z):void 0,ue=!L&&z?er(z):void 0,pe=l.apiAssertions&&l.apiAssertions.length>0?tr(l.apiAssertions):void 0;this.cloudTestsBuffer.push({testId:h,title:g,status:a,duration:n.duration,suiteName:p,filePath:m,testType:y,isFlaky:b,startedAt:o,completedAt:i,tags:d,failure:c,retry:n.retry,retryCount:e.results.length-1,retryStatus:k,browser:x,os:this.detectedOs||void 0,source:this.detectedSource||void 0,...S&&S.length>0?{actions:S}:{},...l.navigations&&l.navigations.length>0?{navigations:l.navigations}:{},..._?{consoleLogs:_}:{},...L?{networkRequests:L}:ue?{networkRequests:ue}:{},...ce?{apiCalls:ce}:{},...pe?{protocolAssertions:pe}:{}});try{let I=this.config.cloud?.uploadStrategy;if(this.cloudClient?.isCloudMode()&&this.cloudRunId&&(I==="realtime"||I==="both")){let M=this.cloudClient.getAccessToken();M&&ys(this.cloudClient.getEndpoint(),M,this.cloudRunId,this.buildCloudTestPayload(C));}}catch{}if(this.activeReportMode==="streaming"&&this.streamingWriter){let I=et(m,u,e.id??"",n.retry),M=n.retry===e.results.length-1,N={id:I,navigations:l.navigations,apiAssertions:l.apiAssertions,actions:S??[],artifacts:T,visual:w,failureDiagnostic:c,hasNetworkFile:l.networkRequestsFile!==null,networkRequestsCount:l.networkRequestsCount,hasConsoleFile:l.consoleLogsFile!==null,consoleLogsCount:l.consoleLogsCount,hasApiCallsFile:l.apiCallsFile!==null,apiCallsCount:l.apiCallsCount,startedAt:o};this.streamingWriter.writeTestDetail(I,N,{networkRequestsFile:l.networkRequestsFile,consoleLogsFile:l.consoleLogsFile,apiCallsFile:l.apiCallsFile});let j={id:I,title:u[u.length-1]??e.title,titlePath:u,filePath:m,status:a,duration:n.duration,project:u[1]??"",retryIndex:n.retry,tags:d,hasNetworkData:l.networkRequestsCount>0,hasConsoleData:l.consoleLogsCount>0,hasArtifacts:T!==null,hasActionSteps:(S?.length??0)>0,visualCount:w?.length??0,visualFailedCount:w?.filter(P=>P.status==="failed").length??0,domChangeCount:w?.reduce((P,re)=>P+(re.domChangeCount??0),0)??0,...(()=>{let P=w?.find(re=>re.diff);return P?.diff?{visualDiffPath:P.diff}:{}})(),errorMessage:Xl(c?.message),networkCount:l.networkRequestsCount,consoleCount:l.consoleLogsCount,actionCount:S?.length??0,testType:y,apiCallCount:l.apiCallsCount,...l.apiStatusMix?{apiStatusMix:l.apiStatusMix}:{},assertionsPassed:We(l.apiAssertions,"passed"),assertionsFailed:We(l.apiAssertions,"failed"),...(()=>{let P=Fs(l.apiAssertions);return P?{firstFailedAssertion:P}:{}})(),isRetry:!M,retryGroupKey:tt(m,u,e.id??"")};if(this.testIndex.push(j),M)switch(this.summaryCounters.total++,a){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}::${u.join(" > ")}`,Os(n.retry,z,l.apiAssertions,l.navigations,l.networkRequestsCount,l.consoleLogsCount,S)),(T||w)&&this.pendingArtifactEntries.push({testId:h,artifacts:T??{},visual:w,outputDir:dirname(this.config.outputPath)}),C.actions=null,C.apiAssertions=null,C.navigations=[],C.artifacts=null,C.failure=null,C.networkRequestsFile=null,C.consoleLogsFile=null,C.apiCallsFile=null;}this.activeReportMode!=="streaming"&&this.collectedTests.push(C),this.testCount++,this.checkMemoryPressure();}catch{}}async onEnd(e){try{let r=new Date().toISOString(),n=new Date(this.startedAt).getTime(),a=new Date(r).getTime()-n,o=this.activeReportMode==="streaming"?this.testIndex.some(d=>d.status!=="skipped"):this.collectedTests.some(d=>d.status!=="skipped");!this.fixtureDataReceived&&o&&process.stderr.write(`
|
|
2596
4301
|
\u26A0 TestRelic: No fixture data received from any test.
|
|
2597
4302
|
To enable network logs, video sync, and Test Navigation tracking, import the TestRelic fixture:
|
|
2598
4303
|
import { test, expect } from '@testrelic/playwright-analytics/fixture';
|
|
2599
4304
|
instead of:
|
|
2600
4305
|
import { test, expect } from '@playwright/test';
|
|
2601
4306
|
|
|
2602
|
-
`),this.config.captureNavigation?this.fixtureDataReceived&&!this.navigationsCaptured&&
|
|
4307
|
+
`),this.config.captureNavigation?this.fixtureDataReceived&&!this.navigationsCaptured&&o&&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).
|
|
2603
4308
|
`):process.stderr.write(`[testrelic] Test Navigation capture is disabled (captureNavigation: false). Remove the option or set it to true to capture the navigation graph.
|
|
2604
|
-
`),await
|
|
2605
|
-
`);let
|
|
4309
|
+
`),await Q(()=>kn(),zt,"Artifact flush");let i=null;if(this.config.includeArtifacts&&this.runTimestamp)try{let d=dirname(this.config.outputPath);i=Gn(d,this.runTimestamp);let l=join(d,"artifact-manifest.json"),c=JSON.stringify(i),u=l+".tmp";writeFileSync(u,c,"utf-8"),renameSync(u,l);}catch{}if(this.activeReportMode==="streaming"&&this.streamingWriter)await Q(()=>this.finalizeStreamingReport(r,a,e,i),Ut,"Report finalization");else {let d=this.buildTimeline(),l=new Map;for(let p of this.collectedTests){let g=l.get(p.testId);(!g||p.retry>g.retry)&&l.set(p.testId,p);}let c=Array.from(l.values()),u=ns(c,d.length),f={schemaVersion:Ce,testRunId:this.testRunId,startedAt:this.startedAt,completedAt:r,totalDuration:a,summary:u,ci:V(),metadata:this.config.metadata,timeline:d,shardRunIds:null};if(this.writeReport(f),await Q(()=>Mt(f,this.config,i),zt,"HTML report write"),Vt(u,this.config.outputPath,this.config.htmlReportPath,this.config.quiet),this.cloudClient){let p=this.collectArtifactEntries(),g=this.cloudTestsBuffer.slice();this.cloudTestsBuffer.length=0;let m=await Q(()=>pr(this.cloudClient,this.config.cloud,this.config.cloud?.uploadStrategy,this.testRunId,this.cloudRunId,f,r,a,u,p,g),Ut,"Cloud upload");this.cloudReceipt=fr(this.cloudReceipt,m,this.testRunId);}}}catch{}finally{await this.disposeCloudClient();}}async disposeCloudClient(){let e=this.cloudClient;e&&(this.cloudClient=null,await Q(()=>e.dispose(),Qn,"Cloud client shutdown"));}collectArtifactEntries(){if(this.activeReportMode==="streaming")return this.pendingArtifactEntries;let e=dirname(this.config.outputPath),r=[];for(let n of this.collectedTests)(n.artifacts||n.visual)&&r.push({testId:n.testId,artifacts:n.artifacts??{},visual:n.visual,outputDir:e});return r}async finalizeStreamingReport(e,r,n,s){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 a=new Map,o=new Map;for(let h of this.testIndex){let v=`${h.filePath}::${h.titlePath.join(" > ")}`,y=a.get(v);(y===void 0||h.retryIndex>y)&&a.set(v,h.retryIndex);}let i=0;for(let h of this.testIndex){let v=`${h.filePath}::${h.titlePath.join(" > ")}`,y=a.get(v);if(h.isRetry=h.retryIndex<y,h.isRetry)continue;switch(i++,this.summaryCounters.total++,h.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+=h.visualCount??0,this.summaryCounters.visualFailures+=h.visualFailedCount??0,this.summaryCounters.visualDomChanges+=h.domChangeCount??0;let b=o.get(h.filePath);switch(b||(b={filePath:h.filePath,total:0,passed:0,failed:0,flaky:0,skipped:0,timedOut:0},o.set(h.filePath,b)),b.total++,h.status){case "passed":b.passed++;break;case "failed":b.failed++;break;case "flaky":b.flaky++;break;case "skipped":b.skipped++;break;case "timedout":b.timedOut++;break}}let d=Array.from(o.values()),l=nd();for(let h of this.streamingTestAgg.values()){l.volApiCalls+=h.apiCalls,l.volAssertions+=h.assertions,l.volNavigations+=h.navigations,l.volNetworkRequests+=h.networkRequests,l.volConsoleLogs+=h.consoleLogs,l.volActionSteps+=h.actionSteps;for(let v in h.methodCounts)l.methodCounts[v]=(l.methodCounts[v]??0)+h.methodCounts[v];l.statusRanges["2xx"]+=h.statusRanges["2xx"],l.statusRanges["3xx"]+=h.statusRanges["3xx"],l.statusRanges["4xx"]+=h.statusRanges["4xx"],l.statusRanges["5xx"]+=h.statusRanges["5xx"],l.statusRanges.error+=h.statusRanges.error,l.passedAssertions+=h.passedAssertions,l.failedAssertions+=h.failedAssertions;for(let v in h.actionCategories)l.actionCategoryCounts[v]=(l.actionCategoryCounts[v]??0)+h.actionCategories[v];for(let v of h.apiUrls){if(l.apiUrlSet.size>=Ge)break;l.apiUrlSet.add(v);}for(let v of h.navUrls){if(l.navUrlSet.size>=Ge)break;l.navUrlSet.add(v);}for(let v of h.responseTimes){if(l.responseTimes.length>=Us)break;l.responseTimes.push(v);}}this.summaryCounters.totalApiCalls=l.volApiCalls,this.summaryCounters.totalAssertions=l.volAssertions,this.summaryCounters.totalNavigations=l.volNavigations,this.summaryCounters.totalNetworkRequests=l.volNetworkRequests,this.summaryCounters.totalConsoleLogs=l.volConsoleLogs,this.summaryCounters.totalActionSteps=l.volActionSteps,this.streamingWriter.writeIndex(this.testIndex),this.streamingWriter.writeCompactIndex(this.testIndex);let c={reporterTotal:this.summaryCounters.total,indexTotal:i,playwrightStatus:n.status,isValid:this.summaryCounters.total===i};c.isValid||process.stderr.write(`[testrelic] WARNING: Summary count mismatch \u2014 reporter: ${c.reporterTotal}, index: ${c.indexTotal}
|
|
4310
|
+
`);let u={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:l.apiUrlSet.size,apiCallsByMethod:{...l.methodCounts},apiCallsByStatusRange:{"2xx":l.statusRanges["2xx"],"3xx":l.statusRanges["3xx"],"4xx":l.statusRanges["4xx"],"5xx":l.statusRanges["5xx"],error:l.statusRanges.error},apiResponseTime:rd(l.responseTimes),totalAssertions:this.summaryCounters.totalAssertions,passedAssertions:l.passedAssertions,failedAssertions:l.failedAssertions,totalNavigations:this.summaryCounters.totalNavigations,uniqueNavigationUrls:l.navUrlSet.size,totalTimelineSteps:i,totalActionSteps:this.summaryCounters.totalActionSteps,actionStepsByCategory:{...l.actionCategoryCounts},visualChecks:this.summaryCounters.visualChecks,visualFailures:this.summaryCounters.visualFailures,visualDomChanges:this.summaryCounters.visualDomChanges},f={testRunId:this.testRunId,startedAt:this.startedAt,completedAt:e,totalDuration:r,...this.summaryCounters,metadata:this.config.metadata,reportMode:"streaming",files:d,enrichedSummary:u,validation:c,writeErrors:this.streamingWriter.getWriteErrors(),cloudUpload:this.cloudReceipt};this.streamingWriter.writeSummary(f);let p={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(p),await Mt(this.buildStreamingReport(f,u,e,r),this.config,s),Vt(u,this.config.outputPath,this.config.htmlReportPath,this.config.quiet);let g=this.streamingWriter.getReportDir();process.stderr.write(`[testrelic] Streaming report written to ${g}
|
|
2606
4311
|
[testrelic] ${this.testIndex.length} test detail files on disk
|
|
2607
|
-
[testrelic] View report: npx testrelic serve ${
|
|
4312
|
+
[testrelic] View report: npx testrelic serve ${g}
|
|
2608
4313
|
`),this.streamingWriter.getWriteErrors().length>0&&process.stderr.write(`[testrelic] WARNING: ${this.streamingWriter.getWriteErrors().length} write error(s) during streaming
|
|
2609
|
-
`);let
|
|
2610
|
-
`),this.switchToStreamingMode()):n>.8&&process.stderr.write(`[testrelic] WARNING: Memory pressure detected (${Math.round(r)} MB used).
|
|
2611
|
-
`);}catch{}}switchToStreamingMode(){try{if(this.activeReportMode==="streaming")return;this.activeReportMode="streaming";let e=dirname(this.config.outputPath);this.streamingWriter=new
|
|
2612
|
-
`)[0]??null,networkCount:r.networkRequestsCount,consoleCount:r.consoleLogsCount,actionCount:r.actions?.length??0,isRetry:!1};switch(this.testIndex.push(
|
|
2613
|
-
`);}}};var
|
|
2614
|
-
`);return
|
|
2615
|
-
`);for(let n of r){if(n.includes("/assertion-tracker.")||n.includes("/api-request-tracker.")||n.includes("node:internal")||n.includes(" at new Error")||n.includes(" at captureAssertionLocation")||n.includes(" at expectWrapper")||n.includes(" at wrappedMatcher"))continue;let s=n.match(/at\s+(?:.*?\s+\()?(.+?):(\d+):(\d+)\)?$/);if(s)return {file:s[1],line:parseInt(s[2],10),column:parseInt(s[3],10)}}return null}function eo(t,e){if(!t)return "custom";let r=t.toLowerCase();return r.includes(".status()")&&!r.includes(".statustext()")?r.includes(".ok()")?"statusOk":"status":r.includes(".ok()")?"statusOk":r.includes(".headers()")||r.includes(".headersarray()")?"header":r.includes("tomatchobject")||r.includes("toequal")?"bodyMatch":r.includes("tocontain")||r.includes("tostringcontaining")?"bodyContains":r.includes(".json()")||r.includes(".text()")?"bodyField":"custom"}function to(t,e,r){let n=function(a,...l){let i=t(),d=e();if(!i||!d)return r(a,...l);let o=null;if(a!=null&&typeof a=="object"){let u=a[Dt];typeof u=="string"&&(o=u);}if(!o&&a!==null&&a!==void 0&&typeof a=="object"&&(o=W.get(a)??null),o||(o=d.getCallIdForValue(a)),o||(o=i.getCurrentCallId()),!o)return r(a,...l);let c=Xi(),p;if(c){let u=Zi(c.file);u&&c.line>0&&c.line<=u.length&&(p=u[c.line-1].trim());}let g=r(a,...l);return o?Nn(g,{tracker:i,callId:o,received:a,location:c??{file:"unknown",line:0},expression:p}):g};for(let s of Object.keys(r))n[s]=r[s];return n}var En=null,Pn=null;function Z(t,e){En=t,Pn=e;}function Ee(t){return to(()=>En,()=>Pn,t)}function Nn(t,e){return new Proxy(t,{get(r,n,s){let a=Reflect.get(r,n,s);return typeof a!="function"||typeof n=="symbol"?a:n==="not"?Nn(a,e):function(...i){let d=i[0],o="passed",c=e.received,p=null;try{let g=a.apply(this??r,i);return g&&typeof g=="object"&&typeof g.then=="function"?g.then(()=>{Ft(e,n,d,c,"passed");},u=>{throw Ft(e,n,d,c,"failed"),u}):g}catch(g){if(o="failed",p=g,g instanceof Error&&g.message){let u=g.message.match(/Received:\s*(.+)/);u&&(c=u[1]);}throw g}finally{(p!==null||o==="passed")&&Ft(e,n,d,c,o);}}}})}function Ft(t,e,r,n,s){let a=eo(t.expression,t.received),l={callId:t.callId,type:a,expected:Ln(r),actual:Ln(n),status:s,location:t.location,expression:t.expression};t.tracker.recordAssertion(l);}function Ln(t){if(t==null||typeof t=="string"||typeof t=="number"||typeof t=="boolean")return t;try{return JSON.stringify(t),t}catch{return String(t)}}var Fn="[REDACTED]";function Ot(t,e){if(t===null||e.length===0)return t;let r=new Set(e.map(s=>s.toLowerCase())),n={};for(let s of Object.keys(t))Object.hasOwn(t,s)&&(n[s]=r.has(s.toLowerCase())?Fn:t[s]);return n}function Pe(t,e){if(t===null||e.length===0)return t;let r;try{r=JSON.parse(t);}catch{return t}if(typeof r!="object"||r===null)return t;let n=new Set(e.map(a=>a.toLowerCase())),s=Bt(r,n);return JSON.stringify(s)}function Bt(t,e){if(Array.isArray(t))return t.map(r=>Bt(r,e));if(typeof t=="object"&&t!==null){let r={};for(let n of Object.keys(t)){if(!Object.hasOwn(t,n))continue;let s=t[n];e.has(n.toLowerCase())?r[n]=Fn:r[n]=Bt(s,e);}return r}return t}var ro=new Set(["access_token","api_key","token","sig"]);function qt(t){if(!t)return t;let e;try{e=new URL(t);}catch{return t}let r=false;for(let n of [...e.searchParams.keys()])ro.has(n.toLowerCase())&&(e.searchParams.delete(n),r=true);return r?e.toString():t}function Bn(t,e,r){if(r.length>0){for(let n of r)if(Dn(t,n))return false}if(e.length>0){for(let n of e)if(Dn(t,n))return true;return false}return true}function Dn(t,e){try{return e instanceof RegExp?e.test(t):no(e).test(t)}catch{return console.warn(`[testrelic] Invalid URL filter pattern: ${String(e)}`),false}}function no(t){let e="",r=0;for(;r<t.length;){let n=t[r];n==="*"&&t[r+1]==="*"?(e+=".*",r+=2,t[r]==="/"&&r++):n==="*"?(e+="[^/]*",r++):n==="?"?(e+="[^/]",r++):".+^${}()|[]\\".includes(n)?(e+="\\"+n,r++):(e+=n,r++);}return new RegExp(e)}var so=["get","post","put","patch","delete","head","fetch"],ao=["text/","application/json","application/xml","application/javascript","application/x-www-form-urlencoded","application/graphql"];function io(t){let e=t.toLowerCase();return ao.some(r=>e.includes(r))}function oo(t){if(!t)return null;if(t.data!==void 0&&t.data!==null){let e=t.data;return typeof e=="string"?e:Buffer.isBuffer(e)?e.toString("base64"):JSON.stringify(e)}if(t.form!==void 0&&t.form!==null)return JSON.stringify(t.form);if(t.multipart!==void 0&&t.multipart!==null){let e=t.multipart,r={};for(let[n,s]of Object.entries(e))typeof s=="string"||typeof s=="number"||typeof s=="boolean"?r[n]=String(s):s&&typeof s=="object"&&"name"in s?r[n]=`[file: ${s.name}]`:r[n]="[binary]";return JSON.stringify(r)}return null}function On(t){return t?"\u2022\u2022\u2022\u2022":""}function lo(t){if(!t)return [];let e;for(let[n,s]of Object.entries(t))if(n.toLowerCase()==="cookie"){e=s;break}if(!e)return [];let r=[];for(let n of e.split(";")){let s=n.trim();if(!s)continue;let a=s.indexOf("="),l=a===-1?s:s.slice(0,a).trim(),i=a===-1?"":s.slice(a+1).trim();l&&r.push({name:l,value:On(i)});}return r}function co(t){let e=[];for(let r of t)for(let n of r.split(/,(?=\s*[A-Za-z0-9_.\-]+=)/)){let s=n.trim();if(!s)continue;let a=s.split(";"),l=a[0].indexOf("=");if(l===-1)continue;let i=a[0].slice(0,l).trim(),d=a[0].slice(l+1).trim(),o=a.slice(1).map(c=>c.trim()).filter(Boolean).join(" \xB7 ")||void 0;i&&e.push({name:i,value:On(d),attributes:o});}return e}var Ne=class Ne{constructor(e,r,n){this.originals=new Map;this.apiCallWriter=null;this.callCounter=0;this.apiCallCount=0;this.disposed=false;this._lastCallId=null;this.primitiveCallIds=new Map;this.context=e,this.assertionTracker=r??null,this.apiConfig=n??Ne.DEFAULT_API_CONFIG;try{this.apiCallWriter=new P("api-calls");}catch{}}get lastCallId(){return this._lastCallId}getCallIdForValue(e){return e!=null&&typeof e=="object"?W.get(e)??null:this.primitiveCallIds.get(e)??null}intercept(){for(let r of so){let n=this.context[r].bind(this.context);this.originals.set(r,n),this.context[r]=this.createWrapper(r,n);}let e=this.context.dispose.bind(this.context);this.originals.set("dispose",e),this.context.dispose=async r=>(this.disposed=true,e(r));}getFileData(){return this.apiCallWriter?.close(),{apiCallsFile:this.apiCallWriter?.getCount()?this.apiCallWriter.getPath():null,apiCallsCount:this.apiCallWriter?.getCount()??0}}flushLegacyAnnotations(e){}dispose(){for(let[e,r]of this.originals)this.context[e]=r;this.originals.clear(),this.apiCallWriter?.close(),this.callCounter=0,this.apiCallCount=0,this._lastCallId=null,this.primitiveCallIds.clear();}get isDisposed(){return this.disposed}getCapturedCallCount(){return this.apiCallCount}tagResponseMethods(e,r){let n=this,s=e.headers.bind(e);e.headers=function(){let u=s();return W.set(u,r),u};let a=e.headersArray.bind(e);e.headersArray=function(){let u=a();return W.set(u,r),u};let l=e.json.bind(e);e.json=async function(){let u=await l();return u!=null&&typeof u=="object"&&W.set(u,r),u};let i=e.status.bind(e);e.status=function(){let u=i();return n.primitiveCallIds.set(u,r),u};let d=e.statusText.bind(e);e.statusText=function(){let u=d();return n.primitiveCallIds.set(u,r),u};let o=e.ok.bind(e);e.ok=function(){let u=o();return n.primitiveCallIds.set(u,r),u};let c=e.text.bind(e);e.text=async function(){let u=await c();return n.primitiveCallIds.set(u,r),u};let p=e.body.bind(e);e.body=async function(){let u=await p();return W.set(u,r),u};}createWrapper(e,r){let n=this;return async function(a,l){if(!Bn(a,n.apiConfig.apiIncludeUrls,n.apiConfig.apiExcludeUrls))return r(a,l);let i=`api-call-${n.callCounter++}`,d=new Date().toISOString(),o=e==="fetch"?(l?.method??"GET").toUpperCase():e.toUpperCase(),c=oo(l),p=performance.now();n._lastCallId=i,n.assertionTracker&&n.assertionTracker.setCurrentCallId(i);let g;try{g=await r(a,l);}catch(u){let f=performance.now();try{let h=n.apiConfig.captureRequestBody?Pe(c,n.apiConfig.redactBodyFields):null,m={id:i,timestamp:d,method:o,url:qt(a),requestHeaders:null,requestBody:h,responseStatusCode:null,responseStatusText:null,responseHeaders:null,responseBody:null,responseTimeMs:Math.round((f-p)*100)/100,isBinary:!1,error:u instanceof Error?u.message:String(u)};n.apiCallWriter&&(n.apiCallWriter.append(m),n.apiCallCount++);}catch{}throw u}try{let u=performance.now(),f=g.headers(),h=f["content-type"]??null,m=h?!io(h):!1,y=null;n.apiConfig.captureRequestHeaders&&l?.headers&&(y=l.headers);let w=null;n.apiConfig.captureResponseHeaders&&(w=f);let v=n.apiConfig.captureRequestBody?c:null,k=null;if(n.apiConfig.captureResponseBody)try{m?k=(await g.body()).toString("base64"):k=await g.text();}catch{}let C=[],b=[];try{if(n.apiConfig.captureRequestHeaders&&(C=lo(l?.headers)),n.apiConfig.captureResponseHeaders){let S=g.headersArray().filter(L=>L.name.toLowerCase()==="set-cookie").map(L=>L.value);b=co(S);}}catch{}y=Ot(y,n.apiConfig.redactHeaders),w=Ot(w,n.apiConfig.redactHeaders),v=Pe(v,n.apiConfig.redactBodyFields),k=Pe(k,n.apiConfig.redactBodyFields);let T={id:i,timestamp:d,method:o,url:qt(g.url()),requestHeaders:y,requestBody:v,responseStatusCode:g.status(),responseStatusText:g.statusText(),responseHeaders:w,responseBody:k,requestCookies:C.length?C:null,responseCookies:b.length?b:null,responseTimeMs:Math.round((u-p)*100)/100,isBinary:m,error:null};n.apiCallWriter&&(n.apiCallWriter.append(T),n.apiCallCount++);}catch{}try{g[Dt]=i,n.tagResponseMethods(g,i);}catch{}return g}}};Ne.DEFAULT_API_CONFIG=Object.freeze({trackApiCalls:true,captureRequestHeaders:true,captureResponseHeaders:true,captureRequestBody:true,captureResponseBody:true,captureAssertions:true,redactHeaders:["authorization","cookie","set-cookie","x-api-key"],redactBodyFields:["password","secret","token","apiKey","api_key"],apiIncludeUrls:[],apiExcludeUrls:[]});var X=Ne;Ee(expect);var fo="__testrelic_api_config",go="__testrelic_config_trackApiCalls";function Ht(t){let e=t.annotations.find(n=>n.type===fo&&n.description!==void 0);if(e)try{return JSON.parse(e.description,ho)}catch{}let r=t.annotations.find(n=>n.type===go&&n.description!==void 0);return r?{trackApiCalls:r.description!=="false",captureRequestHeaders:true,captureResponseHeaders:true,captureRequestBody:true,captureResponseBody:true,captureAssertions:true,redactHeaders:["authorization","cookie","set-cookie","x-api-key"],redactBodyFields:["password","secret","token","apiKey","api_key"],apiIncludeUrls:[],apiExcludeUrls:[]}:{trackApiCalls:true,captureRequestHeaders:true,captureResponseHeaders:true,captureRequestBody:true,captureResponseBody:true,captureAssertions:true,redactHeaders:["authorization","cookie","set-cookie","x-api-key"],redactBodyFields:["password","secret","token","apiKey","api_key"],apiIncludeUrls:[],apiExcludeUrls:[]}}function ho(t,e){if(typeof e=="object"&&e!==null&&e.__regexp===true&&typeof e.source=="string"){let{source:r,flags:n}=e;return new RegExp(r,n)}return e}var zn={page:async({page:t},e,r)=>{let n=new Le(t);try{await n.init();}catch{}await e(t);try{let{navigations:s,networkRequestsFile:a,networkRequestsCount:l,consoleLogsFile:i,consoleLogsCount:d}=await n.getFileData(),o={testRelicData:!0,version:PAYLOAD_VERSION,navigations:s,apiAssertions:[],networkRequestsFile:a,networkRequestsCount:l,consoleLogsFile:i,consoleLogsCount:d,apiCallsFile:null,apiCallsCount:0};await r.attach(ATTACHMENT_NAME,{body:Buffer.from(JSON.stringify(o)),contentType:ATTACHMENT_CONTENT_TYPE});}catch{}n.dispose();},request:async({request:t},e,r)=>{let n=Ht(r);if(!n.trackApiCalls){await e(t);return}let s=new Q,a=new X(t,s,n);a.intercept(),Z(s,a);try{await e(t);}finally{Z(null,null);}try{let{apiCallsFile:l,apiCallsCount:i}=a.getFileData(),d=n.captureAssertions?s.getData():[],o={testRelicData:!0,version:PAYLOAD_VERSION,navigations:[],apiAssertions:d,networkRequestsFile:null,networkRequestsCount:0,consoleLogsFile:null,consoleLogsCount:0,apiCallsFile:l,apiCallsCount:i};await r.attach(ATTACHMENT_NAME,{body:Buffer.from(JSON.stringify(o)),contentType:ATTACHMENT_CONTENT_TYPE});}catch{}a.dispose(),s.dispose();}};test.extend(zn);Ee(expect);var wo={request:async({request:t},e,r)=>{let n=Ht(r);if(!n.trackApiCalls){await e(t);return}let s=new Q,a=new X(t,s,n);a.intercept(),Z(s,a);let l=null;try{l=new P("api-console");}catch{}let i=0,d=process.stdout.write,o=process.stderr.write;process.stdout.write=function(c,...p){try{let g=typeof c=="string"?c:Buffer.isBuffer(c)?c.toString("utf-8"):String(c);!g.startsWith("[testrelic]")&&!g.startsWith("\u2139 TestRelic")&&!g.startsWith("\u2713 TestRelic")&&!g.startsWith("\u26A0 TestRelic")&&l&&(l.append({level:"stdout",text:g.replace(/\n$/,""),timestamp:new Date().toISOString(),location:null}),i++);}catch{}return d.apply(process.stdout,[c,...p])},process.stderr.write=function(c,...p){try{let g=typeof c=="string"?c:Buffer.isBuffer(c)?c.toString("utf-8"):String(c);!g.startsWith("[testrelic]")&&!g.startsWith("\u26A0 TestRelic")&&l&&(l.append({level:"stderr",text:g.replace(/\n$/,""),timestamp:new Date().toISOString(),location:null}),i++);}catch{}return o.apply(process.stderr,[c,...p])};try{await e(t);}finally{process.stdout.write=d,process.stderr.write=o,Z(null,null);}try{l?.close();let{apiCallsFile:c,apiCallsCount:p}=a.getFileData(),g=n.captureAssertions?s.getData():[],u={testRelicData:!0,version:PAYLOAD_VERSION,navigations:[],apiAssertions:g,networkRequestsFile:null,networkRequestsCount:0,consoleLogsFile:i>0?l?.getPath()??null:null,consoleLogsCount:i,apiCallsFile:c,apiCallsCount:p};await r.attach(ATTACHMENT_NAME,{body:Buffer.from(JSON.stringify(u)),contentType:ATTACHMENT_CONTENT_TYPE});}catch{}a.dispose(),s.dispose();}};var $n="@testrelic/playwright-analytics";function ko(t){return Array.isArray(t)?t.some(e=>Array.isArray(e)&&typeof e[0]=="string"&&e[0]===$n):false}function To(t,e){let n={video:"on",screenshot:"on",trace:"on",...t.use??{}},s;Array.isArray(t.reporter)?s=t.reporter:typeof t.reporter=="string"?s=[[t.reporter]]:s=[["list"]];let a=ko(s)?s:[...s,[$n,e??{}]];return defineConfig({...t,use:n,reporter:a})}var jn=false;function Gc(t,e,r="manual_record"){if(!t||!t.annotations){jn||(jn=true,process.stderr.write(`[testrelic] recordNavigation: reporter not active, navigation not recorded
|
|
2616
|
-
`));return}let n={url:e,navigationType:r,timestamp:new Date().toISOString()};t.annotations.push({type:"lambdatest-navigation",description:JSON.stringify(n)});}export{pe as SCHEMA_VERSION,Me as default,To as defineConfig,Gc as recordNavigation,wo as testRelicApiFixture,zn as testRelicFixture};//# sourceMappingURL=index.js.map
|
|
4314
|
+
`);let m=this.streamingWriter;try{if(this.cloudClient){let h=this.buildStreamingReport(f,u,e,r),v=this.collectArtifactEntries(),y=this.cloudTestsBuffer.slice();this.cloudTestsBuffer.length=0;let b=await pr(this.cloudClient,this.config.cloud,this.config.cloud?.uploadStrategy,this.testRunId,this.cloudRunId,h,e,r,u,v,y);this.cloudReceipt=fr(this.cloudReceipt,b,this.testRunId);let k={...f,cloudUpload:this.cloudReceipt};try{m.writeSummary(k);}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(e,r,n,s){return {schemaVersion:Ce,testRunId:this.testRunId,startedAt:this.startedAt,completedAt:n,totalDuration:s,summary:r,ci:V(),metadata:this.config.metadata,timeline:[],shardRunIds:null}}printsToStdio(){return false}checkMemoryPressure(){try{let e=process.memoryUsage(),r=e.heapUsed/1024/1024;if(r<200)return;let n=e.heapUsed/e.heapTotal;n>.9&&this.activeReportMode==="embedded"?(process.stderr.write(`[testrelic] WARNING: High memory (${Math.round(r)} MB). Auto-switching to streaming mode.
|
|
4315
|
+
`),this.switchToStreamingMode()):n>.8&&r>=500&&!this.memoryPressureWarned&&(this.memoryPressureWarned=!0,process.stderr.write(`[testrelic] WARNING: Memory pressure detected (${Math.round(r)} MB used).
|
|
4316
|
+
`));}catch{}}switchToStreamingMode(){try{if(this.activeReportMode==="streaming")return;this.activeReportMode="streaming";let e=dirname(this.config.outputPath);this.streamingWriter=new he(e);for(let r of this.collectedTests){let n=et(r.filePath,r.titlePath,r.testId,r.retry),s=r.apiCallsFile&&r.apiCallsCount>0?Y(r.apiCallsFile):void 0,a={id:n,navigations:r.navigations??[],apiAssertions:r.apiAssertions??[],actions:r.actions??[],artifacts:r.artifacts,failureDiagnostic:r.failure,hasNetworkFile:r.networkRequestsFile!==null,networkRequestsCount:r.networkRequestsCount,hasConsoleFile:r.consoleLogsFile!==null,consoleLogsCount:r.consoleLogsCount,hasApiCallsFile:r.apiCallsFile!==null,apiCallsCount:r.apiCallsCount,startedAt:r.startedAt};this.streamingWriter.writeTestDetail(n,a,{networkRequestsFile:r.networkRequestsFile,consoleLogsFile:r.consoleLogsFile,apiCallsFile:r.apiCallsFile});let o={id:n,title:r.titlePath[r.titlePath.length-1]??r.title,titlePath:r.titlePath,filePath:r.filePath,status:r.status,duration:r.duration,project:r.titlePath[1]??"",retryIndex:r.retry,tags:r.tags,hasNetworkData:r.networkRequestsCount>0,hasConsoleData:r.consoleLogsCount>0,hasArtifacts:r.artifacts!==null,hasActionSteps:(r.actions?.length??0)>0,errorMessage:r.failure?.message?.split(`
|
|
4317
|
+
`)[0]??null,networkCount:r.networkRequestsCount,consoleCount:r.consoleLogsCount,actionCount:r.actions?.length??0,testType:r.testType,apiCallCount:r.apiCallsCount,...r.apiStatusMix?{apiStatusMix:r.apiStatusMix}:s?{apiStatusMix:td(s)}:{},assertionsPassed:We(r.apiAssertions,"passed"),assertionsFailed:We(r.apiAssertions,"failed"),...(()=>{let i=Fs(r.apiAssertions);return i?{firstFailedAssertion:i}:{}})(),isRetry:!1,retryGroupKey:tt(r.filePath,r.titlePath,r.testId)};switch(this.testIndex.push(o),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(" > ")}`,Os(r.retry,s,r.apiAssertions??[],r.navigations??[],r.networkRequestsCount,r.consoleLogsCount,r.actions??null));}this.collectedTests.length=0;}catch{}}buildTimeline(){let e=this.collectedTests.map(r=>({titlePath:r.titlePath,title:r.title,status:r.status,duration:r.duration,startedAt:r.startedAt,completedAt:r.completedAt,retryCount:r.retryCount,retry:r.retry,tags:r.tags,failure:r.failure,specFile:r.specFile,navigations:r.navigations,apiCalls:null,apiAssertions:r.apiAssertions,testId:r.testId,filePath:r.filePath,suiteName:r.suiteName,testType:r.testType,isFlaky:r.isFlaky,retryStatus:r.retryStatus,expectedStatus:r.expectedStatus,actualStatus:r.actualStatus,artifacts:r.artifacts,networkRequests:null,actions:r.actions,consoleLogs:null}));return rs(e,{navigationTypes:this.config.navigationTypes})}readJsonlFile(e){if(!e)return null;let r=Y(e);return r.length>0?r:null}toTestResult(e,r){let n=null,s=null,a=null;return r?.loadFiles&&(n=this.readJsonlFile(e.networkRequestsFile),s=this.readJsonlFile(e.consoleLogsFile),a=this.readJsonlFile(e.apiCallsFile)),{title:e.title,status:e.status,duration:e.duration,startedAt:e.startedAt,completedAt:e.completedAt,retryCount:e.retryCount,tags:e.tags,failure:e.failure,testId:e.testId,filePath:e.filePath,suiteName:e.suiteName,testType:e.testType,isFlaky:e.isFlaky,retryStatus:e.retryStatus,expectedStatus:e.expectedStatus,actualStatus:e.actualStatus,artifacts:e.artifacts,visual:e.visual,networkRequests:n,apiCalls:a,apiAssertions:e.apiAssertions,actions:e.actions??null,consoleLogs:s}}buildCloudTestPayload(e){let r={title:e.title,status:e.status,duration:e.duration,startedAt:e.startedAt,completedAt:e.completedAt,retry:e.retry,retryCount:e.retryCount,retryStatus:e.retryStatus,tags:e.tags,failure:e.failure,testId:e.testId,filePath:e.filePath,suiteName:e.suiteName,testType:e.testType,isFlaky:e.isFlaky,expectedStatus:e.expectedStatus,actualStatus:e.actualStatus,artifacts:e.artifacts,visual:e.visual??[],actions:e.actions??[],navigations:e.navigations??[],steps:e.actions??[],browser:e.browser,os:e.os,source:e.source};if(e.apiAssertions&&e.apiAssertions.length>0&&(r.protocolAssertions=tr(e.apiAssertions)),e.consoleLogsFile&&(r.consoleLogs=Y(e.consoleLogsFile)),e.networkRequestsFile&&(r.networkRequests=Y(e.networkRequestsFile)),e.apiCallsFile){let n=Y(e.apiCallsFile);r.apiCalls=Qt(n),(!r.networkRequests||r.networkRequests.length===0)&&(r.networkRequests=er(n));}return r}writeReport(e){try{let r=JSON.stringify(e),n=this.config.outputPath,s=dirname(n);mkdirSync(s,{recursive:!0});let a=n+".tmp";writeFileSync(a,r,"utf-8"),renameSync(a,n);}catch(r){process.stderr.write(`[testrelic] Failed to write report: ${r instanceof Error?r.message:String(r)}
|
|
4318
|
+
`);}}};var gd=["text/","application/json","application/xml","application/javascript","application/x-www-form-urlencoded","application/graphql"];function hd(t){let e=t.toLowerCase();return gd.some(r=>e.includes(r))}var Je=class t{constructor(e,r){this.page=e;this.records=[];this.listeners=[];this.navCounters=[];this.pendingRequests=new Map;this.networkWriter=null;this.consoleWriter=null;this.pendingBodyReads=[];this.requestIdCounter=0;this.networkRequestCount=0;this.consoleLogCount=0;this.includeNetworkStats=r?.includeNetworkStats??true,this.origGoto=e.goto.bind(e),this.origGoBack=e.goBack.bind(e),this.origGoForward=e.goForward.bind(e),this.origReload=e.reload.bind(e);try{this.includeNetworkStats&&(this.networkWriter=new q("network")),this.consoleWriter=new q("console");}catch{}this.interceptMethods(),this.attachListeners();}async init(){await this.injectSPADetection();}async finalizeCapturedRequests(){await Promise.allSettled(this.pendingBodyReads),this.pendingBodyReads=[];for(let[,e]of this.pendingRequests)this.networkWriter&&(this.networkWriter.append({url:e.url,method:e.method,resourceType:e.resourceType,statusCode:0,responseTimeMs:Date.now()-e.startTimeMs,startedAt:e.startedAt,requestHeaders:e.headers,requestBody:e.postData,responseBody:null,responseHeaders:null,contentType:null,responseSize:0,requestBodyTruncated:e.postDataTruncated,responseBodyTruncated:false,isBinary:false,error:"incomplete"}),this.networkRequestCount++);this.pendingRequests.clear(),this.networkWriter?.close(),this.consoleWriter?.close();}static mapConsoleType(e){switch(e){case "log":return "log";case "warning":return "warn";case "error":return "error";case "info":return "info";case "debug":return "debug";default:return "log"}}async getFileData(){this.assembleNetworkStats();let e=this.records.map(r=>({url:r.url,navigationType:r.navigationType,timestamp:r.timestamp,domContentLoadedAt:r.domContentLoadedAt,networkIdleAt:r.networkIdleAt,networkStats:r.networkStats}));return this.includeNetworkStats?await this.finalizeCapturedRequests():this.consoleWriter?.close(),{navigations:e,networkRequestsFile:this.networkWriter?.getCount()?this.networkWriter.getPath():null,networkRequestsCount:this.networkWriter?.getCount()??0,consoleLogsFile:this.consoleWriter?.getCount()?this.consoleWriter.getPath():null,consoleLogsCount:this.consoleWriter?.getCount()??0}}async flushLegacyAnnotations(e){}dispose(){let e=t.asPatchable(this.page);e.goto=this.origGoto,e.goBack=this.origGoBack,e.goForward=this.origGoForward,e.reload=this.origReload;for(let{event:r,handler:n}of this.listeners)this.page.off(r,n);this.listeners=[],this.records=[],this.pendingRequests.clear(),this.pendingBodyReads=[],this.networkWriter?.close(),this.consoleWriter?.close();}getRecords(){return this.assembleNetworkStats(),this.records}static asPatchable(e){return e}interceptMethods(){let e=this,r=this.page,n=t.asPatchable(r);n.goto=async function(s,a){return e.recordNavigation(s,"goto"),e.origGoto(s,a)},n.goBack=async function(s){let a=await e.origGoBack(s);return e.recordNavigation(r.url(),"back"),a},n.goForward=async function(s){let a=await e.origGoForward(s);return e.recordNavigation(r.url(),"forward"),a},n.reload=async function(s){return e.recordNavigation(r.url(),"refresh"),e.origReload(s)};}attachListeners(){let e=()=>{this.lastDomContentLoaded=new Date().toISOString(),this.records.length>0&&(this.records[this.records.length-1].domContentLoadedAt=this.lastDomContentLoaded);};this.page.on("domcontentloaded",e),this.listeners.push({event:"domcontentloaded",handler:e});let r=s=>{try{let a=s;if(typeof a.parentFrame=="function"&&a.parentFrame()!==null)return;let o=a.url(),i=this.records[this.records.length-1];if(i&&Date.now()-new Date(i.timestamp).getTime()<50&&i.url===o)return;this.recordNavigation(o,"navigation");}catch{}};this.page.on("framenavigated",r),this.listeners.push({event:"framenavigated",handler:r});let n=s=>{try{let a=s,o=a.type(),i=a.text();if(o==="debug"&&i.startsWith("__testrelic_nav:")){try{let d=JSON.parse(i.slice(16));d.type&&d.url&&this.recordNavigation(d.url,d.type);}catch{}return}{let d=null;try{let c=a.location();c&&c.url&&(d=`${c.url}:${c.lineNumber}:${c.columnNumber}`);}catch{}let l=t.mapConsoleType(o);this.consoleWriter&&(this.consoleWriter.append({level:l,text:i,timestamp:new Date().toISOString(),location:d}),this.consoleLogCount++);}}catch{}};if(this.page.on("console",n),this.listeners.push({event:"console",handler:n}),this.includeNetworkStats){let s=i=>{let d=this.records.length-1,l=this.counterForNav(d);l&&l.totalRequests++;try{let c=i,u=String(this.requestIdCounter++),f=c.postData()??null,p={url:c.url(),method:c.method(),resourceType:this.mapResourceType(c.resourceType()),headers:c.headers(),postData:f,postDataTruncated:!1,startedAt:new Date().toISOString(),startTimeMs:Date.now(),navIndex:d};this.pendingRequests.set(u,p),i.__testrelic_id=u;}catch{}};this.page.on("request",s),this.listeners.push({event:"request",handler:s});let a=i=>{try{let d=i,l=d.request().__testrelic_id,c=l?this.pendingRequests.get(l):void 0,u=this.counterForNav(c?c.navIndex:this.records.length-1);if(u){let y=d.status();y>=400&&(u.failedRequests++,u.failedRequestUrls.push(y+" "+d.url()));let b=d.headers()["content-length"];b&&(u.totalBytes+=parseInt(b,10)||0);let k=d.request().resourceType(),A=this.mapResourceType(k);u.byType[A]++;}if(!l||!c)return;this.pendingRequests.delete(l);let f=Date.now()-c.startTimeMs,p=d.headers(),g=p["content-type"]??null,m=parseInt(p["content-length"]??"0",10)||0,h=g?!hd(g):!1,v=(async()=>{let y=null;if(!h)try{y=(await d.body()).toString("utf-8");}catch{}let b={url:c.url,method:c.method,resourceType:c.resourceType,statusCode:d.status(),responseTimeMs:f,startedAt:c.startedAt,requestHeaders:c.headers,requestBody:c.postData,responseBody:y,responseHeaders:p,contentType:g,responseSize:m,requestBodyTruncated:c.postDataTruncated,responseBodyTruncated:!1,isBinary:h,error:null};this.networkWriter&&(this.networkWriter.append(b),this.networkRequestCount++);})();this.pendingBodyReads.push(v);}catch{}};this.page.on("response",a),this.listeners.push({event:"response",handler:a});let o=i=>{let d=i.__testrelic_id,l=d?this.pendingRequests.get(d):void 0,c=this.counterForNav(l?l.navIndex:this.records.length-1);if(c){c.failedRequests++;try{let u=i;c.failedRequestUrls.push("ERR "+u.url());}catch{}}try{let u=i;if(!d||!l)return;this.pendingRequests.delete(d);let f={url:l.url,method:l.method,resourceType:l.resourceType,statusCode:0,responseTimeMs:Date.now()-l.startTimeMs,startedAt:l.startedAt,requestHeaders:l.headers,requestBody:l.postData,responseBody:null,responseHeaders:null,contentType:null,responseSize:0,requestBodyTruncated:l.postDataTruncated,responseBodyTruncated:!1,isBinary:!1,error:u.failure()?.errorText??"Unknown error"};this.networkWriter&&(this.networkWriter.append(f),this.networkRequestCount++);}catch{}};this.page.on("requestfailed",o),this.listeners.push({event:"requestfailed",handler:o});}}async injectSPADetection(){try{await this.page.addInitScript(()=>{let e=history.pushState.bind(history),r=history.replaceState.bind(history);history.pushState=function(...n){e(...n),console.debug("__testrelic_nav:"+JSON.stringify({type:"spa_route",url:location.href}));},history.replaceState=function(...n){r(...n),console.debug("__testrelic_nav:"+JSON.stringify({type:"spa_replace",url:location.href}));},window.addEventListener("popstate",()=>{console.debug("__testrelic_nav:"+JSON.stringify({type:"popstate",url:location.href}));}),window.addEventListener("hashchange",()=>{console.debug("__testrelic_nav:"+JSON.stringify({type:"hash_change",url:location.href}));});});}catch{}}recordNavigation(e,r){this.records.push({url:e,navigationType:r,timestamp:new Date().toISOString()}),this.includeNetworkStats&&this.navCounters.push(this.createNetworkCounter());}createNetworkCounter(){return {totalRequests:0,failedRequests:0,failedRequestUrls:[],totalBytes:0,byType:{xhr:0,document:0,script:0,stylesheet:0,image:0,font:0,other:0}}}counterForNav(e){return e>=0?this.navCounters[e]:void 0}assembleNetworkStats(){if(this.includeNetworkStats)for(let e=0;e<this.records.length;e++){let r=this.navCounters[e];r&&(this.records[e].networkStats={totalRequests:r.totalRequests,failedRequests:r.failedRequests,failedRequestUrls:[...r.failedRequestUrls],totalBytes:r.totalBytes,byType:{...r.byType}});}}mapResourceType(e){switch(e){case "xhr":case "fetch":return "xhr";case "document":return "document";case "script":return "script";case "stylesheet":return "stylesheet";case "image":return "image";case "font":return "font";default:return "other"}}};var yr=Symbol.for("__testrelic_call_id"),vd="__testrelic_api_assertions",te=new WeakMap,oe=class{constructor(){this.assertions=[];this.currentCallId=null;}recordAssertion(e){this.assertions.push(e);}getAssertions(){return this.assertions}setCurrentCallId(e){this.currentCallId=e;}getCurrentCallId(){return this.currentCallId}getData(){return [...this.assertions]}flushLegacyAnnotations(e){this.assertions.length!==0&&e.annotations.push({type:vd,description:JSON.stringify(this.assertions)});}dispose(){this.assertions=[],this.currentCallId=null;}},zs=new Map;function yd(t){let e=zs.get(t);if(e)return e;try{let r=t.startsWith("file://")?fileURLToPath(t):t,s=readFileSync(r,"utf-8").split(`
|
|
4319
|
+
`);return zs.set(t,s),s}catch{return null}}var js=(()=>{let t=new Error().stack;if(!t)return null;for(let e of t.split(`
|
|
4320
|
+
`)){let r=e.match(/at\s+(?:.*?\s+\()?(.+?):\d+:\d+\)?$/);if(!r||r[1].startsWith("node:"))continue;let n=Math.max(r[1].lastIndexOf("/"),r[1].lastIndexOf("\\"));return n>0?r[1].slice(0,n+1):null}return null})();function bd(t){if(!t.startsWith("file:"))return t;try{return fileURLToPath(t)}catch{return t}}function xd(){let e=new Error().stack;if(!e)return null;let r=e.split(`
|
|
4321
|
+
`);return Vs(r,true)??Vs(r,false)}function Vs(t,e){for(let r of t){if(e&&js!==null&&r.includes(js)||r.includes("node_modules")||r.includes("node:internal")||r.includes(" at new Error")||r.includes(" at captureAssertionLocation")||r.includes(" at findFrame")||r.includes(" at expectWrapper")||r.includes(" at wrappedMatcher"))continue;let n=r.match(/at\s+(?:.*?\s+\()?(.+?):(\d+):(\d+)\)?$/);if(n)return {file:bd(n[1]),line:parseInt(n[2],10),column:parseInt(n[3],10)}}return null}function wd(t,e){if(!t)return "custom";let r=t.toLowerCase();return r.includes(".status()")&&!r.includes(".statustext()")?r.includes(".ok()")?"statusOk":"status":r.includes(".ok()")?"statusOk":r.includes(".headers()")||r.includes(".headersarray()")?"header":r.includes("tomatchobject")||r.includes("toequal")?"bodyMatch":r.includes("tocontain")||r.includes("tostringcontaining")?"bodyContains":r.includes(".json()")||r.includes(".text()")?"bodyField":"custom"}function kd(t,e,r){let n=function(i,...d){let l=t(),c=e();if(!l||!c)return r(i,...d);let u=null;if(i!=null&&typeof i=="object"){let m=i[yr];typeof m=="string"&&(u=m);}if(!u&&i!==null&&i!==void 0&&typeof i=="object"&&(u=te.get(i)??null),u||(u=c.getCallIdForValue(i)),u||(u=l.getCurrentCallId()),!u)return r(i,...d);let f=xd(),p;if(f){let m=yd(f.file);m&&f.line>0&&f.line<=m.length&&(p=m[f.line-1].trim());}let g=r(i,...d);return u?Ks(g,{tracker:l,callId:u,received:i,location:f??{file:"unknown",line:0},expression:p}):g},s=r,a=n;for(let o of Object.keys(s))a[o]=s[o];return n}var $s=Symbol.for("testrelic.activeAssertionContext");function vr(){let t=globalThis,e=t[$s];return e||(e={tracker:null,apiTracker:null},t[$s]=e),e}function le(t,e){let r=vr();r.tracker=t,r.apiTracker=e;}function Ye(t){return kd(()=>vr().tracker,()=>vr().apiTracker,t)}function Ks(t,e){return new Proxy(t,{get(r,n,s){let a=Reflect.get(r,n,s);return typeof a!="function"||typeof n=="symbol"?a:n==="not"?Ks(a,e):function(...i){let d=i[0],l="passed",c=e.received,u=null;try{let f=a.apply(this??r,i);return f&&typeof f=="object"&&typeof f.then=="function"?f.then(()=>{mr(e,n,d,c,"passed");},p=>{throw mr(e,n,d,c,"failed"),p}):f}catch(f){if(l="failed",u=f,f instanceof Error&&f.message){let p=f.message.match(/Received:\s*(.+)/);p&&(c=p[1]);}throw f}finally{(u!==null||l==="passed")&&mr(e,n,d,c,l);}}}})}function mr(t,e,r,n,s){let a=wd(t.expression,t.received),o={callId:t.callId,type:a,expected:Ws(r),actual:Ws(n),status:s,location:t.location,expression:t.expression};t.tracker.recordAssertion(o);}function Ws(t){if(t==null)return t;if(typeof t=="string")return se(t);if(typeof t=="number"||typeof t=="boolean")return t;try{return JSON.stringify(t),t}catch{return String(t)}}var Js="[REDACTED]";function xr(t,e){if(t===null||e.length===0)return t;let r=new Set(e.map(s=>s.toLowerCase())),n={};for(let s of Object.keys(t))Object.prototype.hasOwnProperty.call(t,s)&&(n[s]=r.has(s.toLowerCase())?Js:t[s]);return n}function Xe(t,e){if(t===null||e.length===0)return t;let r;try{r=JSON.parse(t);}catch{return t}if(typeof r!="object"||r===null)return t;let n=new Set(e.map(a=>a.toLowerCase())),s=br(r,n);return JSON.stringify(s)}function br(t,e){if(Array.isArray(t))return t.map(r=>br(r,e));if(typeof t=="object"&&t!==null){let r={};for(let n of Object.keys(t)){if(!Object.prototype.hasOwnProperty.call(t,n))continue;let s=t[n];e.has(n.toLowerCase())?r[n]=Js:r[n]=br(s,e);}return r}return t}var Cd=new Set(["access_token","api_key","token","sig"]);function wr(t){if(!t)return t;let e;try{e=new URL(t);}catch{return t}let r=false;for(let n of [...e.searchParams.keys()])Cd.has(n.toLowerCase())&&(e.searchParams.delete(n),r=true);return r?e.toString():t}function Xs(t,e,r){if(r.length>0){for(let n of r)if(Ys(t,n))return false}if(e.length>0){for(let n of e)if(Ys(t,n))return true;return false}return true}function Ys(t,e){try{return e instanceof RegExp?e.test(t):Td(e).test(t)}catch{return console.warn(`[testrelic] Invalid URL filter pattern: ${String(e)}`),false}}function Td(t){let e="",r=0;for(;r<t.length;){let n=t[r];n==="*"&&t[r+1]==="*"?(e+=".*",r+=2,t[r]==="/"&&r++):n==="*"?(e+="[^/]*",r++):n==="?"?(e+="[^/]",r++):".+^${}()|[]\\".includes(n)?(e+="\\"+n,r++):(e+=n,r++);}return new RegExp(e)}var Sd=["get","post","put","patch","delete","head","fetch"],Rd=["text/","application/json","application/xml","application/javascript","application/x-www-form-urlencoded","application/graphql"];function Ad(t){let e=t.toLowerCase();return Rd.some(r=>e.includes(r))}function _d(t){if(!t)return null;if(t.data!==void 0&&t.data!==null){let e=t.data;return typeof e=="string"?e:Buffer.isBuffer(e)?e.toString("base64"):JSON.stringify(e)}if(t.form!==void 0&&t.form!==null)return JSON.stringify(t.form);if(t.multipart!==void 0&&t.multipart!==null){let e=t.multipart,r={};for(let[n,s]of Object.entries(e))typeof s=="string"||typeof s=="number"||typeof s=="boolean"?r[n]=String(s):s&&typeof s=="object"&&"name"in s?r[n]=`[file: ${s.name}]`:r[n]="[binary]";return JSON.stringify(r)}return null}function Zs(t){return t?"\u2022\u2022\u2022\u2022":""}function Id(t){if(!t)return [];let e;for(let[n,s]of Object.entries(t))if(n.toLowerCase()==="cookie"){e=s;break}if(!e)return [];let r=[];for(let n of e.split(";")){let s=n.trim();if(!s)continue;let a=s.indexOf("="),o=a===-1?s:s.slice(0,a).trim(),i=a===-1?"":s.slice(a+1).trim();o&&r.push({name:o,value:Zs(i)});}return r}function Md(t){let e=[];for(let r of t)for(let n of r.split(/,(?=\s*[A-Za-z0-9_.\-]+=)/)){let s=n.trim();if(!s)continue;let a=s.split(";"),o=a[0].indexOf("=");if(o===-1)continue;let i=a[0].slice(0,o).trim(),d=a[0].slice(o+1).trim(),l=a.slice(1).map(c=>c.trim()).filter(Boolean).join(" \xB7 ")||void 0;i&&e.push({name:i,value:Zs(d),attributes:l});}return e}var Ze=class Ze{constructor(e,r,n){this.originals=new Map;this.apiCallWriter=null;this.statusMix={"2xx":0,"3xx":0,"4xx":0,"5xx":0,error:0};this.callCounter=0;this.apiCallCount=0;this.disposed=false;this._lastCallId=null;this.primitiveCallIds=new Map;this.context=e,this.assertionTracker=r??null,this.apiConfig=n??Ze.DEFAULT_API_CONFIG;try{this.apiCallWriter=new q("api-calls");}catch{}}get lastCallId(){return this._lastCallId}getCallIdForValue(e){return e!=null&&typeof e=="object"?te.get(e)??null:this.primitiveCallIds.get(e)??null}intercept(){for(let r of Sd){let n=this.context[r].bind(this.context);this.originals.set(r,n),this.context[r]=this.createWrapper(r,n);}let e=this.context.dispose.bind(this.context);this.originals.set("dispose",e),this.context.dispose=async r=>(this.disposed=true,e(r));}tallyStatus(e){if(e===null||e<=0){this.statusMix.error++;return}if(e>=500){this.statusMix["5xx"]++;return}if(e>=400){this.statusMix["4xx"]++;return}if(e>=300){this.statusMix["3xx"]++;return}if(e>=200){this.statusMix["2xx"]++;return}this.statusMix.error++;}getFileData(){return this.apiCallWriter?.close(),{apiCallsFile:this.apiCallWriter?.getCount()?this.apiCallWriter.getPath():null,apiCallsCount:this.apiCallWriter?.getCount()??0,apiStatusMix:{...this.statusMix}}}flushLegacyAnnotations(e){}dispose(){for(let[e,r]of this.originals)this.context[e]=r;this.originals.clear(),this.apiCallWriter?.close(),this.callCounter=0,this.apiCallCount=0,this._lastCallId=null,this.primitiveCallIds.clear();}get isDisposed(){return this.disposed}getCapturedCallCount(){return this.apiCallCount}tagResponseMethods(e,r){let n=this,s=e.headers.bind(e);e.headers=function(){let p=s();return te.set(p,r),p};let a=e.headersArray.bind(e);e.headersArray=function(){let p=a();return te.set(p,r),p};let o=e.json.bind(e);e.json=async function(){let p=await o();return p!=null&&typeof p=="object"&&te.set(p,r),p};let i=e.status.bind(e);e.status=function(){let p=i();return n.primitiveCallIds.set(p,r),p};let d=e.statusText.bind(e);e.statusText=function(){let p=d();return n.primitiveCallIds.set(p,r),p};let l=e.ok.bind(e);e.ok=function(){let p=l();return n.primitiveCallIds.set(p,r),p};let c=e.text.bind(e);e.text=async function(){let p=await c();return n.primitiveCallIds.set(p,r),p};let u=e.body.bind(e);e.body=async function(){let p=await u();return te.set(p,r),p};}createWrapper(e,r){let n=this;return async function(a,o){if(!Xs(a,n.apiConfig.apiIncludeUrls,n.apiConfig.apiExcludeUrls))return r(a,o);let i=`api-call-${n.callCounter++}`,d=new Date().toISOString(),l=e==="fetch"?(o?.method??"GET").toUpperCase():e.toUpperCase(),c=_d(o),u=performance.now();n._lastCallId=i,n.assertionTracker&&n.assertionTracker.setCurrentCallId(i);let f;try{f=await r(a,o);}catch(p){let g=performance.now();try{let m=n.apiConfig.captureRequestBody?Xe(c,n.apiConfig.redactBodyFields):null,h={id:i,timestamp:d,method:l,url:wr(a),requestHeaders:null,requestBody:m,responseStatusCode:null,responseStatusText:null,responseHeaders:null,responseBody:null,responseTimeMs:Math.round((g-u)*100)/100,isBinary:!1,error:p instanceof Error?p.message:String(p)};n.apiCallWriter&&(n.apiCallWriter.append(h),n.apiCallCount++,n.tallyStatus(null));}catch{}throw p}try{let p=performance.now(),g=f.headers(),m=g["content-type"]??null,h=m?!Ad(m):!1,v=null;n.apiConfig.captureRequestHeaders&&o?.headers&&(v=o.headers);let y=null;n.apiConfig.captureResponseHeaders&&(y=g);let b=n.apiConfig.captureRequestBody?c:null,k=null;if(n.apiConfig.captureResponseBody)try{h?k=(await f.body()).toString("base64"):k=await f.text();}catch{}let A=[],E=[];try{if(n.apiConfig.captureRequestHeaders&&(A=Id(o?.headers)),n.apiConfig.captureResponseHeaders){let w=f.headersArray().filter(S=>S.name.toLowerCase()==="set-cookie").map(S=>S.value);E=Md(w);}}catch{}v=xr(v,n.apiConfig.redactHeaders),y=xr(y,n.apiConfig.redactHeaders),b=Xe(b,n.apiConfig.redactBodyFields),k=Xe(k,n.apiConfig.redactBodyFields);let T={id:i,timestamp:d,method:l,url:wr(f.url()),requestHeaders:v,requestBody:b,responseStatusCode:f.status(),responseStatusText:f.statusText(),responseHeaders:y,responseBody:k,requestCookies:A.length?A:null,responseCookies:E.length?E:null,responseTimeMs:Math.round((p-u)*100)/100,isBinary:h,error:null};n.apiCallWriter&&(n.apiCallWriter.append(T),n.apiCallCount++,n.tallyStatus(T.responseStatusCode));}catch{}try{f[yr]=i,n.tagResponseMethods(f,i);}catch{}return f}}};Ze.DEFAULT_API_CONFIG=Object.freeze({trackApiCalls:true,captureRequestHeaders:true,captureResponseHeaders:true,captureRequestBody:true,captureResponseBody:true,captureAssertions:true,redactHeaders:["authorization","cookie","set-cookie","x-api-key"],redactBodyFields:["password","secret","token","apiKey","api_key"],apiIncludeUrls:[],apiExcludeUrls:[]});var de=Ze;var Dd=[".png",".jpg",".jpeg"];function Fd(t){let e=t.toLowerCase();return Dd.some(r=>e.endsWith(r))?t:`${t}.png`}function Bd(){try{return test.info()}catch{return null}}function Od(t,e){for(let r=e;r<t.length;r++){let n=/^(.+)-(expected|actual|diff|previous)\.[A-Za-z0-9]+$/.exec(t[r]?.name??"");if(n)return n[1]}return null}async function qd(t,e){if(!Ne(t))return De("Attaching the baseline of a passing comparison"),null;try{let r=t.snapshotPath(e,{kind:"screenshot"}),n=extname(r)||".png",s=basename(r,n);return await t.attach(`${s}-expected${n}`,{path:r}),s}catch{return null}}async function Hd(t,e){try{await t.attach(qt,{body:Buffer.from(JSON.stringify([e])),contentType:"application/json"});}catch{}}async function Cr(t,e,r={}){let n={...r};delete n.tags;let s=Fd(e),a=Bd(),o=a?.attachments.length??0,i=true,d="";try{await expect(t).toHaveScreenshot(s,n);}catch(l){i=false,d=l instanceof Error?l.message:String(l);}if(a){let l=i?await qd(a,s):Od(a.attachments,o);if(l){let c=!i&&!Ee(d)&&!a.attachments.some((u,f)=>f>=o&&u.name.startsWith(`${l}-diff.`));await Hd(a,{stem:l,name:e,status:i?"passed":c?"new":"failed"});}}return {pass:i,name:"toMatchVisualBaseline",message:()=>i?`Expected "${e}" to differ from its visual baseline, but it matched.`:d}}expect.extend({toMatchVisualBaseline:Cr});function jd(){try{return test.info()}catch{return null}}On(Ye(expect),jd);var Vd="__testrelic_api_config",$d="__testrelic_config_trackApiCalls";function Tr(t){let e=t.annotations.find(n=>n.type===Vd&&n.description!==void 0);if(e)try{return JSON.parse(e.description,Wd)}catch{}let r=t.annotations.find(n=>n.type===$d&&n.description!==void 0);return r?{trackApiCalls:r.description!=="false",captureRequestHeaders:true,captureResponseHeaders:true,captureRequestBody:true,captureResponseBody:true,captureAssertions:true,redactHeaders:["authorization","cookie","set-cookie","x-api-key"],redactBodyFields:["password","secret","token","apiKey","api_key"],apiIncludeUrls:[],apiExcludeUrls:[]}:{trackApiCalls:true,captureRequestHeaders:true,captureResponseHeaders:true,captureRequestBody:true,captureResponseBody:true,captureAssertions:true,redactHeaders:["authorization","cookie","set-cookie","x-api-key"],redactBodyFields:["password","secret","token","apiKey","api_key"],apiIncludeUrls:[],apiExcludeUrls:[]}}function Wd(t,e){if(typeof e=="object"&&e!==null&&e.__regexp===true&&typeof e.source=="string"){let{source:r,flags:n}=e;return new RegExp(r,n)}return e}var na={page:async({page:t},e,r)=>{let n=new Je(t);try{await n.init();}catch{}await e(t);try{let{navigations:s,networkRequestsFile:a,networkRequestsCount:o,consoleLogsFile:i,consoleLogsCount:d}=await n.getFileData(),l={testRelicData:!0,version:PAYLOAD_VERSION,navigations:s,apiAssertions:[],networkRequestsFile:a,networkRequestsCount:o,consoleLogsFile:i,consoleLogsCount:d,apiCallsFile:null,apiCallsCount:0};await r.attach(ATTACHMENT_NAME,{body:Buffer.from(JSON.stringify(l)),contentType:ATTACHMENT_CONTENT_TYPE});}catch{}n.dispose();},request:async({request:t},e,r)=>{let n=Tr(r);if(!n.trackApiCalls){await e(t);return}let s=new oe,a=new de(t,s,n);a.intercept(),le(s,a);try{await e(t);}finally{le(null,null);}try{let{apiCallsFile:o,apiCallsCount:i,apiStatusMix:d}=a.getFileData(),l=n.captureAssertions?s.getData():[],c={testRelicData:!0,version:PAYLOAD_VERSION,navigations:[],apiAssertions:l,networkRequestsFile:null,networkRequestsCount:0,consoleLogsFile:null,consoleLogsCount:0,apiCallsFile:o,apiCallsCount:i,apiStatusMix:d};await r.attach(ATTACHMENT_NAME,{body:Buffer.from(JSON.stringify(c)),contentType:ATTACHMENT_CONTENT_TYPE});}catch{}a.dispose(),s.dispose();}};test.extend(na);Ye(expect);var Xd={request:async({request:t},e,r)=>{let n=Tr(r);if(!n.trackApiCalls){await e(t);return}let s=new oe,a=new de(t,s,n);a.intercept(),le(s,a);let o=null;try{o=new q("api-console");}catch{}let i=0,d=process.stdout.write,l=process.stderr.write;process.stdout.write=function(c,...u){try{let f=typeof c=="string"?c:Buffer.isBuffer(c)?c.toString("utf-8"):String(c);!f.startsWith("[testrelic]")&&!f.startsWith("\u2139 TestRelic")&&!f.startsWith("\u2713 TestRelic")&&!f.startsWith("\u26A0 TestRelic")&&o&&(o.append({level:"stdout",text:f.replace(/\n$/,""),timestamp:new Date().toISOString(),location:null}),i++);}catch{}return d.apply(process.stdout,[c,...u])},process.stderr.write=function(c,...u){try{let f=typeof c=="string"?c:Buffer.isBuffer(c)?c.toString("utf-8"):String(c);!f.startsWith("[testrelic]")&&!f.startsWith("\u26A0 TestRelic")&&o&&(o.append({level:"stderr",text:f.replace(/\n$/,""),timestamp:new Date().toISOString(),location:null}),i++);}catch{}return l.apply(process.stderr,[c,...u])};try{await e(t);}finally{process.stdout.write=d,process.stderr.write=l,le(null,null);}try{o?.close();let{apiCallsFile:c,apiCallsCount:u,apiStatusMix:f}=a.getFileData(),p=n.captureAssertions?s.getData():[],g={testRelicData:!0,version:PAYLOAD_VERSION,navigations:[],apiAssertions:p,networkRequestsFile:null,networkRequestsCount:0,consoleLogsFile:i>0?o?.getPath()??null:null,consoleLogsCount:i,apiCallsFile:c,apiCallsCount:u,apiStatusMix:f};await r.attach(ATTACHMENT_NAME,{body:Buffer.from(JSON.stringify(g)),contentType:ATTACHMENT_CONTENT_TYPE});}catch{}a.dispose(),s.dispose();}};var sa="@testrelic/playwright-analytics";function Qd(t){return Array.isArray(t)?t.some(e=>Array.isArray(e)&&typeof e[0]=="string"&&e[0]===sa):false}function ec(t,e){let n={video:"on",screenshot:"on",trace:"on",...t.use??{}},s;Array.isArray(t.reporter)?s=t.reporter:typeof t.reporter=="string"?s=[[t.reporter]]:s=[["list"]];let a=Qd(s)?s:[...s,[sa,e??{}]];return defineConfig({...t,use:n,reporter:a})}var aa=false;function kg(t,e,r="manual_record"){if(!t||!t.annotations){aa||(aa=true,process.stderr.write(`[testrelic] recordNavigation: reporter not active, navigation not recorded
|
|
4322
|
+
`));return}let n={url:e,navigationType:r,timestamp:new Date().toISOString()};t.annotations.push({type:"lambdatest-navigation",description:JSON.stringify(n)});}export{Ce as SCHEMA_VERSION,Ke as default,ec as defineConfig,kg as recordNavigation,Xd as testRelicApiFixture,na as testRelicFixture};//# sourceMappingURL=index.js.map
|
|
2617
4323
|
//# sourceMappingURL=index.js.map
|