ui5-test-runner 6.0.0-beta.5 → 6.0.0

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.
Files changed (130) hide show
  1. package/README.md +20 -29
  2. package/dist/Command.js +52 -0
  3. package/dist/Npm.js +154 -74
  4. package/dist/browsers/RecoverableBrowserError.js +6 -0
  5. package/dist/browsers/chromeArguments.js +5 -0
  6. package/dist/browsers/chromeOptions.js +29 -0
  7. package/dist/browsers/consoleMessage.js +23 -0
  8. package/dist/browsers/factory.js +168 -3
  9. package/dist/browsers/networkResponse.js +15 -0
  10. package/dist/browsers/playwright.js +104 -0
  11. package/dist/browsers/puppeteer.js +54 -88
  12. package/dist/browsers/seleniumWebdriver.js +124 -0
  13. package/dist/browsers/webdriverio.js +85 -0
  14. package/dist/cli.js +18 -12
  15. package/dist/configuration/CommandLine.js +46 -26
  16. package/dist/configuration/ConfigurationValidator.js +132 -18
  17. package/dist/configuration/OptionValidationError.js +50 -2
  18. package/dist/configuration/derivations.js +8 -0
  19. package/dist/configuration/options.js +380 -23
  20. package/dist/configuration/validations.js +135 -0
  21. package/dist/configuration/validators/boolean.js +1 -1
  22. package/dist/configuration/validators/enumeration.js +7 -0
  23. package/dist/configuration/validators/fsEntry.js +12 -12
  24. package/dist/configuration/validators/index.js +8 -2
  25. package/dist/configuration/validators/integer.js +7 -1
  26. package/dist/configuration/validators/json.js +23 -0
  27. package/dist/configuration/validators/libraryMapping.js +28 -0
  28. package/dist/configuration/validators/percent.js +2 -2
  29. package/dist/configuration/validators/regexp.js +2 -2
  30. package/dist/configuration/validators/secret.js +8 -0
  31. package/dist/configuration/validators/string.js +1 -1
  32. package/dist/configuration/validators/timeout.js +2 -2
  33. package/dist/configuration/validators/url.js +3 -3
  34. package/dist/end.js +58 -0
  35. package/dist/if.js +22 -0
  36. package/dist/modes/Modes.js +2 -1
  37. package/dist/modes/batch/BatchItem.js +1 -0
  38. package/dist/modes/batch/batchTask.js +159 -0
  39. package/dist/modes/batch/index.js +98 -0
  40. package/dist/modes/batch/output.js +22 -0
  41. package/dist/modes/batch/report.js +45 -0
  42. package/dist/modes/batch/resolve.js +105 -0
  43. package/dist/modes/dumpConfig.js +6 -0
  44. package/dist/modes/execute.js +6 -5
  45. package/dist/modes/help.js +86 -2
  46. package/dist/modes/log/LogReader.js +3 -2
  47. package/dist/modes/log/LogStorage.js +31 -27
  48. package/dist/modes/log/index.js +38 -6
  49. package/dist/modes/log/{REserve.js → reserve.js} +25 -2
  50. package/dist/modes/mcp/IDocument.js +2 -0
  51. package/dist/modes/mcp/frontmatter.js +142 -0
  52. package/dist/modes/mcp/index.js +49 -0
  53. package/dist/modes/mcp/knowledgeBase.js +104 -0
  54. package/dist/modes/mcp/remoteKnowledgeBase.js +79 -0
  55. package/dist/modes/mcp/reserve.js +175 -0
  56. package/dist/modes/mcp/tools/getTopic.js +46 -0
  57. package/dist/modes/mcp/tools/index.js +6 -0
  58. package/dist/modes/mcp/tools/listTopics.js +14 -0
  59. package/dist/modes/test/PageContext.js +1 -0
  60. package/dist/modes/test/Server.js +99 -0
  61. package/dist/modes/test/browser.js +31 -17
  62. package/dist/modes/test/browserConfig.js +17 -0
  63. package/dist/modes/test/coverage/collect.js +38 -0
  64. package/dist/modes/test/coverage/index.js +3 -0
  65. package/dist/modes/test/coverage/instrument.js +70 -0
  66. package/dist/modes/test/coverage/nyc.js +15 -0
  67. package/dist/modes/test/coverage/report.js +71 -0
  68. package/dist/modes/test/coverage/settings.js +53 -0
  69. package/dist/modes/test/index.js +94 -34
  70. package/dist/modes/test/pageTask.js +293 -98
  71. package/dist/modes/test/report.js +20 -3
  72. package/dist/modes/test/reserve.js +76 -0
  73. package/dist/modes/test/screenshot.js +49 -0
  74. package/dist/platform/Crypto.js +9 -0
  75. package/dist/platform/Exit.js +33 -24
  76. package/dist/platform/FileSystem.js +6 -2
  77. package/dist/platform/Host.js +9 -1
  78. package/dist/platform/Http.js +54 -29
  79. package/dist/platform/Module.js +2 -1
  80. package/dist/platform/Path.js +5 -1
  81. package/dist/platform/Process.js +67 -13
  82. package/dist/platform/Terminal.js +12 -8
  83. package/dist/platform/Thread.js +1 -1
  84. package/dist/platform/assert.js +13 -12
  85. package/dist/platform/environment.js +9 -18
  86. package/dist/platform/index.js +1 -0
  87. package/dist/platform/logger/CompressionContext.js +117 -0
  88. package/dist/platform/logger/allCompressed.js +8 -7
  89. package/dist/platform/logger/compress.js +29 -129
  90. package/dist/platform/logger/output/BaseLoggerOutput.js +49 -31
  91. package/dist/platform/logger/output/InteractiveLoggerOutput.js +1 -1
  92. package/dist/platform/logger/output/LlmLoggerOutput.js +29 -0
  93. package/dist/platform/logger/output/factory.js +5 -1
  94. package/dist/platform/logger/output.js +24 -6
  95. package/dist/platform/logger/toInternalLogAttributes.js +10 -11
  96. package/dist/platform/logger/traceFileName.js +1 -0
  97. package/dist/platform/logger.js +52 -21
  98. package/dist/platform/mock.js +110 -19
  99. package/dist/platform/version.js +1 -2
  100. package/dist/platform/workerBootstrap.js +1 -1
  101. package/dist/reports/initReportBuilder.js +18 -0
  102. package/dist/reports/junit.js +64 -0
  103. package/dist/reports/saveReport.js +10 -0
  104. package/dist/reports/textReport.js +92 -0
  105. package/dist/reserveLogger.js +22 -0
  106. package/dist/sendToParentProcess.js +6 -0
  107. package/dist/start.js +79 -0
  108. package/dist/types/ISecretValue.js +1 -0
  109. package/dist/types/SecretValue.js +15 -0
  110. package/dist/ui/agent.js +4 -2
  111. package/dist/ui/html-report.js +2 -2
  112. package/dist/ui/lib.js +1 -1
  113. package/dist/ui/log-viewer.js +1 -1
  114. package/dist/utils/node/Folder.js +2 -4
  115. package/dist/utils/node/FramedStreamReader.js +4 -5
  116. package/dist/utils/node/FramedStreamWriter.js +6 -5
  117. package/dist/utils/node/anonymize.js +11 -0
  118. package/dist/utils/node/timeout.js +11 -0
  119. package/dist/utils/shared/ProgressBar.js +1 -2
  120. package/dist/utils/shared/TestReportBuilder.js +11 -7
  121. package/dist/utils/shared/host.js +11 -0
  122. package/dist/utils/shared/object.js +12 -3
  123. package/dist/utils/shared/schema.js +20 -0
  124. package/dist/utils/shared/string.js +1 -0
  125. package/dist/utils/shared/toIError.js +3 -7
  126. package/dist/utils/shared/version.js +52 -0
  127. package/package.json +2 -78
  128. package/dist/configuration/validators/browser.js +0 -11
  129. package/dist/modes/test/REserve.js +0 -31
  130. package/dist/modes/test/server.js +0 -109
