Skip to content

Conversation

krichprollsch
Copy link
Member

@krichprollsch krichprollsch commented Sep 6, 2025

Adjust CDP support to improve compatibility with patchright-nodejs client.

Some issues in progress

🟠 target.createBrowserContext with disposeOnDetach: true parameter

patchright sends disposeOnDetach: true. We didn't implement it for now.
ℹ️ It raises a warning.

https://chromedevtools.github.io/devtools-protocol/tot/Target/#method-createBrowserContext

🟠 DOM.describeNode with pierce: true parameter

patchright sends pierce: true. We didn't implement it for now.
ℹ️ I turned the error into a log warning.

https://chromedevtools.github.io/devtools-protocol/tot/DOM/#method-describeNode

✔️ Two successive Page.createIsolatedWorld calls

> {"id":38,"method":"Page.createIsolatedWorld","params":{"frameId":"TID-1","grantUniveralAccess":true,"worldName":"utility"},"sessionId":"SID-1"}
< {"id":38,"result":{"executionContextId":2},"sessionId":"SID-1"}
[...]
> {"id":64,"method":"Page.createIsolatedWorld","params":{"frameId":"TID-1","grantUniveralAccess":true,"worldName":"utility"},"sessionId":"SID-1"}
< {"id":64,"error":{"code":-31998,"message":"CurrentlyOnly1IsolatedWorldSupported"},"sessionId":"SID-1"}

Relates with https://github.com/lightpanda-io/project/discussions/163 for the risks of reusing the same world+context.

In order to support 2 isolated world we only have to change isolated_world: ?IsolatedWorld, to isolated_world: [2]?IsolatedWorld, or a dynamic array, and fix access to it (cdp functions that take an executionContextId).
Lazy creation and of the worlds and such is already prepared for it

ℹ️ implemented in bc1ad98

❌ JS exection error DispatchRequest

INFO  js : function call error . . . . . . . . . . . . . . .  [+1843ms]
      name = browser.dom.event_target.EventTarget._dispatchEvent
      err = DispatchRequest
      args =
        1: #<CustomEvent> (object)
      stack =
        markTargetElements:7436
        eval:3
        evaluate:293
        <anonymous>:1
< {"id":237,"result":{"result":{"type":"object","className":"DOMException","description":"DOMException","objectId":"5987356902031041503.5.27","preview":{"type":"object","description":"DOMException","overflow":false,"properties":[{"name":"code","type":"number","value":"129"},{"name":"name","type":"string","value":"DispatchRequestError"},{"name":"message","type":"string","value":"Failed to execute 'dispatchEvent' : DispatchRequestError"},{"name":"Symbol(Symbol.toStringTag)","type":"string","value":"DOMException"}]}},"exceptionDetails":{"exceptionId":1,"text":"Uncaught","lineNumber":7435,"columnNumber":16,"scriptId":"8","exception":{"type":"object","className":"DOMException","description":"DOMException","objectId":"5987356902031041503.5.28","preview":{"type":"object","description":"DOMException","overflow":false,"properties":[{"name":"code","type":"number","value":"129"},{"name":"name","type":"string","value":"DispatchRequestError"},{"name":"message","type":"string","value":"Failed to execute 'dispatchEvent' : DispatchRequestError"},{"name":"Symbol(Symbol.toStringTag)","type":"string","value":"DOMException"}]}}}},"sessionId":"SID-1"}
> {"id":238,"method":"Runtime.callFunctionOn","params":{"functionDeclaration":"(utilityScript, ...args) => utilityScript.evaluate(...args)","objectId":"5987356902031041503.5.3","arguments":[{"objectId":"5987356902031041503.5.3"},{"value":true},{"value":false},{"value":"(injected, { error }) => {\n        throw error;\n      }"},{"value":2},{"value":{"h":0}},{"value":{"o":[{"k":"error","v":{"e":{"n":"Error","m":"DOMException","s":"Error: DOMException\n    at CRExecutionContext.evaluateWithArguments (/home/pierre/wrk/patchright/node_modules/patchright-core/lib/server/chromium/crExecutionContext.js:80:13)\n    at async LongStandingScope._race (/home/pierre/wrk/patchright/node_modules/patchright-core/lib/utils/isomorphic/manualPromise.js:94:14)\n    at async evaluateExpression (/home/pierre/wrk/patchright/node_modules/patchright-core/lib/server/javascript.js:233:12)\n    at async ElementHandle.evaluateHandleInUtility (/home/pierre/wrk/patchright/node_modules/patchright-core/lib/server/dom.js:133:14)\n    at async Frame._customFindElementsByParsed (/home/pierre/wrk/patchright/node_modules/patchright-core/lib/server/frames.js:1665:32)\n    at async Frame._retryWithoutProgress (/home/pierre/wrk/patchright/node_modules/patchright-core/lib/server/frames.js:1546:32)\n    at async Frame.retryWithProgressAndTimeouts (/home/pierre/wrk/patchright/node_modules/patchright-core/lib/server/frames.js:943:24)\n    at async FrameDispatcher.textContent (/home/pierre/wrk/patchright/node_modules/patchright-core/lib/server/dispatchers/frameDispatcher.js:138:19)\n    at async ProgressController.run (/home/pierre/wrk/patchright/node_modules/patchright-core/lib/server/progress.js:78:22)\n    at async FrameDispatcher._runCommand (/home/pierre/wrk/patchright/node_modules/patchright-core/lib/server/dispatchers/dispatcher.js:95:14)"}}}],"id":1}},{"objectId":"5987356902031041503.5.1"}],"returnByValue":false,"awaitPromise":true,"userGesture":true},"sessionId":"SID-1"}

