Skip to content

Commit b70d636

Browse files
feat(interface): add clear cache action to command palette (#757)
1 parent dd1dc47 commit b70d636

17 files changed

Lines changed: 197 additions & 16 deletions

File tree

.changeset/chilly-parts-cross.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@nodesecure/server": minor
3+
---
4+
5+
Add `CLEAR` WebSocket command to clear the payload cache

.changeset/famous-plants-lay.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@nodesecure/cache": minor
3+
---
4+
5+
Add support for `NODESECURE_PAYLOADS_PATH` env

.changeset/many-clowns-deny.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@nodesecure/cache": patch
3+
---
4+
5+
Reset `currentSpec` to `null` when manifest load fails

i18n/english.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,7 @@ const ui = {
284284
action_reset_view: "Reset view",
285285
action_copy_packages: "Copy packages",
286286
action_export_payload: "Export payload",
287+
action_clear_cache: "Clear all cached packages",
287288
section_presets: "Quick filters",
288289
preset_has_vulnerabilities: "Has vulnerabilities",
289290
preset_has_scripts: "Has install scripts",

i18n/french.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,7 @@ const ui = {
284284
action_reset_view: "Réinitialiser la vue",
285285
action_copy_packages: "Copier les packages",
286286
action_export_payload: "Exporter le payload",
287+
action_clear_cache: "Vider tous les packages en cache",
287288
section_presets: "Filtres rapides",
288289
preset_has_vulnerabilities: "Contient des vulnérabilités",
289290
preset_has_scripts: "Scripts d'installation",

playwright.config.js

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,25 @@
1+
// Import Node.js Dependencies
2+
import os from "node:os";
3+
import path from "node:path";
4+
15
// Import Third-party Dependencies
26
import { defineConfig } from "@playwright/test";
37

8+
// CONSTANTS
9+
const kE2ECachePath = path.join(os.tmpdir(), "nsecure-e2e-payloads");
10+
411
export default defineConfig({
512
testDir: "./test/e2e",
13+
globalTeardown: "./test/e2e/global-teardown.js",
614
use: {
715
baseURL: "http://localhost:3000"
816
},
917
webServer: {
1018
command: "node . open ./test/e2e/fixtures/nsecure-result.json --port 3000 --ws-port 1339",
11-
env: { NODESECURE_NO_OPEN: true },
19+
env: {
20+
NODESECURE_NO_OPEN: true,
21+
NODESECURE_PAYLOADS_PATH: kE2ECachePath
22+
},
1223
port: 3000,
1324
timeout: 15_000
1425
}

public/components/command-palette/command-palette.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,8 @@ const kActions = [
3434
{ id: "toggle_theme", shortcut: "t" },
3535
{ id: "reset_view", shortcut: "r" },
3636
{ id: "copy_packages", shortcut: "c" },
37-
{ id: "export_payload", shortcut: "e" }
37+
{ id: "export_payload", shortcut: "e" },
38+
{ id: "clear_cache", shortcut: "x" }
3839
];
3940
const kWarningItems = Object.keys(warnings)
4041
.map((id) => {
@@ -432,6 +433,9 @@ class CommandPalette extends LitElement {
432433
}
433434
break;
434435
}
436+
case "clear_cache":
437+
window.socket.commands.clear();
438+
break;
435439
}
436440

437441
this.#close();
@@ -577,6 +581,9 @@ class CommandPalette extends LitElement {
577581
case "export_payload":
578582
label = i18n.action_export_payload;
579583
break;
584+
case "clear_cache":
585+
label = i18n.action_clear_cache;
586+
break;
580587
default:
581588
label = action.id;
582589
}

public/main.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -224,9 +224,10 @@ async function onSocketInitOrReload(event) {
224224
searchview.cachedSpecs = cache;
225225
searchview.reset();
226226

227-
if (data.status === "RELOAD" && cache.length === 0) {
227+
if (cache.length === 0) {
228228
window.navigation.hideMenu("network--view");
229229
window.navigation.hideMenu("home--view");
230+
window.navigation.hideMenu("tree--view");
230231
window.navigation.hideMenu("warnings--view");
231232
window.navigation.setNavByName("search--view");
232233
}

public/websocket.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ export class WebSocketClient extends EventTarget {
1919
*/
2020
this.commands = {
2121
search: (spec) => this.send({ commandName: "SEARCH", spec }),
22-
remove: (spec) => this.send({ commandName: "REMOVE", spec })
22+
remove: (spec) => this.send({ commandName: "REMOVE", spec }),
23+
clear: () => this.send({ commandName: "CLEAR" })
2324
};
2425

2526
window.socket = this;

test/e2e/command-palette.spec.js

Lines changed: 102 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ test.describe("[command-palette] presets and actions", () => {
1616

1717
test.beforeEach(async({ page }) => {
1818
await page.goto("/");
19-
await page.waitForSelector(`[data-menu="network--view"].active`);
19+
await page.waitForFunction(() => window.cachedSpecs?.length > 0);
2020

2121
i18n = await page.evaluate(() => {
2222
const lang = document.getElementById("lang").dataset.lang;
@@ -40,9 +40,9 @@ test.describe("[command-palette] presets and actions", () => {
4040
await expect(presetsSection.locator(".range-preset")).toHaveCount(5);
4141
});
4242

43-
test("renders all four action buttons", async({ page }) => {
43+
test("renders all five action buttons", async({ page }) => {
4444
const actionsSection = page.locator(".section").filter({ hasText: i18n.section_actions });
45-
await expect(actionsSection.locator(".range-preset")).toHaveCount(4);
45+
await expect(actionsSection.locator(".range-preset")).toHaveCount(5);
4646
});
4747

4848
test("clicking a preset adds a chip and hides the presets section", async({ page }) => {
@@ -186,7 +186,7 @@ test.describe("[command-palette] dep filter", () => {
186186

187187
test.beforeEach(async({ page }) => {
188188
await page.goto("/");
189-
await page.waitForSelector(`[data-menu="network--view"].active`);
189+
await page.waitForFunction(() => window.cachedSpecs?.length > 0);
190190

191191
i18n = await page.evaluate(() => {
192192
const lang = document.getElementById("lang").dataset.lang;
@@ -268,7 +268,7 @@ test.describe("[command-palette] ignore flags and warnings", () => {
268268

269269
test.beforeEach(async({ page }) => {
270270
await page.goto("/");
271-
await page.waitForSelector(`[data-menu="network--view"].active`);
271+
await page.waitForFunction(() => window.cachedSpecs?.length > 0);
272272

273273
i18n = await page.evaluate(() => {
274274
const lang = document.getElementById("lang").dataset.lang;
@@ -369,3 +369,100 @@ test.describe("[command-palette] ignore flags and warnings", () => {
369369
await expect(page.locator(".section").filter({ hasText: i18n.section_ignore_warnings })).not.toBeVisible();
370370
});
371371
});
372+
373+
test.describe("[command-palette] clear cache action", () => {
374+
async function loadI18n(page) {
375+
return page.evaluate(() => {
376+
const lang = document.getElementById("lang").dataset.lang;
377+
const activeLang = lang in window.i18n ? lang : "english";
378+
379+
return window.i18n[activeLang].search_command;
380+
});
381+
}
382+
383+
async function openPalette(page) {
384+
await page.goto("/");
385+
await page.waitForFunction(() => window.cachedSpecs?.length > 0);
386+
await page.locator(`[data-menu="network--view"].active`).click();
387+
await page.keyboard.press("Control+k");
388+
await expect(page.locator(".backdrop")).toBeVisible();
389+
}
390+
391+
test("renders the clear cache action button", async({ page }) => {
392+
await openPalette(page);
393+
const i18n = await loadI18n(page);
394+
395+
const actionsSection = page.locator(".section").filter({ hasText: i18n.section_actions });
396+
await expect(actionsSection.locator(".range-preset").filter({ hasText: i18n.action_clear_cache })).toBeVisible();
397+
});
398+
399+
// Alt+X is tested via WebSocket interception so the server cache is not actually
400+
// cleared, keeping network--view available for the next test.
401+
test("Alt+X sends the CLEAR command and closes the palette", async({ page }) => {
402+
let clearSent = false;
403+
404+
await page.routeWebSocket("ws://localhost:1339", (ws) => {
405+
const server = ws.connectToServer();
406+
407+
ws.onMessage((msg) => {
408+
const data = JSON.parse(msg);
409+
if (data.commandName === "CLEAR") {
410+
clearSent = true;
411+
ws.send(JSON.stringify({ status: "RELOAD", cache: [] }));
412+
}
413+
else {
414+
server.send(msg);
415+
}
416+
});
417+
418+
server.onMessage((msg) => {
419+
ws.send(msg);
420+
});
421+
});
422+
423+
await openPalette(page);
424+
425+
await page.keyboard.press("Alt+x");
426+
427+
await expect(page.locator(".backdrop")).not.toBeVisible();
428+
await page.waitForSelector(`[data-menu="search--view"].active`);
429+
expect(clearSent).toBe(true);
430+
});
431+
432+
test("clicking clear cache closes the palette and hides data views", async({ page }) => {
433+
let clearSent = false;
434+
435+
await page.routeWebSocket("ws://localhost:1339", (ws) => {
436+
const server = ws.connectToServer();
437+
438+
ws.onMessage((msg) => {
439+
const data = JSON.parse(msg);
440+
if (data.commandName === "CLEAR") {
441+
clearSent = true;
442+
ws.send(JSON.stringify({ status: "RELOAD", cache: [] }));
443+
}
444+
else {
445+
server.send(msg);
446+
}
447+
});
448+
449+
server.onMessage((msg) => {
450+
ws.send(msg);
451+
});
452+
});
453+
454+
await openPalette(page);
455+
const i18n = await loadI18n(page);
456+
457+
const actionsSection = page.locator(".section").filter({ hasText: i18n.section_actions });
458+
await actionsSection.locator(".range-preset").filter({ hasText: i18n.action_clear_cache }).click();
459+
460+
await expect(page.locator(".backdrop")).not.toBeVisible();
461+
await page.waitForSelector(`[data-menu="search--view"].active`);
462+
expect(clearSent).toBe(true);
463+
464+
for (const menu of ["network--view", "home--view", "tree--view", "warnings--view"]) {
465+
await expect(page.locator(`[data-menu="${menu}"]`)).toContainClass("hidden");
466+
}
467+
});
468+
});

0 commit comments

Comments
 (0)