Skip to content

Conversation

@KuznetsovRoman
Copy link
Member

No description provided.

@KuznetsovRoman KuznetsovRoman force-pushed the TESTPLANE-693.cdp branch 2 times, most recently from 8cc1b46 to 1223919 Compare September 5, 2025 20:53
@pkg-pr-new
Copy link

pkg-pr-new bot commented Sep 5, 2025

Open in StackBlitz

npm i https://pkg.pr.new/gemini-testing/testplane@1129

commit: bb74561

@KuznetsovRoman KuznetsovRoman force-pushed the TESTPLANE-693.cdp branch 4 times, most recently from 9232a6d to 56b81a4 Compare September 8, 2025 22:08
"vite": "5.1.6",
"wait-port": "1.1.0",
"worker-farm": "1.7.0",
"ws": "8.18.3",
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

using this version because we already have it as "@testplane/[email protected]" dependency

import * as logger from "../../utils/logger";
import { EventEmitter } from "events";

export class CDPEventEmitter<Events extends { [key in keyof Events]: unknown }> extends EventEmitter {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This class provides typings for eventemitter, like that:
image
image

Also it wraps callbacks so they don't produce unhandled rejection

public readonly profiler: CDPProfiler;

static async create(browser: Browser): Promise<CDP | null> {
if (!browser.publicAPI.isChromium) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cdp is only available in chromium-based browsers

import { CDPEventEmitter } from "./emitter";
import type { CDPDebuggerLocation, CDPSessionId, CDPScriptCoverage, CDPProfile } from "./types";

interface StartPreciseCoverageRequest {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

return new Promise(resolve => setTimeout(resolve, delay).unref());
};

export const extractRequestIdFromBrokenResponse = (message: string): number | null => {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

as we have seen, we might get broken message, which can't be parsed with json.parse
This function extracts request "id", so we can stop waiting for response for it until timeout and just retry it

* @description creates ws connection with retries or returns existing one
* @note Concurrent requests with same params produce same ws connection
*/
private async getWsConnection(): Promise<WebSocket> {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Retries "tryToEstablishWsConnection"

}

/** @description establishes ws connection, sends request with timeout and waits for response */
private async tryToSendRequest(
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes single try to send a request and resolves its response

}

/** @description Performs high-level CDP request with retries and timeouts */
async request<T>(
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

retries "tryToSendRequest"

Comment on lines +481 to +482
/** @description Closes websocket connection, terminating all pending requests */
close(): void {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should only be called once, when we would not need this connection in the future (for example, when deleting browser session)

Comment on lines 531 to 534
if (!this.isWebSocketActive(ws)) {
clearInterval(pingInterval);
return;
}
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

on each tick we have to check it first, because we might catch a frame where this callback is called when this pingInterval is no longer active because ws connection is reestablished

@KuznetsovRoman KuznetsovRoman force-pushed the TESTPLANE-693.cdp branch 2 times, most recently from fc679c0 to 0805224 Compare September 9, 2025 15:00
@KuznetsovRoman KuznetsovRoman force-pushed the TESTPLANE-693.cdp branch 2 times, most recently from 0e0c4ed to 641a566 Compare September 17, 2025 10:24
public readonly target: CDPTarget;
public readonly profiler: CDPProfiler;

static async create(browser: Browser): Promise<CDP | null> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This may be controversial, but IMO create methods don't bring any value and only pollute codebase. But not critical to me

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Its async
Since we can't make async constructor, i think its better to encapsulate "get browser ws endpoint" logic to "cdp" scope

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a common complaint, but you don't need async constructor. You can get all necessary values before calling the constructor and just pass all those values

}

/** @description Creates CDPConnection without establishing it */
static async create(browser: Browser): Promise<CDPConnection> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here as well I think there's little value in create() method and in receiving our Browser god-object if we in fact only need headers and wsEndpoint. This increases code coupling. For example when we chose to expose this API, users will be forced to create our Browser object, when they could just pass the endpoint and it would work fine

Not critical

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think i will leave it be for now, and its not critical while it is internals
We can change it when (and if) we will decide to make it public

ws.close();
done(
new CDPTimeoutError({
message: `Couldn't establish CDP connection to "${endpoint}" in ${CDP_CONNECTION_TIMEOUT}ms`,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be really nice if we had more actionable errors here and in all other places as well. I think a good error message states clearly "what happenned and why" and "what can be done to fix". Imagining myself as a user receiving the "Couldn't establish CDP connection to ... in 1000ms", I'd be pretty puzzled.

Not critical, but very nice to have. I don't think that all these new errors are actionable right now.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would love to leave actionable errors here, explaining "why it happened" and "what to do", if i knew, why could it happen and how to resolve these issues.

My plan to these kind of errors where you dont know, why it might happen, is the following:

  • Release this version with non-actionable errors
  • Saturate error messages with actionability, based on user stories (user comes with this error, we check, why it happened and what to do in this case, then update error message)


this._wsConnectionStatus = WsConnectionStatus.CONNECTING;

this._wsConnectionPromise = (async (): Promise<WebSocket> => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we move this iife to a helper/protected method with human readable name? The logic here is pretty hard, especially the part where you reassign wsConnectionPromise inside.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

especially the part where you reassign wsConnectionPromise inside

Thats why i keep it inside, so we could see all complex _wsConnectionPromise transitions scoped to just one method, instead it being reassigned in multiple ones

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would move domain implementations to a separate directory "domains". Now, they are mixed with types, utils, etc.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Its done in next PR

await cdpTarget.activateTarget(incognitoWindowId);
}

if (this._session.isBidi) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We got rid of this in master, because puppeteer could do cleanup of unnecessary contexts with bidi, too. Would it remain possible with our own implementation?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will take a look at the code

});

it("should handle connection termination and reconnect", async () => {
const result1 = await connection.request("Successfull.Method");
Copy link
Member

@shadowusr shadowusr Oct 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

minor typo — successful, not critical

@KuznetsovRoman KuznetsovRoman merged commit 41647d2 into master Oct 15, 2025
5 checks passed
@KuznetsovRoman KuznetsovRoman deleted the TESTPLANE-693.cdp branch October 15, 2025 10:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants