Path: blob/main/src/vs/sessions/test/e2e/generate.cjs
13394 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45// @ts-check67/**8* Compile .scenario.md files into .commands.json using Copilot CLI.9*10* For each scenario, this script:11* 1. Starts the web server and opens the Sessions window in playwright-cli12* 2. Takes a snapshot of the current page state13* 3. Sends each step + snapshot to Copilot CLI to get the playwright-cli commands14* 4. Executes the commands (to advance UI state for the next step)15* 5. Writes the compiled commands to a .commands.json file16*17* Usage:18* node generate.cjs # compile all scenarios19* node generate.cjs 01-repo-picker # compile matching scenario(s)20*/2122const fs = require('fs');23const path = require('path');24const cp = require('child_process');25const {26APP_ROOT,27discoverScenarios,28runPlaywrightCli,29getSnapshot,30startServer,31waitForServer,32commandsPathForScenario,33} = require('./common.cjs');3435const PORT = 9100 + Math.floor(Math.random() * 900);36const BASE_URL = `http://localhost:${PORT}/?skip-sessions-welcome`;3738const SYSTEM_PROMPT = [39'You are a test automation assistant. Given a snapshot of a web page\'s',40'accessibility tree and a test step written in natural language, output the',41'exact semantic commands needed to execute that step.',42'',43'Rules:',44'- Output ONLY the commands, one per line. No explanation, no markdown.',45'- Use SEMANTIC selectors with role and label, NOT element refs.',46' Examples:',47' click button "Send"',48' click textbox "Chat input"',49' click listitem "Today sessions section"',50' click tab "Changes - 3 files changed"',51' click treeitem "build.ts"',52'- The format is: <action> <role> "<label>"',53'- For typing text, use: type "the text here"',54'- For pressing keys, use: press Enter (or other key name)',55'- For assertions that something is visible, output: # ASSERT_VISIBLE: the text to check for',56'- For assertions that a button is disabled, output: # ASSERT_DISABLED: the button label',57'- For assertions that a button is enabled, output: # ASSERT_ENABLED: the button label',58'- Icon characters (like codicons from the Unicode Private Use Area) appear in labels.',59' Strip them from your selectors — match by readable text only.',60'- For labels with leading/trailing whitespace or icon chars, use only the readable text portion.',61'- NEVER use element refs like e43, e155, etc. Always use role "label" selectors.',62'- NEVER include dates, times, or timestamps in selectors — they change between runs.',63' Use only the stable portion of the label. For example, instead of:',64' click listitem "Background session explain the code (Completed), created 3/5/2026, 8:48:50 PM"',65' Use:',66' click listitem "explain the code"',67].join('\n');6869// ---------------------------------------------------------------------------70// Ask Copilot CLI to translate a step71// ---------------------------------------------------------------------------7273function askCopilot(step, snapshot) {74const prompt = `Snapshot:\n\`\`\`\n${snapshot}\n\`\`\`\n\nStep: ${step}\n\nOutput the semantic commands:`;7576const result = cp.spawnSync('copilot', ['-p', `${SYSTEM_PROMPT}\n\n${prompt}`, '--model', 'claude-sonnet-4.6'], {77cwd: APP_ROOT,78stdio: ['ignore', 'pipe', 'pipe'],79timeout: 60_000,80env: { ...process.env },81});8283const stdout = (result.stdout || '').toString().trim();84const stderr = (result.stderr || '').toString().trim();8586if (result.status !== 0) {87throw new Error(`Copilot CLI failed: ${stderr || stdout}`);88}8990return stdout.split('\n')91.map(l => l.trim())92.filter(l => l.length > 0);93}9495// ---------------------------------------------------------------------------96// Resolve a semantic command to a ref-based command using a snapshot97// ---------------------------------------------------------------------------9899function resolveSemanticCommand(cmd, snapshotText) {100// Match: <action> <role> "<label>"101const match = cmd.match(/^(click|focus)\s+(\w+)\s+"([^"]+)"$/);102if (!match) { return cmd; }103104const [, action, role, label] = match;105const needle = label.replace(/[\uE000-\uF8FF]/g, '').trim().toLowerCase();106107for (const line of snapshotText.split('\n')) {108const refMatch = line.match(/\[ref=(e\d+)\]/);109if (!refMatch) { continue; }110if (!line.includes(role)) { continue; }111const labelMatch = line.match(/"([^"]+)"/);112if (!labelMatch) { continue; }113const lineLabel = labelMatch[1].replace(/[\uE000-\uF8FF]/g, '').trim().toLowerCase();114if (lineLabel.includes(needle) || needle.includes(lineLabel)) {115return `${action} ${refMatch[1]}`;116}117}118119// Fallback: return as-is (will likely fail, but gives a clear error)120console.error(` ⚠ Could not resolve: ${cmd}`);121return cmd;122}123124// ---------------------------------------------------------------------------125// Compile a single scenario126// ---------------------------------------------------------------------------127128function compileScenario(scenario) {129console.log(`\n▶ Compiling: ${scenario.name}`);130131const compiledSteps = [];132for (const [i, step] of scenario.steps.entries()) {133console.log(` step ${i + 1}: ${step}`);134135const snapshot = getSnapshot();136if (!snapshot.stdout) {137console.error(` ⚠ Could not get snapshot, skipping step`);138compiledSteps.push({ description: step, commands: [], error: 'Failed to get snapshot' });139continue;140}141142try {143const commands = askCopilot(step, snapshot.stdout);144console.log(` → ${commands.join(' ; ')}`);145146compiledSteps.push({ description: step, commands });147148// Execute the commands to advance the UI state for the next step149// Resolve semantic selectors to refs using the current snapshot150for (const cmd of commands) {151if (cmd.startsWith('#')) { continue; }152const resolved = resolveSemanticCommand(cmd, snapshot.stdout);153if (resolved !== cmd) {154console.log(` [resolve] ${cmd} → ${resolved}`);155}156const result = runPlaywrightCli(resolved);157if (!result.ok) {158console.error(` ⚠ Command failed: ${resolved} — ${result.stderr}`);159}160}161162cp.spawnSync('sleep', ['1']);163} catch (err) {164console.error(` ✗ ${err.message}`);165compiledSteps.push({ description: step, commands: [], error: err.message });166}167}168169return {170scenario: scenario.name,171generatedAt: new Date().toISOString(),172steps: compiledSteps,173};174}175176// ---------------------------------------------------------------------------177// Main178// ---------------------------------------------------------------------------179180async function main() {181const filter = process.argv[2] || '';182let scenarios = discoverScenarios();183184if (filter) {185scenarios = scenarios.filter(s =>186s.filePath.includes(filter) || s.name.toLowerCase().includes(filter.toLowerCase())187);188}189190if (scenarios.length === 0) {191console.error('No scenarios found' + (filter ? ` matching "${filter}"` : ''));192process.exit(1);193}194195console.log(`Found ${scenarios.length} scenario(s) to compile`);196197// Start web server198console.log(`Starting sessions web server on port ${PORT}…`);199const server = startServer(PORT, { mock: true });200await waitForServer(`http://localhost:${PORT}/`, 30_000);201console.log('Server ready.');202203// Open browser204const openResult = runPlaywrightCli(['open', '--headed']);205if (!openResult.ok) {206console.error('Failed to open browser:', openResult.stdout, openResult.stderr);207cleanup(server);208process.exit(1);209}210const gotoResult = runPlaywrightCli(['goto', BASE_URL]);211if (!gotoResult.ok) {212console.error('Failed to navigate:', gotoResult.stdout, gotoResult.stderr);213cleanup(server);214process.exit(1);215}216217// Wait for workbench to render218cp.spawnSync('sleep', ['5']);219220for (const scenario of scenarios) {221// Reset state between scenarios222runPlaywrightCli(['press', 'Escape']);223runPlaywrightCli(['goto', BASE_URL]);224cp.spawnSync('sleep', ['3']);225226const compiled = compileScenario(scenario);227const outPath = commandsPathForScenario(scenario.filePath);228fs.mkdirSync(path.dirname(outPath), { recursive: true });229fs.writeFileSync(outPath, JSON.stringify(compiled, null, '\t') + '\n');230console.log(` ✓ Saved: ${outPath}`);231}232233cleanup(server);234console.log('\nDone.');235}236237function cleanup(server) {238runPlaywrightCli('close');239server.kill('SIGTERM');240}241242main();243244245