diff --git a/packages/playwright-core/src/web/htmlReport/htmlReport.css b/packages/playwright-core/src/web/htmlReport/htmlReport.css index e0b215bc53..0fca4e4ad7 100644 --- a/packages/playwright-core/src/web/htmlReport/htmlReport.css +++ b/packages/playwright-core/src/web/htmlReport/htmlReport.css @@ -21,7 +21,7 @@ rgb(0 0 0 / 15%) 0px 6.1px 6.3px, rgb(0 0 0 / 10%) 0px -2px 4px, rgb(0 0 0 / 15%) 0px -6.1px 12px, - rgb(0 0 0 / 25%) 0px 27px 28px; + rgb(0 0 0 / 25%) 0px 6px 12px; } html, body { @@ -51,6 +51,8 @@ body { * { box-sizing: border-box; + min-width: 0; + min-height: 0; } svg { @@ -153,17 +155,15 @@ svg { box-shadow: inset 0 -1px 0 var(--color-border-muted) !important; } -.columns > .tab-strip .tab-element.selected { +.test-case-column .tab-element.selected { font-weight: 600; border-bottom-color: var(--color-primer-border-active); } .test-case-column .tab-element { border: none; - text-transform: uppercase; - font-weight: bold; - font-size: 11px; color: var(--color-fg-default); + border-bottom: 2px solid transparent; } .test-case-column .tab-element:hover { @@ -514,16 +514,14 @@ article, aside, details, figcaption, figure, footer, header, main, menu, nav, se background-color: var(--color-canvas-inset); } -.width-full { - width: 100% !important; -} - .subnav-search { position: relative; + flex: auto; + display: flex; } .subnav-search-input { - width: 320px; + flex: auto; padding-left: 32px; color: var(--color-fg-muted); } @@ -543,9 +541,10 @@ article, aside, details, figcaption, figure, footer, header, main, menu, nav, se } .subnav-item { + flex: none; position: relative; float: left; - padding: 5px 16px; + padding: 5px 10px; font-weight: 500; line-height: 20px; color: var(--color-fg-default); diff --git a/packages/playwright-core/src/web/htmlReport/htmlReport.tsx b/packages/playwright-core/src/web/htmlReport/htmlReport.tsx index 258a04147a..c0ec33a8f6 100644 --- a/packages/playwright-core/src/web/htmlReport/htmlReport.tsx +++ b/packages/playwright-core/src/web/htmlReport/htmlReport.tsx @@ -94,10 +94,10 @@ const AllTestFilesSummaryView: React.FC<{ }, [report, filter]); return
{report &&
-
{ event.preventDefault(); - navigate(`#?q=${filterText || ''}`); + navigate(`#?q=${filterText ? encodeURIComponent(filterText) : ''}`); } }> {projectName} @@ -484,53 +486,120 @@ const Route: React.FunctionComponent<{ }; class Filter { - project = new Set(); - outcome = new Set(); + project: string[] = []; + status: string[] = []; text: string[] = []; - private static regex = /(".*?"|[\w]+:|[^"\s:]+)(?=\s*|\s*$)/g; empty(): boolean { - return this.project.size + this.outcome.size + this.text.length === 0; + return this.project.length + this.status.length + this.text.length === 0; } static parse(expression: string): Filter { - const filter = new Filter(); - const match = (expression.match(Filter.regex) || []).map(t => { - return t.startsWith('"') && t.endsWith('"') ? t.substring(1, t.length - 1) : t; - }); - for (let i = 0; i < match.length; ++i) { - if (match[i] === 'p:' && match[i + 1]) { - filter.project.add(match[++i]); + const tokens = Filter.tokenize(expression); + const project = new Set(); + const status = new Set(); + const text: string[] = []; + for (const token of tokens) { + if (token.startsWith('p:')) { + project.add(token.slice(2)); continue; } - if (match[i] === 's:' && match[i + 1]) { - const status = match[++i]; - if (status === 'passed') - filter.outcome.add('expected'); - else if (status === 'failed') - filter.outcome.add('unexpected'); - else - filter.outcome.add(status); + if (token.startsWith('s:')) { + status.add(token.slice(2)); continue; } - filter.text.push(match[i].toLowerCase()); + 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 (this.project.size && !this.project.has(test.projectName)) - return false; - if (this.outcome.size && !this.outcome.has(test.outcome)) - return false; - if (this.text.length) { - if (!(test as any).fullTitle) - (test as any).fullTitle = (test.path.join(' ') + test.title).toLowerCase(); - const fullTitle = (test as any).fullTitle; - const matches = !!this.text.find(t => fullTitle.includes(t)); + 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'; +};