Skip to content

Commit 91e5e4a

Browse files
feat(html-reporter): add step filter in test steps section (#39595)
1 parent e495544 commit 91e5e4a

File tree

4 files changed

+68
-4
lines changed

4 files changed

+68
-4
lines changed

packages/html-reporter/src/testResultView.css

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,10 @@
102102
}
103103
}
104104

105+
.step-filter {
106+
margin-bottom: 8px;
107+
}
108+
105109
@media only screen and (max-width: 600px) {
106110
.test-result {
107111
padding: 0 !important;

packages/html-reporter/src/testResultView.tsx

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,9 @@ export const TestResultView: React.FC<{
9292
return { screenshots: [...screenshots], videos, traces, otherAttachments, diffs, errors, otherAttachmentAnchors, screenshotAnchors, errorContext };
9393
}, [result]);
9494

95+
const [stepFilterText, setStepFilterText] = React.useState('');
96+
React.useEffect(() => setStepFilterText(''), [result]);
97+
9598
const prompt = useAsyncMemo(async () => {
9699
if (report.json().options?.noCopyPrompt)
97100
return undefined;
@@ -134,7 +137,11 @@ export const TestResultView: React.FC<{
134137
})}
135138
</AutoChip>}
136139
{!!result.steps.length && <AutoChip header='Test Steps'>
137-
{result.steps.map((step, i) => <StepTreeItem key={`step-${i}`} step={step} result={result} test={test} depth={0}/>)}
140+
<form className='subnav-search step-filter' onSubmit={e => e.preventDefault()}>
141+
{icons.search()}
142+
<input className='form-control subnav-search-input input-contrast width-full' type='search' spellCheck={false} placeholder='Filter steps' aria-label='Filter steps' value={stepFilterText} onChange={e => setStepFilterText(e.target.value)} />
143+
</form>
144+
{result.steps.map((step, i) => <StepTreeItem key={`step-${i}`} step={step} result={result} test={test} depth={0} filterText={stepFilterText}/>)}
138145
</AutoChip>}
139146

140147
{diffs.map((diff, index) =>
@@ -201,13 +208,22 @@ function pickDiffForError(error: string, diffs: ImageDiff[]): ImageDiff | undefi
201208
return diffs.find(diff => error.includes(diff.name));
202209
}
203210

211+
function stepMatchesFilter(step: TestStep, filterText: string): boolean {
212+
if (step.title.toLowerCase().includes(filterText.toLowerCase()))
213+
return true;
214+
return step.steps.some(s => stepMatchesFilter(s, filterText));
215+
}
216+
204217
const StepTreeItem: React.FC<{
205218
test: TestCase;
206219
result: TestResult;
207220
step: TestStep;
208221
depth: number,
209-
}> = ({ test, step, result, depth }) => {
222+
filterText?: string,
223+
}> = ({ test, step, result, depth, filterText }) => {
210224
const searchParams = useSearchParams();
225+
if (filterText && !stepMatchesFilter(step, filterText))
226+
return null;
211227
return <TreeItem title={<div aria-label={step.title} className='step-title-container'>
212228
{statusIcon(step.error || step.duration === -1 ? 'failed' : (step.skipped ? 'skipped' : 'passed'))}
213229
<span className='step-title-text'>
@@ -226,9 +242,9 @@ const StepTreeItem: React.FC<{
226242
<span className='step-duration'>{msToString(step.duration)}</span>
227243
</div>} loadChildren={step.steps.length || step.snippet ? () => {
228244
const snippet = step.snippet ? [<CodeSnippet testId='test-snippet' key='line' code={step.snippet} />] : [];
229-
const steps = step.steps.map((s, i) => <StepTreeItem key={i} step={s} depth={depth + 1} result={result} test={test} />);
245+
const steps = step.steps.map((s, i) => <StepTreeItem key={i} step={s} depth={depth + 1} result={result} test={test} filterText={filterText} />);
230246
return snippet.concat(steps);
231-
} : undefined} depth={depth}/>;
247+
} : undefined} depth={depth} expandByDefault={!!filterText}/>;
232248
};
233249

234250
type WorkerLists = Map<number, { tests: TestCaseSummary[], runs: number[] }>;

packages/html-reporter/src/treeItem.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@ export const TreeItem: React.FunctionComponent<{
2929
flash?: boolean
3030
}> = ({ title, loadChildren, onClick, expandByDefault, depth, style, flash }) => {
3131
const [expanded, setExpanded] = React.useState(expandByDefault || false);
32+
React.useEffect(() => {
33+
setExpanded(expandByDefault || false);
34+
}, [expandByDefault]);
3235
return <div role='treeitem' className={clsx('tree-item', flash && 'yellow-flash')} style={style}>
3336
<div className='tree-item-title' style={{ paddingLeft: depth * 22 + 4 }} onClick={() => { onClick?.(); setExpanded(!expanded); }} >
3437
{loadChildren && !!expanded && icons.downArrow()}

tests/playwright-test/reporter-html.spec.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -822,6 +822,47 @@ for (const useIntermediateMergeReport of [true, false] as const) {
822822
]);
823823
});
824824

825+
test('should filter steps', async ({ runInlineTest, page, showReport }) => {
826+
const result = await runInlineTest({
827+
'a.test.js': `
828+
import { test, expect } from '@playwright/test';
829+
test('has steps', async ({}) => {
830+
await test.step('click button', async () => {});
831+
await test.step('fill form', async () => {
832+
await test.step('fill username', async () => {});
833+
await test.step('fill password', async () => {});
834+
});
835+
await test.step('submit form', async () => {});
836+
});
837+
`,
838+
}, { reporter: 'dot,html' }, { PLAYWRIGHT_HTML_OPEN: 'never' });
839+
expect(result.exitCode).toBe(0);
840+
expect(result.passed).toBe(1);
841+
842+
await showReport();
843+
await page.getByRole('link', { name: 'has steps' }).click();
844+
845+
const filterInput = page.getByLabel('Filter steps');
846+
await expect(filterInput).toBeVisible();
847+
848+
// filter matching a subset of steps
849+
await filterInput.fill('fill');
850+
await expect(page.locator('.tree-item-title', { hasText: 'fill form' })).toBeVisible();
851+
await expect(page.locator('.tree-item-title', { hasText: 'click button' })).toBeHidden();
852+
await expect(page.locator('.tree-item-title', { hasText: 'submit form' })).toBeHidden();
853+
// matching parent is auto-expanded to show matching children (like trace viewer)
854+
await expect(page.locator('.tree-item-title', { hasText: 'fill username' })).toBeVisible();
855+
await expect(page.locator('.tree-item-title', { hasText: 'fill password' })).toBeVisible();
856+
857+
// clear filter restores all steps collapsed
858+
await filterInput.clear();
859+
await expect(page.locator('.tree-item-title', { hasText: 'click button' })).toBeVisible();
860+
await expect(page.locator('.tree-item-title', { hasText: 'submit form' })).toBeVisible();
861+
// children of fill form are collapsed again after clearing the filter
862+
await expect(page.locator('.tree-item-title', { hasText: 'fill username' })).toBeHidden();
863+
await expect(page.locator('.tree-item-title', { hasText: 'fill password' })).toBeHidden();
864+
});
865+
825866
test('should show step snippets from non-root', async ({ runInlineTest, page, showReport }) => {
826867
const result = await runInlineTest({
827868
'playwright.config.js': `

0 commit comments

Comments
 (0)