Skip to content

Commit

Permalink
feat(watch): batch watch runs (#551)
Browse files Browse the repository at this point in the history
  • Loading branch information
Skn0tt authored Nov 12, 2024
1 parent 3f51ee0 commit 6e26030
Show file tree
Hide file tree
Showing 2 changed files with 102 additions and 3 deletions.
34 changes: 31 additions & 3 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,8 @@ export class Extension implements RunHooks {
private _debugProfile: vscodeTypes.TestRunProfile;
private _playwrightTestLog: string[] = [];
private _commandQueue = Promise.resolve();
private _watchFilesBatch?: vscodeTypes.TestItem[];
private _watchItemsBatch?: vscodeTypes.TestItem[];
overridePlaywrightVersion: number | null = null;

constructor(vscode: vscodeTypes.VSCode, context: vscodeTypes.ExtensionContext) {
Expand Down Expand Up @@ -338,10 +340,36 @@ export class Extension implements RunHooks {
}
}

private async _queueTestRun(include: readonly vscodeTypes.TestItem[] | undefined, mode: 'run' | 'debug' | 'watch') {
private async _queueTestRun(include: readonly vscodeTypes.TestItem[] | undefined, mode: 'run' | 'debug') {
await this._queueCommand(() => this._runTests(include, mode), undefined);
}

private async _queueWatchRun(include: readonly vscodeTypes.TestItem[], type: 'files' | 'items') {
const batch = type === 'files' ? this._watchFilesBatch : this._watchItemsBatch;
if (batch) {
batch.push(...include); // `narrowDownLocations` dedupes before sending to the testserver, no need to dedupe here
return;
}

if (type === 'files')
this._watchFilesBatch = [...include];
else
this._watchItemsBatch = [...include];

await this._queueCommand(() => {
const items = type === 'files' ? this._watchFilesBatch : this._watchItemsBatch;
if (typeof items === 'undefined')
throw new Error(`_watchRunBatches['${type}'] is undefined, expected array`);

if (type === 'files')
this._watchFilesBatch = undefined;
else
this._watchItemsBatch = undefined;

return this._runTests(items, 'watch');
}, undefined);
}

private async _queueGlobalHooks(type: 'setup' | 'teardown'): Promise<reporterTypes.FullResult['status']> {
return await this._queueCommand(() => this._runGlobalHooks(type), 'failed');
}
Expand Down Expand Up @@ -573,10 +601,10 @@ export class Extension implements RunHooks {
// Run either locations or test ids to always be compatible with the test server (it can run either or).
if (files.length) {
const fileItems = files.map(f => this._testTree.testItemForFile(f)).filter(Boolean) as vscodeTypes.TestItem[];
await this._queueTestRun(fileItems, 'watch');
await this._queueWatchRun(fileItems, 'files');
}
if (testItems.length)
await this._queueTestRun(testItems, 'watch');
await this._queueWatchRun(testItems, 'items');
}

private async _updateVisibleEditorItems() {
Expand Down
71 changes: 71 additions & 0 deletions tests/watch.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import { TestRun } from './mock/vscode';
import { enableConfigs, enableProjects, escapedPathSep, expect, selectConfig, test } from './utils';
import path from 'path';
import { writeFile } from 'node:fs/promises';

test.skip(({ overridePlaywrightVersion }) => !!overridePlaywrightVersion);

Expand Down Expand Up @@ -409,6 +410,76 @@ test('should watch two tests in a file', async ({ activate }) => {
]);
});

test('should batch watched tests, not queue', async ({ activate }, testInfo) => {
if (process.platform === 'win32')
test.slow();

const semaphore = testInfo.outputPath('semaphore.txt');
const { testController, workspaceFolder } = await activate({
'playwright.config.js': `module.exports = { testDir: 'tests' }`,
'tests/watched.spec.ts': `
import { test } from '@playwright/test';
test('foo', async () => {
console.log('watched content #1');
});
`,
'tests/long-test.spec.ts': `
import { test } from '@playwright/test';
import { existsSync } from 'node:fs';
import { setTimeout } from 'node:timers/promises';
test('long test', async () => {
console.log('long test started');
while (!existsSync('${semaphore}'))
await setTimeout(10);
});
`,
});

await testController.expandTestItems(/.*/);
await testController.watch(testController.findTestItems(/watched/));

// start blocking run
const longTestRun = await new Promise<TestRun>(f => {
testController.onDidCreateTestRun(f);
testController.run(testController.findTestItems(/long-test/));
});
await expect.poll(() => longTestRun.renderOutput()).toContain('long test started');

// fill up queue
const queuedTestRuns: TestRun[] = [];
testController.onDidCreateTestRun(r => queuedTestRuns.push(r));
await workspaceFolder.changeFile('tests/watched.spec.ts', `
import { test } from '@playwright/test';
test('foo', async () => {
console.log('watched content #2');
});
`);
await workspaceFolder.changeFile('tests/watched.spec.ts', `
import { test } from '@playwright/test';
test('foo', async () => {
console.log('watched content #3');
});
`);
await workspaceFolder.changeFile('tests/watched.spec.ts', `
import { test } from '@playwright/test';
test('foo', async () => {
console.log('watched content #4');
});
`);

// end blocking run to start queued runs
await new Promise(async f => {
longTestRun.onDidEnd(f);
await writeFile(semaphore, '');
});

// wait for another run to be done, so we know the queue is empty
await testController.run(testController.findTestItems(/watched/));

// one batched run for all changes plus the one above
expect(queuedTestRuns.length).toBe(2);
});

test('should only watch a test from the enabled project when multiple projects share the same test directory', async ({ activate }) => {
const { vscode, testController, workspaceFolder } = await activate({
'playwright-1.config.js': `module.exports = { testDir: 'tests', projects: [{ name: 'project-from-config1' }] }`,
Expand Down

0 comments on commit 6e26030

Please sign in to comment.