feat(html): allow projects with spaces, lax filter matching (#9913)
This commit is contained in:
parent
d79aae633c
commit
d234030b9a
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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';
|
||||||
|
};
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue