@usero/sdk 1.1.5 → 1.1.7
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/dist/plugins/user-test.cjs +75 -11
- package/dist/plugins/user-test.cjs.map +1 -1
- package/dist/plugins/user-test.d.cts +52 -0
- package/dist/plugins/user-test.d.ts +52 -0
- package/dist/plugins/user-test.js +75 -11
- package/dist/plugins/user-test.js.map +1 -1
- package/dist/react.cjs +1 -1
- package/dist/react.cjs.map +1 -1
- package/dist/react.js +1 -1
- package/dist/react.js.map +1 -1
- package/dist/usero.iife.js +7 -7
- package/dist/usero.iife.js.map +1 -1
- package/dist/vanilla.cjs +1 -1
- package/dist/vanilla.cjs.map +1 -1
- package/dist/vanilla.js +1 -1
- package/dist/vanilla.js.map +1 -1
- package/package.json +1 -1
|
@@ -56,14 +56,66 @@ interface UserTestOptions {
|
|
|
56
56
|
testerName?: string;
|
|
57
57
|
hideIndicator?: boolean;
|
|
58
58
|
}
|
|
59
|
+
interface UserTestTask {
|
|
60
|
+
id: string;
|
|
61
|
+
prompt: string;
|
|
62
|
+
sortOrder: number;
|
|
63
|
+
}
|
|
64
|
+
interface MutedSegment {
|
|
65
|
+
startMs: number;
|
|
66
|
+
endMs: number;
|
|
67
|
+
}
|
|
68
|
+
interface InFlightNote {
|
|
69
|
+
atMs: number;
|
|
70
|
+
text: string;
|
|
71
|
+
acked: boolean;
|
|
72
|
+
serverId?: string;
|
|
73
|
+
}
|
|
74
|
+
interface RecorderStore {
|
|
75
|
+
cancelled: boolean;
|
|
76
|
+
slug: string;
|
|
77
|
+
sessionId: string | null;
|
|
78
|
+
clientId: string | null;
|
|
79
|
+
recorder: MediaRecorder | null;
|
|
80
|
+
stream: MediaStream | null;
|
|
81
|
+
chunkIndex: number;
|
|
82
|
+
uploadQueue: Promise<void>;
|
|
83
|
+
pendingUploads: number;
|
|
84
|
+
startedAt: number;
|
|
85
|
+
indicator: HTMLElement | null;
|
|
86
|
+
indicatorRoot: ShadowRoot | null;
|
|
87
|
+
indicatorState: 'recording' | 'finishing' | 'done' | 'no-audio' | 'error';
|
|
88
|
+
pageHideHandler: (() => void) | null;
|
|
89
|
+
options: Required<UserTestOptions>;
|
|
90
|
+
tasks: UserTestTask[];
|
|
91
|
+
tasksPanelOpen: boolean;
|
|
92
|
+
outsidePointerHandler: ((event: PointerEvent) => void) | null;
|
|
93
|
+
keydownHandler: ((event: KeyboardEvent) => void) | null;
|
|
94
|
+
hasMicPermission: boolean;
|
|
95
|
+
micAcquiring: boolean;
|
|
96
|
+
micFailReason: 'blocked' | 'not-found' | 'unsupported' | null;
|
|
97
|
+
muted: boolean;
|
|
98
|
+
mutedSinceMs: number | null;
|
|
99
|
+
mutedSegments: MutedSegment[];
|
|
100
|
+
muteToastShown: boolean;
|
|
101
|
+
muteToastTimers: number[];
|
|
102
|
+
notes: InFlightNote[];
|
|
103
|
+
notesPopoverOpen: boolean;
|
|
104
|
+
notePopoverAtMs: number | null;
|
|
105
|
+
endNote: string;
|
|
106
|
+
finishFlowRan: boolean;
|
|
107
|
+
replayOffsetAtStartMs: number | null;
|
|
108
|
+
}
|
|
59
109
|
declare function getTestSlug(queryParam: string): string | null;
|
|
60
110
|
declare function isMediaRecorderSupported(): boolean;
|
|
61
111
|
declare function pickMimeType(): string | undefined;
|
|
112
|
+
declare function micChipState(store: RecorderStore): 'recording' | 'muted' | 'none' | 'connecting' | 'inactive';
|
|
62
113
|
declare function userTest(options?: UserTestOptions): UseroPlugin;
|
|
63
114
|
declare const __test__: {
|
|
64
115
|
getTestSlug: typeof getTestSlug;
|
|
65
116
|
pickMimeType: typeof pickMimeType;
|
|
66
117
|
isMediaRecorderSupported: typeof isMediaRecorderSupported;
|
|
118
|
+
micChipState: typeof micChipState;
|
|
67
119
|
};
|
|
68
120
|
|
|
69
121
|
export { type UserTestOptions, __test__, userTest };
|
|
@@ -56,14 +56,66 @@ interface UserTestOptions {
|
|
|
56
56
|
testerName?: string;
|
|
57
57
|
hideIndicator?: boolean;
|
|
58
58
|
}
|
|
59
|
+
interface UserTestTask {
|
|
60
|
+
id: string;
|
|
61
|
+
prompt: string;
|
|
62
|
+
sortOrder: number;
|
|
63
|
+
}
|
|
64
|
+
interface MutedSegment {
|
|
65
|
+
startMs: number;
|
|
66
|
+
endMs: number;
|
|
67
|
+
}
|
|
68
|
+
interface InFlightNote {
|
|
69
|
+
atMs: number;
|
|
70
|
+
text: string;
|
|
71
|
+
acked: boolean;
|
|
72
|
+
serverId?: string;
|
|
73
|
+
}
|
|
74
|
+
interface RecorderStore {
|
|
75
|
+
cancelled: boolean;
|
|
76
|
+
slug: string;
|
|
77
|
+
sessionId: string | null;
|
|
78
|
+
clientId: string | null;
|
|
79
|
+
recorder: MediaRecorder | null;
|
|
80
|
+
stream: MediaStream | null;
|
|
81
|
+
chunkIndex: number;
|
|
82
|
+
uploadQueue: Promise<void>;
|
|
83
|
+
pendingUploads: number;
|
|
84
|
+
startedAt: number;
|
|
85
|
+
indicator: HTMLElement | null;
|
|
86
|
+
indicatorRoot: ShadowRoot | null;
|
|
87
|
+
indicatorState: 'recording' | 'finishing' | 'done' | 'no-audio' | 'error';
|
|
88
|
+
pageHideHandler: (() => void) | null;
|
|
89
|
+
options: Required<UserTestOptions>;
|
|
90
|
+
tasks: UserTestTask[];
|
|
91
|
+
tasksPanelOpen: boolean;
|
|
92
|
+
outsidePointerHandler: ((event: PointerEvent) => void) | null;
|
|
93
|
+
keydownHandler: ((event: KeyboardEvent) => void) | null;
|
|
94
|
+
hasMicPermission: boolean;
|
|
95
|
+
micAcquiring: boolean;
|
|
96
|
+
micFailReason: 'blocked' | 'not-found' | 'unsupported' | null;
|
|
97
|
+
muted: boolean;
|
|
98
|
+
mutedSinceMs: number | null;
|
|
99
|
+
mutedSegments: MutedSegment[];
|
|
100
|
+
muteToastShown: boolean;
|
|
101
|
+
muteToastTimers: number[];
|
|
102
|
+
notes: InFlightNote[];
|
|
103
|
+
notesPopoverOpen: boolean;
|
|
104
|
+
notePopoverAtMs: number | null;
|
|
105
|
+
endNote: string;
|
|
106
|
+
finishFlowRan: boolean;
|
|
107
|
+
replayOffsetAtStartMs: number | null;
|
|
108
|
+
}
|
|
59
109
|
declare function getTestSlug(queryParam: string): string | null;
|
|
60
110
|
declare function isMediaRecorderSupported(): boolean;
|
|
61
111
|
declare function pickMimeType(): string | undefined;
|
|
112
|
+
declare function micChipState(store: RecorderStore): 'recording' | 'muted' | 'none' | 'connecting' | 'inactive';
|
|
62
113
|
declare function userTest(options?: UserTestOptions): UseroPlugin;
|
|
63
114
|
declare const __test__: {
|
|
64
115
|
getTestSlug: typeof getTestSlug;
|
|
65
116
|
pickMimeType: typeof pickMimeType;
|
|
66
117
|
isMediaRecorderSupported: typeof isMediaRecorderSupported;
|
|
118
|
+
micChipState: typeof micChipState;
|
|
67
119
|
};
|
|
68
120
|
|
|
69
121
|
export { type UserTestOptions, __test__, userTest };
|
|
@@ -217,12 +217,37 @@ function buildIndicator(host, store, callbacks) {
|
|
|
217
217
|
color: #fcd34d;
|
|
218
218
|
}
|
|
219
219
|
.mic[data-mic-state="muted"]:hover { background: rgba(251, 191, 36, 0.26); }
|
|
220
|
-
.
|
|
221
|
-
|
|
222
|
-
|
|
220
|
+
/* Connecting: getUserMedia pending. Steady amber tint reads as "working",
|
|
221
|
+
distinct from the live red pulse and the failed state below. The icon
|
|
222
|
+
gets a gentle non-pulsing breathe so it feels alive without alarming. */
|
|
223
|
+
.mic[data-mic-state="connecting"] {
|
|
224
|
+
background: rgba(251, 191, 36, 0.14);
|
|
225
|
+
border-color: rgba(251, 191, 36, 0.32);
|
|
226
|
+
color: #fcd34d;
|
|
223
227
|
cursor: default;
|
|
224
228
|
}
|
|
225
|
-
.mic[data-mic-state="
|
|
229
|
+
.mic[data-mic-state="connecting"]:hover { background: rgba(251, 191, 36, 0.14); }
|
|
230
|
+
.mic[data-mic-state="connecting"] .mic-icon {
|
|
231
|
+
color: #fbbf24;
|
|
232
|
+
animation: micBreathe 1.4s ease-in-out infinite;
|
|
233
|
+
}
|
|
234
|
+
/* Failed terminal state, actionable. Tappable affordance: clearer border,
|
|
235
|
+
pointer cursor, brightens on hover/focus to invite the retry tap. */
|
|
236
|
+
.mic[data-mic-state="none"] {
|
|
237
|
+
background: rgba(255,255,255,0.05);
|
|
238
|
+
border-color: rgba(255,255,255,0.14);
|
|
239
|
+
color: rgba(255,255,255,0.72);
|
|
240
|
+
cursor: pointer;
|
|
241
|
+
}
|
|
242
|
+
.mic[data-mic-state="none"]:hover {
|
|
243
|
+
background: rgba(255,255,255,0.12);
|
|
244
|
+
border-color: rgba(255,255,255,0.24);
|
|
245
|
+
color: #fff;
|
|
246
|
+
}
|
|
247
|
+
@keyframes micBreathe {
|
|
248
|
+
0%, 100% { opacity: 0.55; }
|
|
249
|
+
50% { opacity: 1; }
|
|
250
|
+
}
|
|
226
251
|
.mic-icon { width: 13px; height: 13px; display: inline-block; flex-shrink: 0; }
|
|
227
252
|
.mic-label { font-weight: 500; letter-spacing: 0.01em; white-space: nowrap; }
|
|
228
253
|
|
|
@@ -691,7 +716,10 @@ function micChipState(store) {
|
|
|
691
716
|
if (store.indicatorState === "finishing" || store.indicatorState === "done" || store.indicatorState === "error") {
|
|
692
717
|
return "inactive";
|
|
693
718
|
}
|
|
694
|
-
if (!store.hasMicPermission)
|
|
719
|
+
if (!store.hasMicPermission) {
|
|
720
|
+
if (store.micAcquiring) return "connecting";
|
|
721
|
+
return "none";
|
|
722
|
+
}
|
|
695
723
|
return store.muted ? "muted" : "recording";
|
|
696
724
|
}
|
|
697
725
|
function renderIndicatorState(store) {
|
|
@@ -706,6 +734,8 @@ function renderIndicatorState(store) {
|
|
|
706
734
|
dot.setAttribute("data-state", store.indicatorState);
|
|
707
735
|
const chipState = micChipState(store);
|
|
708
736
|
mic.setAttribute("data-mic-state", chipState === "inactive" ? "none" : chipState);
|
|
737
|
+
mic.removeAttribute("data-mic-fail");
|
|
738
|
+
if (chipState === "none") mic.setAttribute("data-mic-fail", store.micFailReason ?? "blocked");
|
|
709
739
|
switch (store.indicatorState) {
|
|
710
740
|
case "recording":
|
|
711
741
|
case "no-audio":
|
|
@@ -740,13 +770,23 @@ function renderIndicatorState(store) {
|
|
|
740
770
|
mic.setAttribute("aria-pressed", "true");
|
|
741
771
|
mic.removeAttribute("tabindex");
|
|
742
772
|
break;
|
|
743
|
-
case "
|
|
744
|
-
micIcon.innerHTML =
|
|
745
|
-
micLabel.textContent = "
|
|
746
|
-
mic.setAttribute("aria-label", "
|
|
773
|
+
case "connecting":
|
|
774
|
+
micIcon.innerHTML = MIC_ICON_SVG;
|
|
775
|
+
micLabel.textContent = "Connecting mic";
|
|
776
|
+
mic.setAttribute("aria-label", "Connecting microphone");
|
|
747
777
|
mic.setAttribute("aria-pressed", "false");
|
|
748
778
|
mic.setAttribute("tabindex", "-1");
|
|
749
779
|
break;
|
|
780
|
+
case "none": {
|
|
781
|
+
micIcon.innerHTML = MIC_MUTED_ICON_SVG;
|
|
782
|
+
const failLabel = store.micFailReason === "not-found" ? "No mic found, tap to retry" : "Mic blocked, tap to retry";
|
|
783
|
+
const failAria = store.micFailReason === "not-found" ? "No microphone found, tap to retry. Replay continues." : "Microphone blocked, tap to retry. Replay continues.";
|
|
784
|
+
micLabel.textContent = failLabel;
|
|
785
|
+
mic.setAttribute("aria-label", failAria);
|
|
786
|
+
mic.setAttribute("aria-pressed", "false");
|
|
787
|
+
mic.removeAttribute("tabindex");
|
|
788
|
+
break;
|
|
789
|
+
}
|
|
750
790
|
case "inactive":
|
|
751
791
|
micIcon.innerHTML = MIC_ICON_SVG;
|
|
752
792
|
micLabel.textContent = store.indicatorState === "finishing" ? "Saving" : store.indicatorState === "done" ? "Saved" : "Save failed";
|
|
@@ -1325,8 +1365,13 @@ function enqueueChunk(store, ctx, blob) {
|
|
|
1325
1365
|
});
|
|
1326
1366
|
}
|
|
1327
1367
|
async function startRecording(store, ctx) {
|
|
1368
|
+
store.micAcquiring = true;
|
|
1369
|
+
store.micFailReason = null;
|
|
1370
|
+
renderIndicatorState(store);
|
|
1328
1371
|
if (!isMediaRecorderSupported()) {
|
|
1329
1372
|
ctx.logger.warn("MediaRecorder not supported, continuing without audio");
|
|
1373
|
+
store.micAcquiring = false;
|
|
1374
|
+
store.micFailReason = "unsupported";
|
|
1330
1375
|
store.indicatorState = "no-audio";
|
|
1331
1376
|
renderIndicatorState(store);
|
|
1332
1377
|
return;
|
|
@@ -1336,12 +1381,17 @@ async function startRecording(store, ctx) {
|
|
|
1336
1381
|
stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
|
1337
1382
|
} catch (err) {
|
|
1338
1383
|
ctx.logger.warn("mic permission denied or unavailable", err);
|
|
1384
|
+
store.micAcquiring = false;
|
|
1385
|
+
const name = err instanceof Error ? err.name : "";
|
|
1386
|
+
store.micFailReason = name === "NotFoundError" || name === "DevicesNotFoundError" ? "not-found" : "blocked";
|
|
1339
1387
|
store.indicatorState = "no-audio";
|
|
1340
1388
|
renderIndicatorState(store);
|
|
1341
1389
|
return;
|
|
1342
1390
|
}
|
|
1343
1391
|
store.stream = stream;
|
|
1344
1392
|
store.hasMicPermission = true;
|
|
1393
|
+
store.micAcquiring = false;
|
|
1394
|
+
store.micFailReason = null;
|
|
1345
1395
|
const mimeType = pickMimeType();
|
|
1346
1396
|
let recorder;
|
|
1347
1397
|
try {
|
|
@@ -1350,6 +1400,9 @@ async function startRecording(store, ctx) {
|
|
|
1350
1400
|
ctx.logger.error("MediaRecorder construction failed", err);
|
|
1351
1401
|
stream.getTracks().forEach((t) => t.stop());
|
|
1352
1402
|
store.stream = null;
|
|
1403
|
+
store.hasMicPermission = false;
|
|
1404
|
+
store.micAcquiring = false;
|
|
1405
|
+
store.micFailReason = "unsupported";
|
|
1353
1406
|
store.indicatorState = "no-audio";
|
|
1354
1407
|
renderIndicatorState(store);
|
|
1355
1408
|
return;
|
|
@@ -1364,6 +1417,10 @@ async function startRecording(store, ctx) {
|
|
|
1364
1417
|
ctx.logger.error("MediaRecorder error", event);
|
|
1365
1418
|
});
|
|
1366
1419
|
recorder.start(store.options.chunkSeconds * 1e3);
|
|
1420
|
+
if (store.indicatorState === "no-audio") {
|
|
1421
|
+
store.indicatorState = "recording";
|
|
1422
|
+
}
|
|
1423
|
+
renderIndicatorState(store);
|
|
1367
1424
|
}
|
|
1368
1425
|
function toggleMute(store) {
|
|
1369
1426
|
if (!store.stream || !store.hasMicPermission) return false;
|
|
@@ -1519,6 +1576,8 @@ function userTest(options = {}) {
|
|
|
1519
1576
|
outsidePointerHandler: null,
|
|
1520
1577
|
keydownHandler: null,
|
|
1521
1578
|
hasMicPermission: false,
|
|
1579
|
+
micAcquiring: true,
|
|
1580
|
+
micFailReason: null,
|
|
1522
1581
|
muted: false,
|
|
1523
1582
|
mutedSinceMs: null,
|
|
1524
1583
|
mutedSegments: [],
|
|
@@ -1543,7 +1602,12 @@ function userTest(options = {}) {
|
|
|
1543
1602
|
};
|
|
1544
1603
|
const onToggleTasks = () => setPanelOpen(!store.tasksPanelOpen);
|
|
1545
1604
|
const onToggleMute = () => {
|
|
1546
|
-
if (!store.hasMicPermission)
|
|
1605
|
+
if (!store.hasMicPermission) {
|
|
1606
|
+
if (!store.micAcquiring && store.indicatorState !== "finishing" && store.indicatorState !== "done" && store.indicatorState !== "error") {
|
|
1607
|
+
void startRecording(store, ctx);
|
|
1608
|
+
}
|
|
1609
|
+
return;
|
|
1610
|
+
}
|
|
1547
1611
|
const ok = toggleMute(store);
|
|
1548
1612
|
if (!ok) return;
|
|
1549
1613
|
if (store.muted) showMuteToast(store);
|
|
@@ -1672,7 +1736,7 @@ function userTest(options = {}) {
|
|
|
1672
1736
|
}
|
|
1673
1737
|
};
|
|
1674
1738
|
}
|
|
1675
|
-
var __test__ = { getTestSlug, pickMimeType, isMediaRecorderSupported };
|
|
1739
|
+
var __test__ = { getTestSlug, pickMimeType, isMediaRecorderSupported, micChipState };
|
|
1676
1740
|
|
|
1677
1741
|
export { __test__, userTest };
|
|
1678
1742
|
//# sourceMappingURL=user-test.js.map
|