opengstack 0.13.7 → 0.13.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/opengstack.js +35 -90
- package/package.json +2 -3
- package/scripts/install-skills.js +29 -58
- package/skills/browse/bin/find-browse +21 -0
- package/skills/browse/bin/remote-slug +14 -0
- package/skills/browse/scripts/build-node-server.sh +48 -0
- package/skills/browse/src/activity.ts +208 -0
- package/skills/browse/src/browser-manager.ts +959 -0
- package/skills/browse/src/buffers.ts +137 -0
- package/skills/browse/src/bun-polyfill.cjs +109 -0
- package/skills/browse/src/cli.ts +678 -0
- package/skills/browse/src/commands.ts +128 -0
- package/skills/browse/src/config.ts +150 -0
- package/skills/browse/src/cookie-import-browser.ts +625 -0
- package/skills/browse/src/cookie-picker-routes.ts +230 -0
- package/skills/browse/src/cookie-picker-ui.ts +688 -0
- package/skills/browse/src/find-browse.ts +61 -0
- package/skills/browse/src/meta-commands.ts +550 -0
- package/skills/browse/src/platform.ts +17 -0
- package/skills/browse/src/read-commands.ts +358 -0
- package/skills/browse/src/server.ts +1192 -0
- package/skills/browse/src/sidebar-agent.ts +280 -0
- package/skills/browse/src/sidebar-utils.ts +21 -0
- package/skills/browse/src/snapshot.ts +407 -0
- package/skills/browse/src/url-validation.ts +95 -0
- package/skills/browse/src/write-commands.ts +364 -0
- package/skills/browse/test/activity.test.ts +120 -0
- package/skills/browse/test/adversarial-security.test.ts +32 -0
- package/skills/browse/test/browser-manager-unit.test.ts +17 -0
- package/skills/browse/test/bun-polyfill.test.ts +72 -0
- package/skills/browse/test/commands.test.ts +2075 -0
- package/skills/browse/test/compare-board.test.ts +342 -0
- package/skills/browse/test/config.test.ts +316 -0
- package/skills/browse/test/cookie-import-browser.test.ts +519 -0
- package/skills/browse/test/cookie-picker-routes.test.ts +260 -0
- package/skills/browse/test/file-drop.test.ts +271 -0
- package/skills/browse/test/find-browse.test.ts +50 -0
- package/skills/browse/test/findport.test.ts +191 -0
- package/skills/browse/test/fixtures/basic.html +33 -0
- package/skills/browse/test/fixtures/cursor-interactive.html +22 -0
- package/skills/browse/test/fixtures/dialog.html +15 -0
- package/skills/browse/test/fixtures/empty.html +2 -0
- package/skills/browse/test/fixtures/forms.html +55 -0
- package/skills/browse/test/fixtures/iframe.html +30 -0
- package/skills/browse/test/fixtures/network-idle.html +30 -0
- package/skills/browse/test/fixtures/qa-eval-checkout.html +108 -0
- package/skills/browse/test/fixtures/qa-eval-spa.html +98 -0
- package/skills/browse/test/fixtures/qa-eval.html +51 -0
- package/skills/browse/test/fixtures/responsive.html +49 -0
- package/skills/browse/test/fixtures/snapshot.html +55 -0
- package/skills/browse/test/fixtures/spa.html +24 -0
- package/skills/browse/test/fixtures/states.html +17 -0
- package/skills/browse/test/fixtures/upload.html +25 -0
- package/skills/browse/test/gstack-config.test.ts +138 -0
- package/skills/browse/test/gstack-update-check.test.ts +514 -0
- package/skills/browse/test/handoff.test.ts +235 -0
- package/skills/browse/test/path-validation.test.ts +91 -0
- package/skills/browse/test/platform.test.ts +37 -0
- package/skills/browse/test/server-auth.test.ts +65 -0
- package/skills/browse/test/sidebar-agent-roundtrip.test.ts +226 -0
- package/skills/browse/test/sidebar-agent.test.ts +199 -0
- package/skills/browse/test/sidebar-integration.test.ts +320 -0
- package/skills/browse/test/sidebar-unit.test.ts +96 -0
- package/skills/browse/test/snapshot.test.ts +467 -0
- package/skills/browse/test/state-ttl.test.ts +35 -0
- package/skills/browse/test/test-server.ts +57 -0
- package/skills/browse/test/url-validation.test.ts +72 -0
- package/skills/browse/test/watch.test.ts +129 -0
- package/skills/careful/bin/check-careful.sh +112 -0
- package/skills/cso/ACKNOWLEDGEMENTS.md +14 -0
- package/skills/freeze/bin/check-freeze.sh +79 -0
- package/skills/qa/references/issue-taxonomy.md +85 -0
- package/skills/qa/templates/qa-report-template.md +126 -0
- package/skills/review/TODOS-format.md +62 -0
- package/skills/review/checklist.md +220 -0
- package/skills/review/design-checklist.md +132 -0
- package/skills/review/greptile-triage.md +220 -0
- /package/{autoplan → skills/autoplan}/SKILL.md +0 -0
- /package/{autoplan → skills/autoplan}/SKILL.md.tmpl +0 -0
- /package/{benchmark → skills/benchmark}/SKILL.md +0 -0
- /package/{benchmark → skills/benchmark}/SKILL.md.tmpl +0 -0
- /package/{browse → skills/browse}/SKILL.md +0 -0
- /package/{browse → skills/browse}/SKILL.md.tmpl +0 -0
- /package/{canary → skills/canary}/SKILL.md +0 -0
- /package/{canary → skills/canary}/SKILL.md.tmpl +0 -0
- /package/{careful → skills/careful}/SKILL.md +0 -0
- /package/{careful → skills/careful}/SKILL.md.tmpl +0 -0
- /package/{codex → skills/codex}/SKILL.md +0 -0
- /package/{codex → skills/codex}/SKILL.md.tmpl +0 -0
- /package/{connect-chrome → skills/connect-chrome}/SKILL.md +0 -0
- /package/{connect-chrome → skills/connect-chrome}/SKILL.md.tmpl +0 -0
- /package/{cso → skills/cso}/SKILL.md +0 -0
- /package/{cso → skills/cso}/SKILL.md.tmpl +0 -0
- /package/{design-consultation → skills/design-consultation}/SKILL.md +0 -0
- /package/{design-consultation → skills/design-consultation}/SKILL.md.tmpl +0 -0
- /package/{design-review → skills/design-review}/SKILL.md +0 -0
- /package/{design-review → skills/design-review}/SKILL.md.tmpl +0 -0
- /package/{design-shotgun → skills/design-shotgun}/SKILL.md +0 -0
- /package/{design-shotgun → skills/design-shotgun}/SKILL.md.tmpl +0 -0
- /package/{document-release → skills/document-release}/SKILL.md +0 -0
- /package/{document-release → skills/document-release}/SKILL.md.tmpl +0 -0
- /package/{freeze → skills/freeze}/SKILL.md +0 -0
- /package/{freeze → skills/freeze}/SKILL.md.tmpl +0 -0
- /package/{gstack-upgrade → skills/gstack-upgrade}/SKILL.md +0 -0
- /package/{gstack-upgrade → skills/gstack-upgrade}/SKILL.md.tmpl +0 -0
- /package/{guard → skills/guard}/SKILL.md +0 -0
- /package/{guard → skills/guard}/SKILL.md.tmpl +0 -0
- /package/{investigate → skills/investigate}/SKILL.md +0 -0
- /package/{investigate → skills/investigate}/SKILL.md.tmpl +0 -0
- /package/{land-and-deploy → skills/land-and-deploy}/SKILL.md +0 -0
- /package/{land-and-deploy → skills/land-and-deploy}/SKILL.md.tmpl +0 -0
- /package/{office-hours → skills/office-hours}/SKILL.md +0 -0
- /package/{office-hours → skills/office-hours}/SKILL.md.tmpl +0 -0
- /package/{plan-ceo-review → skills/plan-ceo-review}/SKILL.md +0 -0
- /package/{plan-ceo-review → skills/plan-ceo-review}/SKILL.md.tmpl +0 -0
- /package/{plan-design-review → skills/plan-design-review}/SKILL.md +0 -0
- /package/{plan-design-review → skills/plan-design-review}/SKILL.md.tmpl +0 -0
- /package/{plan-eng-review → skills/plan-eng-review}/SKILL.md +0 -0
- /package/{plan-eng-review → skills/plan-eng-review}/SKILL.md.tmpl +0 -0
- /package/{qa → skills/qa}/SKILL.md +0 -0
- /package/{qa → skills/qa}/SKILL.md.tmpl +0 -0
- /package/{qa-only → skills/qa-only}/SKILL.md +0 -0
- /package/{qa-only → skills/qa-only}/SKILL.md.tmpl +0 -0
- /package/{retro → skills/retro}/SKILL.md +0 -0
- /package/{retro → skills/retro}/SKILL.md.tmpl +0 -0
- /package/{review → skills/review}/SKILL.md +0 -0
- /package/{review → skills/review}/SKILL.md.tmpl +0 -0
- /package/{setup-browser-cookies → skills/setup-browser-cookies}/SKILL.md +0 -0
- /package/{setup-browser-cookies → skills/setup-browser-cookies}/SKILL.md.tmpl +0 -0
- /package/{setup-deploy → skills/setup-deploy}/SKILL.md +0 -0
- /package/{setup-deploy → skills/setup-deploy}/SKILL.md.tmpl +0 -0
- /package/{ship → skills/ship}/SKILL.md +0 -0
- /package/{ship → skills/ship}/SKILL.md.tmpl +0 -0
- /package/{unfreeze → skills/unfreeze}/SKILL.md +0 -0
- /package/{unfreeze → skills/unfreeze}/SKILL.md.tmpl +0 -0
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { describe, test, expect } from 'bun:test';
|
|
2
|
+
import * as net from 'net';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
|
|
5
|
+
const polyfillPath = path.resolve(import.meta.dir, '../src/bun-polyfill.cjs');
|
|
6
|
+
|
|
7
|
+
// Helper: bind a port and hold it open, returning a cleanup function
|
|
8
|
+
function occupyPort(port: number): Promise<() => Promise<void>> {
|
|
9
|
+
return new Promise((resolve, reject) => {
|
|
10
|
+
const srv = net.createServer();
|
|
11
|
+
srv.once('error', reject);
|
|
12
|
+
srv.listen(port, '127.0.0.1', () => {
|
|
13
|
+
resolve(() => new Promise<void>((r) => srv.close(() => r())));
|
|
14
|
+
});
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Helper: find a known-free port by binding to 0
|
|
19
|
+
function getFreePort(): Promise<number> {
|
|
20
|
+
return new Promise((resolve, reject) => {
|
|
21
|
+
const srv = net.createServer();
|
|
22
|
+
srv.once('error', reject);
|
|
23
|
+
srv.listen(0, '127.0.0.1', () => {
|
|
24
|
+
const port = (srv.address() as net.AddressInfo).port;
|
|
25
|
+
srv.close(() => resolve(port));
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
describe('findPort / isPortAvailable', () => {
|
|
31
|
+
|
|
32
|
+
test('isPortAvailable returns true for a free port', async () => {
|
|
33
|
+
// Use the same isPortAvailable logic from server.ts
|
|
34
|
+
const port = await getFreePort();
|
|
35
|
+
|
|
36
|
+
const available = await new Promise<boolean>((resolve) => {
|
|
37
|
+
const srv = net.createServer();
|
|
38
|
+
srv.once('error', () => resolve(false));
|
|
39
|
+
srv.listen(port, '127.0.0.1', () => {
|
|
40
|
+
srv.close(() => resolve(true));
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
expect(available).toBe(true);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test('isPortAvailable returns false for an occupied port', async () => {
|
|
48
|
+
const port = await getFreePort();
|
|
49
|
+
const release = await occupyPort(port);
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
const available = await new Promise<boolean>((resolve) => {
|
|
53
|
+
const srv = net.createServer();
|
|
54
|
+
srv.once('error', () => resolve(false));
|
|
55
|
+
srv.listen(port, '127.0.0.1', () => {
|
|
56
|
+
srv.close(() => resolve(true));
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
expect(available).toBe(false);
|
|
61
|
+
} finally {
|
|
62
|
+
await release();
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test('port is actually free after isPortAvailable returns true', async () => {
|
|
67
|
+
// This is the core race condition test: after isPortAvailable says
|
|
68
|
+
// a port is free, can we IMMEDIATELY bind to it?
|
|
69
|
+
const port = await getFreePort();
|
|
70
|
+
|
|
71
|
+
// Simulate isPortAvailable
|
|
72
|
+
const isFree = await new Promise<boolean>((resolve) => {
|
|
73
|
+
const srv = net.createServer();
|
|
74
|
+
srv.once('error', () => resolve(false));
|
|
75
|
+
srv.listen(port, '127.0.0.1', () => {
|
|
76
|
+
srv.close(() => resolve(true));
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
expect(isFree).toBe(true);
|
|
81
|
+
|
|
82
|
+
// Now immediately try to bind — this would fail with the old
|
|
83
|
+
// Bun.serve() polyfill approach because the test server's
|
|
84
|
+
// listen() would still be pending
|
|
85
|
+
const canBind = await new Promise<boolean>((resolve) => {
|
|
86
|
+
const srv = net.createServer();
|
|
87
|
+
srv.once('error', () => resolve(false));
|
|
88
|
+
srv.listen(port, '127.0.0.1', () => {
|
|
89
|
+
srv.close(() => resolve(true));
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
expect(canBind).toBe(true);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test('polyfill Bun.serve stop() is fire-and-forget (async)', async () => {
|
|
97
|
+
// Verify that the polyfill's stop() does NOT wait for the socket
|
|
98
|
+
// to actually close — this is the root cause of the race condition.
|
|
99
|
+
// On macOS/Linux the OS reclaims the port fast enough that the race
|
|
100
|
+
// rarely manifests, but on Windows TIME_WAIT makes it 100% repro.
|
|
101
|
+
const result = Bun.spawnSync(['node', '-e', `
|
|
102
|
+
require('${polyfillPath}');
|
|
103
|
+
const net = require('net');
|
|
104
|
+
|
|
105
|
+
async function test() {
|
|
106
|
+
const port = 10000 + Math.floor(Math.random() * 50000);
|
|
107
|
+
|
|
108
|
+
const testServer = Bun.serve({
|
|
109
|
+
port,
|
|
110
|
+
hostname: '127.0.0.1',
|
|
111
|
+
fetch: () => new Response('ok'),
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
// stop() returns undefined — it does NOT return a Promise,
|
|
115
|
+
// so callers cannot await socket teardown
|
|
116
|
+
const retval = testServer.stop();
|
|
117
|
+
console.log(typeof retval === 'undefined' ? 'FIRE_AND_FORGET' : 'AWAITABLE');
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
test();
|
|
121
|
+
`], { stdout: 'pipe', stderr: 'pipe' });
|
|
122
|
+
|
|
123
|
+
const output = result.stdout.toString().trim();
|
|
124
|
+
// Confirms the polyfill's stop() is fire-and-forget — callers
|
|
125
|
+
// cannot wait for the port to be released, hence the race
|
|
126
|
+
expect(output).toBe('FIRE_AND_FORGET');
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test('net.createServer approach does not have the race condition', async () => {
|
|
130
|
+
// Prove the fix: net.createServer with proper async bind/close
|
|
131
|
+
// releases the port cleanly
|
|
132
|
+
const result = Bun.spawnSync(['node', '-e', `
|
|
133
|
+
const net = require('net');
|
|
134
|
+
|
|
135
|
+
async function testFix() {
|
|
136
|
+
const port = 10000 + Math.floor(Math.random() * 50000);
|
|
137
|
+
|
|
138
|
+
// Simulate the NEW isPortAvailable: proper async bind/close
|
|
139
|
+
const isFree = await new Promise((resolve) => {
|
|
140
|
+
const srv = net.createServer();
|
|
141
|
+
srv.once('error', () => resolve(false));
|
|
142
|
+
srv.listen(port, '127.0.0.1', () => {
|
|
143
|
+
srv.close(() => resolve(true));
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
if (!isFree) {
|
|
148
|
+
console.log('PORT_BUSY');
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Immediately try to bind — should succeed because close()
|
|
153
|
+
// completed before the Promise resolved
|
|
154
|
+
const canBind = await new Promise((resolve) => {
|
|
155
|
+
const srv = net.createServer();
|
|
156
|
+
srv.once('error', () => resolve(false));
|
|
157
|
+
srv.listen(port, '127.0.0.1', () => {
|
|
158
|
+
srv.close(() => resolve(true));
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
console.log(canBind ? 'FIX_WORKS' : 'FIX_BROKEN');
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
testFix();
|
|
166
|
+
`], { stdout: 'pipe', stderr: 'pipe' });
|
|
167
|
+
|
|
168
|
+
const output = result.stdout.toString().trim();
|
|
169
|
+
expect(output).toBe('FIX_WORKS');
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
test('isPortAvailable handles rapid sequential checks', async () => {
|
|
173
|
+
// Stress test: check the same port multiple times in sequence
|
|
174
|
+
const port = await getFreePort();
|
|
175
|
+
const results: boolean[] = [];
|
|
176
|
+
|
|
177
|
+
for (let i = 0; i < 5; i++) {
|
|
178
|
+
const available = await new Promise<boolean>((resolve) => {
|
|
179
|
+
const srv = net.createServer();
|
|
180
|
+
srv.once('error', () => resolve(false));
|
|
181
|
+
srv.listen(port, '127.0.0.1', () => {
|
|
182
|
+
srv.close(() => resolve(true));
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
results.push(available);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// All 5 checks should succeed — no leaked sockets
|
|
189
|
+
expect(results).toEqual([true, true, true, true, true]);
|
|
190
|
+
});
|
|
191
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<title>Test Page - Basic</title>
|
|
6
|
+
<style>
|
|
7
|
+
body { font-family: "Helvetica Neue", sans-serif; color: #333; margin: 20px; }
|
|
8
|
+
h1 { color: navy; font-size: 24px; }
|
|
9
|
+
.highlight { background: yellow; padding: 4px; }
|
|
10
|
+
.hidden { display: none; }
|
|
11
|
+
nav a { margin-right: 10px; color: blue; }
|
|
12
|
+
</style>
|
|
13
|
+
</head>
|
|
14
|
+
<body>
|
|
15
|
+
<nav>
|
|
16
|
+
<a href="/page1">Page 1</a>
|
|
17
|
+
<a href="/page2">Page 2</a>
|
|
18
|
+
<a href="https://external.com/link">External</a>
|
|
19
|
+
</nav>
|
|
20
|
+
<h1 id="title">Hello World</h1>
|
|
21
|
+
<p class="highlight">This is a highlighted paragraph.</p>
|
|
22
|
+
<p class="hidden">This should be hidden.</p>
|
|
23
|
+
<div id="content" data-testid="main-content" data-version="1.0">
|
|
24
|
+
<p>Some body text here.</p>
|
|
25
|
+
<ul>
|
|
26
|
+
<li>Item one</li>
|
|
27
|
+
<li>Item two</li>
|
|
28
|
+
<li>Item three</li>
|
|
29
|
+
</ul>
|
|
30
|
+
</div>
|
|
31
|
+
<footer>Footer text</footer>
|
|
32
|
+
</body>
|
|
33
|
+
</html>
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<title>Test Page - Cursor Interactive</title>
|
|
6
|
+
<style>
|
|
7
|
+
.clickable-div { cursor: pointer; padding: 10px; border: 1px solid #ccc; }
|
|
8
|
+
.hover-card { cursor: pointer; padding: 20px; background: #f0f0f0; }
|
|
9
|
+
</style>
|
|
10
|
+
</head>
|
|
11
|
+
<body>
|
|
12
|
+
<h1>Cursor Interactive Test</h1>
|
|
13
|
+
<!-- These are NOT standard interactive elements but have cursor:pointer -->
|
|
14
|
+
<div class="clickable-div" id="click-div" onclick="this.textContent = 'clicked!'">Click me (div)</div>
|
|
15
|
+
<span class="hover-card" id="hover-span">Hover card (span)</span>
|
|
16
|
+
<div tabindex="0" id="focusable-div">Focusable div</div>
|
|
17
|
+
<div onclick="alert('hi')" id="onclick-div">Onclick div</div>
|
|
18
|
+
<!-- Standard interactive element (should NOT appear in -C output) -->
|
|
19
|
+
<button id="normal-btn">Normal Button</button>
|
|
20
|
+
<a href="/test">Normal Link</a>
|
|
21
|
+
</body>
|
|
22
|
+
</html>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<title>Test Page - Dialog</title>
|
|
6
|
+
</head>
|
|
7
|
+
<body>
|
|
8
|
+
<h1>Dialog Test</h1>
|
|
9
|
+
<button id="alert-btn" onclick="alert('Hello from alert')">Alert</button>
|
|
10
|
+
<button id="confirm-btn" onclick="document.getElementById('confirm-result').textContent = confirm('Are you sure?') ? 'confirmed' : 'cancelled'">Confirm</button>
|
|
11
|
+
<button id="prompt-btn" onclick="document.getElementById('prompt-result').textContent = prompt('Enter name:', 'default') || 'null'">Prompt</button>
|
|
12
|
+
<p id="confirm-result"></p>
|
|
13
|
+
<p id="prompt-result"></p>
|
|
14
|
+
</body>
|
|
15
|
+
</html>
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<title>Test Page - Forms</title>
|
|
6
|
+
<style>
|
|
7
|
+
body { font-family: sans-serif; padding: 20px; }
|
|
8
|
+
form { margin-bottom: 20px; padding: 10px; border: 1px solid #ccc; }
|
|
9
|
+
label { display: block; margin: 5px 0; }
|
|
10
|
+
input, select, textarea { margin-bottom: 10px; padding: 5px; }
|
|
11
|
+
#result { color: green; display: none; }
|
|
12
|
+
</style>
|
|
13
|
+
</head>
|
|
14
|
+
<body>
|
|
15
|
+
<h1>Form Test Page</h1>
|
|
16
|
+
|
|
17
|
+
<form id="login-form" action="/login" method="post">
|
|
18
|
+
<label for="email">Email:</label>
|
|
19
|
+
<input type="email" id="email" name="email" placeholder="your@email.com" required>
|
|
20
|
+
<label for="password">Password:</label>
|
|
21
|
+
<input type="password" id="password" name="password" required>
|
|
22
|
+
<button type="submit" id="login-btn">Log In</button>
|
|
23
|
+
</form>
|
|
24
|
+
|
|
25
|
+
<form id="profile-form" action="/profile" method="post">
|
|
26
|
+
<label for="name">Name:</label>
|
|
27
|
+
<input type="text" id="name" name="name" placeholder="Your name">
|
|
28
|
+
<label for="bio">Bio:</label>
|
|
29
|
+
<textarea id="bio" name="bio" placeholder="Tell us about yourself"></textarea>
|
|
30
|
+
<label for="role">Role:</label>
|
|
31
|
+
<select id="role" name="role">
|
|
32
|
+
<option value="">Choose...</option>
|
|
33
|
+
<option value="admin">Admin</option>
|
|
34
|
+
<option value="user">User</option>
|
|
35
|
+
<option value="guest">Guest</option>
|
|
36
|
+
</select>
|
|
37
|
+
<label>
|
|
38
|
+
<input type="checkbox" id="newsletter" name="newsletter"> Subscribe to newsletter
|
|
39
|
+
</label>
|
|
40
|
+
<button type="submit" id="profile-btn">Save Profile</button>
|
|
41
|
+
</form>
|
|
42
|
+
|
|
43
|
+
<div id="result">Form submitted!</div>
|
|
44
|
+
|
|
45
|
+
<script>
|
|
46
|
+
document.querySelectorAll('form').forEach(form => {
|
|
47
|
+
form.addEventListener('submit', (e) => {
|
|
48
|
+
e.preventDefault();
|
|
49
|
+
document.getElementById('result').style.display = 'block';
|
|
50
|
+
console.log('[Form] Submitted:', form.id);
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
</script>
|
|
54
|
+
</body>
|
|
55
|
+
</html>
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<title>Test Page - Iframe</title>
|
|
6
|
+
<style>
|
|
7
|
+
body { font-family: sans-serif; padding: 20px; }
|
|
8
|
+
iframe { border: 1px solid #ccc; width: 400px; height: 200px; }
|
|
9
|
+
</style>
|
|
10
|
+
</head>
|
|
11
|
+
<body>
|
|
12
|
+
<h1 id="main-title">Main Page</h1>
|
|
13
|
+
<iframe id="test-frame" name="testframe" srcdoc='
|
|
14
|
+
<!DOCTYPE html>
|
|
15
|
+
<html>
|
|
16
|
+
<body>
|
|
17
|
+
<h1 id="frame-title">Inside Frame</h1>
|
|
18
|
+
<button id="frame-btn">Frame Button</button>
|
|
19
|
+
<input id="frame-input" type="text" placeholder="Type here">
|
|
20
|
+
<div id="frame-result"></div>
|
|
21
|
+
<script>
|
|
22
|
+
document.getElementById("frame-btn").addEventListener("click", () => {
|
|
23
|
+
document.getElementById("frame-result").textContent = "Frame button clicked";
|
|
24
|
+
});
|
|
25
|
+
</script>
|
|
26
|
+
</body>
|
|
27
|
+
</html>
|
|
28
|
+
'></iframe>
|
|
29
|
+
</body>
|
|
30
|
+
</html>
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<title>Test Page - Network Idle</title>
|
|
6
|
+
<style>
|
|
7
|
+
body { font-family: sans-serif; padding: 20px; }
|
|
8
|
+
#result { margin-top: 10px; color: green; }
|
|
9
|
+
</style>
|
|
10
|
+
</head>
|
|
11
|
+
<body>
|
|
12
|
+
<button id="fetch-btn">Load Data</button>
|
|
13
|
+
<div id="result"></div>
|
|
14
|
+
<button id="static-btn">Static Action</button>
|
|
15
|
+
<div id="static-result"></div>
|
|
16
|
+
<script>
|
|
17
|
+
document.getElementById('fetch-btn').addEventListener('click', async () => {
|
|
18
|
+
// Simulate an XHR that takes 200ms
|
|
19
|
+
const res = await fetch('/echo');
|
|
20
|
+
const data = await res.json();
|
|
21
|
+
document.getElementById('result').textContent = 'Data loaded: ' + Object.keys(data).length + ' headers';
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
document.getElementById('static-btn').addEventListener('click', () => {
|
|
25
|
+
// No network activity — purely client-side
|
|
26
|
+
document.getElementById('static-result').textContent = 'Static action done';
|
|
27
|
+
});
|
|
28
|
+
</script>
|
|
29
|
+
</body>
|
|
30
|
+
</html>
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<title>QA Eval — Checkout</title>
|
|
6
|
+
<style>
|
|
7
|
+
body { font-family: sans-serif; padding: 20px; }
|
|
8
|
+
.checkout-form { max-width: 500px; }
|
|
9
|
+
.form-group { margin-bottom: 15px; }
|
|
10
|
+
.form-group label { display: block; margin-bottom: 4px; font-weight: bold; }
|
|
11
|
+
.form-group input { width: 100%; padding: 8px; box-sizing: border-box; border: 1px solid #ccc; border-radius: 4px; }
|
|
12
|
+
.form-group input.invalid { border-color: red; }
|
|
13
|
+
.form-group .error-msg { color: red; font-size: 12px; display: none; }
|
|
14
|
+
.total { font-size: 24px; font-weight: bold; margin: 20px 0; }
|
|
15
|
+
button[type="submit"] { padding: 12px 24px; background: #0066cc; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 16px; }
|
|
16
|
+
.order-summary { background: #f5f5f5; padding: 15px; border-radius: 4px; margin-bottom: 20px; }
|
|
17
|
+
</style>
|
|
18
|
+
</head>
|
|
19
|
+
<body>
|
|
20
|
+
<h1>Checkout</h1>
|
|
21
|
+
|
|
22
|
+
<div class="order-summary">
|
|
23
|
+
<h2>Order Summary</h2>
|
|
24
|
+
<p>Widget Pro — $99.99 x <input type="number" id="quantity" value="1" min="1" style="width: 50px;"></p>
|
|
25
|
+
<p class="total" id="total">Total: $99.99</p> <!-- BUG 2: shows $NaN when quantity is cleared -->
|
|
26
|
+
</div>
|
|
27
|
+
|
|
28
|
+
<form class="checkout-form" id="checkout-form">
|
|
29
|
+
<h2>Shipping Information</h2>
|
|
30
|
+
|
|
31
|
+
<div class="form-group">
|
|
32
|
+
<label for="email">Email</label>
|
|
33
|
+
<input type="text" id="email" name="email" placeholder="you@example.com" required
|
|
34
|
+
pattern="[^@]+@[^@]"> <!-- BUG 1: broken regex — accepts "user@" as valid -->
|
|
35
|
+
<span class="error-msg" id="email-error">Please enter a valid email</span>
|
|
36
|
+
</div>
|
|
37
|
+
|
|
38
|
+
<div class="form-group">
|
|
39
|
+
<label for="address">Address</label>
|
|
40
|
+
<input type="text" id="address" name="address" placeholder="123 Main St" required>
|
|
41
|
+
</div>
|
|
42
|
+
|
|
43
|
+
<div class="form-group">
|
|
44
|
+
<label for="city">City</label>
|
|
45
|
+
<input type="text" id="city" name="city" placeholder="San Francisco" required>
|
|
46
|
+
</div>
|
|
47
|
+
|
|
48
|
+
<div class="form-group">
|
|
49
|
+
<label for="zip">Zip Code</label>
|
|
50
|
+
<input type="text" id="zip" name="zip" placeholder="94105"> <!-- BUG 4: missing required attribute -->
|
|
51
|
+
</div>
|
|
52
|
+
|
|
53
|
+
<h2>Payment</h2>
|
|
54
|
+
|
|
55
|
+
<div class="form-group">
|
|
56
|
+
<label for="cc">Credit Card Number</label>
|
|
57
|
+
<input type="text" id="cc" name="cc" placeholder="4111 1111 1111 1111" required>
|
|
58
|
+
<!-- BUG 3: no maxlength — overflows container at >20 chars -->
|
|
59
|
+
</div>
|
|
60
|
+
|
|
61
|
+
<div class="form-group">
|
|
62
|
+
<label for="exp">Expiration</label>
|
|
63
|
+
<input type="text" id="exp" name="exp" placeholder="MM/YY" required maxlength="5">
|
|
64
|
+
</div>
|
|
65
|
+
|
|
66
|
+
<div class="form-group">
|
|
67
|
+
<label for="cvv">CVV</label>
|
|
68
|
+
<input type="text" id="cvv" name="cvv" placeholder="123" required maxlength="4">
|
|
69
|
+
</div>
|
|
70
|
+
|
|
71
|
+
<button type="submit">Place Order — $<span id="submit-total">99.99</span></button>
|
|
72
|
+
</form>
|
|
73
|
+
|
|
74
|
+
<script>
|
|
75
|
+
// Update total when quantity changes
|
|
76
|
+
const quantityInput = document.getElementById('quantity');
|
|
77
|
+
const totalEl = document.getElementById('total');
|
|
78
|
+
const submitTotalEl = document.getElementById('submit-total');
|
|
79
|
+
|
|
80
|
+
quantityInput.addEventListener('input', () => {
|
|
81
|
+
// BUG 2: parseInt on empty string returns NaN, no fallback
|
|
82
|
+
const qty = parseInt(quantityInput.value);
|
|
83
|
+
const total = (qty * 99.99).toFixed(2);
|
|
84
|
+
totalEl.textContent = 'Total: $' + total;
|
|
85
|
+
submitTotalEl.textContent = total;
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// Email validation (broken)
|
|
89
|
+
const emailInput = document.getElementById('email');
|
|
90
|
+
emailInput.addEventListener('blur', () => {
|
|
91
|
+
// BUG 1: this regex accepts "user@" — missing domain part check
|
|
92
|
+
const valid = /[^@]+@/.test(emailInput.value);
|
|
93
|
+
emailInput.classList.toggle('invalid', !valid && emailInput.value.length > 0);
|
|
94
|
+
document.getElementById('email-error').style.display = (!valid && emailInput.value.length > 0) ? 'block' : 'none';
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// Form submit
|
|
98
|
+
document.getElementById('checkout-form').addEventListener('submit', (e) => {
|
|
99
|
+
e.preventDefault();
|
|
100
|
+
// BUG 5: stripe is not defined — console error on submit
|
|
101
|
+
stripe.createPaymentMethod({
|
|
102
|
+
type: 'card',
|
|
103
|
+
card: { number: document.getElementById('cc').value }
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
</script>
|
|
107
|
+
</body>
|
|
108
|
+
</html>
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<title>QA Eval — SPA Store</title>
|
|
6
|
+
<style>
|
|
7
|
+
body { font-family: sans-serif; padding: 20px; margin: 0; }
|
|
8
|
+
nav { background: #333; padding: 10px 20px; }
|
|
9
|
+
nav a { color: white; margin-right: 15px; text-decoration: none; cursor: pointer; }
|
|
10
|
+
nav a:hover { text-decoration: underline; }
|
|
11
|
+
#app { padding: 20px; }
|
|
12
|
+
.product { border: 1px solid #ddd; padding: 10px; margin: 10px 0; border-radius: 4px; }
|
|
13
|
+
.product button { padding: 6px 12px; background: #0066cc; color: white; border: none; cursor: pointer; }
|
|
14
|
+
.cart-count { background: #cc0000; color: white; padding: 2px 8px; border-radius: 10px; font-size: 12px; }
|
|
15
|
+
.error { color: red; padding: 10px; }
|
|
16
|
+
.loading { color: #666; padding: 10px; }
|
|
17
|
+
</style>
|
|
18
|
+
</head>
|
|
19
|
+
<body>
|
|
20
|
+
<nav>
|
|
21
|
+
<a href="#/home">Home</a>
|
|
22
|
+
<a href="#/prodcts">Products</a> <!-- BUG 1: broken route — typo "prodcts" instead of "products" -->
|
|
23
|
+
<a href="#/contact">Contact</a>
|
|
24
|
+
<span class="cart-count" id="cart-count">0</span>
|
|
25
|
+
</nav>
|
|
26
|
+
|
|
27
|
+
<div id="app">
|
|
28
|
+
<p>Welcome to SPA Store. Use the navigation above.</p>
|
|
29
|
+
</div>
|
|
30
|
+
|
|
31
|
+
<script>
|
|
32
|
+
let cartCount = 0;
|
|
33
|
+
|
|
34
|
+
// BUG 2: cart count never resets on route change — stale state
|
|
35
|
+
function addToCart() {
|
|
36
|
+
cartCount++;
|
|
37
|
+
document.getElementById('cart-count').textContent = cartCount;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function renderHome() {
|
|
41
|
+
document.getElementById('app').innerHTML = `
|
|
42
|
+
<h1>Welcome to SPA Store</h1>
|
|
43
|
+
<p>Browse our products using the navigation above.</p>
|
|
44
|
+
`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function renderProducts() {
|
|
48
|
+
document.getElementById('app').innerHTML = '<p class="loading">Loading products...</p>';
|
|
49
|
+
|
|
50
|
+
// BUG 3: async race — shows data briefly, then shows error
|
|
51
|
+
setTimeout(() => {
|
|
52
|
+
document.getElementById('app').innerHTML = `
|
|
53
|
+
<h1>Products</h1>
|
|
54
|
+
<div class="product">
|
|
55
|
+
<h3>Widget A</h3>
|
|
56
|
+
<p>$29.99</p>
|
|
57
|
+
<button onclick="addToCart()">Add to Cart</button>
|
|
58
|
+
</div>
|
|
59
|
+
<div class="product">
|
|
60
|
+
<h3>Widget B</h3>
|
|
61
|
+
<p>$49.99</p>
|
|
62
|
+
<button onclick="addToCart()">Add to Cart</button>
|
|
63
|
+
</div>
|
|
64
|
+
`;
|
|
65
|
+
}, 300);
|
|
66
|
+
|
|
67
|
+
setTimeout(() => {
|
|
68
|
+
document.getElementById('app').innerHTML = '<p class="error">Error: Failed to fetch products from API</p>';
|
|
69
|
+
}, 1000);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function renderContact() {
|
|
73
|
+
document.getElementById('app').innerHTML = `
|
|
74
|
+
<h1>Contact Us</h1>
|
|
75
|
+
<p>Email: support@spastore.example.com</p>
|
|
76
|
+
`;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// BUG 4: nav links have no aria-current attribute on active route
|
|
80
|
+
function router() {
|
|
81
|
+
const hash = window.location.hash || '#/home';
|
|
82
|
+
switch (hash) {
|
|
83
|
+
case '#/home': renderHome(); break;
|
|
84
|
+
case '#/products': renderProducts(); break;
|
|
85
|
+
case '#/contact': renderContact(); break;
|
|
86
|
+
default:
|
|
87
|
+
document.getElementById('app').innerHTML = '<p>Page not found</p>';
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// BUG 5: console.warn on every route change — simulates listener leak
|
|
91
|
+
console.warn('Possible memory leak detected: 11 event listeners added to window');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
window.addEventListener('hashchange', router);
|
|
95
|
+
router();
|
|
96
|
+
</script>
|
|
97
|
+
</body>
|
|
98
|
+
</html>
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<title>QA Eval — Widget Dashboard</title>
|
|
6
|
+
<style>
|
|
7
|
+
body { font-family: sans-serif; padding: 20px; }
|
|
8
|
+
nav { margin-bottom: 20px; }
|
|
9
|
+
nav a { margin-right: 15px; color: #0066cc; }
|
|
10
|
+
form { margin: 20px 0; padding: 15px; border: 1px solid #ccc; border-radius: 4px; }
|
|
11
|
+
input { display: block; margin: 8px 0; padding: 6px; }
|
|
12
|
+
button { padding: 8px 16px; margin-top: 8px; }
|
|
13
|
+
.stats { margin: 20px 0; }
|
|
14
|
+
img { display: block; margin: 20px 0; }
|
|
15
|
+
</style>
|
|
16
|
+
</head>
|
|
17
|
+
<body>
|
|
18
|
+
<nav>
|
|
19
|
+
<a href="/">Home</a>
|
|
20
|
+
<a href="/about">About</a>
|
|
21
|
+
<a href="/nonexistent-404-page">Resources</a> <!-- BUG 1: broken link (404) -->
|
|
22
|
+
</nav>
|
|
23
|
+
|
|
24
|
+
<h1>Widget Dashboard</h1>
|
|
25
|
+
|
|
26
|
+
<form id="contact">
|
|
27
|
+
<h2>Contact Us</h2>
|
|
28
|
+
<input type="text" name="name" placeholder="Name" required>
|
|
29
|
+
<input type="email" name="email" placeholder="Email" required>
|
|
30
|
+
<button type="submit" disabled>Submit</button> <!-- BUG 2: submit button permanently disabled -->
|
|
31
|
+
</form>
|
|
32
|
+
|
|
33
|
+
<div class="stats" style="width: 400px; overflow: hidden;">
|
|
34
|
+
<h2>Statistics</h2>
|
|
35
|
+
<p style="white-space: nowrap; width: 600px;">
|
|
36
|
+
Revenue: $1,234,567.89 | Users: 45,678 | Conversion: 3.2% | Growth: +12.5% MoM | Retention: 87.3%
|
|
37
|
+
</p> <!-- BUG 3: content overflow/clipping — text wider than container with overflow:hidden -->
|
|
38
|
+
</div>
|
|
39
|
+
|
|
40
|
+
<img src="/logo.png"> <!-- BUG 4: missing alt text on image -->
|
|
41
|
+
|
|
42
|
+
<footer>
|
|
43
|
+
<p>© 2026 Widget Co. All rights reserved.</p>
|
|
44
|
+
</footer>
|
|
45
|
+
|
|
46
|
+
<script>
|
|
47
|
+
console.error("TypeError: Cannot read properties of undefined (reading 'map')");
|
|
48
|
+
// BUG 5: console error on page load
|
|
49
|
+
</script>
|
|
50
|
+
</body>
|
|
51
|
+
</html>
|