Skip to content

Make duck player page more resilient to race conditions on macOS #571

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jun 14, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -69,9 +69,15 @@ jobs:
if: always()
with:
name: playwright-report
path: playwright-report/
path: test-results
retention-days: 5
- run: npm run test-int-x
- uses: actions/upload-artifact@v2
if: always()
with:
name: playwright-report-pages
path: packages/special-pages/test-results
retention-days: 5
- name: Build docs
run: npm run docs

Expand Down
1 change: 1 addition & 0 deletions packages/special-pages/.gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
test-results
playwright-report
6 changes: 3 additions & 3 deletions packages/special-pages/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,16 @@
"main": "index.js",
"scripts": {
"build": "node index.mjs",
"build.dev": "node index.mjs --debug",
"build.dev": "node index.mjs --env development",
"test": "playwright test",
"test.windows": "npm run test -- --project duckplayer-windows",
"test.apple": "npm run test -- --project duckplayer-apple",
"test.headed": "npm run test -- --headed",
"test.ui": "npm run test -- --ui",
"pretest": "npm run build.dev",
"pretest.headed": "npm run build.dev",
"test-int-x": "playwright test",
"test-int": "playwright test",
"test-int-x": "npm run test",
"test-int": "npm run test",
"serve": "http-server -c-1 --port 3210 ../../"
},
"license": "ISC",
Expand Down
47 changes: 32 additions & 15 deletions packages/special-pages/pages/duckplayer/src/js/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,12 @@
*
* Please see {@link DuckPlayerPageMessages} for the up-to-date list
*/
import { createDuckPlayerPageMessaging, DuckPlayerPageMessages, UserValues } from './messages'
import {
callWithRetry,
createDuckPlayerPageMessaging,
DuckPlayerPageMessages,
UserValues
} from './messages'
import { html } from '../../../../../../src/dom-utils'
import { initStorage } from './storage'

Expand Down Expand Up @@ -325,23 +330,31 @@ const Comms = {
* @param {ImportMeta['env']} opts.env
* @param {ImportMeta['injectName']} opts.injectName
*/
init: (opts) => {
Comms.messaging = createDuckPlayerPageMessaging(opts)
// eslint-disable-next-line promise/prefer-await-to-then
Comms.messaging.getUserValues().then((value) => {
if ('enabled' in value.privatePlayerMode) {
Setting.setState(true)
} else {
Setting.setState(false)
}
init: async (opts) => {
const messaging = createDuckPlayerPageMessaging(opts)
// try to make communication with the native side.
const result = await callWithRetry(() => {
return messaging.getUserValues()
})
Comms.messaging?.onUserValuesChanged(value => {
if ('enabled' in value.privatePlayerMode) {
// if we received a connection, use the initial values
if ('value' in result) {
Comms.messaging = messaging
if ('enabled' in result.value.privatePlayerMode) {
Setting.setState(true)
} else {
Setting.setState(false)
}
})
// eslint-disable-next-line promise/prefer-await-to-then
Comms.messaging?.onUserValuesChanged(value => {
if ('enabled' in value.privatePlayerMode) {
Setting.setState(true)
} else {
Setting.setState(false)
}
})
} else {
console.error(result.error)
}
},
/**
* From the player page, all we can do is 'setUserValues' to {enabled: {}}
Expand Down Expand Up @@ -700,14 +713,18 @@ const MouseMove = {
/**
* Initializes all parts of the page on load.
*/
document.addEventListener('DOMContentLoaded', () => {
document.addEventListener('DOMContentLoaded', async () => {
Setting.init({
settingsUrl: settingsUrl(import.meta.injectName)
})
Comms.init({
await Comms.init({
injectName: import.meta.injectName,
env: import.meta.env
})
if (!Comms.messaging) {
console.warn('cannot continue as messaging was not resolved')
return
}
VideoPlayer.init()
Tooltip.init()
PlayOnYouTube.init({
Expand Down
37 changes: 37 additions & 0 deletions packages/special-pages/pages/duckplayer/src/js/messages.js
Original file line number Diff line number Diff line change
Expand Up @@ -159,3 +159,40 @@ export function createDuckPlayerPageMessaging (opts) {
}
throw new Error('unreachable - platform not supported')
}

/**
* This will return either { value: awaited value },
* { error: error message }
*
* It will execute the given function in uniform intervals
* until either:
* 1: the given function stops throwing errors
* 2: the maxAttempts limit is reached
*
* This is useful for situations where you don't want to continue
* until a result is found - normally to work around race-conditions
*
* @template {(...args: any[]) => any} FN
* @param {FN} fn
* @param {{maxAttempts?: number, intervalMs?: number}} params
* @returns {Promise<{ value: Awaited<ReturnType<FN>>, attempt: number } | { error: string }>}
*/
export async function callWithRetry (fn, params = {}) {
const { maxAttempts = 10, intervalMs = 300 } = params
let attempt = 1

while (attempt <= maxAttempts) {
try {
return { value: await fn(), attempt }
} catch (error) {
if (attempt === maxAttempts) {
return { error: `Max attempts reached: ${error}` }
}

await new Promise((resolve) => setTimeout(resolve, intervalMs))
attempt++
}
}

return { error: 'Unreachable: value not retrieved' }
}
9 changes: 9 additions & 0 deletions packages/special-pages/playwright.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,20 @@ export default defineConfig({
}
],
fullyParallel: !process.env.CI,
/* Retry on CI only */
retries: process.env.CI ? 2 : 0,
/* Opt out of parallel tests on CI. */
workers: process.env.CI ? 1 : undefined,
reporter: process.env.CI ? 'github' : [['html', { open: 'never' }]],
// @ts-expect-error - Type 'undefined' is not assignable to type 'string'. process.env
webServer: {
command: 'npm run serve',
port: 3210,
reuseExistingServer: true,
env: process.env
},
use: {
actionTimeout: 1000,
trace: 'on-first-retry'
}
})
4 changes: 2 additions & 2 deletions packages/special-pages/tests/duckplayer.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ test.describe('duckplayer toolbar', () => {
})

test.describe('duckplayer settings', () => {
test.skip('always open setting', async ({ page }, workerInfo) => {
test('always open setting', async ({ page }, workerInfo) => {
const duckplayer = DuckPlayerPage.create(page, workerInfo)
// load as normal
await duckplayer.openWithVideoID()
Expand All @@ -98,7 +98,7 @@ test.describe('duckplayer settings', () => {
await duckplayer.toggleAlwaysOpenSetting()
await duckplayer.sentUpdatedSettings()
})
test.skip('when a new value arrives via subscription', async ({ page }, workerInfo) => {
test('when a new value arrives via subscription', async ({ page }, workerInfo) => {
const duckplayer = DuckPlayerPage.create(page, workerInfo)
// load as normal
await duckplayer.openWithVideoID()
Expand Down
1 change: 1 addition & 0 deletions playwright.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ export default defineConfig({
retries: process.env.CI ? 2 : 0,
/* Opt out of parallel tests on CI. */
workers: process.env.CI ? 1 : undefined,
reporter: process.env.CI ? 'github' : [['html', { open: 'never' }]],
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
webServer: {
reuseExistingServer: true,
Expand Down