ep_webrtc 2.5.37 → 2.5.39

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.
@@ -2,6 +2,12 @@ name: Dependabot Automerge
2
2
  permissions:
3
3
  contents: write
4
4
  pull-requests: write
5
+ # `actions: write` lets the post-merge step kick off Node.js Package on
6
+ # the default branch via `gh workflow run`. Without this, automerge'd
7
+ # PRs land on main but the on-push release job never fires (GitHub
8
+ # Actions intentionally suppresses on:push triggers when the push is
9
+ # authenticated with GITHUB_TOKEN).
10
+ actions: write
5
11
  on:
6
12
  workflow_run:
7
13
  workflows:
@@ -21,6 +27,7 @@ jobs:
21
27
  uses: actions/checkout@v6
22
28
 
23
29
  - name: Automerge
30
+ id: automerge
24
31
  uses: "pascalgn/automerge-action@v0.16.4"
25
32
  env:
26
33
  GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -28,3 +35,11 @@ jobs:
28
35
  MERGE_LABELS: ""
29
36
  MERGE_RETRY_SLEEP: "100000"
30
37
 
38
+ - name: Trigger release on default branch
39
+ # `pascalgn/automerge-action` exits 0 whether or not it merged. Skip
40
+ # the dispatch when nothing was actually merged so we don't kick a
41
+ # phantom release run on every Dependabot Automerge invocation.
42
+ if: steps.automerge.outputs.mergeResult == 'merged'
43
+ env:
44
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
45
+ run: gh workflow run test-and-release.yml --ref ${{ github.event.repository.default_branch }}
@@ -26,7 +26,7 @@ jobs:
26
26
  with:
27
27
  repository: ether/etherpad-lite
28
28
  path: etherpad-lite
29
- - uses: actions/setup-node@v4
29
+ - uses: actions/setup-node@v6
30
30
  name: Install Node.js
31
31
  with:
32
32
  node-version: 22
@@ -16,7 +16,7 @@ jobs:
16
16
  with:
17
17
  repository: ether/etherpad-lite
18
18
  path: etherpad-lite
19
- - uses: actions/setup-node@v4
19
+ - uses: actions/setup-node@v6
20
20
  name: Install Node.js
21
21
  with:
22
22
  node-version: 22
@@ -1,5 +1,11 @@
1
1
  name: Node.js Package
2
- on: [push]
2
+ on:
3
+ push:
4
+ # Invoked by automerge.yml after a Dependabot PR is merged. GitHub
5
+ # Actions doesn't fire on:push when the push is authored by GITHUB_TOKEN
6
+ # (the automerge action's only available identity), so without this
7
+ # dispatch trigger the release job never runs after auto-merges.
8
+ workflow_dispatch:
3
9
 
4
10
  # id-token: write must be granted here so the reusable npmpublish workflow
5
11
  # can request an OIDC token for npm trusted publishing.
package/package.json CHANGED
@@ -5,7 +5,7 @@
5
5
  "url": "git@github.com:ether/ep_webrtc.git",
6
6
  "type": "git"
7
7
  },
8
- "version": "2.5.37",
8
+ "version": "2.5.39",
9
9
  "description": "WebRTC based audio/video chat to Etherpad",
10
10
  "author": "John McLear <john@mclear.co.uk>",
11
11
  "contributors": [],
