Files
vdm/extension/tests/context-menus.test.ts
T
samiandClaude Sonnet 5 ec288db5ea ext: context-menus.ts — link/media menu items + grab-tab command
docs/05 §3: "Download with Velox" on a link (linkUrl) or a media element
(srcUrl) hands that one URL to download.add with the page as referrer; a
configurable command (Ctrl+Shift+U) does the same for the active tab. These
are explicit user requests, so they skip shouldCapture and the fail-open
path — a failure is surfaced to the user via notifications instead.

The page/selection "Download all links…" item waits on the content-script
link harvester (build-order step 7) and will be added there.

manifest: commands.velox-grab-current-tab. 9 tests. 110 total, lint clean.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_012Y9RU58hD1BuwP82DySUHk
2026-09-10 15:41:54 +04:00

135 lines
5.3 KiB
TypeScript

import { describe, expect, it, vi } from 'vitest';
import type { DownloadSpec } from '../src/shared/protocol/index.js';
import {
ContextMenus,
GRAB_TAB_COMMAND,
LINK_MENU_ID,
MEDIA_MENU_ID,
type ContextMenusDeps,
} from '../src/background/context-menus.js';
class Event<A extends unknown[]> {
fns: Array<(...a: A) => void> = [];
addListener = (f: (...a: A) => void): void => void this.fns.push(f);
removeListener = (f: (...a: A) => void): void => {
this.fns = this.fns.filter((x) => x !== f);
};
emit(...a: A): void {
for (const f of this.fns) f(...a);
}
}
function harness(over: Partial<ContextMenusDeps> = {}) {
const created: Array<{ id: string; contexts: string[] }> = [];
const onClicked = new Event<[{ menuItemId: string | number; linkUrl?: string; srcUrl?: string; pageUrl?: string }, unknown]>();
const onCommand = new Event<[string, unknown]>();
const menus = {
create: (p: { id: string; title: string; contexts: string[] }) => void created.push({ id: p.id, contexts: p.contexts }),
removeAll: vi.fn(async () => undefined),
onClicked,
};
const addDownload = vi.fn(async (_spec: DownloadSpec) => ({ taskId: 't' }));
const tabs = { query: vi.fn(async () => [{ url: 'https://example.com/watch' }]) };
const onError = vi.fn();
const deps: ContextMenusDeps = {
addDownload: addDownload as unknown as ContextMenusDeps['addDownload'],
menus: menus as unknown as ContextMenusDeps['menus'],
tabs: tabs as unknown as ContextMenusDeps['tabs'],
commands: { onCommand } as unknown as ContextMenusDeps['commands'],
onError,
...over,
};
return { cm: new ContextMenus(deps), created, onClicked, onCommand, addDownload, tabs, onError, menus };
}
describe('ContextMenus', () => {
it('registers a link item and a media item after clearing stale ones', async () => {
const { cm, created, menus } = harness();
await cm.register();
expect(menus.removeAll).toHaveBeenCalled();
expect(created).toEqual([
{ id: LINK_MENU_ID, contexts: ['link'] },
{ id: MEDIA_MENU_ID, contexts: ['image', 'video', 'audio'] },
]);
});
it('a link click sends linkUrl + pageUrl referrer to download.add', async () => {
const { cm, onClicked, addDownload } = harness();
await cm.register();
onClicked.emit(
{ menuItemId: LINK_MENU_ID, linkUrl: 'https://cdn.example.com/a.zip', pageUrl: 'https://example.com/p' },
undefined,
);
await Promise.resolve();
expect(addDownload).toHaveBeenCalledWith({
url: 'https://cdn.example.com/a.zip',
referrer: 'https://example.com/p',
} satisfies DownloadSpec);
});
it('a media click uses srcUrl', async () => {
const { cm, onClicked, addDownload } = harness();
await cm.register();
onClicked.emit({ menuItemId: MEDIA_MENU_ID, srcUrl: 'https://v.example.com/clip.mp4', pageUrl: 'https://v.example.com' }, undefined);
await Promise.resolve();
expect(addDownload.mock.calls[0]![0].url).toBe('https://v.example.com/clip.mp4');
});
it('ignores a click on some other extension menu item', async () => {
const { cm, onClicked, addDownload } = harness();
await cm.register();
onClicked.emit({ menuItemId: 'someone-elses-item', linkUrl: 'https://x/a.zip' }, undefined);
await Promise.resolve();
expect(addDownload).not.toHaveBeenCalled();
});
it('rejects a non-http link with a user-facing error, no download', async () => {
const { cm, onClicked, addDownload, onError } = harness();
await cm.register();
onClicked.emit({ menuItemId: LINK_MENU_ID, linkUrl: 'ftp://legacy/a.zip' }, undefined);
await Promise.resolve();
expect(addDownload).not.toHaveBeenCalled();
expect(onError).toHaveBeenCalledWith(expect.stringMatching(/http/));
});
it('the grab-tab command downloads the active tab URL', async () => {
const { cm, onCommand, addDownload, tabs } = harness();
await cm.register();
onCommand.emit(GRAB_TAB_COMMAND, undefined);
await Promise.resolve();
await Promise.resolve();
expect(tabs.query).toHaveBeenCalled();
expect(addDownload.mock.calls[0]![0].url).toBe('https://example.com/watch');
});
it('the grab-tab command prefers the tab handed to the listener', async () => {
const { cm, onCommand, addDownload, tabs } = harness();
await cm.register();
onCommand.emit(GRAB_TAB_COMMAND, { url: 'https://direct.example/page' });
await Promise.resolve();
expect(tabs.query).not.toHaveBeenCalled();
expect(addDownload.mock.calls[0]![0].url).toBe('https://direct.example/page');
});
it('surfaces a download.add failure through onError', async () => {
const { cm, onClicked, onError } = harness({
addDownload: vi.fn().mockRejectedValue(new Error('daemon down')) as unknown as ContextMenusDeps['addDownload'],
});
await cm.register();
onClicked.emit({ menuItemId: LINK_MENU_ID, linkUrl: 'https://x/a.zip' }, undefined);
await Promise.resolve();
await Promise.resolve();
expect(onError).toHaveBeenCalledWith(expect.stringMatching(/daemon down/));
});
it('dispose() detaches listeners', async () => {
const { cm, onClicked, addDownload } = harness();
await cm.register();
cm.dispose();
onClicked.emit({ menuItemId: LINK_MENU_ID, linkUrl: 'https://x/a.zip' }, undefined);
await Promise.resolve();
expect(addDownload).not.toHaveBeenCalled();
});
});