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.
@@ -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
+ });