Add more comprehensive tests

This implements the following cases:

* Basic runs for a specific version/platform/arch with default options
  (testing for existence of expected output files/folders)
* Runs for each target platform with `out` set
* Runs for each target platform with `asar` set
* Runs for each target platform with `prune` set
* Runs for each target platform with `ignore` set
  * This includes testing `ignore` including a path separator
  * This also includes testing that `ignore` entries are applied
    with the app folder trimmed
* Runs for each target platform with `overwrite` set / not set
* Runs with `all` set, with `arch` or `platform` each set to `all`,
  and with `platform` set to multiple platforms (comma-delimited or
  array)
* Tests specifically for the darwin target platform, to test
  `icon` and `sign`

The test logic is organized such that the version of Electron being
tested can be configured once centrally (in config.json), and the tests
will not run until after downloading any necessary electron versions
(to avoid timing out due to long downloads).

Resolves #77.
This commit is contained in:
Kenneth G. Franqueiro 2015-06-24 00:05:36 -04:00
parent a157f716a5
commit a360f6c2ee
18 changed files with 684 additions and 65 deletions

1
.gitignore vendored
View File

@ -1,3 +1,2 @@
node_modules
.DS_Store
test/dist

View File

@ -30,15 +30,16 @@
"run-series": "^1.1.1"
},
"devDependencies": {
"run-waterfall": "^1.1.1",
"standard": "^3.3.2",
"tape": "^4.0.0"
},
"scripts": {
"test": "standard && node test/test.js"
"test": "standard && tape test"
},
"standard": {
"ignore": [
"test/dist"
"test/fixtures/**/node_modules"
]
}
}

273
test/basic.js Normal file
View File

