Skip to content
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

Improve reliability of Click to Load integration tests #1182

Merged
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
25 changes: 16 additions & 9 deletions integration-test/background/click-to-load-facebook.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ const harness = require('../helpers/harness')
const { logPageRequests } = require('../helpers/requests')
const { loadTestConfig, unloadTestConfig } = require('../helpers/testConfig')
const backgroundWait = require('../helpers/backgroundWait')
const pageWait = require('../helpers/pageWait')
const { setupAPISchemaTest } = require('../helpers/apiSchema')

const testSite = 'https://privacy-test-pages.glitch.me/privacy-protections/click-to-load/'
Expand Down Expand Up @@ -76,8 +77,7 @@ describe('Test Facebook Click To Load', () => {
// Initially there should be a bunch of requests. The SDK should
// be redirected to our surrogate but otherwise Facebook requests should
// be blocked.
await page.goto(testSite, { waitUntil: 'networkidle0' })
await page.waitForNetworkIdle({ idleTime: 1000 })
await pageWait.forGoto(page, testSite)
{
const {
requestCount, blockCount, allowCount, facebookSDKRedirect
Expand All @@ -102,9 +102,17 @@ describe('Test Facebook Click To Load', () => {
})
for (let i = 0; i < buttonCount; i++) {
const button = await page.evaluateHandle(i => window.buttons[i], i)
await button.click()
try {
await button.click()
} catch (e) { }
}
await page.waitForNetworkIdle({ idleTime: 1000 })

// Wait for the embedded content to finish loading, but give up after
// 15 seconds. That avoids the tests failing if the network is slow.
try {
await page.waitForNetworkIdle({ idleTime: 1000, timeout: 15000 })
} catch (e) { }

{
const {
requestCount, blockCount, allowCount, facebookSDKRedirect
Expand All @@ -119,8 +127,7 @@ describe('Test Facebook Click To Load', () => {

// When the page is reloaded, requests should be blocked again.
clearRequests()
await page.reload({ waitUntil: 'networkidle0' })
await page.waitForNetworkIdle({ idleTime: 1000 })
await pageWait.forReload(page)
{
const {
requestCount, blockCount, allowCount, facebookSDKRedirect
Expand All @@ -135,7 +142,7 @@ describe('Test Facebook Click To Load', () => {
expect(allowCount).toEqual(0)
}

page.close()
await page.close()
})
})

Expand All @@ -152,7 +159,7 @@ describe('Facebook SDK schema', () => {

it('CTL: Facebook SDK schema hasn\'t changed', async () => {
const page = await browser.newPage()
await page.goto(testSite, { waitUntil: 'networkidle2' })
await pageWait.forGoto(page, testSite)

// Note: If these tests fail, update the
// /integration-test/data/api_schemas/facebook-sdk.json file
Expand All @@ -170,6 +177,6 @@ describe('Facebook SDK schema', () => {
)
expect(actualSchema).toEqual(expectedSchema)

page.close()
await page.close()
})
})
26 changes: 18 additions & 8 deletions integration-test/background/click-to-load-youtube.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ const harness = require('../helpers/harness')
const { logPageRequests } = require('../helpers/requests')
const { loadTestConfig, unloadTestConfig } = require('../helpers/testConfig')
const backgroundWait = require('../helpers/backgroundWait')
const pageWait = require('../helpers/pageWait')
const { setupAPISchemaTest } = require('../helpers/apiSchema')

const testSite = 'https://privacy-test-pages.glitch.me/privacy-protections/youtube-click-to-load/'
Expand Down Expand Up @@ -81,7 +82,7 @@ describe('Test YouTube Click To Load', () => {
// Initially there should be a bunch of requests. The iframe_api should
// be redirected to our surrogate but otherwise YouTube requests should
// be blocked.
await page.goto(testSite, { waitUntil: 'networkidle0' })
await pageWait.forGoto(page, testSite)
{
const {
youTubeIframeApi, youTubeStandard, youTubeNocookie
Expand Down Expand Up @@ -119,7 +120,7 @@ describe('Test YouTube Click To Load', () => {

// When the page is reloaded, requests should be blocked again.
clearRequests()
await page.reload({ waitUntil: 'networkidle0' })
await pageWait.forReload(page)
{
const {
youTubeIframeApi, youTubeStandard, youTubeNocookie
Expand Down Expand Up @@ -154,12 +155,12 @@ describe('Test YouTube Click To Load', () => {
expect(youTubeNocookie.allowed).toBeGreaterThanOrEqual(1)
}

page.close()
await page.close()
})

it('CTL: YouTube interacting with iframe API', async () => {
const page = await browser.newPage()
await page.goto(testSite, { waitUntil: 'networkidle0' })
await pageWait.forGoto(page, testSite)

// Test the Iframe API controls and events function correctly, even when
// used with an existing video.
Expand All @@ -179,6 +180,7 @@ describe('Test YouTube Click To Load', () => {
)
await button.click()
await waitForExpectedBorder('orange')
await page.waitForNetworkIdle({ idleTime: 1000 })

await page.click('#play-existing-video')
await waitForExpectedBorder('green')
Expand Down Expand Up @@ -210,18 +212,26 @@ describe('Test YouTube Click To Load', () => {
await button.focus()
await button.click()
await waitForExpectedRoll('0.0000')
await page.waitForNetworkIdle({ idleTime: 1000 })

// Play video and keep clicking roll button until it flips. The
// video doesn't flip until its finished loading, so this way we
// avoid unnecessary waiting and flaky failures.
await page.click('#spherical-video')
await waitForExpectedRoll('180.0000', true)
try {
await waitForExpectedRoll('180.0000', true)
} catch (e) {
// If the video fails to load within the 30 seconds allowed,
// this test case fails. That does happen occasionally, so let's
// note it but avoid a hard-fail.
pending('Spherical video roll test timed out.')
}

await page.click('#spherical-video-flip')
await waitForExpectedRoll('0.0000')
}

page.close()
await page.close()
})
})

Expand All @@ -239,7 +249,7 @@ describe('YouTube Iframe Player API schema', () => {

it('CTL: Iframe Player API schema hasn\'t changed', async () => {
const page = await browser.newPage()
await page.goto(testSite, { waitUntil: 'networkidle2' })
await pageWait.forGoto(page, testSite)

// Note: If this test fails, update
// /integration-test/data/api_schemas/youtube-iframe-api.json file
Expand All @@ -255,6 +265,6 @@ describe('YouTube Iframe Player API schema', () => {
)
expect(actualSchema).toEqual(expectedSchema)

page.close()
await page.close()
})
})
12 changes: 11 additions & 1 deletion integration-test/helpers/apiSchema.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
const fs = require('fs')
const path = require('path')

const puppeteer = require('puppeteer')

/**
* Returns the schema of a given JavaScript Object in the given Page. Schema is
* similar to the output of Object.getOwnPropertyDescriptors.
Expand Down Expand Up @@ -100,7 +102,15 @@ async function setupAPISchemaTest (page, schemaFilename, targetObjectNames) {

const actualSchema = Object.create(null)
for (const objectName of targetObjectNames) {
actualSchema[objectName] = await getObjectSchema(page, objectName)
try {
actualSchema[objectName] = await getObjectSchema(page, objectName)
} catch (e) {
if (e instanceof puppeteer.errors.TimeoutError) {
pending('Timed out setting up API schema test.')
} else {
throw e
}
}
}

// Write the actual schema to a file as a test artifact.
Expand Down
67 changes: 37 additions & 30 deletions integration-test/helpers/backgroundWait.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
// Helpers that aid in waiting for the background page's state.
// Note: Puppeteer's `waitForFunction` uses `requestAnimationFrame` by default,
// but that appears to be broken for extension background pages. Polling
// every 10ms works however.

// Notes:
// - Puppeteer's `waitForFunction` uses `requestAnimationFrame` by default,
// but that appears to be broken for extension background pages. Polling
// every 10ms works however.
// - Jasmine's timeout is 20 seconds, so go with a timeout of 15 seconds
// (rather than the default of 30 seconds) to improve the error output on
// timeout.
function forFunction (bgPage, func, ...args) {
return bgPage.waitForFunction(func, { polling: 10 }, ...args)
return bgPage.waitForFunction(func, { polling: 10, timeout: 15000 }, ...args)
}

function forSetting (bgPage, key) {
Expand All @@ -15,34 +18,38 @@ function forSetting (bgPage, key) {

// Note: This is likely incomplete. Please add further checks as they come up!
async function forAllConfiguration (bgPage) {
await forFunction(bgPage, () =>
// Expected configuration Objects/properties exist.
window.dbg?.https &&
window.dbg?.tds?.ClickToLoadConfig &&
window.dbg?.tds?.config?.features?.clickToPlay?.state &&
window.dbg?.tds?.tds?.domains &&
window.dbg?.tds?.tds?.entities &&
window.dbg?.tds?.tds?.trackers &&
try {
await forFunction(bgPage, () =>
// Expected configuration Objects/properties exist.
window.dbg?.https &&
window.dbg?.tds?.ClickToLoadConfig &&
window.dbg?.tds?.config?.features?.clickToPlay?.state &&
window.dbg?.tds?.tds?.domains &&
window.dbg?.tds?.tds?.entities &&
window.dbg?.tds?.tds?.trackers &&

// Expected configuration state.
window.dbg.tds.isInstalling === false &&
window.dbg.https.isReady === true &&
Object.keys(window.dbg.tds.ClickToLoadConfig).length > 0 &&
Object.keys(window.dbg.tds.tds.domains).length > 0 &&
Object.keys(window.dbg.tds.tds.entities).length > 0
)
// Expected configuration state.
window.dbg.tds.isInstalling === false &&
window.dbg.https.isReady === true &&
Object.keys(window.dbg.tds.ClickToLoadConfig).length > 0 &&
Object.keys(window.dbg.tds.tds.domains).length > 0 &&
Object.keys(window.dbg.tds.tds.entities).length > 0)

await forFunction(bgPage, () => {
const firstEntity = Object.keys(window.dbg.tds.tds.entities)[0]
const { domains } = window.dbg.tds.tds.entities[firstEntity]
return firstEntity &&
domains && domains.length > 0 &&
window.dbg.tds.tds.domains[domains[0]] === firstEntity
})
await Promise.all([
forFunction(bgPage, () => {
const firstEntity = Object.keys(window.dbg.tds.tds.entities)[0]
const { domains } = window.dbg.tds.tds.entities[firstEntity]
return firstEntity &&
domains && domains.length > 0 &&
window.dbg.tds.tds.domains[domains[0]] === firstEntity
}),

// This is a synchronous way to check that the Promise returned by
// `settings.ready()` has resolved.
await forSetting(bgPage, 'atb')
// Ensures that `settings.ready()` has resolved.
forSetting(bgPage, 'atb')
])
} catch (e) {
throw new Error('Failed to load extension configuration.')
}
}

module.exports = {
Expand Down
24 changes: 24 additions & 0 deletions integration-test/helpers/pageWait.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
async function forGoto (page, url) {
try {
await page.goto(
url, { waitUntil: 'networkidle0', timeout: 15000 }
)
await page.waitForNetworkIdle({ idleTime: 1000, timeout: 15000 })
} catch (e) {
pending('Failed to load URL: ' + url)
}
}

async function forReload (page) {
try {
await page.reload({ waitUntil: 'networkidle0', timeout: 15000 })
await page.waitForNetworkIdle({ idleTime: 1000, timeout: 15000 })
} catch (e) {
pending('Failed to reload page: ' + page.url())
}
}

module.exports = {
forGoto,
forReload
}
6 changes: 5 additions & 1 deletion integration-test/helpers/testConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,11 @@ async function loadTestConfig (bgPage, testConfigFilename) {
*/
async function unloadTestConfig (bgPage) {
await bgPage.evaluate(parsePathString => {
const { configBackup } = window || {}
const { configBackup } = window
if (!configBackup) {
return
}

// eslint-disable-next-line no-eval
eval(parsePathString)

Expand Down