@elench/testkit 0.1.85 → 0.1.87

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.
@@ -1,220 +0,0 @@
1
- import fs from "fs";
2
- import path from "path";
3
- import React, { createElement, useEffect, useMemo, useState } from "react";
4
- import { Box, Text, useApp, useInput } from "ink";
5
- import { formatDuration } from "../../runner/formatting.mjs";
6
- import { formatFileDetail, loadCurrentRunArtifact } from "../viewer.mjs";
7
-
8
- export function WatchApp({ productDir, serviceFilter = null }) {
9
- const { exit } = useApp();
10
- const [artifact, setArtifact] = useState(() => safeLoadArtifact(productDir));
11
- const [view, setView] = useState("files");
12
- const [selectedFileIndex, setSelectedFileIndex] = useState(0);
13
- const [selectedSetupIndex, setSelectedSetupIndex] = useState(0);
14
-
15
- const files = useMemo(() => collectFiles(artifact, serviceFilter), [artifact, serviceFilter]);
16
- const setupOperations = useMemo(
17
- () => collectSetupOperations(artifact, serviceFilter),
18
- [artifact, serviceFilter]
19
- );
20
- const selectedFile = files[Math.min(selectedFileIndex, Math.max(0, files.length - 1))] || null;
21
- const selectedSetup =
22
- setupOperations[Math.min(selectedSetupIndex, Math.max(0, setupOperations.length - 1))] || null;
23
-
24
- useEffect(() => {
25
- const timer = setInterval(() => {
26
- setArtifact(safeLoadArtifact(productDir));
27
- }, 1_000);
28
- return () => clearInterval(timer);
29
- }, [productDir]);
30
-
31
- useEffect(() => {
32
- setSelectedFileIndex((current) => Math.min(current, Math.max(0, files.length - 1)));
33
- }, [files.length]);
34
-
35
- useEffect(() => {
36
- setSelectedSetupIndex((current) => Math.min(current, Math.max(0, setupOperations.length - 1)));
37
- }, [setupOperations.length]);
38
-
39
- useInput((input, key) => {
40
- if (input === "q") {
41
- exit();
42
- return;
43
- }
44
- if (input === "r") {
45
- setArtifact(safeLoadArtifact(productDir));
46
- return;
47
- }
48
- if (key.tab || input === "s") {
49
- setView((current) => {
50
- if (current === "files" && setupOperations.length > 0) return "setup";
51
- return "files";
52
- });
53
- return;
54
- }
55
- if (key.downArrow) {
56
- if (view === "setup") {
57
- setSelectedSetupIndex((current) => Math.min(current + 1, Math.max(0, setupOperations.length - 1)));
58
- } else {
59
- setSelectedFileIndex((current) => Math.min(current + 1, Math.max(0, files.length - 1)));
60
- }
61
- return;
62
- }
63
- if (key.upArrow) {
64
- if (view === "setup") {
65
- setSelectedSetupIndex((current) => Math.max(0, current - 1));
66
- } else {
67
- setSelectedFileIndex((current) => Math.max(0, current - 1));
68
- }
69
- }
70
- });
71
-
72
- if (!artifact) {
73
- return createElement(Text, null, `No run artifact found in ${productDir}`);
74
- }
75
-
76
- return createElement(
77
- Box,
78
- { flexDirection: "column" },
79
- createElement(
80
- Text,
81
- null,
82
- `testkit watch · q quit · r reload · tab toggle · ${artifact.run.status} · ${formatDuration(artifact.run.durationMs)}`
83
- ),
84
- createElement(
85
- Box,
86
- { marginTop: 1 },
87
- createElement(
88
- Box,
89
- { width: "40%", flexDirection: "column", marginRight: 2 },
90
- createElement(Text, null, view === "setup" ? "Setup" : "Files"),
91
- ...(view === "setup"
92
- ? renderSetupEntries(setupOperations, selectedSetupIndex)
93
- : renderFileEntries(files, selectedFileIndex))
94
- ),
95
- createElement(
96
- Box,
97
- { width: "60%", flexDirection: "column" },
98
- createElement(Text, null, "Details"),
99
- ...(view === "setup"
100
- ? formatSetupDetail(productDir, selectedSetup)
101
- .slice(0, 28)
102
- .map((line, index) => createElement(Text, { key: `${index}:${line}` }, line))
103
- : selectedFile
104
- ? formatFileDetail(productDir, artifact, selectedFile, { logTail: 8 })
105
- .slice(0, 28)
106
- .map((line, index) => createElement(Text, { key: `${index}:${line}` }, line))
107
- : [createElement(Text, { key: "empty" }, "No file results")])
108
- )
109
- )
110
- );
111
- }
112
-
113
- function safeLoadArtifact(productDir) {
114
- try {
115
- return loadCurrentRunArtifact(productDir);
116
- } catch {
117
- return null;
118
- }
119
- }
120
-
121
- function collectFiles(runArtifact, serviceFilter) {
122
- if (!runArtifact) return [];
123
- const entries = [];
124
- for (const service of runArtifact.services || []) {
125
- if (serviceFilter && service.name !== serviceFilter) continue;
126
- for (const suite of service.suites || []) {
127
- for (const file of suite.files || []) {
128
- entries.push({ service, suite, file });
129
- }
130
- }
131
- }
132
- const failed = entries.filter((entry) => entry.file.status === "failed");
133
- return (failed.length > 0 ? failed : entries).sort((left, right) =>
134
- left.file.path.localeCompare(right.file.path)
135
- );
136
- }
137
-
138
- function renderFileEntries(files, selectedIndex) {
139
- return files.map((entry, index) => {
140
- const prefix = index === selectedIndex ? ">" : " ";
141
- return createElement(
142
- Text,
143
- { key: `${entry.service.name}:${entry.file.path}` },
144
- `${prefix} ${entry.file.status.toUpperCase().padEnd(7)} ${entry.file.path}`
145
- );
146
- });
147
- }
148
-
149
- function collectSetupOperations(runArtifact, serviceFilter) {
150
- if (!runArtifact) return [];
151
- return [...(runArtifact.setup?.operations || [])]
152
- .filter((entry) => !serviceFilter || entry.serviceName === serviceFilter)
153
- .sort((left, right) => {
154
- return (
155
- setupStatusRank(left.status) - setupStatusRank(right.status) ||
156
- String(left.serviceName || "").localeCompare(String(right.serviceName || "")) ||
157
- String(left.startedAt || "").localeCompare(String(right.startedAt || "")) ||
158
- String(left.stage || "").localeCompare(String(right.stage || ""))
159
- );
160
- });
161
- }
162
-
163
- function renderSetupEntries(operations, selectedIndex) {
164
- return operations.map((entry, index) => {
165
- const prefix = index === selectedIndex ? ">" : " ";
166
- const duration = entry.durationMs == null ? "" : ` ${formatDuration(entry.durationMs)}`;
167
- return createElement(
168
- Text,
169
- { key: entry.id },
170
- `${prefix} ${String(entry.status || "").toUpperCase().padEnd(7)} ${entry.serviceName} ${entry.stage}${duration}`
171
- );
172
- });
173
- }
174
-
175
- function formatSetupDetail(productDir, operation) {
176
- if (!operation) return ["No setup operations"];
177
- const lines = [
178
- `Service: ${operation.serviceName}`,
179
- `Stage: ${operation.stage}`,
180
- `Status: ${operation.status}`,
181
- ];
182
- if (operation.durationMs != null) {
183
- lines.push(`Duration: ${formatDuration(operation.durationMs)}`);
184
- }
185
- if (operation.summary) {
186
- lines.push(`Summary: ${operation.summary}`);
187
- }
188
- if (operation.error) {
189
- lines.push(`Error: ${operation.error}`);
190
- }
191
- if (operation.logRef?.path) {
192
- lines.push("");
193
- lines.push("Log:");
194
- lines.push(` ${operation.logRef.path}`);
195
- const absolutePath = path.join(productDir, operation.logRef.path);
196
- for (const line of readTailSafe(absolutePath, 12)) {
197
- lines.push(` ${line}`);
198
- }
199
- }
200
- return lines;
201
- }
202
-
203
- function readTailSafe(absolutePath, maxLines) {
204
- try {
205
- return fs.readFileSync(absolutePath, "utf8")
206
- .split(/\r?\n/)
207
- .filter(Boolean)
208
- .slice(-maxLines);
209
- } catch {
210
- return [];
211
- }
212
- }
213
-
214
- function setupStatusRank(status) {
215
- if (status === "failed") return 1;
216
- if (status === "running") return 2;
217
- if (status === "passed") return 3;
218
- if (status === "cached") return 4;
219
- return 5;
220
- }