feat: add darkMode and hideSelectors screenshot parameters
All checks were successful
Build & Deploy to Staging / Build & Deploy to Staging (push) Successful in 9m31s

- darkMode: emulates prefers-color-scheme: dark before navigation
- hideSelectors: injects CSS to hide elements before capture
  - POST: accepts string or string array
  - GET: accepts comma-separated string
  - Validation: max 10 selectors, each max 200 chars
- OpenAPI docs updated for both GET and POST endpoints
- 13 new tests added (service + route)
This commit is contained in:
Hoid 2026-03-04 12:06:26 +01:00
parent 9575d312fe
commit 96d21aa63b
5 changed files with 232 additions and 4 deletions

View file

@ -71,6 +71,19 @@ export const screenshotRouter = Router();
* maximum: 5000
* default: 0
* description: Extra delay in ms after page load before capturing
* darkMode:
* type: boolean
* default: false
* description: Emulate prefers-color-scheme dark mode
* hideSelectors:
* oneOf:
* - type: string
* description: Single CSS selector or comma-separated list
* - type: array
* items:
* type: string
* maxItems: 10
* description: CSS selectors to hide before capture (max 10 items, each max 200 chars)
* waitUntil:
* type: string
* enum: [load, domcontentloaded, networkidle0, networkidle2]
@ -221,6 +234,18 @@ export const screenshotRouter = Router();
* enum: [load, domcontentloaded, networkidle0, networkidle2]
* default: domcontentloaded
* description: Page load event to wait for before capturing
* - name: darkMode
* in: query
* schema:
* type: boolean
* default: false
* description: Emulate prefers-color-scheme dark mode
* - name: hideSelectors
* in: query
* schema:
* type: string
* description: Comma-separated CSS selectors to hide before capture (max 10 items, each max 200 chars)
* example: ".ad,#cookie-banner,.popup"
* - name: cache
* in: query
* schema:
@ -291,6 +316,8 @@ async function handleScreenshotRequest(req: any, res: any) {
delay,
waitUntil,
cache,
darkMode,
hideSelectors,
} = source;
if (!url || typeof url !== "string") {
@ -298,6 +325,28 @@ async function handleScreenshotRequest(req: any, res: any) {
return;
}
// Normalize hideSelectors: string | string[] → string[]
let normalizedHideSelectors: string[] | undefined;
if (hideSelectors) {
if (typeof hideSelectors === 'string') {
normalizedHideSelectors = hideSelectors.split(',').map((s: string) => s.trim()).filter(Boolean);
} else if (Array.isArray(hideSelectors)) {
normalizedHideSelectors = hideSelectors;
}
}
// Validate hideSelectors
if (normalizedHideSelectors) {
if (normalizedHideSelectors.length > 10) {
res.status(400).json({ error: "hideSelectors: maximum 10 selectors allowed" });
return;
}
if (normalizedHideSelectors.some((s: string) => s.length > 200)) {
res.status(400).json({ error: "hideSelectors: each selector must be 200 characters or less" });
return;
}
}
// Normalize parameters
const params = {
url,
@ -311,6 +360,8 @@ async function handleScreenshotRequest(req: any, res: any) {
delay: delay ? parseInt(delay, 10) : undefined,
waitUntil: ["load", "domcontentloaded", "networkidle0", "networkidle2"].includes(waitUntil) ? waitUntil : undefined,
cache,
darkMode: darkMode === true || darkMode === "true",
hideSelectors: normalizedHideSelectors,
};
try {