@polderlabs/bizar 4.5.2 → 4.7.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/main-DGGq-iZI.js +361 -0
- package/bizar-dash/dist/assets/main-DGGq-iZI.js.map +1 -0
- package/bizar-dash/dist/assets/{mobile-lbH6szyX.js → mobile-CWqPoGaT.js} +18 -18
- package/bizar-dash/dist/assets/mobile-CWqPoGaT.js.map +1 -0
- package/bizar-dash/dist/assets/{mobile-BRhoDOUz.js → mobile-i4Uv9eW8.js} +1 -1
- package/bizar-dash/dist/assets/{mobile-BRhoDOUz.js.map → mobile-i4Uv9eW8.js.map} +1 -1
- package/bizar-dash/dist/index.html +2 -2
- package/bizar-dash/dist/mobile.html +2 -2
- package/bizar-dash/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
- package/bizar-dash/src/server/logger.mjs +71 -0
- package/bizar-dash/src/server/memory-lightrag.mjs +45 -6
- package/bizar-dash/src/server/metrics.mjs +193 -0
- package/bizar-dash/src/server/routes/chat.mjs +6 -3
- package/bizar-dash/src/server/routes/lightrag.mjs +3 -2
- package/bizar-dash/src/server/routes/memory.mjs +5 -4
- package/bizar-dash/src/server/routes/misc.mjs +2 -1
- package/bizar-dash/src/server/routes/overview.mjs +2 -1
- package/bizar-dash/src/server/server.mjs +62 -0
- package/bizar-dash/src/web/components/SearchModal.tsx +3 -2
- package/bizar-dash/src/web/components/VirtualList.tsx +53 -0
- package/bizar-dash/src/web/components/chat/ChatThread.tsx +17 -11
- package/bizar-dash/src/web/hooks/useI18n.ts +13 -0
- package/bizar-dash/src/web/lib/i18n.ts +25 -0
- package/bizar-dash/src/web/locales/en.json +52 -0
- package/bizar-dash/src/web/main.tsx +5 -0
- package/bizar-dash/src/web/views/Activity.tsx +24 -17
- package/bizar-dash/src/web/views/History.tsx +90 -75
- package/bizar-dash/src/web/views/Overview.tsx +11 -8
- package/bizar-dash/src/web/views/Settings.tsx +7 -1
- package/bizar-dash/tests/cli-bugfixes.test.mjs +4 -4
- package/bizar-dash/tests/cli-refactor.test.mjs +184 -0
- package/bizar-dash/tests/components/Button.test.tsx +41 -0
- package/bizar-dash/tests/components/Card.test.tsx +42 -0
- package/bizar-dash/tests/components/Modal.test.tsx +104 -0
- package/bizar-dash/tests/components/Spinner.test.tsx +32 -0
- package/bizar-dash/tests/components/StatusBadge.test.tsx +35 -0
- package/bizar-dash/tests/components/Toast.test.tsx +108 -0
- package/bizar-dash/tests/hooks/useModal.test.tsx +84 -0
- package/bizar-dash/tests/hooks/useToast.test.tsx +50 -0
- package/bizar-dash/tests/lib/i18n.test.ts +46 -0
- package/bizar-dash/tests/lib/utils.test.ts +194 -0
- package/bizar-dash/tests/logger.test.mjs +207 -0
- package/bizar-dash/tests/metrics.test.mjs +183 -0
- package/bizar-dash/tests/server-bugfixes.test.mjs +2 -2
- package/bizar-dash/tests/setup.ts +7 -0
- package/bizar-dash/vitest.config.ts +13 -0
- package/cli/artifact-cli.mjs +605 -0
- package/cli/artifact-render.mjs +621 -0
- package/cli/artifact-server.mjs +847 -0
- package/cli/artifact.mjs +38 -2096
- package/cli/bg.mjs +5 -13
- package/cli/bin.mjs +149 -1361
- package/cli/commands/artifact.mjs +20 -0
- package/cli/commands/dash.mjs +160 -0
- package/cli/commands/headroom.mjs +204 -0
- package/cli/commands/install.mjs +169 -0
- package/cli/commands/memory.mjs +25 -0
- package/cli/commands/minimax.mjs +265 -0
- package/cli/commands/mod.mjs +185 -0
- package/cli/commands/service.mjs +65 -0
- package/cli/commands/usage.mjs +109 -0
- package/cli/commands/util.mjs +306 -0
- package/cli/doctor.mjs +1 -13
- package/cli/provision.mjs +2 -13
- package/cli/service-controller.mjs +1 -11
- package/cli/service.mjs +1 -11
- package/cli/utils.mjs +41 -1
- package/package.json +6 -1
- package/bizar-dash/dist/assets/main-B4OfGAwz.js +0 -361
- package/bizar-dash/dist/assets/main-B4OfGAwz.js.map +0 -1
- package/bizar-dash/dist/assets/mobile-lbH6szyX.js.map +0 -1
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* bizar-dash/tests/cli-refactor.test.mjs
|
|
3
|
+
*
|
|
4
|
+
* v4.6 — CLI refactor tests.
|
|
5
|
+
*
|
|
6
|
+
* Verifies:
|
|
7
|
+
* 1. cli/bin.mjs imports from cli/commands/*.mjs correctly
|
|
8
|
+
* 2. Each command module exports a `run` function
|
|
9
|
+
* 3. cli/utils.mjs exports `which()` and `bizarConfigDir()`
|
|
10
|
+
* 4. cli/artifact-cli.mjs, cli/artifact-server.mjs, cli/artifact-render.mjs
|
|
11
|
+
* exist and export the right things
|
|
12
|
+
* 5. cli/artifact.mjs is a backward-compatible re-export shell
|
|
13
|
+
*/
|
|
14
|
+
import { test, describe } from 'node:test';
|
|
15
|
+
import assert from 'node:assert/strict';
|
|
16
|
+
import { resolve, dirname } from 'node:path';
|
|
17
|
+
import { fileURLToPath } from 'node:url';
|
|
18
|
+
import { readdirSync } from 'node:fs';
|
|
19
|
+
|
|
20
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
21
|
+
const CLI_ROOT = resolve(__dirname, '..', '..', 'cli');
|
|
22
|
+
const COMMANDS_DIR = resolve(CLI_ROOT, 'commands');
|
|
23
|
+
|
|
24
|
+
// ── utils.mjs ────────────────────────────────────────────────────────────────
|
|
25
|
+
|
|
26
|
+
describe('cli/utils.mjs exports', () => {
|
|
27
|
+
test('exports which()', async () => {
|
|
28
|
+
const { which } = await import(`${CLI_ROOT}/utils.mjs`);
|
|
29
|
+
assert.equal(typeof which, 'function');
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test('exports whichPath()', async () => {
|
|
33
|
+
const { whichPath } = await import(`${CLI_ROOT}/utils.mjs`);
|
|
34
|
+
assert.equal(typeof whichPath, 'function');
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test('exports bizarConfigDir()', async () => {
|
|
38
|
+
const { bizarConfigDir } = await import(`${CLI_ROOT}/utils.mjs`);
|
|
39
|
+
assert.equal(typeof bizarConfigDir, 'function');
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test('bizarConfigDir() returns a non-empty string', async () => {
|
|
43
|
+
const { bizarConfigDir } = await import(`${CLI_ROOT}/utils.mjs`);
|
|
44
|
+
const dir = bizarConfigDir();
|
|
45
|
+
assert.equal(typeof dir, 'string');
|
|
46
|
+
assert.ok(dir.length > 0);
|
|
47
|
+
assert.ok(dir.includes('bizar'));
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('which() returns boolean', async () => {
|
|
51
|
+
const { which } = await import(`${CLI_ROOT}/utils.mjs`);
|
|
52
|
+
const result = which('node');
|
|
53
|
+
assert.equal(typeof result, 'boolean');
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test('which() returns false for nonexistent command', async () => {
|
|
57
|
+
const { which } = await import(`${CLI_ROOT}/utils.mjs`);
|
|
58
|
+
const result = which('this-command-does-not-exist-xyz');
|
|
59
|
+
assert.equal(result, false);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test('whichPath() returns null for nonexistent command', async () => {
|
|
63
|
+
const { whichPath } = await import(`${CLI_ROOT}/utils.mjs`);
|
|
64
|
+
const result = whichPath('this-command-does-not-exist-xyz');
|
|
65
|
+
assert.equal(result, null);
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// ── Command modules ────────────────────────────────────────────────────────────
|
|
70
|
+
|
|
71
|
+
const COMMAND_FILES = [
|
|
72
|
+
'install',
|
|
73
|
+
'service',
|
|
74
|
+
'dash',
|
|
75
|
+
'minimax',
|
|
76
|
+
'headroom',
|
|
77
|
+
'mod',
|
|
78
|
+
'artifact',
|
|
79
|
+
'memory',
|
|
80
|
+
'usage',
|
|
81
|
+
'util',
|
|
82
|
+
];
|
|
83
|
+
|
|
84
|
+
describe('cli/commands/*.mjs exports', () => {
|
|
85
|
+
for (const name of COMMAND_FILES) {
|
|
86
|
+
test(`${name}.mjs exports run()`, async () => {
|
|
87
|
+
const mod = await import(`${COMMANDS_DIR}/${name}.mjs`);
|
|
88
|
+
assert.equal(typeof mod.run, 'function', `${name}.mjs should export run()`);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
// Note: calling run() directly would execute the command (network calls, etc.)
|
|
92
|
+
// which is not appropriate in a module structure test. The run() signature
|
|
93
|
+
// is validated via TypeScript / node --check instead.
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// ── Artifact split ────────────────────────────────────────────────────────────
|
|
98
|
+
|
|
99
|
+
describe('artifact module split', () => {
|
|
100
|
+
test('artifact-cli.mjs exists and exports runArtifact', async () => {
|
|
101
|
+
const mod = await import(`${CLI_ROOT}/artifact-cli.mjs`);
|
|
102
|
+
assert.equal(typeof mod.runArtifact, 'function');
|
|
103
|
+
assert.equal(typeof mod.default, 'function');
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test('artifact-cli.mjs exports showHelp', async () => {
|
|
107
|
+
const mod = await import(`${CLI_ROOT}/artifact-cli.mjs`);
|
|
108
|
+
assert.equal(typeof mod.showHelp, 'function');
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test('artifact-cli.mjs exports regenerateHtml', async () => {
|
|
112
|
+
const mod = await import(`${CLI_ROOT}/artifact-cli.mjs`);
|
|
113
|
+
assert.equal(typeof mod.regenerateHtml, 'function');
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test('artifact-server.mjs exists and exports startServer', async () => {
|
|
117
|
+
const mod = await import(`${CLI_ROOT}/artifact-server.mjs`);
|
|
118
|
+
assert.equal(typeof mod.startServer, 'function');
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test('artifact-render.mjs exports CANVAS_SCHEMA_VERSION', async () => {
|
|
122
|
+
const mod = await import(`${CLI_ROOT}/artifact-render.mjs`);
|
|
123
|
+
assert.equal(mod.CANVAS_SCHEMA_VERSION, 2);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test('artifact-render.mjs exports canvas helpers', async () => {
|
|
127
|
+
const mod = await import(`${CLI_ROOT}/artifact-render.mjs`);
|
|
128
|
+
assert.equal(typeof mod.emptyCanvas, 'function');
|
|
129
|
+
assert.equal(typeof mod.readCanvasFile, 'function');
|
|
130
|
+
assert.equal(typeof mod.writeCanvasFile, 'function');
|
|
131
|
+
assert.equal(typeof mod.loadOrMigrateCanvas, 'function');
|
|
132
|
+
assert.equal(typeof mod.canvasToMarkdown, 'function');
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test('artifact-render.mjs exports ID generators', async () => {
|
|
136
|
+
const mod = await import(`${CLI_ROOT}/artifact-render.mjs`);
|
|
137
|
+
assert.equal(typeof mod.makeElementId, 'function');
|
|
138
|
+
assert.equal(typeof mod.makeConnectionId, 'function');
|
|
139
|
+
assert.equal(typeof mod.makeCommentId, 'function');
|
|
140
|
+
assert.equal(typeof mod.makeReplyId, 'function');
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test('artifact-render.mjs exports HTML renderers', async () => {
|
|
144
|
+
const mod = await import(`${CLI_ROOT}/artifact-render.mjs`);
|
|
145
|
+
assert.equal(typeof mod.renderElementHTML, 'function');
|
|
146
|
+
assert.equal(typeof mod.renderConnectionHTML, 'function');
|
|
147
|
+
assert.equal(typeof mod.renderCommentPinHTML, 'function');
|
|
148
|
+
assert.equal(typeof mod.escapeHtml, 'function');
|
|
149
|
+
assert.equal(typeof mod.formatDate, 'function');
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test('artifact.mjs is a re-export shell (backward compat)', async () => {
|
|
153
|
+
const mod = await import(`${CLI_ROOT}/artifact.mjs`);
|
|
154
|
+
// Should re-export runArtifact
|
|
155
|
+
assert.equal(typeof mod.runArtifact, 'function');
|
|
156
|
+
// Should re-export runPlan as alias for runArtifact (backward compat)
|
|
157
|
+
assert.equal(typeof mod.runPlan, 'function');
|
|
158
|
+
// Should re-export startServer
|
|
159
|
+
assert.equal(typeof mod.startServer, 'function');
|
|
160
|
+
// Should re-export canvas helpers
|
|
161
|
+
assert.equal(typeof mod.emptyCanvas, 'function');
|
|
162
|
+
assert.equal(typeof mod.canvasToMarkdown, 'function');
|
|
163
|
+
// Should re-export render helpers
|
|
164
|
+
assert.equal(typeof mod.renderElementHTML, 'function');
|
|
165
|
+
assert.equal(typeof mod.escapeHtml, 'function');
|
|
166
|
+
// Should re-export regenerateHtml
|
|
167
|
+
assert.equal(typeof mod.regenerateHtml, 'function');
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
// ── bin.mjs structure ─────────────────────────────────────────────────────────
|
|
172
|
+
|
|
173
|
+
describe('cli/bin.mjs structure', () => {
|
|
174
|
+
test('bin.mjs is a valid ESM module', async () => {
|
|
175
|
+
// Just verify it can be parsed as ESM — don't run main()
|
|
176
|
+
const { readFileSync } = await import('node:fs');
|
|
177
|
+
const src = readFileSync(`${CLI_ROOT}/bin.mjs`, 'utf8');
|
|
178
|
+
assert.ok(src.includes('#!/usr/bin/env node'));
|
|
179
|
+
assert.ok(src.includes('export'));
|
|
180
|
+
assert.ok(src.includes('import'));
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
console.log(' cli-refactor.test.mjs loaded — run with: node --test bizar-dash/tests/cli-refactor.test.mjs');
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { render, screen } from '@testing-library/react';
|
|
2
|
+
import userEvent from '@testing-library/user-event';
|
|
3
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
4
|
+
import { Button } from '../../src/web/components/Button';
|
|
5
|
+
|
|
6
|
+
describe('Button', () => {
|
|
7
|
+
it('renders text', () => {
|
|
8
|
+
render(<Button>Click me</Button>);
|
|
9
|
+
expect(screen.getByRole('button', { name: 'Click me' })).toBeInTheDocument();
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it('fires onClick when clicked', async () => {
|
|
13
|
+
const onClick = vi.fn();
|
|
14
|
+
const user = userEvent.setup();
|
|
15
|
+
render(<Button onClick={onClick}>Click</Button>);
|
|
16
|
+
await user.click(screen.getByRole('button'));
|
|
17
|
+
expect(onClick).toHaveBeenCalledTimes(1);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('is disabled when disabled prop is set', () => {
|
|
21
|
+
render(<Button disabled>Disabled</Button>);
|
|
22
|
+
expect(screen.getByRole('button')).toBeDisabled();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('shows loading state and disables button', () => {
|
|
26
|
+
render(<Button loading>Saving</Button>);
|
|
27
|
+
const btn = screen.getByRole('button');
|
|
28
|
+
expect(btn).toBeDisabled();
|
|
29
|
+
expect(btn.querySelector('.btn-spinner')).toBeInTheDocument();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('applies variant classes', () => {
|
|
33
|
+
const { container } = render(<Button variant="primary">Primary</Button>);
|
|
34
|
+
expect(container.firstChild).toHaveClass('btn-primary');
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('applies size classes', () => {
|
|
38
|
+
const { container } = render(<Button size="lg">Large</Button>);
|
|
39
|
+
expect(container.firstChild).toHaveClass('btn-size-lg');
|
|
40
|
+
});
|
|
41
|
+
});
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { render, screen } from '@testing-library/react';
|
|
2
|
+
import { describe, it, expect } from 'vitest';
|
|
3
|
+
import { Card, CardTitle, CardMeta } from '../../src/web/components/Card';
|
|
4
|
+
|
|
5
|
+
describe('Card', () => {
|
|
6
|
+
it('renders children', () => {
|
|
7
|
+
render(<Card>Hello</Card>);
|
|
8
|
+
expect(screen.getByText('Hello')).toBeInTheDocument();
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it('has expected class name', () => {
|
|
12
|
+
const { container } = render(<Card>Test</Card>);
|
|
13
|
+
expect(container.firstChild).toHaveClass('card');
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('applies variant classes', () => {
|
|
17
|
+
const { container, rerender } = render(<Card variant="elevated">Test</Card>);
|
|
18
|
+
expect(container.firstChild).toHaveClass('card-elevated');
|
|
19
|
+
|
|
20
|
+
rerender(<Card variant="outlined">Test</Card>);
|
|
21
|
+
expect(container.firstChild).toHaveClass('card-outlined');
|
|
22
|
+
|
|
23
|
+
rerender(<Card variant="filled">Test</Card>);
|
|
24
|
+
expect(container.firstChild).toHaveClass('card-filled');
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('renders CardTitle and CardMeta', () => {
|
|
28
|
+
render(
|
|
29
|
+
<Card>
|
|
30
|
+
<CardTitle>My Title</CardTitle>
|
|
31
|
+
<CardMeta>Meta info</CardMeta>
|
|
32
|
+
</Card>,
|
|
33
|
+
);
|
|
34
|
+
expect(screen.getByText('My Title')).toBeInTheDocument();
|
|
35
|
+
expect(screen.getByText('Meta info')).toBeInTheDocument();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('sets interactive class when interactive prop is true', () => {
|
|
39
|
+
const { container } = render(<Card interactive>Clickable</Card>);
|
|
40
|
+
expect(container.firstChild).toHaveClass('card-interactive');
|
|
41
|
+
});
|
|
42
|
+
});
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { render, screen } from '@testing-library/react';
|
|
2
|
+
import userEvent from '@testing-library/user-event';
|
|
3
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
4
|
+
import { ModalProvider, useModal } from '../../src/web/components/Modal';
|
|
5
|
+
|
|
6
|
+
function ModalHarness() {
|
|
7
|
+
const modal = useModal();
|
|
8
|
+
return (
|
|
9
|
+
<div>
|
|
10
|
+
<button onClick={() => modal.open({ title: 'Dialog', children: <p>Content</p> })}>
|
|
11
|
+
Open
|
|
12
|
+
</button>
|
|
13
|
+
</div>
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function ModalHarnessWithClose() {
|
|
18
|
+
const modal = useModal();
|
|
19
|
+
return (
|
|
20
|
+
<div>
|
|
21
|
+
<button
|
|
22
|
+
onClick={() =>
|
|
23
|
+
modal.open({
|
|
24
|
+
title: 'Dialog',
|
|
25
|
+
children: <p>Content</p>,
|
|
26
|
+
footer: <button onClick={() => modal.close()}>Close</button>,
|
|
27
|
+
})
|
|
28
|
+
}
|
|
29
|
+
>
|
|
30
|
+
Open
|
|
31
|
+
</button>
|
|
32
|
+
</div>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
describe('Modal', () => {
|
|
37
|
+
it('renders when open', async () => {
|
|
38
|
+
const user = userEvent.setup();
|
|
39
|
+
render(
|
|
40
|
+
<ModalProvider>
|
|
41
|
+
<ModalHarness />
|
|
42
|
+
</ModalProvider>,
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
await user.click(screen.getByText('Open'));
|
|
46
|
+
expect(screen.getByRole('dialog')).toBeInTheDocument();
|
|
47
|
+
expect(screen.getByText('Dialog')).toBeInTheDocument();
|
|
48
|
+
expect(screen.getByText('Content')).toBeInTheDocument();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('does not render dialog when closed', () => {
|
|
52
|
+
render(
|
|
53
|
+
<ModalProvider>
|
|
54
|
+
<ModalHarness />
|
|
55
|
+
</ModalProvider>,
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('has role="dialog"', async () => {
|
|
62
|
+
const user = userEvent.setup();
|
|
63
|
+
render(
|
|
64
|
+
<ModalProvider>
|
|
65
|
+
<ModalHarness />
|
|
66
|
+
</ModalProvider>,
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
await user.click(screen.getByText('Open'));
|
|
70
|
+
expect(screen.getByRole('dialog')).toBeInTheDocument();
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('closes on Escape key', async () => {
|
|
74
|
+
const user = userEvent.setup();
|
|
75
|
+
render(
|
|
76
|
+
<ModalProvider>
|
|
77
|
+
<ModalHarness />
|
|
78
|
+
</ModalProvider>,
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
await user.click(screen.getByText('Open'));
|
|
82
|
+
expect(screen.getByRole('dialog')).toBeInTheDocument();
|
|
83
|
+
|
|
84
|
+
await user.keyboard('{Escape}');
|
|
85
|
+
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('closes when clicking the backdrop', async () => {
|
|
89
|
+
const user = userEvent.setup();
|
|
90
|
+
render(
|
|
91
|
+
<ModalProvider>
|
|
92
|
+
<ModalHarness />
|
|
93
|
+
</ModalProvider>,
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
await user.click(screen.getByText('Open'));
|
|
97
|
+
const dialog = screen.getByRole('dialog');
|
|
98
|
+
expect(dialog).toBeInTheDocument();
|
|
99
|
+
|
|
100
|
+
const backdrop = dialog.parentElement!;
|
|
101
|
+
await user.click(backdrop);
|
|
102
|
+
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
|
|
103
|
+
});
|
|
104
|
+
});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { render, screen } from '@testing-library/react';
|
|
2
|
+
import { describe, it, expect } from 'vitest';
|
|
3
|
+
import { Spinner } from '../../src/web/components/Spinner';
|
|
4
|
+
|
|
5
|
+
describe('Spinner', () => {
|
|
6
|
+
it('renders with default size', () => {
|
|
7
|
+
const { container } = render(<Spinner />);
|
|
8
|
+
const spinner = container.firstChild;
|
|
9
|
+
expect(spinner).toHaveClass('spinner');
|
|
10
|
+
expect(spinner).toHaveClass('spinner-md');
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('renders with custom size', () => {
|
|
14
|
+
const { container } = render(<Spinner size="lg" />);
|
|
15
|
+
expect(container.firstChild).toHaveClass('spinner-lg');
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('has role="status" for accessibility', () => {
|
|
19
|
+
render(<Spinner />);
|
|
20
|
+
expect(screen.getByRole('status')).toBeInTheDocument();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('has default aria-label', () => {
|
|
24
|
+
render(<Spinner />);
|
|
25
|
+
expect(screen.getByRole('status')).toHaveAttribute('aria-label', 'Loading');
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('accepts a custom aria-label', () => {
|
|
29
|
+
render(<Spinner label="Please wait" />);
|
|
30
|
+
expect(screen.getByRole('status')).toHaveAttribute('aria-label', 'Please wait');
|
|
31
|
+
});
|
|
32
|
+
});
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { render, screen } from '@testing-library/react';
|
|
2
|
+
import { describe, it, expect } from 'vitest';
|
|
3
|
+
import { StatusBadge } from '../../src/web/components/StatusBadge';
|
|
4
|
+
|
|
5
|
+
describe('StatusBadge', () => {
|
|
6
|
+
it('renders with default neutral kind', () => {
|
|
7
|
+
const { container } = render(<StatusBadge>Default</StatusBadge>);
|
|
8
|
+
expect(container.firstChild).toHaveClass('badge');
|
|
9
|
+
expect(container.firstChild).toHaveClass('badge-neutral');
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it('renders with each variant class', () => {
|
|
13
|
+
const variants = ['success', 'warning', 'error', 'info', 'accent'] as const;
|
|
14
|
+
for (const kind of variants) {
|
|
15
|
+
const { container, unmount } = render(<StatusBadge kind={kind}>{kind}</StatusBadge>);
|
|
16
|
+
expect(container.firstChild).toHaveClass(`badge-${kind}`);
|
|
17
|
+
unmount();
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('renders children', () => {
|
|
22
|
+
render(<StatusBadge>Active</StatusBadge>);
|
|
23
|
+
expect(screen.getByText('Active')).toBeInTheDocument();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('shows dot element when dot prop is true', () => {
|
|
27
|
+
const { container } = render(<StatusBadge dot>With dot</StatusBadge>);
|
|
28
|
+
expect(container.querySelector('.badge-dot')).toBeInTheDocument();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('does not show dot when dot prop is false', () => {
|
|
32
|
+
const { container } = render(<StatusBadge>No dot</StatusBadge>);
|
|
33
|
+
expect(container.querySelector('.badge-dot')).not.toBeInTheDocument();
|
|
34
|
+
});
|
|
35
|
+
});
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { render, screen, act, fireEvent } from '@testing-library/react';
|
|
2
|
+
import userEvent from '@testing-library/user-event';
|
|
3
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
4
|
+
import { ToastContainer, ToastProvider, useToast } from '../../src/web/components/Toast';
|
|
5
|
+
|
|
6
|
+
function ToastHarness() {
|
|
7
|
+
const toast = useToast();
|
|
8
|
+
return (
|
|
9
|
+
<div>
|
|
10
|
+
<button onClick={() => toast.show('Info message')}>Show Info</button>
|
|
11
|
+
<button onClick={() => toast.success('Success!')}>Show Success</button>
|
|
12
|
+
<button onClick={() => toast.error('Error!')}>Show Error</button>
|
|
13
|
+
</div>
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
describe('ToastContainer', () => {
|
|
18
|
+
it('renders toast with message', () => {
|
|
19
|
+
render(
|
|
20
|
+
<ToastContainer
|
|
21
|
+
toasts={[{ id: 1, kind: 'info', message: 'Test toast' }]}
|
|
22
|
+
onDismiss={() => {}}
|
|
23
|
+
/>,
|
|
24
|
+
);
|
|
25
|
+
expect(screen.getByText('Test toast')).toBeInTheDocument();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('has role="alert" and aria-live="assertive"', () => {
|
|
29
|
+
render(
|
|
30
|
+
<ToastContainer
|
|
31
|
+
toasts={[{ id: 1, kind: 'info', message: 'Accessible toast' }]}
|
|
32
|
+
onDismiss={() => {}}
|
|
33
|
+
/>,
|
|
34
|
+
);
|
|
35
|
+
const toast = screen.getByRole('alert');
|
|
36
|
+
expect(toast).toBeInTheDocument();
|
|
37
|
+
expect(toast).toHaveAttribute('aria-live', 'assertive');
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
describe('ToastProvider + useToast', () => {
|
|
42
|
+
afterEach(() => {
|
|
43
|
+
vi.useRealTimers();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('displays a toast when show is called', async () => {
|
|
47
|
+
const user = userEvent.setup();
|
|
48
|
+
render(
|
|
49
|
+
<ToastProvider>
|
|
50
|
+
<ToastHarness />
|
|
51
|
+
</ToastProvider>,
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
await user.click(screen.getByText('Show Info'));
|
|
55
|
+
expect(screen.getByText('Info message')).toBeInTheDocument();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('displays multiple toasts stacked', async () => {
|
|
59
|
+
const user = userEvent.setup();
|
|
60
|
+
render(
|
|
61
|
+
<ToastProvider>
|
|
62
|
+
<ToastHarness />
|
|
63
|
+
</ToastProvider>,
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
await user.click(screen.getByText('Show Success'));
|
|
67
|
+
await user.click(screen.getByText('Show Error'));
|
|
68
|
+
|
|
69
|
+
expect(screen.getByText('Success!')).toBeInTheDocument();
|
|
70
|
+
expect(screen.getByText('Error!')).toBeInTheDocument();
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('auto-dismisses after the default timeout', () => {
|
|
74
|
+
// Use fireEvent (synchronous) instead of userEvent to avoid
|
|
75
|
+
// timer conflicts with vi.useFakeTimers
|
|
76
|
+
vi.useFakeTimers();
|
|
77
|
+
|
|
78
|
+
render(
|
|
79
|
+
<ToastProvider>
|
|
80
|
+
<ToastHarness />
|
|
81
|
+
</ToastProvider>,
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
fireEvent.click(screen.getByText('Show Info'));
|
|
85
|
+
expect(screen.getByText('Info message')).toBeInTheDocument();
|
|
86
|
+
|
|
87
|
+
act(() => {
|
|
88
|
+
vi.advanceTimersByTime(4000);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
expect(screen.queryByText('Info message')).not.toBeInTheDocument();
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('dismisses a toast when dismiss button is clicked', async () => {
|
|
95
|
+
const user = userEvent.setup();
|
|
96
|
+
render(
|
|
97
|
+
<ToastProvider>
|
|
98
|
+
<ToastHarness />
|
|
99
|
+
</ToastProvider>,
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
await user.click(screen.getByText('Show Info'));
|
|
103
|
+
expect(screen.getByText('Info message')).toBeInTheDocument();
|
|
104
|
+
|
|
105
|
+
await user.click(screen.getByLabelText('Dismiss'));
|
|
106
|
+
expect(screen.queryByText('Info message')).not.toBeInTheDocument();
|
|
107
|
+
});
|
|
108
|
+
});
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { render, screen, renderHook } from '@testing-library/react';
|
|
2
|
+
import userEvent from '@testing-library/user-event';
|
|
3
|
+
import { describe, it, expect } from 'vitest';
|
|
4
|
+
import { ModalProvider, useModal } from '../../src/web/components/Modal';
|
|
5
|
+
|
|
6
|
+
function TestHarness() {
|
|
7
|
+
const modal = useModal();
|
|
8
|
+
return (
|
|
9
|
+
<div>
|
|
10
|
+
<button onClick={() => modal.open({ title: 'Dialog', children: <p>Content</p> })}>
|
|
11
|
+
Open
|
|
12
|
+
</button>
|
|
13
|
+
<span data-testid="modal-open">{String(modal.isModalOpen)}</span>
|
|
14
|
+
</div>
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
describe('useModal', () => {
|
|
19
|
+
it('isModalOpen starts as false', () => {
|
|
20
|
+
render(
|
|
21
|
+
<ModalProvider>
|
|
22
|
+
<TestHarness />
|
|
23
|
+
</ModalProvider>,
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
expect(screen.getByTestId('modal-open')).toHaveTextContent('false');
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('open() renders a modal dialog', async () => {
|
|
30
|
+
const user = userEvent.setup();
|
|
31
|
+
render(
|
|
32
|
+
<ModalProvider>
|
|
33
|
+
<TestHarness />
|
|
34
|
+
</ModalProvider>,
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
await user.click(screen.getByText('Open'));
|
|
38
|
+
expect(screen.getByRole('dialog')).toBeInTheDocument();
|
|
39
|
+
expect(screen.getByText('Dialog')).toBeInTheDocument();
|
|
40
|
+
expect(screen.getByText('Content')).toBeInTheDocument();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('close() removes the dialog', async () => {
|
|
44
|
+
const user = userEvent.setup();
|
|
45
|
+
|
|
46
|
+
function HarnessWithClose() {
|
|
47
|
+
const modal = useModal();
|
|
48
|
+
return (
|
|
49
|
+
<div>
|
|
50
|
+
<button
|
|
51
|
+
onClick={() =>
|
|
52
|
+
modal.open({
|
|
53
|
+
title: 'Dialog',
|
|
54
|
+
children: <p>Content</p>,
|
|
55
|
+
footer: <button onClick={() => modal.close()}>Close Modal</button>,
|
|
56
|
+
})
|
|
57
|
+
}
|
|
58
|
+
>
|
|
59
|
+
Open
|
|
60
|
+
</button>
|
|
61
|
+
</div>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
render(
|
|
66
|
+
<ModalProvider>
|
|
67
|
+
<HarnessWithClose />
|
|
68
|
+
</ModalProvider>,
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
await user.click(screen.getByText('Open'));
|
|
72
|
+
expect(screen.getByRole('dialog')).toBeInTheDocument();
|
|
73
|
+
|
|
74
|
+
await user.click(screen.getByText('Close Modal'));
|
|
75
|
+
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('returns fallback api when no provider is present', () => {
|
|
79
|
+
const { result } = renderHook(() => useModal());
|
|
80
|
+
expect(result.current.isModalOpen).toBe(false);
|
|
81
|
+
expect(result.current.open({ children: <p>test</p> })).toBe('');
|
|
82
|
+
expect(() => result.current.close()).not.toThrow();
|
|
83
|
+
});
|
|
84
|
+
});
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { render, screen, act, renderHook } from '@testing-library/react';
|
|
2
|
+
import userEvent from '@testing-library/user-event';
|
|
3
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
4
|
+
import { ToastProvider, useToast } from '../../src/web/components/Toast';
|
|
5
|
+
|
|
6
|
+
function TestHarness() {
|
|
7
|
+
const toast = useToast();
|
|
8
|
+
return (
|
|
9
|
+
<div>
|
|
10
|
+
<button onClick={() => toast.success('Success toast body')}>Trigger Success</button>
|
|
11
|
+
<button onClick={() => toast.error('Error toast body')}>Trigger Error</button>
|
|
12
|
+
<button onClick={() => toast.info('Info toast body')}>Trigger Info</button>
|
|
13
|
+
</div>
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
describe('useToast', () => {
|
|
18
|
+
it('calling success() renders a toast', async () => {
|
|
19
|
+
const user = userEvent.setup();
|
|
20
|
+
render(
|
|
21
|
+
<ToastProvider>
|
|
22
|
+
<TestHarness />
|
|
23
|
+
</ToastProvider>,
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
await user.click(screen.getByText('Trigger Success'));
|
|
27
|
+
expect(screen.getByText('Success toast body')).toBeInTheDocument();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('calling error() renders an error toast', async () => {
|
|
31
|
+
const user = userEvent.setup();
|
|
32
|
+
render(
|
|
33
|
+
<ToastProvider>
|
|
34
|
+
<TestHarness />
|
|
35
|
+
</ToastProvider>,
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
await user.click(screen.getByText('Trigger Error'));
|
|
39
|
+
expect(screen.getByText('Error toast body')).toBeInTheDocument();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('returns fallback api when no provider is present', () => {
|
|
43
|
+
const { result } = renderHook(() => useToast());
|
|
44
|
+
expect(result.current.toasts).toEqual([]);
|
|
45
|
+
expect(result.current.isModalOpen).toBeUndefined();
|
|
46
|
+
// Calling methods should not throw
|
|
47
|
+
expect(() => result.current.show('test')).not.toThrow();
|
|
48
|
+
expect(() => result.current.dismiss(1)).not.toThrow();
|
|
49
|
+
});
|
|
50
|
+
});
|