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
|
@@ -1,161 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test, vi } from 'vitest';
|
|
2
|
-
import { createDetailView } from './detail.js';
|
|
3
|
-
|
|
4
|
-
/** @type {(impl: (type: string, payload?: unknown) => Promise<any>) => (type: string, payload?: unknown) => Promise<any>} */
|
|
5
|
-
const mockSend = (impl) => vi.fn(impl);
|
|
6
|
-
|
|
7
|
-
describe('views/detail assignee edit', () => {
|
|
8
|
-
test('edits assignee via Properties control', async () => {
|
|
9
|
-
document.body.innerHTML =
|
|
10
|
-
'<section class="panel"><div id="mount"></div></section>';
|
|
11
|
-
const mount = /** @type {HTMLElement} */ (document.getElementById('mount'));
|
|
12
|
-
|
|
13
|
-
const issue = {
|
|
14
|
-
id: 'UI-57',
|
|
15
|
-
title: 'Detail screen',
|
|
16
|
-
description: '',
|
|
17
|
-
status: 'open',
|
|
18
|
-
priority: 2,
|
|
19
|
-
assignee: 'alice',
|
|
20
|
-
dependencies: [],
|
|
21
|
-
dependents: []
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
const send = mockSend(async (type, payload) => {
|
|
25
|
-
if (type === 'show-issue') {
|
|
26
|
-
return issue;
|
|
27
|
-
}
|
|
28
|
-
if (type === 'update-assignee') {
|
|
29
|
-
expect(payload).toEqual({ id: 'UI-57', assignee: 'max' });
|
|
30
|
-
const next = { ...issue, assignee: 'max' };
|
|
31
|
-
return next;
|
|
32
|
-
}
|
|
33
|
-
throw new Error('Unexpected');
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
const view = createDetailView(mount, send);
|
|
37
|
-
await view.load('UI-57');
|
|
38
|
-
|
|
39
|
-
const assigneeSpan = /** @type {HTMLSpanElement} */ (
|
|
40
|
-
mount.querySelector('#detail-root .prop.assignee .value .editable')
|
|
41
|
-
);
|
|
42
|
-
expect(assigneeSpan).toBeTruthy();
|
|
43
|
-
expect(assigneeSpan.textContent).toBe('alice');
|
|
44
|
-
|
|
45
|
-
assigneeSpan.click();
|
|
46
|
-
const input = /** @type {HTMLInputElement} */ (
|
|
47
|
-
mount.querySelector('#detail-root .prop.assignee input')
|
|
48
|
-
);
|
|
49
|
-
const saveBtn = /** @type {HTMLButtonElement} */ (
|
|
50
|
-
mount.querySelector('#detail-root .prop.assignee button')
|
|
51
|
-
);
|
|
52
|
-
input.value = 'max';
|
|
53
|
-
saveBtn.click();
|
|
54
|
-
|
|
55
|
-
await Promise.resolve();
|
|
56
|
-
|
|
57
|
-
const assigneeSpan2 = /** @type {HTMLSpanElement} */ (
|
|
58
|
-
mount.querySelector('#detail-root .prop.assignee .value .editable')
|
|
59
|
-
);
|
|
60
|
-
expect(assigneeSpan2.textContent).toBe('max');
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
test('shows editable placeholder when unassigned', async () => {
|
|
64
|
-
document.body.innerHTML =
|
|
65
|
-
'<section class="panel"><div id="mount"></div></section>';
|
|
66
|
-
const mount = /** @type {HTMLElement} */ (document.getElementById('mount'));
|
|
67
|
-
|
|
68
|
-
const issue = {
|
|
69
|
-
id: 'UI-88',
|
|
70
|
-
title: 'No assignee yet',
|
|
71
|
-
description: '',
|
|
72
|
-
status: 'open',
|
|
73
|
-
priority: 2,
|
|
74
|
-
// no assignee field
|
|
75
|
-
dependencies: [],
|
|
76
|
-
dependents: []
|
|
77
|
-
};
|
|
78
|
-
|
|
79
|
-
const send = mockSend(async (type, payload) => {
|
|
80
|
-
if (type === 'show-issue') {
|
|
81
|
-
return issue;
|
|
82
|
-
}
|
|
83
|
-
if (type === 'update-assignee') {
|
|
84
|
-
const next = {
|
|
85
|
-
...issue,
|
|
86
|
-
assignee: /** @type {any} */ (payload).assignee
|
|
87
|
-
};
|
|
88
|
-
return next;
|
|
89
|
-
}
|
|
90
|
-
throw new Error('Unexpected');
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
const view = createDetailView(mount, send);
|
|
94
|
-
await view.load('UI-88');
|
|
95
|
-
|
|
96
|
-
const ph = /** @type {HTMLSpanElement} */ (
|
|
97
|
-
mount.querySelector('#detail-root .prop.assignee .value .editable')
|
|
98
|
-
);
|
|
99
|
-
expect(ph).toBeTruthy();
|
|
100
|
-
expect(ph.className).toContain('muted');
|
|
101
|
-
expect(ph.textContent).toBe('Unassigned');
|
|
102
|
-
|
|
103
|
-
ph.click();
|
|
104
|
-
const input = /** @type {HTMLInputElement} */ (
|
|
105
|
-
mount.querySelector('#detail-root .prop.assignee input')
|
|
106
|
-
);
|
|
107
|
-
expect(input).toBeTruthy();
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
test('clears assignee to empty string and shows placeholder', async () => {
|
|
111
|
-
document.body.innerHTML =
|
|
112
|
-
'<section class="panel"><div id="mount"></div></section>';
|
|
113
|
-
const mount = /** @type {HTMLElement} */ (document.getElementById('mount'));
|
|
114
|
-
|
|
115
|
-
const issue = {
|
|
116
|
-
id: 'UI-31',
|
|
117
|
-
title: 'Clearable',
|
|
118
|
-
status: 'open',
|
|
119
|
-
priority: 2,
|
|
120
|
-
assignee: 'bob',
|
|
121
|
-
dependencies: [],
|
|
122
|
-
dependents: []
|
|
123
|
-
};
|
|
124
|
-
|
|
125
|
-
const send = mockSend(async (type, payload) => {
|
|
126
|
-
if (type === 'show-issue') {
|
|
127
|
-
return issue;
|
|
128
|
-
}
|
|
129
|
-
if (type === 'update-assignee') {
|
|
130
|
-
const next = {
|
|
131
|
-
...issue,
|
|
132
|
-
assignee: /** @type {any} */ (payload).assignee
|
|
133
|
-
};
|
|
134
|
-
return next;
|
|
135
|
-
}
|
|
136
|
-
throw new Error('Unexpected');
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
const view = createDetailView(mount, send);
|
|
140
|
-
await view.load('UI-31');
|
|
141
|
-
|
|
142
|
-
const span = /** @type {HTMLSpanElement} */ (
|
|
143
|
-
mount.querySelector('#detail-root .prop.assignee .value .editable')
|
|
144
|
-
);
|
|
145
|
-
span.click();
|
|
146
|
-
const input = /** @type {HTMLInputElement} */ (
|
|
147
|
-
mount.querySelector('#detail-root .prop.assignee input')
|
|
148
|
-
);
|
|
149
|
-
const save = /** @type {HTMLButtonElement} */ (
|
|
150
|
-
mount.querySelector('#detail-root .prop.assignee button')
|
|
151
|
-
);
|
|
152
|
-
input.value = '';
|
|
153
|
-
save.click();
|
|
154
|
-
await Promise.resolve();
|
|
155
|
-
const span2 = /** @type {HTMLSpanElement} */ (
|
|
156
|
-
mount.querySelector('#detail-root .prop.assignee .value .editable')
|
|
157
|
-
);
|
|
158
|
-
expect(span2.textContent).toBe('Unassigned');
|
|
159
|
-
expect(span2.className).toContain('muted');
|
|
160
|
-
});
|
|
161
|
-
});
|
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test, vi } from 'vitest';
|
|
2
|
-
import { createDetailView } from './detail.js';
|
|
3
|
-
|
|
4
|
-
function setupDom() {
|
|
5
|
-
const root = document.createElement('div');
|
|
6
|
-
document.body.appendChild(root);
|
|
7
|
-
return root;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
describe('views/detail dependencies', () => {
|
|
11
|
-
test('adds Dependencies link and re-renders', async () => {
|
|
12
|
-
const mount = setupDom();
|
|
13
|
-
const send = vi
|
|
14
|
-
.fn()
|
|
15
|
-
// initial show
|
|
16
|
-
.mockResolvedValueOnce({
|
|
17
|
-
id: 'UI-10',
|
|
18
|
-
title: 'X',
|
|
19
|
-
dependencies: [],
|
|
20
|
-
dependents: []
|
|
21
|
-
})
|
|
22
|
-
// dep-add returns updated issue
|
|
23
|
-
.mockResolvedValueOnce({
|
|
24
|
-
id: 'UI-10',
|
|
25
|
-
dependencies: [{ id: 'UI-2' }],
|
|
26
|
-
dependents: []
|
|
27
|
-
});
|
|
28
|
-
const view = createDetailView(mount, /** @type {any} */ (send));
|
|
29
|
-
await view.load('UI-10');
|
|
30
|
-
|
|
31
|
-
const input = mount.querySelector('[data-testid="add-dependency"]');
|
|
32
|
-
expect(input).toBeTruthy();
|
|
33
|
-
/** @type {HTMLInputElement} */
|
|
34
|
-
const el = /** @type {any} */ (input);
|
|
35
|
-
el.value = 'UI-2';
|
|
36
|
-
const addBtn = el.nextElementSibling;
|
|
37
|
-
addBtn?.dispatchEvent(new window.Event('click'));
|
|
38
|
-
|
|
39
|
-
// Next tick
|
|
40
|
-
await Promise.resolve();
|
|
41
|
-
|
|
42
|
-
// Should have called dep-add
|
|
43
|
-
const calls = send.mock.calls.map((c) => c[0]);
|
|
44
|
-
expect(calls.includes('dep-add')).toBe(true);
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
test('removes Blocks link', async () => {
|
|
48
|
-
const mount = setupDom();
|
|
49
|
-
const send = vi
|
|
50
|
-
.fn()
|
|
51
|
-
// initial show
|
|
52
|
-
.mockResolvedValueOnce({
|
|
53
|
-
id: 'UI-20',
|
|
54
|
-
title: 'Y',
|
|
55
|
-
dependencies: [],
|
|
56
|
-
dependents: [{ id: 'UI-5' }]
|
|
57
|
-
})
|
|
58
|
-
// dep-remove returns updated issue
|
|
59
|
-
.mockResolvedValueOnce({ id: 'UI-20', dependencies: [], dependents: [] });
|
|
60
|
-
const view = createDetailView(mount, /** @type {any} */ (send));
|
|
61
|
-
await view.load('UI-20');
|
|
62
|
-
|
|
63
|
-
// Find the remove button next to link #5
|
|
64
|
-
const btns = mount.querySelectorAll('button');
|
|
65
|
-
const rm = Array.from(btns).find((b) =>
|
|
66
|
-
b.getAttribute('aria-label')?.includes('#5')
|
|
67
|
-
);
|
|
68
|
-
expect(rm).toBeTruthy();
|
|
69
|
-
rm?.dispatchEvent(new window.Event('click'));
|
|
70
|
-
|
|
71
|
-
await Promise.resolve();
|
|
72
|
-
const calls = send.mock.calls.map((c) => c[0]);
|
|
73
|
-
expect(calls.includes('dep-remove')).toBe(true);
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
test('prevents duplicate link add', async () => {
|
|
77
|
-
const mount = setupDom();
|
|
78
|
-
const send = vi.fn().mockResolvedValueOnce({
|
|
79
|
-
id: 'UI-30',
|
|
80
|
-
dependencies: [{ id: 'UI-9' }],
|
|
81
|
-
dependents: []
|
|
82
|
-
});
|
|
83
|
-
const view = createDetailView(mount, /** @type {any} */ (send));
|
|
84
|
-
await view.load('UI-30');
|
|
85
|
-
|
|
86
|
-
const input = mount.querySelector('[data-testid="add-dependency"]');
|
|
87
|
-
const el = /** @type {HTMLInputElement} */ (/** @type {any} */ (input));
|
|
88
|
-
el.value = 'UI-9';
|
|
89
|
-
const addBtn = el.nextElementSibling;
|
|
90
|
-
addBtn?.dispatchEvent(new window.Event('click'));
|
|
91
|
-
|
|
92
|
-
await Promise.resolve();
|
|
93
|
-
// send should not be called with dep-add
|
|
94
|
-
const calls = send.mock.calls.map((c) => c[0]);
|
|
95
|
-
expect(calls.includes('dep-add')).toBe(false);
|
|
96
|
-
});
|
|
97
|
-
});
|
|
@@ -1,146 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test, vi } from 'vitest';
|
|
2
|
-
import { createDetailView } from './detail.js';
|
|
3
|
-
|
|
4
|
-
/** @type {(impl: (type: string, payload?: unknown) => Promise<any>) => (type: string, payload?: unknown) => Promise<any>} */
|
|
5
|
-
const mockSend = (impl) => vi.fn(impl);
|
|
6
|
-
|
|
7
|
-
describe('views/detail edits', () => {
|
|
8
|
-
test('updates status via dropdown and disables while pending', async () => {
|
|
9
|
-
document.body.innerHTML =
|
|
10
|
-
'<section class="panel"><div id="mount"></div></section>';
|
|
11
|
-
const mount = /** @type {HTMLElement} */ (document.getElementById('mount'));
|
|
12
|
-
|
|
13
|
-
const initial = {
|
|
14
|
-
id: 'UI-7',
|
|
15
|
-
title: 'T',
|
|
16
|
-
description: 'D',
|
|
17
|
-
status: 'open',
|
|
18
|
-
priority: 2
|
|
19
|
-
};
|
|
20
|
-
const updated = { ...initial, status: 'in_progress' };
|
|
21
|
-
|
|
22
|
-
const send = mockSend(async (type, payload) => {
|
|
23
|
-
if (type === 'show-issue') {
|
|
24
|
-
return initial;
|
|
25
|
-
}
|
|
26
|
-
if (type === 'update-status') {
|
|
27
|
-
expect(payload).toEqual({ id: 'UI-7', status: 'in_progress' });
|
|
28
|
-
// simulate server reconcile payload
|
|
29
|
-
return updated;
|
|
30
|
-
}
|
|
31
|
-
throw new Error('Unexpected');
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
const view = createDetailView(mount, send);
|
|
35
|
-
await view.load('UI-7');
|
|
36
|
-
|
|
37
|
-
const select = /** @type {HTMLSelectElement} */ (
|
|
38
|
-
mount.querySelector('select')
|
|
39
|
-
);
|
|
40
|
-
expect(select.value).toBe('open');
|
|
41
|
-
|
|
42
|
-
// Trigger change
|
|
43
|
-
select.value = 'in_progress';
|
|
44
|
-
const beforeDisabled = select.disabled;
|
|
45
|
-
select.dispatchEvent(new Event('change'));
|
|
46
|
-
// After dispatch, the component sets disabled & will re-render upon reply
|
|
47
|
-
expect(beforeDisabled || select.disabled).toBe(true);
|
|
48
|
-
|
|
49
|
-
// After async flow, DOM should reflect updated status
|
|
50
|
-
await Promise.resolve(); // allow microtasks
|
|
51
|
-
const select2 = /** @type {HTMLSelectElement} */ (
|
|
52
|
-
mount.querySelector('select')
|
|
53
|
-
);
|
|
54
|
-
expect(select2.value).toBe('in_progress');
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
test('saves title and re-renders from reply', async () => {
|
|
58
|
-
document.body.innerHTML =
|
|
59
|
-
'<section class="panel"><div id="mount"></div></section>';
|
|
60
|
-
const mount = /** @type {HTMLElement} */ (document.getElementById('mount'));
|
|
61
|
-
const initial = {
|
|
62
|
-
id: 'UI-8',
|
|
63
|
-
title: 'Old',
|
|
64
|
-
description: '',
|
|
65
|
-
status: 'open',
|
|
66
|
-
priority: 1
|
|
67
|
-
};
|
|
68
|
-
const send = mockSend(async (type, payload) => {
|
|
69
|
-
if (type === 'show-issue') {
|
|
70
|
-
return initial;
|
|
71
|
-
}
|
|
72
|
-
if (type === 'edit-text') {
|
|
73
|
-
const next = { ...initial, title: /** @type {any} */ (payload).value };
|
|
74
|
-
return next;
|
|
75
|
-
}
|
|
76
|
-
throw new Error('Unexpected');
|
|
77
|
-
});
|
|
78
|
-
const view = createDetailView(mount, send);
|
|
79
|
-
await view.load('UI-8');
|
|
80
|
-
// Enter edit mode by clicking the span
|
|
81
|
-
const titleSpan = /** @type {HTMLSpanElement} */ (
|
|
82
|
-
mount.querySelector('h2 .editable')
|
|
83
|
-
);
|
|
84
|
-
titleSpan.click();
|
|
85
|
-
const titleInput = /** @type {HTMLInputElement} */ (
|
|
86
|
-
mount.querySelector('h2 input')
|
|
87
|
-
);
|
|
88
|
-
const titleSave = /** @type {HTMLButtonElement} */ (
|
|
89
|
-
mount.querySelector('h2 button')
|
|
90
|
-
);
|
|
91
|
-
titleInput.value = 'New Title';
|
|
92
|
-
titleSave.click();
|
|
93
|
-
await Promise.resolve();
|
|
94
|
-
// After save, returns to read mode with updated text
|
|
95
|
-
const titleSpan2 = /** @type {HTMLSpanElement} */ (
|
|
96
|
-
mount.querySelector('h2 .editable')
|
|
97
|
-
);
|
|
98
|
-
expect(titleSpan2.textContent).toBe('New Title');
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
test('shows toast on description save error and re-enables', async () => {
|
|
102
|
-
vi.useFakeTimers();
|
|
103
|
-
document.body.innerHTML =
|
|
104
|
-
'<section class="panel"><div id="mount"></div></section>';
|
|
105
|
-
const mount = /** @type {HTMLElement} */ (document.getElementById('mount'));
|
|
106
|
-
const initial = {
|
|
107
|
-
id: 'UI-9',
|
|
108
|
-
title: 'T',
|
|
109
|
-
description: 'D',
|
|
110
|
-
status: 'open',
|
|
111
|
-
priority: 2
|
|
112
|
-
};
|
|
113
|
-
const send = mockSend(async (type) => {
|
|
114
|
-
if (type === 'show-issue') {
|
|
115
|
-
return initial;
|
|
116
|
-
}
|
|
117
|
-
if (type === 'edit-text') {
|
|
118
|
-
throw new Error('boom');
|
|
119
|
-
}
|
|
120
|
-
throw new Error('Unexpected');
|
|
121
|
-
});
|
|
122
|
-
const view = createDetailView(mount, send);
|
|
123
|
-
await view.load('UI-9');
|
|
124
|
-
// Enter edit mode
|
|
125
|
-
const md = /** @type {HTMLDivElement} */ (mount.querySelector('.md'));
|
|
126
|
-
md.click();
|
|
127
|
-
const ta = /** @type {HTMLTextAreaElement} */ (
|
|
128
|
-
mount.querySelector('textarea')
|
|
129
|
-
);
|
|
130
|
-
const btn = /** @type {HTMLButtonElement} */ (
|
|
131
|
-
mount.querySelector('.editable-actions button')
|
|
132
|
-
);
|
|
133
|
-
ta.value = 'New D';
|
|
134
|
-
btn.click();
|
|
135
|
-
await Promise.resolve();
|
|
136
|
-
// Toast appears
|
|
137
|
-
const toast = /** @type {HTMLElement} */ (mount.querySelector('.toast'));
|
|
138
|
-
expect(toast).not.toBeNull();
|
|
139
|
-
expect((toast.textContent || '').toLowerCase()).toContain(
|
|
140
|
-
'failed to save description'
|
|
141
|
-
);
|
|
142
|
-
// Auto-dismiss after a while
|
|
143
|
-
await vi.advanceTimersByTimeAsync(3000);
|
|
144
|
-
vi.useRealTimers();
|
|
145
|
-
});
|
|
146
|
-
});
|
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test, vi } from 'vitest';
|
|
2
|
-
import { createDetailView } from './detail.js';
|
|
3
|
-
|
|
4
|
-
function mountDiv() {
|
|
5
|
-
const div = document.createElement('div');
|
|
6
|
-
document.body.appendChild(div);
|
|
7
|
-
return div;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
describe('detail view labels', () => {
|
|
11
|
-
test('shows labels and allows add/remove', async () => {
|
|
12
|
-
const mount = mountDiv();
|
|
13
|
-
let current = {
|
|
14
|
-
id: 'UI-5',
|
|
15
|
-
title: 'With labels',
|
|
16
|
-
status: 'open',
|
|
17
|
-
priority: 2,
|
|
18
|
-
labels: ['frontend']
|
|
19
|
-
};
|
|
20
|
-
const sendFn = vi.fn(async (type, payload) => {
|
|
21
|
-
if (type === 'show-issue') {
|
|
22
|
-
return current;
|
|
23
|
-
}
|
|
24
|
-
if (type === 'label-add') {
|
|
25
|
-
current = { ...current, labels: [...current.labels, payload.label] };
|
|
26
|
-
return current;
|
|
27
|
-
}
|
|
28
|
-
if (type === 'label-remove') {
|
|
29
|
-
current = {
|
|
30
|
-
...current,
|
|
31
|
-
labels: current.labels.filter((l) => l !== payload.label)
|
|
32
|
-
};
|
|
33
|
-
return current;
|
|
34
|
-
}
|
|
35
|
-
return current;
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
const view = createDetailView(mount, sendFn);
|
|
39
|
-
await view.load('UI-5');
|
|
40
|
-
|
|
41
|
-
// Initial chip present
|
|
42
|
-
expect(mount.querySelectorAll('.prop.labels .badge').length).toBe(1);
|
|
43
|
-
|
|
44
|
-
// Add a label via input + Enter
|
|
45
|
-
const input = /** @type {HTMLInputElement} */ (
|
|
46
|
-
mount.querySelector('.prop.labels input')
|
|
47
|
-
);
|
|
48
|
-
input.value = 'backend';
|
|
49
|
-
input.dispatchEvent(new Event('input', { bubbles: true }));
|
|
50
|
-
input.dispatchEvent(
|
|
51
|
-
new KeyboardEvent('keydown', { key: 'Enter', bubbles: true })
|
|
52
|
-
);
|
|
53
|
-
await Promise.resolve();
|
|
54
|
-
|
|
55
|
-
expect(sendFn).toHaveBeenCalledWith('label-add', {
|
|
56
|
-
id: 'UI-5',
|
|
57
|
-
label: 'backend'
|
|
58
|
-
});
|
|
59
|
-
expect(mount.querySelectorAll('.prop.labels .badge').length).toBe(2);
|
|
60
|
-
|
|
61
|
-
// Remove the first label by clicking the × button
|
|
62
|
-
const removeBtn = /** @type {HTMLButtonElement} */ (
|
|
63
|
-
mount.querySelector('.prop.labels .badge button')
|
|
64
|
-
);
|
|
65
|
-
removeBtn.click();
|
|
66
|
-
await Promise.resolve();
|
|
67
|
-
expect(sendFn).toHaveBeenCalledWith('label-remove', {
|
|
68
|
-
id: 'UI-5',
|
|
69
|
-
label: 'frontend'
|
|
70
|
-
});
|
|
71
|
-
expect(mount.querySelectorAll('.prop.labels .badge').length).toBe(1);
|
|
72
|
-
});
|
|
73
|
-
});
|
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test, vi } from 'vitest';
|
|
2
|
-
import { createDetailView } from './detail.js';
|
|
3
|
-
|
|
4
|
-
/** @type {(impl: (type: string, payload?: unknown) => Promise<any>) => (type: string, payload?: unknown) => Promise<any>} */
|
|
5
|
-
const mockSend = (impl) => vi.fn(impl);
|
|
6
|
-
|
|
7
|
-
describe('views/detail priority edit', () => {
|
|
8
|
-
test('updates priority via dropdown and re-renders from reply', async () => {
|
|
9
|
-
document.body.innerHTML =
|
|
10
|
-
'<section class="panel"><div id="mount"></div></section>';
|
|
11
|
-
const mount = /** @type {HTMLElement} */ (document.getElementById('mount'));
|
|
12
|
-
|
|
13
|
-
const initial = {
|
|
14
|
-
id: 'UI-70',
|
|
15
|
-
title: 'P',
|
|
16
|
-
description: '',
|
|
17
|
-
status: 'open',
|
|
18
|
-
priority: 2
|
|
19
|
-
};
|
|
20
|
-
const send = mockSend(async (type, payload) => {
|
|
21
|
-
if (type === 'show-issue') {
|
|
22
|
-
return initial;
|
|
23
|
-
}
|
|
24
|
-
if (type === 'update-priority') {
|
|
25
|
-
expect(payload).toEqual({ id: 'UI-70', priority: 4 });
|
|
26
|
-
return { ...initial, priority: 4 };
|
|
27
|
-
}
|
|
28
|
-
throw new Error('Unexpected');
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
const view = createDetailView(mount, send);
|
|
32
|
-
await view.load('UI-70');
|
|
33
|
-
|
|
34
|
-
const selects = mount.querySelectorAll('select.badge--priority');
|
|
35
|
-
expect(selects.length).toBe(1);
|
|
36
|
-
const prio = /** @type {HTMLSelectElement} */ (selects[0]);
|
|
37
|
-
expect(prio.value).toBe('2');
|
|
38
|
-
|
|
39
|
-
prio.value = '4';
|
|
40
|
-
prio.dispatchEvent(new Event('change'));
|
|
41
|
-
|
|
42
|
-
await Promise.resolve();
|
|
43
|
-
|
|
44
|
-
const prio2 = /** @type {HTMLSelectElement} */ (
|
|
45
|
-
mount.querySelector('select.badge--priority')
|
|
46
|
-
);
|
|
47
|
-
expect(prio2.value).toBe('4');
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
test('shows toast on error and restores previous value', async () => {
|
|
51
|
-
document.body.innerHTML =
|
|
52
|
-
'<section class="panel"><div id="mount"></div></section>';
|
|
53
|
-
const mount = /** @type {HTMLElement} */ (document.getElementById('mount'));
|
|
54
|
-
|
|
55
|
-
const initial = { id: 'UI-71', title: 'Q', status: 'open', priority: 1 };
|
|
56
|
-
const send = mockSend(async (type) => {
|
|
57
|
-
if (type === 'show-issue') {
|
|
58
|
-
return initial;
|
|
59
|
-
}
|
|
60
|
-
if (type === 'update-priority') {
|
|
61
|
-
throw new Error('oops');
|
|
62
|
-
}
|
|
63
|
-
throw new Error('Unexpected');
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
const view = createDetailView(mount, send);
|
|
67
|
-
await view.load('UI-71');
|
|
68
|
-
const prio = /** @type {HTMLSelectElement} */ (
|
|
69
|
-
mount.querySelector('select.badge--priority')
|
|
70
|
-
);
|
|
71
|
-
expect(prio.value).toBe('1');
|
|
72
|
-
|
|
73
|
-
prio.value = '3';
|
|
74
|
-
prio.dispatchEvent(new Event('change'));
|
|
75
|
-
|
|
76
|
-
await Promise.resolve();
|
|
77
|
-
|
|
78
|
-
const toast = mount.querySelector('.toast');
|
|
79
|
-
expect(toast).toBeTruthy();
|
|
80
|
-
// Should restore previous value
|
|
81
|
-
const prio2 = /** @type {HTMLSelectElement} */ (
|
|
82
|
-
mount.querySelector('select.badge--priority')
|
|
83
|
-
);
|
|
84
|
-
expect(prio2.value).toBe('1');
|
|
85
|
-
});
|
|
86
|
-
});
|