ep_webrtc 2.5.35 → 2.5.36

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,271 @@
1
+ import {expect, test} from '@playwright/test';
2
+ import {goToNewPadWithParams, installFakeGetUserMedia, setPadPrefsCookie} from '../helper/utils';
3
+
4
+ // Helpers for inspecting the in-page tracks captured by the fake
5
+ // getUserMedia (see installFakeGetUserMedia({track:true})).
6
+ const tracksInfo = async (page: import('@playwright/test').Page) =>
7
+ page.evaluate(() => {
8
+ const w = window as any;
9
+ const stream: MediaStream | undefined = w.__webrtcLastStream;
10
+ const a = stream && stream.getAudioTracks()[0];
11
+ const v = stream && stream.getVideoTracks()[0];
12
+ return {
13
+ hasStream: !!stream,
14
+ audio: a ? {enabled: a.enabled} : null,
15
+ video: v ? {enabled: v.enabled} : null,
16
+ };
17
+ });
18
+
19
+ test.describe('Test the behavior of the interface buttons: Mute, Video Disable, Enlarge', () => {
20
+ test.describe('audio and video on by default', () => {
21
+ test.beforeEach(async ({page, context}) => {
22
+ test.setTimeout(60_000);
23
+ await context.clearCookies();
24
+ await setPadPrefsCookie(page, {
25
+ rtcEnabled: false,
26
+ audioEnabledOnStart: true,
27
+ videoEnabledOnStart: true,
28
+ });
29
+ await goToNewPadWithParams(page, {});
30
+ await installFakeGetUserMedia(page, {track: true});
31
+ await page.evaluate(() => (window as any).ep_webrtc.activate());
32
+ });
33
+
34
+ test('enlarges then shrinks', async ({page}) => {
35
+ test.setTimeout(60_000);
36
+ // i.e., "160.25px" -> 160.25 the number
37
+ const numFromCss = (s: string | null): number => {
38
+ expect(s && s.endsWith('px')).toBeTruthy();
39
+ return Number((s as string).slice(0, -2));
40
+ };
41
+
42
+ await page.waitForFunction(() => {
43
+ const v = document.querySelector('video') as HTMLElement | null;
44
+ if (!v) return false;
45
+ const cs = getComputedStyle(v);
46
+ const w = parseFloat(cs.width);
47
+ const h = parseFloat(cs.height);
48
+ return 159 < w && w < 161 && 119 < h && h < 121;
49
+ });
50
+
51
+ await page.locator('.enlarge-btn').first().evaluate(
52
+ (el) => (window as any).$(el).click());
53
+ // Expect grow to 260, 190 (test originally used >259 width and >194 height).
54
+ await page.waitForFunction(() => {
55
+ const v = document.querySelector('video') as HTMLElement | null;
56
+ if (!v) return false;
57
+ const cs = getComputedStyle(v);
58
+ return parseFloat(cs.width) > 259 && parseFloat(cs.height) > 194;
59
+ }, undefined, {timeout: 1000});
60
+ await page.locator('.enlarge-btn').first().evaluate(
61
+ (el) => (window as any).$(el).click());
62
+ // Shrink back to <161, <121.
63
+ await page.waitForFunction(() => {
64
+ const v = document.querySelector('video') as HTMLElement | null;
65
+ if (!v) return false;
66
+ const cs = getComputedStyle(v);
67
+ return parseFloat(cs.width) < 161 && parseFloat(cs.height) < 121;
68
+ }, undefined, {timeout: 1000});
69
+ // Silence numFromCss "unused" warning by referencing it in a noop.
70
+ void numFromCss;
71
+ });
72
+
73
+ test('mutes then unmutes', async ({page}) => {
74
+ test.setTimeout(60_000);
75
+ const info0 = await tracksInfo(page);
76
+ expect(info0.audio?.enabled).toBe(true);
77
+ expect(await page.locator('.audio-btn.muted').count()).toBe(0);
78
+ expect(await page.locator('.audio-btn').first().getAttribute('title')).toBe('Mute');
79
+
80
+ await page.locator('.audio-btn').first().evaluate(
81
+ (el) => (window as any).$(el).click());
82
+
83
+ await page.waitForFunction(() => {
84
+ const w = window as any;
85
+ const stream: MediaStream | undefined = w.__webrtcLastStream;
86
+ const a = stream && stream.getAudioTracks()[0];
87
+ return document.querySelectorAll('.audio-btn.muted').length === 1 && a && a.enabled === false;
88
+ }, undefined, {timeout: 3000});
89
+ expect(await page.locator('.audio-btn').first().getAttribute('title')).toBe('Unmute');
90
+
91
+ await page.locator('.audio-btn').first().evaluate(
92
+ (el) => (window as any).$(el).click());
93
+ await page.waitForFunction(() => {
94
+ const w = window as any;
95
+ const stream: MediaStream | undefined = w.__webrtcLastStream;
96
+ const a = stream && stream.getAudioTracks()[0];
97
+ return document.querySelectorAll('.audio-btn.muted').length === 0 && a && a.enabled === true;
98
+ }, undefined, {timeout: 3000});
99
+ expect(await page.locator('.audio-btn').first().getAttribute('title')).toBe('Mute');
100
+ });
101
+
102
+ test('disables then enables video', async ({page}) => {
103
+ test.setTimeout(60_000);
104
+ const info0 = await tracksInfo(page);
105
+ expect(info0.video?.enabled).toBe(true);
106
+ expect(await page.locator('.video-btn.off').count()).toBe(0);
107
+ expect(await page.locator('.video-btn').first().getAttribute('title'))
108
+ .toContain('Disable');
109
+
110
+ await page.locator('.video-btn').first().evaluate(
111
+ (el) => (window as any).$(el).click());
112
+ await page.waitForFunction(() => {
113
+ const w = window as any;
114
+ const stream: MediaStream | undefined = w.__webrtcLastStream;
115
+ const v = stream && stream.getVideoTracks()[0];
116
+ return document.querySelectorAll('.video-btn.off').length === 1 && v && v.enabled === false;
117
+ }, undefined, {timeout: 3000});
118
+ expect(await page.locator('.video-btn').first().getAttribute('title'))
119
+ .toContain('Enable');
120
+
121
+ await page.locator('.video-btn').first().evaluate(
122
+ (el) => (window as any).$(el).click());
123
+ await page.waitForFunction(() => {
124
+ const w = window as any;
125
+ const stream: MediaStream | undefined = w.__webrtcLastStream;
126
+ const v = stream && stream.getVideoTracks()[0];
127
+ return document.querySelectorAll('.video-btn.off').length === 0 && v && v.enabled === true;
128
+ }, undefined, {timeout: 3000});
129
+ expect(await page.locator('.video-btn').first().getAttribute('title'))
130
+ .toContain('Disable');
131
+ });
132
+ });
133
+
134
+ test.describe('audio and video off by default', () => {
135
+ test.beforeEach(async ({page, context}) => {
136
+ test.setTimeout(60_000);
137
+ await context.clearCookies();
138
+ await setPadPrefsCookie(page, {
139
+ rtcEnabled: false,
140
+ audioEnabledOnStart: false,
141
+ videoEnabledOnStart: false,
142
+ });
143
+ await goToNewPadWithParams(page, {});
144
+ await installFakeGetUserMedia(page, {track: true});
145
+ await page.evaluate(() => (window as any).ep_webrtc.activate());
146
+ });
147
+
148
+ test('unmutes then mutes', async ({page}) => {
149
+ test.setTimeout(60_000);
150
+ const info0 = await tracksInfo(page);
151
+ // No getUserMedia call yet because audio/video are off on start.
152
+ expect(info0.hasStream).toBe(false);
153
+ expect(await page.locator('.audio-btn.muted').count()).toBe(1);
154
+ expect(await page.locator('.audio-btn').first().getAttribute('title')).toBe('Unmute');
155
+
156
+ await page.locator('.audio-btn').first().evaluate(
157
+ (el) => (window as any).$(el).click());
158
+ await page.waitForFunction(() => {
159
+ const w = window as any;
160
+ const stream: MediaStream | undefined = w.__webrtcLastStream;
161
+ const a = stream && stream.getAudioTracks()[0];
162
+ return document.querySelectorAll('.audio-btn.muted').length === 0 &&
163
+ a != null && a.enabled;
164
+ }, undefined, {timeout: 3000});
165
+ expect(await page.locator('.audio-btn').first().getAttribute('title')).toBe('Mute');
166
+
167
+ await page.locator('.audio-btn').first().evaluate(
168
+ (el) => (window as any).$(el).click());
169
+ await page.waitForFunction(() => {
170
+ const w = window as any;
171
+ const stream: MediaStream | undefined = w.__webrtcLastStream;
172
+ const a = stream && stream.getAudioTracks()[0];
173
+ return document.querySelectorAll('.audio-btn.muted').length === 1 &&
174
+ a && a.enabled === false;
175
+ }, undefined, {timeout: 3000});
176
+ expect(await page.locator('.audio-btn').first().getAttribute('title')).toBe('Unmute');
177
+ });
178
+
179
+ test('enables then disables video', async ({page}) => {
180
+ test.setTimeout(60_000);
181
+ const info0 = await tracksInfo(page);
182
+ expect(info0.hasStream).toBe(false);
183
+ expect(await page.locator('.video-btn.off').count()).toBe(1);
184
+ expect(await page.locator('.video-btn').first().getAttribute('title'))
185
+ .toContain('Enable');
186
+
187
+ await page.locator('.video-btn').first().evaluate(
188
+ (el) => (window as any).$(el).click());
189
+ await page.waitForFunction(() => {
190
+ const w = window as any;
191
+ const stream: MediaStream | undefined = w.__webrtcLastStream;
192
+ const v = stream && stream.getVideoTracks()[0];
193
+ return document.querySelectorAll('.video-btn.off').length === 0 &&
194
+ v != null && v.enabled;
195
+ }, undefined, {timeout: 3000});
196
+ expect(await page.locator('.video-btn').first().getAttribute('title'))
197
+ .toContain('Disable');
198
+
199
+ await page.locator('.video-btn').first().evaluate(
200
+ (el) => (window as any).$(el).click());
201
+ await page.waitForFunction(() => {
202
+ const w = window as any;
203
+ const stream: MediaStream | undefined = w.__webrtcLastStream;
204
+ const v = stream && stream.getVideoTracks()[0];
205
+ return document.querySelectorAll('.video-btn.off').length === 1 &&
206
+ v && v.enabled === false;
207
+ }, undefined, {timeout: 3000});
208
+ expect(await page.locator('.video-btn').first().getAttribute('title'))
209
+ .toContain('Enable');
210
+ });
211
+ });
212
+
213
+ test.describe('audio and video hard-disabled', () => {
214
+ test.beforeEach(async ({page, context}) => {
215
+ test.setTimeout(60_000);
216
+ await context.clearCookies();
217
+ // Disable WebRTC so we can install fake getUserMedia and tweak settings
218
+ // before activation.
219
+ await goToNewPadWithParams(page, {av: false});
220
+ await page.waitForFunction(
221
+ () => (window as any).$('#rtcbox').data('initialized'));
222
+ await page.evaluate(() => {
223
+ const w = window as any;
224
+ w.ep_webrtc._settings.audio.disabled = 'hard';
225
+ w.ep_webrtc._settings.video.disabled = 'hard';
226
+ });
227
+ await installFakeGetUserMedia(page, {track: true});
228
+ await page.evaluate(() => (window as any).ep_webrtc.activate());
229
+ });
230
+
231
+ test('cannot mute or unmute', async ({page}) => {
232
+ test.setTimeout(60_000);
233
+ expect(await page.locator('.audio-btn.disallowed').count()).toBe(1);
234
+ expect(await page.locator('.audio-btn.muted').count()).toBe(1);
235
+ expect(await page.locator('.audio-btn').first().getAttribute('title'))
236
+ .toBe('Audio disallowed by admin');
237
+ const info = await tracksInfo(page);
238
+ expect(info.hasStream).toBe(false);
239
+
240
+ await page.locator('.audio-btn').first().evaluate(
241
+ (el) => (window as any).$(el).click());
242
+ await page.waitForTimeout(200);
243
+ expect(await page.locator('.audio-btn.disallowed').count()).toBe(1);
244
+ expect(await page.locator('.audio-btn.muted').count()).toBe(1);
245
+ expect(await page.locator('.audio-btn').first().getAttribute('title'))
246
+ .toBe('Audio disallowed by admin');
247
+ const info2 = await tracksInfo(page);
248
+ expect(info2.hasStream).toBe(false);
249
+ });
250
+
251
+ test('cannot enable or disable video', async ({page}) => {
252
+ test.setTimeout(60_000);
253
+ expect(await page.locator('.video-btn.disallowed').count()).toBe(1);
254
+ expect(await page.locator('.video-btn.off').count()).toBe(1);
255
+ expect(await page.locator('.video-btn').first().getAttribute('title'))
256
+ .toBe('Video disallowed by admin');
257
+ const info = await tracksInfo(page);
258
+ expect(info.hasStream).toBe(false);
259
+
260
+ await page.locator('.video-btn').first().evaluate(
261
+ (el) => (window as any).$(el).click());
262
+ await page.waitForTimeout(200);
263
+ expect(await page.locator('.video-btn.disallowed').count()).toBe(1);
264
+ expect(await page.locator('.video-btn.off').count()).toBe(1);
265
+ expect(await page.locator('.video-btn').first().getAttribute('title'))
266
+ .toBe('Video disallowed by admin');
267
+ const info2 = await tracksInfo(page);
268
+ expect(info2.hasStream).toBe(false);
269
+ });
270
+ });
271
+ });
@@ -0,0 +1,271 @@
1
+ import {expect, test} from '@playwright/test';
2
+ import {goToNewPadWithParams, installFakeGetUserMedia, setPadPrefsCookie} from '../helper/utils';
3
+
4
+ // 1:1 port of static/tests/frontend/specs/race_conditions.js. The
5
+ // per-iteration assertions about track identity/readyState live inside
6
+ // page.evaluate() so we can compare track object identity in browser
7
+ // context.
8
+
9
+ test.describe('Race conditions that leave audio/video track enabled', () => {
10
+ for (const enabledOnStart of [false, true]) {
11
+ test.describe(`audio and video ${enabledOnStart ? 'en' : 'dis'}abled on start`, () => {
12
+ test.beforeEach(async ({page, context}) => {
13
+ test.setTimeout(60_000);
14
+ await context.clearCookies();
15
+ await setPadPrefsCookie(page, {
16
+ audioEnabledOnStart: enabledOnStart,
17
+ videoEnabledOnStart: enabledOnStart,
18
+ });
19
+ // Disable WebRTC so we can install a fake getUserMedia() before it is called.
20
+ await goToNewPadWithParams(page, {av: false});
21
+ await installFakeGetUserMedia(page);
22
+ await page.locator('#options-enablertc').evaluate(
23
+ (el) => (window as any).$(el).click());
24
+ await page.waitForFunction(
25
+ () => (window as any).$('#rtcbox').data('initialized'));
26
+ await page.waitForFunction(() => document.querySelector('video') != null);
27
+ const initOk = await page.evaluate((enabledOnStart) => {
28
+ const w = window as any;
29
+ const ownVideoId = `video_${w.ep_webrtc.getUserId().replace(/\./g, '_')}`;
30
+ const $interface = w.$(`#interface_${ownVideoId}`);
31
+ if ($interface.length !== 1) return false;
32
+ if (w.$('.audio-btn').length !== 1) return false;
33
+ if (w.$('.video-btn').length !== 1) return false;
34
+ return true;
35
+ }, enabledOnStart);
36
+ expect(initOk).toBe(true);
37
+ await page.waitForFunction((enabledOnStart) => {
38
+ const v = document.querySelector('video') as HTMLVideoElement | null;
39
+ const stream = v && (v.srcObject as MediaStream | null);
40
+ return stream != null && (!enabledOnStart || stream.getTracks().length === 2);
41
+ }, enabledOnStart);
42
+ // assertTracks() initial check.
43
+ const ok = await page.evaluate((enabledOnStart) => {
44
+ const w = window as any;
45
+ const v = document.querySelector('video') as HTMLVideoElement;
46
+ const stream = v.srcObject as MediaStream;
47
+ if (enabledOnStart) {
48
+ if (stream.getTracks().length !== 2) return false;
49
+ if (!stream.getTracks().every((t) => t.enabled)) return false;
50
+ } else if (stream.getTracks().some((t) => t.enabled)) {
51
+ return false;
52
+ }
53
+ const audio = stream.getAudioTracks()[0];
54
+ const video = stream.getVideoTracks()[0];
55
+ if (w.$('.audio-btn').hasClass('muted') !== (audio == null || !audio.enabled)) return false;
56
+ if (w.$('.video-btn').hasClass('off') !== (video == null || !video.enabled)) return false;
57
+ return true;
58
+ }, enabledOnStart);
59
+ expect(ok).toBe(true);
60
+ });
61
+
62
+ test('deactivate, click, activate', async ({page}) => {
63
+ test.setTimeout(60_000);
64
+ for (let i = 0; i < 10; ++i) {
65
+ const ok = await page.evaluate(async (enabledOnStart) => {
66
+ const w = window as any;
67
+ const v = document.querySelector('video') as HTMLVideoElement;
68
+ const oldStream = v.srcObject as MediaStream;
69
+ const [oldA] = oldStream.getAudioTracks();
70
+ const [oldV] = oldStream.getVideoTracks();
71
+ await w.ep_webrtc.deactivate();
72
+ w.$('.audio-btn').click();
73
+ w.$('.video-btn').click();
74
+ await w.ep_webrtc.activate();
75
+ const v2 = document.querySelector('video') as HTMLVideoElement;
76
+ const stream = v2.srcObject as MediaStream;
77
+ const audio = stream.getAudioTracks()[0];
78
+ const video = stream.getVideoTracks()[0];
79
+ const aBtnOk = w.$('.audio-btn').hasClass('muted') === (audio == null || !audio.enabled);
80
+ const vBtnOk = w.$('.video-btn').hasClass('off') === (video == null || !video.enabled);
81
+ if (!aBtnOk || !vBtnOk) return {ok: false};
82
+ if (enabledOnStart) {
83
+ if (stream.getTracks().length !== 2) return {ok: false};
84
+ if (!stream.getTracks().every((t) => t.enabled)) return {ok: false};
85
+ } else if (stream.getTracks().some((t) => t.enabled)) {
86
+ return {ok: false};
87
+ }
88
+ const [newA] = stream.getAudioTracks();
89
+ const [newV] = stream.getVideoTracks();
90
+ if (enabledOnStart) {
91
+ if (newA === oldA) return {ok: false};
92
+ if (oldA.readyState !== 'ended') return {ok: false};
93
+ if (newA.readyState !== 'live') return {ok: false};
94
+ if (newV === oldV) return {ok: false};
95
+ if (oldV.readyState !== 'ended') return {ok: false};
96
+ if (newV.readyState !== 'live') return {ok: false};
97
+ }
98
+ return {ok: true};
99
+ }, enabledOnStart);
100
+ expect(ok.ok).toBe(true);
101
+ }
102
+ });
103
+
104
+ test('click, deactivate, activate', async ({page}) => {
105
+ test.setTimeout(60_000);
106
+ for (let i = 0; i < 10; ++i) {
107
+ const ok = await page.evaluate(async (enabledOnStart) => {
108
+ const w = window as any;
109
+ const v = document.querySelector('video') as HTMLVideoElement;
110
+ const oldStream = v.srcObject as MediaStream;
111
+ const [oldA] = oldStream.getAudioTracks();
112
+ const [oldV] = oldStream.getVideoTracks();
113
+ w.$('.audio-btn').click();
114
+ w.$('.video-btn').click();
115
+ await w.ep_webrtc.deactivate();
116
+ await w.ep_webrtc.activate();
117
+ const v2 = document.querySelector('video') as HTMLVideoElement;
118
+ const stream = v2.srcObject as MediaStream;
119
+ const audio = stream.getAudioTracks()[0];
120
+ const video = stream.getVideoTracks()[0];
121
+ const aBtnOk = w.$('.audio-btn').hasClass('muted') === (audio == null || !audio.enabled);
122
+ const vBtnOk = w.$('.video-btn').hasClass('off') === (video == null || !video.enabled);
123
+ if (!aBtnOk || !vBtnOk) return {ok: false};
124
+ if (enabledOnStart) {
125
+ if (stream.getTracks().length !== 2) return {ok: false};
126
+ if (!stream.getTracks().every((t) => t.enabled)) return {ok: false};
127
+ } else if (stream.getTracks().some((t) => t.enabled)) {
128
+ return {ok: false};
129
+ }
130
+ const [newA] = stream.getAudioTracks();
131
+ const [newV] = stream.getVideoTracks();
132
+ if (enabledOnStart) {
133
+ if (newA === oldA) return {ok: false};
134
+ if (oldA.readyState !== 'ended') return {ok: false};
135
+ if (newA.readyState !== 'live') return {ok: false};
136
+ if (newV === oldV) return {ok: false};
137
+ if (oldV.readyState !== 'ended') return {ok: false};
138
+ if (newV.readyState !== 'live') return {ok: false};
139
+ }
140
+ return {ok: true};
141
+ }, enabledOnStart);
142
+ expect(ok.ok).toBe(true);
143
+ }
144
+ });
145
+
146
+ test('deactivate, activate, click', async ({page}) => {
147
+ test.setTimeout(60_000);
148
+ for (let i = 0; i < 10; ++i) {
149
+ const ok = await page.evaluate(async (enabledOnStart) => {
150
+ const w = window as any;
151
+ const v = document.querySelector('video') as HTMLVideoElement;
152
+ const oldStream = v.srcObject as MediaStream;
153
+ const [oldA] = oldStream.getAudioTracks();
154
+ const [oldV] = oldStream.getVideoTracks();
155
+ await w.ep_webrtc.deactivate();
156
+ const p = w.ep_webrtc.activate();
157
+ // Wait for interface-container to be present (legacy waitForPromise).
158
+ const t0 = Date.now();
159
+ while (w.$('.interface-container').length !== 1) {
160
+ if (Date.now() - t0 > 2000) return {ok: false, reason: 'no interface'};
161
+ await new Promise((r) => setTimeout(r, 10));
162
+ }
163
+ w.$('.audio-btn').click();
164
+ w.$('.video-btn').click();
165
+ await Promise.all([
166
+ p,
167
+ w.$('.audio-btn').data('idle')('click'),
168
+ w.$('.video-btn').data('idle')('click'),
169
+ ]);
170
+ // assertTracks(!enabledOnStart)
171
+ const v2 = document.querySelector('video') as HTMLVideoElement;
172
+ const stream = v2.srcObject as MediaStream;
173
+ const audio = stream.getAudioTracks()[0];
174
+ const video = stream.getVideoTracks()[0];
175
+ const expectEnabled = !enabledOnStart;
176
+ if (expectEnabled) {
177
+ if (stream.getTracks().length !== 2) return {ok: false};
178
+ if (!stream.getTracks().every((t) => t.enabled)) return {ok: false};
179
+ } else if (stream.getTracks().some((t) => t.enabled)) {
180
+ return {ok: false};
181
+ }
182
+ if (w.$('.audio-btn').hasClass('muted') !== (audio == null || !audio.enabled)) return {ok: false};
183
+ if (w.$('.video-btn').hasClass('off') !== (video == null || !video.enabled)) return {ok: false};
184
+ const [newA] = stream.getAudioTracks();
185
+ const [newV] = stream.getVideoTracks();
186
+ if (newA === oldA) return {ok: false};
187
+ if (oldA != null && oldA.readyState !== 'ended') return {ok: false};
188
+ if (newA != null && newA.readyState !== 'live') return {ok: false};
189
+ if (newV === oldV) return {ok: false};
190
+ if (oldV != null && oldV.readyState !== 'ended') return {ok: false};
191
+ if (newV != null && newV.readyState !== 'live') return {ok: false};
192
+ return {ok: true};
193
+ }, enabledOnStart);
194
+ expect(ok.ok).toBe(true);
195
+ }
196
+ });
197
+
198
+ test('click while reactivate', async ({page}) => {
199
+ test.setTimeout(60_000);
200
+ for (let i = 0; i < 10; i++) {
201
+ const ok = await page.evaluate(async (enabledOnStart) => {
202
+ const w = window as any;
203
+ const v = document.querySelector('video') as HTMLVideoElement;
204
+ const oldStream = v.srcObject as MediaStream;
205
+ const [oldA] = oldStream.getAudioTracks();
206
+ const [oldV] = oldStream.getVideoTracks();
207
+
208
+ await w.ep_webrtc.deactivate();
209
+ const p = w.ep_webrtc.activate();
210
+ w.$('.audio-btn').click();
211
+ w.$('.video-btn').click();
212
+ await Promise.all([
213
+ p,
214
+ w.$('.audio-btn').data('idle')(),
215
+ w.$('.video-btn').data('idle')(),
216
+ ]);
217
+ const v2 = document.querySelector('video') as HTMLVideoElement;
218
+ const stream = v2.srcObject as MediaStream;
219
+ const audio = stream.getAudioTracks()[0];
220
+ const video = stream.getVideoTracks()[0];
221
+ const expectEnabled = !enabledOnStart;
222
+ if (expectEnabled) {
223
+ if (stream.getTracks().length !== 2) return {ok: false};
224
+ if (!stream.getTracks().every((t) => t.enabled)) return {ok: false};
225
+ } else if (stream.getTracks().some((t) => t.enabled)) {
226
+ return {ok: false};
227
+ }
228
+ if (w.$('.audio-btn').hasClass('muted') !== (audio == null || !audio.enabled)) return {ok: false};
229
+ if (w.$('.video-btn').hasClass('off') !== (video == null || !video.enabled)) return {ok: false};
230
+ const [newA] = stream.getAudioTracks();
231
+ const [newV] = stream.getVideoTracks();
232
+ if (!enabledOnStart) {
233
+ if (newA === oldA) return {ok: false};
234
+ if (oldA != null && oldA.readyState !== 'ended') return {ok: false};
235
+ if (newA != null && newA.readyState !== 'live') return {ok: false};
236
+ if (newV === oldV) return {ok: false};
237
+ if (oldV != null && oldV.readyState !== 'ended') return {ok: false};
238
+ if (newV != null && newV.readyState !== 'live') return {ok: false};
239
+ }
240
+ return {ok: true};
241
+ }, enabledOnStart);
242
+ expect(ok.ok).toBe(true);
243
+ }
244
+ });
245
+
246
+ test('many clicks', async ({page}) => {
247
+ test.setTimeout(60_000);
248
+ for (let i = 0; i < 10; ++i) {
249
+ const ok = await page.evaluate(async ({i, enabledOnStart}) => {
250
+ const w = window as any;
251
+ w.$('.audio-btn').click();
252
+ w.$('.audio-btn').click();
253
+ w.$('.audio-btn').click();
254
+ w.$('.video-btn').click();
255
+ w.$('.video-btn').click();
256
+ await Promise.all([
257
+ w.$('.audio-btn').data('idle')(),
258
+ w.$('.video-btn').data('idle')(),
259
+ ]);
260
+ const aMuted = w.$('.audio-btn').hasClass('muted');
261
+ const vOff = w.$('.video-btn').hasClass('off');
262
+ const wantA = ((i + 1) * 3 + (enabledOnStart ? 1 : 0)) % 2 === 0;
263
+ const wantV = ((i + 1) * 2 + (enabledOnStart ? 1 : 0)) % 2 === 0;
264
+ return aMuted === wantA && vOff === wantV;
265
+ }, {i, enabledOnStart});
266
+ expect(ok).toBe(true);
267
+ }
268
+ });
269
+ });
270
+ }
271
+ });