Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion impit-node/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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.mjs",
"universal": "napi universal",
"copy-version": "napi version"
},
Expand Down
15 changes: 15 additions & 0 deletions impit-node/test/e2e/basic.e2e.mjs
Original file line number Diff line number Diff line change
@@ -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.`);
102 changes: 102 additions & 0 deletions impit-node/test/e2e/run-e2e-tests.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
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) => (path.isAbsolute(p) ? p : path.resolve(process.cwd(), p));

function buildSpawnArgsFor(filePath) {
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) {
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) => {
if (settled) return;
settled = true;
clearTimeout(timer);
if (err) {
reject(err);
} else {
resolve();
}
};

const timer = setTimeout(() => {
try {
cp.kill('SIGKILL');
} catch {
// ignore
}
const err = new Error(`Subprocess timed out after ${timeoutMs} ms`);
console.error(err);
err.code = 'ETIMEDOUT';
done(err);
}, timeoutMs);

cp.on('error', (err) => done(err));
cp.on('exit', (code, signal) => {
if (signal) {
done(new Error(`Subprocess terminated by signal ${signal}`));
} else if (code && code !== 0) {
const err = 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);
try {
await runOne(abs);
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') {
process.exit(err.code);
} else {
process.exit(1);
}
}
}

console.log('[e2e] All done.');
12 changes: 6 additions & 6 deletions impit-python/python/impit/impit.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -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"`).
Expand Down Expand Up @@ -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"`).
Expand Down
Loading