@@ -1061,12 +1061,28 @@ exports.rtc = new class {
1061
1061
  click: async () => {
1062
1062
  const screenshareEnabled = !this._selfViewButtons.screenshare.enabled;
1063
1063
  _debug(`button clicked to ${screenshareEnabled ? 'en' : 'dis'}able screen sharing`);
1064
+ // Capture the previous camera state so we can restore it if
1065
+ // the screen-picker is canceled (getDisplayMedia rejects).
1066
+ // Without this, clicking screenshare → cancel turns the
1067
+ // camera off permanently from the user's perspective and
1068
+ // leaves a stale disabled camera track in the local stream.
1069
+ const wasCameraOn = this._selfViewButtons.video.enabled;
1064
1070
  // Unconditionally disable the camera. Either screen sharing was previously disabled in
1065
1071
  // which case the user now wants to share the screen, or screen sharing was previously
1066
1072
  // enabled in which case the user wants to shut off all video.
1067
1073
  this._selfViewButtons.video.enabled = false;
1068
1074
  this._selfViewButtons.screenshare.enabled = screenshareEnabled;
1069
1075
  await this.updateLocalTracks({updateVideo: true});
1076
+ // If the user wanted to start screensharing but the picker
1077
+ // was canceled or denied, updateLocalTracks has flipped
1078
+ // screenshare.enabled back to false. Bring the camera back
1079
+ // to the state it was in before the click so the user isn't
1080
+ // left with no video at all.
1081
+ if (screenshareEnabled && wasCameraOn &&
1082
+ !this._selfViewButtons.screenshare.enabled) {
1083
+ this._selfViewButtons.video.enabled = true;
1084
+ await this.updateLocalTracks({updateVideo: true});
1085
+ }
1070
1086
  // Don't use `await` here -- see the comment for the audio button click handler above.
1071
1087
  this.unmuteAndPlayAll();
1072
1088
  },
@@ -0,0 +1,106 @@
1
+ import {expect, test} from '@playwright/test';
2
+ import {goToNewPadWithParams, installFakeGetUserMedia, setPadPrefsCookie} from '../helper/utils';
3
+
4
+ test.describe('screen share button', () => {
5
+ test.beforeEach(async ({page, context}) => {
6
+ test.setTimeout(60_000);
7
+ await context.clearCookies();
8
+ await setPadPrefsCookie(page, {
9
+ rtcEnabled: false,
10
+ audioEnabledOnStart: false,
11
+ videoEnabledOnStart: true,
12
+ });
13
+ await goToNewPadWithParams(page, {});
14
+ await installFakeGetUserMedia(page);
15
+ // Stub getDisplayMedia onto navigator.mediaDevices so the
16
+ // screenshare-btn doesn't get hidden by addInterface (it checks
17
+ // typeof navigator.mediaDevices.getDisplayMedia === 'function').
18
+ // Tests below override this stub per-case to control behavior.
19
+ await page.evaluate(() => {
20
+ const w = window as any;
21
+ w.__getDisplayMediaCallCount = 0;
22
+ w.navigator.mediaDevices.getDisplayMedia = async () => {
23
+ w.__getDisplayMediaCallCount++;
24
+ const err: any = new Error('user cancelled the picker');
25
+ err.name = 'NotAllowedError';
26
+ throw err;
27
+ };
28
+ });
29
+ await page.evaluate(() => (window as any).ep_webrtc.activate());
30
+ await page.waitForFunction(
31
+ () => (window as any).$('#rtcbox').data('initialized'));
32
+ // Confirm the starting state: camera on, screenshare off.
33
+ await page.waitForFunction(() => {
34
+ const w = window as any;
35
+ const videoBtn = w.$('.video-btn');
36
+ const ssBtn = w.$('.screenshare-btn');
37
+ return videoBtn.length === 1 && !videoBtn.hasClass('off') &&
38
+ ssBtn.length === 1 && ssBtn.hasClass('off');
39
+ }, undefined, {timeout: 5000});
40
+ });
41
+
42
+ test('canceling the screen-picker restores the camera', async ({page}) => {
43
+ // Click the screenshare button. Our stubbed getDisplayMedia
44
+ // rejects with NotAllowedError (the same error Chrome dispatches
45
+ // when the user clicks Cancel in the picker).
46
+ await page.evaluate(() => (window as any).$('.screenshare-btn').click());
47
+ await page.evaluate(
48
+ () => (window as any).$('.screenshare-btn').data('idle')('click'));
49
+ // The picker really fired.
50
+ const callCount = await page.evaluate(
51
+ () => (window as any).__getDisplayMediaCallCount);
52
+ expect(callCount).toBeGreaterThanOrEqual(1);
53
+ // Final state: camera is back on, screenshare is off.
54
+ const finalState = await page.evaluate(() => {
55
+ const w = window as any;
56
+ const videoBtn = w.$('.video-btn');
57
+ const ssBtn = w.$('.screenshare-btn');
58
+ return {
59
+ videoOff: videoBtn.hasClass('off'),
60
+ screenshareOff: ssBtn.hasClass('off'),
61
+ };
62
+ });
63
+ expect(finalState).toEqual({videoOff: false, screenshareOff: true});
64
+ // And there's a live, enabled video track in the local stream so
65
+ // the user actually sees their camera again (not just a button
66
+ // toggled on with no underlying media).
67
+ const trackState = await page.evaluate(() => {
68
+ const w = window as any;
69
+ const v = document.querySelector('video') as HTMLVideoElement | null;
70
+ const stream = v && (v.srcObject as MediaStream | null);
71
+ const t = stream && stream.getVideoTracks()[0];
72
+ return t == null ? null : {enabled: t.enabled, readyState: t.readyState};
73
+ });
74
+ expect(trackState).toEqual({enabled: true, readyState: 'live'});
75
+ });
76
+
77
+ test('canceling screen-picker when camera was off leaves both off', async ({page}) => {
78
+ // Turn camera off first.
79
+ await page.evaluate(() => (window as any).$('.video-btn').click());
80
+ await page.evaluate(
81
+ () => (window as any).$('.video-btn').data('idle')('click'));
82
+ // Sanity-check both buttons off, no live enabled video track.
83
+ const before = await page.evaluate(() => {
84
+ const w = window as any;
85
+ return {
86
+ videoOff: w.$('.video-btn').hasClass('off'),
87
+ screenshareOff: w.$('.screenshare-btn').hasClass('off'),
88
+ };
89
+ });
90
+ expect(before).toEqual({videoOff: true, screenshareOff: true});
91
+ // Click screenshare → cancel.
92
+ await page.evaluate(() => (window as any).$('.screenshare-btn').click());
93
+ await page.evaluate(
94
+ () => (window as any).$('.screenshare-btn').data('idle')('click'));
95
+ // Both should remain off (don't auto-enable the camera the user
96
+ // had explicitly turned off).
97
+ const after = await page.evaluate(() => {
98
+ const w = window as any;
99
+ return {
100
+ videoOff: w.$('.video-btn').hasClass('off'),
101
+ screenshareOff: w.$('.screenshare-btn').hasClass('off'),
102
+ };
103
+ });
104
+ expect(after).toEqual({videoOff: true, screenshareOff: true});
105
+ });
106
+ });