diff --git a/manifest.json b/manifest.json index f50295f..234bc1e 100644 --- a/manifest.json +++ b/manifest.json @@ -3,7 +3,7 @@ "name": "browser.cpp", "short_name": "browser.cpp", "description": "In-browser C++20 IDE powered by Monaco Editor and WASM Clang", - "version": "0.4.8", + "version": "0.4.9", "minimum_chrome_version": "105", "icons": { "16": "icons/icon16.png", diff --git a/package-lock.json b/package-lock.json index 8033747..b6cfe0a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "browser.cpp", - "version": "0.4.8", + "version": "0.4.9", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "browser.cpp", - "version": "0.4.8", + "version": "0.4.9", "dependencies": { "@xterm/addon-fit": "^0.11.0", "@xterm/addon-web-links": "^0.12.0", diff --git a/package.json b/package.json index b8f98fd..07f66ea 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "browser.cpp", - "version": "0.4.8", + "version": "0.4.9", "description": "In-browser C++20 IDE with WASM Clang toolchain", "private": true, "scripts": { diff --git a/scripts/e2e-terminal-stop.test.mjs b/scripts/e2e-terminal-stop.test.mjs index 8cfacc1..559ca71 100644 --- a/scripts/e2e-terminal-stop.test.mjs +++ b/scripts/e2e-terminal-stop.test.mjs @@ -149,20 +149,55 @@ test('e2e: prompt restoration follows newline-less program output on a new line' assert.match(ctx.writes.join(''), /program output\r\n.*browser\.cpp.*:~\$ /); }); -test('e2e: Ctrl+C while running stops the program once and restores the prompt', async () => { +test('e2e: Ctrl+C while running resets to one pristine prompt', async () => { const ctx = setupTerminalHarness(); assert.equal(await startRun(), true); onRunStart({ stdinMode: 'interactive' }); - __handleTerminalKeyForTesting('', ctrlCEvent()); + writeStdout('runaway output'); + const writesBeforeStop = ctx.writes.length; __handleTerminalKeyForTesting('', ctrlCEvent()); assert.equal(ctx.runCalls.length, 1); assert.deepEqual(ctx.stopCalls, ['stop']); assert.deepEqual(ctx.runStateChanges, [true, false]); assert.equal(__getTerminalStateForTesting().running, false); - assert.ok(ctx.writes.join('').includes('^C')); - assert.ok(ctx.writes.join('').includes('Process interrupted.')); + const resetWrites = ctx.writes.slice(writesBeforeStop).join(''); + assert.ok(resetWrites.includes('browser.cpp')); + assert.equal(resetWrites.match(/browser\.cpp/g)?.length, 1); + assert.ok(!resetWrites.includes('^C')); + assert.ok(!resetWrites.includes('Process interrupted.')); + assert.equal(ctx.clearCalls.length, 1); +}); + +test('e2e: STOP discards stdout queued after a stopped run', async () => { + const ctx = setupTerminalHarness(); + + showInitialPrompt(); + assert.equal(await startRun(), true); + onRunStart({ stdinMode: 'interactive' }); + writeStdout('runaway output'); + assert.equal(stopRun(), true); + const writesAfterStop = ctx.writes.length; + + writeStdout('late output'); + + assert.equal(ctx.writes.length, writesAfterStop); + assert.ok(!ctx.writes.join('').includes('Process interrupted.')); +}); + +test('e2e: STOP ignores a late nonzero run result', async () => { + const ctx = setupTerminalHarness(); + + showInitialPrompt(); + assert.equal(await startRun(), true); + onRunStart({ stdinMode: 'interactive' }); + assert.equal(stopRun(), true); + const writesAfterStop = ctx.writes.length; + + onRunResult({ exitCode: 1 }); + + assert.equal(ctx.writes.length, writesAfterStop); }); test('e2e: stopRun is idempotent for repeated button presses during one run', async () => { diff --git a/src/ui/app.js b/src/ui/app.js index 3798326..d911f6a 100644 --- a/src/ui/app.js +++ b/src/ui/app.js @@ -139,8 +139,12 @@ window.addEventListener('DOMContentLoaded', async () => { if (terminalPanel) resizeObserver.observe(terminalPanel); initPanelResizers(); - // 9. Persist session on unload - window.addEventListener('beforeunload', () => persistenceGate.persist()); + // 9. Persist session on unload. Worker teardown is synchronous: browser + // unload handlers cannot safely wait for terminal or worker cleanup. + window.addEventListener('beforeunload', () => { + worker.terminate(); + persistenceGate.persist(); + }); editorAPI.focus(); }); diff --git a/src/ui/terminal.js b/src/ui/terminal.js index d8427be..2034609 100644 --- a/src/ui/terminal.js +++ b/src/ui/terminal.js @@ -466,17 +466,11 @@ export function onRunStart({ stdinMode = 'none', stdinSessionId = null } = {}) { * CPU-bound WASM cannot observe stdin EOF, so the main thread terminates the * worker via _onStopRun after terminal state has been reset. * - * @param {{ echoCtrlC?: boolean }} [options] * @returns {boolean} true when a running program was stopped */ -export function stopRun({ echoCtrlC = false } = {}) { +export function stopRun() { if (!running) return false; - if (echoCtrlC) { - term?.write('^C' + CRLF); - } else { - term?.write(CRLF); - } inputBuffer = ''; _clearSAB(); setRunPreparationState(false); @@ -486,7 +480,7 @@ export function stopRun({ echoCtrlC = false } = {}) { busy = false; runDone?.(); runDone = null; - term?.write(`${C.yellow}Process interrupted.${C.reset}${CRLF}`); + clearScreen(); writePrompt(); _onStopRun?.(); return true; @@ -496,6 +490,7 @@ export function stopRun({ echoCtrlC = false } = {}) { /** Write stdout text from the running program. */ export function writeStdout(text) { + if (!running) return; term?.write(text.replace(/\n/g, CRLF)); } @@ -531,6 +526,7 @@ export function onCompileResult({ success, diagnostics, outputPath }) { * @param {{ exitCode:number }} result */ export function onRunResult({ exitCode }) { + if (!running && !preparingRun) return; const shouldRestorePrompt = running || preparingRun; if (exitCode !== 0) { term?.write(`${CRLF}${C.yellow}Process exited with code ${exitCode}.${C.reset}${CRLF}`); @@ -609,7 +605,7 @@ function handleKey({ key, domEvent }) { if (activeStdinMode === 'interactive' || activeStdinMode === 'interactive-message') { handleStdinKey(key, domEvent); } else if (domEvent.ctrlKey && domEvent.key === 'c') { - stopRun({ echoCtrlC: true }); + stopRun(); } return; } @@ -740,7 +736,7 @@ function handleStdinKey(key, domEvent) { // Ctrl+C – interrupt the running program if (domEvent.ctrlKey && code === 'c') { - stopRun({ echoCtrlC: true }); + stopRun(); return; }