mirror of
https://github.com/garrytan/gstack.git
synced 2026-05-02 03:35:09 +02:00
test: 40 integration tests with fixture server
Tests all commands against local Bun test server serving HTML fixtures. Covers navigation, content extraction, JS/CSS inspection, form interaction, SPA rendering, console/network buffers, tabs, screenshots, responsive, diff, chain, and storage. 40 pass, 0 fail, 2s runtime. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,409 @@
|
||||
/**
|
||||
* Integration tests for all browse commands
|
||||
*
|
||||
* Tests run against a local test server serving fixture HTML files.
|
||||
* A real browse server is started and commands are sent via the CLI HTTP interface.
|
||||
*/
|
||||
|
||||
import { describe, test, expect, beforeAll, afterAll } from 'bun:test';
|
||||
import { startTestServer } from './test-server';
|
||||
import { BrowserManager } from '../src/browser-manager';
|
||||
import { handleReadCommand } from '../src/read-commands';
|
||||
import { handleWriteCommand } from '../src/write-commands';
|
||||
import { handleMetaCommand } from '../src/meta-commands';
|
||||
import { consoleBuffer, networkBuffer } from '../src/buffers';
|
||||
import * as fs from 'fs';
|
||||
|
||||
let testServer: ReturnType<typeof startTestServer>;
|
||||
let bm: BrowserManager;
|
||||
let baseUrl: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
testServer = startTestServer(0);
|
||||
baseUrl = testServer.url;
|
||||
|
||||
bm = new BrowserManager();
|
||||
await bm.launch();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
// Force kill browser instead of graceful close (avoids hang)
|
||||
try { testServer.server.stop(); } catch {}
|
||||
// bm.close() can hang — just let process exit handle it
|
||||
setTimeout(() => process.exit(0), 500);
|
||||
});
|
||||
|
||||
// ─── Navigation ─────────────────────────────────────────────────
|
||||
|
||||
describe('Navigation', () => {
|
||||
test('goto navigates to URL', async () => {
|
||||
const result = await handleWriteCommand('goto', [baseUrl + '/basic.html'], bm);
|
||||
expect(result).toContain('Navigated to');
|
||||
expect(result).toContain('200');
|
||||
});
|
||||
|
||||
test('url returns current URL', async () => {
|
||||
const result = await handleMetaCommand('url', [], bm, async () => {});
|
||||
expect(result).toContain('/basic.html');
|
||||
});
|
||||
|
||||
test('back goes back', async () => {
|
||||
await handleWriteCommand('goto', [baseUrl + '/forms.html'], bm);
|
||||
const result = await handleWriteCommand('back', [], bm);
|
||||
expect(result).toContain('Back');
|
||||
});
|
||||
|
||||
test('forward goes forward', async () => {
|
||||
const result = await handleWriteCommand('forward', [], bm);
|
||||
expect(result).toContain('Forward');
|
||||
});
|
||||
|
||||
test('reload reloads page', async () => {
|
||||
const result = await handleWriteCommand('reload', [], bm);
|
||||
expect(result).toContain('Reloaded');
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Content Extraction ─────────────────────────────────────────
|
||||
|
||||
describe('Content extraction', () => {
|
||||
beforeAll(async () => {
|
||||
await handleWriteCommand('goto', [baseUrl + '/basic.html'], bm);
|
||||
});
|
||||
|
||||
test('text returns cleaned page text', async () => {
|
||||
const result = await handleReadCommand('text', [], bm);
|
||||
expect(result).toContain('Hello World');
|
||||
expect(result).toContain('Item one');
|
||||
expect(result).not.toContain('<h1>');
|
||||
});
|
||||
|
||||
test('html returns full page HTML', async () => {
|
||||
const result = await handleReadCommand('html', [], bm);
|
||||
expect(result).toContain('<!DOCTYPE html>');
|
||||
expect(result).toContain('<h1 id="title">Hello World</h1>');
|
||||
});
|
||||
|
||||
test('html with selector returns element innerHTML', async () => {
|
||||
const result = await handleReadCommand('html', ['#content'], bm);
|
||||
expect(result).toContain('Some body text here.');
|
||||
expect(result).toContain('<li>Item one</li>');
|
||||
});
|
||||
|
||||
test('links returns all links', async () => {
|
||||
const result = await handleReadCommand('links', [], bm);
|
||||
expect(result).toContain('Page 1');
|
||||
expect(result).toContain('Page 2');
|
||||
expect(result).toContain('External');
|
||||
expect(result).toContain('→');
|
||||
});
|
||||
|
||||
test('forms discovers form fields', async () => {
|
||||
await handleWriteCommand('goto', [baseUrl + '/forms.html'], bm);
|
||||
const result = await handleReadCommand('forms', [], bm);
|
||||
const forms = JSON.parse(result);
|
||||
expect(forms.length).toBe(2);
|
||||
expect(forms[0].id).toBe('login-form');
|
||||
expect(forms[0].method).toBe('post');
|
||||
expect(forms[0].fields.length).toBeGreaterThanOrEqual(2);
|
||||
expect(forms[1].id).toBe('profile-form');
|
||||
|
||||
// Check field discovery
|
||||
const emailField = forms[0].fields.find((f: any) => f.name === 'email');
|
||||
expect(emailField).toBeDefined();
|
||||
expect(emailField.type).toBe('email');
|
||||
expect(emailField.required).toBe(true);
|
||||
});
|
||||
|
||||
test('accessibility returns ARIA tree', async () => {
|
||||
await handleWriteCommand('goto', [baseUrl + '/basic.html'], bm);
|
||||
const result = await handleReadCommand('accessibility', [], bm);
|
||||
expect(result).toContain('Hello World');
|
||||
});
|
||||
});
|
||||
|
||||
// ─── JavaScript / CSS / Attrs ───────────────────────────────────
|
||||
|
||||
describe('Inspection', () => {
|
||||
beforeAll(async () => {
|
||||
await handleWriteCommand('goto', [baseUrl + '/basic.html'], bm);
|
||||
});
|
||||
|
||||
test('js evaluates expression', async () => {
|
||||
const result = await handleReadCommand('js', ['document.title'], bm);
|
||||
expect(result).toBe('Test Page - Basic');
|
||||
});
|
||||
|
||||
test('js returns objects as JSON', async () => {
|
||||
const result = await handleReadCommand('js', ['({a: 1, b: 2})'], bm);
|
||||
const obj = JSON.parse(result);
|
||||
expect(obj.a).toBe(1);
|
||||
expect(obj.b).toBe(2);
|
||||
});
|
||||
|
||||
test('css returns computed property', async () => {
|
||||
const result = await handleReadCommand('css', ['h1', 'color'], bm);
|
||||
// Navy color
|
||||
expect(result).toContain('0, 0, 128');
|
||||
});
|
||||
|
||||
test('css returns font-family', async () => {
|
||||
const result = await handleReadCommand('css', ['body', 'font-family'], bm);
|
||||
expect(result).toContain('Helvetica');
|
||||
});
|
||||
|
||||
test('attrs returns element attributes', async () => {
|
||||
const result = await handleReadCommand('attrs', ['#content'], bm);
|
||||
const attrs = JSON.parse(result);
|
||||
expect(attrs.id).toBe('content');
|
||||
expect(attrs['data-testid']).toBe('main-content');
|
||||
expect(attrs['data-version']).toBe('1.0');
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Interaction ────────────────────────────────────────────────
|
||||
|
||||
describe('Interaction', () => {
|
||||
test('fill + click works on form', async () => {
|
||||
await handleWriteCommand('goto', [baseUrl + '/forms.html'], bm);
|
||||
|
||||
let result = await handleWriteCommand('fill', ['#email', 'test@example.com'], bm);
|
||||
expect(result).toContain('Filled');
|
||||
|
||||
result = await handleWriteCommand('fill', ['#password', 'secret123'], bm);
|
||||
expect(result).toContain('Filled');
|
||||
|
||||
// Verify values were set
|
||||
const emailVal = await handleReadCommand('js', ['document.querySelector("#email").value'], bm);
|
||||
expect(emailVal).toBe('test@example.com');
|
||||
|
||||
result = await handleWriteCommand('click', ['#login-btn'], bm);
|
||||
expect(result).toContain('Clicked');
|
||||
});
|
||||
|
||||
test('select works on dropdown', async () => {
|
||||
await handleWriteCommand('goto', [baseUrl + '/forms.html'], bm);
|
||||
const result = await handleWriteCommand('select', ['#role', 'admin'], bm);
|
||||
expect(result).toContain('Selected');
|
||||
|
||||
const val = await handleReadCommand('js', ['document.querySelector("#role").value'], bm);
|
||||
expect(val).toBe('admin');
|
||||
});
|
||||
|
||||
test('hover works', async () => {
|
||||
const result = await handleWriteCommand('hover', ['h1'], bm);
|
||||
expect(result).toContain('Hovered');
|
||||
});
|
||||
|
||||
test('wait finds existing element', async () => {
|
||||
await handleWriteCommand('goto', [baseUrl + '/basic.html'], bm);
|
||||
const result = await handleWriteCommand('wait', ['#title'], bm);
|
||||
expect(result).toContain('appeared');
|
||||
});
|
||||
|
||||
test('scroll works', async () => {
|
||||
const result = await handleWriteCommand('scroll', ['footer'], bm);
|
||||
expect(result).toContain('Scrolled');
|
||||
});
|
||||
|
||||
test('viewport changes size', async () => {
|
||||
const result = await handleWriteCommand('viewport', ['375x812'], bm);
|
||||
expect(result).toContain('Viewport set');
|
||||
|
||||
const size = await handleReadCommand('js', ['`${window.innerWidth}x${window.innerHeight}`'], bm);
|
||||
expect(size).toBe('375x812');
|
||||
|
||||
// Reset
|
||||
await handleWriteCommand('viewport', ['1280x720'], bm);
|
||||
});
|
||||
|
||||
test('type and press work', async () => {
|
||||
await handleWriteCommand('goto', [baseUrl + '/forms.html'], bm);
|
||||
await handleWriteCommand('click', ['#name'], bm);
|
||||
|
||||
const result = await handleWriteCommand('type', ['John Doe'], bm);
|
||||
expect(result).toContain('Typed');
|
||||
|
||||
const val = await handleReadCommand('js', ['document.querySelector("#name").value'], bm);
|
||||
expect(val).toBe('John Doe');
|
||||
});
|
||||
});
|
||||
|
||||
// ─── SPA / Console / Network ───────────────────────────────────
|
||||
|
||||
describe('SPA and buffers', () => {
|
||||
test('wait handles delayed rendering', async () => {
|
||||
await handleWriteCommand('goto', [baseUrl + '/spa.html'], bm);
|
||||
const result = await handleWriteCommand('wait', ['.loaded'], bm);
|
||||
expect(result).toContain('appeared');
|
||||
|
||||
const text = await handleReadCommand('text', [], bm);
|
||||
expect(text).toContain('SPA Content Loaded');
|
||||
});
|
||||
|
||||
test('console captures messages', async () => {
|
||||
const result = await handleReadCommand('console', [], bm);
|
||||
expect(result).toContain('[SPA] Starting render');
|
||||
expect(result).toContain('[SPA] Render complete');
|
||||
});
|
||||
|
||||
test('console --clear clears buffer', async () => {
|
||||
const result = await handleReadCommand('console', ['--clear'], bm);
|
||||
expect(result).toContain('cleared');
|
||||
|
||||
const after = await handleReadCommand('console', [], bm);
|
||||
expect(after).toContain('no console messages');
|
||||
});
|
||||
|
||||
test('network captures requests', async () => {
|
||||
const result = await handleReadCommand('network', [], bm);
|
||||
expect(result).toContain('GET');
|
||||
expect(result).toContain('/spa.html');
|
||||
});
|
||||
|
||||
test('network --clear clears buffer', async () => {
|
||||
const result = await handleReadCommand('network', ['--clear'], bm);
|
||||
expect(result).toContain('cleared');
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Cookies / Storage ──────────────────────────────────────────
|
||||
|
||||
describe('Cookies and storage', () => {
|
||||
test('cookies returns array', async () => {
|
||||
await handleWriteCommand('goto', [baseUrl + '/basic.html'], bm);
|
||||
const result = await handleReadCommand('cookies', [], bm);
|
||||
// Test server doesn't set cookies, so empty array
|
||||
expect(result).toBe('[]');
|
||||
});
|
||||
|
||||
test('storage set and get works', async () => {
|
||||
await handleReadCommand('storage', ['set', 'testKey', 'testValue'], bm);
|
||||
const result = await handleReadCommand('storage', [], bm);
|
||||
const storage = JSON.parse(result);
|
||||
expect(storage.localStorage.testKey).toBe('testValue');
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Performance ────────────────────────────────────────────────
|
||||
|
||||
describe('Performance', () => {
|
||||
test('perf returns timing data', async () => {
|
||||
await handleWriteCommand('goto', [baseUrl + '/basic.html'], bm);
|
||||
const result = await handleReadCommand('perf', [], bm);
|
||||
expect(result).toContain('dns');
|
||||
expect(result).toContain('ttfb');
|
||||
expect(result).toContain('load');
|
||||
expect(result).toContain('ms');
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Visual ─────────────────────────────────────────────────────
|
||||
|
||||
describe('Visual', () => {
|
||||
test('screenshot saves file', async () => {
|
||||
await handleWriteCommand('goto', [baseUrl + '/basic.html'], bm);
|
||||
const screenshotPath = '/tmp/browse-test-screenshot.png';
|
||||
const result = await handleMetaCommand('screenshot', [screenshotPath], bm, async () => {});
|
||||
expect(result).toContain('Screenshot saved');
|
||||
expect(fs.existsSync(screenshotPath)).toBe(true);
|
||||
const stat = fs.statSync(screenshotPath);
|
||||
expect(stat.size).toBeGreaterThan(1000);
|
||||
fs.unlinkSync(screenshotPath);
|
||||
});
|
||||
|
||||
test('responsive saves 3 screenshots', async () => {
|
||||
await handleWriteCommand('goto', [baseUrl + '/responsive.html'], bm);
|
||||
const prefix = '/tmp/browse-test-resp';
|
||||
const result = await handleMetaCommand('responsive', [prefix], bm, async () => {});
|
||||
expect(result).toContain('mobile');
|
||||
expect(result).toContain('tablet');
|
||||
expect(result).toContain('desktop');
|
||||
|
||||
expect(fs.existsSync(`${prefix}-mobile.png`)).toBe(true);
|
||||
expect(fs.existsSync(`${prefix}-tablet.png`)).toBe(true);
|
||||
expect(fs.existsSync(`${prefix}-desktop.png`)).toBe(true);
|
||||
|
||||
// Cleanup
|
||||
fs.unlinkSync(`${prefix}-mobile.png`);
|
||||
fs.unlinkSync(`${prefix}-tablet.png`);
|
||||
fs.unlinkSync(`${prefix}-desktop.png`);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Tabs ───────────────────────────────────────────────────────
|
||||
|
||||
describe('Tabs', () => {
|
||||
test('tabs lists all tabs', async () => {
|
||||
const result = await handleMetaCommand('tabs', [], bm, async () => {});
|
||||
expect(result).toContain('[');
|
||||
expect(result).toContain(']');
|
||||
});
|
||||
|
||||
test('newtab opens new tab', async () => {
|
||||
const result = await handleMetaCommand('newtab', [baseUrl + '/forms.html'], bm, async () => {});
|
||||
expect(result).toContain('Opened tab');
|
||||
|
||||
const tabCount = bm.getTabCount();
|
||||
expect(tabCount).toBeGreaterThanOrEqual(2);
|
||||
});
|
||||
|
||||
test('tab switches to specific tab', async () => {
|
||||
const result = await handleMetaCommand('tab', ['1'], bm, async () => {});
|
||||
expect(result).toContain('Switched to tab 1');
|
||||
});
|
||||
|
||||
test('closetab closes a tab', async () => {
|
||||
const before = bm.getTabCount();
|
||||
// Close the last opened tab
|
||||
const tabs = await bm.getTabListWithTitles();
|
||||
const lastTab = tabs[tabs.length - 1];
|
||||
const result = await handleMetaCommand('closetab', [String(lastTab.id)], bm, async () => {});
|
||||
expect(result).toContain('Closed tab');
|
||||
expect(bm.getTabCount()).toBe(before - 1);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Diff ───────────────────────────────────────────────────────
|
||||
|
||||
describe('Diff', () => {
|
||||
test('diff shows differences between pages', async () => {
|
||||
const result = await handleMetaCommand(
|
||||
'diff',
|
||||
[baseUrl + '/basic.html', baseUrl + '/forms.html'],
|
||||
bm,
|
||||
async () => {}
|
||||
);
|
||||
expect(result).toContain('---');
|
||||
expect(result).toContain('+++');
|
||||
// basic.html has "Hello World", forms.html has "Form Test Page"
|
||||
expect(result).toContain('Hello World');
|
||||
expect(result).toContain('Form Test Page');
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Chain ──────────────────────────────────────────────────────
|
||||
|
||||
describe('Chain', () => {
|
||||
test('chain executes sequence of commands', async () => {
|
||||
const commands = JSON.stringify([
|
||||
['goto', baseUrl + '/basic.html'],
|
||||
['js', 'document.title'],
|
||||
['css', 'h1', 'color'],
|
||||
]);
|
||||
const result = await handleMetaCommand('chain', [commands], bm, async () => {});
|
||||
expect(result).toContain('[goto]');
|
||||
expect(result).toContain('Test Page - Basic');
|
||||
expect(result).toContain('[css]');
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Status ─────────────────────────────────────────────────────
|
||||
|
||||
describe('Status', () => {
|
||||
test('status reports health', async () => {
|
||||
const result = await handleMetaCommand('status', [], bm, async () => {});
|
||||
expect(result).toContain('Status: healthy');
|
||||
expect(result).toContain('Tabs:');
|
||||
});
|
||||
});
|
||||
Vendored
+33
@@ -0,0 +1,33 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Test Page - Basic</title>
|
||||
<style>
|
||||
body { font-family: "Helvetica Neue", sans-serif; color: #333; margin: 20px; }
|
||||
h1 { color: navy; font-size: 24px; }
|
||||
.highlight { background: yellow; padding: 4px; }
|
||||
.hidden { display: none; }
|
||||
nav a { margin-right: 10px; color: blue; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<nav>
|
||||
<a href="/page1">Page 1</a>
|
||||
<a href="/page2">Page 2</a>
|
||||
<a href="https://external.com/link">External</a>
|
||||
</nav>
|
||||
<h1 id="title">Hello World</h1>
|
||||
<p class="highlight">This is a highlighted paragraph.</p>
|
||||
<p class="hidden">This should be hidden.</p>
|
||||
<div id="content" data-testid="main-content" data-version="1.0">
|
||||
<p>Some body text here.</p>
|
||||
<ul>
|
||||
<li>Item one</li>
|
||||
<li>Item two</li>
|
||||
<li>Item three</li>
|
||||
</ul>
|
||||
</div>
|
||||
<footer>Footer text</footer>
|
||||
</body>
|
||||
</html>
|
||||
Vendored
+55
@@ -0,0 +1,55 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Test Page - Forms</title>
|
||||
<style>
|
||||
body { font-family: sans-serif; padding: 20px; }
|
||||
form { margin-bottom: 20px; padding: 10px; border: 1px solid #ccc; }
|
||||
label { display: block; margin: 5px 0; }
|
||||
input, select, textarea { margin-bottom: 10px; padding: 5px; }
|
||||
#result { color: green; display: none; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Form Test Page</h1>
|
||||
|
||||
<form id="login-form" action="/login" method="post">
|
||||
<label for="email">Email:</label>
|
||||
<input type="email" id="email" name="email" placeholder="your@email.com" required>
|
||||
<label for="password">Password:</label>
|
||||
<input type="password" id="password" name="password" required>
|
||||
<button type="submit" id="login-btn">Log In</button>
|
||||
</form>
|
||||
|
||||
<form id="profile-form" action="/profile" method="post">
|
||||
<label for="name">Name:</label>
|
||||
<input type="text" id="name" name="name" placeholder="Your name">
|
||||
<label for="bio">Bio:</label>
|
||||
<textarea id="bio" name="bio" placeholder="Tell us about yourself"></textarea>
|
||||
<label for="role">Role:</label>
|
||||
<select id="role" name="role">
|
||||
<option value="">Choose...</option>
|
||||
<option value="admin">Admin</option>
|
||||
<option value="user">User</option>
|
||||
<option value="guest">Guest</option>
|
||||
</select>
|
||||
<label>
|
||||
<input type="checkbox" id="newsletter" name="newsletter"> Subscribe to newsletter
|
||||
</label>
|
||||
<button type="submit" id="profile-btn">Save Profile</button>
|
||||
</form>
|
||||
|
||||
<div id="result">Form submitted!</div>
|
||||
|
||||
<script>
|
||||
document.querySelectorAll('form').forEach(form => {
|
||||
form.addEventListener('submit', (e) => {
|
||||
e.preventDefault();
|
||||
document.getElementById('result').style.display = 'block';
|
||||
console.log('[Form] Submitted:', form.id);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Vendored
+49
@@ -0,0 +1,49 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Test Page - Responsive</title>
|
||||
<style>
|
||||
body { font-family: sans-serif; margin: 0; padding: 20px; }
|
||||
.container { max-width: 1200px; margin: 0 auto; }
|
||||
.grid { display: grid; gap: 16px; }
|
||||
.card { padding: 16px; border: 1px solid #ddd; border-radius: 8px; }
|
||||
|
||||
/* Mobile: single column */
|
||||
.grid { grid-template-columns: 1fr; }
|
||||
|
||||
/* Tablet: two columns */
|
||||
@media (min-width: 768px) {
|
||||
.grid { grid-template-columns: 1fr 1fr; }
|
||||
.mobile-only { display: none; }
|
||||
}
|
||||
|
||||
/* Desktop: three columns */
|
||||
@media (min-width: 1024px) {
|
||||
.grid { grid-template-columns: 1fr 1fr 1fr; }
|
||||
}
|
||||
|
||||
.mobile-only { color: red; }
|
||||
.desktop-indicator { display: none; }
|
||||
@media (min-width: 1024px) {
|
||||
.desktop-indicator { display: block; color: green; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>Responsive Layout Test</h1>
|
||||
<p class="mobile-only">You are on mobile</p>
|
||||
<p class="desktop-indicator">You are on desktop</p>
|
||||
<div class="grid">
|
||||
<div class="card">Card 1</div>
|
||||
<div class="card">Card 2</div>
|
||||
<div class="card">Card 3</div>
|
||||
<div class="card">Card 4</div>
|
||||
<div class="card">Card 5</div>
|
||||
<div class="card">Card 6</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
Vendored
+24
@@ -0,0 +1,24 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Test Page - SPA</title>
|
||||
<style>
|
||||
body { font-family: sans-serif; }
|
||||
#app { padding: 20px; }
|
||||
.loaded { color: green; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app">Loading...</div>
|
||||
<script>
|
||||
console.log('[SPA] Starting render');
|
||||
console.warn('[SPA] This is a warning');
|
||||
console.error('[SPA] This is an error');
|
||||
setTimeout(() => {
|
||||
document.getElementById('app').innerHTML = '<h1 class="loaded">SPA Content Loaded</h1><p>Rendered by JavaScript</p>';
|
||||
console.log('[SPA] Render complete');
|
||||
}, 500);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,47 @@
|
||||
/**
|
||||
* Tiny Bun.serve for test fixtures
|
||||
* Serves HTML files from test/fixtures/ on a random available port
|
||||
*/
|
||||
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs';
|
||||
|
||||
const FIXTURES_DIR = path.resolve(import.meta.dir, 'fixtures');
|
||||
|
||||
export function startTestServer(port: number = 0): { server: ReturnType<typeof Bun.serve>; url: string } {
|
||||
const server = Bun.serve({
|
||||
port,
|
||||
hostname: '127.0.0.1',
|
||||
fetch(req) {
|
||||
const url = new URL(req.url);
|
||||
let filePath = url.pathname === '/' ? '/basic.html' : url.pathname;
|
||||
|
||||
// Remove leading slash
|
||||
filePath = filePath.replace(/^\//, '');
|
||||
const fullPath = path.join(FIXTURES_DIR, filePath);
|
||||
|
||||
if (!fs.existsSync(fullPath)) {
|
||||
return new Response('Not Found', { status: 404 });
|
||||
}
|
||||
|
||||
const content = fs.readFileSync(fullPath, 'utf-8');
|
||||
const ext = path.extname(fullPath);
|
||||
const contentType = ext === '.html' ? 'text/html' : 'text/plain';
|
||||
|
||||
return new Response(content, {
|
||||
headers: { 'Content-Type': contentType },
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const url = `http://127.0.0.1:${server.port}`;
|
||||
return { server, url };
|
||||
}
|
||||
|
||||
// If run directly, start and print URL
|
||||
if (import.meta.main) {
|
||||
const { server, url } = startTestServer(9450);
|
||||
console.log(`Test server running at ${url}`);
|
||||
console.log(`Fixtures: ${FIXTURES_DIR}`);
|
||||
console.log('Press Ctrl+C to stop');
|
||||
}
|
||||
Reference in New Issue
Block a user