server.test.js 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162
  1. const { spawn } = require('child_process');
  2. const http = require('http');
  3. const WebSocket = require('ws');
  4. const fs = require('fs');
  5. const path = require('path');
  6. const assert = require('assert');
  7. const SERVER_PATH = path.join(__dirname, '../../lib/brainstorm-server/index.js');
  8. const TEST_PORT = 3334;
  9. const TEST_DIR = '/tmp/brainstorm-test';
  10. function cleanup() {
  11. if (fs.existsSync(TEST_DIR)) {
  12. fs.rmSync(TEST_DIR, { recursive: true });
  13. }
  14. }
  15. async function sleep(ms) {
  16. return new Promise(resolve => setTimeout(resolve, ms));
  17. }
  18. async function fetch(url) {
  19. return new Promise((resolve, reject) => {
  20. http.get(url, (res) => {
  21. let data = '';
  22. res.on('data', chunk => data += chunk);
  23. res.on('end', () => resolve({ status: res.statusCode, body: data }));
  24. }).on('error', reject);
  25. });
  26. }
  27. function startServer() {
  28. return spawn('node', [SERVER_PATH], {
  29. env: { ...process.env, BRAINSTORM_PORT: TEST_PORT, BRAINSTORM_DIR: TEST_DIR }
  30. });
  31. }
  32. async function runTests() {
  33. cleanup();
  34. fs.mkdirSync(TEST_DIR, { recursive: true });
  35. const server = startServer();
  36. let stdout = '';
  37. let stderr = '';
  38. server.stdout.on('data', (data) => { stdout += data.toString(); });
  39. server.stderr.on('data', (data) => { stderr += data.toString(); });
  40. // Wait for server to start (up to 3 seconds)
  41. for (let i = 0; i < 30; i++) {
  42. if (stdout.includes('server-started')) break;
  43. await sleep(100);
  44. }
  45. if (stderr) console.error('Server stderr:', stderr);
  46. try {
  47. // Test 1: Server starts and outputs JSON
  48. console.log('Test 1: Server startup message');
  49. assert(stdout.includes('server-started'), 'Should output server-started');
  50. assert(stdout.includes(TEST_PORT.toString()), 'Should include port');
  51. console.log(' PASS');
  52. // Test 2: GET / returns waiting page with helper injected when no screens exist
  53. console.log('Test 2: Serves waiting page with helper injected');
  54. const res = await fetch(`http://localhost:${TEST_PORT}/`);
  55. assert.strictEqual(res.status, 200);
  56. assert(res.body.includes('Waiting for Claude'), 'Should show waiting message');
  57. assert(res.body.includes('WebSocket'), 'Should have helper.js injected');
  58. console.log(' PASS');
  59. // Test 3: WebSocket connection and event relay
  60. console.log('Test 3: WebSocket relays events to stdout');
  61. stdout = '';
  62. const ws = new WebSocket(`ws://localhost:${TEST_PORT}`);
  63. await new Promise(resolve => ws.on('open', resolve));
  64. ws.send(JSON.stringify({ type: 'click', text: 'Test Button' }));
  65. await sleep(300);
  66. assert(stdout.includes('"source":"user-event"'), 'Should relay user events with source field');
  67. assert(stdout.includes('Test Button'), 'Should include event data');
  68. ws.close();
  69. console.log(' PASS');
  70. // Test 4: File change triggers reload notification
  71. console.log('Test 4: File change notifies browsers');
  72. const ws2 = new WebSocket(`ws://localhost:${TEST_PORT}`);
  73. await new Promise(resolve => ws2.on('open', resolve));
  74. let gotReload = false;
  75. ws2.on('message', (data) => {
  76. const msg = JSON.parse(data.toString());
  77. if (msg.type === 'reload') gotReload = true;
  78. });
  79. fs.writeFileSync(path.join(TEST_DIR, 'test-screen.html'), '<html><body>Full doc</body></html>');
  80. await sleep(500);
  81. assert(gotReload, 'Should send reload message on file change');
  82. ws2.close();
  83. console.log(' PASS');
  84. // Test 5: Full HTML document served as-is (not wrapped)
  85. console.log('Test 5: Full HTML document served without frame wrapping');
  86. const fullDoc = '<!DOCTYPE html>\n<html><head><title>Custom</title></head><body><h1>Custom Page</h1></body></html>';
  87. fs.writeFileSync(path.join(TEST_DIR, 'full-doc.html'), fullDoc);
  88. await sleep(300);
  89. const fullRes = await fetch(`http://localhost:${TEST_PORT}/`);
  90. assert(fullRes.body.includes('<h1>Custom Page</h1>'), 'Should contain original content');
  91. assert(fullRes.body.includes('WebSocket'), 'Should still inject helper.js');
  92. // Should NOT have the frame template's feedback footer
  93. assert(!fullRes.body.includes('feedback-footer') || fullDoc.includes('feedback-footer'),
  94. 'Should not wrap full documents in frame template');
  95. console.log(' PASS');
  96. // Test 6: Bare HTML fragment gets wrapped in frame template
  97. console.log('Test 6: Content fragment wrapped in frame template');
  98. const fragment = '<h2>Pick a layout</h2>\n<p class="subtitle">Choose one</p>\n<div class="options"><div class="option" data-choice="a"><div class="letter">A</div><div class="content"><h3>Simple</h3></div></div></div>';
  99. fs.writeFileSync(path.join(TEST_DIR, 'fragment.html'), fragment);
  100. await sleep(300);
  101. const fragRes = await fetch(`http://localhost:${TEST_PORT}/`);
  102. // Should have the frame template structure
  103. assert(fragRes.body.includes('feedback-footer'), 'Fragment should get feedback footer from frame');
  104. assert(fragRes.body.includes('Brainstorm Companion'), 'Fragment should get header from frame');
  105. assert(fragRes.body.includes('--bg-primary'), 'Fragment should get theme CSS from frame');
  106. // Should have the original content inside
  107. assert(fragRes.body.includes('Pick a layout'), 'Fragment content should be present');
  108. assert(fragRes.body.includes('data-choice="a"'), 'Fragment content should be intact');
  109. // Should have helper.js injected
  110. assert(fragRes.body.includes('WebSocket'), 'Fragment should have helper.js injected');
  111. console.log(' PASS');
  112. // Test 7: Helper.js includes toggleSelect and send functions
  113. console.log('Test 7: Helper.js provides toggleSelect and send');
  114. const helperContent = fs.readFileSync(
  115. path.join(__dirname, '../../lib/brainstorm-server/helper.js'), 'utf-8'
  116. );
  117. assert(helperContent.includes('toggleSelect'), 'helper.js should define toggleSelect');
  118. assert(helperContent.includes('send'), 'helper.js should define send function');
  119. assert(helperContent.includes('selectedChoice'), 'helper.js should track selectedChoice');
  120. console.log(' PASS');
  121. // Test 8: sendToClaude confirmation uses CSS variables (dark mode support)
  122. console.log('Test 8: sendToClaude confirmation respects theming');
  123. assert(!helperContent.includes('color: #666'), 'Should not use hardcoded light-mode colors');
  124. assert(!helperContent.includes('color: #333'), 'Should not use hardcoded light-mode colors');
  125. console.log(' PASS');
  126. console.log('\nAll tests passed!');
  127. } finally {
  128. server.kill();
  129. cleanup();
  130. }
  131. }
  132. runTests().catch(err => {
  133. console.error('Test failed:', err);
  134. process.exit(1);
  135. });