UNPKG

usezap-cli

Version:

Zap CLI - Command-line interface for Zap API client

530 lines (516 loc) 17.8 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>Zap</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="Zap run dashboard"> <template #avatar> <n-avatar size="large" style="background-color: transparent"> <img src="https://zap-cli.github.io/zap-cli/assets/logo.png" alt="Zap Logo" /> </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>