Skip to content

Commit 3f33e7e

Browse files
authored
feat(ui-mode): Add filter option to only show changed files (microsoft#39500)
1 parent 1ad1f55 commit 3f33e7e

7 files changed

Lines changed: 97 additions & 54 deletions

File tree

packages/playwright/src/isomorphic/testServerInterface.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ export interface TestServerInterface {
8383
locations?: string[];
8484
grep?: string;
8585
grepInvert?: string;
86+
onlyChanged?: string;
8687
}): Promise<{
8788
report: ReportEntry[],
8889
status: reporterTypes.FullResult['status']

packages/playwright/src/runner/testRunner.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ export type ListTestsParams = {
5656
locations?: string[];
5757
grep?: string;
5858
grepInvert?: string;
59+
onlyChanged?: string;
5960
};
6061

6162
export type RunTestsParams = {
@@ -271,6 +272,7 @@ export class TestRunner extends EventEmitter<TestRunnerEventMap> {
271272
config.cliGrep = params.grep;
272273
config.cliGrepInvert = params.grepInvert;
273274
config.cliProjectFilter = params.projects?.length ? params.projects : undefined;
275+
config.cliOnlyChanged = params.onlyChanged;
274276
config.cliListOnly = true;
275277

276278
const status = await runTasks(new TestRun(config, reporter), [

packages/trace-viewer/src/ui/uiModeFiltersView.tsx

Lines changed: 44 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,11 @@ export const FiltersView: React.FC<{
2929
setStatusFilters: (filters: Map<string, boolean>) => void;
3030
projectFilters: Map<string, boolean>;
3131
setProjectFilters: (filters: Map<string, boolean>) => void;
32+
onlyChanged: boolean;
33+
setOnlyChanged: (value: boolean) => void;
3234
testModel: TeleSuiteUpdaterTestModel | undefined,
3335
runTests: () => void;
34-
}> = ({ filterText, setFilterText, statusFilters, setStatusFilters, projectFilters, setProjectFilters, testModel, runTests }) => {
36+
}> = ({ filterText, setFilterText, statusFilters, setStatusFilters, projectFilters, setProjectFilters, onlyChanged, setOnlyChanged, testModel, runTests }) => {
3537
const [expanded, setExpanded] = React.useState(false);
3638
const inputRef = React.useRef<HTMLInputElement>(null);
3739
React.useEffect(() => {
@@ -53,42 +55,51 @@ export const FiltersView: React.FC<{
5355
runTests();
5456
}} />}>
5557
</Expandable>
56-
<div className='filter-summary' title={'Status: ' + statusLine + '\nProjects: ' + projectsLine} onClick={() => setExpanded(!expanded)}>
58+
<div className='filter-summary' title={'Status: ' + statusLine + '\nProjects: ' + projectsLine + (onlyChanged ? '\nOnly changed' : '')} onClick={() => setExpanded(!expanded)}>
5759
<span className='filter-label'>Status:</span> {statusLine}
5860
<span className='filter-label'>Projects:</span> {projectsLine}
61+
{onlyChanged && <><span className='filter-label'>Only changed</span></>}
5962
</div>
60-
{expanded && <div className='hbox' style={{ marginLeft: 14, maxHeight: 200, overflowY: 'auto' }}>
61-
<div className='filter-list' role='list' data-testid='status-filters'>
62-
{[...statusFilters.entries()].map(([status, value]) => {
63-
return <div className='filter-entry' key={status} role='listitem'>
64-
<label>
65-
<input type='checkbox' checked={value} onChange={() => {
66-
const copy = new Map(statusFilters);
67-
copy.set(status, !copy.get(status));
68-
setStatusFilters(copy);
69-
}}/>
70-
<div>{status}</div>
71-
</label>
72-
</div>;
73-
})}
63+
{expanded && <>
64+
<div className='hbox' style={{ marginLeft: 14, maxHeight: 200, overflowY: 'auto' }}>
65+
<div className='filter-list' role='list' data-testid='status-filters'>
66+
{[...statusFilters.entries()].map(([status, value]) => {
67+
return <div className='filter-entry' key={status} role='listitem'>
68+
<label>
69+
<input type='checkbox' checked={value} onChange={() => {
70+
const copy = new Map(statusFilters);
71+
copy.set(status, !copy.get(status));
72+
setStatusFilters(copy);
73+
}}/>
74+
<div>{status}</div>
75+
</label>
76+
</div>;
77+
})}
78+
</div>
79+
<div className='filter-list' role='list' data-testid='project-filters'>
80+
{[...projectFilters.entries()].map(([projectName, value]) => {
81+
return <div className='filter-entry' key={projectName} role='listitem'>
82+
<label>
83+
<input type='checkbox' checked={value} onChange={() => {
84+
const copy = new Map(projectFilters);
85+
copy.set(projectName, !copy.get(projectName));
86+
setProjectFilters(copy);
87+
const configFile = testModel?.config?.configFile;
88+
if (configFile)
89+
settings.setObject(configFile + ':projects', [...copy.entries()].filter(([_, v]) => v).map(([k]) => k));
90+
}}/>
91+
<div>{projectName || 'untitled'}</div>
92+
</label>
93+
</div>;
94+
})}
95+
</div>
7496
</div>
75-
<div className='filter-list' role='list' data-testid='project-filters'>
76-
{[...projectFilters.entries()].map(([projectName, value]) => {
77-
return <div className='filter-entry' key={projectName} role='listitem'>
78-
<label>
79-
<input type='checkbox' checked={value} onChange={() => {
80-
const copy = new Map(projectFilters);
81-
copy.set(projectName, !copy.get(projectName));
82-
setProjectFilters(copy);
83-
const configFile = testModel?.config?.configFile;
84-
if (configFile)
85-
settings.setObject(configFile + ':projects', [...copy.entries()].filter(([_, v]) => v).map(([k]) => k));
86-
}}/>
87-
<div>{projectName || 'untitled'}</div>
88-
</label>
89-
</div>;
90-
})}
97+
<div className='filter-entry' style={{ marginLeft: 24 }}>
98+
<label>
99+
<input type='checkbox' checked={onlyChanged} onChange={() => setOnlyChanged(!onlyChanged)}/>
100+
<div>Show only changed files</div>
101+
</label>
91102
</div>
92-
</div>}
103+
</>}
93104
</div>;
94105
};

packages/trace-viewer/src/ui/uiModeView.tsx

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ export const UIModeView: React.FC<{}> = ({
109109

110110
const [singleWorker, setSingleWorker] = useSetting<boolean>('single-worker', false);
111111
const [updateSnapshots, setUpdateSnapshots] = useSetting<reporterTypes.FullConfig['updateSnapshots']>('updateSnapshots', 'missing');
112+
const [onlyChanged, setOnlyChanged] = useSetting<boolean>('only-changed', false);
112113
const [mergeFiles] = useSetting('mergeFiles', false);
113114

114115
const inputRef = React.useRef<HTMLInputElement>(null);
@@ -197,7 +198,7 @@ export const UIModeView: React.FC<{}> = ({
197198
if (status !== 'passed')
198199
return;
199200

200-
const result = await testServerConnection.listTests({ projects: queryParams.projects, locations: queryParams.args, grep: queryParams.grep, grepInvert: queryParams.grepInvert });
201+
const result = await testServerConnection.listTests({ projects: queryParams.projects, locations: queryParams.args, grep: queryParams.grep, grepInvert: queryParams.grepInvert, onlyChanged: onlyChanged ? 'HEAD' : undefined });
201202
teleSuiteUpdater.processListReport(result.report);
202203

203204
testServerConnection.onReport(params => {
@@ -213,7 +214,7 @@ export const UIModeView: React.FC<{}> = ({
213214
return () => {
214215
clearTimeout(throttleTimer);
215216
};
216-
}, [testServerConnection]);
217+
}, [onlyChanged, testServerConnection]);
217218

218219
// Update project filter default values.
219220
React.useEffect(() => {
@@ -321,7 +322,7 @@ export const UIModeView: React.FC<{}> = ({
321322
commandQueue.current = commandQueue.current.then(async () => {
322323
setIsLoading(true);
323324
try {
324-
const result = await testServerConnection.listTests({ projects: queryParams.projects, locations: queryParams.args, grep: queryParams.grep, grepInvert: queryParams.grepInvert });
325+
const result = await testServerConnection.listTests({ projects: queryParams.projects, locations: queryParams.args, grep: queryParams.grep, grepInvert: queryParams.grepInvert, onlyChanged: onlyChanged ? 'HEAD' : undefined });
325326
teleSuiteUpdater.processListReport(result.report);
326327
} catch (e) {
327328
// eslint-disable-next-line no-console
@@ -375,7 +376,7 @@ export const UIModeView: React.FC<{}> = ({
375376
runTests('queue-if-busy', { locations, testIds });
376377
});
377378
return () => disposable.dispose();
378-
}, [runTests, testServerConnection, watchAll, watchedTreeIds, teleSuiteUpdater, projectFilters, mergeFiles]);
379+
}, [runTests, testServerConnection, watchAll, watchedTreeIds, teleSuiteUpdater, projectFilters, mergeFiles, onlyChanged]);
379380

380381
// Shortcuts.
381382
React.useEffect(() => {
@@ -480,6 +481,8 @@ export const UIModeView: React.FC<{}> = ({
480481
setStatusFilters={setStatusFilters}
481482
projectFilters={projectFilters}
482483
setProjectFilters={setProjectFilters}
484+
onlyChanged={onlyChanged}
485+
setOnlyChanged={setOnlyChanged}
483486
testModel={testModel}
484487
runTests={runVisibleTests} />
485488
<Toolbar className='section-toolbar' noMinHeight={true}>

tests/playwright-test/only-changed.spec.ts

Lines changed: 1 addition & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -14,23 +14,7 @@
1414
* limitations under the License.
1515
*/
1616

17-
import { test as baseTest, expect, playwrightCtConfigText } from './playwright-test-fixtures';
18-
import { execSync } from 'node:child_process';
19-
20-
const test = baseTest.extend<{ git(command: string): void }>({
21-
git: async ({}, use, testInfo) => {
22-
const baseDir = testInfo.outputPath();
23-
24-
const git = (command: string) => execSync(`git ${command}`, { cwd: baseDir, stdio: process.env.PWTEST_DEBUG ? 'inherit' : 'ignore' });
25-
26-
git(`init --initial-branch=main`);
27-
git(`config --local user.name "Robert Botman"`);
28-
git(`config --local user.email "botty@mcbotface.com"`);
29-
git(`config --local core.autocrlf false`);
30-
31-
await use((command: string) => git(command));
32-
},
33-
});
17+
import { test, expect, playwrightCtConfigText } from './playwright-test-fixtures';
3418

3519
test.slow();
3620

tests/playwright-test/playwright-test-fixtures.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import type { JSONReport, JSONReportSpec, JSONReportSuite, JSONReportTest, JSONR
1818
import * as fs from 'fs';
1919
import * as os from 'os';
2020
import * as path from 'path';
21+
import { execSync } from 'node:child_process';
2122
import { PNG } from 'playwright-core/lib/utilsBundle';
2223
import type { CommonFixtures, CommonWorkerFixtures, TestChildProcess } from '../config/commonFixtures';
2324
import { commonFixtures } from '../config/commonFixtures';
@@ -252,6 +253,7 @@ export type RunOptions = {
252253
type Fixtures = {
253254
writeFiles: (files: Files) => Promise<string>;
254255
deleteFile: (file: string) => Promise<void>;
256+
git: (command: string) => void;
255257
runInlineTest: (files: Files, params?: Params, env?: NodeJS.ProcessEnv, options?: RunOptions) => Promise<RunResult>;
256258
runCLICommand: (files: Files, command: string, args?: string[]) => Promise<{ stdout: string, stderr: string, exitCode: number }>;
257259
startCLICommand: (files: Files, command: string, args?: string[], options?: RunOptions, env?: NodeJS.ProcessEnv) => Promise<TestChildProcess>;
@@ -278,6 +280,16 @@ export const test = base
278280
});
279281
},
280282

283+
git: async ({}, use, testInfo) => {
284+
const baseDir = testInfo.outputPath();
285+
const git = (command: string) => execSync(`git ${command}`, { cwd: baseDir, stdio: process.env.PWTEST_DEBUG ? 'inherit' : 'ignore' });
286+
git(`init --initial-branch=main`);
287+
git(`config --local user.name "Robert Botman"`);
288+
git(`config --local user.email "botty@mcbotface.com"`);
289+
git(`config --local core.autocrlf false`);
290+
await use((command: string) => git(command));
291+
},
292+
281293
runInlineTest: async ({ childProcess, mergeReports, useIntermediateMergeReport }, use, testInfo: TestInfo) => {
282294
const cacheDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'playwright-test-cache-'));
283295
await use(async (files: Files, params: Params = {}, env: NodeJS.ProcessEnv = {}, options: RunOptions = {}) => {

tests/playwright-test/ui-mode-test-filters.spec.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,3 +245,33 @@ test('should not show tests filtered with --grep-invert', async ({ runUITest })
245245
await expect.poll(dumpTestTree(page)).toContain('passes');
246246
await expect.poll(dumpTestTree(page)).not.toContain('fails');
247247
});
248+
249+
test('should filter by only changed files', async ({ runUITest, git, writeFiles }) => {
250+
const committedFiles = {
251+
'a.test.ts': `
252+
import { test, expect } from '@playwright/test';
253+
test('committed test', () => {});
254+
`,
255+
};
256+
257+
await writeFiles(committedFiles);
258+
git(`add .`);
259+
git(`commit -m init`);
260+
261+
const { page } = await runUITest({
262+
...committedFiles,
263+
'b.test.ts': `
264+
import { test, expect } from '@playwright/test';
265+
test('new untracked test', () => {});
266+
`,
267+
});
268+
269+
await expect.poll(dumpTestTree(page)).toContain('a.test.ts');
270+
await expect.poll(dumpTestTree(page)).toContain('b.test.ts');
271+
272+
await page.getByText('Status:').click();
273+
await page.getByLabel('Show only changed files').setChecked(true);
274+
275+
await expect.poll(dumpTestTree(page)).toContain('b.test.ts');
276+
await expect.poll(dumpTestTree(page)).not.toContain('a.test.ts');
277+
});

0 commit comments

Comments
 (0)