beads-ui 0.1.0 → 0.1.2

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.
Files changed (57) hide show
  1. package/CHANGES.md +8 -0
  2. package/README.md +7 -3
  3. package/package.json +12 -2
  4. package/.beads/issues.jsonl +0 -107
  5. package/.editorconfig +0 -10
  6. package/.eslintrc.json +0 -36
  7. package/.github/workflows/ci.yml +0 -38
  8. package/.prettierignore +0 -5
  9. package/AGENTS.md +0 -85
  10. package/app/data/providers.test.js +0 -126
  11. package/app/main.board-switch.test.js +0 -94
  12. package/app/main.deep-link.test.js +0 -64
  13. package/app/main.live-updates.test.js +0 -229
  14. package/app/main.test.js +0 -17
  15. package/app/main.theme.test.js +0 -41
  16. package/app/main.view-sync.test.js +0 -54
  17. package/app/protocol.test.js +0 -57
  18. package/app/router.test.js +0 -34
  19. package/app/state.test.js +0 -21
  20. package/app/utils/markdown.test.js +0 -103
  21. package/app/utils/type-badge.test.js +0 -30
  22. package/app/views/board.test.js +0 -184
  23. package/app/views/detail.acceptance-notes.test.js +0 -67
  24. package/app/views/detail.assignee.test.js +0 -161
  25. package/app/views/detail.deps.test.js +0 -97
  26. package/app/views/detail.edits.test.js +0 -146
  27. package/app/views/detail.labels.test.js +0 -73
  28. package/app/views/detail.priority.test.js +0 -86
  29. package/app/views/detail.test.js +0 -188
  30. package/app/views/detail.ui47.test.js +0 -78
  31. package/app/views/epics.test.js +0 -283
  32. package/app/views/list.inline-edits.test.js +0 -84
  33. package/app/views/list.test.js +0 -479
  34. package/app/views/nav.test.js +0 -43
  35. package/app/ws.test.js +0 -168
  36. package/eslint.config.js +0 -59
  37. package/media/bdui-board.png +0 -0
  38. package/media/bdui-epics.png +0 -0
  39. package/media/bdui-issues.png +0 -0
  40. package/prettier.config.js +0 -13
  41. package/server/app.test.js +0 -29
  42. package/server/bd.test.js +0 -93
  43. package/server/cli/cli.test.js +0 -109
  44. package/server/cli/commands.integration.test.js +0 -155
  45. package/server/cli/commands.unit.test.js +0 -94
  46. package/server/cli/open.test.js +0 -26
  47. package/server/db.test.js +0 -70
  48. package/server/protocol.test.js +0 -87
  49. package/server/watcher.test.js +0 -100
  50. package/server/ws.handlers.test.js +0 -174
  51. package/server/ws.labels.test.js +0 -95
  52. package/server/ws.mutations.test.js +0 -261
  53. package/server/ws.subscriptions.test.js +0 -116
  54. package/server/ws.test.js +0 -52
  55. package/test/setup-vitest.js +0 -12
  56. package/tsconfig.json +0 -23
  57. package/vitest.config.mjs +0 -14
