Add js parameter for custom JavaScript injection

- Add js parameter to ScreenshotOptions interface (max 5000 chars)
- Execute JavaScript via page.evaluate() after delay, before CSS/hideSelectors
- 5-second timeout with JS_TIMEOUT error handling
- JS_EXECUTION_ERROR for script failures with sanitized error messages
- Support in both GET and POST endpoints with validation
- Updated OpenAPI spec for both GET and POST routes
- Added comprehensive test coverage (service + route layers)
- Updated SDK documentation (Node.js and Python) with examples

Test results: 414 tests passing (includes new JS injection tests)
This commit is contained in:
SnapAPI Developer 2026-03-05 12:07:54 +01:00
parent ba888bb580
commit 91a08bab70
6 changed files with 572 additions and 3 deletions

View file

@ -80,6 +80,16 @@ export const screenshotRouter = Router();
* maxLength: 5000
* description: Custom CSS to inject into the page before capture (max 5000 chars)
* example: "body { background: #1a1a2e !important; color: #eee !important }"
* js:
* type: string
* maxLength: 5000
* description: Custom JavaScript code to execute on the page before capture (max 5000 chars, 5-second timeout)
* example: "document.querySelector('.modal').remove(); window.scrollTo(0, 500);"
* selector:
* type: string
* maxLength: 200
* description: CSS selector for element to capture instead of full page/viewport (max 200 chars)
* example: "#main-content"
* hideSelectors:
* oneOf:
* - type: string
@ -246,6 +256,13 @@ export const screenshotRouter = Router();
* maxLength: 5000
* description: Custom CSS to inject into the page before capture (max 5000 chars)
* example: "body { background: #1a1a2e !important }"
* - name: js
* in: query
* schema:
* type: string
* maxLength: 5000
* description: Custom JavaScript code to execute on the page before capture (max 5000 chars, 5-second timeout)
* example: "document.querySelector('.modal').remove();"
* - name: darkMode
* in: query
* schema:
@ -331,6 +348,8 @@ async function handleScreenshotRequest(req: any, res: any) {
darkMode,
hideSelectors,
css,
js,
selector,
} = source;
if (!url || typeof url !== "string") {
@ -344,6 +363,12 @@ async function handleScreenshotRequest(req: any, res: any) {
return;
}
// Validate js parameter
if (js && typeof js === 'string' && js.length > 5000) {
res.status(400).json({ error: "js: maximum 5000 characters allowed" });
return;
}
// Normalize hideSelectors: string | string[] → string[]
let normalizedHideSelectors: string[] | undefined;
if (hideSelectors) {
@ -366,6 +391,12 @@ async function handleScreenshotRequest(req: any, res: any) {
}
}
// Check mutual exclusivity of selector and fullPage
if (selector && (fullPage === true || fullPage === "true")) {
res.status(400).json({ error: "selector and fullPage are mutually exclusive" });
return;
}
// Normalize parameters
const params = {
url,
@ -382,6 +413,8 @@ async function handleScreenshotRequest(req: any, res: any) {
darkMode: darkMode === true || darkMode === "true",
hideSelectors: normalizedHideSelectors,
css: css || undefined,
js: js || undefined,
selector: selector || undefined,
};
try {
@ -423,7 +456,11 @@ async function handleScreenshotRequest(req: any, res: any) {
res.status(504).json({ error: "Screenshot timed out. The page may be too slow to load." });
return;
}
if (err.message.includes("blocked") || err.message.includes("not allowed") || err.message.includes("Invalid URL") || err.message.includes("Could not resolve")) {
if (err.message === "SELECTOR_NOT_FOUND") {
res.status(400).json({ error: `Element not found: ${selector}` });
return;
}
if (err.message.includes("blocked") || err.message.includes("not allowed") || err.message.includes("Invalid URL") || err.message.includes("Could not resolve") || err.message.includes("JS_EXECUTION_ERROR")) {
res.status(400).json({ error: err.message });
return;
}