mirror of
https://github.com/garrytan/gstack.git
synced 2026-05-06 21:46:40 +02:00
test: add tests for network idle, chain pipe format, state, and frame
- Network idle: click on fetch button waits for XHR, static click is fast - Chain pipe: pipe-delimited commands, quoted args, JSON still works - State: save/load round-trip, name sanitization, missing state error - Frame: switch to iframe + back, snapshot context header, fill in frame, goto-in-frame guard, usage error New fixtures: network-idle.html (fetch + static buttons), iframe.html (srcdoc) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1833,3 +1833,232 @@ describe('Chain with cookie-import', () => {
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Network Idle Detection ─────────────────────────────────────
|
||||
|
||||
describe('Network idle', () => {
|
||||
test('click on fetch button waits for XHR to complete', async () => {
|
||||
await handleWriteCommand('goto', [baseUrl + '/network-idle.html'], bm);
|
||||
// Click the button that triggers a fetch → networkidle waits for it
|
||||
await handleWriteCommand('click', ['#fetch-btn'], bm);
|
||||
// The DOM should be updated by the time click returns
|
||||
const result = await handleReadCommand('js', ['document.getElementById("result").textContent'], bm);
|
||||
expect(result).toContain('Data loaded');
|
||||
});
|
||||
|
||||
test('click on static button has no latency penalty', async () => {
|
||||
await handleWriteCommand('goto', [baseUrl + '/network-idle.html'], bm);
|
||||
const start = Date.now();
|
||||
await handleWriteCommand('click', ['#static-btn'], bm);
|
||||
const elapsed = Date.now() - start;
|
||||
// Static click should complete well under 2s (the networkidle timeout)
|
||||
// networkidle resolves immediately when no requests are in flight
|
||||
expect(elapsed).toBeLessThan(1500);
|
||||
const result = await handleReadCommand('js', ['document.getElementById("static-result").textContent'], bm);
|
||||
expect(result).toBe('Static action done');
|
||||
});
|
||||
|
||||
test('fill triggers networkidle wait', async () => {
|
||||
await handleWriteCommand('goto', [baseUrl + '/forms.html'], bm);
|
||||
// fill should complete without error (networkidle resolves immediately on static page)
|
||||
const result = await handleWriteCommand('fill', ['#email', 'idle@test.com'], bm);
|
||||
expect(result).toContain('Filled');
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Chain Pipe Format ──────────────────────────────────────────
|
||||
|
||||
describe('Chain pipe format', () => {
|
||||
test('pipe-delimited commands work', async () => {
|
||||
const result = await handleMetaCommand(
|
||||
'chain',
|
||||
[`goto ${baseUrl}/basic.html | js document.title`],
|
||||
bm,
|
||||
async () => {}
|
||||
);
|
||||
expect(result).toContain('[goto]');
|
||||
expect(result).toContain('[js]');
|
||||
expect(result).toContain('Test Page - Basic');
|
||||
});
|
||||
|
||||
test('pipe format with quoted args', async () => {
|
||||
const result = await handleMetaCommand(
|
||||
'chain',
|
||||
[`goto ${baseUrl}/forms.html | fill #email "pipe@test.com"`],
|
||||
bm,
|
||||
async () => {}
|
||||
);
|
||||
expect(result).toContain('[fill]');
|
||||
expect(result).toContain('Filled');
|
||||
// Verify the fill actually worked
|
||||
const val = await handleReadCommand('js', ['document.querySelector("#email").value'], bm);
|
||||
expect(val).toBe('pipe@test.com');
|
||||
});
|
||||
|
||||
test('JSON format still works', async () => {
|
||||
const commands = JSON.stringify([
|
||||
['goto', baseUrl + '/basic.html'],
|
||||
['js', 'document.title'],
|
||||
]);
|
||||
const result = await handleMetaCommand('chain', [commands], bm, async () => {});
|
||||
expect(result).toContain('[goto]');
|
||||
expect(result).toContain('Test Page - Basic');
|
||||
});
|
||||
|
||||
test('pipe format with unknown command includes error', async () => {
|
||||
const result = await handleMetaCommand(
|
||||
'chain',
|
||||
['bogus command'],
|
||||
bm,
|
||||
async () => {}
|
||||
);
|
||||
expect(result).toContain('ERROR');
|
||||
expect(result).toContain('Unknown command: bogus');
|
||||
});
|
||||
});
|
||||
|
||||
// ─── State Persistence ──────────────────────────────────────────
|
||||
|
||||
describe('State persistence', () => {
|
||||
test('state save and load round-trip', async () => {
|
||||
await handleWriteCommand('goto', [baseUrl + '/basic.html'], bm);
|
||||
// Set a cookie so we can verify it persists
|
||||
await handleWriteCommand('cookie', ['state_test=hello'], bm);
|
||||
|
||||
// Save state
|
||||
const saveResult = await handleMetaCommand('state', ['save', 'test-roundtrip'], bm, async () => {});
|
||||
expect(saveResult).toContain('State saved');
|
||||
expect(saveResult).toContain('treat as sensitive');
|
||||
|
||||
// Navigate away
|
||||
await handleWriteCommand('goto', [baseUrl + '/forms.html'], bm);
|
||||
|
||||
// Load state — should restore to basic.html with cookie
|
||||
const loadResult = await handleMetaCommand('state', ['load', 'test-roundtrip'], bm, async () => {});
|
||||
expect(loadResult).toContain('State loaded');
|
||||
|
||||
// Verify we're back on basic.html
|
||||
const url = await handleReadCommand('js', ['location.pathname'], bm);
|
||||
expect(url).toContain('basic.html');
|
||||
|
||||
// Clean up
|
||||
try {
|
||||
const { resolveConfig } = await import('../src/config');
|
||||
const config = resolveConfig();
|
||||
fs.unlinkSync(`${config.stateDir}/browse-states/test-roundtrip.json`);
|
||||
} catch {}
|
||||
});
|
||||
|
||||
test('state save rejects invalid names', async () => {
|
||||
try {
|
||||
await handleMetaCommand('state', ['save', '../../evil'], bm, async () => {});
|
||||
expect(true).toBe(false);
|
||||
} catch (err: any) {
|
||||
expect(err.message).toContain('alphanumeric');
|
||||
}
|
||||
});
|
||||
|
||||
test('state save accepts valid names', async () => {
|
||||
const result = await handleMetaCommand('state', ['save', 'my-state_1'], bm, async () => {});
|
||||
expect(result).toContain('State saved');
|
||||
// Clean up
|
||||
try {
|
||||
const { resolveConfig } = await import('../src/config');
|
||||
const config = resolveConfig();
|
||||
fs.unlinkSync(`${config.stateDir}/browse-states/my-state_1.json`);
|
||||
} catch {}
|
||||
});
|
||||
|
||||
test('state load rejects missing state', async () => {
|
||||
try {
|
||||
await handleMetaCommand('state', ['load', 'nonexistent-state-xyz'], bm, async () => {});
|
||||
expect(true).toBe(false);
|
||||
} catch (err: any) {
|
||||
expect(err.message).toContain('State not found');
|
||||
}
|
||||
});
|
||||
|
||||
test('state requires action and name', async () => {
|
||||
try {
|
||||
await handleMetaCommand('state', [], bm, async () => {});
|
||||
expect(true).toBe(false);
|
||||
} catch (err: any) {
|
||||
expect(err.message).toContain('Usage');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Frame (Iframe Support) ─────────────────────────────────────
|
||||
|
||||
describe('Frame', () => {
|
||||
test('frame switch to iframe and back', async () => {
|
||||
await handleWriteCommand('goto', [baseUrl + '/iframe.html'], bm);
|
||||
|
||||
// Verify we're on the main page
|
||||
const mainTitle = await handleReadCommand('js', ['document.getElementById("main-title").textContent'], bm);
|
||||
expect(mainTitle).toBe('Main Page');
|
||||
|
||||
// Switch to iframe by CSS selector
|
||||
const switchResult = await handleMetaCommand('frame', ['#test-frame'], bm, async () => {});
|
||||
expect(switchResult).toContain('Switched to frame');
|
||||
|
||||
// Verify we can read iframe content
|
||||
const frameTitle = await handleReadCommand('js', ['document.getElementById("frame-title").textContent'], bm);
|
||||
expect(frameTitle).toBe('Inside Frame');
|
||||
|
||||
// Switch back to main
|
||||
const mainResult = await handleMetaCommand('frame', ['main'], bm, async () => {});
|
||||
expect(mainResult).toBe('Switched to main frame');
|
||||
|
||||
// Verify we're back on the main page
|
||||
const mainTitleAgain = await handleReadCommand('js', ['document.getElementById("main-title").textContent'], bm);
|
||||
expect(mainTitleAgain).toBe('Main Page');
|
||||
});
|
||||
|
||||
test('snapshot shows frame context header', async () => {
|
||||
await handleWriteCommand('goto', [baseUrl + '/iframe.html'], bm);
|
||||
await handleMetaCommand('frame', ['#test-frame'], bm, async () => {});
|
||||
|
||||
const snap = await handleMetaCommand('snapshot', ['-i'], bm, async () => {});
|
||||
expect(snap).toContain('[Context: iframe');
|
||||
|
||||
// Clean up — return to main
|
||||
await handleMetaCommand('frame', ['main'], bm, async () => {});
|
||||
});
|
||||
|
||||
test('goto throws error when in frame context', async () => {
|
||||
await handleWriteCommand('goto', [baseUrl + '/iframe.html'], bm);
|
||||
await handleMetaCommand('frame', ['#test-frame'], bm, async () => {});
|
||||
|
||||
try {
|
||||
await handleWriteCommand('goto', ['https://example.com'], bm);
|
||||
expect(true).toBe(false);
|
||||
} catch (err: any) {
|
||||
expect(err.message).toContain('Cannot use goto inside a frame');
|
||||
}
|
||||
|
||||
await handleMetaCommand('frame', ['main'], bm, async () => {});
|
||||
});
|
||||
|
||||
test('frame requires argument', async () => {
|
||||
try {
|
||||
await handleMetaCommand('frame', [], bm, async () => {});
|
||||
expect(true).toBe(false);
|
||||
} catch (err: any) {
|
||||
expect(err.message).toContain('Usage');
|
||||
}
|
||||
});
|
||||
|
||||
test('fill works inside iframe', async () => {
|
||||
await handleWriteCommand('goto', [baseUrl + '/iframe.html'], bm);
|
||||
await handleMetaCommand('frame', ['#test-frame'], bm, async () => {});
|
||||
|
||||
const result = await handleWriteCommand('fill', ['#frame-input', 'hello from frame'], bm);
|
||||
expect(result).toContain('Filled');
|
||||
|
||||
const value = await handleReadCommand('js', ['document.getElementById("frame-input").value'], bm);
|
||||
expect(value).toBe('hello from frame');
|
||||
|
||||
await handleMetaCommand('frame', ['main'], bm, async () => {});
|
||||
});
|
||||
});
|
||||
|
||||
Vendored
+30
@@ -0,0 +1,30 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Test Page - Iframe</title>
|
||||
<style>
|
||||
body { font-family: sans-serif; padding: 20px; }
|
||||
iframe { border: 1px solid #ccc; width: 400px; height: 200px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1 id="main-title">Main Page</h1>
|
||||
<iframe id="test-frame" name="testframe" srcdoc='
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<body>
|
||||
<h1 id="frame-title">Inside Frame</h1>
|
||||
<button id="frame-btn">Frame Button</button>
|
||||
<input id="frame-input" type="text" placeholder="Type here">
|
||||
<div id="frame-result"></div>
|
||||
<script>
|
||||
document.getElementById("frame-btn").addEventListener("click", () => {
|
||||
document.getElementById("frame-result").textContent = "Frame button clicked";
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
'></iframe>
|
||||
</body>
|
||||
</html>
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Test Page - Network Idle</title>
|
||||
<style>
|
||||
body { font-family: sans-serif; padding: 20px; }
|
||||
#result { margin-top: 10px; color: green; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<button id="fetch-btn">Load Data</button>
|
||||
<div id="result"></div>
|
||||
<button id="static-btn">Static Action</button>
|
||||
<div id="static-result"></div>
|
||||
<script>
|
||||
document.getElementById('fetch-btn').addEventListener('click', async () => {
|
||||
// Simulate an XHR that takes 200ms
|
||||
const res = await fetch('/echo');
|
||||
const data = await res.json();
|
||||
document.getElementById('result').textContent = 'Data loaded: ' + Object.keys(data).length + ' headers';
|
||||
});
|
||||
|
||||
document.getElementById('static-btn').addEventListener('click', () => {
|
||||
// No network activity — purely client-side
|
||||
document.getElementById('static-result').textContent = 'Static action done';
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user