~cytrogen/gstack

ref: 66894601e3de98c0c3b32869944edc241484b42e gstack/browse/test/compare-board.test.ts -rw-r--r-- 11.3 KiB
66894601 — Garry Tan chore: gitignore .factory and remove tracked files (v0.13.5.1) (#642) 11 days ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
/**
 * Integration test for the design comparison board feedback loop.
 *
 * Tests the DOM polling pattern that plan-design-review, office-hours,
 * and design-consultation use to read user feedback from the comparison board.
 *
 * Flow: generate board HTML → open in browser → verify DOM elements →
 *       simulate user interaction → verify structured JSON feedback.
 *
 * No LLM involved — this is a deterministic functional test.
 */

import { describe, test, expect, beforeAll, afterAll } from 'bun:test';
import { BrowserManager } from '../src/browser-manager';
import { handleReadCommand } from '../src/read-commands';
import { handleWriteCommand } from '../src/write-commands';
import { generateCompareHtml } from '../../design/src/compare';
import * as fs from 'fs';
import * as path from 'path';

let bm: BrowserManager;
let boardUrl: string;
let server: ReturnType<typeof Bun.serve>;
let tmpDir: string;

// Create a minimal 1x1 pixel PNG for test variants
function createTestPng(filePath: string): void {
  // Minimal valid PNG: 1x1 red pixel
  const png = Buffer.from(
    'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/58BAwAI/AL+hc2rNAAAAABJRU5ErkJggg==',
    'base64'
  );
  fs.writeFileSync(filePath, png);
}

beforeAll(async () => {
  // Create test PNG files
  tmpDir = '/tmp/compare-board-test-' + Date.now();
  fs.mkdirSync(tmpDir, { recursive: true });

  createTestPng(path.join(tmpDir, 'variant-A.png'));
  createTestPng(path.join(tmpDir, 'variant-B.png'));
  createTestPng(path.join(tmpDir, 'variant-C.png'));

  // Generate comparison board HTML using the real compare module
  const html = generateCompareHtml([
    path.join(tmpDir, 'variant-A.png'),
    path.join(tmpDir, 'variant-B.png'),
    path.join(tmpDir, 'variant-C.png'),
  ]);

  // Serve the board via HTTP (browse blocks file:// URLs for security)
  server = Bun.serve({
    port: 0,
    fetch() {
      return new Response(html, { headers: { 'Content-Type': 'text/html' } });
    },
  });
  boardUrl = `http://localhost:${server.port}`;

  // Launch browser and navigate to the board
  bm = new BrowserManager();
  await bm.launch();
  await handleWriteCommand('goto', [boardUrl], bm);
});

afterAll(() => {
  try { server.stop(); } catch {}
  fs.rmSync(tmpDir, { recursive: true, force: true });
  setTimeout(() => process.exit(0), 500);
});

// ─── DOM Structure ──────────────────────────────────────────────

describe('Comparison board DOM structure', () => {
  test('has hidden status element', async () => {
    const status = await handleReadCommand('js', [
      'document.getElementById("status").textContent'
    ], bm);
    expect(status).toBe('');
  });

  test('has hidden feedback-result element', async () => {
    const result = await handleReadCommand('js', [
      'document.getElementById("feedback-result").textContent'
    ], bm);
    expect(result).toBe('');
  });

  test('has submit button', async () => {
    const exists = await handleReadCommand('js', [
      '!!document.getElementById("submit-btn")'
    ], bm);
    expect(exists).toBe('true');
  });

  test('has regenerate button', async () => {
    const exists = await handleReadCommand('js', [
      '!!document.getElementById("regen-btn")'
    ], bm);
    expect(exists).toBe('true');
  });

  test('has 3 variant cards', async () => {
    const count = await handleReadCommand('js', [
      'document.querySelectorAll(".variant").length'
    ], bm);
    expect(count).toBe('3');
  });

  test('has pick radio buttons for each variant', async () => {
    const count = await handleReadCommand('js', [
      'document.querySelectorAll("input[name=\\"preferred\\"]").length'
    ], bm);
    expect(count).toBe('3');
  });

  test('has star ratings for each variant', async () => {
    const count = await handleReadCommand('js', [
      'document.querySelectorAll(".stars").length'
    ], bm);
    expect(count).toBe('3');
  });
});

// ─── Submit Flow ────────────────────────────────────────────────

describe('Submit feedback flow', () => {
  test('submit without interaction returns empty preferred', async () => {
    // Reset page state
    await handleWriteCommand('goto', [boardUrl], bm);

    // Click submit without picking anything
    await handleReadCommand('js', [
      'document.getElementById("submit-btn").click()'
    ], bm);

    // Status should be "submitted"
    const status = await handleReadCommand('js', [
      'document.getElementById("status").textContent'
    ], bm);
    expect(status).toBe('submitted');

    // Read feedback JSON
    const raw = await handleReadCommand('js', [
      'document.getElementById("feedback-result").textContent'
    ], bm);
    const feedback = JSON.parse(raw);
    expect(feedback.preferred).toBeNull();
    expect(feedback.regenerated).toBe(false);
    expect(feedback.ratings).toBeDefined();
  });

  test('submit with pick + rating + comment returns structured JSON', async () => {
    // Fresh page
    await handleWriteCommand('goto', [boardUrl], bm);

    // Pick variant B
    await handleReadCommand('js', [
      'document.querySelectorAll("input[name=\\"preferred\\"]")[1].click()'
    ], bm);

    // Rate variant A: 4 stars (click the 4th star)
    await handleReadCommand('js', [
      'document.querySelectorAll(".stars")[0].querySelectorAll(".star")[3].click()'
    ], bm);

    // Rate variant B: 5 stars
    await handleReadCommand('js', [
      'document.querySelectorAll(".stars")[1].querySelectorAll(".star")[4].click()'
    ], bm);

    // Add comment on variant A
    await handleReadCommand('js', [
      'document.querySelectorAll(".feedback-input")[0].value = "Good spacing but wrong colors"'
    ], bm);

    // Add overall feedback
    await handleReadCommand('js', [
      'document.getElementById("overall-feedback").value = "Go with B, make the CTA bigger"'
    ], bm);

    // Submit
    await handleReadCommand('js', [
      'document.getElementById("submit-btn").click()'
    ], bm);

    // Verify status
    const status = await handleReadCommand('js', [
      'document.getElementById("status").textContent'
    ], bm);
    expect(status).toBe('submitted');

    // Read and verify structured feedback
    const raw = await handleReadCommand('js', [
      'document.getElementById("feedback-result").textContent'
    ], bm);
    const feedback = JSON.parse(raw);

    expect(feedback.preferred).toBe('B');
    expect(feedback.ratings.A).toBe(4);
    expect(feedback.ratings.B).toBe(5);
    expect(feedback.comments.A).toBe('Good spacing but wrong colors');
    expect(feedback.overall).toBe('Go with B, make the CTA bigger');
    expect(feedback.regenerated).toBe(false);
  });

  test('submit button is disabled after submission', async () => {
    const disabled = await handleReadCommand('js', [
      'document.getElementById("submit-btn").disabled'
    ], bm);
    expect(disabled).toBe('true');
  });

  test('success message is visible after submission', async () => {
    const display = await handleReadCommand('js', [
      'document.getElementById("success-msg").style.display'
    ], bm);
    expect(display).toBe('block');
  });
});

// ─── Regenerate Flow ────────────────────────────────────────────

describe('Regenerate flow', () => {
  test('regenerate button sets status to "regenerate"', async () => {
    // Fresh page
    await handleWriteCommand('goto', [boardUrl], bm);

    // Click "Totally different" chiclet then regenerate
    await handleReadCommand('js', [
      'document.querySelector(".regen-chiclet[data-action=\\"different\\"]").click()'
    ], bm);
    await handleReadCommand('js', [
      'document.getElementById("regen-btn").click()'
    ], bm);

    const status = await handleReadCommand('js', [
      'document.getElementById("status").textContent'
    ], bm);
    expect(status).toBe('regenerate');

    // Verify regenerate action in feedback
    const raw = await handleReadCommand('js', [
      'document.getElementById("feedback-result").textContent'
    ], bm);
    const feedback = JSON.parse(raw);
    expect(feedback.regenerated).toBe(true);
    expect(feedback.regenerateAction).toBe('different');
  });

  test('"More like this" sets regenerate with variant reference', async () => {
    // Fresh page
    await handleWriteCommand('goto', [boardUrl], bm);

    // Click "More like this" on variant B
    await handleReadCommand('js', [
      'document.querySelectorAll(".more-like-this")[1].click()'
    ], bm);

    const status = await handleReadCommand('js', [
      'document.getElementById("status").textContent'
    ], bm);
    expect(status).toBe('regenerate');

    const raw = await handleReadCommand('js', [
      'document.getElementById("feedback-result").textContent'
    ], bm);
    const feedback = JSON.parse(raw);
    expect(feedback.regenerated).toBe(true);
    expect(feedback.regenerateAction).toBe('more_like_B');
  });

  test('regenerate with custom text', async () => {
    // Fresh page
    await handleWriteCommand('goto', [boardUrl], bm);

    // Type custom regeneration text
    await handleReadCommand('js', [
      'document.getElementById("regen-custom-input").value = "V3 layout with V1 colors"'
    ], bm);

    // Click regenerate (no chiclet selected = custom)
    await handleReadCommand('js', [
      'document.getElementById("regen-btn").click()'
    ], bm);

    const raw = await handleReadCommand('js', [
      'document.getElementById("feedback-result").textContent'
    ], bm);
    const feedback = JSON.parse(raw);
    expect(feedback.regenerated).toBe(true);
    expect(feedback.regenerateAction).toBe('V3 layout with V1 colors');
  });
});

// ─── Agent Polling Pattern ──────────────────────────────────────

describe('Agent polling pattern (simulates what $B eval does)', () => {
  test('status is empty before user action', async () => {
    // Fresh page — simulates agent's first poll
    await handleWriteCommand('goto', [boardUrl], bm);

    const status = await handleReadCommand('js', [
      'document.getElementById("status").textContent'
    ], bm);
    expect(status).toBe('');
  });

  test('full polling cycle: empty → submitted → read JSON', async () => {
    await handleWriteCommand('goto', [boardUrl], bm);

    // Poll 1: empty (user hasn't acted)
    const poll1 = await handleReadCommand('js', [
      'document.getElementById("status").textContent'
    ], bm);
    expect(poll1).toBe('');

    // User acts: pick A, submit
    await handleReadCommand('js', [
      'document.querySelectorAll("input[name=\\"preferred\\"]")[0].click()'
    ], bm);
    await handleReadCommand('js', [
      'document.getElementById("submit-btn").click()'
    ], bm);

    // Poll 2: submitted
    const poll2 = await handleReadCommand('js', [
      'document.getElementById("status").textContent'
    ], bm);
    expect(poll2).toBe('submitted');

    // Read feedback (what the agent does after seeing "submitted")
    const raw = await handleReadCommand('js', [
      'document.getElementById("feedback-result").textContent'
    ], bm);
    const feedback = JSON.parse(raw);
    expect(feedback.preferred).toBe('A');
    expect(typeof feedback.ratings).toBe('object');
    expect(typeof feedback.comments).toBe('object');
  });
});