@@ -1,2 +1,2 @@
1
1
  !function(){try{if("undefined"!=typeof document){var e=document.createElement("style");e.appendChild(document.createTextNode('body,#app{height:100vh;font-family:var(--sapFontFamily,"72", Arial, sans-serif);background:var(--sapBackgroundColor,#f7f7f7);flex-direction:column;margin:0;display:flex;overflow:hidden}#header{background:var(--sapShellBar_Background,#354a5e);color:var(--sapShellBar_TextColor,#fff);flex:none;align-items:center;gap:1rem;padding:.5rem 1rem;display:flex}#header h1{color:inherit;flex:1;margin:0;font-size:1rem;font-weight:700}#toolbar{background:var(--sapGroup_ContentBackground,#fff);border-bottom:1px solid var(--sapGroup_TitleBorderColor,#d9d9d9);flex-wrap:wrap;flex:none;align-items:center;gap:.75rem;padding:.5rem 1rem;display:flex}#toolbar ui5-message-strip{flex:0 0 100%;margin-top:.25rem}#logTableWrapper{flex:1;overflow-y:auto}#logTable{border-collapse:collapse;width:100%;font-size:.875rem}#logTable thead th{background:var(--sapList_HeaderBackground,#f7f7f7);text-align:left;border-bottom:1px solid var(--sapList_BorderColor,#d9d9d9);white-space:nowrap;padding:.4rem .5rem;font-weight:700;position:sticky;top:0}#logTable thead th.col-clickable{cursor:pointer;-webkit-user-select:none;user-select:none}#logTable thead th.col-clickable:hover{background:var(--sapList_Hover_Background,#f1f1f1);text-decoration:underline}#logTable tbody tr{cursor:pointer;border-bottom:1px solid var(--sapList_BorderColor,#d9d9d9)}#logTable tbody tr:hover{background:var(--sapList_Hover_Background,#f1f1f1)}#logTable tbody td{vertical-align:top;text-overflow:ellipsis;white-space:nowrap;max-width:400px;padding:.25rem .5rem;overflow:hidden}#logTable tbody td.col-message{max-width:600px}.log-details-row{align-items:baseline;gap:.5rem;min-width:400px;padding:.2rem 0;display:flex}.log-details-label{color:var(--sapContent_LabelColor,#6a6d70);flex-shrink:0;min-width:6rem;font-size:.8rem}.log-details-value{word-break:break-all;flex:1}.log-details-actions{flex-shrink:0;gap:.25rem;display:flex}.log-details-error{white-space:pre-wrap;background:var(--sapField_ReadOnly_Background,#f2f2f2);border-radius:.25rem;flex:1;max-height:200px;padding:.5rem;font-family:monospace;font-size:.75rem;overflow-y:auto}.log-details-section-label{color:var(--sapContent_LabelColor,#6a6d70);margin-top:.5rem;margin-bottom:.25rem;font-size:.8rem;font-weight:700}.column-filter-row{align-items:center;padding:.2rem 0;display:flex}\n/*$vite$:1*/')),document.head.appendChild(e)}}catch(o){console.error("vite-plugin-css-injected-by-js",o)}}();
