@testrelic/maestro-analytics 1.3.1 → 1.4.0-next.121
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -0
- package/dist/cli.cjs +680 -23
- package/dist/index.cjs +643 -59
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +9 -1
- package/dist/index.d.ts +9 -1
- package/dist/index.js +643 -59
- package/dist/index.js.map +1 -1
- package/dist/merge.cjs +2 -2
- package/dist/merge.cjs.map +1 -1
- package/dist/merge.js +2 -2
- package/dist/merge.js.map +1 -1
- package/package.json +2 -2
package/dist/cli.cjs
CHANGED
|
@@ -778,6 +778,9 @@ var import_node_fs18 = require("fs");
|
|
|
778
778
|
var import_node_path16 = require("path");
|
|
779
779
|
var import_node_crypto4 = require("crypto");
|
|
780
780
|
|
|
781
|
+
// src/schema.ts
|
|
782
|
+
var SCHEMA_VERSION = "1.1.0";
|
|
783
|
+
|
|
781
784
|
// src/parsers/junit-parser.ts
|
|
782
785
|
var import_fast_xml_parser = require("fast-xml-parser");
|
|
783
786
|
var import_node_fs5 = require("fs");
|
|
@@ -1877,6 +1880,30 @@ function collectArtifacts(testOutputDir, debugOutputDir) {
|
|
|
1877
1880
|
}
|
|
1878
1881
|
|
|
1879
1882
|
// src/timeline-builder.ts
|
|
1883
|
+
function mapStepKind(category) {
|
|
1884
|
+
switch (category) {
|
|
1885
|
+
case "interaction":
|
|
1886
|
+
return "interaction";
|
|
1887
|
+
case "assertion":
|
|
1888
|
+
return "assertion";
|
|
1889
|
+
case "ai":
|
|
1890
|
+
return "ai";
|
|
1891
|
+
case "navigation":
|
|
1892
|
+
return "navigation";
|
|
1893
|
+
case "device":
|
|
1894
|
+
return "device";
|
|
1895
|
+
case "media":
|
|
1896
|
+
return "media";
|
|
1897
|
+
case "script":
|
|
1898
|
+
return "script";
|
|
1899
|
+
case "flow_control":
|
|
1900
|
+
return "flow_control";
|
|
1901
|
+
case "recording":
|
|
1902
|
+
return "media";
|
|
1903
|
+
default:
|
|
1904
|
+
return "other";
|
|
1905
|
+
}
|
|
1906
|
+
}
|
|
1880
1907
|
function mapCommandCategory(category) {
|
|
1881
1908
|
switch (category) {
|
|
1882
1909
|
case "interaction":
|
|
@@ -1965,6 +1992,8 @@ function commandToAction(cmd, flowStartedMs) {
|
|
|
1965
1992
|
return {
|
|
1966
1993
|
title: buildStepTitle(cmd),
|
|
1967
1994
|
category: mapCommandCategory(cmd.category),
|
|
1995
|
+
kind: mapStepKind(cmd.category),
|
|
1996
|
+
selector: cmd.selector ?? null,
|
|
1968
1997
|
status: cmd.status === "failed" ? "failed" : "passed",
|
|
1969
1998
|
duration: cmd.duration,
|
|
1970
1999
|
timestamp: cmd.timestamp,
|
|
@@ -1979,6 +2008,40 @@ function sortActions(a, b) {
|
|
|
1979
2008
|
if (Number.isFinite(ta) && Number.isFinite(tb) && ta !== tb) return ta - tb;
|
|
1980
2009
|
return 0;
|
|
1981
2010
|
}
|
|
2011
|
+
function flowDevice(flow) {
|
|
2012
|
+
return {
|
|
2013
|
+
platform: flow.platform,
|
|
2014
|
+
// Maestro never reports an OS version or a UDID distinct from the device
|
|
2015
|
+
// id; saying so with null beats inventing a value.
|
|
2016
|
+
platformVersion: null,
|
|
2017
|
+
deviceName: flow.deviceId ?? null,
|
|
2018
|
+
automationName: "maestro",
|
|
2019
|
+
appId: flow.appId,
|
|
2020
|
+
appTestMode: flow.platform === "web" ? "mobile-web" : "native",
|
|
2021
|
+
udid: flow.deviceId ?? null
|
|
2022
|
+
};
|
|
2023
|
+
}
|
|
2024
|
+
function flowLogs(flow) {
|
|
2025
|
+
return flow.logEntries.map((e) => ({
|
|
2026
|
+
level: e.level === "ERROR" ? "error" : e.level === "WARN" ? "warn" : e.level === "DEBUG" ? "debug" : "info",
|
|
2027
|
+
text: e.message,
|
|
2028
|
+
timestamp: e.timestamp,
|
|
2029
|
+
location: null,
|
|
2030
|
+
source: "driver",
|
|
2031
|
+
tag: e.source ?? null,
|
|
2032
|
+
pid: null,
|
|
2033
|
+
stackTrace: null
|
|
2034
|
+
}));
|
|
2035
|
+
}
|
|
2036
|
+
function flowDefects(flow) {
|
|
2037
|
+
return flow.aiDefects.map((d) => ({
|
|
2038
|
+
type: d.type,
|
|
2039
|
+
severity: d.severity,
|
|
2040
|
+
description: d.description,
|
|
2041
|
+
location: d.location,
|
|
2042
|
+
screenshot: d.screenshot
|
|
2043
|
+
}));
|
|
2044
|
+
}
|
|
1982
2045
|
function buildTestResult(flow, apiCalls) {
|
|
1983
2046
|
const flowStartedMs = Date.parse(flow.startedAt);
|
|
1984
2047
|
const orderedCommands = [...flow.commands].sort((a, b) => {
|
|
@@ -2004,12 +2067,12 @@ function buildTestResult(flow, apiCalls) {
|
|
|
2004
2067
|
actions.sort(sortActions);
|
|
2005
2068
|
const artifacts = flow.screenshotPaths.length > 0 || flow.videoPath ? {
|
|
2006
2069
|
screenshot: flow.screenshotPaths[0] ?? void 0,
|
|
2070
|
+
// The flow's whole strip, not just its first frame. The Screenshots
|
|
2071
|
+
// tab used to ignore the flow entirely and render the run's shots.
|
|
2072
|
+
...flow.screenshotPaths.length > 0 ? { screenshots: [...flow.screenshotPaths] } : {},
|
|
2007
2073
|
video: flow.videoPath ?? void 0
|
|
2008
2074
|
} : null;
|
|
2009
2075
|
const tags = [...flow.tags];
|
|
2010
|
-
if (flow.platform !== "unknown" && !tags.includes(flow.platform)) {
|
|
2011
|
-
tags.push(flow.platform);
|
|
2012
|
-
}
|
|
2013
2076
|
return {
|
|
2014
2077
|
title: flow.flowName,
|
|
2015
2078
|
status: flow.status,
|
|
@@ -2032,7 +2095,11 @@ function buildTestResult(flow, apiCalls) {
|
|
|
2032
2095
|
apiCalls: apiCalls.length > 0 ? [...apiCalls] : null,
|
|
2033
2096
|
apiAssertions: null,
|
|
2034
2097
|
actions: actions.length > 0 ? actions : null,
|
|
2035
|
-
|
|
2098
|
+
// The driver log reached the cloud payload and nothing else; the local
|
|
2099
|
+
// report showed no log lines at all.
|
|
2100
|
+
consoleLogs: flow.logEntries.length > 0 ? flowLogs(flow) : null,
|
|
2101
|
+
device: flowDevice(flow),
|
|
2102
|
+
aiDefects: flow.aiDefects.length > 0 ? flowDefects(flow) : null
|
|
2036
2103
|
};
|
|
2037
2104
|
}
|
|
2038
2105
|
function buildTimelineEntry(flow, apiCallsByFlow) {
|
|
@@ -2598,13 +2665,11 @@ var currentFilter='all';
|
|
|
2598
2665
|
var searchQuery='';
|
|
2599
2666
|
var openFlowIdx=-1;
|
|
2600
2667
|
|
|
2668
|
+
/* The platform is a device fact now, not a tag the builder appended. */
|
|
2601
2669
|
function detectPlatform(){
|
|
2602
2670
|
for(var i=0;i<flows.length;i++){
|
|
2603
|
-
var
|
|
2604
|
-
|
|
2605
|
-
var t=tags[j].toLowerCase();
|
|
2606
|
-
if(t==='android'||t==='ios'||t==='web')return t;
|
|
2607
|
-
}
|
|
2671
|
+
var d=flows[i].test.device;
|
|
2672
|
+
if(d&&d.platform&&d.platform!=='unknown')return d.platform;
|
|
2608
2673
|
}
|
|
2609
2674
|
return '';
|
|
2610
2675
|
}
|
|
@@ -2880,18 +2945,28 @@ function closeDrawer(){
|
|
|
2880
2945
|
}
|
|
2881
2946
|
|
|
2882
2947
|
function renderDrawerContent(flow){
|
|
2883
|
-
var
|
|
2948
|
+
var t=flow.test;
|
|
2949
|
+
/* Every tab is now about THIS flow. Screenshots and AI Defects used to take
|
|
2950
|
+
no flow argument at all and showed the whole run on every flow. */
|
|
2951
|
+
var tabs=[
|
|
2952
|
+
{name:'Overview',html:function(){return renderOverviewTab(flow);}},
|
|
2953
|
+
{name:'Steps',html:function(){return renderStepsTab(flow);}},
|
|
2954
|
+
{name:'Replay',html:function(){return renderReplay(t);}},
|
|
2955
|
+
{name:'Assertions',html:function(){return renderAssertionsTab(flow);}},
|
|
2956
|
+
{name:'Driver log',n:(t.consoleLogs||[]).length,html:function(){return renderDriverLogTab(t);}},
|
|
2957
|
+
{name:'Screenshots',n:testScreenshots(t).length,html:function(){return renderShotStrip(t);}},
|
|
2958
|
+
{name:'AI Defects',n:(t.aiDefects||[]).length,html:function(){return renderFlowAiDefects(t);}}
|
|
2959
|
+
];
|
|
2960
|
+
|
|
2884
2961
|
var h='<div class="seg-ctrl">';
|
|
2885
|
-
tabs.forEach(function(
|
|
2886
|
-
|
|
2962
|
+
tabs.forEach(function(tab,i){
|
|
2963
|
+
var label=tab.name+(tab.n?' ('+tab.n+')':'');
|
|
2964
|
+
h+='<button class="seg-btn'+(i===0?' active':'')+'" data-seg="tab'+i+'">'+label+'</button>';
|
|
2887
2965
|
});
|
|
2888
2966
|
h+='</div>';
|
|
2889
|
-
|
|
2890
|
-
|
|
2891
|
-
|
|
2892
|
-
h+='<div class="seg-pane" data-seg-pane="tab2">'+renderAssertionsTab(flow)+'</div>';
|
|
2893
|
-
h+='<div class="seg-pane" data-seg-pane="tab3">'+renderScreenshotsTab()+'</div>';
|
|
2894
|
-
h+='<div class="seg-pane" data-seg-pane="tab4">'+renderAiDefectsTab()+'</div>';
|
|
2967
|
+
tabs.forEach(function(tab,i){
|
|
2968
|
+
h+='<div class="seg-pane'+(i===0?' active':'')+'" data-seg-pane="tab'+i+'">'+tab.html()+'</div>';
|
|
2969
|
+
});
|
|
2895
2970
|
|
|
2896
2971
|
$('drawer-body').innerHTML=h;
|
|
2897
2972
|
}
|
|
@@ -3044,7 +3119,7 @@ function renderStepsTab(flow){
|
|
|
3044
3119
|
var dotCls='cmd-dot '+(isFailed?'cmd-dot-fail':'cmd-dot-pass');
|
|
3045
3120
|
h+='<div class="'+rowCls+'">';
|
|
3046
3121
|
h+='<div class="'+dotCls+'"></div>';
|
|
3047
|
-
h+=getCmdBadge(a.category);
|
|
3122
|
+
h+=a.kind?'<span class="cmd-kind k-'+esc(a.kind)+'">'+esc(a.kind)+'</span>':getCmdBadge(a.category);
|
|
3048
3123
|
h+='<div class="cmd-title" title="'+esc(a.title)+'">'+esc(a.title)+'</div>';
|
|
3049
3124
|
if(isSlow||isFailed&&a.duration>2000)h+='<span class="cmd-badge cmd-badge-slow">SLOW</span>';
|
|
3050
3125
|
h+='<div class="cmd-dur">'+fmtDur(a.duration)+'</div>';
|
|
@@ -3212,7 +3287,10 @@ function renderScreenshotsTab(){
|
|
|
3212
3287
|
var name=p.split(/[\\/\\\\]/).pop()||'screenshot';
|
|
3213
3288
|
var isFail=name.toLowerCase().indexOf('fail')>=0||name.toLowerCase().indexOf('error')>=0;
|
|
3214
3289
|
h+='<div class="screenshot-card'+(isFail?' failure-shot':'')+'" data-lb-idx="'+i+'">'
|
|
3215
|
-
|
|
3290
|
+
/* No lazy loading: this grid lives in a seg-pane that starts hidden, and
|
|
3291
|
+
Chrome never fetches a lazy image inside display:none \u2014 the cards came
|
|
3292
|
+
up blank until something else forced a reflow. */
|
|
3293
|
+
+'<img src="'+esc(p)+'" alt="'+esc(name)+'" onerror="this.parentElement.style.display=\\'none\\'"/>'
|
|
3216
3294
|
+'<div class="screenshot-card-label">'+(isFail?'Failure Screenshot':esc(name))+'</div></div>';
|
|
3217
3295
|
});
|
|
3218
3296
|
h+='</div>';
|
|
@@ -3359,6 +3437,578 @@ document.addEventListener('keydown',function(e){
|
|
|
3359
3437
|
});
|
|
3360
3438
|
`;
|
|
3361
3439
|
|
|
3440
|
+
// src/html-js-mobile.ts
|
|
3441
|
+
var JS_MOBILE = `
|
|
3442
|
+
function vNum(v){return typeof v==='number'?v:0;}
|
|
3443
|
+
|
|
3444
|
+
/* Every flow's TestResult, in the order the list shows them. */
|
|
3445
|
+
function flowTests(){
|
|
3446
|
+
var out=[];
|
|
3447
|
+
for(var i=0;i<flows.length;i++)out.push(flows[i].test);
|
|
3448
|
+
return out;
|
|
3449
|
+
}
|
|
3450
|
+
|
|
3451
|
+
/* \u2500\u2500 Device identity \u2500\u2500 */
|
|
3452
|
+
|
|
3453
|
+
var DEVICE_GLYPH={
|
|
3454
|
+
android:'<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"><rect x="5" y="2" width="14" height="20" rx="2.5"/><path d="M10 18.5h4"/></svg>',
|
|
3455
|
+
ios:'<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"><rect x="6" y="2" width="12" height="20" rx="3"/><path d="M10 19h4"/></svg>',
|
|
3456
|
+
web:'<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><circle cx="12" cy="12" r="9"/><path d="M3 12h18M12 3a15 15 0 010 18a15 15 0 010-18"/></svg>'
|
|
3457
|
+
};
|
|
3458
|
+
|
|
3459
|
+
function deviceGlyph(p){
|
|
3460
|
+
return DEVICE_GLYPH[p]||'<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><rect x="5" y="2" width="14" height="20" rx="2.5"/></svg>';
|
|
3461
|
+
}
|
|
3462
|
+
|
|
3463
|
+
function deviceHeadline(d){
|
|
3464
|
+
if(!d)return '';
|
|
3465
|
+
var os=d.platform==='ios'?'iOS':d.platform==='android'?'Android':d.platform==='web'?'Web':'Device';
|
|
3466
|
+
return d.platformVersion?os+' '+d.platformVersion:os;
|
|
3467
|
+
}
|
|
3468
|
+
|
|
3469
|
+
function shortAppId(v){
|
|
3470
|
+
var s=String(v);
|
|
3471
|
+
if(s.indexOf('/')<0&&s.indexOf('\\\\')<0)return s;
|
|
3472
|
+
var parts=s.split(/[\\/\\\\]/);
|
|
3473
|
+
return parts[parts.length-1]||s;
|
|
3474
|
+
}
|
|
3475
|
+
|
|
3476
|
+
function renderDeviceStrip(){
|
|
3477
|
+
var el=$('device-strip');
|
|
3478
|
+
if(!el)return;
|
|
3479
|
+
var ts=flowTests(),d=null,seen={},count=0;
|
|
3480
|
+
for(var i=0;i<ts.length;i++){
|
|
3481
|
+
var dv=ts[i].device;
|
|
3482
|
+
if(!dv||dv.platform==='unknown')continue;
|
|
3483
|
+
if(!d)d=dv;
|
|
3484
|
+
var key=(dv.platform||'')+'|'+(dv.deviceName||'');
|
|
3485
|
+
if(!seen[key]){seen[key]=1;count++;}
|
|
3486
|
+
}
|
|
3487
|
+
if(!d){el.innerHTML='';return;}
|
|
3488
|
+
|
|
3489
|
+
var h='<div class="devstrip"><div class="dev-id">';
|
|
3490
|
+
h+='<span class="dev-icon dev-icon-'+esc(d.platform||'unknown')+'">'+deviceGlyph(d.platform)+'</span>';
|
|
3491
|
+
h+='<span><span class="dev-name">'+esc(deviceHeadline(d))+'</span><br>';
|
|
3492
|
+
h+='<span class="dev-sub">'+esc(d.deviceName||'device not named')+'</span></span>';
|
|
3493
|
+
h+='</div><span class="dev-rule"></span><div class="dev-facts">';
|
|
3494
|
+
var facts=[{k:'Driver',v:d.automationName},{k:'App',v:d.appId},{k:'Mode',v:d.appTestMode}];
|
|
3495
|
+
for(var f=0;f<facts.length;f++){
|
|
3496
|
+
if(!facts[f].v)continue;
|
|
3497
|
+
h+='<span class="dev-fact"><span class="dev-fact-k">'+esc(facts[f].k)+'</span>';
|
|
3498
|
+
h+='<span class="dev-fact-v">'+esc(shortAppId(facts[f].v))+'</span></span>';
|
|
3499
|
+
}
|
|
3500
|
+
h+='</div>';
|
|
3501
|
+
if(count>1)h+='<span class="dev-more">'+count+' devices in this run</span>';
|
|
3502
|
+
el.innerHTML=h+'</div>';
|
|
3503
|
+
}
|
|
3504
|
+
|
|
3505
|
+
function deviceBadge(d){
|
|
3506
|
+
if(!d||!d.platform||d.platform==='unknown')return '';
|
|
3507
|
+
var cls=d.platform==='ios'?'trb trb-device-ios':d.platform==='web'?'trb trb-device-web':'trb trb-device';
|
|
3508
|
+
var label=deviceHeadline(d);
|
|
3509
|
+
if(d.deviceName)label+=' \\u00b7 '+d.deviceName;
|
|
3510
|
+
return '<span class="'+cls+'" title="'+esc(label)+'">'+esc(label)+'</span>';
|
|
3511
|
+
}
|
|
3512
|
+
|
|
3513
|
+
/* \u2500\u2500 Mobile cards \u2500\u2500 */
|
|
3514
|
+
|
|
3515
|
+
var KIND_ORDER=['interaction','assertion','gesture','lifecycle','device','keyboard','navigation','find','alert','context','screenshot','media','script','flow_control','ai','other'];
|
|
3516
|
+
var KIND_COLOR={interaction:'#60a5fa',assertion:'#34d399',gesture:'#e879f9',lifecycle:'#fbbf24',device:'#fb923c',keyboard:'#f59e0b',navigation:'#a78bfa',find:'#8b949e',alert:'#ef4444',context:'#484f58',screenshot:'#60a5fa',media:'#60a5fa',script:'#484f58',flow_control:'#484f58',ai:'#2dd4a8',other:'#484f58'};
|
|
3517
|
+
|
|
3518
|
+
function countKinds(steps,into){
|
|
3519
|
+
for(var i=0;i<steps.length;i++){
|
|
3520
|
+
var k=steps[i].kind||'other';
|
|
3521
|
+
into[k]=(into[k]||0)+1;
|
|
3522
|
+
if(steps[i].children&&steps[i].children.length)countKinds(steps[i].children,into);
|
|
3523
|
+
}
|
|
3524
|
+
}
|
|
3525
|
+
|
|
3526
|
+
function renderStepKindCard(){
|
|
3527
|
+
var ts=flowTests(),counts={},total=0,k;
|
|
3528
|
+
for(var i=0;i<ts.length;i++)countKinds(ts[i].actions||[],counts);
|
|
3529
|
+
for(k in counts){if(counts.hasOwnProperty(k))total+=counts[k];}
|
|
3530
|
+
if(total===0)return '';
|
|
3531
|
+
var present=[];
|
|
3532
|
+
for(var o=0;o<KIND_ORDER.length;o++){
|
|
3533
|
+
if(counts[KIND_ORDER[o]])present.push({k:KIND_ORDER[o],n:counts[KIND_ORDER[o]]});
|
|
3534
|
+
}
|
|
3535
|
+
present.sort(function(a,b){return b.n-a.n;});
|
|
3536
|
+
var max=present[0].n||1;
|
|
3537
|
+
var h='<div class="tcard"><div class="tcard-h">By step category<span class="tcard-sub">'+total+' steps</span></div>';
|
|
3538
|
+
for(var p=0;p<Math.min(present.length,7);p++){
|
|
3539
|
+
var c=KIND_COLOR[present[p].k]||'#8b949e';
|
|
3540
|
+
var pct=Math.max(2,Math.round((present[p].n/max)*100));
|
|
3541
|
+
h+='<div class="krow"><span class="krow-k" style="color:'+c+'">'+esc(present[p].k)+'</span>';
|
|
3542
|
+
h+='<span class="krow-track"><span class="krow-fill" style="width:'+pct+'%;background:'+c+'"></span></span>';
|
|
3543
|
+
h+='<span class="krow-n">'+present[p].n+'</span></div>';
|
|
3544
|
+
}
|
|
3545
|
+
return h+'</div>';
|
|
3546
|
+
}
|
|
3547
|
+
|
|
3548
|
+
function renderDriverLogCard(){
|
|
3549
|
+
var ts=flowTests(),total=0,errors=0,warns=0,sample=null;
|
|
3550
|
+
for(var i=0;i<ts.length;i++){
|
|
3551
|
+
var logs=ts[i].consoleLogs||[];
|
|
3552
|
+
total+=logs.length;
|
|
3553
|
+
for(var j=0;j<logs.length;j++){
|
|
3554
|
+
if(logs[j].level==='error'){errors++;if(!sample)sample=logs[j];}
|
|
3555
|
+
else if(logs[j].level==='warn')warns++;
|
|
3556
|
+
}
|
|
3557
|
+
}
|
|
3558
|
+
if(total===0)return '';
|
|
3559
|
+
var other=Math.max(0,total-errors-warns);
|
|
3560
|
+
var h='<div class="tcard"><div class="tcard-h">Driver log<span class="tcard-sub">'+total+' lines</span></div>';
|
|
3561
|
+
h+='<div class="verdict-bar">';
|
|
3562
|
+
if(errors)h+='<span style="flex:'+errors+';background:#ef4444;border-radius:999px"></span>';
|
|
3563
|
+
if(warns)h+='<span style="flex:'+warns+';background:#f59e0b;border-radius:999px"></span>';
|
|
3564
|
+
if(other)h+='<span style="flex:'+other+';background:rgba(255,255,255,.08);border-radius:999px"></span>';
|
|
3565
|
+
h+='</div><div class="lgrid">';
|
|
3566
|
+
h+='<div class="lrow"><span class="lrow-src" style="background:rgba(239,68,68,.12);color:#ef4444">error</span><span class="lrow-l">lines</span><span class="lrow-n" style="color:#ef4444">'+errors+'</span></div>';
|
|
3567
|
+
h+='<div class="lrow"><span class="lrow-src" style="background:rgba(245,158,11,.12);color:#f59e0b">warn</span><span class="lrow-l">lines</span><span class="lrow-n" style="color:#f59e0b">'+warns+'</span></div>';
|
|
3568
|
+
h+='</div>';
|
|
3569
|
+
if(sample)h+='<div class="lsample">'+esc(String(sample.text||''))+'</div>';
|
|
3570
|
+
return h+'</div>';
|
|
3571
|
+
}
|
|
3572
|
+
|
|
3573
|
+
function renderAiDefectCard(){
|
|
3574
|
+
var ts=flowTests(),bySev={critical:0,warning:0,info:0},total=0;
|
|
3575
|
+
for(var i=0;i<ts.length;i++){
|
|
3576
|
+
var ds=ts[i].aiDefects||[];
|
|
3577
|
+
for(var j=0;j<ds.length;j++){total++;bySev[ds[j].severity]=(bySev[ds[j].severity]||0)+1;}
|
|
3578
|
+
}
|
|
3579
|
+
if(total===0)return '';
|
|
3580
|
+
var h='<div class="tcard"><div class="tcard-h">AI defects</div>';
|
|
3581
|
+
h+='<div class="tbig"><span class="tbig-n">'+total+'</span><span class="tbig-l">found</span></div><div class="lgrid">';
|
|
3582
|
+
var rows=[{k:'critical',c:'#ef4444'},{k:'warning',c:'#f59e0b'},{k:'info',c:'#60a5fa'}];
|
|
3583
|
+
for(var r=0;r<rows.length;r++){
|
|
3584
|
+
h+='<div class="lrow"><span class="vc-dot" style="background:'+rows[r].c+'"></span>';
|
|
3585
|
+
h+='<span class="lrow-l">'+rows[r].k+'</span><span class="lrow-n">'+(bySev[rows[r].k]||0)+'</span></div>';
|
|
3586
|
+
}
|
|
3587
|
+
return h+'</div></div>';
|
|
3588
|
+
}
|
|
3589
|
+
|
|
3590
|
+
function renderTypeBand(){
|
|
3591
|
+
var el=$('type-band');
|
|
3592
|
+
if(!el)return;
|
|
3593
|
+
var cards=[];
|
|
3594
|
+
var a=renderStepKindCard();if(a)cards.push(a);
|
|
3595
|
+
var b=renderDriverLogCard();if(b)cards.push(b);
|
|
3596
|
+
var c=renderAiDefectCard();if(c)cards.push(c);
|
|
3597
|
+
el.innerHTML=cards.length?'<div class="tband">'+cards.join('')+'</div>':'';
|
|
3598
|
+
}
|
|
3599
|
+
|
|
3600
|
+
/* \u2500\u2500 Needs attention \u2500\u2500 */
|
|
3601
|
+
|
|
3602
|
+
function failingStep(t){
|
|
3603
|
+
var acts=t.actions||[];
|
|
3604
|
+
for(var i=0;i<acts.length;i++)if(acts[i].status==='failed')return acts[i];
|
|
3605
|
+
return null;
|
|
3606
|
+
}
|
|
3607
|
+
|
|
3608
|
+
function correlatedLog(t,atMs){
|
|
3609
|
+
var logs=t.consoleLogs||[],best=null,bestGap=Infinity;
|
|
3610
|
+
for(var i=0;i<logs.length;i++){
|
|
3611
|
+
var e=logs[i];
|
|
3612
|
+
if(e.level!=='error')continue;
|
|
3613
|
+
var ms=Date.parse(e.timestamp);
|
|
3614
|
+
if(!isFinite(ms))continue;
|
|
3615
|
+
var gap=Math.abs(ms-atMs);
|
|
3616
|
+
if(gap<bestGap&&gap<=5000){best=e;bestGap=gap;}
|
|
3617
|
+
}
|
|
3618
|
+
return best;
|
|
3619
|
+
}
|
|
3620
|
+
|
|
3621
|
+
function mobileEvidenceChips(t){
|
|
3622
|
+
var h='',acts=t.actions||[],gestures=0,failedSteps=0,i;
|
|
3623
|
+
for(i=0;i<acts.length;i++){
|
|
3624
|
+
if(acts[i].kind==='gesture')gestures++;
|
|
3625
|
+
if(acts[i].status==='failed')failedSteps++;
|
|
3626
|
+
}
|
|
3627
|
+
if(acts.length>0)h+='<span class="nchip'+(failedSteps>0?' nchip-bad':'')+'">'+acts.length+' step'+(acts.length===1?'':'s')+(failedSteps>0?' \\u00b7 '+failedSteps+' failed':'')+'</span>';
|
|
3628
|
+
if(gestures>0)h+='<span class="nchip nchip-gesture">'+gestures+' gesture'+(gestures===1?'':'s')+'</span>';
|
|
3629
|
+
var errs=0,logs=t.consoleLogs||[];
|
|
3630
|
+
for(i=0;i<logs.length;i++)if(logs[i].level==='error')errs++;
|
|
3631
|
+
if(errs>0)h+='<span class="nchip nchip-bad">'+errs+' log error'+(errs===1?'':'s')+'</span>';
|
|
3632
|
+
var shots=testScreenshots(t).length;
|
|
3633
|
+
if(shots>0)h+='<span class="nchip">'+shots+' screenshot'+(shots===1?'':'s')+'</span>';
|
|
3634
|
+
if(t.artifacts&&t.artifacts.video)h+='<span class="nchip">recording</span>';
|
|
3635
|
+
var defects=(t.aiDefects||[]).length;
|
|
3636
|
+
if(defects>0)h+='<span class="nchip nchip-bad">'+defects+' AI defect'+(defects===1?'':'s')+'</span>';
|
|
3637
|
+
return h;
|
|
3638
|
+
}
|
|
3639
|
+
|
|
3640
|
+
function renderNeedsAttention(){
|
|
3641
|
+
var el=$('needs-attention');
|
|
3642
|
+
if(!el)return;
|
|
3643
|
+
var bad=[],i;
|
|
3644
|
+
for(i=0;i<flows.length;i++){
|
|
3645
|
+
var st=flows[i].test.status;
|
|
3646
|
+
if(st==='failed'||st==='timedout'||st==='flaky')bad.push(flows[i]);
|
|
3647
|
+
}
|
|
3648
|
+
if(bad.length===0){el.innerHTML='';return;}
|
|
3649
|
+
|
|
3650
|
+
var h='<div class="needs"><div class="needs-head"><span class="needs-title">Needs attention</span>';
|
|
3651
|
+
h+='<span class="needs-count">'+bad.length+'</span></div><div class="needs-list">';
|
|
3652
|
+
for(var j=0;j<Math.min(bad.length,8);j++){
|
|
3653
|
+
var t=bad[j].test;
|
|
3654
|
+
h+='<div class="needs-card" data-flow-idx="'+bad[j].idx+'">';
|
|
3655
|
+
h+='<div class="needs-body"><div class="needs-row1"><span class="needs-name">'+esc(t.title)+'</span>';
|
|
3656
|
+
h+='<span class="trb trb-type">mobile</span>'+deviceBadge(t.device);
|
|
3657
|
+
h+='<span class="needs-file">'+esc(t.filePath)+'</span></div>';
|
|
3658
|
+
|
|
3659
|
+
var step=failingStep(t);
|
|
3660
|
+
if(step){
|
|
3661
|
+
h+='<div class="needs-evidence"><div class="needs-step">';
|
|
3662
|
+
h+='<span class="cmd-kind k-'+esc(step.kind||'other')+'">'+esc(step.kind||'step')+'</span>';
|
|
3663
|
+
h+='<span class="needs-step-t">'+esc(step.title||'')+'</span>';
|
|
3664
|
+
h+='<span class="needs-step-d">'+fmtDur(step.duration||0)+'</span></div>';
|
|
3665
|
+
if(step.error)h+='<div class="needs-cause">'+esc(String(step.error))+'</div>';
|
|
3666
|
+
var log=correlatedLog(t,Date.parse(step.timestamp));
|
|
3667
|
+
if(log){
|
|
3668
|
+
var ts='';
|
|
3669
|
+
try{ts=new Date(log.timestamp).toISOString().substr(11,12);}catch(e){ts='';}
|
|
3670
|
+
h+='<div class="needs-log"><span class="needs-log-t">driver log \\u00b7 '+esc(ts)+'</span> '+esc(String(log.text||''))+'</div>';
|
|
3671
|
+
}
|
|
3672
|
+
h+='</div>';
|
|
3673
|
+
}else if(t.failure&&t.failure.message){
|
|
3674
|
+
h+='<div class="needs-cause">'+esc(String(t.failure.message).split('\\n')[0])+'</div>';
|
|
3675
|
+
}
|
|
3676
|
+
|
|
3677
|
+
h+='<div class="needs-chips">'+mobileEvidenceChips(t)+'</div></div>';
|
|
3678
|
+
h+='<div class="needs-right"><div class="needs-dur">'+fmtDur(t.duration)+'</div>';
|
|
3679
|
+
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></div>';
|
|
3680
|
+
}
|
|
3681
|
+
el.innerHTML=h+'</div></div>';
|
|
3682
|
+
}
|
|
3683
|
+
|
|
3684
|
+
/* \u2500\u2500 Per-flow evidence \u2500\u2500 */
|
|
3685
|
+
|
|
3686
|
+
/* Screenshots belong to the flow that produced them. The Screenshots tab
|
|
3687
|
+
took no flow argument and showed the whole run's shots on every flow. */
|
|
3688
|
+
function testScreenshots(t){
|
|
3689
|
+
var a=t.artifacts;
|
|
3690
|
+
if(a&&a.screenshots&&a.screenshots.length)return a.screenshots;
|
|
3691
|
+
if(a&&a.screenshot)return [a.screenshot];
|
|
3692
|
+
return [];
|
|
3693
|
+
}
|
|
3694
|
+
|
|
3695
|
+
function renderShotStrip(t){
|
|
3696
|
+
var shots=testScreenshots(t);
|
|
3697
|
+
if(shots.length===0)return '<div class="artifact-empty">No screenshots for this flow</div>';
|
|
3698
|
+
var h='<div class="shot-strip">';
|
|
3699
|
+
for(var i=0;i<shots.length;i++){
|
|
3700
|
+
var src=String(shots[i]);
|
|
3701
|
+
var name=src.split(/[\\/\\\\]/).pop()||('shot '+(i+1));
|
|
3702
|
+
var isFail=/fail|error/i.test(name);
|
|
3703
|
+
h+='<div class="shot'+(isFail?' is-failure':'')+'" data-lightbox="'+esc(src)+'">';
|
|
3704
|
+
h+='<img src="'+esc(src)+'" alt="'+esc(name)+'">';
|
|
3705
|
+
h+='<div class="shot-cap" title="'+esc(name)+'">'+esc(name)+'</div></div>';
|
|
3706
|
+
}
|
|
3707
|
+
return h+'</div>';
|
|
3708
|
+
}
|
|
3709
|
+
|
|
3710
|
+
/* Steps against the recording, using the videoOffset the timeline builder
|
|
3711
|
+
has always computed and the local report never read. */
|
|
3712
|
+
function renderReplay(t){
|
|
3713
|
+
var video=t.artifacts&&t.artifacts.video;
|
|
3714
|
+
var acts=t.actions||[];
|
|
3715
|
+
if(!video&&acts.length===0)return '<div class="artifact-empty">No recording for this flow</div>';
|
|
3716
|
+
|
|
3717
|
+
var withOffsets=[],i,maxOff=0;
|
|
3718
|
+
for(i=0;i<acts.length;i++){
|
|
3719
|
+
if(typeof acts[i].videoOffset==='number'){
|
|
3720
|
+
withOffsets.push(acts[i]);
|
|
3721
|
+
if(acts[i].videoOffset>maxOff)maxOff=acts[i].videoOffset;
|
|
3722
|
+
}
|
|
3723
|
+
}
|
|
3724
|
+
var span=maxOff>0?maxOff:(t.duration||0)/1000;
|
|
3725
|
+
if(span<=0)span=1;
|
|
3726
|
+
|
|
3727
|
+
var h='<div class="replay">';
|
|
3728
|
+
if(video)h+='<div class="replay-video"><video id="replay-video" src="'+esc(video)+'" controls preload="metadata"></video></div>';
|
|
3729
|
+
h+='<div class="replay-side">';
|
|
3730
|
+
if(withOffsets.length>0){
|
|
3731
|
+
h+='<div class="scrub">';
|
|
3732
|
+
for(i=0;i<withOffsets.length;i++){
|
|
3733
|
+
var w=withOffsets[i],pct=Math.min(99,Math.max(0,(w.videoOffset/span)*100));
|
|
3734
|
+
var failed=w.status==='failed';
|
|
3735
|
+
var col=failed?'#ef4444':(KIND_COLOR[w.kind||'other']||'#8b949e');
|
|
3736
|
+
h+='<span class="scrub-mark'+(failed?' is-failed':'')+'" style="left:'+pct.toFixed(2)+'%;background:'+col+'" data-seek="'+w.videoOffset.toFixed(2)+'" title="'+esc(w.title||'')+'"></span>';
|
|
3737
|
+
}
|
|
3738
|
+
h+='</div><div class="scrub-legend"><span>0.0s</span><span>'+span.toFixed(1)+'s</span></div>';
|
|
3739
|
+
}
|
|
3740
|
+
h+='<div class="replay-steps">';
|
|
3741
|
+
for(i=0;i<acts.length;i++){
|
|
3742
|
+
var a=acts[i],failed2=a.status==='failed';
|
|
3743
|
+
h+='<div class="rstep'+(failed2?' is-failed':'')+'"'+(typeof a.videoOffset==='number'?' data-seek="'+a.videoOffset.toFixed(2)+'"':'')+'>';
|
|
3744
|
+
h+='<span class="rstep-n">'+(i+1<10?'0':'')+(i+1)+'</span>';
|
|
3745
|
+
h+='<span class="cmd-kind k-'+esc(a.kind||'other')+'">'+esc(a.kind||'step')+'</span>';
|
|
3746
|
+
h+='<span class="rstep-t" title="'+esc(a.title||'')+'">'+esc(a.title||'')+'</span>';
|
|
3747
|
+
if(typeof a.videoOffset==='number')h+='<span class="rstep-off">'+a.videoOffset.toFixed(1)+'s</span>';
|
|
3748
|
+
h+='<span class="rstep-d">'+fmtDur(a.duration||0)+'</span></div>';
|
|
3749
|
+
}
|
|
3750
|
+
return h+'</div></div></div>';
|
|
3751
|
+
}
|
|
3752
|
+
|
|
3753
|
+
/* The driver log, per flow. Maestro's report had no log surface at all. */
|
|
3754
|
+
function renderDriverLogTab(t){
|
|
3755
|
+
var logs=t.consoleLogs||[];
|
|
3756
|
+
if(logs.length===0)return '<div class="artifact-empty">No driver log lines for this flow</div>';
|
|
3757
|
+
var errs=0,warns=0,i;
|
|
3758
|
+
for(i=0;i<logs.length;i++){
|
|
3759
|
+
if(logs[i].level==='error')errs++;else if(logs[i].level==='warn')warns++;
|
|
3760
|
+
}
|
|
3761
|
+
var h='<div class="console-panel"><div class="console-header">';
|
|
3762
|
+
h+='<span class="console-title">Driver log ('+logs.length+')</span>';
|
|
3763
|
+
if(errs>0)h+='<span class="apisec-badge ab-bad" style="background:rgba(239,68,68,.12);color:#ef4444;font-size:10px;font-weight:700;padding:1px 7px;border-radius:4px">'+errs+' errors</span>';
|
|
3764
|
+
if(warns>0)h+='<span style="background:rgba(245,158,11,.12);color:#f59e0b;font-size:10px;font-weight:700;padding:1px 7px;border-radius:4px">'+warns+' warnings</span>';
|
|
3765
|
+
h+='</div><div class="console-list">';
|
|
3766
|
+
var SHOW=200;
|
|
3767
|
+
for(i=0;i<Math.min(logs.length,SHOW);i++)h+=renderLogRow(logs[i]);
|
|
3768
|
+
h+='</div>';
|
|
3769
|
+
if(logs.length>SHOW)h+='<div class="console-more">'+(logs.length-SHOW)+' more lines not shown</div>';
|
|
3770
|
+
return h+'</div>';
|
|
3771
|
+
}
|
|
3772
|
+
|
|
3773
|
+
function renderLogRow(e){
|
|
3774
|
+
var lv=(e.level||'info').toLowerCase();
|
|
3775
|
+
var color=lv==='error'?'#f87171':lv==='warn'?'#fbbf24':lv==='debug'?'#a78bfa':'#60a5fa';
|
|
3776
|
+
var badge=lv==='error'?'E':lv==='warn'?'W':lv==='debug'?'D':'I';
|
|
3777
|
+
var h='<div class="console-entry console-level-'+esc(lv)+'" data-console-level="'+esc(lv)+'">';
|
|
3778
|
+
h+='<span class="console-level-badge" style="color:'+color+'">'+badge+'</span>';
|
|
3779
|
+
var ts='';
|
|
3780
|
+
try{ts=new Date(e.timestamp).toISOString().substr(11,12);}catch(err){ts=String(e.timestamp||'');}
|
|
3781
|
+
h+='<span class="console-time">'+esc(ts)+'</span>';
|
|
3782
|
+
if(e.tag)h+='<span class="console-tag" title="'+esc(String(e.tag))+'">'+esc(String(e.tag))+'</span>';
|
|
3783
|
+
h+='<span class="console-text">'+esc(String(e.text||''))+'</span></div>';
|
|
3784
|
+
return h;
|
|
3785
|
+
}
|
|
3786
|
+
|
|
3787
|
+
function renderFlowAiDefects(t){
|
|
3788
|
+
var defects=t.aiDefects||[];
|
|
3789
|
+
if(defects.length===0)return '<div class="artifact-empty">No AI defects for this flow</div>';
|
|
3790
|
+
var h='';
|
|
3791
|
+
for(var i=0;i<defects.length;i++){
|
|
3792
|
+
var d=defects[i];
|
|
3793
|
+
h+='<div class="aidef aidef-'+esc(d.severity||'info')+'">';
|
|
3794
|
+
if(d.screenshot)h+='<div class="aidef-shot"><img src="'+esc(d.screenshot)+'" alt=""></div>';
|
|
3795
|
+
h+='<div class="aidef-body"><div class="aidef-pills">';
|
|
3796
|
+
h+='<span class="aidef-sev aidef-sev-'+esc(d.severity||'info')+'">'+esc(d.severity||'info')+'</span>';
|
|
3797
|
+
h+='<span class="aidef-type">'+esc(d.type||'ui')+'</span></div>';
|
|
3798
|
+
h+='<div class="aidef-desc">'+esc(d.description||'')+'</div>';
|
|
3799
|
+
if(d.location)h+='<div class="aidef-loc">'+esc(d.location)+'</div>';
|
|
3800
|
+
h+='</div></div>';
|
|
3801
|
+
}
|
|
3802
|
+
return h;
|
|
3803
|
+
}
|
|
3804
|
+
|
|
3805
|
+
/* Clicking a step or a scrub mark seeks the recording. */
|
|
3806
|
+
document.addEventListener('click',function(e){
|
|
3807
|
+
var el=e.target.closest?e.target.closest('[data-seek]'):null;
|
|
3808
|
+
if(el){
|
|
3809
|
+
var v=document.getElementById('replay-video');
|
|
3810
|
+
if(v){
|
|
3811
|
+
var at=parseFloat(el.getAttribute('data-seek'));
|
|
3812
|
+
if(isFinite(at)){try{v.currentTime=at;}catch(err){}}
|
|
3813
|
+
}
|
|
3814
|
+
return;
|
|
3815
|
+
}
|
|
3816
|
+
var card=e.target.closest?e.target.closest('.needs-card'):null;
|
|
3817
|
+
if(card){
|
|
3818
|
+
var idx=parseInt(card.getAttribute('data-flow-idx'),10);
|
|
3819
|
+
if(!isNaN(idx)&&typeof openDrawer==='function')openDrawer(idx);
|
|
3820
|
+
}
|
|
3821
|
+
});
|
|
3822
|
+
`;
|
|
3823
|
+
|
|
3824
|
+
// src/html-css-mobile.ts
|
|
3825
|
+
var CSS_MOBILE = `
|
|
3826
|
+
/* \u2500\u2500 Device strip \u2500\u2500 */
|
|
3827
|
+
.devstrip{background:var(--bg-1);border:1px solid var(--bd);border-radius:12px;padding:14px 20px;display:flex;align-items:center;gap:26px;flex-wrap:wrap;margin-bottom:20px}
|
|
3828
|
+
.dev-id{display:flex;align-items:center;gap:11px}
|
|
3829
|
+
.dev-icon{width:34px;height:34px;border-radius:9px;display:flex;align-items:center;justify-content:center;flex-shrink:0}
|
|
3830
|
+
.dev-icon-android{background:rgba(52,211,153,.1);border:1px solid rgba(52,211,153,.22);color:#34d399}
|
|
3831
|
+
.dev-icon-ios{background:rgba(96,165,250,.1);border:1px solid rgba(96,165,250,.22);color:#60a5fa}
|
|
3832
|
+
.dev-icon-web{background:rgba(167,139,250,.1);border:1px solid rgba(167,139,250,.22);color:#a78bfa}
|
|
3833
|
+
.dev-icon-unknown{background:rgba(255,255,255,.04);border:1px solid var(--bd);color:var(--fg-1)}
|
|
3834
|
+
.dev-name{font-size:14px;font-weight:700;line-height:1.25}
|
|
3835
|
+
.dev-sub{font-size:10.5px;color:var(--fg-1)}
|
|
3836
|
+
.dev-rule{width:1px;height:30px;background:var(--bd)}
|
|
3837
|
+
.dev-facts{display:flex;gap:28px;flex-wrap:wrap}
|
|
3838
|
+
.dev-fact{display:flex;flex-direction:column;gap:2px}
|
|
3839
|
+
.dev-fact-k{font-size:9.5px;color:var(--fg-2);letter-spacing:.06em;text-transform:uppercase}
|
|
3840
|
+
.dev-fact-v{font-size:11.5px;color:var(--fg);font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}
|
|
3841
|
+
.dev-more{margin-left:auto;font-size:11px;color:var(--fg-1);border:1px solid var(--bd);border-radius:8px;padding:5px 11px;display:flex;align-items:center;gap:7px}
|
|
3842
|
+
|
|
3843
|
+
/* Row + drawer device badge */
|
|
3844
|
+
.trb-device{background:rgba(52,211,153,.1);color:#34d399}
|
|
3845
|
+
.trb-device-ios{background:rgba(96,165,250,.1);color:#60a5fa}
|
|
3846
|
+
.trb-device-web{background:rgba(167,139,250,.1);color:#a78bfa}
|
|
3847
|
+
|
|
3848
|
+
/* \u2500\u2500 Verdict band \u2500\u2500 */
|
|
3849
|
+
.verdict{display:flex;gap:16px;align-items:stretch;margin-bottom:20px;flex-wrap:wrap}
|
|
3850
|
+
.verdict-main{flex:1 1 480px;background:var(--bg-1);border:1px solid var(--bd);border-radius:12px;padding:18px 22px;display:flex;flex-direction:column;gap:14px;min-width:0}
|
|
3851
|
+
.verdict-main.v-failed{border-color:rgba(239,68,68,.22)}
|
|
3852
|
+
.verdict-main.v-passed{border-color:rgba(34,197,94,.18)}
|
|
3853
|
+
.verdict-head{display:flex;align-items:baseline;gap:12px;flex-wrap:wrap}
|
|
3854
|
+
.verdict-title{font-size:21px;font-weight:700;letter-spacing:-.01em}
|
|
3855
|
+
.verdict-where{font-size:12px;color:var(--fg-1)}
|
|
3856
|
+
.verdict-where code{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;color:var(--fg)}
|
|
3857
|
+
.verdict-dur{margin-left:auto;display:flex;align-items:center;gap:6px;font-size:12px;color:var(--fg-1)}
|
|
3858
|
+
.verdict-bar{display:flex;height:8px;gap:2px}
|
|
3859
|
+
.verdict-bar>span{border-radius:999px}
|
|
3860
|
+
.verdict-bar-empty{flex:1;background:rgba(255,255,255,.06)}
|
|
3861
|
+
.vb-passed{background:#22c55e}.vb-failed{background:#ef4444}.vb-flaky{background:#f59e0b}
|
|
3862
|
+
.vb-timedout{background:#f97316}.vb-skipped{background:#6b7280}
|
|
3863
|
+
.verdict-counts{display:flex;align-items:center;gap:20px;flex-wrap:wrap}
|
|
3864
|
+
.vc{display:flex;align-items:center;gap:7px}
|
|
3865
|
+
.vc-dot{width:9px;height:9px;border-radius:50%}
|
|
3866
|
+
.vc-n{font-size:14px;font-weight:700}
|
|
3867
|
+
.vc-l{font-size:12px;color:var(--fg-1)}
|
|
3868
|
+
.verdict-zeros{margin-left:auto;font-size:11px;color:var(--fg-2)}
|
|
3869
|
+
.verdict-side{width:300px;flex:0 0 auto;background:var(--bg-1);border:1px solid var(--bd);border-radius:12px;padding:18px 20px;display:flex;flex-direction:column;gap:12px}
|
|
3870
|
+
.verdict-side-label{font-size:10px;font-weight:600;color:var(--fg-2);letter-spacing:.06em;text-transform:uppercase}
|
|
3871
|
+
.verdict-side-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px 10px}
|
|
3872
|
+
.vs-n{font-size:17px;font-weight:700}
|
|
3873
|
+
.vs-n small{font-size:11px;font-weight:500;color:var(--fg-1)}
|
|
3874
|
+
.vs-l{font-size:10px;color:var(--fg-1)}
|
|
3875
|
+
.vs-bad{color:#ef4444}
|
|
3876
|
+
|
|
3877
|
+
/* \u2500\u2500 Type band cards \u2500\u2500 */
|
|
3878
|
+
.tband{display:grid;gap:16px;margin-bottom:20px;grid-template-columns:repeat(auto-fit,minmax(300px,1fr))}
|
|
3879
|
+
.tcard{background:var(--bg-1);border:1px solid var(--bd);border-radius:12px;padding:16px 20px;display:flex;flex-direction:column;gap:12px;min-width:0}
|
|
3880
|
+
.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}
|
|
3881
|
+
.tcard-sub{margin-left:auto;font-weight:500;letter-spacing:0;text-transform:none;font-size:10px;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}
|
|
3882
|
+
.krow{display:flex;align-items:center;gap:10px}
|
|
3883
|
+
.krow-k{width:78px;font-size:11px;font-weight:700}
|
|
3884
|
+
.krow-track{display:block;flex:1;height:6px;border-radius:999px;background:rgba(255,255,255,.06);overflow:hidden}
|
|
3885
|
+
.krow-fill{display:block;height:100%;border-radius:999px;min-width:3px}
|
|
3886
|
+
.krow-n{font-size:11px;color:var(--fg-1);width:34px;text-align:right}
|
|
3887
|
+
.lgrid{display:flex;flex-direction:column;gap:7px}
|
|
3888
|
+
.lrow{display:flex;align-items:center;gap:9px}
|
|
3889
|
+
.lrow-src{font-size:9px;font-weight:700;padding:1px 6px;border-radius:4px}
|
|
3890
|
+
.lrow-l{font-size:11px;color:var(--fg-1);flex:1}
|
|
3891
|
+
.lrow-n{font-size:12px;font-weight:700}
|
|
3892
|
+
.lsample{font-size:10px;color:var(--fg-2);border-top:1px solid var(--bd);padding-top:8px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}
|
|
3893
|
+
.tbig{display:flex;align-items:baseline;gap:8px}
|
|
3894
|
+
.tbig-n{font-size:28px;font-weight:800;line-height:1}
|
|
3895
|
+
.tbig-l{font-size:11px;color:var(--fg-1)}
|
|
3896
|
+
|
|
3897
|
+
/* \u2500\u2500 Needs attention \u2500\u2500 */
|
|
3898
|
+
.needs{margin-bottom:20px}
|
|
3899
|
+
.needs-head{display:flex;align-items:center;gap:8px;padding:0 2px 10px}
|
|
3900
|
+
.needs-title{font-size:11px;font-weight:700;letter-spacing:.06em;text-transform:uppercase}
|
|
3901
|
+
.needs-count{font-size:10px;font-weight:700;color:#ef4444;background:rgba(239,68,68,.12);padding:1px 7px;border-radius:4px}
|
|
3902
|
+
.needs-list{display:flex;flex-direction:column;gap:10px}
|
|
3903
|
+
.needs-card{background:var(--bg-1);border:1px solid rgba(239,68,68,.2);border-left:3px solid #ef4444;border-radius:10px;padding:14px 18px;display:flex;gap:20px;align-items:flex-start;cursor:pointer}
|
|
3904
|
+
.needs-card:hover{border-color:rgba(239,68,68,.4)}
|
|
3905
|
+
.needs-card.nc-flaky{border-color:rgba(245,158,11,.2);border-left-color:#f59e0b}
|
|
3906
|
+
.needs-body{flex:1;min-width:0;display:flex;flex-direction:column;gap:9px}
|
|
3907
|
+
.needs-row1{display:flex;align-items:center;gap:10px;flex-wrap:wrap}
|
|
3908
|
+
.needs-name{font-size:14px;font-weight:600}
|
|
3909
|
+
.needs-file{font-size:11px;color:var(--fg-2);font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}
|
|
3910
|
+
.needs-cause{font-size:12.5px;color:var(--fg-err)}
|
|
3911
|
+
.needs-cause em{font-style:normal}
|
|
3912
|
+
.needs-evidence{background:var(--bg-code);border:1px solid var(--bd);border-radius:8px;padding:10px 13px;display:flex;flex-direction:column;gap:7px}
|
|
3913
|
+
.needs-step{display:flex;align-items:center;gap:9px;flex-wrap:wrap}
|
|
3914
|
+
.needs-step-t{font-size:11px;color:#d4d4d4;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}
|
|
3915
|
+
.needs-step-d{margin-left:auto;font-size:10px;color:var(--fg-2)}
|
|
3916
|
+
.needs-log{font-size:10.5px;color:#fb923c;border-top:1px solid var(--bd);padding-top:7px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}
|
|
3917
|
+
.needs-log-t{color:var(--fg-2)}
|
|
3918
|
+
.needs-chips{display:flex;gap:6px;flex-wrap:wrap;align-items:center}
|
|
3919
|
+
.nchip{display:flex;align-items:center;gap:5px;font-size:10px;color:var(--fg-1);background:var(--bg-2);border:1px solid var(--bd);padding:2px 8px;border-radius:999px}
|
|
3920
|
+
.nchip-bad{color:#ef4444;background:rgba(239,68,68,.1);border-color:rgba(239,68,68,.22)}
|
|
3921
|
+
.nchip-gesture{color:#e879f9;background:rgba(232,121,249,.1);border-color:rgba(232,121,249,.22)}
|
|
3922
|
+
.needs-right{display:flex;flex-direction:column;align-items:flex-end;gap:8px;flex-shrink:0}
|
|
3923
|
+
.needs-dur{font-size:13px;font-weight:700;color:var(--fg-1)}
|
|
3924
|
+
.needs-open{display:flex;align-items:center;gap:4px;font-size:11px;font-weight:600;color:#2dd4a8}
|
|
3925
|
+
|
|
3926
|
+
/* \u2500\u2500 Step kind badges \u2500\u2500 */
|
|
3927
|
+
.cmd-kind{font-size:9px;font-weight:700;padding:1px 6px;border-radius:999px;letter-spacing:.04em;text-transform:uppercase;flex-shrink:0;min-width:60px;text-align:center}
|
|
3928
|
+
.k-gesture{background:rgba(232,121,249,.12);color:#e879f9}
|
|
3929
|
+
.k-interaction{background:rgba(96,165,250,.12);color:#60a5fa}
|
|
3930
|
+
.k-assertion{background:rgba(52,211,153,.12);color:#34d399}
|
|
3931
|
+
.k-lifecycle{background:rgba(251,191,36,.12);color:#fbbf24}
|
|
3932
|
+
.k-device{background:rgba(251,146,60,.12);color:#fb923c}
|
|
3933
|
+
.k-keyboard{background:rgba(245,158,11,.12);color:#f59e0b}
|
|
3934
|
+
.k-navigation{background:rgba(167,139,250,.12);color:#a78bfa}
|
|
3935
|
+
.k-find{background:rgba(255,255,255,.05);color:var(--fg-1)}
|
|
3936
|
+
.k-alert{background:rgba(239,68,68,.12);color:#ef4444}
|
|
3937
|
+
.k-screenshot,.k-media{background:rgba(96,165,250,.1);color:#60a5fa}
|
|
3938
|
+
.k-context,.k-script,.k-flow_control,.k-other{background:rgba(255,255,255,.04);color:var(--fg-2)}
|
|
3939
|
+
.k-ai{background:rgba(3,183,156,.14);color:#2dd4a8}
|
|
3940
|
+
.cmd-selector{font-size:10px;color:var(--fg-2);font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:220px}
|
|
3941
|
+
|
|
3942
|
+
/* \u2500\u2500 Device log panel \u2500\u2500
|
|
3943
|
+
Maestro had no log surface at all, so it has none of these base classes;
|
|
3944
|
+
the new Driver log tab rendered as unstyled text without them. */
|
|
3945
|
+
.console-panel{border:1px solid var(--bd);border-radius:8px;overflow:hidden;background:var(--bg-1)}
|
|
3946
|
+
.console-header{font-size:11px;font-weight:700;color:var(--fg-1);text-transform:uppercase;letter-spacing:.06em;display:flex;align-items:center;gap:8px;padding:8px 12px;border-bottom:1px solid var(--bd);background:var(--bg-2)}
|
|
3947
|
+
.console-title{font-size:11px;font-weight:700}
|
|
3948
|
+
.console-list{max-height:400px;overflow-y:auto;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:11px;background:var(--bg)}
|
|
3949
|
+
.console-entry{display:flex;align-items:baseline;gap:8px;padding:3px 10px;border-bottom:1px solid rgba(255,255,255,.03);line-height:20px}
|
|
3950
|
+
.console-entry:hover{background:rgba(255,255,255,.03)}
|
|
3951
|
+
.console-level-badge{font-weight:700;font-size:9px;min-width:14px;text-align:center;flex-shrink:0}
|
|
3952
|
+
.console-time{color:var(--fg-2);font-size:10px;white-space:nowrap;min-width:76px;flex-shrink:0}
|
|
3953
|
+
.console-text{flex:1;min-width:0;word-break:break-word;white-space:pre-wrap;color:var(--fg)}
|
|
3954
|
+
.console-loc{color:var(--fg-2);font-size:10px;white-space:nowrap;margin-left:auto;max-width:180px;overflow:hidden;text-overflow:ellipsis}
|
|
3955
|
+
.console-more{font-size:10.5px;color:var(--fg-2);padding:8px 12px;border-top:1px solid var(--bd)}
|
|
3956
|
+
|
|
3957
|
+
/* \u2500\u2500 Device log provenance \u2500\u2500 */
|
|
3958
|
+
.console-source{font-size:9px;font-weight:700;padding:1px 6px;border-radius:4px;background:rgba(255,255,255,.04);color:var(--fg-1);flex-shrink:0}
|
|
3959
|
+
.console-source-crashlog{background:rgba(239,68,68,.12);color:#ef4444}
|
|
3960
|
+
.console-tag{font-size:10px;color:#fb923c;flex-shrink:0;width:104px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}
|
|
3961
|
+
.console-pid{font-size:10px;color:var(--fg-2);flex-shrink:0;width:52px;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}
|
|
3962
|
+
.console-stack{grid-column:1/-1;font-size:10px;color:var(--fg-2);white-space:pre-wrap;padding:4px 0 6px 24px;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}
|
|
3963
|
+
|
|
3964
|
+
/* \u2500\u2500 Replay: steps against the recording \u2500\u2500 */
|
|
3965
|
+
.replay{display:flex;gap:22px;flex-wrap:wrap}
|
|
3966
|
+
.replay-video{width:232px;flex-shrink:0}
|
|
3967
|
+
.replay-video video{width:100%;border-radius:12px;border:1px solid var(--bd);background:#000;display:block}
|
|
3968
|
+
.replay-side{flex:1;min-width:280px;display:flex;flex-direction:column;gap:12px}
|
|
3969
|
+
.scrub{position:relative;height:10px;border-radius:999px;background:rgba(255,255,255,.06)}
|
|
3970
|
+
.scrub-mark{position:absolute;top:-3px;width:3px;height:16px;border-radius:2px;cursor:pointer}
|
|
3971
|
+
.scrub-mark.is-failed{top:-5px;width:4px;height:20px;background:#ef4444}
|
|
3972
|
+
.scrub-legend{display:flex;justify-content:space-between;font-size:10px;color:var(--fg-2);font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}
|
|
3973
|
+
.replay-steps{display:flex;flex-direction:column;gap:4px;max-height:320px;overflow-y:auto}
|
|
3974
|
+
.rstep{display:flex;align-items:center;gap:10px;padding:6px 10px;border-radius:7px;cursor:pointer}
|
|
3975
|
+
.rstep:hover{background:var(--bg-2)}
|
|
3976
|
+
.rstep.is-failed{background:rgba(239,68,68,.07);border:1px solid rgba(239,68,68,.18)}
|
|
3977
|
+
.rstep-n{font-size:10px;color:var(--fg-2);width:22px;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}
|
|
3978
|
+
.rstep-t{font-size:11.5px;flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}
|
|
3979
|
+
.rstep-off{font-size:10px;color:var(--fg-2);font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}
|
|
3980
|
+
.rstep-d{font-size:11px;color:var(--fg-1);width:52px;text-align:right}
|
|
3981
|
+
.rstep.is-failed .rstep-d{color:#ef4444;font-weight:700}
|
|
3982
|
+
|
|
3983
|
+
/* \u2500\u2500 Screenshot strip: every shot, not only the first \u2500\u2500 */
|
|
3984
|
+
.shot-strip{display:flex;gap:10px;flex-wrap:wrap}
|
|
3985
|
+
.shot{width:118px;border-radius:8px;border:1px solid var(--bd);overflow:hidden;background:var(--bg-2);cursor:pointer}
|
|
3986
|
+
.shot img{width:100%;display:block}
|
|
3987
|
+
.shot-cap{font-size:9.5px;color:var(--fg-2);padding:4px 6px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
3988
|
+
.shot.is-failure{border-color:rgba(239,68,68,.4)}
|
|
3989
|
+
|
|
3990
|
+
/* \u2500\u2500 AI defects \u2500\u2500 */
|
|
3991
|
+
.aidef{border-left:3px solid #6b7280;background:var(--bg-1);border:1px solid var(--bd);border-radius:10px;padding:14px 18px;display:flex;gap:16px;align-items:flex-start;margin-bottom:10px}
|
|
3992
|
+
.aidef-critical{border-left-color:#ef4444}
|
|
3993
|
+
.aidef-warning{border-left-color:#f59e0b}
|
|
3994
|
+
.aidef-info{border-left-color:#60a5fa}
|
|
3995
|
+
.aidef-body{flex:1;min-width:0;display:flex;flex-direction:column;gap:9px}
|
|
3996
|
+
.aidef-pills{display:flex;align-items:center;gap:8px}
|
|
3997
|
+
.aidef-sev{font-size:9px;font-weight:700;padding:1px 7px;border-radius:999px;letter-spacing:.04em;text-transform:uppercase}
|
|
3998
|
+
.aidef-sev-critical{background:rgba(239,68,68,.14);color:#ef4444}
|
|
3999
|
+
.aidef-sev-warning{background:rgba(245,158,11,.14);color:#f59e0b}
|
|
4000
|
+
.aidef-sev-info{background:rgba(96,165,250,.14);color:#60a5fa}
|
|
4001
|
+
.aidef-type{font-size:9px;font-weight:700;padding:1px 7px;border-radius:999px;background:rgba(255,255,255,.04);color:var(--fg-1);letter-spacing:.04em;text-transform:uppercase}
|
|
4002
|
+
.aidef-desc{font-size:13.5px;line-height:1.55}
|
|
4003
|
+
.aidef-loc{font-size:10.5px;color:var(--fg-2);font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}
|
|
4004
|
+
.aidef-shot{width:92px;flex-shrink:0;border-radius:8px;border:1px solid var(--bd);overflow:hidden}
|
|
4005
|
+
.aidef-shot img{width:100%;display:block}
|
|
4006
|
+
|
|
4007
|
+
@media (max-width:900px){
|
|
4008
|
+
.verdict-side{width:100%}
|
|
4009
|
+
}
|
|
4010
|
+
`;
|
|
4011
|
+
|
|
3362
4012
|
// src/html-template.ts
|
|
3363
4013
|
var JS = `
|
|
3364
4014
|
(function(){
|
|
@@ -3373,10 +4023,14 @@ var JS = `
|
|
|
3373
4023
|
${JS_ASSERTIONS}
|
|
3374
4024
|
${JS_AI_DEFECTS}
|
|
3375
4025
|
${JS_INTERACTIONS}
|
|
4026
|
+
${JS_MOBILE}
|
|
3376
4027
|
|
|
3377
4028
|
applyTheme();
|
|
3378
4029
|
renderRunMeta();
|
|
4030
|
+
renderDeviceStrip();
|
|
3379
4031
|
renderHeroStrip();
|
|
4032
|
+
renderTypeBand();
|
|
4033
|
+
renderNeedsAttention();
|
|
3380
4034
|
renderWaterfall();
|
|
3381
4035
|
renderFilterBar();
|
|
3382
4036
|
renderFlowList();
|
|
@@ -3391,10 +4045,10 @@ function renderHtmlDocument(reportJson) {
|
|
|
3391
4045
|
<head>
|
|
3392
4046
|
<meta charset="UTF-8">
|
|
3393
4047
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
3394
|
-
<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: 'self'; connect-src 'self';">
|
|
4048
|
+
<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';">
|
|
3395
4049
|
<title>TestRelic Maestro Test Report</title>
|
|
3396
4050
|
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml;base64,${Buffer.from(LOGO_SVG_RAW).toString("base64")}">
|
|
3397
|
-
<style>${CSS}</style>
|
|
4051
|
+
<style>${CSS}${CSS_MOBILE}</style>
|
|
3398
4052
|
<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>
|
|
3399
4053
|
</head>
|
|
3400
4054
|
<body>
|
|
@@ -3418,7 +4072,10 @@ function renderHtmlDocument(reportJson) {
|
|
|
3418
4072
|
</div>
|
|
3419
4073
|
</div>
|
|
3420
4074
|
|
|
4075
|
+
<div id="device-strip"></div>
|
|
3421
4076
|
<div id="hero-strip" class="hero-strip"></div>
|
|
4077
|
+
<div id="type-band"></div>
|
|
4078
|
+
<div id="needs-attention"></div>
|
|
3422
4079
|
<div id="waterfall" class="waterfall-card"></div>
|
|
3423
4080
|
<div id="filter-bar" class="filter-bar"></div>
|
|
3424
4081
|
<div id="flow-list"></div>
|
|
@@ -4893,7 +5550,7 @@ async function orchestrateReport(input) {
|
|
|
4893
5550
|
const completedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
4894
5551
|
const totalDuration = Date.now() - new Date(startedAt).getTime();
|
|
4895
5552
|
const report = {
|
|
4896
|
-
schemaVersion:
|
|
5553
|
+
schemaVersion: SCHEMA_VERSION,
|
|
4897
5554
|
testRunId,
|
|
4898
5555
|
startedAt,
|
|
4899
5556
|
completedAt,
|