UNPKG

chappie-runner

Version:
676 lines (662 loc) 24.4 kB
<!doctype html> <html lang="en"> <head> <meta charset="utf-8" /> <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script> <!-- Would use latest version, you'd better specify a version --> <script src="https://unpkg.com/naive-ui"></script> <title>Bruno</title> <style> .error > .status { color: red; } .success > .status { color: green; } .n-collapse-item.success > .n-collapse-item__header { background-color: rgba(237, 247, 242, 1); } .n-collapse-item.error > .n-collapse-item__header { background-color: rgba(251, 238, 241, 1); } .min-width-150 { min-width: 150px; } </style> </head> <body> <div id="app"> <n-config-provider :theme="theme"> <n-layout embedded position="absolute" content-style="padding: 24px;"> <n-card> <n-flex> <n-page-header title="Bruno run dashboard"> <template #avatar> <n-avatar size="large" style="background-color: transparent"> <svg id="emoji" width="34" viewBox="0 0 72 72" xmlns="http://www.w3.org/2000/svg" > <g id="color"> <path fill="#F4AA41" stroke="none" d="M23.5,14.5855l-4.5,1.75l-7.25,8.5l-4.5,10.75l2,5.25c1.2554,3.7911,3.5231,7.1832,7.25,10l2.5-3.3333 c0,0,3.8218,7.7098,10.7384,8.9598c0,0,10.2616,1.936,15.5949-0.8765c3.4203-1.8037,4.4167-4.4167,4.4167-4.4167l3.4167-3.4167 l1.5833,2.3333l2.0833-0.0833l5.4167-7.25L64,37.3355l-0.1667-4.5l-2.3333-5.5l-4.8333-7.4167c0,0-2.6667-4.9167-8.1667-3.9167 c0,0-6.5-4.8333-11.8333-4.0833S32.0833,10.6688,23.5,14.5855z" ></path> <polygon fill="#EA5A47" stroke="none" points="36,47.2521 32.9167,49.6688 30.4167,49.6688 30.3333,53.5021 31.0833,57.0021 32.1667,58.9188 35,60.4188 39.5833,59.8355 41.1667,58.0855 42.1667,53.8355 41.9167,49.8355 39.9167,50.0855" ></polygon> <polygon fill="#3F3F3F" stroke="none" points="32.5,36.9188 30.9167,40.6688 33.0833,41.9188 34.3333,42.4188 38.6667,42.5855 41.5833,40.3355 39.8333,37.0855" ></polygon> </g> <g id="hair"></g> <g id="skin"></g> <g id="skin-shadow"></g> <g id="line"> <path fill="#000000" stroke="none" d="M29.5059,30.1088c0,0-1.8051,1.2424-2.7484,0.6679c-0.9434-0.5745-1.2424-1.8051-0.6679-2.7484 s1.805-1.2424,2.7484-0.6679S29.5059,30.1088,29.5059,30.1088z" ></path> <path fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M33.1089,37.006h6.1457c0.4011,0,0.7634,0.2397,0.9203,0.6089l1.1579,2.7245l-2.1792,1.1456 c-0.6156,0.3236-1.3654-0.0645-1.4567-0.754" ></path> <path fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M34.7606,40.763c-0.1132,0.6268-0.7757,0.9895-1.3647,0.7471l-2.3132-0.952l1.0899-2.9035 c0.1465-0.3901,0.5195-0.6486,0.9362-0.6486" ></path> <path fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M30.4364,50.0268c0,0-0.7187,8.7934,3.0072,9.9375c2.6459,0.8125,5.1497,0.5324,6.0625-0.25 c0.875-0.75,2.6323-4.4741,1.8267-9.6875" ></path> <path fill="#000000" stroke="none" d="M44.2636,30.1088c0,0,1.805,1.2424,2.7484,0.6679c0.9434-0.5745,1.2424-1.8051,0.6679-2.7484 c-0.5745-0.9434-1.805-1.2424-2.7484-0.6679C43.9881,27.9349,44.2636,30.1088,44.2636,30.1088z" ></path> <path fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M25.6245,42.8393c-0.475,3.6024,2.2343,5.7505,4.2847,6.8414c1.1968,0.6367,2.6508,0.5182,3.7176-0.3181l2.581-2.0233l2.581,2.0233 c1.0669,0.8363,2.5209,0.9548,3.7176,0.3181c2.0504-1.0909,4.7597-3.239,4.2847-6.8414" ></path> <path fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M19.9509,28.3572c-2.3166,5.1597-0.5084,13.0249,0.119,15.3759c0.122,0.4571,0.0755,0.9355-0.1271,1.3631l-1.9874,4.1937 c-0.623,1.3146-2.3934,1.5533-3.331,0.4409c-3.1921-3.7871-8.5584-11.3899-6.5486-16.686 c7.0625-18.6104,15.8677-18.1429,15.8677-18.1429c2.8453-1.9336,13.1042-6.9375,24.8125,0.875c0,0,8.6323-1.7175,14.9375,16.9375 c1.8036,5.3362-3.4297,12.8668-6.5506,16.6442c-0.9312,1.127-2.7162,0.8939-3.3423-0.4272l-1.9741-4.1656 c-0.2026-0.4275-0.2491-0.906-0.1271-1.3631c0.6275-2.3509,2.4356-10.2161,0.119-15.3759" ></path> <path fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M52.6309,46.4628c0,0-3.0781,6.7216-7.8049,8.2712" ></path> <path fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M19.437,46.969c0,0,3.0781,6.0823,7.8049,7.632" ></path> <line x1="36.2078" x2="36.2078" y1="47.3393" y2="44.3093" fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" ></line> </g> </svg> </n-avatar> </template> <template #extra> <n-flex justify="end"> <n-switch v-model:value="darkMode" :rail-style="darkModeRailStyle" > <template #checked> Dark </template> <template #unchecked> Light </template> </n-switch> </n-flex> </template> </n-page-header> <n-tabs type="segment" animated> <n-tab-pane name="summary" tab="Summary"> <x-summary :res="res"></x-summary> </n-tab-pane> <n-tab-pane name="Requests" tab="Requests"> <x-requests :res="res"></x-requests> </n-tab-pane> </n-tabs> </n-flex> </n-card> </n-layout> </n-config-provider> </div> <script type="text/x-template" id="summary-component"> <n-flex vertical> <n-flex justify="center"> <n-alert type="success"> <n-statistic label="Total Controls" :value="summaryTotalControls" > </n-statistic> </n-alert> <n-alert :type="summaryFailedControls ? 'error' : 'success'"> <n-statistic label="Total Failed Controls" :value="summaryFailedControls" > </n-statistic> </n-alert> <n-alert :type="summaryErrors ? 'error' : 'success'"> <n-statistic label="Total errors" :value="summaryErrors"> </n-statistic> </n-alert> </n-flex> <n-card title="TIMINGS AND DATA"> <n-flex justify="center"> <n-statistic label="Total run duration" :value="Math.round(totalRunDuration*1000)/1000" > <template #suffix>s</template> </n-statistic> <n-statistic label="Total requests" :value="summaryTotalRequests" > </n-statistic> </n-flex> </n-card> <n-data-table :columns="summaryColumns" :data="summaryData" /> </n-flex> </script> <script type="text/x-template" id="requests-component"> <n-flex vertical> <n-switch v-model:value="onlyFailed" :rail-style="railStyle" > <template #checked> Only Failed </template> <template #unchecked> Only Failed </template> </n-switch> <n-collapse> <x-results-group v-for="(results, group) in groupedResults" :results="results" :group="group" :key="group + '-' + results.length"></x-results-group> </n-collapse> </n-flex> </script> <script type="text/x-template" id="results-group-component"> <n-collapse-item :name="group" arrow-placement="right" > <template #header> <n-alert :type="hasError || hasFailure ? 'error' : 'success'" :bordered="false" > <template #header> {{group}} - {{totalPassed}} / {{total}} Passed {{ hasError? " - Error" : "" }} </template> </n-alert> </template> <n-collapse> <x-result v-for="(result, index) in results" :result="result" :group="group" :key="index"></x-result> </n-collapse> </n-collapse-item> </script> <script type="text/x-template" id="result-component"> <n-collapse-item :name="name" arrow-placement="right" > <template #header> <n-alert :type="hasError || hasFailure ? 'error' : 'success'" :bordered="false" > <template #header> {{suitename}} - {{totalPassed}} / {{total}} Passed {{hasError ? " - Error" : "" }} </template> </n-alert> </template> <n-flex vertical> <n-grid x-gap="12" :cols="2"> <n-gi> <n-card title="REQUEST INFORMATION"> <n-list> <n-list-item> <n-thing title="File" :description="result.test.filename" /> </n-list-item> <n-list-item> <n-thing title="Request Method" :description="result.request.method" /> </n-list-item> <n-list-item> <n-thing title="Request URL" :description="result.request.url" /> </n-list-item> </n-list> </n-card> </n-gi> <n-gi> <n-card title="RESPONSE INFORMATION"> <n-list> <n-list-item> <n-thing title="Response Code" :description="'' + result.response.status" /> </n-list-item> <n-list-item> <n-thing title="Response time" :description="result.response.responseTime + ' ms'" /> </n-list-item> <n-list-item> <n-thing title="Test duration" :description="testDuration" /> </n-list-item> </n-list> </n-card> </n-gi> </n-grid> <n-alert v-if="hasError" title="Error" type="error"> {{result.error}} </n-alert> <n-card title="REQUEST HEADERS"> <n-data-table :columns="headerColumns" :data="headerDataRequest" /> </n-card> <n-card v-if="result.request.data" title="REQUEST BODY" > <pre>{{result.request.data}}</pre> </n-card> <n-card title="RESPONSE HEADERS"> <n-data-table :columns="headerColumns" :data="headerDataResponse" /> </n-card> <n-card v-if="result.response.data" title="RESPONSE BODY" > <pre>{{result.response.data}}</pre> </n-card> <n-card title="ASSERTIONS INFORMATION"> <n-data-table :columns="assertionsColumns" :data="result.assertionResults" :row-class-name="assertionsRowClassName" /> </n-card> <n-card title="TESTS INFORMATION"> <n-data-table :columns="testsColumns" :data="result.testResults" :row-class-name="testsRowClassName" /> </n-card> </n-flex> </n-collapse-item> </script> <script> const { createApp, ref, computed } = Vue; const App = { setup() { const res = __RESULTS_JSON__; const darkMode = ref(false); const theme = computed(() => { return darkMode.value ? naive.darkTheme : null; }); if ( window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches ) { darkMode.value = true; } // To watch for os theme changes window .matchMedia("(prefers-color-scheme: dark)") .addEventListener("change", (event) => { darkMode.value = event.matches; }); return { res, theme, darkMode, darkModeRailStyle: () => ({ background: "var(--n-rail-color)" }), }; }, }; const app = Vue.createApp(App); app.component("x-summary", { template: `#summary-component`, props: ["res"], setup(props) { const summaryColumns = [ { title: "SUMMARY ITEM", key: "title", }, { title: "TOTAL", key: "total", }, { title: "PASSED", key: "passed", }, { title: "FAILED", key: "failed", }, ]; const summaryData = computed(() => [ { title: "Requests", total: props.res.summary.totalRequests, passed: props.res.summary.passedRequests, failed: props.res.summary.failedRequests, }, { title: "Assertions", total: props.res.summary.totalAssertions, passed: props.res.summary.passedAssertions, failed: props.res.summary.failedAssertions, }, { title: "Tests", total: props.res.summary.totalTests, passed: props.res.summary.passedTests, failed: props.res.summary.failedTests, }, ]); const summaryTotalRequests = computed(() => { return props.res.summary.totalRequests; }); const summaryTotalControls = computed(() => { return ( props.res.summary.totalTests + props.res.summary.totalAssertions ); }); const summaryFailedControls = computed( () => props.res.summary.failedRequests + props.res.summary.failedTests + props.res.summary.failedAssertions, ); const summaryErrors = computed( () => props.res.results.filter((r) => r.error).length, ); const totalRunDuration = computed(() => props.res?.results?.reduce( (total, test) => test.runtime + total, 0, ), ); return { summaryColumns, summaryData, summaryTotalControls, summaryTotalRequests, summaryFailedControls, summaryErrors, totalRunDuration, }; }, }); app.component("x-requests", { template: `#requests-component`, props: ["res"], setup(props) { const onlyFailed = ref(false); const filteredResults = computed(() => { if (onlyFailed.value) { return props.res.results.filter( (r) => !!r.error || !!r.testResults.find((t) => t.status !== "pass") || !!r.assertionResults.find((t) => t.status !== "pass"), ); } return props.res.results; }); const groupedResults = computed(() => { return filteredResults.value.reduce((groups, curr) => { const path = curr.suitename.split("/"); const test = path.pop(); const name = path.length ? path.join("/") : "(root)"; if (!groups[name]) { groups[name] = []; } groups[name].push(curr); return groups; }, {}); }); return { onlyFailed, groupedResults, railStyle: ({ checked }) => { const style = {}; if (checked) { style.background = "#d03050"; } return style; }, }; }, }); app.component("x-results-group", { template: `#results-group-component`, props: ["group", "results"], setup(props) { const totalPassed = computed(() => { return props.results.reduce((total, curr) => { return ( total + curr.testResults.filter((t) => t.status === "pass").length + curr.assertionResults.filter((t) => t.status === "pass").length ); }, 0); }); const total = computed(() => { return props.results.reduce((total, curr) => { return ( total + curr.testResults.length + curr.assertionResults.length ); }, 0); }); const hasError = computed(() => props.results.some((r) => !!r.error)); const hasFailure = computed(() => totalPassed.value !== total.value); return { totalPassed, total, hasFailure, hasError, group: props.group, results: props.results, }; }, }); app.component("x-result", { template: `#result-component`, props: ["group", "result"], setup(props) { const headerColumns = [ { title: "Header Name", key: "name", className: "min-width-150", }, { title: "Header Value", key: "value", }, ]; const assertionsColumns = [ { title: "Expression", key: "lhsExpr", }, { title: "Operator", key: "operator", }, { title: "Operand", key: "rhsOperand", }, { title: "Status", key: "status", className: "status", }, { title: "Error", key: "error", }, ]; const assertionsRowClassName = (row) => { return row.status === "fail" ? "error" : "success"; }; const testsRowClassName = (row) => { return row.status === "fail" ? "error" : "success"; }; const testsColumns = [ { title: "Description", key: "description", }, { title: "Status", key: "status", className: "status", }, { title: "Error", key: "error", }, ]; function mapHeaderToTableData(headers) { if (!headers) { return []; } return Object.keys(headers).map((name) => ({ name, value: headers[name], })); } const headerDataRequest = computed(() => { return mapHeaderToTableData(props.result.request.headers); }); const headerDataResponse = computed(() => { return mapHeaderToTableData(props.result.response.headers); }); const totalPassed = computed(() => { return ( props.result.testResults.filter((t) => t.status === "pass") .length + props.result.assertionResults.filter((t) => t.status === "pass") .length ); }); const total = computed(() => { return ( props.result.testResults.length + props.result.assertionResults.length ); }); const hasError = computed(() => !!props.result.error); const hasFailure = computed(() => total.value !== totalPassed.value); const suitename = computed(() => props.result.suitename.replace(props.group + "/", ""), ); const testDuration = computed( () => Math.round(props.result.runtime * 1000) + " ms", ); const name = computed( () => props.result.suitename + props.result.runtime, ); return { headerColumns, headerDataRequest, headerDataResponse, assertionsColumns, assertionsRowClassName, testsRowClassName, totalPassed, total, hasFailure, hasError, testsColumns, result: props.result, suitename, testDuration, name, }; }, }); app.use(naive); app.mount("#app"); </script> </body> </html>