From bfa8db7045f0cd3233d1f33eb6899e237dbf009e Mon Sep 17 00:00:00 2001 From: "n8n-assistant[bot]" <100856346+n8n-assistant[bot]@users.noreply.github.com> Date: Mon, 6 Jul 2026 12:43:21 +0000 Subject: [PATCH] fix(core): Support element refs when capturing browser secrets (no-changelog) (backport to release-candidate/2.29.x) (#33661) Co-authored-by: Bernhard Wittmann --- .../playwright.get-element-value.test.ts | 85 +++++++++++++++++++ .../mcp-browser/src/adapters/playwright.ts | 12 ++- 2 files changed, 95 insertions(+), 2 deletions(-) create mode 100644 packages/@n8n/mcp-browser/src/adapters/playwright.get-element-value.test.ts diff --git a/packages/@n8n/mcp-browser/src/adapters/playwright.get-element-value.test.ts b/packages/@n8n/mcp-browser/src/adapters/playwright.get-element-value.test.ts new file mode 100644 index 00000000000..e93389dc4dc --- /dev/null +++ b/packages/@n8n/mcp-browser/src/adapters/playwright.get-element-value.test.ts @@ -0,0 +1,85 @@ +import type { Page } from 'playwright-core'; + +import { PlaywrightAdapter } from './playwright'; +import { configureLogger } from '../logger'; +import type { ResolvedConfig } from '../types'; + +configureLogger({ level: 'silent' }); + +const config: ResolvedConfig = { + defaultBrowser: 'chrome', + browsers: new Map(), + adapter: 'playwright', + mode: 'local', +}; + +interface FakeLocator { + count: ReturnType; + inputValue: ReturnType; + innerText: ReturnType; +} + +function adapterWithLocator(pageId: string, locator: FakeLocator): PlaywrightAdapter { + const adapter = new PlaywrightAdapter(config); + const page = { + locator: vi.fn().mockReturnValue(locator), + } as unknown as Page; + const pageStates = (adapter as unknown as { pageStates: Map }).pageStates; + pageStates.set(pageId, { page }); + return adapter; +} + +describe('PlaywrightAdapter.getElementValue', () => { + it('returns the live input value for a ref target', async () => { + const locator: FakeLocator = { + count: vi.fn().mockResolvedValue(1), + inputValue: vi.fn().mockResolvedValue('xoxb-signing-secret'), + innerText: vi.fn(), + }; + const adapter = adapterWithLocator('p1', locator); + + const value = await adapter.getElementValue('p1', { ref: 'e5' }); + + expect(value).toBe('xoxb-signing-secret'); + expect(locator.inputValue).toHaveBeenCalled(); + }); + + it('falls back to inner text when the element is not an input', async () => { + const locator: FakeLocator = { + count: vi.fn().mockResolvedValue(1), + inputValue: vi.fn().mockRejectedValue(new Error('Node is not an ')), + innerText: vi.fn().mockResolvedValue('secret-in-code-block'), + }; + const adapter = adapterWithLocator('p1', locator); + + const value = await adapter.getElementValue('p1', { ref: 'e5' }); + + expect(value).toBe('secret-in-code-block'); + }); + + it('falls back to inner text when the input value is empty', async () => { + const locator: FakeLocator = { + count: vi.fn().mockResolvedValue(1), + inputValue: vi.fn().mockResolvedValue(''), + innerText: vi.fn().mockResolvedValue('text-content'), + }; + const adapter = adapterWithLocator('p1', locator); + + const value = await adapter.getElementValue('p1', { ref: 'e5' }); + + expect(value).toBe('text-content'); + }); + + it('reads the value for a selector target', async () => { + const locator: FakeLocator = { + count: vi.fn().mockResolvedValue(1), + inputValue: vi.fn().mockResolvedValue('from-selector'), + innerText: vi.fn(), + }; + const adapter = adapterWithLocator('p1', locator); + + const value = await adapter.getElementValue('p1', { selector: '#token' }); + + expect(value).toBe('from-selector'); + }); +}); diff --git a/packages/@n8n/mcp-browser/src/adapters/playwright.ts b/packages/@n8n/mcp-browser/src/adapters/playwright.ts index 1be6bd140a3..94cad0fc721 100644 --- a/packages/@n8n/mcp-browser/src/adapters/playwright.ts +++ b/packages/@n8n/mcp-browser/src/adapters/playwright.ts @@ -1039,8 +1039,16 @@ export class PlaywrightAdapter { } } - getElementValue(_pageId: string, _target: ElementTarget): never { - throw new Error('Not implemented'); + async getElementValue(pageId: string, target: ElementTarget): Promise { + const locator = await this.resolveLocator(pageId, target); + let value = ''; + try { + value = await locator.inputValue(); + } catch { + // Not an /