feat(html): allow projects with spaces, lax filter matching (#9913)

This commit is contained in:
Pavel Feldman 2021-11-01 09:53:58 -08:00 committed by GitHub
parent d79aae633c
commit d234030b9a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 111 additions and 43 deletions

View file

@ -21,7 +21,7 @@
rgb(0 0 0 / 15%) 0px 6.1px 6.3px, rgb(0 0 0 / 15%) 0px 6.1px 6.3px,
rgb(0 0 0 / 10%) 0px -2px 4px, rgb(0 0 0 / 10%) 0px -2px 4px,
rgb(0 0 0 / 15%) 0px -6.1px 12px, 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 { html, body {
@ -51,6 +51,8 @@ body {
* { * {
box-sizing: border-box; box-sizing: border-box;
min-width: 0;
min-height: 0;
} }
svg { svg {
@ -153,17 +155,15 @@ svg {
box-shadow: inset 0 -1px 0 var(--color-border-muted) !important; 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; font-weight: 600;
border-bottom-color: var(--color-primer-border-active); border-bottom-color: var(--color-primer-border-active);
} }
.test-case-column .tab-element { .test-case-column .tab-element {
border: none; border: none;
text-transform: uppercase;
font-weight: bold;
font-size: 11px;
color: var(--color-fg-default); color: var(--color-fg-default);
border-bottom: 2px solid transparent;
} }
.test-case-column .tab-element:hover { .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); background-color: var(--color-canvas-inset);
} }
.width-full {
width: 100% !important;
}
.subnav-search { .subnav-search {
position: relative; position: relative;
flex: auto;
display: flex;
} }
.subnav-search-input { .subnav-search-input {
width: 320px; flex: auto;
padding-left: 32px; padding-left: 32px;
color: var(--color-fg-muted); color: var(--color-fg-muted);
} }
@ -543,9 +541,10 @@ article, aside, details, figcaption, figure, footer, header, main, menu, nav, se
} }
.subnav-item { .subnav-item {
flex: none;
position: relative; position: relative;
float: left; float: left;
padding: 5px 16px; padding: 5px 10px;
font-weight: 500; font-weight: 500;
line-height: 20px; line-height: 20px;
color: var(--color-fg-default); color: var(--color-fg-default);

View file

@ -94,10 +94,10 @@ const AllTestFilesSummaryView: React.FC<{
}, [report, filter]); }, [report, filter]);
return <div className='file-summary-list'> return <div className='file-summary-list'>
{report && <div className='d-flex'> {report && <div className='d-flex'>
<form className='subnav-search width-full' onSubmit={ <form className='subnav-search' onSubmit={
event => { event => {
event.preventDefault(); event.preventDefault();
navigate(`#?q=${filterText || ''}`); 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'> <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'>
@ -450,7 +450,9 @@ const ProjectLink: React.FunctionComponent<{
report: HTMLReport, report: HTMLReport,
projectName: string, projectName: string,
}> = ({ report, projectName }) => { }> = ({ report, projectName }) => {
return <Link href={`#?q=p:${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)}> <span className={'label label-color-' + (report.projectNames.indexOf(projectName) % 6)}>
{projectName} {projectName}
</span> </span>
@ -484,53 +486,120 @@ const Route: React.FunctionComponent<{
}; };
class Filter { class Filter {
project = new Set<string>(); project: string[] = [];
outcome = new Set<string>(); status: string[] = [];
text: string[] = []; text: string[] = [];
private static regex = /(".*?"|[\w]+:|[^"\s:]+)(?=\s*|\s*$)/g;
empty(): boolean { 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 { static parse(expression: string): Filter {
const filter = new Filter(); const tokens = Filter.tokenize(expression);
const match = (expression.match(Filter.regex) || []).map(t => { const project = new Set<string>();
return t.startsWith('"') && t.endsWith('"') ? t.substring(1, t.length - 1) : t; const status = new Set<string>();
}); const text: string[] = [];
for (let i = 0; i < match.length; ++i) { for (const token of tokens) {
if (match[i] === 'p:' && match[i + 1]) { if (token.startsWith('p:')) {
filter.project.add(match[++i]); project.add(token.slice(2));
continue; continue;
} }
if (match[i] === 's:' && match[i + 1]) { if (token.startsWith('s:')) {
const status = match[++i]; status.add(token.slice(2));
if (status === 'passed')
filter.outcome.add('expected');
else if (status === 'failed')
filter.outcome.add('unexpected');
else
filter.outcome.add(status);
continue; 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; 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 { matches(test: TestCaseSummary): boolean {
if (this.project.size && !this.project.has(test.projectName)) if (!(test as any).searchValues) {
return false; let status = 'passed';
if (this.outcome.size && !this.outcome.has(test.outcome)) if (test.outcome === 'unexpected')
return false; status = 'failed';
if (this.text.length) { if (test.outcome === 'flaky')
if (!(test as any).fullTitle) status = 'flaky';
(test as any).fullTitle = (test.path.join(' ') + test.title).toLowerCase(); if (test.outcome === 'skipped')
const fullTitle = (test as any).fullTitle; status = 'skipped';
const matches = !!this.text.find(t => fullTitle.includes(t)); 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) if (!matches)
return false; 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; return true;
} }
} }
type SearchValues = {
text: string;
project: string;
status: 'passed' | 'failed' | 'flaky' | 'skipped';
};