❌ JS execution error DOMException\n at CRExecutionContext.evaluateWithArguments

> {"id":238,"method":"Runtime.callFunctionOn","params":{"functionDeclaration":"(utilityScript, ...args) => utilityScript.evaluate(...args)","objectId":"5987356902031041503.5.3","arguments":[{"objectId":"5987356902031041503.5.3"},{"value":true},{"value":false},{"value":"(injected, { error }) => {\n        throw error;\n      }"},{"value":2},{"value":{"h":0}},{"value":{"o":[{"k":"error","v":{"e":{"n":"Error","m":"DOMException","s":"Error: DOMException\n    at CRExecutionContext.evaluateWithArguments (/home/pierre/wrk/patchright/node_modules/patchright-core/lib/server/chromium/crExecutionContext.js:80:13)\n    at async LongStandingScope._race (/home/pierre/wrk/patchright/node_modules/patchright-core/lib/utils/isomorphic/manualPromise.js:94:14)\n    at async evaluateExpression (/home/pierre/wrk/patchright/node_modules/patchright-core/lib/server/javascript.js:233:12)\n    at async ElementHandle.evaluateHandleInUtility (/home/pierre/wrk/patchright/node_modules/patchright-core/lib/server/dom.js:133:14)\n    at async Frame._customFindElementsByParsed (/home/pierre/wrk/patchright/node_modules/patchright-core/lib/server/frames.js:1665:32)\n    at async Frame._retryWithoutProgress (/home/pierre/wrk/patchright/node_modules/patchright-core/lib/server/frames.js:1546:32)\n    at async Frame.retryWithProgressAndTimeouts (/home/pierre/wrk/patchright/node_modules/patchright-core/lib/server/frames.js:943:24)\n    at async FrameDispatcher.textContent (/home/pierre/wrk/patchright/node_modules/patchright-core/lib/server/dispatchers/frameDispatcher.js:138:19)\n    at async ProgressController.run (/home/pierre/wrk/patchright/node_modules/patchright-core/lib/server/progress.js:78:22)\n    at async FrameDispatcher._runCommand (/home/pierre/wrk/patchright/node_modules/patchright-core/lib/server/dispatchers/dispatcher.js:95:14)"}}}],"id":1}},{"objectId":"5987356902031041503.5.1"}],"returnByValue":false,"awaitPromise":true,"userGesture":true},"sessionId":"SID-1"}
< {"id":238,"result":{"result":{"type":"object","subtype":"error","className":"Error","description":"Error: DOMException\n    at CRExecutionContext.evaluateWithArguments (/home/pierre/wrk/patchright/node_modules/patchright-core/lib/server/chromium/crExecutionContext.js:80:13)\n    at async LongStandingScope._race (/home/pierre/wrk/patchright/node_modules/patchright-core/lib/utils/isomorphic/manualPromise.js:94:14)\n    at async evaluateExpression (/home/pierre/wrk/patchright/node_modules/patchright-core/lib/server/javascript.js:233:12)\n    at async ElementHandle.evaluateHandleInUtility (/home/pierre/wrk/patchright/node_modules/patchright-core/lib/server/dom.js:133:14)\n    at async Frame._customFindElementsByParsed (/home/pierre/wrk/patchright/node_modules/patchright-core/lib/server/frames.js:1665:32)\n    at async Frame._retryWithoutProgress (/home/pierre/wrk/patchright/node_modules/patchright-core/lib/server/frames.js:1546:32)\n    at async Frame.retryWithProgressAndTimeouts (/home/pierre/wrk/patchright/node_modules/patchright-core/lib/server/frames.js:943:24)\n    at async FrameDispatcher.textContent (/home/pierre/wrk/patchright/node_modules/patchright-core/lib/server/dispatchers/frameDispatcher.js:138:19)\n    at async ProgressController.run (/home/pierre/wrk/patchright/node_modules/patchright-core/lib/server/progress.js:78:22)\n    at async FrameDispatcher._runCommand (/home/pierre/wrk/patchright/node_modules/patchright-core/lib/server/dispatchers/dispatcher.js:95:14)","objectId":"5987356902031041503.5.29"},"exceptionDetails":{"exceptionId":2,"text":"Uncaught","lineNumber":1,"columnNumber":8,"scriptId":"66","exception":{"type":"object","subtype":"error","className":"Error","description":"Error: DOMException\n    at CRExecutionContext.evaluateWithArguments (/home/pierre/wrk/patchright/node_modules/patchright-core/lib/server/chromium/crExecutionContext.js:80:13)\n    at async LongStandingScope._race (/home/pierre/wrk/patchright/node_modules/patchright-core/lib/utils/isomorphic/manualPromise.js:94:14)\n    at async evaluateExpression (/home/pierre/wrk/patchright/node_modules/patchright-core/lib/server/javascript.js:233:12)\n    at async ElementHandle.evaluateHandleInUtility (/home/pierre/wrk/patchright/node_modules/patchright-core/lib/server/dom.js:133:14)\n    at async Frame._customFindElementsByParsed (/home/pierre/wrk/patchright/node_modules/patchright-core/lib/server/frames.js:1665:32)\n    at async Frame._retryWithoutProgress (/home/pierre/wrk/patchright/node_modules/patchright-core/lib/server/frames.js:1546:32)\n    at async Frame.retryWithProgressAndTimeouts (/home/pierre/wrk/patchright/node_modules/patchright-core/lib/server/frames.js:943:24)\n    at async FrameDispatcher.textContent (/home/pierre/wrk/patchright/node_modules/patchright-core/lib/server/dispatchers/frameDispatcher.js:138:19)\n    at async ProgressController.run (/home/pierre/wrk/patchright/node_modules/patchright-core/lib/server/progress.js:78:22)\n    at async FrameDispatcher._runCommand (/home/pierre/wrk/patchright/node_modules/patchright-core/lib/server/dispatchers/dispatcher.js:95:14)","objectId":"5987356902031041503.5.30"}}},"sessionId":"SID-1"}