@ -0,0 +1,273 @@
var fs = require('fs')
var path = require('path')
var packager = require('..')
var waterfall = require('run-waterfall')
var config = require('./config.json')
var util = require('./util')
function generateBasename (opts) {
return opts.name + '-' + opts.platform + '-' + opts.arch
}
function generateNamePath (opts) {
// Generates path to verify reflects the name given in the options.
// Returns the Helper.app location on darwin since the top-level .app is already tested for the resources path;
// returns the executable for other OSes
if (opts.platform === 'darwin') {
return path.join(opts.name + '.app', 'Contents', 'Frameworks', opts.name + ' Helper.app')
}
return opts.name + (opts.platform === 'win32' ? '.exe' : '')
}
function createDefaultsTest (combination) {
return function (t) {
t.timeoutAfter(config.timeout)
var opts = Object.create(combination)
opts.name = 'basicTest'
opts.dir = path.join(__dirname, 'fixtures', 'basic')
var finalPath
var resourcesPath
waterfall([
function (cb) {
packager(opts, cb)
}, function (paths, cb) {
t.true(Array.isArray(paths), 'packager call should resolve to an array')
t.equal(paths.length, 1, 'Single-target run should resolve to a 1-item array')
finalPath = paths[0]
t.equal(finalPath, path.join(util.getWorkCwd(), generateBasename(opts)),
'Path should follow the expected format and be in the cwd')
fs.stat(finalPath, cb)
}, function (stats, cb) {
t.true(stats.isDirectory(), 'The expected output directory should exist')
resourcesPath = path.join(finalPath, util.generateResourcesPath(opts))
fs.stat(path.join(finalPath, generateNamePath(opts)), cb)
}, function (stats, cb) {
if (opts.platform === 'darwin') {
t.true(stats.isDirectory(), 'The Helper.app should reflect opts.name')
} else {
t.true(stats.isFile(), 'The executable should reflect opts.name')
}
fs.stat(resourcesPath, cb)
}, function (stats, cb) {
t.true(stats.isDirectory(), 'The output directory should contain the expected resources subdirectory')
fs.stat(path.join(resourcesPath, 'app', 'node_modules', 'run-waterfall'), cb)
}, function (stats, cb) {
t.true(stats.isDirectory(), 'The output directory should contain devDependencies by default (no prune)')
util.areFilesEqual(path.join(opts.dir, 'main.js'), path.join(resourcesPath, 'app', 'main.js'), cb)
}, function (equal, cb) {
t.true(equal, 'File under packaged app directory should match source file')
util.areFilesEqual(path.join(opts.dir, 'ignore', 'this.txt'),
path.join(resourcesPath, 'app', 'ignore', 'this.txt'),
cb)
}, function (equal, cb) {
t.true(equal,
'File under subdirectory of packaged app directory should match source file and not be ignored by default')
cb()
}
], function (err) {
t.end(err)
})
}
}
function createOutTest (combination) {
return function (t) {
t.timeoutAfter(config.timeout)
var opts = Object.create(combination)
opts.name = 'basicTest'
opts.dir = path.join(__dirname, 'fixtures', 'basic')
opts.out = 'dist'
var finalPath
waterfall([
function (cb) {
packager(opts, cb)
}, function (paths, cb) {
finalPath = paths[0]
t.equal(finalPath, path.join('dist', generateBasename(opts)),
'Path should follow the expected format and be under the folder specifed in `out`')
fs.stat(finalPath, cb)
}, function (stats, cb) {
t.true(stats.isDirectory(), 'The expected output directory should exist')
fs.stat(path.join(finalPath, util.generateResourcesPath(opts)), cb)
}, function (stats, cb) {
t.true(stats.isDirectory(), 'The output directory should contain the expected resources subdirectory')
cb()
}
], function (err) {
t.end(err)
})
}
}
function createAsarTest (combination) {
return function (t) {
t.timeoutAfter(config.timeout)
var opts = Object.create(combination)
opts.name = 'basicTest'
opts.dir = path.join(__dirname, 'fixtures', 'basic')
opts.asar = true
var finalPath
var resourcesPath
waterfall([
function (cb) {
packager(opts, cb)
}, function (paths, cb) {
finalPath = paths[0]
fs.stat(finalPath, cb)
}, function (stats, cb) {
t.true(stats.isDirectory(), 'The expected output directory should exist')
resourcesPath = path.join(finalPath, util.generateResourcesPath(opts))
fs.stat(resourcesPath, cb)
}, function (stats, cb) {
t.true(stats.isDirectory(), 'The output directory should contain the expected resources subdirectory')
fs.stat(path.join(resourcesPath, 'app.asar'), cb)
}, function (stats, cb) {
t.true(stats.isFile(), 'app.asar should exist under the resources subdirectory when opts.asar is true')
fs.exists(path.join(resourcesPath, 'app'), function (exists) {
t.false(exists, 'app subdirectory should NOT exist when app.asar is built')
cb()
})
}
], function (err) {
t.end(err)
})
}
}
function createPruneTest (combination) {
return function (t) {
t.timeoutAfter(config.timeout)
var opts = Object.create(combination)
opts.name = 'basicTest'
opts.dir = path.join(__dirname, 'fixtures', 'basic')
opts.prune = true
var finalPath
var resourcesPath
waterfall([
function (cb) {
packager(opts, cb)
}, function (paths, cb) {
finalPath = paths[0]
fs.stat(finalPath, cb)
}, function (stats, cb) {
t.true(stats.isDirectory(), 'The expected output directory should exist')
resourcesPath = path.join(finalPath, util.generateResourcesPath(opts))
fs.stat(resourcesPath, cb)
}, function (stats, cb) {
t.true(stats.isDirectory(), 'The output directory should contain the expected resources subdirectory')
fs.stat(path.join(resourcesPath, 'app', 'node_modules', 'run-series'), cb)
}, function (stats, cb) {
t.true(stats.isDirectory(), 'npm dependency should exist under app/node_modules')
fs.exists(path.join(resourcesPath, 'app', 'node_modules', 'run-waterfall'), function (exists) {
t.false(exists, 'npm devDependency should NOT exist under app/node_modules')
cb()
})
}
], function (err) {
t.end(err)
})
}
}
function createIgnoreTest (combination, ignorePattern, ignoredFile) {
return function (t) {
t.timeoutAfter(config.timeout)
var opts = Object.create(combination)
opts.name = 'basicTest'
opts.dir = path.join(__dirname, 'fixtures', 'basic')
opts.ignore = ignorePattern
var appPath
waterfall([
function (cb) {
packager(opts, cb)
}, function (paths, cb) {
appPath = path.join(paths[0], util.generateResourcesPath(opts), 'app')
fs.stat(path.join(appPath, 'package.json'), cb)
}, function (stats, cb) {
t.true(stats.isFile(), 'The expected output directory should exist and contain files')
fs.exists(path.join(appPath, ignoredFile), function (exists) {
t.false(exists, 'Ignored file should not exist in output app directory')
cb()
})
}
], function (err) {
t.end(err)
})
}
}
function createOverwriteTest (combination) {
return function (t) {
t.timeoutAfter(config.timeout * 2) // Multiplied since this test packages the application twice
var opts = Object.create(combination)
opts.name = 'basicTest'
opts.dir = path.join(__dirname, 'fixtures', 'basic')
var finalPath
var testPath
waterfall([
function (cb) {
packager(opts, cb)
}, function (paths, cb) {
finalPath = paths[0]
fs.stat(finalPath, cb)
}, function (stats, cb) {
t.true(stats.isDirectory(), 'The expected output directory should exist')
// Create a dummy file to detect whether the output directory is replaced in subsequent runs
testPath = path.join(finalPath, 'test.txt')
fs.writeFile(testPath, 'test', cb)
}, function (cb) {
// Run again, defaulting to overwrite false
packager(opts, cb)
}, function (paths, cb) {
fs.stat(testPath, cb)
}, function (stats, cb) {
t.true(stats.isFile(), 'The existing output directory should exist as before (skipped by default)')
// Run a third time, explicitly setting overwrite to true
opts.overwrite = true
packager(opts, cb)
}, function (paths, cb) {
fs.exists(testPath, function (exists) {
t.false(exists, 'The output directory should be regenerated when overwrite is true')
cb()
})
}
], function (err) {
t.end(err)
})
}
}
util.testAllPlatforms('defaults test', createDefaultsTest)
util.testAllPlatforms('out test', createOutTest)
util.testAllPlatforms('asar test', createAsarTest)
util.testAllPlatforms('prune test', createPruneTest)
util.testAllPlatforms('ignore test: string in array', createIgnoreTest, ['ignorethis'], 'ignorethis.txt')
util.testAllPlatforms('ignore test: string', createIgnoreTest, 'ignorethis', 'ignorethis.txt')
util.testAllPlatforms('ignore test: RegExp', createIgnoreTest, /ignorethis/, 'ignorethis.txt')
util.testAllPlatforms('ignore test: string with slash', createIgnoreTest, 'ignore/this',
path.join('ignore', 'this.txt'))
util.testAllPlatforms('ignore test: only match subfolder of app', createIgnoreTest, 'electron-packager',
path.join('electron-packager', 'readme.txt'))
util.testAllPlatforms('overwrite test', createOverwriteTest)

