Skip to content

Commit 58c09e3

Browse files
author
Shane Osbourne
committed
fix(duckplayer): fix overlays on shorts
1 parent 1014a0f commit 58c09e3

File tree

8 files changed

+186
-13
lines changed

8 files changed

+186
-13
lines changed
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { test } from '@playwright/test'
2+
import { DuckplayerOverlays } from './page-objects/duckplayer-overlays.js'
3+
4+
test.describe('e2e: Duck Player Thumbnail Overlays on YouTube.com', () => {
5+
test('e2e: Overlays never appear on "shorts"', async ({ page }, workerInfo) => {
6+
// @ts-expect-error - TS doesn't know about the "use.e2e" property
7+
workerInfo.skip(!workerInfo.project.use?.e2e)
8+
9+
const overlays = DuckplayerOverlays.create(page, workerInfo)
10+
11+
await overlays.overlaysEnabled({ json: 'overlays-live' })
12+
await overlays.gotoYoutubeHomepage()
13+
14+
// Ensure the hover works normally to prevent false positives
15+
await overlays.hoverAYouTubeThumbnail()
16+
await overlays.isVisible()
17+
18+
// now ensure the hover doesn't work on shorts
19+
await overlays.hoverShort()
20+
await overlays.overlaysDontShow()
21+
})
22+
})

