feat: add support for content scripts 'all_frames' option (#17258)
* feat: add support for content scripts 'all_frames' option * merged content script tests 'all_frames' test now runs on all variants of sandbox/contentIsolation configurations :D
This commit is contained in:
parent
b7fc50b7ca
commit
8ee153dae1
11 changed files with 196 additions and 45 deletions
|
@ -345,7 +345,8 @@ const injectContentScripts = function (manifest) {
|
|||
matches: script.matches,
|
||||
js: script.js ? script.js.map(readArrayOfFiles) : [],
|
||||
css: script.css ? script.css.map(readArrayOfFiles) : [],
|
||||
runAt: script.run_at || 'document_idle'
|
||||
runAt: script.run_at || 'document_idle',
|
||||
allFrames: script.all_frames || false
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -69,6 +69,7 @@ const runAllStylesheet = function (css: Array<Electron.InjectionBase>) {
|
|||
// Run injected scripts.
|
||||
// https://developer.chrome.com/extensions/content_scripts
|
||||
const injectContentScript = function (extensionId: string, script: Electron.ContentScript) {
|
||||
if (!process.isMainFrame && !script.allFrames) return
|
||||
if (!script.matches.some(matchesPattern)) return
|
||||
|
||||
if (script.js) {
|
||||
|
|
|
@ -85,9 +85,7 @@ switch (window.location.protocol) {
|
|||
windowSetup(ipcRendererInternal, guestInstanceId, openerId, isHiddenPage, usesNativeWindowOpen)
|
||||
|
||||
// Inject content scripts.
|
||||
if (process.isMainFrame) {
|
||||
require('@electron/internal/renderer/content-scripts-injector')(process.getRenderProcessPreferences)
|
||||
}
|
||||
require('@electron/internal/renderer/content-scripts-injector')(process.getRenderProcessPreferences)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -3,68 +3,148 @@ const { remote } = require('electron')
|
|||
const path = require('path')
|
||||
|
||||
const { closeWindow } = require('./window-helpers')
|
||||
const { emittedNTimes } = require('./events-helpers')
|
||||
|
||||
const { BrowserWindow } = remote
|
||||
const { BrowserWindow, ipcMain } = remote
|
||||
|
||||
describe('chrome extension content scripts', () => {
|
||||
const fixtures = path.resolve(__dirname, 'fixtures')
|
||||
const extensionPath = path.resolve(fixtures, 'extensions')
|
||||
|
||||
const addExtension = (name) => BrowserWindow.addExtension(path.resolve(extensionPath, name))
|
||||
const removeAllExtensions = () => {
|
||||
Object.keys(BrowserWindow.getExtensions()).map(extName => {
|
||||
BrowserWindow.removeExtension(extName)
|
||||
})
|
||||
}
|
||||
|
||||
let responseIdCounter = 0
|
||||
const executeJavaScriptInFrame = (webContents, frameRoutingId, code) => {
|
||||
return new Promise(resolve => {
|
||||
const responseId = responseIdCounter++
|
||||
ipcMain.once(`executeJavaScriptInFrame_${responseId}`, (event, result) => {
|
||||
resolve(result)
|
||||
})
|
||||
webContents.send('executeJavaScriptInFrame', frameRoutingId, code, responseId)
|
||||
})
|
||||
}
|
||||
|
||||
describe('chrome content scripts', () => {
|
||||
const generateTests = (sandboxEnabled, contextIsolationEnabled) => {
|
||||
describe(`with sandbox ${sandboxEnabled ? 'enabled' : 'disabled'} and context isolation ${contextIsolationEnabled ? 'enabled' : 'disabled'}`, () => {
|
||||
let w
|
||||
|
||||
beforeEach(async () => {
|
||||
await closeWindow(w)
|
||||
w = new BrowserWindow({
|
||||
show: false,
|
||||
width: 400,
|
||||
height: 400,
|
||||
webPreferences: {
|
||||
contextIsolation: contextIsolationEnabled,
|
||||
sandbox: sandboxEnabled
|
||||
}
|
||||
describe('supports "run_at" option', () => {
|
||||
beforeEach(async () => {
|
||||
await closeWindow(w)
|
||||
w = new BrowserWindow({
|
||||
show: false,
|
||||
width: 400,
|
||||
height: 400,
|
||||
webPreferences: {
|
||||
contextIsolation: contextIsolationEnabled,
|
||||
sandbox: sandboxEnabled
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
Object.keys(BrowserWindow.getExtensions()).map(extName => {
|
||||
BrowserWindow.removeExtension(extName)
|
||||
afterEach(() => {
|
||||
removeAllExtensions()
|
||||
return closeWindow(w).then(() => { w = null })
|
||||
})
|
||||
return closeWindow(w).then(() => { w = null })
|
||||
})
|
||||
|
||||
const addExtension = (name) => {
|
||||
const extensionPath = path.join(__dirname, 'fixtures', 'extensions', name)
|
||||
BrowserWindow.addExtension(extensionPath)
|
||||
}
|
||||
it('should run content script at document_start', (done) => {
|
||||
addExtension('content-script-document-start')
|
||||
w.webContents.once('dom-ready', () => {
|
||||
w.webContents.executeJavaScript('document.documentElement.style.backgroundColor', (result) => {
|
||||
expect(result).to.equal('red')
|
||||
done()
|
||||
})
|
||||
})
|
||||
w.loadURL('about:blank')
|
||||
})
|
||||
|
||||
it('should run content script at document_start', (done) => {
|
||||
addExtension('content-script-document-start')
|
||||
w.webContents.once('dom-ready', () => {
|
||||
w.webContents.executeJavaScript('document.documentElement.style.backgroundColor', (result) => {
|
||||
it('should run content script at document_idle', (done) => {
|
||||
addExtension('content-script-document-idle')
|
||||
w.loadURL('about:blank')
|
||||
w.webContents.executeJavaScript('document.body.style.backgroundColor', (result) => {
|
||||
expect(result).to.equal('red')
|
||||
done()
|
||||
})
|
||||
})
|
||||
w.loadURL('about:blank')
|
||||
})
|
||||
|
||||
it('should run content script at document_idle', (done) => {
|
||||
addExtension('content-script-document-idle')
|
||||
w.loadURL('about:blank')
|
||||
w.webContents.executeJavaScript('document.body.style.backgroundColor', (result) => {
|
||||
expect(result).to.equal('red')
|
||||
done()
|
||||
it('should run content script at document_end', (done) => {
|
||||
addExtension('content-script-document-end')
|
||||
w.webContents.once('did-finish-load', () => {
|
||||
w.webContents.executeJavaScript('document.documentElement.style.backgroundColor', (result) => {
|
||||
expect(result).to.equal('red')
|
||||
done()
|
||||
})
|
||||
})
|
||||
w.loadURL('about:blank')
|
||||
})
|
||||
})
|
||||
|
||||
it('should run content script at document_end', (done) => {
|
||||
addExtension('content-script-document-end')
|
||||
w.webContents.once('did-finish-load', () => {
|
||||
w.webContents.executeJavaScript('document.documentElement.style.backgroundColor', (result) => {
|
||||
expect(result).to.equal('red')
|
||||
done()
|
||||
describe('supports "all_frames" option', () => {
|
||||
const contentScript = path.resolve(fixtures, 'extensions/content-script')
|
||||
|
||||
// Computed style values
|
||||
const COLOR_RED = `rgb(255, 0, 0)`
|
||||
const COLOR_BLUE = `rgb(0, 0, 255)`
|
||||
const COLOR_TRANSPARENT = `rgba(0, 0, 0, 0)`
|
||||
|
||||
before(() => {
|
||||
BrowserWindow.addExtension(contentScript)
|
||||
})
|
||||
|
||||
after(() => {
|
||||
BrowserWindow.removeExtension('content-script-test')
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
w = new BrowserWindow({
|
||||
show: false,
|
||||
webPreferences: {
|
||||
// enable content script injection in subframes
|
||||
nodeIntegrationInSubFrames: true,
|
||||
preload: path.join(contentScript, 'all_frames-preload.js')
|
||||
}
|
||||
})
|
||||
})
|
||||
w.loadURL('about:blank')
|
||||
|
||||
afterEach(() =>
|
||||
closeWindow(w).then(() => {
|
||||
w = null
|
||||
})
|
||||
)
|
||||
|
||||
it('applies matching rules in subframes', async () => {
|
||||
const detailsPromise = emittedNTimes(w.webContents, 'did-frame-finish-load', 2)
|
||||
w.loadFile(path.join(contentScript, 'frame-with-frame.html'))
|
||||
const frameEvents = await detailsPromise
|
||||
await Promise.all(
|
||||
frameEvents.map(async frameEvent => {
|
||||
const [, isMainFrame, , frameRoutingId] = frameEvent
|
||||
const result = await executeJavaScriptInFrame(
|
||||
w.webContents,
|
||||
frameRoutingId,
|
||||
`(() => {
|
||||
const a = document.getElementById('all_frames_enabled')
|
||||
const b = document.getElementById('all_frames_disabled')
|
||||
return {
|
||||
enabledColor: getComputedStyle(a).backgroundColor,
|
||||
disabledColor: getComputedStyle(b).backgroundColor
|
||||
}
|
||||
})()`
|
||||
)
|
||||
expect(result.enabledColor).to.equal(COLOR_RED)
|
||||
if (isMainFrame) {
|
||||
expect(result.disabledColor).to.equal(COLOR_BLUE)
|
||||
} else {
|
||||
expect(result.disabledColor).to.equal(COLOR_TRANSPARENT) // null color
|
||||
}
|
||||
})
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
|
3
spec/fixtures/extensions/content-script/all_frames-disabled.css
vendored
Normal file
3
spec/fixtures/extensions/content-script/all_frames-disabled.css
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
#all_frames_disabled {
|
||||
background: blue;
|
||||
}
|
3
spec/fixtures/extensions/content-script/all_frames-enabled.css
vendored
Normal file
3
spec/fixtures/extensions/content-script/all_frames-enabled.css
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
#all_frames_enabled {
|
||||
background: red;
|
||||
}
|
14
spec/fixtures/extensions/content-script/all_frames-preload.js
vendored
Normal file
14
spec/fixtures/extensions/content-script/all_frames-preload.js
vendored
Normal file
|
@ -0,0 +1,14 @@
|
|||
const { ipcRenderer, webFrame } = require('electron')
|
||||
|
||||
if (process.isMainFrame) {
|
||||
// https://github.com/electron/electron/issues/17252
|
||||
ipcRenderer.on('executeJavaScriptInFrame', (event, frameRoutingId, code, responseId) => {
|
||||
const frame = webFrame.findFrameByRoutingId(frameRoutingId)
|
||||
if (!frame) {
|
||||
throw new Error(`Can't find frame for routing ID ${frameRoutingId}`)
|
||||
}
|
||||
frame.executeJavaScript(code, false, result => {
|
||||
event.sender.send(`executeJavaScriptInFrame_${responseId}`, result)
|
||||
})
|
||||
})
|
||||
}
|
15
spec/fixtures/extensions/content-script/frame-with-frame.html
vendored
Normal file
15
spec/fixtures/extensions/content-script/frame-with-frame.html
vendored
Normal file
|
@ -0,0 +1,15 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
||||
<title>Document</title>
|
||||
</head>
|
||||
<body>
|
||||
This is a frame, is has one child
|
||||
<iframe src="./frame.html"></iframe>
|
||||
<div id="all_frames_enabled"></div>
|
||||
<div id="all_frames_disabled"></div>
|
||||
</body>
|
||||
</html>
|
12
spec/fixtures/extensions/content-script/frame.html
vendored
Normal file
12
spec/fixtures/extensions/content-script/frame.html
vendored
Normal file
|
@ -0,0 +1,12 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Document</title>
|
||||
</head>
|
||||
<body>
|
||||
This is a frame, it has no children
|
||||
<div id="all_frames_enabled"></div>
|
||||
<div id="all_frames_disabled"></div>
|
||||
</body>
|
||||
</html>
|
19
spec/fixtures/extensions/content-script/manifest.json
vendored
Normal file
19
spec/fixtures/extensions/content-script/manifest.json
vendored
Normal file
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"name": "content-script-test",
|
||||
"version": "1.0",
|
||||
"content_scripts": [
|
||||
{
|
||||
"matches": ["<all_urls>"],
|
||||
"css": ["all_frames-enabled.css"],
|
||||
"run_at": "document_start",
|
||||
"all_frames": true
|
||||
},
|
||||
{
|
||||
"matches": ["<all_urls>"],
|
||||
"css": ["all_frames-disabled.css"],
|
||||
"run_at": "document_start",
|
||||
"all_frames": false
|
||||
}
|
||||
],
|
||||
"manifest_version": 2
|
||||
}
|
5
typings/internal-electron.d.ts
vendored
5
typings/internal-electron.d.ts
vendored
|
@ -39,6 +39,11 @@ declare namespace Electron {
|
|||
matches: {
|
||||
some: (input: (pattern: string) => boolean | RegExpMatchArray | null) => boolean;
|
||||
}
|
||||
/**
|
||||
* Whether to match all frames, or only the top one.
|
||||
* https://developer.chrome.com/extensions/content_scripts#frames
|
||||
*/
|
||||
allFrames: boolean
|
||||
}
|
||||
|
||||
interface RendererProcessPreference {
|
||||
|
|
Loading…
Reference in a new issue