From cb9ca4eedf33033ffa5f167908648f04032ed7fd Mon Sep 17 00:00:00 2001 From: Ninad Sinha Date: Mon, 11 May 2026 16:50:06 -0700 Subject: [PATCH 1/2] Add integration tests --- .github/workflows/integration-tests.yml | 46 ++++++++ package.json | 1 + tests/integration/client-http.test.ts | 136 ++++++++++++++++++++++++ vitest.config.ts | 2 +- 4 files changed, 184 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/integration-tests.yml create mode 100644 tests/integration/client-http.test.ts diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml new file mode 100644 index 0000000..7ea2258 --- /dev/null +++ b/.github/workflows/integration-tests.yml @@ -0,0 +1,46 @@ +name: Integration Tests + +on: + pull_request: + push: + branches: [main] + +concurrency: + group: node-integration-tests-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + integration-tests: + runs-on: ubuntu-latest + + steps: + - name: Check out repository + uses: actions/checkout@v5 + + - name: Set up Node.js + uses: actions/setup-node@v6 + with: + node-version: 22.22.1 + cache: yarn + + - name: Set up Yarn + run: | + corepack enable + corepack prepare yarn@1.22.22 --activate + + - name: Show tool versions + run: | + node --version + yarn --version + + - name: Install dependencies + run: yarn install --frozen-lockfile + + - name: Build package + run: yarn build + + - name: Run integration tests + run: yarn test:integration diff --git a/package.json b/package.json index e88908e..3232932 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "prepare": "yarn build", "test": "vitest run", "test:e2e": "vitest run tests/sandbox/e2e", + "test:integration": "vitest run tests/integration", "test:watch": "vitest", "format": "prettier --write 'src/**/*.ts'" }, diff --git a/tests/integration/client-http.test.ts b/tests/integration/client-http.test.ts new file mode 100644 index 0000000..5dbd9db --- /dev/null +++ b/tests/integration/client-http.test.ts @@ -0,0 +1,136 @@ +import http, { IncomingMessage, ServerResponse } from "node:http"; +import { afterEach, describe, expect, test } from "vitest"; +import { HyperbrowserClient } from "../../src/client"; + +type RecordedRequest = { + method?: string; + url?: string; + apiKey?: string; + contentType?: string; + body?: unknown; +}; + +type TestServer = { + baseUrl: string; + requests: RecordedRequest[]; + close: () => Promise; +}; + +const readJsonBody = async (request: IncomingMessage): Promise => { + const chunks: Buffer[] = []; + for await (const chunk of request) { + chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)); + } + + if (chunks.length === 0) { + return undefined; + } + + return JSON.parse(Buffer.concat(chunks).toString("utf8")); +}; + +const sendJson = (response: ServerResponse, statusCode: number, payload: unknown): void => { + const encoded = Buffer.from(JSON.stringify(payload)); + response.writeHead(statusCode, { + "content-type": "application/json", + "content-length": encoded.length, + }); + response.end(encoded); +}; + +const startServer = async (): Promise => { + const requests: RecordedRequest[] = []; + const server = http.createServer(async (request, response) => { + const body = await readJsonBody(request); + requests.push({ + method: request.method, + url: request.url, + apiKey: request.headers["x-api-key"]?.toString(), + contentType: request.headers["content-type"]?.toString(), + body, + }); + + if (request.method === "POST" && request.url === "/api/scrape") { + sendJson(response, 200, { jobId: "job_123" }); + return; + } + + if (request.method === "GET" && request.url === "/api/scrape/job_123/status") { + sendJson(response, 200, { status: "completed" }); + return; + } + + sendJson(response, 404, { message: `unexpected route ${request.method} ${request.url}` }); + }); + + await new Promise((resolve, reject) => { + server.once("error", reject); + server.listen(0, "127.0.0.1", () => { + server.off("error", reject); + resolve(); + }); + }); + + const address = server.address(); + if (!address || typeof address === "string") { + throw new Error("test server did not bind to a TCP port"); + } + + return { + baseUrl: `http://127.0.0.1:${address.port}`, + requests, + close: () => + new Promise((resolve, reject) => { + server.close((error) => (error ? reject(error) : resolve())); + }), + }; +}; + +const servers: TestServer[] = []; + +afterEach(async () => { + await Promise.all(servers.splice(0).map((server) => server.close())); +}); + +describe("client HTTP integration", () => { + test("scrape requests use the configured API endpoint and parse responses", async () => { + const server = await startServer(); + servers.push(server); + const client = new HyperbrowserClient({ + apiKey: "test-api-key", + baseUrl: server.baseUrl, + }); + + const started = await client.scrape.start({ + url: "https://example.com", + scrapeOptions: { + formats: ["markdown"], + }, + }); + const status = await client.scrape.getStatus(started.jobId); + + expect(started).toEqual({ jobId: "job_123" }); + expect(status).toEqual({ status: "completed" }); + expect(server.requests).toEqual([ + { + method: "POST", + url: "/api/scrape", + apiKey: "test-api-key", + contentType: "application/json", + body: { + url: "https://example.com", + scrapeOptions: { + formats: ["markdown"], + }, + }, + }, + { + method: "GET", + url: "/api/scrape/job_123/status", + apiKey: "test-api-key", + contentType: "application/json", + body: undefined, + }, + ]); + }); +}); diff --git a/vitest.config.ts b/vitest.config.ts index ebec04b..2ea13a1 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -2,7 +2,7 @@ import { defineConfig } from "vitest/config"; export default defineConfig({ test: { - include: ["tests/sandbox/e2e/**/*.test.ts"], + include: ["tests/integration/**/*.test.ts", "tests/sandbox/e2e/**/*.test.ts"], setupFiles: ["./tests/load-env.ts"], environment: "node", fileParallelism: false, From 78ebac85e50698cf4339ea44b8741d0eb6710a14 Mon Sep 17 00:00:00 2001 From: Ninad Sinha Date: Mon, 11 May 2026 17:28:54 -0700 Subject: [PATCH 2/2] Update workflow to use merge_queues --- .github/workflows/integration-tests.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index 7ea2258..f3e5834 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -1,9 +1,9 @@ name: Integration Tests on: - pull_request: - push: - branches: [main] + merge_group: + types: [checks_requested] + workflow_dispatch: concurrency: group: node-integration-tests-${{ github.workflow }}-${{ github.ref }} @@ -14,7 +14,7 @@ permissions: jobs: integration-tests: - runs-on: ubuntu-latest + runs-on: depot-ubuntu-24.04-16 steps: - name: Check out repository