Skip to content

Commit 64565d1

Browse files
committed
Duck Player Native tests
1 parent d6bf0d8 commit 64565d1

File tree

10 files changed

+248
-17
lines changed

10 files changed

+248
-17
lines changed
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { test } from '@playwright/test';
2+
import { DuckPlayerNative } from './page-objects/duckplayer-native.js';
3+
4+
test.describe('Duck Player Native messaging', () => {
5+
test('Calls initial setup', async ({ page }, workerInfo) => {
6+
const duckPlayer = DuckPlayerNative.create(page, workerInfo);
7+
8+
// Given the duckPlayerNative feature is enabled
9+
await duckPlayer.withRemoteConfig();
10+
11+
// When I go to a YouTube page
12+
await duckPlayer.gotoYouTubePage();
13+
14+
// Then Initial Setup should be called
15+
await duckPlayer.didSendInitialHandshake();
16+
});
17+
18+
test('Polls timestamp on YouTube', async ({ page }, workerInfo) => {
19+
const duckPlayer = DuckPlayerNative.create(page, workerInfo);
20+
21+
// Given the duckPlayerNative feature is enabled
22+
await duckPlayer.withRemoteConfig();
23+
24+
// When I go to a YouTube page
25+
await duckPlayer.gotoYouTubePage();
26+
27+
// Then the current timestamp should be polled back to the browser
28+
await duckPlayer.didSendCurrentTimestamp();
29+
});
30+
});
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
import { readFileSync } from 'fs';
2+
import { expect } from '@playwright/test';
3+
import { perPlatform } from '../type-helpers.mjs';
4+
import { ResultsCollector } from './results-collector.js';
5+
6+
/**
7+
* @import { PageType} from '../../src/features/duck-player-native.js'
8+
*/
9+
10+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
11+
const configFiles = /** @type {const} */ (['native.json']);
12+
13+
export class DuckPlayerNative {
14+
/** @type {Partial<Record<PageType, string>>} */
15+
pages = {
16+
YOUTUBE: '/duckplayer-native/pages/player.html',
17+
};
18+
19+
/**
20+
* @param {import("@playwright/test").Page} page
21+
* @param {import("../type-helpers.mjs").Build} build
22+
* @param {import("@duckduckgo/messaging/lib/test-utils.mjs").PlatformInfo} platform
23+
*/
24+
constructor(page, build, platform) {
25+
this.page = page;
26+
this.build = build;
27+
this.platform = platform;
28+
this.collector = new ResultsCollector(page, build, platform);
29+
this.collector.withMockResponse({
30+
initialSetup: {
31+
locale: 'en',
32+
},
33+
onCurrentTimestamp: {},
34+
});
35+
this.collector.withUserPreferences({
36+
messageSecret: 'ABC',
37+
javascriptInterface: 'javascriptInterface',
38+
messageCallback: 'messageCallback',
39+
});
40+
page.on('console', (msg) => {
41+
console.log(msg.type(), msg.text());
42+
});
43+
}
44+
45+
async reducedMotion() {
46+
await this.page.emulateMedia({ reducedMotion: 'reduce' });
47+
}
48+
49+
/**
50+
* @param {object} [params]
51+
* @param {"default" | "incremental-dom"} [params.variant]
52+
* @param {string} [params.videoID]
53+
*/
54+
async gotoYouTubePage(params = {}) {
55+
await this.gotoPage('YOUTUBE', params);
56+
}
57+
58+
async gotoNoCookiePage() {
59+
await this.gotoPage('NOCOOKIE', {});
60+
}
61+
62+
async gotoSERP() {
63+
await this.gotoPage('SERP', {});
64+
}
65+
66+
/**
67+
* @param {PageType} pageType
68+
* @param {object} [params]
69+
* @param {"default" | "incremental-dom"} [params.variant]
70+
* @param {string} [params.videoID]
71+
*/
72+
async gotoPage(pageType, params = {}) {
73+
await this.pageTypeIs(pageType);
74+
75+
const { variant = 'default', videoID = '123' } = params;
76+
const urlParams = new URLSearchParams([
77+
['v', videoID],
78+
['variant', variant],
79+
['pageType', pageType],
80+
]);
81+
82+
const page = this.pages[pageType];
83+
84+
await this.page.goto(page + '?' + urlParams.toString());
85+
}
86+
87+
/**
88+
* @param {object} [params]
89+
* @param {configFiles[number]} [params.json="native"] - default is settings for localhost
90+
* @param {string} [params.locale] - optional locale
91+
*/
92+
async withRemoteConfig(params = {}) {
93+
const { json = 'native.json', locale = 'en' } = params;
94+
95+
await this.collector.setup({ config: loadConfig(json), locale });
96+
}
97+
98+
/**
99+
* @param {PageType} pageType
100+
* @return {Promise<void>}
101+
*/
102+
async pageTypeIs(pageType) {
103+
const initialSetupResponse = {
104+
locale: 'en',
105+
pageType,
106+
};
107+
108+
await this.collector.updateMockResponse({
109+
initialSetup: initialSetupResponse,
110+
});
111+
}
112+
113+
async didSendInitialHandshake() {
114+
const messages = await this.collector.waitForMessage('initialSetup');
115+
expect(messages).toMatchObject([
116+
{
117+
payload: {
118+
context: this.collector.messagingContextName,
119+
featureName: 'duckPlayerNative',
120+
method: 'initialSetup',
121+
params: {},
122+
},
123+
},
124+
]);
125+
}
126+
127+
async didSendCurrentTimestamp() {
128+
const messages = await this.collector.waitForMessage('onCurrentTimestamp');
129+
expect(messages).toMatchObject([
130+
{
131+
payload: {
132+
context: this.collector.messagingContextName,
133+
featureName: 'duckPlayerNative',
134+
method: 'onCurrentTimestamp',
135+
params: { timestamp: 0 },
136+
},
137+
},
138+
]);
139+
}
140+
141+
/**
142+
* Helper for creating an instance per platform
143+
* @param {import("@playwright/test").Page} page
144+
* @param {import("@playwright/test").TestInfo} testInfo
145+
*/
146+
static create(page, testInfo) {
147+
// Read the configuration object to determine which platform we're testing against
148+
const { platformInfo, build } = perPlatform(testInfo.project.use);
149+
return new DuckPlayerNative(page, build, platformInfo);
150+
}
151+
152+
/**
153+
* @return {Promise<string>}
154+
*/
155+
requestWillFail() {
156+
return new Promise((resolve, reject) => {
157+
// on windows it will be a failed request
158+
const timer = setTimeout(() => {
159+
reject(new Error('timed out'));
160+
}, 5000);
161+
this.page.on('framenavigated', (req) => {
162+
clearTimeout(timer);
163+
resolve(req.url());
164+
});
165+
});
166+
}
167+
}
168+
169+
/**
170+
* @param {configFiles[number]} name
171+
* @return {Record<string, any>}
172+
*/
173+
function loadConfig(name) {
174+
return JSON.parse(readFileSync(`./integration-test/test-pages/duckplayer-native/config/${name}`, 'utf8'));
175+
}

