diff --git a/src/server/report/app.js b/src/server/report/app.js index f0abe727..46fcd2a8 100644 --- a/src/server/report/app.js +++ b/src/server/report/app.js @@ -1,7 +1,6 @@ import './test.js'; import { css, html, LitElement, nothing } from 'lit'; import { FILTER_STATUS, FULL_MODE, LAYOUTS } from './common.js'; -import { classMap } from 'lit/directives/class-map.js'; import data from './data.js'; import page from 'page'; @@ -42,6 +41,7 @@ class App extends LitElement { table { background-color: #ffffff; border-collapse: collapse; + width: 100%; } td, th { border: 1px solid #dfe6ef; @@ -120,10 +120,15 @@ class App extends LitElement { } else { hasPadding = false; view = html` - -
- -
+ `; } } @@ -134,6 +139,9 @@ class App extends LitElement { view = this._files.map(f => this._renderFile(f)); } } + if (hasPadding) { + view = html`
${view}
`; + } return html`
-
${view}
+
${view}
`; } - _goHome() { - this._updateSearchParams({ file: undefined, test: undefined }); - } _handleFilterBrowserChange(e) { const browsers = data.browsers.map(b => b.name).filter(b => { if (b === e.target.value) { @@ -162,6 +167,13 @@ class App extends LitElement { _handleFilterStatusChange(e) { this._updateSearchParams({ status: e.target.value }); } + _handleNavigation(e) { + switch (e.detail.location) { + case 'home': + this._updateSearchParams({ file: undefined, test: undefined }); + break; + } + } _handleSettingChange(e) { this[`_${e.detail.name}`] = e.detail.value; } @@ -217,15 +229,18 @@ class App extends LitElement { `; }; + const browserFilter = data.browsers.length > 1 ? html` +
+ Browsers + ${ data.browsers.map(b => renderBrowser(b))} +
` : nothing; + return html`
Test Status ${statusFilters.map(f => renderStatusFilter(f))}
-
- Browsers - ${ data.browsers.map(b => renderBrowser(b))} -
+ ${browserFilter} `; } diff --git a/src/server/report/button.js b/src/server/report/button.js new file mode 100644 index 00000000..3df083c3 --- /dev/null +++ b/src/server/report/button.js @@ -0,0 +1,40 @@ +import { css, html, LitElement } from 'lit'; + +class Button extends LitElement { + static properties = { + text: { type: String } + }; + static styles = [css` + :host { + display: inline-block; + } + button { + align-items: center; + background-color: #ffffff; + border: 1px solid #cdd5dc; + border-radius: 5px; + cursor: pointer; + display: flex; + gap: 5px; + line-height: 24px; + margin: 0; + outline: none; + padding: 10px; + user-select: none; + } + button:hover, + button:focus { + background-color: #007bff; + color: #ffffff; + } + button:focus-visible { + border-color: #007bff; + box-shadow: 0 0 0 2px #ffffff, 0 0 0 4px #007bff; + } + `]; + render() { + return html``; + } +} + +customElements.define('d2l-vdiff-report-button', Button); diff --git a/src/server/report/icons.js b/src/server/report/icons.js index 56cb0b2e..47b9b53d 100644 --- a/src/server/report/icons.js +++ b/src/server/report/icons.js @@ -1,5 +1,11 @@ import { svg } from 'lit'; +export const ICON_HOME = svg` + + + + `; + export const ICON_FULL = svg` @@ -12,3 +18,144 @@ export const ICON_SPLIT = svg` `; + +export const ICON_BROWSERS = { + Chromium: svg``, + Firefox: svg` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + `, + Webkit: svg` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +` +}; diff --git a/src/server/report/test-result.js b/src/server/report/test-result.js index 045ae85b..8f085d0c 100644 --- a/src/server/report/test-result.js +++ b/src/server/report/test-result.js @@ -12,6 +12,10 @@ class TestResult extends LitElement { test: { type: String }, }; static styles = [css` + :host { + display: block; + padding: 20px; + } .split { flex-direction: row; flex-wrap: nowrap; @@ -39,7 +43,6 @@ class TestResult extends LitElement { position: absolute; top: 0; left: 0; - z-index: 1; } .no-changes { border: 1px solid #cdd5dc; @@ -48,11 +51,10 @@ class TestResult extends LitElement { `]; render() { - const { browserData, fileData, resultData, testData } = this._fetchData(); - if (!browserData || !fileData || !resultData || !testData) return nothing; + const resultData = this._fetchData(); + if (!resultData) return nothing; return html` -

${resultData.name} v${browserData.version} (${resultData.duration}ms)

${this._renderBody(resultData)} `; @@ -71,7 +73,7 @@ class TestResult extends LitElement { const browserData = data.browsers.find(b => b.name === this.browser); if (!browserData) return {}; - return { browserData, fileData, resultData, testData }; + return resultData; } _renderBody(resultData) { diff --git a/src/server/report/test.js b/src/server/report/test.js index da1ffca5..25cedd06 100644 --- a/src/server/report/test.js +++ b/src/server/report/test.js @@ -1,32 +1,85 @@ +import './button.js'; import './test-result.js'; import { css, html, LitElement, nothing } from 'lit'; import { FULL_MODE, getId, LAYOUTS } from './common.js'; +import { ICON_BROWSERS, ICON_HOME } from './icons.js'; +import { classMap } from 'lit/directives/class-map.js'; import data from './data.js'; class Test extends LitElement { static properties = { + browsers: { type: String }, file: { type: String }, fullMode: { attribute: 'full-mode', type: String }, layout: { type: String }, showOverlay: { attribute: 'show-overlay', type: Boolean }, test: { type: String }, + _selectedBrowserIndex: { state: true } }; static styles = [css` + :host { + display: grid; + grid-template-rows: auto 1fr auto; + grid-template-areas: + 'header' + 'content' + 'footer'; + height: 100vh; + } .header { + border-bottom: 1px solid #cdd5dc; + grid-area: header; + } + .tab-panels { + grid-area: content; + overflow: auto; + } + .footer { + align-items: center; + border-top: 1px solid #cdd5dc; + display: flex; + gap: 10px; + grid-area: footer; + padding: 20px; + } + .footer > svg { + flex: 0 0 auto; + height: 50px; + width: 50px; + } + .footer-info { + flex: 1 0 auto; + } + .footer-browser-name { + font-size: 1.2rem; + font-weight: bold; + } + .footer-timing { + flex: 0 0 auto; + font-size: 2rem; + font-weight: bold; + } + .header, .footer { background-color: #f0f0f0; - border-bottom: 1px solid #e6e6e6; box-shadow: 0 0 6px rgba(0,0,0,.07); - position: sticky; - top: 0; - z-index: 2; + } + .title { + display: flex; + padding: 20px 20px 0 20px; } .title h2 { margin: 0; } + .title-info { + flex: 1 0 auto; + } + .title-navigation { + flex: 0 0 auto; + } .settings { align-items: center; display: flex; - padding-top: 20px; + padding: 20px; gap: 20px; } .settings-box { @@ -37,9 +90,6 @@ class Test extends LitElement { padding: 10px; user-select: none; } - .header, .results { - padding: 20px; - } .pill-box { border-radius: 5px; display: flex; @@ -84,10 +134,66 @@ class Test extends LitElement { border-color: #007bff; box-shadow: 0 0 0 2px #ffffff, 0 0 0 4px #007bff; } + [role="tablist"] { + align-items: stretch; + border-top: 1px solid #cdd5dc; + display: flex; + flex: 0 0 auto; + flex-wrap: nowrap; + } + [role="tab"] { + background: none; + border: none; + border-right: 1px solid #cdd5dc; + cursor: pointer; + flex: 1 0 auto; + margin: 0; + outline: none; + padding: 10px 15px; + position: relative; + user-select: none; + } + [role="tab"]:last-child { + border-right: none; + } + [role="tab"] > span { + display: inline-block; + padding: 5px; + } + [role="tab"]:focus-visible > span { + border: 2px solid #007bff; + border-radius: 3px; + padding: 3px; + } + [role="tab"]:hover > span { + color: #007bff; + } + .tab-selected-indicator { + border-block-start: 4px solid #007bff; + border-start-start-radius: 4px; + border-start-end-radius: 4px; + bottom: 0; + position: absolute; + width: calc(100% - 30px); + } + .pass { + color: #46a661; + } + .error { + color: #cd2026; + } + .warning { + color: #e87511; + } `]; + constructor() { + super(); + this.browsers = []; + this._selectedBrowserIndex = -1; + } render() { - const { fileData, testData } = this._fetchData(); + const { browsers, fileData, testData } = this._fetchData(); if (!fileData || !testData) return nothing; let fullMode = nothing; @@ -95,21 +201,30 @@ class Test extends LitElement { fullMode = this._renderPillbox('fullMode', this.fullMode, [FULL_MODE.GOLDEN, FULL_MODE.NEW]); } + const selectedBrowser = this._getSelectedBrowser(browsers, testData); + const selectedResult = testData.results.find(r => r.name === selectedBrowser.name); + const tabButtons = browsers.length > 1 ? this._renderTabButtons(browsers, selectedBrowser, testData) : nothing; + return html`
-

${testData.name}

-
${fileData.name}
+
+

${testData.name}

+
${fileData.name}
+
+
+ ${ICON_HOME} +
${this._renderPillbox('layout', this.layout, [LAYOUTS.FULL, LAYOUTS.SPLIT])} ${fullMode}
+ ${tabButtons}
-
- ${testData.results.map(r => html``)} -
+ ${this._renderTabPanels(browsers, selectedBrowser, fileData, testData)} + ${this._renderFooter(selectedBrowser, selectedResult)} `; } @@ -121,15 +236,45 @@ class Test extends LitElement { const testData = fileData.tests.find(t => t.name === this.test); if (!testData) return {}; - return { fileData, testData }; + const filteredBrowsers = this.browsers.split(','); + const browsers = data.browsers.filter(b => filteredBrowsers.includes(b.name)); + + return { browsers, fileData, testData }; } + _getSelectedBrowser(browsers, { results }) { + return browsers[this._selectedBrowserIndex] || + browsers.find(b => results.find(r => r.name === b.name && !r.passed)) || + browsers[0]; + } + _handleBackClick() { + this._triggerNavigation('home'); + } _handleOverlayChange(e) { this._triggerChange('overlay', e.target.checked); } _handlePillboxChange(e) { this._triggerChange(e.target.name, e.target.value); } + _renderFooter(selectedBrowser, selectedResult) { + const duration = selectedResult.duration; + const durationClass = { + 'error': duration >= 1000, + 'footer-timing': true, + 'pass': duration < 500, + 'warning': duration >= 500 && duration < 1000 + }; + return html` + + `; + } _renderPillbox(name, selectedValue, items) { const renderItem = (i) => { const id = getId(); @@ -142,6 +287,79 @@ class Test extends LitElement { }; return html`
${items.map(i => renderItem(i))}
`; } + _renderTabButtons(browsers, selectedBrowser, testData) { + + const onKeyDown = (e) => { + let focusOn; + switch (e.key) { + case 'ArrowRight': + focusOn = e.target.nextElementSibling || e.target.parentNode.firstElementChild; + break; + case 'ArrowLeft': + focusOn = e.target.previousElementSibling || e.target.parentNode.lastElementChild; + break; + case 'Home': + focusOn = e.target.parentNode.firstElementChild; + break; + case 'End': + focusOn = e.target.parentNode.lastElementChild; + break; + } + if (focusOn) focusOn.focus(); + }; + + const renderTabButton = (browser, index) => { + const result = testData.results.find(r => r.name === browser); + const selected = (browser === selectedBrowser.name); + const status = result.passed ? 'passed' : 'failed'; + const onClick = () => { + return () => this._selectedBrowserIndex = index; + }; + const statusClass = { + pass: result.passed, + error: !result.passed + }; + return html` + `; + }; + + return html` +
+ ${browsers.map((b, i) => renderTabButton(b.name, i))} +
+ `; + } + _renderTabPanels(browsers, selectedBrowser, fileData, testData) { + + const renderTabPanel = (browser) => { + return html` +
+ +
+ `; + }; + + return html`
${browsers.map(b => renderTabPanel(b.name))}
`; + + } _triggerChange(name, value) { this.dispatchEvent(new CustomEvent( 'setting-change', { @@ -154,6 +372,17 @@ class Test extends LitElement { } )); } + _triggerNavigation(location) { + this.dispatchEvent(new CustomEvent( + 'navigation', { + bubbles: false, + composed: false, + detail: { + location: location + } + } + )); + } } customElements.define('d2l-vdiff-report-test', Test);