Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"lint": "eslint .",
"build": "npm run build:webpack && npm run build:targets",
"build:firefox": "npm run build",
"test:e2e": "node --experimental-detect-module --test scripts/e2e-session-persistence.test.mjs scripts/e2e-session-restore-choice.test.mjs scripts/e2e-multifile-build.test.mjs scripts/e2e-workspace-file-tracking.test.mjs scripts/e2e-workspace-scan-progress.test.mjs scripts/e2e-terminal-mkdir.test.mjs scripts/e2e-terminal-stop.test.mjs scripts/e2e-terminal-git-removal.test.mjs scripts/e2e-terminal-stop-icon.test.mjs scripts/e2e-browser-compatibility.test.mjs scripts/e2e-firefox-compatibility.test.mjs scripts/e2e-firefox-jspi-stdin.test.mjs scripts/e2e-wasi-shim.test.mjs scripts/e2e-run-request.test.mjs scripts/e2e-release-packaging.test.mjs",
"test:e2e": "node --experimental-detect-module --test scripts/e2e-page-lifecycle.test.mjs scripts/e2e-session-persistence.test.mjs scripts/e2e-session-restore-choice.test.mjs scripts/e2e-multifile-build.test.mjs scripts/e2e-workspace-file-tracking.test.mjs scripts/e2e-workspace-scan-progress.test.mjs scripts/e2e-terminal-mkdir.test.mjs scripts/e2e-terminal-stop.test.mjs scripts/e2e-terminal-git-removal.test.mjs scripts/e2e-terminal-stop-icon.test.mjs scripts/e2e-browser-compatibility.test.mjs scripts/e2e-firefox-compatibility.test.mjs scripts/e2e-firefox-jspi-stdin.test.mjs scripts/e2e-wasi-shim.test.mjs scripts/e2e-run-request.test.mjs scripts/e2e-release-packaging.test.mjs",
"test:e2e:compiler": "npm run test:preflight-clang && node --experimental-detect-module --test scripts/e2e-compiler-link.test.mjs",
"test:preflight-clang": "node scripts/preflight-clang-artifacts.js",
"test:browser:chrome": "npm run test:e2e:compiler && node scripts/smoke-browser.mjs chrome",
Expand Down
24 changes: 24 additions & 0 deletions scripts/e2e-page-lifecycle.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import test from 'node:test';
import assert from 'node:assert/strict';

test('e2e: page unload only terminates the worker synchronously', async () => {
const { registerPageUnload } = await import('../src/ui/page-lifecycle.mjs');
const listeners = new Map();
const events = [];
const target = {
addEventListener(type, listener) {
listeners.set(type, listener);
},
};
const worker = {
terminate() {
events.push('terminate');
},
};

registerPageUnload(target, worker);
const result = listeners.get('beforeunload')();

assert.equal(result, undefined);
assert.deepEqual(events, ['terminate']);
});
222 changes: 207 additions & 15 deletions scripts/e2e-session-persistence.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
restoreWorkspace as restoreToolbarWorkspace,
getOpenTabPaths as getToolbarOpenTabPaths,
getActiveTabPath as getToolbarActiveTabPath,
getOpenTabsSnapshot as getToolbarOpenTabsSnapshot,
} from '../src/ui/toolbar.js';

class FakeElement {
Expand Down Expand Up @@ -197,6 +198,191 @@ function createFailingHandleStore() {
};
}

test('e2e: state-only persistence never writes or clears the directory handle', async () => {
const storage = createStorageArea();
const directoryHandle = { name: 'project' };
const handleCalls = [];
const persistence = createSessionPersistence({
fsAPI: {
getDirectoryHandle: () => directoryHandle,
getWorkspaceSnapshot: () => ({
name: 'project',
entries: [{ path: 'main.cpp', kind: 'file' }],
}),
openFolderFromHandle: async () => null,
},
getOpenTabPaths: () => ['main.cpp'],
getActiveTabPath: () => 'main.cpp',
getOpenTabsSnapshot: () => ({ 'main.cpp': 'int main() {}\n' }),
restoreWorkspace: async () => {},
storage,
handleStore: {
async save(handle) {
handleCalls.push(['save', handle]);
},
async load() {
return null;
},
async clear() {
handleCalls.push(['clear']);
},
},
});

await persistence.persistSessionState();

assert.deepEqual(handleCalls, []);
assert.deepEqual(
(await storage.get('browser_cpp_session')).browser_cpp_session.openTabPaths,
['main.cpp']
);
});

