-
Notifications
You must be signed in to change notification settings - Fork 5.4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
NPM: Playwright does not work #16899
Comments
It seems deno's https://github.com/denoland/deno_std/blob/0.167.0/node/internal/child_process.ts#L153 |
http://docs.libuv.org/en/v1.x/guide/processes.html This is correct. Deno doesn't currently support libuv's cross platform api for fds other than std{in,out,err}. via #16298 libuv's icon is a dino with a unicorn. Kind of makes you wonder. :-) But yes. My incident is about supporting all of libuv. Not just a few additional pipelines beyond stdin/stdout/stderr. |
As @tamusjroyce wrote:
So if signals & streams are too difficult to implement (in the short-term), then Websockets might be a more feasible way to run Playwright under Deno. I actually tried that in the past already (see here), but that attempt also failed. After re-reading everything about Deno/Playwright, I realized that there might be different reasons for why the Websocket route doesn't work, so I tried again. The resulting error is as follows:
If I run my test script (see below) with Node.js, everything works as expected. So somehow Websockets are not behaving as expected under Deno. If someone wants to give this a try, here is how to do it:
import { chromium } from 'npm:playwright';
// import { chromium } from 'playwright';
// To run this script with Node.js (e.g. for testing),
// replace the above lines (don't forget to install
// Playwright via `npm i playwright`)
async function main() {
// If this line is uncommented, the script fails.
// This will print the executed command, which we'll
// need soon:
const browser = await chromium.launch({ headless: false });
// If this line is uncommented (the above line should be commented out),
// then Playwright will connect to the browser via Websocket.
// `$WEBSOCKET_ADDRESS` needs to be replaced with a valid Websocke
// address (see later step)
// const browser = await chromium.connectOverCDP("$WEBSOCKET_ADDRESS");
const page = await browser.newPage();
await page.goto('http://example.com');
const title = await page.title();
console.log(`=> Page title: ${title}`)
await browser.close();
};
main()
As mentioned above, this will result in a Websocket error. Does anyone have any clue what the problem could be? |
I've set up a "man-in-the-middle" proxy to see what Websocket requests Node.js and Deno are sending, to figure out what's going on. Here is the request and response when Node.js executes the above script: Request:
Response:
The The last displayed message was Here is the request and response when Deno executes the above script: Request:
Response:
The The last displayed message was There are a few differences I see:
With Node.js the user agent is With Deno the user agent is The question is, why does Deno not use the latest version of Playwright? And why is the Node.js version so old with Deno?
With Node.js the exit code is Here is a list of Websocket status codes, and their meaning. For
With Deno the exit code is
So something doesn't seem to work properly. Does anybody have any clue what the reason for 1) and 2) could be? PS: If anybody wants to see themselves what Websocket messages are sent, here is how:
|
I've tried running my test script with the First, I'll list the steps to reproduce what I did:
Now script execution stops on any caught exceptions. The following two exceptions seem odd to me:
Could these lead to problems? Besides that, there are some breakpoints/exceptions where I have no clue what they are about. And there is (of course) the exception that is printed to the terminal. With the above approach it should be not too difficult to find the location where things break. |
Today I continued to debug "Playwright via Websockets". (Btw., I'm describing what I do in so much detail partly so I myself can reproduce it, e.g., if I have to stop and come back a few weeks/months later).
Node.js also has a While debugging, it makes sense to disable the timeout of the const browser = await chromium.connectOverCDP("http://127.0.0.1:8080/", { timeout: 0 }); (Defining the endpoint as the address of the HTTP server breaks support for the "man-in-the-middle" proxy. If the proxy is needed, then the Websocket address needs to be supplied directly to Playwright)
Importing Playwright via However, that doesn't seem to change anything Request & response of the upgraded version
Here is where the exception is triggered (playwright-core/src/server/transport.ts#L82): transport._ws.on('unexpected-response', (request: ClientRequest, response: IncomingMessage) => {
const chunks: Buffer[] = [];
const errorPrefix = `${url} ${response.statusCode} ${response.statusMessage}`;
response.on('data', chunk => chunks.push(chunk));
response.on('close', () => {
const error = chunks.length ? `${errorPrefix}\n${Buffer.concat(chunks)}` : errorPrefix;
progress?.log(`<ws unexpected response> ${error}`);
reject(new Error('WebSocket error: ' + error));
transport._ws.close();
});
}); The value of the error that this code triggers is:
Here are the API docs for the The Here is the location where the This code is within an event handler that handles responses for the WebSocket client (which are expected to be redirects). The odd thing is, that the response has the status code This is odd, because below the response handler is a It seems, the response is somehow wrongly categories as regular response, instead of as an upgrade... So I guess, the 'NPM compat' code does not implement https://nodejs.org/api/http.html#event-upgrade correctly. This can be reproduced via the example of Event: 'upgrade' (replace the import at the top with This example is definitely a much better target to debug, than Playwright directly. I tried to figure out why the I think it makes sense to open a separate issue for this (to eliminate all the unnecessary information). It should be linked below this comment. |
Hey @d4h0 thanks for detailed examination, this should be enough to debug the problem on our side. I can answer some questions outright:
That's probably because you used unversioned import and you had older version of Playwright cached, as you noticed
This seems like main crux of the problem and we received reports that Vite is not working properly with Deno too; it uses HTTP upgrade as well. We are currently working on a rewrite of our HTTP server that should help us fix this problem. Obviously if we could fix up the problem before rewrite of HTTP server lands that would be preferable. @kt3k could you take a look at this problem? EDIT: Ooops, it seems the problem is with the HTTP client, not HTTP server. So the Vite problem is not related to this one. |
@bartlomieju: Thank you for the feedback!
Is there also a rewrite of the HTTP client planned? In theory, it shouldn't be too difficult to add HTTP upgrade support to the client. In practice, I gave up because I'm too unfamiliar with the code base and TypeScript itself (also, because I already spent three full days debugging this). I think, Deno is in almost every way better than Node.js. Unfortunately, Playwright is essential for me, and it is too much work to maintain systems written for two different JavaScript runtimes. So, at least for me, the lack of Playwright on Deno is blocker for switching to Deno (besides Playwright, everything I need can be replaced with something else that works on Deno). |
@d4h0 if that is your only blocker, try https://github.com/kt3k/deno-bin Then package.json can be upgraded to use deno where it makes sense. And e2e test can run via node. At least until deno is fully compatible. My biggest issue with deno is my fingers will still type node and npm. Should be easy to have deno compile bin to run package.json scripts & alias deno & translate command line params when running node & npm from folder :) |
@tamusjroyce: Thanks for the tip, this looks pretty interesting. The biggest advantage of Deno over Node.js (for me) is the fact that you can embed Deno easily into Rust applications (via the deno_core Rust library). Currently, I'm using Node.js via a homegrown RPC utility. Deno would allow me to integrate much more ergonomically with my applications (also because Deno natively runs Wasm, so I even can run Rust directly in Deno, which makes many things easier). My main goal for Playwright on Deno is to somehow make it possible to ergonomically use Playwright from Rust (there is already playwright-rust, but it doesn't support the latest Playwright version, and embeds a Zip binary of a Node.js application, which is unzipped at runtime, which I don't really like). That being said, I probably will still start using |
With #19412 is this closable? |
The steps in #16899 (comment) seem working now! But it looks too hacky to me (involving a lot of manual copy pasting during test run). Is there any reasonable option to start playwright with websocket configuration? cc @d4h0 |
That sounds fantastic! Unfortunately, I didn't have any time yet, to play with Playwright on Deno after the recent changes related to this issue.
It should be possible to run Playwright similarly to how you'd run Playwright on Node.js. For example, BrowserType::launchServer should be usable to start the browser and to get a WebSocket address via BrowserServer::wsEndpoint. After that, we should be able to use BrowserType::connect to connect to the WebSocket endpoint (instead of using BrowserType::connectOverCDP, as the last test script does). For example: import { chromium } from 'npm:playwright';
async function main() {
const browserServer = await chromium.launchServer();
const wsEndpoint = browserServer.wsEndpoint();
const browser = await chromium.connect(wsEndpoint);
const page = await browser.newPage();
await page.goto('http://example.com');
const title = await page.title();
console.log(`=> Page title: ${title}`)
await browser.close();
};
main() (Save to Unfortunate, that fails with the following error:
Full error
The const browserServer = await chromium.launchServer({ignoreDefaultArgs: ["--remote-debugging-pipe"]}); ...but this doesn't seem to change anything, unfortunately. The error is triggered by the first line (that contains
Both options are not great, however, so a fix for the above error would be the ideal solution. Unfortunately, I don't have more time to play with this, right now. |
The code in @d4h0 's code actually can partially work today when the option The problem is that, when the browser is navigated, such as in
This is effectively covering up the actual error which is somehow leading to the page getting closed by Playwright:
Unfortunately, I didn't copy the whole stack trace to my notes, and I lost track of where it originally occurred. Might have been from the "Connection" class. Not sure now. In any case, I'm pretty sure that this seems indicative that there's something either incorrect with Deno's WebSocket implementation or with Playwright's handling of its Node shim for WebSocket. Though it's possible to successfully use some methods like In the meantime, I came up with a way to automate the import { chromium } from 'playwright';
// Since there's no good way for us to get the
// websocket that Chromium randomly generates,
// we must generate our own and keep track of it.
const port = getRandomPortNum();
const browserServer = await chromium.launchServer({
// This option isn't documented, but sets up the server
// to listen to a debugging port. Unfortunately, this doesn't
// seem to work out of the box, at least in Deno. By itself,
// a window can be launched, but encounters an error:
// `Uncaught RangeError: Invalid WebSocket frame: RSV2 and RSV3 must be clear`.
// So instead, we're going to connect to a devtools websocket
// later. However, I'm still including this because it
// allows us to await the browserServer object.
//
// If this option isn't available to you with your
// version of Playwright, you can remove it if you
// also remove the `await` keyword before `chromium.launchServer`.
// However, you won't have access to a BrowserServer
// object if you go down that route.
useWebSocket: true,
// Pass in our randomly generated port.
args: [`--remote-debugging-port=${port}`],
// Prevent Playwright from overriding our custom port number.
// The "--remote-debugging-pipe" flag is already removed by
// `useWebSocket`, but I left it here in case you want to
// remove that option and not await `chromium.launchServer`.
ignoreDefaultArgs: ["--remote-debugging-pipe", "--remote-debugging-port=0"],
// For demonstration purposes
headless: false,
});
// Even though we've awaited the browser server, sometimes
// the endpoint for information on the devtools websocket
// still isn't totally available. This seems to happen
// 1/10 attempts. I wrapped our call to connect in a function
// that will retry until the socket is available. There's
// probably a better way, but I wanted to make sure this
// was more reliable.
const browser = await tryWithBackoff({
// I've tried the approach in the following article and it
// simply isn't usable (yet) in Deno:
// https://playwright.dev/docs/api/class-browsertype#browser-type-launch-server
//
// Instead, we're going to let Playwright ask Chromium for the
// devtools websocket endpoint.
fn: () => chromium.connectOverCDP(`http://localhost:${port}`)
});
const page = await browser.newPage();
await page.goto('https://duck.com');
const title = await page.title();
console.log(`=> Page title: ${title}`)
await new Promise(resolve => setTimeout(resolve, 5000));
await browserServer.close();
Deno.exit();
/**
* This is finds a random port number that is not being used by something else.
**/
function getRandomPortNum (): number {
const MIN_PORT_NUM = 1024;
const MAX_PORT_NUM = 65535;
const portNum = Math.ceil(Math.random() * ((MAX_PORT_NUM - 1) - MIN_PORT_NUM + 1) + MIN_PORT_NUM + 1);
try {
const server = Deno.listen({ port: portNum });
server.close();
return portNum;
} catch (e) {
if (e.name !== 'AddrInUse') throw e;
return getRandomPortNum();
}
}
/**
* Tries to execute a function and retries (with backoff and timeout) if an error occurs.
**/
async function tryWithBackoff (args: {
fn: () => any;
delay?: number;
timeout?: number;
startedAt?: number;
error?: Error;
}): Promise<any> {
const { fn, delay, timeout, startedAt, error } = {
delay: 0,
timeout: 30000,
startedAt: Date.now(),
...args,
};
await new Promise(resolve => setTimeout(resolve, delay));
if ((Date.now() - startedAt) > timeout) {
throw (error || new Error('Function call timed out'));
}
try {
return await fn();
} catch (error) {
console.error(error);
return tryWithBackoff({
fn,
delay: delay + 1000,
timeout,
startedAt,
error,
});
}
} |
Just trying playwright out myself, in Deno 1.38.0, when running:
I get the follow error:
Running on MacOS btw, so this won't affect Windows, but will for Linux users too, looks like this is the cause... |
Deno 1.38.2 now implements deno run -A npm:playwright@1.40.0 install
seems ChildProcess is missing But also, after installing browsers via node and manually caching a package, then attempting to run tests... yarn playwright install
deno cache npm:@playwright/test
deno run -A deno run -A npm:playwright@1.40.0 test it did attempt to run the entire test suite, but with lots of warnings...
and then successfully started the test report server and opened the report in my browser. So, I wonder if it's actually quite close to working once |
I meant making a local copy of the playwright lib and change the source code at your will (basically a fork). It's not great as you imagine. Maybe monkey-patching could work too, I gave up using deno with playwright for the moment... |
@d4h0 @bartlomieju does this library can solve this? https://github.com/gildas-lormeau/simple-cdp/ |
It is due to a bug in |
The current blocker is the lack of support for extra stdio pipes (so more than just stdin, stdout, stderr). The main complication is in how these pipes are consumed by the child process. If the child process is a deno subprocess, then generally you would create a new socket with a specific FD and use that as the child end of the pipe. For instance, // child.js
import net from "node:net";
const sock = new net.Socket({ fd: 4 }); // assuming the parent process opened a pipe on fd 4 The issue with that is that we don't currently support interacting with resources using raw file descriptors, as it's a larger architectural change and requires careful thought about how it interacts with the permissions system. I initially thought that this may be a blocker for this issue. Digging into playwright specifically, however, I think we can sidestep this issue as the child processes are actually native code. So for chromium, for instance, playwright spawns a child process with pipes at fds 3 and 4. Then, chromium opens pipes on those file descriptors (from C++). Since the child process isn't JS, we don't have to solve the more general issue of using raw FDs in deno, so I believe I can make progress on this issue. |
This should now be working on canary, and will be in the next release (only on macOS and linux, for now). import { chromium } from "npm:playwright";
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
async function main() {
const browser = await chromium.launch({
headless: false,
});
const page = await browser.newPage();
await page.goto("http://deno.com");
await sleep(2000);
await browser.close();
}
if (import.meta.main) {
await main();
} Screen.Recording.2024-08-15.at.12.56.47.PM.mov |
@nathanwhit You are a hero! 🦸🏻 💪🏻 💪🏻 💪🏻 |
Just trying out Playwright 1.46.1 on Deno 1.46.1, which is a strange coincidence in itself! Anyway, I can confirm that But I can't get my test suite to run...
Here is my playwright.config.js... https://github.com/jollytoad/ahx/blob/playwright-deno/playwright.config.js I've deliberated removed node and related tools, so it should be running pure deno. Any ideas? Anyone else got a playwright test suite to run yet on the recent releases? |
@jollytoad Hello! I dug into this and it seems we have a bug in this case, specifically when you aren't using a node modules directory (#25189). To work around this for now, you can add
to your You'll also need to make sure that the dependencies get cached for Testing this, it does seem while the tests do run correctly, the process doesn't always exit cleanly, and I sometimes have to exit with |
@nathanwhit Thank you so much for pushing this forward! Is there a write up anywhere of what it will take to get playwright working on Windows? Unfortunately, many of the testers on our team use it, and so I can't fully migrate us until it lands. |
@nathanwhit thanks for the advice, I can confirm my tests run after setting |
Not a full writeup, but the gist of it is that we need to either vendor and modify or handroll |
The Trait std::os::windows::process::CommandExt provides Windows-specific extensions to the process::Command builder. Which is a process builder, providing fine-grained control over how a new process should be spawned. Maybe this can be used directly or at least as a starting point. |
Unfortunately that still doesn't give us enough control. We need to set one of the arguments to the underlying As a result we would need to modify the internals of |
Thank you for the detailed explanation. I found the code you are referring to: sys::pal::windows::process. I do not know if it can be reused somehow. Maybe it has to be reimplemented using the windows crate from Microsoft. |
I tried to run the following script both on macOS and Linux (on GitHub Actions). On macOS it works, while on Linux it doesn't. import { chromium } from "npm:playwright@1.47.2";
async function main() {
await using _browser = await chromium.launch({
headless: true,
});
}
if (import.meta.main) {
await main();
} Firefox doesn't work either on Linux (but again, does work on macOS) import { firefox } from "npm:playwright@1.47.2";
async function main() {
await using _browser = await firefox.launch({
headless: true,
});
}
if (import.meta.main) {
await main();
} Checked deno 1.46.3, 2.0.0-rc.6, and canary (eff6423), all of these yield the error for the code above. The actual code and error logs can be seen at magurotuna/playwright-deno-ci#1 |
Same problem and same exception here, on macos works but not in linux |
…mode (#26495) Fixes playwright on linux, as reported in #16899 (comment). The issue was that we were opening the socket in nonblocking mode, which meant that subprocesses trying to use it would get a `EWOULDBLOCK` error (unexpectedly). The fix here is to only set nonblocking mode on our end (which we need to use asynchronously)
@magurotuna @Jess182 playwright on linux should be fixed in the latest canary ( |
…mode (#26495) Fixes playwright on linux, as reported in #16899 (comment). The issue was that we were opening the socket in nonblocking mode, which meant that subprocesses trying to use it would get a `EWOULDBLOCK` error (unexpectedly). The fix here is to only set nonblocking mode on our end (which we need to use asynchronously)
Thanks so much @nathanwhit , now works on linux, deno v2.0.3! |
Awesome work @nathanwhit, thank you very much. Just to clarify the config and commands to run for anyone else stumbling across this: Ensure
I now get results consistent with node, and the process now exits cleanly.
Looks like that is relying on node specific module loader features! |
Ah, yes that is a known issue. Playwright primarily uses loaders to transpile typescript ESM at runtime. That isn't actually needed in deno, since we support typescript natively. You can set the |
Thanks so much @nathanwhit, I've confirmed that my very simple test suite passes with the canary version on Linux: https://github.com/magurotuna/playwright-deno-ci/actions/runs/11568994515/job/32201894429?pr=2 |
The fix is also in the sable release starting with v2.0.3 |
Hi,
I'm trying to use Playwright via the new NPM compatibility layer, but it fails with:
Uncaught TypeError: browserType.launch: Cannot read properties of undefined (reading 'on')
The following snippet can be used to reproduce the error:
After running the above via
deno run --unstable --allow-all main.ts
, the following message is displayed:Executing
deno run --unstable --allow-all npm:playwright install
downloads the required binaries.Executing
deno run --unstable --allow-all main.ts
again, leads to the above-mentioned error:Playwright
/Deno
was already discussed in #16298, but I thought it makes sense to open an issue that focuses only onPlaywright
. There was also some discussion in thePlaywright
repo: microsoft/playwright#3146The text was updated successfully, but these errors were encountered: