Skip to content

Commit 91b0696

Browse files
committed
ifix(view): add localStorage persistence to mock VSCode API
Mock data was lost on page refresh, preventing E2E testing - Implemented localStorage save/restore for config data - Persisted buttons, button sets, active set, and config scope - Added Configuration Persistence E2E tests
1 parent 27c2597 commit 91b0696

2 files changed

Lines changed: 181 additions & 6 deletions

File tree

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
import { expect, test, type Page } from "@playwright/test";
2+
import { fillCommandForm, saveCommandDialog, verifySuccessToast } from "./helpers/test-helpers";
3+
4+
const STORAGE_KEY_PREFIX = "vscode-mock";
5+
6+
const clearMockStorage = async (page: Page) => {
7+
await page.evaluate((prefix: string) => {
8+
Object.keys(localStorage)
9+
.filter((k) => k.startsWith(prefix))
10+
.forEach((k) => localStorage.removeItem(k));
11+
}, STORAGE_KEY_PREFIX);
12+
};
13+
14+
test.describe("Configuration Persistence Across Page Refresh", () => {
15+
test.beforeEach(async ({ page }) => {
16+
await page.goto("/");
17+
await clearMockStorage(page);
18+
await page.reload();
19+
await page.waitForLoadState("networkidle");
20+
});
21+
22+
test("should persist newly added command after page refresh", async ({ page }) => {
23+
// Given: Add a new command
24+
await page.getByRole("button", { name: /add/i }).first().click();
25+
await fillCommandForm(page, {
26+
name: "Refresh Test Command",
27+
command: "echo refresh-test",
28+
shortcut: "r",
29+
});
30+
await saveCommandDialog(page);
31+
32+
// When: Apply changes and refresh
33+
await page.getByRole("button", { name: "Apply configuration changes" }).click();
34+
await verifySuccessToast(page, "Configuration saved successfully");
35+
await page.reload();
36+
await page.waitForLoadState("networkidle");
37+
38+
// Then: Command should still be visible
39+
const commandCard = page.locator('[data-testid="command-card"]', {
40+
hasText: "Refresh Test Command",
41+
});
42+
await expect(commandCard).toBeVisible();
43+
await expect(commandCard.locator("code")).toContainText("echo refresh-test");
44+
});
45+
46+
test("should persist edited command after page refresh", async ({ page }) => {
47+
// Given: Add and save a command
48+
await page.getByRole("button", { name: /add/i }).first().click();
49+
await fillCommandForm(page, {
50+
name: "Edit Persist Test",
51+
command: "echo original",
52+
});
53+
await saveCommandDialog(page);
54+
await page.getByRole("button", { name: "Apply configuration changes" }).click();
55+
await verifySuccessToast(page, "Configuration saved successfully");
56+
57+
// When: Edit the command
58+
const commandCard = page.locator('[data-testid="command-card"]', {
59+
hasText: "Edit Persist Test",
60+
});
61+
await commandCard.getByRole("button", { name: /edit command/i }).click();
62+
63+
const commandInput = page.getByRole("textbox", { exact: true, name: "Command" });
64+
await commandInput.clear();
65+
await commandInput.fill("echo modified");
66+
await saveCommandDialog(page);
67+
68+
// Apply and refresh
69+
await page.getByRole("button", { name: "Apply configuration changes" }).click();
70+
await verifySuccessToast(page, "Configuration saved successfully");
71+
await page.reload();
72+
await page.waitForLoadState("networkidle");
73+
74+
// Then: Modified command should be visible
75+
const modifiedCard = page.locator('[data-testid="command-card"]', {
76+
hasText: "Edit Persist Test",
77+
});
78+
await expect(modifiedCard).toBeVisible();
79+
await expect(modifiedCard.locator("code")).toContainText("echo modified");
80+
});
81+
82+
test("should persist deleted command state after page refresh", async ({ page }) => {
83+
// Given: Add two commands
84+
await page.getByRole("button", { name: /add/i }).first().click();
85+
await fillCommandForm(page, { name: "Keep This", command: "echo keep" });
86+
await saveCommandDialog(page);
87+
88+
await page.getByRole("button", { name: /add/i }).first().click();
89+
await fillCommandForm(page, { name: "Delete This", command: "echo delete" });
90+
await saveCommandDialog(page);
91+
92+
await page.getByRole("button", { name: "Apply configuration changes" }).click();
93+
await verifySuccessToast(page, "Configuration saved successfully");
94+
95+
// When: Delete one command
96+
const deleteCard = page.locator('[data-testid="command-card"]', {
97+
hasText: "Delete This",
98+
});
99+
await deleteCard.getByRole("button", { name: /delete command/i }).click();
100+
await page.getByRole("button", { name: "Delete" }).click();
101+
102+
await page.getByRole("button", { name: "Apply configuration changes" }).click();
103+
await verifySuccessToast(page, "Configuration saved successfully");
104+
await page.reload();
105+
await page.waitForLoadState("networkidle");
106+
107+
// Then: Only the kept command should be visible
108+
const keptCard = page.locator('[data-testid="command-card"]', {
109+
hasText: "Keep This",
110+
});
111+
await expect(keptCard).toBeVisible();
112+
113+
const deletedCard = page.locator('[data-testid="command-card"]', {
114+
hasText: "Delete This",
115+
});
116+
await expect(deletedCard).not.toBeVisible();
117+
});
118+
119+
test("should persist configuration scope selection after page refresh", async ({ page }) => {
120+
// Given: Switch to global scope
121+
await page.getByRole("radio", { name: /global/i }).click();
122+
await page.waitForTimeout(200);
123+
124+
// When: Refresh the page
125+
await page.reload();
126+
await page.waitForLoadState("networkidle");
127+
128+
// Then: Global scope should still be selected
129+
const globalRadio = page.getByRole("radio", { name: /global/i });
130+
await expect(globalRadio).toBeChecked();
131+
});
132+
});

