@happyvertical/smrt-ui 0.53.1 → 0.53.3
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/package.json +3 -3
- package/dist/actions/__tests__/ripple.test.js +0 -82
- package/dist/actions/__tests__/swipe-dismiss.test.js +0 -69
- package/dist/components/calendar/__tests__/CalendarView.test.js +0 -226
- package/dist/components/calendar/__tests__/calendar-model.test.js +0 -138
- package/dist/components/chat/__tests__/chat-primitives.test.js +0 -118
- package/dist/components/data/__tests__/CollectionList.test.js +0 -37
- package/dist/components/data/__tests__/DataTable.test.js +0 -807
- package/dist/components/data/__tests__/DataTableConformance.test.js +0 -212
- package/dist/components/data/__tests__/DataTableController.test.js +0 -371
- package/dist/components/data/__tests__/DataTableIdentity.test.js +0 -29
- package/dist/components/data/__tests__/DataTableLayout.test.js +0 -195
- package/dist/components/data/__tests__/DataTablePerformance.test.js +0 -21
- package/dist/components/data/__tests__/DataTableResponsive.test.js +0 -80
- package/dist/components/data/__tests__/DataTableVirtualization.test.js +0 -80
- package/dist/components/data/__tests__/DataTableVirtualizationComponent.test.js +0 -255
- package/dist/components/data/__tests__/SortableHeader.test.js +0 -101
- package/dist/components/data/__tests__/data-surface.test.js +0 -746
- package/dist/components/data/__tests__/data-table-helpers.test.js +0 -36
- package/dist/components/data/__tests__/data-table-surface.test.js +0 -49
- package/dist/components/data/__tests__/link-step-surface.test.js +0 -172
- package/dist/components/data/__tests__/list-sort-state.svelte.test.js +0 -71
- package/dist/components/data/__tests__/list-sort.test.js +0 -91
- package/dist/components/data/__tests__/mounted-data-surface.test.js +0 -794
- package/dist/components/data/__tests__/responsive-table.fixture.svelte +0 -29
- package/dist/components/data/__tests__/responsive-table.fixture.svelte.d.ts +0 -7
- package/dist/components/data/__tests__/responsive-table.fixture.svelte.d.ts.map +0 -1
- package/dist/components/data/__tests__/surface-navigation.test.js +0 -121
- package/dist/components/display/__tests__/ConfidenceBadge.test.js +0 -109
- package/dist/components/display/__tests__/CurrencyDisplay.test.js +0 -304
- package/dist/components/display/__tests__/CurrencyDisplayI18nHarness.svelte +0 -20
- package/dist/components/display/__tests__/CurrencyDisplayI18nHarness.svelte.d.ts +0 -19
- package/dist/components/display/__tests__/CurrencyDisplayI18nHarness.svelte.d.ts.map +0 -1
- package/dist/components/display/__tests__/CurrencyDisplaySsrHarness.svelte +0 -7
- package/dist/components/display/__tests__/CurrencyDisplaySsrHarness.svelte.d.ts +0 -19
- package/dist/components/display/__tests__/CurrencyDisplaySsrHarness.svelte.d.ts.map +0 -1
- package/dist/components/display/__tests__/DateDisplay.test.js +0 -195
- package/dist/components/display/__tests__/Icon.test.js +0 -223
- package/dist/components/display/__tests__/StatusBadge.test.js +0 -191
- package/dist/components/feedback/__tests__/ConfirmDialog.test.js +0 -181
- package/dist/components/feedback/__tests__/LoadingOverlay.test.js +0 -113
- package/dist/components/feedback/__tests__/Modal.test.js +0 -72
- package/dist/components/feedback/__tests__/PhoneSheet.test.js +0 -101
- package/dist/components/feedback/__tests__/ProgressBar.test.js +0 -89
- package/dist/components/feedback/__tests__/WorkingStrip.test.js +0 -172
- package/dist/components/feedback/__tests__/confirm-message.fixture.svelte +0 -9
- package/dist/components/feedback/__tests__/confirm-message.fixture.svelte.d.ts +0 -19
- package/dist/components/feedback/__tests__/confirm-message.fixture.svelte.d.ts.map +0 -1
- package/dist/components/feedback/__tests__/toast.test.js +0 -190
- package/dist/components/forms/__tests__/CameraCapture.test.js +0 -504
- package/dist/components/forms/__tests__/Combobox.test.js +0 -354
- package/dist/components/forms/__tests__/ControlHitArea.test.js +0 -73
- package/dist/components/forms/__tests__/DictationButton.test.js +0 -176
- package/dist/components/forms/__tests__/Fieldset.test.js +0 -37
- package/dist/components/forms/__tests__/Form.test.js +0 -201
- package/dist/components/forms/__tests__/FormActionBar.test.js +0 -22
- package/dist/components/forms/__tests__/FormGroup.test.js +0 -48
- package/dist/components/forms/__tests__/Input.test.js +0 -49
- package/dist/components/forms/__tests__/Listbox.test.js +0 -303
- package/dist/components/forms/__tests__/MultiSelect.test.js +0 -281
- package/dist/components/forms/__tests__/SearchInput.test.js +0 -92
- package/dist/components/forms/__tests__/Select.test.js +0 -37
- package/dist/components/forms/__tests__/SignaturePad.test.js +0 -338
- package/dist/components/forms/__tests__/StagedControlReview.test.js +0 -702
- package/dist/components/forms/__tests__/Textarea.test.js +0 -39
- package/dist/components/forms/__tests__/Toggle.test.js +0 -87
- package/dist/components/forms/__tests__/camera-capture-session.test.js +0 -207
- package/dist/components/forms/__tests__/capture-form-retry.test.js +0 -166
- package/dist/components/forms/__tests__/capture-form.fixture.svelte +0 -38
- package/dist/components/forms/__tests__/capture-form.fixture.svelte.d.ts +0 -10
- package/dist/components/forms/__tests__/capture-form.fixture.svelte.d.ts.map +0 -1
- package/dist/components/forms/__tests__/capture-test-env.d.ts +0 -70
- package/dist/components/forms/__tests__/capture-test-env.d.ts.map +0 -1
- package/dist/components/forms/__tests__/capture-test-env.js +0 -122
- package/dist/components/forms/__tests__/capture-touch.fixture.svelte +0 -15
- package/dist/components/forms/__tests__/capture-touch.fixture.svelte.d.ts +0 -8
- package/dist/components/forms/__tests__/capture-touch.fixture.svelte.d.ts.map +0 -1
- package/dist/components/forms/__tests__/combobox-form.fixture.svelte +0 -29
- package/dist/components/forms/__tests__/combobox-form.fixture.svelte.d.ts +0 -14
- package/dist/components/forms/__tests__/combobox-form.fixture.svelte.d.ts.map +0 -1
- package/dist/components/forms/__tests__/combobox-labels.fixture.svelte +0 -19
- package/dist/components/forms/__tests__/combobox-labels.fixture.svelte.d.ts +0 -11
- package/dist/components/forms/__tests__/combobox-labels.fixture.svelte.d.ts.map +0 -1
- package/dist/components/forms/__tests__/composite-controls.fixture.svelte +0 -47
- package/dist/components/forms/__tests__/composite-controls.fixture.svelte.d.ts +0 -12
- package/dist/components/forms/__tests__/composite-controls.fixture.svelte.d.ts.map +0 -1
- package/dist/components/forms/__tests__/composite-controls.test.js +0 -128
- package/dist/components/forms/__tests__/composite-registration.fixture.svelte +0 -29
- package/dist/components/forms/__tests__/composite-registration.fixture.svelte.d.ts +0 -8
- package/dist/components/forms/__tests__/composite-registration.fixture.svelte.d.ts.map +0 -1
- package/dist/components/forms/__tests__/composite-user-edit.fixture.svelte +0 -41
- package/dist/components/forms/__tests__/composite-user-edit.fixture.svelte.d.ts +0 -13
- package/dist/components/forms/__tests__/composite-user-edit.fixture.svelte.d.ts.map +0 -1
- package/dist/components/forms/__tests__/control-interaction.test.js +0 -3421
- package/dist/components/forms/__tests__/control-proposals.test.js +0 -63
- package/dist/components/forms/__tests__/core-controls.fixture.svelte +0 -50
- package/dist/components/forms/__tests__/core-controls.fixture.svelte.d.ts +0 -9
- package/dist/components/forms/__tests__/core-controls.fixture.svelte.d.ts.map +0 -1
- package/dist/components/forms/__tests__/core-controls.test.js +0 -311
- package/dist/components/forms/__tests__/decimal-sliders.fixture.svelte +0 -25
- package/dist/components/forms/__tests__/decimal-sliders.fixture.svelte.d.ts +0 -8
- package/dist/components/forms/__tests__/decimal-sliders.fixture.svelte.d.ts.map +0 -1
- package/dist/components/forms/__tests__/dictation-recording.test.js +0 -582
- package/dist/components/forms/__tests__/dictation.test.js +0 -486
- package/dist/components/forms/__tests__/form-attachment.fixture.svelte +0 -28
- package/dist/components/forms/__tests__/form-attachment.fixture.svelte.d.ts +0 -11
- package/dist/components/forms/__tests__/form-attachment.fixture.svelte.d.ts.map +0 -1
- package/dist/components/forms/__tests__/form-group-input.fixture.svelte +0 -16
- package/dist/components/forms/__tests__/form-group-input.fixture.svelte.d.ts +0 -9
- package/dist/components/forms/__tests__/form-group-input.fixture.svelte.d.ts.map +0 -1
- package/dist/components/forms/__tests__/form-retry-form.fixture.svelte +0 -31
- package/dist/components/forms/__tests__/form-retry-form.fixture.svelte.d.ts +0 -13
- package/dist/components/forms/__tests__/form-retry-form.fixture.svelte.d.ts.map +0 -1
- package/dist/components/forms/__tests__/form-retry-form.test.js +0 -107
- package/dist/components/forms/__tests__/hidden-controls-form-retry.test.js +0 -69
- package/dist/components/forms/__tests__/hidden-controls-form.fixture.svelte +0 -29
- package/dist/components/forms/__tests__/hidden-controls-form.fixture.svelte.d.ts +0 -9
- package/dist/components/forms/__tests__/hidden-controls-form.fixture.svelte.d.ts.map +0 -1
- package/dist/components/forms/__tests__/listbox-form.fixture.svelte +0 -27
- package/dist/components/forms/__tests__/listbox-form.fixture.svelte.d.ts +0 -13
- package/dist/components/forms/__tests__/listbox-form.fixture.svelte.d.ts.map +0 -1
- package/dist/components/forms/__tests__/long-press.test.js +0 -175
- package/dist/components/forms/__tests__/multiselect-form.fixture.svelte +0 -27
- package/dist/components/forms/__tests__/multiselect-form.fixture.svelte.d.ts +0 -13
- package/dist/components/forms/__tests__/multiselect-form.fixture.svelte.d.ts.map +0 -1
- package/dist/components/forms/__tests__/native-file-field.test.js +0 -177
- package/dist/components/forms/__tests__/place-picker.fixture.svelte +0 -33
- package/dist/components/forms/__tests__/place-picker.fixture.svelte.d.ts +0 -12
- package/dist/components/forms/__tests__/place-picker.fixture.svelte.d.ts.map +0 -1
- package/dist/components/forms/__tests__/segmented-posting.fixture.svelte +0 -28
- package/dist/components/forms/__tests__/segmented-posting.fixture.svelte.d.ts +0 -12
- package/dist/components/forms/__tests__/segmented-posting.fixture.svelte.d.ts.map +0 -1
- package/dist/components/forms/__tests__/segmented-posting.test.js +0 -92
- package/dist/components/forms/__tests__/select-interaction.fixture.svelte +0 -25
- package/dist/components/forms/__tests__/select-interaction.fixture.svelte.d.ts +0 -9
- package/dist/components/forms/__tests__/select-interaction.fixture.svelte.d.ts.map +0 -1
- package/dist/components/forms/__tests__/signature-pad-logic.test.js +0 -54
- package/dist/components/forms/__tests__/staged-review-fieldset.fixture.svelte +0 -21
- package/dist/components/forms/__tests__/staged-review-fieldset.fixture.svelte.d.ts +0 -8
- package/dist/components/forms/__tests__/staged-review-fieldset.fixture.svelte.d.ts.map +0 -1
- package/dist/components/forms/__tests__/staged-review-outer-fieldset.fixture.svelte +0 -23
- package/dist/components/forms/__tests__/staged-review-outer-fieldset.fixture.svelte.d.ts +0 -8
- package/dist/components/forms/__tests__/staged-review-outer-fieldset.fixture.svelte.d.ts.map +0 -1
- package/dist/components/forms/__tests__/staged-review.fixture.svelte +0 -56
- package/dist/components/forms/__tests__/staged-review.fixture.svelte.d.ts +0 -12
- package/dist/components/forms/__tests__/staged-review.fixture.svelte.d.ts.map +0 -1
- package/dist/components/forms/__tests__/touch-density.fixture.svelte +0 -34
- package/dist/components/forms/__tests__/touch-density.fixture.svelte.d.ts +0 -8
- package/dist/components/forms/__tests__/touch-density.fixture.svelte.d.ts.map +0 -1
- package/dist/components/forms/__tests__/touch-density.test.js +0 -66
- package/dist/components/forms/form-retry/__tests__/boundaries.test.js +0 -23
- package/dist/components/forms/form-retry/__tests__/content-digest.test.js +0 -70
- package/dist/components/forms/form-retry/__tests__/controller.test.js +0 -802
- package/dist/components/forms/form-retry/__tests__/fake-kit.d.ts +0 -82
- package/dist/components/forms/form-retry/__tests__/fake-kit.d.ts.map +0 -1
- package/dist/components/forms/form-retry/__tests__/fake-kit.js +0 -180
- package/dist/components/forms/form-retry/__tests__/form-draft.test.js +0 -169
- package/dist/components/forms/form-retry/__tests__/ssr.test.js +0 -37
- package/dist/components/forms/form-retry/__tests__/storage.d.ts +0 -26
- package/dist/components/forms/form-retry/__tests__/storage.d.ts.map +0 -1
- package/dist/components/forms/form-retry/__tests__/storage.js +0 -63
- package/dist/components/forms/form-retry/__tests__/submission-key.test.js +0 -236
- package/dist/components/forms/form-retry/__tests__/submitted-draft.test.js +0 -334
- package/dist/components/layout/__tests__/Container.test.js +0 -62
- package/dist/components/layout/__tests__/EmptyState.test.js +0 -83
- package/dist/components/layout/__tests__/Footer.test.js +0 -50
- package/dist/components/layout/__tests__/Grid.test.js +0 -121
- package/dist/components/layout/__tests__/Header.test.js +0 -48
- package/dist/components/layout/__tests__/Masthead.test.js +0 -93
- package/dist/components/layout/__tests__/PageHeader.test.js +0 -166
- package/dist/components/layout/__tests__/SummaryCard.test.js +0 -104
- package/dist/components/nav/__tests__/FilterChips.test.js +0 -94
- package/dist/components/nav/__tests__/LinkTabs.test.js +0 -117
- package/dist/components/nav/__tests__/Tabs.test.js +0 -128
- package/dist/components/roles/__tests__/RoleSelector.test.js +0 -134
- package/dist/components/ui/__tests__/Badge.test.js +0 -76
- package/dist/components/ui/__tests__/Button.test.js +0 -166
- package/dist/components/ui/__tests__/Card.test.js +0 -103
- package/dist/components/ui/__tests__/IconToggle.test.js +0 -59
- package/dist/components/ui/__tests__/IconToggleGroup.test.js +0 -30
- package/dist/components/ui/__tests__/Pagination.test.js +0 -113
- package/dist/components/ui/__tests__/Popover.test.js +0 -44
- package/dist/components/ui/__tests__/dropdown-popover-icon.test.js +0 -125
- package/dist/components/ui/__tests__/gap-primitives-interactive.test.js +0 -112
- package/dist/components/ui/__tests__/gap-primitives.test.js +0 -84
- package/dist/components/ui/__tests__/interaction-primitives.fixture.svelte +0 -13
- package/dist/components/ui/__tests__/interaction-primitives.fixture.svelte.d.ts +0 -19
- package/dist/components/ui/__tests__/interaction-primitives.fixture.svelte.d.ts.map +0 -1
- package/dist/components/ui/__tests__/interaction-primitives.test.js +0 -31
- package/dist/components/ui/__tests__/phone-touch-targets.test.js +0 -39
- package/dist/i18n/__tests__/i18n.test.js +0 -74
- package/dist/i18n/__tests__/render-parity.spec.js +0 -37
- package/dist/svelte/__tests__/playground.test.js +0 -20
- package/dist/themes/__tests__/control-boundary-contrast.test.js +0 -112
- package/dist/themes/__tests__/create-theme.test.js +0 -79
- package/dist/themes/__tests__/css-generator.test.js +0 -129
- package/dist/themes/__tests__/happyvertical-theme.test.js +0 -231
- package/dist/themes/__tests__/registry.test.js +0 -43
- package/dist/themes/__tests__/styles-lockstep.test.js +0 -32
- package/dist/themes/__tests__/theme-provider.test.js +0 -147
- package/dist/themes/__tests__/theme-script.test.js +0 -130
- package/dist/themes/__tests__/token-aliases.test.js +0 -178
- package/dist/utils/forms/__tests__/formatters.test.js +0 -27
- package/dist/utils/theme/__tests__/color.test.js +0 -89
- package/dist/utils/theme/__tests__/typography.test.js +0 -11
|
@@ -1,582 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Dictation's recording fallback: when the browser has no speech recognition
|
|
3
|
-
* (Firefox) or it fails with no speech service behind it (Brave), record with
|
|
4
|
-
* MediaRecorder and hand the audio to `transcribe`. Fake speech source, fake
|
|
5
|
-
* recorder, fake MediaRecorder: nothing here touches a real microphone.
|
|
6
|
-
*/
|
|
7
|
-
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
8
|
-
import { createMediaRecorderCapture, DictationError, pickDictationMimeType, } from '../audio-capture.js';
|
|
9
|
-
import { Dictation, } from '../dictation.svelte.js';
|
|
10
|
-
import { createHttpTranscriber } from '../dictation-transcribe.js';
|
|
11
|
-
function fakeSource(options = {}) {
|
|
12
|
-
const results = new Set();
|
|
13
|
-
const errors = new Set();
|
|
14
|
-
const ends = new Set();
|
|
15
|
-
return {
|
|
16
|
-
start: vi.fn(async () => {
|
|
17
|
-
if (options.startError)
|
|
18
|
-
throw options.startError;
|
|
19
|
-
}),
|
|
20
|
-
stop: vi.fn(async () => {
|
|
21
|
-
for (const cb of ends)
|
|
22
|
-
cb();
|
|
23
|
-
}),
|
|
24
|
-
onResult: (cb) => (results.add(cb), () => results.delete(cb)),
|
|
25
|
-
onError: (cb) => (errors.add(cb), () => errors.delete(cb)),
|
|
26
|
-
onEnd: (cb) => (ends.add(cb), () => ends.delete(cb)),
|
|
27
|
-
emitResult(text, isFinal) {
|
|
28
|
-
for (const cb of results)
|
|
29
|
-
cb({ text, isFinal });
|
|
30
|
-
},
|
|
31
|
-
emitError(error) {
|
|
32
|
-
for (const cb of errors)
|
|
33
|
-
cb(error);
|
|
34
|
-
},
|
|
35
|
-
emitEnd() {
|
|
36
|
-
for (const cb of ends)
|
|
37
|
-
cb();
|
|
38
|
-
},
|
|
39
|
-
};
|
|
40
|
-
}
|
|
41
|
-
function speechError(code) {
|
|
42
|
-
return Object.assign(new Error(`Speech recognition error: ${code}`), {
|
|
43
|
-
speechError: code,
|
|
44
|
-
});
|
|
45
|
-
}
|
|
46
|
-
function fakeCaptureFactory(options = {}) {
|
|
47
|
-
const made = [];
|
|
48
|
-
const factory = vi.fn(() => {
|
|
49
|
-
const limits = new Set();
|
|
50
|
-
const capture = {
|
|
51
|
-
start: vi.fn(async () => {
|
|
52
|
-
await options.startGate;
|
|
53
|
-
if (options.startError)
|
|
54
|
-
throw options.startError;
|
|
55
|
-
}),
|
|
56
|
-
stop: vi.fn(async () => {
|
|
57
|
-
if (options.stopError)
|
|
58
|
-
throw options.stopError;
|
|
59
|
-
return {
|
|
60
|
-
audio: new Blob(['opus-bytes'], { type: 'audio/webm;codecs=opus' }),
|
|
61
|
-
mimeType: 'audio/webm;codecs=opus',
|
|
62
|
-
durationMs: 3200,
|
|
63
|
-
reachedTimeLimit: false,
|
|
64
|
-
...options.recording,
|
|
65
|
-
};
|
|
66
|
-
}),
|
|
67
|
-
cancel: vi.fn(),
|
|
68
|
-
onLimit: (cb) => (limits.add(cb), () => limits.delete(cb)),
|
|
69
|
-
hitLimit(reason) {
|
|
70
|
-
for (const cb of limits)
|
|
71
|
-
cb(reason);
|
|
72
|
-
},
|
|
73
|
-
};
|
|
74
|
-
made.push(capture);
|
|
75
|
-
return capture;
|
|
76
|
-
});
|
|
77
|
-
return { factory, made };
|
|
78
|
-
}
|
|
79
|
-
afterEach(() => {
|
|
80
|
-
vi.useRealTimers();
|
|
81
|
-
vi.unstubAllGlobals();
|
|
82
|
-
});
|
|
83
|
-
describe('Dictation recording fallback', () => {
|
|
84
|
-
it('records when there is no speech source, then writes it down', async () => {
|
|
85
|
-
const { factory, made } = fakeCaptureFactory();
|
|
86
|
-
const transcribe = vi.fn(async () => 'make it square');
|
|
87
|
-
const onText = vi.fn();
|
|
88
|
-
const beep = vi.fn(async () => true);
|
|
89
|
-
const dictation = new Dictation({
|
|
90
|
-
onText,
|
|
91
|
-
transcribe,
|
|
92
|
-
capture: factory,
|
|
93
|
-
beep,
|
|
94
|
-
language: 'en-CA',
|
|
95
|
-
maxDurationMs: 90_000,
|
|
96
|
-
maxBytes: 5_000_000,
|
|
97
|
-
});
|
|
98
|
-
expect(dictation.available).toBe(true);
|
|
99
|
-
await dictation.start();
|
|
100
|
-
expect(dictation.state).toBe('listening');
|
|
101
|
-
expect(dictation.recording).toBe(true);
|
|
102
|
-
expect(factory).toHaveBeenCalledWith({
|
|
103
|
-
maxDurationMs: 90_000,
|
|
104
|
-
maxBytes: 5_000_000,
|
|
105
|
-
});
|
|
106
|
-
await vi.waitFor(() => expect(beep).toHaveBeenCalledTimes(1));
|
|
107
|
-
let release;
|
|
108
|
-
transcribe.mockImplementationOnce(() => new Promise((resolve) => (release = resolve)));
|
|
109
|
-
const stopped = dictation.stop();
|
|
110
|
-
await vi.waitFor(() => expect(transcribe).toHaveBeenCalled());
|
|
111
|
-
expect(dictation.state).toBe('transcribing');
|
|
112
|
-
expect(dictation.active).toBe(false);
|
|
113
|
-
release(' make it square ');
|
|
114
|
-
await stopped;
|
|
115
|
-
expect(made[0].stop).toHaveBeenCalled();
|
|
116
|
-
expect(transcribe).toHaveBeenCalledWith(expect.any(Blob), {
|
|
117
|
-
mimeType: 'audio/webm;codecs=opus',
|
|
118
|
-
language: 'en-CA',
|
|
119
|
-
durationMs: 3200,
|
|
120
|
-
signal: expect.any(AbortSignal),
|
|
121
|
-
});
|
|
122
|
-
expect(onText).toHaveBeenCalledWith('make it square');
|
|
123
|
-
expect(dictation.state).toBe('idle');
|
|
124
|
-
expect(dictation.recording).toBe(false);
|
|
125
|
-
});
|
|
126
|
-
it('switches to recording when Brave fails with `network`, and records straight away next time', async () => {
|
|
127
|
-
const source = fakeSource();
|
|
128
|
-
const { factory, made } = fakeCaptureFactory();
|
|
129
|
-
const log = vi.fn();
|
|
130
|
-
const onText = vi.fn();
|
|
131
|
-
const dictation = new Dictation({
|
|
132
|
-
source: () => source,
|
|
133
|
-
onText,
|
|
134
|
-
transcribe: async () => 'the arena at night',
|
|
135
|
-
capture: factory,
|
|
136
|
-
beep: false,
|
|
137
|
-
requestMicrophone: false,
|
|
138
|
-
log,
|
|
139
|
-
});
|
|
140
|
-
await dictation.start();
|
|
141
|
-
expect(dictation.state).toBe('listening');
|
|
142
|
-
expect(dictation.recording).toBe(false);
|
|
143
|
-
source.emitError(speechError('network'));
|
|
144
|
-
await vi.waitFor(() => expect(made).toHaveLength(1));
|
|
145
|
-
await vi.waitFor(() => expect(made[0].start).toHaveBeenCalled());
|
|
146
|
-
await made[0].start.mock.results[0].value;
|
|
147
|
-
await Promise.resolve();
|
|
148
|
-
expect(dictation.state).toBe('listening');
|
|
149
|
-
expect(dictation.recording).toBe(true);
|
|
150
|
-
expect(source.stop).toHaveBeenCalled();
|
|
151
|
-
expect(log).toHaveBeenCalledWith(expect.objectContaining({
|
|
152
|
-
kind: 'unsupported',
|
|
153
|
-
code: 'network',
|
|
154
|
-
fallback: 'recording',
|
|
155
|
-
}));
|
|
156
|
-
await dictation.stop();
|
|
157
|
-
expect(onText).toHaveBeenCalledWith('the arena at night');
|
|
158
|
-
expect(dictation.state).toBe('idle');
|
|
159
|
-
source.start.mockClear();
|
|
160
|
-
await dictation.start();
|
|
161
|
-
expect(source.start).not.toHaveBeenCalled();
|
|
162
|
-
expect(made).toHaveLength(2);
|
|
163
|
-
expect(dictation.recording).toBe(true);
|
|
164
|
-
});
|
|
165
|
-
it('records when the recogniser refuses to start (`service-not-allowed`)', async () => {
|
|
166
|
-
const source = fakeSource({
|
|
167
|
-
startError: speechError('service-not-allowed'),
|
|
168
|
-
});
|
|
169
|
-
const { factory, made } = fakeCaptureFactory();
|
|
170
|
-
const dictation = new Dictation({
|
|
171
|
-
source: () => source,
|
|
172
|
-
onText: () => { },
|
|
173
|
-
transcribe: async () => 'hello',
|
|
174
|
-
capture: factory,
|
|
175
|
-
beep: false,
|
|
176
|
-
requestMicrophone: false,
|
|
177
|
-
log: () => { },
|
|
178
|
-
});
|
|
179
|
-
await dictation.start();
|
|
180
|
-
await vi.waitFor(() => expect(dictation.state).toBe('listening'));
|
|
181
|
-
expect(dictation.recording).toBe(true);
|
|
182
|
-
expect(made).toHaveLength(1);
|
|
183
|
-
});
|
|
184
|
-
it('records when the browser has no speech recognition at all (Firefox)', async () => {
|
|
185
|
-
const { factory } = fakeCaptureFactory();
|
|
186
|
-
const dictation = new Dictation({
|
|
187
|
-
source: () => {
|
|
188
|
-
throw Object.assign(new Error('Speech recognition not available'), {
|
|
189
|
-
code: 'CAPABILITY_NOT_AVAILABLE',
|
|
190
|
-
});
|
|
191
|
-
},
|
|
192
|
-
onText: () => { },
|
|
193
|
-
transcribe: async () => 'hello',
|
|
194
|
-
capture: factory,
|
|
195
|
-
beep: false,
|
|
196
|
-
requestMicrophone: false,
|
|
197
|
-
log: () => { },
|
|
198
|
-
});
|
|
199
|
-
await dictation.start();
|
|
200
|
-
await vi.waitFor(() => expect(dictation.state).toBe('listening'));
|
|
201
|
-
expect(dictation.recording).toBe(true);
|
|
202
|
-
});
|
|
203
|
-
it('records when the recogniser ends straight away without an error', async () => {
|
|
204
|
-
const source = fakeSource();
|
|
205
|
-
const { factory } = fakeCaptureFactory();
|
|
206
|
-
const dictation = new Dictation({
|
|
207
|
-
source: () => source,
|
|
208
|
-
onText: () => { },
|
|
209
|
-
transcribe: async () => 'hello',
|
|
210
|
-
capture: factory,
|
|
211
|
-
beep: false,
|
|
212
|
-
requestMicrophone: false,
|
|
213
|
-
log: () => { },
|
|
214
|
-
});
|
|
215
|
-
await dictation.start();
|
|
216
|
-
source.emitEnd();
|
|
217
|
-
await vi.waitFor(() => expect(dictation.recording).toBe(true));
|
|
218
|
-
expect(dictation.state).toBe('listening');
|
|
219
|
-
});
|
|
220
|
-
it('keeps the plain error without a transcribe function', async () => {
|
|
221
|
-
const source = fakeSource();
|
|
222
|
-
const dictation = new Dictation({
|
|
223
|
-
source: () => source,
|
|
224
|
-
onText: () => { },
|
|
225
|
-
beep: false,
|
|
226
|
-
requestMicrophone: false,
|
|
227
|
-
log: () => { },
|
|
228
|
-
});
|
|
229
|
-
await dictation.start();
|
|
230
|
-
source.emitError(speechError('network'));
|
|
231
|
-
expect(dictation.state).toBe('error');
|
|
232
|
-
expect(dictation.errorKind).toBe('unsupported');
|
|
233
|
-
});
|
|
234
|
-
it('does not switch after words were already heard', async () => {
|
|
235
|
-
const source = fakeSource();
|
|
236
|
-
const { factory } = fakeCaptureFactory();
|
|
237
|
-
const dictation = new Dictation({
|
|
238
|
-
source: () => source,
|
|
239
|
-
onText: () => { },
|
|
240
|
-
transcribe: async () => 'x',
|
|
241
|
-
capture: factory,
|
|
242
|
-
beep: false,
|
|
243
|
-
requestMicrophone: false,
|
|
244
|
-
log: () => { },
|
|
245
|
-
});
|
|
246
|
-
await dictation.start();
|
|
247
|
-
source.emitResult('hello', true);
|
|
248
|
-
source.emitError(speechError('network'));
|
|
249
|
-
expect(dictation.state).toBe('error');
|
|
250
|
-
expect(factory).not.toHaveBeenCalled();
|
|
251
|
-
});
|
|
252
|
-
it('does not record over a blocked microphone', async () => {
|
|
253
|
-
const { factory } = fakeCaptureFactory({
|
|
254
|
-
startError: Object.assign(new Error('Permission denied'), {
|
|
255
|
-
name: 'NotAllowedError',
|
|
256
|
-
}),
|
|
257
|
-
});
|
|
258
|
-
const dictation = new Dictation({
|
|
259
|
-
onText: () => { },
|
|
260
|
-
transcribe: async () => 'x',
|
|
261
|
-
capture: factory,
|
|
262
|
-
beep: false,
|
|
263
|
-
log: () => { },
|
|
264
|
-
});
|
|
265
|
-
await dictation.start();
|
|
266
|
-
expect(dictation.state).toBe('error');
|
|
267
|
-
expect(dictation.errorKind).toBe('denied');
|
|
268
|
-
});
|
|
269
|
-
it.each([
|
|
270
|
-
[new DictationError('too-long', 'too big'), 'too-long'],
|
|
271
|
-
[new DictationError('unavailable', 'not set up'), 'unavailable'],
|
|
272
|
-
[new DictationError('forbidden', 'no'), 'forbidden'],
|
|
273
|
-
[new Error('Failed to fetch: network down'), 'not-transcribed'],
|
|
274
|
-
])('says why writing it down failed (%s)', async (error, kind) => {
|
|
275
|
-
const { factory } = fakeCaptureFactory();
|
|
276
|
-
const onText = vi.fn();
|
|
277
|
-
const dictation = new Dictation({
|
|
278
|
-
onText,
|
|
279
|
-
transcribe: async () => {
|
|
280
|
-
throw error;
|
|
281
|
-
},
|
|
282
|
-
capture: factory,
|
|
283
|
-
beep: false,
|
|
284
|
-
log: () => { },
|
|
285
|
-
});
|
|
286
|
-
await dictation.start();
|
|
287
|
-
await dictation.stop();
|
|
288
|
-
expect(dictation.state).toBe('error');
|
|
289
|
-
expect(dictation.errorKind).toBe(kind);
|
|
290
|
-
expect(onText).not.toHaveBeenCalled();
|
|
291
|
-
});
|
|
292
|
-
it('says nothing was heard when the text comes back empty', async () => {
|
|
293
|
-
const { factory } = fakeCaptureFactory();
|
|
294
|
-
const dictation = new Dictation({
|
|
295
|
-
onText: () => { },
|
|
296
|
-
transcribe: async () => ({ text: ' ' }),
|
|
297
|
-
capture: factory,
|
|
298
|
-
beep: false,
|
|
299
|
-
log: () => { },
|
|
300
|
-
});
|
|
301
|
-
await dictation.start();
|
|
302
|
-
await dictation.stop();
|
|
303
|
-
expect(dictation.errorKind).toBe('no-speech');
|
|
304
|
-
});
|
|
305
|
-
it('writes the recording down by itself at the time limit', async () => {
|
|
306
|
-
const { factory, made } = fakeCaptureFactory({
|
|
307
|
-
recording: { reachedTimeLimit: true, durationMs: 120_000 },
|
|
308
|
-
});
|
|
309
|
-
const onText = vi.fn();
|
|
310
|
-
const dictation = new Dictation({
|
|
311
|
-
onText,
|
|
312
|
-
transcribe: async () => 'a long story',
|
|
313
|
-
capture: factory,
|
|
314
|
-
beep: false,
|
|
315
|
-
});
|
|
316
|
-
await dictation.start();
|
|
317
|
-
made[0].hitLimit('time');
|
|
318
|
-
await vi.waitFor(() => expect(onText).toHaveBeenCalledWith('a long story'));
|
|
319
|
-
expect(dictation.state).toBe('idle');
|
|
320
|
-
});
|
|
321
|
-
it('fails as too long when the recording outgrows the size limit', async () => {
|
|
322
|
-
const { factory, made } = fakeCaptureFactory({
|
|
323
|
-
stopError: new DictationError('too-long', 'too large'),
|
|
324
|
-
});
|
|
325
|
-
const transcribe = vi.fn(async () => 'x');
|
|
326
|
-
const dictation = new Dictation({
|
|
327
|
-
onText: () => { },
|
|
328
|
-
transcribe,
|
|
329
|
-
capture: factory,
|
|
330
|
-
beep: false,
|
|
331
|
-
log: () => { },
|
|
332
|
-
});
|
|
333
|
-
await dictation.start();
|
|
334
|
-
made[0].hitLimit('size');
|
|
335
|
-
await vi.waitFor(() => expect(dictation.state).toBe('error'));
|
|
336
|
-
expect(dictation.errorKind).toBe('too-long');
|
|
337
|
-
expect(transcribe).not.toHaveBeenCalled();
|
|
338
|
-
});
|
|
339
|
-
it('stops quietly when the person stops before the microphone opened', async () => {
|
|
340
|
-
let open;
|
|
341
|
-
const { factory, made } = fakeCaptureFactory({
|
|
342
|
-
startGate: new Promise((resolve) => (open = resolve)),
|
|
343
|
-
});
|
|
344
|
-
const transcribe = vi.fn(async () => 'x');
|
|
345
|
-
const dictation = new Dictation({
|
|
346
|
-
onText: () => { },
|
|
347
|
-
transcribe,
|
|
348
|
-
capture: factory,
|
|
349
|
-
beep: false,
|
|
350
|
-
});
|
|
351
|
-
const started = dictation.start();
|
|
352
|
-
await vi.waitFor(() => expect(made).toHaveLength(1));
|
|
353
|
-
expect(dictation.state).toBe('starting');
|
|
354
|
-
await dictation.stop();
|
|
355
|
-
expect(dictation.state).toBe('idle');
|
|
356
|
-
expect(made[0].cancel).toHaveBeenCalled();
|
|
357
|
-
open();
|
|
358
|
-
await started;
|
|
359
|
-
expect(dictation.state).toBe('idle');
|
|
360
|
-
expect(transcribe).not.toHaveBeenCalled();
|
|
361
|
-
});
|
|
362
|
-
it('cancels writing it down when disposed', async () => {
|
|
363
|
-
const { factory } = fakeCaptureFactory();
|
|
364
|
-
let signal;
|
|
365
|
-
const onText = vi.fn();
|
|
366
|
-
const dictation = new Dictation({
|
|
367
|
-
onText,
|
|
368
|
-
transcribe: (_audio, options) => {
|
|
369
|
-
signal = options.signal;
|
|
370
|
-
return new Promise(() => { });
|
|
371
|
-
},
|
|
372
|
-
capture: factory,
|
|
373
|
-
beep: false,
|
|
374
|
-
});
|
|
375
|
-
await dictation.start();
|
|
376
|
-
void dictation.stop();
|
|
377
|
-
await vi.waitFor(() => expect(signal).toBeDefined());
|
|
378
|
-
expect(dictation.state).toBe('transcribing');
|
|
379
|
-
dictation.dispose();
|
|
380
|
-
expect(signal.aborted).toBe(true);
|
|
381
|
-
expect(dictation.state).toBe('idle');
|
|
382
|
-
expect(onText).not.toHaveBeenCalled();
|
|
383
|
-
});
|
|
384
|
-
it('gives up writing it down after the timeout', async () => {
|
|
385
|
-
const { factory } = fakeCaptureFactory();
|
|
386
|
-
const dictation = new Dictation({
|
|
387
|
-
onText: () => { },
|
|
388
|
-
transcribe: (_audio, { signal }) => new Promise((_resolve, reject) => signal.addEventListener('abort', () => reject(new DOMException('aborted', 'AbortError')))),
|
|
389
|
-
capture: factory,
|
|
390
|
-
beep: false,
|
|
391
|
-
transcribeTimeoutMs: 20,
|
|
392
|
-
log: () => { },
|
|
393
|
-
});
|
|
394
|
-
await dictation.start();
|
|
395
|
-
await dictation.stop();
|
|
396
|
-
expect(dictation.errorKind).toBe('not-transcribed');
|
|
397
|
-
expect(dictation.errorCode).toBe('timeout');
|
|
398
|
-
});
|
|
399
|
-
it('ignores taps while writing it down', async () => {
|
|
400
|
-
const { factory, made } = fakeCaptureFactory();
|
|
401
|
-
const dictation = new Dictation({
|
|
402
|
-
onText: () => { },
|
|
403
|
-
transcribe: () => new Promise(() => { }),
|
|
404
|
-
capture: factory,
|
|
405
|
-
beep: false,
|
|
406
|
-
});
|
|
407
|
-
await dictation.start();
|
|
408
|
-
void dictation.stop();
|
|
409
|
-
await vi.waitFor(() => expect(dictation.state).toBe('transcribing'));
|
|
410
|
-
await dictation.toggle();
|
|
411
|
-
expect(dictation.state).toBe('transcribing');
|
|
412
|
-
expect(made).toHaveLength(1);
|
|
413
|
-
dictation.dispose();
|
|
414
|
-
});
|
|
415
|
-
});
|
|
416
|
-
describe('createHttpTranscriber', () => {
|
|
417
|
-
it('posts the raw audio with its type, language and length, and reads the text', async () => {
|
|
418
|
-
const fetch = vi.fn(async () => new Response(JSON.stringify({ text: 'hello there' }), {
|
|
419
|
-
status: 200,
|
|
420
|
-
headers: { 'content-type': 'application/json' },
|
|
421
|
-
}));
|
|
422
|
-
const transcribe = createHttpTranscriber('/sites/x/api/transcribe', {
|
|
423
|
-
fetch: fetch,
|
|
424
|
-
});
|
|
425
|
-
const audio = new Blob(['bytes'], { type: 'audio/mp4' });
|
|
426
|
-
const controller = new AbortController();
|
|
427
|
-
const text = await transcribe(audio, {
|
|
428
|
-
mimeType: 'audio/mp4',
|
|
429
|
-
language: 'en-US',
|
|
430
|
-
durationMs: 4321.4,
|
|
431
|
-
signal: controller.signal,
|
|
432
|
-
});
|
|
433
|
-
expect(text).toBe('hello there');
|
|
434
|
-
const [url, init] = fetch.mock.calls[0];
|
|
435
|
-
const parsed = new URL(url);
|
|
436
|
-
expect(parsed.pathname).toBe('/sites/x/api/transcribe');
|
|
437
|
-
expect(parsed.searchParams.get('language')).toBe('en-US');
|
|
438
|
-
expect(parsed.searchParams.get('durationMs')).toBe('4321');
|
|
439
|
-
expect(init.method).toBe('POST');
|
|
440
|
-
expect(init.body).toBe(audio);
|
|
441
|
-
expect(init.headers['Content-Type']).toBe('audio/mp4');
|
|
442
|
-
expect(init.signal).toBe(controller.signal);
|
|
443
|
-
});
|
|
444
|
-
it.each([
|
|
445
|
-
[413, 'too-long'],
|
|
446
|
-
[503, 'unavailable'],
|
|
447
|
-
[403, 'forbidden'],
|
|
448
|
-
[401, 'forbidden'],
|
|
449
|
-
[502, 'not-transcribed'],
|
|
450
|
-
])('turns HTTP %i into %s', async (status, kind) => {
|
|
451
|
-
const transcribe = createHttpTranscriber('/t', {
|
|
452
|
-
fetch: (async () => new Response(JSON.stringify({ error: 'nope' }), {
|
|
453
|
-
status,
|
|
454
|
-
})),
|
|
455
|
-
});
|
|
456
|
-
const error = await transcribe(new Blob(['x']), {
|
|
457
|
-
mimeType: 'audio/webm',
|
|
458
|
-
language: 'en',
|
|
459
|
-
durationMs: 1,
|
|
460
|
-
signal: new AbortController().signal,
|
|
461
|
-
}).catch((e) => e);
|
|
462
|
-
expect(error).toBeInstanceOf(DictationError);
|
|
463
|
-
expect(error.dictationKind).toBe(kind);
|
|
464
|
-
expect(error.message).toBe('nope');
|
|
465
|
-
});
|
|
466
|
-
});
|
|
467
|
-
class FakeMediaRecorder extends EventTarget {
|
|
468
|
-
stream;
|
|
469
|
-
static supported = new Set(['audio/webm;codecs=opus', 'audio/webm']);
|
|
470
|
-
static isTypeSupported(type) {
|
|
471
|
-
return FakeMediaRecorder.supported.has(type);
|
|
472
|
-
}
|
|
473
|
-
static last = null;
|
|
474
|
-
state = 'inactive';
|
|
475
|
-
mimeType;
|
|
476
|
-
timeslice;
|
|
477
|
-
constructor(stream, options) {
|
|
478
|
-
super();
|
|
479
|
-
this.stream = stream;
|
|
480
|
-
this.mimeType = options?.mimeType ?? '';
|
|
481
|
-
FakeMediaRecorder.last = this;
|
|
482
|
-
}
|
|
483
|
-
start(timeslice) {
|
|
484
|
-
this.timeslice = timeslice;
|
|
485
|
-
this.state = 'recording';
|
|
486
|
-
}
|
|
487
|
-
stop() {
|
|
488
|
-
this.state = 'inactive';
|
|
489
|
-
queueMicrotask(() => this.dispatchEvent(new Event('stop')));
|
|
490
|
-
}
|
|
491
|
-
emitData(bytes) {
|
|
492
|
-
const event = Object.assign(new Event('dataavailable'), {
|
|
493
|
-
data: new Blob([new Uint8Array(bytes)]),
|
|
494
|
-
});
|
|
495
|
-
this.dispatchEvent(event);
|
|
496
|
-
}
|
|
497
|
-
}
|
|
498
|
-
function stubMedia() {
|
|
499
|
-
const track = { stop: vi.fn() };
|
|
500
|
-
const stream = { getTracks: () => [track] };
|
|
501
|
-
const getUserMedia = vi.fn(async () => stream);
|
|
502
|
-
vi.stubGlobal('MediaRecorder', FakeMediaRecorder);
|
|
503
|
-
vi.stubGlobal('navigator', { mediaDevices: { getUserMedia } });
|
|
504
|
-
return { track, getUserMedia };
|
|
505
|
-
}
|
|
506
|
-
describe('createMediaRecorderCapture', () => {
|
|
507
|
-
it('prefers WebM/Opus, then MP4 (Safari)', () => {
|
|
508
|
-
stubMedia();
|
|
509
|
-
expect(pickDictationMimeType()).toBe('audio/webm;codecs=opus');
|
|
510
|
-
FakeMediaRecorder.supported = new Set(['audio/mp4']);
|
|
511
|
-
expect(pickDictationMimeType()).toBe('audio/mp4');
|
|
512
|
-
FakeMediaRecorder.supported = new Set([
|
|
513
|
-
'audio/webm;codecs=opus',
|
|
514
|
-
'audio/webm',
|
|
515
|
-
]);
|
|
516
|
-
});
|
|
517
|
-
it('records, stops, and closes the microphone', async () => {
|
|
518
|
-
const { track, getUserMedia } = stubMedia();
|
|
519
|
-
const capture = createMediaRecorderCapture({});
|
|
520
|
-
await capture.start();
|
|
521
|
-
expect(getUserMedia).toHaveBeenCalledWith({ audio: true });
|
|
522
|
-
const recorder = FakeMediaRecorder.last;
|
|
523
|
-
expect(recorder.mimeType).toBe('audio/webm;codecs=opus');
|
|
524
|
-
expect(recorder.timeslice).toBe(1000);
|
|
525
|
-
recorder.emitData(100);
|
|
526
|
-
recorder.emitData(50);
|
|
527
|
-
const recording = await capture.stop();
|
|
528
|
-
expect(recording.audio.size).toBe(150);
|
|
529
|
-
expect(recording.mimeType).toBe('audio/webm;codecs=opus');
|
|
530
|
-
expect(recording.reachedTimeLimit).toBe(false);
|
|
531
|
-
expect(track.stop).toHaveBeenCalled();
|
|
532
|
-
});
|
|
533
|
-
it('stops by itself at the time limit and keeps the audio', async () => {
|
|
534
|
-
vi.useFakeTimers();
|
|
535
|
-
const { track } = stubMedia();
|
|
536
|
-
const capture = createMediaRecorderCapture({ maxDurationMs: 5000 });
|
|
537
|
-
const onLimit = vi.fn();
|
|
538
|
-
capture.onLimit(onLimit);
|
|
539
|
-
await capture.start();
|
|
540
|
-
FakeMediaRecorder.last.emitData(10);
|
|
541
|
-
vi.advanceTimersByTime(5000);
|
|
542
|
-
expect(onLimit).toHaveBeenCalledWith('time');
|
|
543
|
-
expect(track.stop).toHaveBeenCalled();
|
|
544
|
-
const recording = await capture.stop();
|
|
545
|
-
expect(recording.reachedTimeLimit).toBe(true);
|
|
546
|
-
expect(recording.audio.size).toBe(10);
|
|
547
|
-
});
|
|
548
|
-
it('fails as too long past the size limit', async () => {
|
|
549
|
-
stubMedia();
|
|
550
|
-
const capture = createMediaRecorderCapture({ maxBytes: 100 });
|
|
551
|
-
const onLimit = vi.fn();
|
|
552
|
-
capture.onLimit(onLimit);
|
|
553
|
-
await capture.start();
|
|
554
|
-
FakeMediaRecorder.last.emitData(80);
|
|
555
|
-
FakeMediaRecorder.last.emitData(80);
|
|
556
|
-
expect(onLimit).toHaveBeenCalledWith('size');
|
|
557
|
-
const error = await capture.stop().catch((e) => e);
|
|
558
|
-
expect(error.dictationKind).toBe('too-long');
|
|
559
|
-
});
|
|
560
|
-
it('stops waiting when the browser never fires the recorder stop event', async () => {
|
|
561
|
-
vi.useFakeTimers();
|
|
562
|
-
stubMedia();
|
|
563
|
-
const capture = createMediaRecorderCapture({});
|
|
564
|
-
await capture.start();
|
|
565
|
-
const recorder = FakeMediaRecorder.last;
|
|
566
|
-
recorder.stop = () => {
|
|
567
|
-
recorder.state = 'inactive';
|
|
568
|
-
};
|
|
569
|
-
recorder.emitData(42);
|
|
570
|
-
const pending = capture.stop();
|
|
571
|
-
await vi.advanceTimersByTimeAsync(3000);
|
|
572
|
-
const recording = await pending;
|
|
573
|
-
expect(recording.audio.size).toBe(42);
|
|
574
|
-
vi.useRealTimers();
|
|
575
|
-
});
|
|
576
|
-
it('reports a browser that cannot record', async () => {
|
|
577
|
-
vi.stubGlobal('MediaRecorder', undefined);
|
|
578
|
-
const capture = createMediaRecorderCapture({});
|
|
579
|
-
const error = await capture.start().catch((e) => e);
|
|
580
|
-
expect(error.dictationKind).toBe('unsupported');
|
|
581
|
-
});
|
|
582
|
-
});
|