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.
- package/CHANGES.md +8 -0
- package/README.md +7 -3
- package/package.json +12 -2
- package/.beads/issues.jsonl +0 -107
- package/.editorconfig +0 -10
- package/.eslintrc.json +0 -36
- package/.github/workflows/ci.yml +0 -38
- package/.prettierignore +0 -5
- package/AGENTS.md +0 -85
- package/app/data/providers.test.js +0 -126
- package/app/main.board-switch.test.js +0 -94
- package/app/main.deep-link.test.js +0 -64
- package/app/main.live-updates.test.js +0 -229
- package/app/main.test.js +0 -17
- package/app/main.theme.test.js +0 -41
- package/app/main.view-sync.test.js +0 -54
- package/app/protocol.test.js +0 -57
- package/app/router.test.js +0 -34
- package/app/state.test.js +0 -21
- package/app/utils/markdown.test.js +0 -103
- package/app/utils/type-badge.test.js +0 -30
- package/app/views/board.test.js +0 -184
- package/app/views/detail.acceptance-notes.test.js +0 -67
- package/app/views/detail.assignee.test.js +0 -161
- package/app/views/detail.deps.test.js +0 -97
- package/app/views/detail.edits.test.js +0 -146
- package/app/views/detail.labels.test.js +0 -73
- package/app/views/detail.priority.test.js +0 -86
- package/app/views/detail.test.js +0 -188
- package/app/views/detail.ui47.test.js +0 -78
- package/app/views/epics.test.js +0 -283
- package/app/views/list.inline-edits.test.js +0 -84
- package/app/views/list.test.js +0 -479
- package/app/views/nav.test.js +0 -43
- package/app/ws.test.js +0 -168
- package/eslint.config.js +0 -59
- package/media/bdui-board.png +0 -0
- package/media/bdui-epics.png +0 -0
- package/media/bdui-issues.png +0 -0
- package/prettier.config.js +0 -13
- package/server/app.test.js +0 -29
- package/server/bd.test.js +0 -93
- package/server/cli/cli.test.js +0 -109
- package/server/cli/commands.integration.test.js +0 -155
- package/server/cli/commands.unit.test.js +0 -94
- package/server/cli/open.test.js +0 -26
- package/server/db.test.js +0 -70
- package/server/protocol.test.js +0 -87
- package/server/watcher.test.js +0 -100
- package/server/ws.handlers.test.js +0 -174
- package/server/ws.labels.test.js +0 -95
- package/server/ws.mutations.test.js +0 -261
- package/server/ws.subscriptions.test.js +0 -116
- package/server/ws.test.js +0 -52
- package/test/setup-vitest.js +0 -12
- package/tsconfig.json +0 -23
- 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
|
-
}
|
package/.github/workflows/ci.yml
DELETED
|
@@ -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
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
|
-
});
|