@testrelic/playwright-analytics 2.3.10 → 2.3.12
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 +42 -0
- package/dist/cli.cjs +1 -1
- package/dist/index.cjs +3 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +3 -3
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -359,6 +359,23 @@ Each navigation in the timeline includes detailed network statistics:
|
|
|
359
359
|
|
|
360
360
|
Disable with `includeNetworkStats: false`.
|
|
361
361
|
|
|
362
|
+
## Viewing Reports
|
|
363
|
+
|
|
364
|
+
For **small test suites** (< 50 tests), the report is a self-contained HTML file — just open it in your browser.
|
|
365
|
+
|
|
366
|
+
For **large test suites** (50+ tests), TestRelic uses streaming mode to avoid memory issues. Test details are stored as separate files and loaded on demand via a local server. After tests complete, the terminal shows:
|
|
367
|
+
|
|
368
|
+
```
|
|
369
|
+
[testrelic] View report: npx testrelic serve ./test-results/.testrelic-report
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
Run that command and open `http://127.0.0.1:9323` in your browser. The server auto-starts after test runs when not in CI, but if you need to restart it later:
|
|
373
|
+
|
|
374
|
+
```bash
|
|
375
|
+
npx testrelic serve ./test-results/.testrelic-report
|
|
376
|
+
npx testrelic serve ./test-results/.testrelic-report --port 9400 # custom port
|
|
377
|
+
```
|
|
378
|
+
|
|
362
379
|
## Merging Shard Reports
|
|
363
380
|
|
|
364
381
|
When running Playwright with sharding, each shard produces its own report. Merge them with the CLI:
|
|
@@ -415,6 +432,31 @@ test('CRUD operations', { tag: ['@api'] }, async ({ request }) => {
|
|
|
415
432
|
});
|
|
416
433
|
```
|
|
417
434
|
|
|
435
|
+
### Custom Page Fixtures (authenticated pages, etc.)
|
|
436
|
+
|
|
437
|
+
If your tests create pages manually via `browser.newContext()` / `context.newPage()` (e.g., for auth with `storageState`), the built-in `page` fixture is bypassed and **no network/console/navigation data is captured**. Use `trackPage()` to enable tracking on any manually-created page:
|
|
438
|
+
|
|
439
|
+
```typescript
|
|
440
|
+
import { test as base, expect, trackPage } from '@testrelic/playwright-analytics/fixture';
|
|
441
|
+
|
|
442
|
+
type MyFixtures = {
|
|
443
|
+
authenticatedPage: Page;
|
|
444
|
+
};
|
|
445
|
+
|
|
446
|
+
export const test = base.extend<MyFixtures>({
|
|
447
|
+
authenticatedPage: async ({ browser }, use, testInfo) => {
|
|
448
|
+
const ctx = await browser.newContext({ storageState: 'auth.json' });
|
|
449
|
+
const page = await ctx.newPage();
|
|
450
|
+
const cleanup = await trackPage(page, testInfo); // ← enables tracking
|
|
451
|
+
await use(page);
|
|
452
|
+
await cleanup(); // ← flushes data to report
|
|
453
|
+
await ctx.close();
|
|
454
|
+
},
|
|
455
|
+
});
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
> **Important:** Without `trackPage()`, custom page fixtures will only show screenshots, videos, and action steps — but no network requests, console logs, or navigation timeline.
|
|
459
|
+
|
|
418
460
|
### Unified Testing (Browser + API)
|
|
419
461
|
|
|
420
462
|
Use the unified fixture which provides **both** `page` and `request` — the TestRelic report shows navigation timeline AND API call details for the same test. This is ideal for:
|
package/dist/cli.cjs
CHANGED
|
@@ -361,7 +361,7 @@ var init_report_server_routes = __esm({
|
|
|
361
361
|
init_jsonl_stream();
|
|
362
362
|
SAFE_ID_PATTERN = /^[a-f0-9]{12}$/;
|
|
363
363
|
TIMESTAMP_PATTERN = /^\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}(-\d+)?$/;
|
|
364
|
-
MAX_PAGE_SIZE =
|
|
364
|
+
MAX_PAGE_SIZE = 5e3;
|
|
365
365
|
MIME_TYPES = {
|
|
366
366
|
".png": "image/png",
|
|
367
367
|
".jpg": "image/jpeg",
|
package/dist/index.cjs
CHANGED
|
@@ -947,7 +947,7 @@ body{
|
|
|
947
947
|
/* Load first page of network requests */
|
|
948
948
|
if(merged._networkCount>0){
|
|
949
949
|
pending++;
|
|
950
|
-
fetchData(testId,'network',1,
|
|
950
|
+
fetchData(testId,'network',1,5000,function(err,result){
|
|
951
951
|
if(!err&&result){
|
|
952
952
|
merged.networkRequests=result.items;
|
|
953
953
|
merged._networkTotal=result.total;
|
|
@@ -960,7 +960,7 @@ body{
|
|
|
960
960
|
/* Load first page of console logs */
|
|
961
961
|
if(merged._consoleCount>0){
|
|
962
962
|
pending++;
|
|
963
|
-
fetchData(testId,'console',1,
|
|
963
|
+
fetchData(testId,'console',1,5000,function(err,result){
|
|
964
964
|
if(!err&&result){
|
|
965
965
|
merged.consoleLogs=result.items;
|
|
966
966
|
merged._consoleTotal=result.total;
|
|
@@ -2478,7 +2478,7 @@ ${o?`<script id="artifact-manifest-data" type="application/json">${o}</script>`:
|
|
|
2478
2478
|
</body>
|
|
2479
2479
|
</html>`}function Le(t){try{let e=process.platform,r;e==="darwin"?r=`open "${t}"`:e==="win32"?r=`start "" "${t}"`:r=`xdg-open "${t}"`,child_process.exec(r,n=>{n&&process.stderr.write(`[testrelic] Failed to open browser: ${n.message}
|
|
2480
2480
|
`);});}catch{}}var yt=path.join(os$1.tmpdir(),"testrelic-data"),N=class{constructor(e){this.count=0;this.closed=false;fs$1.mkdirSync(yt,{recursive:true}),this.filePath=path.join(yt,`${e}-${crypto.randomUUID().substring(0,8)}.jsonl`),this.fd=fs$1.openSync(this.filePath,"w");}append(e){if(this.closed)return;let r=JSON.stringify(e)+`
|
|
2481
|
-
`;fs$1.writeSync(this.fd,r),this.count++;}getPath(){return this.filePath}getCount(){return this.count}close(){if(!this.closed){this.closed=true;try{fs$1.closeSync(this.fd);}catch{}}}cleanup(){try{fs$1.unlinkSync(this.filePath);}catch{}}};async function wt(t,e,r,n){let s=(e-1)*r,a=[],o=0,i=readline.createInterface({input:fs$1.createReadStream(t,{encoding:"utf-8"}),crlfDelay:1/0});for await(let c of i)if(c.length!==0){if(o>=s&&a.length<r)try{a.push(JSON.parse(c));}catch{}if(o++,a.length>=r&&n!==void 0)break}let l=n??o,d=Math.max(1,Math.ceil(l/r));return {items:a,total:l,page:e,pageSize:r,totalPages:d}}var Ct=/^[a-f0-9]{12}$/,Ne=/^\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}(-\d+)?$/,Tt=
|
|
2481
|
+
`;fs$1.writeSync(this.fd,r),this.count++;}getPath(){return this.filePath}getCount(){return this.count}close(){if(!this.closed){this.closed=true;try{fs$1.closeSync(this.fd);}catch{}}}cleanup(){try{fs$1.unlinkSync(this.filePath);}catch{}}};async function wt(t,e,r,n){let s=(e-1)*r,a=[],o=0,i=readline.createInterface({input:fs$1.createReadStream(t,{encoding:"utf-8"}),crlfDelay:1/0});for await(let c of i)if(c.length!==0){if(o>=s&&a.length<r)try{a.push(JSON.parse(c));}catch{}if(o++,a.length>=r&&n!==void 0)break}let l=n??o,d=Math.max(1,Math.ceil(l/r));return {items:a,total:l,page:e,pageSize:r,totalPages:d}}var Ct=/^[a-f0-9]{12}$/,Ne=/^\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}(-\d+)?$/,Tt=5e3;function x(t,e,r){t.writeHead(e,{"Content-Type":"application/json"}),t.end(JSON.stringify(r));}function St(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 re(t){try{return fs$1.existsSync(t)?JSON.parse(fs$1.readFileSync(t,"utf-8")):null}catch{return null}}function ne(t){let e=0;try{let r=fs$1.readdirSync(t,{withFileTypes:!0});for(let n of r){let s=path.join(t,n.name);n.isFile()?e+=fs$1.statSync(s).size:n.isDirectory()&&(e+=ne(s));}}catch{}return e}function Rt(t,e,r,n){let s=re(path.join(r,"index.json"));x(e,200,{status:"ok",reportMode:"streaming",testCount:s?.length??0,uptime:Math.floor((Date.now()-n)/1e3)});}function At(t,e,r){let n=re(path.join(r,"summary.json"));if(!n){x(e,404,{error:"Summary not found"});return}x(e,200,n);}function _t(t,e,r){let n=re(path.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,o=Math.max(1,parseInt(a.get("page")??"1",10)||1),i=Math.min(Tt,Math.max(1,parseInt(a.get("pageSize")??"100",10)||100)),l=a.get("status")?.split(",").filter(Boolean)??null,d=a.get("file")??null,c=a.get("search")?.toLowerCase()??null,p=a.get("tag")?.split(",").filter(Boolean)??null,f=a.get("sort")??"file",u=a.get("order")==="desc"?-1:1,g=n;l&&l.length>0&&(g=g.filter(w=>l.includes(w.status))),d&&(g=g.filter(w=>w.filePath===d||w.filePath.startsWith(d+"/"))),c&&(g=g.filter(w=>w.title.toLowerCase().includes(c)||w.filePath.toLowerCase().includes(c))),p&&p.length>0&&(g=g.filter(w=>p.some(y=>w.tags.includes(y)))),g=[...g].sort((w,y)=>{let T=0;switch(f){case "duration":T=w.duration-y.duration;break;case "status":T=w.status.localeCompare(y.status);break;case "title":T=w.title.localeCompare(y.title);break;default:T=w.filePath.localeCompare(y.filePath);break}return T*u});let h=g.length,m=Math.max(1,Math.ceil(h/i)),b=(o-1)*i,C=g.slice(b,b+i);x(e,200,{tests:C,pagination:{page:o,pageSize:i,totalItems:h,totalPages:m},filters:{status:l,file:d,search:c,tag:p}});}function It(t,e,r,n){if(!Ct.test(n)){x(e,400,{error:"Invalid test ID format"});return}let s=path.join(r,"tests",n,"meta.json"),a=path.join(r,"tests",`${n}.json`),o=fs$1.existsSync(s)?s:fs$1.existsSync(a)?a:null;if(!o){x(e,404,{error:`Test not found: ${n}`});return}try{let i=fs$1.readFileSync(o,"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 Lt(t,e,r,n,s){if(!Ct.test(n)){x(e,400,{error:"Invalid test ID format"});return}let o=path.join(r,"tests",n,{network:"network.jsonl",console:"console.jsonl","api-calls":"api-calls.jsonl"}[s]);if(!fs$1.existsSync(o)){x(e,200,{items:[],total:0,page:1,pageSize:50,totalPages:0});return}try{let l=new URL(t.url??"/",`http://${t.headers.host}`).searchParams,d=Math.max(1,parseInt(l.get("page")??"1",10)||1),c=Math.min(Tt,Math.max(1,parseInt(l.get("pageSize")??"50",10)||50)),p,f=path.join(r,"tests",n,"meta.json");if(fs$1.existsSync(f))try{let g=JSON.parse(fs$1.readFileSync(f,"utf-8"));switch(s){case "network":p=g.networkRequestsCount;break;case "console":p=g.consoleLogsCount;break;case "api-calls":p=g.apiCallsCount;break}}catch{}let u=await wt(o,d,c,p);x(e,200,u);}catch(i){x(e,500,{error:i instanceof Error?i.message:String(i)});}}function Nt(t,e,r){let n=re(path.join(r,"index.json"));if(!n){x(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));x(e,200,{files:a});}function Mt(t,e,r){if(!fs$1.existsSync(r)){x(e,200,{runs:[],totalSizeBytes:0});return}try{let n=[],s=0,a=fs$1.readdirSync(r,{withFileTypes:!0});for(let o of a){if(!o.isDirectory()||!Ne.test(o.name))continue;let i=path.join(r,o.name),l=ne(i),d=fs$1.readdirSync(i,{withFileTypes:!0}).filter(c=>c.isDirectory());n.push({folderName:o.name,totalSizeBytes:l,testCount:d.length}),s+=l;}n.sort((o,i)=>i.folderName.localeCompare(o.folderName)),x(e,200,{runs:n,totalSizeBytes:s});}catch(n){x(e,500,{error:n instanceof Error?n.message:String(n)});}}function Et(t,e,r){try{let n=0,s=0;if(fs$1.existsSync(r)){let a=fs$1.readdirSync(r,{withFileTypes:!0});for(let o of a){if(!o.isDirectory()||!Ne.test(o.name))continue;let i=path.join(r,o.name),l=ne(i);fs$1.rmSync(i,{recursive:!0,force:!0}),s+=l,n++;}}x(e,200,{deletedCount:n,freedBytes:s});}catch(n){x(e,500,{error:n instanceof Error?n.message:String(n)});}}function Pt(t,e,r,n){if(!Ne.test(n)){x(e,400,{error:"Invalid folder name"});return}let s=path.join(r,n);try{if(!fs$1.statSync(s).isDirectory()){x(e,404,{error:"Not found"});return}}catch{x(e,404,{error:"Not found"});return}try{let a=ne(s);fs$1.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 Ft(t,e,r){x(e,200,{status:"shutting_down"}),r.close();}function Me(t,e,r){if(!fs$1.existsSync(r)){x(e,404,{error:"File not found"});return}try{let n=fs$1.readFileSync(r),s=path.extname(r).toLowerCase(),a=wn[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 wn={".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 Tn=9323,Sn=10,Rn=1800*1e3;function G(t,e){return new Promise((r,n)=>{let s=e?.port??Tn,a=Date.now(),o,i=0,l=fs$1.existsSync(path.join(t,"artifacts"))?path.join(t,"artifacts"):fs$1.existsSync(path.join(t,"..","artifacts"))?path.join(t,"..","artifacts"):path.join(t,"artifacts");function d(){clearTimeout(o),o=setTimeout(()=>{p.close();},Rn);}let c=e?.htmlPath??null;if(!c){let u=path.dirname(t);try{let g=fs$1.readdirSync(u).find(h=>h.endsWith(".html"));g&&(c=path.join(u,g));}catch{}}let p=http.createServer((u,g)=>{if(d(),St(g),u.method==="OPTIONS"){g.writeHead(204),g.end();return}let h;try{h=new URL(u.url??"/",`http://${u.headers.host??"localhost"}`).pathname;}catch{x(g,400,{error:"Invalid URL"});return}if(u.method==="GET"&&(h==="/"||h==="/index.html")){if(c&&fs$1.existsSync(c)){Me(u,g,c);return}x(g,404,{error:"HTML report not found"});return}if(u.method==="GET"&&h==="/api/health"){Rt(u,g,t,a);return}if(u.method==="GET"&&h==="/api/summary"){At(u,g,t);return}if(u.method==="GET"&&h==="/api/tests"){_t(u,g,t);return}if(u.method==="GET"&&h==="/api/files"){Nt(u,g,t);return}let m=h.match(/^\/api\/tests\/([a-f0-9]+)\/(network|console|api-calls)$/);if(u.method==="GET"&&m){Lt(u,g,t,m[1],m[2]);return}let b=h.match(/^\/api\/tests\/([a-f0-9]+)$/);if(u.method==="GET"&&b){It(u,g,t,b[1]);return}if(u.method==="GET"&&h==="/api/artifacts"){Mt(u,g,l);return}if(u.method==="DELETE"&&h==="/api/artifacts"){Et(u,g,l);return}let C=h.match(/^\/api\/artifacts\/(.+)$/);if(u.method==="DELETE"&&C){Pt(u,g,l,decodeURIComponent(C[1]));return}if(u.method==="GET"&&h.startsWith("/artifacts/")){let w=decodeURIComponent(h.slice(11));if(w.includes("..")||w.includes("\0")){x(g,400,{error:"Invalid path"});return}Me(u,g,path.join(l,w));return}if(u.method==="POST"&&h==="/api/shutdown"){Ft(u,g,p);return}x(g,404,{error:"Not found"});});function f(u){let g=h=>{h.code==="EADDRINUSE"&&i<Sn?(i++,f(u+1)):n(h);};p.once("error",g),p.listen(u,"127.0.0.1",()=>{p.removeListener("error",g);let h=p.address();if(!h||typeof h=="string"){n(new Error("Failed to get server address"));return}d(),r({port:h.port,dispose:()=>new Promise(m=>{clearTimeout(o),p.close(()=>m());})});});}f(s);})}async function Dt(t){let e=await G(t);return {port:e.port,dispose:e.dispose}}function In(t,e,r){let n=JSON.stringify(t),s=r?JSON.stringify(r):null;return vt(n,e,s)}async function Pe(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,l=path.dirname(i);fs$1.mkdirSync(l,{recursive:!0});let d="",c="[]",p=null;if(a){d=JSON.stringify(t.summary);let u=path.dirname(e.outputPath),g=path.join(u,".testrelic-report");try{let h=path.join(g,"index-compact.json"),m=path.join(g,"index.json");fs$1.existsSync(h)?c=fs$1.readFileSync(h,"utf-8"):fs$1.existsSync(m)&&(c=fs$1.readFileSync(m,"utf-8"));}catch{}p=r?JSON.stringify(r):null,o=Ie(d,c,null,p);}else o=In(t,null,r);let f=i+".tmp";if(fs$1.writeFileSync(f,o,"utf-8"),fs$1.renameSync(f,i),a){if(t.ci===null){let u=path.dirname(e.outputPath),g=path.join(u,".testrelic-report"),h=path.resolve(i);try{if(await Nn(),n=await Mn(g),!n){s=await G(g,{htmlPath:h}),n=s.port;let m=setInterval(()=>{},6e4),b=()=>{clearInterval(m),s?.dispose();};process.on("SIGINT",b),process.on("SIGTERM",b),setTimeout(b,1800*1e3).unref();}if(n){process.stderr.write(`
|
|
2482
2482
|
Report server: http://127.0.0.1:${n}
|
|
2483
2483
|
`);try{let m=Ie(d,c,n,p),b=i+".tmp";fs$1.writeFileSync(b,m,"utf-8"),fs$1.renameSync(b,i);}catch{}}}catch{}}}else if(e.openReport&&t.ci===null&&e.includeArtifacts){let u=path.dirname(e.outputPath),g=path.join(u,"artifacts");if(fs$1.existsSync(g))try{let h=await Dt(g);n=h.port,process.on("exit",()=>{h.dispose();});}catch{}}if(e.openReport&&t.ci===null)if(a&&n)Le(`http://127.0.0.1:${n}`);else {let u=path.resolve(i);Le(u);}}catch(n){process.stderr.write(`[testrelic] Failed to write HTML report: ${n instanceof Error?n.message:String(n)}
|
|
2484
2484
|
`);}}var ie=9323,Ut=10;async function Ln(){for(let t=ie;t<ie+Ut;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 Nn(){for(let t=ie;t<ie+Ut;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 Mn(t){let r=[path.join(__dirname,"cli.cjs"),path.join(__dirname,"cli.js"),path.join(__dirname,"..","dist","cli.cjs"),path.join(__dirname,"..","dist","cli.js")].find(n=>fs$1.existsSync(n));if(!r){let n=await G(t);return process.on("exit",()=>{n?.dispose();}),n.port}return new Promise(n=>{let s=child_process.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(),Ln().then(n));},5e3);s.stderr?.on("data",l=>{a+=l.toString();let d=a.match(/127\.0\.0\.1:(\d+)/);d&&!o&&(o=true,clearTimeout(i),s.stderr?.removeAllListeners(),s.unref(),n(Number(d[1])));}),s.on("error",()=>{o||(o=true,clearTimeout(i),n(null));}),s.on("exit",()=>{o||(o=true,clearTimeout(i),n(null));}),s.unref();})}var Fn=20,Fe=0,De=[];async function jt(t,e){Fe>=Fn&&await new Promise(r=>De.push(r)),Fe++;try{return await promises.copyFile(t,e),!0}catch{return false}finally{Fe--,De.length>0&&De.shift()();}}var K=[];async function zt(){K.length!==0&&(await Promise.allSettled(K),K.length=0);}function Vt(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(!fs$1.existsSync(path.join(t,n)))return n;let s=1;for(;fs$1.existsSync(path.join(t,`${n}-${s}`));)s++;return `${n}-${s}`}function Dn(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"}function Wt(t,e,r,n,s){let a=t.find(f=>f.name==="screenshot"&&f.path),o=t.find(f=>f.name==="video"&&f.path);if(!a&&!o)return null;let i=Dn(e);r>0&&(i+=`--retry-${r}`);let l=s?["artifacts",s,i]:["artifacts",i],d=path.join(n,...l),c=l,p={};try{fs$1.mkdirSync(d,{recursive:!0});}catch{return null}if(a?.path&&fs$1.existsSync(a.path)){let u=`screenshot${path.extname(a.path)||".png"}`,g=path.join(d,u);K.push(jt(a.path,g).then(()=>{})),p.screenshot=`${c.join("/")}/${u}`;}if(o?.path&&fs$1.existsSync(o.path)){let u=`video${path.extname(o.path)||".webm"}`,g=path.join(d,u);K.push(jt(o.path,g).then(()=>{})),p.video=`${c.join("/")}/${u}`;}return !p.screenshot&&!p.video?null:p}var qn=/^\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}(-\d+)?$/,Hn="1.0";function Un(t){let e=path.extname(t).toLowerCase();return [".png",".jpg",".jpeg",".gif",".bmp",".webp"].includes(e)?"screenshot":[".webm",".mp4",".avi",".mov"].includes(e)?"video":"other"}function Gt(t,e,r){let n=[];try{let s=fs$1.readdirSync(t,{withFileTypes:!0});for(let a of s){if(!a.isFile())continue;let o=path.join(t,a.name),i=fs$1.statSync(o);n.push({name:a.name,type:Un(a.name),relativePath:`artifacts/${e}/${r}/${a.name}`,sizeBytes:i.size});}}catch{}return {testName:r,files:n}}function $n(t,e){let r=path.join(t,e),n=[],s=0;try{let o=fs$1.readdirSync(r,{withFileTypes:!0});for(let i of o){if(!i.isDirectory())continue;let l=Gt(path.join(r,i.name),e,i.name);n.push(l);for(let d of l.files)s+=d.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 Jt(t,e){let r=path.join(t,"artifacts"),n=[],s=[],a=0;try{let i=fs$1.readdirSync(r,{withFileTypes:!0});for(let l of i)if(l.isDirectory())if(qn.test(l.name)){let d=$n(r,l.name);n.push({...d,isCurrentRun:l.name===e});}else {let d=Gt(path.join(r,l.name),l.name,l.name);s.push(d);for(let c of d.files)a+=c.sizeBytes;}}catch{}n.sort((i,l)=>l.timestamp.localeCompare(i.timestamp)),s.length>0&&(s.sort((i,l)=>i.testName.localeCompare(l.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,l)=>i+l.totalSizeBytes,0);return {schemaVersion:Hn,generatedAt:new Date().toISOString(),artifactBaseDir:"artifacts",totalSizeBytes:o,runs:n,serverPort:null}}function Gn(t){let e=t,{root:r}=path.parse(e);for(;e!==r;){if(fs$1.existsSync(path.join(e,".git")))return e;e=path.join(e,"..");}return null}function Yt(t){try{let e=Gn(t);if(!e)return;let r=path.join(e,".gitignore"),n=path.relative(e,t).replace(/\\/g,"/"),s=n.endsWith("/")?n:`${n}/`;if(fs$1.existsSync(r)&&fs$1.readFileSync(r,"utf-8").split(`
|