package/.eslintrc.json DELETED
@@ -1,36 +0,0 @@
1
- {
2
- "root": true,
3
- "env": {
4
- "es2023": true,
5
- "node": true,
6
- "browser": true
7
- },
8
- "parserOptions": {
9
- "ecmaVersion": "latest",
10
- "sourceType": "module"
11
- },
12
- "extends": ["eslint:recommended"],
13
- "plugins": ["jsdoc", "import", "n", "promise"],
14
- "rules": {
15
- "no-unused-vars": [
16
- "error",
17
- { "argsIgnorePattern": "^_", "varsIgnorePattern": "^_" }
18
- ],
19
- "no-console": "off",
20
- "eqeqeq": ["error", "always"],
21
- "jsdoc/check-alignment": "warn",
22
- "jsdoc/check-param-names": "warn",
23
- "jsdoc/require-param": "warn",
24
- "jsdoc/require-returns": "off"
25
- },
26
- "overrides": [
27
- {
28
- "files": ["test/**/*.js"],
29
- "globals": {
30
- "describe": "readonly",
31
- "test": "readonly",
32
- "expect": "readonly"
33
- }
34
- }
35
- ]
36
- }
@@ -1,38 +0,0 @@
1
- name: Build
2
-
3
- on:
4
- push:
5
- branches:
6
- - main
7
- pull_request:
8
- branches:
9
- - main
10
-
11
- jobs:
12
- build:
13
- runs-on: ubuntu-latest
14
- strategy:
15
- matrix:
16
- node-version: ['22.x', '24.x']
17
-
18
- steps:
19
- - name: Checkout
20
- uses: actions/checkout@v4
21
- - name: Use Node.js ${{ matrix.node-version }}
22
- uses: actions/setup-node@v4
23
- with:
24
- node-version: ${{ matrix.node-version }}
25
- cache: 'npm'
26
- - name: Install
27
- run: npm ci
28
- - name: Lint
29
- if: matrix.node-version == '24.x'
30
- run: npm run lint
31
- - name: Types
32
- if: matrix.node-version == '24.x'
33
- run: npm run typecheck
34
- - name: Prettier
35
- if: matrix.node-version == '24.x'
36
- run: npm run format:check
37
- - name: Test
38
- run: npm test
package/.prettierignore DELETED
@@ -1,5 +0,0 @@
1
- node_modules
2
- coverage
3
- dist
4
- .beads
5
-
package/AGENTS.md DELETED
@@ -1,85 +0,0 @@
1
- # Agents
2
-
3
- ## Beads (bd) — Work Tracking
4
-
5
- Use MCP `beads` (bd) as our dependency‑aware issue tracker. Run
6
- `beads/quickstart` to learn how to use it.
7
-
8
- ### Issue Types
9
-
10
- - `bug` - Something broken that needs fixing
11
- - `feature` - New functionality
12
- - `task` - Work item (tests, docs, refactoring)
13
- - `epic` - Large feature composed of multiple issues
14
- - `chore` - Maintenance work (dependencies, tooling)
15
-
16
- ### Priorities
17
-
18
- - `0` - Critical (security, data loss, broken builds)
19
- - `1` - High (major features, important bugs)
20
- - `2` - Medium (nice-to-have features, minor bugs)
21
- - `3` - Low (polish, optimization)
22
- - `4` - Backlog (future ideas)
23
-
24
- ### Dependency Types
25
-
26
- - `blocks` - Hard dependency (issue X blocks issue Y)
27
- - `related` - Soft relationship (issues are connected)
28
- - `parent-child` - Epic/subtask relationship
29
- - `discovered-from` - Track issues discovered during work
30
-
31
- Only `blocks` dependencies affect the ready work queue.
32
-
33
- ### Structured Fields and Labels
34
-
35
- - Use issue `type` and `priority` fields.
36
- - Use issue type "epic" and `parent-child` dependencies.
37
- - Use `related` or `discovered-from` dependencies.
38
- - Area pointers are labels, e.g.: `frontend`, `backend`
39
-
40
- ### Agent Workflow
41
-
42
- If no issue is specified, run `bd ready` and claim an unblocked issue.
43
-
44
- 1. Open issue with `bd show <id>` and read all linked docs.
45
- 2. Assign to `agent`, update status as you work (`in_progress` → `closed`);
46
- maintain dependencies, and attach notes/links for traceability.
47
- 3. Discover new work? Create linked issue with dependency
48
- `discovered-from:<parent-id>`.
49
- 4. Land the change; run tests/lint; update any referenced docs.
50
- 5. Close the issue with `bd close <id>`.
51
-
52
- ## Coding Standards
53
-
54
- - Use ECMAScript modules.
55
- - Classes, interfaces, and factory types use `PascalCase`.
56
- - Functions and methods use `camelCase`.
57
- - Variables and parameters use `lower_snake_case`, unless they refer to a
58
- function or class.
59
- - Constants are `UPPER_SNAKE_CASE`.
60
- - File and directory names are `kebab-case`.
61
- - Use `.js` files with JSDoc type annotations (TypeScript mode).
62
- - Use `.ts` files only for interface definitions.
63
- - Type only imports: `@import { X, Y, Z } from './file.js` in top-of-file JSDoc.
64
- - Add JSDoc to all functions and methods with `@param` (and `@returns` for non
65
- trivial return types).
66
- - Annotate local variables with `@type` blocks if their type is not obvious from
67
- the initializer.
68
- - Use blocks for all control flow statements, even single-line bodies.
69
- - Avoid runtime type checks, undefined/null checks and optional chaining
70
- operators (`?.`, `??`) unless strictly necessary.
71
-
72
- ## Unit Testing Standards
73
-
74
- - Write short, focused test-case functions asserting one behavior each.
75
- - Do not use "should" in test names; use verbs like "returns", "throws",
76
- "emits", or "calls"
77
- - Structure: setup → execution → assertion (separate with blank lines).
78
- - Never change implementation code to make tests pass.
79
-
80
- ## Pre‑Handoff Validation
81
-
82
- - Run type checks: `npm run typecheck`
83
- - Run tests: `npm test`
84
- - Run eslint: `npm run lint`
85
- - Run prettier: `npm run format`
@@ -1,126 +0,0 @@
1
- import { describe, expect, test } from 'vitest';
2
- import { createDataLayer } from './providers.js';
3
-
4
- // Using a minimal fixture shaped like epic-status-example.json
5
- const epicFixture = [
6
- {
7
- epic: {
8
- id: 'WK-1',
9
- title: 'Example Epic',
10
- description: 'Example',
11
- acceptance_criteria: 'Demo',
12
- notes: '',
13
- status: 'open',
14
- priority: 1,
15
- issue_type: 'epic',
16
- created_at: '2025-10-21T00:00:00.000Z',
17
- updated_at: '2025-10-21T00:00:00.000Z'
18
- },
19
- total_children: 2,
20
- closed_children: 1,
21
- eligible_for_close: false
22
- }
23
- ];
24
-
25
- /**
26
- * @returns {{ calls: { type: string, payload: any }[], send: (type: string, payload?: any) => Promise<any> }}
27
- */
28
- function makeTransportRecorder() {
29
- /** @type {{ type: string, payload: any }[]} */
30
- const calls = [];
31
- return {
32
- calls,
33
- /**
34
- * @param {string} type
35
- * @param {any} [payload]
36
- */
37
- async send(type, payload) {
38
- calls.push({ type, payload });
39
- // default fake payloads
40
- if (type === 'epic-status') {
41
- return [];
42
- }
43
- if (type === 'list-issues') {
44
- return [];
45
- }
46
- if (type === 'show-issue') {
47
- return { id: payload?.id || 'X' };
48
- }
49
- if (
50
- type === 'update-status' ||
51
- type === 'update-priority' ||
52
- type === 'edit-text' ||
53
- type === 'update-assignee'
54
- ) {
55
- return { id: payload?.id || 'X' };
56
- }
57
- return null;
58
- }
59
- };
60
- }
61
-
62
- describe('data/providers', () => {
63
- test('getClosed requests list-issues with status and limit=10 by default', async () => {
64
- const rec = makeTransportRecorder();
65
- const data = createDataLayer((t, p) => rec.send(t, p));
66
- await data.getClosed();
67
- const last = rec.calls[rec.calls.length - 1];
68
- expect(last.type).toBe('list-issues');
69
- expect(last.payload.filters.status).toBe('closed');
70
- expect(last.payload.filters.limit).toBe(10);
71
- });
72
-
73
- test('getInProgress requests list-issues with status=in_progress', async () => {
74
- const rec = makeTransportRecorder();
75
- const data = createDataLayer((t, p) => rec.send(t, p));
76
- await data.getInProgress();
77
- const last = rec.calls[rec.calls.length - 1];
78
- expect(last.type).toBe('list-issues');
79
- expect(last.payload.filters.status).toBe('in_progress');
80
- });
81
-
82
- test('getReady uses list-issues with ready:true', async () => {
83
- const rec = makeTransportRecorder();
84
- const data = createDataLayer((t, p) => rec.send(t, p));
85
- await data.getReady();
86
- const last = rec.calls[rec.calls.length - 1];
87
- expect(last.type).toBe('list-issues');
88
- expect(last.payload.filters.ready).toBe(true);
89
- });
90
-
91
- test('getEpicStatus calls epic-status and returns fixture-shaped data', async () => {
92
- const rec = makeTransportRecorder();
93
- const data = createDataLayer(async (t, p) => {
94
- if (t === 'epic-status') {
95
- rec.calls.push({ type: t, payload: p });
96
- return epicFixture;
97
- }
98
- return rec.send(t, p);
99
- });
100
- const res = await data.getEpicStatus();
101
- const last = rec.calls[rec.calls.length - 1];
102
- expect(last.type).toBe('epic-status');
103
- expect(Array.isArray(res)).toBe(true);
104
- // basic shape check from fixture
105
- // @ts-ignore
106
- expect(res[0].epic?.id).toBeDefined();
107
- });
108
-
109
- test('updateIssue dispatches field-specific mutations', async () => {
110
- const rec = makeTransportRecorder();
111
- const data = createDataLayer((t, p) => rec.send(t, p));
112
- await data.updateIssue({
113
- id: 'UI-1',
114
- title: 'X',
115
- acceptance: 'Y',
116
- status: 'in_progress',
117
- priority: 2,
118
- assignee: 'max'
119
- });
120
- const types = rec.calls.map((c) => c.type);
121
- expect(types).toContain('edit-text');
122
- expect(types).toContain('update-status');
123
- expect(types).toContain('update-priority');
124
- expect(types).toContain('update-assignee');
125
- });
126
- });
@@ -1,94 +0,0 @@
1
- import { describe, expect, test, vi } from 'vitest';
2
- import { bootstrap } from './main.js';
3
-
4
- // Mock the Board view to manipulate DOM content deterministically
5
- vi.mock('./views/board.js', () => ({
6
- /**
7
- * @param {HTMLElement} mount
8
- */
9
- createBoardView: (mount) => ({
10
- async load() {
11
- // Simulate a rendered board shell
12
- mount.innerHTML = '<div class="panel__body board-root"></div>';
13
- },
14
- clear() {
15
- // No-op in this test; we no longer depend on clearing when switching views
16
- }
17
- })
18
- }));
19
-
20
- // Mock WS client to avoid network and provide minimal data
21
- vi.mock('./ws.js', () => ({
22
- createWsClient: () => ({
23
- /**
24
- * @param {string} type
25
- */
26
- async send(type) {
27
- if (type === 'list-issues') {
28
- return [];
29
- }
30
- if (type === 'show-issue') {
31
- return null;
32
- }
33
- if (type === 'epic-status') {
34
- return [];
35
- }
36
- return null;
37
- },
38
- on() {
39
- return () => {};
40
- },
41
- close() {},
42
- getState() {
43
- return 'open';
44
- }
45
- })
46
- }));
47
-
48
- describe('board visibility on view change', () => {
49
- test('hides board when leaving and shows again when returning', async () => {
50
- // Start on issues, then go to board so subscribers are active
51
- window.location.hash = '#/issues';
52
- document.body.innerHTML = '<main id="app"></main>';
53
- const root = /** @type {HTMLElement} */ (document.getElementById('app'));
54
-
55
- bootstrap(root);
56
-
57
- // Allow initial render to flush
58
- await Promise.resolve();
59
- await Promise.resolve();
60
-
61
- const boardRoot = /** @type {HTMLElement} */ (
62
- document.getElementById('board-root')
63
- );
64
-
65
- // Navigate to board
66
- window.location.hash = '#/board';
67
- window.dispatchEvent(new HashChangeEvent('hashchange'));
68
- await Promise.resolve();
69
- await Promise.resolve();
70
-
71
- // Board is visible and rendered with its internal shell
72
- expect(boardRoot.hidden).toBe(false);
73
- expect(boardRoot.querySelector('.board-root')).not.toBeNull();
74
-
75
- // Navigate away to issues
76
- window.location.hash = '#/issues';
77
- window.dispatchEvent(new HashChangeEvent('hashchange'));
78
-
79
- await Promise.resolve();
80
- await Promise.resolve();
81
-
82
- // Board route gets hidden but DOM may remain; CSS [hidden] must hide it
83
- expect(boardRoot.hidden).toBe(true);
84
- expect(boardRoot.querySelector('.board-root')).not.toBeNull();
85
-
86
- // Go back to Board, content is still there (or re-rendered by load)
87
- window.location.hash = '#/board';
88
- window.dispatchEvent(new HashChangeEvent('hashchange'));
89
- await Promise.resolve();
90
- await Promise.resolve();
91
- expect(boardRoot.hidden).toBe(false);
92
- expect(boardRoot.querySelector('.board-root')).not.toBeNull();
93
- });
94
- });
@@ -1,64 +0,0 @@
1
- import { describe, expect, test, vi } from 'vitest';
2
- // Import after mocking
3
- import { bootstrap } from './main.js';
4
-
5
- // Mock WS client before importing the app
6
- const calls = [];
7
- const issues = [
8
- { id: 'UI-1', title: 'One', status: 'open', priority: 1 },
9
- { id: 'UI-2', title: 'Two', status: 'open', priority: 2 }
10
- ];
11
- vi.mock('./ws.js', () => ({
12
- createWsClient: () => ({
13
- /**
14
- * @param {string} type
15
- * @param {any} payload
16
- */
17
- async send(type, payload) {
18
- calls.push({ type, payload });
19
- if (type === 'list-issues') {
20
- return issues;
21
- }
22
- if (type === 'show-issue') {
23
- const id = /** @type {any} */ (payload).id;
24
- const it = issues.find((i) => i.id === id);
25
- return it || null;
26
- }
27
- return null;
28
- },
29
- on() {
30
- return () => {};
31
- },
32
- close() {},
33
- getState() {
34
- return 'open';
35
- }
36
- })
37
- }));
38
-
39
- describe('deep link on initial load (UI-44)', () => {
40
- test('loads detail and highlights list item when hash includes issue id', async () => {
41
- window.location.hash = '#/issue/UI-2';
42
- document.body.innerHTML = '<main id="app"></main>';
43
- const root = /** @type {HTMLElement} */ (document.getElementById('app'));
44
-
45
- bootstrap(root);
46
-
47
- // Allow async loads to complete
48
- await Promise.resolve();
49
- await Promise.resolve();
50
-
51
- const detailId = /** @type {HTMLElement} */ (
52
- document.querySelector('#detail-panel .detail-title .detail-id')
53
- );
54
- expect(detailId && detailId.textContent).toBe('#2');
55
-
56
- const list = /** @type {HTMLElement} */ (
57
- document.getElementById('list-root')
58
- );
59
- const selected = /** @type {HTMLElement|null} */ (
60
- list.querySelector('tr.issue-row.selected')
61
- );
62
- expect(selected && selected.getAttribute('data-issue-id')).toBe('UI-2');
63
- });
64
- });
@@ -1,229 +0,0 @@
1
- import { describe, expect, test, vi } from 'vitest';
2
- import { bootstrap } from './main.js';
3
-
4
- // Provide a mutable client instance for module-level mock
5
- /** @type {any} */
6
- let CLIENT = null;
7
- vi.mock('./ws.js', () => ({
8
- createWsClient: () => CLIENT
9
- }));
10
-
11
- describe('live updates: issues-changed handling', () => {
12
- test('refreshes list only when on issues view and preserves scroll', async () => {
13
- /** @type {{ send: import('vitest').Mock, on: (t: string, h: (p:any)=>void)=>void, trigger: (t:string, p:any)=>void }} */
14
- CLIENT = {
15
- send: vi.fn(async (type) => {
16
- if (type === 'list-issues') {
17
- return [
18
- { id: 'UI-1', title: 'A', status: 'open' },
19
- { id: 'UI-2', title: 'B', status: 'open' }
20
- ];
21
- }
22
- if (type === 'show-issue') {
23
- return { id: 'UI-1' };
24
- }
25
- if (type === 'epic-status') {
26
- return [];
27
- }
28
- return null;
29
- }),
30
- /**
31
- * @param {string} _type
32
- * @param {(p:any)=>void} handler
33
- */
34
- on(_type, handler) {
35
- this._handler = handler;
36
- return () => {};
37
- },
38
- /**
39
- * @param {string} type
40
- * @param {any} payload
41
- */
42
- trigger(type, payload) {
43
- if (type === 'issues-changed' && this._handler) this._handler(payload);
44
- },
45
- close() {},
46
- getState() {
47
- return 'open';
48
- }
49
- };
50
-
51
- document.body.innerHTML = '<main id="app"></main>';
52
- const root = /** @type {HTMLElement} */ (document.getElementById('app'));
53
-
54
- bootstrap(root);
55
- await Promise.resolve();
56
-
57
- // Simulate a scrolled list container
58
- const listRoot = /** @type {HTMLElement} */ (
59
- document.getElementById('list-root')
60
- );
61
- if (listRoot) {
62
- listRoot.scrollTop = 120;
63
- }
64
-
65
- const callsBefore = CLIENT.send.mock.calls.length;
66
- CLIENT.trigger('issues-changed', { ts: Date.now() });
67
- await Promise.resolve();
68
-
69
- const callsAfter = CLIENT.send.mock.calls.length;
70
- // One additional list-issues request, no detail fetch
71
- const newCalls = CLIENT.send.mock.calls.slice(callsBefore);
72
- const types = newCalls.map(/** @param {any} c */ (c) => c[0]);
73
- expect(types).toEqual(['list-issues']);
74
-
75
- // Scroll should remain
76
- const listRootAfter = /** @type {HTMLElement} */ (
77
- document.getElementById('list-root')
78
- );
79
- expect(listRootAfter.scrollTop).toBe(120);
80
- expect(callsAfter).toBe(callsBefore + 1);
81
- });
82
-
83
- test('refreshes detail only when detail is visible and id matches hint', async () => {
84
- /** @type {{ send: import('vitest').Mock, on: (t: string, h: (p:any)=>void)=>void, trigger: (t:string, p:any)=>void }} */
85
- CLIENT = {
86
- send: vi.fn(async (type, payload) => {
87
- if (type === 'list-issues') {
88
- return [];
89
- }
90
- if (type === 'show-issue') {
91
- return { id: payload.id };
92
- }
93
- if (type === 'epic-status') {
94
- return [];
95
- }
96
- return null;
97
- }),
98
- /**
99
- * @param {string} _type
100
- * @param {(p:any)=>void} handler
101
- */
102
- on(_type, handler) {
103
- this._handler = handler;
104
- return () => {};
105
- },
106
- /**
107
- * @param {string} type
108
- * @param {any} payload
109
- */
110
- trigger(type, payload) {
111
- if (type === 'issues-changed' && this._handler) this._handler(payload);
112
- },
113
- close() {},
114
- getState() {
115
- return 'open';
116
- }
117
- };
118
-
119
- // Navigate to detail view
120
- window.location.hash = '#/issue/UI-1';
121
- document.body.innerHTML = '<main id="app"></main>';
122
- const root = /** @type {HTMLElement} */ (document.getElementById('app'));
123
-
124
- bootstrap(root);
125
- await Promise.resolve();
126
-
127
- CLIENT.send.mockClear();
128
- CLIENT.trigger('issues-changed', {
129
- ts: Date.now(),
130
- hint: { ids: ['UI-1'] }
131
- });
132
- await Promise.resolve();
133
-
134
- const calls = CLIENT.send.mock.calls.map(/** @param {any} c */ (c) => c[0]);
135
- expect(calls).toEqual(['show-issue']);
136
- });
137
-
138
- test('refreshes epics when epics view visible', async () => {
139
- CLIENT = {
140
- send: vi.fn(async (type) => {
141
- if (type === 'epic-status') {
142
- return [];
143
- }
144
- return [];
145
- }),
146
- /**
147
- * @param {string} _type
148
- * @param {(p:any)=>void} handler
149
- */
150
- on(_type, handler) {
151
- this._handler = handler;
152
- return () => {};
153
- },
154
- /**
155
- * @param {string} type
156
- * @param {any} payload
157
- */
158
- trigger(type, payload) {
159
- if (type === 'issues-changed' && this._handler) this._handler(payload);
160
- },
161
- close() {},
162
- getState() {
163
- return 'open';
164
- }
165
- };
166
-
167
- window.location.hash = '#/epics';
168
- document.body.innerHTML = '<main id="app"></main>';
169
- const root = /** @type {HTMLElement} */ (document.getElementById('app'));
170
- bootstrap(root);
171
- await Promise.resolve();
172
-
173
- // Ignore initial load
174
- CLIENT.send.mockClear();
175
- CLIENT.trigger('issues-changed', { ts: Date.now() });
176
- await Promise.resolve();
177
-
178
- const calls = CLIENT.send.mock.calls.map(/** @param {any} c */ (c) => c[0]);
179
- expect(calls).toEqual(['epic-status']);
180
- });
181
-
182
- test('refreshes board when board view visible', async () => {
183
- CLIENT = {
184
- send: vi.fn(async (type) => {
185
- if (type === 'list-issues') {
186
- return [];
187
- }
188
- if (type === 'epic-status') {
189
- return [];
190
- }
191
- return [];
192
- }),
193
- /**
194
- * @param {string} _type
195
- * @param {(p:any)=>void} handler
196
- */
197
- on(_type, handler) {
198
- this._handler = handler;
199
- return () => {};
200
- },
201
- /**
202
- * @param {string} type
203
- * @param {any} payload
204
- */
205
- trigger(type, payload) {
206
- if (type === 'issues-changed' && this._handler) this._handler(payload);
207
- },
208
- close() {},
209
- getState() {
210
- return 'open';
211
- }
212
- };
213
-
214
- window.location.hash = '#/board';
215
- document.body.innerHTML = '<main id="app"></main>';
216
- const root = /** @type {HTMLElement} */ (document.getElementById('app'));
217
- bootstrap(root);
218
- await Promise.resolve();
219
-
220
- CLIENT.send.mockClear();
221
- CLIENT.trigger('issues-changed', { ts: Date.now() });
222
- await Promise.resolve();
223
-
224
- const calls = CLIENT.send.mock.calls.map(/** @param {any} c */ (c) => c[0]);
225
- // Board loads multiple list-issues, assert at least one
226
- expect(calls.length > 0).toBe(true);
227
- expect(new Set(calls).has('list-issues')).toBe(true);
228
- });
229
- });
package/app/main.test.js DELETED
@@ -1,17 +0,0 @@
1
- import { describe, expect, test } from 'vitest';
2
- import { bootstrap } from './main.js';
3
-
4
- describe('app/main (jsdom)', () => {
5
- test('renders two-panel shell into root', () => {
6
- document.body.innerHTML = '<main id="app"></main>';
7
- const root_element = /** @type {HTMLElement} */ (
8
- document.getElementById('app')
9
- );
10
- bootstrap(root_element);
11
-
12
- const list_panel = root_element.querySelector('#list-panel');
13
- const detail_panel = root_element.querySelector('#detail-panel');
14
- expect(list_panel).not.toBeNull();
15
- expect(detail_panel).not.toBeNull();
16
- });
17
- });