Skip to content

Commit 86d8758

Browse files
committed
add shared CLI for terminal and browser based on ink/web-ink; fix E2E tests; fix VirtualPod type
1 parent 2b2758c commit 86d8758

24 files changed

+880
-444
lines changed

docs/CODING_GUIDELINES.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,13 @@
22

33
Keep the codebase maintainable: strict types, small units of work, and simple React components.
44

5+
## React
6+
7+
- **Use the latest stable React** and follow current React best practices. Prefer modern APIs (e.g. hooks, concurrent features where applicable) over legacy patterns.
8+
- **Effects and event logic:** When a `useEffect` needs to call a callback (e.g. a prop like `onOutputLines`) that should not be in the dependency array—to avoid re-running the effect when the parent passes a new function reference—use **`useEffectEvent`** (React 19+) to wrap that callback. The Effect Event always sees the latest props/state when it runs but does not trigger the effect to re-run when it changes. This keeps the effect’s dependencies minimal and avoids stale closures.
9+
- **Custom hooks for stateful logic.** If a component has non-trivial state or effects, move that logic into a `useSomething` hook and keep the component mostly presentational.
10+
- **Composition over complexity.** Prefer many small components and clear data flow over large components with many branches.
11+
512
## TypeScript
613

714
- **Strict types only.** The project uses `strict: true` in `tsconfig.json`. Do not relax it.
@@ -20,7 +27,6 @@ Keep the codebase maintainable: strict types, small units of work, and simple Re
2027

2128
- **Keep components small.** If a component is long or has many branches, extract subcomponents or custom hooks.
2229
- **Avoid overly complex components.** Prefer many small components over one large one. Use composition.
23-
- **Custom hooks for stateful logic.** If a component has non-trivial state or effects, move that logic into a `useSomething` hook and keep the component mostly presentational.
2430
- **Props: typed and minimal.** Define a clear props interface; avoid spreading large objects “just in case.”
2531

2632
## General