src/view/src/mock/vscode-mock.tsx

Lines changed: 49 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,45 @@ const MOCK_IMPORT_BUTTONS: ButtonConfigWithOptionalId[] = [
1717
{ command: "npm run updated", name: "Build Project", shortcut: "b" },
1818
];
1919

20+
const STORAGE_KEYS = {
21+
ACTIVE_SET: "vscode-mock-active-set",
22+
BUTTON_SETS: "vscode-mock-button-sets",
23+
CONFIGURATION_TARGET: "vscode-mock-configuration-target",
24+
GLOBAL_DATA: "vscode-mock-global-data",
25+
LOCAL_DATA: "vscode-mock-local-data",
26+
WORKSPACE_DATA: "vscode-mock-workspace-data",
27+
} as const;
28+
29+
const loadFromStorage = <T,>(key: string, defaultValue: T): T => {
30+
try {
31+
const stored = localStorage.getItem(key);
32+
return stored ? (JSON.parse(stored) as T) : defaultValue;
33+
} catch {
34+
return defaultValue;
35+
}
36+
};
37+
38+
const saveToStorage = <T,>(key: string, value: T): void => {
39+
try {
40+
localStorage.setItem(key, JSON.stringify(value));
41+
} catch {
42+
console.warn(`Failed to save to localStorage: ${key}`);
43+
}
44+
};
45+
2046
class VSCodeMock {
21-
private activeSet: string | null = null;
22-
private buttonSets: ButtonSet[] = [];
23-
private configurationTarget: string = CONFIGURATION_TARGET.WORKSPACE;
24-
private globalData: ButtonConfig[] = [];
25-
private localData: ButtonConfig[] = [];
26-
private workspaceData: ButtonConfig[] = mockCommands;
47+
private activeSet: string | null = loadFromStorage(STORAGE_KEYS.ACTIVE_SET, null);
48+
private buttonSets: ButtonSet[] = loadFromStorage(STORAGE_KEYS.BUTTON_SETS, []);
49+
private configurationTarget: string = loadFromStorage(
50+
STORAGE_KEYS.CONFIGURATION_TARGET,
51+
CONFIGURATION_TARGET.WORKSPACE
52+
);
53+
private globalData: ButtonConfig[] = loadFromStorage(STORAGE_KEYS.GLOBAL_DATA, []);
54+
private localData: ButtonConfig[] = loadFromStorage(STORAGE_KEYS.LOCAL_DATA, []);
55+
private workspaceData: ButtonConfig[] = loadFromStorage(
56+
STORAGE_KEYS.WORKSPACE_DATA,
57+
mockCommands
58+
);
2759

2860
private get currentData(): ButtonConfig[] {
2961
if (this.configurationTarget === CONFIGURATION_TARGET.LOCAL) {
@@ -37,10 +69,13 @@ class VSCodeMock {
3769
private set currentData(data: ButtonConfig[]) {
3870
if (this.configurationTarget === CONFIGURATION_TARGET.LOCAL) {
3971
this.localData = data;
72+
saveToStorage(STORAGE_KEYS.LOCAL_DATA, data);
4073
} else if (this.configurationTarget === CONFIGURATION_TARGET.WORKSPACE) {
4174
this.workspaceData = data;
75+
saveToStorage(STORAGE_KEYS.WORKSPACE_DATA, data);
4276
} else {
4377
this.globalData = data;
78+
saveToStorage(STORAGE_KEYS.GLOBAL_DATA, data);
4479
}
4580
}
4681

@@ -82,6 +117,7 @@ class VSCodeMock {
82117
const target = message.target ?? (message.data as { target?: string } | undefined)?.target;
83118
if (!target) return;
84119
this.configurationTarget = target;
120+
saveToStorage(STORAGE_KEYS.CONFIGURATION_TARGET, target);
85121
console.log("Mock VSCode changed configuration target:", this.configurationTarget);
86122
setTimeout(() => {
87123
const mockMessage = {
@@ -169,6 +205,8 @@ class VSCodeMock {
169205
};
170206
this.buttonSets.push(newSet);
171207
this.activeSet = name;
208+
saveToStorage(STORAGE_KEYS.BUTTON_SETS, this.buttonSets);
209+
saveToStorage(STORAGE_KEYS.ACTIVE_SET, this.activeSet);
172210
this.sendConfigData(message.requestId);
173211
}, 100);
174212
} else if (message.type === MESSAGE_TYPE.DELETE_BUTTON_SET) {
@@ -178,8 +216,10 @@ class VSCodeMock {
178216
setTimeout(() => {
179217
if (name) {
180218
this.buttonSets = this.buttonSets.filter((s) => s.name !== name);
219+
saveToStorage(STORAGE_KEYS.BUTTON_SETS, this.buttonSets);
181220
if (this.activeSet === name) {
182221
this.activeSet = null;
222+
saveToStorage(STORAGE_KEYS.ACTIVE_SET, this.activeSet);
183223
}
184224
}
185225
this.sendConfigData(message.requestId);
@@ -220,8 +260,10 @@ class VSCodeMock {
220260
const set = this.buttonSets.find((s) => s.name === data.currentName);
221261
if (set) {
222262
set.name = data.newName;
263+
saveToStorage(STORAGE_KEYS.BUTTON_SETS, this.buttonSets);
223264
if (this.activeSet === data.currentName) {
224265
this.activeSet = data.newName;
266+
saveToStorage(STORAGE_KEYS.ACTIVE_SET, this.activeSet);
225267
}
226268
}
227269
this.sendConfigData(message.requestId);
@@ -232,6 +274,7 @@ class VSCodeMock {
232274
console.log("Mock VSCode received setActiveSet:", setName);
233275
setTimeout(() => {
234276
this.activeSet = setName;
277+
saveToStorage(STORAGE_KEYS.ACTIVE_SET, this.activeSet);
235278
this.sendConfigData(message.requestId);
236279
}, 100);
237280
}

0 commit comments

Comments
 (0)