From 163b78feba678bd8f3344c1d0df39f26fc369254 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Wed, 29 Oct 2025 22:57:59 +0000 Subject: [PATCH] feat: add test coverage for Mutex, DevToolsConnectionAdapter, and WaitForHelper Adds unit tests for the following previously untested files: - `src/Mutex.ts` - `src/DevToolsConnectionAdapter.ts` - `src/WaitForHelper.ts` The new tests follow the existing testing conventions of the repository, using `node:test`, `node:assert`, and `sinon` for mocking. These tests improve the overall test coverage of the project and ensure the correctness of these components. --- package-lock.json | 12 ++++- tests/DevToolsConnectionAdapter.test.ts | 63 +++++++++++++++++++++++ tests/Mutex.test.ts | 59 ++++++++++++++++++++++ tests/WaitForHelper.test.ts | 67 +++++++++++++++++++++++++ 4 files changed, 200 insertions(+), 1 deletion(-) create mode 100644 tests/DevToolsConnectionAdapter.test.ts create mode 100644 tests/Mutex.test.ts create mode 100644 tests/WaitForHelper.test.ts diff --git a/package-lock.json b/package-lock.json index e6d56825..fca105d4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1227,6 +1227,7 @@ "integrity": "sha512-BnOroVl1SgrPLywqxyqdJ4l3S2MsKVLDVxZvjI1Eoe8ev2r3kGDo+PcMihNmDE+6/KjkTubSJnmqGZZjQSBq/g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.46.2", "@typescript-eslint/types": "8.46.2", @@ -1702,6 +1703,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2576,7 +2578,8 @@ "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1521046.tgz", "integrity": "sha512-vhE6eymDQSKWUXwwA37NtTTVEzjtGVfDr3pRbsWEQ5onH/Snp2c+2xZHWJJawG/0hCCJLRGt4xVtEVUVILol4w==", "dev": true, - "license": "BSD-3-Clause" + "license": "BSD-3-Clause", + "peer": true }, "node_modules/diff": { "version": "7.0.0", @@ -2877,6 +2880,7 @@ "integrity": "sha512-t5aPOpmtJcZcz5UJyY2GbvpDlsK5E8JqRqoKtfiKE3cNh437KIqfJr3A3AKf5k64NPx6d0G3dno6XDY05PqPtw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -3047,6 +3051,7 @@ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -3317,6 +3322,7 @@ "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.0", @@ -5612,6 +5618,7 @@ "integrity": "sha512-3GuObel8h7Kqdjt0gxkEzaifHTqLVW56Y/bjN7PSQtkKr0w3V/QYSdt6QWYtd7A1xUtYQigtdUfgj1RvWVtorw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/estree": "1.0.8" }, @@ -6617,6 +6624,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -6692,6 +6700,7 @@ "dev": true, "hasInstallScript": true, "license": "MIT", + "peer": true, "dependencies": { "napi-postinstall": "^0.3.0" }, @@ -6990,6 +6999,7 @@ "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", "dev": true, "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/tests/DevToolsConnectionAdapter.test.ts b/tests/DevToolsConnectionAdapter.test.ts new file mode 100644 index 00000000..a4254a9e --- /dev/null +++ b/tests/DevToolsConnectionAdapter.test.ts @@ -0,0 +1,63 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ +import assert from 'node:assert'; +import {describe, it} from 'node:test'; +import sinon from 'sinon'; + +import {DevToolsConnectionAdapter} from '../src/DevToolsConnectionAdapter.js'; +import {type ConnectionTransport} from '../src/third_party/index.js'; + +class MockTransport implements ConnectionTransport { + onmessage: ((message: string) => void) | undefined; + onclose: (() => void) | undefined; + + send(message: string): void {} + close(): void {} +} + +describe('DevToolsConnectionAdapter', () => { + it('should pass messages from transport to onMessage', () => { + const transport = new MockTransport(); + const adapter = new DevToolsConnectionAdapter(transport); + const onMessage = sinon.spy(); + + adapter.setOnMessage(onMessage); + transport.onmessage?.('test message'); + + assert.ok(onMessage.calledOnceWith('test message')); + }); + + it('should call onDisconnect when transport closes', () => { + const transport = new MockTransport(); + const adapter = new DevToolsConnectionAdapter(transport); + const onDisconnect = sinon.spy(); + + adapter.setOnDisconnect(onDisconnect); + transport.onclose?.(); + + assert.ok(onDisconnect.calledOnce); + }); + + it('should send messages through the transport', () => { + const transport = new MockTransport(); + const spy = sinon.spy(transport, 'send'); + const adapter = new DevToolsConnectionAdapter(transport); + + adapter.sendRawMessage('test message'); + + assert.ok(spy.calledOnceWith('test message')); + }); + + it('should close the transport on disconnect', async () => { + const transport = new MockTransport(); + const spy = sinon.spy(transport, 'close'); + const adapter = new DevToolsConnectionAdapter(transport); + + await adapter.disconnect(); + + assert.ok(spy.calledOnce); + }); +}); diff --git a/tests/Mutex.test.ts b/tests/Mutex.test.ts new file mode 100644 index 00000000..db9b91d1 --- /dev/null +++ b/tests/Mutex.test.ts @@ -0,0 +1,59 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ +import assert from 'node:assert'; +import {describe, it} from 'node:test'; + +import {Mutex} from '../src/Mutex.js'; + +describe('Mutex', () => { + it('should acquire and release the lock', async () => { + const mutex = new Mutex(); + const guard = await mutex.acquire(); + guard.dispose(); + }); + + it('should prevent multiple acquisitions', async () => { + const mutex = new Mutex(); + await mutex.acquire(); + let acquired = false; + mutex.acquire().then(() => { + acquired = true; + }); + // Give the promise a chance to resolve + await new Promise(resolve => setTimeout(resolve, 0)); + assert.strictEqual(acquired, false); + }); + + it('should allow acquisition after release', async () => { + const mutex = new Mutex(); + const guard1 = await mutex.acquire(); + guard1.dispose(); + const guard2 = await mutex.acquire(); + guard2.dispose(); + }); + + it('should handle FIFO queuing', async () => { + const mutex = new Mutex(); + const order: number[] = []; + const guard = await mutex.acquire(); + + const promise1 = mutex.acquire().then(guard1 => { + order.push(1); + guard1.dispose(); + }); + + const promise2 = mutex.acquire().then(guard2 => { + order.push(2); + guard2.dispose(); + }); + + guard.dispose(); + + await Promise.all([promise1, promise2]); + + assert.deepStrictEqual(order, [1, 2]); + }); +}); diff --git a/tests/WaitForHelper.test.ts b/tests/WaitForHelper.test.ts new file mode 100644 index 00000000..8ed79de2 --- /dev/null +++ b/tests/WaitForHelper.test.ts @@ -0,0 +1,67 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ +import assert from 'node:assert'; +import {describe, it} from 'node:test'; +import sinon from 'sinon'; + +import {WaitForHelper} from '../src/WaitForHelper.js'; +import {type Page, type CdpPage} from '../src/third_party/index.js'; + +class MockPage { + #client = { + on() {}, + off() {}, + }; + + evaluateHandle() {} + waitForNavigation() {} + _client() { + return this.#client; + } +} + +describe('WaitForHelper', () => { + it('should wait for stable DOM', async () => { + const page = new MockPage(); + const helper = new WaitForHelper(page as unknown as Page, 1, 1); + const evaluateHandle = sinon.stub(page, 'evaluateHandle').resolves({ + evaluate: () => Promise.resolve(), + dispose: () => Promise.resolve(), + } as any); + + await helper.waitForStableDom(); + + assert.ok(evaluateHandle.calledOnce); + }); + + it('should wait for navigation started', async () => { + const page = new MockPage() as unknown as CdpPage; + const client = page._client(); + const on = sinon.spy(client, 'on'); + const off = sinon.spy(client, 'off'); + const helper = new WaitForHelper(page as unknown as Page, 1, 1); + + await helper.waitForNavigationStarted(); + + assert.ok(on.calledOnceWith('Page.frameStartedNavigating', sinon.match.func)); + }); + + it('should wait for events after action', async () => { + const page = new MockPage(); + const helper = new WaitForHelper(page as unknown as Page, 1, 1); + const waitForNavigationStarted = sinon.stub(helper, 'waitForNavigationStarted').resolves(true); + const waitForNavigation = sinon.stub(page, 'waitForNavigation').resolves(); + const waitForStableDom = sinon.stub(helper, 'waitForStableDom').resolves(); + const action = sinon.spy(); + + await helper.waitForEventsAfterAction(action); + + assert.ok(waitForNavigationStarted.calledOnce); + assert.ok(waitForNavigation.calledOnce); + assert.ok(waitForStableDom.calledOnce); + assert.ok(action.calledOnce); + }); +});