Skip to content

Commit 7a9bcf4

Browse files
committed
Attempt to make Playwright run with Firefox
1 parent af4ac78 commit 7a9bcf4

File tree

5 files changed

+244
-64
lines changed

5 files changed

+244
-64
lines changed

examples/content/template.spec.ts

Lines changed: 47 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import path from 'path'
22
import {execSync} from 'child_process'
33
import {extensionFixtures} from '../extension-fixtures'
4-
import {extensionFixturesFirefox} from '../extension-fixtures-firefox'
4+
// import {extensionFixturesFirefox} from '../extension-fixtures-firefox'
55
import {
66
TestType,
77
PlaywrightTestArgs,
@@ -10,13 +10,13 @@ import {
1010

1111
const exampleDir = 'examples/content'
1212
const pathToChromeExtension = path.join(__dirname, `dist/chrome`)
13-
const pathToFirefoxExtension = path.join(__dirname, `dist/firefox`)
13+
// const pathToFirefoxExtension = path.join(__dirname, `dist/firefox`)
1414

1515
// Use Playwright's default test arguments (PlaywrightTestArgs, PlaywrightWorkerArgs)
1616
const testChrome: TestType<PlaywrightTestArgs, PlaywrightWorkerArgs> =
1717
extensionFixtures(pathToChromeExtension, true)
18-
const testFirefox: TestType<PlaywrightTestArgs, PlaywrightWorkerArgs> =
19-
extensionFixturesFirefox(pathToFirefoxExtension, true)
18+
// const testFirefox: TestType<PlaywrightTestArgs, PlaywrightWorkerArgs> =
19+
// extensionFixturesFirefox(pathToFirefoxExtension, true)
2020

2121
interface TestBrowsersType {
2222
name: string
@@ -26,52 +26,78 @@ interface TestBrowsersType {
2626

2727
const browsers: TestBrowsersType[] = [
2828
{
29-
name: 'chromium',
29+
name: 'chrome',
3030
test: testChrome,
3131
extensionPath: pathToChromeExtension
3232
},
33-
{
34-
name: 'firefox',
35-
test: testFirefox,
36-
extensionPath: pathToFirefoxExtension
37-
}
33+
// {
34+
// name: 'firefox',
35+
// test: testFirefox,
36+
// extensionPath: pathToFirefoxExtension
37+
// }
3838
]
3939

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

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

56-
test(`as ${name} extension - should exist an h1 element with specified content`, async ({
56+
test(`as ${name} extension - should inject an h1 element with the specified content`, async ({
57+
page
58+
}) => {
59+
await page.goto('https://extension.js.org/')
60+
const h1 = page.locator('body > div.content_script > h1.content_title')
61+
await test.expect(h1).toHaveText('Welcome to your Content Script Extension')
62+
})
63+
64+
test(`as ${name} extension - should ensure the logo image is loaded correctly`, async ({
65+
page
66+
}) => {
67+
await page.goto('https://extension.js.org/')
68+
const logo = page.locator('body > div.content_script > img.content_logo')
69+
const logoSrc = await logo.getAttribute('src')
70+
71+
// Ensure the logo src is correct and the image is loaded
72+
test.expect(logoSrc).toContain('logo.svg')
73+
await test.expect(logo).toBeVisible()
74+
})
75+
76+
test(`as ${name} extension - should check the description link is rendered correctly`, async ({
5777
page
5878
}) => {
5979
await page.goto('https://extension.js.org/')
60-
const h1 = page.locator('body > div.content_script > h1')
61-
await test.expect(h1).toHaveText('Change the background-color ⬇')
80+
const link = page.locator(
81+
'body > div.content_script > p.content_description > a'
82+
)
83+
84+
// Ensure the href attribute is correct and the link is visible
85+
await test.expect(link).toHaveAttribute('href', 'https://extension.js.org')
86+
await test.expect(link).toBeVisible()
6287
})
6388

64-
test(`as ${name} extension - should exist a default color value`, async ({
89+
test(`as ${name} extension - should ensure the h1 element has the default color`, async ({
6590
page
6691
}) => {
6792
await page.goto('https://extension.js.org/')
68-
const h1 = page.locator('body > div.content_script > h1')
93+
const h1 = page.locator('body > div.content_script > h1.content_title')
94+
6995
const color = await page.evaluate(
70-
(locator) => {
71-
return window.getComputedStyle(locator!).getPropertyValue('color')
72-
},
96+
(locator) => window.getComputedStyle(locator!).getPropertyValue('color'),
7397
await h1.elementHandle()
7498
)
75-
await test.expect(color).toEqual('rgb(51, 51, 51)')
99+
100+
// Verify that the color is set correctly
101+
test.expect(color).toEqual('rgb(201, 201, 201)')
76102
})
77103
})
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import {test as base, firefox, type BrowserContext} from '@playwright/test'
2+
import {loadFirefoxAddon} from './messaging-client'
3+
4+
export const extensionFixturesFirefox = (
5+
pathToExtension: string,
6+
headless: boolean
7+
) => {
8+
return base.extend<{
9+
context: BrowserContext
10+
extensionId: string
11+
}>({
12+
context: async ({}, use) => {
13+
// Connect with the Extension.js remote desktop client
14+
const RDP_PORT = 9222
15+
16+
// Override or add custom preferences here if needed
17+
const masterPreferences = {}
18+
19+
// Create a temporary profile path for Firefox
20+
const firefoxProfilePath = ''
21+
22+
const context = await firefox.launchPersistentContext(
23+
firefoxProfilePath,
24+
{
25+
headless: headless,
26+
args: [`-start-debugger-server=${String(RDP_PORT)}`].filter(
27+
(arg) => !!arg
28+
),
29+
firefoxUserPrefs: {
30+
...masterPreferences,
31+
'devtools.debugger.remote-enabled': true,
32+
'devtools.debugger.prompt-connection': false
33+
}
34+
}
35+
)
36+
37+
38+
// Use the context in the test
39+
await use(context)
40+
41+
// Await the addon loading to ensure it's complete before proceeding
42+
await loadFirefoxAddon(RDP_PORT, '127.0.0.1', pathToExtension)
43+
44+
// Close the context after the test
45+
// await context.close()
46+
},
47+
extensionId: async ({context}, use) => {
48+
// For manifest v2:
49+
let [background] = context.backgroundPages()
50+
if (!background) background = await context.waitForEvent('backgroundpage')
51+
52+
const extensionId = background.url().split('/')[2]
53+
await use(extensionId)
54+
}
55+
})
56+
}

examples/extension-fixtures.ts

Lines changed: 48 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -9,49 +9,59 @@ export const extensionFixtures = (
99
extensionId: string
1010
}>({
1111
context: async ({}, use) => {
12-
const context = await chromium.launchPersistentContext('', {
13-
headless: false,
14-
args: [
15-
headless ? `--headless=new` : '',
16-
`--disable-extensions-except=${pathToExtension}`,
17-
`--load-extension=${pathToExtension}`,
18-
'--no-first-run', // Disable Chrome's native first run experience.
19-
'--disable-client-side-phishing-detection', // Disables client-side phishing detection
20-
'--disable-component-extensions-with-background-pages', // Disable some built-in extensions that aren't affected by '--disable-extensions'
21-
'--disable-default-apps', // Disable installation of default apps
22-
'--disable-features=InterestFeedContentSuggestions', // Disables the Discover feed on NTP
23-
'--disable-features=Translate', // Disables Chrome translation, both the manual option and the popup prompt when a page with differing language is detected.
24-
'--hide-scrollbars', // Hide scrollbars from screenshots.
25-
'--mute-audio', // Mute any audio
26-
'--no-default-browser-check', // Disable the default browser check, do not prompt to set it as such
27-
'--no-first-run', // Skip first run wizards
28-
'--ash-no-nudges', // Avoids blue bubble "user education" nudges (eg., "… give your browser a new look", Memory Saver)
29-
'--disable-search-engine-choice-screen', // Disable the 2023+ search engine choice screen
30-
'--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.
31-
'--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"
32-
'--disable-background-networking', // Disable various background network services, including extension updating, safe browsing service, upgrade detector, translate, UMA
33-
'--disable-breakpad', // Disable crashdump collection (reporting is already disabled in Chromium)
34-
'--disable-component-update', // Don't update the browser 'components' listed at chrome://components/
35-
'--disable-domain-reliability', // Disables Domain Reliability Monitoring, which tracks whether the browser has difficulty contacting Google-owned sites and uploads reports to Google.
36-
'--disable-features=AutofillServerCommunicatio', // Disables autofill server communication. This feature isn't disabled via other 'parent' flags.
37-
'--disable-features=CertificateTransparencyComponentUpdate',
38-
'--disable-sync', // Disable syncing to a Google account
39-
'--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
40-
'--disable-features=DialMediaRouteProvider', // A weaker form of disabling the MediaRouter feature. See that flag's details.
41-
'--no-pings', // Don't send hyperlink auditing pings
42-
'--enable-features=SidePanelUpdates' // Ensure the side panel is visible. This is used for testing the side panel feature.
43-
].filter((arg) => !!arg)
44-
})
12+
// Override or add custom preferences here if needed
13+
// const masterPreferences = {}
14+
15+
// Create a temporary profile path for Firefox
16+
// Optionally set a custom profile path if needed, or leave it as ''
17+
const chromiumProfilePath = ''
18+
19+
const context = await chromium.launchPersistentContext(
20+
chromiumProfilePath,
21+
{
22+
headless: false,
23+
args: [
24+
headless ? `--headless=new` : '',
25+
`--disable-extensions-except=${pathToExtension}`,
26+
`--load-extension=${pathToExtension}`,
27+
'--no-first-run', // Disable Chrome's native first run experience.
28+
'--disable-client-side-phishing-detection', // Disables client-side phishing detection
29+
'--disable-component-extensions-with-background-pages', // Disable some built-in extensions that aren't affected by '--disable-extensions'
30+
'--disable-default-apps', // Disable installation of default apps
31+
'--disable-features=InterestFeedContentSuggestions', // Disables the Discover feed on NTP
32+
'--disable-features=Translate', // Disables Chrome translation, both the manual option and the popup prompt when a page with differing language is detected.
33+
'--hide-scrollbars', // Hide scrollbars from screenshots.
34+
'--mute-audio', // Mute any audio
35+
'--no-default-browser-check', // Disable the default browser check, do not prompt to set it as such
36+
'--no-first-run', // Skip first run wizards
37+
'--ash-no-nudges', // Avoids blue bubble "user education" nudges (eg., "… give your browser a new look", Memory Saver)
38+
'--disable-search-engine-choice-screen', // Disable the 2023+ search engine choice screen
39+
'--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.
40+
'--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"
41+
'--disable-background-networking', // Disable various background network services, including extension updating, safe browsing service, upgrade detector, translate, UMA
42+
'--disable-breakpad', // Disable crashdump collection (reporting is already disabled in Chromium)
43+
'--disable-component-update', // Don't update the browser 'components' listed at chrome://components/
44+
'--disable-domain-reliability', // Disables Domain Reliability Monitoring, which tracks whether the browser has difficulty contacting Google-owned sites and uploads reports to Google.
45+
'--disable-features=AutofillServerCommunicatio', // Disables autofill server communication. This feature isn't disabled via other 'parent' flags.
46+
'--disable-features=CertificateTransparencyComponentUpdate',
47+
'--disable-sync', // Disable syncing to a Google account
48+
'--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
49+
'--disable-features=DialMediaRouteProvider', // A weaker form of disabling the MediaRouter feature. See that flag's details.
50+
'--no-pings', // Don't send hyperlink auditing pings
51+
'--enable-features=SidePanelUpdates' // Ensure the side panel is visible. This is used for testing the side panel feature.
52+
].filter((arg) => !!arg)
53+
}
54+
)
4555
await use(context)
4656
await context.close()
4757
},
4858
extensionId: async ({context}, use) => {
4959
/*
50-
// for manifest v2:
51-
let [background] = context.backgroundPages()
52-
if (!background)
53-
background = await context.waitForEvent('backgroundpage')
54-
*/
60+
// for manifest v2:
61+
let [background] = context.backgroundPages()
62+
if (!background)
63+
background = await context.waitForEvent('backgroundpage')
64+
*/
5565

5666
// for manifest v3:
5767
let [background] = context.serviceWorkers()

examples/messaging-client.ts

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import {Buffer} from 'buffer'
2+
import net from 'net'
3+
4+
export const loadFirefoxAddon = (
5+
port: number,
6+
host: string,
7+
addonPath: string
8+
) => {
9+
return new Promise<boolean>((resolve) => {
10+
const socket = net.connect({
11+
port,
12+
host
13+
})
14+
15+
let success = false
16+
17+
socket.once('error', () => {})
18+
socket.once('close', () => {
19+
resolve(success)
20+
})
21+
22+
const send = (data: Record<string, string>) => {
23+
const raw = Buffer.from(JSON.stringify(data))
24+
25+
socket.write(`${raw.length}`)
26+
socket.write(':')
27+
socket.write(raw)
28+
}
29+
30+
send({
31+
to: 'root',
32+
type: 'getRoot'
33+
})
34+
35+
const onMessage = (message: any) => {
36+
if (message.addonsActor) {
37+
send({
38+
to: message.addonsActor,
39+
type: 'installTemporaryAddon',
40+
addonPath
41+
})
42+
}
43+
44+
if (message.addon) {
45+
success = true
46+
socket.end()
47+
}
48+
49+
if (message.error) {
50+
socket.end()
51+
}
52+
}
53+
54+
const buffers: Buffer[] = []
55+
// let remainingBytes = 0
56+
57+
socket.on('data', (data) => {
58+
buffers.push(data)
59+
60+
const buffer = Buffer.concat(buffers)
61+
const colonIndex = buffer.indexOf(':')
62+
63+
if (colonIndex === -1) return
64+
65+
const expectedLength = parseInt(
66+
buffer.subarray(0, colonIndex).toString(),
67+
10
68+
)
69+
70+
if (!Number.isFinite(expectedLength)) {
71+
throw new Error('Invalid message size')
72+
}
73+
74+
const remainingData = buffer.subarray(colonIndex + 1)
75+
if (remainingData.length >= expectedLength) {
76+
const message = remainingData.subarray(0, expectedLength).toString()
77+
buffers.length = 0 // Clear buffer after processing
78+
79+
try {
80+
const json = JSON.parse(message)
81+
onMessage(json)
82+
} catch (error) {
83+
console.error('Error parsing JSON:', error)
84+
}
85+
}
86+
})
87+
})
88+
}

playwright.config.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -36,12 +36,12 @@ export default defineConfig({
3636
{
3737
name: 'chromium',
3838
use: {...devices['Desktop Chrome']}
39-
}
39+
},
4040

41-
// {
42-
// name: 'firefox',
43-
// use: { ...devices['Desktop Firefox'] },
44-
// },
41+
{
42+
name: 'firefox',
43+
use: {...devices['Desktop Firefox']}
44+
}
4545

4646
// {
4747
// name: 'webkit',

0 commit comments

Comments
 (0)