Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/sessions/test/e2e/generate.cjs
13394 views
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
6
// @ts-check
7
8
/**
9
* Compile .scenario.md files into .commands.json using Copilot CLI.
10
*
11
* For each scenario, this script:
12
* 1. Starts the web server and opens the Sessions window in playwright-cli
13
* 2. Takes a snapshot of the current page state
14
* 3. Sends each step + snapshot to Copilot CLI to get the playwright-cli commands
15
* 4. Executes the commands (to advance UI state for the next step)
16
* 5. Writes the compiled commands to a .commands.json file
17
*
18
* Usage:
19
* node generate.cjs # compile all scenarios
20
* node generate.cjs 01-repo-picker # compile matching scenario(s)
21
*/
22
23
const fs = require('fs');
24
const path = require('path');
25
const cp = require('child_process');
26
const {
27
APP_ROOT,
28
discoverScenarios,
29
runPlaywrightCli,
30
getSnapshot,
31
startServer,
32
waitForServer,
33
commandsPathForScenario,
34
} = require('./common.cjs');
35
36
const PORT = 9100 + Math.floor(Math.random() * 900);
37
const BASE_URL = `http://localhost:${PORT}/?skip-sessions-welcome`;
38
39
const SYSTEM_PROMPT = [
40
'You are a test automation assistant. Given a snapshot of a web page\'s',
41
'accessibility tree and a test step written in natural language, output the',
42
'exact semantic commands needed to execute that step.',
43
'',
44
'Rules:',
45
'- Output ONLY the commands, one per line. No explanation, no markdown.',
46
'- Use SEMANTIC selectors with role and label, NOT element refs.',
47
' Examples:',
48
' click button "Send"',
49
' click textbox "Chat input"',
50
' click listitem "Today sessions section"',
51
' click tab "Changes - 3 files changed"',
52
' click treeitem "build.ts"',
53
'- The format is: <action> <role> "<label>"',
54
'- For typing text, use: type "the text here"',
55
'- For pressing keys, use: press Enter (or other key name)',
56
'- For assertions that something is visible, output: # ASSERT_VISIBLE: the text to check for',
57
'- For assertions that a button is disabled, output: # ASSERT_DISABLED: the button label',
58
'- For assertions that a button is enabled, output: # ASSERT_ENABLED: the button label',
59
'- Icon characters (like codicons from the Unicode Private Use Area) appear in labels.',
60
' Strip them from your selectors — match by readable text only.',
61
'- For labels with leading/trailing whitespace or icon chars, use only the readable text portion.',
62
'- NEVER use element refs like e43, e155, etc. Always use role "label" selectors.',
63
'- NEVER include dates, times, or timestamps in selectors — they change between runs.',
64
' Use only the stable portion of the label. For example, instead of:',
65
' click listitem "Background session explain the code (Completed), created 3/5/2026, 8:48:50 PM"',
66
' Use:',
67
' click listitem "explain the code"',
68
].join('\n');
69
70
// ---------------------------------------------------------------------------
71
// Ask Copilot CLI to translate a step
72
// ---------------------------------------------------------------------------
73
74
function askCopilot(step, snapshot) {
75
const prompt = `Snapshot:\n\`\`\`\n${snapshot}\n\`\`\`\n\nStep: ${step}\n\nOutput the semantic commands:`;
76
77
const result = cp.spawnSync('copilot', ['-p', `${SYSTEM_PROMPT}\n\n${prompt}`, '--model', 'claude-sonnet-4.6'], {
78
cwd: APP_ROOT,
79
stdio: ['ignore', 'pipe', 'pipe'],
80
timeout: 60_000,
81
env: { ...process.env },
82
});
83
84
const stdout = (result.stdout || '').toString().trim();
85
const stderr = (result.stderr || '').toString().trim();
86
87
if (result.status !== 0) {
88
throw new Error(`Copilot CLI failed: ${stderr || stdout}`);
89
}
90
91
return stdout.split('\n')
92
.map(l => l.trim())
93
.filter(l => l.length > 0);
94
}
95
96
// ---------------------------------------------------------------------------
97
// Resolve a semantic command to a ref-based command using a snapshot
98
// ---------------------------------------------------------------------------
99
100
function resolveSemanticCommand(cmd, snapshotText) {
101
// Match: <action> <role> "<label>"
102
const match = cmd.match(/^(click|focus)\s+(\w+)\s+"([^"]+)"$/);
103
if (!match) { return cmd; }
104
105
const [, action, role, label] = match;
106
const needle = label.replace(/[\uE000-\uF8FF]/g, '').trim().toLowerCase();
107
108
for (const line of snapshotText.split('\n')) {
109
const refMatch = line.match(/\[ref=(e\d+)\]/);
110
if (!refMatch) { continue; }
111
if (!line.includes(role)) { continue; }
112
const labelMatch = line.match(/"([^"]+)"/);
113
if (!labelMatch) { continue; }
114
const lineLabel = labelMatch[1].replace(/[\uE000-\uF8FF]/g, '').trim().toLowerCase();
115
if (lineLabel.includes(needle) || needle.includes(lineLabel)) {
116
return `${action} ${refMatch[1]}`;
117
}
118
}
119
120
// Fallback: return as-is (will likely fail, but gives a clear error)
121
console.error(` ⚠ Could not resolve: ${cmd}`);
122
return cmd;
123
}
124
125
// ---------------------------------------------------------------------------
126
// Compile a single scenario
127
// ---------------------------------------------------------------------------
128
129
function compileScenario(scenario) {
130
console.log(`\n▶ Compiling: ${scenario.name}`);
131
132
const compiledSteps = [];
133
for (const [i, step] of scenario.steps.entries()) {
134
console.log(` step ${i + 1}: ${step}`);
135
136
const snapshot = getSnapshot();
137
if (!snapshot.stdout) {
138
console.error(` ⚠ Could not get snapshot, skipping step`);
139
compiledSteps.push({ description: step, commands: [], error: 'Failed to get snapshot' });
140
continue;
141
}
142
143
try {
144
const commands = askCopilot(step, snapshot.stdout);
145
console.log(` → ${commands.join(' ; ')}`);
146
147
compiledSteps.push({ description: step, commands });
148
149
// Execute the commands to advance the UI state for the next step
150
// Resolve semantic selectors to refs using the current snapshot
151
for (const cmd of commands) {
152
if (cmd.startsWith('#')) { continue; }
153
const resolved = resolveSemanticCommand(cmd, snapshot.stdout);
154
if (resolved !== cmd) {
155
console.log(` [resolve] ${cmd} → ${resolved}`);
156
}
157
const result = runPlaywrightCli(resolved);
158
if (!result.ok) {
159
console.error(` ⚠ Command failed: ${resolved} — ${result.stderr}`);
160
}
161
}
162
163
cp.spawnSync('sleep', ['1']);
164
} catch (err) {
165
console.error(` ✗ ${err.message}`);
166
compiledSteps.push({ description: step, commands: [], error: err.message });
167
}
168
}
169
170
return {
171
scenario: scenario.name,
172
generatedAt: new Date().toISOString(),
173
steps: compiledSteps,
174
};
175
}
176
177
// ---------------------------------------------------------------------------
178
// Main
179
// ---------------------------------------------------------------------------
180
181
async function main() {
182
const filter = process.argv[2] || '';
183
let scenarios = discoverScenarios();
184
185
if (filter) {
186
scenarios = scenarios.filter(s =>
187
s.filePath.includes(filter) || s.name.toLowerCase().includes(filter.toLowerCase())
188
);
189
}
190
191
if (scenarios.length === 0) {
192
console.error('No scenarios found' + (filter ? ` matching "${filter}"` : ''));
193
process.exit(1);
194
}
195
196
console.log(`Found ${scenarios.length} scenario(s) to compile`);
197
198
// Start web server
199
console.log(`Starting sessions web server on port ${PORT}…`);
200
const server = startServer(PORT, { mock: true });
201
await waitForServer(`http://localhost:${PORT}/`, 30_000);
202
console.log('Server ready.');
203
204
// Open browser
205
const openResult = runPlaywrightCli(['open', '--headed']);
206
if (!openResult.ok) {
207
console.error('Failed to open browser:', openResult.stdout, openResult.stderr);
208
cleanup(server);
209
process.exit(1);
210
}
211
const gotoResult = runPlaywrightCli(['goto', BASE_URL]);
212
if (!gotoResult.ok) {
213
console.error('Failed to navigate:', gotoResult.stdout, gotoResult.stderr);
214
cleanup(server);
215
process.exit(1);
216
}
217
218
// Wait for workbench to render
219
cp.spawnSync('sleep', ['5']);
220
221
for (const scenario of scenarios) {
222
// Reset state between scenarios
223
runPlaywrightCli(['press', 'Escape']);
224
runPlaywrightCli(['goto', BASE_URL]);
225
cp.spawnSync('sleep', ['3']);
226
227
const compiled = compileScenario(scenario);
228
const outPath = commandsPathForScenario(scenario.filePath);
229
fs.mkdirSync(path.dirname(outPath), { recursive: true });
230
fs.writeFileSync(outPath, JSON.stringify(compiled, null, '\t') + '\n');
231
console.log(` ✓ Saved: ${outPath}`);
232
}
233
234
cleanup(server);
235
console.log('\nDone.');
236
}
237
238
function cleanup(server) {
239
runPlaywrightCli('close');
240
server.kill('SIGTERM');
241
}
242
243
main();
244
245