From 6521a6f3ab4d142f50632ca71d06493e1e4150f6 Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Sun, 12 Dec 2021 14:56:12 -0800 Subject: [PATCH] chore: split html report into files (#10876) --- .../src/web/htmlReport/chip.css | 70 +++ .../src/web/htmlReport/chip.tsx | 47 ++ .../src/web/htmlReport/common.css | 266 ++++++++ .../src/web/htmlReport/filter.ts | 138 +++++ .../src/web/htmlReport/htmlReport.css | 579 +----------------- .../src/web/htmlReport/htmlReport.tsx | 513 ++-------------- .../src/web/htmlReport/links.css | 104 ++++ .../src/web/htmlReport/links.tsx | 64 ++ .../src/web/htmlReport/statsNavView.tsx | 43 ++ .../src/web/htmlReport/statusIcon.tsx | 43 ++ .../src/web/htmlReport/tabbedPane.css | 76 +++ .../src/web/htmlReport/tabbedPane.tsx | 53 ++ .../src/web/htmlReport/testCaseView.css | 69 +++ .../src/web/htmlReport/testCaseView.tsx | 57 ++ .../src/web/htmlReport/testFileView.css | 38 ++ .../src/web/htmlReport/testFileView.tsx | 54 ++ .../src/web/htmlReport/testResultView.css | 64 ++ .../src/web/htmlReport/testResultView.tsx | 182 ++++++ .../src/web/htmlReport/treeItem.css | 30 + .../{components => htmlReport}/treeItem.tsx | 5 +- tests/playwright-test/reporter-html.spec.ts | 20 +- 21 files changed, 1464 insertions(+), 1051 deletions(-) create mode 100644 packages/playwright-core/src/web/htmlReport/chip.css create mode 100644 packages/playwright-core/src/web/htmlReport/chip.tsx create mode 100644 packages/playwright-core/src/web/htmlReport/common.css create mode 100644 packages/playwright-core/src/web/htmlReport/filter.ts create mode 100644 packages/playwright-core/src/web/htmlReport/links.css create mode 100644 packages/playwright-core/src/web/htmlReport/links.tsx create mode 100644 packages/playwright-core/src/web/htmlReport/statsNavView.tsx create mode 100644 packages/playwright-core/src/web/htmlReport/statusIcon.tsx create mode 100644 packages/playwright-core/src/web/htmlReport/tabbedPane.css create mode 100644 packages/playwright-core/src/web/htmlReport/tabbedPane.tsx create mode 100644 packages/playwright-core/src/web/htmlReport/testCaseView.css create mode 100644 packages/playwright-core/src/web/htmlReport/testCaseView.tsx create mode 100644 packages/playwright-core/src/web/htmlReport/testFileView.css create mode 100644 packages/playwright-core/src/web/htmlReport/testFileView.tsx create mode 100644 packages/playwright-core/src/web/htmlReport/testResultView.css create mode 100644 packages/playwright-core/src/web/htmlReport/testResultView.tsx create mode 100644 packages/playwright-core/src/web/htmlReport/treeItem.css rename packages/playwright-core/src/web/{components => htmlReport}/treeItem.tsx (96%) diff --git a/packages/playwright-core/src/web/htmlReport/chip.css b/packages/playwright-core/src/web/htmlReport/chip.css new file mode 100644 index 0000000000..f8d7938f90 --- /dev/null +++ b/packages/playwright-core/src/web/htmlReport/chip.css @@ -0,0 +1,70 @@ +/* + Copyright (c) Microsoft Corporation. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +.chip-header { + border: 1px solid var(--color-border-default); + border-top-left-radius: 6px; + border-top-right-radius: 6px; + background-color: var(--color-canvas-subtle); + padding: 0 8px; + border-bottom: none; + margin-top: 24px; + font-weight: 600; + line-height: 38px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.chip-header.expanded-false { + border: 1px solid var(--color-border-default); + border-radius: 6px; +} + +.chip-header.expanded-false, +.chip-header.expanded-true { + cursor: pointer; +} + +.chip-body { + border: 1px solid var(--color-border-default); + border-bottom-left-radius: 6px; + border-bottom-right-radius: 6px; + padding: 16px; +} + +.chip-body-no-insets { + padding: 0; +} + +@media only screen and (max-width: 600px) { + .chip-header { + border-radius: 0; + border-right: none; + border-left: none; + } + + .chip-body { + border-radius: 0; + border-right: none; + border-left: none; + padding: 8px; + } + + .chip-body-no-insets { + padding: 0; + } +} diff --git a/packages/playwright-core/src/web/htmlReport/chip.tsx b/packages/playwright-core/src/web/htmlReport/chip.tsx new file mode 100644 index 0000000000..2ca3811764 --- /dev/null +++ b/packages/playwright-core/src/web/htmlReport/chip.tsx @@ -0,0 +1,47 @@ +/* + Copyright (c) Microsoft Corporation. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +import * as React from 'react'; +import './chip.css'; + +export const Chip: React.FunctionComponent<{ + header: JSX.Element | string, + expanded?: boolean, + noInsets?: boolean, + setExpanded?: (expanded: boolean) => void, + children?: any, +}> = ({ header, expanded, setExpanded, children, noInsets }) => { + return
+
setExpanded?.(!expanded)}> + {setExpanded && !!expanded && downArrow()} + {setExpanded && !expanded && rightArrow()} + {header} +
+ {(!setExpanded || expanded) &&
{children}
} +
; +}; + +function downArrow() { + return ; +} + +function rightArrow() { + return ; +} diff --git a/packages/playwright-core/src/web/htmlReport/common.css b/packages/playwright-core/src/web/htmlReport/common.css new file mode 100644 index 0000000000..6788413099 --- /dev/null +++ b/packages/playwright-core/src/web/htmlReport/common.css @@ -0,0 +1,266 @@ +/* + Copyright (c) Microsoft Corporation. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +* { + box-sizing: border-box; + min-width: 0; + min-height: 0; +} + +svg { + fill: currentColor; +} + +.vbox { + display: flex; + flex-direction: column; + flex: auto; + position: relative; +} + +.hbox { + display: flex; + flex: auto; + position: relative; +} + +.d-flex { + display: flex !important; +} + +.d-inline { + display: inline !important; +} + +.m-1 { margin: 4px; } +.m-2 { margin: 8px; } +.m-3 { margin: 16px; } +.m-4 { margin: 24px; } +.m-5 { margin: 32px; } + +.mx-1 { margin: 0 4px; } +.mx-2 { margin: 0 8px; } +.mx-3 { margin: 0 16px; } +.mx-4 { margin: 0 24px; } +.mx-5 { margin: 0 32px; } + +.my-1 { margin: 4px 0; } +.my-2 { margin: 8px 0; } +.my-3 { margin: 16px 0; } +.my-4 { margin: 24px 0; } +.my-5 { margin: 32px 0; } + +.mt-1 { margin-top: 4px; } +.mt-2 { margin-top: 8px; } +.mt-3 { margin-top: 16px; } +.mt-4 { margin-top: 24px; } +.mt-5 { margin-top: 32px; } + +.mr-1 { margin-right: 4px; } +.mr-2 { margin-right: 8px; } +.mr-3 { margin-right: 16px; } +.mr-4 { margin-right: 24px; } +.mr-5 { margin-right: 32px; } + +.mb-1 { margin-bottom: 4px; } +.mb-2 { margin-bottom: 8px; } +.mb-3 { margin-bottom: 16px; } +.mb-4 { margin-bottom: 24px; } +.mb-5 { margin-bottom: 32px; } + +.ml-1 { margin-left: 4px; } +.ml-2 { margin-left: 8px; } +.ml-3 { margin-left: 16px; } +.ml-4 { margin-left: 24px; } +.ml-5 { margin-left: 32px; } + +.p-1 { padding: 4px; } +.p-2 { padding: 8px; } +.p-3 { padding: 16px; } +.p-4 { padding: 24px; } +.p-5 { padding: 32px; } + +.px-1 { padding: 0 4px; } +.px-2 { padding: 0 8px; } +.px-3 { padding: 0 16px; } +.px-4 { padding: 0 24px; } +.px-5 { padding: 0 32px; } + +.py-1 { padding: 4px 0; } +.py-2 { padding: 8px 0; } +.py-3 { padding: 16px 0; } +.py-4 { padding: 24px 0; } +.py-5 { padding: 32px 0; } + +.pt-1 { padding-top: 4px; } +.pt-2 { padding-top: 8px; } +.pt-3 { padding-top: 16px; } +.pt-4 { padding-top: 24px; } +.pt-5 { padding-top: 32px; } + +.pr-1 { padding-right: 4px; } +.pr-2 { padding-right: 8px; } +.pr-3 { padding-right: 16px; } +.pr-4 { padding-right: 24px; } +.pr-5 { padding-right: 32px; } + +.pb-1 { padding-bottom: 4px; } +.pb-2 { padding-bottom: 8px; } +.pb-3 { padding-bottom: 16px; } +.pb-4 { padding-bottom: 24px; } +.pb-5 { padding-bottom: 32px; } + +.pl-1 { padding-left: 4px; } +.pl-2 { padding-left: 8px; } +.pl-3 { padding-left: 16px; } +.pl-4 { padding-left: 24px; } +.pl-5 { padding-left: 32px; } + +.no-wrap { + white-space: nowrap !important; +} + +.float-left { + float: left !important; +} + +article, aside, details, figcaption, figure, footer, header, main, menu, nav, section { + display: block; +} + +.form-control, .form-select { + padding: 5px 12px; + font-size: 14px; + line-height: 20px; + color: var(--color-fg-default); + vertical-align: middle; + background-color: var(--color-canvas-default); + background-repeat: no-repeat; + background-position: right 8px center; + border: 1px solid var(--color-border-default); + border-radius: 6px; + outline: none; + box-shadow: var(--color-primer-shadow-inset); +} + +.input-contrast { + background-color: var(--color-canvas-inset); +} + +.subnav-search { + position: relative; + flex: auto; + display: flex; +} + +.subnav-search-input { + flex: auto; + padding-left: 32px; + color: var(--color-fg-muted); +} + +.subnav-search-icon { + position: absolute; + top: 9px; + left: 8px; + display: block; + color: var(--color-fg-muted); + text-align: center; + pointer-events: none; +} + +.subnav-search-context + .subnav-search { + margin-left: -1px; +} + +.subnav-item { + flex: none; + position: relative; + float: left; + padding: 5px 10px; + font-weight: 500; + line-height: 20px; + color: var(--color-fg-default); + border: 1px solid var(--color-border-default); +} + +.subnav-item:hover { + background-color: var(--color-canvas-subtle); +} + +.subnav-item:first-child { + border-top-left-radius: 6px; + border-bottom-left-radius: 6px; +} + +.subnav-item:last-child { + border-top-right-radius: 6px; + border-bottom-right-radius: 6px; +} + +.subnav-item + .subnav-item { + margin-left: -1px; +} + +.counter { + display: inline-block; + min-width: 20px; + padding: 0 6px; + font-size: 12px; + font-weight: 500; + line-height: 18px; + color: var(--color-fg-default); + text-align: center; + background-color: var(--color-neutral-muted); + border: 1px solid transparent; + border-radius: 2em; +} + +.color-icon-success { + color: var(--color-success-fg) !important; +} + +.color-text-danger { + color: var(--color-danger-fg) !important; +} + +.color-text-warning { + color: var(--color-checks-step-warning-text) !important; +} + +.color-fg-muted { + color: var(--color-fg-muted) !important; +} + +.octicon { + display: inline-block; + overflow: visible !important; + vertical-align: text-bottom; + fill: currentColor; + margin-right: 7px; + flex: none; +} + +@media only screen and (max-width: 600px) { + .subnav-item, .form-control { + border-radius: 0 !important; + } + + .subnav-item { + padding: 5px 3px; + border: none; + } +} diff --git a/packages/playwright-core/src/web/htmlReport/filter.ts b/packages/playwright-core/src/web/htmlReport/filter.ts new file mode 100644 index 0000000000..ed0ff9ee95 --- /dev/null +++ b/packages/playwright-core/src/web/htmlReport/filter.ts @@ -0,0 +1,138 @@ +/* + Copyright (c) Microsoft Corporation. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +import type { TestCaseSummary } from '@playwright/test/src/reporters/html'; +import './htmlReport.css'; + +export class Filter { + project: string[] = []; + status: string[] = []; + text: string[] = []; + + empty(): boolean { + return this.project.length + this.status.length + this.text.length === 0; + } + + static parse(expression: string): Filter { + const tokens = Filter.tokenize(expression); + const project = new Set(); + const status = new Set(); + const text: string[] = []; + for (const token of tokens) { + if (token.startsWith('p:')) { + project.add(token.slice(2)); + continue; + } + if (token.startsWith('s:')) { + status.add(token.slice(2)); + continue; + } + text.push(token.toLowerCase()); + } + + const filter = new Filter(); + filter.text = text; + filter.project = [...project]; + filter.status = [...status]; + return filter; + } + + private static tokenize(expression: string): string[] { + const result: string[] = []; + let quote: '\'' | '"' | undefined; + let token: string[] = []; + for (let i = 0; i < expression.length; ++i) { + const c = expression[i]; + if (quote && c === '\\' && expression[i + 1] === quote) { + token.push(quote); + ++i; + continue; + } + if (c === '"' || c === '\'') { + if (quote === c) { + result.push(token.join('').toLowerCase()); + token = []; + quote = undefined; + } else if (quote) { + token.push(c); + } else { + quote = c; + } + continue; + } + if (quote) { + token.push(c); + continue; + } + if (c === ' ') { + if (token.length) { + result.push(token.join('').toLowerCase()); + token = []; + } + continue; + } + token.push(c); + } + if (token.length) + result.push(token.join('').toLowerCase()); + return result; + } + + matches(test: TestCaseSummary): boolean { + if (!(test as any).searchValues) { + let status = 'passed'; + if (test.outcome === 'unexpected') + status = 'failed'; + if (test.outcome === 'flaky') + status = 'flaky'; + if (test.outcome === 'skipped') + status = 'skipped'; + const searchValues: SearchValues = { + text: (status + ' ' + test.projectName + ' ' + test.path.join(' ') + test.title).toLowerCase(), + project: test.projectName.toLowerCase(), + status: status as any + }; + (test as any).searchValues = searchValues; + } + + const searchValues = (test as any).searchValues as SearchValues; + if (this.project.length) { + const matches = !!this.project.find(p => searchValues.project.includes(p)); + if (!matches) + return false; + } + if (this.status.length) { + const matches = !!this.status.find(s => searchValues.status.includes(s)); + if (!matches) + return false; + } + + if (this.text.length) { + const matches = this.text.filter(t => searchValues.text.includes(t)).length === this.text.length; + if (!matches) + return false; + } + + return true; + } +} + +type SearchValues = { + text: string; + project: string; + status: 'passed' | 'failed' | 'flaky' | 'skipped'; +}; + diff --git a/packages/playwright-core/src/web/htmlReport/htmlReport.css b/packages/playwright-core/src/web/htmlReport/htmlReport.css index 27e9368120..c8b623c313 100644 --- a/packages/playwright-core/src/web/htmlReport/htmlReport.css +++ b/packages/playwright-core/src/web/htmlReport/htmlReport.css @@ -24,22 +24,6 @@ rgb(0 0 0 / 25%) 0px 6px 12px; } -html, body { - width: 100%; - height: 100%; - padding: 0; - margin: 0; - overscroll-behavior-x: none; -} - -body { - width: 100vw; -} - -body { - overflow: auto; -} - #root { width: 100%; height: 100%; @@ -49,575 +33,42 @@ body { -webkit-font-smoothing: antialiased; } -* { - box-sizing: border-box; - min-width: 0; - min-height: 0; -} - -svg { - fill: currentColor; -} - -.vbox { - display: flex; - flex-direction: column; - flex: auto; - position: relative; -} - -.hbox { - display: flex; - flex: auto; - position: relative; -} - -.global-stats { - padding-left: 34px; - margin-top: 20px; - font-weight: 600; -} - -.test-case-column { - border-radius: 6px; - margin: 20px 0; -} - -.tree-item { - text-overflow: ellipsis; - overflow: hidden; - white-space: nowrap; -} - -.tree-item-title { - cursor: pointer; -} - -.chip-body > .tree-item { - line-height: 38px; -} - -.tree-item-body { - min-height: 18px; -} - -.error-message { - white-space: pre; - font-family: monospace; - overflow: auto; - flex: none; +html, body { + width: 100%; + height: 100%; padding: 0; - background-color: var(--color-canvas-subtle); - border-radius: 6px; - padding: 16px; - line-height: initial; + margin: 0; + overscroll-behavior-x: none; } -.status-icon { - padding-right: 3px; -} - -.test-result { - flex: auto; - display: flex; - flex-direction: column; - margin-bottom: 20px; -} - -.test-result .tabbed-pane .tab-content { - display: flex; - align-items: center; - justify-content: center; -} - -.attachment-body { - white-space: pre-wrap; - font-family: monospace; - background-color: var(--color-canvas-subtle); - margin-left: 24px; - line-height: normal; - padding: 5px; -} - -.test-result > div { - flex: none; -} - -.columns > .tab-strip { - font-size: 14px; - line-height: 30px; - color: var(--color-fg-default); - height: 48px; - background-color: var(--color-canvas-subtle); - min-width: 70px; -} - -.tab-strip { - box-shadow: inset 0 -1px 0 var(--color-border-muted) !important; -} - -.test-case-column .tab-element.selected { - font-weight: 600; - border-bottom-color: var(--color-primer-border-active); -} - -.test-case-column .tab-element { - border: none; - color: var(--color-fg-default); - border-bottom: 2px solid transparent; -} - -.test-case-column .tab-element:hover { - color: var(--color-fg-default); -} - -.test-case-column .tab-strip { - margin-top: 10px; - background-color: inherit; -} - -.test-case-title { - flex: none; - padding: 10px; - font-weight: 400; - font-size: 32px !important; - line-height: 1.25 !important; -} - -.test-case-location { - flex: none; - align-items: center; - padding: 0 10px 20px; -} - -.test-case-path { - flex: none; - align-items: center; - padding: 0 10px; -} - -.test-case-annotation { - flex: none; - align-items: center; - padding: 0 10px; - line-height: 24px; -} - -.test-details-column { - overflow-y: auto; -} - -.step-log { - line-height: 20px; - white-space: pre; - padding: 8px; -} - -.tree-text { - overflow: hidden; - text-overflow: ellipsis; -} - -.stats-line { - font-weight: normal; - padding-left: 25px; -} - -.stats { - margin: 0 2px; - padding: 0 2px; -} - -video, img { - flex: none; - box-shadow: var(--box-shadow-thick); - margin: 20px auto; - min-width: 200px; - max-width: 80%; -} - -.flow-container { +body { + overflow: auto; max-width: 1024px; margin: 0 auto; width: 100%; - padding: 0 16px; } -.file-summary-list { - padding: 20px 0; -} - -.file-summary-list .chip-body .test-summary:not(:first-child), -.failed-test:not(:first-child) { +.test-file-test:not(:first-child) { border-top: 1px solid var(--color-border-default); } -.failed-file-subtitle { - padding-left: 5px; - font-weight: 600; - color: var(--color-danger-fg); -} - -.failed-test { - padding: 0 15px 0 10px; - line-height: 28px; -} - -.failed-test-title { - font-weight: 600; -} - -.failed-test-path { - padding: 5px 5px 0 0; - color: var(--color-fg-muted); -} - -.failed-test .error-message { - margin: 20px 0 0; -} - -.failed-test:hover { - background-color: var(--color-canvas-subtle); -} - -a.no-decorations { - text-decoration: none; - color: initial; -} - -.chip-header { - border: 1px solid var(--color-border-default); - border-top-left-radius: 6px; - border-top-right-radius: 6px; - background-color: var(--color-canvas-subtle); - padding: 0 10px; - border-bottom: none; - margin-top: 20px; - font-weight: 600; - line-height: 38px; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -.chip-header.expanded-false { - border: 1px solid var(--color-border-default); - border-radius: 6px; -} - -.chip-header.expanded-false, -.chip-header.expanded-true { - cursor: pointer; -} - -.chip-body { - border: 1px solid var(--color-border-default); - border-bottom-left-radius: 6px; - border-bottom-right-radius: 6px; - padding: 15px; -} - -.failed-tests { - padding-bottom: 20px; -} - -.file-summary-list .chip-body, -.failed-tests .chip-body { - padding: 0; -} - -.test-summary { - height: 38px; - line-height: 38px; - align-items: center; - padding: 0 10px; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -.test-summary:hover { - background-color: var(--color-canvas-subtle); -} - -.test-summary-path { - padding: 0 0 0 5px; - color: var(--color-fg-muted); -} - -.test-summary.outcome-skipped { - color: var(--color-fg-muted); -} - - .status-container { float: right; } -.octicon { - display: inline-block; - overflow: visible !important; - vertical-align: text-bottom; - fill: currentColor; -} - -.color-icon-success { - color: var(--color-success-fg) !important; -} - -.color-text-danger { - color: var(--color-danger-fg) !important; -} - -.color-text-warning { - color: var(--color-checks-step-warning-text) !important; -} - -.color-fg-muted { - color: var(--color-fg-muted) !important; -} - -.octicon { - margin-right: 7px; - flex: none; -} - -.label { - display: inline-block; - padding: 0 7px; - font-size: 12px; - font-weight: 500; - line-height: 18px; - border: 1px solid transparent; - border-radius: 2em; - background-color: var(--color-scale-gray-4); - color: white; - margin: 0 10px; - flex: none; - font-weight: 600; -} - -@media(prefers-color-scheme: light) { - .label-color-0 { - background-color: var(--color-scale-blue-0); - color: var(--color-scale-blue-6); - border: 1px solid var(--color-scale-blue-4); - } - .label-color-1 { - background-color: var(--color-scale-yellow-0); - color: var(--color-scale-yellow-6); - border: 1px solid var(--color-scale-yellow-4); - } - .label-color-2 { - background-color: var(--color-scale-purple-0); - color: var(--color-scale-purple-6); - border: 1px solid var(--color-scale-purple-4); - } - .label-color-3 { - background-color: var(--color-scale-pink-0); - color: var(--color-scale-pink-6); - border: 1px solid var(--color-scale-pink-4); - } - .label-color-4 { - background-color: var(--color-scale-coral-0); - color: var(--color-scale-coral-6); - border: 1px solid var(--color-scale-coral-4); - } - .label-color-5 { - background-color: var(--color-scale-orange-0); - color: var(--color-scale-orange-6); - border: 1px solid var(--color-scale-orange-4); - } -} - -@media(prefers-color-scheme: dark) { - .label-color-0 { - background-color: var(--color-scale-blue-9); - color: var(--color-scale-blue-2); - border: 1px solid var(--color-scale-blue-4); - } - .label-color-1 { - background-color: var(--color-scale-yellow-9); - color: var(--color-scale-yellow-2); - border: 1px solid var(--color-scale-yellow-4); - } - .label-color-2 { - background-color: var(--color-scale-purple-9); - color: var(--color-scale-purple-2); - border: 1px solid var(--color-scale-purple-4); - } - .label-color-3 { - background-color: var(--color-scale-pink-9); - color: var(--color-scale-pink-2); - border: 1px solid var(--color-scale-pink-4); - } - .label-color-4 { - background-color: var(--color-scale-coral-9); - color: var(--color-scale-coral-2); - border: 1px solid var(--color-scale-coral-4); - } - .label-color-5 { - background-color: var(--color-scale-orange-9); - color: var(--color-scale-orange-2); - border: 1px solid var(--color-scale-orange-4); - } -} - -.d-flex { - display: flex !important; -} - -.d-inline { - display: inline !important; -} - -.pl-2 { - padding-left: 8px !important; -} - -.ml-2 { - margin-left: 8px !important; -} - -.no-wrap { - white-space: nowrap !important; -} - -.float-left { - float: left !important; -} - -article, aside, details, figcaption, figure, footer, header, main, menu, nav, section { - display: block; -} - -.form-control, .form-select { - padding: 5px 12px; - font-size: 14px; - line-height: 20px; - color: var(--color-fg-default); - vertical-align: middle; - background-color: var(--color-canvas-default); - background-repeat: no-repeat; - background-position: right 8px center; - border: 1px solid var(--color-border-default); - border-radius: 6px; - outline: none; - box-shadow: var(--color-primer-shadow-inset); -} - -.input-contrast { - background-color: var(--color-canvas-inset); -} - -.subnav-search { - position: relative; - flex: auto; - display: flex; -} - -.subnav-search-input { - flex: auto; - padding-left: 32px; - color: var(--color-fg-muted); -} - -.subnav-search-icon { - position: absolute; - top: 9px; - left: 8px; - display: block; - color: var(--color-fg-muted); - text-align: center; - pointer-events: none; -} - -.subnav-search-context + .subnav-search { - margin-left: -1px; -} - -.subnav-item { - flex: none; - position: relative; - float: left; - padding: 5px 10px; - font-weight: 500; - line-height: 20px; - color: var(--color-fg-default); - border: 1px solid var(--color-border-default); -} - -.subnav-item:hover { - background-color: var(--color-canvas-subtle); -} - -.subnav-item:first-child { - border-top-left-radius: 6px; - border-bottom-left-radius: 6px; -} - -.subnav-item:last-child { - border-top-right-radius: 6px; - border-bottom-right-radius: 6px; -} - -.subnav-item + .subnav-item { - margin-left: -1px; -} - -.counter { - display: inline-block; - min-width: 20px; - padding: 0 6px; - font-size: 12px; - font-weight: 500; - line-height: 18px; - color: var(--color-fg-default); - text-align: center; - background-color: var(--color-neutral-muted); - border: 1px solid transparent; - border-radius: 2em; -} - @media only screen and (max-width: 600px) { - .flow-container { - padding: 0; - } - - .chip-header { - border-radius: 0 !important; - border-right: none !important; - border-left: none !important; - } - - .chip-body { - border-radius: 0 !important; - border-right: none !important; - border-left: none !important; - padding: 5px !important; - } - - .test-result { + .htmlreport { padding: 0 !important; } - .test-case-column { - border-radius: 0 !important; - margin: 0 !important; - } - - .subnav-item, .form-control { - border-radius: 0 !important; - } - - .subnav-item { - padding: 5px 3px; - border: none; - } - .status-container { float: none; - margin: 0 !important; + margin: 0 0 10px 0 !important; overflow: hidden; } + + .subnav-search-input { + border-left: none; + border-right: none; + } } diff --git a/packages/playwright-core/src/web/htmlReport/htmlReport.tsx b/packages/playwright-core/src/web/htmlReport/htmlReport.tsx index 638150ea35..350e13ee91 100644 --- a/packages/playwright-core/src/web/htmlReport/htmlReport.tsx +++ b/packages/playwright-core/src/web/htmlReport/htmlReport.tsx @@ -14,15 +14,16 @@ limitations under the License. */ -import './htmlReport.css'; -import * as React from 'react'; -import ansi2html from 'ansi-to-html'; -import { downArrow, rightArrow, TreeItem } from '../components/treeItem'; -import { TabbedPane } from '../traceViewer/ui/tabbedPane'; -import { msToString } from '../uiUtils'; -import { traceImage } from './images'; -import type { TestCase, TestResult, TestStep, TestFile, Stats, TestAttachment, HTMLReport, TestFileSummary, TestCaseSummary } from '@playwright/test/src/reporters/html'; +import type { HTMLReport, TestFileSummary, TestCase, TestFile } from '@playwright/test/src/reporters/html'; import type zip from '@zip.js/zip.js'; +import * as React from 'react'; +import { Filter } from './filter'; +import './colors.css'; +import './common.css'; +import './htmlReport.css'; +import { StatsNavView } from './statsNavView'; +import { TestCaseView } from './testCaseView'; +import { TestFileView } from './testFileView'; const zipjs = (self as any).zip; @@ -58,8 +59,27 @@ export const Report: React.FC = () => { const filter = React.useMemo(() => Filter.parse(filterText), [filterText]); - return
- {
+ return
+ {report &&
+
+ +
+
{ + event.preventDefault(); + navigate(`#?q=${filterText ? encodeURIComponent(filterText) : ''}`); + } + }> + + {/* Use navigationId to reset defaultValue */} + { + setFilterText(e.target.value); + }}> +
+
} + {<> @@ -67,9 +87,9 @@ export const Report: React.FC = () => { - {!!report && } + {!!report && } -
} + }
; }; @@ -93,28 +113,9 @@ const AllTestFilesSummaryView: React.FC<{ } return result; }, [report, filter]); - return
- {report &&
-
- -
-
{ - event.preventDefault(); - navigate(`#?q=${filterText ? encodeURIComponent(filterText) : ''}`); - } - }> - - {/* Use navigationId to reset defaultValue */} - { - setFilterText(e.target.value); - }}> -
-
} + return <> {report && filteredFiles.map(({ file, defaultExpanded }) => { - return - ; + ; })} -
; + ; }; -const TestFileSummaryView: React.FC<{ - report: HTMLReport; - file: TestFileSummary; - isFileExpanded: (fileId: string) => boolean; - setFileExpanded: (fileId: string, expanded: boolean) => void; - filter: Filter; -}> = ({ file, report, isFileExpanded, setFileExpanded, filter }) => { - return setFileExpanded(file.fileId, expanded))} - header={ - {msToString(file.stats.duration)} - {file.fileName} - }> - {file.tests.filter(t => filter.matches(t)).map(test => -
- {msToString(test.duration)} - {report.projectNames.length > 1 && !!test.projectName && - } - {statusIcon(test.outcome)} - - {[...test.path, test.title].join(' › ')} - — {test.location.file}:{test.location.line} - -
- )} -
; -}; - -const TestCaseView: React.FC<{ +const TestCaseViewWrapper: React.FC<{ report: HTMLReport, }> = ({ report }) => { const searchParams = new URLSearchParams(window.location.hash.slice(1)); @@ -186,273 +158,7 @@ const TestCaseView: React.FC<{ } })(); }, [test, report, testId]); - - const [selectedResultIndex, setSelectedResultIndex] = React.useState(0); - return
-
- -
- {test &&
{test.path.join(' › ')}
} - {test &&
{test?.title}
} - {test &&
{test.location.file}:{test.location.line}
} - {test && !!test.projectName && } - {test && !!test.annotations.length && - {test.annotations.map(a =>
- {a.type} - {a.description && : {a.description}} -
)} -
} - {test && ({ - id: String(index), - title:
{statusIcon(result.status)} {retryLabel(index)}
, - render: () => - })) || []} selectedTab={String(selectedResultIndex)} setSelectedTab={id => setSelectedResultIndex(+id)} />} -
; -}; - -const TestResultView: React.FC<{ - test: TestCase, - result: TestResult, -}> = ({ result }) => { - - const { screenshots, videos, traces, otherAttachments, attachmentsMap } = React.useMemo(() => { - const attachmentsMap = new Map(); - const attachments = result?.attachments || []; - const otherAttachments: TestAttachment[] = []; - const screenshots = attachments.filter(a => a.name === 'screenshot'); - const videos = attachments.filter(a => a.name === 'video'); - const traces = attachments.filter(a => a.name === 'trace'); - const knownNames = new Set(['screenshot', 'image', 'expected', 'actual', 'diff', 'video', 'trace']); - for (const a of attachments) { - attachmentsMap.set(a.name, a); - if (!knownNames.has(a.name)) - otherAttachments.push(a); - } - return { attachmentsMap, screenshots, videos, otherAttachments, traces }; - }, [ result ]); - - const expected = attachmentsMap.get('expected'); - const actual = attachmentsMap.get('actual'); - const diff = attachmentsMap.get('diff'); - return
- {result.error && - - } - {!!result.steps.length && - {result.steps.map((step, i) => )} - } - - {expected && actual && - - - - {diff && } - } - - {!!screenshots.length && - {screenshots.map((a, i) => { - return
- - -
; - })} -
} - - {!!traces.length && - {traces.map((a, i) =>
- - - - -
)} -
} - - {!!videos.length && - {videos.map((a, i) =>
- - -
)} -
} - - {!!otherAttachments.length && - {otherAttachments.map((a, i) => )} - } -
; -}; - -const StepTreeItem: React.FC<{ - step: TestStep; - depth: number, -}> = ({ step, depth }) => { - return - {msToString(step.duration)} - {statusIcon(step.error || step.duration === -1 ? 'failed' : 'passed')} - {step.title} - {step.location && — {step.location.file}:{step.location.line}} - } loadChildren={step.steps.length + (step.snippet ? 1 : 0) ? () => { - const children = step.steps.map((s, i) => ); - if (step.snippet) - children.unshift(); - return children; - } : undefined} depth={depth}>; -}; - -const StatsNavView: React.FC<{ - stats: Stats -}> = ({ stats }) => { - return ; -}; - -const AttachmentLink: React.FunctionComponent<{ - attachment: TestAttachment, - href?: string, -}> = ({ attachment, href }) => { - return - {attachment.contentType === kMissingContentType ? - : - - } - {attachment.path && {attachment.name}} - {attachment.body && {attachment.name}} - } loadChildren={attachment.body ? () => { - return [
{attachment.body}
]; - } : undefined} depth={0}>
; -}; - -const ImageDiff: React.FunctionComponent<{ - actual: TestAttachment, - expected: TestAttachment, - diff?: TestAttachment, -}> = ({ actual, expected, diff }) => { - const [selectedTab, setSelectedTab] = React.useState('actual'); - const tabs = []; - tabs.push({ - id: 'actual', - title: 'Actual', - render: () => - }); - tabs.push({ - id: 'expected', - title: 'Expected', - render: () => - }); - if (diff) { - tabs.push({ - id: 'diff', - title: 'Diff', - render: () => - }); - } - return
- -
; -}; - -function statusIcon(status: 'failed' | 'timedOut' | 'skipped' | 'passed' | 'expected' | 'unexpected' | 'flaky'): JSX.Element { - switch (status) { - case 'failed': - case 'unexpected': - return ; - case 'passed': - case 'expected': - return ; - case 'timedOut': - return ; - case 'flaky': - return ; - case 'skipped': - return ; - } -} - -function retryLabel(index: number) { - if (!index) - return 'Run'; - return `Retry #${index}`; -} - -const ErrorMessage: React.FC<{ - error: string; -}> = ({ error }) => { - const html = React.useMemo(() => { - const config: any = { - bg: 'var(--color-canvas-subtle)', - fg: 'var(--color-fg-default)', - }; - config.colors = ansiColors; - return new ansi2html(config).toHtml(escapeHTML(error)); - }, [error]); - return
; -}; - -const ansiColors = { - 0: '#000', - 1: '#C00', - 2: '#0C0', - 3: '#C50', - 4: '#00C', - 5: '#C0C', - 6: '#0CC', - 7: '#CCC', - 8: '#555', - 9: '#F55', - 10: '#5F5', - 11: '#FF5', - 12: '#55F', - 13: '#F5F', - 14: '#5FF', - 15: '#FFF' -}; - -function escapeHTML(text: string): string { - return text.replace(/[&"<>]/g, c => ({ '&': '&', '"': '"', '<': '<', '>': '>' }[c]!)); -} - -const Chip: React.FunctionComponent<{ - header: JSX.Element | string, - expanded?: boolean, - setExpanded?: (expanded: boolean) => void, - children?: any -}> = ({ header, expanded, setExpanded, children }) => { - return
-
setExpanded?.(!expanded)}> - {setExpanded && !!expanded && downArrow()} - {setExpanded && !expanded && rightArrow()} - {header} -
- { (!setExpanded || expanded) &&
{children}
} -
; + return ; }; function navigate(href: string) { @@ -461,28 +167,6 @@ function navigate(href: string) { window.dispatchEvent(navEvent); } -const ProjectLink: React.FunctionComponent<{ - report: HTMLReport, - projectName: string, -}> = ({ report, projectName }) => { - const encoded = encodeURIComponent(projectName); - const value = projectName === encoded ? projectName : `"${encoded.replace(/%22/g, '%5C%22')}"`; - return - - {projectName} - - ; -}; - -const Link: React.FunctionComponent<{ - href: string, - className?: string, - title?: string, - children: any, -}> = ({ href, className, children, title }) => { - return {children}; -}; - const Route: React.FunctionComponent<{ params: string, children: any @@ -500,130 +184,9 @@ const Route: React.FunctionComponent<{ return currentParams === params ? children : null; }; -class Filter { - project: string[] = []; - status: string[] = []; - text: string[] = []; - - empty(): boolean { - return this.project.length + this.status.length + this.text.length === 0; - } - - static parse(expression: string): Filter { - const tokens = Filter.tokenize(expression); - const project = new Set(); - const status = new Set(); - const text: string[] = []; - for (const token of tokens) { - if (token.startsWith('p:')) { - project.add(token.slice(2)); - continue; - } - if (token.startsWith('s:')) { - status.add(token.slice(2)); - continue; - } - text.push(token.toLowerCase()); - } - - const filter = new Filter(); - filter.text = text; - filter.project = [...project]; - filter.status = [...status]; - return filter; - } - - private static tokenize(expression: string): string[] { - const result: string[] = []; - let quote: '\'' | '"' | undefined; - let token: string[] = []; - for (let i = 0; i < expression.length; ++i) { - const c = expression[i]; - if (quote && c === '\\' && expression[i + 1] === quote) { - token.push(quote); - ++i; - continue; - } - if (c === '"' || c === '\'') { - if (quote === c) { - result.push(token.join('').toLowerCase()); - token = []; - quote = undefined; - } else if (quote) { - token.push(c); - } else { - quote = c; - } - continue; - } - if (quote) { - token.push(c); - continue; - } - if (c === ' ') { - if (token.length) { - result.push(token.join('').toLowerCase()); - token = []; - } - continue; - } - token.push(c); - } - if (token.length) - result.push(token.join('').toLowerCase()); - return result; - } - - matches(test: TestCaseSummary): boolean { - if (!(test as any).searchValues) { - let status = 'passed'; - if (test.outcome === 'unexpected') - status = 'failed'; - if (test.outcome === 'flaky') - status = 'flaky'; - if (test.outcome === 'skipped') - status = 'skipped'; - const searchValues: SearchValues = { - text: (status + ' ' + test.projectName + ' ' + test.path.join(' ') + test.title).toLowerCase(), - project: test.projectName.toLowerCase(), - status: status as any - }; - (test as any).searchValues = searchValues; - } - - const searchValues = (test as any).searchValues as SearchValues; - if (this.project.length) { - const matches = !!this.project.find(p => searchValues.project.includes(p)); - if (!matches) - return false; - } - if (this.status.length) { - const matches = !!this.status.find(s => searchValues.status.includes(s)); - if (!matches) - return false; - } - - if (this.text.length) { - const matches = this.text.filter(t => searchValues.text.includes(t)).length === this.text.length; - if (!matches) - return false; - } - - return true; - } -} - async function readJsonEntry(entryName: string): Promise { const reportEntry = window.entries.get(entryName); const writer = new zipjs.TextWriter() as zip.TextWriter; await reportEntry!.getData!(writer); return JSON.parse(await writer.getData()); } - -type SearchValues = { - text: string; - project: string; - status: 'passed' | 'failed' | 'flaky' | 'skipped'; -}; - -const kMissingContentType = 'x-playwright/missing'; diff --git a/packages/playwright-core/src/web/htmlReport/links.css b/packages/playwright-core/src/web/htmlReport/links.css new file mode 100644 index 0000000000..a7ee4a8f7b --- /dev/null +++ b/packages/playwright-core/src/web/htmlReport/links.css @@ -0,0 +1,104 @@ +/* + Copyright (c) Microsoft Corporation. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +.label { + display: inline-block; + padding: 0 8px; + font-size: 12px; + font-weight: 500; + line-height: 18px; + border: 1px solid transparent; + border-radius: 2em; + background-color: var(--color-scale-gray-4); + color: white; + margin: 0 10px; + flex: none; + font-weight: 600; +} + +@media(prefers-color-scheme: light) { + .label-color-0 { + background-color: var(--color-scale-blue-0); + color: var(--color-scale-blue-6); + border: 1px solid var(--color-scale-blue-4); + } + .label-color-1 { + background-color: var(--color-scale-yellow-0); + color: var(--color-scale-yellow-6); + border: 1px solid var(--color-scale-yellow-4); + } + .label-color-2 { + background-color: var(--color-scale-purple-0); + color: var(--color-scale-purple-6); + border: 1px solid var(--color-scale-purple-4); + } + .label-color-3 { + background-color: var(--color-scale-pink-0); + color: var(--color-scale-pink-6); + border: 1px solid var(--color-scale-pink-4); + } + .label-color-4 { + background-color: var(--color-scale-coral-0); + color: var(--color-scale-coral-6); + border: 1px solid var(--color-scale-coral-4); + } + .label-color-5 { + background-color: var(--color-scale-orange-0); + color: var(--color-scale-orange-6); + border: 1px solid var(--color-scale-orange-4); + } +} + +@media(prefers-color-scheme: dark) { + .label-color-0 { + background-color: var(--color-scale-blue-9); + color: var(--color-scale-blue-2); + border: 1px solid var(--color-scale-blue-4); + } + .label-color-1 { + background-color: var(--color-scale-yellow-9); + color: var(--color-scale-yellow-2); + border: 1px solid var(--color-scale-yellow-4); + } + .label-color-2 { + background-color: var(--color-scale-purple-9); + color: var(--color-scale-purple-2); + border: 1px solid var(--color-scale-purple-4); + } + .label-color-3 { + background-color: var(--color-scale-pink-9); + color: var(--color-scale-pink-2); + border: 1px solid var(--color-scale-pink-4); + } + .label-color-4 { + background-color: var(--color-scale-coral-9); + color: var(--color-scale-coral-2); + border: 1px solid var(--color-scale-coral-4); + } + .label-color-5 { + background-color: var(--color-scale-orange-9); + color: var(--color-scale-orange-2); + border: 1px solid var(--color-scale-orange-4); + } +} + +.attachment-link { + white-space: pre-wrap; + background-color: var(--color-canvas-subtle); + margin-left: 24px; + line-height: normal; + padding: 8px; +} diff --git a/packages/playwright-core/src/web/htmlReport/links.tsx b/packages/playwright-core/src/web/htmlReport/links.tsx new file mode 100644 index 0000000000..80e21aba51 --- /dev/null +++ b/packages/playwright-core/src/web/htmlReport/links.tsx @@ -0,0 +1,64 @@ +/* + Copyright (c) Microsoft Corporation. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +import type { HTMLReport, TestAttachment } from '@playwright/test/src/reporters/html'; +import * as React from 'react'; +import { TreeItem } from './treeItem'; +import './links.css'; + +export const Link: React.FunctionComponent<{ + href: string, + className?: string, + title?: string, + children: any, +}> = ({ href, className, children, title }) => { + return {children}; +}; + +export const ProjectLink: React.FunctionComponent<{ + report: HTMLReport, + projectName: string, +}> = ({ report, projectName }) => { + const encoded = encodeURIComponent(projectName); + const value = projectName === encoded ? projectName : `"${encoded.replace(/%22/g, '%5C%22')}"`; + return + + {projectName} + + ; +}; + +export const AttachmentLink: React.FunctionComponent<{ + attachment: TestAttachment, + href?: string, +}> = ({ attachment, href }) => { + return + {attachment.contentType === kMissingContentType ? + : + + } + {attachment.path && {attachment.name}} + {attachment.body && {attachment.name}} + } loadChildren={attachment.body ? () => { + return [
{attachment.body}
]; + } : undefined} depth={0}>
; +}; + +const kMissingContentType = 'x-playwright/missing'; diff --git a/packages/playwright-core/src/web/htmlReport/statsNavView.tsx b/packages/playwright-core/src/web/htmlReport/statsNavView.tsx new file mode 100644 index 0000000000..d84381bb97 --- /dev/null +++ b/packages/playwright-core/src/web/htmlReport/statsNavView.tsx @@ -0,0 +1,43 @@ +/* + Copyright (c) Microsoft Corporation. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +import type { Stats } from '@playwright/test/src/reporters/html'; +import * as React from 'react'; +import './htmlReport.css'; +import { Link } from './links'; +import { statusIcon } from './statusIcon'; + +export const StatsNavView: React.FC<{ + stats: Stats +}> = ({ stats }) => { + return ; +}; diff --git a/packages/playwright-core/src/web/htmlReport/statusIcon.tsx b/packages/playwright-core/src/web/htmlReport/statusIcon.tsx new file mode 100644 index 0000000000..5af1ad0759 --- /dev/null +++ b/packages/playwright-core/src/web/htmlReport/statusIcon.tsx @@ -0,0 +1,43 @@ +/* + Copyright (c) Microsoft Corporation. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +import * as React from 'react'; +import './htmlReport.css'; + +export function statusIcon(status: 'failed' | 'timedOut' | 'skipped' | 'passed' | 'expected' | 'unexpected' | 'flaky'): JSX.Element { + switch (status) { + case 'failed': + case 'unexpected': + return ; + case 'passed': + case 'expected': + return ; + case 'timedOut': + return ; + case 'flaky': + return ; + case 'skipped': + return ; + } +} diff --git a/packages/playwright-core/src/web/htmlReport/tabbedPane.css b/packages/playwright-core/src/web/htmlReport/tabbedPane.css new file mode 100644 index 0000000000..41084fe6fb --- /dev/null +++ b/packages/playwright-core/src/web/htmlReport/tabbedPane.css @@ -0,0 +1,76 @@ +/* + Copyright (c) Microsoft Corporation. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +.tabbed-pane { + display: flex; + flex: auto; + overflow: hidden; +} + +.tabbed-pane-tab-content { + display: flex; + flex: auto; + overflow: hidden; +} + +.tabbed-pane-tab-strip { + display: flex; + align-items: center; + padding-right: 10px; + flex: none; + width: 100%; + z-index: 2; + font-size: 14px; + line-height: 32px; + color: var(--color-fg-default); + height: 48px; + min-width: 70px; + box-shadow: inset 0 -1px 0 var(--color-border-muted) !important; +} + +.tabbed-pane-tab-strip:focus { + outline: none; +} + +.tabbed-pane-tab-element { + padding: 4px 8px 0 8px; + margin-right: 4px; + cursor: pointer; + display: flex; + flex: none; + align-items: center; + justify-content: center; + user-select: none; + border-bottom: 2px solid transparent; + outline: none; + height: 100%; +} + +.tabbed-pane-tab-label { + max-width: 250px; + white-space: pre; + overflow: hidden; + text-overflow: ellipsis; + display: inline-block; +} + +.tabbed-pane-tab-element.selected { + border-bottom-color: #666; +} + +.tabbed-pane-tab-element:hover { + color: #333; +} diff --git a/packages/playwright-core/src/web/htmlReport/tabbedPane.tsx b/packages/playwright-core/src/web/htmlReport/tabbedPane.tsx new file mode 100644 index 0000000000..769622137a --- /dev/null +++ b/packages/playwright-core/src/web/htmlReport/tabbedPane.tsx @@ -0,0 +1,53 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import './tabbedPane.css'; +import * as React from 'react'; + +export interface TabbedPaneTab { + id: string; + title: string | JSX.Element; + count?: number; + render: () => React.ReactElement; +} + +export const TabbedPane: React.FunctionComponent<{ + tabs: TabbedPaneTab[], + selectedTab: string, + setSelectedTab: (tab: string) => void +}> = ({ tabs, selectedTab, setSelectedTab }) => { + return
+
+
+
{ + tabs.map(tab => ( +
setSelectedTab(tab.id)} + key={tab.id}> +
{tab.title}
+
+ )) + }
+
+ { + tabs.map(tab => { + if (selectedTab === tab.id) + return
{tab.render()}
; + }) + } +
+
; +}; diff --git a/packages/playwright-core/src/web/htmlReport/testCaseView.css b/packages/playwright-core/src/web/htmlReport/testCaseView.css new file mode 100644 index 0000000000..5aa3e006e6 --- /dev/null +++ b/packages/playwright-core/src/web/htmlReport/testCaseView.css @@ -0,0 +1,69 @@ +/* + Copyright (c) Microsoft Corporation. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +.test-case-column { + border-radius: 6px; + margin: 24px 0; +} + +.test-case-column .tab-element.selected { + font-weight: 600; + border-bottom-color: var(--color-primer-border-active); +} + +.test-case-column .tab-element { + border: none; + color: var(--color-fg-default); + border-bottom: 2px solid transparent; +} + +.test-case-column .tab-element:hover { + color: var(--color-fg-default); +} + +.test-case-title { + flex: none; + padding: 8px; + font-weight: 400; + font-size: 32px !important; + line-height: 1.25 !important; +} + +.test-case-location { + flex: none; + align-items: center; + padding: 0 8px 24px; +} + +.test-case-path { + flex: none; + align-items: center; + padding: 0 8px; +} + +.test-case-annotation { + flex: none; + align-items: center; + padding: 0 8px; + line-height: 24px; +} + +@media only screen and (max-width: 600px) { + .test-case-column { + border-radius: 0 !important; + margin: 0 !important; + } +} diff --git a/packages/playwright-core/src/web/htmlReport/testCaseView.tsx b/packages/playwright-core/src/web/htmlReport/testCaseView.tsx new file mode 100644 index 0000000000..5d4205c97e --- /dev/null +++ b/packages/playwright-core/src/web/htmlReport/testCaseView.tsx @@ -0,0 +1,57 @@ +/* + Copyright (c) Microsoft Corporation. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +import type { HTMLReport, TestCase } from '@playwright/test/src/reporters/html'; +import * as React from 'react'; +import { TabbedPane } from './tabbedPane'; +import { Chip } from './chip'; +import './common.css'; +import { ProjectLink } from './links'; +import { statusIcon } from './statusIcon'; +import './testCaseView.css'; +import { TestResultView } from './testResultView'; + +export const TestCaseView: React.FC<{ + report: HTMLReport, + test: TestCase | undefined, +}> = ({ report, test }) => { + const [selectedResultIndex, setSelectedResultIndex] = React.useState(0); + + return
+ {test &&
{test.path.join(' › ')}
} + {test &&
{test?.title}
} + {test &&
{test.location.file}:{test.location.line}
} + {test && !!test.projectName && } + {test && !!test.annotations.length && + {test.annotations.map(a =>
+ {a.type} + {a.description && : {a.description}} +
)} +
} + {test && ({ + id: String(index), + title:
{statusIcon(result.status)} {retryLabel(index)}
, + render: () => + })) || []} selectedTab={String(selectedResultIndex)} setSelectedTab={id => setSelectedResultIndex(+id)} />} +
; +}; + +function retryLabel(index: number) { + if (!index) + return 'Run'; + return `Retry #${index}`; +} diff --git a/packages/playwright-core/src/web/htmlReport/testFileView.css b/packages/playwright-core/src/web/htmlReport/testFileView.css new file mode 100644 index 0000000000..78ddef899e --- /dev/null +++ b/packages/playwright-core/src/web/htmlReport/testFileView.css @@ -0,0 +1,38 @@ +/* + Copyright (c) Microsoft Corporation. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +.test-file-test { + height: 38px; + line-height: 38px; + align-items: center; + padding: 0 10px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.test-file-test:hover { + background-color: var(--color-canvas-subtle); +} + +.test-file-path { + padding: 0 0 0 8px; + color: var(--color-fg-muted); +} + +.test-file-test-outcome-skipped { + color: var(--color-fg-muted); +} diff --git a/packages/playwright-core/src/web/htmlReport/testFileView.tsx b/packages/playwright-core/src/web/htmlReport/testFileView.tsx new file mode 100644 index 0000000000..d50d09c008 --- /dev/null +++ b/packages/playwright-core/src/web/htmlReport/testFileView.tsx @@ -0,0 +1,54 @@ +/* + Copyright (c) Microsoft Corporation. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +import type { HTMLReport, TestFileSummary } from '@playwright/test/src/reporters/html'; +import * as React from 'react'; +import { msToString } from '../uiUtils'; +import { Chip } from './chip'; +import { Filter } from './filter'; +import { Link, ProjectLink } from './links'; +import { statusIcon } from './statusIcon'; +import './testFileView.css'; + +export const TestFileView: React.FC<{ + report: HTMLReport; + file: TestFileSummary; + isFileExpanded: (fileId: string) => boolean; + setFileExpanded: (fileId: string, expanded: boolean) => void; + filter: Filter; +}> = ({ file, report, isFileExpanded, setFileExpanded, filter }) => { + return setFileExpanded(file.fileId, expanded))} + header={ + {msToString(file.stats.duration)} + {file.fileName} + }> + {file.tests.filter(t => filter.matches(t)).map(test => +
+ {msToString(test.duration)} + {report.projectNames.length > 1 && !!test.projectName && + } + {statusIcon(test.outcome)} + + {[...test.path, test.title].join(' › ')} + — {test.location.file}:{test.location.line} + +
+ )} +
; +}; diff --git a/packages/playwright-core/src/web/htmlReport/testResultView.css b/packages/playwright-core/src/web/htmlReport/testResultView.css new file mode 100644 index 0000000000..c89e13341e --- /dev/null +++ b/packages/playwright-core/src/web/htmlReport/testResultView.css @@ -0,0 +1,64 @@ +/* + Copyright (c) Microsoft Corporation. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +.test-result { + flex: auto; + display: flex; + flex-direction: column; + margin-bottom: 24px; +} + +.test-result .tabbed-pane .tab-content { + display: flex; + align-items: center; + justify-content: center; +} + +.test-result > div { + flex: none; +} + +.test-result video, +.test-result img { + flex: none; + box-shadow: var(--box-shadow-thick); + margin: 24px auto; + min-width: 200px; + max-width: 80%; +} + +.test-result-path { + padding: 0 0 0 5px; + color: var(--color-fg-muted); +} + +.test-result-error-message { + white-space: pre; + font-family: monospace; + overflow: auto; + flex: none; + padding: 0; + background-color: var(--color-canvas-subtle); + border-radius: 6px; + padding: 16px; + line-height: initial; +} + +@media only screen and (max-width: 600px) { + .test-result { + padding: 0 !important; + } +} diff --git a/packages/playwright-core/src/web/htmlReport/testResultView.tsx b/packages/playwright-core/src/web/htmlReport/testResultView.tsx new file mode 100644 index 0000000000..175e218840 --- /dev/null +++ b/packages/playwright-core/src/web/htmlReport/testResultView.tsx @@ -0,0 +1,182 @@ +/* + Copyright (c) Microsoft Corporation. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +import type { TestAttachment, TestCase, TestResult, TestStep } from '@playwright/test/src/reporters/html'; +import ansi2html from 'ansi-to-html'; +import * as React from 'react'; +import { TreeItem } from './treeItem'; +import { TabbedPane } from './tabbedPane'; +import { msToString } from '../uiUtils'; +import { Chip } from './chip'; +import { traceImage } from './images'; +import { AttachmentLink } from './links'; +import { statusIcon } from './statusIcon'; +import './testResultView.css'; + +export const TestResultView: React.FC<{ + test: TestCase, + result: TestResult, +}> = ({ result }) => { + + const { screenshots, videos, traces, otherAttachments, attachmentsMap } = React.useMemo(() => { + const attachmentsMap = new Map(); + const attachments = result?.attachments || []; + const otherAttachments: TestAttachment[] = []; + const screenshots = attachments.filter(a => a.name === 'screenshot'); + const videos = attachments.filter(a => a.name === 'video'); + const traces = attachments.filter(a => a.name === 'trace'); + const knownNames = new Set(['screenshot', 'image', 'expected', 'actual', 'diff', 'video', 'trace']); + for (const a of attachments) { + attachmentsMap.set(a.name, a); + if (!knownNames.has(a.name)) + otherAttachments.push(a); + } + return { attachmentsMap, screenshots, videos, otherAttachments, traces }; + }, [ result ]); + + const expected = attachmentsMap.get('expected'); + const actual = attachmentsMap.get('actual'); + const diff = attachmentsMap.get('diff'); + return
+ {result.error && + + } + {!!result.steps.length && + {result.steps.map((step, i) => )} + } + + {expected && actual && + + + + {diff && } + } + + {!!screenshots.length && + {screenshots.map((a, i) => { + return
+ + +
; + })} +
} + + {!!traces.length && + {traces.map((a, i) =>
+ + + + +
)} +
} + + {!!videos.length && + {videos.map((a, i) =>
+ + +
)} +
} + + {!!otherAttachments.length && + {otherAttachments.map((a, i) => )} + } +
; +}; + +const StepTreeItem: React.FC<{ + step: TestStep; + depth: number, +}> = ({ step, depth }) => { + return + {msToString(step.duration)} + {statusIcon(step.error || step.duration === -1 ? 'failed' : 'passed')} + {step.title} + {step.location && — {step.location.file}:{step.location.line}} + } loadChildren={step.steps.length + (step.snippet ? 1 : 0) ? () => { + const children = step.steps.map((s, i) => ); + if (step.snippet) + children.unshift(); + return children; + } : undefined} depth={depth}>; +}; + +const ImageDiff: React.FunctionComponent<{ + actual: TestAttachment, + expected: TestAttachment, + diff?: TestAttachment, +}> = ({ actual, expected, diff }) => { + const [selectedTab, setSelectedTab] = React.useState('actual'); + const tabs = []; + tabs.push({ + id: 'actual', + title: 'Actual', + render: () => + }); + tabs.push({ + id: 'expected', + title: 'Expected', + render: () => + }); + if (diff) { + tabs.push({ + id: 'diff', + title: 'Diff', + render: () => + }); + } + return
+ +
; +}; + +const ErrorMessage: React.FC<{ + error: string; +}> = ({ error }) => { + const html = React.useMemo(() => { + const config: any = { + bg: 'var(--color-canvas-subtle)', + fg: 'var(--color-fg-default)', + }; + config.colors = ansiColors; + return new ansi2html(config).toHtml(escapeHTML(error)); + }, [error]); + return
; +}; + +const ansiColors = { + 0: '#000', + 1: '#C00', + 2: '#0C0', + 3: '#C50', + 4: '#00C', + 5: '#C0C', + 6: '#0CC', + 7: '#CCC', + 8: '#555', + 9: '#F55', + 10: '#5F5', + 11: '#FF5', + 12: '#55F', + 13: '#F5F', + 14: '#5FF', + 15: '#FFF' +}; + +function escapeHTML(text: string): string { + return text.replace(/[&"<>]/g, c => ({ '&': '&', '"': '"', '<': '<', '>': '>' }[c]!)); +} diff --git a/packages/playwright-core/src/web/htmlReport/treeItem.css b/packages/playwright-core/src/web/htmlReport/treeItem.css new file mode 100644 index 0000000000..a8cedc4f6a --- /dev/null +++ b/packages/playwright-core/src/web/htmlReport/treeItem.css @@ -0,0 +1,30 @@ +/* + Copyright (c) Microsoft Corporation. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +.tree-item { + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + line-height: 38px; +} + +.tree-item-title { + cursor: pointer; +} + +.tree-item-body { + min-height: 18px; +} diff --git a/packages/playwright-core/src/web/components/treeItem.tsx b/packages/playwright-core/src/web/htmlReport/treeItem.tsx similarity index 96% rename from packages/playwright-core/src/web/components/treeItem.tsx rename to packages/playwright-core/src/web/htmlReport/treeItem.tsx index 6b09180bc3..270c626e86 100644 --- a/packages/playwright-core/src/web/components/treeItem.tsx +++ b/packages/playwright-core/src/web/htmlReport/treeItem.tsx @@ -15,6 +15,7 @@ */ import * as React from 'react'; +import './treeItem.css'; export const TreeItem: React.FunctionComponent<{ title: JSX.Element, @@ -37,13 +38,13 @@ export const TreeItem: React.FunctionComponent<{
; }; -export function downArrow() { +function downArrow() { return ; } -export function rightArrow() { +function rightArrow() { return ; diff --git a/tests/playwright-test/reporter-html.spec.ts b/tests/playwright-test/reporter-html.spec.ts index 6d309c722d..92fc248425 100644 --- a/tests/playwright-test/reporter-html.spec.ts +++ b/tests/playwright-test/reporter-html.spec.ts @@ -61,10 +61,10 @@ test('should generate report', async ({ runInlineTest, showReport, page }) => { await expect(page.locator('.subnav-item:has-text("Flaky") .counter')).toHaveText('1'); await expect(page.locator('.subnav-item:has-text("Skipped") .counter')).toHaveText('1'); - await expect(page.locator('.test-summary.outcome-unexpected >> text=fails')).toBeVisible(); - await expect(page.locator('.test-summary.outcome-flaky >> text=flaky')).toBeVisible(); - await expect(page.locator('.test-summary.outcome-expected >> text=passes')).toBeVisible(); - await expect(page.locator('.test-summary.outcome-skipped >> text=skipped')).toBeVisible(); + await expect(page.locator('.test-file-test-outcome-unexpected >> text=fails')).toBeVisible(); + await expect(page.locator('.test-file-test-outcome-flaky >> text=flaky')).toBeVisible(); + await expect(page.locator('.test-file-test-outcome-expected >> text=passes')).toBeVisible(); + await expect(page.locator('.test-file-test-outcome-skipped >> text=skipped')).toBeVisible(); }); test('should not throw when attachment is missing', async ({ runInlineTest, page, showReport }, testInfo) => { @@ -88,7 +88,7 @@ test('should not throw when attachment is missing', async ({ runInlineTest, page await page.click('text=passes'); await page.locator('text=Missing attachment "screenshot"').click(); const screenshotFile = testInfo.outputPath('test-results' , 'a-passes', 'screenshot.png'); - await expect(page.locator('.attachment-body')).toHaveText(`Attachment file ${screenshotFile} is missing`); + await expect(page.locator('.attachment-link')).toHaveText(`Attachment file ${screenshotFile} is missing`); }); test('should include image diff', async ({ runInlineTest, page, showReport }) => { @@ -114,7 +114,7 @@ test('should include image diff', async ({ runInlineTest, page, showReport }) => await showReport(); await page.click('text=fails'); - const imageDiff = page.locator('.test-image-mismatch'); + const imageDiff = page.locator('data-testid=test-result-image-mismatch'); const image = imageDiff.locator('img'); await expect(image).toHaveAttribute('src', /.*png/); const actualSrc = await image.getAttribute('src'); @@ -173,9 +173,9 @@ test('should include stdio', async ({ runInlineTest, page, showReport }) => { await showReport(); await page.click('text=fails'); await page.locator('text=stdout').click(); - await expect(page.locator('.attachment-body')).toHaveText('First line\nSecond line'); + await expect(page.locator('.attachment-link')).toHaveText('First line\nSecond line'); await page.locator('text=stderr').click(); - await expect(page.locator('.attachment-body').nth(1)).toHaveText('Third line'); + await expect(page.locator('.attachment-link').nth(1)).toHaveText('Third line'); }); test('should highlight error', async ({ runInlineTest, page, showReport }) => { @@ -192,7 +192,7 @@ test('should highlight error', async ({ runInlineTest, page, showReport }) => { await showReport(); await page.click('text=fails'); - await expect(page.locator('.error-message span:has-text("received")').nth(1)).toHaveCSS('color', 'rgb(204, 0, 0)'); + await expect(page.locator('.test-result-error-message span:has-text("received")').nth(1)).toHaveCSS('color', 'rgb(204, 0, 0)'); }); test('should show trace source', async ({ runInlineTest, page, showReport }) => { @@ -333,5 +333,5 @@ test('should render text attachments as text', async ({ runInlineTest, page, sho await page.click('text=example.txt'); await page.click('text=example.json'); await page.click('text=example-utf16.txt'); - await expect(page.locator('.attachment-body')).toHaveText(['foo', '{"foo":1}', 'utf16 encoded']); + await expect(page.locator('.attachment-link')).toHaveText(['foo', '{"foo":1}', 'utf16 encoded']); });