add screencast (#67)

* docs

* updates

* Fix: The handleCopy function fails to handle errors from navigator.clipboard.writeText(), causing unhandled exceptions and misleading UI feedback when clipboard operations fail.

Co-authored-by: ctate <chris@ctate.dev>

* Fix: The benchmark file uses emojis (📊, 🚀, 🔨, 📈, 📋, , ⏱️, ⚠) in console output, violating repository guidelines that forbid emojis in code and output.

Co-authored-by: ctate <chris@ctate.dev>

* Remove benchmark/run.ts from PR

* screencast

* update docs

* address comments

---------

Co-authored-by: Vercel <vercel[bot]@users.noreply.github.com>
This commit is contained in:
Chris Tate
2026-01-13 14:53:27 -06:00
committed by GitHub
co-authored by Vercel <vercel[bot]@users.noreply.github.com>
parent 673e2e266e
commit 2dc093cd62
14 changed files with 1885 additions and 6 deletions
+252
View File
@@ -378,4 +378,256 @@ describe('BrowserManager', () => {
await expect(browser.clearScopedHeaders('https://never-set.com')).resolves.not.toThrow();
});
});
describe('CDP session', () => {
it('should create CDP session on demand', async () => {
const cdp = await browser.getCDPSession();
expect(cdp).toBeDefined();
});
it('should reuse existing CDP session', async () => {
const cdp1 = await browser.getCDPSession();
const cdp2 = await browser.getCDPSession();
expect(cdp1).toBe(cdp2);
});
});
describe('screencast', () => {
it('should report screencasting state correctly', () => {
expect(browser.isScreencasting()).toBe(false);
});
it('should start screencast', async () => {
const frames: Array<{ data: string }> = [];
await browser.startScreencast((frame) => {
frames.push(frame);
});
expect(browser.isScreencasting()).toBe(true);
// Wait a bit for at least one frame
await new Promise((resolve) => setTimeout(resolve, 200));
await browser.stopScreencast();
expect(browser.isScreencasting()).toBe(false);
expect(frames.length).toBeGreaterThan(0);
});
it('should start screencast with custom options', async () => {
const frames: Array<{ data: string }> = [];
await browser.startScreencast(
(frame) => {
frames.push(frame);
},
{
format: 'png',
quality: 100,
maxWidth: 800,
maxHeight: 600,
everyNthFrame: 1,
}
);
expect(browser.isScreencasting()).toBe(true);
// Wait for a frame
await new Promise((resolve) => setTimeout(resolve, 200));
await browser.stopScreencast();
expect(frames.length).toBeGreaterThan(0);
});
it('should throw when starting screencast twice', async () => {
await browser.startScreencast(() => {});
await expect(browser.startScreencast(() => {})).rejects.toThrow('Screencast already active');
await browser.stopScreencast();
});
it('should handle stop when not screencasting', async () => {
// Should not throw
await expect(browser.stopScreencast()).resolves.not.toThrow();
});
});
describe('tab switch invalidates CDP session', () => {
// Clean up any extra tabs before each test
beforeEach(async () => {
// Close all tabs except the first one
const tabs = await browser.listTabs();
for (let i = tabs.length - 1; i > 0; i--) {
await browser.closeTab(i);
}
// Ensure we're on tab 0
await browser.switchTo(0);
// Stop any active screencast
if (browser.isScreencasting()) {
await browser.stopScreencast();
}
});
it('should not invalidate CDP when switching to same tab', async () => {
// Get CDP session for current tab
const cdp1 = await browser.getCDPSession();
// Switch to same tab - should NOT invalidate
await browser.switchTo(0);
// Should be the same session
const cdp2 = await browser.getCDPSession();
expect(cdp2).toBe(cdp1);
});
it('should invalidate CDP session on tab switch', async () => {
// Get CDP session for tab 0
const cdp1 = await browser.getCDPSession();
expect(cdp1).toBeDefined();
// Create new tab - this switches to the new tab automatically
await browser.newTab();
// Get CDP session - should be different since we're on a new page
const cdp2 = await browser.getCDPSession();
expect(cdp2).toBeDefined();
// Sessions should be different objects (different pages have different CDP sessions)
expect(cdp2).not.toBe(cdp1);
});
it('should stop screencast on tab switch', async () => {
// Start screencast on tab 0
await browser.startScreencast(() => {});
expect(browser.isScreencasting()).toBe(true);
// Create new tab and switch
await browser.newTab();
await browser.switchTo(1);
// Screencast should be stopped (it's page-specific)
expect(browser.isScreencasting()).toBe(false);
});
});
describe('input injection', () => {
it('should inject mouse move event', async () => {
await expect(
browser.injectMouseEvent({
type: 'mouseMoved',
x: 100,
y: 100,
})
).resolves.not.toThrow();
});
it('should inject mouse click events', async () => {
await expect(
browser.injectMouseEvent({
type: 'mousePressed',
x: 100,
y: 100,
button: 'left',
clickCount: 1,
})
).resolves.not.toThrow();
await expect(
browser.injectMouseEvent({
type: 'mouseReleased',
x: 100,
y: 100,
button: 'left',
})
).resolves.not.toThrow();
});
it('should inject mouse wheel event', async () => {
await expect(
browser.injectMouseEvent({
type: 'mouseWheel',
x: 100,
y: 100,
deltaX: 0,
deltaY: 100,
})
).resolves.not.toThrow();
});
it('should inject keyboard events', async () => {
await expect(
browser.injectKeyboardEvent({
type: 'keyDown',
key: 'a',
code: 'KeyA',
})
).resolves.not.toThrow();
await expect(
browser.injectKeyboardEvent({
type: 'keyUp',
key: 'a',
code: 'KeyA',
})
).resolves.not.toThrow();
});
it('should inject char event', async () => {
// CDP char events only accept single characters
await expect(
browser.injectKeyboardEvent({
type: 'char',
text: 'h',
})
).resolves.not.toThrow();
});
it('should inject keyboard with modifiers', async () => {
await expect(
browser.injectKeyboardEvent({
type: 'keyDown',
key: 'c',
code: 'KeyC',
modifiers: 2, // Ctrl
})
).resolves.not.toThrow();
});
it('should inject touch events', async () => {
await expect(
browser.injectTouchEvent({
type: 'touchStart',
touchPoints: [{ x: 100, y: 100 }],
})
).resolves.not.toThrow();
await expect(
browser.injectTouchEvent({
type: 'touchMove',
touchPoints: [{ x: 150, y: 150 }],
})
).resolves.not.toThrow();
await expect(
browser.injectTouchEvent({
type: 'touchEnd',
touchPoints: [],
})
).resolves.not.toThrow();
});
it('should inject multi-touch events', async () => {
await expect(
browser.injectTouchEvent({
type: 'touchStart',
touchPoints: [
{ x: 100, y: 100, id: 0 },
{ x: 200, y: 200, id: 1 },
],
})
).resolves.not.toThrow();
await expect(
browser.injectTouchEvent({
type: 'touchEnd',
touchPoints: [],
})
).resolves.not.toThrow();
});
});
});