diff --git a/.env.example b/.env.example index 55aeeb2..a63d786 100644 --- a/.env.example +++ b/.env.example @@ -1 +1,8 @@ -HEADLESS=false \ No newline at end of file +HEADLESS=false + +# Remote browser configuration (BrowserStack, LambdaTest, etc.) +# Set BROWSER_WS_ENDPOINT to connect to a remote browser via CDP +# Leave empty or unset to use a local browser +# Example BrowserStack: wss://cdp.browserstack.com/playwright?caps= +# Example LambdaTest: wss://cdp.lambdatest.com/playwright?capabilities= +BROWSER_WS_ENDPOINT= \ No newline at end of file diff --git a/README.md b/README.md index 432217b..5434695 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,33 @@ dev - Starts the app in development mode with live reloading build - Compiles TypeScript to JavaScript start - Runs the compiled app test - Runs unit tests + +## Remote Browser Support + +This application supports connecting to remote browser services like BrowserStack or LambdaTest via CDP (Chrome DevTools Protocol). + +### Configuration + +Set the `BROWSER_WS_ENDPOINT` environment variable to connect to a remote browser: + +```bash +# BrowserStack example +BROWSER_WS_ENDPOINT=wss://cdp.browserstack.com/playwright?caps= + +# LambdaTest example +BROWSER_WS_ENDPOINT=wss://cdp.lambdatest.com/playwright?capabilities= +``` + +When `BROWSER_WS_ENDPOINT` is not set, the application uses a local browser. + +### Environment Variables + +| Variable | Description | Default | +|----------|-------------|---------| +| `BROWSER_WS_ENDPOINT` | WebSocket endpoint for remote browser connection | (empty - uses local browser) | +| `HEADLESS` | Run browser in headless mode (only applies to local browser) | `true` | +| `PORT` | Server port | `5510` | + Deployment Notes on how to deploy the app to production. diff --git a/src/index.ts b/src/index.ts index 155003a..8bd07b1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,7 +1,8 @@ -import { Browser, chromium } from '@playwright/test'; +import { Browser } from '@playwright/test'; import express, { NextFunction, Request, Response } from 'express'; import 'dotenv/config'; +import { getBrowser } from './utils'; export type CaptureOptions = { width?: number; @@ -10,6 +11,11 @@ export type CaptureOptions = { fullPage?: boolean; }; +export type BrowserConfig = { + wsEndpoint?: string; + headless?: boolean; +}; + // Function to capture a screenshot using a shared browser instance const capturePage = async (browser: Browser, url, options?: CaptureOptions) => { const context = await browser.newContext(); @@ -52,9 +58,6 @@ const port = process.env.PORT || 5510; // Launch browser once when the server starts (async () => { - const browser = await chromium.launch({ headless: process.env.HEADLESS !== 'false' }); - app.locals.browser = browser; - // Health check endpoint app.get('/health', (req, res) => { res.type('text').send('ok'); @@ -62,6 +65,10 @@ const port = process.env.PORT || 5510; // Main endpoint to capture and serve screenshots app.get('/', async (req: Request, res: Response, next: NextFunction) => { + const browser = await getBrowser(); + + app.locals.browser = browser; + try { const { url, format, width, height, selector, fullPage } = req.query as any; @@ -108,6 +115,8 @@ const port = process.env.PORT || 5510; } } catch (error) { next(error); + } finally { + await browser.close(); } return; }); diff --git a/src/utils/index.ts b/src/utils/index.ts new file mode 100644 index 0000000..b0e7a63 --- /dev/null +++ b/src/utils/index.ts @@ -0,0 +1,109 @@ +import { Browser, chromium } from '@playwright/test'; + +import 'dotenv/config'; + +function uuidv4() { + let d = new Date().getTime(); + if (typeof performance !== 'undefined' && typeof performance.now === 'function') { + d += performance.now(); + } + const chars = '0123456789abcdef'; + let uuid = ''; + + // Tạo 16 byte ngẫu nhiên + const bytes = []; + for (let i = 0; i < 16; i++) { + bytes[i] = Math.floor((1 + Math.random()) * 0x10000) & 0xff; + } + + // Đảm bảo phiên bản UUID 4 (bit 4-5 của byte 6 = 0100) + bytes[6] = (bytes[6] & 0x0f) | 0x40; + + // Đảm bảo nhóm bit (bit 6-7 của byte 8 = 10xx) + bytes[8] = (bytes[8] & 0x3f) | 0x80; + + // Định dạng UUID + for (let i = 0; i < 16; i++) { + if (i === 4 || i === 6 || i === 8 || i === 10) { + uuid += '-'; + } + uuid += chars[(bytes[i] >> 4) & 0xf]; + uuid += chars[bytes[i] & 0xf]; + } + + return uuid; +} + +export async function createLambdaTestBrowser() { + const capabilities = { + browserName: 'Chrome', // Browsers allowed: `Chrome`, `MicrosoftEdge`, `pw-chromium`, `pw-firefox` and `pw-webkit` + browserVersion: 'latest', + 'LT:Options': { + platform: 'Windows 10', + build: 'dev', + name: `${uuidv4()} - web-capture`, + user: process.env.WS_USER, + accessKey: process.env.WS_PASSWORD, + network: true, + video: true, + console: true, + }, + }; + const wsEndpoint = `wss://cdp.lambdatest.com/playwright?capabilities=${encodeURIComponent( + JSON.stringify(capabilities) + )}`; + console.log('use lambdatest ws endpoint:', wsEndpoint); + return await chromium.connect(wsEndpoint); +} + +export async function createBrowserStackBrowser() { + const caps = { + os: 'Windows', // 'os x', + os_version: '11', //'big sur', + browser: 'chrome', // You can choose `chrome`, `edge` or `firefox` in this capability + browser_version: 'latest', // We support v83 and above. You can choose `latest`, `latest-beta`, `latest-1`, `latest-2` and so on, in this capability + 'browserstack.username': process.env.WS_USER, + 'browserstack.accessKey': process.env.WS_PASSWORD, + // 'browserstack.geoLocation': 'FR', + project: `web-capture`, + build: `playwright-build-$${uuidv4()}`, + name: 'Capture Test', // The name of your test and build. See browserstack.com/docs/automate/playwright/organize tests for more details + buildTag: 'reg', + resolution: '1920x1080', + // 'browserstack.local': 'true', + // 'browserstack.localIdentifier': 'local_connection_name', + // 'browserstack.playwrightVersion': '1.latest', + // 'client.playwrightVersion': '1.latest', + // 'browserstack.debug': 'true', // enabling visual logs + // 'browserstack.console': 'info', // Enabling Console logs for the test + // 'browserstack.networkLogs': 'true', // Enabling network logs for the test + // 'browserstack.interactiveDebugging': 'true', + }; + + const wsEndpoint = `wss://cdp.browserstack.com/playwright?caps=${encodeURIComponent( + JSON.stringify(caps) + )}`; + console.log('use browserstack ws endpoint:', wsEndpoint); + return await chromium.connect(wsEndpoint); +} + +export async function createBrowserWs() { + const wsEndpoint = process.env.BROWSER_WS_ENDPOINT; + console.log('use custom ws endpoint:', wsEndpoint); + return await chromium.connect(wsEndpoint!); +} + +export async function getBrowser() { + switch (process.env.BROWSER_PROVIDER) { + case 'lambdatest': + return await createLambdaTestBrowser(); + case 'browserstack': + return await createBrowserStackBrowser(); + default: + console.log('launch local browser'); + if (process.env.BROWSER_WS_ENDPOINT) { + return await createBrowserWs(); + } + return await chromium.launch({ headless: process.env.HEADLESS !== 'false' }); + } +} diff --git a/tests/browser-config.spec.ts b/tests/browser-config.spec.ts new file mode 100644 index 0000000..f0c0b00 --- /dev/null +++ b/tests/browser-config.spec.ts @@ -0,0 +1,43 @@ +describe('Browser Configuration Types', () => { + describe('BrowserConfig environment variable handling', () => { + it('should handle BROWSER_WS_ENDPOINT environment variable', () => { + // Test the logic for determining browser config + const wsEndpoint = process.env.BROWSER_WS_ENDPOINT; + const isRemote = wsEndpoint && wsEndpoint.length > 0; + + // When wsEndpoint is not set, should use local browser + expect(isRemote).toBeFalsy(); + }); + + it('should handle HEADLESS environment variable', () => { + // Test the logic for determining headless mode + const headless = process.env.HEADLESS !== 'false'; + + // When HEADLESS is not set to 'false', should be true (default) + expect(typeof headless).toBe('boolean'); + }); + + it('should correctly parse headless as false when HEADLESS=false', () => { + const originalHeadless = process.env.HEADLESS; + process.env.HEADLESS = 'false'; + + const headless = process.env.HEADLESS !== 'false'; + expect(headless).toBe(false); + + // Restore + process.env.HEADLESS = originalHeadless; + }); + + it('should correctly use wsEndpoint for remote browser connection', () => { + const originalWsEndpoint = process.env.BROWSER_WS_ENDPOINT; + process.env.BROWSER_WS_ENDPOINT = 'wss://cdp.browserstack.com/playwright'; + + const wsEndpoint = process.env.BROWSER_WS_ENDPOINT; + const isRemote = wsEndpoint && wsEndpoint.length > 0; + expect(isRemote).toBe(true); + + // Restore + process.env.BROWSER_WS_ENDPOINT = originalWsEndpoint; + }); + }); +});