ep_webrtc 2.5.35 → 2.5.37
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/backend-tests.yml +4 -0
- package/.github/workflows/frontend-tests.yml +19 -41
- package/package.json +1 -1
- package/static/css/styles.css +19 -0
- package/static/js/index.js +56 -2
- package/static/tests/frontend-new/helper/utils.ts +145 -0
- package/static/tests/frontend-new/specs/audio_video_on_start.spec.ts +37 -0
- package/static/tests/frontend-new/specs/checkbox.spec.ts +154 -0
- package/static/tests/frontend-new/specs/enable_disable.spec.ts +81 -0
- package/static/tests/frontend-new/specs/errors.spec.ts +165 -0
- package/static/tests/frontend-new/specs/interface_buttons.spec.ts +271 -0
- package/static/tests/frontend-new/specs/race_conditions.spec.ts +301 -0
- package/static/tests/frontend-new/specs/setStream.spec.ts +185 -0
- package/static/tests/frontend/specs/audio_video_on_start.js +0 -35
- package/static/tests/frontend/specs/checkbox.js +0 -107
- package/static/tests/frontend/specs/enable_disable.js +0 -50
- package/static/tests/frontend/specs/errors.js +0 -58
- package/static/tests/frontend/specs/interface_buttons.js +0 -251
- package/static/tests/frontend/specs/race_conditions.js +0 -206
- package/static/tests/frontend/specs/setStream.js +0 -138
- package/static/tests/frontend/utils.js +0 -44
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import {expect, test} from '@playwright/test';
|
|
2
|
+
import {goToNewPadWithParams} from '../helper/utils';
|
|
3
|
+
|
|
4
|
+
const testCases: Array<[string, string]> = [
|
|
5
|
+
// Hard to test the version of NotAllowedError that is the SSL error
|
|
6
|
+
// because it requires changing window.location
|
|
7
|
+
['NotAllowedError', 'Failed to get permission to access'],
|
|
8
|
+
['NotFoundError', 'Failed to access'],
|
|
9
|
+
['NotReadableError', 'hardware error occurred'],
|
|
10
|
+
['AbortError', 'not a hardware error'],
|
|
11
|
+
];
|
|
12
|
+
|
|
13
|
+
test.describe('error handling', () => {
|
|
14
|
+
test.describe.configure({mode: 'serial'});
|
|
15
|
+
let sharedPage: import('@playwright/test').Page;
|
|
16
|
+
let videoBtnSelector: string;
|
|
17
|
+
|
|
18
|
+
test.beforeAll(async ({browser}) => {
|
|
19
|
+
sharedPage = await browser.newPage();
|
|
20
|
+
test.setTimeout(60_000);
|
|
21
|
+
// ep_webrtc's postAceInit checks enumerateDevices() and skips auto-
|
|
22
|
+
// activation when no audio/video device is visible. This test
|
|
23
|
+
// exercises the error-toast flow which needs the .video-btn button
|
|
24
|
+
// (created by activate → addInterface) to exist, so install a fake
|
|
25
|
+
// mediaDevices BEFORE the pad loads so auto-activation runs.
|
|
26
|
+
// Each test below patches getUserMedia separately to throw the
|
|
27
|
+
// specific DOMException it wants to verify.
|
|
28
|
+
await sharedPage.addInitScript(() => {
|
|
29
|
+
const w = window as any;
|
|
30
|
+
w.navigator.mediaDevices.enumerateDevices = async () => [
|
|
31
|
+
{kind: 'audioinput', deviceId: 'fake-audio', groupId: 'fake', label: 'fake mic'},
|
|
32
|
+
{kind: 'videoinput', deviceId: 'fake-video', groupId: 'fake', label: 'fake cam'},
|
|
33
|
+
];
|
|
34
|
+
w.navigator.mediaDevices.getUserMedia = async () => new MediaStream();
|
|
35
|
+
});
|
|
36
|
+
await goToNewPadWithParams(sharedPage, {
|
|
37
|
+
av: true,
|
|
38
|
+
webrtcaudioenabled: false,
|
|
39
|
+
webrtcvideoenabled: false,
|
|
40
|
+
});
|
|
41
|
+
await sharedPage.waitForFunction(
|
|
42
|
+
() => (window as any).$('#rtcbox').data('initialized'));
|
|
43
|
+
const ownInterfaceId: string = await sharedPage.evaluate(() => {
|
|
44
|
+
const w = window as any;
|
|
45
|
+
const ownUserId = w.ep_webrtc.getUserId();
|
|
46
|
+
const ownVideoId = `video_${ownUserId.replace(/\./g, '_')}`;
|
|
47
|
+
return `interface_${ownVideoId}`;
|
|
48
|
+
});
|
|
49
|
+
videoBtnSelector = `#${ownInterfaceId} .video-btn`;
|
|
50
|
+
// Save the original getUserMedia for restore in afterAll.
|
|
51
|
+
await sharedPage.evaluate(() => {
|
|
52
|
+
const w = window as any;
|
|
53
|
+
w.__getUserMediaBackup = w.navigator.mediaDevices.getUserMedia;
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test.afterAll(async () => {
|
|
58
|
+
await sharedPage.evaluate(() => {
|
|
59
|
+
const w = window as any;
|
|
60
|
+
w.navigator.mediaDevices.getUserMedia = w.__getUserMediaBackup;
|
|
61
|
+
});
|
|
62
|
+
await sharedPage.close();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test.beforeEach(async () => {
|
|
66
|
+
// No idea why but this needs to be called twice to actually make
|
|
67
|
+
// #gritter-container hidden.
|
|
68
|
+
await sharedPage.evaluate(() => {
|
|
69
|
+
const w = window as any;
|
|
70
|
+
// gritter is exposed via jQuery in modern Etherpad rather than as
|
|
71
|
+
// a bare window.gritter global. Fall back to manual DOM cleanup
|
|
72
|
+
// if neither is present so the beforeEach never throws. Remove
|
|
73
|
+
// the entire #gritter-container so the next gritter.add cleanly
|
|
74
|
+
// re-creates it via _verifyWrapper — leaving the container in
|
|
75
|
+
// place leaks .gritter-title elements from earlier tests and
|
|
76
|
+
// confuses the title-html assertion below.
|
|
77
|
+
const gritter = w.gritter || (w.$ && w.$.gritter);
|
|
78
|
+
if (gritter && typeof gritter.removeAll === 'function') {
|
|
79
|
+
gritter.removeAll({fade: false});
|
|
80
|
+
gritter.removeAll({fade: false});
|
|
81
|
+
}
|
|
82
|
+
document.querySelectorAll('#gritter-container').forEach((el) => el.remove());
|
|
83
|
+
});
|
|
84
|
+
const off = await sharedPage.locator(videoBtnSelector).evaluate(
|
|
85
|
+
(el) => el.classList.contains('off'));
|
|
86
|
+
expect(off).toBe(true);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
for (const [errName, checkString] of testCases) {
|
|
90
|
+
test(errName, async () => {
|
|
91
|
+
await sharedPage.evaluate((errName) => {
|
|
92
|
+
const w = window as any;
|
|
93
|
+
w.navigator.mediaDevices.getUserMedia = async () => {
|
|
94
|
+
const err: any = new Error();
|
|
95
|
+
err.name = errName;
|
|
96
|
+
throw err;
|
|
97
|
+
};
|
|
98
|
+
}, errName);
|
|
99
|
+
await sharedPage.waitForFunction(() => {
|
|
100
|
+
const w = window as any;
|
|
101
|
+
return w.$('#gritter-container:visible').length === 0;
|
|
102
|
+
}, undefined, {timeout: 1000});
|
|
103
|
+
await sharedPage.locator(videoBtnSelector).evaluate((el) => {
|
|
104
|
+
(window as any).$(el).click();
|
|
105
|
+
});
|
|
106
|
+
await sharedPage.waitForFunction(() => {
|
|
107
|
+
const w = window as any;
|
|
108
|
+
return w.$('#gritter-container:visible').length === 1;
|
|
109
|
+
}, undefined, {timeout: 1000});
|
|
110
|
+
const titleHtml = await sharedPage.evaluate(
|
|
111
|
+
() => (window as any).$('.gritter-title').html());
|
|
112
|
+
expect(titleHtml).toBe('Error');
|
|
113
|
+
const contentHtml = await sharedPage.evaluate(
|
|
114
|
+
() => (window as any).$('.gritter-content p').html());
|
|
115
|
+
expect(contentHtml).toContain(checkString);
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
test('gritter container does not intercept editor clicks', async () => {
|
|
120
|
+
// Regression: a sticky "Failed to access camera/microphone" toast used
|
|
121
|
+
// to span the top of the page and block all pointer events on the
|
|
122
|
+
// editor underneath, which made the pad unusable in production and
|
|
123
|
+
// broke every Playwright test that clicked into the editor.
|
|
124
|
+
await sharedPage.evaluate(() => {
|
|
125
|
+
const w = window as any;
|
|
126
|
+
w.navigator.mediaDevices.getUserMedia = async () => {
|
|
127
|
+
const err: any = new Error();
|
|
128
|
+
err.name = 'NotFoundError';
|
|
129
|
+
throw err;
|
|
130
|
+
};
|
|
131
|
+
});
|
|
132
|
+
await sharedPage.locator(videoBtnSelector).evaluate((el) => {
|
|
133
|
+
(window as any).$(el).click();
|
|
134
|
+
});
|
|
135
|
+
await sharedPage.waitForFunction(() => {
|
|
136
|
+
const w = window as any;
|
|
137
|
+
return w.$('#gritter-container:visible').length === 1;
|
|
138
|
+
}, undefined, {timeout: 1000});
|
|
139
|
+
// Verify pointer-events is none on the container AND on the toast
|
|
140
|
+
// subtree (.gritter-item, .gritter-content, the <p> with the error
|
|
141
|
+
// text). The previous fix only set the container itself to none and
|
|
142
|
+
// restored auto on .gritter-item — that left the actual toast
|
|
143
|
+
// subtree intercepting pointer events, which is what made
|
|
144
|
+
// Playwright report `<p>Failed to access...</p> from <#gritter-container>
|
|
145
|
+
// subtree intercepts pointer events` in CI.
|
|
146
|
+
const subtreePointerEvents = await sharedPage.evaluate(() => ({
|
|
147
|
+
container: getComputedStyle(document.querySelector('#gritter-container')!).pointerEvents,
|
|
148
|
+
item: getComputedStyle(document.querySelector('#gritter-container .gritter-item')!).pointerEvents,
|
|
149
|
+
content: getComputedStyle(document.querySelector('#gritter-container .gritter-content')!).pointerEvents,
|
|
150
|
+
close: getComputedStyle(document.querySelector('#gritter-container .gritter-close')!).pointerEvents,
|
|
151
|
+
}));
|
|
152
|
+
expect(subtreePointerEvents).toMatchObject({
|
|
153
|
+
container: 'none',
|
|
154
|
+
item: 'none',
|
|
155
|
+
content: 'none',
|
|
156
|
+
close: 'auto',
|
|
157
|
+
});
|
|
158
|
+
// Click the editor at the location overlaid by the gritter toast.
|
|
159
|
+
// Without the fix Playwright's stability check sees the toast
|
|
160
|
+
// subtree intercepting and times out at 90s.
|
|
161
|
+
const innerFrame = sharedPage.frame('ace_inner');
|
|
162
|
+
if (!innerFrame) throw new Error('ace_inner frame missing');
|
|
163
|
+
await innerFrame.locator('#innerdocbody').first().click({timeout: 5000});
|
|
164
|
+
});
|
|
165
|
+
});
|
|
@@ -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
|
+
});
|