siesa-agents 2.1.72-qa.2 → 2.1.72-qa.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/bmad/bmm/config.yaml +33 -0
  2. package/claude/skills/sa-agent-sre-sentinel/SKILL.md +180 -0
  3. package/claude/skills/sa-aplicar/SKILL.md +268 -0
  4. package/claude/skills/sa-auditar-servicio/SKILL.md +255 -0
  5. package/claude/skills/sa-nueva-transversal/SKILL.md +317 -0
  6. package/claude/skills/sa-nuevo-ambiente/SKILL.md +147 -0
  7. package/claude/skills/sa-nuevo-servicio/SKILL.md +530 -0
  8. package/claude/skills/sa-onboard-db/SKILL.md +122 -0
  9. package/claude/skills/sa-qa-data-generator/SKILL.md +200 -51
  10. package/claude/skills/sa-quality-process/SKILL.md +6 -0
  11. package/claude/skills/sa-registrar-permisos/SKILL.md +233 -0
  12. package/package.json +1 -1
  13. package/siesa-agents/bmm/workflows/3-solutioning/quality-process/prompts/prompt_design_test.md +1037 -60
  14. package/siesa-agents/bmm/workflows/3-solutioning/quality-process/prompts/prompt_dor_gate.md +419 -379
  15. package/siesa-agents/bmm/workflows/3-solutioning/quality-process/prompts/prompt_e2e_executor.md +273 -0
  16. package/siesa-agents/bmm/workflows/3-solutioning/quality-process/prompts/prompt_playwright_impl.md +359 -355
  17. package/siesa-agents/bmm/workflows/3-solutioning/quality-process/prompts/prompt_test_plan.md +73 -111
  18. package/siesa-agents/bmm/workflows/3-solutioning/quality-process/workflow.md +1320 -997
  19. package/siesa-agents/resources/playwright-kit/.env.example +42 -0
  20. package/siesa-agents/resources/playwright-kit/README.md +228 -0
  21. package/siesa-agents/resources/playwright-kit/allure/categories.json +26 -0
  22. package/siesa-agents/resources/playwright-kit/global-setup.ts +34 -0
  23. package/siesa-agents/resources/playwright-kit/helpers/blazor-e2e-helpers.ts +417 -0
  24. package/siesa-agents/resources/playwright-kit/helpers/react-e2e-helpers.ts +297 -0
  25. package/siesa-agents/resources/playwright-kit/package-lock.json +850 -0
  26. package/siesa-agents/resources/playwright-kit/package.json +27 -0
  27. package/siesa-agents/resources/playwright-kit/playwright.config.ts +60 -0
  28. package/siesa-agents/resources/playwright-kit/proposed/.gitkeep +0 -0
  29. package/siesa-agents/resources/playwright-kit/reporters/agiletest-reporter.ts +252 -0
  30. package/siesa-agents/resources/playwright-kit/specs/.gitkeep +0 -0
  31. package/siesa-agents/resources/playwright-kit/specs/architecture-brief-siesa-erp.md +85 -0
  32. package/siesa-agents/resources/playwright-kit/specs/architecture-brief-siesa-react.md +109 -0
  33. package/siesa-agents/resources/playwright-kit/specs/architecture-brief-template.md +69 -0
  34. package/siesa-agents/resources/playwright-kit/tests/seed-example.spec.ts +95 -0
  35. package/siesa-agents/resources/playwright-kit/tools/locator-overlay.js +1306 -0
  36. package/siesa-agents/resources/playwright-kit/tools/locator-recorder.spec.ts +986 -0
  37. package/siesa-agents/scripts/__pycache__/bmad_to_agiletest.cpython-36.pyc +0 -0
  38. package/siesa-agents/scripts/bmad_to_agiletest.py +352 -337
  39. package/siesa-agents/scripts/converters/convert_csv_to_test_design.py +327 -0
  40. package/siesa-agents/scripts/converters/convert_excel_to_test_design.py +360 -0
  41. package/siesa-agents/scripts/converters/convert_md_to_test_design.py +359 -0
  42. package/siesa-agents/scripts/converters/convert_pdf_to_test_design.py +412 -0
  43. package/siesa-agents/scripts/converters/export_yaml_to_csv.py +149 -0
  44. package/siesa-agents/scripts/converters/export_yaml_to_excel.py +233 -0
  45. package/siesa-agents/scripts/converters/export_yaml_to_markdown.py +172 -0
  46. package/siesa-agents/scripts/converters/export_yaml_to_pdf.py +297 -0
  47. package/siesa-agents/scripts/merge_test_design.py +106 -106
  48. package/siesa-agents/scripts/playwright/yaml-to-playwright-spec.ts +203 -0
  49. package/siesa-agents/scripts/requirements.txt +7 -0
