From 0776b02fe8ed3581383d46c6ec8e8cca8df0c1f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jind=C5=99ich=20B=C3=A4r?= Date: Mon, 20 Oct 2025 08:57:34 +0200 Subject: [PATCH 1/6] chore(ci): add e2e test scaffolding and an example test --- impit-node/package.json | 2 +- impit-node/test/e2e/basic.e2e.mts | 15 ++++ impit-node/test/e2e/run-e2e-tests.mts | 100 ++++++++++++++++++++++++++ 3 files changed, 116 insertions(+), 1 deletion(-) create mode 100644 impit-node/test/e2e/basic.e2e.mts create mode 100644 impit-node/test/e2e/run-e2e-tests.mts diff --git a/impit-node/package.json b/impit-node/package.json index 15b7118d..e3183444 100644 --- a/impit-node/package.json +++ b/impit-node/package.json @@ -46,7 +46,7 @@ "build:debug": "napi build --platform --no-const-enum", "docs": "npm run build:debug && typedoc --plugin typedoc-plugin-mdn-links ./index.d.ts --out ./docs", "prepublishOnly": "napi prepublish -t npm --no-gh-release", - "test": "vitest --retry=3", + "test": "vitest --retry=3 && node ./test/e2e/run-e2e-tests.mts", "universal": "napi universal", "copy-version": "napi version" }, diff --git a/impit-node/test/e2e/basic.e2e.mts b/impit-node/test/e2e/basic.e2e.mts new file mode 100644 index 00000000..21c9b6ff --- /dev/null +++ b/impit-node/test/e2e/basic.e2e.mts @@ -0,0 +1,15 @@ +import { Impit } from "../../index.wrapper.js"; +import assert from "node:assert"; + +const impit = new Impit({ + browser: "chrome", + ignoreTlsErrors: true, +}); + +const response = await impit.fetch("https://api.apify.com/v2/browser-info"); + +assert.equal(response.status, 200, "Response status should be 200"); +assert.ok(response.headers.get("content-type")?.includes("application/json"), "Response should be JSON"); +assert.equal(await response.json().then(data => data.headers['accept-encoding']), "gzip, deflate, br, zstd", "Accept-Encoding header should be correct"); + +console.log(`[${import.meta.filename.split('/').pop()}] All assertions passed.`); diff --git a/impit-node/test/e2e/run-e2e-tests.mts b/impit-node/test/e2e/run-e2e-tests.mts new file mode 100644 index 00000000..f6ed2315 --- /dev/null +++ b/impit-node/test/e2e/run-e2e-tests.mts @@ -0,0 +1,100 @@ +import { readdir } from 'node:fs/promises'; +import { spawn } from 'node:child_process'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const exts = new Set(['.mts', '.mjs', '.js', '.cts', '.cjs', '.ts']); + +const selfPath = fileURLToPath(import.meta.url); +const selfDir = path.dirname(selfPath); +const selfBase = path.basename(selfPath); + +const toAbs = (p: string) => (path.isAbsolute(p) ? p : path.resolve(process.cwd(), p)); + +function buildSpawnArgsFor(filePath: string): { cmd: string; args: string[] } { + const nodeCmd = process.argv[0]; // path to node + const resolvedArgv = process.argv.map(toAbs); + const selfIdx = resolvedArgv.findIndex((a) => a === selfPath); + + if (selfIdx > -1) { + // Replicate the original invocation "prefix" (e.g. tsx/ts-node loaders etc.) + const prefix = process.argv.slice(1, selfIdx); // keep as-is (can include non-absolute args) + return { cmd: nodeCmd, args: [...prefix, filePath] }; + } + + // Fallback: use execArgv (e.g. --loader ts-node/esm, --experimental-loader, etc.) + return { cmd: nodeCmd, args: [...process.execArgv, filePath] }; +} + +async function runOne(filePath: string): Promise { + const { cmd, args } = buildSpawnArgsFor(filePath); + await new Promise((resolve, reject) => { + const cp = spawn(cmd, args, { stdio: 'inherit', cwd: process.cwd(), env: process.env }); + + let settled = false; + const timeoutMs = 30_000; + + const done = (err?: NodeJS.ErrnoException) => { + if (settled) return; + settled = true; + clearTimeout(timer); + if (err) { + reject(err); + } else { + resolve(); + } + }; + + const timer = setTimeout(() => { + try { + cp.kill('SIGKILL'); + } catch { + // ignore + } + const err: NodeJS.ErrnoException = new Error(`Subprocess timed out after ${timeoutMs} ms`); + err.code = 'ETIMEDOUT' as any; + done(err); + }, timeoutMs); + + cp.on('error', (err) => done(err as NodeJS.ErrnoException)); + cp.on('exit', (code, signal) => { + if (signal) { + done(new Error(`Subprocess terminated by signal ${signal}`) as NodeJS.ErrnoException); + } else if (code && code !== 0) { + const err: NodeJS.ErrnoException = new Error(`Subprocess exited with code ${code}`); + // @ts-expect-error attach code for upper-level handling + err.code = code; + done(err); + } else { + done(); + } + }); + }); +} + +const entries = await readdir(selfDir, { withFileTypes: true }); + +const files = entries + .filter((ent) => ent.isFile()) + .map((ent) => ent.name) + .filter((name) => name !== selfBase) + .filter((name) => exts.has(path.extname(name))) + .sort((a, b) => a.localeCompare(b)); + +for (let i = 0; i < files.length; i++) { + const file = files[i]; + const abs = path.join(selfDir, file); + console.log(`[e2e] (${i + 1}/${files.length}) ${file}`); + try { + await runOne(abs); + } catch (err: any) { + console.error(`[e2e] Failed: ${file}`); + if (err && typeof err.code === 'number') { + process.exit(err.code); + } else { + process.exit(1); + } + } +} + +console.log('[e2e] All done.'); From 1c724ed650a785656ecfe6359e48dd72ec9f5ba7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jind=C5=99ich=20B=C3=A4r?= Date: Mon, 20 Oct 2025 09:10:32 +0200 Subject: [PATCH 2/6] chore: rewrite e2e tests to JavaScript for Node < 22 CI runner compatibility --- impit-node/package.json | 2 +- .../test/e2e/{basic.e2e.mts => basic.e2e.mjs} | 0 .../{run-e2e-tests.mts => run-e2e-tests.mjs} | 24 +++++++++---------- 3 files changed, 13 insertions(+), 13 deletions(-) rename impit-node/test/e2e/{basic.e2e.mts => basic.e2e.mjs} (100%) rename impit-node/test/e2e/{run-e2e-tests.mts => run-e2e-tests.mjs} (77%) diff --git a/impit-node/package.json b/impit-node/package.json index e3183444..d29316e1 100644 --- a/impit-node/package.json +++ b/impit-node/package.json @@ -46,7 +46,7 @@ "build:debug": "napi build --platform --no-const-enum", "docs": "npm run build:debug && typedoc --plugin typedoc-plugin-mdn-links ./index.d.ts --out ./docs", "prepublishOnly": "napi prepublish -t npm --no-gh-release", - "test": "vitest --retry=3 && node ./test/e2e/run-e2e-tests.mts", + "test": "vitest --retry=3 && node ./test/e2e/run-e2e-tests.mjs", "universal": "napi universal", "copy-version": "napi version" }, diff --git a/impit-node/test/e2e/basic.e2e.mts b/impit-node/test/e2e/basic.e2e.mjs similarity index 100% rename from impit-node/test/e2e/basic.e2e.mts rename to impit-node/test/e2e/basic.e2e.mjs diff --git a/impit-node/test/e2e/run-e2e-tests.mts b/impit-node/test/e2e/run-e2e-tests.mjs similarity index 77% rename from impit-node/test/e2e/run-e2e-tests.mts rename to impit-node/test/e2e/run-e2e-tests.mjs index f6ed2315..35024fbf 100644 --- a/impit-node/test/e2e/run-e2e-tests.mts +++ b/impit-node/test/e2e/run-e2e-tests.mjs @@ -9,9 +9,9 @@ const selfPath = fileURLToPath(import.meta.url); const selfDir = path.dirname(selfPath); const selfBase = path.basename(selfPath); -const toAbs = (p: string) => (path.isAbsolute(p) ? p : path.resolve(process.cwd(), p)); +const toAbs = (p) => (path.isAbsolute(p) ? p : path.resolve(process.cwd(), p)); -function buildSpawnArgsFor(filePath: string): { cmd: string; args: string[] } { +function buildSpawnArgsFor(filePath) { const nodeCmd = process.argv[0]; // path to node const resolvedArgv = process.argv.map(toAbs); const selfIdx = resolvedArgv.findIndex((a) => a === selfPath); @@ -26,15 +26,15 @@ function buildSpawnArgsFor(filePath: string): { cmd: string; args: string[] } { return { cmd: nodeCmd, args: [...process.execArgv, filePath] }; } -async function runOne(filePath: string): Promise { +async function runOne(filePath) { const { cmd, args } = buildSpawnArgsFor(filePath); - await new Promise((resolve, reject) => { + await new Promise((resolve, reject) => { const cp = spawn(cmd, args, { stdio: 'inherit', cwd: process.cwd(), env: process.env }); let settled = false; const timeoutMs = 30_000; - const done = (err?: NodeJS.ErrnoException) => { + const done = (err) => { if (settled) return; settled = true; clearTimeout(timer); @@ -51,17 +51,17 @@ async function runOne(filePath: string): Promise { } catch { // ignore } - const err: NodeJS.ErrnoException = new Error(`Subprocess timed out after ${timeoutMs} ms`); - err.code = 'ETIMEDOUT' as any; + const err = new Error(`Subprocess timed out after ${timeoutMs} ms`); + err.code = 'ETIMEDOUT'; done(err); }, timeoutMs); - cp.on('error', (err) => done(err as NodeJS.ErrnoException)); + cp.on('error', (err) => done(err)); cp.on('exit', (code, signal) => { if (signal) { - done(new Error(`Subprocess terminated by signal ${signal}`) as NodeJS.ErrnoException); + done(new Error(`Subprocess terminated by signal ${signal}`)); } else if (code && code !== 0) { - const err: NodeJS.ErrnoException = new Error(`Subprocess exited with code ${code}`); + const err = new Error(`Subprocess exited with code ${code}`); // @ts-expect-error attach code for upper-level handling err.code = code; done(err); @@ -84,10 +84,10 @@ const files = entries for (let i = 0; i < files.length; i++) { const file = files[i]; const abs = path.join(selfDir, file); - console.log(`[e2e] (${i + 1}/${files.length}) ${file}`); try { await runOne(abs); - } catch (err: any) { + console.log(`[e2e] (${i + 1}/${files.length}) Passed ${file}`); + } catch (err) { console.error(`[e2e] Failed: ${file}`); if (err && typeof err.code === 'number') { process.exit(err.code); From 4aff51b75adbd322d8a97ad054be19479d76ffe8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jind=C5=99ich=20B=C3=A4r?= Date: Mon, 20 Oct 2025 13:59:04 +0200 Subject: [PATCH 3/6] chore: log process errors on non-0 exitcode --- impit-node/test/e2e/run-e2e-tests.mjs | 1 + 1 file changed, 1 insertion(+) diff --git a/impit-node/test/e2e/run-e2e-tests.mjs b/impit-node/test/e2e/run-e2e-tests.mjs index 35024fbf..b2b5aad9 100644 --- a/impit-node/test/e2e/run-e2e-tests.mjs +++ b/impit-node/test/e2e/run-e2e-tests.mjs @@ -90,6 +90,7 @@ for (let i = 0; i < files.length; i++) { } catch (err) { console.error(`[e2e] Failed: ${file}`); if (err && typeof err.code === 'number') { + console.error(err); process.exit(err.code); } else { process.exit(1); From d2d73812852f77ca9294b218ad1c2ba6d9214b09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jind=C5=99ich=20B=C3=A4r?= Date: Mon, 20 Oct 2025 14:20:17 +0200 Subject: [PATCH 4/6] chore: log on timeout errors --- impit-node/test/e2e/run-e2e-tests.mjs | 1 + 1 file changed, 1 insertion(+) diff --git a/impit-node/test/e2e/run-e2e-tests.mjs b/impit-node/test/e2e/run-e2e-tests.mjs index b2b5aad9..159352e4 100644 --- a/impit-node/test/e2e/run-e2e-tests.mjs +++ b/impit-node/test/e2e/run-e2e-tests.mjs @@ -52,6 +52,7 @@ async function runOne(filePath) { // ignore } const err = new Error(`Subprocess timed out after ${timeoutMs} ms`); + console.error(err); err.code = 'ETIMEDOUT'; done(err); }, timeoutMs); From d1be6a8caa97dc3522770a383270fd10a20eb6af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jind=C5=99ich=20B=C3=A4r?= Date: Mon, 20 Oct 2025 14:25:30 +0200 Subject: [PATCH 5/6] docs: fix wrong documentation rendering --- impit-python/python/impit/impit.pyi | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/impit-python/python/impit/impit.pyi b/impit-python/python/impit/impit.pyi index d3a09603..34b4f3c5 100644 --- a/impit-python/python/impit/impit.pyi +++ b/impit-python/python/impit/impit.pyi @@ -370,10 +370,10 @@ class Response: class Client: """Synchronous HTTP client with browser impersonation capabilities. - .. note:: - You can reuse the :class:`Client` instance to make multiple requests. + .. note:: + You can reuse the :class:`Client` instance to make multiple requests. - All requests made by the same client will share the same configuration, resources (e.g., cookie jar and connection pool), and other settings. + All requests made by the same client will share the same configuration, resources (e.g., cookie jar and connection pool), and other settings. Args: browser: Browser to impersonate (`"chrome"` or `"firefox"`). @@ -701,10 +701,10 @@ class Client: class AsyncClient: """Asynchronous HTTP client with browser impersonation capabilities. - .. note:: - You can reuse the :class:`Client` instance to make multiple requests. + .. note:: + You can reuse the :class:`Client` instance to make multiple requests. - All requests made by the same client will share the same configuration, resources (e.g., cookie jar and connection pool), and other settings. + All requests made by the same client will share the same configuration, resources (e.g., cookie jar and connection pool), and other settings. Args: browser: Browser to impersonate (`"chrome"` or `"firefox"`). From 23f5a8f2521386711f35f8fd3f7e29bf934302ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jind=C5=99ich=20B=C3=A4r?= Date: Mon, 20 Oct 2025 14:28:04 +0200 Subject: [PATCH 6/6] chore: try getting the error description on failed e2e test --- impit-node/test/e2e/run-e2e-tests.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/impit-node/test/e2e/run-e2e-tests.mjs b/impit-node/test/e2e/run-e2e-tests.mjs index 159352e4..18ca08a6 100644 --- a/impit-node/test/e2e/run-e2e-tests.mjs +++ b/impit-node/test/e2e/run-e2e-tests.mjs @@ -90,8 +90,8 @@ for (let i = 0; i < files.length; i++) { console.log(`[e2e] (${i + 1}/${files.length}) Passed ${file}`); } catch (err) { console.error(`[e2e] Failed: ${file}`); + console.error(err); if (err && typeof err.code === 'number') { - console.error(err); process.exit(err.code); } else { process.exit(1);