Skip to content

Commit 7aaca5b

Browse files
authored
Duck Player Native pausing bugs (#1705)
1 parent 5e16034 commit 7aaca5b

File tree

11 files changed

+278
-31
lines changed

11 files changed

+278
-31
lines changed

injected/integration-test/duckplayer-native.spec.js

Lines changed: 47 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,23 @@ test.describe('Duck Player Native thumbnail overlay', () => {
7676
await duckPlayer.didShowOverlay();
7777
await duckPlayer.didShowLogoInOverlay();
7878
});
79+
test('Does not duplicate overlay on repeated calls', async ({ page }, workerInfo) => {
80+
const duckPlayer = DuckPlayerNative.create(page, workerInfo);
81+
82+
// Given the duckPlayerNative feature is enabled
83+
await duckPlayer.withRemoteConfig();
84+
85+
// When I go to a YouTube page
86+
await duckPlayer.gotoYouTubePage();
87+
await duckPlayer.sendOnMediaControl();
88+
89+
// And the browser fires multiple pause messages
90+
await duckPlayer.sendOnMediaControl();
91+
await duckPlayer.sendOnMediaControl();
92+
93+
// Then I should see only one thumbnail overlay on the page
94+
await duckPlayer.overlayIsUnique();
95+
});
7996
test('Dismisses overlay on click', async ({ page }, workerInfo) => {
8097
const duckPlayer = DuckPlayerNative.create(page, workerInfo);
8198

@@ -101,6 +118,19 @@ test.describe('Duck Player Native thumbnail overlay', () => {
101118
});
102119

103120
test.describe('Duck Player Native custom error view', () => {
121+
test('Shows sign-in error', async ({ page }, workerInfo) => {
122+
const duckPlayer = DuckPlayerNative.create(page, workerInfo);
123+
124+
// Given the duckPlayerNative feature is enabled
125+
await duckPlayer.withRemoteConfig();
126+
127+
// When I go to a YouTube page with an age-restricted error
128+
await duckPlayer.gotoSignInErrorPage();
129+
130+
// Then I should see the generic error screen
131+
await duckPlayer.didShowSignInError();
132+
});
133+
104134
test('Shows age-restricted error', async ({ page }, workerInfo) => {
105135
const duckPlayer = DuckPlayerNative.create(page, workerInfo);
106136

@@ -111,19 +141,32 @@ test.describe('Duck Player Native custom error view', () => {
111141
await duckPlayer.gotoAgeRestrictedErrorPage();
112142

113143
// Then I should see the generic error screen
114-
await duckPlayer.didShowGenericError();
144+
await duckPlayer.didShowAgeRestrictedError();
115145
});
116146

117-
test('Shows sign-in error', async ({ page }, workerInfo) => {
147+
test('Shows no-embed error', async ({ page }, workerInfo) => {
118148
const duckPlayer = DuckPlayerNative.create(page, workerInfo);
119149

120150
// Given the duckPlayerNative feature is enabled
121151
await duckPlayer.withRemoteConfig();
122152

123153
// When I go to a YouTube page with an age-restricted error
124-
await duckPlayer.gotoSignInErrorPage();
154+
await duckPlayer.gotoNoEmbedErrorPage();
125155

126156
// Then I should see the generic error screen
127-
await duckPlayer.didShowSignInError();
157+
await duckPlayer.didShowNoEmbedError();
158+
});
159+
160+
test('Shows generic/unknown error', async ({ page }, workerInfo) => {
161+
const duckPlayer = DuckPlayerNative.create(page, workerInfo);
162+
163+
// Given the duckPlayerNative feature is enabled
164+
await duckPlayer.withRemoteConfig();
165+
166+
// When I go to a YouTube page with an age-restricted error
167+
await duckPlayer.gotoUnknownErrorPage();
168+
169+
// Then I should see the generic error screen
170+
await duckPlayer.didShowUnknownError();
128171
});
129172
});

injected/integration-test/page-objects/duckplayer-native.js

Lines changed: 39 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { ResultsCollector } from './results-collector.js';
55

66
/**
77
* @import { PageType } from '../../src/features/duckplayer-native/messages.js'
8-
* @typedef {"default" | "incremental-dom" | "age-restricted-error" | "sign-in-error"} PlayerPageVariants
8+
* @typedef {"default" | "incremental-dom" | "age-restricted-error" | "sign-in-error" | "no-embed-error" | "unknown-error"} PlayerPageVariants
99
*/
1010

1111
// eslint-disable-next-line @typescript-eslint/no-unused-vars
@@ -33,6 +33,7 @@ export class DuckPlayerNative {
3333
this.page = page;
3434
this.build = build;
3535
this.platform = platform;
36+
this.isMobile = platform.name === 'android' || platform.name === 'ios';
3637
this.collector = new ResultsCollector(page, build, platform);
3738
this.collector.withMockResponse({
3839
initialSetup: defaultInitialSetup,
@@ -68,6 +69,14 @@ export class DuckPlayerNative {
6869
await this.gotoPage('NOCOOKIE', { variant: 'age-restricted-error' });
6970
}
7071

72+
async gotoNoEmbedErrorPage() {
73+
await this.gotoPage('NOCOOKIE', { variant: 'no-embed-error' });
74+
}
75+
76+
async gotoUnknownErrorPage() {
77+
await this.gotoPage('NOCOOKIE', { variant: 'unknown-error' });
78+
}
79+
7180
async gotoSignInErrorPage() {
7281
await this.gotoPage('NOCOOKIE', { variant: 'sign-in-error' });
7382
}
@@ -85,7 +94,8 @@ export class DuckPlayerNative {
8594
async gotoPage(pageType, params = {}) {
8695
await this.withPageType(pageType);
8796

88-
const { variant = 'default', videoID = '123' } = params;
97+
const defaultVariant = this.isMobile ? 'mobile' : 'default';
98+
const { variant = defaultVariant, videoID = '123' } = params;
8999
const urlParams = new URLSearchParams([
90100
['v', videoID],
91101
['variant', variant],
@@ -236,6 +246,11 @@ export class DuckPlayerNative {
236246
await this.page.locator('ddg-video-thumbnail-overlay-mobile .logo').waitFor({ state: 'visible', timeout: 1000 });
237247
}
238248

249+
async overlayIsUnique() {
250+
const count = await this.page.locator('ddg-video-thumbnail-overlay-mobile').count();
251+
expect(count).toBe(1);
252+
}
253+
239254
async clickOnOverlay() {
240255
await this.page.locator('ddg-video-thumbnail-overlay-mobile').click();
241256
}
@@ -260,19 +275,35 @@ export class DuckPlayerNative {
260275

261276
/* Custom Error assertions */
262277

263-
async didShowGenericError() {
278+
async didShowAgeRestrictedError() {
279+
await expect(this.page.locator('ddg-video-error')).toMatchAriaSnapshot(`
280+
- heading "Sorry, this video is age-restricted" [level=1]
281+
- paragraph: To watch age-restricted videos, you need to sign in to YouTube to verify your age.
282+
- paragraph: You can still watch this video, but you’ll have to sign in and watch it on YouTube without the added privacy of Duck Player.
283+
`);
284+
}
285+
286+
async didShowNoEmbedError() {
287+
await expect(this.page.locator('ddg-video-error')).toMatchAriaSnapshot(`
288+
- heading "Sorry, this video can only be played on YouTube" [level=1]
289+
- paragraph: The creator of this video has chosen not to allow it to be viewed on other sites.
290+
- paragraph: You can still watch it on YouTube, but without the added privacy of Duck Player.
291+
`);
292+
}
293+
294+
async didShowUnknownError() {
264295
await expect(this.page.locator('ddg-video-error')).toMatchAriaSnapshot(`
265-
- heading "YouTube won’t let Duck Player load this video" [level=1]
266-
- paragraph: YouTube doesn’t allow this video to be viewed outside of YouTube.
296+
- heading "Duck Player can’t load this video" [level=1]
297+
- paragraph: This video can’t be viewed outside of YouTube.
267298
- paragraph: You can still watch this video on YouTube, but without the added privacy of Duck Player.
268299
`);
269300
}
270301

271302
async didShowSignInError() {
272303
await expect(this.page.locator('ddg-video-error')).toMatchAriaSnapshot(`
273-
- heading "YouTube won’t let Duck Player load this video" [level=1]
274-
- paragraph: YouTube is blocking this video from loading. If you’re using a VPN, try turning it off and reloading this page.
275-
- paragraph: If this doesn’t work, you can still watch this video on YouTube, but without the added privacy of Duck Player.
304+
- heading "Sorry, YouTube thinks you’re a bot" [level=1]
305+
- paragraph: This can happen if you’re using a VPN. Try turning the VPN off or switching server locations and reloading this page.
306+
- paragraph: If that doesn’t work, you’ll have to sign in and watch this video on YouTube without the added privacy of Duck Player.
276307
`);
277308
}
278309

injected/integration-test/test-pages/duckplayer-native/pages/player.html

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,50 @@
215215
</div>
216216
</template>
217217

218+
<template id="no-embed-error-template">
219+
<div class="ytp-error" role="alert" data-layer="4">
220+
<div class="ytp-error-content" style="padding-top: 395.5px;">
221+
<div class="ytp-error-icon-container"><svg fill="#fff" viewBox="0 0 48 48">
222+
<path d="M0 0h48v48H0V0z" fill="none"></path>
223+
<path
224+
d="M22 30h4v4h-4zm0-16h4v12h-4zm1.99-10C12.94 4 4 12.95 4 24s8.94 20 19.99 20S44 35.05 44 24 35.04 4 23.99 4zM24 40c-8.84 0-16-7.16-16-16S15.16 8 24 8s16 7.16 16 16-7.16 16-16 16z"
225+
fill-opacity="0.7"></path>
226+
</svg></div>
227+
<div class="ytp-error-content-wrap">
228+
<div class="ytp-error-content-wrap-reason"><span><span>The uploader has not made this video available in your country
229+
</span><a href="//support.google.com/youtube/answer/2802167?hl=en"
230+
target="_blank">Learn more</a></span></div>
231+
<div class="ytp-error-content-wrap-subreason">
232+
<div><a href="http://www.youtube.com/watch?v=PGaO6KUR9fk" target="TARGET_NEW_WINDOW">Watch on
233+
YouTube</a></div>
234+
</div>
235+
</div>
236+
</div>
237+
</div>
238+
</template>
239+
240+
<template id="unknown-error-template">
241+
<div class="ytp-error" role="alert" data-layer="4">
242+
<div class="ytp-error-content" style="padding-top: 395.5px;">
243+
<div class="ytp-error-icon-container"><svg fill="#fff" viewBox="0 0 48 48">
244+
<path d="M0 0h48v48H0V0z" fill="none"></path>
245+
<path
246+
d="M22 30h4v4h-4zm0-16h4v12h-4zm1.99-10C12.94 4 4 12.95 4 24s8.94 20 19.99 20S44 35.05 44 24 35.04 4 23.99 4zM24 40c-8.84 0-16-7.16-16-16S15.16 8 24 8s16 7.16 16 16-7.16 16-16 16z"
247+
fill-opacity="0.7"></path>
248+
</svg></div>
249+
<div class="ytp-error-content-wrap">
250+
<div class="ytp-error-content-wrap-reason"><span><span>This video isn't available anymore
251+
</span><a href="//support.google.com/youtube/answer/2802167?hl=en"
252+
target="_blank">Learn more</a></span></div>
253+
<div class="ytp-error-content-wrap-subreason">
254+
<div><a href="http://www.youtube.com/watch?v=PGaO6KUR9fk" target="TARGET_NEW_WINDOW">Watch on
255+
YouTube</a></div>
256+
</div>
257+
</div>
258+
</div>
259+
</div>
260+
</template>
261+
218262
<template id="sign-in-error-template">
219263
<div class="ytp-error" role="alert" data-layer="4">
220264
<div class="ytp-error-content" style="padding-top: 395.5px;">
@@ -322,9 +366,47 @@
322366
insertTemplateInMain('playlist-template');
323367
}, 200);
324368
setTimeout(() => {
369+
// Simulate conditions for this error in YouTube config object
370+
window.ytcfg = {
371+
get: () => ({
372+
embedded_player_response: JSON.stringify({
373+
previewPlayabilityStatus: { desktopLegacyAgeGateReason: 1, status: 'UNPLAYABLE' }
374+
})
375+
})
376+
};
325377
insertTemplateInVideo('age-restricted-error-template');
326378
}, 300);
327379
},
380+
"no-embed-error": () => {
381+
main.innerHTML += `<div class="container"><div id="player"></div></div>`
382+
setTimeout(() => {
383+
insertHTMLVideoPlayer();
384+
insertTemplateInMain('related-template');
385+
insertTemplateInMain('playlist-template');
386+
}, 200);
387+
setTimeout(() => {
388+
// Simulate conditions for this error in YouTube config object
389+
window.ytcfg = {
390+
get: () => ({
391+
embedded_player_response: JSON.stringify({
392+
previewPlayabilityStatus: { status: 'UNPLAYABLE' }
393+
})
394+
})
395+
};
396+
insertTemplateInVideo('no-embed-error-template');
397+
}, 300);
398+
},
399+
"unknown-error": () => {
400+
main.innerHTML += `<div class="container"><div id="player"></div></div>`
401+
setTimeout(() => {
402+
insertHTMLVideoPlayer();
403+
insertTemplateInMain('related-template');
404+
insertTemplateInMain('playlist-template');
405+
}, 200);
406+
setTimeout(() => {
407+
insertTemplateInVideo('unknown-error-template');
408+
}, 300);
409+
},
328410
"sign-in-error": () => {
329411
main.innerHTML += `<div class="container"><div id="player"></div></div>`
330412
setTimeout(() => {

injected/src/features/duck-player-native.js

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,16 @@ import { Logger } from './duckplayer/util.js';
1818
* @property {boolean} playbackPaused - Should video start playing or paused
1919
*/
2020

21+
/**
22+
* @import localeStrings from '../locales/duckplayer/en/native.json'
23+
* @typedef {(key: keyof localeStrings) => string} TranslationFn
24+
*/
25+
2126
export class DuckPlayerNativeFeature extends ContentFeature {
2227
/** @type {DuckPlayerNativeSubFeature | null} */
2328
currentPage;
29+
/** @type {TranslationFn} */
30+
t;
2431

2532
async init(args) {
2633
/**
@@ -42,10 +49,13 @@ export class DuckPlayerNativeFeature extends ContentFeature {
4249
locale,
4350
});
4451

52+
// Translation function to be used by view components
53+
this.t = (key) => env.strings('native.json')[key];
54+
4555
const messages = new DuckPlayerNativeMessages(this.messaging, env);
4656
messages.subscribeToURLChange(({ pageType }) => {
4757
const playbackPaused = false; // This can be added to the event data in the future if needed
48-
this.urlChanged(pageType, selectors, playbackPaused, env, messages);
58+
this.urlDidChange(pageType, selectors, playbackPaused, env, messages);
4959
});
5060

5161
/** @type {InitialSettings} */
@@ -60,7 +70,7 @@ export class DuckPlayerNativeFeature extends ContentFeature {
6070

6171
if (initialSetup.pageType) {
6272
const playbackPaused = initialSetup.playbackPaused || false;
63-
this.urlChanged(initialSetup.pageType, selectors, playbackPaused, env, messages);
73+
this.urlDidChange(initialSetup.pageType, selectors, playbackPaused, env, messages);
6474
}
6575
}
6676

@@ -72,7 +82,7 @@ export class DuckPlayerNativeFeature extends ContentFeature {
7282
* @param {Environment} env
7383
* @param {DuckPlayerNativeMessages} messages
7484
*/
75-
urlChanged(pageType, selectors, playbackPaused, env, messages) {
85+
urlDidChange(pageType, selectors, playbackPaused, env, messages) {
7686
/** @type {DuckPlayerNativeSubFeature | null} */
7787
let nextPage = null;
7888

@@ -83,7 +93,7 @@ export class DuckPlayerNativeFeature extends ContentFeature {
8393

8494
switch (pageType) {
8595
case 'NOCOOKIE':
86-
nextPage = setupDuckPlayerForNoCookie(selectors, env, messages);
96+
nextPage = setupDuckPlayerForNoCookie(selectors, env, messages, this.t);
8797
break;
8898
case 'YOUTUBE':
8999
nextPage = setupDuckPlayerForYouTube(selectors, playbackPaused, env, messages);

0 commit comments

Comments
 (0)