mirror of
https://github.com/marktext/marktext.git
synced 2025-05-03 00:19:35 +08:00
Update Electron to v15 (#2772)
* Prepare Electron >=14 upgrade * Replace spectron with playwright * Upgrade Electron to v15 * Fix unit test issue with @electron/remote * Use per day cache directory for E2E tests * Fix code style
This commit is contained in:
parent
0dd09cc684
commit
bdaca98876
@ -15,7 +15,7 @@
|
||||
"build:clean": "cross-env BUILD_TARGET=clean node .electron-vue/build.js",
|
||||
"build:dev": "node .electron-vue/build.js",
|
||||
"dev": "cross-env node .electron-vue/dev-runner.js",
|
||||
"e2e": "yarn run pack && cross-env MARKTEXT_EXIT_ON_ERROR=1 mocha --timeout 10000 test/e2e",
|
||||
"e2e": "yarn run pack && cross-env MARKTEXT_EXIT_ON_ERROR=1 playwright test -c test/e2e/playwright.config.js test/e2e",
|
||||
"lint": "eslint --ext .js,.vue -f ./node_modules/eslint-friendly-formatter src test",
|
||||
"lint:fix": "eslint --ext .js,.vue -f ./node_modules/eslint-friendly-formatter --fix src test",
|
||||
"pack": "yarn run pack:main && yarn run pack:renderer",
|
||||
@ -33,6 +33,7 @@
|
||||
"validate-licenses": "node tools/validateLicenses.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@electron/remote": "^2.0.1",
|
||||
"@hfelix/electron-localshortcut": "^3.1.1",
|
||||
"@hfelix/electron-spellchecker": "^2.0.0",
|
||||
"@octokit/rest": "^16.43.2",
|
||||
@ -98,6 +99,7 @@
|
||||
"@babel/register": "^7.16.0",
|
||||
"@babel/runtime": "^7.16.3",
|
||||
"@markedjs/html-differ": "^3.0.4",
|
||||
"@playwright/test": "^1.17.1",
|
||||
"babel-eslint": "^10.1.0",
|
||||
"babel-loader": "^8.2.3",
|
||||
"babel-plugin-component": "^1.1.1",
|
||||
@ -112,7 +114,7 @@
|
||||
"del": "^5.1.0",
|
||||
"devtron": "^1.4.0",
|
||||
"dotenv": "^10.0.0",
|
||||
"electron": "^13.6.1",
|
||||
"electron": "^15.3.4",
|
||||
"electron-builder": "^22.14.8",
|
||||
"electron-devtools-installer": "^3.2.0",
|
||||
"electron-rebuild": "^3.2.5",
|
||||
@ -148,11 +150,11 @@
|
||||
"multispinner": "^0.2.1",
|
||||
"node-fetch": "^2.6.6",
|
||||
"node-loader": "^1.0.3",
|
||||
"playwright": "^1.17.1",
|
||||
"postcss-loader": "^3.0.0",
|
||||
"postcss-preset-env": "^6.7.0",
|
||||
"raw-loader": "^4.0.2",
|
||||
"require-dir": "^1.2.0",
|
||||
"spectron": "^15.0.0",
|
||||
"style-loader": "^2.0.0",
|
||||
"svg-sprite-loader": "^4.3.0",
|
||||
"svgo": "^1.3.2",
|
||||
|
@ -6,7 +6,6 @@ export const editorWinOptions = Object.freeze({
|
||||
minWidth: 550,
|
||||
minHeight: 350,
|
||||
webPreferences: {
|
||||
enableRemoteModule: true,
|
||||
contextIsolation: false,
|
||||
spellcheck: false,
|
||||
nodeIntegration: true,
|
||||
@ -23,7 +22,6 @@ export const preferencesWinOptions = Object.freeze({
|
||||
width: 950,
|
||||
height: 650,
|
||||
webPreferences: {
|
||||
enableRemoteModule: true,
|
||||
contextIsolation: false,
|
||||
spellcheck: false,
|
||||
nodeIntegration: true,
|
||||
|
@ -1,6 +1,7 @@
|
||||
import './globalSetting'
|
||||
import path from 'path'
|
||||
import { app, dialog } from 'electron'
|
||||
import { initialize as remoteInitializeServer } from '@electron/remote/main'
|
||||
import cli from './cli'
|
||||
import setupExceptionHandler, { initExceptionLogger } from './exceptionHandler'
|
||||
import log from 'electron-log'
|
||||
@ -12,10 +13,9 @@ import { getLogLevel } from './utils'
|
||||
const initializeLogger = appEnvironment => {
|
||||
log.transports.console.level = process.env.NODE_ENV === 'development' ? true : 'error'
|
||||
log.transports.rendererConsole = null
|
||||
log.transports.file.file = path.join(appEnvironment.paths.logPath, 'main.log')
|
||||
log.transports.file.resolvePath = () => path.join(appEnvironment.paths.logPath, 'main.log')
|
||||
log.transports.file.level = getLogLevel()
|
||||
log.transports.file.sync = true
|
||||
log.transports.file.init()
|
||||
initExceptionLogger()
|
||||
}
|
||||
|
||||
@ -80,5 +80,9 @@ log.transports.file.sync = false
|
||||
// Be careful when changing code before this line!
|
||||
// NOTE: Do not create classes or other code before this line!
|
||||
|
||||
// TODO: We should switch to another async API like https://nornagon.medium.com/electrons-remote-module-considered-harmful-70d69500f31.
|
||||
// Enable remote module
|
||||
remoteInitializeServer()
|
||||
|
||||
const marktext = new App(accessor, args)
|
||||
marktext.init()
|
||||
|
@ -1,5 +1,6 @@
|
||||
import path from 'path'
|
||||
import { BrowserWindow, dialog, ipcMain } from 'electron'
|
||||
import { enable as remoteEnable } from '@electron/remote/main'
|
||||
import log from 'electron-log'
|
||||
import windowStateKeeper from 'electron-window-state'
|
||||
import { isChildOfDirectory, isSamePathSync } from 'common/filesystem/paths'
|
||||
@ -68,6 +69,7 @@ class EditorWindow extends BaseWindow {
|
||||
winOptions.backgroundColor = this._getPreferredBackgroundColor(theme)
|
||||
|
||||
let win = this.browserWindow = new BrowserWindow(winOptions)
|
||||
remoteEnable(win.webContents)
|
||||
this.id = win.id
|
||||
|
||||
// Create a menu for the current window
|
||||
|
@ -1,5 +1,6 @@
|
||||
import path from 'path'
|
||||
import { BrowserWindow, ipcMain } from 'electron'
|
||||
import { enable as remoteEnable } from '@electron/remote/main'
|
||||
import electronLocalshortcut from '@hfelix/electron-localshortcut'
|
||||
import BaseWindow, { WindowLifecycle, WindowType } from './base'
|
||||
import { centerWindowOptions } from './utils'
|
||||
@ -45,6 +46,7 @@ class SettingWindow extends BaseWindow {
|
||||
winOptions.backgroundColor = this._getPreferredBackgroundColor(theme)
|
||||
|
||||
let win = this.browserWindow = new BrowserWindow(winOptions)
|
||||
remoteEnable(win.webContents)
|
||||
this.id = win.id
|
||||
|
||||
// Create a menu for the current window
|
||||
|
3
src/renderer/bootstrap.js
vendored
3
src/renderer/bootstrap.js
vendored
@ -9,10 +9,9 @@ const configureLogger = () => {
|
||||
const { debug, paths, windowId } = global.marktext.env
|
||||
log.transports.console.level = process.env.NODE_ENV === 'development' // mirror to window console
|
||||
log.transports.mainConsole = null
|
||||
log.transports.file.file = path.join(paths.logPath, `editor-${windowId}.log`)
|
||||
log.transports.file.resolvePath = () => path.join(paths.logPath, `editor-${windowId}.log`)
|
||||
log.transports.file.level = debug ? 'debug' : 'info'
|
||||
log.transports.file.sync = false
|
||||
log.transports.file.init()
|
||||
exceptionLogger = log.error
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,6 @@
|
||||
// List of all static commands that are loaded into command center.
|
||||
import { ipcRenderer, remote, shell } from 'electron'
|
||||
import { ipcRenderer, shell } from 'electron'
|
||||
import { getCurrentWindow } from '@electron/remote'
|
||||
import bus from '../bus'
|
||||
import { delay, isOsx } from '@/util'
|
||||
import { isUpdatable } from './utils'
|
||||
@ -509,7 +510,7 @@ const commands = [
|
||||
id: 'window.minimize',
|
||||
description: 'Window: Minimize',
|
||||
execute: async () => {
|
||||
remote.getCurrentWindow().minimize()
|
||||
getCurrentWindow().minimize()
|
||||
}
|
||||
}, {
|
||||
id: 'window.always-on-top',
|
||||
@ -521,7 +522,7 @@ const commands = [
|
||||
id: 'window.toggle-full-screen',
|
||||
description: 'Window: Toggle Full Screen',
|
||||
execute: async () => {
|
||||
const win = remote.getCurrentWindow()
|
||||
const win = getCurrentWindow()
|
||||
win.setFullScreen(!win.isFullScreen())
|
||||
}
|
||||
},
|
||||
|
@ -98,7 +98,8 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ipcRenderer, remote } from 'electron'
|
||||
import { ipcRenderer } from 'electron'
|
||||
import { getCurrentWindow, Menu as RemoteMenu } from '@electron/remote'
|
||||
import { mapState } from 'vuex'
|
||||
import { minimizePath, restorePath, maximizePath, closePath } from '../../assets/window-controls.js'
|
||||
import { PATH_SEPARATOR } from '../../config'
|
||||
@ -130,8 +131,8 @@ export default {
|
||||
this.windowIconMaximize = maximizePath
|
||||
this.windowIconClose = closePath
|
||||
return {
|
||||
isFullScreen: remote.getCurrentWindow().isFullScreen(),
|
||||
isMaximized: remote.getCurrentWindow().isMaximized(),
|
||||
isFullScreen: getCurrentWindow().isFullScreen(),
|
||||
isMaximized: getCurrentWindow().isMaximized(),
|
||||
show: 'word'
|
||||
}
|
||||
},
|
||||
@ -189,11 +190,11 @@ export default {
|
||||
},
|
||||
|
||||
handleCloseClick () {
|
||||
remote.getCurrentWindow().close()
|
||||
getCurrentWindow().close()
|
||||
},
|
||||
|
||||
handleMaximizeClick () {
|
||||
const win = remote.getCurrentWindow()
|
||||
const win = getCurrentWindow()
|
||||
if (win.isFullScreen()) {
|
||||
win.setFullScreen(false)
|
||||
} else if (win.isMaximized()) {
|
||||
@ -210,15 +211,12 @@ export default {
|
||||
},
|
||||
|
||||
handleMinimizeClick () {
|
||||
remote.getCurrentWindow().minimize()
|
||||
getCurrentWindow().minimize()
|
||||
},
|
||||
|
||||
handleMenuClick () {
|
||||
const win = remote.getCurrentWindow()
|
||||
remote
|
||||
.Menu
|
||||
.getApplicationMenu()
|
||||
.popup({ window: win, x: 23, y: 20 })
|
||||
const win = getCurrentWindow()
|
||||
RemoteMenu.getApplicationMenu().popup({ window: win, x: 23, y: 20 })
|
||||
},
|
||||
|
||||
rename () {
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { remote } from 'electron'
|
||||
import { getCurrentWindow, Menu as RemoteMenu, MenuItem as RemoteMenuItem } from '@electron/remote'
|
||||
import {
|
||||
CUT,
|
||||
COPY,
|
||||
@ -12,7 +12,6 @@ import {
|
||||
} from './menuItems'
|
||||
import spellcheckMenuBuilder from './spellcheck'
|
||||
|
||||
const { Menu, MenuItem } = remote
|
||||
const CONTEXT_ITEMS = [INSERT_BEFORE, INSERT_AFTER, SEPARATOR, CUT, COPY, PASTE, SEPARATOR, COPY_AS_MARKDOWN, COPY_AS_HTML, PASTE_AS_PLAIN_TEXT]
|
||||
|
||||
/**
|
||||
@ -27,17 +26,17 @@ const CONTEXT_ITEMS = [INSERT_BEFORE, INSERT_AFTER, SEPARATOR, CUT, COPY, PASTE,
|
||||
*/
|
||||
export const showContextMenu = (event, selection, spellchecker, selectedWord, wordSuggestions, replaceCallback) => {
|
||||
const { start, end } = selection
|
||||
const menu = new Menu()
|
||||
const win = remote.getCurrentWindow()
|
||||
const menu = new RemoteMenu()
|
||||
const win = getCurrentWindow()
|
||||
const disableCutAndCopy = start.key === end.key && start.offset === end.offset
|
||||
|
||||
const spellingSubmenu = spellcheckMenuBuilder(spellchecker, selectedWord, wordSuggestions, replaceCallback)
|
||||
if (spellingSubmenu) {
|
||||
menu.append(new MenuItem({
|
||||
menu.append(new RemoteMenuItem({
|
||||
label: 'Spelling...',
|
||||
submenu: spellingSubmenu
|
||||
}))
|
||||
menu.append(new MenuItem(SEPARATOR))
|
||||
menu.append(new RemoteMenuItem(SEPARATOR))
|
||||
}
|
||||
|
||||
[CUT, COPY, COPY_AS_HTML, COPY_AS_MARKDOWN].forEach(item => {
|
||||
@ -45,7 +44,7 @@ export const showContextMenu = (event, selection, spellchecker, selectedWord, wo
|
||||
})
|
||||
|
||||
CONTEXT_ITEMS.forEach(item => {
|
||||
menu.append(new MenuItem(item))
|
||||
menu.append(new RemoteMenuItem(item))
|
||||
})
|
||||
menu.popup([{ window: win, x: event.clientX, y: event.clientY }])
|
||||
}
|
||||
|
@ -1,11 +1,9 @@
|
||||
import { remote } from 'electron'
|
||||
import { MenuItem as RemoteMenuItem } from '@electron/remote'
|
||||
import log from 'electron-log'
|
||||
import bus from '@/bus'
|
||||
import { getLanguageName } from '@/spellchecker/languageMap'
|
||||
import { SEPARATOR } from './menuItems'
|
||||
|
||||
const { MenuItem } = remote
|
||||
|
||||
/**
|
||||
* Build the spell checker menu depending on input.
|
||||
*
|
||||
@ -24,7 +22,7 @@ export default (spellchecker, selectedWord, wordSuggestions, replaceCallback) =>
|
||||
const availableDictionaries = spellchecker.getAvailableDictionaries()
|
||||
const availableDictionariesSubmenu = []
|
||||
for (const dict of availableDictionaries) {
|
||||
availableDictionariesSubmenu.push(new MenuItem({
|
||||
availableDictionariesSubmenu.push(new RemoteMenuItem({
|
||||
label: getLanguageName(dict),
|
||||
enabled: dict !== currentLanguage,
|
||||
click () {
|
||||
@ -33,7 +31,7 @@ export default (spellchecker, selectedWord, wordSuggestions, replaceCallback) =>
|
||||
}))
|
||||
}
|
||||
|
||||
spellingSubmenu.push(new MenuItem({
|
||||
spellingSubmenu.push(new RemoteMenuItem({
|
||||
label: 'Change Language...',
|
||||
submenu: availableDictionariesSubmenu
|
||||
}))
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { remote } from 'electron'
|
||||
import { getCurrentWindow, Menu as RemoteMenu, MenuItem as RemoteMenuItem } from '@electron/remote'
|
||||
import {
|
||||
SEPARATOR,
|
||||
NEW_FILE,
|
||||
@ -11,11 +11,9 @@ import {
|
||||
SHOW_IN_FOLDER
|
||||
} from './menuItems'
|
||||
|
||||
const { Menu, MenuItem } = remote
|
||||
|
||||
export const showContextMenu = (event, hasPathCache) => {
|
||||
const menu = new Menu()
|
||||
const win = remote.getCurrentWindow()
|
||||
const menu = new RemoteMenu()
|
||||
const win = getCurrentWindow()
|
||||
const CONTEXT_ITEMS = [
|
||||
NEW_FILE,
|
||||
NEW_DIRECTORY,
|
||||
@ -33,7 +31,7 @@ export const showContextMenu = (event, hasPathCache) => {
|
||||
PASTE.enabled = hasPathCache
|
||||
|
||||
CONTEXT_ITEMS.forEach(item => {
|
||||
menu.append(new MenuItem(item))
|
||||
menu.append(new RemoteMenuItem(item))
|
||||
})
|
||||
menu.popup([{ window: win, x: event.clientX, y: event.clientY }])
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { remote } from 'electron'
|
||||
import { getCurrentWindow, Menu as RemoteMenu, MenuItem as RemoteMenuItem } from '@electron/remote'
|
||||
import {
|
||||
CLOSE_THIS,
|
||||
CLOSE_OTHERS,
|
||||
@ -10,11 +10,9 @@ import {
|
||||
SHOW_IN_FOLDER
|
||||
} from './menuItems'
|
||||
|
||||
const { Menu, MenuItem } = remote
|
||||
|
||||
export const showContextMenu = (event, tab) => {
|
||||
const menu = new Menu()
|
||||
const win = remote.getCurrentWindow()
|
||||
const menu = new RemoteMenu()
|
||||
const win = getCurrentWindow()
|
||||
const { pathname } = tab
|
||||
const CONTEXT_ITEMS = [CLOSE_THIS, CLOSE_OTHERS, CLOSE_SAVED, CLOSE_ALL, SEPARATOR, RENAME, COPY_PATH, SHOW_IN_FOLDER]
|
||||
const FILE_CONTEXT_ITEMS = [RENAME, COPY_PATH, SHOW_IN_FOLDER]
|
||||
@ -24,7 +22,7 @@ export const showContextMenu = (event, tab) => {
|
||||
})
|
||||
|
||||
CONTEXT_ITEMS.forEach(item => {
|
||||
const menuItem = new MenuItem(item)
|
||||
const menuItem = new RemoteMenuItem(item)
|
||||
menuItem._tabId = tab.id
|
||||
menu.append(menuItem)
|
||||
})
|
||||
|
@ -11,7 +11,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { remote } from 'electron'
|
||||
import { getCurrentWindow } from '@electron/remote'
|
||||
import { closePath } from '../../assets/window-controls.js'
|
||||
|
||||
export default {
|
||||
@ -21,7 +21,7 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
handleCloseClick () {
|
||||
remote.getCurrentWindow().close()
|
||||
getCurrentWindow().close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,7 +2,7 @@ import fs from 'fs-extra'
|
||||
import path from 'path'
|
||||
import { SpellChecker } from '@hfelix/electron-spellchecker'
|
||||
import axios from '../axios'
|
||||
import { dictionaryPath } from '../spellchecker'
|
||||
import { getDictionaryPath } from '../spellchecker'
|
||||
|
||||
/**
|
||||
* Try to download the given Hunspell dictionary.
|
||||
@ -15,6 +15,7 @@ export const downloadHunspellDictionary = async lang => {
|
||||
responseType: 'stream'
|
||||
})
|
||||
|
||||
const dictionaryPath = getDictionaryPath()
|
||||
await fs.ensureDir(dictionaryPath)
|
||||
|
||||
const dstFile = path.join(dictionaryPath, `${lang}.bdic`)
|
||||
@ -48,5 +49,5 @@ export const downloadHunspellDictionary = async lang => {
|
||||
* @param {string} lang The language to remove.
|
||||
*/
|
||||
export const deleteHunspellDictionary = async lang => {
|
||||
return await fs.remove(path.join(dictionaryPath, `${lang}.bdic`))
|
||||
return await fs.remove(path.join(getDictionaryPath(), `${lang}.bdic`))
|
||||
}
|
||||
|
@ -1,13 +1,15 @@
|
||||
import fs from 'fs-extra'
|
||||
import path from 'path'
|
||||
import os from 'os'
|
||||
import { remote } from 'electron'
|
||||
import { SpellCheckHandler, fallbackLocales, normalizeLanguageCode } from '@hfelix/electron-spellchecker'
|
||||
import { isDirectory, isFile } from 'common/filesystem'
|
||||
import { cloneObj, isOsx, isLinux, isWindows } from '@/util'
|
||||
|
||||
// NOTE: Hardcoded in "@hfelix/electron-spellchecker/src/spell-check-handler.js"
|
||||
export const dictionaryPath = path.join(remote.app.getPath('userData'), 'dictionaries')
|
||||
export const getDictionaryPath = () => {
|
||||
const { userDataPath } = global.marktext.paths
|
||||
return path.join(userDataPath, 'dictionaries')
|
||||
}
|
||||
|
||||
// Source: https://github.com/Microsoft/vscode/blob/master/src/vs/editor/common/model/wordHelper.ts
|
||||
// /(-?\d*\.\d\w*)|([^\`\~\!\@\#\$\%\^\&\*\(\)\-\=\+\[\{\]\}\\\|\;\:\'\"\,\.\<\>\/\?\s]+)/
|
||||
@ -73,6 +75,7 @@ export const validateLineCursor = selection => {
|
||||
* @returns {string[]} List of available Hunspell dictionary language codes.
|
||||
*/
|
||||
export const getAvailableHunspellDictionaries = () => {
|
||||
const dictionaryPath = getDictionaryPath()
|
||||
const dict = []
|
||||
// Search for dictionaries on filesystem.
|
||||
if (isDirectory(dictionaryPath)) {
|
||||
@ -140,7 +143,7 @@ export class SpellChecker {
|
||||
throw new Error('Invalid state.')
|
||||
}
|
||||
|
||||
this.provider = new SpellCheckHandler(dictionaryPath)
|
||||
this.provider = new SpellCheckHandler(getDictionaryPath())
|
||||
this.isHunspell = this.provider.isHunspell
|
||||
|
||||
// The spell checker is now initialized but not yet enabled. You need to call `init`.
|
||||
|
@ -1,14 +1,14 @@
|
||||
import { isLinux, isOsx, isWindows } from './index'
|
||||
import plist from 'plist'
|
||||
import { remote } from 'electron'
|
||||
import { clipboard as remoteClipboard } from '@electron/remote'
|
||||
|
||||
const hasClipboardFiles = () => {
|
||||
return remote.clipboard.has('NSFilenamesPboardType')
|
||||
return remoteClipboard.has('NSFilenamesPboardType')
|
||||
}
|
||||
|
||||
const getClipboardFiles = () => {
|
||||
if (!hasClipboardFiles()) { return [] }
|
||||
return plist.parse(remote.clipboard.read('NSFilenamesPboardType'))
|
||||
return plist.parse(remoteClipboard.read('NSFilenamesPboardType'))
|
||||
}
|
||||
|
||||
export const guessClipboardFilePath = () => {
|
||||
@ -17,7 +17,7 @@ export const guessClipboardFilePath = () => {
|
||||
const result = getClipboardFiles()
|
||||
return Array.isArray(result) && result.length ? result[0] : ''
|
||||
} else if (isWindows) {
|
||||
const rawFilePath = remote.clipboard.read('FileNameW')
|
||||
const rawFilePath = remoteClipboard.read('FileNameW')
|
||||
const filePath = rawFilePath.replace(new RegExp(String.fromCharCode(0), 'g'), '')
|
||||
return filePath && typeof filePath === 'string' ? filePath : ''
|
||||
} else {
|
||||
|
37
test/e2e/helpers.js
Normal file
37
test/e2e/helpers.js
Normal file
@ -0,0 +1,37 @@
|
||||
const os = require('os')
|
||||
const path = require('path')
|
||||
const { _electron } = require('playwright')
|
||||
|
||||
const mainEntrypoint = 'dist/electron/main.js'
|
||||
|
||||
const getDateAsFilename = () => {
|
||||
const date = new Date()
|
||||
return '' + date.getFullYear() + (date.getMonth() + 1) + date.getDay()
|
||||
}
|
||||
|
||||
const getTempPath = () => {
|
||||
const name = 'marktext-e2etest-' + getDateAsFilename()
|
||||
return path.join(os.tmpdir(), name)
|
||||
}
|
||||
|
||||
const getElectronPath = () => {
|
||||
const launcherName = process.platform === 'win32' ? 'electron.cmd' : 'electron'
|
||||
return path.resolve(path.join('node_modules', '.bin', launcherName))
|
||||
}
|
||||
|
||||
const launchElectron = async userArgs => {
|
||||
userArgs = userArgs || []
|
||||
const executablePath = getElectronPath()
|
||||
const args = [mainEntrypoint, '--user-data-dir', getTempPath()].concat(userArgs)
|
||||
const app = await _electron.launch({
|
||||
executablePath,
|
||||
args,
|
||||
timeout: 30000
|
||||
})
|
||||
const page = await app.firstWindow()
|
||||
await page.waitForLoadState('domcontentloaded')
|
||||
await new Promise((resolve) => setTimeout(resolve, 500))
|
||||
return { app, page }
|
||||
}
|
||||
|
||||
module.exports = { getElectronPath, launchElectron}
|
@ -1,18 +0,0 @@
|
||||
'use strict'
|
||||
|
||||
// Set BABEL_ENV to use proper env config
|
||||
process.env.BABEL_ENV = 'test'
|
||||
|
||||
// Enable use of ES6+ on required files
|
||||
require('@babel/register')({
|
||||
ignore: [/node_modules/]
|
||||
})
|
||||
|
||||
// Attach Chai APIs to global scope
|
||||
const { expect, should, assert } = require('chai')
|
||||
global.expect = expect
|
||||
global.should = should
|
||||
global.assert = assert
|
||||
|
||||
// Require all JS files in `./specs` for Mocha to consume
|
||||
require('require-dir')('./specs')
|
22
test/e2e/launch.spec.js
Normal file
22
test/e2e/launch.spec.js
Normal file
@ -0,0 +1,22 @@
|
||||
const { expect, test } = require('@playwright/test')
|
||||
const { launchElectron } = require('./helpers')
|
||||
|
||||
test.describe('Check Launch Mark Text', async () => {
|
||||
let app = null
|
||||
let page = null
|
||||
|
||||
test.beforeAll(async () => {
|
||||
const { app: electronApp, page: firstPage } = await launchElectron()
|
||||
app = electronApp
|
||||
page = firstPage
|
||||
})
|
||||
|
||||
test.afterAll(async () => {
|
||||
await app.close()
|
||||
})
|
||||
|
||||
test('Empty Mark Text', async () => {
|
||||
const title = await page.title()
|
||||
expect(/^Mark Text|Untitled-1 - Mark Text$/.test(title)).toBeTruthy()
|
||||
})
|
||||
})
|
9
test/e2e/playwright.config.js
Normal file
9
test/e2e/playwright.config.js
Normal file
@ -0,0 +1,9 @@
|
||||
const config = {
|
||||
workers: 1,
|
||||
use: {
|
||||
headless: false,
|
||||
viewport: { width: 1280, height: 720 },
|
||||
timeout: 30000
|
||||
}
|
||||
}
|
||||
module.exports = config
|
@ -1,17 +0,0 @@
|
||||
import utils from '../utils'
|
||||
|
||||
describe('Launch', function () {
|
||||
beforeEach(utils.beforeEach)
|
||||
afterEach(utils.afterEach)
|
||||
|
||||
it('shows the proper application title', function () {
|
||||
return this.app.client.getTitle()
|
||||
.then(title => {
|
||||
const result = /^Mark Text|Untitled-1 - Mark Text$/.test(title)
|
||||
if (!result) {
|
||||
console.error(`AssertionError: expected '${title}' to equal 'Mark Text' or 'Untitled-1'`)
|
||||
expect(false).to.equal(true)
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
@ -1,23 +0,0 @@
|
||||
import utils from '../utils'
|
||||
|
||||
describe('Cross-site Scripting Test', function () {
|
||||
beforeEach(utils.beforeXss)
|
||||
afterEach(utils.afterEach)
|
||||
|
||||
it('Load malicious document', function () {
|
||||
return new Promise((resolve, reject) => {
|
||||
setTimeout(() => {
|
||||
resolve()
|
||||
}, 3000)
|
||||
})
|
||||
.then(() => {
|
||||
return this.app.client.getRenderProcessLogs()
|
||||
.then(function (logs) {
|
||||
const xssErrorCount = logs.filter(log => {
|
||||
return log.level === 'SEVERE' && /XSS/i.test(log.message) && log.source === 'javascript'
|
||||
}).length
|
||||
expect(xssErrorCount).to.equal(0)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
@ -1,32 +0,0 @@
|
||||
import electron from 'electron'
|
||||
import { Application } from 'spectron'
|
||||
|
||||
export default {
|
||||
afterEach () {
|
||||
this.timeout(20000)
|
||||
|
||||
if (this.app && this.app.isRunning()) {
|
||||
return this.app.stop()
|
||||
}
|
||||
},
|
||||
beforeEach () {
|
||||
this.timeout(20000)
|
||||
this.app = new Application({
|
||||
path: electron,
|
||||
args: ['dist/electron/main.js'],
|
||||
startTimeout: 20000,
|
||||
waitTimeout: 20000
|
||||
})
|
||||
return this.app.start()
|
||||
},
|
||||
beforeXss () {
|
||||
this.timeout(20000)
|
||||
this.app = new Application({
|
||||
path: electron,
|
||||
args: ['dist/electron/main.js', 'test/e2e/data/xss.md'],
|
||||
startTimeout: 20000,
|
||||
waitTimeout: 20000
|
||||
})
|
||||
return this.app.start()
|
||||
}
|
||||
}
|
33
test/e2e/xss.spec.js
Normal file
33
test/e2e/xss.spec.js
Normal file
@ -0,0 +1,33 @@
|
||||
const { expect, test } = require('@playwright/test')
|
||||
const { launchElectron } = require('./helpers')
|
||||
|
||||
test.describe('Test XSS Vulnerabilities', async () => {
|
||||
let app = null
|
||||
let page = null
|
||||
|
||||
test.beforeAll(async () => {
|
||||
const { app: electronApp, page: firstPage } = await launchElectron(['test/e2e/data/xss.md'])
|
||||
app = electronApp
|
||||
page = firstPage
|
||||
|
||||
// Wait to parse and render the document.
|
||||
await new Promise((resolve) => setTimeout(resolve, 3000))
|
||||
})
|
||||
|
||||
test.afterAll(async () => {
|
||||
await app.close()
|
||||
})
|
||||
|
||||
test('Load malicious document', async () => {
|
||||
const { isVisible, isCrashed } = await app.evaluate(async process => {
|
||||
const mainWindow = process.BrowserWindow.getAllWindows()[0]
|
||||
return {
|
||||
isVisible: mainWindow.isVisible(),
|
||||
isCrashed: mainWindow.webContents.isCrashed()
|
||||
}
|
||||
})
|
||||
|
||||
expect(isVisible).toBeTruthy()
|
||||
expect(isCrashed).toBeFalsy()
|
||||
})
|
||||
})
|
@ -48,7 +48,6 @@ module.exports = config => {
|
||||
base: 'Electron',
|
||||
browserWindowOptions: {
|
||||
webPreferences: {
|
||||
enableRemoteModule: true,
|
||||
contextIsolation: false,
|
||||
spellcheck: false,
|
||||
nodeIntegration: true,
|
||||
|
Loading…
Reference in New Issue
Block a user