beads-ui 0.1.1 → 0.2.0

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 (82) hide show
  1. package/CHANGES.md +27 -1
  2. package/README.md +39 -45
  3. package/app/data/providers.js +57 -26
  4. package/app/index.html +8 -0
  5. package/app/main.js +179 -33
  6. package/app/protocol.md +3 -4
  7. package/app/router.js +45 -9
  8. package/app/state.js +27 -11
  9. package/app/styles.css +170 -6
  10. package/app/utils/issue-id-renderer.js +71 -0
  11. package/app/utils/issue-url.js +9 -0
  12. package/app/utils/toast.js +35 -0
  13. package/app/views/board.js +347 -17
  14. package/app/views/detail.js +292 -92
  15. package/app/views/epics.js +2 -2
  16. package/app/views/issue-dialog.js +170 -0
  17. package/app/views/issue-row.js +9 -8
  18. package/app/views/list.js +85 -11
  19. package/app/views/new-issue-dialog.js +352 -0
  20. package/app/ws.js +30 -0
  21. package/docs/architecture.md +1 -1
  22. package/package.json +17 -1
  23. package/server/cli/commands.js +11 -3
  24. package/server/cli/index.js +35 -4
  25. package/server/cli/usage.js +1 -1
  26. package/server/watcher.js +3 -3
  27. package/server/ws.js +39 -19
  28. package/.beads/issues.jsonl +0 -107
  29. package/.editorconfig +0 -10
  30. package/.eslintrc.json +0 -36
  31. package/.github/workflows/ci.yml +0 -38
  32. package/.prettierignore +0 -5
  33. package/AGENTS.md +0 -85
  34. package/app/data/providers.test.js +0 -126
  35. package/app/main.board-switch.test.js +0 -94
  36. package/app/main.deep-link.test.js +0 -64
  37. package/app/main.live-updates.test.js +0 -229
  38. package/app/main.test.js +0 -17
  39. package/app/main.theme.test.js +0 -41
  40. package/app/main.view-sync.test.js +0 -54
  41. package/app/protocol.test.js +0 -57
  42. package/app/router.test.js +0 -34
  43. package/app/state.test.js +0 -21
  44. package/app/utils/markdown.test.js +0 -103
  45. package/app/utils/type-badge.test.js +0 -30
  46. package/app/views/board.test.js +0 -184
  47. package/app/views/detail.acceptance-notes.test.js +0 -67
  48. package/app/views/detail.assignee.test.js +0 -161
  49. package/app/views/detail.deps.test.js +0 -97
  50. package/app/views/detail.edits.test.js +0 -146
  51. package/app/views/detail.labels.test.js +0 -73
  52. package/app/views/detail.priority.test.js +0 -86
  53. package/app/views/detail.test.js +0 -188
  54. package/app/views/detail.ui47.test.js +0 -78
  55. package/app/views/epics.test.js +0 -283
  56. package/app/views/list.inline-edits.test.js +0 -84
  57. package/app/views/list.test.js +0 -479
  58. package/app/views/nav.test.js +0 -43
  59. package/app/ws.test.js +0 -168
  60. package/docs/quickstart.md +0 -142
  61. package/eslint.config.js +0 -59
  62. package/media/bdui-board.png +0 -0
  63. package/media/bdui-epics.png +0 -0
  64. package/media/bdui-issues.png +0 -0
  65. package/prettier.config.js +0 -13
  66. package/server/app.test.js +0 -29
  67. package/server/bd.test.js +0 -93
  68. package/server/cli/cli.test.js +0 -109
  69. package/server/cli/commands.integration.test.js +0 -155
  70. package/server/cli/commands.unit.test.js +0 -94
  71. package/server/cli/open.test.js +0 -26
  72. package/server/db.test.js +0 -70
  73. package/server/protocol.test.js +0 -87
  74. package/server/watcher.test.js +0 -100
  75. package/server/ws.handlers.test.js +0 -174
  76. package/server/ws.labels.test.js +0 -95
  77. package/server/ws.mutations.test.js +0 -261
  78. package/server/ws.subscriptions.test.js +0 -116
  79. package/server/ws.test.js +0 -52
  80. package/test/setup-vitest.js +0 -12
  81. package/tsconfig.json +0 -23
  82. package/vitest.config.mjs +0 -14
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
- });
@@ -1,41 +0,0 @@
1
- import { describe, expect, test } from 'vitest';
2
-
3
- describe('theme toggle', () => {
4
- test('sets dark data-theme and persists preference', async () => {
5
- document.body.innerHTML = `
6
- <header class="app-header">
7
- <h1 class="app-title">beads-ui</h1>
8
- <div class="header-actions">
9
- <label class="theme-toggle">
10
- <span>Dark</span>
11
- <input id="theme-switch" type="checkbox" />
12
- </label>
13
- </div>
14
- </header>
15
- <main id="app"></main>`;
16
-
17
- // Simulate the DOMContentLoaded logic from main.js
18
- const themeSwitch = /** @type {HTMLInputElement} */ (
19
- document.getElementById('theme-switch')
20
- );
21
- themeSwitch.checked = true;
22
- themeSwitch.dispatchEvent(new Event('change'));
23
-
24
- // Apply attribute as in main.js handler
25
- document.documentElement.setAttribute('data-theme', 'dark');
26
- window.localStorage.setItem('beads-ui.theme', 'dark');
27
-
28
- expect(document.documentElement.getAttribute('data-theme')).toBe('dark');
29
- expect(window.localStorage.getItem('beads-ui.theme')).toBe('dark');
30
- });
31
-
32
- test('can switch back to light explicitly', async () => {
33
- document.documentElement.setAttribute('data-theme', 'dark');
34
- window.localStorage.setItem('beads-ui.theme', 'dark');
35
- // Simulate toggle off
36
- document.documentElement.setAttribute('data-theme', 'light');
37
- window.localStorage.setItem('beads-ui.theme', 'light');
38
- expect(document.documentElement.getAttribute('data-theme')).toBe('light');
39
- expect(window.localStorage.getItem('beads-ui.theme')).toBe('light');
40
- });
41
- });