2
- !function(){var e=class{_state={};get state(){return this._state}_settings={};get settings(){return this._settings}_updateCb=()=>{throw new Error("UI not connected")};_update({...e}){for(const t of Object.keys(e))e[t]===this._state[t]&&delete e[t];return Object.assign(this._state,e),console.log("🎮⏩",e),Object.keys(e).length>0&&this._updateCb({...e}),e}_onConnect(){}connect(e){this._updateCb=e,this._onConnect(),console.log("🎮🔛",{initialState:{...this._state},settings:this._settings})}interaction(e){const{action:t,...n}=e,r=this._update(n);console.log("🎮⏪",{event:e,action:t,stateDiff:r}),(Object.keys(r).length>0||void 0!==t)&&this._onInteraction(r,t)}},t=3e5,n=[{label:"5 minutes",key:t},{label:"15 minutes",key:9e5},{label:"30 minutes",key:18e5},{label:"1 hour",key:36e5},{label:"3 hours",key:108e5}],r=[{label:"5 seconds",key:5e3},{label:"10 seconds",key:1e4},{label:"30 seconds",key:3e4},{label:"60 seconds",key:6e4}];function o(e){return e.replaceAll("&","&amp;").replaceAll("<","&lt;").replaceAll(">","&gt;").replaceAll('"',"&quot;")}function a(e){return e.toString().padStart(2,"0")}function s(e){const t=new Date(e);return`${a(t.getMonth()+1)}/${a(t.getDate())}/${t.getFullYear()}, ${a(t.getHours())}:${a(t.getMinutes())}:${a(t.getSeconds())}`}function i(e,t){const n="relative"===e.timerangeType,r=n?function(e,t){const n=t.relativeTimerange.map(t=>`<ui5-option value="${t.key}"${e.relativeTimerange===t.key?" selected":""}>${t.label}</ui5-option>`).join(""),r=t.autorefresh.map(t=>`<ui5-option value="${t.key}"${e.autorefreshInterval===t.key?" selected":""}>${t.label}</ui5-option>`).join("");return`<ui5-select id="relativeTimerangeSelect">${n}</ui5-select>\n <span>Auto Refresh:</span>\n <ui5-select id="autorefreshSelect">\n <ui5-option value="none"${e.autorefresh?"":" selected"}>None</ui5-option>\n ${r}\n </ui5-select>`}(e,t):function(e){return`<ui5-datetime-picker id="absoluteFromPicker" value="${s(e.absoluteTimerangeFrom)}" placeholder="From"></ui5-datetime-picker>\n <ui5-datetime-picker id="absoluteToPicker" value="${s(e.absoluteTimerangeTo)}" placeholder="To"></ui5-datetime-picker>`}(e),a=e.errorMessage?`<ui5-message-strip id="errorStrip" design="Negative" hide-close-button>${o(e.errorMessage)}</ui5-message-strip>`:"";return`<ui5-input id="filterInput" value="${o(e.filter)}" placeholder="Filter expression..." style="flex:1;min-width:200px;"></ui5-input>\n <ui5-select id="timerangeTypeSelect">\n <ui5-option value="relative"${n?" selected":""}>Relative</ui5-option>\n <ui5-option value="absolute"${n?"":" selected"}>Absolute</ui5-option>\n </ui5-select>\n ${r}\n <ui5-button id="refreshNowBtn">Refresh now</ui5-button>\n ${a}`}var l={0:"&#128269;",1:"&#128172;",2:"&#9888;&#65039;",3:"&#10060;",4:"&#128163;"},c={0:"debug",1:"info",2:"warn",3:"error",4:"fatal"},u=["B","KB","MB","GB"];function d(e){return l[e]??"?"}function p(e){return e?new Date(e).toLocaleString():"—"}function g(e){let t=e,n=0;for(;t>=1024&&n<u.length-1;)t/=1024,n++;return`${0===n?t.toString():t.toFixed(1)} ${u[n]}`}function m(e){const t='<thead><tr>\n <th data-col="timestamp" class="col-clickable">Timestamp (local)</th>\n <th data-col="level" class="col-clickable">Level</th>\n <th data-col="source" class="col-clickable">Source</th>\n <th data-col="processId" class="col-clickable">PID</th>\n <th data-col="threadId" class="col-clickable">TID</th>\n <th data-col="pageId" class="col-clickable">Page ID</th>\n <th class="col-message">Message</th>\n</tr></thead>';return 0===e.length?`${t}<tbody><tr><td colspan="7" style="text-align:center;padding:1rem;color:var(--sapContent_LabelColor);">No log entries</td></tr></tbody>`:`${t}<tbody>${e.map((e,t)=>{const n=e.message.length>200?`${e.message.slice(0,200)}…`:e.message;return`<tr data-index="${t}">\n <td>${p(e.timestamp)}</td>\n <td>${d(e.level)}</td>\n <td>${e.source}</td>\n <td>${e.processId}</td>\n <td>${e.threadId}</td>\n <td>${e.pageId??""}</td>\n <td class="col-message">${r=n,r.replaceAll("&","&amp;").replaceAll("<","&lt;").replaceAll(">","&gt;").replaceAll('"',"&quot;")}</td>\n</tr>`;var r}).join("")}</tbody>`}function v(e,t){const n=JSON.stringify(t);return`<span class="log-details-actions">\n <ui5-button class="filter-add-btn" design="Transparent" data-field="${e}" data-value='${n}' data-op="===">&#10133;</ui5-button>\n <ui5-button class="filter-remove-btn" design="Transparent" data-field="${e}" data-value='${n}' data-op="!==">&#10134;</ui5-button>\n </span>`}function f(e,t,n=""){return`<div class="log-details-row">\n <span class="log-details-label">${e}</span>\n <span class="log-details-value">${t}</span>\n ${n}\n</div>`}function h(e,t="data"){return Object.entries(e).map(([e,n])=>{const r=`${t}.${e}`;return Array.isArray(n)?n.map((e,t)=>{const n=`${r}[${t}]`;return"object"==typeof e&&null!==e?h(e,n):f(n,b(String(e)),v(n,e))}).join(""):"object"==typeof n&&null!==n?h(n,r):f(r,b(String(n)),v(r,n))}).join("")}function b(e){return e.replaceAll("&","&amp;").replaceAll("<","&lt;").replaceAll(">","&gt;").replaceAll('"',"&quot;")}function y(e){const t=d(e.level),n=(r=e.level,c[r]??"unknown");var r;let o=f("timestamp",p(e.timestamp));var a;return o+=f("level",`${t} ${n}`,v("level",n)),o+=f("source",b(e.source),v("source",e.source)),o+=f("processId",String(e.processId),v("processId",e.processId)),o+=f("threadId",String(e.threadId),v("threadId",e.threadId)),void 0!==e.pageId&&(o+=f("pageId",String(e.pageId),v("pageId",e.pageId))),o+=f("message",b(e.message)),e.error&&(o+=(a=e.error,`<div class="log-details-row"><span class="log-details-section-label">error (JSON):</span></div>\n<div class="log-details-row"><pre class="log-details-error">${b(JSON.stringify(a,null,2))}</pre></div>`)),void 0!==e.data&&(o+='<div class="log-details-row"><span class="log-details-section-label">data (JSON):</span></div>',o+=h(e.data)),o}function S(e,t,n,r){const o=`${e} ${n} ${"string"==typeof t?`"${t}"`:String(t)}`;return r?`${r} && ${o}`:o}var $=new class extends e{constructor(){super(),this._state={timerangeType:"relative",relativeTimerange:t,absoluteTimerangeFrom:0,absoluteTimerangeTo:0,autorefresh:!1,autorefreshInterval:5e3,filter:"",errorMessage:"",logs:[],metrics:{inputSize:0,chunksCount:0,outputSize:0,minTimestamp:Number.MAX_SAFE_INTEGER,maxTimestamp:0,logCount:0,reading:!0}},this._settings={relativeTimerange:n,autorefresh:r}}async _executeQuery(e){this._update({errorMessage:""});const t=new URLSearchParams;for(const a of Object.keys(e))t.set(a,e[a].toString());const n=await fetch("/query?"+t.toString());if(!n.ok){const e=await n.text()||`${n.status} ${n.statusText}`;return void this._update({errorMessage:e})}const{metrics:r,logs:o}=await n.json();return{metrics:r,logs:o}}async _checkInitialQuery(){const e=await this._executeQuery({});if(!e)return;const{metrics:t,logs:n}=e,r=Date.now(),o={absoluteTimerangeFrom:t.minTimestamp,absoluteTimerangeTo:r,metrics:t,logs:n};t.minTimestamp<=r-3e5&&(o.timerangeType="absolute"),this._update(o)}_onConnect(){this._checkInitialQuery()}_onInteraction(e,t){("autorefresh"in e||"autorefreshInterval"in e)&&this._autorefresh(e),void 0!==t&&this[t]()}async refresh_now(){const e={};"absolute"===this._state.timerangeType?(e.from=this._state.absoluteTimerangeFrom,e.to=this._state.absoluteTimerangeTo):e.from=Date.now()-this._state.relativeTimerange,this._state.filter&&(e.filter=this._state.filter);const t=await this._executeQuery(e);t&&this._update(t)}_autorefreshIntervalId;_startAutorefresh(){this._autorefreshIntervalId=setInterval(()=>{this.refresh_now()},this._state.autorefreshInterval)}_stopAutorefresh(){clearInterval(this._autorefreshIntervalId),this._autorefreshIntervalId=void 0}_autorefresh({autorefresh:e}){!1===e?this._stopAutorefresh():(!0===e||this._stopAutorefresh(),this._startAutorefresh())}};function T(){const e=document.querySelector("#filterInput"),t=function(e,t){let n;return(...r)=>{clearTimeout(n),n=setTimeout(()=>e(...r),t)}}(e=>$.interaction({filter:e}),250);e?.addEventListener("input",e=>{const n=e.target;t(n.value)}),document.querySelector("#timerangeTypeSelect")?.addEventListener("change",e=>{const t=e.detail;$.interaction({timerangeType:t.selectedOption.value})}),document.querySelector("#relativeTimerangeSelect")?.addEventListener("change",e=>{const t=e.detail;$.interaction({relativeTimerange:Number(t.selectedOption.value)})}),document.querySelector("#autorefreshSelect")?.addEventListener("change",e=>{const t=e.detail.selectedOption.value;"none"===t?$.interaction({autorefresh:!1}):$.interaction({autorefresh:!0,autorefreshInterval:Number(t)})}),document.querySelector("#absoluteFromPicker")?.addEventListener("change",e=>{const t=e.target;t.dateValue&&$.interaction({absoluteTimerangeFrom:t.dateValue.getTime()})}),document.querySelector("#absoluteToPicker")?.addEventListener("change",e=>{const t=e.target;t.dateValue&&$.interaction({absoluteTimerangeTo:t.dateValue.getTime()})}),document.querySelector("#refreshNowBtn")?.addEventListener("click",()=>$.interaction({action:"refresh_now"}))}function _(e){e.addEventListener("click",e=>{const t=e.target.closest("th[data-col]");if(!t)return;const n=t.dataset.col;if(!n)return;const r=$.state.logs,o=document.querySelector("#columnFilterPopover"),a=document.querySelector("#columnFilterPopoverContent");if(!o||!a)return;if("timestamp"===n){if(0===r.length)return;const e=r.map(e=>e.timestamp),t=Math.min(...e),n=Math.max(...e);return void $.interaction({timerangeType:"absolute",absoluteTimerangeFrom:t,absoluteTimerangeTo:n})}a.innerHTML="level"===n?["debug","info","warn","error","fatal"].map(e=>`<div class="column-filter-row"><ui5-checkbox data-filter-value="${e}" text="${e}"></ui5-checkbox></div>`).join(""):function(e,t){return[...new Set(t.map(t=>t[e]))].toSorted().map(e=>{const t=void 0===e;return`<div class="column-filter-row"><ui5-checkbox data-filter-value='${t?"__undefined__":JSON.stringify(e)}' text="${t?"none":String(e)}"></ui5-checkbox></div>`}).join("")}(n,r);const s=document.querySelector("#columnFilterApply"),i=s?.cloneNode(!0);s&&i&&(s.replaceWith(i),i.addEventListener("click",()=>function(e){const t=document.querySelector("#columnFilterPopoverContent");if(!t)return;const n=[...t.querySelectorAll("ui5-checkbox")].filter(e=>!0===e.checked);if(0===n.length)return;const r=n.map(e=>{const t=e.dataset.filterValue??"null";if("__undefined__"!==t)try{return JSON.parse(t)}catch{return t}}),o=r.map(t=>S(e,t,"===","")),a=r.length>1?`(${o.join(" || ")})`:o[0],s=$.state.filter?`${$.state.filter} && ${a}`:a;$.interaction({filter:s});const i=document.querySelector("#filterInput");i&&(i.value=s),document.querySelector("#columnFilterPopover").open=!1}("level"===n?"level":n))),o.opener=t,o.open=!0})}function k(){T(),document.querySelector("#statusBtn")?.addEventListener("click",e=>{const t=document.querySelector("#metricsPopover"),n=document.querySelector("#metricsPopoverContent");t&&n&&(n.innerHTML=function(){const e=$.state.metrics;return`<div class="log-details-row"><span class="log-details-label">Status</span>\n <span>${e.reading?"Live (reading)":"Replay (complete)"}</span></div>\n <div class="log-details-row"><span class="log-details-label">Log count</span>\n <span>${e.logCount}</span></div>\n <div class="log-details-row"><span class="log-details-label">Input size</span>\n <span>${g(e.inputSize)}</span></div>\n <div class="log-details-row"><span class="log-details-label">Output size</span>\n <span>${g(e.outputSize)}</span></div>\n <div class="log-details-row"><span class="log-details-label">Chunks</span>\n <span>${e.chunksCount}</span></div>\n <div class="log-details-row"><span class="log-details-label">From</span>\n <span>${p(e.minTimestamp)}</span></div>\n <div class="log-details-row"><span class="log-details-label">To</span>\n <span>${p(e.maxTimestamp)}</span></div>`}(),t.opener=e.currentTarget,t.open=!0)});const e=document.querySelector("#logTable thead");e&&_(e);const t=document.querySelector("#logTable tbody");t&&I(t),document.querySelector("#logDetailsClose")?.addEventListener("click",()=>{const e=document.querySelector("#logDetailsPopover");e&&(e.open=!1)})}function I(e){e.addEventListener("click",e=>{const t=e.target.closest("tr[data-index]");if(!t)return;const n=Number(t.dataset.index),r=$.state.logs[n];if(!r)return;const o=document.querySelector("#logDetailsPopover"),a=document.querySelector("#logDetailsPopoverContent");o&&a&&(a.innerHTML=y(r),function(e){for(const t of e.querySelectorAll(".filter-add-btn, .filter-remove-btn"))t.addEventListener("click",()=>{const e=t,n=e.dataset.field??"",r=e.dataset.value??"null",o=e.dataset.op??"===";let a;try{a=JSON.parse(r)}catch{a=r}const s=S(n,a,o,$.state.filter);$.interaction({filter:s});const i=document.querySelector("#filterInput");i&&(i.value=s)})}(a),o.opener=t,o.open=!0)})}var w=["timerangeType","relativeTimerange","autorefresh","autorefreshInterval","absoluteTimerangeFrom","absoluteTimerangeTo"];function q(e){if(w.some(t=>t in e)&&function(){const e=document.querySelector("#toolbar");e&&(e.innerHTML=i($.state,$.settings),T())}(),"metrics"in e&&function(){const e=document.querySelector("#statusBtn");if(!e)return;const t=$.state.metrics.reading;e.textContent=t?"Status: Live":"Status: Replay",e.setAttribute("design",t?"Positive":"Neutral")}(),"logs"in e&&function(){const e=document.querySelector("#logTable");if(!e)return;e.innerHTML=m($.state.logs);const t=e.querySelector("thead");t&&_(t);const n=e.querySelector("tbody");n&&I(n)}(),"errorMessage"in e&&function(){const e=document.querySelector("#toolbar");if(!e)return;const t=document.querySelector("#errorStrip");$.state.errorMessage?t?t.textContent=$.state.errorMessage:e.insertAdjacentHTML("beforeend",`<ui5-message-strip id="errorStrip" design="Negative" hide-close-button>${$.state.errorMessage}</ui5-message-strip>`):t?.remove()}(),"filter"in e){const e=document.querySelector("#filterInput");e&&document.activeElement!==e&&(e.value=$.state.filter)}}document.addEventListener("DOMContentLoaded",()=>{!function(){const e=document.querySelector("#app");e&&e.insertAdjacentHTML("beforebegin",'<ui5-popover id="metricsPopover" header-text="Metrics" placement="Bottom">\n <div id="metricsPopoverContent" style="padding: 0.5rem;"></div>\n </ui5-popover>\n <ui5-popover id="logDetailsPopover" header-text="Log Details" placement="Bottom" style="max-width: 600px;">\n <div id="logDetailsPopoverContent" style="padding: 0.5rem;"></div>\n <div slot="footer" style="display: flex; justify-content: flex-end; padding: 0.5rem;">\n <ui5-button id="logDetailsClose" design="Transparent">Close</ui5-button>\n </div>\n </ui5-popover>\n <ui5-popover id="columnFilterPopover" placement="Bottom">\n <div id="columnFilterPopoverContent" style="padding: 0.5rem; min-width: 180px;"></div>\n <div slot="footer" style="display: flex; justify-content: flex-end; padding: 0.5rem;">\n <ui5-button id="columnFilterApply">Filter</ui5-button>\n </div>\n </ui5-popover>')}(),$.connect(q),function(){const e=document.querySelector("#app");if(!e)return;const{state:t,settings:n}=$;e.innerHTML=function(e){const t=e.reading;return`<div id="header">\n <h1>UI5 Test Runner Log Viewer</h1>\n <ui5-button id="statusBtn" design="${t?"Positive":"Neutral"}">Status: ${t?"Live":"Replay"}</ui5-button>\n</div>`}(t.metrics)+`<div id="toolbar">${i(t,n)}</div><div id="logTableWrapper"><table id="logTable">${m(t.logs)}</table></div>`,k()}()})}();
2
+ !function(){var e=class{_state={};get state(){return this._state}_settings={};get settings(){return this._settings}_updateCb=()=>{throw new Error("UI not connected")};_update({...e}){for(const t of Object.keys(e))e[t]===this._state[t]&&delete e[t];return Object.assign(this._state,e),Object.keys(e).length>0&&this._updateCb({...e}),e}_onConnect(){}connect(e){this._updateCb=e,this._onConnect()}interaction(e){const{action:t,...n}=e,r=this._update(n);(void 0!==t||Object.keys(r).length>0)&&this._onInteraction(r,t)}},t=3e5,n=[{label:"5 minutes",key:t},{label:"15 minutes",key:9e5},{label:"30 minutes",key:18e5},{label:"1 hour",key:36e5},{label:"3 hours",key:108e5}],r=[{label:"5 seconds",key:5e3},{label:"10 seconds",key:1e4},{label:"30 seconds",key:3e4},{label:"60 seconds",key:6e4}];function o(e){return e.replaceAll("&","&amp;").replaceAll("<","&lt;").replaceAll(">","&gt;").replaceAll('"',"&quot;")}function a(e){return e.toString().padStart(2,"0")}function s(e){const t=new Date(e);return`${a(t.getMonth()+1)}/${a(t.getDate())}/${t.getFullYear()}, ${a(t.getHours())}:${a(t.getMinutes())}:${a(t.getSeconds())}`}function i(e,t){const n="relative"===e.timerangeType,r=n?function(e,t){const n=t.relativeTimerange.map(t=>`<ui5-option value="${t.key}"${e.relativeTimerange===t.key?" selected":""}>${t.label}</ui5-option>`).join(""),r=t.autorefresh.map(t=>`<ui5-option value="${t.key}"${e.autorefreshInterval===t.key?" selected":""}>${t.label}</ui5-option>`).join("");return`<ui5-select id="relativeTimerangeSelect">${n}</ui5-select>\n <span>Auto Refresh:</span>\n <ui5-select id="autorefreshSelect">\n <ui5-option value="none"${e.autorefresh?"":" selected"}>None</ui5-option>\n ${r}\n </ui5-select>`}(e,t):function(e){return`<ui5-datetime-picker id="absoluteFromPicker" value="${s(e.absoluteTimerangeFrom)}" placeholder="From"></ui5-datetime-picker>\n <ui5-datetime-picker id="absoluteToPicker" value="${s(e.absoluteTimerangeTo)}" placeholder="To"></ui5-datetime-picker>`}(e),a=e.errorMessage?`<ui5-message-strip id="errorStrip" design="Negative" hide-close-button>${o(e.errorMessage)}</ui5-message-strip>`:"";return`<ui5-input id="filterInput" value="${o(e.filter)}" placeholder="Filter expression..." style="flex:1;min-width:200px;"></ui5-input>\n <ui5-select id="timerangeTypeSelect">\n <ui5-option value="relative"${n?" selected":""}>Relative</ui5-option>\n <ui5-option value="absolute"${n?"":" selected"}>Absolute</ui5-option>\n </ui5-select>\n ${r}\n <ui5-button id="refreshNowBtn">Refresh now</ui5-button>\n ${a}`}var l={0:"&#128269;",1:"&#128172;",2:"&#9888;&#65039;",3:"&#10060;",4:"&#128163;"},c={0:"debug",1:"info",2:"warn",3:"error",4:"fatal"},u=["B","KB","MB","GB"];function d(e){return l[e]??"?"}function p(e){return e?new Date(e).toLocaleString():"—"}function m(e){let t=e,n=0;for(;t>=1024&&n<u.length-1;)t/=1024,n++;return`${0===n?t.toString():t.toFixed(1)} ${u[n]}`}function g(e){const t='<thead><tr>\n <th data-col="timestamp" class="col-clickable">Timestamp (local)</th>\n <th data-col="level" class="col-clickable">Level</th>\n <th data-col="source" class="col-clickable">Source</th>\n <th data-col="processId" class="col-clickable">PID</th>\n <th data-col="threadId" class="col-clickable">TID</th>\n <th data-col="pageId" class="col-clickable">Page ID</th>\n <th class="col-message">Message</th>\n</tr></thead>';return 0===e.length?`${t}<tbody><tr><td colspan="7" style="text-align:center;padding:1rem;color:var(--sapContent_LabelColor);">No log entries</td></tr></tbody>`:`${t}<tbody>${e.map((e,t)=>{const n=e.message.length>200?`${e.message.slice(0,200)}…`:e.message;return`<tr data-index="${t}">\n <td>${p(e.timestamp)}</td>\n <td>${d(e.level)}</td>\n <td>${e.source}</td>\n <td>${e.processId}</td>\n <td>${e.threadId}</td>\n <td>${e.pageId??""}</td>\n <td class="col-message">${r=n,r.replaceAll("&","&amp;").replaceAll("<","&lt;").replaceAll(">","&gt;").replaceAll('"',"&quot;")}</td>\n</tr>`;var r}).join("")}</tbody>`}function v(e,t){const n=JSON.stringify(t);return`<span class="log-details-actions">\n <ui5-button class="filter-add-btn" design="Transparent" data-field="${e}" data-value='${n}' data-op="===">&#10133;</ui5-button>\n <ui5-button class="filter-remove-btn" design="Transparent" data-field="${e}" data-value='${n}' data-op="!==">&#10134;</ui5-button>\n </span>`}function f(e,t,n=""){return`<div class="log-details-row">\n <span class="log-details-label">${e}</span>\n <span class="log-details-value">${t}</span>\n ${n}\n</div>`}function h(e,t="data"){return Object.entries(e).map(([e,n])=>{const r=`${t}.${e}`;return Array.isArray(n)?n.map((e,t)=>{const n=`${r}[${t}]`;return"object"==typeof e&&null!==e?h(e,n):f(n,b(String(e)),v(n,e))}).join(""):"object"==typeof n&&null!==n?h(n,r):f(r,b(String(n)),v(r,n))}).join("")}function b(e){return e.replaceAll("&","&amp;").replaceAll("<","&lt;").replaceAll(">","&gt;").replaceAll('"',"&quot;")}function y(e){const t=d(e.level),n=(r=e.level,c[r]??"unknown");var r;let o=f("timestamp",p(e.timestamp));var a;return o+=f("level",`${t} ${n}`,v("level",n)),o+=f("source",b(e.source),v("source",e.source)),o+=f("processId",String(e.processId),v("processId",e.processId)),o+=f("threadId",String(e.threadId),v("threadId",e.threadId)),void 0!==e.pageId&&(o+=f("pageId",String(e.pageId),v("pageId",e.pageId))),o+=f("message",b(e.message)),e.error&&(o+=(a=e.error,`<div class="log-details-row"><span class="log-details-section-label">error (JSON):</span></div>\n<div class="log-details-row"><pre class="log-details-error">${b(JSON.stringify(a,null,2))}</pre></div>`)),void 0!==e.data&&(o+='<div class="log-details-row"><span class="log-details-section-label">data (JSON):</span></div>',o+=h(e.data)),o}function S(e,t,n,r){const o=`${e} ${n} ${"string"==typeof t?`"${t}"`:String(t)}`;return r?`${r} && ${o}`:o}var $=new class extends e{constructor(){super(),this._state={timerangeType:"relative",relativeTimerange:t,absoluteTimerangeFrom:0,absoluteTimerangeTo:0,autorefresh:!1,autorefreshInterval:5e3,filter:"",errorMessage:"",logs:[],metrics:{inputSize:0,chunksCount:0,outputSize:0,minTimestamp:Number.MAX_SAFE_INTEGER,maxTimestamp:0,logCount:0,reading:!0}},this._settings={relativeTimerange:n,autorefresh:r}}async _executeQuery(e){this._update({errorMessage:""});const t=new URLSearchParams;for(const a of Object.keys(e))t.set(a,e[a].toString());const n=await fetch("/query?"+t.toString());if(!n.ok){const e=await n.text()||`${n.status} ${n.statusText}`;return void this._update({errorMessage:e})}const{metrics:r,logs:o}=await n.json();return{metrics:r,logs:o}}async _checkInitialQuery(){const e=await this._executeQuery({});if(!e)return;const{metrics:t,logs:n}=e,r=Date.now(),o={absoluteTimerangeFrom:t.minTimestamp,absoluteTimerangeTo:r,metrics:t,logs:n,...t.minTimestamp<=r-3e5&&{timerangeType:"absolute"}};this._update(o)}_onConnect(){this._checkInitialQuery()}_onInteraction(e,t){("autorefresh"in e||"autorefreshInterval"in e)&&this._autorefresh(e),void 0!==t&&this[t]()}async refresh_now(){const e={};"absolute"===this._state.timerangeType?(e.from=this._state.absoluteTimerangeFrom,e.to=this._state.absoluteTimerangeTo):e.from=Date.now()-this._state.relativeTimerange,this._state.filter&&(e.filter=this._state.filter);const t=await this._executeQuery(e);t&&this._update(t)}_autorefreshIntervalId;_startAutorefresh(){this._autorefreshIntervalId=setInterval(()=>{this.refresh_now()},this._state.autorefreshInterval)}_stopAutorefresh(){clearInterval(this._autorefreshIntervalId),this._autorefreshIntervalId=void 0}_autorefresh({autorefresh:e}){!1===e?this._stopAutorefresh():(!0===e||this._stopAutorefresh(),this._startAutorefresh())}};function T(){const e=document.querySelector("#filterInput"),t=function(e,t){let n;return(...r)=>{clearTimeout(n),n=setTimeout(()=>e(...r),t)}}(e=>$.interaction({filter:e}),250);e?.addEventListener("input",e=>{const n=e.target;t(n.value)}),document.querySelector("#timerangeTypeSelect")?.addEventListener("change",e=>{const t=e.detail;$.interaction({timerangeType:t.selectedOption.value})}),document.querySelector("#relativeTimerangeSelect")?.addEventListener("change",e=>{const t=e.detail;$.interaction({relativeTimerange:Number(t.selectedOption.value)})}),document.querySelector("#autorefreshSelect")?.addEventListener("change",e=>{const t=e.detail.selectedOption.value;"none"===t?$.interaction({autorefresh:!1}):$.interaction({autorefresh:!0,autorefreshInterval:Number(t)})}),document.querySelector("#absoluteFromPicker")?.addEventListener("change",e=>{const t=e.target;t.dateValue&&$.interaction({absoluteTimerangeFrom:t.dateValue.getTime()})}),document.querySelector("#absoluteToPicker")?.addEventListener("change",e=>{const t=e.target;t.dateValue&&$.interaction({absoluteTimerangeTo:t.dateValue.getTime()})}),document.querySelector("#refreshNowBtn")?.addEventListener("click",()=>$.interaction({action:"refresh_now"}))}function _(e){e.addEventListener("click",e=>{const t=e.target.closest("th[data-col]");if(!t)return;const n=t.dataset.col;if(!n)return;const r=$.state.logs,o=document.querySelector("#columnFilterPopover"),a=document.querySelector("#columnFilterPopoverContent");if(!o||!a)return;if("timestamp"===n){if(0===r.length)return;const e=r.map(e=>e.timestamp),t=Math.min(...e),n=Math.max(...e);return void $.interaction({timerangeType:"absolute",absoluteTimerangeFrom:t,absoluteTimerangeTo:n})}a.innerHTML="level"===n?["debug","info","warn","error","fatal"].map(e=>`<div class="column-filter-row"><ui5-checkbox data-filter-value="${e}" text="${e}"></ui5-checkbox></div>`).join(""):function(e,t){return[...new Set(t.map(t=>t[e]))].toSorted().map(e=>{const t=void 0===e;return`<div class="column-filter-row"><ui5-checkbox data-filter-value='${t?"__undefined__":JSON.stringify(e)}' text="${t?"none":String(e)}"></ui5-checkbox></div>`}).join("")}(n,r);const s=document.querySelector("#columnFilterApply"),i=s?.cloneNode(!0);s&&i&&(s.replaceWith(i),i.addEventListener("click",()=>function(e){const t=document.querySelector("#columnFilterPopoverContent");if(!t)return;const n=[...t.querySelectorAll("ui5-checkbox")].filter(e=>e.checked);if(0===n.length)return;const r=n.map(e=>{const t=e.dataset.filterValue??"null";if("__undefined__"!==t)try{return JSON.parse(t)}catch{return t}}),o=r.map(t=>S(e,t,"===","")),a=r.length>1?`(${o.join(" || ")})`:o[0],s=$.state.filter?`${$.state.filter} && ${a}`:a;$.interaction({filter:s});const i=document.querySelector("#filterInput");i&&(i.value=s),document.querySelector("#columnFilterPopover").open=!1}("level"===n?"level":n))),o.opener=t,o.open=!0})}function k(){T(),document.querySelector("#statusBtn")?.addEventListener("click",e=>{const t=document.querySelector("#metricsPopover"),n=document.querySelector("#metricsPopoverContent");t&&n&&(n.innerHTML=function(){const e=$.state.metrics;return`<div class="log-details-row"><span class="log-details-label">Status</span>\n <span>${e.reading?"Live (reading)":"Replay (complete)"}</span></div>\n <div class="log-details-row"><span class="log-details-label">Log count</span>\n <span>${e.logCount}</span></div>\n <div class="log-details-row"><span class="log-details-label">Input size</span>\n <span>${m(e.inputSize)}</span></div>\n <div class="log-details-row"><span class="log-details-label">Output size</span>\n <span>${m(e.outputSize)}</span></div>\n <div class="log-details-row"><span class="log-details-label">Chunks</span>\n <span>${e.chunksCount}</span></div>\n <div class="log-details-row"><span class="log-details-label">From</span>\n <span>${p(e.minTimestamp)}</span></div>\n <div class="log-details-row"><span class="log-details-label">To</span>\n <span>${p(e.maxTimestamp)}</span></div>`}(),t.opener=e.currentTarget,t.open=!0)});const e=document.querySelector("#logTable thead");e&&_(e);const t=document.querySelector("#logTable tbody");t&&I(t),document.querySelector("#logDetailsClose")?.addEventListener("click",()=>{const e=document.querySelector("#logDetailsPopover");e&&(e.open=!1)})}function I(e){e.addEventListener("click",e=>{const t=e.target.closest("tr[data-index]");if(!t)return;const n=Number(t.dataset.index),r=$.state.logs[n];if(!r)return;const o=document.querySelector("#logDetailsPopover"),a=document.querySelector("#logDetailsPopoverContent");o&&a&&(a.innerHTML=y(r),function(e){for(const t of e.querySelectorAll(".filter-add-btn, .filter-remove-btn"))t.addEventListener("click",()=>{const e=t,n=e.dataset.field??"",r=e.dataset.value??"null",o=e.dataset.op??"===";let a;try{a=JSON.parse(r)}catch{a=r}const s=S(n,a,o,$.state.filter);$.interaction({filter:s});const i=document.querySelector("#filterInput");i&&(i.value=s)})}(a),o.opener=t,o.open=!0)})}var w=["timerangeType","relativeTimerange","autorefresh","autorefreshInterval","absoluteTimerangeFrom","absoluteTimerangeTo"];function q(e){if(w.some(t=>Object.hasOwn(e,t))&&function(){const e=document.querySelector("#toolbar");e&&(e.innerHTML=i($.state,$.settings),T())}(),"metrics"in e&&function(){const e=document.querySelector("#statusBtn");if(!e)return;const t=$.state.metrics.reading;e.textContent=t?"Status: Live":"Status: Replay",e.setAttribute("design",t?"Positive":"Neutral")}(),"logs"in e&&function(){const e=document.querySelector("#logTable");if(!e)return;e.innerHTML=g($.state.logs);const t=e.querySelector("thead");t&&_(t);const n=e.querySelector("tbody");n&&I(n)}(),"errorMessage"in e&&function(){const e=document.querySelector("#toolbar");if(!e)return;const t=document.querySelector("#errorStrip");$.state.errorMessage?t?t.textContent=$.state.errorMessage:e.insertAdjacentHTML("beforeend",`<ui5-message-strip id="errorStrip" design="Negative" hide-close-button>${$.state.errorMessage}</ui5-message-strip>`):t?.remove()}(),!("filter"in e))return;const t=document.querySelector("#filterInput");t&&document.activeElement!==t&&(t.value=$.state.filter)}document.addEventListener("DOMContentLoaded",()=>{!function(){const e=document.querySelector("#app");e&&e.insertAdjacentHTML("beforebegin",'<ui5-popover id="metricsPopover" header-text="Metrics" placement="Bottom">\n <div id="metricsPopoverContent" style="padding: 0.5rem;"></div>\n </ui5-popover>\n <ui5-popover id="logDetailsPopover" header-text="Log Details" placement="Bottom" style="max-width: 600px;">\n <div id="logDetailsPopoverContent" style="padding: 0.5rem;"></div>\n <div slot="footer" style="display: flex; justify-content: flex-end; padding: 0.5rem;">\n <ui5-button id="logDetailsClose" design="Transparent">Close</ui5-button>\n </div>\n </ui5-popover>\n <ui5-popover id="columnFilterPopover" placement="Bottom">\n <div id="columnFilterPopoverContent" style="padding: 0.5rem; min-width: 180px;"></div>\n <div slot="footer" style="display: flex; justify-content: flex-end; padding: 0.5rem;">\n <ui5-button id="columnFilterApply">Filter</ui5-button>\n </div>\n </ui5-popover>')}(),$.connect(q),function(){const e=document.querySelector("#app");if(!e)return;const{state:t,settings:n}=$;e.innerHTML=function(e){const t=e.reading;return`<div id="header">\n <h1>UI5 Test Runner Log Viewer</h1>\n <ui5-button id="statusBtn" design="${t?"Positive":"Neutral"}">Status: ${t?"Live":"Replay"}</ui5-button>\n</div>`}(t.metrics)+`<div id="toolbar">${i(t,n)}</div><div id="logTableWrapper"><table id="logTable">${g(t.logs)}</table></div>`,k()}()})}();
@@ -15,10 +15,8 @@ export const Folder = {
15
15
  try {
16
16
  await FileSystem.mkdir(path, recursive);
17
17
  }
18
- catch (error_) {
19
- const error = new Error(`Failed to create folder: ${path}`);
20
- error.cause = error_;
21
- throw error;
18
+ catch (error) {
19
+ throw new Error(`Failed to create folder: ${path}`, { cause: error });
22
20
  }
23
21
  },
24
22
  async recreate(path) {
@@ -1,5 +1,4 @@
1
- import { Exit, FileSystem, assert } from '../../platform/index.js';
2
- import { setTimeout } from 'node:timers/promises';
1
+ import { Exit, FileSystem, Process, assert } from '../../platform/index.js';
3
2
  export class FramedStreamReader {
4
3
  static create(fileName, pollIntervalMs = 500) {
5
4
  return new this(fileName, pollIntervalMs);
@@ -33,7 +32,7 @@ export class FramedStreamReader {
33
32
  _reading = true;
34
33
  _buffer = Buffer.alloc(0);
35
34
  _task;
36
- async *_read(end) {
35
+ async *#read(end) {
37
36
  const fileStream = FileSystem.createReadStream(this._fileName, {
38
37
  start: this._startPos,
39
38
  end,
@@ -76,10 +75,10 @@ export class FramedStreamReader {
76
75
  while (this._reading) {
77
76
  const stats = await FileSystem.stat(this._fileName);
78
77
  if (stats.size > this._startPos) {
79
- yield* this._read(stats.size - 1);
78
+ yield* this.#read(stats.size - 1);
80
79
  }
81
80
  if (this._reading) {
82
- await Promise.race([setTimeout(this._pollIntervalMs), abortSignal]);
81
+ await Promise.race([Process.sleep(this._pollIntervalMs), abortSignal]);
83
82
  }
84
83
  }
85
84
  }
@@ -13,12 +13,13 @@ export class FramedStreamWriter {
13
13
  await promise;
14
14
  }
15
15
  async write(buffer) {
16
- if (buffer.length > 0) {
17
- const size = Buffer.alloc(4);
18
- size.writeUInt32BE(buffer.length, 0);
19
- await this._write(size);
20
- await this._write(buffer);
16
+ if (buffer.length === 0) {
17
+ return;
21
18
  }
19
+ const size = Buffer.alloc(4);
20
+ size.writeUInt32BE(buffer.length, 0);
21
+ await this._write(size);
22
+ await this._write(buffer);
22
23
  }
23
24
  async end() {
24
25
  await this._write(Buffer.from([0, 0, 0, 0]));
@@ -0,0 +1,11 @@
1
+ import { Host } from '../../platform/Host.js';
2
+ import { isSecretValue } from '../../types/ISecretValue.js';
3
+ const redactSecretsReplacer = (_, value) => (isSecretValue(value) ? '***' : value);
4
+ export function redactSecrets(object) {
5
+ return JSON.parse(JSON.stringify(object, redactSecretsReplacer));
6
+ }
7
+ export function anonymize(object) {
8
+ const home = Host.homedir();
9
+ const json = JSON.stringify(redactSecrets(object));
10
+ return json.includes(home) ? JSON.parse(json.replaceAll(home, '~')) : object;
11
+ }
@@ -0,0 +1,11 @@
1
+ export const getEffectiveTimeout = (itemTimeout = 0, globalTimeout = 0, startTime) => {
2
+ if (itemTimeout || globalTimeout) {
3
+ if (globalTimeout) {
4
+ const remainingGlobal = globalTimeout - (Date.now() - startTime);
5
+ return itemTimeout ? Math.min(remainingGlobal, itemTimeout) : remainingGlobal;
6
+ }
7
+ return itemTimeout;
8
+ }
9
+ return 0;
10
+ };
11
+ export const isGloballyTimedOut = (globalTimeout = 0, startTime) => globalTimeout > 0 && Date.now() - startTime > globalTimeout;
@@ -31,8 +31,7 @@ export class ProgressBar {
31
31
  ']',
32
32
  Math.floor(100 * ratio)
33
33
  .toString()
34
- .padStart(3, ' ')
35
- .toString(),
34
+ .padStart(3, ' '),
36
35
  '% '
37
36
  ];
38
37
  }
@@ -23,18 +23,22 @@ export class TestReportBuilder {
23
23
  this._suites[pageUrl] = [...parentUrls, suiteUrl];
24
24
  }
25
25
  }
26
- merge(url, testResults) {
27
- const { name: toolName } = this._report.results.tool;
28
- if (testResults.tool.name && toolName === '') {
29
- this._report.results.tool = testResults.tool;
26
+ merge(url, testResults, extra) {
27
+ if (testResults.tool.version && !this._report.results.tool.extra?.['qunitVersion']) {
28
+ this._report.results.tool.extra = {
29
+ ...this._report.results.tool.extra,
30
+ qunitVersion: testResults.tool.version
31
+ };
30
32
  }
31
33
  const { results } = this._report;
32
34
  const suites = [...(this._suites[url] ?? []), url];
33
35
  for (const test of testResults.tests) {
34
- results.tests.push({
36
+ const resolvedTest = {
35
37
  ...test,
36
- suite: [...suites, ...(test.suite ?? [])]
37
- });
38
+ suite: [...suites, ...(test.suite ?? [])],
39
+ ...((extra || test.extra) && { extra: { ...test.extra, ...extra } })
40
+ };
41
+ results.tests.push(resolvedTest);
38
42
  }
39
43
  for (const summaryField of SUMMARY_FIELDS) {
40
44
  results.summary[summaryField] += testResults.summary[summaryField];
@@ -0,0 +1,11 @@
1
+ export const countCpuModels = (cpus) => {
2
+ const counts = {};
3
+ for (const { model } of cpus) {
4
+ counts[model] = (counts[model] ?? 0) + 1;
5
+ }
6
+ return Object.entries(counts).map(([model, count]) => ({ model, count: count }));
7
+ };
8
+ export const formatHostLabel = ({ machine, cpus }) => {
9
+ const entries = countCpuModels(cpus).map(({ model, count }) => (count === 1 ? model : `${count}x ${model}`));
10
+ return entries.length > 0 ? `${machine} / ${entries.join(', ')}` : machine;
11
+ };
@@ -1,8 +1,17 @@
1
1
  const literalPrototype = Object.getPrototypeOf({});
2
2
  export const toPlainObject = (object) => {
3
3
  const prototype = Object.getPrototypeOf(object);
4
- if (Object.getPrototypeOf(object) === literalPrototype) {
5
- return object;
4
+ return Object.getPrototypeOf(object) === literalPrototype
5
+ ? object
6
+ : Object.assign({}, toPlainObject(prototype), object);
7
+ };
8
+ export const objectValues = (object) => Object.values(object).filter((v) => v !== undefined);
9
+ export const pick = (object, keys) => {
10
+ const result = {};
11
+ for (const key of keys) {
12
+ if (key in object) {
13
+ result[key] = object[key];
14
+ }
6
15
  }
7
- return Object.assign({}, toPlainObject(prototype), object);
16
+ return result;
8
17
  };
@@ -0,0 +1,20 @@
1
+ export function validate(value, schema) {
2
+ if (value === null || typeof value !== 'object') {
3
+ throw new TypeError(`Expected an object, got ${typeof value}`);
4
+ }
5
+ const object = value;
6
+ for (const [key, schemaValue] of Object.entries(schema)) {
7
+ if (!(key in object)) {
8
+ throw new TypeError(`Missing required property: "${key}"`);
9
+ }
10
+ if (typeof schemaValue === 'string') {
11
+ const actualType = typeof object[key];
12
+ if (actualType !== schemaValue) {
13
+ throw new TypeError(`Property "${key}" has type "${actualType}", expected "${schemaValue}"`);
14
+ }
15
+ }
16
+ else {
17
+ validate(object[key], schemaValue);
18
+ }
19
+ }
20
+ }
@@ -1,3 +1,4 @@
1
+ export const toKebabCase = (name) => name.replaceAll(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`);
1
2
  export function split(string, ...lengthes) {
2
3
  const result = [];
3
4
  let from = 0;
@@ -5,13 +5,9 @@ export const toIError = (error) => {
5
5
  const attributes = {
6
6
  name: error.name,
7
7
  message: error.message,
8
- stack: error.stack
8
+ stack: error.stack,
9
+ ...(error.cause ? { cause: toIError(error.cause) } : {}),
10
+ ...(error instanceof AggregateError && { errors: error.errors.map((item) => toIError(item)) })
9
11
  };
10
- if (error.cause) {
11
- attributes.cause = toIError(error.cause);
12
- }
13
- if (error instanceof AggregateError) {
14
- attributes.errors = error.errors.map((item) => toIError(item));
15
- }
16
12
  return attributes;
17
13
  };
@@ -0,0 +1,52 @@
1
+ const parseVersion = (version) => {
2
+ const [release, prerelease] = version.split('-', 2);
3
+ if (release) {
4
+ const releaseParts = release.split('.').map(Number);
5
+ const prereleaseParts = prerelease
6
+ ? prerelease.split('.').map((part) => (Number.isNaN(Number(part)) ? part : Number(part)))
7
+ : [];
8
+ return [releaseParts, prereleaseParts];
9
+ }
10
+ return [[], []];
11
+ };
12
+ export const compareVersions = (version1, version2) => {
13
+ const [v1Release, v1Prerelease] = parseVersion(version1);
14
+ const [v2Release, v2Prerelease] = parseVersion(version2);
15
+ const maxLength = Math.max(v1Release.length, v2Release.length);
16
+ while (v1Release.length < maxLength) {
17
+ v1Release.push(0);
18
+ }
19
+ while (v2Release.length < maxLength) {
20
+ v2Release.push(0);
21
+ }
22
+ for (let index = 0; index < maxLength; index++) {
23
+ if (v1Release[index] > v2Release[index])
24
+ return 1;
25
+ if (v1Release[index] < v2Release[index])
26
+ return -1;
27
+ }
28
+ return comparePreReleases(v1Prerelease, v2Prerelease);
29
+ };
30
+ const comparePreReleases = (v1Prerelease, v2Prerelease) => {
31
+ if (v1Prerelease.length === 0 && v2Prerelease.length === 0) {
32
+ return 0;
33
+ }
34
+ if (v1Prerelease.length === 0)
35
+ return 1;
36
+ if (v2Prerelease.length === 0)
37
+ return -1;
38
+ for (let index = 0; index < Math.max(v1Prerelease.length, v2Prerelease.length); index++) {
39
+ const p1 = v1Prerelease[index] ?? 0;
40
+ const p2 = v2Prerelease[index] ?? 0;
41
+ const p1Type = typeof p1;
42
+ const p2Type = typeof p2;
43
+ if (p1Type !== p2Type) {
44
+ return p1Type === 'number' ? -1 : 1;
45
+ }
46
+ if (p1 > p2)
47
+ return 1;
48
+ if (p1 < p2)
49
+ return -1;
50
+ }
51
+ return 0;
52
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ui5-test-runner",
3
- "version": "6.0.0-beta.5",
3
+ "version": "6.0.0",
4
4
  "description": "Standalone test runner for UI5",
5
5
  "main": "dist/cli.js",
6
6
  "type": "module",
@@ -17,42 +17,6 @@
17
17
  "engines": {
18
18
  "node": ">=24.11.1"
19
19
  },
20
- "scripts": {
21
- "ts-run": "node --no-warnings --experimental-strip-types --import ./src/platform/js2ts.mjs",
22
- "cli": "make precli && npm run ts-run -- src/cli.ts",
23
- "test:cli1": "npm run cli -- --url https://ui5.sap.com/test-resources/sap/m/demokit/cart/webapp/test/testsuite.qunit.html --report-dir tmp",
24
- "test:cli2": "npm run cli -- --url \"https://ui5.sap.com/test-resources/sap/m/demokit/cart/webapp/test/Test.qunit.html?testsuite=test-resources/sap/ui/demo/cart/testsuite.qunit&test=unit/unitTests\" --report-dir tmp",
25
- "test:cli3": "npm run cli -- --url \"https://ui5.sap.com/test-resources/sap/m/demokit/cart/webapp/test/Test.qunit.html?testsuite=test-resources%2Fsap%2Fui%2Fdemo%2Fcart%2Ftestsuite.qunit&test=integration%2FopaTestsComponent#/categories\" --report-dir tmp",
26
- "test:cli4": "npm run cli -- --url \"https://ui5.sap.com/test-resources/sap/m/demokit/cart/webapp/test/Test.qunit.html?testsuite=test-resources/sap/ui/demo/cart/testsuite.qunit&test=integration/opaTestsIFrame\" --report-dir tmp",
27
- "test:cli5": "npm run cli -- --cwd test/sample.js",
28
- "test:openui5": "npm run cli -- --url http://localhost:8000/test-resources/sap/ui/core/qunit/testsuite.qunit.html --parallel 4",
29
- "test:openui5.uncaught": "npm run cli -- --url \"http://localhost:8000/resources/sap/ui/test/starter/Test.qunit.html?testsuite=test-resources/sap/ui/core/qunit/odata/v2/testsuite.odatav2.qunit&test=ODataV2TreeBindingFlat_MockSrv\"",
30
- "lint": "dpdm src/cli.ts --circular --tree false --warning false --skip-dynamic-imports circular && eslint \"**/*.mjs\" \"./src/**/*.ts\" --fix --cache --cache-strategy content && tsc --build --noEmit",
31
- "build:options": "node build/options.mjs > ./src/configuration/options.ts && npm run lint",
32
- "build:agent": "vite build",
33
- "build:ui:report": "vite build -c src/ui/report/vite.config.mjs",
34
- "start:ui:report": "vite -c src/ui/report/vite.config.mjs",
35
- "build:ui:log": "vite build -c src/ui/log/vite.config.mjs",
36
- "build:ui:lib": "vite build -c src/ui/lib/vite.config.mjs",
37
- "start:ui:log": "vite -c src/ui/log/vite.config.mjs",
38
- "test": "npm run lint && npm run test:unit && npm run test:e2e",
39
- "test:samples": "npm run test:samples:js && npm run test:samples:ts",
40
- "test:samples:js": "npm run test:sample:js:legacy && npm run test:sample:js:coverage:legacy && npm run test:sample:js:legacy-remote && npm run test:sample:js:coverage:legacy-remote && npm run test:sample:js:remote && npm run test:sample:js:coverage:remote && npm run test:sample:js:basic-authent && npm run test:sample:js:legacy:split-opa && npm run test:sample:js:remote:split-opa",
41
- "test:coverall": "rimraf .nyc_output && jest --coverageDirectory .nyc_output --coverageReporters json && nyc --silent --no-clean npm run test:e2e && nyc merge .nyc_output .nyc_output/final/coverage.json && nyc report --temp-dir .nyc_output/final/ --report-dir coverage --branches 80 --functions 80 --lines 80 --statements 80",
42
- "test:unit": "vitest run --coverage",
43
- "test:unit:watch": "vitest",
44
- "test:unit:debug": "node --inspect node_modules/jest/bin/jest.js --runInBand --no-coverage",
45
- "pretest:e2e0": "npm install -g puppeteer selenium-webdriver playwright webdriverio",
46
- "test:e2e0": "npm run cli -- --batch \"test/e2e/[\\w_]*\\.json\" --report-dir e2e --start \"node test/e2e/serve.js\" --start-wait-url http://localhost:8081 --start-wait-method HEAD --start-timeout 30s",
47
- "test:e2e": "echo No E2E tests until the runner is ready",
48
- "test:report": "node ./src/defaults/report.js ./test/report && reserve --config ./test/report/reserve.json",
49
- "test:text-report": "node ./src/defaults/text-report.js ./test/report",
50
- "build:doc": "node build/doc",
51
- "build": "make precli && npx tsc --project tsconfig.cli.json",
52
- "clean": "npm uninstall -g ui5-test-runner puppeteer nyc selenium-webdriver playwright webdriverio jsdom",
53
- "update": "ncu -i --format group",
54
- "semantic-release": "semantic-release"
55
- },
56
20
  "repository": {
57
21
  "type": "git",
58
22
  "url": "git+https://github.com/ArnaudBuchholz/ui5-test-runner.git"
@@ -74,46 +38,6 @@
74
38
  "homepage": "https://github.com/ArnaudBuchholz/ui5-test-runner#readme",
75
39
  "dependencies": {
76
40
  "punyexpr": "1.3.1",
77
- "reserve": "2.3.5"
78
- },
79
- "devDependencies": {
80
- "@openui5/types": "^1.148.0",
81
- "@stylistic/eslint-plugin": "^5.10.0",
82
- "@stylistic/eslint-plugin-js": "^4.4.1",
83
- "@types/node": "^24.12.2",
84
- "@types/qunit": "^2.19.14",
85
- "@ui5/cli": "^4.0.55",
86
- "@ui5/middleware-code-coverage": "^2.0.3",
87
- "@ui5/webcomponents": "^2.22.0",
88
- "@ui5/webcomponents-fiori": "^2.22.0",
89
- "@vitest/coverage-v8": "^4.1.8",
90
- "baseline-browser-mapping": "^2.10.33",
91
- "dotenv": "^17.4.2",
92
- "dpdm": "^4.2.0",
93
- "eslint": "^9.39.2",
94
- "eslint-config-prettier": "^10.1.8",
95
- "eslint-plugin-import": "^2.32.0",
96
- "eslint-plugin-no-only-tests": "^3.4.0",
97
- "eslint-plugin-prettier": "^5.5.6",
98
- "eslint-plugin-security": "^4.0.0",
99
- "eslint-plugin-sonarjs": "^4.0.3",
100
- "eslint-plugin-unicorn": "^64.0.0",
101
- "globals": "^17.6.0",
102
- "jsdom": "^29.1.1",
103
- "markdownlint-cli": "^0.48.0",
104
- "npm-check-updates": "^22.2.2",
105
- "puppeteer": "^25.1.0",
106
- "qunit": "^2.26.0",
107
- "rimraf": "^6.1.3",
108
- "semantic-release": "^25.0.3",
109
- "stylelint": "^17.12.0",
110
- "stylelint-config-standard": "^40.0.0",
111
- "terser": "^5.48.0",
112
- "typescript": "^6.0.3",
113
- "typescript-eslint": "^8.60.1",
114
- "ui5-tooling-transpile": "^3.11.2",
115
- "vite": "^8.0.16",
116
- "vite-plugin-css-injected-by-js": "^5.0.1",
117
- "vitest": "^4.1.8"
41
+ "reserve": "2.3.8"
118
42
  }
119
43
  }
@@ -1,11 +0,0 @@
1
- import { OptionValidationError } from '../OptionValidationError.js';
2
- const KNOWN_BROWSERS = new Set(['puppeteer']);
3
- export const browser = (option, value) => {
4
- if (typeof value === 'string') {
5
- const name = value.startsWith('$/') && value.endsWith('.js') ? value.slice(2, -3) : value;
6
- if (KNOWN_BROWSERS.has(name)) {
7
- return name;
8
- }
9
- }
10
- throw new OptionValidationError(option);
11
- };
@@ -1,31 +0,0 @@
1
- import { logger } from '../../platform/index.js';
2
- export const buildREserveConfiguration = (configuration) => {
3
- const match = /\/((?:test-)?resources\/.*)/;
4
- let { ui5 } = configuration;
5
- if (!ui5.endsWith('/')) {
6
- ui5 += '/';
7
- }
8
- const mappingUrl = new URL('$1', ui5).toString();
9
- return {
10
- port: configuration.port ?? 0,
11
- mappings: [
12
- {
13
- method: 'GET,HEAD',
14
- match,
15
- url: mappingUrl,
16
- 'ignore-unverifiable-certificate': true
17
- },
18
- {
19
- match: /^\/(.*)/,
20
- cwd: configuration.webapp,
21
- file: '$1'
22
- },
23
- {
24
- custom: (request) => logger.debug({ source: 'server', message: 'Unhandled request', data: { url: request.url } })
25
- },
26
- {
27
- status: 404
28
- }
29
- ]
30
- };
31
- };
@@ -1,109 +0,0 @@
1
- import { assert, Exit, logger, Thread } from '../../platform/index.js';
2
- import { serve } from 'reserve';
3
- import { toPlainObject } from '../../utils/shared/object.js';
4
- import { buildREserveConfiguration } from './REserve.js';
5
- let channel;
6
- let serverWorker;
7
- let stopping = false;
8
- export const server = {
9
- async start(configuration) {
10
- assert(serverWorker === undefined);
11
- channel = Thread.createBroadcastChannel('server');
12
- Exit.registerAsyncTask({
13
- name: 'server',
14
- stop: () => server.stop()
15
- });
16
- logger.debug({ source: 'server', message: 'Starting server' });
17
- serverWorker = Thread.createWorker('modes/test/server', toPlainObject(configuration));
18
- const { promise, resolve, reject } = Promise.withResolvers();
19
- channel.onmessage = ({ data: message }) => {
20
- if (message.command === 'ready') {
21
- resolve(message.port);
22
- }
23
- else if (message.command === 'error') {
24
- reject(new Error('failed to start'));
25
- }
26
- else {
27
- assert(false, 'unexpected');
28
- }
29
- };
30
- return promise;
31
- },
32
- async stop() {
33
- if (stopping) {
34
- return;
35
- }
36
- stopping = true;
37
- try {
38
- assert(serverWorker !== undefined);
39
- logger.debug({ source: 'server', message: 'Stopping server' });
40
- channel.postMessage({
41
- command: 'terminate'
42
- });
43
- const { promise, resolve } = Promise.withResolvers();
44
- channel.onmessage = ({ data: message }) => {
45
- if (message.command === 'terminated') {
46
- channel.close();
47
- resolve();
48
- }
49
- };
50
- await promise;
51
- }
52
- finally {
53
- channel.close();
54
- }
55
- }
56
- };
57
- export const workerMain = (configuration) => {
58
- logger.debug({ source: 'server', message: 'Starting server...' });
59
- channel = Thread.createBroadcastChannel('server');
60
- channel.onmessage = ({ data: message }) => {
61
- if (message.command === 'terminate') {
62
- logger.debug({ source: 'server', message: 'Stopping server...' });
63
- void server.close().finally(() => {
64
- logger.debug({ source: 'server', message: 'Server stopped.' });
65
- channel.postMessage({
66
- command: 'terminated'
67
- });
68
- channel.close();
69
- });
70
- }
71
- };
72
- let server;
73
- try {
74
- server = serve(buildREserveConfiguration(configuration));
75
- }
76
- catch (error) {
77
- logger.error({ source: 'server', message: 'An error occurred while configuring', error });
78
- channel.postMessage({
79
- command: 'error'
80
- });
81
- return;
82
- }
83
- server.on('created', () => {
84
- logger.debug({ source: 'reserve', message: 'created', data: {} });
85
- });
86
- for (const eventName of ['incoming', 'redirecting', 'redirected', 'aborted', 'closed']) {
87
- server.on(eventName, (event) => {
88
- const { eventName: message, ...data } = event;
89
- logger.debug({ source: 'reserve', message, data });
90
- });
91
- }
92
- server
93
- .on('ready', (event) => {
94
- const { eventName: message, ...data } = event;
95
- logger.debug({ source: 'reserve', message, data });
96
- logger.info({ source: 'server', message: `Server listening on: ${event.url}` });
97
- channel.postMessage({
98
- command: 'ready',
99
- port: event.port
100
- });
101
- })
102
- .on('error', (event) => {
103
- const { eventName: message, reason: error, ...data } = event;
104
- logger.debug({ source: 'reserve', message, data, error });
105
- channel.postMessage({
106
- command: 'error'
107
- });
108
- });
109
- };