docs/testing/bdd-tests.md

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@ E2E tests are written in Gherkin (`.feature` files) and run with [Playwright](ht
44

55
## Commands
66

7-
- **Generate specs and run:** `npx bddgen && npx playwright test` (or `npm run test:e2e` / `npm run test:bdd`)
7+
- **Generate specs and run (browser only):** `npx bddgen && npx playwright test` (or `npm run test:e2e` / `npm run test:bdd`) — runs only **browser** CLI scenarios (Example #1); no browser is launched for terminal scenarios.
88
- **With browser visible:** `npm run test:e2e:headed`
9+
- **Terminal CLI only (no browser):** `npm run test:e2e:terminal` — runs terminal CLI E2E via Vitest (spawns Node CLI, no Playwright, no browser).
910
- **View last report:** `npx playwright show-report`
1011

1112
After changing `.feature` files or step definitions, run `npx bddgen` before `npx playwright test`.
@@ -60,7 +61,12 @@ These are implemented in `tests/features/*.feature` and run with Playwright.
6061

6162
### CLI in browser vs terminal (Scenario Outline)
6263

63-
The same CLI scenarios run **in the browser** (Terminal tab) and **in the terminal** (Node CLI spawned with piped stdin). One feature file per area (e.g. `cli-contacts.feature`) uses a **Scenario Outline** with `Examples: | context | browser | terminal |`. The step "Given I have the CLI in the <context>" either opens the Terminal tab (browser) or spawns `npx tsx src/cli/run-node.tsx` with a unique temp store path (terminal). "When I run the command" and "Then I should see ... in the output" branch on context. Run with the same command as other E2E: `npm run test:e2e` (dev server must be running for browser; terminal runs without it). In Node, `exit` quits the process; `export` prints JSON (or `--download` writes a file). See README and [INTEGRATION_GUIDE.md](../INTEGRATION_GUIDE.md) for full CLI usage.
64+
The same CLI scenarios are defined **for both browser and terminal** in the feature files (e.g. `cli-contacts.feature` uses `Examples: | context | browser | terminal |`). In practice they run in two ways so that a browser is never opened for terminal scenarios:
65+
66+
- **Browser (Example #1):** Run with `npm run test:e2e`. Playwright is configured to run only tests named "Example #1" (browser context). The dev server must be running; the test opens the Terminal tab and asserts via the in-page CLI output mirror.
67+
- **Terminal (no browser):** Run with `npm run test:e2e:terminal`. Vitest runs `tests/e2e-cli-terminal.spec.ts`, which spawns the Node CLI with piped stdin and asserts on stdout. No Playwright, no browser.
68+
69+
The step "Given I have the CLI in the <context>" either opens the Terminal tab (browser) or spawns `npx tsx src/cli/run-node.tsx` (terminal). See README and [INTEGRATION_GUIDE.md](../INTEGRATION_GUIDE.md) for full CLI usage.
6470

6571
### Not yet automated (manual verification)
6672

eslint.config.js

Lines changed: 60 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,66 @@
11
// For more info, see https://github.com/storybookjs/eslint-plugin-storybook#configuration-flat-config-format
22
import storybook from "eslint-plugin-storybook";
3+
import tseslint from 'typescript-eslint';
4+
import js from '@eslint/js';
5+
import globals from 'globals';
6+
import reactHooks from 'eslint-plugin-react-hooks';
7+
import reactRefresh from 'eslint-plugin-react-refresh';
8+
import { defineConfig, globalIgnores } from 'eslint/config';
39

4-
import js from '@eslint/js'
5-
import globals from 'globals'
6-
import reactHooks from 'eslint-plugin-react-hooks'
7-
import reactRefresh from 'eslint-plugin-react-refresh'
8-
import { defineConfig, globalIgnores } from 'eslint/config'
9-
10-
export default defineConfig([globalIgnores(['dist']), {
11-
files: ['**/*.{js,jsx}'],
12-
extends: [
13-
js.configs.recommended,
14-
reactHooks.configs.flat.recommended,
15-
reactRefresh.configs.vite,
16-
],
17-
languageOptions: {
18-
ecmaVersion: 2020,
19-
globals: globals.browser,
20-
parserOptions: {
21-
ecmaVersion: 'latest',
22-
ecmaFeatures: { jsx: true },
23-
sourceType: 'module',
10+
export default defineConfig([
11+
globalIgnores(['dist', '.features-gen']),
12+
// Node/Config files: process etc.
13+
{
14+
files: ['vite.config.js', 'vitest.config.ts', 'playwright.config.ts'],
15+
languageOptions: { globals: { ...globals.node } },
16+
},
17+
// JS/JSX
18+
{
19+
files: ['**/*.{js,jsx}'],
20+
extends: [
21+
js.configs.recommended,
22+
reactHooks.configs.flat.recommended,
23+
reactRefresh.configs.vite,
24+
],
25+
languageOptions: {
26+
ecmaVersion: 2020,
27+
globals: globals.browser,
28+
parserOptions: {
29+
ecmaVersion: 'latest',
30+
ecmaFeatures: { jsx: true },
31+
sourceType: 'module',
32+
},
33+
},
34+
rules: {
35+
'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }],
2436
},
2537
},
26-
rules: {
27-
'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }],
38+
// TypeScript/TSX: parser so .stories.tsx and .storybook parse correctly
39+
{
40+
files: ['**/*.{ts,tsx}'],
41+
extends: [
42+
js.configs.recommended,
43+
...tseslint.configs.recommended,
44+
reactHooks.configs.flat.recommended,
45+
reactRefresh.configs.vite,
46+
],
47+
languageOptions: {
48+
ecmaVersion: 2020,
49+
globals: { ...globals.browser },
50+
parserOptions: {
51+
ecmaVersion: 'latest',
52+
ecmaFeatures: { jsx: true },
53+
sourceType: 'module',
54+
},
55+
},
56+
rules: {
57+
'no-unused-vars': 'off',
58+
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_', varsIgnorePattern: '^[A-Z_]' }],
59+
'no-empty-pattern': 'off',
60+
'@typescript-eslint/no-require-imports': 'off',
61+
'react-refresh/only-export-components': 'off',
62+
'react-hooks/set-state-in-effect': 'off',
63+
},
2864
},
29-
}, ...storybook.configs["flat/recommended"]])
65+
...storybook.configs['flat/recommended'],
66+
]);

package-lock.json

Lines changed: 114 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
"build-storybook": "storybook build",
4343
"test:e2e": "npx bddgen && playwright test",
4444
"test:e2e:headed": "npx bddgen && playwright test --headed",
45+
"test:e2e:terminal": "vitest run --config vitest.terminal-e2e.config.ts",
4546
"test:bdd": "npx bddgen && playwright test",
4647
"cli": "tsx src/cli/run-node.tsx"
4748
},
@@ -84,6 +85,7 @@
8485
"storybook": "^10.2.3",
8586
"tsx": "^4.19.2",
8687
"typescript": "^5.9.3",
88+
"typescript-eslint": "^8.54.0",
8789
"vite": "npm:rolldown-vite@7.2.5",
8890
"vitest": "^4.0.18"
8991
},

playwright.config.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@ export default defineConfig({
2222
navigationTimeout: 15_000,
2323
},
2424
projects: [
25-
{ name: 'chromium', use: { ...devices['Desktop Chrome'] } },
25+
// Browser scenarios only (Example #1 in each outline). Terminal scenarios run via test:e2e:terminal (no browser).
26+
{ name: 'chromium', use: { ...devices['Desktop Chrome'] }, grep: /Example #1/ },
2627
],
2728
// No webServer: start the dev server yourself (e.g. npm run dev) before running BDD/E2E tests.
2829
});

src/App.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1001,7 +1001,7 @@ const styles: Record<string, CSSProperties> = {
10011001
navGitHubLink: { display: 'flex', alignItems: 'center', justifyContent: 'center', padding: '6px 12px', borderRadius: 4, border: '1px solid #444', background: '#2a2a2a', color: '#ccc', textDecoration: 'none', marginLeft: 4, fontSize: 12, fontWeight: 500 },
10021002
topNavTab: { padding: '14px 24px', border: 'none', background: 'transparent', color: '#888', cursor: 'pointer', fontSize: 14, fontWeight: 500, borderBottom: '2px solid transparent', transition: 'all 0.2s' },
10031003
topNavTabActive: { color: '#4ecdc4', borderBottom: '2px solid #4ecdc4' },
1004-
terminalView: { padding: 0, height: 'calc(100vh - 49px)', background: '#1e1e1e' },
1004+
terminalView: { padding: 0, height: 'calc(100vh - 49px)', minHeight: 320, width: '100%', background: '#1e1e1e', display: 'flex', flexDirection: 'column' },
10051005
personasViewContainer: { padding: 24, flex: 1, maxWidth: 800, margin: '0 auto', width: '100%', boxSizing: 'border-box' },
10061006
contactsViewContainer: { padding: 24, flex: 1, maxWidth: 800, margin: '0 auto', width: '100%', boxSizing: 'border-box' },
10071007
groupsViewContainer: { padding: 24, flex: 1, maxWidth: 800, margin: '0 auto', width: '100%', boxSizing: 'border-box' },

0 commit comments

Comments
 (0)