4
test/config.json Normal file
View File

@ -0,0 +1,4 @@
{
"timeout": 15000,
"version": "0.28.3"
}

View File

@ -0,0 +1,2 @@
This file exists to test ability to ignore paths under app, without also
ignoring the entire app folder due to a match above it (#54 / #55).

0
test/fixtures/basic/ignore/this.txt vendored Normal file
View File

1
test/fixtures/basic/ignorethis.txt vendored Normal file
View File

@ -0,0 +1 @@

4
test/fixtures/basic/index.html vendored Normal file
View File

@ -0,0 +1,4 @@
<!DOCTYPE html>
<html>
<body>Hello, world!</body>
</html>

22
test/fixtures/basic/main.js vendored Normal file
View File

@ -0,0 +1,22 @@
var app = require('app')
var BrowserWindow = require('browser-window')
var mainWindow
app.on('window-all-closed', function () {
app.quit()
})
app.on('ready', function () {
mainWindow = new BrowserWindow({
center: true,
title: 'Basic Test',
width: 800,
height: 600
})
mainWindow.loadUrl('file://' + require('path').resolve(__dirname, 'index.html'))
mainWindow.on('closed', function () {
mainWindow = null
})
})

9
test/fixtures/basic/package.json vendored Normal file
View File

@ -0,0 +1,9 @@
{
"main": "main.js",
"dependencies": {
"run-series": "^1.1.1"
},
"devDependencies": {
"run-waterfall": "^1.1.1"
}
}

BIN
test/fixtures/monochrome.icns vendored Normal file

Binary file not shown.

27
test/index.js Normal file
View File

@ -0,0 +1,27 @@
var exec = require('child_process').exec
var path = require('path')
var series = require('run-series')
var config = require('./config.json')
var util = require('./util')
// Download all Electron distributions before running tests to avoid timing out due to network speed
series([
function (cb) {
console.log('Calling electron-download before running tests...')
util.downloadAll(config.version, cb)
}, function (cb) {
console.log('Running npm install in fixtures/basic...')
exec('npm install', {cwd: path.join(__dirname, 'fixtures', 'basic')}, cb)
}
], function () {
console.log('Running tests...')
require('./basic')
require('./multitarget')
if (process.platform !== 'win32') {
// Perform additional tests specific to building for OS X
require('./mac')
}
})

94
test/mac.js Normal file
View File

@ -0,0 +1,94 @@
var exec = require('child_process').exec
var fs = require('fs')
var path = require('path')
var packager = require('..')
var test = require('tape')
var waterfall = require('run-waterfall')
var config = require('./config.json')
var util = require('./util')
function createIconTest (icon, iconPath) {
return function (t) {
t.timeoutAfter(config.timeout)
var opts = {
name: 'basicTest',
dir: path.join(__dirname, 'fixtures', 'basic'),
version: config.version,
arch: 'x64',
platform: 'darwin',
icon: icon
}
var resourcesPath
waterfall([
function (cb) {
packager(opts, cb)
}, function (paths, cb) {
resourcesPath = path.join(paths[0], util.generateResourcesPath(opts))
fs.stat(resourcesPath, cb)
}, function (stats, cb) {
t.true(stats.isDirectory(), 'The output directory should contain the expected resources subdirectory')
util.areFilesEqual(iconPath, path.join(resourcesPath, 'atom.icns'), cb)
}, function (equal, cb) {
t.true(equal, 'atom.icns should be identical to the specified icon file')
cb()
}
], function (err) {
t.end(err)
})
}
}
var iconBase = path.join(__dirname, 'fixtures', 'monochrome')
var icnsPath = iconBase + '.icns'
util.setup()
test('icon test: .icns specified', createIconTest(icnsPath, icnsPath))
util.teardown()
util.setup()
test('icon test: .ico specified (should replace with .icns)', createIconTest(iconBase + '.ico', icnsPath))
util.teardown()
util.setup()
test('icon test: basename only (should add .icns)', createIconTest(iconBase, icnsPath))
util.teardown()
util.setup()
test('codesign test', function (t) {
t.timeoutAfter(config.timeout)
var opts = {
name: 'basicTest',
dir: path.join(__dirname, 'fixtures', 'basic'),
version: config.version,
arch: 'x64',
platform: 'darwin',
sign: '-' // Ad-hoc
}
var appPath
waterfall([
function (cb) {
packager(opts, cb)
}, function (paths, cb) {
appPath = path.join(paths[0], opts.name + '.app')
fs.stat(appPath, cb)
}, function (stats, cb) {
t.true(stats.isDirectory(), 'The expected .app directory should exist')
exec('codesign --verify --deep ' + appPath, cb)
}, function (stdout, stderr, cb) {
t.pass('codesign should verify successfully')
cb()
}
], function (err) {
var notFound = err && err.code === 127
if (notFound) console.log('codesign not installed; skipped')
t.end(notFound ? null : err)
})
})
util.teardown()

145
test/multitarget.js Normal file
View File

@ -0,0 +1,145 @@
var fs = require('fs')
var path = require('path')
var packager = require('..')
var series = require('run-series')
var test = require('tape')
var waterfall = require('run-waterfall')
var config = require('./config.json')
var util = require('./util')
function verifyPackageExistence (finalPaths, callback) {
series(finalPaths.map(function (finalPath) {
return function (cb) {
fs.stat(finalPath, cb)
}
}), function (err, statsResults) {
if (err) return callback(null, false)
callback(null, statsResults.every(function (stats) {
return stats.isDirectory()
}))
})
}
util.setup()
test('all test', function (t) {
t.timeoutAfter(config.timeout * 5) // 4-5 packages will be built during this test
var opts = {
name: 'basicTest',
dir: path.join(__dirname, 'fixtures', 'basic'),
version: config.version,
all: true
}
waterfall([
function (cb) {
packager(opts, cb)
}, function (finalPaths, cb) {
// Windows skips packaging for OS X, and OS X only has 64-bit releases
t.equal(finalPaths.length, process.platform === 'win32' ? 4 : 5,
'packager call should resolve with expected number of paths')
verifyPackageExistence(finalPaths, cb)
}, function (exists, cb) {
t.true(exists, 'Packages should be generated for all possible platforms')
cb()
}
], function (err) {
t.end(err)
})
})
util.teardown()
util.setup()
test('platform=all test (one arch)', function (t) {
t.timeoutAfter(config.timeout * 2) // 2 packages will be built during this test
var opts = {
name: 'basicTest',
dir: path.join(__dirname, 'fixtures', 'basic'),
version: config.version,
arch: 'ia32',
platform: 'all'
}
waterfall([
function (cb) {
packager(opts, cb)
}, function (finalPaths, cb) {
t.equal(finalPaths.length, 2, 'packager call should resolve with expected number of paths')
verifyPackageExistence(finalPaths, cb)
}, function (exists, cb) {
t.true(exists, 'Packages should be generated for both 32-bit platforms')
cb()
}
], function (err) {
t.end(err)
})
})
util.teardown()
util.setup()
test('arch=all test (one platform)', function (t) {
t.timeoutAfter(config.timeout * 2) // 2 packages will be built during this test
var opts = {
name: 'basicTest',
dir: path.join(__dirname, 'fixtures', 'basic'),
version: config.version,
arch: 'all',
platform: 'linux'
}
waterfall([
function (cb) {
packager(opts, cb)
}, function (finalPaths, cb) {
t.equal(finalPaths.length, 2, 'packager call should resolve with expected number of paths')
verifyPackageExistence(finalPaths, cb)
}, function (exists, cb) {
t.true(exists, 'Packages should be generated for both architectures')
cb()
}
], function (err) {
t.end(err)
})
})
util.teardown()
function createMultiTest (arch, platform) {
return function (t) {
t.timeoutAfter(config.timeout * 4) // 4 packages will be built during this test
var opts = {
name: 'basicTest',
dir: path.join(__dirname, 'fixtures', 'basic'),
version: config.version,
arch: arch,
platform: platform
}
waterfall([
function (cb) {
packager(opts, cb)
}, function (finalPaths, cb) {
t.equal(finalPaths.length, 4, 'packager call should resolve with expected number of paths')
verifyPackageExistence(finalPaths, cb)
}, function (exists, cb) {
t.true(exists, 'Packages should be generated for all combinations of specified archs and platforms')
cb()
}
], function (err) {
t.end(err)
})
}
}
util.setup()
test('multi-platform / multi-arch test, from arrays', createMultiTest(['ia32', 'x64'], ['linux', 'win32']))
util.teardown()
util.setup()
test('multi-platform / multi-arch test, from strings', createMultiTest('ia32,x64', 'linux,win32'))
util.teardown()

View File

@ -1,45 +0,0 @@
var test = require('tape')
var mkdirp = require('mkdirp')
var rimraf = require('rimraf')
var packager = require('../index.js')
var distdir = __dirname + '/dist'
rimraf.sync(distdir)
mkdirp.sync(distdir)
var opts = {
dir: __dirname + '/testapp',
name: 'Test',
version: '0.28.2',
out: distdir
}
test('package for windows', function (t) {
opts.platform = 'win32'
opts.arch = 'ia32'
packager(opts, function done (err, appPath) {
t.notOk(err, 'no err')
t.end()
})
})
test('package for linux', function (t) {
opts.platform = 'linux'
opts.arch = 'x64'
packager(opts, function done (err, appPath) {
t.notOk(err, 'no err')
t.end()
})
})
test('package for darwin', function (t) {
opts.platform = 'darwin'
opts.arch = 'x64'
packager(opts, function done (err, appPath) {
t.notOk(err, 'no err')
t.end()
})
})

View File

@ -1,6 +0,0 @@
var app = require('app')
app.on('ready', function () {
console.log('pizza')
app.exit()
})

View File

@ -1,11 +0,0 @@
{
"name": "testapp",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "BSD-2-Clause"
}

100
test/util.js Normal file
View File

@ -0,0 +1,100 @@
var fs = require('fs')
var path = require('path')
var test = require('tape')
var download = require('electron-download')
var mkdirp = require('mkdirp')
var rimraf = require('rimraf')
var series = require('run-series')
var ORIGINAL_CWD = process.cwd()
var WORK_CWD = path.join(__dirname, 'work')
var archs = ['ia32', 'x64']
var platforms = ['darwin', 'linux', 'win32']
var slice = Array.prototype.slice
var version = require('./config.json').version
var combinations = []
archs.forEach(function (arch) {
platforms.forEach(function (platform) {
// Electron does not have 32-bit releases for Mac OS X, so skip that combination
// Also skip testing darwin target on Windows since electron-packager itself skips it
// (see https://github.com/maxogden/electron-packager/issues/71)
if (platform === 'darwin' && (arch === 'ia32' || require('os').platform() === 'win32')) return
combinations.push({
arch: arch,
platform: platform,
version: version
})
})
})
exports.areFilesEqual = function areFilesEqual (file1, file2, callback) {
series([
function (cb) {
fs.readFile(file1, cb)
},
function (cb) {
fs.readFile(file2, cb)
}
], function (err, buffers) {
callback(err, slice.call(buffers[0]).every(function (b, i) {
return b === buffers[1][i]
}))
})
}
exports.downloadAll = function downloadAll (version, callback) {
series(combinations.map(function (combination) {
return function (cb) {
download(combination, cb)
}
}), callback)
}
exports.forEachCombination = function forEachCombination (cb) {
combinations.forEach(cb)
}
exports.generateResourcesPath = function generateResourcesPath (opts) {
return opts.platform === 'darwin' ? path.join(opts.name + '.app', 'Contents', 'Resources') : 'resources'
}
exports.getWorkCwd = function getWorkCwd () {
return WORK_CWD
}
// tape doesn't seem to have a provision for before/beforeEach/afterEach/after,
// so run setup/teardown and cleanup tasks as additional "tests" to put them in sequence
// and run them irrespective of test failures
exports.setup = function setup () {
test('setup', function (t) {
mkdirp(WORK_CWD, function (err) {
if (err) t.end(err)
process.chdir(WORK_CWD)
t.end()
})
})
}
exports.teardown = function teardown () {
test('teardown', function (t) {
process.chdir(ORIGINAL_CWD)
rimraf(WORK_CWD, function (err) {
t.end(err)
})
})
}
exports.testAllPlatforms = function testAllPlatforms (name, createTest /*, ...createTestArgs */) {
var args = slice.call(arguments, 2)
exports.setup()
exports.forEachCombination(function (combination) {
test(name + ': ' + combination.platform + '-' + combination.arch,
createTest.apply(null, [combination].concat(args)))
})
exports.teardown()
}