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.
- package/bmad/bmm/config.yaml +33 -0
- package/claude/skills/sa-agent-sre-sentinel/SKILL.md +180 -0
- package/claude/skills/sa-aplicar/SKILL.md +268 -0
- package/claude/skills/sa-auditar-servicio/SKILL.md +255 -0
- package/claude/skills/sa-nueva-transversal/SKILL.md +317 -0
- package/claude/skills/sa-nuevo-ambiente/SKILL.md +147 -0
- package/claude/skills/sa-nuevo-servicio/SKILL.md +530 -0
- package/claude/skills/sa-onboard-db/SKILL.md +122 -0
- package/claude/skills/sa-qa-data-generator/SKILL.md +200 -51
- package/claude/skills/sa-quality-process/SKILL.md +6 -0
- package/claude/skills/sa-registrar-permisos/SKILL.md +233 -0
- package/package.json +1 -1
- package/siesa-agents/bmm/workflows/3-solutioning/quality-process/prompts/prompt_design_test.md +1037 -60
- package/siesa-agents/bmm/workflows/3-solutioning/quality-process/prompts/prompt_dor_gate.md +419 -379
- package/siesa-agents/bmm/workflows/3-solutioning/quality-process/prompts/prompt_e2e_executor.md +273 -0
- package/siesa-agents/bmm/workflows/3-solutioning/quality-process/prompts/prompt_playwright_impl.md +359 -355
- package/siesa-agents/bmm/workflows/3-solutioning/quality-process/prompts/prompt_test_plan.md +73 -111
- package/siesa-agents/bmm/workflows/3-solutioning/quality-process/workflow.md +1320 -997
- package/siesa-agents/resources/playwright-kit/.env.example +42 -0
- package/siesa-agents/resources/playwright-kit/README.md +228 -0
- package/siesa-agents/resources/playwright-kit/allure/categories.json +26 -0
- package/siesa-agents/resources/playwright-kit/global-setup.ts +34 -0
- package/siesa-agents/resources/playwright-kit/helpers/blazor-e2e-helpers.ts +417 -0
- package/siesa-agents/resources/playwright-kit/helpers/react-e2e-helpers.ts +297 -0
- package/siesa-agents/resources/playwright-kit/package-lock.json +850 -0
- package/siesa-agents/resources/playwright-kit/package.json +27 -0
- package/siesa-agents/resources/playwright-kit/playwright.config.ts +60 -0
- package/siesa-agents/resources/playwright-kit/proposed/.gitkeep +0 -0
- package/siesa-agents/resources/playwright-kit/reporters/agiletest-reporter.ts +252 -0
- package/siesa-agents/resources/playwright-kit/specs/.gitkeep +0 -0
- package/siesa-agents/resources/playwright-kit/specs/architecture-brief-siesa-erp.md +85 -0
- package/siesa-agents/resources/playwright-kit/specs/architecture-brief-siesa-react.md +109 -0
- package/siesa-agents/resources/playwright-kit/specs/architecture-brief-template.md +69 -0
- package/siesa-agents/resources/playwright-kit/tests/seed-example.spec.ts +95 -0
- package/siesa-agents/resources/playwright-kit/tools/locator-overlay.js +1306 -0
- package/siesa-agents/resources/playwright-kit/tools/locator-recorder.spec.ts +986 -0
- package/siesa-agents/scripts/__pycache__/bmad_to_agiletest.cpython-36.pyc +0 -0
- package/siesa-agents/scripts/bmad_to_agiletest.py +352 -337
- package/siesa-agents/scripts/converters/convert_csv_to_test_design.py +327 -0
- package/siesa-agents/scripts/converters/convert_excel_to_test_design.py +360 -0
- package/siesa-agents/scripts/converters/convert_md_to_test_design.py +359 -0
- package/siesa-agents/scripts/converters/convert_pdf_to_test_design.py +412 -0
- package/siesa-agents/scripts/converters/export_yaml_to_csv.py +149 -0
- package/siesa-agents/scripts/converters/export_yaml_to_excel.py +233 -0
- package/siesa-agents/scripts/converters/export_yaml_to_markdown.py +172 -0
- package/siesa-agents/scripts/converters/export_yaml_to_pdf.py +297 -0
- package/siesa-agents/scripts/merge_test_design.py +106 -106
- package/siesa-agents/scripts/playwright/yaml-to-playwright-spec.ts +203 -0
- 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()
|