Skip to content

Commit

Permalink
move remaining tests to playwright
Browse files Browse the repository at this point in the history
  • Loading branch information
Shane Osbourne committed Oct 3, 2024
1 parent 234875e commit a50c481
Show file tree
Hide file tree
Showing 18 changed files with 522 additions and 2,492 deletions.
12 changes: 0 additions & 12 deletions integration-test/config.js

This file was deleted.

259 changes: 121 additions & 138 deletions integration-test/helpers/harness.js
Original file line number Diff line number Diff line change
@@ -1,150 +1,133 @@
import * as fs from 'fs'
import * as os from 'os'
import * as path from 'path'
import * as http from 'http'
import puppeteer from 'puppeteer'
import { spawnSync } from 'child_process'

jasmine.DEFAULT_TIMEOUT_INTERVAL = 20000
if (process.env.KEEP_OPEN) {
jasmine.DEFAULT_TIMEOUT_INTERVAL = 20000 * 1000
}
import { mkdtempSync, rmSync } from 'node:fs'
import { tmpdir } from 'os'
import { join } from 'path'
import { chromium, firefox } from '@playwright/test'
import { fork } from 'node:child_process'

const DATA_DIR_PREFIX = 'ddg-temp-'

export async function setup (ops = {}) {
const { withExtension = false } = ops
const tmpDirPrefix = path.join(os.tmpdir(), DATA_DIR_PREFIX)
const dataDir = fs.mkdtempSync(tmpDirPrefix)
const args = [
`--user-data-dir=${dataDir}`
]
if (withExtension) {
args.push('--disable-extensions-except=integration-test/extension')
args.push('--load-extension=integration-test/extension')
}

// github actions
if (process.env.CI) {
args.push('--no-sandbox')
}

const puppeteerOps = {
args,
headless: false
}

const browser = await puppeteer.launch(puppeteerOps)
const servers = []

async function teardown () {
if (process.env.KEEP_OPEN) {
return new Promise((resolve) => {
browser.on('disconnected', async () => {
await teardownInternal()
// @ts-expect-error - error TS2810: Expected 1 argument, but got 0. 'new Promise()'
resolve()
/**
* A single place
* @param {typeof import("@playwright/test").test} test
*/
export function testContext (test) {
return test.extend({
context: async ({ browserName }, use) => {
const tmpDirPrefix = join(tmpdir(), DATA_DIR_PREFIX)
const dataDir = mkdtempSync(tmpDirPrefix)
const browserTypes = { chromium, firefox }

const launchOptions = {
devtools: true,
headless: false,
viewport: {
width: 1920,
height: 1080
},
args: [
'--disable-extensions-except=integration-test/extension',
'--load-extension=integration-test/extension'
]
}

const context = await browserTypes[browserName].launchPersistentContext(
dataDir,
launchOptions
)

// actually run the tests
await use(context)

// clean up
await context.close()

// Clean up temporary data directory
rmSync(dataDir, { recursive: true, force: true })
},
altServerPort: async ({ browserName }, use) => {
console.log('browserName:', browserName)
const serverScript = fork('./scripts/server.mjs', {
env: {
...process.env,
SERVER_DIR: 'integration-test/test-pages',
SERVER_PORT: '8383'
}
})
const opened = new Promise((resolve, reject) => {
serverScript.on('message', (/** @type {any} */resp) => {
if (typeof resp.port === 'number') {
resolve(resp.port)
} else {
reject(resp.port)
}
})
})
} else {
await teardownInternal()
}
}

async function teardownInternal () {
await Promise.all(servers.map(server => server.close()))
await browser.close()

// necessary so e.g. local storage
// doesn't carry over between test runs
spawnSync('rm', ['-rf', dataDir])
}

/**
* @param {number|string} [port]
* @returns {http.Server}
*/
function setupServer (port) {
return _startupServerInternal('../pages', port)
}

/**
* @param {number|string} [port]
* @returns {http.Server}
*/
function setupIntegrationPagesServer (port) {
return _startupServerInternal('../test-pages', port)
}

/**
* @param {string} pathName
* @param {number|string} [port]
* @returns {http.Server}
*/
function _startupServerInternal (pathName, port) {
const server = http.createServer(function (req, res) {
// @ts-expect-error - error TS2345: Argument of type 'string | undefined' is not assignable to parameter of type 'string | URL'.
const url = new URL(req.url, `http://${req.headers.host}`)
const importUrl = new URL(import.meta.url)
const dirname = importUrl.pathname.replace(/\/[^/]*$/, '')
const pathname = path.join(dirname, pathName, url.pathname)

fs.readFile(pathname, (err, data) => {
if (err) {
res.writeHead(404)
res.end(JSON.stringify(err))
return
}
res.writeHead(200)
res.end(data)
const closed = new Promise((resolve, reject) => {
serverScript.on('close', (err) => {
if (err) {
reject(new Error('server did not exit, code: ' + err))
} else {
resolve(null)
}
})
serverScript.on('error', () => {
reject(new Error('server errored'))
})
})
}).listen(port)
servers.push(server)
return server
}

/**
* A wrapper around page.goto() that supports sending additional
* arguments to content-scope's init methods + waits for a known
* indicators to avoid race conditions
*
* @param {import("puppeteer").Page} page
* @param {string} urlString
* @param {Record<string, any>} [args]
* @returns {Promise<void>}
*/
async function gotoAndWait (page, urlString, args = {}, evalBeforeInit = null) {
const url = new URL(urlString)

// Append the flag so that the script knows to wait for incoming args.
url.searchParams.append('wait-for-init-args', 'true')

await page.goto(url.href)

// wait until contentScopeFeatures.load() has completed
await page.waitForFunction(() => {
// @ts-expect-error https://app.asana.com/0/1201614831475344/1203979574128023/f
return window.__content_scope_status === 'loaded'
})

if (evalBeforeInit) {
await page.evaluate(evalBeforeInit)
const port = await opened
await use(port)
serverScript.kill()
await closed
}
})
}

const evalString = `
const detail = ${JSON.stringify(args)}
const evt = new CustomEvent('content-scope-init-args', { detail })
document.dispatchEvent(evt)
`
await page.evaluate(evalString)

// wait until contentScopeFeatures.init(args) has completed
await page.waitForFunction(() => {
window.dispatchEvent(new Event('content-scope-init-complete'))
// @ts-expect-error https://app.asana.com/0/1201614831475344/1203979574128023/f
return window.__content_scope_status === 'initialized'
})
/**
* A wrapper around page.goto() that supports sending additional
* arguments to content-scope's init methods + waits for a known
* indicators to avoid race conditions
*
* @param {import("@playwright/test").Page} page
* @param {string} urlString
* @param {Record<string, any>} [args]
* @param {string|null} [evalBeforeInit]
* @returns {Promise<void>}
*/
export async function gotoAndWait (page, urlString, args = {}, evalBeforeInit = null) {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [_, search] = urlString.split('?')
const searchParams = new URLSearchParams(search)

// Append the flag so that the script knows to wait for incoming args.
searchParams.append('wait-for-init-args', 'true')

await page.goto(urlString + '?' + searchParams.toString())

// wait until contentScopeFeatures.load() has completed
await page.waitForFunction(() => {
// @ts-expect-error https://app.asana.com/0/1201614831475344/1203979574128023/f
return window.__content_scope_status === 'loaded'
})

if (evalBeforeInit) {
await page.evaluate(evalBeforeInit)
}

return { browser, teardown, setupServer, setupIntegrationPagesServer, gotoAndWait }
const evalString = `
;(() => {
const detail = ${JSON.stringify(args)}
const evt = new CustomEvent('content-scope-init-args', { detail })
document.dispatchEvent(evt);
})();
`

await page.evaluate(evalString)

// wait until contentScopeFeatures.init(args) has completed
await page.waitForFunction(() => {
window.dispatchEvent(new Event('content-scope-init-complete'))
// @ts-expect-error https://app.asana.com/0/1201614831475344/1203979574128023/f
return window.__content_scope_status === 'initialized'
})
}
6 changes: 0 additions & 6 deletions integration-test/pages/runtimeChecks/index.html

This file was deleted.

12 changes: 0 additions & 12 deletions integration-test/pages/runtimeChecks/script.js

This file was deleted.

2 changes: 0 additions & 2 deletions integration-test/pages/runtimeChecks/script2.js

This file was deleted.

36 changes: 12 additions & 24 deletions integration-test/test-cookie.js
Original file line number Diff line number Diff line change
@@ -1,22 +1,11 @@
import { setup } from './helpers/harness.js'
import { test as base, expect } from '@playwright/test'
import { gotoAndWait, testContext } from './helpers/harness.js'

describe('Cookie protection tests', () => {
let browser
let teardown
let setupServer

beforeAll(async () => {
({ browser, teardown, setupServer } = await setup())
setupServer('8080')
})
afterAll(async () => {
await teardown()
})

it('should restrict the expiry of first-party cookies', async () => {
const page = await browser.newPage()
await page.goto('http://localhost:8080/index.html')
const test = testContext(base)

test.describe('Cookie protection tests', () => {
test('should restrict the expiry of first-party cookies', async ({ page }) => {
await gotoAndWait(page, '/index.html')
const result = await page.evaluate(async () => {
document.cookie = 'test=1; expires=Wed, 21 Aug 2040 20:00:00 UTC;'
// wait for a tick, as cookie modification happens in a promise
Expand All @@ -30,9 +19,8 @@ describe('Cookie protection tests', () => {
expect(result.expires).toBeLessThan(Date.now() + 605_000_000)
})

it('non-string cookie values do not bypass protection', async () => {
const page = await browser.newPage()
await page.goto('http://localhost:8080/index.html')
test('non-string cookie values do not bypass protection', async ({ page }) => {
await gotoAndWait(page, '/index.html')

const result = await page.evaluate(async () => {
// @ts-expect-error - Invalid argument to document.cookie on purpose for test
Expand All @@ -53,11 +41,11 @@ describe('Cookie protection tests', () => {
expect(result.expires).toBeLessThan(Date.now() + 605_000_000)
})

it('Erroneous values do not throw', async () => {
const page = await browser.newPage()
await page.goto('http://localhost:8080/index.html')

test('Erroneous values do not throw', async ({ page }) => {
await gotoAndWait(page, '/index.html')
const result = await page.evaluate(async () => {
document.cookie = 'a=b; expires=Wed, 21 Aug 2040 20:00:00 UTC;'

// @ts-expect-error - Invalid argument to document.cookie on purpose for test
document.cookie = null

Expand Down
Loading

0 comments on commit a50c481

Please sign in to comment.