Skip to main content
Glama
element-screenshots.test.ts14 kB
/* * This file is part of BrowserLoop. * * BrowserLoop is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published * by the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * BrowserLoop is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with BrowserLoop. If not, see <https://www.gnu.org/licenses/>. */ import assert from 'node:assert'; import { readFile } from 'node:fs/promises'; import { createServer, type Server } from 'node:http'; import { dirname, join } from 'node:path'; import { after, before, describe, test } from 'node:test'; import { fileURLToPath } from 'node:url'; import { ScreenshotService } from '../../src/screenshot-service.js'; import { createTestScreenshotServiceConfig, isValidBase64Image, } from '../../src/test-utils.js'; import type { ScreenshotServiceConfig } from '../../src/types.js'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); describe('Element Screenshots E2E', () => { let server: Server; let screenshotService: ScreenshotService; const port = 3003; const baseUrl = `http://localhost:${port}`; function createTestConfig(): ScreenshotServiceConfig { return createTestScreenshotServiceConfig({ screenshot: { defaultFormat: 'png', defaultQuality: 80, defaultTimeout: 30000, defaultWaitForNetworkIdle: false, }, }); } before(async () => { server = createServer(async (req, res) => { let filePath: string; if (req.url === '/element-test.html' || req.url === '/') { filePath = join(__dirname, '../../../tests/fixtures/element-test.html'); } else if (req.url === '/simple-page.html') { filePath = join(__dirname, '../../../tests/fixtures/simple-page.html'); } else { res.writeHead(404, { 'Content-Type': 'text/plain' }); res.end('Not Found'); return; } try { const content = await readFile(filePath, 'utf-8'); res.writeHead(200, { 'Content-Type': 'text/html' }); res.end(content); } catch (_error) { res.writeHead(500, { 'Content-Type': 'text/plain' }); res.end('Server Error'); } }); await new Promise<void>((resolve) => { server.listen(port, () => { resolve(); }); }); screenshotService = new ScreenshotService(createTestConfig()); await screenshotService.initialize(); }); after(async () => { if (screenshotService) { await screenshotService.cleanup(); } if (server) { server.close(); } }); describe('Element Selection and Capture', () => { test('should capture screenshot of element by class selector', async () => { const url = `${baseUrl}/simple-page.html`; const options = { url, selector: '.container', format: 'png' as const, timeout: 10000, }; const result = await screenshotService.takeElementScreenshot(options); assert.ok( isValidBase64Image(result.data), 'Element screenshot has valid base64 data' ); assert.strictEqual( result.mimeType, 'image/png', 'Element screenshot has correct MIME type' ); assert.ok( typeof result.width === 'number' && result.width > 0, 'Element has valid width' ); assert.ok( typeof result.height === 'number' && result.height > 0, 'Element has valid height' ); assert.ok( typeof result.timestamp === 'number', 'Element screenshot has timestamp' ); console.log( `Container element dimensions: ${result.width}x${result.height}` ); }); test('should capture screenshot of h1 element', async () => { const url = `${baseUrl}/simple-page.html`; const options = { url, selector: 'h1', format: 'png' as const, timeout: 10000, }; const result = await screenshotService.takeElementScreenshot(options); assert.ok( isValidBase64Image(result.data), 'H1 element screenshot is valid' ); assert.strictEqual( result.mimeType, 'image/png', 'H1 element has correct MIME type' ); assert.ok( result.width > 0 && result.height > 0, 'H1 element has valid dimensions' ); console.log(`H1 element dimensions: ${result.width}x${result.height}`); }); test('should capture screenshot of test element', async () => { const url = `${baseUrl}/simple-page.html`; const options = { url, selector: '.test-element', format: 'webp' as const, timeout: 10000, }; const result = await screenshotService.takeElementScreenshot(options); assert.ok( isValidBase64Image(result.data), 'Test element screenshot is valid' ); assert.strictEqual( result.mimeType, 'image/webp', 'Test element has correct MIME type' ); assert.ok( result.width > 0 && result.height > 0, 'Test element has valid dimensions' ); console.log(`Test element dimensions: ${result.width}x${result.height}`); }); test('should capture screenshot by ID selector', async () => { const url = `${baseUrl}/simple-page.html`; const options = { url, selector: '#timestamp', format: 'png' as const, timeout: 10000, }; const result = await screenshotService.takeElementScreenshot(options); assert.ok( isValidBase64Image(result.data), 'Timestamp element screenshot is valid' ); assert.ok( result.width > 0 && result.height > 0, 'Timestamp element has valid dimensions' ); console.log( `Timestamp element dimensions: ${result.width}x${result.height}` ); }); }); describe('Element Screenshot vs Other Methods', () => { test('should produce different results than viewport screenshot', async () => { const url = `${baseUrl}/simple-page.html`; const baseOptions = { url, width: 1280, height: 720, format: 'png' as const, timeout: 10000, }; const viewportResult = await screenshotService.takeScreenshot(baseOptions); const elementResult = await screenshotService.takeElementScreenshot({ ...baseOptions, selector: '.container', }); assert.ok( isValidBase64Image(viewportResult.data), 'Viewport screenshot is valid' ); assert.ok( isValidBase64Image(elementResult.data), 'Element screenshot is valid' ); assert.strictEqual( viewportResult.width, 1280, 'Viewport screenshot has correct width' ); assert.strictEqual( viewportResult.height, 720, 'Viewport screenshot has correct height' ); assert.ok( elementResult.width !== viewportResult.width || elementResult.height !== viewportResult.height, 'Element screenshot should have different dimensions than viewport' ); assert.ok( elementResult.width < viewportResult.width, 'Element width should be smaller than viewport' ); assert.ok( elementResult.height < viewportResult.height, 'Element height should be smaller than viewport' ); console.log(`Viewport: ${viewportResult.width}x${viewportResult.height}`); console.log(`Element: ${elementResult.width}x${elementResult.height}`); }); test('should handle small and large elements differently', async () => { const url = `${baseUrl}/simple-page.html`; const baseOptions = { url, format: 'png' as const, timeout: 10000, }; const smallElementResult = await screenshotService.takeElementScreenshot({ ...baseOptions, selector: '#timestamp', }); const largeElementResult = await screenshotService.takeElementScreenshot({ ...baseOptions, selector: '.container', }); assert.ok( isValidBase64Image(smallElementResult.data), 'Small element screenshot is valid' ); assert.ok( isValidBase64Image(largeElementResult.data), 'Large element screenshot is valid' ); assert.ok( largeElementResult.width > smallElementResult.width, 'Large element should be wider than small element' ); assert.ok( largeElementResult.height > smallElementResult.height, 'Large element should be taller than small element' ); console.log( `Small element: ${smallElementResult.width}x${smallElementResult.height}` ); console.log( `Large element: ${largeElementResult.width}x${largeElementResult.height}` ); }); }); describe('Element Screenshot Error Handling', () => { test('should throw error for non-existent element', async () => { const options = { url: `${baseUrl}/simple-page.html`, selector: '#non-existent-element', format: 'png' as const, timeout: 5000, }; try { await screenshotService.takeElementScreenshot(options); assert.fail('Should have thrown error for non-existent element'); } catch (error) { assert.ok(error instanceof Error, 'Should throw Error instance'); assert.ok( error.message.includes('Element not found'), 'Error message should mention element not found' ); assert.ok( error.message.includes('#non-existent-element'), 'Error message should include selector' ); } }); test('should throw error when selector is missing', async () => { const options = { url: `${baseUrl}/simple-page.html`, format: 'png' as const, timeout: 5000, }; try { await screenshotService.takeElementScreenshot(options); assert.fail('Should have thrown error for missing selector'); } catch (error) { assert.ok(error instanceof Error, 'Should throw Error instance'); assert.ok( error.message.includes('Selector is required'), 'Error message should mention selector requirement' ); } }); test('should handle invalid CSS selectors gracefully', async () => { const options = { url: `${baseUrl}/simple-page.html`, selector: ':::invalid-selector', format: 'png' as const, timeout: 5000, }; try { await screenshotService.takeElementScreenshot(options); assert.fail('Should have thrown error for invalid CSS selector'); } catch (error) { assert.ok(error instanceof Error, 'Should throw Error instance'); assert.ok( error.message.length > 0, 'Error should have descriptive message' ); } }); }); describe('Element Screenshot Quality and Formats', () => { test('should handle different image formats for element screenshots', async () => { const url = `${baseUrl}/simple-page.html`; const baseOptions = { url, selector: '.container', timeout: 10000, }; const pngResult = await screenshotService.takeElementScreenshot({ ...baseOptions, format: 'png' as const, }); const webpResult = await screenshotService.takeElementScreenshot({ ...baseOptions, format: 'webp' as const, }); assert.ok( isValidBase64Image(pngResult.data), 'PNG element screenshot is valid' ); assert.ok( isValidBase64Image(webpResult.data), 'WebP element screenshot is valid' ); assert.strictEqual( pngResult.mimeType, 'image/png', 'PNG element has correct MIME type' ); assert.strictEqual( webpResult.mimeType, 'image/webp', 'WebP element has correct MIME type' ); assert.strictEqual( pngResult.width, webpResult.width, 'Both formats have same width' ); assert.strictEqual( pngResult.height, webpResult.height, 'Both formats have same height' ); }); }); describe('Performance and Resource Management', () => { test('should handle multiple element screenshots efficiently', async () => { const url = `${baseUrl}/simple-page.html`; const selectors = ['.container', 'h1', '.test-element']; const startTime = Date.now(); const screenshots = await Promise.all( selectors.map((selector) => screenshotService.takeElementScreenshot({ url, selector, format: 'png' as const, timeout: 10000, }) ) ); const endTime = Date.now(); const totalTime = endTime - startTime; screenshots.forEach((result, index) => { assert.ok( isValidBase64Image(result.data), `Element screenshot ${index + 1} is valid` ); assert.ok( result.width > 0 && result.height > 0, `Element screenshot ${index + 1} has valid dimensions` ); console.log( `Element ${selectors[index]}: ${result.width}x${result.height}` ); }); assert.ok( totalTime < 30000, `Multiple element screenshots should complete in reasonable time (${totalTime}ms)` ); }); }); });

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/mattiasw/browserloop'

If you have feedback or need assistance with the MCP directory API, please join our Discord server