mirror of
https://github.com/microsoft/vscode.git
synced 2026-04-02 00:09:30 +01:00
Add mock web extension for E2E testing
New extension at extensions/sessions-e2e-mock/ provides: - Mock GitHub auth provider (fake token, skips sign-in) - Mock chat participant (canned responses based on input keywords) - Mock file system for github-remote-file:// (in-memory files) Server loads the extension when --mock flag is passed. The generate and test runners both use --mock automatically. New npm scripts: - serve:mock — opens Sessions in browser with mocks loaded Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -15,7 +15,7 @@ const APP_ROOT = path.join(__dirname, '..');
|
||||
|
||||
async function main() {
|
||||
const args = minimist(process.argv.slice(2), {
|
||||
boolean: ['help', 'no-open', 'skip-welcome'],
|
||||
boolean: ['help', 'no-open', 'skip-welcome', 'mock'],
|
||||
string: ['host', 'port'],
|
||||
});
|
||||
|
||||
@@ -24,7 +24,9 @@ async function main() {
|
||||
'./scripts/code-sessions-web.sh [options]\n' +
|
||||
' --host <host> Host to bind to (default: localhost)\n' +
|
||||
' --port <port> Port to bind to (default: 8081)\n' +
|
||||
' --no-open Do not open browser automatically\n'
|
||||
' --no-open Do not open browser automatically\n' +
|
||||
' --skip-welcome Skip the sessions welcome overlay\n' +
|
||||
' --mock Load mock extension for E2E testing\n'
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -50,7 +52,7 @@ async function main() {
|
||||
// Serve the sessions workbench HTML at the root
|
||||
if (url.pathname === '/' || url.pathname === '/index.html') {
|
||||
res.writeHead(200, { 'Content-Type': 'text/html' });
|
||||
res.end(getSessionsHTML(HOST, PORT, cssModules));
|
||||
res.end(getSessionsHTML(HOST, PORT, cssModules, args['mock']));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -95,7 +97,7 @@ async function main() {
|
||||
process.on('SIGTERM', () => { server.close(); process.exit(0); });
|
||||
}
|
||||
|
||||
function getSessionsHTML(host, port, cssModules) {
|
||||
function getSessionsHTML(host, port, cssModules, useMock) {
|
||||
const baseUrl = `http://${host}:${port}`;
|
||||
const fileRoot = `${baseUrl}/out`;
|
||||
|
||||
@@ -112,6 +114,11 @@ function getSessionsHTML(host, port, cssModules) {
|
||||
}
|
||||
const importMapJson = JSON.stringify({ imports }, null, 2);
|
||||
|
||||
// When --mock is passed, load the E2E mock extension
|
||||
const additionalBuiltinExtensions = useMock
|
||||
? `additionalBuiltinExtensions: [{ scheme: 'http', authority: '${host}:${port}', path: '/src/vs/sessions/test/e2e/extensions/sessions-e2e-mock' }],`
|
||||
: '';
|
||||
|
||||
return `<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
@@ -137,6 +144,7 @@ ${importMapJson}
|
||||
nameLong: 'Sessions (Web)',
|
||||
enableTelemetry: false,
|
||||
},
|
||||
${additionalBuiltinExtensions}
|
||||
workspaceProvider: {
|
||||
workspace: undefined,
|
||||
open: async () => false,
|
||||
|
||||
@@ -90,10 +90,12 @@ function getSnapshot() {
|
||||
// Server management
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function startServer(port) {
|
||||
function startServer(port, { mock = false } = {}) {
|
||||
const args = ['--no-open', '--port', String(port)];
|
||||
if (mock) { args.push('--mock'); }
|
||||
const server = cp.spawn(process.execPath, [
|
||||
path.join(APP_ROOT, 'scripts', 'code-sessions-web.js'),
|
||||
'--no-open', '--port', String(port),
|
||||
...args,
|
||||
], {
|
||||
cwd: APP_ROOT,
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
|
||||
@@ -0,0 +1,303 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
// @ts-check
|
||||
|
||||
/// <reference types="vscode" />
|
||||
|
||||
/**
|
||||
* Mock extension for Sessions E2E testing.
|
||||
*
|
||||
* Provides fake implementations of:
|
||||
* - GitHub authentication (skips sign-in)
|
||||
* - Chat participant (returns canned responses)
|
||||
* - File system provider for github-remote-file:// (in-memory files)
|
||||
*/
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Mock data
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/** @type {Map<string, Uint8Array>} */
|
||||
const fileStore = new Map();
|
||||
|
||||
// Pre-populate a fake repo
|
||||
const MOCK_FILES = {
|
||||
'/src/index.ts': 'export function main() {\n\tconsole.log("Hello from mock repo");\n}\n',
|
||||
'/src/utils.ts': 'export function add(a: number, b: number): number {\n\treturn a + b;\n}\n',
|
||||
'/package.json': '{\n\t"name": "mock-repo",\n\t"version": "1.0.0"\n}\n',
|
||||
'/README.md': '# Mock Repository\n\nThis is a mock repository for E2E testing.\n',
|
||||
};
|
||||
|
||||
for (const [path, content] of Object.entries(MOCK_FILES)) {
|
||||
fileStore.set(path, new TextEncoder().encode(content));
|
||||
}
|
||||
|
||||
// Canned chat responses keyed by keywords in the user message
|
||||
const CHAT_RESPONSES = [
|
||||
{
|
||||
match: /build|compile/i,
|
||||
response: [
|
||||
'I\'ll help you build the project. Here are the changes:',
|
||||
'',
|
||||
'```typescript',
|
||||
'// src/build.ts',
|
||||
'import { main } from "./index";',
|
||||
'',
|
||||
'async function build() {',
|
||||
'\tconsole.log("Building...");',
|
||||
'\tmain();',
|
||||
'\tconsole.log("Build complete!");',
|
||||
'}',
|
||||
'',
|
||||
'build();',
|
||||
'```',
|
||||
'',
|
||||
'I\'ve created a new build script that imports and runs the main function.',
|
||||
].join('\n'),
|
||||
},
|
||||
{
|
||||
match: /fix|bug/i,
|
||||
response: [
|
||||
'I found the issue. Here\'s the fix:',
|
||||
'',
|
||||
'```diff',
|
||||
'- export function add(a: number, b: number): number {',
|
||||
'+ export function add(a: number, b: number): number {',
|
||||
'+ if (typeof a !== "number" || typeof b !== "number") {',
|
||||
'+ throw new TypeError("Both arguments must be numbers");',
|
||||
'+ }',
|
||||
' return a + b;',
|
||||
' }',
|
||||
'```',
|
||||
'',
|
||||
'Added input validation to prevent NaN results.',
|
||||
].join('\n'),
|
||||
},
|
||||
{
|
||||
match: /explain/i,
|
||||
response: [
|
||||
'This project has a simple structure:',
|
||||
'',
|
||||
'- **src/index.ts** — Main entry point with a `main()` function',
|
||||
'- **src/utils.ts** — Utility functions like `add()`',
|
||||
'- **package.json** — Project metadata',
|
||||
'',
|
||||
'The `main()` function logs a greeting to the console.',
|
||||
].join('\n'),
|
||||
},
|
||||
];
|
||||
|
||||
const DEFAULT_RESPONSE = [
|
||||
'I understand your request. Let me work on that.',
|
||||
'',
|
||||
'Here\'s what I\'d suggest:',
|
||||
'',
|
||||
'1. Review the current codebase structure',
|
||||
'2. Make the necessary changes',
|
||||
'3. Run the tests to verify',
|
||||
'',
|
||||
'Would you like me to proceed?',
|
||||
].join('\n');
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Activation
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @param {import('vscode').ExtensionContext} context
|
||||
*/
|
||||
function activate(context) {
|
||||
const vscode = require('vscode');
|
||||
|
||||
console.log('[sessions-e2e-mock] Activating mock extension');
|
||||
|
||||
// 1. Mock GitHub Authentication Provider
|
||||
context.subscriptions.push(registerMockAuth(vscode));
|
||||
|
||||
// 2. Mock Chat Participant
|
||||
context.subscriptions.push(registerMockChat(vscode));
|
||||
|
||||
// 3. Mock File System Provider
|
||||
context.subscriptions.push(registerMockFileSystem(vscode));
|
||||
|
||||
console.log('[sessions-e2e-mock] All mocks registered');
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Mock Authentication Provider
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @param {typeof import('vscode')} vscode
|
||||
* @returns {import('vscode').Disposable}
|
||||
*/
|
||||
function registerMockAuth(vscode) {
|
||||
const sessionChangeEmitter = new vscode.EventEmitter();
|
||||
|
||||
/** @type {import('vscode').AuthenticationSession} */
|
||||
const mockSession = {
|
||||
id: 'mock-session-1',
|
||||
accessToken: 'gho_mock_e2e_test_token_00000000000000000000',
|
||||
account: {
|
||||
id: 'e2e-test-user',
|
||||
label: 'E2E Test User',
|
||||
},
|
||||
scopes: ['read:user', 'repo', 'workflow'],
|
||||
};
|
||||
|
||||
/** @type {import('vscode').AuthenticationProvider} */
|
||||
const provider = {
|
||||
onDidChangeSessions: sessionChangeEmitter.event,
|
||||
async getSessions(_scopes, _options) {
|
||||
return [mockSession];
|
||||
},
|
||||
async createSession(_scopes, _options) {
|
||||
sessionChangeEmitter.fire({ added: [mockSession], removed: [], changed: [] });
|
||||
return mockSession;
|
||||
},
|
||||
async removeSession(_sessionId) {
|
||||
sessionChangeEmitter.fire({ added: [], removed: [mockSession], changed: [] });
|
||||
},
|
||||
};
|
||||
|
||||
console.log('[sessions-e2e-mock] Registering mock GitHub auth provider');
|
||||
return vscode.authentication.registerAuthenticationProvider('github', 'GitHub (Mock)', provider, {
|
||||
supportsMultipleAccounts: false,
|
||||
});
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Mock Chat Participant
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @param {typeof import('vscode')} vscode
|
||||
* @returns {import('vscode').Disposable}
|
||||
*/
|
||||
function registerMockChat(vscode) {
|
||||
const participant = vscode.chat.createChatParticipant('copilot', async (request, _context, response, _token) => {
|
||||
const userMessage = request.prompt || '';
|
||||
console.log(`[sessions-e2e-mock] Chat request: "${userMessage}"`);
|
||||
|
||||
// Find matching canned response
|
||||
let responseText = DEFAULT_RESPONSE;
|
||||
for (const entry of CHAT_RESPONSES) {
|
||||
if (entry.match.test(userMessage)) {
|
||||
responseText = entry.response;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Stream the response with a small delay to simulate typing
|
||||
const lines = responseText.split('\n');
|
||||
for (const line of lines) {
|
||||
response.markdown(line + '\n');
|
||||
}
|
||||
|
||||
return { metadata: { mock: true } };
|
||||
});
|
||||
|
||||
participant.iconPath = new vscode.ThemeIcon('copilot');
|
||||
|
||||
console.log('[sessions-e2e-mock] Registered mock chat participant "copilot"');
|
||||
return participant;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Mock File System Provider (github-remote-file scheme)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @param {typeof import('vscode')} vscode
|
||||
* @returns {import('vscode').Disposable}
|
||||
*/
|
||||
function registerMockFileSystem(vscode) {
|
||||
const fileChangeEmitter = new vscode.EventEmitter();
|
||||
|
||||
/** @type {import('vscode').FileSystemProvider} */
|
||||
const provider = {
|
||||
onDidChangeFile: fileChangeEmitter.event,
|
||||
|
||||
stat(uri) {
|
||||
const filePath = uri.path;
|
||||
if (fileStore.has(filePath)) {
|
||||
return {
|
||||
type: vscode.FileType.File,
|
||||
ctime: 0,
|
||||
mtime: Date.now(),
|
||||
size: fileStore.get(filePath).byteLength,
|
||||
};
|
||||
}
|
||||
// Check if it's a directory (any file starts with this path)
|
||||
const dirPrefix = filePath.endsWith('/') ? filePath : filePath + '/';
|
||||
const isDir = filePath === '/' || [...fileStore.keys()].some(k => k.startsWith(dirPrefix));
|
||||
if (isDir) {
|
||||
return { type: vscode.FileType.Directory, ctime: 0, mtime: Date.now(), size: 0 };
|
||||
}
|
||||
throw vscode.FileSystemError.FileNotFound(uri);
|
||||
},
|
||||
|
||||
readDirectory(uri) {
|
||||
const dirPath = uri.path.endsWith('/') ? uri.path : uri.path + '/';
|
||||
const entries = new Map();
|
||||
for (const filePath of fileStore.keys()) {
|
||||
if (!filePath.startsWith(dirPath)) { continue; }
|
||||
const relative = filePath.slice(dirPath.length);
|
||||
const parts = relative.split('/');
|
||||
if (parts.length === 1) {
|
||||
entries.set(parts[0], vscode.FileType.File);
|
||||
} else {
|
||||
entries.set(parts[0], vscode.FileType.Directory);
|
||||
}
|
||||
}
|
||||
return [...entries.entries()];
|
||||
},
|
||||
|
||||
readFile(uri) {
|
||||
const content = fileStore.get(uri.path);
|
||||
if (!content) { throw vscode.FileSystemError.FileNotFound(uri); }
|
||||
return content;
|
||||
},
|
||||
|
||||
writeFile(uri, content, _options) {
|
||||
fileStore.set(uri.path, content);
|
||||
fileChangeEmitter.fire([{ type: vscode.FileChangeType.Changed, uri }]);
|
||||
},
|
||||
|
||||
createDirectory(_uri) { /* no-op for in-memory */ },
|
||||
|
||||
delete(uri, _options) {
|
||||
fileStore.delete(uri.path);
|
||||
fileChangeEmitter.fire([{ type: vscode.FileChangeType.Deleted, uri }]);
|
||||
},
|
||||
|
||||
rename(oldUri, newUri, _options) {
|
||||
const content = fileStore.get(oldUri.path);
|
||||
if (!content) { throw vscode.FileSystemError.FileNotFound(oldUri); }
|
||||
fileStore.delete(oldUri.path);
|
||||
fileStore.set(newUri.path, content);
|
||||
fileChangeEmitter.fire([
|
||||
{ type: vscode.FileChangeType.Deleted, uri: oldUri },
|
||||
{ type: vscode.FileChangeType.Created, uri: newUri },
|
||||
]);
|
||||
},
|
||||
|
||||
watch(_uri, _options) {
|
||||
return { dispose() { } };
|
||||
},
|
||||
};
|
||||
|
||||
console.log('[sessions-e2e-mock] Registering mock file system for github-remote-file://');
|
||||
return vscode.workspace.registerFileSystemProvider('github-remote-file', provider, {
|
||||
isCaseSensitive: true,
|
||||
});
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Exports
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
module.exports = { activate };
|
||||
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"name": "sessions-e2e-mock",
|
||||
"displayName": "Sessions E2E Mock",
|
||||
"description": "Mock auth, chat participant, and file system for E2E testing",
|
||||
"version": "0.0.1",
|
||||
"publisher": "vscode",
|
||||
"engines": {
|
||||
"vscode": "*"
|
||||
},
|
||||
"extensionKind": ["ui", "workspace"],
|
||||
"browser": "./extension.js",
|
||||
"activationEvents": ["*"],
|
||||
"capabilities": {
|
||||
"virtualWorkspaces": true,
|
||||
"untrustedWorkspaces": {
|
||||
"supported": true
|
||||
}
|
||||
},
|
||||
"contributes": {
|
||||
"authentication": [
|
||||
{
|
||||
"id": "github",
|
||||
"label": "GitHub (Mock)"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -155,7 +155,7 @@ async function main() {
|
||||
|
||||
// Start web server
|
||||
console.log(`Starting sessions web server on port ${PORT}…`);
|
||||
const server = startServer(PORT);
|
||||
const server = startServer(PORT, { mock: true });
|
||||
await waitForServer(`http://localhost:${PORT}/`, 30_000);
|
||||
console.log('Server ready.');
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
"description": "Automated E2E tests for the Agent Sessions window using playwright-cli",
|
||||
"scripts": {
|
||||
"serve": "node ../../../../../scripts/code-sessions-web.js --port 9222 --skip-welcome",
|
||||
"serve:mock": "node ../../../../../scripts/code-sessions-web.js --port 9222 --skip-welcome --mock",
|
||||
"generate": "node generate.cjs",
|
||||
"test": "node test.cjs"
|
||||
},
|
||||
|
||||
@@ -129,7 +129,7 @@ async function main() {
|
||||
|
||||
// Start web server
|
||||
console.log(`Starting sessions web server on port ${PORT}…`);
|
||||
const server = startServer(PORT);
|
||||
const server = startServer(PORT, { mock: true });
|
||||
await waitForServer(`http://localhost:${PORT}/`, 30_000);
|
||||
console.log('Server ready.\n');
|
||||
|
||||
|
||||
Reference in New Issue
Block a user