opengstack 0.13.7 → 0.13.9

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.
Files changed (135) hide show
  1. package/bin/opengstack.js +35 -90
  2. package/package.json +2 -3
  3. package/scripts/install-skills.js +47 -58
  4. package/skills/browse/bin/find-browse +21 -0
  5. package/skills/browse/bin/remote-slug +14 -0
  6. package/skills/browse/scripts/build-node-server.sh +48 -0
  7. package/skills/browse/src/activity.ts +208 -0
  8. package/skills/browse/src/browser-manager.ts +959 -0
  9. package/skills/browse/src/buffers.ts +137 -0
  10. package/skills/browse/src/bun-polyfill.cjs +109 -0
  11. package/skills/browse/src/cli.ts +678 -0
  12. package/skills/browse/src/commands.ts +128 -0
  13. package/skills/browse/src/config.ts +150 -0
  14. package/skills/browse/src/cookie-import-browser.ts +625 -0
  15. package/skills/browse/src/cookie-picker-routes.ts +230 -0
  16. package/skills/browse/src/cookie-picker-ui.ts +688 -0
  17. package/skills/browse/src/find-browse.ts +61 -0
  18. package/skills/browse/src/meta-commands.ts +550 -0
  19. package/skills/browse/src/platform.ts +17 -0
  20. package/skills/browse/src/read-commands.ts +358 -0
  21. package/skills/browse/src/server.ts +1192 -0
  22. package/skills/browse/src/sidebar-agent.ts +280 -0
  23. package/skills/browse/src/sidebar-utils.ts +21 -0
  24. package/skills/browse/src/snapshot.ts +407 -0
  25. package/skills/browse/src/url-validation.ts +95 -0
  26. package/skills/browse/src/write-commands.ts +364 -0
  27. package/skills/browse/test/activity.test.ts +120 -0
  28. package/skills/browse/test/adversarial-security.test.ts +32 -0
  29. package/skills/browse/test/browser-manager-unit.test.ts +17 -0
  30. package/skills/browse/test/bun-polyfill.test.ts +72 -0
  31. package/skills/browse/test/commands.test.ts +2075 -0
  32. package/skills/browse/test/compare-board.test.ts +342 -0
  33. package/skills/browse/test/config.test.ts +316 -0
  34. package/skills/browse/test/cookie-import-browser.test.ts +519 -0
  35. package/skills/browse/test/cookie-picker-routes.test.ts +260 -0
  36. package/skills/browse/test/file-drop.test.ts +271 -0
  37. package/skills/browse/test/find-browse.test.ts +50 -0
  38. package/skills/browse/test/findport.test.ts +191 -0
  39. package/skills/browse/test/fixtures/basic.html +33 -0
  40. package/skills/browse/test/fixtures/cursor-interactive.html +22 -0
  41. package/skills/browse/test/fixtures/dialog.html +15 -0
  42. package/skills/browse/test/fixtures/empty.html +2 -0
  43. package/skills/browse/test/fixtures/forms.html +55 -0
  44. package/skills/browse/test/fixtures/iframe.html +30 -0
  45. package/skills/browse/test/fixtures/network-idle.html +30 -0
  46. package/skills/browse/test/fixtures/qa-eval-checkout.html +108 -0
  47. package/skills/browse/test/fixtures/qa-eval-spa.html +98 -0
  48. package/skills/browse/test/fixtures/qa-eval.html +51 -0
  49. package/skills/browse/test/fixtures/responsive.html +49 -0
  50. package/skills/browse/test/fixtures/snapshot.html +55 -0
  51. package/skills/browse/test/fixtures/spa.html +24 -0
  52. package/skills/browse/test/fixtures/states.html +17 -0
  53. package/skills/browse/test/fixtures/upload.html +25 -0
  54. package/skills/browse/test/gstack-config.test.ts +138 -0
  55. package/skills/browse/test/gstack-update-check.test.ts +514 -0
  56. package/skills/browse/test/handoff.test.ts +235 -0
  57. package/skills/browse/test/path-validation.test.ts +91 -0
  58. package/skills/browse/test/platform.test.ts +37 -0
  59. package/skills/browse/test/server-auth.test.ts +65 -0
  60. package/skills/browse/test/sidebar-agent-roundtrip.test.ts +226 -0
  61. package/skills/browse/test/sidebar-agent.test.ts +199 -0
  62. package/skills/browse/test/sidebar-integration.test.ts +320 -0
  63. package/skills/browse/test/sidebar-unit.test.ts +96 -0
  64. package/skills/browse/test/snapshot.test.ts +467 -0
  65. package/skills/browse/test/state-ttl.test.ts +35 -0
  66. package/skills/browse/test/test-server.ts +57 -0
  67. package/skills/browse/test/url-validation.test.ts +72 -0
  68. package/skills/browse/test/watch.test.ts +129 -0
  69. package/skills/careful/bin/check-careful.sh +112 -0
  70. package/skills/cso/ACKNOWLEDGEMENTS.md +14 -0
  71. package/skills/freeze/bin/check-freeze.sh +79 -0
  72. package/skills/qa/references/issue-taxonomy.md +85 -0
  73. package/skills/qa/templates/qa-report-template.md +126 -0
  74. package/skills/review/TODOS-format.md +62 -0
  75. package/skills/review/checklist.md +220 -0
  76. package/skills/review/design-checklist.md +132 -0
  77. package/skills/review/greptile-triage.md +220 -0
  78. /package/{autoplan → skills/autoplan}/SKILL.md +0 -0
  79. /package/{autoplan → skills/autoplan}/SKILL.md.tmpl +0 -0
  80. /package/{benchmark → skills/benchmark}/SKILL.md +0 -0
  81. /package/{benchmark → skills/benchmark}/SKILL.md.tmpl +0 -0
  82. /package/{browse → skills/browse}/SKILL.md +0 -0
  83. /package/{browse → skills/browse}/SKILL.md.tmpl +0 -0
  84. /package/{canary → skills/canary}/SKILL.md +0 -0
  85. /package/{canary → skills/canary}/SKILL.md.tmpl +0 -0
  86. /package/{careful → skills/careful}/SKILL.md +0 -0
  87. /package/{careful → skills/careful}/SKILL.md.tmpl +0 -0
  88. /package/{codex → skills/codex}/SKILL.md +0 -0
  89. /package/{codex → skills/codex}/SKILL.md.tmpl +0 -0
  90. /package/{connect-chrome → skills/connect-chrome}/SKILL.md +0 -0
  91. /package/{connect-chrome → skills/connect-chrome}/SKILL.md.tmpl +0 -0
  92. /package/{cso → skills/cso}/SKILL.md +0 -0
  93. /package/{cso → skills/cso}/SKILL.md.tmpl +0 -0
  94. /package/{design-consultation → skills/design-consultation}/SKILL.md +0 -0
  95. /package/{design-consultation → skills/design-consultation}/SKILL.md.tmpl +0 -0
  96. /package/{design-review → skills/design-review}/SKILL.md +0 -0
  97. /package/{design-review → skills/design-review}/SKILL.md.tmpl +0 -0
  98. /package/{design-shotgun → skills/design-shotgun}/SKILL.md +0 -0
  99. /package/{design-shotgun → skills/design-shotgun}/SKILL.md.tmpl +0 -0
  100. /package/{document-release → skills/document-release}/SKILL.md +0 -0
  101. /package/{document-release → skills/document-release}/SKILL.md.tmpl +0 -0
  102. /package/{freeze → skills/freeze}/SKILL.md +0 -0
  103. /package/{freeze → skills/freeze}/SKILL.md.tmpl +0 -0
  104. /package/{gstack-upgrade → skills/gstack-upgrade}/SKILL.md +0 -0
  105. /package/{gstack-upgrade → skills/gstack-upgrade}/SKILL.md.tmpl +0 -0
  106. /package/{guard → skills/guard}/SKILL.md +0 -0
  107. /package/{guard → skills/guard}/SKILL.md.tmpl +0 -0
  108. /package/{investigate → skills/investigate}/SKILL.md +0 -0
  109. /package/{investigate → skills/investigate}/SKILL.md.tmpl +0 -0
  110. /package/{land-and-deploy → skills/land-and-deploy}/SKILL.md +0 -0
  111. /package/{land-and-deploy → skills/land-and-deploy}/SKILL.md.tmpl +0 -0
  112. /package/{office-hours → skills/office-hours}/SKILL.md +0 -0
  113. /package/{office-hours → skills/office-hours}/SKILL.md.tmpl +0 -0
  114. /package/{plan-ceo-review → skills/plan-ceo-review}/SKILL.md +0 -0
  115. /package/{plan-ceo-review → skills/plan-ceo-review}/SKILL.md.tmpl +0 -0
  116. /package/{plan-design-review → skills/plan-design-review}/SKILL.md +0 -0
  117. /package/{plan-design-review → skills/plan-design-review}/SKILL.md.tmpl +0 -0
  118. /package/{plan-eng-review → skills/plan-eng-review}/SKILL.md +0 -0
  119. /package/{plan-eng-review → skills/plan-eng-review}/SKILL.md.tmpl +0 -0
  120. /package/{qa → skills/qa}/SKILL.md +0 -0
  121. /package/{qa → skills/qa}/SKILL.md.tmpl +0 -0
  122. /package/{qa-only → skills/qa-only}/SKILL.md +0 -0
  123. /package/{qa-only → skills/qa-only}/SKILL.md.tmpl +0 -0
  124. /package/{retro → skills/retro}/SKILL.md +0 -0
  125. /package/{retro → skills/retro}/SKILL.md.tmpl +0 -0
  126. /package/{review → skills/review}/SKILL.md +0 -0
  127. /package/{review → skills/review}/SKILL.md.tmpl +0 -0
  128. /package/{setup-browser-cookies → skills/setup-browser-cookies}/SKILL.md +0 -0
  129. /package/{setup-browser-cookies → skills/setup-browser-cookies}/SKILL.md.tmpl +0 -0
  130. /package/{setup-deploy → skills/setup-deploy}/SKILL.md +0 -0
  131. /package/{setup-deploy → skills/setup-deploy}/SKILL.md.tmpl +0 -0
  132. /package/{ship → skills/ship}/SKILL.md +0 -0
  133. /package/{ship → skills/ship}/SKILL.md.tmpl +0 -0
  134. /package/{unfreeze → skills/unfreeze}/SKILL.md +0 -0
  135. /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,2 @@
1
+ <!DOCTYPE html>
2
+ <html><body></body></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>&copy; 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>