Skip to content

Commit 63af079

Browse files
authored
Merge pull request #16 from link-foundation/issue-15-85e23cad40a2
fix: PlaywrightAdapter.evaluateOnPage() spread multiple arguments correctly
2 parents 7ff4a21 + bca78ce commit 63af079

File tree

4 files changed

+111
-20
lines changed

4 files changed

+111
-20
lines changed
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
'browser-commander': patch
3+
---
4+
5+
Fix PlaywrightAdapter.evaluateOnPage() to spread multiple arguments correctly
6+
7+
When using `evaluateOnPage()` with multiple arguments, the arguments are now properly spread to the function in the browser context, matching Puppeteer's behavior.
8+
9+
Previously, the function would receive the entire array as its first parameter instead of spread arguments, causing issues like invalid selectors when passing selector + array combinations.

js/src/core/engine-adapter.js

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -304,13 +304,25 @@ export class PlaywrightAdapter extends EngineAdapter {
304304

305305
async evaluateOnPage(fn, args = []) {
306306
// Playwright only accepts a single argument (can be array/object)
307+
// To match Puppeteer's behavior where args are spread, we wrap the function
308+
// and pass all args as a single array, then apply them in the browser context
307309
if (args.length === 0) {
308310
return await this.page.evaluate(fn);
309311
} else if (args.length === 1) {
310312
return await this.page.evaluate(fn, args[0]);
311313
} else {
312-
// Multiple args - pass as array
313-
return await this.page.evaluate(fn, args);
314+
// Multiple args - wrap function to accept array and spread them
315+
// This makes Playwright behave like Puppeteer's spread behavior
316+
// We pass the function string and args array, then reconstruct and call in browser
317+
const fnString = fn.toString();
318+
return await this.page.evaluate(
319+
({ fnStr, argsArray }) => {
320+
// Reconstruct the function in browser context and call with spread args
321+
const reconstructedFn = new Function(`return (${fnStr})`)();
322+
return reconstructedFn(...argsArray);
323+
},
324+
{ fnStr: fnString, argsArray: args }
325+
);
314326
}
315327
}
316328

js/tests/helpers/mocks.js

Lines changed: 5 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -105,28 +105,15 @@ export function createMockPlaywrightPage(options = {}) {
105105
return locs.map((l) => l.evaluate(fn, ...args));
106106
},
107107
locator: locatorMock,
108-
evaluate: async (fn, ...args) => {
108+
// Playwright's page.evaluate() only accepts a single argument (not spread)
109+
// This is the key difference from Puppeteer
110+
evaluate: async (fn, arg) => {
109111
if (evaluateResult !== null) {
110112
return evaluateResult;
111113
}
112-
// Create mock window/document context
113-
const mockContext = {
114-
innerHeight: 800,
115-
innerWidth: 1200,
116-
sessionStorage: {
117-
_data: {},
118-
getItem: (key) => mockContext.sessionStorage._data[key] || null,
119-
setItem: (key, val) => {
120-
mockContext.sessionStorage._data[key] = val;
121-
},
122-
removeItem: (key) => {
123-
delete mockContext.sessionStorage._data[key];
124-
},
125-
},
126-
querySelectorAll: () => [],
127-
};
128114
try {
129-
return fn(...args);
115+
// Playwright passes exactly one argument (can be object/array)
116+
return fn(arg);
130117
} catch {
131118
return fn;
132119
}

js/tests/unit/core/engine-adapter.test.js

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,53 @@ describe('engine-adapter', () => {
7878
const count = await adapter.count('button');
7979
assert.strictEqual(count, 5);
8080
});
81+
82+
describe('evaluateOnPage', () => {
83+
it('should handle zero arguments', async () => {
84+
page = createMockPlaywrightPage();
85+
adapter = new PlaywrightAdapter(page);
86+
const result = await adapter.evaluateOnPage(() => 42);
87+
assert.strictEqual(result, 42);
88+
});
89+
90+
it('should handle single argument', async () => {
91+
page = createMockPlaywrightPage();
92+
adapter = new PlaywrightAdapter(page);
93+
const result = await adapter.evaluateOnPage((x) => x * 2, [5]);
94+
assert.strictEqual(result, 10);
95+
});
96+
97+
it('should handle multiple arguments by spreading them', async () => {
98+
page = createMockPlaywrightPage();
99+
adapter = new PlaywrightAdapter(page);
100+
// This is the bug case - multiple args should be spread, not passed as array
101+
const result = await adapter.evaluateOnPage((a, b) => a + b, [3, 7]);
102+
assert.strictEqual(result, 10);
103+
});
104+
105+
it('should handle selector + array arguments (real-world bug case)', async () => {
106+
page = createMockPlaywrightPage();
107+
adapter = new PlaywrightAdapter(page);
108+
// This reproduces the original bug: selector and processedIds
109+
const result = await adapter.evaluateOnPage(
110+
(selector, processedIds) =>
111+
`Selector: ${selector}, Count: ${processedIds.length}`,
112+
['[data-qa="test"]', ['id1', 'id2']]
113+
);
114+
assert.ok(result.includes('Selector: [data-qa="test"]'));
115+
assert.ok(result.includes('Count: 2'));
116+
});
117+
118+
it('should handle multiple arguments of different types', async () => {
119+
page = createMockPlaywrightPage();
120+
adapter = new PlaywrightAdapter(page);
121+
const result = await adapter.evaluateOnPage(
122+
(str, num, obj) => `${str}-${num}-${obj.key}`,
123+
['hello', 42, { key: 'world' }]
124+
);
125+
assert.strictEqual(result, 'hello-42-world');
126+
});
127+
});
81128
});
82129

83130
describe('PuppeteerAdapter', () => {
@@ -130,6 +177,42 @@ describe('engine-adapter', () => {
130177
const count = await adapter.count('button');
131178
assert.strictEqual(count, 5);
132179
});
180+
181+
describe('evaluateOnPage', () => {
182+
it('should handle zero arguments', async () => {
183+
page = createMockPuppeteerPage();
184+
adapter = new PuppeteerAdapter(page);
185+
const result = await adapter.evaluateOnPage(() => 42);
186+
assert.strictEqual(result, 42);
187+
});
188+
189+
it('should handle single argument', async () => {
190+
page = createMockPuppeteerPage();
191+
adapter = new PuppeteerAdapter(page);
192+
const result = await adapter.evaluateOnPage((x) => x * 2, [5]);
193+
assert.strictEqual(result, 10);
194+
});
195+
196+
it('should handle multiple arguments by spreading them', async () => {
197+
page = createMockPuppeteerPage();
198+
adapter = new PuppeteerAdapter(page);
199+
// Puppeteer natively spreads args - ensure same behavior as Playwright fix
200+
const result = await adapter.evaluateOnPage((a, b) => a + b, [3, 7]);
201+
assert.strictEqual(result, 10);
202+
});
203+
204+
it('should handle selector + array arguments (parity with PlaywrightAdapter)', async () => {
205+
page = createMockPuppeteerPage();
206+
adapter = new PuppeteerAdapter(page);
207+
const result = await adapter.evaluateOnPage(
208+
(selector, processedIds) =>
209+
`Selector: ${selector}, Count: ${processedIds.length}`,
210+
['[data-qa="test"]', ['id1', 'id2']]
211+
);
212+
assert.ok(result.includes('Selector: [data-qa="test"]'));
213+
assert.ok(result.includes('Count: 2'));
214+
});
215+
});
133216
});
134217

135218
describe('createEngineAdapter', () => {

0 commit comments

Comments
 (0)