mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
test(teams-meetings): consolidate transport fixtures (#118090)
This commit is contained in:
committed by
GitHub
parent
c887279d0f
commit
21b07093a0
@@ -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);
|
||||
|
||||
+85
-4
@@ -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>,
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user