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
135 lines
5.3 KiB
TypeScript
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();
|
|
});
|
|
});
|