test('e2e: workspace persistence stores the directory handle before serializable state', async () => {
const events = [];
const directoryHandle = { name: 'project' };
const persistence = createSessionPersistence({
fsAPI: {
getDirectoryHandle: () => directoryHandle,
getWorkspaceSnapshot: () => ({ name: 'project', entries: [] }),
openFolderFromHandle: async () => null,
},
getOpenTabPaths: () => [],
getActiveTabPath: () => null,
getOpenTabsSnapshot: () => ({}),
restoreWorkspace: async () => {},
storage: {
async get() {
return {};
},
async set() {
events.push('state');
},
},
handleStore: {
async save(handle) {
assert.equal(handle, directoryHandle);
events.push('handle');
},
async load() {
return null;
},
async clear() {
events.push('clear');
},
},
});

await persistence.persistWorkspaceSession();

assert.deepEqual(events, ['handle', 'state']);
});

test('e2e: active tab snapshots include edits made without switching tabs', async () => {
const originalDocument = global.document;
global.document = createFakeDocument();
try {
let editorValue = '';
initToolbar(
{ onmessage: null, postMessage() {} },
{
getValue: () => editorValue,
setValue: (value) => { editorValue = value; },
clearDiagnostics: () => {},
setLanguage: () => {},
},
{ setWorkspace: () => {}, clearTerminal: () => {} },
{ readWorkspaceFile: async () => 'int main() { return 0; }\n' },
() => {}
);
await restoreToolbarWorkspace(
{ name: 'project', entries: [{ path: 'main.cpp', kind: 'file' }] },
['main.cpp'],
'main.cpp'
);

editorValue = 'int main() { return 42; }\n';

assert.deepEqual(getToolbarOpenTabsSnapshot(), {
'main.cpp': 'int main() { return 42; }\n',
});
} finally {
global.document = originalDocument;
}
});

test('e2e: persistence gate preserves workspace intent while restore is pending', async () => {
const events = [];
const gate = createPersistenceGate({
persistSessionState: async () => events.push('state'),
persistWorkspaceSession: async () => events.push('workspace'),
});

await gate.persistState();
await gate.persistWorkspace();
assert.deepEqual(events, []);

await gate.enable();

assert.deepEqual(events, ['workspace']);
});

test('e2e: scheduled state persistence coalesces rapid changes', async () => {
let stateSaves = 0;
const gate = createPersistenceGate({
persistSessionState: async () => { stateSaves += 1; },
persistWorkspaceSession: async () => {},
}, { debounceMs: 5 });
await gate.enable();

gate.scheduleState();
gate.scheduleState();
gate.scheduleState();
await new Promise((resolve) => setTimeout(resolve, 15));

assert.equal(stateSaves, 1);
});

test('e2e: opening a folder uses explicit workspace persistence', async () => {
const originalDocument = global.document;
global.document = createFakeDocument();
try {
const persistenceEvents = [];
initToolbar(
{ onmessage: null, postMessage() {} },
{
getValue: () => '',
setValue: () => {},
clearDiagnostics: () => {},
setLanguage: () => {},
},
{
setWorkspace: () => {},
resetTerminalSession: () => {},
clearTerminal: () => {},
},
{
openFolder: async () => ({ name: 'empty', entries: [] }),
},
{
persistState: async () => persistenceEvents.push('state'),
persistWorkspace: async () => persistenceEvents.push('workspace'),
scheduleState: () => persistenceEvents.push('scheduled'),
}
);

global.document.getElementById('btn-open').click();
await waitFor(
() => persistenceEvents.length > 0,
'workspace persistence after folder open'
);

assert.deepEqual(persistenceEvents, ['workspace']);
} finally {
global.document = originalDocument;
}
});

