kctl-react 0.6.2__py3-none-any.whl
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.
- kctl_react/__init__.py +3 -0
- kctl_react/__main__.py +5 -0
- kctl_react/cli.py +201 -0
- kctl_react/commands/__init__.py +0 -0
- kctl_react/commands/a11y.py +78 -0
- kctl_react/commands/affected.py +170 -0
- kctl_react/commands/apps.py +353 -0
- kctl_react/commands/build.py +376 -0
- kctl_react/commands/bundle_cmd.py +217 -0
- kctl_react/commands/cap.py +1465 -0
- kctl_react/commands/clean.py +76 -0
- kctl_react/commands/codegen.py +491 -0
- kctl_react/commands/compliance.py +587 -0
- kctl_react/commands/config_cmd.py +368 -0
- kctl_react/commands/dashboard.py +163 -0
- kctl_react/commands/deploy.py +318 -0
- kctl_react/commands/deps.py +792 -0
- kctl_react/commands/dev.py +96 -0
- kctl_react/commands/docker_cmd.py +73 -0
- kctl_react/commands/doctor.py +170 -0
- kctl_react/commands/e2e.py +343 -0
- kctl_react/commands/env.py +155 -0
- kctl_react/commands/i18n.py +310 -0
- kctl_react/commands/lint.py +306 -0
- kctl_react/commands/maintenance.py +308 -0
- kctl_react/commands/monitor_cmd.py +50 -0
- kctl_react/commands/observe.py +34 -0
- kctl_react/commands/packages.py +129 -0
- kctl_react/commands/perf.py +762 -0
- kctl_react/commands/pipeline.py +289 -0
- kctl_react/commands/pwa.py +193 -0
- kctl_react/commands/scaffold.py +323 -0
- kctl_react/commands/security.py +660 -0
- kctl_react/commands/skill_cmd.py +54 -0
- kctl_react/commands/state.py +254 -0
- kctl_react/commands/test_cmd.py +418 -0
- kctl_react/commands/ui_audit.py +889 -0
- kctl_react/core/__init__.py +0 -0
- kctl_react/core/analyzers.py +200 -0
- kctl_react/core/callbacks.py +70 -0
- kctl_react/core/compliance/__init__.py +3 -0
- kctl_react/core/compliance/api_check/__init__.py +3 -0
- kctl_react/core/compliance/api_check/checks/__init__.py +18 -0
- kctl_react/core/compliance/api_check/checks/endpoints.py +53 -0
- kctl_react/core/compliance/api_check/checks/envelope.py +44 -0
- kctl_react/core/compliance/api_check/checks/naming.py +60 -0
- kctl_react/core/compliance/api_check/checks/params.py +44 -0
- kctl_react/core/compliance/api_check/checks/requests.py +57 -0
- kctl_react/core/compliance/api_check/checks/types.py +55 -0
- kctl_react/core/compliance/api_check/hooks.py +133 -0
- kctl_react/core/compliance/api_check/matcher.py +55 -0
- kctl_react/core/compliance/api_check/schema.py +151 -0
- kctl_react/core/compliance/api_health/__init__.py +35 -0
- kctl_react/core/compliance/api_health/checks/__init__.py +9 -0
- kctl_react/core/compliance/api_health/checks/auth.py +72 -0
- kctl_react/core/compliance/api_health/checks/reachable.py +44 -0
- kctl_react/core/compliance/api_health/checks/response.py +55 -0
- kctl_react/core/compliance/api_health/checks/timing.py +38 -0
- kctl_react/core/compliance/api_health/client.py +99 -0
- kctl_react/core/compliance/api_health/sampler.py +16 -0
- kctl_react/core/compliance/checks/__init__.py +47 -0
- kctl_react/core/compliance/checks/api.py +101 -0
- kctl_react/core/compliance/checks/codegen.py +94 -0
- kctl_react/core/compliance/checks/darkmode.py +57 -0
- kctl_react/core/compliance/checks/errors.py +68 -0
- kctl_react/core/compliance/checks/features.py +66 -0
- kctl_react/core/compliance/checks/i18n_check.py +105 -0
- kctl_react/core/compliance/checks/imports.py +86 -0
- kctl_react/core/compliance/checks/navigation.py +62 -0
- kctl_react/core/compliance/checks/practices.py +122 -0
- kctl_react/core/compliance/checks/providers.py +85 -0
- kctl_react/core/compliance/checks/pwa.py +101 -0
- kctl_react/core/compliance/checks/responsive.py +47 -0
- kctl_react/core/compliance/checks/scripts.py +85 -0
- kctl_react/core/compliance/checks/shadcn.py +51 -0
- kctl_react/core/compliance/checks/structure.py +76 -0
- kctl_react/core/compliance/checks/testing.py +83 -0
- kctl_react/core/compliance/checks/theme.py +92 -0
- kctl_react/core/compliance/checks/ui_standard.py +185 -0
- kctl_react/core/compliance/checks/vite.py +83 -0
- kctl_react/core/compliance/engine.py +87 -0
- kctl_react/core/compliance/exceptions_map.py +15 -0
- kctl_react/core/compliance/fixes/__init__.py +33 -0
- kctl_react/core/compliance/fixes/codegen_fix.py +28 -0
- kctl_react/core/compliance/fixes/i18n_fix.py +61 -0
- kctl_react/core/compliance/fixes/imports_fix.py +36 -0
- kctl_react/core/compliance/fixes/structure_fix.py +20 -0
- kctl_react/core/compliance/fixes/theme_fix.py +29 -0
- kctl_react/core/compliance/models.py +106 -0
- kctl_react/core/config.py +201 -0
- kctl_react/core/discovery.py +185 -0
- kctl_react/core/exceptions.py +17 -0
- kctl_react/core/git.py +146 -0
- kctl_react/core/history.py +121 -0
- kctl_react/core/output.py +5 -0
- kctl_react/core/plugins.py +13 -0
- kctl_react/core/runner.py +34 -0
- kctl_react/py.typed +0 -0
- kctl_react-0.6.2.dist-info/METADATA +17 -0
- kctl_react-0.6.2.dist-info/RECORD +102 -0
- kctl_react-0.6.2.dist-info/WHEEL +4 -0
- kctl_react-0.6.2.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
"""Test commands with deep Vitest integration."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import contextlib
|
|
6
|
+
import json
|
|
7
|
+
import signal
|
|
8
|
+
import subprocess
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Annotated
|
|
11
|
+
|
|
12
|
+
import typer
|
|
13
|
+
|
|
14
|
+
from kctl_react.core.callbacks import AppContext
|
|
15
|
+
from kctl_react.core.discovery import get_app_dir
|
|
16
|
+
from kctl_react.core.runner import run, run_turbo
|
|
17
|
+
|
|
18
|
+
app = typer.Typer(help="Run tests across the monorepo.")
|
|
19
|
+
|
|
20
|
+
_SUBCOMMANDS = {"count", "summary", "coverage", "naming", "threshold", "snapshots"}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _parse_vitest_json(json_path: Path) -> dict | None:
|
|
24
|
+
"""Parse vitest JSON reporter output."""
|
|
25
|
+
if not json_path.exists():
|
|
26
|
+
return None
|
|
27
|
+
try:
|
|
28
|
+
return json.loads(json_path.read_text())
|
|
29
|
+
except Exception:
|
|
30
|
+
return None
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _run_vitest_json(app_dir: Path, timeout: int = 300) -> dict | None:
|
|
34
|
+
"""Run vitest with JSON reporter and return parsed results."""
|
|
35
|
+
json_output = app_dir / ".vitest-results.json"
|
|
36
|
+
json_output.unlink(missing_ok=True)
|
|
37
|
+
|
|
38
|
+
with contextlib.suppress(Exception):
|
|
39
|
+
run(
|
|
40
|
+
["pnpm", "vitest", "run", "--reporter=json", f"--outputFile={json_output}"],
|
|
41
|
+
cwd=app_dir,
|
|
42
|
+
capture=True,
|
|
43
|
+
timeout=timeout,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
return _parse_vitest_json(json_output)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@app.callback(invoke_without_command=True)
|
|
50
|
+
def test(
|
|
51
|
+
ctx: typer.Context,
|
|
52
|
+
app_name: Annotated[str | None, typer.Option("--app", "-a", help="App name (omit for all apps)")] = None,
|
|
53
|
+
coverage_flag: Annotated[bool, typer.Option("--coverage", "-c", help="Run with coverage.")] = False,
|
|
54
|
+
watch: Annotated[bool, typer.Option("--watch", "-w", help="Run in watch mode.")] = False,
|
|
55
|
+
) -> None:
|
|
56
|
+
"""Run vitest for app(s).
|
|
57
|
+
|
|
58
|
+
Examples:
|
|
59
|
+
kctl-react test --app sfa # Test SFA
|
|
60
|
+
kctl-react test --coverage # All apps with coverage
|
|
61
|
+
kctl-react test --app sfa --watch # Watch mode for SFA
|
|
62
|
+
"""
|
|
63
|
+
if ctx.invoked_subcommand is not None:
|
|
64
|
+
return
|
|
65
|
+
|
|
66
|
+
actx: AppContext = ctx.obj
|
|
67
|
+
out = actx.output
|
|
68
|
+
root = actx.project_root
|
|
69
|
+
|
|
70
|
+
if app_name:
|
|
71
|
+
actx.validate_app(app_name)
|
|
72
|
+
|
|
73
|
+
if watch and not app_name:
|
|
74
|
+
out.warn("--watch requires an app name (e.g. kctl-react test sfa --watch)")
|
|
75
|
+
raise typer.Exit(1) from None
|
|
76
|
+
|
|
77
|
+
task = "test:ci" if coverage_flag else "test"
|
|
78
|
+
target = app_name or "all apps"
|
|
79
|
+
out.info(f"Running tests for {target}{' (coverage)' if coverage_flag else ''}...")
|
|
80
|
+
|
|
81
|
+
if watch and app_name:
|
|
82
|
+
app_dir = get_app_dir(root, app_name)
|
|
83
|
+
try:
|
|
84
|
+
proc = subprocess.Popen(["pnpm", "vitest", "--watch"], cwd=app_dir)
|
|
85
|
+
proc.wait()
|
|
86
|
+
except KeyboardInterrupt:
|
|
87
|
+
proc.send_signal(signal.SIGINT)
|
|
88
|
+
proc.wait()
|
|
89
|
+
return
|
|
90
|
+
|
|
91
|
+
try:
|
|
92
|
+
run_turbo(task, root, filter_app=app_name, capture=False, timeout=600)
|
|
93
|
+
out.success(f"Tests passed: {target}")
|
|
94
|
+
except Exception as e:
|
|
95
|
+
out.error(f"Tests failed: {e}")
|
|
96
|
+
raise typer.Exit(1) from None
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@app.command()
|
|
100
|
+
def count(ctx: typer.Context) -> None:
|
|
101
|
+
"""Count test files per app."""
|
|
102
|
+
actx: AppContext = ctx.obj
|
|
103
|
+
out = actx.output
|
|
104
|
+
root = actx.project_root
|
|
105
|
+
|
|
106
|
+
rows: list[list[str]] = []
|
|
107
|
+
json_data: list[dict] = []
|
|
108
|
+
total = 0
|
|
109
|
+
|
|
110
|
+
for name in actx.app_names:
|
|
111
|
+
app_dir = get_app_dir(root, name) / "src"
|
|
112
|
+
if not app_dir.is_dir():
|
|
113
|
+
rows.append([name, "0", "[dim]no src[/dim]"])
|
|
114
|
+
continue
|
|
115
|
+
|
|
116
|
+
test_files = list(app_dir.rglob("*.test.ts")) + list(app_dir.rglob("*.test.tsx"))
|
|
117
|
+
file_count = len(test_files)
|
|
118
|
+
total += file_count
|
|
119
|
+
|
|
120
|
+
rows.append([name, str(file_count), "[green]OK[/green]" if file_count > 0 else "[yellow]no tests[/yellow]"])
|
|
121
|
+
json_data.append({"app": name, "test_files": file_count})
|
|
122
|
+
|
|
123
|
+
rows.append(["[bold]Total[/bold]", f"[bold]{total}[/bold]", ""])
|
|
124
|
+
|
|
125
|
+
out.table(
|
|
126
|
+
"Test Files",
|
|
127
|
+
[("App", "cyan"), ("Files", "green"), ("Status", "")],
|
|
128
|
+
rows,
|
|
129
|
+
data_for_json=json_data,
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
@app.command()
|
|
134
|
+
def summary(
|
|
135
|
+
ctx: typer.Context,
|
|
136
|
+
app_name: Annotated[str | None, typer.Argument(help="App name (omit for all)")] = None,
|
|
137
|
+
) -> None:
|
|
138
|
+
"""Run tests with JSON reporter and show parsed summary.
|
|
139
|
+
|
|
140
|
+
Shows pass/fail/skip counts per app by parsing vitest JSON output.
|
|
141
|
+
"""
|
|
142
|
+
actx: AppContext = ctx.obj
|
|
143
|
+
out = actx.output
|
|
144
|
+
root = actx.project_root
|
|
145
|
+
|
|
146
|
+
if app_name:
|
|
147
|
+
actx.validate_app(app_name)
|
|
148
|
+
|
|
149
|
+
apps = [app_name] if app_name else actx.app_names
|
|
150
|
+
|
|
151
|
+
rows: list[list[str]] = []
|
|
152
|
+
json_data: list[dict] = []
|
|
153
|
+
total_passed = 0
|
|
154
|
+
total_failed = 0
|
|
155
|
+
total_skipped = 0
|
|
156
|
+
|
|
157
|
+
for name in apps:
|
|
158
|
+
app_dir = get_app_dir(root, name)
|
|
159
|
+
if not (app_dir / "src").is_dir():
|
|
160
|
+
continue
|
|
161
|
+
|
|
162
|
+
out.info(f"Testing {name}...")
|
|
163
|
+
results = _run_vitest_json(app_dir)
|
|
164
|
+
|
|
165
|
+
if results is None:
|
|
166
|
+
rows.append([name, "[dim]--[/dim]", "[dim]--[/dim]", "[dim]--[/dim]", "[yellow]no output[/yellow]"])
|
|
167
|
+
json_data.append({"app": name, "passed": 0, "failed": 0, "skipped": 0, "error": True})
|
|
168
|
+
continue
|
|
169
|
+
|
|
170
|
+
passed = results.get("numPassedTests", 0)
|
|
171
|
+
failed = results.get("numFailedTests", 0)
|
|
172
|
+
skipped = results.get("numPendingTests", 0)
|
|
173
|
+
total_passed += passed
|
|
174
|
+
total_failed += failed
|
|
175
|
+
total_skipped += skipped
|
|
176
|
+
|
|
177
|
+
status = "[green]PASS[/green]" if failed == 0 else "[red]FAIL[/red]"
|
|
178
|
+
rows.append(
|
|
179
|
+
[
|
|
180
|
+
name,
|
|
181
|
+
f"[green]{passed}[/green]",
|
|
182
|
+
f"[red]{failed}[/red]" if failed else "[dim]0[/dim]",
|
|
183
|
+
f"[yellow]{skipped}[/yellow]" if skipped else "[dim]0[/dim]",
|
|
184
|
+
status,
|
|
185
|
+
]
|
|
186
|
+
)
|
|
187
|
+
json_data.append({"app": name, "passed": passed, "failed": failed, "skipped": skipped})
|
|
188
|
+
|
|
189
|
+
# Show failure details
|
|
190
|
+
if failed > 0:
|
|
191
|
+
for suite in results.get("testResults", []):
|
|
192
|
+
for test_case in suite.get("assertionResults", []):
|
|
193
|
+
if test_case.get("status") == "failed":
|
|
194
|
+
test_name = " > ".join(test_case.get("ancestorTitles", []) + [test_case.get("title", "")])
|
|
195
|
+
out.error(f" {name}: {test_name}")
|
|
196
|
+
|
|
197
|
+
rows.append(
|
|
198
|
+
[
|
|
199
|
+
"[bold]Total[/bold]",
|
|
200
|
+
f"[bold green]{total_passed}[/bold green]",
|
|
201
|
+
f"[bold red]{total_failed}[/bold red]" if total_failed else "[bold dim]0[/bold dim]",
|
|
202
|
+
f"[bold yellow]{total_skipped}[/bold yellow]" if total_skipped else "[bold dim]0[/bold dim]",
|
|
203
|
+
"",
|
|
204
|
+
]
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
out.table(
|
|
208
|
+
"Test Summary",
|
|
209
|
+
[("App", "cyan"), ("Passed", "green"), ("Failed", "red"), ("Skipped", "yellow"), ("Status", "")],
|
|
210
|
+
rows,
|
|
211
|
+
data_for_json=json_data,
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
@app.command()
|
|
216
|
+
def coverage(ctx: typer.Context) -> None:
|
|
217
|
+
"""Show aggregated test coverage across all apps.
|
|
218
|
+
|
|
219
|
+
Reads coverage-summary.json from each app's coverage/ directory.
|
|
220
|
+
Run `kctl-react test --coverage` first to generate coverage data.
|
|
221
|
+
"""
|
|
222
|
+
actx: AppContext = ctx.obj
|
|
223
|
+
out = actx.output
|
|
224
|
+
root = actx.project_root
|
|
225
|
+
|
|
226
|
+
rows: list[list[str]] = []
|
|
227
|
+
json_data: list[dict] = []
|
|
228
|
+
|
|
229
|
+
for name in actx.app_names:
|
|
230
|
+
cov_file = get_app_dir(root, name) / "coverage" / "coverage-summary.json"
|
|
231
|
+
if not cov_file.exists():
|
|
232
|
+
rows.append(
|
|
233
|
+
[name, "[dim]--[/dim]", "[dim]--[/dim]", "[dim]--[/dim]", "[dim]--[/dim]", "[dim]no data[/dim]"]
|
|
234
|
+
)
|
|
235
|
+
json_data.append({"app": name, "lines": None, "branches": None, "functions": None, "statements": None})
|
|
236
|
+
continue
|
|
237
|
+
|
|
238
|
+
try:
|
|
239
|
+
data = json.loads(cov_file.read_text())
|
|
240
|
+
total = data.get("total", {})
|
|
241
|
+
lines = total.get("lines", {}).get("pct", 0)
|
|
242
|
+
branches = total.get("branches", {}).get("pct", 0)
|
|
243
|
+
functions = total.get("functions", {}).get("pct", 0)
|
|
244
|
+
statements = total.get("statements", {}).get("pct", 0)
|
|
245
|
+
|
|
246
|
+
def _color_pct(pct: float) -> str:
|
|
247
|
+
if pct >= 80:
|
|
248
|
+
return f"[green]{pct:.1f}%[/green]"
|
|
249
|
+
elif pct >= 60:
|
|
250
|
+
return f"[yellow]{pct:.1f}%[/yellow]"
|
|
251
|
+
return f"[red]{pct:.1f}%[/red]"
|
|
252
|
+
|
|
253
|
+
rows.append(
|
|
254
|
+
[name, _color_pct(lines), _color_pct(branches), _color_pct(functions), _color_pct(statements), ""]
|
|
255
|
+
)
|
|
256
|
+
json_data.append(
|
|
257
|
+
{"app": name, "lines": lines, "branches": branches, "functions": functions, "statements": statements}
|
|
258
|
+
)
|
|
259
|
+
except Exception:
|
|
260
|
+
rows.append([name, "[red]error[/red]", "", "", "", ""])
|
|
261
|
+
json_data.append({"app": name, "error": True})
|
|
262
|
+
|
|
263
|
+
out.table(
|
|
264
|
+
"Test Coverage",
|
|
265
|
+
[("App", "cyan"), ("Lines", ""), ("Branches", ""), ("Functions", ""), ("Stmts", ""), ("", "")],
|
|
266
|
+
rows,
|
|
267
|
+
data_for_json=json_data,
|
|
268
|
+
)
|
|
269
|
+
out.info("Run `kctl-react test --coverage` first to generate coverage data")
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
@app.command()
|
|
273
|
+
def naming(ctx: typer.Context) -> None:
|
|
274
|
+
"""Check test file naming conventions across all apps.
|
|
275
|
+
|
|
276
|
+
Flags .spec.* files as wrong convention — all tests should use .test.* suffix.
|
|
277
|
+
"""
|
|
278
|
+
actx: AppContext = ctx.obj
|
|
279
|
+
out = actx.output
|
|
280
|
+
root = actx.project_root
|
|
281
|
+
|
|
282
|
+
rows: list[list[str]] = []
|
|
283
|
+
json_data: list[dict] = []
|
|
284
|
+
issues: list[str] = []
|
|
285
|
+
|
|
286
|
+
for name in actx.app_names:
|
|
287
|
+
src_dir = get_app_dir(root, name) / "src"
|
|
288
|
+
if not src_dir.is_dir():
|
|
289
|
+
rows.append([name, "[dim]--[/dim]", "[dim]--[/dim]", "[dim]no src[/dim]"])
|
|
290
|
+
json_data.append({"app": name, "test_count": 0, "spec_count": 0})
|
|
291
|
+
continue
|
|
292
|
+
|
|
293
|
+
test_files = list(src_dir.rglob("*.test.ts")) + list(src_dir.rglob("*.test.tsx"))
|
|
294
|
+
spec_files = list(src_dir.rglob("*.spec.ts")) + list(src_dir.rglob("*.spec.tsx"))
|
|
295
|
+
|
|
296
|
+
test_count = len(test_files)
|
|
297
|
+
spec_count = len(spec_files)
|
|
298
|
+
|
|
299
|
+
for sf in spec_files:
|
|
300
|
+
issues.append(f" {name}: {sf.relative_to(root)} (rename to .test{sf.suffix})")
|
|
301
|
+
|
|
302
|
+
status = "[green]OK[/green]" if spec_count == 0 else f"[red]{spec_count} wrong[/red]"
|
|
303
|
+
rows.append([name, str(test_count), str(spec_count), status])
|
|
304
|
+
json_data.append({"app": name, "test_count": test_count, "spec_count": spec_count})
|
|
305
|
+
|
|
306
|
+
out.table(
|
|
307
|
+
"Test File Naming Conventions",
|
|
308
|
+
[("App", "cyan"), (".test.*", "green"), (".spec.*", "red"), ("Status", "")],
|
|
309
|
+
rows,
|
|
310
|
+
data_for_json=json_data,
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
if issues:
|
|
314
|
+
out.warn("Wrong convention (.spec should be .test):")
|
|
315
|
+
for issue in issues:
|
|
316
|
+
out.info(issue)
|
|
317
|
+
else:
|
|
318
|
+
out.success("All test files follow .test.* naming convention")
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
@app.command()
|
|
322
|
+
def threshold(
|
|
323
|
+
ctx: typer.Context,
|
|
324
|
+
min_pct: Annotated[int, typer.Option("--min", help="Minimum line coverage percentage.")] = 70,
|
|
325
|
+
) -> None:
|
|
326
|
+
"""Enforce minimum coverage threshold across all apps.
|
|
327
|
+
|
|
328
|
+
Reads coverage-summary.json from each app's coverage/ directory.
|
|
329
|
+
Exits 1 if any app is below the threshold.
|
|
330
|
+
"""
|
|
331
|
+
actx: AppContext = ctx.obj
|
|
332
|
+
out = actx.output
|
|
333
|
+
root = actx.project_root
|
|
334
|
+
|
|
335
|
+
rows: list[list[str]] = []
|
|
336
|
+
json_data: list[dict] = []
|
|
337
|
+
any_fail = False
|
|
338
|
+
|
|
339
|
+
for name in actx.app_names:
|
|
340
|
+
cov_file = get_app_dir(root, name) / "coverage" / "coverage-summary.json"
|
|
341
|
+
if not cov_file.exists():
|
|
342
|
+
rows.append([name, "[dim]no data[/dim]", f"{min_pct}%", "[dim]SKIP[/dim]"])
|
|
343
|
+
json_data.append({"app": name, "lines_pct": None, "min_pct": min_pct, "status": "skip"})
|
|
344
|
+
continue
|
|
345
|
+
|
|
346
|
+
try:
|
|
347
|
+
data = json.loads(cov_file.read_text())
|
|
348
|
+
lines_pct = data.get("total", {}).get("lines", {}).get("pct", 0)
|
|
349
|
+
passed = lines_pct >= min_pct
|
|
350
|
+
if not passed:
|
|
351
|
+
any_fail = True
|
|
352
|
+
|
|
353
|
+
status = "[green]PASS[/green]" if passed else "[red]FAIL[/red]"
|
|
354
|
+
color = "green" if passed else "red"
|
|
355
|
+
rows.append([name, f"[{color}]{lines_pct:.1f}%[/{color}]", f"{min_pct}%", status])
|
|
356
|
+
json_data.append(
|
|
357
|
+
{"app": name, "lines_pct": lines_pct, "min_pct": min_pct, "status": "pass" if passed else "fail"}
|
|
358
|
+
)
|
|
359
|
+
except Exception:
|
|
360
|
+
rows.append([name, "[red]error[/red]", f"{min_pct}%", "[red]ERROR[/red]"])
|
|
361
|
+
json_data.append({"app": name, "lines_pct": None, "min_pct": min_pct, "status": "error"})
|
|
362
|
+
any_fail = True
|
|
363
|
+
|
|
364
|
+
out.table(
|
|
365
|
+
f"Coverage Threshold (min {min_pct}%)",
|
|
366
|
+
[("App", "cyan"), ("Lines %", ""), ("Min %", ""), ("Status", "")],
|
|
367
|
+
rows,
|
|
368
|
+
data_for_json=json_data,
|
|
369
|
+
)
|
|
370
|
+
|
|
371
|
+
if any_fail:
|
|
372
|
+
out.error("One or more apps are below the coverage threshold")
|
|
373
|
+
raise typer.Exit(1) from None
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
@app.command()
|
|
377
|
+
def snapshots(
|
|
378
|
+
ctx: typer.Context,
|
|
379
|
+
app_name: Annotated[str, typer.Argument(help="App name to inspect snapshots for")],
|
|
380
|
+
) -> None:
|
|
381
|
+
"""List vitest snapshot files for an app.
|
|
382
|
+
|
|
383
|
+
Shows all .snap files in the app's src/ directory with their sizes.
|
|
384
|
+
"""
|
|
385
|
+
actx: AppContext = ctx.obj
|
|
386
|
+
out = actx.output
|
|
387
|
+
root = actx.project_root
|
|
388
|
+
|
|
389
|
+
actx.validate_app(app_name)
|
|
390
|
+
|
|
391
|
+
src_dir = get_app_dir(root, app_name) / "src"
|
|
392
|
+
if not src_dir.is_dir():
|
|
393
|
+
out.warn(f"No src/ directory found for {app_name}")
|
|
394
|
+
return
|
|
395
|
+
|
|
396
|
+
snap_files = sorted(src_dir.rglob("*.snap"))
|
|
397
|
+
|
|
398
|
+
rows: list[list[str]] = []
|
|
399
|
+
json_data: list[dict] = []
|
|
400
|
+
|
|
401
|
+
for snap in snap_files:
|
|
402
|
+
size = snap.stat().st_size
|
|
403
|
+
size_str = f"{size / 1024:.1f} KB" if size >= 1024 else f"{size} B"
|
|
404
|
+
rel_path = str(snap.relative_to(root))
|
|
405
|
+
rows.append([rel_path, size_str])
|
|
406
|
+
json_data.append({"file": rel_path, "size_bytes": size})
|
|
407
|
+
|
|
408
|
+
if not snap_files:
|
|
409
|
+
out.info(f"No snapshot files found in {app_name}/src/")
|
|
410
|
+
else:
|
|
411
|
+
out.table(
|
|
412
|
+
f"Snapshot Files: {app_name}",
|
|
413
|
+
[("File", "cyan"), ("Size", "")],
|
|
414
|
+
rows,
|
|
415
|
+
data_for_json=json_data,
|
|
416
|
+
)
|
|
417
|
+
|
|
418
|
+
out.info(f"To update snapshots: pnpm --filter @kodemeio/{app_name} vitest run --update-snapshots")
|