Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/sessions/test/e2e/test.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
* Deterministic test runner — replays .commands.json files generated by generate.cjs.
10
*
11
* No LLM calls. Just sequential playwright-cli commands and assertions.
12
*
13
* Usage:
14
* node test.cjs # run all compiled scenarios
15
* node test.cjs 01-repo-picker # run matching scenario(s)
16
*/
17
18
const fs = require('fs');
19
const path = require('path');
20
const cp = require('child_process');
21
const {
22
APP_ROOT,
23
SCENARIOS_DIR,
24
runPlaywrightCli,
25
getSnapshot,
26
startServer,
27
waitForServer,
28
} = require('./common.cjs');
29
30
const PORT = 9100 + Math.floor(Math.random() * 900);
31
const BASE_URL = `http://localhost:${PORT}/?skip-sessions-welcome`;
32
33
// ---------------------------------------------------------------------------
34
// Discover compiled command files
35
// ---------------------------------------------------------------------------
36
37
function discoverCommandFiles(filter) {
38
const compiledDir = path.join(SCENARIOS_DIR, 'generated');
39
if (!fs.existsSync(compiledDir)) { return []; }
40
return fs.readdirSync(compiledDir)
41
.filter(f => f.endsWith('.commands.json'))
42
.filter(f => !filter || f.includes(filter))
43
.sort()
44
.map(f => path.join(compiledDir, f));
45
}
46
47
// ---------------------------------------------------------------------------
48
// Normalize label for assertion matching (strip codicons + trim + lowercase)
49
// ---------------------------------------------------------------------------
50
51
function normalizeLabel(text) {
52
return text.replace(/[\uE000-\uF8FF]/g, '').trim().toLowerCase();
53
}
54
55
// ---------------------------------------------------------------------------
56
// Semantic command resolver — resolves role+label selectors to current refs
57
// ---------------------------------------------------------------------------
58
59
/**
60
* Resolve a semantic command like `click button "Send"` to a ref-based
61
* command like `click e170` using a fresh snapshot of the current page.
62
*
63
* Commands that don't match the semantic pattern (type, press, comments)
64
* are returned as-is.
65
*/
66
function resolveSemanticCommand(cmd, snapshotText) {
67
// Match: <action> <role> "<label>"
68
const match = cmd.match(/^(click|focus)\s+(\w+)\s+"([^"]+)"$/);
69
if (!match) { return { resolved: cmd, ok: true }; }
70
71
const [, action, role, label] = match;
72
const needle = normalizeLabel(label);
73
74
for (const line of snapshotText.split('\n')) {
75
const refMatch = line.match(/\[ref=(e\d+)\]/);
76
if (!refMatch) { continue; }
77
if (!line.includes(role)) { continue; }
78
const labelMatch = line.match(/"([^"]+)"/);
79
if (!labelMatch) { continue; }
80
const lineLabel = normalizeLabel(labelMatch[1]);
81
if (lineLabel.includes(needle) || needle.includes(lineLabel)) {
82
return { resolved: `${action} ${refMatch[1]}`, ok: true };
83
}
84
}
85
86
return { resolved: cmd, ok: false, message: `Could not find ${role} "${label}" in snapshot` };
87
}
88
89
// ---------------------------------------------------------------------------
90
// Polling helper — retries a check function until it passes or times out
91
// ---------------------------------------------------------------------------
92
93
const ASSERT_TIMEOUT_MS = 10_000;
94
const ASSERT_POLL_MS = 500;
95
96
/**
97
* @param {() => { ok: boolean; message?: string }} checkFn
98
* @returns {{ ok: boolean; message?: string }}
99
*/
100
function pollAssertion(checkFn) {
101
const deadline = Date.now() + ASSERT_TIMEOUT_MS;
102
let lastResult = checkFn();
103
while (!lastResult.ok && Date.now() < deadline) {
104
cp.spawnSync('sleep', [(ASSERT_POLL_MS / 1000).toString()]);
105
lastResult = checkFn();
106
}
107
return lastResult;
108
}
109
110
// ---------------------------------------------------------------------------
111
// Execute a single command line and handle assertions
112
// ---------------------------------------------------------------------------
113
114
function executeCommand(cmd) {
115
// Assertion comments — poll with retries to handle async rendering
116
if (cmd.startsWith('# ASSERT_VISIBLE:')) {
117
const text = cmd.slice('# ASSERT_VISIBLE:'.length).trim();
118
return pollAssertion(() => {
119
const snap = getSnapshot();
120
if (!snap.stdout) { return { ok: false, message: 'Failed to get snapshot for assertion' }; }
121
if (!snap.stdout.toLowerCase().includes(text.toLowerCase())) {
122
return { ok: false, message: `Expected "${text}" to be visible in snapshot` };
123
}
124
return { ok: true };
125
});
126
}
127
128
if (cmd.startsWith('# ASSERT_DISABLED:')) {
129
const label = cmd.slice('# ASSERT_DISABLED:'.length).trim();
130
return pollAssertion(() => {
131
const snap = getSnapshot();
132
if (!snap.stdout) { return { ok: false, message: 'Failed to get snapshot for assertion' }; }
133
const needle = normalizeLabel(label);
134
const buttonLine = snap.stdout.split('\n').find(l =>
135
l.includes('button') && l.match(/"([^"]+)"/) &&
136
normalizeLabel(l.match(/"([^"]+)"/)[1]) === needle
137
);
138
if (!buttonLine) { return { ok: false, message: `Button "${label}" not found in snapshot` }; }
139
if (!buttonLine.includes('[disabled]')) {
140
return { ok: false, message: `Expected button "${label}" to be disabled` };
141
}
142
return { ok: true };
143
});
144
}
145
146
if (cmd.startsWith('# ASSERT_ENABLED:')) {
147
const label = cmd.slice('# ASSERT_ENABLED:'.length).trim();
148
return pollAssertion(() => {
149
const snap = getSnapshot();
150
if (!snap.stdout) { return { ok: false, message: 'Failed to get snapshot for assertion' }; }
151
const needle = normalizeLabel(label);
152
const buttonLine = snap.stdout.split('\n').find(l =>
153
l.includes('button') && l.match(/"([^"]+)"/) &&
154
normalizeLabel(l.match(/"([^"]+)"/)[1]) === needle
155
);
156
if (!buttonLine) { return { ok: false, message: `Button "${label}" not found in snapshot` }; }
157
if (buttonLine.includes('[disabled]')) {
158
return { ok: false, message: `Expected button "${label}" to be enabled` };
159
}
160
return { ok: true };
161
});
162
}
163
164
// Skip other comments
165
if (cmd.startsWith('#')) { return { ok: true }; }
166
167
// Semantic commands (e.g. `click button "Send"`) — resolve to ref from live snapshot
168
const semanticMatch = cmd.match(/^(click|focus)\s+\w+\s+"[^"]+"$/);
169
if (semanticMatch) {
170
// Poll: the element might not be rendered yet
171
return pollAssertion(() => {
172
const snap = getSnapshot();
173
if (!snap.stdout) { return { ok: false, message: 'Failed to get snapshot for command resolution' }; }
174
const { resolved, ok, message } = resolveSemanticCommand(cmd, snap.stdout);
175
if (!ok) { return { ok: false, message: message || `Could not resolve: ${cmd}` }; }
176
console.log(` [resolve] ${cmd} → ${resolved}`);
177
const result = runPlaywrightCli(resolved);
178
if (!result.ok) {
179
return { ok: false, message: `playwright-cli ${resolved} failed:\n${result.stderr || result.stdout}` };
180
}
181
return { ok: true };
182
});
183
}
184
185
// Regular playwright-cli command (type, press, snapshot, etc.)
186
const result = runPlaywrightCli(cmd);
187
if (!result.ok) {
188
return { ok: false, message: `playwright-cli ${cmd} failed:\n${result.stderr || result.stdout}` };
189
}
190
return { ok: true };
191
}
192
193
// ---------------------------------------------------------------------------
194
// Main
195
// ---------------------------------------------------------------------------
196
197
async function main() {
198
const filter = process.argv[2] || '';
199
const commandFiles = discoverCommandFiles(filter);
200
201
if (commandFiles.length === 0) {
202
console.error('No .commands.json files found' + (filter ? ` matching "${filter}"` : ''));
203
console.error('Run "npm run generate" first to compile scenarios.');
204
process.exit(1);
205
}
206
207
console.log(`Found ${commandFiles.length} compiled scenario(s)\n`);
208
209
// Start web server
210
console.log(`Starting sessions web server on port ${PORT}…`);
211
const server = startServer(PORT, { mock: true });
212
await waitForServer(`http://localhost:${PORT}/`, 30_000);
213
console.log('Server ready.\n');
214
215
// Open browser
216
// --headed if you want to run the test locally and verify the UI interactions
217
// const openResult = runPlaywrightCli(['open', '--headed']);
218
const openResult = runPlaywrightCli(['open', '--headed']);
219
if (!openResult.ok) {
220
console.error('Failed to open browser:', openResult.stdout, openResult.stderr);
221
cleanup(server);
222
process.exit(1);
223
}
224
const gotoResult = runPlaywrightCli(['goto', BASE_URL]);
225
if (!gotoResult.ok) {
226
console.error('Failed to navigate:', gotoResult.stdout, gotoResult.stderr);
227
cleanup(server);
228
process.exit(1);
229
}
230
231
// Wait for workbench to render
232
cp.spawnSync('sleep', ['5']);
233
234
let totalPassed = 0;
235
let totalFailed = 0;
236
237
for (const filePath of commandFiles) {
238
const data = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
239
console.log(`▶ ${data.scenario}`);
240
241
// Reset state between scenarios
242
runPlaywrightCli(['press', 'Escape']);
243
runPlaywrightCli(['goto', BASE_URL]);
244
cp.spawnSync('sleep', ['3']);
245
246
let scenarioPassed = true;
247
248
for (const [i, step] of data.steps.entries()) {
249
// Give the UI time to settle after each step (matches generate.cjs behavior)
250
cp.spawnSync('sleep', ['1']);
251
const label = ` step ${i + 1}: ${step.description}`;
252
253
if (step.error) {
254
console.error(` ❌ ${label}`);
255
console.error(` Compilation error: ${step.error}`);
256
totalFailed++;
257
scenarioPassed = false;
258
continue;
259
}
260
261
let stepPassed = true;
262
for (const cmd of step.commands) {
263
const result = executeCommand(cmd);
264
if (!result.ok) {
265
console.error(` ❌ ${label}`);
266
console.error(` ${result.message}`);
267
// Screenshot on failure
268
const basename = path.basename(filePath, '.commands.json');
269
runPlaywrightCli(`screenshot --filename=out/failure-${basename}-step${i + 1}.png`);
270
totalFailed++;
271
stepPassed = false;
272
scenarioPassed = false;
273
break;
274
}
275
}
276
277
if (stepPassed) {
278
console.log(` ✅ ${label}`);
279
totalPassed++;
280
}
281
}
282
283
console.log();
284
}
285
286
cleanup(server);
287
288
console.log(`Results: ${totalPassed} passed, ${totalFailed} failed`);
289
process.exit(totalFailed > 0 ? 1 : 0);
290
}
291
292
function cleanup(server) {
293
runPlaywrightCli('close');
294
server.kill('SIGTERM');
295
}
296
297
main();
298
299