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
- // Global contract for watch mode:
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
- store.on('update', forceUpdate);
31
- return () => store.off('update', forceUpdate);
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
- // Briefly unblock, forward 'a' to Jest's watch listener, then re-block
60
- global.__jestronaut_block_jest_input__ = false;
61
- if (global.__jestronaut_emit__) global.__jestronaut_emit__('data', 'a');
62
- global.__jestronaut_block_jest_input__ = true;
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 tests [q] quit';
17
- else hint = '[Tab] switch panel [j/k] navigate [Enter] open failure [q] quit';
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
- { justifyContent: 'space-between' },
29
- React.createElement(Text, { color: 'cyan' }, hint),
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.16",
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
  },