Merge pull request #5404 from atom/mb-integration-test

Add integration test to cover browser-side code
This commit is contained in:
Max Brunsfeld
2015-02-09 13:11:53 -08:00
10 changed files with 234 additions and 14 deletions

View File

@@ -222,7 +222,7 @@ module.exports = (grunt) ->
grunt.registerTask('test', ['shell:kill-atom', 'run-specs'])
grunt.registerTask('docs', ['markdown:guides', 'build-docs'])
ciTasks = ['output-disk-space', 'download-atom-shell', 'build']
ciTasks = ['output-disk-space', 'download-atom-shell', 'download-atom-shell-chromedriver', 'build']
ciTasks.push('dump-symbols') if process.platform isnt 'win32'
ciTasks.push('set-version', 'check-licenses', 'lint')
ciTasks.push('mkdeb') if process.platform is 'linux'
@@ -232,6 +232,6 @@ module.exports = (grunt) ->
ciTasks.push('publish-build')
grunt.registerTask('ci', ciTasks)
defaultTasks = ['download-atom-shell', 'build', 'set-version']
defaultTasks = ['download-atom-shell', 'download-atom-shell-chromedriver', 'build', 'set-version']
defaultTasks.push 'install' unless process.platform is 'linux'
grunt.registerTask('default', defaultTasks)

View File

@@ -19,7 +19,7 @@
"grunt-contrib-csslint": "~0.1.2",
"grunt-contrib-less": "~0.8.0",
"grunt-cson": "0.14.0",
"grunt-download-atom-shell": "~0.11.0",
"grunt-download-atom-shell": "~0.12.0",
"grunt-lesslint": "0.13.0",
"grunt-peg": "~1.1.0",
"grunt-shell": "~0.3.1",
@@ -35,6 +35,7 @@
"temp": "~0.8.1",
"underscore-plus": "1.x",
"unzip": "~0.1.9",
"vm-compatibility-layer": "~0.1.0"
"vm-compatibility-layer": "~0.1.0",
"webdriverio": "^2.4.5"
}
}

View File

@@ -85,15 +85,27 @@ module.exports = (grunt) ->
appPath = getAppPath()
resourcePath = process.cwd()
coreSpecsPath = path.resolve('spec')
chromedriverPath = path.join(resourcePath, "atom-shell", "chromedriver")
if process.platform in ['darwin', 'linux']
options =
cmd: appPath
args: ['--test', "--resource-path=#{resourcePath}", "--spec-directory=#{coreSpecsPath}"]
opts:
env: _.extend({}, process.env,
ATOM_INTEGRATION_TESTS_ENABLED: true
PATH: [process.env.path, chromedriverPath].join(":")
)
else if process.platform is 'win32'
options =
cmd: process.env.comspec
args: ['/c', appPath, '--test', "--resource-path=#{resourcePath}", "--spec-directory=#{coreSpecsPath}", "--log-file=ci.log"]
opts:
env: _.extend({}, process.env,
ATOM_INTEGRATION_TESTS_ENABLED: true
PATH: [process.env.path, chromedriverPath].join(";")
)
spawn options, (error, results, code) ->
if process.platform is 'win32'

View File

@@ -0,0 +1,5 @@
"*":
welcome:
showOnStartup: false
"exception-reporting":
userId: "7c0a3c52-795c-5e20-5323-64efcf91f212"

View File

