test(teams-meetings): consolidate transport fixtures (#118090)

This commit is contained in:
Peter Steinberger
2026-08-02 11:10:01 -07:00
committed by GitHub
parent c887279d0f
commit 21b07093a0
4 changed files with 270 additions and 676 deletions
@@ -1,45 +1,30 @@
import { describe, expect, it } from "vitest";
import {
MEETING_STATE_KEY,
abortingMedia,
audioStatusParams,
continueStatusScript,
control,
liveMediaStream,
pageMedia,
runAudioStatusScript,
runStatusScript,
type PageMedia,
} from "./teams-meetings-platform-adapter.test-helpers.js";
describe("Microsoft Teams meeting audio routing", () => {
it("does not let one routed element hide a failed live remote stream", async () => {
const routed: PageMedia = {
sinkId: "",
async setSinkId(value) {
routed.sinkId = value;
},
};
const live: PageMedia = {
const routed = pageMedia();
const live = abortingMedia("The element has no supported source.", {
sinkId: "built-in-output",
srcObject: { getAudioTracks: () => [{ readyState: "live" }] },
async setSinkId() {
throw new DOMException("The element has no supported source.", "AbortError");
},
};
const bridge: PageMedia = {
srcObject: liveMediaStream(),
});
const bridge = abortingMedia("Cannot route bridge.", {
isConnected: false,
sinkId: "",
async play() {},
async setSinkId() {
throw new DOMException("Cannot route bridge.", "AbortError");
},
};
const { result } = await runStatusScript({
allowMicrophone: true,
});
const { result } = await runAudioStatusScript({
bridgeMedia: bridge,
devices: [
{ deviceId: "blackhole-input", kind: "audioinput", label: "BlackHole 2ch" },
{ deviceId: "blackhole-output", kind: "audiooutput", label: "BlackHole 2ch" },
],
leave: control({ label: "Leave" }),
media: [routed, live],
microphone: control({ label: "Turn microphone off", pressed: true }),
microphoneDevice: control({ label: "BlackHole 2ch" }),
priorMeeting: {
audioInputDeviceId: "blackhole-input",
identity: "teams-work:19:meeting_test@thread.v2",
@@ -55,31 +40,15 @@ describe("Microsoft Teams meeting audio routing", () => {
});
it("does not let one routed element hide an unloaded pending remote stream", async () => {
const routed: PageMedia = {
sinkId: "",
async setSinkId(value) {
routed.sinkId = value;
},
};
const pending: PageMedia = {
const routed = pageMedia();
const pending = abortingMedia("The element has no supported source.", {
muted: false,
readyState: 0,
sinkId: "built-in-output",
src: "blob:https://teams.live.com/pending-remote-audio",
async setSinkId() {
throw new DOMException("The element has no supported source.", "AbortError");
},
};
const { result, window } = await runStatusScript({
allowMicrophone: true,
devices: [
{ deviceId: "blackhole-input", kind: "audioinput", label: "BlackHole 2ch" },
{ deviceId: "blackhole-output", kind: "audiooutput", label: "BlackHole 2ch" },
],
leave: control({ label: "Leave" }),
});
const { result, window } = await runAudioStatusScript({
media: [routed, pending],
microphone: control({ label: "Turn microphone off", pressed: true }),
microphoneDevice: control({ label: "BlackHole 2ch" }),
priorMeeting: {
audioInputDeviceId: "blackhole-input",
identity: "teams-work:19:meeting_test@thread.v2",
@@ -98,47 +67,29 @@ describe("Microsoft Teams meeting audio routing", () => {
});
it("retries bridge playback instead of trusting a previously selected sink", async () => {
const source: PageMedia = {
const source = abortingMedia("The element has no supported source.", {
muted: false,
sinkId: "built-in-output",
srcObject: { getAudioTracks: () => [{ readyState: "live" }] },
async setSinkId() {
throw new DOMException("The element has no supported source.", "AbortError");
},
};
srcObject: liveMediaStream(),
});
let playAttempts = 0;
const bridge: PageMedia = {
const bridge = pageMedia({
isConnected: false,
sinkId: "",
async play() {
playAttempts += 1;
throw new DOMException("Playback blocked.", "NotAllowedError");
},
async setSinkId(value) {
bridge.sinkId = value;
},
};
const params = {
allowMicrophone: true,
});
const params = audioStatusParams({
bridgeMedia: bridge,
devices: [
{ deviceId: "blackhole-input", kind: "audioinput", label: "BlackHole 2ch" },
{ deviceId: "blackhole-output", kind: "audiooutput", label: "BlackHole 2ch" },
],
leave: control({ label: "Leave" }),
media: [source],
microphone: control({ label: "Turn microphone off", pressed: true }),
priorMeeting: {
audioInputDeviceId: "blackhole-input",
identity: "teams-work:19:meeting_test@thread.v2",
},
};
const first = await runStatusScript(params);
const second = await runStatusScript({
...params,
priorAudioOutputs: first.window["__openclawTeamsAudioOutputs"] as unknown[],
priorMeeting: first.window[MEETING_STATE_KEY] as Record<string, unknown>,
});
const first = await runStatusScript(params);
const second = await continueStatusScript(first, params);
expect(first.result.audioOutputRouted).toBe(false);
expect(first.result.audioOutputRouteRetryable).toBe(false);
@@ -158,18 +109,9 @@ describe("Microsoft Teams meeting audio routing", () => {
source.sinkId = value;
},
};
const params = {
allowMicrophone: true,
devices: [
{ deviceId: "blackhole-input", kind: "audioinput", label: "BlackHole 2ch" },
{ deviceId: "blackhole-output", kind: "audiooutput", label: "BlackHole 2ch" },
],
leave: control({ label: "Leave" }),
const params = audioStatusParams({
media: [source],
microphone: control({ label: "Turn microphone off", pressed: true }),
microphoneDevice: control({ label: "BlackHole 2ch" }),
priorMeeting: { identity: "teams-work:19:meeting_test@thread.v2" },
};
});
const first = await runStatusScript(params);
expect(first.result.audioOutputRouted).toBe(false);
expect(first.result.audioOutputRouteError).toBeUndefined();
@@ -179,7 +121,7 @@ describe("Microsoft Teams meeting audio routing", () => {
expect.objectContaining({ pending: true, source, sourceMuted: false }),
]);
const stream = { getAudioTracks: () => [{ readyState: "live" }] };
const stream = liveMediaStream();
source.srcObject = stream;
expect(source.sinkId).toBe("built-in-output");
expect(source.muted).toBe(true);
@@ -187,11 +129,7 @@ describe("Microsoft Teams meeting audio routing", () => {
expect.objectContaining({ pending: true, source, sourceMuted: false }),
]);
const second = await runStatusScript({
...params,
priorAudioOutputs: first.window["__openclawTeamsAudioOutputs"] as unknown[],
priorMeeting: first.window[MEETING_STATE_KEY] as Record<string, unknown>,
});
const second = await continueStatusScript(first, params);
expect(second.result.audioOutputRouted).toBe(true);
expect(source.muted).toBe(false);
expect(second.window).not.toHaveProperty("__openclawTeamsAudioOutputs");
@@ -208,26 +146,15 @@ describe("Microsoft Teams meeting audio routing", () => {
source.sinkId = value;
},
};
const params = {
allowMicrophone: true,
devices: [
{ deviceId: "blackhole-input", kind: "audioinput", label: "BlackHole 2ch" },
{ deviceId: "blackhole-output", kind: "audiooutput", label: "BlackHole 2ch" },
],
leave: control({ label: "Leave" }),
const params = audioStatusParams({
media: [source],
microphone: control({ label: "Turn microphone off", pressed: true }),
microphoneDevice: control({ label: "BlackHole 2ch" }),
priorMeeting: { identity: "teams-work:19:meeting_test@thread.v2" },
};
});
const first = await runStatusScript(params);
source.srcObject = { getAudioTracks: () => [{ readyState: "live" }] };
const conflict = await runStatusScript({
source.srcObject = liveMediaStream();
const conflict = await continueStatusScript(first, {
...params,
currentUrl: "https://teams.microsoft.com/l/meetup-join/19%3ameeting_other%40thread.v2/0",
priorAudioOutputs: first.window["__openclawTeamsAudioOutputs"] as unknown[],
priorMeeting: first.window[MEETING_STATE_KEY] as Record<string, unknown>,
});
expect(source.sinkId).toBe("built-in-output");
@@ -237,104 +164,59 @@ describe("Microsoft Teams meeting audio routing", () => {
});
it("keeps the source muted when a previously working bridge fails", async () => {
const source: PageMedia = {
const source = abortingMedia("The element has no supported source.", {
muted: false,
sinkId: "built-in-output",
srcObject: { getAudioTracks: () => [{ readyState: "live" }] },
async setSinkId() {
throw new DOMException("The element has no supported source.", "AbortError");
},
};
srcObject: liveMediaStream(),
});
let failPlayback = false;
const bridge: PageMedia = {
const bridge = pageMedia({
isConnected: false,
sinkId: "",
async play() {
if (failPlayback) {
throw new DOMException("Playback failed.", "AbortError");
}
},
async setSinkId(value) {
bridge.sinkId = value;
},
};
const params = {
allowMicrophone: true,
});
const params = audioStatusParams({
bridgeMedia: bridge,
devices: [
{ deviceId: "blackhole-input", kind: "audioinput", label: "BlackHole 2ch" },
{ deviceId: "blackhole-output", kind: "audiooutput", label: "BlackHole 2ch" },
],
leave: control({ label: "Leave" }),
media: [source],
microphone: control({ label: "Turn microphone off", pressed: true }),
microphoneDevice: control({ label: "BlackHole 2ch" }),
priorMeeting: { identity: "teams-work:19:meeting_test@thread.v2" },
};
});
const first = await runStatusScript(params);
expect(first.result.audioOutputRouted).toBe(true);
expect(source.muted).toBe(true);
failPlayback = true;
const second = await runStatusScript({
...params,
priorAudioOutputs: first.window["__openclawTeamsAudioOutputs"] as unknown[],
priorMeeting: first.window[MEETING_STATE_KEY] as Record<string, unknown>,
});
const second = await continueStatusScript(first, params);
expect(second.result.audioOutputRouted).toBe(false);
expect(second.result.audioOutputRouteRetryable).toBe(true);
expect(source.muted).toBe(true);
failPlayback = false;
const third = await runStatusScript({
...params,
priorAudioOutputs: second.window["__openclawTeamsAudioOutputs"] as unknown[],
priorMeeting: second.window[MEETING_STATE_KEY] as Record<string, unknown>,
});
const third = await continueStatusScript(second, params);
expect(third.result.audioOutputRouted).toBe(true);
expect(source.muted).toBe(true);
});
it("keeps a per-source bridge when another element sharing its stream routes directly", async () => {
const stream = { getAudioTracks: () => [{ readyState: "live" }] };
const bridgedSource: PageMedia = {
const stream = liveMediaStream();
const bridgedSource = abortingMedia("The element has no supported source.", {
muted: false,
sinkId: "built-in-output",
srcObject: stream,
async setSinkId() {
throw new DOMException("The element has no supported source.", "AbortError");
},
};
const directSource: PageMedia = {
});
const directSource = pageMedia({
muted: false,
sinkId: "built-in-output",
srcObject: stream,
async setSinkId(value) {
directSource.sinkId = value;
},
};
const bridge: PageMedia = {
});
const bridge = pageMedia({
isConnected: false,
sinkId: "",
async play() {},
async setSinkId(value) {
bridge.sinkId = value;
},
};
const { result, window } = await runStatusScript({
allowMicrophone: true,
});
const { result, window } = await runAudioStatusScript({
bridgeMedia: bridge,
devices: [
{ deviceId: "blackhole-input", kind: "audioinput", label: "BlackHole 2ch" },
{ deviceId: "blackhole-output", kind: "audiooutput", label: "BlackHole 2ch" },
],
leave: control({ label: "Leave" }),
media: [bridgedSource, directSource],
microphone: control({ label: "Turn microphone off", pressed: true }),
microphoneDevice: control({ label: "BlackHole 2ch" }),
priorMeeting: {
identity: "teams-work:19:meeting_test@thread.v2",
},
});
expect(result.audioOutputRouted).toBe(true);
@@ -347,31 +229,20 @@ describe("Microsoft Teams meeting audio routing", () => {
});
it("restores an unclaimed source when another source from its entry is rerouted", async () => {
const current: PageMedia = {
const current = pageMedia({
currentSrc: "blob:https://teams.live.com/current",
muted: true,
sinkId: "built-in-output",
async setSinkId(value) {
current.sinkId = value;
},
};
const unclaimed: PageMedia = {
});
const unclaimed = pageMedia({
currentSrc: "blob:https://teams.live.com/unclaimed",
muted: true,
sinkId: "built-in-output",
async setSinkId() {},
};
});
const { result, window } = await runStatusScript({
allowMicrophone: true,
devices: [
{ deviceId: "blackhole-input", kind: "audioinput", label: "BlackHole 2ch" },
{ deviceId: "blackhole-output", kind: "audiooutput", label: "BlackHole 2ch" },
],
leave: control({ label: "Leave" }),
const { result, window } = await runAudioStatusScript({
media: [current],
microphone: control({ label: "Turn microphone off", pressed: true }),
microphoneDevice: control({ label: "BlackHole 2ch" }),
priorAudioOutputs: [
{
sessionId: "session-1",
@@ -381,7 +252,6 @@ describe("Microsoft Teams meeting audio routing", () => {
],
},
],
priorMeeting: { identity: "teams-work:19:meeting_test@thread.v2" },
});
expect(result.audioOutputRouted).toBe(true);
@@ -391,57 +261,32 @@ describe("Microsoft Teams meeting audio routing", () => {
});
it("reroutes a replacement stream on an element muted by its prior bridge", async () => {
const firstStream = { getAudioTracks: () => [{ readyState: "live" }] };
const replacementStream = { getAudioTracks: () => [{ readyState: "live" }] };
const source: PageMedia = {
const firstStream = liveMediaStream();
const replacementStream = liveMediaStream();
const source = abortingMedia("The element has no supported source.", {
muted: false,
sinkId: "built-in-output",
srcObject: firstStream,
async setSinkId() {
throw new DOMException("The element has no supported source.", "AbortError");
},
};
const directSource: PageMedia = {
});
const directSource = pageMedia({
muted: false,
sinkId: "built-in-output",
srcObject: firstStream,
async setSinkId(value) {
directSource.sinkId = value;
},
};
const bridge: PageMedia = {
});
const bridge = pageMedia({
isConnected: false,
sinkId: "",
async play() {},
async setSinkId(value) {
bridge.sinkId = value;
},
};
const params = {
allowMicrophone: true,
});
const params = audioStatusParams({
bridgeMedia: bridge,
devices: [
{ deviceId: "blackhole-input", kind: "audioinput", label: "BlackHole 2ch" },
{ deviceId: "blackhole-output", kind: "audiooutput", label: "BlackHole 2ch" },
],
leave: control({ label: "Leave" }),
media: [source, directSource],
microphone: control({ label: "Turn microphone off", pressed: true }),
microphoneDevice: control({ label: "BlackHole 2ch" }),
priorMeeting: {
identity: "teams-work:19:meeting_test@thread.v2",
},
};
});
const first = await runStatusScript(params);
expect(first.result.audioOutputRouted).toBe(true);
expect(source.muted).toBe(true);
source.srcObject = undefined;
const second = await runStatusScript({
...params,
priorAudioOutputs: first.window["__openclawTeamsAudioOutputs"] as unknown[],
priorMeeting: first.window[MEETING_STATE_KEY] as Record<string, unknown>,
});
const second = await continueStatusScript(first, params);
expect(second.result.audioOutputRouted).toBe(false);
expect(source.muted).toBe(true);
expect(second.window["__openclawTeamsAudioOutputs"]).toEqual([
@@ -449,11 +294,7 @@ describe("Microsoft Teams meeting audio routing", () => {
]);
source.srcObject = replacementStream;
const third = await runStatusScript({
...params,
priorAudioOutputs: second.window["__openclawTeamsAudioOutputs"] as unknown[],
priorMeeting: second.window[MEETING_STATE_KEY] as Record<string, unknown>,
});
const third = await continueStatusScript(second, params);
expect(third.result.audioOutputRouted).toBe(true);
expect(source.muted).toBe(true);
expect(
@@ -462,51 +303,30 @@ describe("Microsoft Teams meeting audio routing", () => {
});
it("keeps a detached source muted after its owned stream is cleared", async () => {
const stream = { getAudioTracks: () => [{ readyState: "live" }] };
const source: PageMedia = {
const stream = liveMediaStream();
const source = abortingMedia("The element has no supported source.", {
muted: false,
sinkId: "built-in-output",
srcObject: stream,
async setSinkId() {
throw new DOMException("The element has no supported source.", "AbortError");
},
};
const directSource: PageMedia = {
});
const directSource = pageMedia({
muted: false,
sinkId: "built-in-output",
srcObject: stream,
async setSinkId(value) {
directSource.sinkId = value;
},
};
const bridge: PageMedia = {
});
const bridge = pageMedia({
isConnected: false,
sinkId: "",
async play() {},
async setSinkId(value) {
bridge.sinkId = value;
},
};
const params = {
allowMicrophone: true,
});
const params = audioStatusParams({
bridgeMedia: bridge,
devices: [
{ deviceId: "blackhole-input", kind: "audioinput", label: "BlackHole 2ch" },
{ deviceId: "blackhole-output", kind: "audiooutput", label: "BlackHole 2ch" },
],
leave: control({ label: "Leave" }),
microphone: control({ label: "Turn microphone off", pressed: true }),
microphoneDevice: control({ label: "BlackHole 2ch" }),
priorMeeting: { identity: "teams-work:19:meeting_test@thread.v2" },
};
});
const first = await runStatusScript({ ...params, media: [source] });
expect(source.muted).toBe(true);
const second = await runStatusScript({
const second = await continueStatusScript(first, {
...params,
media: [directSource],
priorAudioOutputs: first.window["__openclawTeamsAudioOutputs"] as unknown[],
priorMeeting: first.window[MEETING_STATE_KEY] as Record<string, unknown>,
});
expect(second.result.audioOutputRouted).toBe(true);
expect(source.muted).toBe(true);
@@ -515,11 +335,9 @@ describe("Microsoft Teams meeting audio routing", () => {
]);
source.srcObject = undefined;
const third = await runStatusScript({
const third = await continueStatusScript(second, {
...params,
media: [directSource],
priorAudioOutputs: second.window["__openclawTeamsAudioOutputs"] as unknown[],
priorMeeting: second.window[MEETING_STATE_KEY] as Record<string, unknown>,
});
expect(source.muted).toBe(true);
expect(third.window).not.toHaveProperty("__openclawTeamsAudioOutputs");
@@ -529,43 +347,26 @@ describe("Microsoft Teams meeting audio routing", () => {
const source: PageMedia = {
muted: false,
sinkId: "built-in-output",
srcObject: { getAudioTracks: () => [{ readyState: "live" }] },
srcObject: liveMediaStream(),
async setSinkId() {
throw new DOMException("The element has no supported source.", "AbortError");
},
};
let pauses = 0;
let removals = 0;
const bridge: PageMedia = {
const bridge = pageMedia({
isConnected: false,
sinkId: "",
async play() {},
pause: () => (pauses += 1),
remove: () => (removals += 1),
async setSinkId(value) {
bridge.sinkId = value;
},
};
const first = await runStatusScript({
allowMicrophone: true,
});
const first = await runAudioStatusScript({
bridgeMedia: bridge,
devices: [
{ deviceId: "blackhole-input", kind: "audioinput", label: "BlackHole 2ch" },
{ deviceId: "blackhole-output", kind: "audiooutput", label: "BlackHole 2ch" },
],
leave: control({ label: "Leave" }),
media: [source],
microphone: control({ label: "Turn microphone off", pressed: true }),
microphoneDevice: control({ label: "BlackHole 2ch" }),
priorMeeting: { identity: "teams-work:19:meeting_test@thread.v2" },
});
expect(source.muted).toBe(true);
const ended = await runStatusScript({
allowMicrophone: true,
priorAudioOutputs: first.window["__openclawTeamsAudioOutputs"] as unknown[],
priorMeeting: first.window[MEETING_STATE_KEY] as Record<string, unknown>,
});
const ended = await continueStatusScript(first, { allowMicrophone: true });
expect(source.muted).toBe(false);
expect(pauses).toBe(1);
expect(removals).toBe(1);
@@ -573,8 +374,8 @@ describe("Microsoft Teams meeting audio routing", () => {
});
it("does not restore a reused media element carrying a replacement stream", async () => {
const bridgedStream = { getAudioTracks: () => [{ readyState: "live" }] };
const replacementStream = { getAudioTracks: () => [{ readyState: "live" }] };
const bridgedStream = liveMediaStream();
const replacementStream = liveMediaStream();
const source: PageMedia = {
muted: true,
sinkId: "built-in-output",
@@ -693,7 +494,7 @@ describe("Microsoft Teams meeting audio routing", () => {
});
it("suspends bridge ownership while an in-call media list is temporarily empty", async () => {
const stream = { getAudioTracks: () => [{ readyState: "live" }] };
const stream = liveMediaStream();
const source: PageMedia = {
muted: true,
sinkId: "built-in-output",
@@ -741,7 +542,7 @@ describe("Microsoft Teams meeting audio routing", () => {
const source: PageMedia = {
muted: true,
sinkId: "built-in-output",
srcObject: { getAudioTracks: () => [{ readyState: "live" }] },
srcObject: liveMediaStream(),
async setSinkId() {},
};
let pauses = 0;
@@ -753,13 +554,9 @@ describe("Microsoft Teams meeting audio routing", () => {
remove: () => (removals += 1),
async setSinkId() {},
};
const { result, window } = await runStatusScript({
allowMicrophone: true,
const { result, window } = await runAudioStatusScript({
devices: [{ deviceId: "blackhole-input", kind: "audioinput", label: "BlackHole 2ch" }],
leave: control({ label: "Leave" }),
media: [source],
microphone: control({ label: "Turn microphone off", pressed: true }),
microphoneDevice: control({ label: "BlackHole 2ch" }),
priorAudioOutputs: [
{
bridge,
@@ -770,7 +567,6 @@ describe("Microsoft Teams meeting audio routing", () => {
stream: source.srcObject,
},
],
priorMeeting: { identity: "teams-work:19:meeting_test@thread.v2" },
});
expect(source.muted).toBe(true);
@@ -786,7 +582,7 @@ describe("Microsoft Teams meeting audio routing", () => {
const source: PageMedia = {
muted: true,
sinkId: "built-in-output",
srcObject: { getAudioTracks: () => [{ readyState: "live" }] },
srcObject: liveMediaStream(),
async setSinkId() {},
};
let pauses = 0;
@@ -806,15 +602,10 @@ describe("Microsoft Teams meeting audio routing", () => {
sourceMuted: false,
stream: source.srcObject,
};
const { window } = await runStatusScript({
allowMicrophone: true,
const { window } = await runAudioStatusScript({
devices: [{ deviceId: "blackhole-input", kind: "audioinput", label: "BlackHole 2ch" }],
leave: control({ label: "Leave" }),
media: [source],
microphone: control({ label: "Turn microphone off", pressed: true }),
microphoneDevice: control({ label: "BlackHole 2ch" }),
priorAudioOutputs: [activeBridge],
priorMeeting: { identity: "teams-work:19:meeting_test@thread.v2" },
readOnly: true,
});
@@ -825,7 +616,7 @@ describe("Microsoft Teams meeting audio routing", () => {
});
it("reassigns a prior session bridge source after the new session identity is verified", async () => {
const stream = { getAudioTracks: () => [{ readyState: "live" }] };
const stream = liveMediaStream();
const source: PageMedia = { muted: true, sinkId: "", srcObject: stream, async setSinkId() {} };
const emptySource: PageMedia = { muted: true, sinkId: "", async setSinkId() {} };
let pauses = 0;
@@ -837,7 +628,6 @@ describe("Microsoft Teams meeting audio routing", () => {
async setSinkId() {},
};
const { window } = await runStatusScript({
allowMicrophone: false,
leave: control({ label: "Leave" }),
priorAudioOutputs: [
{
@@ -879,13 +669,7 @@ describe("Microsoft Teams meeting audio routing", () => {
});
it("ignores an unrelated media element when the live remote stream is routed", async () => {
const live: PageMedia = {
sinkId: "",
srcObject: { getAudioTracks: () => [{ readyState: "live" }] },
async setSinkId(value) {
live.sinkId = value;
},
};
const live = pageMedia({ srcObject: liveMediaStream() });
const unrelated: PageMedia = {
muted: true,
sinkId: "built-in-output",
@@ -893,16 +677,8 @@ describe("Microsoft Teams meeting audio routing", () => {
throw new DOMException("The element has no supported source.", "AbortError");
},
};
const { result } = await runStatusScript({
allowMicrophone: true,
devices: [
{ deviceId: "blackhole-input", kind: "audioinput", label: "BlackHole 2ch" },
{ deviceId: "blackhole-output", kind: "audiooutput", label: "BlackHole 2ch" },
],
leave: control({ label: "Leave" }),
const { result } = await runAudioStatusScript({
media: [live, unrelated],
microphone: control({ label: "Turn microphone off", pressed: true }),
microphoneDevice: control({ label: "BlackHole 2ch" }),
priorMeeting: {
audioInputDeviceId: "blackhole-input",
identity: "teams-work:19:meeting_test@thread.v2",
@@ -917,12 +693,7 @@ describe("Microsoft Teams meeting audio routing", () => {
});
it("keeps a loaded non-MediaStream AbortError retryable", async () => {
const routed: PageMedia = {
sinkId: "",
async setSinkId(value) {
routed.sinkId = value;
},
};
const routed = pageMedia();
const loaded: PageMedia = {
currentSrc: "blob:https://teams.live.com/remote-audio",
readyState: 4,
@@ -931,17 +702,8 @@ describe("Microsoft Teams meeting audio routing", () => {
throw new DOMException("Cannot route loaded media.", "AbortError");
},
};
const { result } = await runStatusScript({
allowMicrophone: true,
devices: [
{ deviceId: "blackhole-input", kind: "audioinput", label: "BlackHole 2ch" },
{ deviceId: "blackhole-output", kind: "audiooutput", label: "BlackHole 2ch" },
],
leave: control({ label: "Leave" }),
const { result } = await runAudioStatusScript({
media: [routed, loaded],
microphone: control({ label: "Turn microphone off", pressed: true }),
microphoneDevice: control({ label: "BlackHole 2ch" }),
priorMeeting: { identity: "teams-work:19:meeting_test@thread.v2" },
});
expect(result).toMatchObject({
@@ -953,36 +715,17 @@ describe("Microsoft Teams meeting audio routing", () => {
});
it("preserves Teams mute state and ignores a muted local media stream", async () => {
const remote: PageMedia = {
muted: false,
sinkId: "",
srcObject: { getAudioTracks: () => [{ readyState: "live" }] },
async setSinkId(value) {
remote.sinkId = value;
},
};
const remote = pageMedia({ muted: false, srcObject: liveMediaStream() });
let localRouteAttempts = 0;
const local: PageMedia = {
muted: true,
sinkId: "built-in-output",
srcObject: { getAudioTracks: () => [{ readyState: "live" }] },
srcObject: liveMediaStream(),
async setSinkId() {
localRouteAttempts += 1;
},
};
const params = {
allowMicrophone: true,
devices: [
{ deviceId: "blackhole-input", kind: "audioinput", label: "BlackHole 2ch" },
{ deviceId: "blackhole-output", kind: "audiooutput", label: "BlackHole 2ch" },
],
leave: control({ label: "Leave" }),
microphone: control({ label: "Turn microphone off", pressed: true }),
microphoneDevice: control({ label: "BlackHole 2ch" }),
priorMeeting: {
identity: "teams-work:19:meeting_test@thread.v2",
},
};
const params = audioStatusParams();
const pending = await runStatusScript({ ...params, media: [local] });
expect(pending.result).toMatchObject({
audioInputRouted: true,
@@ -11,6 +11,8 @@ import {
MEETING_STATE_KEY,
control,
captionRow,
runCaptionRows,
runCaptionStatusScript,
runStatusScript,
} from "./teams-meetings-platform-adapter.test-helpers.js";
@@ -39,13 +41,9 @@ describe("Microsoft Teams meeting captions and permissions", () => {
});
it("enables live captions and captures the validated Teams caption row DOM", async () => {
const leave = control({ label: "Leave" });
const { result, window } = await runStatusScript({
allowMicrophone: false,
captionRows: [captionRow("OpenClaw QA", "Copper lantern validates Teams captions seven.")],
captureCaptions: true,
leave,
});
const { result, window } = await runCaptionRows([
captionRow("OpenClaw QA", "Copper lantern validates Teams captions seven."),
]);
expect(result).toMatchObject({
captioning: true,
@@ -83,13 +81,9 @@ describe("Microsoft Teams meeting captions and permissions", () => {
});
it("retries an unverified live-caption activation", async () => {
const first = await runStatusScript({
allowMicrophone: false,
const first = await runCaptionRows([captionRow("OpenClaw QA", "Retry captions")], undefined, {
captionClickIgnored: true,
captionsInitiallyOn: false,
captionRows: [captionRow("OpenClaw QA", "Retry captions")],
captureCaptions: true,
leave: control({ label: "Leave" }),
});
expect(first.captionButton.clicks).toBe(1);
expect(first.result).toMatchObject({
@@ -97,13 +91,8 @@ describe("Microsoft Teams meeting captions and permissions", () => {
captionsEnabledAttempted: false,
});
const second = await runStatusScript({
allowMicrophone: false,
const second = await runCaptionRows([captionRow("OpenClaw QA", "Retry captions")], first, {
captionsInitiallyOn: false,
captionRows: [captionRow("OpenClaw QA", "Retry captions")],
captureCaptions: true,
leave: control({ label: "Leave" }),
priorCaptions: first.window["__openclawTeamsCaptions"],
priorMeeting: first.window[MEETING_STATE_KEY] as Record<string, unknown>,
});
expect(second.captionButton.clicks).toBe(1);
@@ -114,12 +103,7 @@ describe("Microsoft Teams meeting captions and permissions", () => {
});
it("preserves valid one-character caption lines", async () => {
const { result } = await runStatusScript({
allowMicrophone: false,
captionRows: [captionRow("OpenClaw QA", "I")],
captureCaptions: true,
leave: control({ label: "Leave" }),
});
const { result } = await runCaptionRows([captionRow("OpenClaw QA", "I")]);
expect(result).toMatchObject({
lastCaptionText: "I",
@@ -129,13 +113,10 @@ describe("Microsoft Teams meeting captions and permissions", () => {
});
it("bounds visible and committed caption rows together", async () => {
const { result, window } = await runStatusScript({
allowMicrophone: false,
const { result, window } = await runCaptionStatusScript({
captionRows: Array.from({ length: 505 }, (_, index) =>
captionRow("OpenClaw QA", `Bounded caption ${index}`),
),
captureCaptions: true,
leave: control({ label: "Leave" }),
});
const state = window["__openclawTeamsCaptions"] as {
droppedLines: number;
@@ -163,18 +144,8 @@ describe("Microsoft Teams meeting captions and permissions", () => {
});
it("keeps repeated utterances from distinct caption rows", async () => {
const first = await runStatusScript({
allowMicrophone: false,
captionRows: [captionRow("OpenClaw QA", "Yes")],
captureCaptions: true,
leave: control({ label: "Leave" }),
});
const second = await runStatusScript({
allowMicrophone: false,
captionRows: [captionRow("OpenClaw QA", "Yes")],
captureCaptions: true,
leave: control({ label: "Leave" }),
priorCaptions: first.window["__openclawTeamsCaptions"],
const first = await runCaptionRows([captionRow("OpenClaw QA", "Yes")]);
const second = await runCaptionRows([captionRow("OpenClaw QA", "Yes")], first, {
priorMeeting: first.window[MEETING_STATE_KEY] as Record<string, unknown>,
});
@@ -184,12 +155,8 @@ describe("Microsoft Teams meeting captions and permissions", () => {
it("keeps the latest caption when Teams shortens a provisional row", async () => {
vi.useFakeTimers();
const row = captionRow("OpenClaw QA", "We should leave today");
const first = await runStatusScript({
allowMicrophone: false,
const first = await runCaptionRows([row], undefined, {
captionsInitiallyOn: true,
captionRows: [row],
captureCaptions: true,
leave: control({ label: "Leave" }),
});
await vi.advanceTimersByTimeAsync(1_000);
const caption = row.querySelector('[data-tid="closed-caption-text"]');
@@ -197,13 +164,7 @@ describe("Microsoft Teams meeting captions and permissions", () => {
throw new Error("expected caption text control");
}
caption.textContent = "We should leave";
const second = await runStatusScript({
allowMicrophone: false,
captionRows: [row],
captureCaptions: true,
leave: control({ label: "Leave" }),
priorCaptions: first.window["__openclawTeamsCaptions"],
});
const second = await runCaptionRows([row], first);
expect(second.result.lastCaptionText).toBe("We should leave");
expect(second.result.recentTranscript).toMatchObject([
@@ -213,43 +174,24 @@ describe("Microsoft Teams meeting captions and permissions", () => {
it("keeps a mid-sentence caption correction in the same row lifecycle", async () => {
const row = captionRow("OpenClaw QA", "I like cats");
const first = await runStatusScript({
allowMicrophone: false,
captionRows: [row],
captureCaptions: true,
leave: control({ label: "Leave" }),
});
const first = await runCaptionRows([row]);
const caption = row.querySelector('[data-tid="closed-caption-text"]');
if (!caption) {
throw new Error("expected caption text control");
}
caption.textContent = "I liked cats";
const second = await runStatusScript({
allowMicrophone: false,
captionRows: [row],
captureCaptions: true,
leave: control({ label: "Leave" }),
priorCaptions: first.window["__openclawTeamsCaptions"],
});
const second = await runCaptionRows([row], first);
expect(second.result.transcriptLines).toBe(1);
expect(second.result.lastCaptionText).toBe("I liked cats");
});
it("keeps one utterance when Teams replaces a logically identical caption row", async () => {
const first = await runStatusScript({
allowMicrophone: false,
captionRows: [captionRow("OpenClaw QA", "Logical row", "8")],
captureCaptions: true,
leave: control({ label: "Leave" }),
});
const second = await runStatusScript({
allowMicrophone: false,
captionRows: [captionRow("OpenClaw QA", "Logical row replacement", "8")],
captureCaptions: true,
leave: control({ label: "Leave" }),
priorCaptions: first.window["__openclawTeamsCaptions"],
});
const first = await runCaptionRows([captionRow("OpenClaw QA", "Logical row", "8")]);
const second = await runCaptionRows(
[captionRow("OpenClaw QA", "Logical row replacement", "8")],
first,
);
expect(second.result.transcriptLines).toBe(1);
expect(second.result.lastCaptionText).toBe("Logical row replacement");
@@ -257,21 +199,18 @@ describe("Microsoft Teams meeting captions and permissions", () => {
it("updates a settled logical row in place when Teams corrects it late", async () => {
vi.useFakeTimers();
const first = await runStatusScript({
allowMicrophone: false,
captionsInitiallyOn: true,
captionRows: [captionRow("OpenClaw QA", "Late logical", "9")],
captureCaptions: true,
leave: control({ label: "Leave" }),
});
const first = await runCaptionRows(
[captionRow("OpenClaw QA", "Late logical", "9")],
undefined,
{
captionsInitiallyOn: true,
},
);
await vi.advanceTimersByTimeAsync(1_000);
const second = await runStatusScript({
allowMicrophone: false,
captionRows: [captionRow("OpenClaw QA", "Late logical correction", "9")],
captureCaptions: true,
leave: control({ label: "Leave" }),
priorCaptions: first.window["__openclawTeamsCaptions"],
});
const second = await runCaptionRows(
[captionRow("OpenClaw QA", "Late logical correction", "9")],
first,
);
expect(second.result.transcriptLines).toBe(1);
expect(second.result.recentTranscript).toMatchObject([{ text: "Late logical correction" }]);
@@ -279,28 +218,19 @@ describe("Microsoft Teams meeting captions and permissions", () => {
it("deduplicates a settled logical row after temporary DOM removal", async () => {
vi.useFakeTimers();
const first = await runStatusScript({
allowMicrophone: false,
captionsInitiallyOn: true,
captionRows: [captionRow("OpenClaw QA", "Virtual row return", "13")],
captureCaptions: true,
leave: control({ label: "Leave" }),
});
const first = await runCaptionRows(
[captionRow("OpenClaw QA", "Virtual row return", "13")],
undefined,
{
captionsInitiallyOn: true,
},
);
await vi.advanceTimersByTimeAsync(1_000);
const missing = await runStatusScript({
allowMicrophone: false,
captionRows: [],
captureCaptions: true,
leave: control({ label: "Leave" }),
priorCaptions: first.window["__openclawTeamsCaptions"],
});
const returned = await runStatusScript({
allowMicrophone: false,
captionRows: [captionRow("OpenClaw QA", "Virtual row return", "13")],
captureCaptions: true,
leave: control({ label: "Leave" }),
priorCaptions: missing.window["__openclawTeamsCaptions"],
});
const missing = await runCaptionRows([], first);
const returned = await runCaptionRows(
[captionRow("OpenClaw QA", "Virtual row return", "13")],
missing,
);
expect(returned.result.transcriptLines).toBe(1);
expect(returned.result.recentTranscript).toMatchObject([{ text: "Virtual row return" }]);
@@ -309,32 +239,17 @@ describe("Microsoft Teams meeting captions and permissions", () => {
it("commits an utterance when Teams recycles the same virtual-list row", async () => {
vi.useFakeTimers();
const row = captionRow("OpenClaw QA", "First recycled-row utterance");
const first = await runStatusScript({
allowMicrophone: false,
const first = await runCaptionRows([row], undefined, {
captionsInitiallyOn: true,
captionRows: [row],
captureCaptions: true,
leave: control({ label: "Leave" }),
});
const disappeared = await runStatusScript({
allowMicrophone: false,
captionRows: [],
captureCaptions: true,
leave: control({ label: "Leave" }),
priorCaptions: first.window["__openclawTeamsCaptions"],
});
const disappeared = await runCaptionRows([], first);
await vi.advanceTimersByTimeAsync(1_000);
const caption = row.querySelector('[data-tid="closed-caption-text"]');
if (!caption) {
throw new Error("expected caption text control");
}
caption.textContent = "Completely different second utterance";
const second = await runStatusScript({
allowMicrophone: false,
captionRows: [row],
captureCaptions: true,
leave: control({ label: "Leave" }),
priorCaptions: disappeared.window["__openclawTeamsCaptions"],
const second = await runCaptionRows([row], disappeared, {
priorMeeting: first.window[MEETING_STATE_KEY] as Record<string, unknown>,
});
@@ -347,25 +262,14 @@ describe("Microsoft Teams meeting captions and permissions", () => {
it("commits a removed row before Teams rapidly reuses its DOM node", async () => {
const row = captionRow("OpenClaw QA", "Rapid first utterance");
const first = await runStatusScript({
allowMicrophone: false,
captionRows: [row],
captureCaptions: true,
leave: control({ label: "Leave" }),
});
const first = await runCaptionRows([row]);
const caption = row.querySelector('[data-tid="closed-caption-text"]');
if (!caption) {
throw new Error("expected caption text control");
}
caption.textContent = "Rapid second utterance";
first.triggerCaptionMutation(undefined, row);
const second = await runStatusScript({
allowMicrophone: false,
captionRows: [row],
captureCaptions: true,
leave: control({ label: "Leave" }),
priorCaptions: first.window["__openclawTeamsCaptions"],
});
const second = await runCaptionRows([row], first);
expect(second.result.transcriptLines).toBe(2);
expect(second.result.recentTranscript).toMatchObject([
@@ -377,33 +281,17 @@ describe("Microsoft Teams meeting captions and permissions", () => {
it("does not merge recycled rows that only share a text prefix", async () => {
vi.useFakeTimers();
const row = captionRow("OpenClaw QA", "Thank you");
const first = await runStatusScript({
allowMicrophone: false,
const first = await runCaptionRows([row], undefined, {
captionsInitiallyOn: true,
captionRows: [row],
captureCaptions: true,
leave: control({ label: "Leave" }),
});
const disappeared = await runStatusScript({
allowMicrophone: false,
captionRows: [],
captureCaptions: true,
leave: control({ label: "Leave" }),
priorCaptions: first.window["__openclawTeamsCaptions"],
});
const disappeared = await runCaptionRows([], first);
await vi.advanceTimersByTimeAsync(1_000);
const caption = row.querySelector('[data-tid="closed-caption-text"]');
if (!caption) {
throw new Error("expected caption text control");
}
caption.textContent = "Thank you everyone";
const second = await runStatusScript({
allowMicrophone: false,
captionRows: [row],
captureCaptions: true,
leave: control({ label: "Leave" }),
priorCaptions: disappeared.window["__openclawTeamsCaptions"],
});
const second = await runCaptionRows([row], disappeared);
expect(second.result.recentTranscript).toMatchObject([
{ text: "Thank you" },
@@ -415,29 +303,13 @@ describe("Microsoft Teams meeting captions and permissions", () => {
vi.useFakeTimers();
const firstRow = captionRow("OpenClaw QA", "First settled row", "11");
const secondRow = captionRow("OpenClaw QA", "Second settled row", "12");
const first = await runStatusScript({
allowMicrophone: false,
const first = await runCaptionRows([firstRow], undefined, {
captionsInitiallyOn: true,
captionRows: [firstRow],
captureCaptions: true,
leave: control({ label: "Leave" }),
});
await vi.advanceTimersByTimeAsync(1_000);
const second = await runStatusScript({
allowMicrophone: false,
captionRows: [firstRow, secondRow],
captureCaptions: true,
leave: control({ label: "Leave" }),
priorCaptions: first.window["__openclawTeamsCaptions"],
});
const second = await runCaptionRows([firstRow, secondRow], first);
await vi.advanceTimersByTimeAsync(1_000);
const third = await runStatusScript({
allowMicrophone: false,
captionRows: [firstRow, secondRow],
captureCaptions: true,
leave: control({ label: "Leave" }),
priorCaptions: second.window["__openclawTeamsCaptions"],
});
const third = await runCaptionRows([firstRow, secondRow], second);
expect(third.result.transcriptLines).toBe(2);
expect(third.result.recentTranscript).toMatchObject([
@@ -448,24 +320,13 @@ describe("Microsoft Teams meeting captions and permissions", () => {
it("updates a caption when its speaker label arrives late", async () => {
const row = captionRow("", "Late attribution");
const first = await runStatusScript({
allowMicrophone: false,
captionRows: [row],
captureCaptions: true,
leave: control({ label: "Leave" }),
});
const first = await runCaptionRows([row]);
const author = row.querySelector('[data-tid="author"]');
if (!author) {
throw new Error("expected caption author control");
}
author.textContent = "OpenClaw QA";
const second = await runStatusScript({
allowMicrophone: false,
captionRows: [row],
captureCaptions: true,
leave: control({ label: "Leave" }),
priorCaptions: first.window["__openclawTeamsCaptions"],
});
const second = await runCaptionRows([row], first);
expect(second.result.transcriptLines).toBe(1);
expect(second.result.recentTranscript).toMatchObject([
@@ -474,19 +335,13 @@ describe("Microsoft Teams meeting captions and permissions", () => {
});
it("updates a corrected speaker on the same logical row before settlement", async () => {
const first = await runStatusScript({
allowMicrophone: false,
captionRows: [captionRow("OpenClaw Q", "Stable speaker correction", "10")],
captureCaptions: true,
leave: control({ label: "Leave" }),
});
const second = await runStatusScript({
allowMicrophone: false,
captionRows: [captionRow("OpenClaw QA", "Stable speaker correction", "10")],
captureCaptions: true,
leave: control({ label: "Leave" }),
priorCaptions: first.window["__openclawTeamsCaptions"],
});
const first = await runCaptionRows([
captionRow("OpenClaw Q", "Stable speaker correction", "10"),
]);
const second = await runCaptionRows(
[captionRow("OpenClaw QA", "Stable speaker correction", "10")],
first,
);
expect(second.result.transcriptLines).toBe(1);
expect(second.result.recentTranscript).toMatchObject([
@@ -497,8 +352,6 @@ describe("Microsoft Teams meeting captions and permissions", () => {
it("disconnects stale caption capture outside transcribe mode", async () => {
let disconnects = 0;
const { window } = await runStatusScript({
allowMicrophone: false,
captureCaptions: false,
leave: control({ label: "Leave" }),
priorCaptions: {
observer: { disconnect: () => (disconnects += 1) },
@@ -518,18 +371,13 @@ describe("Microsoft Teams meeting captions and permissions", () => {
sessionId: "another-session",
};
const wrongTab = await runStatusScript({
allowMicrophone: false,
allowSessionAdoption: false,
captureCaptions: false,
currentUrl: "https://teams.live.com/v2/",
leave: control({ label: "Leave" }),
priorCaptions: active,
});
const wrongSession = await runStatusScript({
allowMicrophone: false,
const wrongSession = await runCaptionStatusScript({
allowSessionAdoption: false,
captureCaptions: true,
leave: control({ label: "Leave" }),
priorCaptions: active,
});
@@ -549,7 +397,6 @@ describe("Microsoft Teams meeting captions and permissions", () => {
visible: [{ text: "Last caption before navigation" }],
};
const { window } = await runStatusScript({
allowMicrophone: false,
currentUrl: CONSUMER_URL,
priorCaptions: captions,
priorMeeting: {
@@ -570,11 +417,8 @@ describe("Microsoft Teams meeting captions and permissions", () => {
it("finalizes same-session captions when meeting identity is lost", async () => {
let disconnects = 0;
const currentUrl = "https://teams.live.com/v2/";
const { window } = await runStatusScript({
allowMicrophone: false,
captureCaptions: true,
const { window } = await runCaptionStatusScript({
currentUrl,
leave: control({ label: "Leave" }),
priorCaptions: {
droppedLines: 0,
epoch: "caption-epoch",
@@ -615,7 +459,6 @@ describe("Microsoft Teams meeting captions and permissions", () => {
it("finalizes caption capture before an SPA navigation can mix meetings", async () => {
const params = {
allowMicrophone: false,
captionRows: [captionRow("OpenClaw QA", "Meeting A caption")],
captureCaptions: true,
leave: control({ label: "Leave" }),
@@ -688,7 +531,6 @@ describe("Microsoft Teams meeting captions and permissions", () => {
};
const currentUrl = "https://teams.live.com/v2/";
const { window } = await runStatusScript({
allowMicrophone: false,
captureCaptions: true,
currentUrl,
priorCaptions: active,
@@ -708,10 +550,8 @@ describe("Microsoft Teams meeting captions and permissions", () => {
it("keeps the live caption observer during a bounded in-call control rerender", async () => {
const leave = control({ label: "Leave" });
const currentUrl = "https://teams.live.com/v2/";
const page = await runStatusScript({
allowMicrophone: false,
const page = await runCaptionStatusScript({
captionRows: [captionRow("OpenClaw QA", "Caption before control rerender")],
captureCaptions: true,
currentUrl,
leave,
priorMeeting: {
@@ -740,11 +580,7 @@ describe("Microsoft Teams meeting captions and permissions", () => {
sessionId: "old-session",
visible: [],
};
const { window } = await runStatusScript({
allowMicrophone: false,
captionRows: [captionRow("OpenClaw QA", "New session")],
captureCaptions: true,
leave: control({ label: "Leave" }),
const { window } = await runCaptionRows([captionRow("OpenClaw QA", "New session")], undefined, {
priorCaptions: old,
});
const current = window["__openclawTeamsCaptions"] as Record<string, unknown>;
@@ -773,10 +609,8 @@ describe("Microsoft Teams meeting captions and permissions", () => {
visible: [],
};
const { result, window } = await runStatusScript({
allowMicrophone: false,
const { result, window } = await runCaptionStatusScript({
allowSessionAdoption: false,
captureCaptions: true,
leave,
priorCaptions,
priorMeeting,
@@ -809,10 +643,8 @@ describe("Microsoft Teams meeting captions and permissions", () => {
visible: [],
};
const { result, window } = await runStatusScript({
allowMicrophone: false,
const { result, window } = await runCaptionStatusScript({
allowSessionAdoption: false,
captureCaptions: true,
leave,
meetingSessionId: "",
priorCaptions,
@@ -837,11 +669,8 @@ describe("Microsoft Teams meeting captions and permissions", () => {
sessionId: "stale-session",
visible: [],
};
const { result, window } = await runStatusScript({
allowMicrophone: false,
const { result, window } = await runCaptionStatusScript({
allowSessionAdoption: false,
captureCaptions: true,
leave: control({ label: "Leave" }),
priorCaptions: staleCaptions,
priorMeeting: {
identity: "teams-work:19:meeting_test@thread.v2",
@@ -856,12 +685,7 @@ describe("Microsoft Teams meeting captions and permissions", () => {
});
it("disconnects caption capture when finalizing a transcript", async () => {
const first = await runStatusScript({
allowMicrophone: false,
captionRows: [captionRow("OpenClaw QA", "Final caption")],
captureCaptions: true,
leave: control({ label: "Leave" }),
});
const first = await runCaptionRows([captionRow("OpenClaw QA", "Final caption")]);
let disconnects = 0;
const captions = first.window["__openclawTeamsCaptions"] as Record<string, unknown>;
captions.observer = { disconnect: () => (disconnects += 1) };
@@ -880,11 +704,7 @@ describe("Microsoft Teams meeting captions and permissions", () => {
expect(captions.identity).toBe("teams-work:19:meeting_test@thread.v2");
expect(typeof captions.finalizedAt).toBe("number");
const refreshed = await runStatusScript({
allowMicrophone: false,
captionRows: [captionRow("OpenClaw QA", "Late caption")],
captureCaptions: true,
leave: control({ label: "Leave" }),
const refreshed = await runCaptionRows([captionRow("OpenClaw QA", "Late caption")], undefined, {
priorCaptions: captions,
});
expect(refreshed.window["__openclawTeamsCaptions"]).toBe(captions);
@@ -61,6 +61,30 @@ export type PageMedia = {
setSinkId(value: string): Promise<void>;
};
export const liveMediaStream = () => ({ getAudioTracks: () => [{ readyState: "live" }] });
export function pageMedia(params: Partial<PageMedia> = {}): PageMedia {
const media: PageMedia = {
...params,
sinkId: params.sinkId ?? "",
setSinkId:
params.setSinkId ??
(async (value) => {
media.sinkId = value;
}),
};
return media;
}
export function abortingMedia(message: string, params: Partial<PageMedia> = {}) {
return pageMedia({
...params,
async setSinkId() {
throw new DOMException(message, "AbortError");
},
});
}
export function control(params: {
checked?: boolean;
label: string;
@@ -113,8 +137,8 @@ export function captionRow(
return row;
}
export async function runStatusScript(params: {
allowMicrophone: boolean;
type StatusScriptParams = {
allowMicrophone?: boolean;
allowSessionAdoption?: boolean;
autoJoin?: boolean;
bodyText?: string;
@@ -145,7 +169,9 @@ export async function runStatusScript(params: {
media?: PageMedia[];
meetingSessionId?: string;
devices?: Array<{ deviceId: string; kind: string; label: string }>;
}) {
};
export async function runStatusScript(params: StatusScriptParams) {
const currentUrl = params.currentUrl ?? URL;
const location = new globalThis.URL(currentUrl);
const controls = [
@@ -288,7 +314,7 @@ export async function runStatusScript(params: {
window["__openclawTeamsCaptions"] = params.priorCaptions;
}
const script = teamsMeetingStatusScript({
allowMicrophone: params.allowMicrophone,
allowMicrophone: params.allowMicrophone ?? false,
allowSessionAdoption: params.allowSessionAdoption ?? true,
autoJoin: params.autoJoin ?? true,
captureCaptions: params.captureCaptions ?? false,
@@ -341,6 +367,61 @@ export async function runStatusScript(params: {
};
}
type StatusOverrides = Omit<StatusScriptParams, "allowMicrophone">;
function runInCallStatusScript(params: StatusOverrides = {}) {
return runStatusScript({
allowMicrophone: false,
leave: control({ label: "Leave" }),
...params,
});
}
export function runCaptionStatusScript(params: StatusOverrides = {}) {
return runInCallStatusScript({ captureCaptions: true, ...params });
}
type StatusScriptPage = Awaited<ReturnType<typeof runStatusScript>>;
export function continueStatusScript(previous: StatusScriptPage, params: StatusScriptParams = {}) {
return runStatusScript({
...params,
priorAudioOutputs: previous.window["__openclawTeamsAudioOutputs"] as unknown[],
priorMeeting: previous.window[MEETING_STATE_KEY] as Record<string, unknown>,
});
}
export function runCaptionRows(
captionRows: PageControl[],
previous?: StatusScriptPage,
params: StatusOverrides = {},
) {
return runCaptionStatusScript({
captionRows,
priorCaptions: previous?.window["__openclawTeamsCaptions"],
...params,
});
}
export function runAudioStatusScript(params: StatusOverrides = {}) {
return runStatusScript(audioStatusParams(params));
}
export function audioStatusParams(params: StatusOverrides = {}): StatusScriptParams {
return {
allowMicrophone: true,
devices: [
{ deviceId: "blackhole-input", kind: "audioinput", label: "BlackHole 2ch" },
{ deviceId: "blackhole-output", kind: "audiooutput", label: "BlackHole 2ch" },
],
leave: control({ label: "Leave" }),
microphone: control({ label: "Turn microphone off", pressed: true }),
microphoneDevice: control({ label: "BlackHole 2ch" }),
priorMeeting: { identity: "teams-work:19:meeting_test@thread.v2" },
...params,
};
}
export function runLeaveScript(params: {
bodyText?: string;
currentUrl?: string;
@@ -7,6 +7,9 @@ import {
consumerLightMeetingUrl,
status,
control,
liveMediaStream,
pageMedia,
runAudioStatusScript,
runStatusScript,
runLeaveScript,
type PageMedia,
@@ -97,7 +100,6 @@ describe("Microsoft Teams meeting platform adapter", () => {
...(ariaPressed === undefined ? {} : { pressed: ariaPressed === "true" }),
});
const { result } = await runStatusScript({
allowMicrophone: false,
...(kind === "camera" ? { camera: target } : { microphone: target }),
readOnly: true,
});
@@ -113,7 +115,6 @@ describe("Microsoft Teams meeting platform adapter", () => {
])("reads the live %s switch checked=%s", async (kind, checked, expectedOff) => {
const target = control({ checked, label: kind === "camera" ? "Camera" : "Microphone" });
const { result } = await runStatusScript({
allowMicrophone: false,
...(kind === "camera" ? { camera: target } : { microphone: target }),
readOnly: true,
});
@@ -134,7 +135,6 @@ describe("Microsoft Teams meeting platform adapter", () => {
const join = control({ label: "Join now" });
const { result } = await runStatusScript({
allowMicrophone: false,
camera,
join,
microphone,
@@ -149,7 +149,6 @@ describe("Microsoft Teams meeting platform adapter", () => {
it("allows non-adopting recovery to continue for the current page owner", async () => {
const join = control({ label: "Join now" });
const { result } = await runStatusScript({
allowMicrophone: false,
allowSessionAdoption: false,
camera: control({ label: "Turn camera on", pressed: false }),
join,
@@ -171,7 +170,6 @@ describe("Microsoft Teams meeting platform adapter", () => {
sessionId: "consumer-session",
};
const { result, window } = await runStatusScript({
allowMicrophone: false,
allowSessionAdoption: true,
currentUrl: CONSUMER_URL,
priorMeeting,
@@ -262,7 +260,6 @@ describe("Microsoft Teams meeting platform adapter", () => {
it("does not stamp meeting identity onto unrelated Teams pages", async () => {
const leave = control({ label: "Leave" });
const { result, window } = await runStatusScript({
allowMicrophone: false,
currentUrl: "https://teams.microsoft.com/v2/",
leave,
});
@@ -273,7 +270,6 @@ describe("Microsoft Teams meeting platform adapter", () => {
it("verifies the consumer prejoin redirect from its encoded meeting coordinates", async () => {
const { result, window } = await runStatusScript({
allowMicrophone: false,
currentUrl: consumerLightMeetingUrl("9326458712345", "abc"),
meetingUrl: CONSUMER_URL,
});
@@ -287,7 +283,6 @@ describe("Microsoft Teams meeting platform adapter", () => {
it("rejects consumer prejoin coordinates for a different meeting", async () => {
const { result, window } = await runStatusScript({
allowMicrophone: false,
currentUrl: consumerLightMeetingUrl("1111111111111", "other"),
meetingUrl: CONSUMER_URL,
});
@@ -309,7 +304,6 @@ describe("Microsoft Teams meeting platform adapter", () => {
sessionId: "session-1",
};
const { result, window } = await runStatusScript({
allowMicrophone: false,
currentUrl: inCallUrl,
leave,
priorMeeting,
@@ -323,7 +317,6 @@ describe("Microsoft Teams meeting platform adapter", () => {
const prejoin = await runStatusScript({ allowMicrophone: false });
const leave = control({ label: "Leave" });
const admitted = await runStatusScript({
allowMicrophone: false,
currentUrl: "https://teams.microsoft.com/v2/",
leave,
priorMeeting: prejoin.window[MEETING_STATE_KEY] as Record<string, unknown>,
@@ -340,7 +333,6 @@ describe("Microsoft Teams meeting platform adapter", () => {
it("retains prejoin identity for the configured in-call wait", async () => {
const leave = control({ label: "Leave" });
const admitted = await runStatusScript({
allowMicrophone: false,
currentUrl: "https://teams.microsoft.com/v2/",
leave,
priorMeeting: {
@@ -364,7 +356,6 @@ describe("Microsoft Teams meeting platform adapter", () => {
const currentLeave = control({ label: "Leave" });
const inCallUrl = "https://teams.microsoft.com/v2/";
const { result, window } = await runStatusScript({
allowMicrophone: false,
currentUrl: inCallUrl,
leave: currentLeave,
priorMeeting: {
@@ -507,7 +498,7 @@ describe("Microsoft Teams meeting platform adapter", () => {
isConnected: false,
muted: true,
pause: vi.fn(),
srcObject: { getAudioTracks: () => [{ readyState: "live" }] },
srcObject: liveMediaStream(),
};
const detachedBridge = { pause: vi.fn(), remove: vi.fn(), srcObject: {} };
const foreignBridge = { sessionId: "session-2" };
@@ -549,8 +540,8 @@ describe("Microsoft Teams meeting platform adapter", () => {
});
it("does not unmute a replacement stream during leave cleanup", () => {
const bridgedStream = { getAudioTracks: () => [{ readyState: "live" }] };
const replacementStream = { getAudioTracks: () => [{ readyState: "live" }] };
const bridgedStream = liveMediaStream();
const replacementStream = liveMediaStream();
const source = { muted: true, srcObject: replacementStream };
const bridge = { pause: vi.fn(), remove: vi.fn(), srcObject: bridgedStream };
const { result } = runLeaveScript({
@@ -629,7 +620,7 @@ describe("Microsoft Teams meeting platform adapter", () => {
});
it("retires the page-owned audio bridge from the required URL-only leave callback", () => {
const stream = { getAudioTracks: () => [{ readyState: "live" }] };
const stream = liveMediaStream();
const source = { muted: true, srcObject: stream };
const bridge = { pause: vi.fn(), remove: vi.fn(), srcObject: stream };
const { result, window } = runLeaveScript({
@@ -663,7 +654,6 @@ describe("Microsoft Teams meeting platform adapter", () => {
])("ignores participant-controlled in-call text: %s", async (bodyText) => {
const leave = control({ label: "Leave" });
const { result } = await runStatusScript({
allowMicrophone: false,
bodyText,
leave,
});
@@ -676,7 +666,6 @@ describe("Microsoft Teams meeting platform adapter", () => {
it("classifies a stable device permission prompt outside the call", async () => {
const { result } = await runStatusScript({
allowMicrophone: false,
permissionPrompt: control({ label: "Device permission prompt" }),
});
@@ -689,7 +678,6 @@ describe("Microsoft Teams meeting platform adapter", () => {
it("does not report a prompt that it just dismissed while Teams removes the DOM", async () => {
const continueWithoutDevices = control({ label: "Continue without audio or video" });
const { result } = await runStatusScript({
allowMicrophone: false,
continueWithoutDevices,
permissionPrompt: control({ label: "Device permission prompt" }),
});
@@ -773,7 +761,7 @@ describe("Microsoft Teams meeting platform adapter", () => {
const microphone = control({ label: "Turn microphone off", pressed: true });
const media = {
sinkId: "",
srcObject: { getAudioTracks: () => [{ readyState: "live" }] },
srcObject: liveMediaStream(),
async setSinkId(value: string) {
media.sinkId = value;
},
@@ -806,15 +794,9 @@ describe("Microsoft Teams meeting platform adapter", () => {
it("reports the prepared session input during read-only status inspection", async () => {
const media: PageMedia = { sinkId: "blackhole-output", async setSinkId() {} };
const { result } = await runStatusScript({
allowMicrophone: true,
devices: [
{ deviceId: "blackhole-input", kind: "audioinput", label: "BlackHole 2ch" },
{ deviceId: "blackhole-output", kind: "audiooutput", label: "BlackHole 2ch" },
],
leave: control({ label: "Leave" }),
const { result } = await runAudioStatusScript({
media: [media],
microphone: control({ label: "Turn microphone off", pressed: true }),
microphoneDevice: undefined,
priorMeeting: {
audioInputDeviceId: "blackhole-input",
identity: "teams-work:19:meeting_test@thread.v2",
@@ -831,23 +813,9 @@ describe("Microsoft Teams meeting platform adapter", () => {
});
it("routes a directly playable media element before its MediaStream is attached", async () => {
const media: PageMedia = {
sinkId: "",
async setSinkId(value) {
media.sinkId = value;
},
};
const { result } = await runStatusScript({
allowMicrophone: true,
devices: [
{ deviceId: "blackhole-input", kind: "audioinput", label: "BlackHole 2ch" },
{ deviceId: "blackhole-output", kind: "audiooutput", label: "BlackHole 2ch" },
],
leave: control({ label: "Leave" }),
const media = pageMedia();
const { result } = await runAudioStatusScript({
media: [media],
microphone: control({ label: "Turn microphone off", pressed: true }),
microphoneDevice: control({ label: "BlackHole 2ch" }),
priorMeeting: { identity: "teams-work:19:meeting_test@thread.v2" },
});
expect(result.audioOutputRouted).toBe(true);
@@ -911,14 +879,12 @@ describe("Microsoft Teams meeting platform adapter", () => {
expect(continueWithoutDevices.clicks).toBe(0);
await runStatusScript({
allowMicrophone: false,
continueWithoutDevices,
microphone: control({ label: "Turn microphone on", pressed: false }),
});
expect(continueWithoutDevices.clicks).toBe(1);
await runStatusScript({
allowMicrophone: false,
autoJoin: false,
continueWithoutDevices,
microphone: control({ label: "Turn microphone on", pressed: false }),
@@ -930,7 +896,7 @@ describe("Microsoft Teams meeting platform adapter", () => {
const source: PageMedia = {
muted: false,
sinkId: "built-in-output",
srcObject: { getAudioTracks: () => [{ readyState: "live" }] },
srcObject: liveMediaStream(),
async setSinkId() {
throw new DOMException("The element has no supported source.", "AbortError");
},
@@ -950,17 +916,9 @@ describe("Microsoft Teams meeting platform adapter", () => {
bridge.sinkId = value;
},
};
const { result, window } = await runStatusScript({
allowMicrophone: true,
const { result, window } = await runAudioStatusScript({
bridgeMedia: bridge,
devices: [
{ deviceId: "blackhole-input", kind: "audioinput", label: "BlackHole 2ch" },
{ deviceId: "blackhole-output", kind: "audiooutput", label: "BlackHole 2ch" },
],
leave: control({ label: "Leave" }),
media: [source],
microphone: control({ label: "Turn microphone off", pressed: true }),
microphoneDevice: control({ label: "BlackHole 2ch" }),
priorMeeting: {
audioInputDeviceId: "blackhole-input",
identity: "teams-work:19:meeting_test@thread.v2",
@@ -981,17 +939,9 @@ describe("Microsoft Teams meeting platform adapter", () => {
bridge,
);
const repeated = await runStatusScript({
allowMicrophone: true,
const repeated = await runAudioStatusScript({
bridgeMedia: bridge,
devices: [
{ deviceId: "blackhole-input", kind: "audioinput", label: "BlackHole 2ch" },
{ deviceId: "blackhole-output", kind: "audiooutput", label: "BlackHole 2ch" },
],
leave: control({ label: "Leave" }),
media: [source, bridge],
microphone: control({ label: "Turn microphone off", pressed: true }),
microphoneDevice: control({ label: "BlackHole 2ch" }),
priorAudioOutputs: window["__openclawTeamsAudioOutputs"] as unknown[],
priorMeeting: window[MEETING_STATE_KEY] as Record<string, unknown>,
});