jestronaut 0.3.16 → 0.3.18
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/bin/jestronaut.js
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
+
// If stdout is not a TTY (CI, piped output, etc.), skip the TUI entirely
|
|
4
|
+
// and let Jest run with its default reporter.
|
|
5
|
+
if (!process.stdout.isTTY) {
|
|
6
|
+
const { run } = await import('jest');
|
|
7
|
+
// Override the reporter config so DashboardReporter is never instantiated
|
|
8
|
+
process.argv.push('--reporters=default');
|
|
9
|
+
await run();
|
|
10
|
+
process.exit();
|
|
11
|
+
}
|
|
12
|
+
|
|
3
13
|
// Intercept stdout/stderr before Jest loads anything so its early
|
|
4
14
|
// output ("Determining test suites...") doesn't bleed into the TUI.
|
|
5
15
|
const realStdout = process.stdout.write.bind(process.stdout);
|
|
@@ -68,16 +78,13 @@ global.__jestronaut_emit__ = realEmit;
|
|
|
68
78
|
|
|
69
79
|
const _origEmit = process.stdin.emit;
|
|
70
80
|
process.stdin.emit = function(event, ...args) {
|
|
71
|
-
if (event === 'data' && global.__jestronaut_block_jest_input__) return true;
|
|
81
|
+
if ((event === 'data' || event === 'keypress') && global.__jestronaut_block_jest_input__) return true;
|
|
72
82
|
return _origEmit.apply(this, [event, ...args]);
|
|
73
83
|
};
|
|
74
84
|
|
|
75
|
-
|
|
76
|
-
// __jestronaut_ui__ — { store, unmount } created once, reused across re-runs
|
|
77
|
-
// __jestronaut_block_jest_input__ — boolean; true when TUI holds input focus
|
|
78
|
-
// __jestronaut_jest_keypress__ — Jest's raw onKeypress listener (unwrapped)
|
|
79
|
-
// __jestronaut_emit__ — original process.stdin.emit before any patching
|
|
85
|
+
global.__jestronaut_update_config_and_run__ = null;
|
|
80
86
|
|
|
81
87
|
// Forward all CLI args so flags like --testPathPattern, --watch etc. still work
|
|
82
88
|
const { run } = await import('jest');
|
|
89
|
+
|
|
83
90
|
run();
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Jest watch plugin that intercepts updateConfigAndRun from Jest's watch loop.
|
|
3
|
+
* Activated by sending a null byte (\x00) via stdin — never triggered by real user input.
|
|
4
|
+
* Once activated, calls updateConfigAndRun with the desired config and resolves immediately.
|
|
5
|
+
*/
|
|
6
|
+
export default class JestronautWatchPlugin {
|
|
7
|
+
getUsageInfo() {
|
|
8
|
+
return { key: '\x00', prompt: '' };
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
onKey() {}
|
|
12
|
+
|
|
13
|
+
run(_globalConfig, updateConfigAndRun) {
|
|
14
|
+
global.__jestronaut_update_config_and_run__ = updateConfigAndRun;
|
|
15
|
+
const opts = global.__jestronaut_run_opts__;
|
|
16
|
+
global.__jestronaut_run_opts__ = null;
|
|
17
|
+
if (opts) {
|
|
18
|
+
updateConfigAndRun(opts);
|
|
19
|
+
}
|
|
20
|
+
return Promise.resolve(false);
|
|
21
|
+
}
|
|
22
|
+
}
|
package/lib/reporter.js
CHANGED
|
@@ -88,6 +88,14 @@ export default class DashboardReporter {
|
|
|
88
88
|
}
|
|
89
89
|
|
|
90
90
|
onRunComplete(_, results) {
|
|
91
|
+
// Bootstrap: send null byte to activate our watch plugin and capture updateConfigAndRun
|
|
92
|
+
if (!global.__jestronaut_update_config_and_run__ && global.__jestronaut_emit__) {
|
|
93
|
+
setTimeout(() => {
|
|
94
|
+
global.__jestronaut_block_jest_input__ = false;
|
|
95
|
+
global.__jestronaut_emit__('data', '\x00');
|
|
96
|
+
global.__jestronaut_block_jest_input__ = true;
|
|
97
|
+
}, 500);
|
|
98
|
+
}
|
|
91
99
|
const s = this._state;
|
|
92
100
|
s.runComplete = true;
|
|
93
101
|
s.runOk = results.numFailedTests === 0;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { useState, useEffect, useReducer } from 'react';
|
|
1
|
+
import React, { useState, useEffect, useReducer, useRef } from 'react';
|
|
2
2
|
import { Box, useInput, useApp, useStdout } from 'ink';
|
|
3
3
|
import { SPINNER } from '../../constants.js';
|
|
4
4
|
import { Header } from './Header.js';
|
|
@@ -9,6 +9,7 @@ import { SuitesList } from './SuitesList.js';
|
|
|
9
9
|
import { Footer } from './Footer.js';
|
|
10
10
|
import { SuiteDetailOverlay, buildSuiteDetailItems, moveCursor as moveSuiteDetailCursor } from './SuiteDetailOverlay.js';
|
|
11
11
|
import { TestDetailOverlay, buildLines as buildTestDetailLines } from './TestDetailOverlay.js';
|
|
12
|
+
import { HelpOverlay } from './HelpOverlay.js';
|
|
12
13
|
|
|
13
14
|
const FIXED_ROWS = 2 + 2 + 2 + 3; // header + stats + progress + footer
|
|
14
15
|
|
|
@@ -16,6 +17,9 @@ export function Dashboard({ store }) {
|
|
|
16
17
|
const { exit } = useApp();
|
|
17
18
|
const { stdout } = useStdout();
|
|
18
19
|
const [dims, setDims] = useState({ columns: stdout.columns || 80, rows: stdout.rows || 24 });
|
|
20
|
+
const [helpOpen, setHelpOpen] = useState(false);
|
|
21
|
+
const helpOpenRef = useRef(false);
|
|
22
|
+
const jestOnlyFailuresRef = useRef(false); // mirrors Jest's internal onlyFailures toggle state
|
|
19
23
|
const [, forceUpdate] = useReducer(x => x + 1, 0);
|
|
20
24
|
|
|
21
25
|
useEffect(() => {
|
|
@@ -27,8 +31,9 @@ export function Dashboard({ store }) {
|
|
|
27
31
|
const { columns, rows } = dims;
|
|
28
32
|
|
|
29
33
|
useEffect(() => {
|
|
30
|
-
|
|
31
|
-
|
|
34
|
+
const onUpdate = () => { if (!helpOpenRef.current) forceUpdate(); };
|
|
35
|
+
store.on('update', onUpdate);
|
|
36
|
+
return () => store.off('update', onUpdate);
|
|
32
37
|
}, [store]);
|
|
33
38
|
|
|
34
39
|
useEffect(() => {
|
|
@@ -56,10 +61,50 @@ export function Dashboard({ store }) {
|
|
|
56
61
|
}
|
|
57
62
|
|
|
58
63
|
if (input === 'a' && s.watchWaiting) {
|
|
59
|
-
|
|
60
|
-
global.
|
|
61
|
-
|
|
62
|
-
|
|
64
|
+
jestOnlyFailuresRef.current = false;
|
|
65
|
+
if (global.__jestronaut_update_config_and_run__) {
|
|
66
|
+
// Call updateConfigAndRun directly — no key simulation, no intermediate run
|
|
67
|
+
global.__jestronaut_update_config_and_run__({
|
|
68
|
+
mode: 'watchAll',
|
|
69
|
+
onlyFailures: false,
|
|
70
|
+
testNamePattern: '',
|
|
71
|
+
testPathPatterns: [],
|
|
72
|
+
});
|
|
73
|
+
} else {
|
|
74
|
+
// Fallback: simulate 'a' key
|
|
75
|
+
global.__jestronaut_block_jest_input__ = false;
|
|
76
|
+
if (global.__jestronaut_emit__) global.__jestronaut_emit__('data', 'a');
|
|
77
|
+
global.__jestronaut_block_jest_input__ = true;
|
|
78
|
+
}
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (input === 'r' && s.watchWaiting && s.stats.failed > 0) {
|
|
83
|
+
jestOnlyFailuresRef.current = true;
|
|
84
|
+
if (global.__jestronaut_update_config_and_run__) {
|
|
85
|
+
global.__jestronaut_update_config_and_run__({ onlyFailures: true });
|
|
86
|
+
} else {
|
|
87
|
+
global.__jestronaut_block_jest_input__ = false;
|
|
88
|
+
if (global.__jestronaut_emit__) global.__jestronaut_emit__('data', 'f');
|
|
89
|
+
global.__jestronaut_block_jest_input__ = true;
|
|
90
|
+
}
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
if (input === '?') {
|
|
96
|
+
setHelpOpen(prev => {
|
|
97
|
+
helpOpenRef.current = !prev;
|
|
98
|
+
return !prev;
|
|
99
|
+
});
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (helpOpen) {
|
|
104
|
+
if (key.escape) {
|
|
105
|
+
helpOpenRef.current = false;
|
|
106
|
+
setHelpOpen(false);
|
|
107
|
+
}
|
|
63
108
|
return;
|
|
64
109
|
}
|
|
65
110
|
|
|
@@ -153,6 +198,9 @@ export function Dashboard({ store }) {
|
|
|
153
198
|
}
|
|
154
199
|
});
|
|
155
200
|
|
|
201
|
+
if (helpOpen) {
|
|
202
|
+
return React.createElement(HelpOverlay, { rows, columns });
|
|
203
|
+
}
|
|
156
204
|
if (state.testDetailOpen) {
|
|
157
205
|
return React.createElement(TestDetailOverlay, { state, rows });
|
|
158
206
|
}
|
|
@@ -174,6 +222,6 @@ export function Dashboard({ store }) {
|
|
|
174
222
|
React.createElement(Box, { width: '65%' }, React.createElement(ResultsList, { state, height: listHeight })),
|
|
175
223
|
React.createElement(Box, { width: '35%' }, React.createElement(SuitesList, { state, height: listHeight }))
|
|
176
224
|
),
|
|
177
|
-
React.createElement(Footer, { state })
|
|
225
|
+
React.createElement(Footer, { state, width: columns })
|
|
178
226
|
);
|
|
179
227
|
}
|
|
@@ -2,7 +2,7 @@ import React from 'react';
|
|
|
2
2
|
import { Box, Text } from 'ink';
|
|
3
3
|
import { SPINNER } from '../../constants.js';
|
|
4
4
|
|
|
5
|
-
export function Footer({ state }) {
|
|
5
|
+
export function Footer({ state, width }) {
|
|
6
6
|
const { stats, testDetailOpen, suiteDetailOpen, watchWaiting, spinFrame, suites } = state;
|
|
7
7
|
const elapsed = stats.startTime
|
|
8
8
|
? (((stats.endTime || Date.now()) - stats.startTime) / 1000).toFixed(1) + 's'
|
|
@@ -13,20 +13,27 @@ export function Footer({ state }) {
|
|
|
13
13
|
let hint;
|
|
14
14
|
if (testDetailOpen) hint = '[Esc] close [j/k] scroll';
|
|
15
15
|
else if (suiteDetailOpen) hint = '[j/k] navigate failed [Enter] open failure [Esc] back';
|
|
16
|
-
else if (watchWaiting) hint = '[a] run all
|
|
17
|
-
else hint = '[Tab] switch panel [j/k] navigate [Enter] open
|
|
16
|
+
else if (watchWaiting) hint = stats.failed > 0 ? '[a] run all [r] re-run failed [q] quit' : '[a] run all [q] quit';
|
|
17
|
+
else hint = '[Tab] switch panel [j/k] navigate [Enter] open [?] help [q] quit';
|
|
18
18
|
|
|
19
19
|
const statusText = runningCount > 0
|
|
20
20
|
? `${spin} Running ${runningCount} suite${runningCount > 1 ? 's' : ''}...`
|
|
21
21
|
: `Elapsed: ${elapsed}`;
|
|
22
22
|
|
|
23
|
+
const totalWidth = width || 80;
|
|
24
|
+
const hintMaxWidth = Math.max(10, totalWidth - statusText.length - 6);
|
|
25
|
+
|
|
23
26
|
return React.createElement(
|
|
24
27
|
Box,
|
|
25
|
-
{ height: 3, backgroundColor: '#111133', flexDirection: 'column', justifyContent: 'center', paddingLeft: 2 },
|
|
28
|
+
{ height: 3, width: totalWidth, backgroundColor: '#111133', flexDirection: 'column', justifyContent: 'center', paddingLeft: 2 },
|
|
26
29
|
React.createElement(
|
|
27
30
|
Box,
|
|
28
|
-
{
|
|
29
|
-
React.createElement(
|
|
31
|
+
{ width: totalWidth - 2 },
|
|
32
|
+
React.createElement(
|
|
33
|
+
Box,
|
|
34
|
+
{ width: hintMaxWidth },
|
|
35
|
+
React.createElement(Text, { color: 'cyan', wrap: 'truncate' }, hint)
|
|
36
|
+
),
|
|
30
37
|
React.createElement(Text, { color: 'gray' }, statusText)
|
|
31
38
|
)
|
|
32
39
|
);
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
|
|
4
|
+
const SECTIONS = [
|
|
5
|
+
{ heading: 'Navigation', keys: [
|
|
6
|
+
['Tab', 'Switch focus between Results / Suites'],
|
|
7
|
+
['j / ↓', 'Move cursor down'],
|
|
8
|
+
['k / ↑', 'Move cursor up'],
|
|
9
|
+
]},
|
|
10
|
+
{ heading: 'Actions', keys: [
|
|
11
|
+
['Enter', 'Open suite / test detail'],
|
|
12
|
+
['Escape', 'Close overlay'],
|
|
13
|
+
['?', 'Toggle this help overlay'],
|
|
14
|
+
['q', 'Quit'],
|
|
15
|
+
]},
|
|
16
|
+
{ heading: 'Watch Mode', keys: [
|
|
17
|
+
['a', 'Run all tests'],
|
|
18
|
+
['r', 'Re-run failed tests only'],
|
|
19
|
+
]},
|
|
20
|
+
{ heading: 'Detail Overlay', keys: [
|
|
21
|
+
['j / k', 'Navigate failed tests (suite detail)'],
|
|
22
|
+
['j / k', 'Scroll content (test detail)'],
|
|
23
|
+
]},
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
export function HelpOverlay({ rows, columns }) {
|
|
27
|
+
const width = Math.min(62, (columns || 80) - 4);
|
|
28
|
+
|
|
29
|
+
return React.createElement(
|
|
30
|
+
Box,
|
|
31
|
+
{ height: rows || 24, width: columns || 80, justifyContent: 'center', alignItems: 'center' },
|
|
32
|
+
React.createElement(
|
|
33
|
+
Box,
|
|
34
|
+
{ flexDirection: 'column', borderStyle: 'single', borderColor: 'cyan', width, paddingX: 2, paddingY: 1 },
|
|
35
|
+
React.createElement(Text, { bold: true, color: 'cyan' }, 'Keybindings'),
|
|
36
|
+
React.createElement(Box, { height: 1 }),
|
|
37
|
+
...SECTIONS.flatMap(section => [
|
|
38
|
+
React.createElement(Text, { color: 'yellow', key: section.heading }, section.heading),
|
|
39
|
+
...section.keys.map(([k, desc]) =>
|
|
40
|
+
React.createElement(
|
|
41
|
+
Box, { key: k },
|
|
42
|
+
React.createElement(Text, { color: 'white' }, ` ${k.padEnd(10)} `),
|
|
43
|
+
React.createElement(Text, { color: 'gray', wrap: 'truncate' }, desc)
|
|
44
|
+
)
|
|
45
|
+
),
|
|
46
|
+
React.createElement(Box, { height: 1, key: `${section.heading}-spacer` }),
|
|
47
|
+
]),
|
|
48
|
+
React.createElement(Text, { color: 'gray' }, '[?] or [Esc] to close'),
|
|
49
|
+
)
|
|
50
|
+
);
|
|
51
|
+
}
|
package/package.json
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jestronaut",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.18",
|
|
4
4
|
"description": "An interactive terminal dashboard UI for Jest — navigate live test results, suites, and failure details without leaving your terminal",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./index.js",
|
|
9
|
+
"./watch-plugin": "./lib/JestronautWatchPlugin.js",
|
|
10
|
+
"./lib/*": "./lib/*"
|
|
11
|
+
},
|
|
7
12
|
"bin": {
|
|
8
13
|
"jestronaut": "./bin/jestronaut.js"
|
|
9
14
|
},
|