@@ -0,0 +1,43 @@
#!/bin/bash
# This script wraps the `Atom` binary, allowing the `chromedriver` server to
# execute it with positional arguments and environment variables. `chromedriver`
# only allows 'switches' to be specified when starting a browser, not positional
# arguments, so this script accepts the following special switches:
#
# * `atom-path`: The path to the `Atom` binary.
# * `atom-arg`: A positional argument to pass to Atom. This flag can be specified
# multiple times.
# * `atom-env`: A key=value environment variable to set for Atom. This flag can
# be specified multiple times.
#
# Any other switches will be passed through to `Atom`.
atom_path=""
atom_switches=()
atom_args=()
for arg in "$@"; do
case $arg in
--atom-path=*)
atom_path="${arg#*=}"
;;
--atom-arg=*)
atom_args+=(${arg#*=})
;;
--atom-env=*)
export ${arg#*=}
;;
*)
atom_switches+=($arg)
;;
esac
done
echo "Launching Atom" >&2
echo ${atom_path} ${atom_args[@]} ${atom_switches[@]} >&2
exec ${atom_path} ${atom_args[@]} ${atom_switches[@]}

View File

@@ -0,0 +1,87 @@
path = require "path"
temp = require("temp").track()
remote = require "remote"
{map, extend} = require "underscore-plus"
{spawn, spawnSync} = require "child_process"
webdriverio = require "../../../build/node_modules/webdriverio"
async = require "async"
AtomPath = remote.process.argv[0]
AtomLauncherPath = path.join(__dirname, "..", "helpers", "atom-launcher.sh")
SocketPath = path.join(temp.mkdirSync("socket-dir"), "atom.sock")
ChromedriverPort = 9515
module.exports =
driverTest: (fn) ->
chromedriver = spawn("chromedriver", [
"--verbose",
"--port=#{ChromedriverPort}",
"--url-base=/wd/hub"
])
logs = []
errorCode = null
chromedriver.on "exit", (code, signal) ->
errorCode = code unless signal?
chromedriver.stderr.on "data", (log) ->
logs.push(log.toString())
chromedriver.stderr.on "close", ->
if errorCode?
jasmine.getEnv().currentSpec.fail "Chromedriver exited. code: #{errorCode}. Logs: #{logs.join("\n")}"
waitsFor "webdriver steps to complete", (done) ->
fn()
.catch((error) -> jasmine.getEnv().currentSpec.fail(err.message))
.end()
.call(done)
, 30000
runs -> chromedriver.kill()
# Start Atom using chromedriver.
startAtom: (args, env={}) ->
webdriverio.remote(
host: 'localhost'
port: ChromedriverPort
desiredCapabilities:
browserName: "atom"
chromeOptions:
binary: AtomLauncherPath
args: [
"atom-path=#{AtomPath}"
"dev"
"safe"
"user-data-dir=#{temp.mkdirSync('integration-spec-')}"
"socket-path=#{SocketPath}"
]
.concat(map args, (arg) -> "atom-arg=#{arg}")
.concat(map env, (value, key) -> "atom-env=#{key}=#{value}"))
.init()
.addCommand "waitForCondition", (conditionFn, timeout, cb) ->
timedOut = succeeded = false
pollingInterval = Math.min(timeout, 100)
setTimeout((-> timedOut = true), timeout)
async.until(
(-> succeeded or timedOut),
((next) =>
setTimeout(=>
conditionFn.call(this).then(
((result) ->
succeeded = result
next()),
((err) -> next(err))
)
, pollingInterval)),
((err) -> cb(err, succeeded))
)
# Once one `Atom` window is open, subsequent invocations of `Atom` will exit
# immediately.
startAnotherAtom: (args, env={}) ->
spawnSync(AtomPath, args.concat([
"--dev"
"--safe"
"--socket-path=#{SocketPath}"
]), env: extend({}, process.env, env))

View File

@@ -0,0 +1,67 @@
# These tests are excluded by default. To run them from the command line:
#
# ATOM_INTEGRATION_TESTS_ENABLED=true apm test
return unless process.env.ATOM_INTEGRATION_TESTS_ENABLED
fs = require "fs"
path = require "path"
temp = require("temp").track()
AtomHome = path.join(__dirname, "fixtures", "atom-home")
{startAtom, startAnotherAtom, driverTest} = require("./helpers/start-atom")
describe "Starting Atom", ->
beforeEach ->
jasmine.useRealClock()
describe "opening paths via commmand-line arguments", ->
[tempDirPath, tempFilePath] = []
beforeEach ->
tempDirPath = temp.mkdirSync("empty-dir")
tempFilePath = path.join(tempDirPath, "an-existing-file")
fs.writeFileSync(tempFilePath, "This was already here.")
it "reuses existing windows when directories are reopened", ->
driverTest ->
# Opening a new file creates one window with one empty text editor.
startAtom([path.join(tempDirPath, "new-file")], ATOM_HOME: AtomHome)
.waitForExist("atom-text-editor", 5000)
.then((exists) -> expect(exists).toBe true)
.windowHandles()
.then(({value}) -> expect(value.length).toBe 1)
.execute(-> atom.workspace.getActivePane().getItems().length)
.then(({value}) -> expect(value).toBe 1)
# Typing in the editor changes its text.
.execute(-> atom.workspace.getActiveTextEditor().getText())
.then(({value}) -> expect(value).toBe "")
.click("atom-text-editor")
.keys("Hello!")
.execute(-> atom.workspace.getActiveTextEditor().getText())
.then(({value}) -> expect(value).toBe "Hello!")
# Opening an existing file in the same directory reuses the window and
# adds a new tab for the file.
.call(-> startAnotherAtom([tempFilePath], ATOM_HOME: AtomHome))
.waitForCondition(
(-> @execute((-> atom.workspace.getActivePane().getItems().length)).then ({value}) -> value is 2),
5000)
.then((result) -> expect(result).toBe(true))
.execute(-> atom.workspace.getActiveTextEditor().getText())
.then(({value}) -> expect(value).toBe "This was already here.")
# Opening a different directory creates a second window with no
# tabs open.
.call(-> startAnotherAtom([temp.mkdirSync("another-empty-dir")], ATOM_HOME: AtomHome))
.waitForCondition(
(-> @windowHandles().then(({value}) -> value.length is 2)),
5000)
.then((result) -> expect(result).toBe(true))
.windowHandles()
.then(({value}) ->
@window(value[1])
.waitForExist("atom-workspace", 5000)
.then((exists) -> expect(exists).toBe true)
.execute(-> atom.workspace.getActivePane().getItems().length)
.then(({value}) -> expect(value).toBe 0))

View File

@@ -305,13 +305,13 @@ window.waitsForPromise = (args...) ->
window.waitsFor timeout, (moveOn) ->
promise = fn()
if shouldReject
promise.catch(moveOn)
(promise.catch ? promise.thenCatch).call(promise, moveOn)
promise.then ->
jasmine.getEnv().currentSpec.fail("Expected promise to be rejected, but it was resolved")
moveOn()
else
promise.then(moveOn)
promise.catch (error) ->
(promise.catch ? promise.thenCatch).call promise, (error) ->
jasmine.getEnv().currentSpec.fail("Expected promise to be resolved, but it was rejected with #{jasmine.pp(error)}")
moveOn()

View File

@@ -14,7 +14,7 @@ url = require 'url'
{EventEmitter} = require 'events'
_ = require 'underscore-plus'
socketPath =
DefaultSocketPath =
if process.platform is 'win32'
'\\\\.\\pipe\\atom-sock'
else
@@ -31,17 +31,20 @@ class AtomApplication
# Public: The entry point into the Atom application.
@open: (options) ->
options.socketPath ?= DefaultSocketPath
createAtomApplication = -> new AtomApplication(options)
# FIXME: Sometimes when socketPath doesn't exist, net.connect would strangely
# take a few seconds to trigger 'error' event, it could be a bug of node
# or atom-shell, before it's fixed we check the existence of socketPath to
# speedup startup.
if (process.platform isnt 'win32' and not fs.existsSync socketPath) or options.test
if (process.platform isnt 'win32' and not fs.existsSync options.socketPath) or options.test
createAtomApplication()
return
client = net.connect {path: socketPath}, ->
client = net.connect {path: options.socketPath}, ->
client.write JSON.stringify(options), ->
client.end()
app.terminate()
@@ -57,7 +60,7 @@ class AtomApplication
exit: (status) -> app.exit(status)
constructor: (options) ->
{@resourcePath, @version, @devMode, @safeMode} = options
{@resourcePath, @version, @devMode, @safeMode, @socketPath} = options
# Normalize to make sure drive letter case is consistent on Windows
@resourcePath = path.normalize(@resourcePath) if @resourcePath
@@ -119,15 +122,15 @@ class AtomApplication
connection.on 'data', (data) =>
@openWithOptions(JSON.parse(data))
server.listen socketPath
server.listen @socketPath
server.on 'error', (error) -> console.error 'Application server failed', error
deleteSocketFile: ->
return if process.platform is 'win32'
if fs.existsSync(socketPath)
if fs.existsSync(@socketPath)
try
fs.unlinkSync(socketPath)
fs.unlinkSync(@socketPath)
catch error
# Ignore ENOENT errors in case the file was deleted between the exists
# check and the call to unlink sync. This occurred occasionally on CI

View File

@@ -117,6 +117,7 @@ parseCommandLine = ->
options.alias('t', 'test').boolean('t').describe('t', 'Run the specified specs and exit with error code on failures.')
options.alias('v', 'version').boolean('v').describe('v', 'Print the version.')
options.alias('w', 'wait').boolean('w').describe('w', 'Wait for window to be closed before returning.')
options.string('socket-path')
args = options.argv
if args.help
@@ -137,6 +138,7 @@ parseCommandLine = ->
newWindow = args['new-window']
pidToKillWhenClosed = args['pid'] if args['wait']
logFile = args['log-file']
socketPath = args['socket-path']
if args['resource-path']
devMode = true
@@ -161,6 +163,6 @@ parseCommandLine = ->
# explicitly pass it by command line, see http://git.io/YC8_Ew.
process.env.PATH = args['path-environment'] if args['path-environment']
{resourcePath, pathsToOpen, executedFrom, test, version, pidToKillWhenClosed, devMode, safeMode, newWindow, specDirectory, logFile}
{resourcePath, pathsToOpen, executedFrom, test, version, pidToKillWhenClosed, devMode, safeMode, newWindow, specDirectory, logFile, socketPath}
start()