diff --git a/cli/src/commands.rs b/cli/src/commands.rs index a329889..3cb74aa 100644 --- a/cli/src/commands.rs +++ b/cli/src/commands.rs @@ -198,7 +198,17 @@ pub fn parse_command(args: &[String], flags: &Flags) -> Option { let op = rest.get(1).unwrap_or(&"get"); let key = rest.get(2); let value = rest.get(3); - Some(json!({ "id": id, "action": "storage", "storageType": storage_type, "operation": op, "key": key, "value": value })) + match *op { + "set" => Some(json!({ "id": id, "action": "storage_set", "type": storage_type, "key": key?, "value": value? })), + "clear" => Some(json!({ "id": id, "action": "storage_clear", "type": storage_type })), + _ => { + let mut cmd = json!({ "id": id, "action": "storage_get", "type": storage_type }); + if let Some(k) = key { + cmd.as_object_mut().unwrap().insert("key".to_string(), json!(k)); + } + Some(cmd) + } + } } _ => None, }, @@ -207,10 +217,13 @@ pub fn parse_command(args: &[String], flags: &Flags) -> Option { "cookies" => { let op = rest.get(0).unwrap_or(&"get"); match *op { - "get" => Some(json!({ "id": id, "action": "cookies", "operation": "get", "name": rest.get(1) })), - "set" => Some(json!({ "id": id, "action": "cookies", "operation": "set", "name": rest.get(1)?, "value": rest.get(2)? })), - "clear" => Some(json!({ "id": id, "action": "cookies", "operation": "clear" })), - _ => Some(json!({ "id": id, "action": "cookies", "operation": "get" })), + "set" => { + let name = rest.get(1)?; + let value = rest.get(2)?; + Some(json!({ "id": id, "action": "cookies_set", "cookies": [{ "name": name, "value": value }] })) + } + "clear" => Some(json!({ "id": id, "action": "cookies_clear" })), + _ => Some(json!({ "id": id, "action": "cookies_get" })), } } @@ -355,3 +368,271 @@ fn parse_set(rest: &[&str], id: &str) -> Option { _ => None, } } + +#[cfg(test)] +mod tests { + use super::*; + + fn default_flags() -> Flags { + Flags { + session: "test".to_string(), + json: false, + full: false, + headed: false, + debug: false, + } + } + + fn args(s: &str) -> Vec { + s.split_whitespace().map(String::from).collect() + } + + // === Cookies Tests === + + #[test] + fn test_cookies_get() { + let cmd = parse_command(&args("cookies"), &default_flags()).unwrap(); + assert_eq!(cmd["action"], "cookies_get"); + } + + #[test] + fn test_cookies_get_explicit() { + let cmd = parse_command(&args("cookies get"), &default_flags()).unwrap(); + assert_eq!(cmd["action"], "cookies_get"); + } + + #[test] + fn test_cookies_set() { + let cmd = parse_command(&args("cookies set mycookie myvalue"), &default_flags()).unwrap(); + assert_eq!(cmd["action"], "cookies_set"); + assert_eq!(cmd["cookies"][0]["name"], "mycookie"); + assert_eq!(cmd["cookies"][0]["value"], "myvalue"); + } + + #[test] + fn test_cookies_set_missing_value() { + let result = parse_command(&args("cookies set mycookie"), &default_flags()); + assert!(result.is_none()); + } + + #[test] + fn test_cookies_clear() { + let cmd = parse_command(&args("cookies clear"), &default_flags()).unwrap(); + assert_eq!(cmd["action"], "cookies_clear"); + } + + // === Storage Tests === + + #[test] + fn test_storage_local_get() { + let cmd = parse_command(&args("storage local"), &default_flags()).unwrap(); + assert_eq!(cmd["action"], "storage_get"); + assert_eq!(cmd["type"], "local"); + assert!(cmd.get("key").is_none()); + } + + #[test] + fn test_storage_local_get_key() { + let cmd = parse_command(&args("storage local get mykey"), &default_flags()).unwrap(); + assert_eq!(cmd["action"], "storage_get"); + assert_eq!(cmd["type"], "local"); + assert_eq!(cmd["key"], "mykey"); + } + + #[test] + fn test_storage_session_get() { + let cmd = parse_command(&args("storage session"), &default_flags()).unwrap(); + assert_eq!(cmd["action"], "storage_get"); + assert_eq!(cmd["type"], "session"); + } + + #[test] + fn test_storage_local_set() { + let cmd = parse_command(&args("storage local set mykey myvalue"), &default_flags()).unwrap(); + assert_eq!(cmd["action"], "storage_set"); + assert_eq!(cmd["type"], "local"); + assert_eq!(cmd["key"], "mykey"); + assert_eq!(cmd["value"], "myvalue"); + } + + #[test] + fn test_storage_session_set() { + let cmd = parse_command(&args("storage session set skey svalue"), &default_flags()).unwrap(); + assert_eq!(cmd["action"], "storage_set"); + assert_eq!(cmd["type"], "session"); + assert_eq!(cmd["key"], "skey"); + assert_eq!(cmd["value"], "svalue"); + } + + #[test] + fn test_storage_set_missing_value() { + let result = parse_command(&args("storage local set mykey"), &default_flags()); + assert!(result.is_none()); + } + + #[test] + fn test_storage_local_clear() { + let cmd = parse_command(&args("storage local clear"), &default_flags()).unwrap(); + assert_eq!(cmd["action"], "storage_clear"); + assert_eq!(cmd["type"], "local"); + } + + #[test] + fn test_storage_session_clear() { + let cmd = parse_command(&args("storage session clear"), &default_flags()).unwrap(); + assert_eq!(cmd["action"], "storage_clear"); + assert_eq!(cmd["type"], "session"); + } + + #[test] + fn test_storage_invalid_type() { + let result = parse_command(&args("storage invalid"), &default_flags()); + assert!(result.is_none()); + } + + // === Navigation Tests === + + #[test] + fn test_navigate_with_https() { + let cmd = parse_command(&args("open https://example.com"), &default_flags()).unwrap(); + assert_eq!(cmd["action"], "navigate"); + assert_eq!(cmd["url"], "https://example.com"); + } + + #[test] + fn test_navigate_without_protocol() { + let cmd = parse_command(&args("open example.com"), &default_flags()).unwrap(); + assert_eq!(cmd["action"], "navigate"); + assert_eq!(cmd["url"], "https://example.com"); + } + + #[test] + fn test_back() { + let cmd = parse_command(&args("back"), &default_flags()).unwrap(); + assert_eq!(cmd["action"], "back"); + } + + #[test] + fn test_forward() { + let cmd = parse_command(&args("forward"), &default_flags()).unwrap(); + assert_eq!(cmd["action"], "forward"); + } + + #[test] + fn test_reload() { + let cmd = parse_command(&args("reload"), &default_flags()).unwrap(); + assert_eq!(cmd["action"], "reload"); + } + + // === Core Actions === + + #[test] + fn test_click() { + let cmd = parse_command(&args("click #button"), &default_flags()).unwrap(); + assert_eq!(cmd["action"], "click"); + assert_eq!(cmd["selector"], "#button"); + } + + #[test] + fn test_fill() { + let cmd = parse_command(&args("fill #input hello world"), &default_flags()).unwrap(); + assert_eq!(cmd["action"], "fill"); + assert_eq!(cmd["selector"], "#input"); + assert_eq!(cmd["value"], "hello world"); + } + + #[test] + fn test_type_command() { + let cmd = parse_command(&args("type #input some text"), &default_flags()).unwrap(); + assert_eq!(cmd["action"], "type"); + assert_eq!(cmd["selector"], "#input"); + assert_eq!(cmd["text"], "some text"); + } + + // === Tabs === + + #[test] + fn test_tab_new() { + let cmd = parse_command(&args("tab new"), &default_flags()).unwrap(); + assert_eq!(cmd["action"], "tab_new"); + } + + #[test] + fn test_tab_list() { + let cmd = parse_command(&args("tab list"), &default_flags()).unwrap(); + assert_eq!(cmd["action"], "tab_list"); + } + + #[test] + fn test_tab_switch() { + let cmd = parse_command(&args("tab 2"), &default_flags()).unwrap(); + assert_eq!(cmd["action"], "tab_switch"); + assert_eq!(cmd["index"], 2); + } + + #[test] + fn test_tab_close() { + let cmd = parse_command(&args("tab close"), &default_flags()).unwrap(); + assert_eq!(cmd["action"], "tab_close"); + } + + // === Screenshot === + + #[test] + fn test_screenshot() { + let cmd = parse_command(&args("screenshot"), &default_flags()).unwrap(); + assert_eq!(cmd["action"], "screenshot"); + } + + #[test] + fn test_screenshot_full_page() { + let mut flags = default_flags(); + flags.full = true; + let cmd = parse_command(&args("screenshot"), &flags).unwrap(); + assert_eq!(cmd["action"], "screenshot"); + assert_eq!(cmd["fullPage"], true); + } + + // === Snapshot === + + #[test] + fn test_snapshot() { + let cmd = parse_command(&args("snapshot"), &default_flags()).unwrap(); + assert_eq!(cmd["action"], "snapshot"); + } + + #[test] + fn test_snapshot_interactive() { + let cmd = parse_command(&args("snapshot -i"), &default_flags()).unwrap(); + assert_eq!(cmd["action"], "snapshot"); + assert_eq!(cmd["interactive"], true); + } + + #[test] + fn test_snapshot_compact() { + let cmd = parse_command(&args("snapshot --compact"), &default_flags()).unwrap(); + assert_eq!(cmd["action"], "snapshot"); + assert_eq!(cmd["compact"], true); + } + + #[test] + fn test_snapshot_depth() { + let cmd = parse_command(&args("snapshot -d 3"), &default_flags()).unwrap(); + assert_eq!(cmd["action"], "snapshot"); + assert_eq!(cmd["maxDepth"], 3); + } + + // === Unknown command === + + #[test] + fn test_unknown_command() { + let result = parse_command(&args("unknowncommand"), &default_flags()); + assert!(result.is_none()); + } + + #[test] + fn test_empty_args() { + let result = parse_command(&[], &default_flags()); + assert!(result.is_none()); + } +} diff --git a/src/actions.ts b/src/actions.ts index 0b1fe8c..09e5a8a 100644 --- a/src/actions.ts +++ b/src/actions.ts @@ -791,7 +791,15 @@ async function handleCookiesSet( ): Promise { const page = browser.getPage(); const context = page.context(); - await context.addCookies(command.cookies); + // Auto-fill URL for cookies that don't have domain/path/url set + const pageUrl = page.url(); + const cookies = command.cookies.map((cookie) => { + if (!cookie.url && !cookie.domain && !cookie.path) { + return { ...cookie, url: pageUrl }; + } + return cookie; + }); + await context.addCookies(cookies); return successResponse(command.id, { set: true }); } diff --git a/src/browser.test.ts b/src/browser.test.ts index 5902263..519ba96 100644 --- a/src/browser.test.ts +++ b/src/browser.test.ts @@ -120,6 +120,28 @@ describe('BrowserManager', () => { expect(testCookie?.value).toBe('value'); }); + it('should set cookie with domain', async () => { + const page = browser.getPage(); + const context = page.context(); + await context.addCookies([{ name: 'domainCookie', value: 'domainValue', domain: 'example.com', path: '/' }]); + const cookies = await context.cookies(); + const testCookie = cookies.find((c) => c.name === 'domainCookie'); + expect(testCookie?.value).toBe('domainValue'); + }); + + it('should set multiple cookies at once', async () => { + const page = browser.getPage(); + const context = page.context(); + await context.clearCookies(); + await context.addCookies([ + { name: 'cookie1', value: 'value1', url: 'https://example.com' }, + { name: 'cookie2', value: 'value2', url: 'https://example.com' }, + ]); + const cookies = await context.cookies(); + expect(cookies.find((c) => c.name === 'cookie1')?.value).toBe('value1'); + expect(cookies.find((c) => c.name === 'cookie2')?.value).toBe('value2'); + }); + it('should clear cookies', async () => { const page = browser.getPage(); const context = page.context(); @@ -129,20 +151,83 @@ describe('BrowserManager', () => { }); }); - describe('storage via evaluate', () => { - it('should set and get localStorage', async () => { + describe('localStorage operations', () => { + it('should set and get localStorage item', async () => { const page = browser.getPage(); + await page.goto('https://example.com'); await page.evaluate(() => localStorage.setItem('testKey', 'testValue')); const value = await page.evaluate(() => localStorage.getItem('testKey')); expect(value).toBe('testValue'); }); + it('should get all localStorage items', async () => { + const page = browser.getPage(); + await page.evaluate(() => { + localStorage.clear(); + localStorage.setItem('key1', 'value1'); + localStorage.setItem('key2', 'value2'); + }); + const storage = await page.evaluate(() => { + const items: Record = {}; + for (let i = 0; i < localStorage.length; i++) { + const key = localStorage.key(i); + if (key) items[key] = localStorage.getItem(key) || ''; + } + return items; + }); + expect(storage.key1).toBe('value1'); + expect(storage.key2).toBe('value2'); + }); + it('should clear localStorage', async () => { const page = browser.getPage(); await page.evaluate(() => localStorage.clear()); const value = await page.evaluate(() => localStorage.getItem('testKey')); expect(value).toBeNull(); }); + + it('should return null for non-existent key', async () => { + const page = browser.getPage(); + await page.evaluate(() => localStorage.clear()); + const value = await page.evaluate(() => localStorage.getItem('nonexistent')); + expect(value).toBeNull(); + }); + }); + + describe('sessionStorage operations', () => { + it('should set and get sessionStorage item', async () => { + const page = browser.getPage(); + await page.goto('https://example.com'); + await page.evaluate(() => sessionStorage.setItem('sessionKey', 'sessionValue')); + const value = await page.evaluate(() => sessionStorage.getItem('sessionKey')); + expect(value).toBe('sessionValue'); + }); + + it('should get all sessionStorage items', async () => { + const page = browser.getPage(); + await page.evaluate(() => { + sessionStorage.clear(); + sessionStorage.setItem('skey1', 'svalue1'); + sessionStorage.setItem('skey2', 'svalue2'); + }); + const storage = await page.evaluate(() => { + const items: Record = {}; + for (let i = 0; i < sessionStorage.length; i++) { + const key = sessionStorage.key(i); + if (key) items[key] = sessionStorage.getItem(key) || ''; + } + return items; + }); + expect(storage.skey1).toBe('svalue1'); + expect(storage.skey2).toBe('svalue2'); + }); + + it('should clear sessionStorage', async () => { + const page = browser.getPage(); + await page.evaluate(() => sessionStorage.clear()); + const value = await page.evaluate(() => sessionStorage.getItem('sessionKey')); + expect(value).toBeNull(); + }); }); describe('viewport', () => { diff --git a/src/protocol.test.ts b/src/protocol.test.ts index 6dcff38..39f2a50 100644 --- a/src/protocol.test.ts +++ b/src/protocol.test.ts @@ -112,9 +112,22 @@ describe('parseCommand', () => { it('should parse cookies_get', () => { const result = parseCommand(cmd({ id: '1', action: 'cookies_get' })); expect(result.success).toBe(true); + if (result.success) { + expect(result.command.action).toBe('cookies_get'); + } }); - it('should parse cookies_set', () => { + it('should parse cookies_get with urls filter', () => { + const result = parseCommand( + cmd({ id: '1', action: 'cookies_get', urls: ['https://example.com'] }) + ); + expect(result.success).toBe(true); + if (result.success) { + expect(result.command.urls).toEqual(['https://example.com']); + } + }); + + it('should parse cookies_set with minimal cookie', () => { const result = parseCommand( cmd({ id: '1', @@ -123,18 +136,127 @@ describe('parseCommand', () => { }) ); expect(result.success).toBe(true); + if (result.success) { + expect(result.command.action).toBe('cookies_set'); + expect(result.command.cookies).toHaveLength(1); + expect(result.command.cookies[0].name).toBe('session'); + expect(result.command.cookies[0].value).toBe('abc123'); + } + }); + + it('should parse cookies_set with full cookie options', () => { + const result = parseCommand( + cmd({ + id: '1', + action: 'cookies_set', + cookies: [ + { + name: 'auth', + value: 'token123', + domain: 'example.com', + path: '/', + expires: Date.now() / 1000 + 3600, + httpOnly: true, + secure: true, + sameSite: 'Strict', + }, + ], + }) + ); + expect(result.success).toBe(true); + if (result.success) { + expect(result.command.cookies[0].httpOnly).toBe(true); + expect(result.command.cookies[0].secure).toBe(true); + expect(result.command.cookies[0].sameSite).toBe('Strict'); + } + }); + + it('should parse cookies_set with multiple cookies', () => { + const result = parseCommand( + cmd({ + id: '1', + action: 'cookies_set', + cookies: [ + { name: 'cookie1', value: 'value1' }, + { name: 'cookie2', value: 'value2' }, + ], + }) + ); + expect(result.success).toBe(true); + if (result.success) { + expect(result.command.cookies).toHaveLength(2); + } + }); + + it('should reject cookies_set without cookies array', () => { + const result = parseCommand(cmd({ id: '1', action: 'cookies_set' })); + expect(result.success).toBe(false); + }); + + it('should accept cookies_set with empty cookies array', () => { + // Empty array is technically valid (no-op) + const result = parseCommand(cmd({ id: '1', action: 'cookies_set', cookies: [] })); + expect(result.success).toBe(true); + }); + + it('should reject cookies_set with cookie missing name', () => { + const result = parseCommand( + cmd({ id: '1', action: 'cookies_set', cookies: [{ value: 'test' }] }) + ); + expect(result.success).toBe(false); + }); + + it('should reject cookies_set with cookie missing value', () => { + const result = parseCommand( + cmd({ id: '1', action: 'cookies_set', cookies: [{ name: 'test' }] }) + ); + expect(result.success).toBe(false); + }); + + it('should reject cookies_set with invalid sameSite value', () => { + const result = parseCommand( + cmd({ + id: '1', + action: 'cookies_set', + cookies: [{ name: 'test', value: 'val', sameSite: 'Invalid' }], + }) + ); + expect(result.success).toBe(false); }); it('should parse cookies_clear', () => { const result = parseCommand(cmd({ id: '1', action: 'cookies_clear' })); expect(result.success).toBe(true); + if (result.success) { + expect(result.command.action).toBe('cookies_clear'); + } }); }); describe('storage', () => { - it('should parse storage_get', () => { + it('should parse storage_get for localStorage', () => { const result = parseCommand(cmd({ id: '1', action: 'storage_get', type: 'local' })); expect(result.success).toBe(true); + if (result.success) { + expect(result.command.action).toBe('storage_get'); + expect(result.command.type).toBe('local'); + } + }); + + it('should parse storage_get for sessionStorage', () => { + const result = parseCommand(cmd({ id: '1', action: 'storage_get', type: 'session' })); + expect(result.success).toBe(true); + if (result.success) { + expect(result.command.type).toBe('session'); + } + }); + + it('should parse storage_get with specific key', () => { + const result = parseCommand(cmd({ id: '1', action: 'storage_get', type: 'local', key: 'mykey' })); + expect(result.success).toBe(true); + if (result.success) { + expect(result.command.key).toBe('mykey'); + } }); it('should parse storage_set', () => { @@ -148,6 +270,59 @@ describe('parseCommand', () => { }) ); expect(result.success).toBe(true); + if (result.success) { + expect(result.command.action).toBe('storage_set'); + expect(result.command.key).toBe('test'); + expect(result.command.value).toBe('value'); + } + }); + + it('should reject storage_set without key', () => { + const result = parseCommand( + cmd({ + id: '1', + action: 'storage_set', + type: 'local', + value: 'value', + }) + ); + expect(result.success).toBe(false); + }); + + it('should reject storage_set without value', () => { + const result = parseCommand( + cmd({ + id: '1', + action: 'storage_set', + type: 'local', + key: 'test', + }) + ); + expect(result.success).toBe(false); + }); + + it('should parse storage_clear for localStorage', () => { + const result = parseCommand(cmd({ id: '1', action: 'storage_clear', type: 'local' })); + expect(result.success).toBe(true); + if (result.success) { + expect(result.command.action).toBe('storage_clear'); + expect(result.command.type).toBe('local'); + } + }); + + it('should parse storage_clear for sessionStorage', () => { + const result = parseCommand(cmd({ id: '1', action: 'storage_clear', type: 'session' })); + expect(result.success).toBe(true); + }); + + it('should reject storage_get without type', () => { + const result = parseCommand(cmd({ id: '1', action: 'storage_get' })); + expect(result.success).toBe(false); + }); + + it('should reject storage_get with invalid type', () => { + const result = parseCommand(cmd({ id: '1', action: 'storage_get', type: 'invalid' })); + expect(result.success).toBe(false); }); });