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:
Felix Häusler 2021-12-18 14:52:24 +01:00 committed by GitHub
parent 0dd09cc684
commit bdaca98876
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 719 additions and 725 deletions

View File

@ -15,7 +15,7 @@
"build:clean": "cross-env BUILD_TARGET=clean node .electron-vue/build.js", "build:clean": "cross-env BUILD_TARGET=clean node .electron-vue/build.js",
"build:dev": "node .electron-vue/build.js", "build:dev": "node .electron-vue/build.js",
"dev": "cross-env node .electron-vue/dev-runner.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": "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", "lint:fix": "eslint --ext .js,.vue -f ./node_modules/eslint-friendly-formatter --fix src test",
"pack": "yarn run pack:main && yarn run pack:renderer", "pack": "yarn run pack:main && yarn run pack:renderer",
@ -33,6 +33,7 @@
"validate-licenses": "node tools/validateLicenses.js" "validate-licenses": "node tools/validateLicenses.js"
}, },
"dependencies": { "dependencies": {
"@electron/remote": "^2.0.1",
"@hfelix/electron-localshortcut": "^3.1.1", "@hfelix/electron-localshortcut": "^3.1.1",
"@hfelix/electron-spellchecker": "^2.0.0", "@hfelix/electron-spellchecker": "^2.0.0",
"@octokit/rest": "^16.43.2", "@octokit/rest": "^16.43.2",
@ -98,6 +99,7 @@
"@babel/register": "^7.16.0", "@babel/register": "^7.16.0",
"@babel/runtime": "^7.16.3", "@babel/runtime": "^7.16.3",
"@markedjs/html-differ": "^3.0.4", "@markedjs/html-differ": "^3.0.4",
"@playwright/test": "^1.17.1",
"babel-eslint": "^10.1.0", "babel-eslint": "^10.1.0",
"babel-loader": "^8.2.3", "babel-loader": "^8.2.3",
"babel-plugin-component": "^1.1.1", "babel-plugin-component": "^1.1.1",
@ -112,7 +114,7 @@
"del": "^5.1.0", "del": "^5.1.0",
"devtron": "^1.4.0", "devtron": "^1.4.0",
"dotenv": "^10.0.0", "dotenv": "^10.0.0",
"electron": "^13.6.1", "electron": "^15.3.4",
"electron-builder": "^22.14.8", "electron-builder": "^22.14.8",
"electron-devtools-installer": "^3.2.0", "electron-devtools-installer": "^3.2.0",
"electron-rebuild": "^3.2.5", "electron-rebuild": "^3.2.5",
@ -148,11 +150,11 @@
"multispinner": "^0.2.1", "multispinner": "^0.2.1",
"node-fetch": "^2.6.6", "node-fetch": "^2.6.6",
"node-loader": "^1.0.3", "node-loader": "^1.0.3",
"playwright": "^1.17.1",
"postcss-loader": "^3.0.0", "postcss-loader": "^3.0.0",
"postcss-preset-env": "^6.7.0", "postcss-preset-env": "^6.7.0",
"raw-loader": "^4.0.2", "raw-loader": "^4.0.2",
"require-dir": "^1.2.0", "require-dir": "^1.2.0",
"spectron": "^15.0.0",
"style-loader": "^2.0.0", "style-loader": "^2.0.0",
"svg-sprite-loader": "^4.3.0", "svg-sprite-loader": "^4.3.0",
"svgo": "^1.3.2", "svgo": "^1.3.2",

View File

@ -6,7 +6,6 @@ export const editorWinOptions = Object.freeze({
minWidth: 550, minWidth: 550,
minHeight: 350, minHeight: 350,
webPreferences: { webPreferences: {
enableRemoteModule: true,
contextIsolation: false, contextIsolation: false,
spellcheck: false, spellcheck: false,
nodeIntegration: true, nodeIntegration: true,
@ -23,7 +22,6 @@ export const preferencesWinOptions = Object.freeze({
width: 950, width: 950,
height: 650, height: 650,
webPreferences: { webPreferences: {
enableRemoteModule: true,
contextIsolation: false, contextIsolation: false,
spellcheck: false, spellcheck: false,
nodeIntegration: true, nodeIntegration: true,

View File

@ -1,6 +1,7 @@
import './globalSetting' import './globalSetting'
import path from 'path' import path from 'path'
import { app, dialog } from 'electron' import { app, dialog } from 'electron'
import { initialize as remoteInitializeServer } from '@electron/remote/main'
import cli from './cli' import cli from './cli'
import setupExceptionHandler, { initExceptionLogger } from './exceptionHandler' import setupExceptionHandler, { initExceptionLogger } from './exceptionHandler'
import log from 'electron-log' import log from 'electron-log'
@ -12,10 +13,9 @@ import { getLogLevel } from './utils'
const initializeLogger = appEnvironment => { const initializeLogger = appEnvironment => {
log.transports.console.level = process.env.NODE_ENV === 'development' ? true : 'error' log.transports.console.level = process.env.NODE_ENV === 'development' ? true : 'error'
log.transports.rendererConsole = null 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.level = getLogLevel()
log.transports.file.sync = true log.transports.file.sync = true
log.transports.file.init()
initExceptionLogger() initExceptionLogger()
} }
@ -80,5 +80,9 @@ log.transports.file.sync = false
// Be careful when changing code before this line! // Be careful when changing code before this line!
// NOTE: Do not create classes or other 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) const marktext = new App(accessor, args)
marktext.init() marktext.init()

View File

@ -1,5 +1,6 @@
import path from 'path' import path from 'path'
import { BrowserWindow, dialog, ipcMain } from 'electron' import { BrowserWindow, dialog, ipcMain } from 'electron'
import { enable as remoteEnable } from '@electron/remote/main'
import log from 'electron-log' import log from 'electron-log'
import windowStateKeeper from 'electron-window-state' import windowStateKeeper from 'electron-window-state'
import { isChildOfDirectory, isSamePathSync } from 'common/filesystem/paths' import { isChildOfDirectory, isSamePathSync } from 'common/filesystem/paths'
@ -68,6 +69,7 @@ class EditorWindow extends BaseWindow {
winOptions.backgroundColor = this._getPreferredBackgroundColor(theme) winOptions.backgroundColor = this._getPreferredBackgroundColor(theme)
let win = this.browserWindow = new BrowserWindow(winOptions) let win = this.browserWindow = new BrowserWindow(winOptions)
remoteEnable(win.webContents)
this.id = win.id this.id = win.id
// Create a menu for the current window // Create a menu for the current window

View File

@ -1,5 +1,6 @@
import path from 'path' import path from 'path'
import { BrowserWindow, ipcMain } from 'electron' import { BrowserWindow, ipcMain } from 'electron'
import { enable as remoteEnable } from '@electron/remote/main'
import electronLocalshortcut from '@hfelix/electron-localshortcut' import electronLocalshortcut from '@hfelix/electron-localshortcut'
import BaseWindow, { WindowLifecycle, WindowType } from './base' import BaseWindow, { WindowLifecycle, WindowType } from './base'
import { centerWindowOptions } from './utils' import { centerWindowOptions } from './utils'
@ -45,6 +46,7 @@ class SettingWindow extends BaseWindow {
winOptions.backgroundColor = this._getPreferredBackgroundColor(theme) winOptions.backgroundColor = this._getPreferredBackgroundColor(theme)
let win = this.browserWindow = new BrowserWindow(winOptions) let win = this.browserWindow = new BrowserWindow(winOptions)
remoteEnable(win.webContents)
this.id = win.id this.id = win.id
// Create a menu for the current window // Create a menu for the current window

View File

@ -9,10 +9,9 @@ const configureLogger = () => {
const { debug, paths, windowId } = global.marktext.env const { debug, paths, windowId } = global.marktext.env
log.transports.console.level = process.env.NODE_ENV === 'development' // mirror to window console log.transports.console.level = process.env.NODE_ENV === 'development' // mirror to window console
log.transports.mainConsole = null 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.level = debug ? 'debug' : 'info'
log.transports.file.sync = false log.transports.file.sync = false
log.transports.file.init()
exceptionLogger = log.error exceptionLogger = log.error
} }

View File

@ -1,5 +1,6 @@
// List of all static commands that are loaded into command center. // 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 bus from '../bus'
import { delay, isOsx } from '@/util' import { delay, isOsx } from '@/util'
import { isUpdatable } from './utils' import { isUpdatable } from './utils'
@ -509,7 +510,7 @@ const commands = [
id: 'window.minimize', id: 'window.minimize',
description: 'Window: Minimize', description: 'Window: Minimize',
execute: async () => { execute: async () => {
remote.getCurrentWindow().minimize() getCurrentWindow().minimize()
} }
}, { }, {
id: 'window.always-on-top', id: 'window.always-on-top',
@ -521,7 +522,7 @@ const commands = [
id: 'window.toggle-full-screen', id: 'window.toggle-full-screen',
description: 'Window: Toggle Full Screen', description: 'Window: Toggle Full Screen',
execute: async () => { execute: async () => {
const win = remote.getCurrentWindow() const win = getCurrentWindow()
win.setFullScreen(!win.isFullScreen()) win.setFullScreen(!win.isFullScreen())
} }
}, },

View File

@ -98,7 +98,8 @@
</template> </template>
<script> <script>
import { ipcRenderer, remote } from 'electron' import { ipcRenderer } from 'electron'
import { getCurrentWindow, Menu as RemoteMenu } from '@electron/remote'
import { mapState } from 'vuex' import { mapState } from 'vuex'
import { minimizePath, restorePath, maximizePath, closePath } from '../../assets/window-controls.js' import { minimizePath, restorePath, maximizePath, closePath } from '../../assets/window-controls.js'
import { PATH_SEPARATOR } from '../../config' import { PATH_SEPARATOR } from '../../config'
@ -130,8 +131,8 @@ export default {
this.windowIconMaximize = maximizePath this.windowIconMaximize = maximizePath
this.windowIconClose = closePath this.windowIconClose = closePath
return { return {
isFullScreen: remote.getCurrentWindow().isFullScreen(), isFullScreen: getCurrentWindow().isFullScreen(),
isMaximized: remote.getCurrentWindow().isMaximized(), isMaximized: getCurrentWindow().isMaximized(),
show: 'word' show: 'word'
} }
}, },
@ -189,11 +190,11 @@ export default {
}, },
handleCloseClick () { handleCloseClick () {
remote.getCurrentWindow().close() getCurrentWindow().close()
}, },
handleMaximizeClick () { handleMaximizeClick () {
const win = remote.getCurrentWindow() const win = getCurrentWindow()
if (win.isFullScreen()) { if (win.isFullScreen()) {
win.setFullScreen(false) win.setFullScreen(false)
} else if (win.isMaximized()) { } else if (win.isMaximized()) {
@ -210,15 +211,12 @@ export default {
}, },
handleMinimizeClick () { handleMinimizeClick () {
remote.getCurrentWindow().minimize() getCurrentWindow().minimize()
}, },
handleMenuClick () { handleMenuClick () {
const win = remote.getCurrentWindow() const win = getCurrentWindow()
remote RemoteMenu.getApplicationMenu().popup({ window: win, x: 23, y: 20 })
.Menu
.getApplicationMenu()
.popup({ window: win, x: 23, y: 20 })
}, },
rename () { rename () {

View File

@ -1,4 +1,4 @@
import { remote } from 'electron' import { getCurrentWindow, Menu as RemoteMenu, MenuItem as RemoteMenuItem } from '@electron/remote'
import { import {
CUT, CUT,
COPY, COPY,
@ -12,7 +12,6 @@ import {
} from './menuItems' } from './menuItems'
import spellcheckMenuBuilder from './spellcheck' 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] 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) => { export const showContextMenu = (event, selection, spellchecker, selectedWord, wordSuggestions, replaceCallback) => {
const { start, end } = selection const { start, end } = selection
const menu = new Menu() const menu = new RemoteMenu()
const win = remote.getCurrentWindow() const win = getCurrentWindow()
const disableCutAndCopy = start.key === end.key && start.offset === end.offset const disableCutAndCopy = start.key === end.key && start.offset === end.offset
const spellingSubmenu = spellcheckMenuBuilder(spellchecker, selectedWord, wordSuggestions, replaceCallback) const spellingSubmenu = spellcheckMenuBuilder(spellchecker, selectedWord, wordSuggestions, replaceCallback)
if (spellingSubmenu) { if (spellingSubmenu) {
menu.append(new MenuItem({ menu.append(new RemoteMenuItem({
label: 'Spelling...', label: 'Spelling...',
submenu: spellingSubmenu submenu: spellingSubmenu
})) }))
menu.append(new MenuItem(SEPARATOR)) menu.append(new RemoteMenuItem(SEPARATOR))
} }
[CUT, COPY, COPY_AS_HTML, COPY_AS_MARKDOWN].forEach(item => { [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 => { CONTEXT_ITEMS.forEach(item => {
menu.append(new MenuItem(item)) menu.append(new RemoteMenuItem(item))
}) })
menu.popup([{ window: win, x: event.clientX, y: event.clientY }]) menu.popup([{ window: win, x: event.clientX, y: event.clientY }])
} }

View File

@ -1,11 +1,9 @@
import { remote } from 'electron' import { MenuItem as RemoteMenuItem } from '@electron/remote'
import log from 'electron-log' import log from 'electron-log'
import bus from '@/bus' import bus from '@/bus'
import { getLanguageName } from '@/spellchecker/languageMap' import { getLanguageName } from '@/spellchecker/languageMap'
import { SEPARATOR } from './menuItems' import { SEPARATOR } from './menuItems'
const { MenuItem } = remote
/** /**
* Build the spell checker menu depending on input. * Build the spell checker menu depending on input.
* *
@ -24,7 +22,7 @@ export default (spellchecker, selectedWord, wordSuggestions, replaceCallback) =>
const availableDictionaries = spellchecker.getAvailableDictionaries() const availableDictionaries = spellchecker.getAvailableDictionaries()
const availableDictionariesSubmenu = [] const availableDictionariesSubmenu = []
for (const dict of availableDictionaries) { for (const dict of availableDictionaries) {
availableDictionariesSubmenu.push(new MenuItem({ availableDictionariesSubmenu.push(new RemoteMenuItem({
label: getLanguageName(dict), label: getLanguageName(dict),
enabled: dict !== currentLanguage, enabled: dict !== currentLanguage,
click () { click () {
@ -33,7 +31,7 @@ export default (spellchecker, selectedWord, wordSuggestions, replaceCallback) =>
})) }))
} }
spellingSubmenu.push(new MenuItem({ spellingSubmenu.push(new RemoteMenuItem({
label: 'Change Language...', label: 'Change Language...',
submenu: availableDictionariesSubmenu submenu: availableDictionariesSubmenu
})) }))

View File

@ -1,4 +1,4 @@
import { remote } from 'electron' import { getCurrentWindow, Menu as RemoteMenu, MenuItem as RemoteMenuItem } from '@electron/remote'
import { import {
SEPARATOR, SEPARATOR,
NEW_FILE, NEW_FILE,
@ -11,11 +11,9 @@ import {
SHOW_IN_FOLDER SHOW_IN_FOLDER
} from './menuItems' } from './menuItems'
const { Menu, MenuItem } = remote
export const showContextMenu = (event, hasPathCache) => { export const showContextMenu = (event, hasPathCache) => {
const menu = new Menu() const menu = new RemoteMenu()
const win = remote.getCurrentWindow() const win = getCurrentWindow()
const CONTEXT_ITEMS = [ const CONTEXT_ITEMS = [
NEW_FILE, NEW_FILE,
NEW_DIRECTORY, NEW_DIRECTORY,
@ -33,7 +31,7 @@ export const showContextMenu = (event, hasPathCache) => {
PASTE.enabled = hasPathCache PASTE.enabled = hasPathCache
CONTEXT_ITEMS.forEach(item => { CONTEXT_ITEMS.forEach(item => {
menu.append(new MenuItem(item)) menu.append(new RemoteMenuItem(item))
}) })
menu.popup([{ window: win, x: event.clientX, y: event.clientY }]) menu.popup([{ window: win, x: event.clientX, y: event.clientY }])
} }

View File

@ -1,4 +1,4 @@
import { remote } from 'electron' import { getCurrentWindow, Menu as RemoteMenu, MenuItem as RemoteMenuItem } from '@electron/remote'
import { import {
CLOSE_THIS, CLOSE_THIS,
CLOSE_OTHERS, CLOSE_OTHERS,
@ -10,11 +10,9 @@ import {
SHOW_IN_FOLDER SHOW_IN_FOLDER
} from './menuItems' } from './menuItems'
const { Menu, MenuItem } = remote
export const showContextMenu = (event, tab) => { export const showContextMenu = (event, tab) => {
const menu = new Menu() const menu = new RemoteMenu()
const win = remote.getCurrentWindow() const win = getCurrentWindow()
const { pathname } = tab const { pathname } = tab
const CONTEXT_ITEMS = [CLOSE_THIS, CLOSE_OTHERS, CLOSE_SAVED, CLOSE_ALL, SEPARATOR, RENAME, COPY_PATH, SHOW_IN_FOLDER] 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] const FILE_CONTEXT_ITEMS = [RENAME, COPY_PATH, SHOW_IN_FOLDER]
@ -24,7 +22,7 @@ export const showContextMenu = (event, tab) => {
}) })
CONTEXT_ITEMS.forEach(item => { CONTEXT_ITEMS.forEach(item => {
const menuItem = new MenuItem(item) const menuItem = new RemoteMenuItem(item)
menuItem._tabId = tab.id menuItem._tabId = tab.id
menu.append(menuItem) menu.append(menuItem)
}) })

View File

@ -11,7 +11,7 @@
</template> </template>
<script> <script>
import { remote } from 'electron' import { getCurrentWindow } from '@electron/remote'
import { closePath } from '../../assets/window-controls.js' import { closePath } from '../../assets/window-controls.js'
export default { export default {
@ -21,7 +21,7 @@ export default {
}, },
methods: { methods: {
handleCloseClick () { handleCloseClick () {
remote.getCurrentWindow().close() getCurrentWindow().close()
} }
} }
} }

View File

@ -2,7 +2,7 @@ import fs from 'fs-extra'
import path from 'path' import path from 'path'
import { SpellChecker } from '@hfelix/electron-spellchecker' import { SpellChecker } from '@hfelix/electron-spellchecker'
import axios from '../axios' import axios from '../axios'
import { dictionaryPath } from '../spellchecker' import { getDictionaryPath } from '../spellchecker'
/** /**
* Try to download the given Hunspell dictionary. * Try to download the given Hunspell dictionary.
@ -15,6 +15,7 @@ export const downloadHunspellDictionary = async lang => {
responseType: 'stream' responseType: 'stream'
}) })
const dictionaryPath = getDictionaryPath()
await fs.ensureDir(dictionaryPath) await fs.ensureDir(dictionaryPath)
const dstFile = path.join(dictionaryPath, `${lang}.bdic`) const dstFile = path.join(dictionaryPath, `${lang}.bdic`)
@ -48,5 +49,5 @@ export const downloadHunspellDictionary = async lang => {
* @param {string} lang The language to remove. * @param {string} lang The language to remove.
*/ */
export const deleteHunspellDictionary = async lang => { export const deleteHunspellDictionary = async lang => {
return await fs.remove(path.join(dictionaryPath, `${lang}.bdic`)) return await fs.remove(path.join(getDictionaryPath(), `${lang}.bdic`))
} }

View File

@ -1,13 +1,15 @@
import fs from 'fs-extra' import fs from 'fs-extra'
import path from 'path' import path from 'path'
import os from 'os' import os from 'os'
import { remote } from 'electron'
import { SpellCheckHandler, fallbackLocales, normalizeLanguageCode } from '@hfelix/electron-spellchecker' import { SpellCheckHandler, fallbackLocales, normalizeLanguageCode } from '@hfelix/electron-spellchecker'
import { isDirectory, isFile } from 'common/filesystem' import { isDirectory, isFile } from 'common/filesystem'
import { cloneObj, isOsx, isLinux, isWindows } from '@/util' import { cloneObj, isOsx, isLinux, isWindows } from '@/util'
// NOTE: Hardcoded in "@hfelix/electron-spellchecker/src/spell-check-handler.js" // 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 // Source: https://github.com/Microsoft/vscode/blob/master/src/vs/editor/common/model/wordHelper.ts
// /(-?\d*\.\d\w*)|([^\`\~\!\@\#\$\%\^\&\*\(\)\-\=\+\[\{\]\}\\\|\;\:\'\"\,\.\<\>\/\?\s]+)/ // /(-?\d*\.\d\w*)|([^\`\~\!\@\#\$\%\^\&\*\(\)\-\=\+\[\{\]\}\\\|\;\:\'\"\,\.\<\>\/\?\s]+)/
@ -73,6 +75,7 @@ export const validateLineCursor = selection => {
* @returns {string[]} List of available Hunspell dictionary language codes. * @returns {string[]} List of available Hunspell dictionary language codes.
*/ */
export const getAvailableHunspellDictionaries = () => { export const getAvailableHunspellDictionaries = () => {
const dictionaryPath = getDictionaryPath()
const dict = [] const dict = []
// Search for dictionaries on filesystem. // Search for dictionaries on filesystem.
if (isDirectory(dictionaryPath)) { if (isDirectory(dictionaryPath)) {
@ -140,7 +143,7 @@ export class SpellChecker {
throw new Error('Invalid state.') throw new Error('Invalid state.')
} }
this.provider = new SpellCheckHandler(dictionaryPath) this.provider = new SpellCheckHandler(getDictionaryPath())
this.isHunspell = this.provider.isHunspell this.isHunspell = this.provider.isHunspell
// The spell checker is now initialized but not yet enabled. You need to call `init`. // The spell checker is now initialized but not yet enabled. You need to call `init`.

View File

@ -1,14 +1,14 @@
import { isLinux, isOsx, isWindows } from './index' import { isLinux, isOsx, isWindows } from './index'
import plist from 'plist' import plist from 'plist'
import { remote } from 'electron' import { clipboard as remoteClipboard } from '@electron/remote'
const hasClipboardFiles = () => { const hasClipboardFiles = () => {
return remote.clipboard.has('NSFilenamesPboardType') return remoteClipboard.has('NSFilenamesPboardType')
} }
const getClipboardFiles = () => { const getClipboardFiles = () => {
if (!hasClipboardFiles()) { return [] } if (!hasClipboardFiles()) { return [] }
return plist.parse(remote.clipboard.read('NSFilenamesPboardType')) return plist.parse(remoteClipboard.read('NSFilenamesPboardType'))
} }
export const guessClipboardFilePath = () => { export const guessClipboardFilePath = () => {
@ -17,7 +17,7 @@ export const guessClipboardFilePath = () => {
const result = getClipboardFiles() const result = getClipboardFiles()
return Array.isArray(result) && result.length ? result[0] : '' return Array.isArray(result) && result.length ? result[0] : ''
} else if (isWindows) { } else if (isWindows) {
const rawFilePath = remote.clipboard.read('FileNameW') const rawFilePath = remoteClipboard.read('FileNameW')
const filePath = rawFilePath.replace(new RegExp(String.fromCharCode(0), 'g'), '') const filePath = rawFilePath.replace(new RegExp(String.fromCharCode(0), 'g'), '')
return filePath && typeof filePath === 'string' ? filePath : '' return filePath && typeof filePath === 'string' ? filePath : ''
} else { } else {

37
test/e2e/helpers.js Normal file
View 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}

View File

@ -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
View 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()
})
})

View File

@ -0,0 +1,9 @@
const config = {
workers: 1,
use: {
headless: false,
viewport: { width: 1280, height: 720 },
timeout: 30000
}
}
module.exports = config

View File

@ -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)
}
})
})
})

View File

@ -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)
})
})
})
})

View File

@ -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
View 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()
})
})

View File

@ -48,7 +48,6 @@ module.exports = config => {
base: 'Electron', base: 'Electron',
browserWindowOptions: { browserWindowOptions: {
webPreferences: { webPreferences: {
enableRemoteModule: true,
contextIsolation: false, contextIsolation: false,
spellcheck: false, spellcheck: false,
nodeIntegration: true, nodeIntegration: true,

1133
yarn.lock

File diff suppressed because it is too large Load Diff