mirror of
https://github.com/garrytan/gstack.git
synced 2026-05-01 19:25:10 +02:00
feat: screenshot element/region clipping (v0.3.7) (#56)
* feat: screenshot element/region clipping (--clip, --viewport, CSS/@ref)
Add element crop (CSS selector or @ref), region clip (--clip x,y,w,h),
and viewport-only (--viewport) modes to the screenshot command. Uses
Playwright's native locator.screenshot() and page.screenshot({ clip }).
Full page remains the default. Includes 10 new tests covering all modes
and error paths.
* chore: bump version and changelog (v0.3.7)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* docs: add screenshot modes to BROWSER.md command reference
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -315,6 +315,107 @@ describe('Visual', () => {
|
||||
fs.unlinkSync(screenshotPath);
|
||||
});
|
||||
|
||||
test('screenshot --viewport saves viewport-only', async () => {
|
||||
await handleWriteCommand('goto', [baseUrl + '/basic.html'], bm);
|
||||
const p = '/tmp/browse-test-viewport.png';
|
||||
const result = await handleMetaCommand('screenshot', ['--viewport', p], bm, async () => {});
|
||||
expect(result).toContain('Screenshot saved (viewport)');
|
||||
expect(fs.existsSync(p)).toBe(true);
|
||||
expect(fs.statSync(p).size).toBeGreaterThan(1000);
|
||||
fs.unlinkSync(p);
|
||||
});
|
||||
|
||||
test('screenshot with CSS selector crops to element', async () => {
|
||||
await handleWriteCommand('goto', [baseUrl + '/basic.html'], bm);
|
||||
const p = '/tmp/browse-test-element-css.png';
|
||||
const result = await handleMetaCommand('screenshot', ['#title', p], bm, async () => {});
|
||||
expect(result).toContain('Screenshot saved (element)');
|
||||
expect(fs.existsSync(p)).toBe(true);
|
||||
expect(fs.statSync(p).size).toBeGreaterThan(100);
|
||||
fs.unlinkSync(p);
|
||||
});
|
||||
|
||||
test('screenshot with @ref crops to element', async () => {
|
||||
await handleWriteCommand('goto', [baseUrl + '/basic.html'], bm);
|
||||
await handleMetaCommand('snapshot', [], bm, async () => {});
|
||||
const p = '/tmp/browse-test-element-ref.png';
|
||||
const result = await handleMetaCommand('screenshot', ['@e1', p], bm, async () => {});
|
||||
expect(result).toContain('Screenshot saved (element)');
|
||||
expect(fs.existsSync(p)).toBe(true);
|
||||
expect(fs.statSync(p).size).toBeGreaterThan(100);
|
||||
fs.unlinkSync(p);
|
||||
});
|
||||
|
||||
test('screenshot --clip crops to region', async () => {
|
||||
await handleWriteCommand('goto', [baseUrl + '/basic.html'], bm);
|
||||
const p = '/tmp/browse-test-clip.png';
|
||||
const result = await handleMetaCommand('screenshot', ['--clip', '0,0,100,100', p], bm, async () => {});
|
||||
expect(result).toContain('Screenshot saved (clip 0,0,100,100)');
|
||||
expect(fs.existsSync(p)).toBe(true);
|
||||
expect(fs.statSync(p).size).toBeGreaterThan(100);
|
||||
fs.unlinkSync(p);
|
||||
});
|
||||
|
||||
test('screenshot --clip + selector throws', async () => {
|
||||
await handleWriteCommand('goto', [baseUrl + '/basic.html'], bm);
|
||||
try {
|
||||
await handleMetaCommand('screenshot', ['--clip', '0,0,100,100', '#title'], bm, async () => {});
|
||||
expect(true).toBe(false);
|
||||
} catch (err: any) {
|
||||
expect(err.message).toContain('Cannot use --clip with a selector/ref');
|
||||
}
|
||||
});
|
||||
|
||||
test('screenshot --viewport + --clip throws', async () => {
|
||||
await handleWriteCommand('goto', [baseUrl + '/basic.html'], bm);
|
||||
try {
|
||||
await handleMetaCommand('screenshot', ['--viewport', '--clip', '0,0,100,100'], bm, async () => {});
|
||||
expect(true).toBe(false);
|
||||
} catch (err: any) {
|
||||
expect(err.message).toContain('Cannot use --viewport with --clip');
|
||||
}
|
||||
});
|
||||
|
||||
test('screenshot --clip with invalid coords throws', async () => {
|
||||
await handleWriteCommand('goto', [baseUrl + '/basic.html'], bm);
|
||||
try {
|
||||
await handleMetaCommand('screenshot', ['--clip', 'abc'], bm, async () => {});
|
||||
expect(true).toBe(false);
|
||||
} catch (err: any) {
|
||||
expect(err.message).toContain('all must be numbers');
|
||||
}
|
||||
});
|
||||
|
||||
test('screenshot unknown flag throws', async () => {
|
||||
await handleWriteCommand('goto', [baseUrl + '/basic.html'], bm);
|
||||
try {
|
||||
await handleMetaCommand('screenshot', ['--bogus', '/tmp/foo.png'], bm, async () => {});
|
||||
expect(true).toBe(false);
|
||||
} catch (err: any) {
|
||||
expect(err.message).toContain('Unknown screenshot flag');
|
||||
}
|
||||
});
|
||||
|
||||
test('screenshot --viewport still validates path', async () => {
|
||||
await handleWriteCommand('goto', [baseUrl + '/basic.html'], bm);
|
||||
try {
|
||||
await handleMetaCommand('screenshot', ['--viewport', '/etc/evil.png'], bm, async () => {});
|
||||
expect(true).toBe(false);
|
||||
} catch (err: any) {
|
||||
expect(err.message).toContain('Path must be within');
|
||||
}
|
||||
});
|
||||
|
||||
test('screenshot with nonexistent selector throws timeout', async () => {
|
||||
await handleWriteCommand('goto', [baseUrl + '/basic.html'], bm);
|
||||
try {
|
||||
await handleMetaCommand('screenshot', ['.nonexistent-element-xyz'], bm, async () => {});
|
||||
expect(true).toBe(false);
|
||||
} catch (err: any) {
|
||||
expect(err.message).toBeDefined();
|
||||
}
|
||||
}, 10000);
|
||||
|
||||
test('responsive saves 3 screenshots', async () => {
|
||||
await handleWriteCommand('goto', [baseUrl + '/responsive.html'], bm);
|
||||
const prefix = '/tmp/browse-test-resp';
|
||||
|
||||
Reference in New Issue
Block a user