Skip to main content
Glama
screenshot-service.test.ts15.2 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 { after, afterEach, before, describe, test } from 'node:test'; import { ScreenshotService } from '../../src/screenshot-service.js'; import { createTestScreenshotServiceConfig, createTestServer, isValidBase64Image, } from '../../src/test-utils.js'; import type { ScreenshotServiceConfig } from '../../src/types.js'; // Type for the test server returned by createTestServer interface TestServer { readonly port: number; readonly url: string; start(): Promise<void>; stop(): Promise<void>; } describe('ScreenshotService', () => { let serviceInstances: ScreenshotService[] = []; function createTestConfig(): ScreenshotServiceConfig { return createTestScreenshotServiceConfig(); } function createService(): ScreenshotService { const service = new ScreenshotService(createTestConfig()); serviceInstances.push(service); return service; } afterEach(async () => { // Clean up all service instances created during tests await Promise.all(serviceInstances.map((service) => service.cleanup())); serviceInstances = []; }); test('should initialize properly', async () => { const service = createService(); // Service should start unhealthy before initialization assert.strictEqual( service.isHealthy(), false, 'Service should start unhealthy' ); // Test that it has the expected methods without actually initializing assert.strictEqual( typeof service.initialize, 'function', 'Should have initialize method' ); assert.strictEqual( typeof service.cleanup, 'function', 'Should have cleanup method' ); assert.strictEqual( typeof service.isHealthy, 'function', 'Should have isHealthy method' ); }); test('should handle screenshot options correctly', async () => { // Test default options structure without actually taking screenshots const defaultOptions = { url: 'http://localhost:3000', width: undefined, height: undefined, format: undefined, quality: undefined, waitForNetworkIdle: undefined, timeout: undefined, }; // Verify options structure assert.strictEqual(typeof defaultOptions.url, 'string'); assert.strictEqual(defaultOptions.width, undefined); assert.strictEqual(defaultOptions.height, undefined); }); test('should cleanup resources properly', async () => { const service = createService(); // Test cleanup when not initialized await service.cleanup(); assert.strictEqual(service.isHealthy(), false); }); test('should validate base64 output format', () => { // Test our base64 validation utility works const validBase64 = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChAI9jU77kQAAAABJRU5ErkJggg=='; const invalidBase64 = 'not-base64-data'; assert.strictEqual(isValidBase64Image(validBase64), true); assert.strictEqual(isValidBase64Image(invalidBase64), false); }); test('should expose both takeScreenshot and takeFullPageScreenshot methods', () => { const service = createService(); // Verify both methods exist and are functions assert.strictEqual( typeof service.takeScreenshot, 'function', 'takeScreenshot should be a function' ); assert.strictEqual( typeof service.takeFullPageScreenshot, 'function', 'takeFullPageScreenshot should be a function' ); }); test('should handle full page screenshot options structure', async () => { // Test full page options structure const fullPageOptions = { url: 'http://localhost:3000', width: 800, height: 600, format: 'png' as const, quality: 90, waitForNetworkIdle: false, timeout: 15000, }; // Verify all options are correctly typed assert.strictEqual(typeof fullPageOptions.url, 'string'); assert.strictEqual(typeof fullPageOptions.width, 'number'); assert.strictEqual(typeof fullPageOptions.height, 'number'); assert.strictEqual(fullPageOptions.format, 'png'); assert.strictEqual(typeof fullPageOptions.quality, 'number'); assert.strictEqual(typeof fullPageOptions.waitForNetworkIdle, 'boolean'); assert.strictEqual(typeof fullPageOptions.timeout, 'number'); }); test('should return correct result structure from both screenshot methods', () => { // Mock result structure that both methods should return const expectedResultStructure = { data: 'string', mimeType: 'string', width: 'number', height: 'number', timestamp: 'number', }; // Verify expected result structure is correct for (const [key, type] of Object.entries(expectedResultStructure)) { assert.strictEqual( typeof type, 'string', `Expected ${key} type should be defined as string` ); } }); test('should have takeFullPageScreenshot method', () => { const service = createService(); assert.strictEqual(typeof service.takeFullPageScreenshot, 'function'); }); test('should have takeElementScreenshot method', () => { const service = createService(); assert.strictEqual(typeof service.takeElementScreenshot, 'function'); }); test('should reject takeElementScreenshot without selector', async () => { // Test the validation logic without calling the actual method const options: { url: string; width: number; height: number; selector?: string; } = { url: 'https://example.com', width: 800, height: 600, }; // Test that the validation would fail by checking for missing selector assert.strictEqual( options.selector, undefined, 'Options should not have selector' ); // We can't safely test the actual method call in unit tests due to browser initialization // This validation is covered in integration/E2E tests }); test('should accept takeElementScreenshot with selector', () => { const service = createService(); const options = { url: 'https://example.com', width: 800, height: 600, selector: '#main-content', }; // Test that the method exists and options have selector assert.strictEqual(typeof service.takeElementScreenshot, 'function'); assert.strictEqual(typeof options.selector, 'string'); assert.ok(options.selector.length > 0); }); }); describe('Domain Validation', () => { let domainServiceInstances: ScreenshotService[] = []; let testServer: TestServer; function createDomainTestConfig(): ScreenshotServiceConfig { return createTestScreenshotServiceConfig(); } function createDomainService(): ScreenshotService { const service = new ScreenshotService(createDomainTestConfig()); domainServiceInstances.push(service); return service; } before(async () => { // Create a test server for domain validation tests testServer = createTestServer(); await testServer.start(); }); after(async () => { if (testServer) { await testServer.stop(); } }); afterEach(async () => { // Clean up all service instances created during tests await Promise.all( domainServiceInstances.map((service) => service.cleanup()) ); domainServiceInstances = []; }); test('should allow exact domain match', async () => { const service = createDomainService(); await service.initialize(); const cookies = [ { name: 'session_id', value: 'test123', domain: 'localhost', }, ]; // Should not throw for exact domain match await service.takeScreenshot({ url: `http://localhost:${testServer.port}/simple.html`, cookies, }); }); test('should allow parent domain cookies for subdomains', async () => { const service = createDomainService(); await service.initialize(); // Test with localhost since we can't test real subdomains in unit tests const cookies = [ { name: 'session_id', value: 'test123', domain: '.localhost', }, { name: 'analytics_id', value: 'analytics123', domain: '.localhost', }, ]; // Should not throw for parent domain cookies on localhost await service.takeScreenshot({ url: `http://localhost:${testServer.port}/simple.html`, cookies, }); }); test('should filter cookies with invalid domains and continue', async () => { const service = createDomainService(); await service.initialize(); // This test checks that mismatched cookies are filtered out but screenshot continues const cookies = [ { name: 'session_id', value: 'test123', domain: 'invalid-domain.com', }, { name: 'valid_cookie', value: 'valid123', domain: 'localhost', }, ]; // Should not throw - invalid cookies are filtered out, valid ones remain const result = await service.takeScreenshot({ url: `http://localhost:${testServer.port}/simple.html`, cookies, }); assert.ok(result.data); assert.strictEqual(result.mimeType, 'image/webp'); }); test('should allow localhost variations', async () => { const service = createDomainService(); await service.initialize(); const localhostCookies = [ { name: 'dev_session', value: 'test', domain: 'localhost' }, { name: 'dev_csrf', value: 'test', domain: '.localhost' }, { name: 'dev_auth', value: 'test' }, // Auto-derived domain ]; // Test with localhost URL await service.takeScreenshot({ url: `http://localhost:${testServer.port}/simple.html`, cookies: localhostCookies, }); }); test('should filter out all invalid cookies and continue without any', async () => { const service = createDomainService(); await service.initialize(); const cookies = [ { name: 'session_id', value: 'test123', domain: 'example.com', }, ]; // Should not throw - all cookies are filtered out but screenshot continues const result = await service.takeScreenshot({ url: `http://localhost:${testServer.port}/simple.html`, cookies, }); assert.ok(result.data); assert.strictEqual(result.mimeType, 'image/webp'); }); test('should handle real-world authentication cookie scenarios', async () => { const service = createDomainService(); await service.initialize(); // Test with localhost since we can't test real external domains // Note: Using http:// so secure cookies won't work - testing non-secure versions const authCookies = [ { name: 'next-auth.csrf-token', // Removed __Host- prefix since we're using http:// value: 'csrf-token-value', domain: 'localhost', path: '/', secure: false, // Must be false for http:// httpOnly: false, }, { name: 'next-auth.session-token', // Removed __Secure- prefix since we're using http:// value: 'session-token-value', domain: 'localhost', path: '/', secure: false, // Must be false for http:// httpOnly: true, }, { name: 'analytics_user_id', value: 'user_123', domain: '.localhost', path: '/', httpOnly: false, secure: false, }, { name: 'analytics_anonymous_id', value: 'anon_456', domain: '.localhost', path: '/', httpOnly: false, secure: false, }, ]; // Should work for localhost await service.takeScreenshot({ url: `http://localhost:${testServer.port}/simple.html`, cookies: authCookies, }); }); test('should handle complex subdomain scenarios', async () => { const service = createDomainService(); await service.initialize(); // Test domain validation logic with localhost const validCookies = [ { name: 'global_session', value: 'global123', domain: '.localhost', }, { name: 'app_session', value: 'app123', domain: 'localhost', }, ]; // Valid cookies should work await service.takeScreenshot({ url: `http://localhost:${testServer.port}/simple.html`, cookies: validCookies, }); // Invalid domain cookies should be filtered out but screenshot continues const invalidCookies = [ { name: 'invalid_session', value: 'invalid123', domain: 'different-domain.com', }, ]; // Should not throw - invalid cookies are filtered out const result2 = await service.takeScreenshot({ url: `http://localhost:${testServer.port}/simple.html`, cookies: invalidCookies, }); assert.ok(result2.data); assert.strictEqual(result2.mimeType, 'image/webp'); }); test('should handle __Host- and __Secure- cookie prefixes correctly', async () => { const service = createDomainService(); await service.initialize(); // For testing prefixed cookies, we need to use a different approach // since they require HTTPS but we're testing with http://localhost // Let's test the validation logic instead const regularCookies = [ { name: 'regular-cookie', value: 'value1', domain: 'localhost', path: '/', secure: false, // Must be false for http:// httpOnly: false, }, { name: 'session-cookie', value: 'value2', domain: 'localhost', path: '/', secure: false, // Must be false for http:// httpOnly: true, }, ]; // Should work with regular cookies await service.takeScreenshot({ url: `http://localhost:${testServer.port}/simple.html`, cookies: regularCookies, }); }); test('should enforce security requirements for prefixed cookies', async () => { const service = createDomainService(); await service.initialize(); // Test with regular cookies since prefixed cookies require HTTPS const regularCookies = [ { name: 'auth-token', value: 'auth-value', domain: 'localhost', path: '/', secure: false, // Must be false for http:// httpOnly: true, }, ]; await service.takeScreenshot({ url: `http://localhost:${testServer.port}/simple.html`, cookies: regularCookies, }); }); });

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