bun-ui-tests 1.0.0
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/README.md +115 -0
- package/cli.ts +197 -0
- package/package.json +27 -0
- package/ui-runner.ts +597 -0
package/README.md
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# 🧪 Bun Test UI
|
|
2
|
+
|
|
3
|
+
Uma interface visual moderna e bonita para rodar e visualizar seus testes Bun em tempo real.
|
|
4
|
+
|
|
5
|
+

|
|
6
|
+

|
|
7
|
+

|
|
8
|
+
|
|
9
|
+
## ✨ Features
|
|
10
|
+
|
|
11
|
+
- 🎨 **Interface Moderna** - Design limpo com tema dark
|
|
12
|
+
- ⚡ **Tempo Real** - Veja os testes rodando ao vivo via WebSocket
|
|
13
|
+
- 🔍 **Busca Recursiva** - Encontra automaticamente todos os arquivos de teste no projeto
|
|
14
|
+
- 📂 **Organização por Arquivo** - Testes agrupados por arquivo com expansão/colapso
|
|
15
|
+
- ▶️ **Execução Seletiva** - Rode todos os testes, um arquivo específico, ou um teste individual
|
|
16
|
+
- 📊 **Estatísticas** - Contador de testes passados, falhos e em execução
|
|
17
|
+
- 🎯 **Suporte Completo** - Detecta `.test.ts`, `.spec.js`, `_test.tsx` e mais
|
|
18
|
+
- 🚀 **Performance** - Backend compilado para máxima velocidade
|
|
19
|
+
|
|
20
|
+
## 🚀 Instalação Rápida
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
# Clone o repositório
|
|
24
|
+
git clone <repo-url>
|
|
25
|
+
cd buntestui
|
|
26
|
+
|
|
27
|
+
# Instale globalmente
|
|
28
|
+
bun link
|
|
29
|
+
|
|
30
|
+
# Builde o projeto
|
|
31
|
+
buntestui build
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## 📖 Uso
|
|
35
|
+
|
|
36
|
+
### Em qualquer projeto com testes Bun:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
cd ~/meu-projeto
|
|
40
|
+
buntestui run
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Abra seu navegador em **http://localhost:3000** e veja a mágica acontecer! ✨
|
|
44
|
+
|
|
45
|
+
## 🎮 Comandos
|
|
46
|
+
|
|
47
|
+
| Comando | Descrição |
|
|
48
|
+
|---------|-----------|
|
|
49
|
+
| `buntestui build` | Builda o frontend (Vite) e backend (executável) |
|
|
50
|
+
| `buntestui run` | Inicia o Test UI em modo produção (usa arquivos buildados) |
|
|
51
|
+
| `buntestui dev` | Inicia em modo desenvolvimento (hot reload) |
|
|
52
|
+
| `buntestui help` | Mostra ajuda |
|
|
53
|
+
|
|
54
|
+
## 📋 Padrões de Teste Suportados
|
|
55
|
+
|
|
56
|
+
O Bun Test UI detecta automaticamente:
|
|
57
|
+
|
|
58
|
+
- `*.test.ts` / `*.test.js` / `*.test.tsx` / `*.test.jsx`
|
|
59
|
+
- `*.spec.ts` / `*.spec.js` / `*.spec.tsx` / `*.spec.jsx`
|
|
60
|
+
- `*_test.ts` / `*_test.js` / `*_test.tsx` / `*_test.jsx`
|
|
61
|
+
- `*_spec.ts` / `*_spec.js` / `*_spec.tsx` / `*_spec.jsx`
|
|
62
|
+
|
|
63
|
+
## 🏗️ Arquitetura
|
|
64
|
+
|
|
65
|
+
```
|
|
66
|
+
┌─────────────┐ WebSocket ┌─────────────┐
|
|
67
|
+
│ │ (port 5060) │ │
|
|
68
|
+
│ Frontend │◄─────────────────────────►│ Backend │
|
|
69
|
+
│ (React) │ │ (Bun) │
|
|
70
|
+
│ port 5050 │ │ │
|
|
71
|
+
└─────────────┘ └─────────────┘
|
|
72
|
+
│
|
|
73
|
+
▼
|
|
74
|
+
┌──────────────┐
|
|
75
|
+
│ bun test │
|
|
76
|
+
│ (spawned) │
|
|
77
|
+
└──────────────┘
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
- **Frontend**: React + Vite + Tailwind CSS
|
|
81
|
+
- **Backend**: Bun native WebSocket + process spawning
|
|
82
|
+
- **Comunicação**: WebSocket em tempo real
|
|
83
|
+
- **Compilado**: Executável standalone (~93MB)
|
|
84
|
+
|
|
85
|
+
## 🎨 Interface
|
|
86
|
+
|
|
87
|
+
A interface possui:
|
|
88
|
+
|
|
89
|
+
- **Painel de Testes**: Lista de arquivos e testes com status visual
|
|
90
|
+
- **Painel de Output**: Logs em tempo real com syntax highlighting
|
|
91
|
+
- **Header**: Estatísticas e controles de execução
|
|
92
|
+
- **Design Responsivo**: Funciona em mobile e desktop
|
|
93
|
+
|
|
94
|
+
### Cores:
|
|
95
|
+
|
|
96
|
+
- 🔵 **Azul**: Testes passando, status conectado
|
|
97
|
+
- 🟢 **Verde**: Logs de sucesso `(pass)`
|
|
98
|
+
- 🔴 **Vermelho**: Testes falhando, erros
|
|
99
|
+
- 🟡 **Amarelo**: Testes em execução
|
|
100
|
+
|
|
101
|
+
## 📚 Documentação Completa
|
|
102
|
+
|
|
103
|
+
Veja [INSTALL.md](./INSTALL.md) para instruções detalhadas de instalação e uso.
|
|
104
|
+
|
|
105
|
+
## 🤝 Contribuindo
|
|
106
|
+
|
|
107
|
+
Contribuições são bem-vindas! Sinta-se livre para abrir issues e pull requests.
|
|
108
|
+
|
|
109
|
+
## 📄 Licença
|
|
110
|
+
|
|
111
|
+
MIT
|
|
112
|
+
|
|
113
|
+
## 🙏 Agradecimentos
|
|
114
|
+
|
|
115
|
+
Feito com ❤️ para a comunidade Bun
|
package/cli.ts
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
import { spawn } from "node:child_process";
|
|
4
|
+
import { readFile, access } from "node:fs/promises";
|
|
5
|
+
import { join, dirname } from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
|
|
8
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
9
|
+
const __dirname = dirname(__filename);
|
|
10
|
+
|
|
11
|
+
const COMMANDS = {
|
|
12
|
+
run: "Run the test UI (production mode)",
|
|
13
|
+
dev: "Run in development mode (hot reload)",
|
|
14
|
+
help: "Show this help message"
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
async function showHelp() {
|
|
18
|
+
console.log(`
|
|
19
|
+
🧪 Bun Test UI - A beautiful UI for running Bun tests
|
|
20
|
+
|
|
21
|
+
Usage:
|
|
22
|
+
buntestui <command>
|
|
23
|
+
|
|
24
|
+
Commands:
|
|
25
|
+
run Start the test UI (production mode)
|
|
26
|
+
dev Start in development mode (hot reload enabled)
|
|
27
|
+
help Show this help message
|
|
28
|
+
|
|
29
|
+
Examples:
|
|
30
|
+
buntestui run # Run in production mode
|
|
31
|
+
buntestui dev # Run in development mode (for testing)
|
|
32
|
+
`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function buildFrontend() {
|
|
36
|
+
console.log("🏗️ Building frontend...\n");
|
|
37
|
+
|
|
38
|
+
const appDir = join(__dirname, "app");
|
|
39
|
+
|
|
40
|
+
return new Promise<void>((resolve, reject) => {
|
|
41
|
+
const proc = spawn("bun", ["run", "build"], {
|
|
42
|
+
cwd: appDir,
|
|
43
|
+
stdio: "inherit",
|
|
44
|
+
shell: true
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
proc.on("close", (code) => {
|
|
48
|
+
if (code === 0) {
|
|
49
|
+
console.log("\n✅ Frontend built successfully!");
|
|
50
|
+
resolve();
|
|
51
|
+
} else {
|
|
52
|
+
reject(new Error(`Frontend build failed with code ${code}`));
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
proc.on("error", (err) => {
|
|
57
|
+
reject(err);
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function checkBuildExists(): Promise<boolean> {
|
|
63
|
+
try {
|
|
64
|
+
const distPath = join(__dirname, "app", "dist", "index.html");
|
|
65
|
+
await access(distPath);
|
|
66
|
+
return true;
|
|
67
|
+
} catch {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function runTestUI() {
|
|
73
|
+
// Verifica se o build do frontend existe
|
|
74
|
+
const buildExists = await checkBuildExists();
|
|
75
|
+
|
|
76
|
+
if (!buildExists) {
|
|
77
|
+
console.log("⚠️ Frontend assets not found.\n");
|
|
78
|
+
console.log("If you are running from source, please run: bun run build");
|
|
79
|
+
console.log("If you installed via npm, this might be a packaging issue.\n");
|
|
80
|
+
process.exit(1);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
console.log("🚀 Starting Bun Test UI (Production Mode)...\n");
|
|
84
|
+
console.log("📡 WebSocket server: ws://localhost:5050/ws");
|
|
85
|
+
console.log("🌐 Frontend: http://localhost:5050\n");
|
|
86
|
+
console.log("Press Ctrl+C to stop\n");
|
|
87
|
+
|
|
88
|
+
// Roda o script do backend diretamente com Bun
|
|
89
|
+
// O usuário OBRIGATORIAMENTE tem Bun instalado para usar esta ferramenta
|
|
90
|
+
const runnerScript = join(__dirname, "ui-runner.ts");
|
|
91
|
+
|
|
92
|
+
const proc = spawn("bun", ["run", runnerScript], {
|
|
93
|
+
cwd: process.cwd(), // Roda no diretório atual do usuário
|
|
94
|
+
stdio: "inherit",
|
|
95
|
+
shell: false,
|
|
96
|
+
env: { ...process.env, NODE_ENV: "production" }
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
proc.on("close", (code) => {
|
|
100
|
+
if (code !== 0) {
|
|
101
|
+
console.error(`\n❌ Process exited with code ${code}`);
|
|
102
|
+
process.exit(code || 1);
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
proc.on("error", (err) => {
|
|
107
|
+
console.error("❌ Error starting test UI:", err);
|
|
108
|
+
process.exit(1);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// Handle Ctrl+C
|
|
112
|
+
process.on("SIGINT", () => {
|
|
113
|
+
console.log("\n\n👋 Stopping Bun Test UI...");
|
|
114
|
+
proc.kill("SIGINT");
|
|
115
|
+
process.exit(0);
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async function runDevMode() {
|
|
120
|
+
console.log("🚀 Starting Bun Test UI (Development Mode)...\n");
|
|
121
|
+
console.log("📡 WebSocket server: ws://localhost:5060");
|
|
122
|
+
console.log("🌐 Frontend: http://localhost:5050 (with hot reload)\n");
|
|
123
|
+
console.log("Press Ctrl+C to stop\n");
|
|
124
|
+
|
|
125
|
+
// Inicia o backend (ui-runner.ts) com bun run e flag de dev mode
|
|
126
|
+
const backendPath = join(__dirname, "ui-runner.ts");
|
|
127
|
+
const backendProc = spawn("bun", ["run", backendPath], {
|
|
128
|
+
cwd: process.cwd(),
|
|
129
|
+
stdio: "inherit",
|
|
130
|
+
shell: true,
|
|
131
|
+
env: { ...process.env, BUN_TEST_UI_DEV: "true" }
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// Aguarda um pouco para o backend iniciar
|
|
135
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
136
|
+
|
|
137
|
+
// Inicia o frontend em modo dev
|
|
138
|
+
const appDir = join(__dirname, "app");
|
|
139
|
+
const frontendProc = spawn("bun", ["run", "dev"], {
|
|
140
|
+
cwd: appDir,
|
|
141
|
+
stdio: "inherit",
|
|
142
|
+
shell: true
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
// Handle Ctrl+C
|
|
146
|
+
process.on("SIGINT", () => {
|
|
147
|
+
console.log("\n\n👋 Stopping Bun Test UI...");
|
|
148
|
+
backendProc.kill("SIGINT");
|
|
149
|
+
frontendProc.kill("SIGINT");
|
|
150
|
+
process.exit(0);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
// Se um processo terminar, termina o outro também
|
|
154
|
+
backendProc.on("close", (code) => {
|
|
155
|
+
console.log("\n❌ Backend stopped");
|
|
156
|
+
frontendProc.kill("SIGINT");
|
|
157
|
+
process.exit(code || 1);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
frontendProc.on("close", (code) => {
|
|
161
|
+
console.log("\n❌ Frontend stopped");
|
|
162
|
+
backendProc.kill("SIGINT");
|
|
163
|
+
process.exit(code || 1);
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Main
|
|
168
|
+
const command = process.argv[2];
|
|
169
|
+
|
|
170
|
+
switch (command) {
|
|
171
|
+
case "run":
|
|
172
|
+
runTestUI()
|
|
173
|
+
.catch((err) => {
|
|
174
|
+
console.error("❌ Failed to start:", err);
|
|
175
|
+
process.exit(1);
|
|
176
|
+
});
|
|
177
|
+
break;
|
|
178
|
+
|
|
179
|
+
case "dev":
|
|
180
|
+
runDevMode()
|
|
181
|
+
.catch((err) => {
|
|
182
|
+
console.error("❌ Failed to start dev mode:", err);
|
|
183
|
+
process.exit(1);
|
|
184
|
+
});
|
|
185
|
+
break;
|
|
186
|
+
|
|
187
|
+
case "help":
|
|
188
|
+
case undefined:
|
|
189
|
+
showHelp();
|
|
190
|
+
process.exit(0);
|
|
191
|
+
break;
|
|
192
|
+
|
|
193
|
+
default:
|
|
194
|
+
console.error(`❌ Unknown command: ${command}\n`);
|
|
195
|
+
showHelp();
|
|
196
|
+
process.exit(1);
|
|
197
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "bun-ui-tests",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "A beautiful UI for running Bun tests",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"buntestui": "cli.ts"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"cli.ts",
|
|
11
|
+
"ui-runner.ts",
|
|
12
|
+
"app/dist"
|
|
13
|
+
],
|
|
14
|
+
"scripts": {
|
|
15
|
+
"dev": "bun run ui-runner.ts",
|
|
16
|
+
"prepublishOnly": "cd app && bun install && bun run build"
|
|
17
|
+
},
|
|
18
|
+
"keywords": [
|
|
19
|
+
"bun",
|
|
20
|
+
"test",
|
|
21
|
+
"ui",
|
|
22
|
+
"testing",
|
|
23
|
+
"runner"
|
|
24
|
+
],
|
|
25
|
+
"author": "killd",
|
|
26
|
+
"license": "MIT"
|
|
27
|
+
}
|
package/ui-runner.ts
ADDED
|
@@ -0,0 +1,597 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* UI Runner for Bun Test
|
|
3
|
+
*
|
|
4
|
+
* Este runner NÃO usa APIs internas do bun:test (que não existem publicamente).
|
|
5
|
+
* Ele apenas spawna um processo `bun test` e faz parsing do stdout/stderr.
|
|
6
|
+
*
|
|
7
|
+
* Arquitetura:
|
|
8
|
+
* - Spawna `bun test` como processo filho
|
|
9
|
+
* - Captura stdout/stderr em tempo real
|
|
10
|
+
* - Faz parsing básico da saída (✓, ✗, nomes de testes)
|
|
11
|
+
* - Emite eventos via WebSocket para a UI
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { spawn } from "node:child_process";
|
|
15
|
+
import { readdir, readFile, stat } from "node:fs/promises";
|
|
16
|
+
import { join, relative, dirname } from "node:path";
|
|
17
|
+
import { fileURLToPath } from "node:url";
|
|
18
|
+
import { existsSync } from "node:fs";
|
|
19
|
+
|
|
20
|
+
// Determina o diretório do executável ou script
|
|
21
|
+
const getBaseDir = () => {
|
|
22
|
+
// Se rodando como executável compilado, pega o diretório onde o CLI foi instalado
|
|
23
|
+
// O executável fica na raiz do pacote, então app/dist fica relativo a ele
|
|
24
|
+
if (import.meta.path && !import.meta.path.endsWith('.ts')) {
|
|
25
|
+
// É um executável compilado - usa process.execPath (path real do executável)
|
|
26
|
+
// import.meta.path retorna um path virtual do bunfs
|
|
27
|
+
return dirname(process.execPath);
|
|
28
|
+
}
|
|
29
|
+
// Se rodando como script .ts em dev, usa o dirname do próprio arquivo
|
|
30
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
31
|
+
return dirname(__filename);
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const baseDir = getBaseDir();
|
|
35
|
+
const distPath = join(baseDir, "app", "dist");
|
|
36
|
+
const isDevMode = process.env.BUN_TEST_UI_DEV === "true";
|
|
37
|
+
|
|
38
|
+
// WebSocket Handler (lógica compartilhada)
|
|
39
|
+
const websocketHandler = {
|
|
40
|
+
async open(ws: any) {
|
|
41
|
+
console.log("✓ UI connected");
|
|
42
|
+
|
|
43
|
+
// Escaneia arquivos de teste
|
|
44
|
+
const testFiles = await scanTestFiles();
|
|
45
|
+
|
|
46
|
+
// Escaneia todos os testes de cada arquivo
|
|
47
|
+
console.log("📖 Reading test files to extract test names...");
|
|
48
|
+
const testsMap = await scanAllTests();
|
|
49
|
+
|
|
50
|
+
// Envia evento de conexão com lista de arquivos e testes
|
|
51
|
+
ws.send(JSON.stringify({
|
|
52
|
+
type: "connected",
|
|
53
|
+
payload: {
|
|
54
|
+
message: "Runner ready",
|
|
55
|
+
testFiles,
|
|
56
|
+
testsMap // { "test/example.test.ts": ["test1", "test2", ...] }
|
|
57
|
+
}
|
|
58
|
+
}));
|
|
59
|
+
|
|
60
|
+
console.log(`✓ Found ${testFiles.length} test files with ${Object.values(testsMap).flat().length} tests total`);
|
|
61
|
+
},
|
|
62
|
+
message(ws: any, message: any) {
|
|
63
|
+
try {
|
|
64
|
+
const data = JSON.parse(message.toString());
|
|
65
|
+
console.log('📨 [WEBSOCKET] Mensagem recebida:', data.type, data.payload);
|
|
66
|
+
|
|
67
|
+
// Processa comandos da UI
|
|
68
|
+
if (data.type === "run:request") {
|
|
69
|
+
const file = data.payload?.file;
|
|
70
|
+
const testName = data.payload?.testName;
|
|
71
|
+
|
|
72
|
+
console.log('▶️ [RUN REQUEST] file:', file, 'testName:', testName);
|
|
73
|
+
|
|
74
|
+
if (testName) {
|
|
75
|
+
console.log(`Running specific test: ${testName} in ${file}`);
|
|
76
|
+
} else if (file) {
|
|
77
|
+
console.log(`Running file: ${file}`);
|
|
78
|
+
} else {
|
|
79
|
+
console.log("Running all tests");
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
runTests(ws, file, testName);
|
|
83
|
+
} else {
|
|
84
|
+
console.log('⚠️ [WEBSOCKET] Tipo desconhecido:', data.type);
|
|
85
|
+
}
|
|
86
|
+
} catch (err) {
|
|
87
|
+
console.error("Error processing message:", err);
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
close(ws: any) {
|
|
91
|
+
console.log("✗ UI disconnected");
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
if (isDevMode) {
|
|
96
|
+
// === MODO DESENVOLVIMENTO ===
|
|
97
|
+
// Frontend roda via Vite na porta 5050
|
|
98
|
+
// Backend roda separadamente na porta 5060 (apenas WS)
|
|
99
|
+
|
|
100
|
+
Bun.serve({
|
|
101
|
+
port: 5060,
|
|
102
|
+
fetch(req, server) {
|
|
103
|
+
// Aceita upgrade em qualquer path ou especificamente /ws
|
|
104
|
+
if (server.upgrade(req)) {
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
return new Response("Bun Test UI Backend (Dev Mode)", { status: 200 });
|
|
108
|
+
},
|
|
109
|
+
websocket: websocketHandler
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
console.log("📡 WebSocket server running on ws://localhost:5060");
|
|
113
|
+
|
|
114
|
+
} else {
|
|
115
|
+
// === MODO PRODUÇÃO ===
|
|
116
|
+
// Servidor ÚNICO na porta 5050
|
|
117
|
+
// Serve arquivos estáticos do frontend E WebSocket no mesmo endpoint
|
|
118
|
+
|
|
119
|
+
const PORT = 5050;
|
|
120
|
+
|
|
121
|
+
Bun.serve({
|
|
122
|
+
port: PORT,
|
|
123
|
+
async fetch(req, server) {
|
|
124
|
+
const url = new URL(req.url);
|
|
125
|
+
|
|
126
|
+
// 1. WebSocket Upgrade (/ws)
|
|
127
|
+
if (url.pathname === "/ws") {
|
|
128
|
+
if (server.upgrade(req)) {
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
return new Response("WebSocket upgrade failed", { status: 400 });
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// 2. Arquivos Estáticos (Frontend)
|
|
135
|
+
if (existsSync(distPath)) {
|
|
136
|
+
let filePath = url.pathname === "/" ? "/index.html" : url.pathname;
|
|
137
|
+
const fullPath = join(distPath, filePath);
|
|
138
|
+
|
|
139
|
+
try {
|
|
140
|
+
const file = Bun.file(fullPath);
|
|
141
|
+
if (await file.exists()) {
|
|
142
|
+
return new Response(file);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Se não encontrou o arquivo e não é um asset (SPA fallback)
|
|
146
|
+
if (!filePath.includes(".")) {
|
|
147
|
+
const indexFile = Bun.file(join(distPath, "index.html"));
|
|
148
|
+
return new Response(indexFile);
|
|
149
|
+
}
|
|
150
|
+
} catch (err) {
|
|
151
|
+
console.error("Error serving file:", err);
|
|
152
|
+
}
|
|
153
|
+
} else {
|
|
154
|
+
return new Response("Frontend build not found. Run 'buntestui build' first.", { status: 404 });
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return new Response("Not found", { status: 404 });
|
|
158
|
+
},
|
|
159
|
+
websocket: websocketHandler
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
console.log(`🚀 Server running on http://localhost:${PORT}`);
|
|
163
|
+
console.log(`📡 WebSocket endpoint available at ws://localhost:${PORT}/ws`);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Escaneia recursivamente todo o projeto e retorna lista de arquivos de teste
|
|
169
|
+
* Suporta os padrões do Bun: .test.ts, .test.js, .test.tsx, .test.jsx, _test.ts, _test.js, etc
|
|
170
|
+
*/
|
|
171
|
+
async function scanTestFiles(): Promise<string[]> {
|
|
172
|
+
const testFiles: string[] = [];
|
|
173
|
+
const rootDir = process.cwd();
|
|
174
|
+
|
|
175
|
+
// Pastas a ignorar durante a busca recursiva
|
|
176
|
+
const ignoreDirs = new Set([
|
|
177
|
+
'node_modules',
|
|
178
|
+
'.git',
|
|
179
|
+
'dist',
|
|
180
|
+
'build',
|
|
181
|
+
'.next',
|
|
182
|
+
'.svelte-kit',
|
|
183
|
+
'coverage',
|
|
184
|
+
'.turbo',
|
|
185
|
+
'.cache',
|
|
186
|
+
'out',
|
|
187
|
+
'.vercel',
|
|
188
|
+
'.netlify'
|
|
189
|
+
]);
|
|
190
|
+
|
|
191
|
+
// Padrões de arquivos de teste suportados pelo Bun
|
|
192
|
+
const testPatterns = [
|
|
193
|
+
/\.test\.(ts|tsx|js|jsx)$/, // file.test.ts, file.test.js, etc
|
|
194
|
+
/\.spec\.(ts|tsx|js|jsx)$/, // file.spec.ts, file.spec.js, etc
|
|
195
|
+
/_test\.(ts|tsx|js|jsx)$/, // file_test.ts, file_test.js, etc
|
|
196
|
+
/_spec\.(ts|tsx|js|jsx)$/, // file_spec.ts, file_spec.js, etc
|
|
197
|
+
];
|
|
198
|
+
|
|
199
|
+
async function scanDirectory(dir: string): Promise<void> {
|
|
200
|
+
try {
|
|
201
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
202
|
+
|
|
203
|
+
for (const entry of entries) {
|
|
204
|
+
const fullPath = join(dir, entry.name);
|
|
205
|
+
|
|
206
|
+
if (entry.isDirectory()) {
|
|
207
|
+
// Ignora diretórios na lista de ignorados
|
|
208
|
+
if (!ignoreDirs.has(entry.name)) {
|
|
209
|
+
await scanDirectory(fullPath);
|
|
210
|
+
}
|
|
211
|
+
} else if (entry.isFile()) {
|
|
212
|
+
// Verifica se o arquivo corresponde a algum padrão de teste
|
|
213
|
+
if (testPatterns.some(pattern => pattern.test(entry.name))) {
|
|
214
|
+
// Retorna path relativo ao diretório raiz
|
|
215
|
+
const relativePath = relative(rootDir, fullPath);
|
|
216
|
+
testFiles.push(relativePath);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
} catch (err) {
|
|
221
|
+
// Ignora erros de leitura de diretórios (permissões, etc)
|
|
222
|
+
console.error(`Error scanning directory ${dir}:`, err);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
await scanDirectory(rootDir);
|
|
227
|
+
|
|
228
|
+
// Ordena os arquivos alfabeticamente
|
|
229
|
+
testFiles.sort();
|
|
230
|
+
|
|
231
|
+
return testFiles;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Extrai os nomes dos testes de um arquivo sem executá-lo
|
|
236
|
+
* Faz parsing do código para encontrar test(), it() e describe()
|
|
237
|
+
*/
|
|
238
|
+
async function extractTestsFromFile(filePath: string): Promise<string[]> {
|
|
239
|
+
try {
|
|
240
|
+
const fullPath = join(process.cwd(), filePath);
|
|
241
|
+
const content = await readFile(fullPath, "utf-8");
|
|
242
|
+
const tests: string[] = [];
|
|
243
|
+
|
|
244
|
+
// Regex para capturar test("nome"), it("nome"), describe("nome")
|
|
245
|
+
// Suporta aspas simples e duplas
|
|
246
|
+
const testRegex = /(?:test|it|describe)\s*\(\s*[`"'](.+?)[`"']/g;
|
|
247
|
+
|
|
248
|
+
let match;
|
|
249
|
+
while ((match = testRegex.exec(content)) !== null) {
|
|
250
|
+
tests.push(match[1]);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return tests;
|
|
254
|
+
} catch (err) {
|
|
255
|
+
console.error(`Error reading file ${filePath}:`, err);
|
|
256
|
+
return [];
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Escaneia todos os arquivos e retorna um mapa de arquivo -> testes
|
|
262
|
+
*/
|
|
263
|
+
async function scanAllTests(): Promise<Record<string, string[]>> {
|
|
264
|
+
const testFiles = await scanTestFiles();
|
|
265
|
+
const testsMap: Record<string, string[]> = {};
|
|
266
|
+
|
|
267
|
+
for (const file of testFiles) {
|
|
268
|
+
const tests = await extractTestsFromFile(file);
|
|
269
|
+
testsMap[file] = tests;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
return testsMap;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Executa bun test e faz parsing da saída
|
|
277
|
+
* @param ws WebSocket connection
|
|
278
|
+
* @param file Arquivo específico para testar (opcional)
|
|
279
|
+
* @param testName Nome do teste específico (opcional, requer file)
|
|
280
|
+
*/
|
|
281
|
+
function runTests(ws: any, file?: string, testName?: string) {
|
|
282
|
+
const args = ["test"];
|
|
283
|
+
|
|
284
|
+
if (file) {
|
|
285
|
+
args.push(file);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Bun suporta --test-name-pattern para filtrar testes
|
|
289
|
+
// Escapa caracteres especiais de regex e usa o nome exato
|
|
290
|
+
if (testName) {
|
|
291
|
+
// Escapa caracteres especiais de regex
|
|
292
|
+
const escapedName = testName.replace(/[.*+?^${}()|[\\]/g, '\\$&');
|
|
293
|
+
args.push("--test-name-pattern", escapedName);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
console.log('🚀 [RUN] Comando:', `bun ${args.join(" ")}`);
|
|
297
|
+
console.log(`Starting bun test ${args.slice(1).join(" ") || "(all)"}...`);
|
|
298
|
+
|
|
299
|
+
// Emite evento de início
|
|
300
|
+
ws.send(JSON.stringify({
|
|
301
|
+
type: "run:start",
|
|
302
|
+
payload: {
|
|
303
|
+
timestamp: Date.now(),
|
|
304
|
+
file: file || null,
|
|
305
|
+
testName: testName || null
|
|
306
|
+
}
|
|
307
|
+
}));
|
|
308
|
+
|
|
309
|
+
// Spawna o processo bun test
|
|
310
|
+
// IMPORTANTE: Não usamos nenhuma API interna do bun:test
|
|
311
|
+
const bunTest = spawn("bun", args, {
|
|
312
|
+
cwd: process.cwd(),
|
|
313
|
+
env: { ...process.env, FORCE_COLOR: "0" }, // Desabilita cores para facilitar parsing
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
let currentTestFile = "";
|
|
317
|
+
let buffer = "";
|
|
318
|
+
let errorBlock: string[] = []; // Acumula linhas de erro
|
|
319
|
+
let summaryBlock: string[] = []; // Acumula linhas de resumo
|
|
320
|
+
let inErrorBlock = false;
|
|
321
|
+
let inSummaryBlock = false;
|
|
322
|
+
|
|
323
|
+
// Função para verificar se é linha de bloco de erro
|
|
324
|
+
const isErrorLine = (line: string): boolean => {
|
|
325
|
+
const trimmed = line.trim();
|
|
326
|
+
// Linha que é só espaços seguidos de ^ (indicador de erro)
|
|
327
|
+
const hasErrorPointer = /^\s*\^/.test(line);
|
|
328
|
+
|
|
329
|
+
return (
|
|
330
|
+
/^\d+\s*\|/.test(trimmed) || // Linha de código: "72 |"
|
|
331
|
+
trimmed.startsWith('error:') || // "error: expect..."
|
|
332
|
+
trimmed.startsWith('Expected:') || // "Expected: 3"
|
|
333
|
+
trimmed.startsWith('Received:') || // "Received: 2"
|
|
334
|
+
trimmed.includes('at <anonymous>') || // Stack trace
|
|
335
|
+
hasErrorPointer || // Seta de erro " ^"
|
|
336
|
+
/^\s+at\s/.test(line) || // " at ..." stack trace
|
|
337
|
+
trimmed.startsWith('(fail)') // Linha de fail - FAZ PARTE DO BLOCO!
|
|
338
|
+
);
|
|
339
|
+
};
|
|
340
|
+
|
|
341
|
+
// Função para verificar se é linha de resumo
|
|
342
|
+
const isSummaryLine = (line: string): boolean => {
|
|
343
|
+
const trimmed = line.trim();
|
|
344
|
+
return (
|
|
345
|
+
/^\d+\s+pass$/.test(trimmed) || // "19 pass"
|
|
346
|
+
/^\d+\s+fail$/.test(trimmed) || // "1 fail"
|
|
347
|
+
/^\d+\s+expect\(\)\s+calls/.test(trimmed) || // "33 expect() calls"
|
|
348
|
+
/^Ran\s+\d+/.test(trimmed) // "Ran 20 tests..."
|
|
349
|
+
);
|
|
350
|
+
};
|
|
351
|
+
|
|
352
|
+
// Função para enviar bloco de erro
|
|
353
|
+
const flushErrorBlock = () => {
|
|
354
|
+
if (errorBlock.length > 0) {
|
|
355
|
+
console.log('📦 [ERROR BLOCK] Enviando bloco com', errorBlock.length, 'linhas:');
|
|
356
|
+
console.log('---START---');
|
|
357
|
+
console.log(errorBlock.join('\n'));
|
|
358
|
+
console.log('---END---');
|
|
359
|
+
|
|
360
|
+
ws.send(JSON.stringify({
|
|
361
|
+
type: "log",
|
|
362
|
+
payload: { message: errorBlock.join('\n'), stream: "stdout" }
|
|
363
|
+
}));
|
|
364
|
+
errorBlock = [];
|
|
365
|
+
inErrorBlock = false;
|
|
366
|
+
}
|
|
367
|
+
};
|
|
368
|
+
|
|
369
|
+
// Função para enviar bloco de resumo
|
|
370
|
+
const flushSummaryBlock = () => {
|
|
371
|
+
if (summaryBlock.length > 0) {
|
|
372
|
+
console.log('📊 [SUMMARY BLOCK] Enviando bloco com', summaryBlock.length, 'linhas:');
|
|
373
|
+
console.log('---START---');
|
|
374
|
+
console.log(summaryBlock.join('\n'));
|
|
375
|
+
console.log('---END---');
|
|
376
|
+
|
|
377
|
+
ws.send(JSON.stringify({
|
|
378
|
+
type: "log",
|
|
379
|
+
payload: { message: summaryBlock.join('\n'), stream: "stdout" }
|
|
380
|
+
}));
|
|
381
|
+
summaryBlock = [];
|
|
382
|
+
inSummaryBlock = false;
|
|
383
|
+
}
|
|
384
|
+
};
|
|
385
|
+
|
|
386
|
+
// Processa stdout linha por linha
|
|
387
|
+
bunTest.stdout.on("data", (data) => {
|
|
388
|
+
const text = data.toString();
|
|
389
|
+
buffer += text;
|
|
390
|
+
|
|
391
|
+
// Processa linhas completas
|
|
392
|
+
const lines = buffer.split("\n");
|
|
393
|
+
buffer = lines.pop() || ""; // Guarda última linha incompleta
|
|
394
|
+
|
|
395
|
+
for (const line of lines) {
|
|
396
|
+
currentTestFile = processLine(line, ws, currentTestFile);
|
|
397
|
+
|
|
398
|
+
// Linhas vazias podem fazer parte de blocos
|
|
399
|
+
const trimmed = line.trim();
|
|
400
|
+
|
|
401
|
+
// Verifica se é linha de erro
|
|
402
|
+
if (isErrorLine(line)) {
|
|
403
|
+
console.log('🔴 [ERROR LINE] Detectado:', line.substring(0, 50));
|
|
404
|
+
// Se estava no resumo, envia o resumo primeiro
|
|
405
|
+
flushSummaryBlock();
|
|
406
|
+
|
|
407
|
+
inErrorBlock = true;
|
|
408
|
+
errorBlock.push(line);
|
|
409
|
+
continue;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// Linha vazia dentro de bloco de erro - mantém no bloco
|
|
413
|
+
if (inErrorBlock && !trimmed) {
|
|
414
|
+
console.log('🔴 [ERROR EMPTY] Linha vazia no bloco de erro');
|
|
415
|
+
errorBlock.push(line);
|
|
416
|
+
continue;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// Verifica se é linha de resumo
|
|
420
|
+
if (isSummaryLine(line)) {
|
|
421
|
+
console.log('📊 [SUMMARY LINE] Detectado:', line.substring(0, 50));
|
|
422
|
+
// Se estava no erro, envia o erro primeiro
|
|
423
|
+
flushErrorBlock();
|
|
424
|
+
|
|
425
|
+
inSummaryBlock = true;
|
|
426
|
+
summaryBlock.push(line);
|
|
427
|
+
continue;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// Linha normal - envia blocos pendentes e depois a linha
|
|
431
|
+
flushErrorBlock();
|
|
432
|
+
flushSummaryBlock();
|
|
433
|
+
|
|
434
|
+
// Envia linhas normais separadamente (se não vazia)
|
|
435
|
+
if (trimmed) {
|
|
436
|
+
console.log('📝 [NORMAL LINE] Enviando:', line.substring(0, 50));
|
|
437
|
+
ws.send(JSON.stringify({
|
|
438
|
+
type: "log",
|
|
439
|
+
payload: { message: line, stream: "stdout" }
|
|
440
|
+
}));
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
bunTest.stderr.on("data", (data) => {
|
|
446
|
+
const text = data.toString();
|
|
447
|
+
|
|
448
|
+
// Envia stderr linha por linha também
|
|
449
|
+
const lines = text.split("\n").filter(line => line.trim());
|
|
450
|
+
for (const line of lines) {
|
|
451
|
+
ws.send(JSON.stringify({
|
|
452
|
+
type: "log",
|
|
453
|
+
payload: { message: line, stream: "stderr" }
|
|
454
|
+
}));
|
|
455
|
+
}
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
bunTest.on("close", (code) => {
|
|
459
|
+
console.log(`bun test exited with code ${code}`);
|
|
460
|
+
|
|
461
|
+
// Processa buffer pendente antes de finalizar
|
|
462
|
+
if (buffer.trim()) {
|
|
463
|
+
console.log('⚠️ [CLOSE] Buffer pendente:', buffer.substring(0, 100));
|
|
464
|
+
const lines = buffer.split("\n");
|
|
465
|
+
for (const line of lines) {
|
|
466
|
+
if (line.trim()) {
|
|
467
|
+
currentTestFile = processLine(line, ws, currentTestFile);
|
|
468
|
+
|
|
469
|
+
const trimmed = line.trim();
|
|
470
|
+
|
|
471
|
+
if (isErrorLine(line)) {
|
|
472
|
+
inErrorBlock = true;
|
|
473
|
+
errorBlock.push(line);
|
|
474
|
+
} else if (inErrorBlock && !trimmed) {
|
|
475
|
+
errorBlock.push(line);
|
|
476
|
+
} else if (isSummaryLine(line)) {
|
|
477
|
+
flushErrorBlock();
|
|
478
|
+
inSummaryBlock = true;
|
|
479
|
+
summaryBlock.push(line);
|
|
480
|
+
} else {
|
|
481
|
+
flushErrorBlock();
|
|
482
|
+
flushSummaryBlock();
|
|
483
|
+
ws.send(JSON.stringify({
|
|
484
|
+
type: "log",
|
|
485
|
+
payload: { message: line, stream: "stdout" }
|
|
486
|
+
}));
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// Envia blocos pendentes antes de finalizar
|
|
493
|
+
flushErrorBlock();
|
|
494
|
+
flushSummaryBlock();
|
|
495
|
+
|
|
496
|
+
console.log('✅ [CLOSE] Processo finalizado, todos os logs enviados');
|
|
497
|
+
|
|
498
|
+
ws.send(JSON.stringify({
|
|
499
|
+
type: "run:complete",
|
|
500
|
+
payload: {
|
|
501
|
+
exitCode: code,
|
|
502
|
+
timestamp: Date.now()
|
|
503
|
+
}
|
|
504
|
+
}));
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
bunTest.on("error", (error) => {
|
|
508
|
+
console.error("Error running bun test:", error);
|
|
509
|
+
|
|
510
|
+
ws.send(JSON.stringify({
|
|
511
|
+
type: "error",
|
|
512
|
+
payload: { message: error.message }
|
|
513
|
+
}));
|
|
514
|
+
});
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
/**
|
|
518
|
+
* Faz parsing de uma linha de saída do bun test
|
|
519
|
+
*
|
|
520
|
+
* Formatos esperados (baseado na saída real do bun test):
|
|
521
|
+
* - ✓ test name [0.00ms]
|
|
522
|
+
* - ✗ test name [0.00ms]
|
|
523
|
+
* - test/file.test.ts:
|
|
524
|
+
*
|
|
525
|
+
* @returns O currentTestFile atualizado
|
|
526
|
+
*/
|
|
527
|
+
function processLine(line: string, ws: any, currentTestFile: string): string {
|
|
528
|
+
const trimmed = line.trim();
|
|
529
|
+
|
|
530
|
+
if (!trimmed) return currentTestFile;
|
|
531
|
+
|
|
532
|
+
// Detecta arquivo de teste (com suporte a todos os padrões)
|
|
533
|
+
if (trimmed.match(/\.(test|spec)\.(ts|js|tsx|jsx):/) || trimmed.match(/_(test|spec)\.(ts|js|tsx|jsx):/)) {
|
|
534
|
+
const filePath = trimmed.replace(":", "");
|
|
535
|
+
|
|
536
|
+
// Envia quebra de linha antes do nome do arquivo para separar visualmente
|
|
537
|
+
ws.send(JSON.stringify({
|
|
538
|
+
type: "log",
|
|
539
|
+
payload: { message: "", stream: "stdout" }
|
|
540
|
+
}));
|
|
541
|
+
|
|
542
|
+
ws.send(JSON.stringify({
|
|
543
|
+
type: "file:start",
|
|
544
|
+
payload: { filePath }
|
|
545
|
+
}));
|
|
546
|
+
return filePath; // Atualiza o arquivo atual
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
// Detecta teste que passou - formato: (pass) test name [0.00ms]
|
|
550
|
+
const passMatch = trimmed.match(/^\(pass\)\s+(.+?)\s+\[(.+?)\]/);
|
|
551
|
+
if (passMatch) {
|
|
552
|
+
const [, testName, duration] = passMatch;
|
|
553
|
+
|
|
554
|
+
ws.send(JSON.stringify({
|
|
555
|
+
type: "test:pass",
|
|
556
|
+
payload: {
|
|
557
|
+
testName: testName.trim(),
|
|
558
|
+
duration: duration.trim(),
|
|
559
|
+
timestamp: Date.now(),
|
|
560
|
+
filePath: currentTestFile
|
|
561
|
+
}
|
|
562
|
+
}));
|
|
563
|
+
return currentTestFile;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
// Detecta teste que falhou - formato: (fail) test name [0.00ms]
|
|
567
|
+
const failMatch = trimmed.match(/^\(fail\)\s+(.+?)(?:\s+\[(.+?)\])?/);
|
|
568
|
+
if (failMatch) {
|
|
569
|
+
const [, testName, duration] = failMatch;
|
|
570
|
+
|
|
571
|
+
ws.send(JSON.stringify({
|
|
572
|
+
type: "test:fail",
|
|
573
|
+
payload: {
|
|
574
|
+
testName: testName.trim(),
|
|
575
|
+
duration: duration?.trim() || "N/A",
|
|
576
|
+
timestamp: Date.now(),
|
|
577
|
+
filePath: currentTestFile
|
|
578
|
+
}
|
|
579
|
+
}));
|
|
580
|
+
return currentTestFile;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
// Detecta início de teste (quando aparece sem ✓ ou ✗)
|
|
584
|
+
// Isso pode variar dependendo da versão do Bun
|
|
585
|
+
if (trimmed.match(/^(test|it|describe)\s/)) {
|
|
586
|
+
ws.send(JSON.stringify({
|
|
587
|
+
type: "test:start",
|
|
588
|
+
payload: {
|
|
589
|
+
testName: trimmed,
|
|
590
|
+
timestamp: Date.now(),
|
|
591
|
+
filePath: currentTestFile
|
|
592
|
+
}
|
|
593
|
+
}));
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
return currentTestFile;
|
|
597
|
+
}
|