Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions packages/html-reporter/src/testResultView.css
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,10 @@
}
}

.step-filter {
margin-bottom: 8px;
}

@media only screen and (max-width: 600px) {
.test-result {
padding: 0 !important;
Expand Down
24 changes: 20 additions & 4 deletions packages/html-reporter/src/testResultView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,9 @@ export const TestResultView: React.FC<{
return { screenshots: [...screenshots], videos, traces, otherAttachments, diffs, errors, otherAttachmentAnchors, screenshotAnchors, errorContext };
}, [result]);

const [stepFilterText, setStepFilterText] = React.useState('');
React.useEffect(() => setStepFilterText(''), [result]);

const prompt = useAsyncMemo(async () => {
if (report.json().options?.noCopyPrompt)
return undefined;
Expand Down Expand Up @@ -130,7 +133,11 @@ export const TestResultView: React.FC<{
})}
</AutoChip>}
{!!result.steps.length && <AutoChip header='Test Steps'>
{result.steps.map((step, i) => <StepTreeItem key={`step-${i}`} step={step} result={result} test={test} depth={0}/>)}
<form className='subnav-search step-filter' onSubmit={e => e.preventDefault()}>
{icons.search()}
<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)} />
</form>
{result.steps.map((step, i) => <StepTreeItem key={`step-${i}`} step={step} result={result} test={test} depth={0} filterText={stepFilterText}/>)}
</AutoChip>}

{diffs.map((diff, index) =>
Expand Down Expand Up @@ -197,13 +204,22 @@ function pickDiffForError(error: string, diffs: ImageDiff[]): ImageDiff | undefi
return diffs.find(diff => error.includes(diff.name));
}

function stepMatchesFilter(step: TestStep, filterText: string): boolean {
if (step.title.toLowerCase().includes(filterText.toLowerCase()))
return true;
return step.steps.some(s => stepMatchesFilter(s, filterText));
}

const StepTreeItem: React.FC<{
test: TestCase;
result: TestResult;
step: TestStep;
depth: number,
}> = ({ test, step, result, depth }) => {
filterText?: string,
}> = ({ test, step, result, depth, filterText }) => {
const searchParams = useSearchParams();
if (filterText && !stepMatchesFilter(step, filterText))
return null;
return <TreeItem title={<div aria-label={step.title} className='step-title-container'>
{statusIcon(step.error || step.duration === -1 ? 'failed' : (step.skipped ? 'skipped' : 'passed'))}
<span className='step-title-text'>
Expand All @@ -222,9 +238,9 @@ const StepTreeItem: React.FC<{
<span className='step-duration'>{msToString(step.duration)}</span>
</div>} loadChildren={step.steps.length || step.snippet ? () => {
const snippet = step.snippet ? [<CodeSnippet testId='test-snippet' key='line' code={step.snippet} />] : [];
const steps = step.steps.map((s, i) => <StepTreeItem key={i} step={s} depth={depth + 1} result={result} test={test} />);
const steps = step.steps.map((s, i) => <StepTreeItem key={i} step={s} depth={depth + 1} result={result} test={test} filterText={filterText} />);
return snippet.concat(steps);
} : undefined} depth={depth}/>;
} : undefined} depth={depth} expandByDefault={!!filterText}/>;
};

type WorkerLists = Map<number, { tests: TestCaseSummary[], runs: number[] }>;
Expand Down
3 changes: 3 additions & 0 deletions packages/html-reporter/src/treeItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ export const TreeItem: React.FunctionComponent<{
flash?: boolean
}> = ({ title, loadChildren, onClick, expandByDefault, depth, style, flash }) => {
const [expanded, setExpanded] = React.useState(expandByDefault || false);
React.useEffect(() => {
setExpanded(expandByDefault || false);
}, [expandByDefault]);
return <div role='treeitem' className={clsx('tree-item', flash && 'yellow-flash')} style={style}>
<div className='tree-item-title' style={{ paddingLeft: depth * 22 + 4 }} onClick={() => { onClick?.(); setExpanded(!expanded); }} >
{loadChildren && !!expanded && icons.downArrow()}
Expand Down
41 changes: 41 additions & 0 deletions tests/playwright-test/reporter-html.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -822,6 +822,47 @@ for (const useIntermediateMergeReport of [true, false] as const) {
]);
});

test('should filter steps', async ({ runInlineTest, page, showReport }) => {
const result = await runInlineTest({
'a.test.js': `
import { test, expect } from '@playwright/test';
test('has steps', async ({}) => {
await test.step('click button', async () => {});
await test.step('fill form', async () => {
await test.step('fill username', async () => {});
await test.step('fill password', async () => {});
});
await test.step('submit form', async () => {});
});
`,
}, { reporter: 'dot,html' }, { PLAYWRIGHT_HTML_OPEN: 'never' });
expect(result.exitCode).toBe(0);
expect(result.passed).toBe(1);

await showReport();
await page.getByRole('link', { name: 'has steps' }).click();

const filterInput = page.getByLabel('Filter steps');
await expect(filterInput).toBeVisible();

// filter matching a subset of steps
await filterInput.fill('fill');
await expect(page.locator('.tree-item-title', { hasText: 'fill form' })).toBeVisible();
await expect(page.locator('.tree-item-title', { hasText: 'click button' })).toBeHidden();
await expect(page.locator('.tree-item-title', { hasText: 'submit form' })).toBeHidden();
// matching parent is auto-expanded to show matching children (like trace viewer)
await expect(page.locator('.tree-item-title', { hasText: 'fill username' })).toBeVisible();
await expect(page.locator('.tree-item-title', { hasText: 'fill password' })).toBeVisible();

// clear filter restores all steps collapsed
await filterInput.clear();
await expect(page.locator('.tree-item-title', { hasText: 'click button' })).toBeVisible();
await expect(page.locator('.tree-item-title', { hasText: 'submit form' })).toBeVisible();
// children of fill form are collapsed again after clearing the filter
await expect(page.locator('.tree-item-title', { hasText: 'fill username' })).toBeHidden();
await expect(page.locator('.tree-item-title', { hasText: 'fill password' })).toBeHidden();
});

test('should show step snippets from non-root', async ({ runInlineTest, page, showReport }) => {
const result = await runInlineTest({
'playwright.config.js': `
Expand Down
Loading