integration-test/playwright/duckplayer.spec.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,19 @@ test.describe('Duck Player Thumbnail Overlays on YouTube.com', () => {
1515
// Then our overlay shows
1616
await overlays.isVisible()
1717
})
18+
test('Overlays never appear on "shorts"', async ({ page }, workerInfo) => {
19+
const overlays = DuckplayerOverlays.create(page, workerInfo)
20+
await overlays.overlaysEnabled()
21+
await overlays.gotoThumbsPage()
22+
23+
// Ensure the hover works normally to prevent false positives
24+
await overlays.hoverAThumbnail()
25+
await overlays.isVisible()
26+
27+
// now ensure the hover doesn't work on shorts
28+
await overlays.hoverShort()
29+
await overlays.overlaysDontShow()
30+
})
1831
test('Overlays don\'t show on thumbnails when disabled', async ({ page }, workerInfo) => {
1932
const overlays = DuckplayerOverlays.create(page, workerInfo)
2033

integration-test/playwright/page-objects/duckplayer-overlays.js

Lines changed: 40 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,12 @@ export class DuckplayerOverlays {
5454
await this.page.goto(this.overlaysPage)
5555
}
5656

57+
async gotoYoutubeHomepage () {
58+
await this.page.goto('https://www.youtube.com')
59+
// cookie banner
60+
await this.page.getByRole('button', { name: 'Reject the use of cookies and other data for the purposes described' }).click()
61+
}
62+
5763
/**
5864
* @param {object} [params]
5965
* @param {"default" | "incremental-dom"} [params.variant]
@@ -100,9 +106,14 @@ export class DuckplayerOverlays {
100106
await this.page.getByRole('link', { name: 'Duck Player', exact: true }).waitFor({ state: 'attached' })
101107
}
102108

103-
// Given the "overlays" feature is enabled
104-
async overlaysEnabled () {
105-
await this.setup({ config: loadConfig('overlays') })
109+
/**
110+
* @param {object} [params]
111+
* @param {'overlays' | 'overlays-live'} [params.json="overlays"] - default is settings for localhost
112+
*/
113+
async overlaysEnabled (params = {}) {
114+
const { json = 'overlays' } = params
115+
116+
await this.setup({ config: loadConfig(json) })
106117
}
107118

108119
async serpProxyEnabled () {
@@ -156,6 +167,16 @@ export class DuckplayerOverlays {
156167
await this.page.locator('.thumbnail[href="/watch?v=1"]').first().hover()
157168
}
158169

170+
async hoverAYouTubeThumbnail () {
171+
await this.page.locator('a.ytd-thumbnail[href^="/watch"]').first().hover({ force: true })
172+
}
173+
174+
async hoverShort () {
175+
// this should auto-wait for our test code to modify the DOM like YouTube does
176+
await this.page.getByRole('heading', { name: 'Shorts', exact: true }).scrollIntoViewIfNeeded()
177+
await this.page.locator('a[href*="/shorts"]').first().hover({ force: true })
178+
}
179+
159180
async clickDDGOverlay () {
160181
await this.hoverAThumbnail()
161182
await this.page.locator('.ddg-play-privately').click({ force: true })
@@ -166,7 +187,21 @@ export class DuckplayerOverlays {
166187
}
167188

168189
async overlaysDontShow () {
169-
expect(await this.page.locator('.ddg-overlay.ddg-overlay-hover').count()).toEqual(0)
190+
const elements = await this.page.locator('.ddg-overlay.ddg-overlay-hover').count()
191+
192+
// if the element exists, assert that it is hidden
193+
if (elements > 0) {
194+
const style = await this.page.evaluate(() => {
195+
const div = /** @type {HTMLDivElement|null} */(document.querySelector('.ddg-overlay.ddg-overlay-hover'))
196+
if (div) {
197+
return div.style.display
198+
}
199+
return ''
200+
})
201+
expect(style).toEqual('none')
202+
}
203+
204+
// if we get here, the element was absent
170205
}
171206

172207
async watchInDuckPlayer () {
@@ -316,7 +351,7 @@ export class DuckplayerOverlays {
316351
}
317352

318353
/**
319-
* @param {"overlays"} name
354+
* @param {"overlays" | "overlays-live"} name
320355
* @return {Record<string, any>}
321356
*/
322357
function loadConfig (name) {
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
{
2+
"unprotectedTemporary": [],
3+
"features": {
4+
"duckPlayer": {
5+
"state": "enabled",
6+
"exceptions": [],
7+
"settings": {
8+
"overlays": {
9+
"youtube": {
10+
"state": "disabled"
11+
},
12+
"serpProxy": {
13+
"state": "disabled"
14+
}
15+
},
16+
"domains": [
17+
{
18+
"domain": "youtube.com",
19+
"patchSettings": [
20+
{
21+
"op": "replace",
22+
"path": "/overlays/youtube/state",
23+
"value": "enabled"
24+
}
25+
]
26+
},
27+
{
28+
"domain": "duckduckgo.com",
29+
"patchSettings": [
30+
{
31+
"op": "replace",
32+
"path": "/overlays/serpProxy/state",
33+
"value": "enabled"
34+
}
35+
]
36+
}
37+
]
38+
}
39+
}
40+
}
41+
}

integration-test/test-pages/duckplayer/pages/overlays.html

Lines changed: 54 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,22 +6,34 @@
66
<title>Runtime checks</title>
77
<link rel="stylesheet" href="../../shared/style.css">
88
<style>
9+
.thumbnails {
10+
display: grid;
11+
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
12+
}
913
.thumbnail {
10-
height: 100px;
11-
width: 200px;
1214
margin: 5px;
1315
background: lightblue;
1416
border: 1px solid black;
15-
float: left;
17+
aspect-ratio: 16/9;
18+
display: block;
1619
}
1720

18-
.thumbnail img {
21+
.thumbnail img, .short img {
1922
width: 100%;
2023
height: 100%;
24+
object-fit: cover;
25+
display: block;
26+
}
27+
28+
.short {
29+
margin: 5px;
30+
background: lightblue;
31+
border: 1px solid black;
32+
aspect-ratio: 9/16;
33+
display: block;
2134
}
2235

2336
.playlist {
24-
clear: both;
2537
width: 100px;
2638

2739
}
@@ -37,8 +49,6 @@
3749
}
3850

3951
#loaded {
40-
clear: both;
41-
float: none;
4252
width: 300px;
4353
}
4454

@@ -81,7 +91,44 @@ <h2>MORE THUMBNAILS</h2>
8191
</div>
8292
</div>
8393

94+
<h2>Shorts</h2>
95+
<div class="thumbnails">
96+
<a class="ytd-thumbnail short" href="/watch?v=1"><img src="thumbnail-dark.jpg"></a>
97+
<a class="ytd-thumbnail short" href="/watch?v=2"><img src="thumbnail-light.jpg"></a>
98+
<a class="ytd-thumbnail short" href="/watch?v=3"><img src="thumbnail-dark.jpg"></a>
99+
<a class="ytd-thumbnail short" href="/watch?v=4"><img src="thumbnail-light.jpg"></a>
100+
<a class="ytd-thumbnail short" href="/watch?v=5"><img src="thumbnail-dark.jpg"></a>
101+
</div>
102+
84103
<div id="loaded"></div>
85104

105+
<script type="module">
106+
const variant = new URLSearchParams(window.location.search).get('variant') || 'default';
107+
108+
const knownVariants = {
109+
"default": () => {
110+
setTimeout(() => {
111+
/**
112+
* This will change '/watch?v=3' links to '/shorts/3'
113+
* It roughly mimics the behaviour of the YouTube Homepage
114+
*/
115+
const shorts = document.querySelectorAll('.short');
116+
shorts.forEach((short) => {
117+
const params = new URLSearchParams(short.search);
118+
short.pathname = "/shorts/" + params.get('v');
119+
short.search = ''
120+
})
121+
}, 200);
122+
},
123+
}
124+
125+
if (variant in knownVariants) {
126+
console.log('executing page variant', variant)
127+
knownVariants[variant]();
128+
} else {
129+
console.warn('variant not found', variant)
130+
}
131+
132+
</script>
86133
</body>
87134
</html>

playwright.config.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,13 @@ import { defineConfig, devices } from '@playwright/test'
22

33
export default defineConfig({
44
projects: [
5+
{
6+
name: 'duckplayer-e2e',
7+
testMatch: [
8+
'integration-test/playwright/duckplayer.e2e.spec.js'
9+
],
10+
use: { injectName: 'windows', platform: 'windows', e2e: process.env.E2E }
11+
},
512
{
613
name: 'windows',
714
testMatch: [

src/features/duck-player.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
* For example, to enable the Duck Player Overlay on YouTube, the following config is used:
2828
*
2929
* ```json
30-
* [[include:integration-test/test-pages/duckplayer/config/overlays.json]]```
30+
* [[include:integration-test/test-pages/duckplayer/config/overlays-live.json]]```
3131
*
3232
*/
3333
import ContentFeature from '../content-feature.js'

src/features/duckplayer/overlays.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,15 @@ export async function initOverlays (environment, comms) {
113113
*/
114114
bindEvents: (video) => {
115115
if (video) {
116+
const before = video.href
116117
addTrustedEventListener(video, 'mouseover', () => {
118+
// don't allow the hover overlay to be shown if the video has changed
119+
// this can occur with 'shorts'
120+
if (video.href !== before) {
121+
if (!VideoThumbnail.isSingleVideoURL(video.href)) {
122+
return console.log('bailing because the video has changed')
123+
}
124+
}
117125
IconOverlay.moveHoverOverlayToVideoElement(video)
118126
})
119127

0 commit comments

Comments
 (0)