@polderlabs/bizar 5.3.0 → 5.4.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.
- package/bizar-dash/dist/assets/EnvVarsSection-DqvB8ZcJ.js +4 -0
- package/bizar-dash/dist/assets/EnvVarsSection-DqvB8ZcJ.js.map +1 -0
- package/bizar-dash/dist/assets/MobileChat-DRXP2ERQ.js +1 -0
- package/bizar-dash/dist/assets/{MobileChat-OidrxXR9.js.map → MobileChat-DRXP2ERQ.js.map} +1 -1
- package/bizar-dash/dist/assets/MobileSettings-CVW9VhEo.js +1 -0
- package/bizar-dash/dist/assets/MobileSettings-CVW9VhEo.js.map +1 -0
- package/bizar-dash/dist/assets/Toast-CcZVVN4i.js +1 -0
- package/bizar-dash/dist/assets/Toast-CcZVVN4i.js.map +1 -0
- package/bizar-dash/dist/assets/{icons-Bd8piSb2.js → icons-OMEGx5KA.js} +66 -61
- package/bizar-dash/dist/assets/icons-OMEGx5KA.js.map +1 -0
- package/bizar-dash/dist/assets/main-CMAGZouj.js +16 -0
- package/bizar-dash/dist/assets/main-CMAGZouj.js.map +1 -0
- package/bizar-dash/dist/assets/{main-3G6mOhiK.css → main-O2tZVdHm.css} +1 -1
- package/bizar-dash/dist/assets/mobile-XN2P7YfM.js +1 -0
- package/bizar-dash/dist/assets/mobile-XN2P7YfM.js.map +1 -0
- package/bizar-dash/dist/assets/{mobile-CsZQAswA.css → mobile-layout-D6V-7WnA.css} +1 -1
- package/bizar-dash/dist/assets/mobile-layout-DqTxvXZp.js +2 -0
- package/bizar-dash/dist/assets/mobile-layout-DqTxvXZp.js.map +1 -0
- package/bizar-dash/dist/assets/useSlashCommands-iyoW95UB.js +2 -0
- package/bizar-dash/dist/assets/useSlashCommands-iyoW95UB.js.map +1 -0
- package/bizar-dash/dist/index.html +8 -6
- package/bizar-dash/dist/mobile.html +4 -4
- package/bizar-dash/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -1
- package/bizar-dash/src/server/auth.mjs +14 -1
- package/bizar-dash/src/server/routes/users.mjs +18 -0
- package/bizar-dash/src/web/MobileApp.tsx +26 -19
- package/bizar-dash/src/web/components/MobileBottomNav.tsx +38 -0
- package/bizar-dash/src/web/components/MobileDrawer.tsx +68 -0
- package/bizar-dash/src/web/components/MobileHeader.tsx +50 -0
- package/bizar-dash/src/web/components/MobileLayout.tsx +63 -0
- package/bizar-dash/src/web/components/TaskCard.tsx +69 -0
- package/bizar-dash/src/web/components/TaskCreateSheet.tsx +76 -0
- package/bizar-dash/src/web/main.tsx +1 -0
- package/bizar-dash/src/web/mobile/MobileDoctor.tsx +80 -0
- package/bizar-dash/src/web/mobile/MobileEval.tsx +167 -0
- package/bizar-dash/src/web/mobile/MobileMarketplace.tsx +106 -0
- package/bizar-dash/src/web/mobile/MobileMemory.tsx +108 -0
- package/bizar-dash/src/web/mobile/MobileOverview.tsx +213 -0
- package/bizar-dash/src/web/mobile/MobilePlugins.tsx +71 -0
- package/bizar-dash/src/web/mobile/MobileSettings.tsx +235 -180
- package/bizar-dash/src/web/mobile/MobileTasks.tsx +94 -0
- package/bizar-dash/src/web/mobile.tsx +1 -0
- package/bizar-dash/src/web/styles/main.css +388 -0
- package/bizar-dash/src/web/styles/memory.css +45 -0
- package/bizar-dash/src/web/styles/mobile-layout.css +231 -0
- package/bizar-dash/src/web/styles/mobile.css +180 -0
- package/bizar-dash/src/web/styles/settings.css +110 -0
- package/bizar-dash/src/web/styles/tasks.css +305 -0
- package/bizar-dash/tests/mobile-chat.test.tsx +82 -0
- package/bizar-dash/tests/mobile-layout.test.tsx +296 -0
- package/bizar-dash/tests/mobile-misc.test.tsx +321 -0
- package/bizar-dash/tests/mobile-overview.test.tsx +214 -0
- package/bizar-dash/tests/mobile-settings.test.tsx +181 -0
- package/bizar-dash/tests/mobile-tasks.test.tsx +162 -0
- package/cli/commands/dash.mjs +5 -0
- package/cli/commands/tailscale.mjs +8 -1
- package/package.json +1 -1
- package/bizar-dash/dist/assets/MobileChat-OidrxXR9.js +0 -1
- package/bizar-dash/dist/assets/MobileSettings-B91gVFii.js +0 -1
- package/bizar-dash/dist/assets/MobileSettings-B91gVFii.js.map +0 -1
- package/bizar-dash/dist/assets/icons-Bd8piSb2.js.map +0 -1
- package/bizar-dash/dist/assets/main-6YUZY968.js +0 -19
- package/bizar-dash/dist/assets/main-6YUZY968.js.map +0 -1
- package/bizar-dash/dist/assets/mobile-CSsp30lr.js +0 -2
- package/bizar-dash/dist/assets/mobile-CSsp30lr.js.map +0 -1
- package/bizar-dash/dist/assets/mobile-DtwK7DOV.js +0 -1
- package/bizar-dash/dist/assets/mobile-DtwK7DOV.js.map +0 -1
- package/bizar-dash/dist/assets/useSlashCommands-DEkxEvOO.js +0 -2
- package/bizar-dash/dist/assets/useSlashCommands-DEkxEvOO.js.map +0 -1
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
// tests/mobile-layout.test.tsx — v5.4 mobile navigation shell tests.
|
|
2
|
+
import { render, screen, fireEvent } from '@testing-library/react';
|
|
3
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
4
|
+
import { Activity, MessageSquare, CheckSquare, Grid } from 'lucide-react';
|
|
5
|
+
import { MobileHeader } from '../src/web/components/MobileHeader';
|
|
6
|
+
import { MobileBottomNav } from '../src/web/components/MobileBottomNav';
|
|
7
|
+
import { MobileDrawer } from '../src/web/components/MobileDrawer';
|
|
8
|
+
import { MobileLayout } from '../src/web/components/MobileLayout';
|
|
9
|
+
import type { MobileTab } from '../src/web/components/MobileBottomNav';
|
|
10
|
+
|
|
11
|
+
const TABS: MobileTab[] = [
|
|
12
|
+
{ id: 'activity', label: 'Activity', icon: Activity },
|
|
13
|
+
{ id: 'chat', label: 'Chat', icon: MessageSquare },
|
|
14
|
+
{ id: 'tasks', label: 'Tasks', icon: CheckSquare },
|
|
15
|
+
{ id: 'settings', label: 'Settings', icon: Grid },
|
|
16
|
+
{ id: 'more', label: 'More', icon: Grid },
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
describe('MobileHeader', () => {
|
|
20
|
+
it('renders with title', () => {
|
|
21
|
+
render(
|
|
22
|
+
<MobileHeader
|
|
23
|
+
onMenuClick={vi.fn()}
|
|
24
|
+
scrolled={false}
|
|
25
|
+
activeTab="activity"
|
|
26
|
+
/>,
|
|
27
|
+
);
|
|
28
|
+
expect(screen.getByText('Bizar')).toBeInTheDocument();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('renders menu button', () => {
|
|
32
|
+
render(
|
|
33
|
+
<MobileHeader
|
|
34
|
+
onMenuClick={vi.fn()}
|
|
35
|
+
scrolled={false}
|
|
36
|
+
activeTab="activity"
|
|
37
|
+
/>,
|
|
38
|
+
);
|
|
39
|
+
expect(screen.getByRole('button', { name: /open menu/i })).toBeInTheDocument();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('renders search button by default', () => {
|
|
43
|
+
render(
|
|
44
|
+
<MobileHeader
|
|
45
|
+
onMenuClick={vi.fn()}
|
|
46
|
+
scrolled={false}
|
|
47
|
+
activeTab="activity"
|
|
48
|
+
onSearch={vi.fn()}
|
|
49
|
+
/>,
|
|
50
|
+
);
|
|
51
|
+
expect(screen.getByRole('button', { name: /search/i })).toBeInTheDocument();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('renders settings exit button when onSettings is provided', () => {
|
|
55
|
+
render(
|
|
56
|
+
<MobileHeader
|
|
57
|
+
onMenuClick={vi.fn()}
|
|
58
|
+
scrolled={false}
|
|
59
|
+
activeTab="settings"
|
|
60
|
+
onSettings={vi.fn()}
|
|
61
|
+
/>,
|
|
62
|
+
);
|
|
63
|
+
expect(screen.getByRole('button', { name: /exit settings/i })).toBeInTheDocument();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('shows Settings title when activeTab is settings', () => {
|
|
67
|
+
render(
|
|
68
|
+
<MobileHeader
|
|
69
|
+
onMenuClick={vi.fn()}
|
|
70
|
+
scrolled={false}
|
|
71
|
+
activeTab="settings"
|
|
72
|
+
/>,
|
|
73
|
+
);
|
|
74
|
+
expect(screen.getByText('Settings')).toBeInTheDocument();
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('calls onMenuClick when menu button is clicked', () => {
|
|
78
|
+
const onMenuClick = vi.fn();
|
|
79
|
+
render(
|
|
80
|
+
<MobileHeader
|
|
81
|
+
onMenuClick={onMenuClick}
|
|
82
|
+
scrolled={false}
|
|
83
|
+
activeTab="activity"
|
|
84
|
+
/>,
|
|
85
|
+
);
|
|
86
|
+
fireEvent.click(screen.getByRole('button', { name: /open menu/i }));
|
|
87
|
+
expect(onMenuClick).toHaveBeenCalledTimes(1);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('applies scrolled class when scrolled prop is true', () => {
|
|
91
|
+
const { container } = render(
|
|
92
|
+
<MobileHeader
|
|
93
|
+
onMenuClick={vi.fn()}
|
|
94
|
+
scrolled={true}
|
|
95
|
+
activeTab="activity"
|
|
96
|
+
/>,
|
|
97
|
+
);
|
|
98
|
+
expect(container.querySelector('.mobile-header')).toHaveClass('is-scrolled');
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
describe('MobileBottomNav', () => {
|
|
103
|
+
it('renders all primary tabs', () => {
|
|
104
|
+
render(
|
|
105
|
+
<MobileBottomNav
|
|
106
|
+
tabs={TABS}
|
|
107
|
+
activeTab="activity"
|
|
108
|
+
onTabChange={vi.fn()}
|
|
109
|
+
/>,
|
|
110
|
+
);
|
|
111
|
+
expect(screen.getByText('Activity')).toBeInTheDocument();
|
|
112
|
+
expect(screen.getByText('Chat')).toBeInTheDocument();
|
|
113
|
+
expect(screen.getByText('Tasks')).toBeInTheDocument();
|
|
114
|
+
expect(screen.getByText('Settings')).toBeInTheDocument();
|
|
115
|
+
expect(screen.getByText('More')).toBeInTheDocument();
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('marks the active tab', () => {
|
|
119
|
+
render(
|
|
120
|
+
<MobileBottomNav
|
|
121
|
+
tabs={TABS}
|
|
122
|
+
activeTab="chat"
|
|
123
|
+
onTabChange={vi.fn()}
|
|
124
|
+
/>,
|
|
125
|
+
);
|
|
126
|
+
const chatButton = screen.getByRole('tab', { name: /chat/i });
|
|
127
|
+
expect(chatButton).toHaveAttribute('aria-selected', 'true');
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('calls onTabChange when a tab is clicked', () => {
|
|
131
|
+
const onTabChange = vi.fn();
|
|
132
|
+
render(
|
|
133
|
+
<MobileBottomNav
|
|
134
|
+
tabs={TABS}
|
|
135
|
+
activeTab="activity"
|
|
136
|
+
onTabChange={onTabChange}
|
|
137
|
+
/>,
|
|
138
|
+
);
|
|
139
|
+
fireEvent.click(screen.getByRole('tab', { name: /tasks/i }));
|
|
140
|
+
expect(onTabChange).toHaveBeenCalledWith('tasks');
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
describe('MobileDrawer', () => {
|
|
145
|
+
it('does not render when open is false', () => {
|
|
146
|
+
const { container } = render(
|
|
147
|
+
<MobileDrawer
|
|
148
|
+
open={false}
|
|
149
|
+
onClose={vi.fn()}
|
|
150
|
+
tabs={TABS}
|
|
151
|
+
activeTab="activity"
|
|
152
|
+
onTabChange={vi.fn()}
|
|
153
|
+
/>,
|
|
154
|
+
);
|
|
155
|
+
expect(container.querySelector('.mobile-drawer')).not.toHaveClass('is-open');
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('renders overlay when open', () => {
|
|
159
|
+
render(
|
|
160
|
+
<MobileDrawer
|
|
161
|
+
open={true}
|
|
162
|
+
onClose={vi.fn()}
|
|
163
|
+
tabs={TABS}
|
|
164
|
+
activeTab="activity"
|
|
165
|
+
onTabChange={vi.fn()}
|
|
166
|
+
/>,
|
|
167
|
+
);
|
|
168
|
+
expect(screen.getByLabelText(/all views/i)).toBeInTheDocument();
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it('calls onClose when overlay is clicked', () => {
|
|
172
|
+
const onClose = vi.fn();
|
|
173
|
+
render(
|
|
174
|
+
<MobileDrawer
|
|
175
|
+
open={true}
|
|
176
|
+
onClose={onClose}
|
|
177
|
+
tabs={TABS}
|
|
178
|
+
activeTab="activity"
|
|
179
|
+
onTabChange={vi.fn()}
|
|
180
|
+
/>,
|
|
181
|
+
);
|
|
182
|
+
fireEvent.click(screen.getByLabelText(/all views/i).previousSibling as Element);
|
|
183
|
+
// The overlay is the div before the aside
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it('renders all tabs in the drawer', () => {
|
|
187
|
+
render(
|
|
188
|
+
<MobileDrawer
|
|
189
|
+
open={true}
|
|
190
|
+
onClose={vi.fn()}
|
|
191
|
+
tabs={TABS}
|
|
192
|
+
activeTab="activity"
|
|
193
|
+
onTabChange={vi.fn()}
|
|
194
|
+
/>,
|
|
195
|
+
);
|
|
196
|
+
expect(screen.getByText('Activity')).toBeInTheDocument();
|
|
197
|
+
expect(screen.getByText('Chat')).toBeInTheDocument();
|
|
198
|
+
expect(screen.getByText('Tasks')).toBeInTheDocument();
|
|
199
|
+
expect(screen.getByText('Settings')).toBeInTheDocument();
|
|
200
|
+
expect(screen.getByText('More')).toBeInTheDocument();
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it('calls onTabChange when a drawer item is clicked', () => {
|
|
204
|
+
const onTabChange = vi.fn();
|
|
205
|
+
render(
|
|
206
|
+
<MobileDrawer
|
|
207
|
+
open={true}
|
|
208
|
+
onClose={vi.fn()}
|
|
209
|
+
tabs={TABS}
|
|
210
|
+
activeTab="activity"
|
|
211
|
+
onTabChange={onTabChange}
|
|
212
|
+
/>,
|
|
213
|
+
);
|
|
214
|
+
fireEvent.click(screen.getByRole('tab', { name: /tasks/i }));
|
|
215
|
+
expect(onTabChange).toHaveBeenCalledWith('tasks');
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
describe('MobileLayout', () => {
|
|
220
|
+
it('renders mobile header', () => {
|
|
221
|
+
render(
|
|
222
|
+
<MobileLayout
|
|
223
|
+
activeTab="activity"
|
|
224
|
+
onTabChange={vi.fn()}
|
|
225
|
+
tabs={TABS}
|
|
226
|
+
fullTabs={TABS}
|
|
227
|
+
>
|
|
228
|
+
<div>Child content</div>
|
|
229
|
+
</MobileLayout>,
|
|
230
|
+
);
|
|
231
|
+
expect(screen.getByText('Bizar')).toBeInTheDocument();
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it('renders bottom nav with primary tabs', () => {
|
|
235
|
+
render(
|
|
236
|
+
<MobileLayout
|
|
237
|
+
activeTab="activity"
|
|
238
|
+
onTabChange={vi.fn()}
|
|
239
|
+
tabs={TABS}
|
|
240
|
+
fullTabs={TABS}
|
|
241
|
+
>
|
|
242
|
+
<div>Child content</div>
|
|
243
|
+
</MobileLayout>,
|
|
244
|
+
);
|
|
245
|
+
const nav = screen.getByRole('tablist', { name: /primary/i });
|
|
246
|
+
expect(nav).toBeInTheDocument();
|
|
247
|
+
expect(nav).toHaveTextContent('Activity');
|
|
248
|
+
expect(nav).toHaveTextContent('Chat');
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
it('opens drawer on menu click', () => {
|
|
252
|
+
render(
|
|
253
|
+
<MobileLayout
|
|
254
|
+
activeTab="activity"
|
|
255
|
+
onTabChange={vi.fn()}
|
|
256
|
+
tabs={TABS}
|
|
257
|
+
fullTabs={TABS}
|
|
258
|
+
>
|
|
259
|
+
<div>Child content</div>
|
|
260
|
+
</MobileLayout>,
|
|
261
|
+
);
|
|
262
|
+
fireEvent.click(screen.getByRole('button', { name: /open menu/i }));
|
|
263
|
+
expect(screen.getByLabelText(/all views/i)).toBeInTheDocument();
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
it('closes drawer on overlay click', () => {
|
|
267
|
+
render(
|
|
268
|
+
<MobileLayout
|
|
269
|
+
activeTab="activity"
|
|
270
|
+
onTabChange={vi.fn()}
|
|
271
|
+
tabs={TABS}
|
|
272
|
+
fullTabs={TABS}
|
|
273
|
+
>
|
|
274
|
+
<div>Child content</div>
|
|
275
|
+
</MobileLayout>,
|
|
276
|
+
);
|
|
277
|
+
// Open drawer first
|
|
278
|
+
fireEvent.click(screen.getByRole('button', { name: /open menu/i }));
|
|
279
|
+
const drawer = screen.getByLabelText(/all views/i);
|
|
280
|
+
expect(drawer).toHaveClass('is-open');
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
it('renders children content', () => {
|
|
284
|
+
render(
|
|
285
|
+
<MobileLayout
|
|
286
|
+
activeTab="activity"
|
|
287
|
+
onTabChange={vi.fn()}
|
|
288
|
+
tabs={TABS}
|
|
289
|
+
fullTabs={TABS}
|
|
290
|
+
>
|
|
291
|
+
<div data-testid="child">Child content</div>
|
|
292
|
+
</MobileLayout>,
|
|
293
|
+
);
|
|
294
|
+
expect(screen.getByTestId('child')).toBeInTheDocument();
|
|
295
|
+
});
|
|
296
|
+
});
|
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tests/mobile-misc.test.tsx
|
|
3
|
+
*
|
|
4
|
+
* v5.4 — Mobile misc views tests: Memory, Marketplace, Plugins, Eval, Doctor.
|
|
5
|
+
*/
|
|
6
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
7
|
+
import { render, screen, act, waitFor } from '@testing-library/react';
|
|
8
|
+
import userEvent from '@testing-library/user-event';
|
|
9
|
+
import React from 'react';
|
|
10
|
+
|
|
11
|
+
// ─── Mocks ─────────────────────────────────────────────────────────────────
|
|
12
|
+
|
|
13
|
+
vi.mock('../src/web/lib/api', () => ({
|
|
14
|
+
api: {
|
|
15
|
+
get: vi.fn().mockResolvedValue({}),
|
|
16
|
+
post: vi.fn().mockResolvedValue({ ok: true }),
|
|
17
|
+
del: vi.fn().mockResolvedValue({ ok: true }),
|
|
18
|
+
},
|
|
19
|
+
}));
|
|
20
|
+
|
|
21
|
+
// Stub lucide-react icons.
|
|
22
|
+
vi.mock('lucide-react', async (importOriginal) => {
|
|
23
|
+
const actual = await importOriginal() as Record<string, unknown>;
|
|
24
|
+
const make = (name: string) => {
|
|
25
|
+
const Cmp = (props: { size?: number; 'aria-hidden'?: boolean }) =>
|
|
26
|
+
<svg data-testid={`icon-${name}`} data-size={props.size} />;
|
|
27
|
+
Cmp.displayName = name;
|
|
28
|
+
return Cmp;
|
|
29
|
+
};
|
|
30
|
+
return {
|
|
31
|
+
...actual,
|
|
32
|
+
BarChart: make('BarChart'),
|
|
33
|
+
Brain: make('Brain'),
|
|
34
|
+
FileText: make('FileText'),
|
|
35
|
+
GitBranch: make('GitBranch'),
|
|
36
|
+
Search: make('Search'),
|
|
37
|
+
Mic: make('Mic'),
|
|
38
|
+
Settings: make('Settings'),
|
|
39
|
+
Network: make('Network'),
|
|
40
|
+
Clipboard: make('Clipboard'),
|
|
41
|
+
Scan: make('Scan'),
|
|
42
|
+
Store: make('Store'),
|
|
43
|
+
Puzzle: make('Puzzle'),
|
|
44
|
+
ClipboardCheck: make('ClipboardCheck'),
|
|
45
|
+
Clock: make('Clock'),
|
|
46
|
+
CheckCircle2: make('CheckCircle2'),
|
|
47
|
+
AlertTriangle: make('AlertTriangle'),
|
|
48
|
+
XCircle: make('XCircle'),
|
|
49
|
+
};
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
// Stub heavy memory sub-panels.
|
|
53
|
+
vi.mock('../src/web/views/memory/MemoryOverview', () => ({
|
|
54
|
+
MemoryOverview: () => <div data-testid="memory-overview">MemoryOverview</div>,
|
|
55
|
+
}));
|
|
56
|
+
vi.mock('../src/web/views/memory/LightragPanel', () => ({
|
|
57
|
+
LightragPanel: () => <div data-testid="lightrag-panel">LightragPanel</div>,
|
|
58
|
+
}));
|
|
59
|
+
vi.mock('../src/web/views/memory/ObsidianPanel', () => ({
|
|
60
|
+
ObsidianPanel: () => <div data-testid="obsidian-panel">ObsidianPanel</div>,
|
|
61
|
+
}));
|
|
62
|
+
vi.mock('../src/web/views/memory/GitSyncPanel', () => ({
|
|
63
|
+
GitSyncPanel: () => <div data-testid="git-panel">GitSyncPanel</div>,
|
|
64
|
+
}));
|
|
65
|
+
vi.mock('../src/web/views/memory/SemanticSearchPanel', () => ({
|
|
66
|
+
SemanticSearchPanel: () => <div data-testid="semantic-panel">SemanticSearchPanel</div>,
|
|
67
|
+
}));
|
|
68
|
+
vi.mock('../src/web/views/memory/ConfigPanel', () => ({
|
|
69
|
+
ConfigPanel: () => <div data-testid="config-panel">ConfigPanel</div>,
|
|
70
|
+
}));
|
|
71
|
+
vi.mock('../src/web/views/memory/MemoryGraphPanel', () => ({
|
|
72
|
+
MemoryGraphPanel: () => <div data-testid="graph-panel">MemoryGraphPanel</div>,
|
|
73
|
+
}));
|
|
74
|
+
vi.mock('../src/web/views/memory/VaultFromClipboardPanel', () => ({
|
|
75
|
+
VaultFromClipboardPanel: () => <div data-testid="webclip-panel">VaultFromClipboardPanel</div>,
|
|
76
|
+
}));
|
|
77
|
+
vi.mock('../src/web/views/memory/FromScreenshotPanel', () => ({
|
|
78
|
+
FromScreenshotPanel: () => <div data-testid="screenshot-panel">FromScreenshotPanel</div>,
|
|
79
|
+
}));
|
|
80
|
+
vi.mock('../src/web/components/VoiceNotesPanel', () => ({
|
|
81
|
+
VoiceNotesPanel: () => <div data-testid="voice-panel">VoiceNotesPanel</div>,
|
|
82
|
+
}));
|
|
83
|
+
vi.mock('../src/web/components/MarketplacePluginCard', () => ({
|
|
84
|
+
MarketplacePluginCard: ({ plugin }: { plugin: { name: string } }) =>
|
|
85
|
+
<div data-testid="marketplace-card">{plugin.name}</div>,
|
|
86
|
+
}));
|
|
87
|
+
vi.mock('../src/web/components/PluginPermissions', () => ({
|
|
88
|
+
PluginPermissions: () => <div data-testid="plugin-permissions">PluginPermissions</div>,
|
|
89
|
+
}));
|
|
90
|
+
|
|
91
|
+
// ─── Imports ─────────────────────────────────────────────────────────────────
|
|
92
|
+
|
|
93
|
+
import { MobileMemory } from '../src/web/mobile/MobileMemory';
|
|
94
|
+
import { MobileMarketplace } from '../src/web/mobile/MobileMarketplace';
|
|
95
|
+
import { MobilePlugins } from '../src/web/mobile/MobilePlugins';
|
|
96
|
+
import { MobileEval } from '../src/web/mobile/MobileEval';
|
|
97
|
+
import { MobileDoctor } from '../src/web/mobile/MobileDoctor';
|
|
98
|
+
import { api } from '../src/web/lib/api';
|
|
99
|
+
|
|
100
|
+
// ─── Tests ──────────────────────────────────────────────────────────────────
|
|
101
|
+
|
|
102
|
+
describe('MobileMemory', () => {
|
|
103
|
+
beforeEach(() => {
|
|
104
|
+
vi.clearAllMocks();
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('renders all source tabs', () => {
|
|
108
|
+
render(<MobileMemory />);
|
|
109
|
+
expect(screen.getByText('Overview')).toBeInTheDocument();
|
|
110
|
+
expect(screen.getByText('LightRAG')).toBeInTheDocument();
|
|
111
|
+
expect(screen.getByText('Obsidian')).toBeInTheDocument();
|
|
112
|
+
expect(screen.getByText('Git Sync')).toBeInTheDocument();
|
|
113
|
+
expect(screen.getByText('Search')).toBeInTheDocument();
|
|
114
|
+
expect(screen.getByText('Voice')).toBeInTheDocument();
|
|
115
|
+
expect(screen.getByText('Config')).toBeInTheDocument();
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('renders Overview panel by default', () => {
|
|
119
|
+
render(<MobileMemory />);
|
|
120
|
+
expect(screen.getByTestId('memory-overview')).toBeInTheDocument();
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('switches to LightRAG panel when tab is clicked', async () => {
|
|
124
|
+
const user = userEvent.setup();
|
|
125
|
+
render(<MobileMemory />);
|
|
126
|
+
await user.click(screen.getByText('LightRAG'));
|
|
127
|
+
expect(screen.getByTestId('lightrag-panel')).toBeInTheDocument();
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('switches to Obsidian panel when tab is clicked', async () => {
|
|
131
|
+
const user = userEvent.setup();
|
|
132
|
+
render(<MobileMemory />);
|
|
133
|
+
await user.click(screen.getByText('Obsidian'));
|
|
134
|
+
expect(screen.getByTestId('obsidian-panel')).toBeInTheDocument();
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
describe('MobileMarketplace', () => {
|
|
139
|
+
beforeEach(() => {
|
|
140
|
+
vi.clearAllMocks();
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('renders plugins when API returns data', async () => {
|
|
144
|
+
vi.mocked(api.get).mockResolvedValue({
|
|
145
|
+
plugins: [
|
|
146
|
+
{ id: 'p1', name: 'Test Plugin', version: '1.0', author: 'Test', category: 'util' },
|
|
147
|
+
],
|
|
148
|
+
});
|
|
149
|
+
render(<MobileMarketplace />);
|
|
150
|
+
await waitFor(() => {
|
|
151
|
+
expect(screen.getByText('Test Plugin')).toBeInTheDocument();
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('filters plugins by search term', async () => {
|
|
156
|
+
const user = userEvent.setup();
|
|
157
|
+
vi.mocked(api.get).mockResolvedValue({
|
|
158
|
+
plugins: [
|
|
159
|
+
{ id: 'p1', name: 'Alpha Plugin', version: '1.0', author: 'A', category: 'util' },
|
|
160
|
+
{ id: 'p2', name: 'Beta Plugin', version: '1.0', author: 'B', category: 'util' },
|
|
161
|
+
],
|
|
162
|
+
});
|
|
163
|
+
render(<MobileMarketplace />);
|
|
164
|
+
await waitFor(() => {
|
|
165
|
+
expect(screen.getByText('Alpha Plugin')).toBeInTheDocument();
|
|
166
|
+
expect(screen.getByText('Beta Plugin')).toBeInTheDocument();
|
|
167
|
+
});
|
|
168
|
+
const searchInput = screen.getByPlaceholderText(/search plugins/i);
|
|
169
|
+
await act(async () => {
|
|
170
|
+
await user.type(searchInput, 'alpha');
|
|
171
|
+
});
|
|
172
|
+
await waitFor(() => {
|
|
173
|
+
expect(screen.getByText('Alpha Plugin')).toBeInTheDocument();
|
|
174
|
+
});
|
|
175
|
+
expect(screen.queryByText('Beta Plugin')).not.toBeInTheDocument();
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
describe('MobilePlugins', () => {
|
|
180
|
+
beforeEach(() => {
|
|
181
|
+
vi.clearAllMocks();
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it('renders plugin list when API returns data', async () => {
|
|
185
|
+
vi.mocked(api.get).mockResolvedValue({
|
|
186
|
+
plugins: [
|
|
187
|
+
{ id: 'p1', name: 'My Plugin', version: '2.0', invocations: 42 },
|
|
188
|
+
],
|
|
189
|
+
});
|
|
190
|
+
render(<MobilePlugins />);
|
|
191
|
+
await waitFor(() => {
|
|
192
|
+
expect(screen.getByText('My Plugin')).toBeInTheDocument();
|
|
193
|
+
expect(screen.getByText('v2.0')).toBeInTheDocument();
|
|
194
|
+
expect(screen.getByText('42 invocations')).toBeInTheDocument();
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it('filters plugins by search term', async () => {
|
|
199
|
+
const user = userEvent.setup();
|
|
200
|
+
vi.mocked(api.get).mockResolvedValue({
|
|
201
|
+
plugins: [
|
|
202
|
+
{ id: 'p1', name: 'Alpha Plugin', version: '1.0', invocations: 0 },
|
|
203
|
+
{ id: 'p2', name: 'Beta Plugin', version: '1.0', invocations: 0 },
|
|
204
|
+
],
|
|
205
|
+
});
|
|
206
|
+
render(<MobilePlugins />);
|
|
207
|
+
await waitFor(() => {
|
|
208
|
+
expect(screen.getByText('Alpha Plugin')).toBeInTheDocument();
|
|
209
|
+
expect(screen.getByText('Beta Plugin')).toBeInTheDocument();
|
|
210
|
+
});
|
|
211
|
+
const searchInput = screen.getByPlaceholderText(/search installed plugins/i);
|
|
212
|
+
await act(async () => {
|
|
213
|
+
await user.type(searchInput, 'alpha');
|
|
214
|
+
});
|
|
215
|
+
await waitFor(() => {
|
|
216
|
+
expect(screen.getByText('Alpha Plugin')).toBeInTheDocument();
|
|
217
|
+
});
|
|
218
|
+
expect(screen.queryByText('Beta Plugin')).not.toBeInTheDocument();
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
describe('MobileEval', () => {
|
|
223
|
+
beforeEach(() => {
|
|
224
|
+
vi.clearAllMocks();
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it('renders Runs and Schedules tabs', () => {
|
|
228
|
+
render(<MobileEval />);
|
|
229
|
+
expect(screen.getByText('Runs')).toBeInTheDocument();
|
|
230
|
+
expect(screen.getByText('Schedules')).toBeInTheDocument();
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it('renders runs when API returns data', async () => {
|
|
234
|
+
vi.mocked(api.get).mockResolvedValue({
|
|
235
|
+
runs: [
|
|
236
|
+
{ id: 'run_001', startedAt: '2026-07-05T10:00:00Z', total: 10, passed: 9, failed: 1, suitePath: '/tmp' },
|
|
237
|
+
],
|
|
238
|
+
});
|
|
239
|
+
render(<MobileEval />);
|
|
240
|
+
await waitFor(() => {
|
|
241
|
+
expect(screen.getByText('run_001')).toBeInTheDocument();
|
|
242
|
+
});
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it('switches to Schedules tab when clicked', async () => {
|
|
246
|
+
const user = userEvent.setup();
|
|
247
|
+
vi.mocked(api.get).mockResolvedValue({ runs: [] });
|
|
248
|
+
render(<MobileEval />);
|
|
249
|
+
await waitFor(() => {
|
|
250
|
+
// Runs loaded (empty)
|
|
251
|
+
expect(screen.getByText('No eval runs yet.')).toBeInTheDocument();
|
|
252
|
+
});
|
|
253
|
+
await act(async () => {
|
|
254
|
+
await user.click(screen.getByText('Schedules'));
|
|
255
|
+
});
|
|
256
|
+
// Schedules tab is now active
|
|
257
|
+
expect(screen.getByText('Schedules')).toBeInTheDocument();
|
|
258
|
+
});
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
describe('MobileDoctor', () => {
|
|
262
|
+
beforeEach(() => {
|
|
263
|
+
vi.clearAllMocks();
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
it('renders OK status when health is ok', async () => {
|
|
267
|
+
vi.mocked(api.get).mockResolvedValue({ status: 'ok', issues: [] });
|
|
268
|
+
render(<MobileDoctor />);
|
|
269
|
+
await waitFor(() => {
|
|
270
|
+
expect(screen.getByText('OK')).toBeInTheDocument();
|
|
271
|
+
expect(screen.getByText(/all systems healthy/i)).toBeInTheDocument();
|
|
272
|
+
});
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
it('renders warn status with issue count', async () => {
|
|
276
|
+
vi.mocked(api.get).mockResolvedValue({
|
|
277
|
+
status: 'warn',
|
|
278
|
+
issues: [{ name: 'DiskSpace', status: 'warn', message: 'Low disk space' }],
|
|
279
|
+
});
|
|
280
|
+
render(<MobileDoctor />);
|
|
281
|
+
await waitFor(() => {
|
|
282
|
+
expect(screen.getByText('WARN')).toBeInTheDocument();
|
|
283
|
+
expect(screen.getByText('Issues (1)')).toBeInTheDocument();
|
|
284
|
+
expect(screen.getByText('DiskSpace')).toBeInTheDocument();
|
|
285
|
+
});
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
it('renders fail status with issues', async () => {
|
|
289
|
+
vi.mocked(api.get).mockResolvedValue({
|
|
290
|
+
status: 'fail',
|
|
291
|
+
issues: [{ name: 'OpenCode', status: 'fail', message: 'Not reachable' }],
|
|
292
|
+
});
|
|
293
|
+
render(<MobileDoctor />);
|
|
294
|
+
await waitFor(() => {
|
|
295
|
+
expect(screen.getByText('FAIL')).toBeInTheDocument();
|
|
296
|
+
expect(screen.getByText('Issues (1)')).toBeInTheDocument();
|
|
297
|
+
});
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
it('auto-refreshes via setInterval', () => {
|
|
301
|
+
// Verify the component sets up a 30-second interval by checking setInterval was called
|
|
302
|
+
// with the correct delay. We use fake timers to control the interval.
|
|
303
|
+
vi.useFakeTimers();
|
|
304
|
+
vi.mocked(api.get).mockResolvedValue({ status: 'ok', issues: [] });
|
|
305
|
+
|
|
306
|
+
// Spy on window.setInterval to verify the delay
|
|
307
|
+
const setIntervalSpy = vi.spyOn(globalThis, 'setInterval');
|
|
308
|
+
|
|
309
|
+
render(<MobileDoctor />);
|
|
310
|
+
|
|
311
|
+
// The component should call setInterval with ~30 seconds
|
|
312
|
+
expect(setIntervalSpy).toHaveBeenCalled();
|
|
313
|
+
const intervalCalls = setIntervalSpy.mock.calls;
|
|
314
|
+
// Find the interval delay (30 seconds = 30000ms)
|
|
315
|
+
const hasCorrectInterval = intervalCalls.some(call => call[1] === 30_000);
|
|
316
|
+
expect(hasCorrectInterval).toBe(true);
|
|
317
|
+
|
|
318
|
+
setIntervalSpy.mockRestore();
|
|
319
|
+
vi.useRealTimers();
|
|
320
|
+
});
|
|
321
|
+
});
|