mirror of
https://github.com/marktext/marktext.git
synced 2025-05-03 00:01:19 +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: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",
|
||||||
|
@ -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,
|
||||||
|
@ -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()
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
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
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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())
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -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 () {
|
||||||
|
@ -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 }])
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
}))
|
}))
|
||||||
|
@ -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 }])
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
})
|
})
|
||||||
|
@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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`))
|
||||||
}
|
}
|
||||||
|
@ -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`.
|
||||||
|
@ -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
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',
|
base: 'Electron',
|
||||||
browserWindowOptions: {
|
browserWindowOptions: {
|
||||||
webPreferences: {
|
webPreferences: {
|
||||||
enableRemoteModule: true,
|
|
||||||
contextIsolation: false,
|
contextIsolation: false,
|
||||||
spellcheck: false,
|
spellcheck: false,
|
||||||
nodeIntegration: true,
|
nodeIntegration: true,
|
||||||
|
Loading…
Reference in New Issue
Block a user