test('e2e: does not fall back to read-only permission when readwrite is denied', async () => {
const storage = createStorageArea();
const handleStore = createHandleStore();
Expand Down Expand Up @@ -236,7 +422,7 @@ test('e2e: does not fall back to read-only permission when readwrite is denied',
handleStore,
});

await firstSession.persistSession();
await firstSession.persistWorkspaceSession();

const secondSession = createSessionPersistence({
fsAPI: {
Expand Down Expand Up @@ -298,7 +484,7 @@ test('e2e: prompts to reload and re-requests readwrite, restoring live workspace
storage,
handleStore,
});
await firstSession.persistSession();
await firstSession.persistWorkspaceSession();

const secondSession = createSessionPersistence({
fsAPI: {
Expand Down Expand Up @@ -371,7 +557,7 @@ test('e2e: choosing start-new abandons previous state and clears persisted sessi
storage,
handleStore,
});
await firstSession.persistSession();
await firstSession.persistWorkspaceSession();

const secondSession = createSessionPersistence({
fsAPI: {
Expand Down Expand Up @@ -445,7 +631,7 @@ test('e2e: reload chosen but browser denies permission still restores snapshot',
storage,
handleStore,
});
await firstSession.persistSession();
await firstSession.persistWorkspaceSession();

const secondSession = createSessionPersistence({
fsAPI: {
Expand Down Expand Up @@ -499,7 +685,7 @@ test('e2e: ignores legacy source-only snapshots when no workspace handle is avai
handleStore,
});

await firstSession.persistSession();
await firstSession.persistSessionState();

const secondSession = createSessionPersistence({
fsAPI: {
Expand Down Expand Up @@ -607,7 +793,7 @@ test('e2e: startup gate prevents pre-restore persistence from wiping workspace s
storage,
handleStore,
});
await firstSession.persistSession();
await firstSession.persistWorkspaceSession();

const secondSession = createSessionPersistence({
fsAPI: {
Expand All @@ -628,8 +814,11 @@ test('e2e: startup gate prevents pre-restore persistence from wiping workspace s
handleStore,
});

const gate = createPersistenceGate(secondSession.persistSession);
await gate.persist(); // startup timer fires before restore; must be ignored
const gate = createPersistenceGate({
persistSessionState: secondSession.persistSessionState,
persistWorkspaceSession: secondSession.persistWorkspaceSession,
});
await gate.persistState(); // startup timer fires before restore; must be ignored
await secondSession.restoreSession();
await gate.enable();

Expand Down Expand Up @@ -671,7 +860,7 @@ test('e2e: restores workspace tabs across reopen with callback-style storage', a
storage,
handleStore,
});
await firstSession.persistSession();
await firstSession.persistWorkspaceSession();

const secondSession = createSessionPersistence({
fsAPI: {
Expand Down Expand Up @@ -733,7 +922,7 @@ test('e2e: relaunch requests readwrite permission before restoring workspace', a
storage,
handleStore,
});
await firstSession.persistSession();
await firstSession.persistWorkspaceSession();

const secondSession = createSessionPersistence({
fsAPI: {
Expand Down Expand Up @@ -809,21 +998,24 @@ test('e2e: launch/open-files/close/relaunch restores explorer folder and tabs',
storage,
handleStore,
});
const launchOneGate = createPersistenceGate(launchOne.persistSession);
const launchOneGate = createPersistenceGate({
persistSessionState: launchOne.persistSessionState,
persistWorkspaceSession: launchOne.persistWorkspaceSession,
});
const launchOneRestore = launchOne.restoreSession();

// Simulate user flow before startup restore completes:
// launch -> open folder (grant permission) -> open files -> close tab.
launchOneState.directoryHandle = directoryHandle;
launchOneState.openTabPaths = ['README.md'];
launchOneState.activeTabPath = 'README.md';
await launchOneGate.persist(); // folder open + initial tab
await launchOneGate.persistWorkspace(); // folder open + initial tab
launchOneState.openTabPaths = ['README.md', 'bitmap.h', 'bitmap.cpp', 'test_runner.sh'];
launchOneState.activeTabPath = 'test_runner.sh';
await launchOneGate.persist(); // multiple file tabs open
await launchOneGate.persistState(); // multiple file tabs open
launchOneState.openTabPaths = ['bitmap.h', 'bitmap.cpp', 'test_runner.sh'];
launchOneState.activeTabPath = 'test_runner.sh';
await launchOneGate.persist(); // README closed
await launchOneGate.persistState(); // README closed

// Closing and relaunching the extension tab:
resolveGet();
Expand Down Expand Up @@ -904,7 +1096,7 @@ test('e2e: restores explorer folder and tabs when handle reload is unavailable',
warnings.push(args);
};
try {
await firstSession.persistSession();
await firstSession.persistWorkspaceSession();
} finally {
console.warn = originalWarn;
}
Expand Down
6 changes: 3 additions & 3 deletions scripts/e2e-session-restore-choice.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ async function seedWorkspaceSession({ storage, handleStore, handle, snapshot, op
storage,
handleStore,
});
await first.persistSession();
await first.persistWorkspaceSession();
}

test('e2e: reload choice re-requests readwrite and restores the live workspace', async () => {
Expand Down Expand Up @@ -232,7 +232,7 @@ test('e2e: after start-new, the next persist leaves the untitled state unpersist
});

await persistence.restoreSession();
await persistence.persistSession();
await persistence.persistSessionState();

const saved = (await storage.get('browser_cpp_session')).browser_cpp_session;
assert.equal(saved, null, 'the untitled buffer is not persisted');
Expand Down Expand Up @@ -306,7 +306,7 @@ test('e2e: untitled source is neither persisted nor restored', async () => {
storage,
handleStore,
});
await first.persistSession();
await first.persistSessionState();
assert.equal((await storage.get('browser_cpp_session')).browser_cpp_session, null);

let restoredSource = null;
Expand Down
Loading
Loading