@polderlabs/bizar 5.1.0 → 5.3.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/{MobileChat-BVojK0n5.js → MobileChat-OidrxXR9.js} +1 -1
- package/bizar-dash/dist/assets/{MobileChat-BVojK0n5.js.map → MobileChat-OidrxXR9.js.map} +1 -1
- package/bizar-dash/dist/assets/{MobileSettings-C85ApWKZ.js → MobileSettings-B91gVFii.js} +1 -1
- package/bizar-dash/dist/assets/{MobileSettings-C85ApWKZ.js.map → MobileSettings-B91gVFii.js.map} +1 -1
- package/bizar-dash/dist/assets/{icons-Clz0NR6Y.js → icons-Bd8piSb2.js} +159 -134
- package/bizar-dash/dist/assets/icons-Bd8piSb2.js.map +1 -0
- package/bizar-dash/dist/assets/{main-DTkNlLrw.css → main-3G6mOhiK.css} +1 -1
- package/bizar-dash/dist/assets/main-6YUZY968.js +19 -0
- package/bizar-dash/dist/assets/main-6YUZY968.js.map +1 -0
- package/bizar-dash/dist/assets/{mobile-IaZ47uKC.js → mobile-CSsp30lr.js} +2 -2
- package/bizar-dash/dist/assets/{mobile-IaZ47uKC.js.map → mobile-CSsp30lr.js.map} +1 -1
- package/bizar-dash/dist/assets/{mobile-D2pc-iNh.js → mobile-DtwK7DOV.js} +1 -1
- package/bizar-dash/dist/assets/{mobile-D2pc-iNh.js.map → mobile-DtwK7DOV.js.map} +1 -1
- package/bizar-dash/dist/assets/{useSlashCommands-DjEwHl4n.js → useSlashCommands-DEkxEvOO.js} +2 -2
- package/bizar-dash/dist/assets/{useSlashCommands-DjEwHl4n.js.map → useSlashCommands-DEkxEvOO.js.map} +1 -1
- package/bizar-dash/dist/index.html +5 -5
- package/bizar-dash/dist/mobile.html +3 -3
- package/bizar-dash/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -1
- package/bizar-dash/src/server/api.mjs +4 -0
- package/bizar-dash/src/server/eval-store.mjs +55 -0
- package/bizar-dash/src/server/eval.mjs +63 -0
- package/bizar-dash/src/server/plugins/permission-audit.mjs +109 -0
- package/bizar-dash/src/server/plugins/sandbox.mjs +76 -2
- package/bizar-dash/src/server/plugins/store.mjs +4 -0
- package/bizar-dash/src/server/routes/eval.mjs +42 -2
- package/bizar-dash/src/server/routes/plugins.mjs +55 -7
- package/bizar-dash/src/server/routes/tailscale.mjs +46 -0
- package/bizar-dash/src/server/routes/voice.mjs +42 -6
- package/bizar-dash/src/server/schedules-runner.mjs +61 -0
- package/bizar-dash/src/server/server.mjs +13 -0
- package/bizar-dash/src/server/voice-store.mjs +27 -0
- package/bizar-dash/src/server/workers/transcription-worker.mjs +213 -0
- package/bizar-dash/src/web/App.tsx +9 -0
- package/bizar-dash/src/web/components/EvalDiff.tsx +157 -0
- package/bizar-dash/src/web/components/EvalExportButton.tsx +27 -0
- package/bizar-dash/src/web/components/EvalRunCard.tsx +78 -0
- package/bizar-dash/src/web/components/InstallConfirmDialog.tsx +62 -0
- package/bizar-dash/src/web/components/MarketplacePluginCard.tsx +51 -0
- package/bizar-dash/src/web/components/PluginCard.tsx +64 -0
- package/bizar-dash/src/web/components/PluginPermissions.tsx +36 -0
- package/bizar-dash/src/web/components/ScheduleTemplateCard.tsx +1 -0
- package/bizar-dash/src/web/components/TailscaleSettings.tsx +161 -0
- package/bizar-dash/src/web/components/Toggle.tsx +31 -0
- package/bizar-dash/src/web/components/Topbar.tsx +9 -0
- package/bizar-dash/src/web/components/VoiceNotesPanel.tsx +28 -0
- package/bizar-dash/src/web/styles/main.css +684 -0
- package/bizar-dash/src/web/views/Doctor.tsx +3 -3
- package/bizar-dash/src/web/views/Eval.tsx +372 -0
- package/bizar-dash/src/web/views/EvalReport.tsx +353 -0
- package/bizar-dash/src/web/views/Marketplace.tsx +135 -0
- package/bizar-dash/src/web/views/Plugins.tsx +134 -0
- package/bizar-dash/src/web/views/Schedules.tsx +1 -1
- package/bizar-dash/src/web/views/Settings.tsx +2 -0
- package/bizar-dash/tests/a11y/components.test.tsx +149 -0
- package/bizar-dash/tests/a11y/forms.test.tsx +191 -0
- package/bizar-dash/tests/a11y/navigation.test.tsx +112 -0
- package/bizar-dash/tests/cli-tailscale.test.mjs +113 -0
- package/bizar-dash/tests/components/marketplace-plugin-card.test.tsx +144 -0
- package/bizar-dash/tests/components/plugin-permissions.test.tsx +95 -0
- package/bizar-dash/tests/eval-csv.test.mjs +67 -0
- package/bizar-dash/tests/eval-scheduled.test.mjs +84 -0
- package/bizar-dash/tests/eval-web-ui.test.tsx +220 -0
- package/bizar-dash/tests/plugins-permissions.test.mjs +525 -0
- package/bizar-dash/tests/voice-transcribe-worker.test.mjs +343 -0
- package/cli/bin.mjs +14 -0
- package/cli/commands/dash.mjs +19 -1
- package/cli/commands/service.mjs +7 -0
- package/cli/commands/tailscale.mjs +251 -0
- package/package.json +1 -1
- package/bizar-dash/dist/assets/icons-Clz0NR6Y.js.map +0 -1
- package/bizar-dash/dist/assets/main-8feQWXiF.js +0 -19
- package/bizar-dash/dist/assets/main-8feQWXiF.js.map +0 -1
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
// tests/a11y/components.test.tsx — WCAG 2.2 AA accessibility for interactive components.
|
|
2
|
+
//
|
|
3
|
+
// Tests Toggle role/switch behavior, PluginCard button accessible names,
|
|
4
|
+
// PluginPermissions container label, and semantic structure.
|
|
5
|
+
|
|
6
|
+
import { describe, it, expect } from 'vitest';
|
|
7
|
+
import { render, screen } from '@testing-library/react';
|
|
8
|
+
import { Toggle } from '../../src/web/components/Toggle';
|
|
9
|
+
import { PluginCard } from '../../src/web/components/PluginCard';
|
|
10
|
+
import { PluginPermissions } from '../../src/web/components/PluginPermissions';
|
|
11
|
+
import { ScheduleTemplateCard } from '../../src/web/components/ScheduleTemplateCard';
|
|
12
|
+
|
|
13
|
+
// ─── Helpers ────────────────────────────────────────────────────────────────
|
|
14
|
+
|
|
15
|
+
/** Assert every button in the container has an accessible name. */
|
|
16
|
+
function expectAllButtonsAccessible(container: HTMLElement) {
|
|
17
|
+
const buttons = container.querySelectorAll('button');
|
|
18
|
+
const offenders: string[] = [];
|
|
19
|
+
buttons.forEach((el) => {
|
|
20
|
+
const ariaLabel = el.getAttribute('aria-label');
|
|
21
|
+
const labelledBy = el.getAttribute('aria-labelledby');
|
|
22
|
+
const text = (el.textContent || '').trim();
|
|
23
|
+
const title = el.getAttribute('title');
|
|
24
|
+
if (!ariaLabel && !labelledBy && !text && !title) {
|
|
25
|
+
offenders.push(`<button> has no accessible name`);
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
expect(offenders, offenders.join('\n')).toEqual([]);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// ─── Toggle ────────────────────────────────────────────────────────────────
|
|
32
|
+
|
|
33
|
+
describe('Toggle — WCAG 4.4.2: role=switch + aria-checked', () => {
|
|
34
|
+
it('has role="switch" on the input element', () => {
|
|
35
|
+
render(<Toggle checked={false} onChange={() => {}} />);
|
|
36
|
+
const input = document.querySelector('input[type="checkbox"]');
|
|
37
|
+
expect(input).toHaveAttribute('role', 'switch');
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('has aria-checked matching the checked prop', () => {
|
|
41
|
+
const { rerender } = render(<Toggle checked={true} onChange={() => {}} />);
|
|
42
|
+
expect(screen.getByRole('switch')).toHaveAttribute('aria-checked', 'true');
|
|
43
|
+
rerender(<Toggle checked={false} onChange={() => {}} />);
|
|
44
|
+
expect(screen.getByRole('switch')).toHaveAttribute('aria-checked', 'false');
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('has a visible focus indicator via :focus-visible', () => {
|
|
48
|
+
const { container } = render(<Toggle checked={false} onChange={() => {}} />);
|
|
49
|
+
const input = container.querySelector('input');
|
|
50
|
+
expect(input).toBeInTheDocument();
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// ─── PluginCard ─────────────────────────────────────────────────────────────
|
|
55
|
+
|
|
56
|
+
const FAKE_PLUGIN = {
|
|
57
|
+
id: 'test-plugin',
|
|
58
|
+
name: 'Test Plugin',
|
|
59
|
+
version: '1.0.0',
|
|
60
|
+
description: 'A test plugin',
|
|
61
|
+
permissions: ['net', 'fs'],
|
|
62
|
+
enabled: true,
|
|
63
|
+
invocations: 0,
|
|
64
|
+
lastInvokedAt: null,
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
describe('PluginCard — WCAG 2.1.1: all buttons have accessible names', () => {
|
|
68
|
+
it('the toggle has an accessible name via aria-label', () => {
|
|
69
|
+
render(
|
|
70
|
+
<PluginCard
|
|
71
|
+
plugin={FAKE_PLUGIN}
|
|
72
|
+
onToggle={() => {}}
|
|
73
|
+
onUninstall={() => {}}
|
|
74
|
+
onConfigure={() => {}}
|
|
75
|
+
/>,
|
|
76
|
+
);
|
|
77
|
+
const toggle = screen.getByRole('switch');
|
|
78
|
+
expect(toggle).toHaveAttribute('aria-label', 'Toggle Test Plugin enabled');
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('the Configure button has an aria-label', () => {
|
|
82
|
+
render(
|
|
83
|
+
<PluginCard
|
|
84
|
+
plugin={FAKE_PLUGIN}
|
|
85
|
+
onToggle={() => {}}
|
|
86
|
+
onUninstall={() => {}}
|
|
87
|
+
onConfigure={() => {}}
|
|
88
|
+
/>,
|
|
89
|
+
);
|
|
90
|
+
const configureBtn = screen.getByRole('button', { name: /configure test plugin/i });
|
|
91
|
+
expect(configureBtn).toBeInTheDocument();
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('the Uninstall button has an aria-label', () => {
|
|
95
|
+
render(
|
|
96
|
+
<PluginCard
|
|
97
|
+
plugin={FAKE_PLUGIN}
|
|
98
|
+
onToggle={() => {}}
|
|
99
|
+
onUninstall={() => {}}
|
|
100
|
+
onConfigure={() => {}}
|
|
101
|
+
/>,
|
|
102
|
+
);
|
|
103
|
+
const uninstallBtn = screen.getByRole('button', { name: /uninstall test plugin/i });
|
|
104
|
+
expect(uninstallBtn).toBeInTheDocument();
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('all buttons inside the card have accessible names', () => {
|
|
108
|
+
const { container } = render(
|
|
109
|
+
<PluginCard
|
|
110
|
+
plugin={FAKE_PLUGIN}
|
|
111
|
+
onToggle={() => {}}
|
|
112
|
+
onUninstall={() => {}}
|
|
113
|
+
onConfigure={() => {}}
|
|
114
|
+
/>,
|
|
115
|
+
);
|
|
116
|
+
expectAllButtonsAccessible(container);
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
// ─── PluginPermissions ──────────────────────────────────────────────────────
|
|
121
|
+
|
|
122
|
+
describe('PluginPermissions — WCAG 1.3.1: container has aria-label', () => {
|
|
123
|
+
it('the permissions container has aria-label="Required permissions"', () => {
|
|
124
|
+
render(<PluginPermissions permissions={['net', 'fs']} />);
|
|
125
|
+
const container = document.querySelector('.plugin-permissions');
|
|
126
|
+
expect(container).toHaveAttribute('aria-label', 'Required permissions');
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
// ─── ScheduleTemplateCard ────────────────────────────────────────────────────
|
|
131
|
+
|
|
132
|
+
describe('ScheduleTemplateCard — WCAG 2.1.1: button has accessible name', () => {
|
|
133
|
+
it('the Use this template button has an aria-label', () => {
|
|
134
|
+
render(
|
|
135
|
+
<ScheduleTemplateCard
|
|
136
|
+
template={{
|
|
137
|
+
id: 't1',
|
|
138
|
+
name: 'Weekly Review',
|
|
139
|
+
description: 'Run weekly review',
|
|
140
|
+
type: 'cron',
|
|
141
|
+
action: { type: 'agent', target: 'weekly-reviewer' },
|
|
142
|
+
}}
|
|
143
|
+
onUse={() => {}}
|
|
144
|
+
/>,
|
|
145
|
+
);
|
|
146
|
+
const btn = screen.getByRole('button', { name: /use template: weekly review/i });
|
|
147
|
+
expect(btn).toBeInTheDocument();
|
|
148
|
+
});
|
|
149
|
+
});
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
// tests/a11y/forms.test.tsx — WCAG 2.1.1 / 3.3.1 form accessibility for Doctor and Schedules.
|
|
2
|
+
//
|
|
3
|
+
// Tests that all form inputs have accessible names (label, aria-label,
|
|
4
|
+
// or aria-labelledby), and that form errors and live regions are announced.
|
|
5
|
+
|
|
6
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
7
|
+
import { render, screen } from '@testing-library/react';
|
|
8
|
+
import { Doctor } from '../../src/web/views/Doctor';
|
|
9
|
+
import { Schedules } from '../../src/web/views/Schedules';
|
|
10
|
+
import { ToastProvider } from '../../src/web/components/Toast';
|
|
11
|
+
import { ModalProvider } from '../../src/web/components/Modal';
|
|
12
|
+
|
|
13
|
+
// ─── Helpers ────────────────────────────────────────────────────────────────
|
|
14
|
+
|
|
15
|
+
/** Assert every input/select/textarea in the container has an accessible name. */
|
|
16
|
+
function expectAllInputsAccessible(container: HTMLElement) {
|
|
17
|
+
const inputs = container.querySelectorAll('input, select, textarea');
|
|
18
|
+
const offenders: string[] = [];
|
|
19
|
+
inputs.forEach((el) => {
|
|
20
|
+
if ((el as HTMLInputElement).type === 'hidden') return;
|
|
21
|
+
const accessibleName =
|
|
22
|
+
el.getAttribute('aria-label') ||
|
|
23
|
+
el.getAttribute('aria-labelledby') ||
|
|
24
|
+
el.getAttribute('placeholder') ||
|
|
25
|
+
el.getAttribute('title');
|
|
26
|
+
let labelledByLabel = false;
|
|
27
|
+
if (el.id) {
|
|
28
|
+
labelledByLabel = !!container.querySelector(`label[for="${el.id}"]`);
|
|
29
|
+
}
|
|
30
|
+
if (!accessibleName && !labelledByLabel) {
|
|
31
|
+
offenders.push(
|
|
32
|
+
`<${el.tagName.toLowerCase()}${el.id ? ` id="${el.id}"` : ''}${el.getAttribute('type') ? ` type="${el.getAttribute('type')}"` : ''}> has no accessible name`,
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
expect(offenders, offenders.join('\n')).toEqual([]);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Assert every button in the container has an accessible name. */
|
|
40
|
+
function expectAllButtonsAccessible(container: HTMLElement) {
|
|
41
|
+
const buttons = container.querySelectorAll('button');
|
|
42
|
+
const offenders: string[] = [];
|
|
43
|
+
buttons.forEach((el) => {
|
|
44
|
+
const ariaLabel = el.getAttribute('aria-label');
|
|
45
|
+
const labelledBy = el.getAttribute('aria-labelledby');
|
|
46
|
+
const text = (el.textContent || '').trim();
|
|
47
|
+
const title = el.getAttribute('title');
|
|
48
|
+
if (!ariaLabel && !labelledBy && !text && !title) {
|
|
49
|
+
offenders.push(`<button> has no accessible name`);
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
expect(offenders, offenders.join('\n')).toEqual([]);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function renderWithProviders(ui: React.ReactNode) {
|
|
56
|
+
return render(
|
|
57
|
+
<ToastProvider>
|
|
58
|
+
<ModalProvider>{ui}</ModalProvider>
|
|
59
|
+
</ToastProvider>,
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ─── Doctor ─────────────────────────────────────────────────────────────────
|
|
64
|
+
|
|
65
|
+
const FAKE_SNAPSHOT = {
|
|
66
|
+
bizarreVersion: '99.0.0',
|
|
67
|
+
platform: 'linux',
|
|
68
|
+
arch: 'x64',
|
|
69
|
+
uptime: 3600,
|
|
70
|
+
tasks: 0,
|
|
71
|
+
schedules: 0,
|
|
72
|
+
mods: 0,
|
|
73
|
+
providers: 0,
|
|
74
|
+
mcps: 0,
|
|
75
|
+
agents: 0,
|
|
76
|
+
projects: 0,
|
|
77
|
+
workspaces: 0,
|
|
78
|
+
voiceNotes: 0,
|
|
79
|
+
evalRuns: 0,
|
|
80
|
+
backups: 0,
|
|
81
|
+
activeProject: null,
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const FAKE_SETTINGS = {};
|
|
85
|
+
|
|
86
|
+
describe('Doctor — WCAG 3.3.1: form inputs have accessible names + aria-live', () => {
|
|
87
|
+
beforeEach(() => {
|
|
88
|
+
vi.restoreAllMocks();
|
|
89
|
+
// Mock the API to avoid real network calls
|
|
90
|
+
vi.spyOn(require('../../src/web/lib/api'), 'api').mockImplementation(() => ({
|
|
91
|
+
get: vi.fn().mockResolvedValue({
|
|
92
|
+
health: { status: 'ok', issues: [] },
|
|
93
|
+
checks: { system: [], services: [], config: [], counts: [] },
|
|
94
|
+
bizarreVersion: '99.0.0',
|
|
95
|
+
platform: 'linux',
|
|
96
|
+
arch: 'x64',
|
|
97
|
+
uptime: 3600,
|
|
98
|
+
counts: {},
|
|
99
|
+
recentErrors: [],
|
|
100
|
+
}),
|
|
101
|
+
post: vi.fn().mockResolvedValue({ name: 'test', message: 'ok' }),
|
|
102
|
+
}));
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('the "Run Health Check Now" button has aria-label="Refresh diagnostics"', async () => {
|
|
106
|
+
renderWithProviders(
|
|
107
|
+
<Doctor
|
|
108
|
+
snapshot={FAKE_SNAPSHOT}
|
|
109
|
+
settings={FAKE_SETTINGS}
|
|
110
|
+
activeTab="doctor"
|
|
111
|
+
setActiveTab={() => {}}
|
|
112
|
+
refreshSnapshot={() => Promise.resolve()}
|
|
113
|
+
/>,
|
|
114
|
+
);
|
|
115
|
+
const btn = screen.getByRole('button', { name: /refresh diagnostics/i });
|
|
116
|
+
expect(btn).toBeInTheDocument();
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('the auto-refresh toggle button has an accessible name', async () => {
|
|
120
|
+
renderWithProviders(
|
|
121
|
+
<Doctor
|
|
122
|
+
snapshot={FAKE_SNAPSHOT}
|
|
123
|
+
settings={FAKE_SETTINGS}
|
|
124
|
+
activeTab="doctor"
|
|
125
|
+
setActiveTab={() => {}}
|
|
126
|
+
refreshSnapshot={() => Promise.resolve()}
|
|
127
|
+
/>,
|
|
128
|
+
);
|
|
129
|
+
// The button shows "Pause auto-refresh" or "Resume auto-refresh"
|
|
130
|
+
const btn = screen.getByRole('button', { name: /(pause|resume) auto-refresh/i });
|
|
131
|
+
expect(btn).toBeInTheDocument();
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('header meta region has aria-live="polite"', async () => {
|
|
135
|
+
renderWithProviders(
|
|
136
|
+
<Doctor
|
|
137
|
+
snapshot={FAKE_SNAPSHOT}
|
|
138
|
+
settings={FAKE_SETTINGS}
|
|
139
|
+
activeTab="doctor"
|
|
140
|
+
setActiveTab={() => {}}
|
|
141
|
+
refreshSnapshot={() => Promise.resolve()}
|
|
142
|
+
/>,
|
|
143
|
+
);
|
|
144
|
+
const liveRegion = document.querySelector('[aria-live="polite"]');
|
|
145
|
+
expect(liveRegion).toBeInTheDocument();
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
// ─── Schedules ───────────────────────────────────────────────────────────────
|
|
150
|
+
|
|
151
|
+
describe('Schedules — WCAG 3.3.1: all form inputs have accessible names', () => {
|
|
152
|
+
it('the Refresh button has aria-label="Refresh schedules"', () => {
|
|
153
|
+
renderWithProviders(
|
|
154
|
+
<Schedules
|
|
155
|
+
snapshot={FAKE_SNAPSHOT}
|
|
156
|
+
settings={FAKE_SETTINGS}
|
|
157
|
+
activeTab="schedules"
|
|
158
|
+
setActiveTab={() => {}}
|
|
159
|
+
refreshSnapshot={() => Promise.resolve()}
|
|
160
|
+
/>,
|
|
161
|
+
);
|
|
162
|
+
const btn = screen.getByRole('button', { name: /refresh schedules/i });
|
|
163
|
+
expect(btn).toBeInTheDocument();
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('all visible inputs in the Schedules view have accessible names', () => {
|
|
167
|
+
const { container } = renderWithProviders(
|
|
168
|
+
<Schedules
|
|
169
|
+
snapshot={FAKE_SNAPSHOT}
|
|
170
|
+
settings={FAKE_SETTINGS}
|
|
171
|
+
activeTab="schedules"
|
|
172
|
+
setActiveTab={() => {}}
|
|
173
|
+
refreshSnapshot={() => Promise.resolve()}
|
|
174
|
+
/>,
|
|
175
|
+
);
|
|
176
|
+
expectAllInputsAccessible(container);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it('all visible buttons in the Schedules view have accessible names', () => {
|
|
180
|
+
const { container } = renderWithProviders(
|
|
181
|
+
<Schedules
|
|
182
|
+
snapshot={FAKE_SNAPSHOT}
|
|
183
|
+
settings={FAKE_SETTINGS}
|
|
184
|
+
activeTab="schedules"
|
|
185
|
+
setActiveTab={() => {}}
|
|
186
|
+
refreshSnapshot={() => Promise.resolve()}
|
|
187
|
+
/>,
|
|
188
|
+
);
|
|
189
|
+
expectAllButtonsAccessible(container);
|
|
190
|
+
});
|
|
191
|
+
});
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
// tests/a11y/navigation.test.tsx — WCAG 2.1.1 / 2.4.1 navigation accessibility.
|
|
2
|
+
//
|
|
3
|
+
// Tests that sidebar, tabs, and the Memory source rail all meet
|
|
4
|
+
// keyboard navigation, role, and aria attributes requirements.
|
|
5
|
+
|
|
6
|
+
import { describe, it, expect } from 'vitest';
|
|
7
|
+
import { render, screen } from '@testing-library/react';
|
|
8
|
+
import { Memory } from '../../src/web/views/Memory';
|
|
9
|
+
import { Plugins } from '../../src/web/views/Plugins';
|
|
10
|
+
import { ToastProvider } from '../../src/web/components/Toast';
|
|
11
|
+
import { ModalProvider } from '../../src/web/components/Modal';
|
|
12
|
+
|
|
13
|
+
function renderWithProviders(ui: React.ReactNode) {
|
|
14
|
+
return render(
|
|
15
|
+
<ToastProvider>
|
|
16
|
+
<ModalProvider>{ui}</ModalProvider>
|
|
17
|
+
</ToastProvider>,
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// ─── Memory tab ──────────────────────────────────────────────────────────────
|
|
22
|
+
|
|
23
|
+
describe('Memory — WCAG 2.4.1: nav has aria-label, tabs have role=tab + aria-selected', () => {
|
|
24
|
+
it('source rail has aria-label="Memory sources"', () => {
|
|
25
|
+
renderWithProviders(
|
|
26
|
+
<Memory
|
|
27
|
+
snapshot={{} as any}
|
|
28
|
+
settings={{} as any}
|
|
29
|
+
activeTab="memory"
|
|
30
|
+
setActiveTab={() => {}}
|
|
31
|
+
refreshSnapshot={() => Promise.resolve()}
|
|
32
|
+
/>,
|
|
33
|
+
);
|
|
34
|
+
const nav = document.querySelector('[aria-label="Memory sources"]');
|
|
35
|
+
expect(nav).toBeInTheDocument();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('each source button has role="tab"', () => {
|
|
39
|
+
renderWithProviders(
|
|
40
|
+
<Memory
|
|
41
|
+
snapshot={{} as any}
|
|
42
|
+
settings={{} as any}
|
|
43
|
+
activeTab="memory"
|
|
44
|
+
setActiveTab={() => {}}
|
|
45
|
+
refreshSnapshot={() => Promise.resolve()}
|
|
46
|
+
/>,
|
|
47
|
+
);
|
|
48
|
+
const tabs = screen.getAllByRole('tab');
|
|
49
|
+
expect(tabs.length).toBeGreaterThan(0);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('active tab has aria-selected="true"', () => {
|
|
53
|
+
renderWithProviders(
|
|
54
|
+
<Memory
|
|
55
|
+
snapshot={{} as any}
|
|
56
|
+
settings={{} as any}
|
|
57
|
+
activeTab="memory"
|
|
58
|
+
setActiveTab={() => {}}
|
|
59
|
+
refreshSnapshot={() => Promise.resolve()}
|
|
60
|
+
/>,
|
|
61
|
+
);
|
|
62
|
+
const activeTab = screen.getByRole('tab', { selected: true });
|
|
63
|
+
expect(activeTab).toBeInTheDocument();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('inactive tabs have aria-selected="false"', () => {
|
|
67
|
+
renderWithProviders(
|
|
68
|
+
<Memory
|
|
69
|
+
snapshot={{} as any}
|
|
70
|
+
settings={{} as any}
|
|
71
|
+
activeTab="memory"
|
|
72
|
+
setActiveTab={() => {}}
|
|
73
|
+
refreshSnapshot={() => Promise.resolve()}
|
|
74
|
+
/>,
|
|
75
|
+
);
|
|
76
|
+
const tabs = screen.getAllByRole('tab');
|
|
77
|
+
const unselected = tabs.filter((t) => !t.getAttribute('aria-selected') || t.getAttribute('aria-selected') === 'false');
|
|
78
|
+
expect(unselected.length).toBeGreaterThan(0);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('refresh button has aria-label="Refresh memory"', () => {
|
|
82
|
+
renderWithProviders(
|
|
83
|
+
<Memory
|
|
84
|
+
snapshot={{} as any}
|
|
85
|
+
settings={{} as any}
|
|
86
|
+
activeTab="memory"
|
|
87
|
+
setActiveTab={() => {}}
|
|
88
|
+
refreshSnapshot={() => Promise.resolve()}
|
|
89
|
+
/>,
|
|
90
|
+
);
|
|
91
|
+
const btn = screen.getByRole('button', { name: /refresh memory/i });
|
|
92
|
+
expect(btn).toBeInTheDocument();
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
// ─── Plugins ─────────────────────────────────────────────────────────────────
|
|
97
|
+
|
|
98
|
+
describe('Plugins — WCAG 2.1.1: filter input has accessible name', () => {
|
|
99
|
+
it('the filter input has aria-label="Filter plugins"', () => {
|
|
100
|
+
renderWithProviders(
|
|
101
|
+
<Plugins
|
|
102
|
+
snapshot={{} as any}
|
|
103
|
+
settings={{} as any}
|
|
104
|
+
activeTab="plugins"
|
|
105
|
+
setActiveTab={() => {}}
|
|
106
|
+
refreshSnapshot={() => Promise.resolve()}
|
|
107
|
+
/>,
|
|
108
|
+
);
|
|
109
|
+
const input = document.querySelector('input[type="search"]');
|
|
110
|
+
expect(input).toHaveAttribute('aria-label', 'Filter plugins');
|
|
111
|
+
});
|
|
112
|
+
});
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* bizar-dash/tests/cli-tailscale.test.mjs
|
|
3
|
+
*
|
|
4
|
+
* v5.2 — CLI Tailscale integration tests.
|
|
5
|
+
*
|
|
6
|
+
* Tests the tailscale CLI command module (cli/commands/tailscale.mjs).
|
|
7
|
+
* Verifies module structure and run() doesn't throw for help/invalid subcommands.
|
|
8
|
+
* Full integration testing requires a real or mocked tailscale binary.
|
|
9
|
+
*/
|
|
10
|
+
import { test, describe } from 'node:test';
|
|
11
|
+
import assert from 'node:assert/strict';
|
|
12
|
+
import { resolve, dirname } from 'node:path';
|
|
13
|
+
import { fileURLToPath } from 'node:url';
|
|
14
|
+
|
|
15
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
16
|
+
const CLI_COMMANDS = resolve(__dirname, '..', '..', 'cli', 'commands');
|
|
17
|
+
|
|
18
|
+
// ── Tests ───────────────────────────────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
describe('cli/commands/tailscale.mjs exports', () => {
|
|
21
|
+
test('exports run() function', async () => {
|
|
22
|
+
const mod = await import(`${CLI_COMMANDS}/tailscale.mjs`);
|
|
23
|
+
assert.equal(typeof mod.run, 'function');
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test('exports ensureTailscaleAuth() function', async () => {
|
|
27
|
+
const mod = await import(`${CLI_COMMANDS}/tailscale.mjs`);
|
|
28
|
+
assert.equal(typeof mod.ensureTailscaleAuth, 'function');
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test('exports setupTailscaleServe() function', async () => {
|
|
32
|
+
const mod = await import(`${CLI_COMMANDS}/tailscale.mjs`);
|
|
33
|
+
assert.equal(typeof mod.setupTailscaleServe, 'function');
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test('exports unsetupTailscaleServe() function', async () => {
|
|
37
|
+
const mod = await import(`${CLI_COMMANDS}/tailscale.mjs`);
|
|
38
|
+
assert.equal(typeof mod.unsetupTailscaleServe, 'function');
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test('exports getServeUrl() function', async () => {
|
|
42
|
+
const mod = await import(`${CLI_COMMANDS}/tailscale.mjs`);
|
|
43
|
+
assert.equal(typeof mod.getServeUrl, 'function');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test('exports showTailscaleStatus() function', async () => {
|
|
47
|
+
const mod = await import(`${CLI_COMMANDS}/tailscale.mjs`);
|
|
48
|
+
assert.equal(typeof mod.showTailscaleStatus, 'function');
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test('exports showTailscaleHelp() function', async () => {
|
|
52
|
+
const mod = await import(`${CLI_COMMANDS}/tailscale.mjs`);
|
|
53
|
+
assert.equal(typeof mod.showTailscaleHelp, 'function');
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test('exports isTailscaleInstalled() function', async () => {
|
|
57
|
+
const mod = await import(`${CLI_COMMANDS}/tailscale.mjs`);
|
|
58
|
+
assert.equal(typeof mod.isTailscaleInstalled, 'function');
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
describe('run() — CLI subcommand dispatcher', () => {
|
|
63
|
+
test('run() with no args does not throw (prints help)', async () => {
|
|
64
|
+
const mod = await import(`${CLI_COMMANDS}/tailscale.mjs`);
|
|
65
|
+
// Should not throw - it will print help and return
|
|
66
|
+
await mod.run('tailscale', [], false);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test('run() with --help flag does not throw', async () => {
|
|
70
|
+
const mod = await import(`${CLI_COMMANDS}/tailscale.mjs`);
|
|
71
|
+
await mod.run('tailscale', ['--help'], false);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test('run() with "help" subcommand does not throw', async () => {
|
|
75
|
+
const mod = await import(`${CLI_COMMANDS}/tailscale.mjs`);
|
|
76
|
+
await mod.run('tailscale', ['help'], false);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test('run() with unknown subcommand does not throw (prints error + help)', async () => {
|
|
80
|
+
const mod = await import(`${CLI_COMMANDS}/tailscale.mjs`);
|
|
81
|
+
// Should not throw - it will print an error and help
|
|
82
|
+
await mod.run('tailscale', ['unknown-subcommand'], false);
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
describe('showTailscaleHelp()', () => {
|
|
87
|
+
test('prints help text including env var documentation', async () => {
|
|
88
|
+
const mod = await import(`${CLI_COMMANDS}/tailscale.mjs`);
|
|
89
|
+
let stdout = '';
|
|
90
|
+
const orig = process.stdout.write.bind(process.stdout);
|
|
91
|
+
// eslint-disable-next-line @typescript-eslint/no-implied-eval
|
|
92
|
+
process.stdout.write = (s) => { stdout += String(s); return true; };
|
|
93
|
+
try {
|
|
94
|
+
mod.showTailscaleHelp();
|
|
95
|
+
assert.ok(stdout.includes('bizar tailscale'));
|
|
96
|
+
assert.ok(stdout.includes('TAILSCALE_AUTHKEY'));
|
|
97
|
+
assert.ok(stdout.includes('bizar tailscale status'));
|
|
98
|
+
assert.ok(stdout.includes('bizar tailscale serve'));
|
|
99
|
+
assert.ok(stdout.includes('bizar tailscale unserve'));
|
|
100
|
+
} finally {
|
|
101
|
+
process.stdout.write = orig;
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
describe('getServeUrl()', () => {
|
|
107
|
+
test('returns null when serve config does not exist', async () => {
|
|
108
|
+
const mod = await import(`${CLI_COMMANDS}/tailscale.mjs`);
|
|
109
|
+
const url = mod.getServeUrl();
|
|
110
|
+
// Returns null when no config file exists (test environment)
|
|
111
|
+
assert.equal(url, null);
|
|
112
|
+
});
|
|
113
|
+
});
|