@krichprollsch krichprollsch self-assigned this Sep 6, 2025
@krichprollsch krichprollsch marked this pull request as ready for review September 8, 2025 10:15
@krichprollsch
Copy link
Member Author

krichprollsch commented Sep 8, 2025

🟠 dump.js

The result is flaky, sometimes ok, sometimes err.

$ node dump.js
Connection to browser on ws://127.0.0.1:9222
api => browser.newContext started
api <= browser.newContext succeeded
api => browserContext.newPage started
api <= browserContext.newPage succeeded
api => page.goto started
api <= page.goto succeeded
api => page.content started
api <= page.content failed
node:internal/modules/run_main:122
    triggerUncaughtException(
    ^

page.content: Unable to retrieve content because the page is navigating and changing the content.
    at /home/pierre/wrk/patchright/dump.js:46:25 {
  name: 'Error'
}

Node.js v22.14.0
// Import the Chromium browser into our scraper.
import { chromium } from 'patchright';

// browserAddress
const browserAddress = process.env.BROWSER_ADDRESS ? process.env.BROWSER_ADDRESS : 'ws://127.0.0.1:9222';

// web serveur url
const baseURL = process.env.BASE_URL ? process.env.BASE_URL : 'http://127.0.0.1:1234';

// measure general time.
const gstart = process.hrtime.bigint();
// store all run durations
let metrics = [];

// Connect to an existing browser
console.log("Connection to browser on " + browserAddress);
const browser = await chromium.connectOverCDP({
    endpointURL: browserAddress,
    logger: {
      isEnabled: (name, severity) => true,
      log: (name, severity, message, args) => console.log(`${name} ${message}`)
    }
});


const context = await browser.newContext({
    baseURL: baseURL,
});

const page = await context.newPage();
await page.goto('/campfire-commerce/');
const html = await page.content();

if (html.substring(0, 20) !== "<!DOCTYPE html><html") {
  console.log(html.substring(0, 20));
  throw new Error("html content is not as expected");
}

await page.close();
await context.close();

// Turn off the browser to clean up after ourselves.
await browser.close();

@krichprollsch
Copy link
Member Author

❌ cdp.js

// Import the Chromium browser into our scraper.
import { chromium } from 'patchright';

// browserAddress
const browserAddress = process.env.BROWSER_ADDRESS ? process.env.BROWSER_ADDRESS : 'ws://127.0.0.1:9222';

// web serveur url
const baseURL = process.env.BASE_URL ? process.env.BASE_URL : 'http://127.0.0.1:1234';

// runs
const runs = process.env.RUNS ? parseInt(process.env.RUNS) : 100;

// measure general time.
const gstart = process.hrtime.bigint();
// store all run durations
let metrics = [];

// Connect to an existing browser
console.log("Connection to browser on " + browserAddress);
const browser = await chromium.connectOverCDP(browserAddress);

for (var run = 0; run<runs; run++) {

    // measure run time.
    const rstart = process.hrtime.bigint();

    const context = await browser.newContext({
        baseURL: baseURL,
    });

    const page = await context.newPage();
    await page.goto('/campfire-commerce/');

    // ensure the price is loaded.
    await page.waitForFunction(() => {
        const price = document.querySelector('#product-price');
        return price.textContent.length > 0;
    }, {}, {timeout: 100}); // timeout 100ms


    // ensure the reviews are loaded.
    await page.waitForFunction(() => {
        const reviews = document.querySelectorAll('#product-reviews > div');
        return reviews.length > 0;
    }, {}, {timeout: 100}); // timeout 100ms

    let res = {};

    res.name = await page.locator('#product-name').textContent();
    res.price = parseFloat((await page.locator('#product-price').textContent()).substring(1));
    res.description = await page.locator('#product-description').textContent();
    res.features = await page.locator('#product-features > li').allTextContents();
    res.image = await page.locator('#product-image').getAttribute('src');

    let related = [];
    var i = 0;
    for (const row of await page.locator('#product-related > div').all()) {
        related[i++] = {
            name: await row.locator('h4').textContent(),
            price: parseFloat((await row.locator('p').textContent()).substring(1)),
            image: await row.locator('img').getAttribute('src'),
        };
    }
    res.related = related;

    let reviews = [];
    var i =0;
    for (const row of await page.locator('#product-reviews > div').all()) {
        reviews[i++] = {
            title: await row.locator('h4').textContent(),
            text: await row.locator('p').textContent(),
        };
    }
    res.reviews = reviews;

    // console.log(res);

    // assertions
    if (res['price'] != 244.99) {
      console.log(res);
      throw new Error("invalid product price");
    }
    if (res['image'] != "images/nomad_000.jpg") {
      console.log(res);
      throw new Error("invalid product image");
    }
    if (res['related'].length != 3) {
      console.log(res);
      throw new Error("invalid products related length");
    }
    if (res['reviews'].length != 3) {
      console.log(res);
      throw new Error("invalid reviews length");
    }

    process.stderr.write('.');
    if(run > 0 && run % 80 == 0) process.stderr.write('\n');

    await page.close();
    await context.close();

    metrics[run] = process.hrtime.bigint() - rstart;
}

// Turn off the browser to clean up after ourselves.
await browser.close();

const gduration = process.hrtime.bigint() - gstart;

process.stderr.write('\n');

const avg = metrics.reduce((s, a) => s += a) / BigInt(metrics.length);
const min = metrics.reduce((s, a) => a < s ? a : s);
const max = metrics.reduce((s, a) => a > s ? a : s);

console.log('total runs', runs);
console.log('total duration (ms)', (gduration/1000000n).toString());
console.log('avg run duration (ms)', (avg/1000000n).toString());
console.log('min run duration (ms)', (min/1000000n).toString());
console.log('max run duration (ms)', (max/1000000n).toString());

@krichprollsch krichprollsch merged commit b47b829 into main Sep 17, 2025
10 checks passed
@krichprollsch krichprollsch deleted the patchright branch September 17, 2025 14:14
@github-actions github-actions bot locked and limited conversation to collaborators Sep 17, 2025
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants