feat(html): add filter field (#9874)
This commit is contained in:
parent
34e55007d0
commit
0566af86e1
|
|
@ -41,14 +41,12 @@ body {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
color: var(--color-fg-default);
|
color: var(--color-fg-default);
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-family: SegoeUI-SemiBold-final,Segoe UI Semibold,SegoeUI-Regular-final,Segoe UI,"Segoe UI Web (West European)",Segoe,-apple-system,BlinkMacSystemFont,Roboto,Helvetica Neue,Tahoma,Helvetica,Arial,sans-serif;
|
font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji";
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
}
|
}
|
||||||
|
|
||||||
* {
|
* {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
min-width: 0;
|
|
||||||
min-height: 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
svg {
|
svg {
|
||||||
|
|
@ -206,7 +204,8 @@ svg {
|
||||||
}
|
}
|
||||||
|
|
||||||
.stats-line {
|
.stats-line {
|
||||||
padding-left: 5px;
|
font-weight: normal;
|
||||||
|
padding-left: 25px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stats {
|
.stats {
|
||||||
|
|
@ -214,18 +213,6 @@ svg {
|
||||||
padding: 0 2px;
|
padding: 0 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stats.expected {
|
|
||||||
color: var(--color-success-fg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.stats.unexpected {
|
|
||||||
color: var(--color-danger-fg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.stats.flaky {
|
|
||||||
color: var(--color-attention-fg);
|
|
||||||
}
|
|
||||||
|
|
||||||
video, img {
|
video, img {
|
||||||
flex: none;
|
flex: none;
|
||||||
box-shadow: var(--box-shadow-thick);
|
box-shadow: var(--box-shadow-thick);
|
||||||
|
|
@ -241,11 +228,11 @@ video, img {
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-summary-list {
|
.file-summary-list {
|
||||||
padding-bottom: 20px;
|
padding: 20px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-summary-list .chip-body a:not(:nth-child(1)) .test-summary,
|
.file-summary-list .chip-body .test-summary:not(:first-child),
|
||||||
.failed-test:not(:nth-child(1)) {
|
.failed-test:not(:first-child) {
|
||||||
border-top: 1px solid var(--color-border-default);
|
border-top: 1px solid var(--color-border-default);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -466,6 +453,123 @@ a.no-decorations {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.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);
|
||||||
|
}
|
||||||
|
|
||||||
|
.width-full {
|
||||||
|
width: 100% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subnav-search {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subnav-search-input {
|
||||||
|
width: 320px;
|
||||||
|
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 {
|
||||||
|
position: relative;
|
||||||
|
float: left;
|
||||||
|
padding: 5px 16px;
|
||||||
|
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) {
|
||||||
.chip-header {
|
.chip-header {
|
||||||
border-radius: 0 !important;
|
border-radius: 0 !important;
|
||||||
|
|
@ -488,4 +592,13 @@ a.no-decorations {
|
||||||
border-radius: 0 !important;
|
border-radius: 0 !important;
|
||||||
margin: 0 !important;
|
margin: 0 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.subnav-item, .form-control {
|
||||||
|
border-radius: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subnav-item {
|
||||||
|
padding: 5px 3px;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,12 +20,14 @@ import ansi2html from 'ansi-to-html';
|
||||||
import { downArrow, rightArrow, TreeItem } from '../components/treeItem';
|
import { downArrow, rightArrow, TreeItem } from '../components/treeItem';
|
||||||
import { TabbedPane } from '../traceViewer/ui/tabbedPane';
|
import { TabbedPane } from '../traceViewer/ui/tabbedPane';
|
||||||
import { msToString } from '../uiUtils';
|
import { msToString } from '../uiUtils';
|
||||||
import type { TestCase, TestResult, TestStep, TestFile, Stats, TestAttachment, HTMLReport, TestFileSummary } from '@playwright/test/src/reporters/html';
|
import type { TestCase, TestResult, TestStep, TestFile, Stats, TestAttachment, HTMLReport, TestFileSummary, TestCaseSummary } from '@playwright/test/src/reporters/html';
|
||||||
|
|
||||||
export const Report: React.FC = () => {
|
export const Report: React.FC = () => {
|
||||||
const [fetchError, setFetchError] = React.useState<string | undefined>();
|
const [fetchError, setFetchError] = React.useState<string | undefined>();
|
||||||
const [report, setReport] = React.useState<HTMLReport | undefined>();
|
const [report, setReport] = React.useState<HTMLReport | undefined>();
|
||||||
const [expandedFiles, setExpandedFiles] = React.useState<Set<string>>(new Set());
|
const [expandedFiles, setExpandedFiles] = React.useState<Map<string, boolean>>(new Map());
|
||||||
|
const [navigationId, setNavigationId] = React.useState<number>(Date.now());
|
||||||
|
const [filterText, setFilterText] = React.useState(new URL(window.location.href).searchParams.get('q') || '');
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (report)
|
if (report)
|
||||||
|
|
@ -33,26 +35,26 @@ export const Report: React.FC = () => {
|
||||||
(async () => {
|
(async () => {
|
||||||
try {
|
try {
|
||||||
const report = await fetch('data/report.json', { cache: 'no-cache' }).then(r => r.json() as Promise<HTMLReport>);
|
const report = await fetch('data/report.json', { cache: 'no-cache' }).then(r => r.json() as Promise<HTMLReport>);
|
||||||
if (report.files.length)
|
|
||||||
expandedFiles.add(report.files[0].fileId);
|
|
||||||
setReport(report);
|
setReport(report);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setFetchError(e.message);
|
setFetchError(e.message);
|
||||||
}
|
}
|
||||||
|
window.addEventListener('popstate', () => {
|
||||||
|
setNavigationId(Date.now());
|
||||||
|
setFilterText(new URL(window.location.href).searchParams.get('q') || '');
|
||||||
|
});
|
||||||
})();
|
})();
|
||||||
}, [report, expandedFiles]);
|
}, [report]);
|
||||||
|
|
||||||
|
const filter = React.useMemo(() => Filter.parse(filterText), [filterText]);
|
||||||
|
|
||||||
return <div className='vbox columns'>
|
return <div className='vbox columns'>
|
||||||
{!fetchError && <div className='flow-container'>
|
{!fetchError && <div className='flow-container'>
|
||||||
<Route params=''>
|
<Route params=''>
|
||||||
<AllTestFilesSummaryView report={report} isFileExpanded={fileId => expandedFiles.has(fileId)} setFileExpanded={(fileId, expanded) => {
|
<AllTestFilesSummaryView report={report} filter={filter} expandedFiles={expandedFiles} setExpandedFiles={setExpandedFiles} navigationId={navigationId}></AllTestFilesSummaryView>
|
||||||
const newExpanded = new Set(expandedFiles);
|
</Route>
|
||||||
if (expanded)
|
<Route params='q'>
|
||||||
newExpanded.add(fileId);
|
<AllTestFilesSummaryView report={report} filter={filter} expandedFiles={expandedFiles} setExpandedFiles={setExpandedFiles} navigationId={navigationId}></AllTestFilesSummaryView>
|
||||||
else
|
|
||||||
newExpanded.delete(fileId);
|
|
||||||
setExpandedFiles(newExpanded);
|
|
||||||
}}></AllTestFilesSummaryView>
|
|
||||||
</Route>
|
</Route>
|
||||||
<Route params='testId'>
|
<Route params='testId'>
|
||||||
{!!report && <TestCaseView report={report}></TestCaseView>}
|
{!!report && <TestCaseView report={report}></TestCaseView>}
|
||||||
|
|
@ -62,16 +64,50 @@ export const Report: React.FC = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const AllTestFilesSummaryView: React.FC<{
|
const AllTestFilesSummaryView: React.FC<{
|
||||||
report?: HTMLReport;
|
report?: HTMLReport,
|
||||||
isFileExpanded: (fileId: string) => boolean;
|
expandedFiles: Map<string, boolean>,
|
||||||
setFileExpanded: (fileId: string, expanded: boolean) => void;
|
setExpandedFiles: (value: Map<string, boolean>) => void,
|
||||||
}> = ({ report, isFileExpanded, setFileExpanded }) => {
|
navigationId: number,
|
||||||
|
filter: Filter
|
||||||
|
}> = ({ report, filter, expandedFiles, setExpandedFiles, navigationId }) => {
|
||||||
|
const inputRef = React.useRef<HTMLInputElement | null>(null);
|
||||||
return <div className='file-summary-list'>
|
return <div className='file-summary-list'>
|
||||||
{report && <div className='global-stats'>
|
{report && <div className='d-flex'>
|
||||||
<span>Ran {report.stats.total} tests</span>
|
<form className='subnav-search width-full' onSubmit={
|
||||||
<StatsView stats={report.stats}></StatsView>
|
event => {
|
||||||
|
event.preventDefault();
|
||||||
|
navigate(`?q=${inputRef.current?.value || ''}`);
|
||||||
|
}
|
||||||
|
}>
|
||||||
|
<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 key={`filter-${navigationId}`} type='search' className='form-control subnav-search-input input-contrast width-full' defaultValue={filter.expression} ref={inputRef}></input>
|
||||||
|
</form>
|
||||||
|
<div className='ml-2 pl-2 d-flex'>
|
||||||
|
<StatsNavView stats={report.stats}></StatsNavView>
|
||||||
|
</div>
|
||||||
</div>}
|
</div>}
|
||||||
{report && (report.files || []).map((file, i) => <TestFileSummaryView key={`file-${i}`} report={report} file={file} isFileExpanded={isFileExpanded} setFileExpanded={setFileExpanded}></TestFileSummaryView>)}
|
{report && (report.files || []).filter(f => !!f.tests.find(t => filter.matches(t))).map((file, i) => {
|
||||||
|
return <TestFileSummaryView
|
||||||
|
key={`file-${file.fileId}`}
|
||||||
|
report={report}
|
||||||
|
file={file}
|
||||||
|
isFileExpanded={fileId => {
|
||||||
|
const value = expandedFiles.get(fileId);
|
||||||
|
if (value === undefined)
|
||||||
|
return i === 0;
|
||||||
|
return !!value;
|
||||||
|
}}
|
||||||
|
setFileExpanded={(fileId, expanded) => {
|
||||||
|
const newExpanded = new Map(expandedFiles);
|
||||||
|
newExpanded.set(fileId, expanded);
|
||||||
|
setExpandedFiles(newExpanded);
|
||||||
|
}}
|
||||||
|
filter={filter}>
|
||||||
|
</TestFileSummaryView>;
|
||||||
|
})}
|
||||||
</div>;
|
</div>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -80,24 +116,28 @@ const TestFileSummaryView: React.FC<{
|
||||||
file: TestFileSummary;
|
file: TestFileSummary;
|
||||||
isFileExpanded: (fileId: string) => boolean;
|
isFileExpanded: (fileId: string) => boolean;
|
||||||
setFileExpanded: (fileId: string, expanded: boolean) => void;
|
setFileExpanded: (fileId: string, expanded: boolean) => void;
|
||||||
}> = ({ file, report, isFileExpanded, setFileExpanded }) => {
|
filter: Filter;
|
||||||
|
}> = ({ file, report, isFileExpanded, setFileExpanded, filter }) => {
|
||||||
return <Chip
|
return <Chip
|
||||||
expanded={isFileExpanded(file.fileId)}
|
expanded={isFileExpanded(file.fileId)}
|
||||||
setExpanded={(expanded => setFileExpanded(file.fileId, expanded))}
|
setExpanded={(expanded => setFileExpanded(file.fileId, expanded))}
|
||||||
header={<span>
|
header={<span>
|
||||||
<span style={{ float: 'right' }}>{msToString(file.stats.duration)}</span>
|
<span style={{ float: 'right' }}>{msToString(file.stats.duration)}</span>
|
||||||
{file.fileName}
|
{file.fileName}
|
||||||
<StatsView stats={file.stats}></StatsView>
|
<StatsInlineView stats={file.stats}></StatsInlineView>
|
||||||
</span>}>
|
</span>}>
|
||||||
{file.tests.map((test, i) => <Link key={`test-${i}`} href={`?testId=${test.testId}`}>
|
{file.tests.filter(t => filter.matches(t)).map(test =>
|
||||||
<div className={'test-summary outcome-' + test.outcome}>
|
<div key={`test-${test.testId}`} className={'test-summary outcome-' + test.outcome}>
|
||||||
<span style={{ float: 'right' }}>{msToString(test.duration)}</span>
|
<span style={{ float: 'right' }}>{msToString(test.duration)}</span>
|
||||||
{statusIcon(test.outcome)}
|
{statusIcon(test.outcome)}
|
||||||
{test.title}
|
<Link href={`?testId=${test.testId}`}>
|
||||||
<span className='test-summary-path'>— {test.path.join(' › ')}</span>
|
{test.title}
|
||||||
{report.projectNames.length > 1 && !!test.projectName && <span className={'label label-color-' + (report.projectNames.indexOf(test.projectName) % 8)}>{test.projectName}</span>}
|
<span className='test-summary-path'>— {test.path.join(' › ')}</span>
|
||||||
|
</Link>
|
||||||
|
{report.projectNames.length > 1 && !!test.projectName &&
|
||||||
|
<ProjectLink report={report} projectName={test.projectName}></ProjectLink>}
|
||||||
</div>
|
</div>
|
||||||
</Link>)}
|
)}
|
||||||
</Chip>;
|
</Chip>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -128,7 +168,7 @@ const TestCaseView: React.FC<{
|
||||||
return <div className='test-case-column vbox'>
|
return <div className='test-case-column vbox'>
|
||||||
{test && <div className='test-case-title'>{test?.title}</div>}
|
{test && <div className='test-case-title'>{test?.title}</div>}
|
||||||
{test && <div className='test-case-location'>{test.path.join(' › ')}</div>}
|
{test && <div className='test-case-location'>{test.path.join(' › ')}</div>}
|
||||||
{test && !!test.projectName && <div><span className={'label label-color-' + (report.projectNames.indexOf(test.projectName) % 8)}>{test.projectName}</span></div>}
|
{test && !!test.projectName && <ProjectLink report={report} projectName={test.projectName}></ProjectLink>}
|
||||||
{test && <TabbedPane tabs={
|
{test && <TabbedPane tabs={
|
||||||
test.results.map((result, index) => ({
|
test.results.map((result, index) => ({
|
||||||
id: String(index),
|
id: String(index),
|
||||||
|
|
@ -226,18 +266,38 @@ const StepTreeItem: React.FC<{
|
||||||
} : undefined} depth={depth}></TreeItem>;
|
} : undefined} depth={depth}></TreeItem>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const StatsView: React.FC<{
|
const StatsInlineView: React.FC<{
|
||||||
stats: Stats
|
stats: Stats
|
||||||
}> = ({ stats }) => {
|
}> = ({ stats }) => {
|
||||||
return <span className='stats-line'>
|
return <span className='stats-line'>
|
||||||
—
|
{!!stats.expected && <span className='stats'>Passed <span className='counter' style={{ backgroundColor: 'var(--color-scale-green-1)' }}>{stats.expected}</span></span>}
|
||||||
{!!stats.unexpected && <span className='stats unexpected'>{stats.unexpected} failed</span>}
|
{!!stats.unexpected && <span className='stats'>Failed <span className='counter' style={{ backgroundColor: 'var(--color-scale-red-1)' }}>{stats.unexpected}</span></span>}
|
||||||
{!!stats.flaky && <span className='stats flaky'>{stats.flaky} flaky</span>}
|
{!!stats.flaky && <span className='stats'>Flaky <span className='counter' style={{ backgroundColor: 'var(--color-scale-yellow-1)' }}>{stats.flaky}</span></span>}
|
||||||
{!!stats.expected && <span className='stats expected'>{stats.expected} passed</span>}
|
|
||||||
{!!stats.skipped && <span className='stats skipped'>{stats.skipped} skipped</span>}
|
|
||||||
</span>;
|
</span>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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=outcome:expected'>
|
||||||
|
Passed <span className='d-inline counter' style={{ backgroundColor: 'var(--color-scale-green-1)' }}>{stats.expected}</span>
|
||||||
|
</Link>
|
||||||
|
<Link className='subnav-item' href='?q=outcome:unexpected'>
|
||||||
|
Failed <span className='d-inline counter' style={{ backgroundColor: 'var(--color-scale-red-1)' }}>{stats.unexpected}</span>
|
||||||
|
</Link>
|
||||||
|
<Link className='subnav-item' href='?q=outcome:flaky'>
|
||||||
|
Flaky <span className='d-inline counter' style={{ backgroundColor: 'var(--color-scale-yellow-1)' }}>{stats.flaky}</span>
|
||||||
|
</Link>
|
||||||
|
<Link className='subnav-item' href='?q=outcome:skipped'>
|
||||||
|
Skipped <span className='d-inline counter'>{stats.skipped}</span>
|
||||||
|
</Link>
|
||||||
|
</nav>;
|
||||||
|
};
|
||||||
|
|
||||||
const AttachmentLink: React.FunctionComponent<{
|
const AttachmentLink: React.FunctionComponent<{
|
||||||
attachment: TestAttachment,
|
attachment: TestAttachment,
|
||||||
href?: string,
|
href?: string,
|
||||||
|
|
@ -372,14 +432,27 @@ function navigate(href: string) {
|
||||||
window.dispatchEvent(navEvent);
|
window.dispatchEvent(navEvent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const ProjectLink: React.FunctionComponent<{
|
||||||
|
report: HTMLReport,
|
||||||
|
projectName: string,
|
||||||
|
}> = ({ report, projectName }) => {
|
||||||
|
return <Link href={`?q=project:${projectName}`}>
|
||||||
|
<span className={'label label-color-' + (report.projectNames.indexOf(projectName) % 8)}>
|
||||||
|
{projectName}
|
||||||
|
</span>
|
||||||
|
</Link>;
|
||||||
|
};
|
||||||
|
|
||||||
const Link: React.FunctionComponent<{
|
const Link: React.FunctionComponent<{
|
||||||
href: string,
|
href: string,
|
||||||
children: any
|
className?: string,
|
||||||
}> = ({ href, children }) => {
|
children: any,
|
||||||
return <a onClick={event => {
|
}> = ({ href, className, children }) => {
|
||||||
|
return <a className={`no-decorations${className ? ' ' + className : ''}`} onClick={event => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
navigate(href);
|
navigate(href);
|
||||||
}} className='no-decorations' href={href}>{children}</a>;
|
}} href={href}>{children}</a>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const Route: React.FunctionComponent<{
|
const Route: React.FunctionComponent<{
|
||||||
|
|
@ -398,3 +471,48 @@ const Route: React.FunctionComponent<{
|
||||||
}, []);
|
}, []);
|
||||||
return currentParams === params ? children : null;
|
return currentParams === params ? children : null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
class Filter {
|
||||||
|
project = new Set<string>();
|
||||||
|
outcome = new Set<string>();
|
||||||
|
text: string[] = [];
|
||||||
|
expression: string;
|
||||||
|
private static regex = /(".*?"|[\w]+:|[^"\s:]+)(?=\s*|\s*$)/g;
|
||||||
|
|
||||||
|
private constructor(expression: string) {
|
||||||
|
this.expression = expression;
|
||||||
|
}
|
||||||
|
|
||||||
|
static parse(expression: string): Filter {
|
||||||
|
const filter = new Filter(expression);
|
||||||
|
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] === 'project:' && match[i + 1]) {
|
||||||
|
filter.project.add(match[++i]);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (match[i] === 'outcome:' && match[i + 1]) {
|
||||||
|
filter.outcome.add(match[++i]);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
filter.text.push(match[i]);
|
||||||
|
}
|
||||||
|
return filter;
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
const fullTitle = test.path.join(' ') + test.title;
|
||||||
|
const matches = !!this.text.find(t => fullTitle.includes(t));
|
||||||
|
if (!matches)
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue