Skip to content

Commit 17164bb

Browse files
committed
add support for testing and debugging
1 parent a2258f1 commit 17164bb

File tree

9 files changed

+314
-53
lines changed

9 files changed

+314
-53
lines changed

__mocks__/@vue/cli-service/lib/commands/serve.js

Lines changed: 0 additions & 7 deletions
This file was deleted.

__mocks__/electron-builder.js

Lines changed: 0 additions & 8 deletions
This file was deleted.

__tests__/commands.spec.js

Lines changed: 121 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,37 @@
1-
// jest.enableAutomock()
2-
// jest.unmock('bluebird')
3-
// jest.unmock('once')
4-
51
const pluginIndex = require('../index.js')
2+
const testWithSpectron = pluginIndex.testWithSpectron
63
const Config = require('webpack-chain')
74
const webpack = require('webpack')
85
const builder = require('electron-builder')
96
const buildRenderer = require('@vue/cli-service/lib/commands/build').build
7+
const serve = require('@vue/cli-service/lib/commands/serve').serve
108
const fs = require('fs-extra')
9+
const execa = require('execa')
10+
const portfinder = require('portfinder')
11+
const Application = require('spectron').Application
1112
jest.mock('@vue/cli-service/lib/commands/build')
1213
jest.mock('fs-extra')
1314
jest.mock('electron-builder')
15+
jest.mock('execa', () => jest.fn(() => ({ on: jest.fn() })))
16+
jest.mock('@vue/cli-service/lib/commands/serve', () => ({
17+
serve: jest.fn().mockResolvedValue({ url: 'serveUrl' })
18+
}))
19+
jest.mock('electron-builder', () => ({ build: jest.fn().mockResolvedValue() }))
20+
const mockWait = jest.fn(() => new Promise(resolve => resolve()))
21+
const mockStart = jest.fn()
22+
jest.mock('spectron', () => ({
23+
Application: jest.fn().mockImplementation(() => ({
24+
start: mockStart,
25+
client: { waitUntilWindowLoaded: mockWait }
26+
}))
27+
}))
1428
console.log = jest.fn()
1529

1630
beforeEach(() => {
1731
jest.clearAllMocks()
1832
})
19-
20-
const runCommand = async (command, options = {}, args = { _: [] }) => {
33+
const runCommand = async (command, options = {}, args = {}) => {
34+
if (!args._) args._ = []
2135
const renderConfig = new Config()
2236
// Command expects define plugin to exist
2337
renderConfig
@@ -207,4 +221,105 @@ describe('serve:electron', () => {
207221
// Custom node key is passed through
208222
expect(mainConfig.node.test).toBe('expected')
209223
})
224+
test('If --debug argument is passed, electron is not launched and main process is not minified', async () => {
225+
await runCommand('serve:electron', {}, { debug: true })
226+
const mainConfig = webpack.mock.calls[0][0]
227+
228+
// UglifyJS plugin does not exist
229+
expect(
230+
mainConfig.plugins.find(
231+
p => p.__pluginConstructorName === 'UglifyJsPlugin'
232+
)
233+
).toBeUndefined()
234+
// Electron is not launched
235+
expect(execa).not.toBeCalled()
236+
})
237+
test('If --healdess argument is passed, serve:electron is launched in production mode', async () => {
238+
await runCommand('serve:electron', {}, { headless: true })
239+
240+
// Production mode is used
241+
expect(serve.mock.calls[0][0].mode).toBe('production')
242+
// Electron is not launched
243+
expect(execa).not.toBeCalled()
244+
})
245+
test('If --forceDev argument is passed, serve:electron is launched in dev mode', async () => {
246+
await runCommand('serve:electron', {}, { headless: true, forceDev: true })
247+
248+
// Production mode is used
249+
expect(serve.mock.calls[0][0].mode).toBe('development')
250+
// Electron is not launched
251+
expect(execa).not.toBeCalled()
252+
})
253+
})
254+
255+
describe('testWithSpectron', async () => {
256+
// Mock portfinder's returned port
257+
portfinder.getPortPromise = jest.fn().mockResolvedValue('expectedPort')
258+
259+
const runSpectron = async (spectronOptions, launchOptions = {}) => {
260+
let sendData
261+
execa.mockReturnValueOnce({
262+
on: jest.fn(),
263+
stdout: {
264+
on: (event, callback) => {
265+
if (event === 'data') {
266+
sendData = callback
267+
}
268+
}
269+
}
270+
})
271+
const testPromise = testWithSpectron(spectronOptions)
272+
// Mock console.log from serve:elctron
273+
if (launchOptions.customLog) await sendData(launchOptions.customLog)
274+
await sendData(`$outputDir=${launchOptions.outputDir || 'dist_electron'}`)
275+
await sendData(
276+
`$WEBPACK_DEV_SERVER_URL=${launchOptions.url || 'http://localhost:8080/'}`
277+
)
278+
return testPromise
279+
}
280+
281+
test('uses custom output dir and url', async () => {
282+
const { url } = await runSpectron(
283+
{},
284+
{
285+
url: 'http://localhost:1234/',
286+
outputDir: 'customOutput'
287+
}
288+
)
289+
// Proper url is returned
290+
expect(url).toBe('http://localhost:1234/')
291+
const appArgs = Application.mock.calls[0][0]
292+
// Spectron is launched with proper url
293+
expect(appArgs.env.WEBPACK_DEV_SERVER_URL).toBe('http://localhost:1234/')
294+
// Spectron is launched with proper path to background
295+
expect(appArgs.args).toEqual(['customOutput/background.js'])
296+
})
297+
test('secures an open port with portfinder', async () => {
298+
await runSpectron()
299+
// Port should match portfinder's mock return value
300+
expect(Application.mock.calls[0][0].port).toBe('expectedPort')
301+
})
302+
test("doesn't start app if noStart option is provided", async () => {
303+
await runSpectron({ noStart: true })
304+
// App should not be started nor waited for to load
305+
expect(mockStart).not.toBeCalled()
306+
expect(mockWait).not.toBeCalled()
307+
})
308+
test("doesn't launch spectron if noSpectron option is provided", async () => {
309+
await runSpectron({ noSpectron: true })
310+
// Spectron instance should not be created
311+
expect(Application).not.toBeCalled()
312+
})
313+
test('uses custom spectron options if provided', async () => {
314+
await runSpectron({ spectronOptions: { testKey: 'expected' } })
315+
expect(Application.mock.calls[0][0].testKey).toBe('expected')
316+
})
317+
test('launches dev server in dev mode if forceDev argument is provided', async () => {
318+
await runSpectron({ forceDev: true })
319+
expect(execa.mock.calls[0][1]).toContain('--forceDev')
320+
})
321+
test('returns stdout of command', async () => {
322+
const { stdout } = await runSpectron({}, { customLog: 'shouldBeInLog' })
323+
expect(stdout.indexOf('shouldBeInLog')).not.toBe(-1)
324+
})
210325
})

__tests__/createProject.helper.js

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,19 @@ const create = require('@vue/cli-test-utils/createTestProject')
33
const path = require('path')
44
const fs = require('fs-extra')
55

6-
const createProject = (projectName, useTS) =>
6+
const createProject = (projectName, useTS, customPlugins = {}) =>
77
new Promise(async resolve => {
88
// Prevent modification of import
99
let preset = { ...defaultPreset }
1010
if (useTS) {
1111
// Install typescript plugin
12-
defaultPreset.plugins['@vue/cli-plugin-typescript'] = {}
12+
preset.plugins['@vue/cli-plugin-typescript'] = {}
1313
// Use different project name
1414
projectName += '-ts'
1515
}
1616
// Install vcp-electron-builder
17-
defaultPreset.plugins['vue-cli-plugin-electron-builder'] = {}
17+
preset.plugins['vue-cli-plugin-electron-builder'] = {}
18+
preset.plugins = { ...preset.plugins, ...customPlugins }
1819
const projectPath = p =>
1920
path.join(process.cwd(), '__tests__/projects/' + projectName, p)
2021
const project = await create(

__tests__/testWithSpectron.spec.js

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
jest.setTimeout(100000)
2+
3+
const createProject = require('./createProject.helper.js')
4+
5+
test('basic tests pass', async () => {
6+
const { project } = await createProject('spectron', false, {
7+
'@vue/cli-plugin-unit-jest': {}
8+
})
9+
10+
// Update jest config to find test
11+
const config = JSON.parse(await project.read('package.json'))
12+
config.jest.testMatch = ['<rootDir>/tests/unit/spectron.js']
13+
await project.write('package.json', JSON.stringify(config))
14+
15+
// Create spectron test
16+
await project.write(
17+
'tests/unit/spectron.js',
18+
`jest.setTimeout(30000)
19+
const { testWithSpectron } = require('vue-cli-plugin-electron-builder')
20+
test('app loads a window', async () => {
21+
const { app, stopServe } = await testWithSpectron()
22+
expect(await app.client.getWindowCount()).toBe(1)
23+
await stopServe()
24+
})
25+
`
26+
)
27+
await project.run('vue-cli-service test:unit')
28+
})

index.d.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { Application, AppConstructorOptions } from 'spectron'
2+
interface Options {
3+
/**
4+
Do not launch spectron.
5+
You will have to launch it on your own.
6+
*/
7+
noSpectron: boolean
8+
/** Launch server in dev mode, not in production. */
9+
forceDev: boolean
10+
/** Custom spectron options.These will be merged with default options. */
11+
spectronOptions: AppConstructorOptions
12+
/** Do not start app or wait for it to load.
13+
You will have to run app.start() and app.client.waitUntilWindowLoaded() yourself.
14+
*/
15+
noStart: boolean
16+
}
17+
interface Server {
18+
/** Spectron instance. */
19+
app: Application
20+
/** URL of dev server. */
21+
url: string
22+
/** Close spectron and stop dev server (must be called to prevent continued async operations). */
23+
stopServe: function(): Promise
24+
/** Log of dev server. */
25+
stdout: string
26+
}
27+
28+
/**
29+
Run serve:electron, but instead of launching Electron it returns a Spectron Application instance.
30+
Used for e2e testing with Spectron.
31+
*/
32+
export function testWithSpectron(options: Options): Promise<Server>

index.js

Lines changed: 56 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -202,11 +202,13 @@ module.exports = (api, options) => {
202202
.node.set('__dirname', false)
203203
.set('__filename', false)
204204
mainConfig.output.path(api.resolve(outputDir)).filename('background.js')
205-
mainConfig.plugin('uglify').use(UglifyJSPlugin, [
206-
{
207-
parallel: true
208-
}
209-
])
205+
if (!args.debug) {
206+
mainConfig.plugin('uglify').use(UglifyJSPlugin, [
207+
{
208+
parallel: true
209+
}
210+
])
211+
}
210212
// Set __static to absolute path to public folder
211213
mainConfig.plugin('define').use(webpack.DefinePlugin, [
212214
{
@@ -259,34 +261,59 @@ module.exports = (api, options) => {
259261
console.log('\nStarting development server:\n')
260262
// Run the serve command with custom webpack config
261263
serve(
262-
// Use dashboard if called from ui
263-
{ _: [], dashboard: args.dashboard },
264+
{
265+
_: [],
266+
// Use dashboard if called from ui
267+
dashboard: args.dashboard,
268+
// Serve in development mode if launched in headless mode
269+
mode: args.headless && !args.forceDev ? 'production' : 'development'
270+
},
264271
api,
265272
options,
266273
rendererConfig
267274
).then(server => {
268-
// Launch electron with execa
269-
console.log('\nLaunching Electron...')
270-
const child = execa(
271-
'./node_modules/.bin/electron',
272-
// Have it load the main process file built with webpack
273-
[`${outputDir}/background.js`],
274-
{
275-
cwd: api.resolve('.'),
276-
stdio: 'inherit',
277-
env: {
278-
...process.env,
279-
// Give the main process the url to the dev server
280-
WEBPACK_DEV_SERVER_URL: server.url,
281-
// Disable electron security warnings
282-
ELECTRON_DISABLE_SECURITY_WARNINGS: true
275+
if (args.debug) {
276+
// Do not launch electron and provide instructions on launching through debugger
277+
console.log(
278+
'Not launching electron as debug argument was passed. You must launch electron though your debugger.'
279+
)
280+
console.log(
281+
`Make sure to set the WEBPACK_DEV_SERVER_URL env variable to ${
282+
server.url
283283
}
284-
}
285-
)
286-
child.on('exit', () => {
287-
// Exit when electron is closed
288-
process.exit(0)
289-
})
284+
And IS_TEST to true`
285+
)
286+
console.log(
287+
'Learn more about debugging the main process at https://github.com/nklayman/vue-cli-plugin-electron-builder#debugging.'
288+
)
289+
} else if (args.headless) {
290+
// Log information for spectron
291+
console.log(`$outputDir=${outputDir}`)
292+
console.log(`$WEBPACK_DEV_SERVER_URL=${server.url}`)
293+
} else {
294+
// Launch electron with execa
295+
console.log('\nLaunching Electron...')
296+
const child = execa(
297+
'./node_modules/.bin/electron',
298+
// Have it load the main process file built with webpack
299+
[`${outputDir}/background.js`],
300+
{
301+
cwd: api.resolve('.'),
302+
stdio: 'inherit',
303+
env: {
304+
...process.env,
305+
// Give the main process the url to the dev server
306+
WEBPACK_DEV_SERVER_URL: server.url,
307+
// Disable electron security warnings
308+
ELECTRON_DISABLE_SECURITY_WARNINGS: true
309+
}
310+
}
311+
)
312+
child.on('exit', () => {
313+
// Exit when electron is closed
314+
process.exit(0)
315+
})
316+
}
290317
})
291318
})
292319
}
@@ -296,3 +323,4 @@ module.exports.defaultModes = {
296323
'build:electron': 'production',
297324
'serve:electron': 'development'
298325
}
326+
module.exports.testWithSpectron = require('./lib/testWithSpectron')

0 commit comments

Comments
 (0)