@@ -0,0 +1,233 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ export_yaml_to_excel.py — Exporta test-design YAML a Excel (.xlsx)
4
+ Formato: Hoja "Metadata" (key-value) + Hoja "Pasos" (tabla de pasos)
5
+ Uso: python export_yaml_to_excel.py <input.yml> [output.xlsx]
6
+ """
7
+
8
+ import sys
9
+ from pathlib import Path
10
+
11
+ try:
12
+ import yaml
13
+ except ImportError:
14
+ print("ERROR: pyyaml requerido. Instalar con: pip install pyyaml")
15
+ sys.exit(1)
16
+
17
+ try:
18
+ from openpyxl import Workbook
19
+ from openpyxl.styles import Font, PatternFill, Alignment, Border, Side
20
+ except ImportError:
21
+ print("ERROR: openpyxl requerido. Instalar con: pip install openpyxl")
22
+ sys.exit(1)
23
+
24
+
25
+ # --- Estilos Siesa ---
26
+ TEAL = "009688"
27
+ WHITE = "FFFFFF"
28
+ LIGHT_GRAY = "F5F5F5"
29
+
30
+ HEADER_FONT = Font(name="Calibri", bold=True, color=WHITE, size=11)
31
+ HEADER_FILL = PatternFill(start_color=TEAL, end_color=TEAL, fill_type="solid")
32
+ SECTION_FONT = Font(name="Calibri", bold=True, color=TEAL, size=11)
33
+ NORMAL_FONT = Font(name="Calibri", size=11)
34
+ ALT_FILL = PatternFill(start_color=LIGHT_GRAY, end_color=LIGHT_GRAY, fill_type="solid")
35
+ THIN_BORDER = Border(
36
+ left=Side(style="thin"), right=Side(style="thin"),
37
+ top=Side(style="thin"), bottom=Side(style="thin"),
38
+ )
39
+
40
+
41
+ def style_header_row(ws, row, col_count):
42
+ for col in range(1, col_count + 1):
43
+ cell = ws.cell(row=row, column=col)
44
+ cell.font = HEADER_FONT
45
+ cell.fill = HEADER_FILL
46
+ cell.alignment = Alignment(horizontal="center", vertical="center")
47
+ cell.border = THIN_BORDER
48
+
49
+
50
+ def style_data_cell(cell, row_idx):
51
+ cell.font = NORMAL_FONT
52
+ cell.border = THIN_BORDER
53
+ cell.alignment = Alignment(vertical="top", wrap_text=True)
54
+ if row_idx % 2 == 0:
55
+ cell.fill = ALT_FILL
56
+
57
+
58
+ def style_section_cell(cell):
59
+ cell.font = SECTION_FONT
60
+ cell.border = THIN_BORDER
61
+
62
+
63
+ def flatten_value(value) -> str:
64
+ """Convierte listas y dicts a string legible."""
65
+ if isinstance(value, list):
66
+ return " | ".join(str(v) for v in value)
67
+ if isinstance(value, dict):
68
+ return " | ".join(f"{k}: {v}" for k, v in value.items())
69
+ return str(value).strip()
70
+
71
+
72
+ # --- Metadata sheet ---
73
+
74
+ def build_metadata_rows(data: dict) -> list[tuple[str, str, str]]:
75
+ """Genera filas (seccion, campo, valor) para la hoja Metadata."""
76
+ rows = []
77
+
78
+ def add_section(section_name: str, section_data: dict, fields: list[str]):
79
+ for field in fields:
80
+ val = section_data.get(field)
81
+ if val is not None:
82
+ rows.append((section_name, field, flatten_value(val)))
83
+
84
+ # metadata
85
+ meta = data.get("metadata", {})
86
+ add_section("metadata", meta, ["modulo", "version", "autor", "fecha", "stack"])
87
+
88
+ # requisito
89
+ req = data.get("requisito", {})
90
+ add_section("requisito", req, ["id", "nombre", "descripcion"])
91
+ criterios = req.get("criterios_aceptacion", [])
92
+ if criterios:
93
+ rows.append(("requisito", "criterios_aceptacion", " | ".join(criterios)))
94
+
95
+ # plan_pruebas
96
+ plan = data.get("plan_pruebas", {})
97
+ add_section("plan_pruebas", plan, ["id", "nombre", "objetivo", "framework"])
98
+ precondiciones = plan.get("precondiciones", [])
99
+ if precondiciones:
100
+ rows.append(("plan_pruebas", "precondiciones", " | ".join(precondiciones)))
101
+ ambientes = plan.get("ambientes", [])
102
+ for i, amb in enumerate(ambientes):
103
+ for key in ("nombre", "url", "usuario"):
104
+ if key in amb:
105
+ rows.append((f"plan_pruebas.ambiente_{i}", key, str(amb[key])))
106
+ tiempos = plan.get("tiempos_referencia", {})
107
+ for key, val in tiempos.items():
108
+ rows.append(("plan_pruebas.tiempos", key, str(val)))
109
+
110
+ # caso_prueba (sin pasos)
111
+ caso = data.get("caso_prueba", {})
112
+ add_section("caso_prueba", caso, ["id", "nombre", "prioridad", "tipo"])
113
+ scripts = caso.get("scripts", [])
114
+ if scripts:
115
+ rows.append(("caso_prueba", "scripts", " | ".join(scripts)))
116
+
117
+ # ejecucion
118
+ ejec = data.get("ejecucion", {})
119
+ add_section("ejecucion", ejec, ["id", "nombre"])
120
+ variables = ejec.get("variables_entorno", [])
121
+ for var in variables:
122
+ nombre = var.get("nombre", "")
123
+ desc = var.get("descripcion", "")
124
+ default = var.get("default", "")
125
+ rows.append(("ejecucion.variable", nombre, f"{desc} | {default}"))
126
+ comandos = ejec.get("comandos", {})
127
+ for key, val in comandos.items():
128
+ rows.append(("ejecucion.comando", key, str(val)))
129
+
130
+ # integraciones
131
+ integ = data.get("integraciones", {})
132
+ for integ_name, info in integ.items():
133
+ for key, val in info.items():
134
+ rows.append((f"integracion.{integ_name}", key, str(val)))
135
+
136
+ return rows
137
+
138
+
139
+ def write_metadata_sheet(wb: Workbook, data: dict):
140
+ ws = wb.active
141
+ ws.title = "Metadata"
142
+
143
+ headers = ["Seccion", "Campo", "Valor"]
144
+ for col, h in enumerate(headers, 1):
145
+ ws.cell(row=1, column=col, value=h)
146
+ style_header_row(ws, 1, len(headers))
147
+
148
+ rows = build_metadata_rows(data)
149
+ prev_section = ""
150
+ for i, (seccion, campo, valor) in enumerate(rows):
151
+ row_num = i + 2
152
+ c_sec = ws.cell(row=row_num, column=1, value=seccion)
153
+ c_campo = ws.cell(row=row_num, column=2, value=campo)
154
+ c_valor = ws.cell(row=row_num, column=3, value=valor)
155
+
156
+ if seccion != prev_section:
157
+ style_section_cell(c_sec)
158
+ prev_section = seccion
159
+ else:
160
+ c_sec.font = NORMAL_FONT
161
+ c_sec.border = THIN_BORDER
162
+
163
+ style_data_cell(c_campo, i)
164
+ style_data_cell(c_valor, i)
165
+
166
+ ws.column_dimensions["A"].width = 28
167
+ ws.column_dimensions["B"].width = 24
168
+ ws.column_dimensions["C"].width = 80
169
+
170
+
171
+ def write_pasos_sheet(wb: Workbook, data: dict):
172
+ ws = wb.create_sheet("Pasos")
173
+
174
+ headers = [
175
+ "Paso", "Nombre", "Accion", "Resultado Esperado",
176
+ "Datos", "Script", "Automatizado", "Bloqueado", "Nota",
177
+ ]
178
+ for col, h in enumerate(headers, 1):
179
+ ws.cell(row=1, column=col, value=h)
180
+ style_header_row(ws, 1, len(headers))
181
+
182
+ pasos = data.get("caso_prueba", {}).get("pasos", [])
183
+ for i, p in enumerate(pasos):
184
+ row_num = i + 2
185
+ values = [
186
+ p.get("paso", ""),
187
+ p.get("nombre", ""),
188
+ p.get("accion", ""),
189
+ p.get("resultado", ""),
190
+ p.get("datos", ""),
191
+ p.get("script", ""),
192
+ p.get("automatizado", False),
193
+ p.get("bloqueado", ""),
194
+ p.get("nota", ""),
195
+ ]
196
+ for col, val in enumerate(values, 1):
197
+ cell = ws.cell(row=row_num, column=col, value=val)
198
+ style_data_cell(cell, i)
199
+
200
+ widths = [8, 35, 60, 45, 50, 30, 14, 35, 30]
201
+ for col_idx, w in enumerate(widths):
202
+ ws.column_dimensions[chr(65 + col_idx)].width = w
203
+
204
+
205
+ def main():
206
+ if len(sys.argv) < 2:
207
+ print("Uso: python export_yaml_to_excel.py <input.yml> [output.xlsx]")
208
+ sys.exit(1)
209
+
210
+ input_path = Path(sys.argv[1])
211
+ if not input_path.exists():
212
+ print(f"ERROR: No se encontro {input_path}")
213
+ sys.exit(1)
214
+
215
+ if len(sys.argv) >= 3:
216
+ output_path = Path(sys.argv[2])
217
+ else:
218
+ output_path = input_path.with_suffix(".xlsx")
219
+
220
+ with open(input_path, "r", encoding="utf-8") as f:
221
+ data = yaml.safe_load(f)
222
+
223
+ wb = Workbook()
224
+ write_metadata_sheet(wb, data)
225
+ write_pasos_sheet(wb, data)
226
+ wb.save(output_path)
227
+
228
+ n_pasos = len(data.get("caso_prueba", {}).get("pasos", []))
229
+ print(f"OK: {input_path.name} -> {output_path.name} ({n_pasos} pasos, 2 hojas)")
230
+
231
+
232
+ if __name__ == "__main__":
233
+ main()
@@ -0,0 +1,172 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ export_yaml_to_markdown.py — Exporta test-design YAML a Markdown
4
+ Formato: Secciones ## con key: value + tabla pipe de pasos
5
+ Uso: python export_yaml_to_markdown.py <input.yml> [output.md]
6
+ """
7
+
8
+ import sys
9
+ from pathlib import Path
10
+
11
+ try:
12
+ import yaml
13
+ except ImportError:
14
+ print("ERROR: pyyaml requerido. Instalar con: pip install pyyaml")
15
+ sys.exit(1)
16
+
17
+
18
+ def flatten(value) -> str:
19
+ """Convierte listas y dicts a string con separador pipe."""
20
+ if isinstance(value, list):
21
+ return " | ".join(str(v) for v in value)
22
+ if isinstance(value, dict):
23
+ return " | ".join(f"{k}: {v}" for k, v in value.items())
24
+ return str(value).replace("\n", " ").strip()
25
+
26
+
27
+ def write_kv(f, key: str, value: str):
28
+ f.write(f"- **{key}**: {value}\n")
29
+
30
+
31
+ def export_markdown(data: dict) -> str:
32
+ lines = []
33
+ L = lines.append
34
+
35
+ caso = data.get("caso_prueba", {})
36
+ title = caso.get("nombre", "Test Design")
37
+ L(f"# {title}\n")
38
+
39
+ # --- METADATA ---
40
+ L("## Metadata\n")
41
+ meta = data.get("metadata", {})
42
+ for field in ("modulo", "version", "autor", "fecha", "stack"):
43
+ if field in meta:
44
+ L(f"- **{field}**: {meta[field]}")
45
+ L("")
46
+
47
+ # --- REQUISITO ---
48
+ L("## Requisito\n")
49
+ req = data.get("requisito", {})
50
+ for field in ("id", "nombre"):
51
+ if field in req:
52
+ L(f"- **{field}**: {req[field]}")
53
+ desc = req.get("descripcion", "")
54
+ if desc:
55
+ L(f"- **descripcion**: {flatten(desc)}")
56
+ criterios = req.get("criterios_aceptacion", [])
57
+ if criterios:
58
+ L(f"- **criterios_aceptacion**: {' | '.join(criterios)}")
59
+ L("")
60
+
61
+ # --- PLAN_PRUEBAS ---
62
+ L("## Plan de Pruebas\n")
63
+ plan = data.get("plan_pruebas", {})
64
+ for field in ("id", "nombre", "framework"):
65
+ if field in plan:
66
+ L(f"- **{field}**: {plan[field]}")
67
+ objetivo = plan.get("objetivo", "")
68
+ if objetivo:
69
+ L(f"- **objetivo**: {flatten(objetivo)}")
70
+ precond = plan.get("precondiciones", [])
71
+ if precond:
72
+ L(f"- **precondiciones**: {' | '.join(precond)}")
73
+ ambientes = plan.get("ambientes", [])
74
+ for i, amb in enumerate(ambientes):
75
+ for key in ("nombre", "url", "usuario"):
76
+ if key in amb:
77
+ L(f"- **ambiente_{i}.{key}**: {amb[key]}")
78
+ tiempos = plan.get("tiempos_referencia", {})
79
+ for key, val in tiempos.items():
80
+ L(f"- **tiempos.{key}**: {val}")
81
+ L("")
82
+
83
+ # --- CASO_PRUEBA ---
84
+ L("## Caso de Prueba\n")
85
+ for field in ("id", "nombre", "prioridad", "tipo"):
86
+ if field in caso:
87
+ L(f"- **{field}**: {caso[field]}")
88
+ scripts = caso.get("scripts", [])
89
+ if scripts:
90
+ L(f"- **scripts**: {' | '.join(scripts)}")
91
+ L("")
92
+
93
+ # --- EJECUCION ---
94
+ L("## Ejecucion\n")
95
+ ejec = data.get("ejecucion", {})
96
+ for field in ("id", "nombre"):
97
+ if field in ejec:
98
+ L(f"- **{field}**: {ejec[field]}")
99
+ variables = ejec.get("variables_entorno", [])
100
+ for var in variables:
101
+ nombre = var.get("nombre", "")
102
+ desc = var.get("descripcion", "")
103
+ default = var.get("default", "")
104
+ L(f"- **variable.{nombre}**: {desc} | {default}")
105
+ comandos = ejec.get("comandos", {})
106
+ for key, val in comandos.items():
107
+ L(f"- **comando.{key}**: {val}")
108
+ L("")
109
+
110
+ # --- INTEGRACIONES ---
111
+ integ = data.get("integraciones", {})
112
+ if integ:
113
+ L("## Integraciones\n")
114
+ for integ_name, info in integ.items():
115
+ for key, val in info.items():
116
+ L(f"- **{integ_name}.{key}**: {val}")
117
+ L("")
118
+
119
+ # --- PASOS ---
120
+ L("## Pasos\n")
121
+ pasos = caso.get("pasos", [])
122
+ if pasos:
123
+ L("| Paso | Nombre | Accion | Resultado Esperado | Datos | Script | Automatizado | Bloqueado | Nota |")
124
+ L("|------|--------|--------|--------------------|-------|--------|--------------|-----------|------|")
125
+ for p in pasos:
126
+ paso = str(p.get("paso", ""))
127
+ nombre = p.get("nombre", "")
128
+ accion = p.get("accion", "")
129
+ resultado = p.get("resultado", "")
130
+ datos = p.get("datos", "")
131
+ script = p.get("script", "")
132
+ auto = str(p.get("automatizado", False)).lower()
133
+ bloqueado = p.get("bloqueado", "")
134
+ nota = p.get("nota", "")
135
+ # Escapar pipes dentro de valores
136
+ vals = [paso, nombre, accion, resultado, datos, script, auto, bloqueado, nota]
137
+ vals = [v.replace("|", "\\|") for v in vals]
138
+ L(f"| {' | '.join(vals)} |")
139
+ L("")
140
+
141
+ return "\n".join(lines)
142
+
143
+
144
+ def main():
145
+ if len(sys.argv) < 2:
146
+ print("Uso: python export_yaml_to_markdown.py <input.yml> [output.md]")
147
+ sys.exit(1)
148
+
149
+ input_path = Path(sys.argv[1])
150
+ if not input_path.exists():
151
+ print(f"ERROR: No se encontro {input_path}")
152
+ sys.exit(1)
153
+
154
+ if len(sys.argv) >= 3:
155
+ output_path = Path(sys.argv[2])
156
+ else:
157
+ output_path = input_path.with_suffix(".md")
158
+
159
+ with open(input_path, "r", encoding="utf-8") as f:
160
+ data = yaml.safe_load(f)
161
+
162
+ md = export_markdown(data)
163
+
164
+ with open(output_path, "w", encoding="utf-8") as f:
165
+ f.write(md)
166
+
167
+ n_pasos = len(data.get("caso_prueba", {}).get("pasos", []))
168
+ print(f"OK: {input_path.name} -> {output_path.name} ({n_pasos} pasos)")
169
+
170
+
171
+ if __name__ == "__main__":
172
+ main()
@@ -0,0 +1,297 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ export_yaml_to_pdf.py — Exporta test-design YAML a PDF
4
+ Formato: Secciones tituladas con metadata key-value + tabla de pasos
5
+ Uso: python export_yaml_to_pdf.py <input.yml> [output.pdf]
6
+ """
7
+
8
+ import sys
9
+ from pathlib import Path
10
+
11
+ try:
12
+ import yaml
13
+ except ImportError:
14
+ print("ERROR: pyyaml requerido. Instalar con: pip install pyyaml")
15
+ sys.exit(1)
16
+
17
+ try:
18
+ from reportlab.lib import colors
19
+ from reportlab.lib.pagesizes import letter, landscape
20
+ from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
21
+ from reportlab.lib.units import inch, cm
22
+ from reportlab.platypus import (
23
+ SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle, PageBreak,
24
+ )
25
+ except ImportError:
26
+ print("ERROR: reportlab requerido. Instalar con: pip install reportlab")
27
+ sys.exit(1)
28
+
29
+
30
+ # --- Estilos Siesa ---
31
+ TEAL = colors.HexColor("#009688")
32
+ LIGHT_GRAY = colors.HexColor("#F5F5F5")
33
+ WHITE = colors.white
34
+
35
+
36
+ def get_styles():
37
+ styles = getSampleStyleSheet()
38
+ styles.add(ParagraphStyle(
39
+ "SectionTitle",
40
+ parent=styles["Heading2"],
41
+ textColor=TEAL,
42
+ spaceAfter=6,
43
+ spaceBefore=12,
44
+ fontSize=13,
45
+ fontName="Helvetica-Bold",
46
+ ))
47
+ styles.add(ParagraphStyle(
48
+ "MetaKey",
49
+ parent=styles["Normal"],
50
+ fontName="Helvetica-Bold",
51
+ fontSize=9,
52
+ ))
53
+ styles.add(ParagraphStyle(
54
+ "MetaValue",
55
+ parent=styles["Normal"],
56
+ fontSize=9,
57
+ ))
58
+ styles.add(ParagraphStyle(
59
+ "CellText",
60
+ parent=styles["Normal"],
61
+ fontSize=7,
62
+ leading=9,
63
+ ))
64
+ styles.add(ParagraphStyle(
65
+ "DocTitle",
66
+ parent=styles["Title"],
67
+ textColor=TEAL,
68
+ fontSize=16,
69
+ fontName="Helvetica-Bold",
70
+ ))
71
+ return styles
72
+
73
+
74
+ def build_meta_table(rows: list[tuple[str, str]], styles) -> Table:
75
+ """Construye tabla de metadata key-value."""
76
+ data = []
77
+ for key, value in rows:
78
+ data.append([
79
+ Paragraph(key, styles["MetaKey"]),
80
+ Paragraph(str(value), styles["MetaValue"]),
81
+ ])
82
+ t = Table(data, colWidths=[2.5 * inch, 5.5 * inch])
83
+ t.setStyle(TableStyle([
84
+ ("VALIGN", (0, 0), (-1, -1), "TOP"),
85
+ ("TOPPADDING", (0, 0), (-1, -1), 2),
86
+ ("BOTTOMPADDING", (0, 0), (-1, -1), 2),
87
+ ("LINEBELOW", (0, 0), (-1, -1), 0.5, colors.HexColor("#E0E0E0")),
88
+ ]))
89
+ return t
90
+
91
+
92
+ def flatten_for_pdf(value) -> str:
93
+ if isinstance(value, list):
94
+ return " | ".join(str(v) for v in value)
95
+ if isinstance(value, dict):
96
+ return " | ".join(f"{k}: {v}" for k, v in value.items())
97
+ return str(value).strip()
98
+
99
+
100
+ def build_document(data: dict, styles) -> list:
101
+ """Construye los elementos del PDF."""
102
+ elements = []
103
+
104
+ # Titulo
105
+ caso = data.get("caso_prueba", {})
106
+ title = caso.get("nombre", "Test Design")
107
+ elements.append(Paragraph(title, styles["DocTitle"]))
108
+ elements.append(Spacer(1, 12))
109
+
110
+ # --- [METADATA] ---
111
+ elements.append(Paragraph("[METADATA]", styles["SectionTitle"]))
112
+ meta = data.get("metadata", {})
113
+ meta_rows = []
114
+ for field in ("modulo", "version", "autor", "fecha", "stack"):
115
+ if field in meta:
116
+ meta_rows.append((field, meta[field]))
117
+ if meta_rows:
118
+ elements.append(build_meta_table(meta_rows, styles))
119
+ elements.append(Spacer(1, 8))
120
+
121
+ # --- [REQUISITO] ---
122
+ elements.append(Paragraph("[REQUISITO]", styles["SectionTitle"]))
123
+ req = data.get("requisito", {})
124
+ req_rows = []
125
+ for field in ("id", "nombre", "descripcion"):
126
+ if field in req:
127
+ req_rows.append((field, flatten_for_pdf(req[field])))
128
+ criterios = req.get("criterios_aceptacion", [])
129
+ if criterios:
130
+ req_rows.append(("criterios_aceptacion", " | ".join(criterios)))
131
+ if req_rows:
132
+ elements.append(build_meta_table(req_rows, styles))
133
+ elements.append(Spacer(1, 8))
134
+
135
+ # --- [PLAN_PRUEBAS] ---
136
+ elements.append(Paragraph("[PLAN_PRUEBAS]", styles["SectionTitle"]))
137
+ plan = data.get("plan_pruebas", {})
138
+ plan_rows = []
139
+ for field in ("id", "nombre", "objetivo", "framework"):
140
+ if field in plan:
141
+ plan_rows.append((field, flatten_for_pdf(plan[field])))
142
+ precond = plan.get("precondiciones", [])
143
+ if precond:
144
+ plan_rows.append(("precondiciones", " | ".join(precond)))
145
+ ambientes = plan.get("ambientes", [])
146
+ for i, amb in enumerate(ambientes):
147
+ for key in ("nombre", "url", "usuario"):
148
+ if key in amb:
149
+ plan_rows.append((f"ambiente_{i}.{key}", str(amb[key])))
150
+ tiempos = plan.get("tiempos_referencia", {})
151
+ for key, val in tiempos.items():
152
+ plan_rows.append((f"tiempos.{key}", str(val)))
153
+ if plan_rows:
154
+ elements.append(build_meta_table(plan_rows, styles))
155
+ elements.append(Spacer(1, 8))
156
+
157
+ # --- [CASO_PRUEBA] ---
158
+ elements.append(Paragraph("[CASO_PRUEBA]", styles["SectionTitle"]))
159
+ caso_rows = []
160
+ for field in ("id", "nombre", "prioridad", "tipo"):
161
+ if field in caso:
162
+ caso_rows.append((field, caso[field]))
163
+ scripts = caso.get("scripts", [])
164
+ if scripts:
165
+ caso_rows.append(("scripts", " | ".join(scripts)))
166
+ if caso_rows:
167
+ elements.append(build_meta_table(caso_rows, styles))
168
+ elements.append(Spacer(1, 8))
169
+
170
+ # --- [EJECUCION] ---
171
+ elements.append(Paragraph("[EJECUCION]", styles["SectionTitle"]))
172
+ ejec = data.get("ejecucion", {})
173
+ ejec_rows = []
174
+ for field in ("id", "nombre"):
175
+ if field in ejec:
176
+ ejec_rows.append((field, ejec[field]))
177
+ variables = ejec.get("variables_entorno", [])
178
+ for var in variables:
179
+ nombre = var.get("nombre", "")
180
+ desc = var.get("descripcion", "")
181
+ default = var.get("default", "")
182
+ ejec_rows.append((f"variable.{nombre}", f"{desc} | {default}"))
183
+ comandos = ejec.get("comandos", {})
184
+ for key, val in comandos.items():
185
+ ejec_rows.append((f"comando.{key}", str(val)))
186
+ if ejec_rows:
187
+ elements.append(build_meta_table(ejec_rows, styles))
188
+ elements.append(Spacer(1, 8))
189
+
190
+ # --- [INTEGRACIONES] ---
191
+ integ = data.get("integraciones", {})
192
+ if integ:
193
+ elements.append(Paragraph("[INTEGRACIONES]", styles["SectionTitle"]))
194
+ integ_rows = []
195
+ for integ_name, info in integ.items():
196
+ for key, val in info.items():
197
+ integ_rows.append((f"{integ_name}.{key}", str(val)))
198
+ elements.append(build_meta_table(integ_rows, styles))
199
+ elements.append(Spacer(1, 8))
200
+
201
+ # --- [PASOS] --- (pagina landscape)
202
+ elements.append(PageBreak())
203
+ elements.append(Paragraph("[PASOS]", styles["SectionTitle"]))
204
+
205
+ pasos = caso.get("pasos", [])
206
+ if pasos:
207
+ headers = ["Paso", "Nombre", "Accion", "Resultado Esperado",
208
+ "Datos", "Script", "Auto", "Bloqueado", "Nota"]
209
+ table_data = [[Paragraph(h, styles["MetaKey"]) for h in headers]]
210
+
211
+ for p in pasos:
212
+ row = [
213
+ Paragraph(str(p.get("paso", "")), styles["CellText"]),
214
+ Paragraph(str(p.get("nombre", "")), styles["CellText"]),
215
+ Paragraph(str(p.get("accion", "")), styles["CellText"]),
216
+ Paragraph(str(p.get("resultado", "")), styles["CellText"]),
217
+ Paragraph(str(p.get("datos", "")), styles["CellText"]),
218
+ Paragraph(str(p.get("script", "")), styles["CellText"]),
219
+ Paragraph(str(p.get("automatizado", False)), styles["CellText"]),
220
+ Paragraph(str(p.get("bloqueado", "")), styles["CellText"]),
221
+ Paragraph(str(p.get("nota", "")), styles["CellText"]),
222
+ ]
223
+ table_data.append(row)
224
+
225
+ col_widths = [
226
+ 0.5 * inch, # paso
227
+ 1.2 * inch, # nombre
228
+ 2.2 * inch, # accion
229
+ 1.6 * inch, # resultado
230
+ 1.6 * inch, # datos
231
+ 1.0 * inch, # script
232
+ 0.5 * inch, # auto
233
+ 1.2 * inch, # bloqueado
234
+ 1.0 * inch, # nota
235
+ ]
236
+
237
+ t = Table(table_data, colWidths=col_widths, repeatRows=1)
238
+ style_cmds = [
239
+ ("BACKGROUND", (0, 0), (-1, 0), TEAL),
240
+ ("TEXTCOLOR", (0, 0), (-1, 0), WHITE),
241
+ ("FONTSIZE", (0, 0), (-1, 0), 8),
242
+ ("FONTSIZE", (0, 1), (-1, -1), 7),
243
+ ("VALIGN", (0, 0), (-1, -1), "TOP"),
244
+ ("GRID", (0, 0), (-1, -1), 0.5, colors.HexColor("#BDBDBD")),
245
+ ("TOPPADDING", (0, 0), (-1, -1), 3),
246
+ ("BOTTOMPADDING", (0, 0), (-1, -1), 3),
247
+ ("LEFTPADDING", (0, 0), (-1, -1), 3),
248
+ ("RIGHTPADDING", (0, 0), (-1, -1), 3),
249
+ ]
250
+ # Filas alternas
251
+ for i in range(1, len(table_data)):
252
+ if i % 2 == 0:
253
+ style_cmds.append(("BACKGROUND", (0, i), (-1, i), LIGHT_GRAY))
254
+
255
+ t.setStyle(TableStyle(style_cmds))
256
+ elements.append(t)
257
+
258
+ return elements
259
+
260
+
261
+ def main():
262
+ if len(sys.argv) < 2:
263
+ print("Uso: python export_yaml_to_pdf.py <input.yml> [output.pdf]")
264
+ sys.exit(1)
265
+
266
+ input_path = Path(sys.argv[1])
267
+ if not input_path.exists():
268
+ print(f"ERROR: No se encontro {input_path}")
269
+ sys.exit(1)
270
+
271
+ if len(sys.argv) >= 3:
272
+ output_path = Path(sys.argv[2])
273
+ else:
274
+ output_path = input_path.with_suffix(".pdf")
275
+
276
+ with open(input_path, "r", encoding="utf-8") as f:
277
+ data = yaml.safe_load(f)
278
+
279
+ styles = get_styles()
280
+ elements = build_document(data, styles)
281
+
282
+ doc = SimpleDocTemplate(
283
+ str(output_path),
284
+ pagesize=landscape(letter),
285
+ leftMargin=1 * cm,
286
+ rightMargin=1 * cm,
287
+ topMargin=1.5 * cm,
288
+ bottomMargin=1.5 * cm,
289
+ )
290
+ doc.build(elements)
291
+
292
+ n_pasos = len(data.get("caso_prueba", {}).get("pasos", []))
293
+ print(f"OK: {input_path.name} -> {output_path.name} ({n_pasos} pasos)")
294
+
295
+
296
+ if __name__ == "__main__":
297
+ main()