Skip to content

Commit af1cbea

Browse files
Add --video and --autostart (#271849)
This enables: * Playwright video recording * Starting the Code OSS on server start (for clients like Copilot CLI that don't react to tool list changes)
1 parent e5243d2 commit af1cbea

File tree

9 files changed

+89
-84
lines changed

9 files changed

+89
-84
lines changed

test/automation/src/code.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ export interface LaunchOptions {
2424
readonly logger: Logger;
2525
logsPath: string;
2626
crashesPath: string;
27+
readonly videosPath?: string;
2728
verbose?: boolean;
2829
useInMemorySecretStorage?: boolean;
2930
readonly extraArgs?: string[];

test/automation/src/playwrightBrowser.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,13 @@ async function launchBrowser(options: LaunchOptions, endpoint: string) {
105105

106106
browser.on('disconnected', () => logger.log(`Playwright: browser disconnected`));
107107

108-
const context = await measureAndLog(() => browser.newContext(), 'browser.newContext', logger);
108+
const context = await measureAndLog(
109+
() => browser.newContext({
110+
recordVideo: options.videosPath ? { dir: options.videosPath } : undefined
111+
}),
112+
'browser.newContext',
113+
logger
114+
);
109115

110116
if (tracing) {
111117
try {

test/automation/src/playwrightElectron.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ async function launchElectron(configuration: IElectronConfiguration, options: La
3333
const electron = await measureAndLog(() => playwrightImpl._electron.launch({
3434
executablePath: configuration.electronPath,
3535
args: configuration.args,
36+
recordVideo: options.videosPath ? { dir: options.videosPath } : undefined,
3637
env: configuration.env as { [key: string]: string },
3738
timeout: 0
3839
}), 'playwright-electron#launch', logger);

test/mcp/scripts/start-stdio.sh

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
2+
SCRIPT_DIR="$(dirname -- "$( readlink -f -- "$0"; )")"
3+
# Go to mcp server project root
4+
cd "$SCRIPT_DIR/.."
5+
6+
# Start mcp
7+
npm run start-stdio -- --video --autostart

test/mcp/src/application.ts

Lines changed: 8 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -10,71 +10,12 @@ import * as fs from 'fs';
1010
import * as os from 'os';
1111
import * as vscodetest from '@vscode/test-electron';
1212
import { createApp, retry } from './utils';
13-
import * as minimist from 'minimist';
13+
import { opts } from './options';
1414

1515
const rootPath = path.join(__dirname, '..', '..', '..');
16-
17-
const [, , ...args] = process.argv;
18-
const opts = minimist(args, {
19-
string: [
20-
'browser',
21-
'build',
22-
'stable-build',
23-
'wait-time',
24-
'test-repo',
25-
'electronArgs'
26-
],
27-
boolean: [
28-
'verbose',
29-
'remote',
30-
'web',
31-
'headless',
32-
'tracing'
33-
],
34-
default: {
35-
verbose: false
36-
}
37-
}) as {
38-
verbose?: boolean;
39-
remote?: boolean;
40-
headless?: boolean;
41-
web?: boolean;
42-
tracing?: boolean;
43-
build?: string;
44-
'stable-build'?: string;
45-
browser?: 'chromium' | 'webkit' | 'firefox' | 'chromium-msedge' | 'chromium-chrome' | undefined;
46-
electronArgs?: string;
47-
};
48-
49-
const logsRootPath = (() => {
50-
const logsParentPath = path.join(rootPath, '.build', 'logs');
51-
52-
let logsName: string;
53-
if (opts.web) {
54-
logsName = 'smoke-tests-browser';
55-
} else if (opts.remote) {
56-
logsName = 'smoke-tests-remote';
57-
} else {
58-
logsName = 'smoke-tests-electron';
59-
}
60-
61-
return path.join(logsParentPath, logsName);
62-
})();
63-
64-
const crashesRootPath = (() => {
65-
const crashesParentPath = path.join(rootPath, '.build', 'crashes');
66-
67-
let crashesName: string;
68-
if (opts.web) {
69-
crashesName = 'smoke-tests-browser';
70-
} else if (opts.remote) {
71-
crashesName = 'smoke-tests-remote';
72-
} else {
73-
crashesName = 'smoke-tests-electron';
74-
}
75-
76-
return path.join(crashesParentPath, crashesName);
77-
})();
16+
const logsRootPath = path.join(rootPath, '.build', 'vscode-playwright-mcp', 'logs');
17+
const crashesRootPath = path.join(rootPath, '.build', 'vscode-playwright-mcp', 'crashes');
18+
const videoRootPath = path.join(rootPath, '.build', 'vscode-playwright-mcp', 'videos');
7819

7920
const logger = createLogger();
8021

@@ -291,7 +232,7 @@ async function setup(): Promise<void> {
291232
logger.log('Smoketest setup done!\n');
292233
}
293234

294-
export async function getApplication() {
235+
export async function getApplication({ recordVideo }: { recordVideo?: boolean } = {}) {
295236
const testCodePath = getDevElectronPath();
296237
const electronPath = testCodePath;
297238
if (!fs.existsSync(electronPath || '')) {
@@ -315,6 +256,7 @@ export async function getApplication() {
315256
logger,
316257
logsPath: path.join(logsRootPath, 'suite_unknown'),
317258
crashesPath: path.join(crashesRootPath, 'suite_unknown'),
259+
videosPath: (recordVideo || opts.video) ? videoRootPath : undefined,
318260
verbose: opts.verbose,
319261
remote: opts.remote,
320262
web: opts.web,
@@ -350,12 +292,12 @@ export class ApplicationService {
350292
return this._application;
351293
}
352294

353-
async getOrCreateApplication(): Promise<Application> {
295+
async getOrCreateApplication({ recordVideo }: { recordVideo?: boolean } = {}): Promise<Application> {
354296
if (this._closing) {
355297
await this._closing;
356298
}
357299
if (!this._application) {
358-
this._application = await getApplication();
300+
this._application = await getApplication({ recordVideo });
359301
this._application.code.driver.currentPage.on('close', () => {
360302
this._closing = (async () => {
361303
if (this._application) {

test/mcp/src/automation.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
77
import { ApplicationService } from './application';
88
import { applyAllTools } from './automationTools/index.js';
99
import type { Server } from '@modelcontextprotocol/sdk/server/index.js';
10+
import { z } from 'zod';
1011

1112
export async function getServer(appService: ApplicationService): Promise<Server> {
1213
const server = new McpServer({
@@ -18,9 +19,11 @@ export async function getServer(appService: ApplicationService): Promise<Server>
1819
server.tool(
1920
'vscode_automation_start',
2021
'Start VS Code Build',
21-
{},
22-
async () => {
23-
const app = await appService.getOrCreateApplication();
22+
{
23+
recordVideo: z.boolean().optional()
24+
},
25+
async ({ recordVideo }) => {
26+
const app = await appService.getOrCreateApplication({ recordVideo });
2427
return {
2528
content: [{
2629
type: 'text' as const,

test/mcp/src/automationTools/core.ts

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -32,20 +32,20 @@ export function applyCoreTools(server: McpServer, appService: ApplicationService
3232
// }
3333
// );
3434

35-
// I don't think Playwright needs this
36-
// server.tool(
37-
// 'vscode_automation_stop',
38-
// 'Stop the VS Code application',
39-
// async () => {
40-
// await app.stop();
41-
// return {
42-
// content: [{
43-
// type: 'text' as const,
44-
// text: 'VS Code stopped successfully'
45-
// }]
46-
// };
47-
// }
48-
// );
35+
tools.push(server.tool(
36+
'vscode_automation_stop',
37+
'Stop the VS Code application',
38+
async () => {
39+
const app = await appService.getOrCreateApplication();
40+
await app.stop();
41+
return {
42+
content: [{
43+
type: 'text' as const,
44+
text: 'VS Code stopped successfully'
45+
}]
46+
};
47+
}
48+
));
4949

5050
// This doesn't seem particularly useful
5151
// server.tool(

test/mcp/src/multiplex.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { ApplicationService } from './application';
1111
import { createInMemoryTransportPair } from './inMemoryTransport';
1212
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
1313
import { Application } from '../../automation';
14+
import { opts } from './options';
1415

1516
interface SubServerConfig {
1617
subServer: Client;
@@ -81,6 +82,9 @@ export async function getServer(): Promise<Server> {
8182
}
8283
});
8384

85+
if (opts.autostart) {
86+
await appService.getOrCreateApplication();
87+
}
8488
return multiplexServer.server;
8589
}
8690

test/mcp/src/options.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
import * as minimist from 'minimist';
6+
7+
const [, , ...args] = process.argv;
8+
export const opts = minimist(args, {
9+
string: [
10+
'browser',
11+
'build',
12+
'stable-build',
13+
'wait-time',
14+
'test-repo',
15+
'electronArgs'
16+
],
17+
boolean: [
18+
'verbose',
19+
'remote',
20+
'web',
21+
'headless',
22+
'tracing',
23+
'video',
24+
'autostart'
25+
],
26+
default: {
27+
verbose: false
28+
}
29+
}) as {
30+
verbose?: boolean;
31+
remote?: boolean;
32+
headless?: boolean;
33+
web?: boolean;
34+
tracing?: boolean;
35+
build?: string;
36+
'stable-build'?: string;
37+
browser?: 'chromium' | 'webkit' | 'firefox' | 'chromium-msedge' | 'chromium-chrome' | undefined;
38+
electronArgs?: string;
39+
video?: boolean;
40+
autostart?: boolean;
41+
};

0 commit comments

Comments
 (0)