injected/integration-test/test-pages/duckplayer/config/native.json renamed to injected/integration-test/test-pages/duckplayer-native/config/native.json

File renamed without changes.

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,7 @@
162162

163163
await import("/build/contentScope.js").catch(console.error)
164164

165-
const settingsFile = '/duckplayer/config/native.json';
165+
const settingsFile = '/duckplayer-native/config/native.json';
166166
const settings = await fetch(settingsFile).then(x => x.json())
167167
console.log('Settings', settings);
168168

1017 Bytes
Loading
1.66 KB
Loading

injected/playwright.config.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,11 @@ export default defineConfig({
3737
},
3838
{
3939
name: 'ios',
40-
testMatch: ['integration-test/duckplayer-mobile.spec.js', 'integration-test/duckplayer-mobile-drawer.spec.js'],
40+
testMatch: [
41+
'integration-test/duckplayer-mobile.spec.js',
42+
'integration-test/duckplayer-mobile-drawer.spec.js',
43+
'integration-test/duckplayer-native.spec.js',
44+
],
4145
use: { injectName: 'apple-isolated', platform: 'ios', ...devices['iPhone 13'] },
4246
},
4347
{

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,14 @@ import { mockTransport } from './duckplayer-native/mock-transport.js';
55
import { DuckPlayerNative } from './duckplayer-native/duckplayer-native.js';
66
import { Environment } from './duckplayer-native/environment.js';
77

8+
/**
9+
* @typedef {'UNKNOWN'|'YOUTUBE'|'NOCOOKIE'|'SERP'} PageType
10+
*/
11+
812
/**
913
* @typedef InitialSettings - The initial payload used to communicate render-blocking information
1014
* @property {string} locale - UI locale
15+
* @property {PageType} pageType - The type of page that has been loaded
1116
*/
1217

1318
export class DuckPlayerNativeFeature extends ContentFeature {

injected/src/features/duckplayer-native/duckplayer-native.js

Lines changed: 21 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -63,26 +63,34 @@ export class DuckPlayerNative {
6363

6464
this.logger.log('INITIAL SETUP', initialSetup);
6565

66-
this.setupMessaging();
67-
this.setupErrorDetection();
68-
this.setupTimestampPolling();
66+
switch (initialSetup.pageType) {
67+
case 'YOUTUBE': {
68+
this.messages.onMediaControl(this.mediaControlHandler.bind(this));
69+
this.messages.onMuteAudio(this.muteAudioHandler.bind(this));
70+
this.setupTimestampPolling();
71+
break;
72+
}
73+
case 'NOCOOKIE': {
74+
this.setupTimestampPolling();
75+
this.setupErrorDetection();
76+
break;
77+
}
78+
case 'SERP': {
79+
this.messages.onSerpNotify(this.serpNotifyHandler.bind(this));
80+
break;
81+
}
82+
case 'UNKNOWN':
83+
default: {
84+
this.logger.log('Unknown page. Not doing anything.');
85+
}
86+
}
6987

7088
// TODO: Question - when/how does the native side call the teardown handler?
7189
return async () => {
7290
return await Promise.all(this.sideEffects.map((destroy) => destroy()));
7391
};
7492
}
7593

76-
/**
77-
* Set up messaging event listeners
78-
*/
79-
setupMessaging() {
80-
this.messages.onMediaControl(this.mediaControlHandler.bind(this));
81-
this.messages.onMuteAudio(this.muteAudioHandler.bind(this));
82-
this.messages.onSerpNotify(this.serpNotifyHandler.bind(this));
83-
// this.messages.onCurrentTimestamp(this.currentTimestampHandler.bind(this));
84-
}
85-
8694
setupErrorDetection() {
8795
this.logger.log('Setting up error detection');
8896
const errorContainer = this.settings.selectors?.errorContainer;

injected/src/features/duckplayer-native/mock-transport.js

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,16 @@ const logger = new Logger({
66
shouldLog: () => true,
77
});
88

9-
export class TestTransport {
9+
const url = new URL(window.location.href);
10+
11+
class TestTransport {
12+
getInitialSetupData() {
13+
const locale = url.searchParams.get('locale') || 'en';
14+
const pageType = url.searchParams.get('pageType') || 'UNKNOWN';
15+
16+
return { locale, pageType };
17+
}
18+
1019
notify(_msg) {
1120
logger.log('Notifying', _msg.method);
1221

@@ -28,7 +37,7 @@ export class TestTransport {
2837
const msg = /** @type {any} */ (_msg);
2938
switch (msg.method) {
3039
case constants.MSG_NAME_INITIAL_SETUP: {
31-
return Promise.resolve({ locale: 'en' });
40+
return Promise.resolve(this.getInitialSetupData());
3241
}
3342
default:
3443
return Promise.resolve(null);

0 commit comments

Comments
 (0)