Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
@@ -1 +1,8 @@
HEADLESS=false
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=<encoded_caps>
# Example LambdaTest: wss://cdp.lambdatest.com/playwright?capabilities=<encoded_caps>
BROWSER_WS_ENDPOINT=
27 changes: 27 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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=<encoded_capabilities>

# LambdaTest example
BROWSER_WS_ENDPOINT=wss://cdp.lambdatest.com/playwright?capabilities=<encoded_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.

Expand Down
17 changes: 13 additions & 4 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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();
Expand Down Expand Up @@ -52,16 +58,17 @@ 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');
});

// 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;

Expand Down Expand Up @@ -108,6 +115,8 @@ const port = process.env.PORT || 5510;
}
} catch (error) {
next(error);
} finally {
await browser.close();
}
return;
});
Expand Down
109 changes: 109 additions & 0 deletions src/utils/index.ts
Original file line number Diff line number Diff line change
@@ -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' });
}
}
43 changes: 43 additions & 0 deletions tests/browser-config.spec.ts
Original file line number Diff line number Diff line change
@@ -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;
});
});
});