feat(n8n): expand browser-cli node operations
Testing / remote-protocol-compat (0.9.3) (push) Successful in 46s
Testing / remote-protocol-compat (0.9.5) (push) Successful in 45s
Testing / test (push) Successful in 40s

- Add UI resources and mappings for groups, windows, sessions, storage, performance, extension, and more tab/DOM/page actions.

- Remove the synthetic Gateway Health operation so exposed operations match real browser-cli commands.

- Document the expanded command/policy matrix and cover the new request mappings with tests.

- Cap the node SVG icon at 60x60, bump the n8n package to 0.3.0, and advertise client protocol version 0.16.0.
This commit is contained in:
2026-06-19 11:55:36 +02:00
parent 2c38cc8874
commit b91b29d516
8 changed files with 827 additions and 42 deletions
+120 -4
View File
@@ -11,10 +11,6 @@ test('client:list maps to clients.list', () => {
assert.deepEqual(buildCommand('client', 'list', {}), { command: 'clients.list', args: {} });
});
test('gateway:health pings with tabs.list (serve has no health route)', () => {
assert.deepEqual(buildCommand('gateway', 'health', {}), { command: 'tabs.list', args: {} });
});
test('tab:open sends navigate.open with background derived from focus', () => {
const bg = buildCommand('tab', 'open', { url: 'https://example.com', focus: false });
assert.deepEqual(bg, {
@@ -60,8 +56,128 @@ test('command:execute passes command and args through', () => {
});
});
test('tab read ops map to safe commands', () => {
assert.deepEqual(buildCommand('tab', 'query', { search: 'docs' }), { command: 'tabs.query', args: { search: 'docs' } });
assert.deepEqual(buildCommand('tab', 'filter', { pattern: '*.dev/*' }), { command: 'tabs.filter', args: { pattern: '*.dev/*' } });
assert.deepEqual(buildCommand('tab', 'count', { pattern: '' }).args, {}, 'empty pattern is dropped');
assert.deepEqual(buildCommand('tab', 'get', { tabId: 0 }).args, {}, 'tabId 0 means active tab');
assert.deepEqual(buildCommand('tab', 'get', { tabId: 5 }), { command: 'tabs.status', args: { tabId: 5 } });
assert.deepEqual(buildCommand('tab', 'activeInWindow', { windowId: 3 }), {
command: 'tabs.active_in_window',
args: { windowId: 3 },
});
});
test('tab control ops map to navigate/tabs commands', () => {
assert.deepEqual(buildCommand('tab', 'activate', { tabId: 7 }), { command: 'tabs.active', args: { tabId: 7 } });
assert.deepEqual(buildCommand('tab', 'navigateTo', { tabId: 7, url: 'https://x.dev' }), {
command: 'navigate.to',
args: { tabId: 7, url: 'https://x.dev' },
});
assert.deepEqual(buildCommand('tab', 'reload', { tabId: 0 }), { command: 'navigate.reload', args: {} });
assert.deepEqual(buildCommand('tab', 'back', { tabId: 0 }).command, 'navigate.back');
assert.deepEqual(buildCommand('tab', 'mute', { tabId: 2 }), { command: 'tabs.mute', args: { tabId: 2 } });
assert.deepEqual(buildCommand('tab', 'pin', { tabId: 0 }), { command: 'tabs.pin', args: {} });
});
test('tab move keeps index 0 but drops active tabId fallback for window', () => {
assert.deepEqual(buildCommand('tab', 'move', { tabId: 4, windowId: 0, index: 0 }), {
command: 'tabs.move',
args: { tabId: 4, index: 0 },
});
assert.deepEqual(buildCommand('tab', 'move', { tabId: 4, windowId: 9, index: '' }).args, { tabId: 4, windowId: 9 });
});
test('tab rearrange ops default gentleMode and sort key', () => {
assert.deepEqual(buildCommand('tab', 'dedupe', {}), { command: 'tabs.dedupe', args: { gentleMode: 'auto' } });
assert.deepEqual(buildCommand('tab', 'sort', { by: 'title' }), {
command: 'tabs.sort',
args: { by: 'title', gentleMode: 'auto' },
});
assert.deepEqual(buildCommand('tab', 'mergeWindows', {}).command, 'tabs.merge_windows');
});
test('tab screenshot includes quality only when set', () => {
assert.deepEqual(buildCommand('tab', 'screenshot', { tabId: 0, format: 'png', quality: '' }).args, { format: 'png' });
assert.deepEqual(buildCommand('tab', 'screenshot', { tabId: 1, format: 'jpeg', quality: 80 }).args, {
tabId: 1,
format: 'jpeg',
quality: 80,
});
});
test('dom interaction ops map to dom.* commands', () => {
assert.deepEqual(buildCommand('dom', 'attr', { selector: 'a', attr: 'href' }), {
command: 'dom.attr',
args: { selector: 'a', attr: 'href' },
});
assert.deepEqual(buildCommand('dom', 'select', { selector: '#s', value: 'v' }), {
command: 'dom.select',
args: { selector: '#s', value: 'v' },
});
assert.deepEqual(buildCommand('dom', 'key', { key: 'Enter', selector: '' }).args, { key: 'Enter' });
assert.deepEqual(buildCommand('dom', 'scroll', { selector: '', x: '', y: 500 }).args, { y: 500 });
assert.deepEqual(buildCommand('dom', 'exists', { selector: '#x' }), { command: 'dom.exists', args: { selector: '#x' } });
});
test('page extractJson sends selector', () => {
assert.deepEqual(buildCommand('page', 'extractJson', { selector: 'script' }), {
command: 'extract.json',
args: { selector: 'script' },
});
});
test('group ops map to group.* commands', () => {
assert.deepEqual(buildCommand('group', 'create', { name: 'Research' }), { command: 'group.open', args: { name: 'Research' } });
assert.deepEqual(buildCommand('group', 'tabs', { groupId: 3 }), { command: 'group.tabs', args: { groupId: 3 } });
assert.deepEqual(buildCommand('group', 'addTab', { group: 'Research', url: '' }).args, { group: 'Research' });
assert.deepEqual(buildCommand('group', 'move', { group: '5', direction: 'backward' }), {
command: 'group.move',
args: { group: '5', forward: false, backward: true },
});
assert.deepEqual(buildCommand('group', 'close', { groupId: 2, gentleMode: 'off' }).args, { groupId: 2, gentleMode: 'off' });
});
test('window ops map to windows.* commands', () => {
assert.deepEqual(buildCommand('window', 'open', { url: '' }), { command: 'windows.open', args: {} });
assert.deepEqual(buildCommand('window', 'rename', { windowId: 1, name: 'Work' }), {
command: 'windows.rename',
args: { windowId: 1, name: 'Work' },
});
});
test('session ops map to session.* commands', () => {
assert.deepEqual(buildCommand('session', 'save', { name: 'morning' }), { command: 'session.save', args: { name: 'morning' } });
assert.deepEqual(buildCommand('session', 'export', { name: '' }), { command: 'session.export', args: {} });
assert.deepEqual(buildCommand('session', 'diff', { nameA: 'a', nameB: 'b' }).args, { nameA: 'a', nameB: 'b' });
assert.deepEqual(buildCommand('session', 'autoSave', { enabled: false }), {
command: 'session.auto_save',
args: { enabled: false },
});
});
test('storage ops default to local and drop active tabId', () => {
assert.deepEqual(buildCommand('storage', 'get', { key: 'token', storeType: 'local', tabId: 0 }), {
command: 'storage.get',
args: { key: 'token', type: 'local' },
});
assert.deepEqual(buildCommand('storage', 'set', { key: 'k', value: 'v', storeType: 'session', tabId: 4 }), {
command: 'storage.set',
args: { key: 'k', value: 'v', type: 'session', tabId: 4 },
});
assert.deepEqual(buildCommand('storage', 'get', { key: '', storeType: 'local', tabId: 0 }).args, { type: 'local' });
});
test('perf and extension ops map to safe/control commands', () => {
assert.deepEqual(buildCommand('perf', 'status', {}), { command: 'perf.status', args: {} });
assert.deepEqual(buildCommand('extension', 'capabilities', {}), { command: 'extension.capabilities', args: {} });
assert.deepEqual(buildCommand('extension', 'reload', {}), { command: 'extension.reload', args: {} });
});
test('unknown operation throws', () => {
assert.throws(() => buildCommand('tab', 'nope', {}), /Unsupported operation/);
assert.throws(() => buildCommand('connection', 'health', {}), /Unsupported operation/);
assert.throws(() => buildCommand('gateway', 'health', {}), /Unsupported operation/);
});
test('parseTabIds accepts array, json, and delimited strings', () => {