@testrelic/playwright-analytics 2.15.0 → 2.16.0-next.124
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 +1839 -187
- 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 +1839 -187
- package/dist/index.js.map +1 -1
- package/dist/reporter-entry.cjs +1836 -186
- package/dist/reporter-entry.cjs.map +1 -1
- package/dist/reporter-entry.d.cts +9 -0
- package/dist/reporter-entry.d.ts +9 -0
- package/dist/reporter-entry.js +1836 -186
- 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;
|
|
@@ -567,15 +567,15 @@ body{
|
|
|
567
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}
|
|
568
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}
|
|
569
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}
|
|
570
|
-
`;var
|
|
570
|
+
`;var st=`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 196 247" fill="none">
|
|
571
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)"/>
|
|
572
572
|
<defs><linearGradient id="paint0_linear_55_11" x1="98" y1="0.000244141" x2="98" y2="247" gradientUnits="userSpaceOnUse">
|
|
573
573
|
<stop stop-color="#03B79C"/><stop offset="0.504808" stop-color="#4EDAA4"/><stop offset="0.865285" stop-color="#84F3AA"/>
|
|
574
|
-
</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">
|
|
575
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)"/>
|
|
576
576
|
<defs><linearGradient id="paint0_linear_55_11" x1="98" y1="0.000244141" x2="98" y2="247" gradientUnits="userSpaceOnUse">
|
|
577
577
|
<stop stop-color="#03B79C"/><stop offset="0.504808" stop-color="#4EDAA4"/><stop offset="0.865285" stop-color="#84F3AA"/>
|
|
578
|
-
</linearGradient></defs></svg>`;var
|
|
578
|
+
</linearGradient></defs></svg>`;var it=`
|
|
579
579
|
/* \u2500\u2500 Utilities \u2500\u2500 */
|
|
580
580
|
function esc(s){if(!s)return '';return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"').replace(/'/g,''')}
|
|
581
581
|
function stripAnsi(s){return s?s.replace(/\\u001b\\[[0-9;]*m/g,'').replace(/\\x1b\\[[0-9;]*m/g,''):''}
|
|
@@ -597,11 +597,25 @@ body{
|
|
|
597
597
|
tags:t.tags,failure:t.failure,filePath:t.filePath,suiteName:t.suiteName,
|
|
598
598
|
testType:t.testType,isFlaky:t.isFlaky,retryStatus:t.retryStatus,
|
|
599
599
|
expectedStatus:t.expectedStatus,actualStatus:t.actualStatus,
|
|
600
|
-
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||[]
|
|
601
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
|
+
}
|
|
602
611
|
}
|
|
603
612
|
if(entry.type==='api_call'){
|
|
604
|
-
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({
|
|
605
619
|
callId:entry.callId,method:entry.method,url:entry.url,
|
|
606
620
|
responseTime:entry.responseTime,timestamp:entry.timestamp,
|
|
607
621
|
request:entry.request,response:entry.response,assertions:entry.assertions
|
|
@@ -649,7 +663,7 @@ body{
|
|
|
649
663
|
var st={
|
|
650
664
|
testId:ix2.id,title:ix2.title,status:ix2.status,duration:ix2.duration,
|
|
651
665
|
filePath:ix2.filePath,tags:ix2.tags||[],isFlaky:ix2.status==='flaky',
|
|
652
|
-
testType:'e2e',retryCount:grp.attempts.length,suiteName:'',
|
|
666
|
+
testType:ix2.testType||'e2e',retryCount:grp.attempts.length,suiteName:'',
|
|
653
667
|
startedAt:'',completedAt:'',
|
|
654
668
|
networkRequests:[],steps:[],apiCalls:[],actions:[],consoleLogs:[],
|
|
655
669
|
artifacts:null,failure:null,retryStatus:null,expectedStatus:null,actualStatus:null,
|
|
@@ -657,6 +671,18 @@ body{
|
|
|
657
671
|
hasActionSteps:ix2.hasActionSteps,hasArtifacts:ix2.hasArtifacts,
|
|
658
672
|
errorMessage:ix2.errorMessage,networkCount:ix2.networkCount||0,
|
|
659
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,
|
|
660
686
|
retryAttempts:grp.attempts.slice().sort(function(a,b){return a.retryIndex-b.retryIndex;}).map(function(a){
|
|
661
687
|
return {testId:a.id,retryIndex:a.retryIndex,status:a.status,duration:a.duration,errorMessage:a.errorMessage};
|
|
662
688
|
})
|
|
@@ -709,9 +735,7 @@ body{
|
|
|
709
735
|
var summaryEl=document.getElementById('summary-strip');
|
|
710
736
|
var filterEl=document.getElementById('filter-bar');
|
|
711
737
|
var testGridEl=document.getElementById('test-grid');
|
|
712
|
-
var drawerEl=document.getElementById('drawer');
|
|
713
738
|
var drawerBodyEl=document.getElementById('drawer-body');
|
|
714
|
-
var drawerBackdropEl=document.getElementById('drawer-backdrop');
|
|
715
739
|
|
|
716
740
|
/* \u2500\u2500 Render: Run Meta \u2500\u2500 */
|
|
717
741
|
function renderRunMeta(){
|
|
@@ -731,15 +755,81 @@ body{
|
|
|
731
755
|
}
|
|
732
756
|
|
|
733
757
|
/* \u2500\u2500 Render: Summary \u2500\u2500 */
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
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>';
|
|
743
833
|
}
|
|
744
834
|
|
|
745
835
|
/* \u2500\u2500 Virtual Grid: progressive chunked rendering \u2500\u2500 */
|
|
@@ -771,7 +861,7 @@ body{
|
|
|
771
861
|
var t=g.tests[i];
|
|
772
862
|
h+='<div class="test-row" data-testid="'+esc(t.testId)+'">';
|
|
773
863
|
h+='<div class="status-indicator si-'+t.status+'"></div>';
|
|
774
|
-
h+='<div class="test-row-info">';
|
|
864
|
+
h+='<div class="test-row-info tri-inline">';
|
|
775
865
|
h+='<div class="test-row-title" title="'+esc(t.title)+'">'+esc(shortTitle(t.title))+'</div>';
|
|
776
866
|
var hasBadges=(t.testType&&t.testType!=='unknown')||t.isFlaky||(t.tags&&t.tags.length>0);
|
|
777
867
|
if(hasBadges){
|
|
@@ -783,7 +873,9 @@ body{
|
|
|
783
873
|
h+='</div>';
|
|
784
874
|
}
|
|
785
875
|
h+='</div>';
|
|
786
|
-
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>';
|
|
787
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>';
|
|
788
880
|
h+='</div>';
|
|
789
881
|
if(t.retryAttempts&&t.retryAttempts.length>0){
|
|
@@ -844,6 +936,10 @@ body{
|
|
|
844
936
|
function renderTestGrid(){
|
|
845
937
|
if(_vgObserver){_vgObserver.disconnect();_vgObserver=null;}
|
|
846
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;}
|
|
847
943
|
if(filtered.length===0){testGridEl.innerHTML='<div class="no-tests">No tests match your filters</div>';_vgGroups=[];return;}
|
|
848
944
|
_vgGroups=_vgBuildGroups(filtered);
|
|
849
945
|
_vgRenderedTo=0;
|
|
@@ -872,17 +968,51 @@ body{
|
|
|
872
968
|
|
|
873
969
|
function renderArtifactColumn(artifacts){
|
|
874
970
|
if(!artifacts)return '<div class="artifact-empty">No artifacts captured</div>';
|
|
875
|
-
var hasS=!!artifacts.screenshot,hasV=!!artifacts.video;
|
|
876
|
-
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
|
+
|
|
877
978
|
var h='';
|
|
878
|
-
if(
|
|
979
|
+
if(tabs.length>1){
|
|
879
980
|
h+='<div class="seg-ctrl" data-seg-group="artifacts">';
|
|
880
|
-
|
|
881
|
-
|
|
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
|
+
}
|
|
882
989
|
h+='</div>';
|
|
883
990
|
}
|
|
884
|
-
|
|
885
|
-
|
|
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
|
+
}
|
|
886
1016
|
return h;
|
|
887
1017
|
}
|
|
888
1018
|
|
|
@@ -907,11 +1037,9 @@ body{
|
|
|
907
1037
|
h+='</div><div class="detail-duration">'+fmtDur(t.duration)+'</div></div></div>';
|
|
908
1038
|
|
|
909
1039
|
/* \u2500\u2500 Failure panel (full width) \u2500\u2500 */
|
|
910
|
-
if((t.status==='failed'||t.status==='flaky')&&t.failure)h+=renderFailure(t.failure,t.filePath,t.title,t.tags);
|
|
911
|
-
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>';
|
|
912
|
-
|
|
913
1040
|
/* \u2500\u2500 Stacked sections: 1) Artifacts 2) Timeline / Network 3) API Calls \u2500\u2500 */
|
|
914
|
-
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;
|
|
915
1043
|
var hasSteps=t.steps&&t.steps.length>0;
|
|
916
1044
|
var hasApiCalls=t.apiCalls&&t.apiCalls.length>0;
|
|
917
1045
|
var hasActions=t.actions&&t.actions.length>0;
|
|
@@ -922,30 +1050,69 @@ body{
|
|
|
922
1050
|
var baseTime=0;
|
|
923
1051
|
if(hasVideo&&t.startedAt){baseTime=Date.parse(t.startedAt);}
|
|
924
1052
|
if(!baseTime&&hasVideo&&t.consoleLogs&&t.consoleLogs.length>0&&t.consoleLogs[0].timestamp){baseTime=Date.parse(t.consoleLogs[0].timestamp);}
|
|
925
|
-
|
|
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){
|
|
926
1088
|
h+='<div class="drawer-sections">';
|
|
927
|
-
if(
|
|
928
|
-
if(
|
|
929
|
-
if(
|
|
930
|
-
if(
|
|
931
|
-
if(!hasSteps&&
|
|
932
|
-
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>';
|
|
933
1100
|
h+='</div>';
|
|
934
1101
|
}
|
|
935
1102
|
|
|
1103
|
+
h+='</div></div>';
|
|
1104
|
+
|
|
936
1105
|
h+='</div>';
|
|
937
1106
|
drawerBodyEl.innerHTML=h;
|
|
938
|
-
drawerBodyEl.scrollTop=0;
|
|
939
1107
|
/* Wire up video \u2194 action step sync if both are present */
|
|
940
1108
|
if(typeof syncActionsWithVideo==='function')syncActionsWithVideo();
|
|
941
1109
|
}
|
|
942
1110
|
|
|
943
1111
|
/* \u2500\u2500 Drawer open / close \u2500\u2500 */
|
|
944
|
-
|
|
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){
|
|
945
1115
|
if(data.reportMode==='streaming'){
|
|
946
|
-
drawerEl.classList.add('open');
|
|
947
|
-
drawerBackdropEl.classList.add('open');
|
|
948
|
-
document.body.style.overflow='hidden';
|
|
949
1116
|
if(_detailCache[testId]){renderDrawer(testId,_detailCache[testId]);return;}
|
|
950
1117
|
drawerBodyEl.innerHTML='<div class="detail-loading"><div class="loading-spinner"></div><div class="loading-text">Loading test details...</div></div>';
|
|
951
1118
|
if(window.__trStreamingFetchDetail){
|
|
@@ -963,6 +1130,7 @@ body{
|
|
|
963
1130
|
merged.actions=detail.actions||[];
|
|
964
1131
|
merged.apiAssertions=detail.apiAssertions||[];
|
|
965
1132
|
merged.artifacts=detail.artifacts||null;
|
|
1133
|
+
merged.visual=detail.visual||[];
|
|
966
1134
|
merged.failure=detail.failureDiagnostic||detail.failure||null;
|
|
967
1135
|
merged.steps=detail.navigations||[];
|
|
968
1136
|
if(detail.startedAt)merged.startedAt=detail.startedAt;
|
|
@@ -977,7 +1145,7 @@ body{
|
|
|
977
1145
|
merged.apiCalls=[];
|
|
978
1146
|
cacheDetail(testId,merged);
|
|
979
1147
|
renderDrawer(testId,merged);
|
|
980
|
-
/* Lazy-load heavy data
|
|
1148
|
+
/* Lazy-load heavy data once the page is showing */
|
|
981
1149
|
loadHeavyData(testId,merged);
|
|
982
1150
|
});
|
|
983
1151
|
}else{
|
|
@@ -988,9 +1156,6 @@ body{
|
|
|
988
1156
|
return;
|
|
989
1157
|
}
|
|
990
1158
|
renderDrawer(testId);
|
|
991
|
-
drawerEl.classList.add('open');
|
|
992
|
-
drawerBackdropEl.classList.add('open');
|
|
993
|
-
document.body.style.overflow='hidden';
|
|
994
1159
|
}
|
|
995
1160
|
|
|
996
1161
|
/* Lazy-load heavy data (network, console, API calls) after drawer renders */
|
|
@@ -1039,14 +1204,6 @@ body{
|
|
|
1039
1204
|
});
|
|
1040
1205
|
}
|
|
1041
1206
|
}
|
|
1042
|
-
function closeDrawer(){
|
|
1043
|
-
drawerEl.classList.remove('open');
|
|
1044
|
-
drawerBackdropEl.classList.remove('open');
|
|
1045
|
-
document.body.style.overflow='';
|
|
1046
|
-
/* Pause any playing video */
|
|
1047
|
-
var v=drawerBodyEl.querySelector('video');
|
|
1048
|
-
if(v)v.pause();
|
|
1049
|
-
}
|
|
1050
1207
|
|
|
1051
1208
|
/* \u2500\u2500 Search (debounced) \u2500\u2500 */
|
|
1052
1209
|
var _searchTimer=0;
|
|
@@ -1054,7 +1211,7 @@ body{
|
|
|
1054
1211
|
clearTimeout(_searchTimer);
|
|
1055
1212
|
_searchTimer=setTimeout(function(){searchQuery=q;applyFilters();},150);
|
|
1056
1213
|
}
|
|
1057
|
-
`;var
|
|
1214
|
+
`;var ot=`
|
|
1058
1215
|
/* \u2500\u2500 Network Visualization Helpers \u2500\u2500 */
|
|
1059
1216
|
var RES_COLORS={xhr:'#60a5fa',document:'#818cf8',script:'#fbbf24',stylesheet:'#a78bfa',image:'#34d399',font:'#f472b6',other:'#6b7280'};
|
|
1060
1217
|
var RES_LABELS={xhr:'XHR',document:'Doc',script:'JS',stylesheet:'CSS',image:'Img',font:'Font',other:'Other'};
|
|
@@ -1518,7 +1675,20 @@ body{
|
|
|
1518
1675
|
var sid='s-'+Math.random().toString(36).substr(2,8);
|
|
1519
1676
|
var cpid='cp-'+Math.random().toString(36).substr(2,8);
|
|
1520
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>';
|
|
1521
|
-
|
|
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
|
+
}
|
|
1522
1692
|
if(failure.code)h+=renderCodeSnippet(failure.code);
|
|
1523
1693
|
if(failure.line!==null&&failure.line!==undefined)h+='<div class="line-marker">Line '+failure.line+'</div>';
|
|
1524
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>';}
|
|
@@ -1579,8 +1749,44 @@ body{
|
|
|
1579
1749
|
/* \u2500\u2500 API Calls Column (for API-only tests) \u2500\u2500 */
|
|
1580
1750
|
var API_METHOD_COLORS={GET:'#60a5fa',POST:'#34d399',PUT:'#fbbf24',PATCH:'#a78bfa',DELETE:'#f87171',HEAD:'#6b7280',OPTIONS:'#6b7280'};
|
|
1581
1751
|
|
|
1582
|
-
|
|
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){
|
|
1583
1788
|
var did='ndt-d-api-'+idx;
|
|
1789
|
+
callAsserts=callAsserts||[];
|
|
1584
1790
|
var reqHeaders=ac.requestHeaders||null;
|
|
1585
1791
|
var reqBody=ac.requestBody||null;
|
|
1586
1792
|
var resHeaders=ac.responseHeaders||null;
|
|
@@ -1596,6 +1802,9 @@ body{
|
|
|
1596
1802
|
h+='<button class="ndt-detail-tab" data-ndt-tab="req-body" data-ndt-target="'+did+'">Payload</button>';
|
|
1597
1803
|
h+='<button class="ndt-detail-tab" data-ndt-tab="res-headers" data-ndt-target="'+did+'">Response Headers</button>';
|
|
1598
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>';
|
|
1599
1808
|
h+='</div>';
|
|
1600
1809
|
|
|
1601
1810
|
/* General tab */
|
|
@@ -1604,21 +1813,61 @@ body{
|
|
|
1604
1813
|
h+='<span class="ndt-general-k">Method</span><span class="ndt-general-v">'+esc(ac.method)+'</span>';
|
|
1605
1814
|
var sc=ac.responseStatusCode!=null?ac.responseStatusCode:0;
|
|
1606
1815
|
var st=ac.responseStatusText||'';
|
|
1607
|
-
|
|
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
|
+
}
|
|
1608
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>';
|
|
1609
1825
|
if(ac.timestamp)h+='<span class="ndt-general-k">Timestamp</span><span class="ndt-general-v">'+fmtDate(ac.timestamp)+'</span>';
|
|
1610
1826
|
h+='</div></div>';
|
|
1611
1827
|
|
|
1612
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>';
|
|
1613
1829
|
h+='<div class="ndt-detail-pane" data-ndt-pane="req-body">'+renderNdtBody(reqBodyStr,false,false,ct,'No request body')+'</div>';
|
|
1614
1830
|
h+='<div class="ndt-detail-pane" data-ndt-pane="res-headers">'+renderNdtHeaders(resHeaders,'response')+'</div>';
|
|
1615
|
-
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
|
+
}
|
|
1616
1841
|
h+='</div>';
|
|
1617
1842
|
return h;
|
|
1618
1843
|
}
|
|
1619
1844
|
|
|
1620
|
-
|
|
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){
|
|
1621
1869
|
if(!apiCalls||apiCalls.length===0)return '<div class="artifact-empty">No API calls captured</div>';
|
|
1870
|
+
assertions=assertions||[];
|
|
1622
1871
|
var uid='api-'+Math.random().toString(36).substr(2,6);
|
|
1623
1872
|
var h='<div class="ndt" id="'+uid+'">';
|
|
1624
1873
|
|
|
@@ -1640,16 +1889,28 @@ body{
|
|
|
1640
1889
|
for(var i=0;i<apiCalls.length;i++){
|
|
1641
1890
|
var ac=apiCalls[i];
|
|
1642
1891
|
var sc=ac.responseStatusCode!=null?ac.responseStatusCode:0;
|
|
1643
|
-
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++;
|
|
1644
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())+'">';
|
|
1645
|
-
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>';
|
|
1646
1898
|
h+='<span class="ndt-method" style="color:'+(API_METHOD_COLORS[ac.method]||'#6b7280')+'">'+esc(ac.method)+'</span>';
|
|
1647
1899
|
h+='<span class="ndt-url" title="'+esc(ac.url)+'">'+esc(urlPath(ac.url))+'</span>';
|
|
1648
|
-
|
|
1649
|
-
|
|
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>';
|
|
1650
1911
|
h+='<span class="ndt-time">'+(ac.responseTimeMs!=null?fmtDur(ac.responseTimeMs):'')+'</span>';
|
|
1651
1912
|
h+='</div>';
|
|
1652
|
-
h+=renderApiCallDetail(ac,i);
|
|
1913
|
+
h+=renderApiCallDetail(ac,i,callAsserts);
|
|
1653
1914
|
}
|
|
1654
1915
|
h+='</div></div>';
|
|
1655
1916
|
return h;
|
|
@@ -1685,8 +1946,44 @@ body{
|
|
|
1685
1946
|
var countEl=ndt.querySelector('.ndt-count');
|
|
1686
1947
|
if(countEl)countEl.textContent=visible+' / '+rows.length+' requests';
|
|
1687
1948
|
}
|
|
1688
|
-
`;var
|
|
1689
|
-
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
|
+
}
|
|
1690
1987
|
var _totalTests=0;
|
|
1691
1988
|
var _originalSummary=null;
|
|
1692
1989
|
|
|
@@ -1695,18 +1992,22 @@ function renderFilterBar(){
|
|
|
1695
1992
|
var drawerBodyEl=document.getElementById('filter-drawer-body');
|
|
1696
1993
|
var statusCounts={passed:0,failed:0,flaky:0,skipped:0,timedout:0};
|
|
1697
1994
|
var typeCounts={},specCounts={};
|
|
1995
|
+
var visualCounts={has:0,failed:0};
|
|
1698
1996
|
for(var i=0;i<tests.length;i++){
|
|
1699
1997
|
var t=tests[i];
|
|
1700
1998
|
if(statusCounts[t.status]!==undefined)statusCounts[t.status]++;
|
|
1701
1999
|
var tp=t.testType||'unknown';
|
|
1702
2000
|
typeCounts[tp]=(typeCounts[tp]||0)+1;
|
|
1703
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++;}
|
|
1704
2003
|
}
|
|
1705
2004
|
var hasStatus=Object.keys(_filterState.status).length>0;
|
|
1706
2005
|
var hasType=Object.keys(_filterState.type).length>0;
|
|
1707
2006
|
var hasSpec=Object.keys(_filterState.specFile).length>0;
|
|
1708
|
-
var
|
|
1709
|
-
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;
|
|
1710
2011
|
|
|
1711
2012
|
/* \u2500\u2500 Filter Bar (search + status chips + filter icon) \u2500\u2500 */
|
|
1712
2013
|
var h='';
|
|
@@ -1763,6 +2064,35 @@ function renderFilterBar(){
|
|
|
1763
2064
|
d+='</div></div>';
|
|
1764
2065
|
}
|
|
1765
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
|
+
|
|
1766
2096
|
/* Actions (Clear all + indicator) */
|
|
1767
2097
|
d+='<div class="filter-drawer-actions">';
|
|
1768
2098
|
d+='<button class="filter-clear" id="filter-clear" style="display:'+(hasAny?'inline-block':'none')+'">Clear all</button>';
|
|
@@ -1774,7 +2104,7 @@ function renderFilterBar(){
|
|
|
1774
2104
|
}
|
|
1775
2105
|
|
|
1776
2106
|
function initFilters(){
|
|
1777
|
-
_filterState={status:{},type:{},specFile:{}};
|
|
2107
|
+
_filterState={status:{},type:{},specFile:{},visual:{},evidence:{}};
|
|
1778
2108
|
_totalTests=tests.length;
|
|
1779
2109
|
_originalSummary={total:data.summary.total,passed:data.summary.passed,
|
|
1780
2110
|
failed:data.summary.failed,flaky:data.summary.flaky,
|
|
@@ -1785,12 +2115,27 @@ function getFilteredTests(){
|
|
|
1785
2115
|
var hasStatus=Object.keys(_filterState.status).length>0;
|
|
1786
2116
|
var hasType=Object.keys(_filterState.type).length>0;
|
|
1787
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;
|
|
1788
2120
|
var out=[];
|
|
1789
2121
|
for(var i=0;i<tests.length;i++){
|
|
1790
2122
|
var t=tests[i];
|
|
1791
2123
|
if(hasStatus&&!_filterState.status[t.status])continue;
|
|
1792
2124
|
if(hasType&&!_filterState.type[t.testType||'unknown'])continue;
|
|
1793
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
|
+
}
|
|
1794
2139
|
if(searchQuery){
|
|
1795
2140
|
var q=searchQuery.toLowerCase();
|
|
1796
2141
|
var match=t.title.toLowerCase().indexOf(q)>=0||t.filePath.toLowerCase().indexOf(q)>=0||(t.suiteName&&t.suiteName.toLowerCase().indexOf(q)>=0);
|
|
@@ -1813,7 +2158,7 @@ function toggleFilter(dim,val){
|
|
|
1813
2158
|
}
|
|
1814
2159
|
|
|
1815
2160
|
function clearAllFilters(){
|
|
1816
|
-
_filterState={
|
|
2161
|
+
for(var dim in _filterState){if(_filterState.hasOwnProperty(dim))_filterState[dim]={};}
|
|
1817
2162
|
searchQuery='';
|
|
1818
2163
|
var si=document.getElementById('search-input');
|
|
1819
2164
|
if(si)si.value='';
|
|
@@ -1856,14 +2201,14 @@ function _updateCard(cls,val){
|
|
|
1856
2201
|
var el=document.querySelector('.'+cls+' .s-count');
|
|
1857
2202
|
if(el)el.textContent=''+val;
|
|
1858
2203
|
}
|
|
1859
|
-
`;var
|
|
2204
|
+
`;var dt=`
|
|
1860
2205
|
/* \u2500\u2500 Event Delegation \u2500\u2500 */
|
|
1861
2206
|
document.addEventListener('click',function(e){
|
|
1862
2207
|
var target=e.target;
|
|
1863
2208
|
|
|
1864
2209
|
/* Test row click \u2192 open drawer */
|
|
1865
2210
|
var row=target.closest('.test-row');
|
|
1866
|
-
if(row){
|
|
2211
|
+
if(row){openTest(row.getAttribute('data-testid'));return;}
|
|
1867
2212
|
|
|
1868
2213
|
/* Filter chip (multi-dimensional) */
|
|
1869
2214
|
var chip=target.closest('.filter-chip');
|
|
@@ -2011,8 +2356,9 @@ function _updateCard(cls,val){
|
|
|
2011
2356
|
if(themeBtn){setTheme(themeBtn.getAttribute('data-theme-val'));return;}
|
|
2012
2357
|
|
|
2013
2358
|
/* Drawer close */
|
|
2014
|
-
if(target.closest('#
|
|
2015
|
-
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;}
|
|
2016
2362
|
});
|
|
2017
2363
|
|
|
2018
2364
|
/* Search input \u2014 main test search + network filter search */
|
|
@@ -2030,7 +2376,7 @@ function _updateCard(cls,val){
|
|
|
2030
2376
|
|
|
2031
2377
|
/* Keyboard */
|
|
2032
2378
|
document.addEventListener('keydown',function(e){
|
|
2033
|
-
if(e.key==='Escape'){closeLightbox();
|
|
2379
|
+
if(e.key==='Escape'){closeLightbox();closeTest();closeFilterDrawer();}
|
|
2034
2380
|
});
|
|
2035
2381
|
|
|
2036
2382
|
/* \u2500\u2500 Theme Management \u2500\u2500 */
|
|
@@ -2126,7 +2472,7 @@ function _updateCard(cls,val){
|
|
|
2126
2472
|
var d=document.getElementById('filter-drawer');if(d)d.classList.remove('open');
|
|
2127
2473
|
var b=document.getElementById('filter-drawer-backdrop');if(b)b.classList.remove('open');
|
|
2128
2474
|
}
|
|
2129
|
-
`;var
|
|
2475
|
+
`;var ct=`
|
|
2130
2476
|
/* \u2500\u2500 Category badge helpers \u2500\u2500 */
|
|
2131
2477
|
function actionBadgeClass(cat){
|
|
2132
2478
|
if(cat==='assertion')return 'action-badge-assertion';
|
|
@@ -2172,7 +2518,7 @@ function _updateCard(cls,val){
|
|
|
2172
2518
|
h+='</div>';
|
|
2173
2519
|
return h;
|
|
2174
2520
|
}
|
|
2175
|
-
`;var
|
|
2521
|
+
`;var ut=`
|
|
2176
2522
|
/* \u2500\u2500 Console Log Level Colors & Labels \u2500\u2500 */
|
|
2177
2523
|
var LOG_COLORS={log:'#9ca3af',warn:'#fbbf24',error:'#f87171',info:'#60a5fa',debug:'#a78bfa',stdout:'#9ca3af',stderr:'#f87171'};
|
|
2178
2524
|
var LOG_LABELS={log:'LOG',warn:'WARN',error:'ERR',info:'INFO',debug:'DBG',stdout:'OUT',stderr:'ERR'};
|
|
@@ -2359,7 +2705,7 @@ function _updateCard(cls,val){
|
|
|
2359
2705
|
}
|
|
2360
2706
|
return h;
|
|
2361
2707
|
}
|
|
2362
|
-
`;var
|
|
2708
|
+
`;var pt=`
|
|
2363
2709
|
/* \u2500\u2500 Artifact Utilities \u2500\u2500 */
|
|
2364
2710
|
function formatBytes(bytes){
|
|
2365
2711
|
if(bytes===0)return '0 B';
|
|
@@ -2368,41 +2714,1334 @@ function _updateCard(cls,val){
|
|
|
2368
2714
|
if(i>=sizes.length)i=sizes.length-1;
|
|
2369
2715
|
return parseFloat((bytes/Math.pow(k,i)).toFixed(1))+' '+sizes[i];
|
|
2370
2716
|
}
|
|
2371
|
-
`;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=`
|
|
2372
4003
|
(function(){
|
|
2373
4004
|
var data=JSON.parse(document.getElementById('report-data').textContent);
|
|
2374
|
-
${
|
|
2375
|
-
${
|
|
2376
|
-
${
|
|
2377
|
-
${
|
|
2378
|
-
${
|
|
2379
|
-
${
|
|
2380
|
-
${
|
|
4005
|
+
${it}
|
|
4006
|
+
${ct}
|
|
4007
|
+
${ut}
|
|
4008
|
+
${ot}
|
|
4009
|
+
${lt}
|
|
4010
|
+
${pt}
|
|
4011
|
+
${gt}
|
|
4012
|
+
${ft}
|
|
4013
|
+
${dt}
|
|
4014
|
+
${ht}
|
|
4015
|
+
${vt}
|
|
4016
|
+
${mt}
|
|
2381
4017
|
applyTheme();
|
|
2382
4018
|
renderRunMeta();
|
|
2383
4019
|
renderSummary();
|
|
4020
|
+
renderTypeBand();
|
|
2384
4021
|
initFilters();
|
|
2385
4022
|
renderFilterBar();
|
|
2386
4023
|
renderTestGrid();
|
|
4024
|
+
renderNeedsAttention();
|
|
4025
|
+
routeFromHash();
|
|
2387
4026
|
var _lo=document.getElementById('loading-overlay');if(_lo)_lo.remove();
|
|
2388
|
-
})();`;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 -->
|
|
2389
4028
|
<!DOCTYPE html>
|
|
2390
4029
|
<html lang="en" data-theme="">
|
|
2391
4030
|
<head>
|
|
2392
4031
|
<meta charset="UTF-8">
|
|
2393
4032
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
2394
|
-
<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:*;">
|
|
2395
4034
|
<title>TestRelic AI Analytics Report</title>
|
|
2396
4035
|
${""}
|
|
2397
|
-
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml;base64,${Buffer.from(
|
|
2398
|
-
<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>
|
|
2399
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>
|
|
2400
4039
|
</head>
|
|
2401
4040
|
<body>
|
|
2402
4041
|
<div class="wrap">
|
|
2403
4042
|
<div class="top-bar">
|
|
2404
4043
|
<div class="brand">
|
|
2405
|
-
${
|
|
4044
|
+
${at}
|
|
2406
4045
|
<div class="brand-text">
|
|
2407
4046
|
<span class="brand-title">TestRelic AI</span>
|
|
2408
4047
|
<span class="brand-sub">Analytics Report</span>
|
|
@@ -2416,19 +4055,19 @@ ${""}
|
|
|
2416
4055
|
</div>
|
|
2417
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>
|
|
2418
4057
|
</div>
|
|
2419
|
-
<div id="
|
|
2420
|
-
|
|
2421
|
-
|
|
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>
|
|
2422
4069
|
</div>
|
|
2423
4070
|
<div class="loading-overlay" id="loading-overlay"><div class="loading-spinner"></div><div class="loading-text">Loading report...</div></div>
|
|
2424
|
-
<div class="drawer-backdrop" id="drawer-backdrop"></div>
|
|
2425
|
-
<aside class="drawer" id="drawer">
|
|
2426
|
-
<div class="drawer-bar">
|
|
2427
|
-
<span class="drawer-bar-title">Test Details</span>
|
|
2428
|
-
<button class="drawer-close" id="drawer-close">×</button>
|
|
2429
|
-
</div>
|
|
2430
|
-
<div class="drawer-body" id="drawer-body"></div>
|
|
2431
|
-
</aside>
|
|
2432
4071
|
<div class="filter-drawer-backdrop" id="filter-drawer-backdrop"></div>
|
|
2433
4072
|
<aside class="filter-drawer" id="filter-drawer">
|
|
2434
4073
|
<div class="filter-drawer-bar">
|
|
@@ -2439,9 +4078,9 @@ ${""}
|
|
|
2439
4078
|
</aside>
|
|
2440
4079
|
<script id="report-data" type="application/json">${n}</script>
|
|
2441
4080
|
${s?`<script id="artifact-manifest-data" type="application/json">${s}</script>`:""}
|
|
2442
|
-
<script>${
|
|
4081
|
+
<script>${La}</script>
|
|
2443
4082
|
</body>
|
|
2444
|
-
</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=`{
|
|
2445
4084
|
"schemaVersion":"2.0",
|
|
2446
4085
|
"reportMode":"streaming",
|
|
2447
4086
|
"serverPort":${r??"null"},
|
|
@@ -2455,7 +4094,7 @@ ${s?`<script id="artifact-manifest-data" type="application/json">${s}</script>`:
|
|
|
2455
4094
|
"ci":${i.ci?JSON.stringify(i.ci).replace(/<\//g,"<\\/"):"null"},
|
|
2456
4095
|
"metadata":${i.metadata?JSON.stringify(i.metadata).replace(/<\//g,"<\\/"):"null"},
|
|
2457
4096
|
"shardRunIds":${i.shardRunIds?JSON.stringify(i.shardRunIds).replace(/<\//g,"<\\/"):"null"}
|
|
2458
|
-
}`,
|
|
4097
|
+
}`,l=`
|
|
2459
4098
|
(function(){
|
|
2460
4099
|
var data=JSON.parse(document.getElementById('report-data').textContent);
|
|
2461
4100
|
var _serverPort=data.serverPort||null;
|
|
@@ -2525,20 +4164,30 @@ ${s?`<script id="artifact-manifest-data" type="application/json">${s}</script>`:
|
|
|
2525
4164
|
window.__trServerPort=_serverPort;
|
|
2526
4165
|
window.__trIsStreaming=true;
|
|
2527
4166
|
|
|
2528
|
-
${
|
|
2529
|
-
${
|
|
2530
|
-
${
|
|
2531
|
-
${
|
|
2532
|
-
${
|
|
2533
|
-
${
|
|
2534
|
-
${
|
|
4167
|
+
${it}
|
|
4168
|
+
${ct}
|
|
4169
|
+
${ut}
|
|
4170
|
+
${ot}
|
|
4171
|
+
${lt}
|
|
4172
|
+
${pt}
|
|
4173
|
+
${gt}
|
|
4174
|
+
${ft}
|
|
4175
|
+
${dt}
|
|
4176
|
+
${ht}
|
|
4177
|
+
${vt}
|
|
4178
|
+
${mt}
|
|
2535
4179
|
applyTheme();
|
|
2536
4180
|
renderRunMeta();
|
|
2537
4181
|
renderSummary();
|
|
4182
|
+
renderTypeBand();
|
|
2538
4183
|
initFilters();
|
|
2539
4184
|
renderFilterBar();
|
|
2540
4185
|
renderTestGrid();
|
|
2541
|
-
|
|
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();});
|
|
2542
4191
|
var _lo=document.getElementById('loading-overlay');if(_lo)_lo.remove();
|
|
2543
4192
|
})();`;return `<!-- TestRelic AI Analytics Report \u2014 streaming mode -->
|
|
2544
4193
|
<!DOCTYPE html>
|
|
@@ -2546,19 +4195,19 @@ ${s?`<script id="artifact-manifest-data" type="application/json">${s}</script>`:
|
|
|
2546
4195
|
<head>
|
|
2547
4196
|
<meta charset="UTF-8">
|
|
2548
4197
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
2549
|
-
<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:*;">
|
|
2550
4199
|
<title>TestRelic AI Analytics Report</title>
|
|
2551
4200
|
${r?`<meta name="report-server-port" content="${r}">`:""}
|
|
2552
4201
|
${r?`<meta name="artifact-server-port" content="${r}">`:""}
|
|
2553
|
-
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml;base64,${Buffer.from(
|
|
2554
|
-
<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>
|
|
2555
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>
|
|
2556
4205
|
</head>
|
|
2557
4206
|
<body>
|
|
2558
4207
|
<div class="wrap">
|
|
2559
4208
|
<div class="top-bar">
|
|
2560
4209
|
<div class="brand">
|
|
2561
|
-
${
|
|
4210
|
+
${at}
|
|
2562
4211
|
<div class="brand-text">
|
|
2563
4212
|
<span class="brand-title">TestRelic AI</span>
|
|
2564
4213
|
<span class="brand-sub">Analytics Report</span>
|
|
@@ -2572,19 +4221,19 @@ ${r?`<meta name="artifact-server-port" content="${r}">`:""}
|
|
|
2572
4221
|
</div>
|
|
2573
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>
|
|
2574
4223
|
</div>
|
|
2575
|
-
<div id="
|
|
2576
|
-
|
|
2577
|
-
|
|
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>
|
|
2578
4235
|
</div>
|
|
2579
4236
|
<div class="loading-overlay" id="loading-overlay"><div class="loading-spinner"></div><div class="loading-text">Loading report...</div></div>
|
|
2580
|
-
<div class="drawer-backdrop" id="drawer-backdrop"></div>
|
|
2581
|
-
<aside class="drawer" id="drawer">
|
|
2582
|
-
<div class="drawer-bar">
|
|
2583
|
-
<span class="drawer-bar-title">Test Details</span>
|
|
2584
|
-
<button class="drawer-close" id="drawer-close">×</button>
|
|
2585
|
-
</div>
|
|
2586
|
-
<div class="drawer-body" id="drawer-body"></div>
|
|
2587
|
-
</aside>
|
|
2588
4237
|
<div class="filter-drawer-backdrop" id="filter-drawer-backdrop"></div>
|
|
2589
4238
|
<aside class="filter-drawer" id="filter-drawer">
|
|
2590
4239
|
<div class="filter-drawer-bar">
|
|
@@ -2594,78 +4243,81 @@ ${r?`<meta name="artifact-server-port" content="${r}">`:""}
|
|
|
2594
4243
|
<div class="filter-drawer-body" id="filter-drawer-body"></div>
|
|
2595
4244
|
</aside>
|
|
2596
4245
|
<script id="report-data" type="application/json">${d}</script>
|
|
2597
|
-
${
|
|
2598
|
-
<script>${
|
|
4246
|
+
${o?`<script id="artifact-manifest-data" type="application/json">${o}</script>`:""}
|
|
4247
|
+
<script>${l}</script>
|
|
2599
4248
|
</body>
|
|
2600
|
-
</html>`}function
|
|
2601
|
-
`);});}catch{}}var
|
|
2602
|
-
`;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 fr(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 mr=/^[a-f0-9]{12}$/,Qe=/^\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}(-\d+)?$/,yr=500;function x(t,e,r){t.writeHead(e,{"Content-Type":"application/json"}),t.end(JSON.stringify(r));}function vr(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 br(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 wr(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 xr(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(yr,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,h=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(h){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 g=f.length,m=Math.max(1,Math.ceil(g/i)),y=(l-1)*i,w=f.slice(y,y+i);x(e,200,{tests:w,pagination:{page:l,pageSize:i,totalItems:g,totalPages:m},filters:{status:d,file:o,search:c,tag:p}});}function kr(t,e,r,n){if(!mr.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 Tr(t,e,r,n,s){if(!mr.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(yr,Math.max(1,parseInt(d.get("pageSize")??"50",10)||50)),p,h=join(r,"tests",n,"meta.json");if(existsSync(h))try{let f=JSON.parse(readFileSync(h,"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 fr(l,o,c,p);x(e,200,u);}catch(i){x(e,500,{error:i instanceof Error?i.message:String(i)});}}function Cr(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 Sr(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()||!Qe.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 Rr(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()||!Qe.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 Ar(t,e,r,n){if(!Qe.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 _r(t,e,r){x(e,200,{status:"shutting_down"}),r.close();}function Ze(t,e,r){if(!existsSync(r)){x(e,404,{error:"File not found"});return}try{let n=readFileSync(r),s=extname(r).toLowerCase(),a=Ls[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 Ls={".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 Fs=9323,Ds=10,Bs=1800*1e3;function ae(t,e){return new Promise((r,n)=>{let s=e?.port??Fs,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();},Bs);}let c=e?.htmlPath??null;if(!c){let u=dirname(t);try{let f=readdirSync(u).find(g=>g.endsWith(".html"));f&&(c=join(u,f));}catch{}}let p=createServer((u,f)=>{if(o(),vr(f),u.method==="OPTIONS"){f.writeHead(204),f.end();return}let g;try{g=new URL(u.url??"/",`http://${u.headers.host??"localhost"}`).pathname;}catch{x(f,400,{error:"Invalid URL"});return}if(u.method==="GET"&&(g==="/"||g==="/index.html")){if(c&&existsSync(c)){Ze(u,f,c);return}x(f,404,{error:"HTML report not found"});return}if(u.method==="GET"&&g==="/api/health"){br(u,f,t,a);return}if(u.method==="GET"&&g==="/api/summary"){wr(u,f,t);return}if(u.method==="GET"&&g==="/api/tests"){xr(u,f,t);return}if(u.method==="GET"&&g==="/api/files"){Cr(u,f,t);return}let m=g.match(/^\/api\/tests\/([a-f0-9]+)\/(network|console|api-calls)$/);if(u.method==="GET"&&m){Tr(u,f,t,m[1],m[2]);return}let y=g.match(/^\/api\/tests\/([a-f0-9]+)$/);if(u.method==="GET"&&y){kr(u,f,t,y[1]);return}if(u.method==="GET"&&g==="/api/artifacts"){Sr(u,f,d);return}if(u.method==="DELETE"&&g==="/api/artifacts"){Rr(u,f,d);return}let w=g.match(/^\/api\/artifacts\/(.+)$/);if(u.method==="DELETE"&&w){Ar(u,f,d,decodeURIComponent(w[1]));return}if(u.method==="GET"&&g.startsWith("/artifacts/")){let v=decodeURIComponent(g.slice(11));if(v.includes("..")||v.includes("\0")){x(f,400,{error:"Invalid path"});return}Ze(u,f,join(d,v));return}if(u.method==="POST"&&g==="/api/shutdown"){_r(u,f,p);return}x(f,404,{error:"Not found"});});function h(u){let f=g=>{g.code==="EADDRINUSE"&&i<Ds?(i++,h(u+1)):n(g);};p.once("error",f),p.listen(u,"127.0.0.1",()=>{p.removeListener("error",f);let g=p.address();if(!g||typeof g=="string"){n(new Error("Failed to get server address"));return}o(),r({port:g.port,dispose:()=>new Promise(m=>{clearTimeout(l),p.close(()=>m());})});});}h(s);})}async function Ir(t){let e=await ae(t);return {port:e.port,dispose:e.dispose}}function Us(t,e,r){let n=JSON.stringify(t),s=r?JSON.stringify(r):null;return cr(n,e,s)}async function et(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 g=join(f,"index-compact.json"),m=join(f,"index.json");existsSync(g)?c=readFileSync(g,"utf-8"):existsSync(m)&&(c=readFileSync(m,"utf-8"));}catch{}p=r?JSON.stringify(r):null,l=Je(o,c,null,p);}else l=Us(t,null,r);let h=i+".tmp";if(writeFileSync(h,l,"utf-8"),renameSync(h,i),a){if(t.ci===null){let u=dirname(e.outputPath),f=join(u,".testrelic-report"),g=resolve(i);try{if(await zs(),n=await $s(f),!n){s=await ae(f,{htmlPath:g}),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(`
|
|
2603
4252
|
Report server: http://127.0.0.1:${n}
|
|
2604
|
-
`);try{let
|
|
2605
|
-
`);}}var be=9323,Nr=10;async function Hs(){for(let t=be;t<be+Nr;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 zs(){for(let t=be;t<be+Nr;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 $s(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(),Hs().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 Gs=20,rt=0,nt=[];async function st(t,e){rt>=Gs&&await new Promise(r=>nt.push(r)),rt++;try{return await copyFile(t,e),!0}catch{return false}finally{rt--,nt.length>0&&nt.shift()();}}var K=[];async function Dr(){K.length!==0&&(await Promise.allSettled(K),K.length=0);}function Br(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 Fr(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 Ks(t,e,r){let n=Fr(t),s=r?Fr(r).substring(0,Vs):"";return s&&(n+=`--${s}`),e>0&&(n+=`--retry-${e}`),n}function Or(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=Ks(e,r,a),c=s?["artifacts",s,o]:["artifacts",o],p=join(n,...c),h=c,u={};try{mkdirSync(p,{recursive:!0});}catch{return null}if(l?.path&&existsSync(l.path)){let g=`screenshot${extname(l.path)||".png"}`,m=join(p,g);K.push(st(l.path,m).then(()=>{})),u.screenshot=`${h.join("/")}/${g}`;}if(i?.path&&existsSync(i.path)){let g=`video${extname(i.path)||".webm"}`,m=join(p,g);K.push(st(i.path,m).then(()=>{})),u.video=`${h.join("/")}/${g}`;}if(d?.path&&existsSync(d.path)){let g=`trace${extname(d.path)||".zip"}`,m=join(p,g);K.push(st(d.path,m).then(()=>{})),u.trace=`${h.join("/")}/${g}`;}return !u.screenshot&&!u.video&&!u.trace?null:u}var Zs=/^\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}(-\d+)?$/,Xs="1.0";function ea(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 qr(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:ea(a.name),relativePath:`artifacts/${e}/${r}/${a.name}`,sizeBytes:i.size});}}catch{}return {testName:r,files:n}}function ta(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=qr(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 Ur(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(Zs.test(d.name)){let o=ta(r,d.name);n.push({...o,isCurrentRun:d.name===e});}else {let o=qr(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:Xs,generatedAt:new Date().toISOString(),artifactBaseDir:"artifacts",totalSizeBytes:l,runs:n,serverPort:null}}var oa=256;function la(t){let e=resolve(t);for(let r=0;r<oa;r++){if(existsSync(join(e,".git")))return e;let n=dirname(e);if(n===e)return null;e=n;}return null}function $r(t){try{let e=la(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(`
|
|
2606
4256
|
`).map(d=>d.trim()).some(d=>d===s||d===n))return;let a=`
|
|
2607
4257
|
# TestRelic test artifacts
|
|
2608
4258
|
${s}
|
|
2609
|
-
`;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.
|
|
2610
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)})
|
|
2611
|
-
`);return}finally{n&&clearTimeout(n);}}function
|
|
2612
|
-
`}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
|
|
2613
4263
|
`,a=`\u2514${"\u2500".repeat(58)}\u2518
|
|
2614
|
-
`,
|
|
2615
|
-
`,process.stderr.write(i);}var
|
|
2616
|
-
`)[0]?.trim();return n?
|
|
2617
|
-
`),n=JSON.stringify(
|
|
2618
|
-
`),n=JSON.stringify(
|
|
2619
|
-
`),n=JSON.stringify(
|
|
2620
|
-
`),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.
|
|
2621
4271
|
`):process.stderr.write(`\u26A0 TestRelic: Unable to queue upload data.
|
|
2622
|
-
`);}}async function
|
|
2623
|
-
`),unlinkSync(i);continue}d=c;}catch(
|
|
2624
|
-
`);try{unlinkSync(i);}catch{}continue}try{let{body:
|
|
2625
|
-
`);try{renameSync(i,`${i}.failed`);}catch{try{unlinkSync(i);}catch{}}}else {process.stderr.write(`\u26A0 TestRelic: Queue flush paused \u2014 server returned HTTP ${
|
|
2626
|
-
`);return}}catch(
|
|
2627
|
-
`);return}}}function
|
|
2628
|
-
`);return}try{if(
|
|
2629
|
-
`);return}let r=await
|
|
2630
|
-
`),await this.resolveRepoId(),this.config.queueDirectory&&(
|
|
2631
|
-
`);}}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()+
|
|
2632
|
-
`));}async dispose(){if(this.healthCheckTimer&&(clearInterval(this.healthCheckTimer),this.healthCheckTimer=null),this.flushPromise){try{await Promise.race([this.flushPromise,
|
|
2633
|
-
\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}
|
|
2634
4284
|
`);break;case "expired_key":this.setLocalMode("expired_api_key"),process.stderr.write(`\u26A0 TestRelic: API key has expired. Running in local mode.
|
|
2635
|
-
\u2192 Generate a new key: ${
|
|
4285
|
+
\u2192 Generate a new key: ${ir}
|
|
2636
4286
|
`);break;case "validation_error":this.setLocalMode("validation_error"),process.stderr.write(`\u26A0 TestRelic: API key format invalid. Running in local mode.
|
|
2637
|
-
\u2192 Manage keys: ${
|
|
4287
|
+
\u2192 Manage keys: ${ir}
|
|
2638
4288
|
`);break;case "rate_limited":this.setLocalMode("rate_limited"),process.stderr.write(`\u26A0 TestRelic: Rate limited during authentication. Running in local mode.
|
|
2639
4289
|
`);break;case "timeout":this.setLocalMode("auth_timeout"),process.stderr.write(`\u26A0 TestRelic: Authentication timed out. Running in local mode.
|
|
2640
4290
|
`);break;default:this.setLocalMode(`auth_error_${r??"unknown"}`),process.stderr.write(`\u26A0 TestRelic: Cloud authentication failed. Running in local mode.
|
|
2641
|
-
`);break}}async resolveRepoId(){if(!this.config||!this.authState.accessToken)return;let e=this.gitMetadata?.remoteUrl?
|
|
2642
|
-
`);}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>
|
|
2643
|
-
`),this.startBackgroundFlush();}}}catch{}},
|
|
2644
|
-
`);}return
|
|
2645
|
-
`),{navigations:s,apiAssertions:a,networkRequestsFile:
|
|
2646
|
-
`),{keyMap:a,total:
|
|
2647
|
-
`).filter(r=>r.length>0).map(r=>{try{return JSON.parse(r)}catch{return null}}).filter(r=>r!==null)}catch{return []}}function
|
|
2648
|
-
`)
|
|
2649
|
-
`)
|
|
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(`
|
|
2650
4301
|
\u26A0 TestRelic: No fixture data received from any test.
|
|
2651
4302
|
To enable network logs, video sync, and Test Navigation tracking, import the TestRelic fixture:
|
|
2652
4303
|
import { test, expect } from '@testrelic/playwright-analytics/fixture';
|
|
2653
4304
|
instead of:
|
|
2654
4305
|
import { test, expect } from '@playwright/test';
|
|
2655
4306
|
|
|
2656
|
-
`),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).
|
|
2657
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.
|
|
2658
|
-
`),await
|
|
2659
|
-
`);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}
|
|
2660
4311
|
[testrelic] ${this.testIndex.length} test detail files on disk
|
|
2661
|
-
[testrelic] View report: npx testrelic serve ${
|
|
4312
|
+
[testrelic] View report: npx testrelic serve ${g}
|
|
2662
4313
|
`),this.streamingWriter.getWriteErrors().length>0&&process.stderr.write(`[testrelic] WARNING: ${this.streamingWriter.getWriteErrors().length} write error(s) during streaming
|
|
2663
|
-
`);let
|
|
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.
|
|
2664
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).
|
|
2665
|
-
`));}catch{}}switchToStreamingMode(){try{if(this.activeReportMode==="streaming")return;this.activeReportMode="streaming";let e=dirname(this.config.outputPath);this.streamingWriter=new
|
|
2666
|
-
`)[0]??null,networkCount:r.networkRequestsCount,consoleCount:r.consoleLogsCount,actionCount:r.actions?.length??0,isRetry:!1,retryGroupKey:
|
|
2667
|
-
`);}}};var
|
|
2668
|
-
`);return
|
|
2669
|
-
`);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 to(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 ro(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[Bt];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=eo(),p;if(c){let u=Xi(c.file);u&&c.line>0&&c.line<=u.length&&(p=u[c.line-1].trim());}let h=r(a,...l);return o?Dn(h,{tracker:i,callId:o,received:a,location:c??{file:"unknown",line:0},expression:p}):h};for(let s of Object.keys(r))n[s]=r[s];return n}var Nn=null,Fn=null;function Z(t,e){Nn=t,Fn=e;}function Ee(t){return ro(()=>Nn,()=>Fn,t)}function Dn(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"?Dn(a,e):function(...i){let d=i[0],o="passed",c=e.received,p=null;try{let h=a.apply(this??r,i);return h&&typeof h=="object"&&typeof h.then=="function"?h.then(()=>{Dt(e,n,d,c,"passed");},u=>{throw Dt(e,n,d,c,"failed"),u}):h}catch(h){if(o="failed",p=h,h instanceof Error&&h.message){let u=h.message.match(/Received:\s*(.+)/);u&&(c=u[1]);}throw h}finally{(p!==null||o==="passed")&&Dt(e,n,d,c,o);}}}})}function Dt(t,e,r,n,s){let a=to(t.expression,t.received),l={callId:t.callId,type:a,expected:Pn(r),actual:Pn(n),status:s,location:t.location,expression:t.expression};t.tracker.recordAssertion(l);}function Pn(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 Bn="[REDACTED]";function qt(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())?Bn: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=Ot(r,n);return JSON.stringify(s)}function Ot(t,e){if(Array.isArray(t))return t.map(r=>Ot(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]=Bn:r[n]=Ot(s,e);}return r}return t}var no=new Set(["access_token","api_key","token","sig"]);function Ut(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()])no.has(n.toLowerCase())&&(e.searchParams.delete(n),r=true);return r?e.toString():t}function qn(t,e,r){if(r.length>0){for(let n of r)if(On(t,n))return false}if(e.length>0){for(let n of e)if(On(t,n))return true;return false}return true}function On(t,e){try{return e instanceof RegExp?e.test(t):so(e).test(t)}catch{return console.warn(`[testrelic] Invalid URL filter pattern: ${String(e)}`),false}}function so(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 ao=["get","post","put","patch","delete","head","fetch"],io=["text/","application/json","application/xml","application/javascript","application/x-www-form-urlencoded","application/graphql"];function oo(t){let e=t.toLowerCase();return io.some(r=>e.includes(r))}function lo(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 Un(t){return t?"\u2022\u2022\u2022\u2022":""}function co(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:Un(i)});}return r}function uo(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:Un(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 ao){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(!qn(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=lo(l),p=performance.now();n._lastCallId=i,n.assertionTracker&&n.assertionTracker.setCurrentCallId(i);let h;try{h=await r(a,l);}catch(u){let f=performance.now();try{let g=n.apiConfig.captureRequestBody?Pe(c,n.apiConfig.redactBodyFields):null,m={id:i,timestamp:d,method:o,url:Ut(a),requestHeaders:null,requestBody:g,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=h.headers(),g=f["content-type"]??null,m=g?!oo(g):!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 h.body()).toString("base64"):k=await h.text();}catch{}let C=[],b=[];try{if(n.apiConfig.captureRequestHeaders&&(C=co(l?.headers)),n.apiConfig.captureResponseHeaders){let S=h.headersArray().filter(L=>L.name.toLowerCase()==="set-cookie").map(L=>L.value);b=uo(S);}}catch{}y=qt(y,n.apiConfig.redactHeaders),w=qt(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:Ut(h.url()),requestHeaders:y,requestBody:v,responseStatusCode:h.status(),responseStatusText:h.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{h[Bt]=i,n.tagResponseMethods(h,i);}catch{}return h}}};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 go="__testrelic_api_config",ho="__testrelic_config_trackApiCalls";function zt(t){let e=t.annotations.find(n=>n.type===go&&n.description!==void 0);if(e)try{return JSON.parse(e.description,mo)}catch{}let r=t.annotations.find(n=>n.type===ho&&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 mo(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 jn={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=zt(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(jn);Ee(expect);var xo={request:async({request:t},e,r)=>{let n=zt(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 h=typeof c=="string"?c:Buffer.isBuffer(c)?c.toString("utf-8"):String(c);!h.startsWith("[testrelic]")&&!h.startsWith("\u2139 TestRelic")&&!h.startsWith("\u2713 TestRelic")&&!h.startsWith("\u26A0 TestRelic")&&l&&(l.append({level:"stdout",text:h.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 h=typeof c=="string"?c:Buffer.isBuffer(c)?c.toString("utf-8"):String(c);!h.startsWith("[testrelic]")&&!h.startsWith("\u26A0 TestRelic")&&l&&(l.append({level:"stderr",text:h.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(),h=n.captureAssertions?s.getData():[],u={testRelicData:!0,version:PAYLOAD_VERSION,navigations:[],apiAssertions:h,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 Wn="@testrelic/playwright-analytics";function To(t){return Array.isArray(t)?t.some(e=>Array.isArray(e)&&typeof e[0]=="string"&&e[0]===Wn):false}function Co(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=To(s)?s:[...s,[Wn,e??{}]];return defineConfig({...t,use:n,reporter:a})}var Gn=false;function Kc(t,e,r="manual_record"){if(!t||!t.annotations){Gn||(Gn=true,process.stderr.write(`[testrelic] recordNavigation: reporter not active, navigation not recorded
|
|
2670
|
-
`));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,Co as defineConfig,Kc as recordNavigation,xo as testRelicApiFixture,jn as testRelicFixture};//# sourceMappingURL=index.js.map
|
|
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
|
|
2671
4323
|
//# sourceMappingURL=index.js.map
|