diff --git a/sdk/node/README.md b/sdk/node/README.md index 187f0bc..ac9377f 100644 --- a/sdk/node/README.md +++ b/sdk/node/README.md @@ -121,37 +121,6 @@ const combined = await snap.capture({ }); ``` -### JavaScript Injection - -```typescript -// Execute custom JavaScript before capture -const interactiveScreenshot = await snap.capture({ - url: 'https://example.com', - js: ` - // Dismiss modal popup - document.querySelector('.modal-overlay')?.remove(); - - // Scroll to specific content - window.scrollTo(0, 500); - - // Click button to reveal content - document.querySelector('#show-more-btn')?.click(); - - // Wait for animation to complete - await new Promise(resolve => setTimeout(resolve, 1000)); - `, -}); - -// Combine with other options for complex scenarios -const complexCapture = await snap.capture({ - url: 'https://example.com/app', - js: 'document.querySelector(".sidebar").style.display = "none";', - css: 'body { zoom: 0.8 }', - waitForSelector: '#content-loaded', - hideSelectors: ['.ad-banner', '.cookie-notice'], -}); -``` - ## API Reference ### `new SnapAPI(apiKey, config?)` diff --git a/sdk/python/README.md b/sdk/python/README.md index b314da0..1874b4b 100644 --- a/sdk/python/README.md +++ b/sdk/python/README.md @@ -125,37 +125,6 @@ combined = snap.capture( ) ``` -### JavaScript Injection - -```python -# Execute custom JavaScript before capture -interactive_screenshot = snap.capture( - "https://example.com", - js=""" - // Dismiss modal popup - document.querySelector('.modal-overlay')?.remove(); - - // Scroll to specific content - window.scrollTo(0, 500); - - // Click button to reveal content - document.querySelector('#show-more-btn')?.click(); - - // Wait for animation to complete - await new Promise(resolve => setTimeout(resolve, 1000)); - """, -) - -# Combine with other options for complex scenarios -complex_capture = snap.capture( - "https://example.com/app", - js='document.querySelector(".sidebar").style.display = "none";', - css="body { zoom: 0.8 }", - wait_for_selector="#content-loaded", - hide_selectors=[".ad-banner", ".cookie-notice"], -) -``` - ### Error Handling ```python diff --git a/src/routes/__tests__/screenshot.test.ts b/src/routes/__tests__/screenshot.test.ts index e6e47c1..cc43b64 100644 --- a/src/routes/__tests__/screenshot.test.ts +++ b/src/routes/__tests__/screenshot.test.ts @@ -624,249 +624,4 @@ describe('Screenshot Route', () => { })) }) }) - - describe('JavaScript injection (js parameter)', () => { - it('should accept js parameter in POST request', async () => { - const mockBuffer = Buffer.from('fake-screenshot-data') - mockTakeScreenshot.mockResolvedValueOnce({ - buffer: mockBuffer, - contentType: 'image/png' - }) - - const jsCode = 'document.body.style.background = "red";' - const req = createMockRequest({ - url: "https://example.com", - js: jsCode - }) - const res = createMockResponse() - - const handler = screenshotRouter.stack.find(layer => - layer.route?.methods.post - )?.route.stack[0].handle - await handler(req, res, vi.fn()) - - expect(mockTakeScreenshot).toHaveBeenCalledWith(expect.objectContaining({ - js: jsCode - })) - }) - - it('should accept js parameter in GET request', async () => { - const mockBuffer = Buffer.from('fake-screenshot-data') - mockTakeScreenshot.mockResolvedValueOnce({ - buffer: mockBuffer, - contentType: 'image/png' - }) - - const jsCode = 'window.scrollTo(0, 500);' - const req = createMockRequest({ - url: "https://example.com", - js: jsCode - }, { method: 'GET' }) - const res = createMockResponse() - - const handler = screenshotRouter.stack.find(layer => - layer.route?.methods.get - )?.route.stack[0].handle - await handler(req, res, vi.fn()) - - expect(mockTakeScreenshot).toHaveBeenCalledWith(expect.objectContaining({ - js: jsCode - })) - }) - - it('should reject js parameter longer than 5000 characters', async () => { - const longJs = 'a'.repeat(5001) - const req = createMockRequest({ - url: "https://example.com", - js: longJs - }) - const res = createMockResponse() - - const handler = screenshotRouter.stack.find(layer => - layer.route?.methods.post - )?.route.stack[0].handle - await handler(req, res, vi.fn()) - - expect(res.status).toHaveBeenCalledWith(400) - expect(res.json).toHaveBeenCalledWith({ - error: "js: maximum 5000 characters allowed" - }) - }) - - it('should handle JS_EXECUTION_ERROR from service', async () => { - mockTakeScreenshot.mockRejectedValueOnce(new Error('JS_EXECUTION_ERROR: Script failed')) - - const req = createMockRequest({ - url: "https://example.com", - js: "throw new Error('test error');" - }) - const res = createMockResponse() - - const handler = screenshotRouter.stack.find(layer => - layer.route?.methods.post - )?.route.stack[0].handle - await handler(req, res, vi.fn()) - - expect(res.status).toHaveBeenCalledWith(400) - expect(res.json).toHaveBeenCalledWith({ - error: "JS_EXECUTION_ERROR: Script failed" - }) - }) - - it('should handle JS_TIMEOUT error from service', async () => { - mockTakeScreenshot.mockRejectedValueOnce(new Error('JS_EXECUTION_ERROR: JS_TIMEOUT')) - - const req = createMockRequest({ - url: "https://example.com", - js: "while(true) {}" - }) - const res = createMockResponse() - - const handler = screenshotRouter.stack.find(layer => - layer.route?.methods.post - )?.route.stack[0].handle - await handler(req, res, vi.fn()) - - expect(res.status).toHaveBeenCalledWith(400) - expect(res.json).toHaveBeenCalledWith({ - error: "JS_EXECUTION_ERROR: JS_TIMEOUT" - }) - }) - - describe('selector parameter', () => { - it('should pass selector parameter to takeScreenshot service', async () => { - const mockBuffer = Buffer.from('element-screenshot') - mockTakeScreenshot.mockResolvedValueOnce({ - buffer: mockBuffer, - contentType: 'image/png' - }) - - const req = createMockRequest({ - url: "https://example.com", - selector: "#main-content" - }) - const res = createMockResponse() - - const handler = screenshotRouter.stack.find(layer => - layer.route?.methods.post - )?.route.stack[0].handle - await handler(req, res, vi.fn()) - - expect(mockTakeScreenshot).toHaveBeenCalledWith( - expect.objectContaining({ - url: "https://example.com", - selector: "#main-content" - }) - ) - expect(res.send).toHaveBeenCalledWith(mockBuffer) - }) - - it('should return 400 when selector and fullPage are both provided', async () => { - const req = createMockRequest({ - url: "https://example.com", - selector: "#content", - fullPage: true - }) - const res = createMockResponse() - - const handler = screenshotRouter.stack.find(layer => - layer.route?.methods.post - )?.route.stack[0].handle - await handler(req, res, vi.fn()) - - expect(res.status).toHaveBeenCalledWith(400) - expect(res.json).toHaveBeenCalledWith({ - error: "selector and fullPage are mutually exclusive" - }) - }) - - it('should handle SELECTOR_NOT_FOUND error from service', async () => { - mockTakeScreenshot.mockRejectedValueOnce(new Error('SELECTOR_NOT_FOUND')) - - const req = createMockRequest({ - url: "https://example.com", - selector: "#nonexistent" - }) - const res = createMockResponse() - - const handler = screenshotRouter.stack.find(layer => - layer.route?.methods.post - )?.route.stack[0].handle - await handler(req, res, vi.fn()) - - expect(res.status).toHaveBeenCalledWith(400) - expect(res.json).toHaveBeenCalledWith({ - error: "Element not found: #nonexistent" - }) - }) - }) - }) - - describe('GET /v1/screenshot selector parameter', () => { - it('should pass selector from query string to takeScreenshot service', async () => { - const mockBuffer = Buffer.from('element-screenshot') - mockTakeScreenshot.mockResolvedValueOnce({ - buffer: mockBuffer, - contentType: 'image/png' - }) - - const req = createMockRequest({ - url: "https://example.com", - selector: ".main-section" - }, { method: 'GET' }) - const res = createMockResponse() - - const handler = screenshotRouter.stack.find(layer => - layer.route?.methods.get - )?.route.stack[0].handle - await handler(req, res, vi.fn()) - - expect(mockTakeScreenshot).toHaveBeenCalledWith( - expect.objectContaining({ - url: "https://example.com", - selector: ".main-section" - }) - ) - expect(res.send).toHaveBeenCalledWith(mockBuffer) - }) - - it('should return 400 when selector and fullPage are both provided in GET request', async () => { - const req = createMockRequest({ - url: "https://example.com", - selector: "#content", - fullPage: "true" - }, { method: 'GET' }) - const res = createMockResponse() - - const handler = screenshotRouter.stack.find(layer => - layer.route?.methods.get - )?.route.stack[0].handle - await handler(req, res, vi.fn()) - - expect(res.status).toHaveBeenCalledWith(400) - expect(res.json).toHaveBeenCalledWith({ - error: "selector and fullPage are mutually exclusive" - }) - }) - - it('should handle SELECTOR_NOT_FOUND error from service in GET request', async () => { - mockTakeScreenshot.mockRejectedValueOnce(new Error('SELECTOR_NOT_FOUND')) - - const req = createMockRequest({ - url: "https://example.com", - selector: "#missing" - }, { method: 'GET' }) - const res = createMockResponse() - - const handler = screenshotRouter.stack.find(layer => - layer.route?.methods.get - )?.route.stack[0].handle - await handler(req, res, vi.fn()) - - expect(res.status).toHaveBeenCalledWith(400) - expect(res.json).toHaveBeenCalledWith({ - error: "Element not found: #missing" - }) - }) - }) }) \ No newline at end of file diff --git a/src/routes/screenshot.ts b/src/routes/screenshot.ts index ee3a7a4..bc167bb 100644 --- a/src/routes/screenshot.ts +++ b/src/routes/screenshot.ts @@ -80,16 +80,6 @@ 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 @@ -117,9 +107,6 @@ export const screenshotRouter = Router(); * mobile: * summary: Mobile viewport * value: { "url": "https://example.com", "width": 375, "height": 812, "deviceScale": 2 } - * element: - * summary: Element screenshot - * value: { "url": "https://github.com", "selector": "#readme" } * responses: * 200: * description: Screenshot image binary @@ -259,20 +246,6 @@ 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: selector - * in: query - * schema: - * type: string - * maxLength: 200 - * description: CSS selector for element to capture instead of full page/viewport (max 200 chars) - * example: "#main-content" * - name: darkMode * in: query * schema: @@ -358,8 +331,6 @@ async function handleScreenshotRequest(req: any, res: any) { darkMode, hideSelectors, css, - js, - selector, } = source; if (!url || typeof url !== "string") { @@ -373,12 +344,6 @@ 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) { @@ -401,12 +366,6 @@ 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, @@ -423,8 +382,6 @@ async function handleScreenshotRequest(req: any, res: any) { darkMode: darkMode === true || darkMode === "true", hideSelectors: normalizedHideSelectors, css: css || undefined, - js: js || undefined, - selector: selector || undefined, }; try { @@ -466,11 +423,7 @@ 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 === "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")) { + if (err.message.includes("blocked") || err.message.includes("not allowed") || err.message.includes("Invalid URL") || err.message.includes("Could not resolve")) { res.status(400).json({ error: err.message }); return; } diff --git a/src/services/__tests__/screenshot.test.ts b/src/services/__tests__/screenshot.test.ts index 2e7b77b..e9ae600 100644 --- a/src/services/__tests__/screenshot.test.ts +++ b/src/services/__tests__/screenshot.test.ts @@ -410,184 +410,4 @@ describe('Screenshot Service', () => { expect(releasePage).not.toHaveBeenCalled() }) }) - - describe('JavaScript injection (js parameter)', () => { - beforeEach(() => { - mockPage.evaluate = vi.fn().mockResolvedValue(undefined) - }) - - it('executes custom JavaScript when js parameter is provided', async () => { - const jsCode = 'document.body.style.background = "red";' - - await takeScreenshot({ - url: 'https://example.com', - js: jsCode - }) - - expect(mockPage.evaluate).toHaveBeenCalledWith(jsCode) - }) - - it('does not call page.evaluate when js parameter is not provided', async () => { - await takeScreenshot({ url: 'https://example.com' }) - - expect(mockPage.evaluate).not.toHaveBeenCalled() - }) - - it('executes JavaScript after delay but before CSS injection', async () => { - const jsCode = 'window.scrollTo(0, 100);' - const cssCode = 'body { color: red; }' - - await takeScreenshot({ - url: 'https://example.com', - delay: 1000, - js: jsCode, - css: cssCode - }) - - // Check that methods were called in the right order - expect(mockPage.goto).toHaveBeenCalledBefore(mockPage.evaluate as any) - expect(mockPage.evaluate).toHaveBeenCalledBefore(mockPage.addStyleTag as any) - }) - - it('throws JS_EXECUTION_ERROR when JavaScript execution fails', async () => { - mockPage.evaluate = vi.fn().mockRejectedValueOnce(new Error('ReferenceError: foo is not defined')) - - await expect(takeScreenshot({ - url: 'https://example.com', - js: 'console.log(foo);' - })).rejects.toThrow('JS_EXECUTION_ERROR: ReferenceError: foo is not defined') - }) - - it('throws JS_TIMEOUT error when JavaScript execution takes too long', async () => { - // Mock a long-running script that never resolves - mockPage.evaluate = vi.fn().mockReturnValue(new Promise(() => {})) - - await expect(takeScreenshot({ - url: 'https://example.com', - js: 'while(true) {}' - })).rejects.toThrow('JS_EXECUTION_ERROR: JS_TIMEOUT') - }) - - it('handles JavaScript execution with hideSelectors and CSS', async () => { - const jsCode = 'document.querySelector(".modal").remove();' - - await takeScreenshot({ - url: 'https://example.com', - js: jsCode, - hideSelectors: ['.popup'], - css: 'body { font-size: 16px; }' - }) - - expect(mockPage.evaluate).toHaveBeenCalledWith(jsCode) - expect(mockPage.addStyleTag).toHaveBeenCalledTimes(2) // Once for CSS, once for hideSelectors - }) - }) - - describe('selector parameter for element screenshots', () => { - let mockElement: any - - beforeEach(() => { - mockElement = { - screenshot: vi.fn().mockResolvedValue(Buffer.from('element-screenshot')) - } - mockPage.$ = vi.fn().mockResolvedValue(mockElement) - }) - - it('uses element.screenshot() when selector is provided', async () => { - await takeScreenshot({ url: 'https://example.com', selector: '#main-content' }) - expect(mockPage.$).toHaveBeenCalledWith('#main-content') - expect(mockElement.screenshot).toHaveBeenCalledWith( - expect.objectContaining({ - type: 'png', - encoding: 'binary' - }) - ) - expect(mockPage.screenshot).not.toHaveBeenCalled() - }) - - it('passes through format and quality to element.screenshot()', async () => { - await takeScreenshot({ - url: 'https://example.com', - selector: '.widget', - format: 'jpeg', - quality: 85 - }) - expect(mockElement.screenshot).toHaveBeenCalledWith( - expect.objectContaining({ - type: 'jpeg', - quality: 85, - encoding: 'binary' - }) - ) - }) - - it('throws SELECTOR_NOT_FOUND when element is not found', async () => { - mockPage.$.mockResolvedValueOnce(null) - await expect(takeScreenshot({ - url: 'https://example.com', - selector: '#nonexistent' - })).rejects.toThrow('SELECTOR_NOT_FOUND') - }) - - it('validates selector length (max 200 chars)', async () => { - const longSelector = 'div'.repeat(100) // 300 chars - await expect(takeScreenshot({ - url: 'https://example.com', - selector: longSelector - })).rejects.toThrow('selector is too long') - }) - - it('validates selector for dangerous content (javascript:)', async () => { - await expect(takeScreenshot({ - url: 'https://example.com', - selector: 'javascript:alert(1)' - })).rejects.toThrow('selector contains dangerous content') - }) - - it('validates selector for dangerous content (script tag)', async () => { - await expect(takeScreenshot({ - url: 'https://example.com', - selector: '' - })).rejects.toThrow('selector contains dangerous content') - }) - - it('rejects selector and fullPage used together', async () => { - await expect(takeScreenshot({ - url: 'https://example.com', - selector: '#content', - fullPage: true - })).rejects.toThrow('selector and fullPage are mutually exclusive') - }) - - it('works with all other parameters (delay, css, hideSelectors, etc.)', async () => { - await takeScreenshot({ - url: 'https://example.com', - selector: '#main', - delay: 1000, - css: 'body { color: red; }', - hideSelectors: ['.ad'], - darkMode: true - }) - - expect(mockPage.emulateMediaFeatures).toHaveBeenCalledWith([ - { name: 'prefers-color-scheme', value: 'dark' } - ]) - expect(mockPage.addStyleTag).toHaveBeenCalledWith({ content: 'body { color: red; }' }) - expect(mockPage.addStyleTag).toHaveBeenCalledWith({ content: '.ad { display: none !important }' }) - expect(mockPage.$).toHaveBeenCalledWith('#main') - expect(mockElement.screenshot).toHaveBeenCalled() - }) - - it('does not use page.screenshot() when selector is provided', async () => { - await takeScreenshot({ url: 'https://example.com', selector: '.element' }) - expect(mockPage.screenshot).not.toHaveBeenCalled() - expect(mockElement.screenshot).toHaveBeenCalled() - }) - - it('uses page.screenshot() when selector is not provided', async () => { - await takeScreenshot({ url: 'https://example.com' }) - expect(mockPage.screenshot).toHaveBeenCalled() - expect(mockPage.$).not.toHaveBeenCalled() - }) - }) }) diff --git a/src/services/screenshot.ts b/src/services/screenshot.ts index 3eb85f6..eaa50e3 100644 --- a/src/services/screenshot.ts +++ b/src/services/screenshot.ts @@ -17,8 +17,6 @@ export interface ScreenshotOptions { darkMode?: boolean; hideSelectors?: string[]; css?: string; - js?: string; - selector?: string; } export interface ScreenshotResult { @@ -62,15 +60,6 @@ function validateCSS(css: string): void { } } -function validateSelector(selector: string): void { - if (selector.length > 200) { - throw new Error("selector is too long"); - } - if (selector.includes("javascript:") || selector.includes(" { // Validate URL for SSRF await validateUrl(opts.url); @@ -88,15 +77,6 @@ export async function takeScreenshot(opts: ScreenshotOptions): Promise setTimeout(r, Math.min(opts.delay!, 5000))); } - if (opts.js) { - try { - await Promise.race([ - page.evaluate(opts.js), - new Promise((_, reject) => setTimeout(() => reject(new Error("JS_TIMEOUT")), 5000)) - ]); - } catch (err: any) { - throw new Error("JS_EXECUTION_ERROR: " + (err.message || "Script failed")); - } - } - if (opts.css) { await page.addStyleTag({ content: opts.css }); } @@ -152,26 +121,12 @@ export async function takeScreenshot(opts: ScreenshotOptions): Promise