chore: split html report into files (#10876)
This commit is contained in:
parent
81ab6b3fde
commit
6521a6f3ab
70
packages/playwright-core/src/web/htmlReport/chip.css
Normal file
70
packages/playwright-core/src/web/htmlReport/chip.css
Normal file
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
47
packages/playwright-core/src/web/htmlReport/chip.tsx
Normal file
47
packages/playwright-core/src/web/htmlReport/chip.tsx
Normal file
|
|
@ -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 <div className='chip'>
|
||||||
|
<div className={'chip-header' + (setExpanded ? ' expanded-' + expanded : '')} onClick={() => setExpanded?.(!expanded)}>
|
||||||
|
{setExpanded && !!expanded && downArrow()}
|
||||||
|
{setExpanded && !expanded && rightArrow()}
|
||||||
|
{header}
|
||||||
|
</div>
|
||||||
|
{(!setExpanded || expanded) && <div className={'chip-body' + (noInsets ? ' chip-body-no-insets' : '')}>{children}</div>}
|
||||||
|
</div>;
|
||||||
|
};
|
||||||
|
|
||||||
|
function downArrow() {
|
||||||
|
return <svg aria-hidden='true' height='16' viewBox='0 0 16 16' version='1.1' width='16' className='octicon color-fg-muted'>
|
||||||
|
<path fillRule='evenodd' d='M12.78 6.22a.75.75 0 010 1.06l-4.25 4.25a.75.75 0 01-1.06 0L3.22 7.28a.75.75 0 011.06-1.06L8 9.94l3.72-3.72a.75.75 0 011.06 0z'></path>
|
||||||
|
</svg>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function rightArrow() {
|
||||||
|
return <svg aria-hidden='true' height='16' viewBox='0 0 16 16' version='1.1' width='16' data-view-component='true' className='octicon color-fg-muted'>
|
||||||
|
<path fillRule='evenodd' d='M6.22 3.22a.75.75 0 011.06 0l4.25 4.25a.75.75 0 010 1.06l-4.25 4.25a.75.75 0 01-1.06-1.06L9.94 8 6.22 4.28a.75.75 0 010-1.06z'></path>
|
||||||
|
</svg>;
|
||||||
|
}
|
||||||
266
packages/playwright-core/src/web/htmlReport/common.css
Normal file
266
packages/playwright-core/src/web/htmlReport/common.css
Normal file
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
138
packages/playwright-core/src/web/htmlReport/filter.ts
Normal file
138
packages/playwright-core/src/web/htmlReport/filter.ts
Normal file
|
|
@ -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<string>();
|
||||||
|
const status = new Set<string>();
|
||||||
|
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';
|
||||||
|
};
|
||||||
|
|
||||||
|
|
@ -24,22 +24,6 @@
|
||||||
rgb(0 0 0 / 25%) 0px 6px 12px;
|
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 {
|
#root {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|
@ -49,575 +33,42 @@ body {
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
}
|
}
|
||||||
|
|
||||||
* {
|
html, body {
|
||||||
box-sizing: border-box;
|
width: 100%;
|
||||||
min-width: 0;
|
height: 100%;
|
||||||
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;
|
|
||||||
padding: 0;
|
padding: 0;
|
||||||
background-color: var(--color-canvas-subtle);
|
margin: 0;
|
||||||
border-radius: 6px;
|
overscroll-behavior-x: none;
|
||||||
padding: 16px;
|
|
||||||
line-height: initial;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-icon {
|
body {
|
||||||
padding-right: 3px;
|
overflow: auto;
|
||||||
}
|
|
||||||
|
|
||||||
.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 {
|
|
||||||
max-width: 1024px;
|
max-width: 1024px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 0 16px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-summary-list {
|
.test-file-test:not(:first-child) {
|
||||||
padding: 20px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.file-summary-list .chip-body .test-summary:not(:first-child),
|
|
||||||
.failed-test:not(:first-child) {
|
|
||||||
border-top: 1px solid var(--color-border-default);
|
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 {
|
.status-container {
|
||||||
float: right;
|
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) {
|
@media only screen and (max-width: 600px) {
|
||||||
.flow-container {
|
.htmlreport {
|
||||||
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 {
|
|
||||||
padding: 0 !important;
|
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 {
|
.status-container {
|
||||||
float: none;
|
float: none;
|
||||||
margin: 0 !important;
|
margin: 0 0 10px 0 !important;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.subnav-search-input {
|
||||||
|
border-left: none;
|
||||||
|
border-right: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,15 +14,16 @@
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import './htmlReport.css';
|
import type { HTMLReport, TestFileSummary, TestCase, TestFile } from '@playwright/test/src/reporters/html';
|
||||||
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 zip from '@zip.js/zip.js';
|
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;
|
const zipjs = (self as any).zip;
|
||||||
|
|
||||||
|
|
@ -58,8 +59,27 @@ export const Report: React.FC = () => {
|
||||||
|
|
||||||
const filter = React.useMemo(() => Filter.parse(filterText), [filterText]);
|
const filter = React.useMemo(() => Filter.parse(filterText), [filterText]);
|
||||||
|
|
||||||
return <div className='vbox columns'>
|
return <div className='htmlreport vbox px-4'>
|
||||||
{<div className='flow-container'>
|
{report && <div className='pt-3'>
|
||||||
|
<div className='status-container ml-2 pl-2 d-flex'>
|
||||||
|
<StatsNavView stats={report.stats}></StatsNavView>
|
||||||
|
</div>
|
||||||
|
<form className='subnav-search' onSubmit={
|
||||||
|
event => {
|
||||||
|
event.preventDefault();
|
||||||
|
navigate(`#?q=${filterText ? encodeURIComponent(filterText) : ''}`);
|
||||||
|
}
|
||||||
|
}>
|
||||||
|
<svg aria-hidden='true' height='16' viewBox='0 0 16 16' version='1.1' width='16' data-view-component='true' className='octicon subnav-search-icon'>
|
||||||
|
<path fillRule='evenodd' d='M11.5 7a4.499 4.499 0 11-8.998 0A4.499 4.499 0 0111.5 7zm-.82 4.74a6 6 0 111.06-1.06l3.04 3.04a.75.75 0 11-1.06 1.06l-3.04-3.04z'></path>
|
||||||
|
</svg>
|
||||||
|
{/* Use navigationId to reset defaultValue */}
|
||||||
|
<input type='search' spellCheck={false} className='form-control subnav-search-input input-contrast width-full' value={filterText} onChange={e => {
|
||||||
|
setFilterText(e.target.value);
|
||||||
|
}}></input>
|
||||||
|
</form>
|
||||||
|
</div>}
|
||||||
|
{<>
|
||||||
<Route params=''>
|
<Route params=''>
|
||||||
<AllTestFilesSummaryView report={report} filter={filter} expandedFiles={expandedFiles} setExpandedFiles={setExpandedFiles} filterText={filterText} setFilterText={setFilterText}></AllTestFilesSummaryView>
|
<AllTestFilesSummaryView report={report} filter={filter} expandedFiles={expandedFiles} setExpandedFiles={setExpandedFiles} filterText={filterText} setFilterText={setFilterText}></AllTestFilesSummaryView>
|
||||||
</Route>
|
</Route>
|
||||||
|
|
@ -67,9 +87,9 @@ export const Report: React.FC = () => {
|
||||||
<AllTestFilesSummaryView report={report} filter={filter} expandedFiles={expandedFiles} setExpandedFiles={setExpandedFiles} filterText={filterText} setFilterText={setFilterText}></AllTestFilesSummaryView>
|
<AllTestFilesSummaryView report={report} filter={filter} expandedFiles={expandedFiles} setExpandedFiles={setExpandedFiles} filterText={filterText} setFilterText={setFilterText}></AllTestFilesSummaryView>
|
||||||
</Route>
|
</Route>
|
||||||
<Route params='testId'>
|
<Route params='testId'>
|
||||||
{!!report && <TestCaseView report={report}></TestCaseView>}
|
{!!report && <TestCaseViewWrapper report={report}></TestCaseViewWrapper>}
|
||||||
</Route>
|
</Route>
|
||||||
</div>}
|
</>}
|
||||||
</div>;
|
</div>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -93,28 +113,9 @@ const AllTestFilesSummaryView: React.FC<{
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}, [report, filter]);
|
}, [report, filter]);
|
||||||
return <div className='file-summary-list'>
|
return <>
|
||||||
{report && <div>
|
|
||||||
<div className='status-container ml-2 pl-2 d-flex'>
|
|
||||||
<StatsNavView stats={report.stats}></StatsNavView>
|
|
||||||
</div>
|
|
||||||
<form className='subnav-search' onSubmit={
|
|
||||||
event => {
|
|
||||||
event.preventDefault();
|
|
||||||
navigate(`#?q=${filterText ? encodeURIComponent(filterText) : ''}`);
|
|
||||||
}
|
|
||||||
}>
|
|
||||||
<svg aria-hidden='true' height='16' viewBox='0 0 16 16' version='1.1' width='16' data-view-component='true' className='octicon subnav-search-icon'>
|
|
||||||
<path fillRule='evenodd' d='M11.5 7a4.499 4.499 0 11-8.998 0A4.499 4.499 0 0111.5 7zm-.82 4.74a6 6 0 111.06-1.06l3.04 3.04a.75.75 0 11-1.06 1.06l-3.04-3.04z'></path>
|
|
||||||
</svg>
|
|
||||||
{/* Use navigationId to reset defaultValue */}
|
|
||||||
<input type='search' spellCheck={false} className='form-control subnav-search-input input-contrast width-full' value={filterText} onChange={e => {
|
|
||||||
setFilterText(e.target.value);
|
|
||||||
}}></input>
|
|
||||||
</form>
|
|
||||||
</div>}
|
|
||||||
{report && filteredFiles.map(({ file, defaultExpanded }) => {
|
{report && filteredFiles.map(({ file, defaultExpanded }) => {
|
||||||
return <TestFileSummaryView
|
return <TestFileView
|
||||||
key={`file-${file.fileId}`}
|
key={`file-${file.fileId}`}
|
||||||
report={report}
|
report={report}
|
||||||
file={file}
|
file={file}
|
||||||
|
|
@ -130,41 +131,12 @@ const AllTestFilesSummaryView: React.FC<{
|
||||||
setExpandedFiles(newExpanded);
|
setExpandedFiles(newExpanded);
|
||||||
}}
|
}}
|
||||||
filter={filter}>
|
filter={filter}>
|
||||||
</TestFileSummaryView>;
|
</TestFileView>;
|
||||||
})}
|
})}
|
||||||
</div>;
|
</>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const TestFileSummaryView: React.FC<{
|
const TestCaseViewWrapper: React.FC<{
|
||||||
report: HTMLReport;
|
|
||||||
file: TestFileSummary;
|
|
||||||
isFileExpanded: (fileId: string) => boolean;
|
|
||||||
setFileExpanded: (fileId: string, expanded: boolean) => void;
|
|
||||||
filter: Filter;
|
|
||||||
}> = ({ file, report, isFileExpanded, setFileExpanded, filter }) => {
|
|
||||||
return <Chip
|
|
||||||
expanded={isFileExpanded(file.fileId)}
|
|
||||||
setExpanded={(expanded => setFileExpanded(file.fileId, expanded))}
|
|
||||||
header={<span>
|
|
||||||
<span style={{ float: 'right' }}>{msToString(file.stats.duration)}</span>
|
|
||||||
{file.fileName}
|
|
||||||
</span>}>
|
|
||||||
{file.tests.filter(t => filter.matches(t)).map(test =>
|
|
||||||
<div key={`test-${test.testId}`} className={'test-summary outcome-' + test.outcome}>
|
|
||||||
<span style={{ float: 'right' }}>{msToString(test.duration)}</span>
|
|
||||||
{report.projectNames.length > 1 && !!test.projectName &&
|
|
||||||
<span style={{ float: 'right' }}><ProjectLink report={report} projectName={test.projectName}></ProjectLink></span>}
|
|
||||||
{statusIcon(test.outcome)}
|
|
||||||
<Link href={`#?testId=${test.testId}`} title={[...test.path, test.title].join(' › ')}>
|
|
||||||
{[...test.path, test.title].join(' › ')}
|
|
||||||
<span className='test-summary-path'>— {test.location.file}:{test.location.line}</span>
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Chip>;
|
|
||||||
};
|
|
||||||
|
|
||||||
const TestCaseView: React.FC<{
|
|
||||||
report: HTMLReport,
|
report: HTMLReport,
|
||||||
}> = ({ report }) => {
|
}> = ({ report }) => {
|
||||||
const searchParams = new URLSearchParams(window.location.hash.slice(1));
|
const searchParams = new URLSearchParams(window.location.hash.slice(1));
|
||||||
|
|
@ -186,273 +158,7 @@ const TestCaseView: React.FC<{
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
}, [test, report, testId]);
|
}, [test, report, testId]);
|
||||||
|
return <TestCaseView report={report} test={test}></TestCaseView>;
|
||||||
const [selectedResultIndex, setSelectedResultIndex] = React.useState(0);
|
|
||||||
return <div className='test-case-column vbox'>
|
|
||||||
<div className='status-container ml-2 pl-2 d-flex' style={{ flexFlow: 'row-reverse' }}>
|
|
||||||
<StatsNavView stats={report.stats}></StatsNavView>
|
|
||||||
</div>
|
|
||||||
{test && <div className='test-case-path'>{test.path.join(' › ')}</div>}
|
|
||||||
{test && <div className='test-case-title'>{test?.title}</div>}
|
|
||||||
{test && <div className='test-case-location'>{test.location.file}:{test.location.line}</div>}
|
|
||||||
{test && !!test.projectName && <ProjectLink report={report} projectName={test.projectName}></ProjectLink>}
|
|
||||||
{test && !!test.annotations.length && <Chip header='Annotations'>
|
|
||||||
{test.annotations.map(a => <div className='test-case-annotation'>
|
|
||||||
<span style={{ fontWeight: 'bold' }}>{a.type}</span>
|
|
||||||
{a.description && <span>: {a.description}</span>}
|
|
||||||
</div>)}
|
|
||||||
</Chip>}
|
|
||||||
{test && <TabbedPane tabs={
|
|
||||||
test.results.map((result, index) => ({
|
|
||||||
id: String(index),
|
|
||||||
title: <div style={{ display: 'flex', alignItems: 'center' }}>{statusIcon(result.status)} {retryLabel(index)}</div>,
|
|
||||||
render: () => <TestResultView test={test!} result={result}></TestResultView>
|
|
||||||
})) || []} selectedTab={String(selectedResultIndex)} setSelectedTab={id => setSelectedResultIndex(+id)} />}
|
|
||||||
</div>;
|
|
||||||
};
|
|
||||||
|
|
||||||
const TestResultView: React.FC<{
|
|
||||||
test: TestCase,
|
|
||||||
result: TestResult,
|
|
||||||
}> = ({ result }) => {
|
|
||||||
|
|
||||||
const { screenshots, videos, traces, otherAttachments, attachmentsMap } = React.useMemo(() => {
|
|
||||||
const attachmentsMap = new Map<string, TestAttachment>();
|
|
||||||
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 <div className='test-result'>
|
|
||||||
{result.error && <Chip header='Errors'>
|
|
||||||
<ErrorMessage key='error-message' error={result.error}></ErrorMessage>
|
|
||||||
</Chip>}
|
|
||||||
{!!result.steps.length && <Chip header='Test Steps'>
|
|
||||||
{result.steps.map((step, i) => <StepTreeItem key={`step-${i}`} step={step} depth={0}></StepTreeItem>)}
|
|
||||||
</Chip>}
|
|
||||||
|
|
||||||
{expected && actual && <Chip header='Image mismatch'>
|
|
||||||
<ImageDiff actual={actual} expected={expected} diff={diff}></ImageDiff>
|
|
||||||
<AttachmentLink key={`expected`} attachment={expected}></AttachmentLink>
|
|
||||||
<AttachmentLink key={`actual`} attachment={actual}></AttachmentLink>
|
|
||||||
{diff && <AttachmentLink key={`diff`} attachment={diff}></AttachmentLink>}
|
|
||||||
</Chip>}
|
|
||||||
|
|
||||||
{!!screenshots.length && <Chip header='Screenshots'>
|
|
||||||
{screenshots.map((a, i) => {
|
|
||||||
return <div key={`screenshot-${i}`}>
|
|
||||||
<img src={a.path} />
|
|
||||||
<AttachmentLink attachment={a}></AttachmentLink>
|
|
||||||
</div>;
|
|
||||||
})}
|
|
||||||
</Chip>}
|
|
||||||
|
|
||||||
{!!traces.length && <Chip header='Traces'>
|
|
||||||
{traces.map((a, i) => <div key={`trace-${i}`}>
|
|
||||||
<a href={`trace/index.html?trace=${new URL(a.path!, window.location.href)}`}>
|
|
||||||
<img src={traceImage} style={{ width: 192, height: 117, marginLeft: 20 }} />
|
|
||||||
</a>
|
|
||||||
<AttachmentLink attachment={a}></AttachmentLink>
|
|
||||||
</div>)}
|
|
||||||
</Chip>}
|
|
||||||
|
|
||||||
{!!videos.length && <Chip header='Videos'>
|
|
||||||
{videos.map((a, i) => <div key={`video-${i}`}>
|
|
||||||
<video controls>
|
|
||||||
<source src={a.path} type={a.contentType}/>
|
|
||||||
</video>
|
|
||||||
<AttachmentLink attachment={a}></AttachmentLink>
|
|
||||||
</div>)}
|
|
||||||
</Chip>}
|
|
||||||
|
|
||||||
{!!otherAttachments.length && <Chip header='Attachments'>
|
|
||||||
{otherAttachments.map((a, i) => <AttachmentLink key={`attachment-link-${i}`} attachment={a}></AttachmentLink>)}
|
|
||||||
</Chip>}
|
|
||||||
</div>;
|
|
||||||
};
|
|
||||||
|
|
||||||
const StepTreeItem: React.FC<{
|
|
||||||
step: TestStep;
|
|
||||||
depth: number,
|
|
||||||
}> = ({ step, depth }) => {
|
|
||||||
return <TreeItem title={<span>
|
|
||||||
<span style={{ float: 'right' }}>{msToString(step.duration)}</span>
|
|
||||||
{statusIcon(step.error || step.duration === -1 ? 'failed' : 'passed')}
|
|
||||||
<span>{step.title}</span>
|
|
||||||
{step.location && <span className='test-summary-path'>— {step.location.file}:{step.location.line}</span>}
|
|
||||||
</span>} loadChildren={step.steps.length + (step.snippet ? 1 : 0) ? () => {
|
|
||||||
const children = step.steps.map((s, i) => <StepTreeItem key={i} step={s} depth={depth + 1}></StepTreeItem>);
|
|
||||||
if (step.snippet)
|
|
||||||
children.unshift(<ErrorMessage key='line' error={step.snippet}></ErrorMessage>);
|
|
||||||
return children;
|
|
||||||
} : undefined} depth={depth}></TreeItem>;
|
|
||||||
};
|
|
||||||
|
|
||||||
const StatsNavView: React.FC<{
|
|
||||||
stats: Stats
|
|
||||||
}> = ({ stats }) => {
|
|
||||||
return <nav className='subnav-links d-flex no-wrap'>
|
|
||||||
<Link className='subnav-item' href='#?'>
|
|
||||||
All <span className='d-inline counter'>{stats.total}</span>
|
|
||||||
</Link>
|
|
||||||
<Link className='subnav-item' href='#?q=s:passed'>
|
|
||||||
Passed <span className='d-inline counter'>{stats.expected}</span>
|
|
||||||
</Link>
|
|
||||||
<Link className='subnav-item' href='#?q=s:failed'>
|
|
||||||
{!!stats.unexpected && statusIcon('unexpected')} Failed <span className='d-inline counter'>{stats.unexpected}</span>
|
|
||||||
</Link>
|
|
||||||
<Link className='subnav-item' href='#?q=s:flaky'>
|
|
||||||
{!!stats.flaky && statusIcon('flaky')} Flaky <span className='d-inline counter'>{stats.flaky}</span>
|
|
||||||
</Link>
|
|
||||||
<Link className='subnav-item' href='#?q=s:skipped'>
|
|
||||||
Skipped <span className='d-inline counter'>{stats.skipped}</span>
|
|
||||||
</Link>
|
|
||||||
</nav>;
|
|
||||||
};
|
|
||||||
|
|
||||||
const AttachmentLink: React.FunctionComponent<{
|
|
||||||
attachment: TestAttachment,
|
|
||||||
href?: string,
|
|
||||||
}> = ({ attachment, href }) => {
|
|
||||||
return <TreeItem title={<span>
|
|
||||||
{attachment.contentType === kMissingContentType ?
|
|
||||||
<svg aria-hidden='true' height='16' viewBox='0 0 16 16' version='1.1' width='16' data-view-component='true' className='octicon color-text-warning'>
|
|
||||||
<path fillRule='evenodd' d='M8.22 1.754a.25.25 0 00-.44 0L1.698 13.132a.25.25 0 00.22.368h12.164a.25.25 0 00.22-.368L8.22 1.754zm-1.763-.707c.659-1.234 2.427-1.234 3.086 0l6.082 11.378A1.75 1.75 0 0114.082 15H1.918a1.75 1.75 0 01-1.543-2.575L6.457 1.047zM9 11a1 1 0 11-2 0 1 1 0 012 0zm-.25-5.25a.75.75 0 00-1.5 0v2.5a.75.75 0 001.5 0v-2.5z'></path>
|
|
||||||
</svg> :
|
|
||||||
<svg aria-hidden='true' height='16' viewBox='0 0 16 16' version='1.1' width='16' data-view-component='true' className='octicon color-fg-muted'>
|
|
||||||
<path fillRule='evenodd' d='M3.5 1.75a.25.25 0 01.25-.25h3a.75.75 0 000 1.5h.5a.75.75 0 000-1.5h2.086a.25.25 0 01.177.073l2.914 2.914a.25.25 0 01.073.177v8.586a.25.25 0 01-.25.25h-.5a.75.75 0 000 1.5h.5A1.75 1.75 0 0014 13.25V4.664c0-.464-.184-.909-.513-1.237L10.573.513A1.75 1.75 0 009.336 0H3.75A1.75 1.75 0 002 1.75v11.5c0 .649.353 1.214.874 1.515a.75.75 0 10.752-1.298.25.25 0 01-.126-.217V1.75zM8.75 3a.75.75 0 000 1.5h.5a.75.75 0 000-1.5h-.5zM6 5.25a.75.75 0 01.75-.75h.5a.75.75 0 010 1.5h-.5A.75.75 0 016 5.25zm2 1.5A.75.75 0 018.75 6h.5a.75.75 0 010 1.5h-.5A.75.75 0 018 6.75zm-1.25.75a.75.75 0 000 1.5h.5a.75.75 0 000-1.5h-.5zM8 9.75A.75.75 0 018.75 9h.5a.75.75 0 010 1.5h-.5A.75.75 0 018 9.75zm-.75.75a1.75 1.75 0 00-1.75 1.75v3c0 .414.336.75.75.75h2.5a.75.75 0 00.75-.75v-3a1.75 1.75 0 00-1.75-1.75h-.5zM7 12.25a.25.25 0 01.25-.25h.5a.25.25 0 01.25.25v2.25H7v-2.25z'></path>
|
|
||||||
</svg>
|
|
||||||
}
|
|
||||||
{attachment.path && <a href={href || attachment.path} target='_blank'>{attachment.name}</a>}
|
|
||||||
{attachment.body && <span>{attachment.name}</span>}
|
|
||||||
</span>} loadChildren={attachment.body ? () => {
|
|
||||||
return [<div className='attachment-body'>{attachment.body}</div>];
|
|
||||||
} : undefined} depth={0}></TreeItem>;
|
|
||||||
};
|
|
||||||
|
|
||||||
const ImageDiff: React.FunctionComponent<{
|
|
||||||
actual: TestAttachment,
|
|
||||||
expected: TestAttachment,
|
|
||||||
diff?: TestAttachment,
|
|
||||||
}> = ({ actual, expected, diff }) => {
|
|
||||||
const [selectedTab, setSelectedTab] = React.useState<string>('actual');
|
|
||||||
const tabs = [];
|
|
||||||
tabs.push({
|
|
||||||
id: 'actual',
|
|
||||||
title: 'Actual',
|
|
||||||
render: () => <img src={actual.path}/>
|
|
||||||
});
|
|
||||||
tabs.push({
|
|
||||||
id: 'expected',
|
|
||||||
title: 'Expected',
|
|
||||||
render: () => <img src={expected.path}/>
|
|
||||||
});
|
|
||||||
if (diff) {
|
|
||||||
tabs.push({
|
|
||||||
id: 'diff',
|
|
||||||
title: 'Diff',
|
|
||||||
render: () => <img src={diff.path}/>
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return <div className='vbox test-image-mismatch'>
|
|
||||||
<TabbedPane tabs={tabs} selectedTab={selectedTab} setSelectedTab={setSelectedTab} />
|
|
||||||
</div>;
|
|
||||||
};
|
|
||||||
|
|
||||||
function statusIcon(status: 'failed' | 'timedOut' | 'skipped' | 'passed' | 'expected' | 'unexpected' | 'flaky'): JSX.Element {
|
|
||||||
switch (status) {
|
|
||||||
case 'failed':
|
|
||||||
case 'unexpected':
|
|
||||||
return <svg className='octicon color-text-danger' viewBox='0 0 16 16' version='1.1' width='16' height='16' aria-hidden='true'>
|
|
||||||
<path fillRule='evenodd' d='M3.72 3.72a.75.75 0 011.06 0L8 6.94l3.22-3.22a.75.75 0 111.06 1.06L9.06 8l3.22 3.22a.75.75 0 11-1.06 1.06L8 9.06l-3.22 3.22a.75.75 0 01-1.06-1.06L6.94 8 3.72 4.78a.75.75 0 010-1.06z'></path>
|
|
||||||
</svg>;
|
|
||||||
case 'passed':
|
|
||||||
case 'expected':
|
|
||||||
return <svg aria-hidden='true' height='16' viewBox='0 0 16 16' version='1.1' width='16' data-view-component='true' className='octicon color-icon-success'>
|
|
||||||
<path fillRule='evenodd' d='M13.78 4.22a.75.75 0 010 1.06l-7.25 7.25a.75.75 0 01-1.06 0L2.22 9.28a.75.75 0 011.06-1.06L6 10.94l6.72-6.72a.75.75 0 011.06 0z'></path>
|
|
||||||
</svg>;
|
|
||||||
case 'timedOut':
|
|
||||||
return <svg aria-hidden='true' height='16' viewBox='0 0 16 16' version='1.1' width='16' data-view-component='true' className='octicon color-text-danger'>
|
|
||||||
<path fillRule='evenodd' d='M5.75.75A.75.75 0 016.5 0h3a.75.75 0 010 1.5h-.75v1l-.001.041a6.718 6.718 0 013.464 1.435l.007-.006.75-.75a.75.75 0 111.06 1.06l-.75.75-.006.007a6.75 6.75 0 11-10.548 0L2.72 5.03l-.75-.75a.75.75 0 011.06-1.06l.75.75.007.006A6.718 6.718 0 017.25 2.541a.756.756 0 010-.041v-1H6.5a.75.75 0 01-.75-.75zM8 14.5A5.25 5.25 0 108 4a5.25 5.25 0 000 10.5zm.389-6.7l1.33-1.33a.75.75 0 111.061 1.06L9.45 8.861A1.502 1.502 0 018 10.75a1.5 1.5 0 11.389-2.95z'></path>
|
|
||||||
</svg>;
|
|
||||||
case 'flaky':
|
|
||||||
return <svg aria-hidden='true' height='16' viewBox='0 0 16 16' version='1.1' width='16' data-view-component='true' className='octicon color-text-warning'>
|
|
||||||
<path fillRule='evenodd' d='M8.22 1.754a.25.25 0 00-.44 0L1.698 13.132a.25.25 0 00.22.368h12.164a.25.25 0 00.22-.368L8.22 1.754zm-1.763-.707c.659-1.234 2.427-1.234 3.086 0l6.082 11.378A1.75 1.75 0 0114.082 15H1.918a1.75 1.75 0 01-1.543-2.575L6.457 1.047zM9 11a1 1 0 11-2 0 1 1 0 012 0zm-.25-5.25a.75.75 0 00-1.5 0v2.5a.75.75 0 001.5 0v-2.5z'></path>
|
|
||||||
</svg>;
|
|
||||||
case 'skipped':
|
|
||||||
return <svg className='octicon' viewBox='0 0 16 16' version='1.1' width='16' height='16' aria-hidden='true'></svg>;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 <div className='error-message' dangerouslySetInnerHTML={{ __html: html || '' }}></div>;
|
|
||||||
};
|
|
||||||
|
|
||||||
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 <div className='chip'>
|
|
||||||
<div className={'chip-header' + (setExpanded ? ' expanded-' + expanded : '')} onClick={() => setExpanded?.(!expanded)}>
|
|
||||||
{setExpanded && !!expanded && downArrow()}
|
|
||||||
{setExpanded && !expanded && rightArrow()}
|
|
||||||
{header}
|
|
||||||
</div>
|
|
||||||
{ (!setExpanded || expanded) && <div className='chip-body'>{children}</div>}
|
|
||||||
</div>;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
function navigate(href: string) {
|
function navigate(href: string) {
|
||||||
|
|
@ -461,28 +167,6 @@ function navigate(href: string) {
|
||||||
window.dispatchEvent(navEvent);
|
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 <Link href={`#?q=p:${value}`}>
|
|
||||||
<span className={'label label-color-' + (report.projectNames.indexOf(projectName) % 6)}>
|
|
||||||
{projectName}
|
|
||||||
</span>
|
|
||||||
</Link>;
|
|
||||||
};
|
|
||||||
|
|
||||||
const Link: React.FunctionComponent<{
|
|
||||||
href: string,
|
|
||||||
className?: string,
|
|
||||||
title?: string,
|
|
||||||
children: any,
|
|
||||||
}> = ({ href, className, children, title }) => {
|
|
||||||
return <a className={`no-decorations${className ? ' ' + className : ''}`} href={href} title={title}>{children}</a>;
|
|
||||||
};
|
|
||||||
|
|
||||||
const Route: React.FunctionComponent<{
|
const Route: React.FunctionComponent<{
|
||||||
params: string,
|
params: string,
|
||||||
children: any
|
children: any
|
||||||
|
|
@ -500,130 +184,9 @@ const Route: React.FunctionComponent<{
|
||||||
return currentParams === params ? children : null;
|
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<string>();
|
|
||||||
const status = new Set<string>();
|
|
||||||
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<any> {
|
async function readJsonEntry(entryName: string): Promise<any> {
|
||||||
const reportEntry = window.entries.get(entryName);
|
const reportEntry = window.entries.get(entryName);
|
||||||
const writer = new zipjs.TextWriter() as zip.TextWriter;
|
const writer = new zipjs.TextWriter() as zip.TextWriter;
|
||||||
await reportEntry!.getData!(writer);
|
await reportEntry!.getData!(writer);
|
||||||
return JSON.parse(await writer.getData());
|
return JSON.parse(await writer.getData());
|
||||||
}
|
}
|
||||||
|
|
||||||
type SearchValues = {
|
|
||||||
text: string;
|
|
||||||
project: string;
|
|
||||||
status: 'passed' | 'failed' | 'flaky' | 'skipped';
|
|
||||||
};
|
|
||||||
|
|
||||||
const kMissingContentType = 'x-playwright/missing';
|
|
||||||
|
|
|
||||||
104
packages/playwright-core/src/web/htmlReport/links.css
Normal file
104
packages/playwright-core/src/web/htmlReport/links.css
Normal file
|
|
@ -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;
|
||||||
|
}
|
||||||
64
packages/playwright-core/src/web/htmlReport/links.tsx
Normal file
64
packages/playwright-core/src/web/htmlReport/links.tsx
Normal file
|
|
@ -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 <a style={{ textDecoration: 'none', color: 'initial' }} className={`${className || ''}`} href={href} title={title}>{children}</a>;
|
||||||
|
};
|
||||||
|
|
||||||
|
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 <Link href={`#?q=p:${value}`}>
|
||||||
|
<span className={'label label-color-' + (report.projectNames.indexOf(projectName) % 6)}>
|
||||||
|
{projectName}
|
||||||
|
</span>
|
||||||
|
</Link>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AttachmentLink: React.FunctionComponent<{
|
||||||
|
attachment: TestAttachment,
|
||||||
|
href?: string,
|
||||||
|
}> = ({ attachment, href }) => {
|
||||||
|
return <TreeItem title={<span>
|
||||||
|
{attachment.contentType === kMissingContentType ?
|
||||||
|
<svg aria-hidden='true' height='16' viewBox='0 0 16 16' version='1.1' width='16' data-view-component='true' className='octicon color-text-warning'>
|
||||||
|
<path fillRule='evenodd' d='M8.22 1.754a.25.25 0 00-.44 0L1.698 13.132a.25.25 0 00.22.368h12.164a.25.25 0 00.22-.368L8.22 1.754zm-1.763-.707c.659-1.234 2.427-1.234 3.086 0l6.082 11.378A1.75 1.75 0 0114.082 15H1.918a1.75 1.75 0 01-1.543-2.575L6.457 1.047zM9 11a1 1 0 11-2 0 1 1 0 012 0zm-.25-5.25a.75.75 0 00-1.5 0v2.5a.75.75 0 001.5 0v-2.5z'></path>
|
||||||
|
</svg> :
|
||||||
|
<svg aria-hidden='true' height='16' viewBox='0 0 16 16' version='1.1' width='16' data-view-component='true' className='octicon color-fg-muted'>
|
||||||
|
<path fillRule='evenodd' d='M3.5 1.75a.25.25 0 01.25-.25h3a.75.75 0 000 1.5h.5a.75.75 0 000-1.5h2.086a.25.25 0 01.177.073l2.914 2.914a.25.25 0 01.073.177v8.586a.25.25 0 01-.25.25h-.5a.75.75 0 000 1.5h.5A1.75 1.75 0 0014 13.25V4.664c0-.464-.184-.909-.513-1.237L10.573.513A1.75 1.75 0 009.336 0H3.75A1.75 1.75 0 002 1.75v11.5c0 .649.353 1.214.874 1.515a.75.75 0 10.752-1.298.25.25 0 01-.126-.217V1.75zM8.75 3a.75.75 0 000 1.5h.5a.75.75 0 000-1.5h-.5zM6 5.25a.75.75 0 01.75-.75h.5a.75.75 0 010 1.5h-.5A.75.75 0 016 5.25zm2 1.5A.75.75 0 018.75 6h.5a.75.75 0 010 1.5h-.5A.75.75 0 018 6.75zm-1.25.75a.75.75 0 000 1.5h.5a.75.75 0 000-1.5h-.5zM8 9.75A.75.75 0 018.75 9h.5a.75.75 0 010 1.5h-.5A.75.75 0 018 9.75zm-.75.75a1.75 1.75 0 00-1.75 1.75v3c0 .414.336.75.75.75h2.5a.75.75 0 00.75-.75v-3a1.75 1.75 0 00-1.75-1.75h-.5zM7 12.25a.25.25 0 01.25-.25h.5a.25.25 0 01.25.25v2.25H7v-2.25z'></path>
|
||||||
|
</svg>
|
||||||
|
}
|
||||||
|
{attachment.path && <a href={href || attachment.path} target='_blank'>{attachment.name}</a>}
|
||||||
|
{attachment.body && <span>{attachment.name}</span>}
|
||||||
|
</span>} loadChildren={attachment.body ? () => {
|
||||||
|
return [<div className='attachment-link'>{attachment.body}</div>];
|
||||||
|
} : undefined} depth={0}></TreeItem>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const kMissingContentType = 'x-playwright/missing';
|
||||||
43
packages/playwright-core/src/web/htmlReport/statsNavView.tsx
Normal file
43
packages/playwright-core/src/web/htmlReport/statsNavView.tsx
Normal file
|
|
@ -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 <nav className='d-flex no-wrap'>
|
||||||
|
<Link className='subnav-item' href='#?'>
|
||||||
|
All <span className='d-inline counter'>{stats.total}</span>
|
||||||
|
</Link>
|
||||||
|
<Link className='subnav-item' href='#?q=s:passed'>
|
||||||
|
Passed <span className='d-inline counter'>{stats.expected}</span>
|
||||||
|
</Link>
|
||||||
|
<Link className='subnav-item' href='#?q=s:failed'>
|
||||||
|
{!!stats.unexpected && statusIcon('unexpected')} Failed <span className='d-inline counter'>{stats.unexpected}</span>
|
||||||
|
</Link>
|
||||||
|
<Link className='subnav-item' href='#?q=s:flaky'>
|
||||||
|
{!!stats.flaky && statusIcon('flaky')} Flaky <span className='d-inline counter'>{stats.flaky}</span>
|
||||||
|
</Link>
|
||||||
|
<Link className='subnav-item' href='#?q=s:skipped'>
|
||||||
|
Skipped <span className='d-inline counter'>{stats.skipped}</span>
|
||||||
|
</Link>
|
||||||
|
</nav>;
|
||||||
|
};
|
||||||
43
packages/playwright-core/src/web/htmlReport/statusIcon.tsx
Normal file
43
packages/playwright-core/src/web/htmlReport/statusIcon.tsx
Normal file
|
|
@ -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 <svg className='octicon color-text-danger' viewBox='0 0 16 16' version='1.1' width='16' height='16' aria-hidden='true'>
|
||||||
|
<path fillRule='evenodd' d='M3.72 3.72a.75.75 0 011.06 0L8 6.94l3.22-3.22a.75.75 0 111.06 1.06L9.06 8l3.22 3.22a.75.75 0 11-1.06 1.06L8 9.06l-3.22 3.22a.75.75 0 01-1.06-1.06L6.94 8 3.72 4.78a.75.75 0 010-1.06z'></path>
|
||||||
|
</svg>;
|
||||||
|
case 'passed':
|
||||||
|
case 'expected':
|
||||||
|
return <svg aria-hidden='true' height='16' viewBox='0 0 16 16' version='1.1' width='16' data-view-component='true' className='octicon color-icon-success'>
|
||||||
|
<path fillRule='evenodd' d='M13.78 4.22a.75.75 0 010 1.06l-7.25 7.25a.75.75 0 01-1.06 0L2.22 9.28a.75.75 0 011.06-1.06L6 10.94l6.72-6.72a.75.75 0 011.06 0z'></path>
|
||||||
|
</svg>;
|
||||||
|
case 'timedOut':
|
||||||
|
return <svg aria-hidden='true' height='16' viewBox='0 0 16 16' version='1.1' width='16' data-view-component='true' className='octicon color-text-danger'>
|
||||||
|
<path fillRule='evenodd' d='M5.75.75A.75.75 0 016.5 0h3a.75.75 0 010 1.5h-.75v1l-.001.041a6.718 6.718 0 013.464 1.435l.007-.006.75-.75a.75.75 0 111.06 1.06l-.75.75-.006.007a6.75 6.75 0 11-10.548 0L2.72 5.03l-.75-.75a.75.75 0 011.06-1.06l.75.75.007.006A6.718 6.718 0 017.25 2.541a.756.756 0 010-.041v-1H6.5a.75.75 0 01-.75-.75zM8 14.5A5.25 5.25 0 108 4a5.25 5.25 0 000 10.5zm.389-6.7l1.33-1.33a.75.75 0 111.061 1.06L9.45 8.861A1.502 1.502 0 018 10.75a1.5 1.5 0 11.389-2.95z'></path>
|
||||||
|
</svg>;
|
||||||
|
case 'flaky':
|
||||||
|
return <svg aria-hidden='true' height='16' viewBox='0 0 16 16' version='1.1' width='16' data-view-component='true' className='octicon color-text-warning'>
|
||||||
|
<path fillRule='evenodd' d='M8.22 1.754a.25.25 0 00-.44 0L1.698 13.132a.25.25 0 00.22.368h12.164a.25.25 0 00.22-.368L8.22 1.754zm-1.763-.707c.659-1.234 2.427-1.234 3.086 0l6.082 11.378A1.75 1.75 0 0114.082 15H1.918a1.75 1.75 0 01-1.543-2.575L6.457 1.047zM9 11a1 1 0 11-2 0 1 1 0 012 0zm-.25-5.25a.75.75 0 00-1.5 0v2.5a.75.75 0 001.5 0v-2.5z'></path>
|
||||||
|
</svg>;
|
||||||
|
case 'skipped':
|
||||||
|
return <svg className='octicon' viewBox='0 0 16 16' version='1.1' width='16' height='16' aria-hidden='true'></svg>;
|
||||||
|
}
|
||||||
|
}
|
||||||
76
packages/playwright-core/src/web/htmlReport/tabbedPane.css
Normal file
76
packages/playwright-core/src/web/htmlReport/tabbedPane.css
Normal file
|
|
@ -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;
|
||||||
|
}
|
||||||
53
packages/playwright-core/src/web/htmlReport/tabbedPane.tsx
Normal file
53
packages/playwright-core/src/web/htmlReport/tabbedPane.tsx
Normal file
|
|
@ -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 <div className='tabbed-pane'>
|
||||||
|
<div className='vbox'>
|
||||||
|
<div className='hbox' style={{ flex: 'none' }}>
|
||||||
|
<div className='tabbed-pane-tab-strip'>{
|
||||||
|
tabs.map(tab => (
|
||||||
|
<div className={'tabbed-pane-tab-element ' + (selectedTab === tab.id ? 'selected' : '')}
|
||||||
|
onClick={() => setSelectedTab(tab.id)}
|
||||||
|
key={tab.id}>
|
||||||
|
<div className='tabbed-pane-tab-label'>{tab.title}</div>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
}</div>
|
||||||
|
</div>
|
||||||
|
{
|
||||||
|
tabs.map(tab => {
|
||||||
|
if (selectedTab === tab.id)
|
||||||
|
return <div key={tab.id} className='tab-content'>{tab.render()}</div>;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>;
|
||||||
|
};
|
||||||
69
packages/playwright-core/src/web/htmlReport/testCaseView.css
Normal file
69
packages/playwright-core/src/web/htmlReport/testCaseView.css
Normal file
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
57
packages/playwright-core/src/web/htmlReport/testCaseView.tsx
Normal file
57
packages/playwright-core/src/web/htmlReport/testCaseView.tsx
Normal file
|
|
@ -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 <div className='test-case-column vbox'>
|
||||||
|
{test && <div className='test-case-path'>{test.path.join(' › ')}</div>}
|
||||||
|
{test && <div className='test-case-title'>{test?.title}</div>}
|
||||||
|
{test && <div className='test-case-location'>{test.location.file}:{test.location.line}</div>}
|
||||||
|
{test && !!test.projectName && <ProjectLink report={report} projectName={test.projectName}></ProjectLink>}
|
||||||
|
{test && !!test.annotations.length && <Chip header='Annotations'>
|
||||||
|
{test.annotations.map(a => <div className='test-case-annotation'>
|
||||||
|
<span style={{ fontWeight: 'bold' }}>{a.type}</span>
|
||||||
|
{a.description && <span>: {a.description}</span>}
|
||||||
|
</div>)}
|
||||||
|
</Chip>}
|
||||||
|
{test && <TabbedPane tabs={
|
||||||
|
test.results.map((result, index) => ({
|
||||||
|
id: String(index),
|
||||||
|
title: <div style={{ display: 'flex', alignItems: 'center' }}>{statusIcon(result.status)} {retryLabel(index)}</div>,
|
||||||
|
render: () => <TestResultView test={test!} result={result}></TestResultView>
|
||||||
|
})) || []} selectedTab={String(selectedResultIndex)} setSelectedTab={id => setSelectedResultIndex(+id)} />}
|
||||||
|
</div>;
|
||||||
|
};
|
||||||
|
|
||||||
|
function retryLabel(index: number) {
|
||||||
|
if (!index)
|
||||||
|
return 'Run';
|
||||||
|
return `Retry #${index}`;
|
||||||
|
}
|
||||||
38
packages/playwright-core/src/web/htmlReport/testFileView.css
Normal file
38
packages/playwright-core/src/web/htmlReport/testFileView.css
Normal file
|
|
@ -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);
|
||||||
|
}
|
||||||
54
packages/playwright-core/src/web/htmlReport/testFileView.tsx
Normal file
54
packages/playwright-core/src/web/htmlReport/testFileView.tsx
Normal file
|
|
@ -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 <Chip
|
||||||
|
expanded={isFileExpanded(file.fileId)}
|
||||||
|
noInsets={true}
|
||||||
|
setExpanded={(expanded => setFileExpanded(file.fileId, expanded))}
|
||||||
|
header={<span>
|
||||||
|
<span style={{ float: 'right' }}>{msToString(file.stats.duration)}</span>
|
||||||
|
{file.fileName}
|
||||||
|
</span>}>
|
||||||
|
{file.tests.filter(t => filter.matches(t)).map(test =>
|
||||||
|
<div key={`test-${test.testId}`} className={'test-file-test test-file-test-outcome-' + test.outcome}>
|
||||||
|
<span style={{ float: 'right' }}>{msToString(test.duration)}</span>
|
||||||
|
{report.projectNames.length > 1 && !!test.projectName &&
|
||||||
|
<span style={{ float: 'right' }}><ProjectLink report={report} projectName={test.projectName}></ProjectLink></span>}
|
||||||
|
{statusIcon(test.outcome)}
|
||||||
|
<Link href={`#?testId=${test.testId}`} title={[...test.path, test.title].join(' › ')}>
|
||||||
|
{[...test.path, test.title].join(' › ')}
|
||||||
|
<span className='test-file-path'>— {test.location.file}:{test.location.line}</span>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Chip>;
|
||||||
|
};
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
182
packages/playwright-core/src/web/htmlReport/testResultView.tsx
Normal file
182
packages/playwright-core/src/web/htmlReport/testResultView.tsx
Normal file
|
|
@ -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<string, TestAttachment>();
|
||||||
|
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 <div className='test-result'>
|
||||||
|
{result.error && <Chip header='Errors'>
|
||||||
|
<ErrorMessage key='test-result-error-message' error={result.error}></ErrorMessage>
|
||||||
|
</Chip>}
|
||||||
|
{!!result.steps.length && <Chip header='Test Steps'>
|
||||||
|
{result.steps.map((step, i) => <StepTreeItem key={`step-${i}`} step={step} depth={0}></StepTreeItem>)}
|
||||||
|
</Chip>}
|
||||||
|
|
||||||
|
{expected && actual && <Chip header='Image mismatch'>
|
||||||
|
<ImageDiff actual={actual} expected={expected} diff={diff}></ImageDiff>
|
||||||
|
<AttachmentLink key={`expected`} attachment={expected}></AttachmentLink>
|
||||||
|
<AttachmentLink key={`actual`} attachment={actual}></AttachmentLink>
|
||||||
|
{diff && <AttachmentLink key={`diff`} attachment={diff}></AttachmentLink>}
|
||||||
|
</Chip>}
|
||||||
|
|
||||||
|
{!!screenshots.length && <Chip header='Screenshots'>
|
||||||
|
{screenshots.map((a, i) => {
|
||||||
|
return <div key={`screenshot-${i}`}>
|
||||||
|
<img src={a.path} />
|
||||||
|
<AttachmentLink attachment={a}></AttachmentLink>
|
||||||
|
</div>;
|
||||||
|
})}
|
||||||
|
</Chip>}
|
||||||
|
|
||||||
|
{!!traces.length && <Chip header='Traces'>
|
||||||
|
{traces.map((a, i) => <div key={`trace-${i}`}>
|
||||||
|
<a href={`trace/index.html?trace=${new URL(a.path!, window.location.href)}`}>
|
||||||
|
<img src={traceImage} style={{ width: 192, height: 117, marginLeft: 20 }} />
|
||||||
|
</a>
|
||||||
|
<AttachmentLink attachment={a}></AttachmentLink>
|
||||||
|
</div>)}
|
||||||
|
</Chip>}
|
||||||
|
|
||||||
|
{!!videos.length && <Chip header='Videos'>
|
||||||
|
{videos.map((a, i) => <div key={`video-${i}`}>
|
||||||
|
<video controls>
|
||||||
|
<source src={a.path} type={a.contentType}/>
|
||||||
|
</video>
|
||||||
|
<AttachmentLink attachment={a}></AttachmentLink>
|
||||||
|
</div>)}
|
||||||
|
</Chip>}
|
||||||
|
|
||||||
|
{!!otherAttachments.length && <Chip header='Attachments'>
|
||||||
|
{otherAttachments.map((a, i) => <AttachmentLink key={`attachment-link-${i}`} attachment={a}></AttachmentLink>)}
|
||||||
|
</Chip>}
|
||||||
|
</div>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const StepTreeItem: React.FC<{
|
||||||
|
step: TestStep;
|
||||||
|
depth: number,
|
||||||
|
}> = ({ step, depth }) => {
|
||||||
|
return <TreeItem title={<span>
|
||||||
|
<span style={{ float: 'right' }}>{msToString(step.duration)}</span>
|
||||||
|
{statusIcon(step.error || step.duration === -1 ? 'failed' : 'passed')}
|
||||||
|
<span>{step.title}</span>
|
||||||
|
{step.location && <span className='test-result-path'>— {step.location.file}:{step.location.line}</span>}
|
||||||
|
</span>} loadChildren={step.steps.length + (step.snippet ? 1 : 0) ? () => {
|
||||||
|
const children = step.steps.map((s, i) => <StepTreeItem key={i} step={s} depth={depth + 1}></StepTreeItem>);
|
||||||
|
if (step.snippet)
|
||||||
|
children.unshift(<ErrorMessage key='line' error={step.snippet}></ErrorMessage>);
|
||||||
|
return children;
|
||||||
|
} : undefined} depth={depth}></TreeItem>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ImageDiff: React.FunctionComponent<{
|
||||||
|
actual: TestAttachment,
|
||||||
|
expected: TestAttachment,
|
||||||
|
diff?: TestAttachment,
|
||||||
|
}> = ({ actual, expected, diff }) => {
|
||||||
|
const [selectedTab, setSelectedTab] = React.useState<string>('actual');
|
||||||
|
const tabs = [];
|
||||||
|
tabs.push({
|
||||||
|
id: 'actual',
|
||||||
|
title: 'Actual',
|
||||||
|
render: () => <img src={actual.path}/>
|
||||||
|
});
|
||||||
|
tabs.push({
|
||||||
|
id: 'expected',
|
||||||
|
title: 'Expected',
|
||||||
|
render: () => <img src={expected.path}/>
|
||||||
|
});
|
||||||
|
if (diff) {
|
||||||
|
tabs.push({
|
||||||
|
id: 'diff',
|
||||||
|
title: 'Diff',
|
||||||
|
render: () => <img src={diff.path}/>
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return <div className='vbox' data-testid='test-result-image-mismatch'>
|
||||||
|
<TabbedPane tabs={tabs} selectedTab={selectedTab} setSelectedTab={setSelectedTab} />
|
||||||
|
</div>;
|
||||||
|
};
|
||||||
|
|
||||||
|
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 <div className='test-result-error-message' dangerouslySetInnerHTML={{ __html: html || '' }}></div>;
|
||||||
|
};
|
||||||
|
|
||||||
|
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]!));
|
||||||
|
}
|
||||||
30
packages/playwright-core/src/web/htmlReport/treeItem.css
Normal file
30
packages/playwright-core/src/web/htmlReport/treeItem.css
Normal file
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -15,6 +15,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
|
import './treeItem.css';
|
||||||
|
|
||||||
export const TreeItem: React.FunctionComponent<{
|
export const TreeItem: React.FunctionComponent<{
|
||||||
title: JSX.Element,
|
title: JSX.Element,
|
||||||
|
|
@ -37,13 +38,13 @@ export const TreeItem: React.FunctionComponent<{
|
||||||
</div>;
|
</div>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function downArrow() {
|
function downArrow() {
|
||||||
return <svg aria-hidden='true' height='16' viewBox='0 0 16 16' version='1.1' width='16' className='octicon color-fg-muted'>
|
return <svg aria-hidden='true' height='16' viewBox='0 0 16 16' version='1.1' width='16' className='octicon color-fg-muted'>
|
||||||
<path fillRule='evenodd' d='M12.78 6.22a.75.75 0 010 1.06l-4.25 4.25a.75.75 0 01-1.06 0L3.22 7.28a.75.75 0 011.06-1.06L8 9.94l3.72-3.72a.75.75 0 011.06 0z'></path>
|
<path fillRule='evenodd' d='M12.78 6.22a.75.75 0 010 1.06l-4.25 4.25a.75.75 0 01-1.06 0L3.22 7.28a.75.75 0 011.06-1.06L8 9.94l3.72-3.72a.75.75 0 011.06 0z'></path>
|
||||||
</svg>;
|
</svg>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function rightArrow() {
|
function rightArrow() {
|
||||||
return <svg aria-hidden='true' height='16' viewBox='0 0 16 16' version='1.1' width='16' data-view-component='true' className='octicon color-fg-muted'>
|
return <svg aria-hidden='true' height='16' viewBox='0 0 16 16' version='1.1' width='16' data-view-component='true' className='octicon color-fg-muted'>
|
||||||
<path fillRule='evenodd' d='M6.22 3.22a.75.75 0 011.06 0l4.25 4.25a.75.75 0 010 1.06l-4.25 4.25a.75.75 0 01-1.06-1.06L9.94 8 6.22 4.28a.75.75 0 010-1.06z'></path>
|
<path fillRule='evenodd' d='M6.22 3.22a.75.75 0 011.06 0l4.25 4.25a.75.75 0 010 1.06l-4.25 4.25a.75.75 0 01-1.06-1.06L9.94 8 6.22 4.28a.75.75 0 010-1.06z'></path>
|
||||||
</svg>;
|
</svg>;
|
||||||
|
|
@ -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("Flaky") .counter')).toHaveText('1');
|
||||||
await expect(page.locator('.subnav-item:has-text("Skipped") .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-file-test-outcome-unexpected >> text=fails')).toBeVisible();
|
||||||
await expect(page.locator('.test-summary.outcome-flaky >> text=flaky')).toBeVisible();
|
await expect(page.locator('.test-file-test-outcome-flaky >> text=flaky')).toBeVisible();
|
||||||
await expect(page.locator('.test-summary.outcome-expected >> text=passes')).toBeVisible();
|
await expect(page.locator('.test-file-test-outcome-expected >> text=passes')).toBeVisible();
|
||||||
await expect(page.locator('.test-summary.outcome-skipped >> text=skipped')).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) => {
|
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.click('text=passes');
|
||||||
await page.locator('text=Missing attachment "screenshot"').click();
|
await page.locator('text=Missing attachment "screenshot"').click();
|
||||||
const screenshotFile = testInfo.outputPath('test-results' , 'a-passes', 'screenshot.png');
|
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 }) => {
|
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 showReport();
|
||||||
await page.click('text=fails');
|
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');
|
const image = imageDiff.locator('img');
|
||||||
await expect(image).toHaveAttribute('src', /.*png/);
|
await expect(image).toHaveAttribute('src', /.*png/);
|
||||||
const actualSrc = await image.getAttribute('src');
|
const actualSrc = await image.getAttribute('src');
|
||||||
|
|
@ -173,9 +173,9 @@ test('should include stdio', async ({ runInlineTest, page, showReport }) => {
|
||||||
await showReport();
|
await showReport();
|
||||||
await page.click('text=fails');
|
await page.click('text=fails');
|
||||||
await page.locator('text=stdout').click();
|
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 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 }) => {
|
test('should highlight error', async ({ runInlineTest, page, showReport }) => {
|
||||||
|
|
@ -192,7 +192,7 @@ test('should highlight error', async ({ runInlineTest, page, showReport }) => {
|
||||||
|
|
||||||
await showReport();
|
await showReport();
|
||||||
await page.click('text=fails');
|
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 }) => {
|
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.txt');
|
||||||
await page.click('text=example.json');
|
await page.click('text=example.json');
|
||||||
await page.click('text=example-utf16.txt');
|
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']);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue