Compare commits

...
Author SHA1 Message Date
Chris Tate 7701b3f7e5 better ax/dx 2026-01-11 23:41:34 -06:00
Chris Tate 9f7d7d6447 fix cookies/storage (#10) 2026-01-11 23:27:49 -06:00
5 changed files with 1241 additions and 183 deletions
+951 -170
View File
File diff suppressed because it is too large Load Diff
+17 -8
View File
@@ -8,7 +8,7 @@ use serde_json::json;
use std::env;
use std::process::exit;
use commands::{gen_id, parse_command};
use commands::{gen_id, parse_command, ParseError};
use connection::{ensure_daemon, send_command};
use flags::{clean_args, parse_flags};
use install::run_install;
@@ -32,13 +32,22 @@ fn main() {
}
let cmd = match parse_command(&clean, &flags) {
Some(c) => c,
None => {
eprintln!(
"\x1b[31mUnknown command:\x1b[0m {}",
clean.get(0).unwrap_or(&String::new())
);
eprintln!("\x1b[2mRun: agent-browser --help\x1b[0m");
Ok(c) => c,
Err(e) => {
if flags.json {
let error_type = match &e {
ParseError::UnknownCommand { .. } => "unknown_command",
ParseError::UnknownSubcommand { .. } => "unknown_subcommand",
ParseError::MissingArguments { .. } => "missing_arguments",
};
println!(
r#"{{"success":false,"error":"{}","type":"{}"}}"#,
e.format().replace('\n', " "),
error_type
);
} else {
eprintln!("\x1b[31m{}\x1b[0m", e.format());
}
exit(1);
}
};
+9 -1
View File
@@ -791,7 +791,15 @@ async function handleCookiesSet(
): Promise<Response> {
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 });
}
+87 -2
View File
@@ -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<string, string> = {};
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<string, string> = {};
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', () => {
+177 -2
View File
@@ -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);
});
});