Skip to content

Commit

Permalink
Attempt to make Playwright run with Firefox
Browse files Browse the repository at this point in the history
  • Loading branch information
cezaraugusto committed Sep 22, 2024
1 parent af4ac78 commit 7a9bcf4
Show file tree
Hide file tree
Showing 5 changed files with 244 additions and 64 deletions.
68 changes: 47 additions & 21 deletions examples/content/template.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import path from 'path'
import {execSync} from 'child_process'
import {extensionFixtures} from '../extension-fixtures'
import {extensionFixturesFirefox} from '../extension-fixtures-firefox'
// import {extensionFixturesFirefox} from '../extension-fixtures-firefox'
import {
TestType,
PlaywrightTestArgs,
Expand All @@ -10,13 +10,13 @@ import {

const exampleDir = 'examples/content'
const pathToChromeExtension = path.join(__dirname, `dist/chrome`)
const pathToFirefoxExtension = path.join(__dirname, `dist/firefox`)
// const pathToFirefoxExtension = path.join(__dirname, `dist/firefox`)

// Use Playwright's default test arguments (PlaywrightTestArgs, PlaywrightWorkerArgs)
const testChrome: TestType<PlaywrightTestArgs, PlaywrightWorkerArgs> =
extensionFixtures(pathToChromeExtension, true)
const testFirefox: TestType<PlaywrightTestArgs, PlaywrightWorkerArgs> =
extensionFixturesFirefox(pathToFirefoxExtension, true)
// const testFirefox: TestType<PlaywrightTestArgs, PlaywrightWorkerArgs> =
// extensionFixturesFirefox(pathToFirefoxExtension, true)

interface TestBrowsersType {
name: string
Expand All @@ -26,52 +26,78 @@ interface TestBrowsersType {

const browsers: TestBrowsersType[] = [
{
name: 'chromium',
name: 'chrome',
test: testChrome,
extensionPath: pathToChromeExtension
},
{
name: 'firefox',
test: testFirefox,
extensionPath: pathToFirefoxExtension
}
// {
// name: 'firefox',
// test: testFirefox,
// extensionPath: pathToFirefoxExtension
// }
]

browsers.forEach(({name, test}: TestBrowsersType) => {
test.beforeAll(async () => {
// Build the extension before running tests
execSync(`pnpm extension build ${exampleDir} --polyfill`, {
execSync(`pnpm extension build ${exampleDir} --browser=${name}`, {
cwd: path.join(__dirname, '..')
})
})

test(`as ${name} extension - should exist an element with the class name content_script`, async ({
test(`as ${name} extension - should inject an element with the class name content_script`, async ({
page
}) => {
await page.goto('https://extension.js.org/')
const div = page.locator('body > div.content_script')
await test.expect(div).toBeVisible()
})

test(`as ${name} extension - should exist an h1 element with specified content`, async ({
test(`as ${name} extension - should inject an h1 element with the specified content`, async ({
page
}) => {
await page.goto('https://extension.js.org/')
const h1 = page.locator('body > div.content_script > h1.content_title')
await test.expect(h1).toHaveText('Welcome to your Content Script Extension')
})

test(`as ${name} extension - should ensure the logo image is loaded correctly`, async ({
page
}) => {
await page.goto('https://extension.js.org/')
const logo = page.locator('body > div.content_script > img.content_logo')
const logoSrc = await logo.getAttribute('src')

// Ensure the logo src is correct and the image is loaded
test.expect(logoSrc).toContain('logo.svg')
await test.expect(logo).toBeVisible()
})

test(`as ${name} extension - should check the description link is rendered correctly`, async ({
page
}) => {
await page.goto('https://extension.js.org/')
const h1 = page.locator('body > div.content_script > h1')
await test.expect(h1).toHaveText('Change the background-color ⬇')
const link = page.locator(
'body > div.content_script > p.content_description > a'
)

// Ensure the href attribute is correct and the link is visible
await test.expect(link).toHaveAttribute('href', 'https://extension.js.org')
await test.expect(link).toBeVisible()
})

test(`as ${name} extension - should exist a default color value`, async ({
test(`as ${name} extension - should ensure the h1 element has the default color`, async ({
page
}) => {
await page.goto('https://extension.js.org/')
const h1 = page.locator('body > div.content_script > h1')
const h1 = page.locator('body > div.content_script > h1.content_title')

const color = await page.evaluate(
(locator) => {
return window.getComputedStyle(locator!).getPropertyValue('color')
},
(locator) => window.getComputedStyle(locator!).getPropertyValue('color'),
await h1.elementHandle()
)
await test.expect(color).toEqual('rgb(51, 51, 51)')

// Verify that the color is set correctly
test.expect(color).toEqual('rgb(201, 201, 201)')
})
})
56 changes: 56 additions & 0 deletions examples/extension-fixtures-firefox.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import {test as base, firefox, type BrowserContext} from '@playwright/test'
import {loadFirefoxAddon} from './messaging-client'

export const extensionFixturesFirefox = (
pathToExtension: string,
headless: boolean
) => {
return base.extend<{
context: BrowserContext
extensionId: string
}>({
context: async ({}, use) => {
// Connect with the Extension.js remote desktop client
const RDP_PORT = 9222

// Override or add custom preferences here if needed
const masterPreferences = {}

// Create a temporary profile path for Firefox
const firefoxProfilePath = ''

const context = await firefox.launchPersistentContext(
firefoxProfilePath,
{
headless: headless,
args: [`-start-debugger-server=${String(RDP_PORT)}`].filter(
(arg) => !!arg
),
firefoxUserPrefs: {
...masterPreferences,
'devtools.debugger.remote-enabled': true,
'devtools.debugger.prompt-connection': false
}
}
)


// Use the context in the test
await use(context)

// Await the addon loading to ensure it's complete before proceeding
await loadFirefoxAddon(RDP_PORT, '127.0.0.1', pathToExtension)

// Close the context after the test
// await context.close()
},
extensionId: async ({context}, use) => {
// For manifest v2:
let [background] = context.backgroundPages()
if (!background) background = await context.waitForEvent('backgroundpage')

const extensionId = background.url().split('/')[2]
await use(extensionId)
}
})
}
86 changes: 48 additions & 38 deletions examples/extension-fixtures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,49 +9,59 @@ export const extensionFixtures = (
extensionId: string
}>({
context: async ({}, use) => {
const context = await chromium.launchPersistentContext('', {
headless: false,
args: [
headless ? `--headless=new` : '',
`--disable-extensions-except=${pathToExtension}`,
`--load-extension=${pathToExtension}`,
'--no-first-run', // Disable Chrome's native first run experience.
'--disable-client-side-phishing-detection', // Disables client-side phishing detection
'--disable-component-extensions-with-background-pages', // Disable some built-in extensions that aren't affected by '--disable-extensions'
'--disable-default-apps', // Disable installation of default apps
'--disable-features=InterestFeedContentSuggestions', // Disables the Discover feed on NTP
'--disable-features=Translate', // Disables Chrome translation, both the manual option and the popup prompt when a page with differing language is detected.
'--hide-scrollbars', // Hide scrollbars from screenshots.
'--mute-audio', // Mute any audio
'--no-default-browser-check', // Disable the default browser check, do not prompt to set it as such
'--no-first-run', // Skip first run wizards
'--ash-no-nudges', // Avoids blue bubble "user education" nudges (eg., "… give your browser a new look", Memory Saver)
'--disable-search-engine-choice-screen', // Disable the 2023+ search engine choice screen
'--disable-features=MediaRoute', // Avoid the startup dialog for `Do you want the application “Chromium.app” to accept incoming network connections?`. Also disables the Chrome Media Router which creates background networking activity to discover cast targets. A superset of disabling DialMediaRouteProvider.
'--use-mock-keychain', // Use mock keychain on Mac to prevent the blocking permissions dialog about "Chrome wants to use your confidential information stored in your keychain"
'--disable-background-networking', // Disable various background network services, including extension updating, safe browsing service, upgrade detector, translate, UMA
'--disable-breakpad', // Disable crashdump collection (reporting is already disabled in Chromium)
'--disable-component-update', // Don't update the browser 'components' listed at chrome://components/
'--disable-domain-reliability', // Disables Domain Reliability Monitoring, which tracks whether the browser has difficulty contacting Google-owned sites and uploads reports to Google.
'--disable-features=AutofillServerCommunicatio', // Disables autofill server communication. This feature isn't disabled via other 'parent' flags.
'--disable-features=CertificateTransparencyComponentUpdate',
'--disable-sync', // Disable syncing to a Google account
'--disable-features=OptimizationHints', // Used for turning on Breakpad crash reporting in a debug environment where crash reporting is typically compiled but disabled. Disable the Chrome Optimization Guide and networking with its service API
'--disable-features=DialMediaRouteProvider', // A weaker form of disabling the MediaRouter feature. See that flag's details.
'--no-pings', // Don't send hyperlink auditing pings
'--enable-features=SidePanelUpdates' // Ensure the side panel is visible. This is used for testing the side panel feature.
].filter((arg) => !!arg)
})
// Override or add custom preferences here if needed
// const masterPreferences = {}

// Create a temporary profile path for Firefox
// Optionally set a custom profile path if needed, or leave it as ''
const chromiumProfilePath = ''

const context = await chromium.launchPersistentContext(
chromiumProfilePath,
{
headless: false,
args: [
headless ? `--headless=new` : '',
`--disable-extensions-except=${pathToExtension}`,
`--load-extension=${pathToExtension}`,
'--no-first-run', // Disable Chrome's native first run experience.
'--disable-client-side-phishing-detection', // Disables client-side phishing detection
'--disable-component-extensions-with-background-pages', // Disable some built-in extensions that aren't affected by '--disable-extensions'
'--disable-default-apps', // Disable installation of default apps
'--disable-features=InterestFeedContentSuggestions', // Disables the Discover feed on NTP
'--disable-features=Translate', // Disables Chrome translation, both the manual option and the popup prompt when a page with differing language is detected.
'--hide-scrollbars', // Hide scrollbars from screenshots.
'--mute-audio', // Mute any audio
'--no-default-browser-check', // Disable the default browser check, do not prompt to set it as such
'--no-first-run', // Skip first run wizards
'--ash-no-nudges', // Avoids blue bubble "user education" nudges (eg., "… give your browser a new look", Memory Saver)
'--disable-search-engine-choice-screen', // Disable the 2023+ search engine choice screen
'--disable-features=MediaRoute', // Avoid the startup dialog for `Do you want the application “Chromium.app” to accept incoming network connections?`. Also disables the Chrome Media Router which creates background networking activity to discover cast targets. A superset of disabling DialMediaRouteProvider.
'--use-mock-keychain', // Use mock keychain on Mac to prevent the blocking permissions dialog about "Chrome wants to use your confidential information stored in your keychain"
'--disable-background-networking', // Disable various background network services, including extension updating, safe browsing service, upgrade detector, translate, UMA
'--disable-breakpad', // Disable crashdump collection (reporting is already disabled in Chromium)
'--disable-component-update', // Don't update the browser 'components' listed at chrome://components/
'--disable-domain-reliability', // Disables Domain Reliability Monitoring, which tracks whether the browser has difficulty contacting Google-owned sites and uploads reports to Google.
'--disable-features=AutofillServerCommunicatio', // Disables autofill server communication. This feature isn't disabled via other 'parent' flags.
'--disable-features=CertificateTransparencyComponentUpdate',
'--disable-sync', // Disable syncing to a Google account
'--disable-features=OptimizationHints', // Used for turning on Breakpad crash reporting in a debug environment where crash reporting is typically compiled but disabled. Disable the Chrome Optimization Guide and networking with its service API
'--disable-features=DialMediaRouteProvider', // A weaker form of disabling the MediaRouter feature. See that flag's details.
'--no-pings', // Don't send hyperlink auditing pings
'--enable-features=SidePanelUpdates' // Ensure the side panel is visible. This is used for testing the side panel feature.
].filter((arg) => !!arg)
}
)
await use(context)
await context.close()
},
extensionId: async ({context}, use) => {
/*
// for manifest v2:
let [background] = context.backgroundPages()
if (!background)
background = await context.waitForEvent('backgroundpage')
*/
// for manifest v2:
let [background] = context.backgroundPages()
if (!background)
background = await context.waitForEvent('backgroundpage')
*/

// for manifest v3:
let [background] = context.serviceWorkers()
Expand Down
88 changes: 88 additions & 0 deletions examples/messaging-client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import {Buffer} from 'buffer'
import net from 'net'

export const loadFirefoxAddon = (
port: number,
host: string,
addonPath: string
) => {
return new Promise<boolean>((resolve) => {
const socket = net.connect({
port,
host
})

let success = false

socket.once('error', () => {})
socket.once('close', () => {
resolve(success)
})

const send = (data: Record<string, string>) => {
const raw = Buffer.from(JSON.stringify(data))

socket.write(`${raw.length}`)
socket.write(':')
socket.write(raw)
}

send({
to: 'root',
type: 'getRoot'
})

const onMessage = (message: any) => {
if (message.addonsActor) {
send({
to: message.addonsActor,
type: 'installTemporaryAddon',
addonPath
})
}

if (message.addon) {
success = true
socket.end()
}

if (message.error) {
socket.end()
}
}

const buffers: Buffer[] = []
// let remainingBytes = 0

socket.on('data', (data) => {
buffers.push(data)

const buffer = Buffer.concat(buffers)
const colonIndex = buffer.indexOf(':')

if (colonIndex === -1) return

const expectedLength = parseInt(
buffer.subarray(0, colonIndex).toString(),
10
)

if (!Number.isFinite(expectedLength)) {
throw new Error('Invalid message size')
}

const remainingData = buffer.subarray(colonIndex + 1)
if (remainingData.length >= expectedLength) {
const message = remainingData.subarray(0, expectedLength).toString()
buffers.length = 0 // Clear buffer after processing

try {
const json = JSON.parse(message)
onMessage(json)
} catch (error) {
console.error('Error parsing JSON:', error)
}
}
})
})
}
10 changes: 5 additions & 5 deletions playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,12 @@ export default defineConfig({
{
name: 'chromium',
use: {...devices['Desktop Chrome']}
}
},

// {
// name: 'firefox',
// use: { ...devices['Desktop Firefox'] },
// },
{
name: 'firefox',
use: {...devices['Desktop Firefox']}
}

// {
// name: 'webkit',
Expand Down

0 comments on commit 7a9bcf4

Please sign in to comment.