@brunosps00/dev-workflow 0.0.3 → 0.0.5
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 +42 -42
- package/bin/dev-workflow.js +1 -1
- package/lib/constants.js +42 -40
- package/lib/init.js +40 -10
- package/package.json +1 -1
- package/scaffold/en/commands/{analyze-project.md → dw-analyze-project.md} +69 -40
- package/scaffold/en/commands/{brainstorm.md → dw-brainstorm.md} +31 -4
- package/scaffold/en/commands/{bugfix.md → dw-bugfix.md} +63 -19
- package/scaffold/en/commands/{code-review.md → dw-code-review.md} +38 -15
- package/scaffold/en/commands/{commit.md → dw-commit.md} +25 -0
- package/scaffold/en/commands/{create-prd.md → dw-create-prd.md} +24 -10
- package/scaffold/en/commands/{create-tasks.md → dw-create-tasks.md} +11 -4
- package/scaffold/en/commands/{create-techspec.md → dw-create-techspec.md} +38 -11
- package/scaffold/en/commands/{deep-research.md → dw-deep-research.md} +18 -17
- package/scaffold/en/commands/{fix-qa.md → dw-fix-qa.md} +20 -3
- package/scaffold/en/commands/dw-functional-doc.md +276 -0
- package/scaffold/en/commands/{generate-pr.md → dw-generate-pr.md} +20 -5
- package/scaffold/en/commands/dw-help.md +309 -0
- package/scaffold/en/commands/{refactoring-analysis.md → dw-refactoring-analysis.md} +50 -26
- package/scaffold/en/commands/{review-implementation.md → dw-review-implementation.md} +25 -6
- package/scaffold/en/commands/{run-plan.md → dw-run-plan.md} +21 -6
- package/scaffold/en/commands/{run-qa.md → dw-run-qa.md} +32 -13
- package/scaffold/en/commands/{run-task.md → dw-run-task.md} +17 -7
- package/scaffold/en/references/playwright-patterns.md +136 -0
- package/scaffold/en/references/refactoring-catalog.md +167 -0
- package/scaffold/en/templates/brainstorm-matrix.md +44 -0
- package/scaffold/en/templates/functional-doc/case-matrix.md +5 -0
- package/scaffold/en/templates/functional-doc/e2e-runbook.md +3 -0
- package/scaffold/en/templates/functional-doc/features.md +3 -0
- package/scaffold/en/templates/functional-doc/overview.md +21 -0
- package/scaffold/en/templates/functional-doc/playwright.spec.ts.tpl +19 -0
- package/scaffold/en/templates/pr-bugfix-template.md +28 -0
- package/scaffold/en/templates/qa-test-credentials.md +37 -0
- package/scaffold/en/templates/tasks-template.md +1 -1
- package/scaffold/en/templates/techspec-template.md +1 -1
- package/scaffold/pt-br/commands/{analyze-project.md → dw-analyze-project.md} +91 -41
- package/scaffold/pt-br/commands/{brainstorm.md → dw-brainstorm.md} +32 -5
- package/scaffold/pt-br/commands/{bugfix.md → dw-bugfix.md} +70 -13
- package/scaffold/pt-br/commands/{code-review.md → dw-code-review.md} +78 -15
- package/scaffold/pt-br/commands/{commit.md → dw-commit.md} +45 -1
- package/scaffold/pt-br/commands/{create-prd.md → dw-create-prd.md} +25 -10
- package/scaffold/pt-br/commands/{create-tasks.md → dw-create-tasks.md} +20 -13
- package/scaffold/pt-br/commands/{create-techspec.md → dw-create-techspec.md} +40 -13
- package/scaffold/pt-br/commands/{deep-research.md → dw-deep-research.md} +19 -11
- package/scaffold/pt-br/commands/{fix-qa.md → dw-fix-qa.md} +30 -1
- package/scaffold/pt-br/commands/dw-functional-doc.md +276 -0
- package/scaffold/pt-br/commands/{generate-pr.md → dw-generate-pr.md} +58 -3
- package/scaffold/pt-br/commands/{help.md → dw-help.md} +81 -59
- package/scaffold/pt-br/commands/{refactoring-analysis.md → dw-refactoring-analysis.md} +49 -25
- package/scaffold/pt-br/commands/{review-implementation.md → dw-review-implementation.md} +50 -2
- package/scaffold/pt-br/commands/{run-plan.md → dw-run-plan.md} +98 -10
- package/scaffold/pt-br/commands/{run-qa.md → dw-run-qa.md} +93 -18
- package/scaffold/pt-br/commands/{run-task.md → dw-run-task.md} +32 -7
- package/scaffold/pt-br/references/playwright-patterns.md +133 -0
- package/scaffold/pt-br/references/refactoring-catalog.md +166 -0
- package/scaffold/pt-br/templates/brainstorm-matrix.md +44 -0
- package/scaffold/pt-br/templates/functional-doc/case-matrix.md +5 -0
- package/scaffold/pt-br/templates/functional-doc/e2e-runbook.md +3 -0
- package/scaffold/pt-br/templates/functional-doc/features.md +3 -0
- package/scaffold/pt-br/templates/functional-doc/overview.md +21 -0
- package/scaffold/pt-br/templates/functional-doc/playwright.spec.ts.tpl +19 -0
- package/scaffold/pt-br/templates/pr-bugfix-template.md +28 -0
- package/scaffold/pt-br/templates/qa-test-credentials.md +37 -0
- package/scaffold/pt-br/templates/techspec-template.md +1 -1
- package/scaffold/rules-readme.md +3 -3
- package/scaffold/scripts/functional-doc/generate-dossier.mjs +821 -0
- package/scaffold/scripts/functional-doc/run-playwright-flow.mjs +275 -0
- package/scaffold/en/commands/help.md +0 -289
|
@@ -0,0 +1,821 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { execFileSync } from "node:child_process";
|
|
6
|
+
|
|
7
|
+
const cwd = process.cwd();
|
|
8
|
+
const workspaceRoot = cwd;
|
|
9
|
+
|
|
10
|
+
const STRINGS = {
|
|
11
|
+
en: {
|
|
12
|
+
routeAccess: (route) => `Route access ${route}`,
|
|
13
|
+
routeAccessible: (route) => `The target screen should be accessible at \`${route}\`.`,
|
|
14
|
+
routeValidateLoad: "Validate initial load, basic navigation, and visual flow identification.",
|
|
15
|
+
happyRouteLoads: "Route loads with valid context",
|
|
16
|
+
happyNavigate: "Navigate to the route",
|
|
17
|
+
happyAccessible: "Screen accessible",
|
|
18
|
+
happyNoError: "No error message",
|
|
19
|
+
edgeNoData: "Route accessed without sufficient data",
|
|
20
|
+
edgeOpenEmpty: "Open route in empty state",
|
|
21
|
+
edgeEmptyState: "Empty state documented",
|
|
22
|
+
edgeEmptyMsg: "Empty state message or equivalent",
|
|
23
|
+
errorLoadFail: "Load or API failure",
|
|
24
|
+
errorSimulate: "Simulate unavailability when possible",
|
|
25
|
+
errorHandled: "Error handled without crash",
|
|
26
|
+
errorReadable: "Readable error message",
|
|
27
|
+
permNoAccess: "User without permission or compatible context",
|
|
28
|
+
permRestricted: "Access route with restricted profile",
|
|
29
|
+
permBlocked: "Consistent blocking",
|
|
30
|
+
permDenied: "Access denied or action unavailable message",
|
|
31
|
+
derivedFrom: (filePath) => `Derived from file \`${filePath}\`.`,
|
|
32
|
+
reviewBehavior: "Review implemented behavior, visible messages, and related permissions.",
|
|
33
|
+
happyValidPre: "Valid preconditions",
|
|
34
|
+
happyExecute: "Execute the main action",
|
|
35
|
+
happyCompleted: "Action completed",
|
|
36
|
+
happySuccessMsg: "Success message or state change",
|
|
37
|
+
edgeLimitData: "Boundary data or alternative state",
|
|
38
|
+
edgeExecuteVar: "Execute with relevant variation",
|
|
39
|
+
edgeNoRegression: "No regression",
|
|
40
|
+
edgeContextMsg: "Contextual message",
|
|
41
|
+
errorInvalidInput: "Invalid input or operational failure",
|
|
42
|
+
errorForce: "Force applicable error",
|
|
43
|
+
errorHandledLabel: "Error handled",
|
|
44
|
+
errorExplicitMsg: "Explicit message to user",
|
|
45
|
+
permUnauthorized: "Profile without authorization for the action",
|
|
46
|
+
permExecuteRestricted: "Execute the flow with restricted permission",
|
|
47
|
+
permBlockedHidden: "Action blocked or hidden",
|
|
48
|
+
permBlockMsg: "Blocking message or controlled absence of action",
|
|
49
|
+
enterFlow: "Enter the related flow.",
|
|
50
|
+
executeMain: "Execute the main action.",
|
|
51
|
+
executeEdge: "Execute an edge variation.",
|
|
52
|
+
executeError: "Execute an error or blocking scenario.",
|
|
53
|
+
accessRoute: (route) => `Access \`${route}\`.`,
|
|
54
|
+
confirmScreen: "Confirm that the main screen was displayed.",
|
|
55
|
+
recordAlternative: "Record alternative states observed.",
|
|
56
|
+
interactionWith: (component) => `Interaction with ${component}`,
|
|
57
|
+
noSourcesFound: "- No correlated sources found automatically.",
|
|
58
|
+
noPlaywright: "- Project without `playwright.config.*` detected. E2E execution marked as blocked until runner is configured.",
|
|
59
|
+
noEnvCredentials: "- Credentials and environment configuration not validated automatically. Check variables before execution.",
|
|
60
|
+
noBlockers: "- No structural blockers detected in static generation.",
|
|
61
|
+
initialDossier: (route, project) => `Initial dossier generated for \`${route}\`, based on static discovery of project \`${project}\`.`,
|
|
62
|
+
playwrightDetected: "detected",
|
|
63
|
+
playwrightNotDetected: "not detected",
|
|
64
|
+
mandatoryCases: "Mandatory cases",
|
|
65
|
+
validateInitialLoad: "Validate initial load",
|
|
66
|
+
recordEdgeCase: "Record expected edge case",
|
|
67
|
+
recordErrorCase: "Record error or blocking case",
|
|
68
|
+
flowTest: (route) => `flow ${route}`,
|
|
69
|
+
openTargetRoute: "Open target route",
|
|
70
|
+
recordFinalContext: "Record final context",
|
|
71
|
+
},
|
|
72
|
+
"pt-br": {
|
|
73
|
+
routeAccess: (route) => `Acesso à rota ${route}`,
|
|
74
|
+
routeAccessible: (route) => `A tela alvo deve estar acessível em \`${route}\`.`,
|
|
75
|
+
routeValidateLoad: "Validar carregamento inicial, navegação básica e identificação visual do fluxo.",
|
|
76
|
+
happyRouteLoads: "Rota carrega com contexto válido",
|
|
77
|
+
happyNavigate: "Navegar para a rota",
|
|
78
|
+
happyAccessible: "Tela acessível",
|
|
79
|
+
happyNoError: "Sem mensagem de erro",
|
|
80
|
+
edgeNoData: "Rota acessada sem dados suficientes",
|
|
81
|
+
edgeOpenEmpty: "Abrir rota em estado vazio",
|
|
82
|
+
edgeEmptyState: "Estado vazio documentado",
|
|
83
|
+
edgeEmptyMsg: "Mensagem de estado vazio ou equivalente",
|
|
84
|
+
errorLoadFail: "Falha de carregamento ou API",
|
|
85
|
+
errorSimulate: "Simular indisponibilidade quando possível",
|
|
86
|
+
errorHandled: "Erro tratado sem quebra",
|
|
87
|
+
errorReadable: "Mensagem de erro legível",
|
|
88
|
+
permNoAccess: "Usuário sem permissão ou contexto compatível",
|
|
89
|
+
permRestricted: "Acessar a rota com perfil restrito",
|
|
90
|
+
permBlocked: "Bloqueio consistente",
|
|
91
|
+
permDenied: "Mensagem de acesso negado ou ação indisponível",
|
|
92
|
+
derivedFrom: (filePath) => `Derivada do arquivo \`${filePath}\`.`,
|
|
93
|
+
reviewBehavior: "Revisar comportamento implementado, mensagens visíveis e permissões relacionadas.",
|
|
94
|
+
happyValidPre: "Pré-condições válidas",
|
|
95
|
+
happyExecute: "Executar a ação principal",
|
|
96
|
+
happyCompleted: "Ação concluída",
|
|
97
|
+
happySuccessMsg: "Mensagem de sucesso ou mudança de estado",
|
|
98
|
+
edgeLimitData: "Dados limite ou estado alternativo",
|
|
99
|
+
edgeExecuteVar: "Executar com variação relevante",
|
|
100
|
+
edgeNoRegression: "Sem regressão",
|
|
101
|
+
edgeContextMsg: "Mensagem contextual",
|
|
102
|
+
errorInvalidInput: "Entrada inválida ou falha operacional",
|
|
103
|
+
errorForce: "Forçar erro aplicável",
|
|
104
|
+
errorHandledLabel: "Erro tratado",
|
|
105
|
+
errorExplicitMsg: "Mensagem explícita ao usuário",
|
|
106
|
+
permUnauthorized: "Perfil sem autorização para a ação",
|
|
107
|
+
permExecuteRestricted: "Executar o fluxo com permissão restrita",
|
|
108
|
+
permBlockedHidden: "Ação bloqueada ou ocultada",
|
|
109
|
+
permBlockMsg: "Mensagem de bloqueio ou ausência controlada da ação",
|
|
110
|
+
enterFlow: "Entrar no fluxo relacionado.",
|
|
111
|
+
executeMain: "Executar a ação principal.",
|
|
112
|
+
executeEdge: "Executar uma variação de borda.",
|
|
113
|
+
executeError: "Executar um cenário de erro ou bloqueio.",
|
|
114
|
+
accessRoute: (route) => `Acessar \`${route}\`.`,
|
|
115
|
+
confirmScreen: "Confirmar que a tela principal foi exibida.",
|
|
116
|
+
recordAlternative: "Registrar estados alternativos observados.",
|
|
117
|
+
interactionWith: (component) => `Interação com ${component}`,
|
|
118
|
+
noSourcesFound: "- Nenhuma fonte correlata encontrada automaticamente.",
|
|
119
|
+
noPlaywright: "- Projeto sem `playwright.config.*` detectado. Execução E2E marcada como bloqueada até configuração do runner.",
|
|
120
|
+
noEnvCredentials: "- Credenciais e configuração de ambiente não validadas automaticamente. Verificar variáveis antes da execução.",
|
|
121
|
+
noBlockers: "- Nenhum bloqueio estrutural detectado na geração estática.",
|
|
122
|
+
initialDossier: (route, project) => `Dossiê inicial gerado para \`${route}\`, com base em descoberta estática do projeto \`${project}\`.`,
|
|
123
|
+
playwrightDetected: "detectado",
|
|
124
|
+
playwrightNotDetected: "não detectado",
|
|
125
|
+
mandatoryCases: "Casos obrigatórios",
|
|
126
|
+
validateInitialLoad: "Validar carregamento inicial",
|
|
127
|
+
recordEdgeCase: "Registrar edge case esperado",
|
|
128
|
+
recordErrorCase: "Registrar caso de erro ou bloqueio",
|
|
129
|
+
flowTest: (route) => `fluxo ${route}`,
|
|
130
|
+
openTargetRoute: "Abrir rota alvo",
|
|
131
|
+
recordFinalContext: "Registrar contexto final",
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
function parseArgs(argv) {
|
|
136
|
+
const result = {};
|
|
137
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
138
|
+
const token = argv[index];
|
|
139
|
+
if (!token.startsWith("--")) {
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const [rawKey, inlineValue] = token.slice(2).split("=", 2);
|
|
144
|
+
const key = rawKey.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
|
|
145
|
+
if (inlineValue !== undefined) {
|
|
146
|
+
result[key] = inlineValue;
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const next = argv[index + 1];
|
|
151
|
+
if (!next || next.startsWith("--")) {
|
|
152
|
+
result[key] = true;
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
result[key] = next;
|
|
157
|
+
index += 1;
|
|
158
|
+
}
|
|
159
|
+
return result;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function ensureArg(value, name) {
|
|
163
|
+
if (!value) {
|
|
164
|
+
throw new Error(`Missing required argument: --${name}`);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function slugify(value) {
|
|
169
|
+
return value
|
|
170
|
+
.toLowerCase()
|
|
171
|
+
.replace(/^https?:\/\//, "")
|
|
172
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
173
|
+
.replace(/^-+|-+$/g, "")
|
|
174
|
+
.slice(0, 80) || "target";
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function readFile(filePath) {
|
|
178
|
+
return fs.readFileSync(filePath, "utf8");
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function writeFile(filePath, content) {
|
|
182
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
183
|
+
fs.writeFileSync(filePath, content);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function fileExists(filePath) {
|
|
187
|
+
return fs.existsSync(filePath);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function findFiles(globs) {
|
|
191
|
+
const args = ["--files"];
|
|
192
|
+
for (const glob of globs) {
|
|
193
|
+
args.push("-g", glob);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const output = execFileSync("rg", args, {
|
|
197
|
+
cwd: workspaceRoot,
|
|
198
|
+
encoding: "utf8",
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
return output.split("\n").filter(Boolean);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function grep(pattern, globs) {
|
|
205
|
+
const args = ["-n", "--no-heading", pattern];
|
|
206
|
+
for (const glob of globs) {
|
|
207
|
+
args.push("-g", glob);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
try {
|
|
211
|
+
const output = execFileSync("rg", args, {
|
|
212
|
+
cwd: workspaceRoot,
|
|
213
|
+
encoding: "utf8",
|
|
214
|
+
});
|
|
215
|
+
return output.split("\n").filter(Boolean);
|
|
216
|
+
} catch (error) {
|
|
217
|
+
if (error.status === 1) {
|
|
218
|
+
return [];
|
|
219
|
+
}
|
|
220
|
+
throw error;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function detectFramework(projectRoot) {
|
|
225
|
+
const packageJsonPath = path.join(workspaceRoot, projectRoot, "package.json");
|
|
226
|
+
if (!fileExists(packageJsonPath)) {
|
|
227
|
+
return "unknown";
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const packageJson = JSON.parse(readFile(packageJsonPath));
|
|
231
|
+
const deps = {
|
|
232
|
+
...packageJson.dependencies,
|
|
233
|
+
...packageJson.devDependencies,
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
if (deps.next) return "next";
|
|
237
|
+
if (deps.react) return "react";
|
|
238
|
+
if (deps.vue) return "vue";
|
|
239
|
+
if (deps["@angular/core"]) return "angular";
|
|
240
|
+
if (deps.svelte) return "svelte";
|
|
241
|
+
return "unknown";
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function detectProjectByInput(projectHint, target, baseUrl) {
|
|
245
|
+
if (projectHint) {
|
|
246
|
+
return normalizeProject(projectHint);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const packageJsonFiles = findFiles(["**/package.json", "!**/node_modules/**"]);
|
|
250
|
+
const candidateRoots = packageJsonFiles
|
|
251
|
+
.map((file) => path.dirname(file))
|
|
252
|
+
.filter((dir) => dir !== ".");
|
|
253
|
+
|
|
254
|
+
const hostname = baseUrl || target;
|
|
255
|
+
for (const projectRoot of candidateRoots) {
|
|
256
|
+
const playwrightConfigs = [
|
|
257
|
+
path.join(workspaceRoot, projectRoot, "playwright.config.ts"),
|
|
258
|
+
path.join(workspaceRoot, projectRoot, "playwright.config.js"),
|
|
259
|
+
].filter(fileExists);
|
|
260
|
+
|
|
261
|
+
for (const configPath of playwrightConfigs) {
|
|
262
|
+
const content = readFile(configPath);
|
|
263
|
+
if (hostname && content.includes(hostname.replace(/\/$/, ""))) {
|
|
264
|
+
return normalizeProject(projectRoot);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const routePath = toRoutePath(target);
|
|
270
|
+
if (routePath) {
|
|
271
|
+
for (const projectRoot of candidateRoots) {
|
|
272
|
+
const lines = grep(routePath.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), [
|
|
273
|
+
`${projectRoot}/**/*.{ts,tsx,js,jsx,md}`,
|
|
274
|
+
`!${projectRoot}/**/node_modules/**`,
|
|
275
|
+
`!${projectRoot}/**/.next/**`,
|
|
276
|
+
]);
|
|
277
|
+
if (lines.length > 0) {
|
|
278
|
+
return normalizeProject(projectRoot);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
throw new Error("Unable to detect project automatically. Pass --project explicitly.");
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function normalizeProject(projectRoot) {
|
|
287
|
+
return projectRoot.replace(/^\.\/+/, "").replace(/\/+$/, "");
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function toRoutePath(target) {
|
|
291
|
+
if (!target) {
|
|
292
|
+
return "";
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
try {
|
|
296
|
+
const url = new URL(target);
|
|
297
|
+
return url.pathname || "/";
|
|
298
|
+
} catch {
|
|
299
|
+
return target.startsWith("/") ? target : `/${target}`;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function findPlaywrightConfig(projectRoot) {
|
|
304
|
+
const candidates = ["playwright.config.ts", "playwright.config.js"]
|
|
305
|
+
.map((name) => path.join(workspaceRoot, projectRoot, name))
|
|
306
|
+
.filter(fileExists);
|
|
307
|
+
return candidates[0] ?? null;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function detectBaseUrl(projectRoot, baseUrlArg) {
|
|
311
|
+
if (baseUrlArg) {
|
|
312
|
+
return baseUrlArg.replace(/\/$/, "");
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const configPath = findPlaywrightConfig(projectRoot);
|
|
316
|
+
if (configPath) {
|
|
317
|
+
const content = readFile(configPath);
|
|
318
|
+
const match = content.match(/baseURL:\s*"([^"]+)"/);
|
|
319
|
+
if (match) {
|
|
320
|
+
return match[1].replace(/\/$/, "");
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
return "http://localhost:3000";
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function collectSources(projectRoot, routePath) {
|
|
328
|
+
const tokens = routePath.split("/").filter(Boolean);
|
|
329
|
+
const slugToken = tokens[tokens.length - 1] ?? routePath;
|
|
330
|
+
const escapedTokens = [routePath, ...tokens]
|
|
331
|
+
.filter(Boolean)
|
|
332
|
+
.map((token) => token.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
|
|
333
|
+
const filenameCandidates = findFiles([
|
|
334
|
+
`${projectRoot}/**/*.{ts,tsx,js,jsx,md}`,
|
|
335
|
+
`!${projectRoot}/**/node_modules/**`,
|
|
336
|
+
`!${projectRoot}/**/.next/**`,
|
|
337
|
+
]).filter((filePath) => tokens.some((token) => filePath.includes(token)));
|
|
338
|
+
|
|
339
|
+
const matches = [
|
|
340
|
+
...grep(escapedTokens.join("|"), [
|
|
341
|
+
`${projectRoot}/**/*.{ts,tsx,js,jsx,md}`,
|
|
342
|
+
`!${projectRoot}/**/node_modules/**`,
|
|
343
|
+
`!${projectRoot}/**/.next/**`,
|
|
344
|
+
]),
|
|
345
|
+
...grep(slugToken.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), [
|
|
346
|
+
`${projectRoot}/**/*.{ts,tsx,js,jsx,md}`,
|
|
347
|
+
`!${projectRoot}/**/node_modules/**`,
|
|
348
|
+
`!${projectRoot}/**/.next/**`,
|
|
349
|
+
]),
|
|
350
|
+
...filenameCandidates.map((filePath) => `${filePath}:0:filename-match`),
|
|
351
|
+
];
|
|
352
|
+
|
|
353
|
+
const uniqueFiles = new Map();
|
|
354
|
+
for (const line of matches) {
|
|
355
|
+
const [filePath, lineNo, ...rest] = line.split(":");
|
|
356
|
+
const snippet = rest.join(":").trim();
|
|
357
|
+
if (!uniqueFiles.has(filePath)) {
|
|
358
|
+
uniqueFiles.set(filePath, []);
|
|
359
|
+
}
|
|
360
|
+
uniqueFiles.get(filePath).push({
|
|
361
|
+
line: Number(lineNo) || 0,
|
|
362
|
+
snippet,
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
return Array.from(uniqueFiles.entries())
|
|
367
|
+
.map(([filePath, snippets]) => {
|
|
368
|
+
const refinedSnippets = snippets.some((item) => item.line > 0)
|
|
369
|
+
? snippets.slice(0, 6)
|
|
370
|
+
: extractHintsFromFile(filePath);
|
|
371
|
+
return {
|
|
372
|
+
filePath,
|
|
373
|
+
snippets: refinedSnippets.slice(0, 6),
|
|
374
|
+
};
|
|
375
|
+
})
|
|
376
|
+
.sort((left, right) => {
|
|
377
|
+
const leftPenalty = /\.spec\./.test(left.filePath) ? 1 : 0;
|
|
378
|
+
const rightPenalty = /\.spec\./.test(right.filePath) ? 1 : 0;
|
|
379
|
+
return leftPenalty - rightPenalty;
|
|
380
|
+
})
|
|
381
|
+
.slice(0, 20);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function extractHintsFromFile(filePath) {
|
|
385
|
+
const absolutePath = path.join(workspaceRoot, filePath);
|
|
386
|
+
if (!fileExists(absolutePath)) {
|
|
387
|
+
return [{ line: 0, snippet: "filename-match" }];
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
const lines = readFile(absolutePath).split("\n");
|
|
391
|
+
const hints = [];
|
|
392
|
+
const matcher = /(CardTitle|DialogTitle|TabsTrigger|Button|toast|error|success|loading|empty|Title>|aria-label|getByRole|getByText)/i;
|
|
393
|
+
|
|
394
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
395
|
+
const snippet = lines[index].trim();
|
|
396
|
+
if (!matcher.test(snippet)) {
|
|
397
|
+
continue;
|
|
398
|
+
}
|
|
399
|
+
hints.push({
|
|
400
|
+
line: index + 1,
|
|
401
|
+
snippet,
|
|
402
|
+
});
|
|
403
|
+
if (hints.length >= 6) {
|
|
404
|
+
break;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
return hints.length > 0 ? hints : [{ line: 0, snippet: "filename-match" }];
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function inferFeatures(routePath, sources, s) {
|
|
412
|
+
const features = [];
|
|
413
|
+
const tokenTitle = routePath
|
|
414
|
+
.split("/")
|
|
415
|
+
.filter(Boolean)
|
|
416
|
+
.map((segment) => segment.replace(/[-_]/g, " "))
|
|
417
|
+
.join(" / ");
|
|
418
|
+
|
|
419
|
+
if (tokenTitle) {
|
|
420
|
+
features.push({
|
|
421
|
+
title: s.routeAccess(routePath),
|
|
422
|
+
details: [
|
|
423
|
+
s.routeAccessible(routePath),
|
|
424
|
+
s.routeValidateLoad,
|
|
425
|
+
],
|
|
426
|
+
cases: [
|
|
427
|
+
makeCase("F01", "happy-path", s.happyRouteLoads, s.happyNavigate, s.happyAccessible, s.happyNoError),
|
|
428
|
+
makeCase("F02", "edge-case", s.edgeNoData, s.edgeOpenEmpty, s.edgeEmptyState, s.edgeEmptyMsg),
|
|
429
|
+
makeCase("F03", "error", s.errorLoadFail, s.errorSimulate, s.errorHandled, s.errorReadable),
|
|
430
|
+
makeCase("F04", "permission", s.permNoAccess, s.permRestricted, s.permBlocked, s.permDenied),
|
|
431
|
+
],
|
|
432
|
+
steps: [
|
|
433
|
+
s.accessRoute(routePath),
|
|
434
|
+
s.confirmScreen,
|
|
435
|
+
s.recordAlternative,
|
|
436
|
+
],
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
for (const source of sources.slice(0, 4)) {
|
|
441
|
+
const prominent = source.snippets
|
|
442
|
+
.map((entry) => entry.snippet)
|
|
443
|
+
.find((snippet) => deriveFeatureTitle(snippet, s));
|
|
444
|
+
|
|
445
|
+
if (!prominent) {
|
|
446
|
+
continue;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
const featureTitle = deriveFeatureTitle(prominent, s);
|
|
450
|
+
if (!featureTitle) {
|
|
451
|
+
continue;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
features.push({
|
|
455
|
+
title: featureTitle,
|
|
456
|
+
details: [
|
|
457
|
+
s.derivedFrom(source.filePath),
|
|
458
|
+
s.reviewBehavior,
|
|
459
|
+
],
|
|
460
|
+
cases: [
|
|
461
|
+
makeCase(nextCaseId(features, 1), "happy-path", s.happyValidPre, s.happyExecute, s.happyCompleted, s.happySuccessMsg),
|
|
462
|
+
makeCase(nextCaseId(features, 2), "edge-case", s.edgeLimitData, s.edgeExecuteVar, s.edgeNoRegression, s.edgeContextMsg),
|
|
463
|
+
makeCase(nextCaseId(features, 3), "error", s.errorInvalidInput, s.errorForce, s.errorHandledLabel, s.errorExplicitMsg),
|
|
464
|
+
makeCase(nextCaseId(features, 4), "permission", s.permUnauthorized, s.permExecuteRestricted, s.permBlockedHidden, s.permBlockMsg),
|
|
465
|
+
],
|
|
466
|
+
steps: [
|
|
467
|
+
s.enterFlow,
|
|
468
|
+
s.executeMain,
|
|
469
|
+
s.executeEdge,
|
|
470
|
+
s.executeError,
|
|
471
|
+
],
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
return dedupeFeatures(features).slice(0, 8);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
function deriveFeatureTitle(snippet, s) {
|
|
479
|
+
if (!snippet) {
|
|
480
|
+
return "";
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
if (/isLoading|vi\.mock|render\(|use[A-Z][A-Za-z]+:\s|\bfalse\b|\btrue\b/.test(snippet)) {
|
|
484
|
+
return "";
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
const quoted = snippet.match(/["'`](.{4,80}?)["'`]/)?.[1]?.trim();
|
|
488
|
+
if (quoted && !quoted.startsWith("@/") && !quoted.includes("/") && /[A-Za-zÀ-ÿ]{3,}/.test(quoted)) {
|
|
489
|
+
return quoted;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
const component = snippet.match(/\b([A-Z][A-Za-z0-9]+(?:Dialog|Popover|Tabs|Page|Calendar|Legend|Screen))\b/)?.[1];
|
|
493
|
+
if (component) {
|
|
494
|
+
return s.interactionWith(component);
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
return "";
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
function nextCaseId(features, offset) {
|
|
501
|
+
const value = features.length * 4 + offset + 4;
|
|
502
|
+
return `F${String(value).padStart(2, "0")}`;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
function makeCase(id, type, preconditions, actions, expected, message) {
|
|
506
|
+
return {
|
|
507
|
+
id,
|
|
508
|
+
type,
|
|
509
|
+
preconditions,
|
|
510
|
+
actions,
|
|
511
|
+
expected,
|
|
512
|
+
message,
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
function dedupeFeatures(features) {
|
|
517
|
+
const seen = new Set();
|
|
518
|
+
return features.filter((feature) => {
|
|
519
|
+
const key = feature.title.toLowerCase();
|
|
520
|
+
if (seen.has(key)) {
|
|
521
|
+
return false;
|
|
522
|
+
}
|
|
523
|
+
seen.add(key);
|
|
524
|
+
return true;
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
function renderTemplate(templatePath, replacements) {
|
|
529
|
+
let content = readFile(templatePath);
|
|
530
|
+
for (const [key, value] of Object.entries(replacements)) {
|
|
531
|
+
content = content.replaceAll(`{{${key}}}`, value);
|
|
532
|
+
}
|
|
533
|
+
return content;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
function buildSourcesList(sources, s) {
|
|
537
|
+
if (sources.length === 0) {
|
|
538
|
+
return s.noSourcesFound;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
return sources
|
|
542
|
+
.map((source) => {
|
|
543
|
+
const snippet = source.snippets[0];
|
|
544
|
+
if (!snippet) {
|
|
545
|
+
return `- \`${source.filePath}\``;
|
|
546
|
+
}
|
|
547
|
+
return `- \`${source.filePath}:${snippet.line}\` — ${snippet.snippet}`;
|
|
548
|
+
})
|
|
549
|
+
.join("\n");
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
function buildBlockers(projectRoot, hasPlaywright, s) {
|
|
553
|
+
const blockers = [];
|
|
554
|
+
if (!hasPlaywright) {
|
|
555
|
+
blockers.push(s.noPlaywright);
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
const credentialsPath = path.join(workspaceRoot, projectRoot, ".env");
|
|
559
|
+
if (!fileExists(credentialsPath)) {
|
|
560
|
+
blockers.push(s.noEnvCredentials);
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
return blockers.length > 0 ? blockers.join("\n") : s.noBlockers;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
function buildFeaturesMarkdown(features, s) {
|
|
567
|
+
return features
|
|
568
|
+
.map((feature, index) => {
|
|
569
|
+
const details = feature.details.map((detail) => `- ${detail}`).join("\n");
|
|
570
|
+
const cases = feature.cases
|
|
571
|
+
.map((item) => `- \`${item.type}\`: ${item.actions} -> ${item.expected}. ${item.message}.`)
|
|
572
|
+
.join("\n");
|
|
573
|
+
return `## ${index + 1}. ${feature.title}\n\n${details}\n\n### ${s.mandatoryCases}\n\n${cases}`;
|
|
574
|
+
})
|
|
575
|
+
.join("\n\n");
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
function buildCaseRows(features) {
|
|
579
|
+
return features
|
|
580
|
+
.flatMap((feature) =>
|
|
581
|
+
feature.cases.map((item) =>
|
|
582
|
+
`| ${item.id} | ${escapeCell(feature.title)} | ${item.type} | ${escapeCell(item.preconditions)} | ${escapeCell(item.actions)} | ${escapeCell(item.expected)} | ${escapeCell(item.message)} | TODO | TODO |`,
|
|
583
|
+
),
|
|
584
|
+
)
|
|
585
|
+
.join("\n");
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
function escapeCell(value) {
|
|
589
|
+
return String(value).replace(/\|/g, "\\|");
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
function buildStepList(features) {
|
|
593
|
+
const lines = [];
|
|
594
|
+
let index = 1;
|
|
595
|
+
for (const feature of features) {
|
|
596
|
+
for (const step of feature.steps) {
|
|
597
|
+
lines.push(`${index}. ${step}`);
|
|
598
|
+
index += 1;
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
return lines.join("\n");
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
function inferPrimaryHeading(sources, routePath) {
|
|
605
|
+
const line = sources
|
|
606
|
+
.flatMap((source) => source.snippets)
|
|
607
|
+
.find((entry) => /(CardTitle|DialogTitle|title:|<h1|<h2|TabsTrigger)/i.test(entry.snippet));
|
|
608
|
+
if (!line) {
|
|
609
|
+
return toTitleCase(routePath.split("/").filter(Boolean).pop()?.replace(/[-_]/g, " ") || "Screen");
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
const match = line.snippet.match(/["'`](.+?)["'`]/);
|
|
613
|
+
const value = match?.[1]?.trim();
|
|
614
|
+
if (!value || value.toLowerCase() === "page" || value.startsWith("@/")) {
|
|
615
|
+
return toTitleCase(routePath.split("/").filter(Boolean).pop()?.replace(/[-_]/g, " ") || "Screen");
|
|
616
|
+
}
|
|
617
|
+
return value;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
function toTitleCase(value) {
|
|
621
|
+
return value
|
|
622
|
+
.split(/\s+/)
|
|
623
|
+
.filter(Boolean)
|
|
624
|
+
.map((token) => token.charAt(0).toUpperCase() + token.slice(1))
|
|
625
|
+
.join(" ");
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
function buildPlaywrightSteps(features, heading, s) {
|
|
629
|
+
const safeHeading = heading.replace(/"/g, '\\"');
|
|
630
|
+
const steps = [
|
|
631
|
+
` await test.step("${s.validateInitialLoad}", async () => {`,
|
|
632
|
+
` await expect(page.getByText(/${escapeRegex(safeHeading)}/i).first()).toBeVisible();`,
|
|
633
|
+
` });`,
|
|
634
|
+
];
|
|
635
|
+
|
|
636
|
+
const firstEdge = features.flatMap((feature) => feature.cases).find((item) => item.type === "edge-case");
|
|
637
|
+
if (firstEdge) {
|
|
638
|
+
steps.push(
|
|
639
|
+
` await test.step("${s.recordEdgeCase}", async () => {`,
|
|
640
|
+
` await page.screenshot({ path: "evidence-edge-case.png", fullPage: true });`,
|
|
641
|
+
` });`,
|
|
642
|
+
);
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
const firstError = features.flatMap((feature) => feature.cases).find((item) => item.type === "error");
|
|
646
|
+
if (firstError) {
|
|
647
|
+
steps.push(
|
|
648
|
+
` await test.step("${s.recordErrorCase}", async () => {`,
|
|
649
|
+
` await expect(page.locator("body")).toBeVisible();`,
|
|
650
|
+
` });`,
|
|
651
|
+
);
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
return steps.join("\n");
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
function escapeRegex(value) {
|
|
658
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
function buildSrt(features) {
|
|
662
|
+
const entries = [];
|
|
663
|
+
let seconds = 0;
|
|
664
|
+
let index = 1;
|
|
665
|
+
const steps = features.flatMap((feature) => feature.steps.map((step) => `${feature.title}: ${step}`));
|
|
666
|
+
|
|
667
|
+
for (const step of steps) {
|
|
668
|
+
const start = formatSrtTime(seconds);
|
|
669
|
+
const end = formatSrtTime(seconds + 4);
|
|
670
|
+
entries.push(`${index}\n${start} --> ${end}\n${step}\n`);
|
|
671
|
+
seconds += 5;
|
|
672
|
+
index += 1;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
return entries.join("\n");
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
function formatSrtTime(totalSeconds) {
|
|
679
|
+
const hours = String(Math.floor(totalSeconds / 3600)).padStart(2, "0");
|
|
680
|
+
const minutes = String(Math.floor((totalSeconds % 3600) / 60)).padStart(2, "0");
|
|
681
|
+
const seconds = String(totalSeconds % 60).padStart(2, "0");
|
|
682
|
+
return `${hours}:${minutes}:${seconds},000`;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
function detectPackageManager(projectRoot) {
|
|
686
|
+
if (fileExists(path.join(workspaceRoot, projectRoot, "pnpm-lock.yaml"))) return "pnpm";
|
|
687
|
+
if (fileExists(path.join(workspaceRoot, projectRoot, "package-lock.json"))) return "npm";
|
|
688
|
+
if (fileExists(path.join(workspaceRoot, projectRoot, "yarn.lock"))) return "yarn";
|
|
689
|
+
return "npm";
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
function buildManifest({ projectRoot, target, targetType, routePath, baseUrl, hasPlaywright, framework, outputRoot, sources, features }) {
|
|
693
|
+
return {
|
|
694
|
+
generatedAt: new Date().toISOString(),
|
|
695
|
+
project: projectRoot,
|
|
696
|
+
target,
|
|
697
|
+
targetType,
|
|
698
|
+
routePath,
|
|
699
|
+
baseUrl,
|
|
700
|
+
framework,
|
|
701
|
+
packageManager: detectPackageManager(projectRoot),
|
|
702
|
+
playwright: {
|
|
703
|
+
detected: hasPlaywright,
|
|
704
|
+
runnerStatus: hasPlaywright ? "available" : "blocked",
|
|
705
|
+
},
|
|
706
|
+
coverage: {
|
|
707
|
+
featureCount: features.length,
|
|
708
|
+
caseCount: features.reduce((sum, feature) => sum + feature.cases.length, 0),
|
|
709
|
+
requiredTypes: ["happy-path", "edge-case", "error", "permission"],
|
|
710
|
+
},
|
|
711
|
+
sources: sources.map((source) => source.filePath),
|
|
712
|
+
outputs: {
|
|
713
|
+
root: outputRoot,
|
|
714
|
+
overview: path.join(outputRoot, "overview.md"),
|
|
715
|
+
features: path.join(outputRoot, "features.md"),
|
|
716
|
+
caseMatrix: path.join(outputRoot, "case-matrix.md"),
|
|
717
|
+
e2eRunbook: path.join(outputRoot, "e2e-runbook.md"),
|
|
718
|
+
script: path.join(outputRoot, "scripts", `${slugify(routePath || target)}.spec.ts`),
|
|
719
|
+
captions: path.join(outputRoot, "captions", `${slugify(routePath || target)}.srt`),
|
|
720
|
+
},
|
|
721
|
+
blockers: hasPlaywright ? [] : ["Playwright configuration not detected"],
|
|
722
|
+
};
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
function main() {
|
|
726
|
+
const args = parseArgs(process.argv.slice(2));
|
|
727
|
+
ensureArg(args.target, "target");
|
|
728
|
+
|
|
729
|
+
const lang = String(args.lang || "en");
|
|
730
|
+
const s = STRINGS[lang] || STRINGS.en;
|
|
731
|
+
|
|
732
|
+
const target = String(args.target);
|
|
733
|
+
const targetType = String(args.targetType || "url");
|
|
734
|
+
const projectRoot = detectProjectByInput(args.project, target, args.baseUrl);
|
|
735
|
+
const routePath = toRoutePath(target);
|
|
736
|
+
const framework = detectFramework(projectRoot);
|
|
737
|
+
const baseUrl = detectBaseUrl(projectRoot, args.baseUrl);
|
|
738
|
+
const sources = collectSources(projectRoot, routePath);
|
|
739
|
+
const hasPlaywright = Boolean(findPlaywrightConfig(projectRoot));
|
|
740
|
+
const features = inferFeatures(routePath, sources, s);
|
|
741
|
+
const outputRoot = path.join(workspaceRoot, "ai", "flows", slugify(projectRoot), slugify(routePath || target));
|
|
742
|
+
const templatesRoot = path.join(workspaceRoot, "ai", "templates", "functional-doc");
|
|
743
|
+
const heading = inferPrimaryHeading(sources, routePath);
|
|
744
|
+
|
|
745
|
+
fs.mkdirSync(path.join(outputRoot, "scripts"), { recursive: true });
|
|
746
|
+
fs.mkdirSync(path.join(outputRoot, "evidence", "videos"), { recursive: true });
|
|
747
|
+
fs.mkdirSync(path.join(outputRoot, "evidence", "screenshots"), { recursive: true });
|
|
748
|
+
fs.mkdirSync(path.join(outputRoot, "evidence", "logs"), { recursive: true });
|
|
749
|
+
fs.mkdirSync(path.join(outputRoot, "captions"), { recursive: true });
|
|
750
|
+
|
|
751
|
+
writeFile(
|
|
752
|
+
path.join(outputRoot, "overview.md"),
|
|
753
|
+
renderTemplate(path.join(templatesRoot, "overview.md"), {
|
|
754
|
+
projectName: projectRoot,
|
|
755
|
+
target,
|
|
756
|
+
targetType,
|
|
757
|
+
framework,
|
|
758
|
+
baseUrl,
|
|
759
|
+
playwrightStatus: hasPlaywright ? s.playwrightDetected : s.playwrightNotDetected,
|
|
760
|
+
generatedAt: new Date().toISOString(),
|
|
761
|
+
summary: s.initialDossier(routePath || target, projectRoot),
|
|
762
|
+
sources: buildSourcesList(sources, s),
|
|
763
|
+
blockers: buildBlockers(projectRoot, hasPlaywright, s),
|
|
764
|
+
}),
|
|
765
|
+
);
|
|
766
|
+
|
|
767
|
+
writeFile(
|
|
768
|
+
path.join(outputRoot, "features.md"),
|
|
769
|
+
renderTemplate(path.join(templatesRoot, "features.md"), {
|
|
770
|
+
features: buildFeaturesMarkdown(features, s),
|
|
771
|
+
}),
|
|
772
|
+
);
|
|
773
|
+
|
|
774
|
+
writeFile(
|
|
775
|
+
path.join(outputRoot, "case-matrix.md"),
|
|
776
|
+
renderTemplate(path.join(templatesRoot, "case-matrix.md"), {
|
|
777
|
+
rows: buildCaseRows(features),
|
|
778
|
+
}),
|
|
779
|
+
);
|
|
780
|
+
|
|
781
|
+
writeFile(
|
|
782
|
+
path.join(outputRoot, "e2e-runbook.md"),
|
|
783
|
+
renderTemplate(path.join(templatesRoot, "e2e-runbook.md"), {
|
|
784
|
+
steps: buildStepList(features),
|
|
785
|
+
}),
|
|
786
|
+
);
|
|
787
|
+
|
|
788
|
+
writeFile(
|
|
789
|
+
path.join(outputRoot, "scripts", `${slugify(routePath || target)}.spec.ts`),
|
|
790
|
+
renderTemplate(path.join(templatesRoot, "playwright.spec.ts.tpl"), {
|
|
791
|
+
baseUrl,
|
|
792
|
+
testTitle: s.flowTest(routePath || target),
|
|
793
|
+
routePath,
|
|
794
|
+
routeRegex: escapeRegex(routePath || "/"),
|
|
795
|
+
testSteps: buildPlaywrightSteps(features, heading, s),
|
|
796
|
+
}),
|
|
797
|
+
);
|
|
798
|
+
|
|
799
|
+
writeFile(
|
|
800
|
+
path.join(outputRoot, "captions", `${slugify(routePath || target)}.srt`),
|
|
801
|
+
buildSrt(features),
|
|
802
|
+
);
|
|
803
|
+
|
|
804
|
+
const manifest = buildManifest({
|
|
805
|
+
projectRoot,
|
|
806
|
+
target,
|
|
807
|
+
targetType,
|
|
808
|
+
routePath,
|
|
809
|
+
baseUrl,
|
|
810
|
+
hasPlaywright,
|
|
811
|
+
framework,
|
|
812
|
+
outputRoot,
|
|
813
|
+
sources,
|
|
814
|
+
features,
|
|
815
|
+
});
|
|
816
|
+
writeFile(path.join(outputRoot, "manifest.json"), `${JSON.stringify(manifest, null, 2)}\n`);
|
|
817
|
+
|
|
818
|
+
process.stdout.write(`${JSON.stringify(manifest, null, 2)}\n`);
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
main();
|