mirror of
https://github.com/Foundry376/Mailspring.git
synced 2025-09-28 15:35:37 +08:00
feat(tests): add integration tests
comment Adding test harness Using key strokes in main window test Tests work now Clean up argument variables Rename list manager and get rid of old spec-helper methods Extract out time overrides from spec-helper Spectron test for contenteditable fix spec exit codes and boot mode fix(spec): cleanup N1.sh and make specs fail with exit code 1 Revert tests and get it working in window Move to spec_integration and add window load tester Specs pass. Console logs still in Remove console logs Extract N1 Launcher ready method Make integrated unit test runner feat(tests): adding integration tests Summary: The /spectron folder got moved to /spec_integration There are now unit tests (the old ones) run via the renamed `script/grunt run-unit-tests` There are now integration tests run via the command `script/grunt run-integration-tests`. There are two types of integration tests: 1. Tests that operate on the whole app via Selenium/Chromedriver. These tests have access to Spectron APIs but do NOT have access to any JS object running inside the application. See the `app-boot-spec.es6` for an example of these tests. This is tricky because we want to test the Main window, but Spectron may latch onto any other of our loading windows. Code in `integration-helper` give us an API that finds and loads the main window so we can test it 2. Tests that run in the unit test suite that need Spectron to perform integration-like behavior. These are the contentedtiable specs. The Spectron server is accessed from the app and can be used to trigger actions on the running app, from the app. These tests use the windowed-test runner so Spectron can identify whether the tests have completed, passed, or failed. Unfortunately Spectron can't access the logs , nor the exit code of the test script thereby forcing us to parse the HTML DOM. (Note this is still a WIP) I also revamped the `N1.sh` file when getting the launch arguments to work properly. It's much cleaner. We didn't need most of the data. Test Plan: new tests Reviewers: juan, bengotow Differential Revision: https://phab.nylas.com/D2289 Fix composer specs Tests can properly detect when Spectron is in the environment Report plain text output in specs fixing contenteditable specs Testing slow keymaps on contenteditable specs Move to DOm mutation Spell as `subtree` not `subTree`
This commit is contained in:
parent
bc839fb541
commit
73e7c1c52e
28 changed files with 758 additions and 358 deletions
|
@ -356,7 +356,7 @@ module.exports = (grunt) ->
|
|||
|
||||
grunt.registerTask('compile', ['coffee', 'cjsx', 'babel', 'prebuild-less', 'cson', 'peg'])
|
||||
grunt.registerTask('lint', ['coffeelint', 'csslint', 'lesslint', 'nylaslint', 'eslint'])
|
||||
grunt.registerTask('test', ['shell:kill-n1', 'run-edgehill-specs'])
|
||||
grunt.registerTask('test', ['shell:kill-n1', 'run-unit-tests'])
|
||||
grunt.registerTask('docs', ['build-docs', 'render-docs'])
|
||||
|
||||
ciTasks = ['output-disk-space', 'download-electron', 'build']
|
||||
|
|
20
build/tasks/run-integration-tests-task.coffee
Normal file
20
build/tasks/run-integration-tests-task.coffee
Normal file
|
@ -0,0 +1,20 @@
|
|||
path = require 'path'
|
||||
childProcess = require 'child_process'
|
||||
|
||||
module.exports = (grunt) ->
|
||||
desc = "Boots Selenium via Spectron to run integration tests"
|
||||
grunt.registerTask 'run-integration-tests', desc, ->
|
||||
done = @async()
|
||||
|
||||
rootPath = path.resolve('.')
|
||||
npmPath = path.join(rootPath, "build", "node_modules", ".bin", "npm")
|
||||
|
||||
process.chdir('./spec_integration')
|
||||
testProc = childProcess.spawn(npmPath,
|
||||
["test", "NYLAS_ROOT_PATH=#{rootPath}"],
|
||||
{stdio: "inherit"})
|
||||
|
||||
testProc.on 'exit', (exitCode, signal) ->
|
||||
process.chdir('..')
|
||||
if exitCode is 0 then done()
|
||||
else done(false)
|
25
build/tasks/run-unit-tests-task.coffee
Normal file
25
build/tasks/run-unit-tests-task.coffee
Normal file
|
@ -0,0 +1,25 @@
|
|||
childProcess = require 'child_process'
|
||||
|
||||
module.exports = (grunt) ->
|
||||
{notifyAPI} = require('./task-helpers')(grunt)
|
||||
|
||||
desc = "Boots N1 in --test mode to run unit tests"
|
||||
grunt.registerTask 'run-unit-tests', desc, ->
|
||||
done = @async()
|
||||
|
||||
testProc = childProcess.spawn("./N1.sh", ["--test"])
|
||||
|
||||
testOutput = ""
|
||||
testProc.stdout.pipe(process.stdout)
|
||||
testProc.stderr.pipe(process.stderr)
|
||||
testProc.stdout.on 'data', (data) -> testOutput += data.toString()
|
||||
testProc.stderr.on 'data', (data) -> testOutput += data.toString()
|
||||
|
||||
testProc.on 'error', (err) -> grunt.log.error("Process error: #{err}")
|
||||
|
||||
testProc.on 'exit', (exitCode, signal) ->
|
||||
if exitCode is 0 then done()
|
||||
else
|
||||
testOutput = grunt.log.uncolor(testOutput)
|
||||
msg = "Aghhh somebody broke the build. ```#{testOutput}```"
|
||||
notifyAPI msg, -> done(false)
|
|
@ -1,78 +0,0 @@
|
|||
fs = require 'fs'
|
||||
path = require 'path'
|
||||
request = require 'request'
|
||||
childProcess = require 'child_process'
|
||||
|
||||
executeTests = ({cmd, args}, grunt, done) ->
|
||||
testProc = childProcess.spawn(cmd, args)
|
||||
|
||||
testOutput = ""
|
||||
testProc.stdout.pipe(process.stdout)
|
||||
testProc.stderr.pipe(process.stderr)
|
||||
testProc.stdout.on 'data', (data) -> testOutput += data.toString()
|
||||
testProc.stderr.on 'data', (data) -> testOutput += data.toString()
|
||||
|
||||
testProc.on 'error', (err) -> grunt.log.error("Process error: #{err}")
|
||||
|
||||
testProc.on 'exit', (exitCode, signal) ->
|
||||
if exitCode is 0
|
||||
done()
|
||||
else
|
||||
notifyOfTestError testOutput, grunt, ->
|
||||
done(false)
|
||||
|
||||
notifyOfTestError = (testOutput, grunt, callback) ->
|
||||
if (process.env["TEST_ERROR_HOOK_URL"] ? "").length > 0
|
||||
testOutput = grunt.log.uncolor(testOutput)
|
||||
request.post
|
||||
url: process.env["TEST_ERROR_HOOK_URL"]
|
||||
json:
|
||||
username: "Edgehill Builds"
|
||||
text: "Aghhh somebody broke the build. ```#{testOutput}```"
|
||||
, callback
|
||||
else
|
||||
callback()
|
||||
|
||||
|
||||
module.exports = (grunt) ->
|
||||
|
||||
grunt.registerTask 'run-edgehill-specs', 'Run the specs', ->
|
||||
done = @async()
|
||||
executeTests({cmd: './N1.sh', args: ['--test']}, grunt, done)
|
||||
|
||||
grunt.registerTask 'run-spectron-specs', 'Run spectron specs', ->
|
||||
shellAppDir = grunt.config.get('nylasGruntConfig.shellAppDir')
|
||||
|
||||
if process.platform is 'darwin'
|
||||
executablePath = path.join(shellAppDir, 'Contents', 'MacOS', 'Nylas')
|
||||
else
|
||||
executablePath = path.join(shellAppDir, 'nylas')
|
||||
|
||||
done = @async()
|
||||
npmPath = path.resolve "./build/node_modules/.bin/npm"
|
||||
|
||||
if process.platform is 'win32'
|
||||
grunt.log.error("run-spectron-specs only works on Mac OS X at the moment.")
|
||||
done(false)
|
||||
|
||||
if not fs.existsSync(executablePath)
|
||||
grunt.log.error("run-spectron-specs requires the built version of the app at #{executablePath}")
|
||||
done(false)
|
||||
|
||||
process.chdir('./spectron')
|
||||
grunt.log.writeln "Current dir: #{process.cwd()}"
|
||||
installProc = childProcess.exec "#{npmPath} install", (error) ->
|
||||
if error?
|
||||
process.chdir('..')
|
||||
grunt.log.error('Failed while running npm install in spectron folder')
|
||||
grunt.fail.warn(error)
|
||||
done(false)
|
||||
else
|
||||
appArgs = [
|
||||
'test'
|
||||
"APP_PATH=#{executablePath}"
|
||||
"APP_ARGS="
|
||||
]
|
||||
executeTests {cmd: npmPath, args: appArgs}, grunt, (succeeded) ->
|
||||
process.chdir('..')
|
||||
done(succeeded)
|
|
@ -1,5 +1,6 @@
|
|||
fs = require 'fs-plus'
|
||||
path = require 'path'
|
||||
request = require 'request'
|
||||
|
||||
module.exports = (grunt) ->
|
||||
cp: (source, destination, {filter}={}) ->
|
||||
|
@ -66,3 +67,14 @@ module.exports = (grunt) ->
|
|||
engines?.nylas?
|
||||
catch error
|
||||
false
|
||||
|
||||
notifyAPI: (msg, callback) ->
|
||||
if (process.env("TEST_ERROR_HOOK_URL") ? "").length > 0
|
||||
request.post
|
||||
url: process.env("TEST_ERROR_HOOK_URL")
|
||||
json:
|
||||
username: "Edgehill Builds"
|
||||
text: msg
|
||||
, callback
|
||||
else callback()
|
||||
|
||||
|
|
|
@ -124,7 +124,7 @@ useFullDraft = ->
|
|||
replyToMessageId: null
|
||||
|
||||
makeComposer = ->
|
||||
@composer = ReactTestUtils.renderIntoDocument(
|
||||
@composer = NylasTestUtils.renderIntoDocument(
|
||||
<ComposerView draftClientId={DRAFT_CLIENT_ID} />
|
||||
)
|
||||
|
||||
|
@ -135,6 +135,7 @@ describe "populated composer", ->
|
|||
|
||||
afterEach ->
|
||||
DraftStore._cleanupAllSessions()
|
||||
NylasTestUtils.removeFromDocument(@composer)
|
||||
|
||||
describe "when sending a new message", ->
|
||||
it 'makes a request with the message contents', ->
|
||||
|
@ -366,7 +367,7 @@ describe "populated composer", ->
|
|||
it "ignores focuses to participant fields", ->
|
||||
@composer.setState focusedField: Fields.To
|
||||
expect(@body.focus).not.toHaveBeenCalled()
|
||||
expect(React.findDOMNode.calls.length).toBe 3
|
||||
expect(@composer._applyFieldFocus.calls.length).toBe 1
|
||||
|
||||
describe "when participants are added during a draft update", ->
|
||||
it "shows the cc fields and bcc fields to ensure participants are never hidden", ->
|
||||
|
@ -542,12 +543,12 @@ describe "populated composer", ->
|
|||
cmdctrl = 'cmd'
|
||||
else
|
||||
cmdctrl = 'ctrl'
|
||||
NylasTestUtils.keyPress("#{cmdctrl}-enter", React.findDOMNode(@$composer))
|
||||
NylasTestUtils.keyDown("#{cmdctrl}-enter", React.findDOMNode(@$composer))
|
||||
expect(Actions.sendDraft).toHaveBeenCalled()
|
||||
expect(Actions.sendDraft.calls.length).toBe 1
|
||||
|
||||
it "does not send the draft on enter if the button isn't in focus", ->
|
||||
NylasTestUtils.keyPress("enter", React.findDOMNode(@$composer))
|
||||
NylasTestUtils.keyDown("enter", React.findDOMNode(@$composer))
|
||||
expect(Actions.sendDraft).not.toHaveBeenCalled()
|
||||
|
||||
it "doesn't let you send twice", ->
|
||||
|
@ -555,12 +556,12 @@ describe "populated composer", ->
|
|||
cmdctrl = 'cmd'
|
||||
else
|
||||
cmdctrl = 'ctrl'
|
||||
NylasTestUtils.keyPress("#{cmdctrl}-enter", React.findDOMNode(@$composer))
|
||||
NylasTestUtils.keyDown("#{cmdctrl}-enter", React.findDOMNode(@$composer))
|
||||
expect(Actions.sendDraft).toHaveBeenCalled()
|
||||
expect(Actions.sendDraft.calls.length).toBe 1
|
||||
@isSending = true
|
||||
DraftStore.trigger()
|
||||
NylasTestUtils.keyPress("#{cmdctrl}-enter", React.findDOMNode(@$composer))
|
||||
NylasTestUtils.keyDown("#{cmdctrl}-enter", React.findDOMNode(@$composer))
|
||||
expect(Actions.sendDraft).toHaveBeenCalled()
|
||||
expect(Actions.sendDraft.calls.length).toBe 1
|
||||
|
||||
|
|
17
package.json
17
package.json
|
@ -32,8 +32,8 @@
|
|||
"immutable": "3.7.5",
|
||||
"inflection": "^1.7",
|
||||
"jasmine-json": "~0.0",
|
||||
"jasmine-tagged": "^1.1.2",
|
||||
"jasmine-react-helpers": "^0.2",
|
||||
"jasmine-tagged": "^1.1.2",
|
||||
"jquery": "^2.1.1",
|
||||
"juice": "^1.4",
|
||||
"less-cache": "0.21",
|
||||
|
@ -41,13 +41,13 @@
|
|||
"mkdirp": "^0.5",
|
||||
"moment": "^2.8",
|
||||
"moment-timezone": "^0.3",
|
||||
"nslog": "^3",
|
||||
"node-uuid": "^1.4",
|
||||
"nock": "^2",
|
||||
"node-uuid": "^1.4",
|
||||
"nslog": "^3",
|
||||
"optimist": "0.4.0",
|
||||
"pathwatcher": "~6.2",
|
||||
"property-accessors": "^1",
|
||||
"promise-queue": "2.1.1",
|
||||
"property-accessors": "^1",
|
||||
"proxyquire": "1.3.1",
|
||||
"q": "^1.0.1",
|
||||
"raven": "0.7.2",
|
||||
|
@ -63,13 +63,16 @@
|
|||
"semver": "^4.2",
|
||||
"serializable": "^1",
|
||||
"service-hub": "^0.2.0",
|
||||
"space-pen": "3.8.2",
|
||||
"spellchecker": "^3.1.2",
|
||||
"source-map-support": "^0.3.2",
|
||||
"space-pen": "3.8.2",
|
||||
"spectron": "0.34.1",
|
||||
"spellchecker": "^3.1.2",
|
||||
"sqlite3": "https://github.com/mapbox/node-sqlite3/archive/v3.1.1.tar.gz",
|
||||
"temp": "^0.8",
|
||||
"theorist": "^1.0",
|
||||
"underscore": "^1.8",
|
||||
"underscore.string": "^3.0"
|
||||
"underscore.string": "^3.0",
|
||||
"webdriverio": "3.2.6"
|
||||
},
|
||||
"packageDependencies": {},
|
||||
"private": true,
|
||||
|
|
|
@ -145,6 +145,12 @@ function bootstrap() {
|
|||
var downloadElectronCmd = gruntPath + " download-electron --gruntfile build/Gruntfile.coffee"
|
||||
m7 += " $ "+downloadElectronCmd
|
||||
|
||||
var integrationCommand = npmPath + npmFlags + 'install';
|
||||
var integrationOptions = {cwd: path.resolve(__dirname, '..', 'spec_integration')};
|
||||
|
||||
m8 = "\n\n---> Installing integration test modules\n\n"
|
||||
m8 += " $ "+integrationCommand+" "+printArgs(integrationOptions)+"\n"
|
||||
|
||||
var commands = [
|
||||
{
|
||||
command: buildInstallCommand,
|
||||
|
@ -211,6 +217,11 @@ function bootstrap() {
|
|||
command: downloadElectronCmd,
|
||||
message: m7
|
||||
},
|
||||
{
|
||||
command: integrationCommand,
|
||||
options: integrationOptions,
|
||||
message: m8
|
||||
},
|
||||
{
|
||||
command: sqlite3Command,
|
||||
message: "Building sqlite3 with command: "+sqlite3Command
|
||||
|
|
|
@ -1,105 +0,0 @@
|
|||
|
||||
xdescribe "ListManager", ->
|
||||
beforeEach ->
|
||||
@ce = new ContenteditableTestHarness
|
||||
|
||||
it "Creates ordered lists", ->
|
||||
@ce.type ['1', '.', ' ']
|
||||
@ce.expectHTML "<ol><li></li></ol>"
|
||||
@ce.expectSelection (dom) ->
|
||||
dom.querySelectorAll("li")[0]
|
||||
|
||||
it "Undoes ordered list creation with backspace", ->
|
||||
@ce.type ['1', '.', ' ', 'backspace']
|
||||
@ce.expectHTML "1. "
|
||||
@ce.expectSelection (dom) ->
|
||||
node: dom.childNodes[0]
|
||||
offset: 3
|
||||
|
||||
it "Creates unordered lists with star", ->
|
||||
@ce.type ['*', ' ']
|
||||
@ce.expectHTML "<ul><li></li></ul>"
|
||||
@ce.expectSelection (dom) ->
|
||||
dom.querySelectorAll("li")[0]
|
||||
|
||||
it "Undoes unordered list creation with backspace", ->
|
||||
@ce.type ['*', ' ', 'backspace']
|
||||
@ce.expectHTML "* "
|
||||
@ce.expectSelection (dom) ->
|
||||
node: dom.childNodes[0]
|
||||
offset: 2
|
||||
|
||||
it "Creates unordered lists with dash", ->
|
||||
@ce.type ['-', ' ']
|
||||
@ce.expectHTML "<ul><li></li></ul>"
|
||||
@ce.expectSelection (dom) ->
|
||||
dom.querySelectorAll("li")[0]
|
||||
|
||||
it "Undoes unordered list creation with backspace", ->
|
||||
@ce.type ['-', ' ', 'backspace']
|
||||
@ce.expectHTML "- "
|
||||
@ce.expectSelection (dom) ->
|
||||
node: dom.childNodes[0]
|
||||
offset: 2
|
||||
|
||||
it "create a single item then delete it with backspace", ->
|
||||
@ce.type ['-', ' ', 'a', 'left', 'backspace']
|
||||
@ce.expectHTML "a"
|
||||
@ce.expectSelection (dom) ->
|
||||
node: dom.childNodes[0]
|
||||
offset: 0
|
||||
|
||||
it "create a single item then delete it with tab", ->
|
||||
@ce.type ['-', ' ', 'a', 'shift-tab']
|
||||
@ce.expectHTML "a"
|
||||
@ce.expectSelection (dom) -> dom.childNodes[0]
|
||||
node: dom.childNodes[0]
|
||||
offset: 1
|
||||
|
||||
describe "when creating two items in a list", ->
|
||||
beforeEach ->
|
||||
@twoItemKeys = ['-', ' ', 'a', 'enter', 'b']
|
||||
|
||||
it "creates two items with enter at end", ->
|
||||
@ce.type @twoItemKeys
|
||||
@ce.expectHTML "<ul><li>a</li><li>b</li></ul>"
|
||||
@ce.expectSelection (dom) ->
|
||||
node: dom.querySelectorAll('li')[1].childNodes[0]
|
||||
offset: 1
|
||||
|
||||
it "backspace from the start of the 1st item outdents", ->
|
||||
@ce.type @twoItemKeys.concat ['left', 'up', 'backspace']
|
||||
|
||||
it "backspace from the start of the 2nd item outdents", ->
|
||||
@ce.type @twoItemKeys.concat ['left', 'backspace']
|
||||
|
||||
it "shift-tab from the start of the 1st item outdents", ->
|
||||
@ce.type @twoItemKeys.concat ['left', 'up', 'shift-tab']
|
||||
|
||||
it "shift-tab from the start of the 2nd item outdents", ->
|
||||
@ce.type @twoItemKeys.concat ['left', 'shift-tab']
|
||||
|
||||
it "shift-tab from the end of the 1st item outdents", ->
|
||||
@ce.type @twoItemKeys.concat ['up', 'shift-tab']
|
||||
|
||||
it "shift-tab from the end of the 2nd item outdents", ->
|
||||
@ce.type @twoItemKeys.concat ['shift-tab']
|
||||
|
||||
it "backspace from the end of the 1st item doesn't outdent", ->
|
||||
@ce.type @twoItemKeys.concat ['up', 'backspace']
|
||||
|
||||
it "backspace from the end of the 2nd item doesn't outdent", ->
|
||||
@ce.type @twoItemKeys.concat ['backspace']
|
||||
|
||||
describe "multi-depth bullets", ->
|
||||
it "creates multi level bullet when tabbed in", ->
|
||||
@ce.type ['-', ' ', 'a', 'tab']
|
||||
|
||||
it "creates multi level bullet when tabbed in", ->
|
||||
@ce.type ['-', ' ', 'tab', 'a']
|
||||
|
||||
it "returns to single level bullet on backspace", ->
|
||||
@ce.type ['-', ' ', 'a', 'tab', 'left', 'backspace']
|
||||
|
||||
it "returns to single level bullet on shift-tab", ->
|
||||
@ce.type ['-', ' ', 'a', 'tab', 'shift-tab']
|
|
@ -0,0 +1,110 @@
|
|||
_ = require 'underscore'
|
||||
React = require 'react/addons'
|
||||
TimeOverride = require '../../time-override'
|
||||
NylasTestUtils = require '../../nylas-test-utils'
|
||||
|
||||
{Contenteditable} = require 'nylas-component-kit'
|
||||
|
||||
###
|
||||
Public: Easily test contenteditable interactions
|
||||
|
||||
Create a new instance of this on each test. It will render a new
|
||||
Contenteditable into the document wrapped around a class that can keep
|
||||
track of its state.
|
||||
|
||||
For example
|
||||
|
||||
```coffee
|
||||
beforeEach ->
|
||||
@ce = new ContenteditableTestHarness
|
||||
|
||||
it "can create an ordered list", ->
|
||||
@ce.keys ['1', '.', ' ']
|
||||
@ce.expectHTML "<ol><li></li></ol>"
|
||||
@ce.expectSelection (dom) ->
|
||||
node: dom.querySelectorAll("li")[0]
|
||||
|
||||
afterEach ->
|
||||
@ce.cleanup()
|
||||
|
||||
```
|
||||
|
||||
**Be sure to call `cleanup` after each test**
|
||||
|
||||
###
|
||||
class ContenteditableTestHarness
|
||||
constructor: ({@props, @initialValue}={}) ->
|
||||
@props ?= {}
|
||||
@initialValue ?= ""
|
||||
|
||||
@wrap = NylasTestUtils.renderIntoDocument(
|
||||
<Wrap ceProps={@props} initialValue={@initialValue} />
|
||||
)
|
||||
|
||||
cleanup: ->
|
||||
NylasTestUtils.removeFromDocument(@wrap)
|
||||
|
||||
# We send keys to spectron one at a time. We also need ot use a "real"
|
||||
# setTimeout since Spectron is completely outside of the mocked setTimeouts
|
||||
# that we setup. We still `advanceClock` to clear any components or Promises
|
||||
# that need to run inbetween keystrokes.
|
||||
keys: (keyStrokes=[]) -> new Promise (resolve, reject) =>
|
||||
TimeOverride.disableSpies()
|
||||
@getDOM().focus()
|
||||
timeout = 0
|
||||
KEY_DELAY = 1000
|
||||
keyStrokes.forEach (key) ->
|
||||
window.setTimeout ->
|
||||
NylasEnv.spectron.client.keys([key])
|
||||
advanceClock(KEY_DELAY)
|
||||
, timeout
|
||||
timeout += KEY_DELAY
|
||||
window.setTimeout ->
|
||||
resolve()
|
||||
, timeout + KEY_DELAY
|
||||
|
||||
expectHTML: (expectedHTML) ->
|
||||
expect(@wrap.state.value).toBe expectedHTML
|
||||
|
||||
expectSelection: (callback) ->
|
||||
expectSel = callback(@getDOM())
|
||||
|
||||
anchorNode = expectSel.anchorNode ? expectSel.node ? "No anchorNode found"
|
||||
focusNode = expectSel.focusNode ? expectSel.node ? "No focusNode found"
|
||||
anchorOffset = expectSel.anchorOffset ? expectSel.offset ? 0
|
||||
focusOffset = expectSel.focusOffset ? expectSel.offset ? 0
|
||||
|
||||
selection = document.getSelection()
|
||||
|
||||
expect(selection.anchorNode).toBe anchorNode
|
||||
expect(selection.focusNode).toBe focusNode
|
||||
expect(selection.anchorOffset).toBe anchorOffset
|
||||
expect(selection.focusOffset).toBe focusOffset
|
||||
|
||||
getDOM: ->
|
||||
React.findDOMNode(@wrap.refs["ceWrap"].refs["contenteditable"])
|
||||
|
||||
class Wrap extends React.Component
|
||||
@displayName: "wrap"
|
||||
|
||||
constructor: (@props) ->
|
||||
@state = value: @props.initialValue
|
||||
|
||||
render: ->
|
||||
userOnChange = @props.ceProps.onChange ? ->
|
||||
props = _.clone(@props.ceProps)
|
||||
props.onChange = (event) =>
|
||||
userOnChange(event)
|
||||
@onChange(event)
|
||||
props.value = @state.value
|
||||
props.ref = "ceWrap"
|
||||
|
||||
<Contenteditable {...props} />
|
||||
|
||||
onChange: (event) ->
|
||||
@setState value: event.target.value
|
||||
|
||||
componentDidMount: ->
|
||||
@refs.ceWrap.focus()
|
||||
|
||||
module.exports = ContenteditableTestHarness
|
122
spec/components/contenteditable/list-manager-spec.coffee
Normal file
122
spec/components/contenteditable/list-manager-spec.coffee
Normal file
|
@ -0,0 +1,122 @@
|
|||
ContenteditableTestHarness = require './contenteditable-test-harness'
|
||||
|
||||
return unless NylasEnv.inIntegrationSpecMode()
|
||||
|
||||
fdescribe "ListManager", ->
|
||||
beforeEach ->
|
||||
# console.log "--> Before each"
|
||||
@ce = new ContenteditableTestHarness
|
||||
# div = document.querySelector("div[contenteditable]")
|
||||
# console.log div
|
||||
# console.log div?.innerHTML
|
||||
# console.log "Done before each"
|
||||
|
||||
afterEach ->
|
||||
# console.log "<-- After each"
|
||||
@ce.cleanup()
|
||||
|
||||
it "Creates ordered lists", -> waitsForPromise =>
|
||||
@ce.keys(['1', '.', 'Space']).then =>
|
||||
# console.log "Keys typed"
|
||||
@ce.expectHTML "<ol><li></li></ol>"
|
||||
@ce.expectSelection (dom) ->
|
||||
node: dom.querySelectorAll("li")[0]
|
||||
|
||||
ffit "Undoes ordered list creation with backspace", -> waitsForPromise =>
|
||||
@ce.keys(['1', '.', 'Space', 'Back space']).then =>
|
||||
@ce.expectHTML "1. "
|
||||
@ce.expectSelection (dom) ->
|
||||
node: dom.childNodes[0]
|
||||
offset: 3
|
||||
|
||||
it "Creates unordered lists with star", -> waitsForPromise =>
|
||||
@ce.keys(['*', 'Space']).then =>
|
||||
@ce.expectHTML "<ul><li></li></ul>"
|
||||
@ce.expectSelection (dom) ->
|
||||
node: dom.querySelectorAll("li")[0]
|
||||
|
||||
xit "Undoes unordered list creation with backspace", ->
|
||||
aitsForPromise =>
|
||||
@ce.keys(['*', 'Space', 'Back space']).then =>
|
||||
@ce.expectHTML "* "
|
||||
@ce.expectSelection (dom) ->
|
||||
node: dom.childNodes[0]
|
||||
offset: 2
|
||||
|
||||
it "Creates unordered lists with dash", -> waitsForPromise =>
|
||||
@ce.keys(['-', 'Space']).then =>
|
||||
@ce.expectHTML "<ul><li></li></ul>"
|
||||
@ce.expectSelection (dom) ->
|
||||
node: dom.querySelectorAll("li")[0]
|
||||
|
||||
it "Undoes unordered list creation with backspace", ->
|
||||
waitsForPromise =>
|
||||
@ce.keys(['-', 'Space', 'Back space']).then =>
|
||||
@ce.expectHTML "- "
|
||||
@ce.expectSelection (dom) ->
|
||||
node: dom.childNodes[0]
|
||||
offset: 2
|
||||
|
||||
it "create a single item then delete it with backspace", ->
|
||||
waitsForPromise =>
|
||||
@ce.keys(['-', 'Space', 'a', 'Left arrow', 'Back space']).then =>
|
||||
@ce.expectHTML "a"
|
||||
@ce.expectSelection (dom) ->
|
||||
node: dom.childNodes[0]
|
||||
offset: 0
|
||||
|
||||
it "create a single item then delete it with tab", ->
|
||||
waitsForPromise =>
|
||||
@ce.keys(['-', 'Space', 'a', 'Shift', 'Tab']).then =>
|
||||
@ce.expectHTML "a"
|
||||
@ce.expectSelection (dom) -> dom.childNodes[0]
|
||||
node: dom.childNodes[0]
|
||||
offset: 1
|
||||
|
||||
describe "when creating two items in a list", ->
|
||||
beforeEach ->
|
||||
@twoItemKeys = ['-', 'Space', 'a', 'Return', 'b']
|
||||
|
||||
it "creates two items with enter at end", -> waitsForPromise =>
|
||||
@ce.keys(@twoItemKeys).then =>
|
||||
@ce.expectHTML "<ul><li>a</li><li>b</li></ul>"
|
||||
@ce.expectSelection (dom) ->
|
||||
node: dom.querySelectorAll('li')[1].childNodes[0]
|
||||
offset: 1
|
||||
|
||||
xit "backspace from the start of the 1st item outdents", ->
|
||||
@ce.keys @twoItemKeys.concat ['left', 'up', 'backspace']
|
||||
|
||||
xit "backspace from the start of the 2nd item outdents", ->
|
||||
@ce.keys @twoItemKeys.concat ['left', 'backspace']
|
||||
|
||||
xit "shift-tab from the start of the 1st item outdents", ->
|
||||
@ce.keys @twoItemKeys.concat ['left', 'up', 'shift-tab']
|
||||
|
||||
xit "shift-tab from the start of the 2nd item outdents", ->
|
||||
@ce.keys @twoItemKeys.concat ['left', 'shift-tab']
|
||||
|
||||
xit "shift-tab from the end of the 1st item outdents", ->
|
||||
@ce.keys @twoItemKeys.concat ['up', 'shift-tab']
|
||||
|
||||
xit "shift-tab from the end of the 2nd item outdents", ->
|
||||
@ce.keys @twoItemKeys.concat ['shift-tab']
|
||||
|
||||
xit "backspace from the end of the 1st item doesn't outdent", ->
|
||||
@ce.keys @twoItemKeys.concat ['up', 'backspace']
|
||||
|
||||
xit "backspace from the end of the 2nd item doesn't outdent", ->
|
||||
@ce.keys @twoItemKeys.concat ['backspace']
|
||||
|
||||
xdescribe "multi-depth bullets", ->
|
||||
it "creates multi level bullet when tabbed in", ->
|
||||
@ce.keys ['-', ' ', 'a', 'tab']
|
||||
|
||||
it "creates multi level bullet when tabbed in", ->
|
||||
@ce.keys ['-', ' ', 'tab', 'a']
|
||||
|
||||
it "returns to single level bullet on backspace", ->
|
||||
@ce.keys ['-', ' ', 'a', 'tab', 'left', 'backspace']
|
||||
|
||||
it "returns to single level bullet on shift-tab", ->
|
||||
@ce.keys ['-', ' ', 'a', 'tab', 'shift-tab']
|
|
@ -38,16 +38,24 @@ module.exports.runSpecSuite = (specSuite, logFile, logErrors=true) ->
|
|||
N1SpecReporter = require './n1-spec-reporter'
|
||||
reporter = new N1SpecReporter()
|
||||
|
||||
require specSuite
|
||||
NylasEnv.initialize()
|
||||
|
||||
jasmineEnv = jasmine.getEnv()
|
||||
jasmineEnv.addReporter(reporter)
|
||||
jasmineEnv.addReporter(timeReporter)
|
||||
jasmineEnv.setIncludedTags([process.platform])
|
||||
# Tests that run under an integration environment need Spectron to be
|
||||
# asynchronously setup and connected to the Selenium API before proceeding.
|
||||
# Once setup, one can test `NylasEnv.inIntegrationSpecMode()`
|
||||
#
|
||||
# This safely works regardless if Spectron is loaded.
|
||||
NylasEnv.setupSpectron().finally ->
|
||||
require specSuite
|
||||
|
||||
$('body').append $$ -> @div id: 'jasmine-content'
|
||||
jasmineEnv = jasmine.getEnv()
|
||||
jasmineEnv.addReporter(reporter)
|
||||
jasmineEnv.addReporter(timeReporter)
|
||||
jasmineEnv.setIncludedTags([process.platform])
|
||||
|
||||
jasmineEnv.execute()
|
||||
$('body').append $$ -> @div id: 'jasmine-content'
|
||||
|
||||
jasmineEnv.execute()
|
||||
|
||||
disableFocusMethods = ->
|
||||
['fdescribe', 'ffdescribe', 'fffdescribe', 'fit', 'ffit', 'fffit'].forEach (methodName) ->
|
||||
|
|
|
@ -7,7 +7,7 @@ grim = require 'grim'
|
|||
marked = require 'marked'
|
||||
|
||||
sourceMaps = {}
|
||||
formatStackTrace = (spec, message='', stackTrace) ->
|
||||
formatStackTrace = (spec, message='', stackTrace, indent="") ->
|
||||
return stackTrace unless stackTrace
|
||||
|
||||
jasminePattern = /^\s*at\s+.*\(?.*[/\\]jasmine(-[^/\\]*)?\.js:\d+:\d+\)?\s*$/
|
||||
|
@ -32,8 +32,8 @@ formatStackTrace = (spec, message='', stackTrace) ->
|
|||
# Relativize locations to spec directory
|
||||
lines[index] = line.replace("at #{spec.specDirectory}#{path.sep}", 'at ')
|
||||
|
||||
lines = lines.map (line) -> line.trim()
|
||||
lines.join('\n').trim()
|
||||
lines = lines.map (line) -> indent + line.trim()
|
||||
lines.join('\n')
|
||||
|
||||
module.exports =
|
||||
class N1SpecReporter extends View
|
||||
|
@ -60,6 +60,7 @@ class N1SpecReporter extends View
|
|||
@span outlet: 'deprecationStatus', '0 deprecations'
|
||||
@div class: 'deprecation-toggle'
|
||||
@div outlet: 'deprecationList', class: 'deprecation-list'
|
||||
@pre outlet: "plainTextOutput", class: 'plain-text-output'
|
||||
|
||||
startedAt: null
|
||||
runningSpecCount: 0
|
||||
|
@ -92,6 +93,8 @@ class N1SpecReporter extends View
|
|||
else
|
||||
@message.text "#{@failedCount} failures"
|
||||
|
||||
@status.addClass("specs-complete")
|
||||
|
||||
reportSuiteResults: (suite) ->
|
||||
|
||||
reportSpecResults: (spec) ->
|
||||
|
@ -99,6 +102,45 @@ class N1SpecReporter extends View
|
|||
spec.endedAt = Date.now()
|
||||
@specComplete(spec)
|
||||
@updateStatusView(spec)
|
||||
@reportPlainTextSpecResult(spec)
|
||||
|
||||
reportPlainTextSpecResult: (spec) ->
|
||||
str = ""
|
||||
if spec.results().failedCount > 0
|
||||
str += @suiteString(spec) + "\n"
|
||||
indent = @indentationString(spec.suite, 1)
|
||||
stackIndent = @indentationString(spec.suite, 2)
|
||||
|
||||
description = spec.description
|
||||
description = "it #{description}" if description.indexOf('it ') isnt 0
|
||||
str += indent + description + "\n"
|
||||
|
||||
for result in spec.results().getItems()
|
||||
continue if result.passed()
|
||||
str += indent + result.message + "\n"
|
||||
stackTrace = formatStackTrace(spec, result.message, result.trace.stack, stackIndent)
|
||||
str += stackTrace + "\n"
|
||||
str += "\n\n"
|
||||
@plainTextOutput.append(str)
|
||||
|
||||
indentationString: (suite, plus=0) ->
|
||||
rootSuite = suite
|
||||
indentLevel = 0 + plus
|
||||
while rootSuite.parentSuite
|
||||
rootSuite = rootSuite.parentSuite
|
||||
indentLevel += 1
|
||||
return [0...indentLevel].map(-> " ").join("")
|
||||
|
||||
suiteString: (spec) ->
|
||||
descriptions = [spec.suite.description]
|
||||
|
||||
rootSuite = spec.suite
|
||||
while rootSuite.parentSuite
|
||||
indent = @indentationString(rootSuite)
|
||||
descriptions.unshift(indent + rootSuite.description)
|
||||
rootSuite = rootSuite.parentSuite
|
||||
|
||||
descriptions.join("\n")
|
||||
|
||||
reportSpecStarting: (spec) ->
|
||||
@specStarted(spec)
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
# Utils for testing.
|
||||
CSON = require 'season'
|
||||
React = require 'react/addons'
|
||||
KeymapManager = require 'atom-keymap'
|
||||
ReactTestUtils = React.addons.TestUtils
|
||||
|
||||
NylasTestUtils =
|
||||
loadKeymap: (keymapPath) ->
|
||||
|
@ -12,18 +14,30 @@ NylasTestUtils =
|
|||
keymapPath = CSON.resolve("#{resourcePath}/#{keymapPath}")
|
||||
NylasEnv.keymaps.loadKeymap(keymapPath)
|
||||
|
||||
keyPress: (key, target) ->
|
||||
# React's "renderIntoDocument" does not /actually/ attach the component
|
||||
# to the document. It's a sham: http://dragon.ak.fbcdn.net/hphotos-ak-xpf1/t39.3284-6/10956909_1423563877937976_838415501_n.js
|
||||
# The Atom keymap manager doesn't work correctly on elements outside of the
|
||||
# DOM tree, so we need to attach it.
|
||||
unless document.contains(target)
|
||||
parent = target
|
||||
while parent.parentNode?
|
||||
parent = parent.parentNode
|
||||
document.documentElement.appendChild(parent)
|
||||
|
||||
keyDown: (key, target) ->
|
||||
event = KeymapManager.buildKeydownEvent(key, target: target)
|
||||
NylasEnv.keymaps.handleKeyboardEvent(event)
|
||||
|
||||
# React's "renderIntoDocument" does not /actually/ attach the component
|
||||
# to the document. It's a sham: http://dragon.ak.fbcdn.net/hphotos-ak-xpf1/t39.3284-6/10956909_1423563877937976_838415501_n.js
|
||||
# The Atom keymap manager doesn't work correctly on elements outside of the
|
||||
# DOM tree, so we need to attach it.
|
||||
renderIntoDocument: (reactDOM) ->
|
||||
node = ReactTestUtils.renderIntoDocument(reactDOM)
|
||||
$node = React.findDOMNode(node)
|
||||
unless document.body.contains($node)
|
||||
parent = $node
|
||||
while parent.parentNode?
|
||||
parent = parent.parentNode
|
||||
document.body.appendChild(parent)
|
||||
return node
|
||||
|
||||
removeFromDocument: (reactElement) ->
|
||||
$el = React.findDOMNode(reactElement)
|
||||
if document.body.contains($el)
|
||||
for child in Array::slice.call(document.body.childNodes)
|
||||
if child.contains($el)
|
||||
document.body.removeChild(child)
|
||||
return
|
||||
|
||||
module.exports = NylasTestUtils
|
||||
|
|
|
@ -18,7 +18,7 @@ try
|
|||
|
||||
# Show window synchronously so a focusout doesn't fire on input elements
|
||||
# that are focused in the very first spec run.
|
||||
if NylasEnv.getLoadSettings().showSpecsInWindow
|
||||
if not NylasEnv.getLoadSettings().exitWhenDone
|
||||
NylasEnv.getCurrentWindow().show()
|
||||
|
||||
{runSpecSuite} = require './jasmine-helper'
|
||||
|
|
|
@ -1,14 +1,16 @@
|
|||
_ = require 'underscore'
|
||||
_str = require 'underscore.string'
|
||||
fs = require 'fs-plus'
|
||||
path = require 'path'
|
||||
|
||||
require '../src/window'
|
||||
NylasEnv.initialize()
|
||||
NylasEnv.restoreWindowDimensions()
|
||||
|
||||
require 'jasmine-json'
|
||||
require './jasmine-jquery'
|
||||
path = require 'path'
|
||||
_ = require 'underscore'
|
||||
_str = require 'underscore.string'
|
||||
fs = require 'fs-plus'
|
||||
|
||||
Grim = require 'grim'
|
||||
TimeOverride = require './time-override'
|
||||
KeymapManager = require '../src/keymap-manager'
|
||||
|
||||
# FIXME: Remove jquery from this
|
||||
|
@ -50,7 +52,7 @@ jasmine.getEnv().addEqualityTester(_.isEqual) # Use underscore's definition of e
|
|||
if process.env.JANKY_SHA1 and process.platform is 'win32'
|
||||
jasmine.getEnv().defaultTimeoutInterval = 60000
|
||||
else
|
||||
jasmine.getEnv().defaultTimeoutInterval = 1000
|
||||
jasmine.getEnv().defaultTimeoutInterval = 10000
|
||||
|
||||
specPackageName = null
|
||||
specPackagePath = null
|
||||
|
@ -98,13 +100,6 @@ ReactTestUtils.unmountAll = ->
|
|||
React.unmountComponentAtNode(container)
|
||||
ReactElementContainers = []
|
||||
|
||||
# Make Bluebird use setTimeout so that it hooks into our stubs, and you can
|
||||
# advance promises using `advanceClock()`. To avoid breaking any specs that
|
||||
# `dont` manually call advanceClock, call it automatically on the next tick.
|
||||
Promise.setScheduler (fn) ->
|
||||
setTimeout(fn, 0)
|
||||
process.nextTick -> advanceClock(1)
|
||||
|
||||
# So it passes the Utils.isTempId test
|
||||
window.TEST_ACCOUNT_CLIENT_ID = "local-test-account-client-id"
|
||||
window.TEST_ACCOUNT_ID = "test-account-server-id"
|
||||
|
@ -129,19 +124,15 @@ beforeEach ->
|
|||
NylasEnv.styles.restoreSnapshot(styleElementsToRestore)
|
||||
NylasEnv.workspaceViewParentSelector = '#jasmine-content'
|
||||
|
||||
window.resetTimeouts()
|
||||
spyOn(_._, "now").andCallFake -> window.now
|
||||
spyOn(window, "setTimeout").andCallFake window.fakeSetTimeout
|
||||
spyOn(window, "clearTimeout").andCallFake window.fakeClearTimeout
|
||||
spyOn(window, "setInterval").andCallFake window.fakeSetInterval
|
||||
spyOn(window, "clearInterval").andCallFake window.fakeClearInterval
|
||||
|
||||
NylasEnv.packages.packageStates = {}
|
||||
|
||||
serializedWindowState = null
|
||||
|
||||
spyOn(NylasEnv, 'saveSync')
|
||||
|
||||
TimeOverride.resetTime()
|
||||
TimeOverride.enableSpies()
|
||||
|
||||
spy = spyOn(NylasEnv.packages, 'resolvePackagePath').andCallFake (packageName) ->
|
||||
if specPackageName and packageName is specPackageName
|
||||
resolvePackagePath(specPackagePath)
|
||||
|
@ -176,7 +167,6 @@ beforeEach ->
|
|||
"package-with-broken-package-json", "package-with-broken-keymap"]
|
||||
config.set "editor.useShadowDOM", true
|
||||
advanceClock(1000)
|
||||
window.setTimeout.reset()
|
||||
config.load.reset()
|
||||
config.save.reset()
|
||||
|
||||
|
@ -188,6 +178,7 @@ beforeEach ->
|
|||
|
||||
addCustomMatchers(this)
|
||||
|
||||
TimeOverride.resetSpyData()
|
||||
|
||||
original_log = console.log
|
||||
original_warn = console.warn
|
||||
|
@ -266,13 +257,6 @@ jasmine.snapshotDeprecations = ->
|
|||
jasmine.restoreDeprecationsSnapshot = ->
|
||||
Grim.deprecations = deprecationsSnapshot
|
||||
|
||||
jasmine.useRealClock = ->
|
||||
jasmine.unspy(window, 'setTimeout')
|
||||
jasmine.unspy(window, 'clearTimeout')
|
||||
jasmine.unspy(window, 'setInterval')
|
||||
jasmine.unspy(window, 'clearInterval')
|
||||
jasmine.unspy(_._, 'now')
|
||||
|
||||
addCustomMatchers = (spec) ->
|
||||
spec.addMatchers
|
||||
toBeInstanceOf: (expected) ->
|
||||
|
@ -379,85 +363,8 @@ window.waitsForPromise = (args...) ->
|
|||
jasmine.getEnv().currentSpec.fail("Expected promise to be resolved, but it was rejected with #{msg}")
|
||||
moveOn()
|
||||
|
||||
window.resetTimeouts = ->
|
||||
window.now = 0
|
||||
window.timeoutCount = 0
|
||||
window.intervalCount = 0
|
||||
window.timeouts = []
|
||||
window.intervalTimeouts = {}
|
||||
|
||||
window.fakeSetTimeout = (callback, ms) ->
|
||||
id = ++window.timeoutCount
|
||||
window.timeouts.push([id, window.now + ms, callback])
|
||||
id
|
||||
|
||||
window.fakeClearTimeout = (idToClear) ->
|
||||
window.timeouts ?= []
|
||||
window.timeouts = window.timeouts.filter ([id]) -> id != idToClear
|
||||
|
||||
window.fakeSetInterval = (callback, ms) ->
|
||||
id = ++window.intervalCount
|
||||
action = ->
|
||||
callback()
|
||||
window.intervalTimeouts[id] = window.fakeSetTimeout(action, ms)
|
||||
window.intervalTimeouts[id] = window.fakeSetTimeout(action, ms)
|
||||
id
|
||||
|
||||
window.fakeClearInterval = (idToClear) ->
|
||||
window.fakeClearTimeout(@intervalTimeouts[idToClear])
|
||||
|
||||
window.advanceClock = (delta=1) ->
|
||||
window.now += delta
|
||||
callbacks = []
|
||||
|
||||
window.timeouts ?= []
|
||||
window.timeouts = window.timeouts.filter ([id, strikeTime, callback]) ->
|
||||
if strikeTime <= window.now
|
||||
callbacks.push(callback)
|
||||
false
|
||||
else
|
||||
true
|
||||
|
||||
callback() for callback in callbacks
|
||||
|
||||
window.pagePixelPositionForPoint = (editorView, point) ->
|
||||
point = Point.fromObject point
|
||||
top = editorView.renderedLines.offset().top + point.row * editorView.lineHeight
|
||||
left = editorView.renderedLines.offset().left + point.column * editorView.charWidth - editorView.renderedLines.scrollLeft()
|
||||
{ top, left }
|
||||
|
||||
window.tokensText = (tokens) ->
|
||||
_.pluck(tokens, 'value').join('')
|
||||
|
||||
window.setEditorWidthInChars = (editorView, widthInChars, charWidth=editorView.charWidth) ->
|
||||
editorView.width(charWidth * widthInChars + editorView.gutter.outerWidth())
|
||||
$(window).trigger 'resize' # update width of editor view's on-screen lines
|
||||
|
||||
window.setEditorHeightInLines = (editorView, heightInLines, lineHeight=editorView.lineHeight) ->
|
||||
editorView.height(editorView.getEditor().getLineHeightInPixels() * heightInLines)
|
||||
editorView.component?.measureHeightAndWidth()
|
||||
|
||||
$.fn.resultOfTrigger = (type) ->
|
||||
event = $.Event(type)
|
||||
this.trigger(event)
|
||||
event.result
|
||||
|
||||
$.fn.enableKeymap = ->
|
||||
@on 'keydown', (e) ->
|
||||
originalEvent = e.originalEvent ? e
|
||||
Object.defineProperty(originalEvent, 'target', get: -> e.target) unless originalEvent.target?
|
||||
NylasEnv.keymaps.handleKeyboardEvent(originalEvent)
|
||||
not e.originalEvent.defaultPrevented
|
||||
|
||||
$.fn.attachToDom = ->
|
||||
@appendTo($('#jasmine-content')) unless @isOnDom()
|
||||
|
||||
$.fn.simulateDomAttachment = ->
|
||||
$('<html>').append(this)
|
||||
|
||||
$.fn.textInput = (data) ->
|
||||
this.each ->
|
||||
event = document.createEvent('TextEvent')
|
||||
event.initTextEvent('textInput', true, true, window, data)
|
||||
event = $.event.fix(event)
|
||||
$(this).trigger(event)
|
||||
|
|
100
spec/time-override.coffee
Normal file
100
spec/time-override.coffee
Normal file
|
@ -0,0 +1,100 @@
|
|||
_ = require 'underscore'
|
||||
|
||||
# Public: To make specs easier to test, we make all asynchronous behavior
|
||||
# actually synchronous. We do this by overriding all global timeout and
|
||||
# Promise functions.
|
||||
#
|
||||
# You must now manually call `advanceClock()` in order to move the "clock"
|
||||
# forward.
|
||||
class TimeOverride
|
||||
|
||||
@advanceClock = (delta=1) =>
|
||||
@now += delta
|
||||
callbacks = []
|
||||
|
||||
@timeouts ?= []
|
||||
@timeouts = @timeouts.filter ([id, strikeTime, callback]) =>
|
||||
if strikeTime <= @now
|
||||
callbacks.push(callback)
|
||||
false
|
||||
else
|
||||
true
|
||||
|
||||
callback() for callback in callbacks
|
||||
|
||||
@resetTime = =>
|
||||
@now = 0
|
||||
@timeoutCount = 0
|
||||
@intervalCount = 0
|
||||
@timeouts = []
|
||||
@intervalTimeouts = {}
|
||||
@originalPromiseScheduler = null
|
||||
|
||||
@enableSpies = =>
|
||||
window.advanceClock = @advanceClock
|
||||
|
||||
window.originalSetInterval = window.setInterval
|
||||
spyOn(window, "setTimeout").andCallFake @_fakeSetTimeout
|
||||
spyOn(window, "clearTimeout").andCallFake @_fakeClearTimeout
|
||||
spyOn(window, "setInterval").andCallFake @_fakeSetInterval
|
||||
spyOn(window, "clearInterval").andCallFake @_fakeClearInterval
|
||||
spyOn(_._, "now").andCallFake => @now
|
||||
|
||||
# spyOn(Date, "now").andCallFake => @now
|
||||
# spyOn(Date.prototype, "getTime").andCallFake => @now
|
||||
|
||||
@_setPromiseScheduler()
|
||||
|
||||
@_setPromiseScheduler: =>
|
||||
|
||||
# Make Bluebird use setTimeout so that it hooks into our stubs, and you
|
||||
# can advance promises using `advanceClock()`. To avoid breaking any
|
||||
# specs that `dont` manually call advanceClock, call it automatically on
|
||||
# the next tick.
|
||||
@originalPromiseScheduler ?= Promise.setScheduler (fn) =>
|
||||
setTimeout(fn, 0)
|
||||
process.nextTick =>
|
||||
@advanceClock(1)
|
||||
|
||||
@disableSpies = =>
|
||||
window.advanceClock = null
|
||||
|
||||
jasmine.unspy(window, 'setTimeout')
|
||||
jasmine.unspy(window, 'clearTimeout')
|
||||
jasmine.unspy(window, 'setInterval')
|
||||
jasmine.unspy(window, 'clearInterval')
|
||||
|
||||
jasmine.unspy(_._, "now")
|
||||
|
||||
Promise.setScheduler(@originalPromiseScheduler) if @originalPromiseScheduler
|
||||
@originalPromiseScheduler = null
|
||||
|
||||
@resetSpyData = ->
|
||||
window.setTimeout.reset?()
|
||||
window.clearTimeout.reset?()
|
||||
window.setInterval.reset?()
|
||||
window.clearInterval.reset?()
|
||||
Date.now.reset?()
|
||||
Date.prototype.getTime.reset?()
|
||||
|
||||
@_fakeSetTimeout = (callback, ms) =>
|
||||
id = ++@timeoutCount
|
||||
@timeouts.push([id, @now + ms, callback])
|
||||
id
|
||||
|
||||
@_fakeClearTimeout = (idToClear) =>
|
||||
@timeouts ?= []
|
||||
@timeouts = @timeouts.filter ([id]) -> id != idToClear
|
||||
|
||||
@_fakeSetInterval = (callback, ms) =>
|
||||
id = ++@intervalCount
|
||||
action = ->
|
||||
callback()
|
||||
@intervalTimeouts[id] = @_fakeSetTimeout(action, ms)
|
||||
@intervalTimeouts[id] = @_fakeSetTimeout(action, ms)
|
||||
id
|
||||
|
||||
@_fakeClearInterval = (idToClear) =>
|
||||
@_fakeClearTimeout(@intervalTimeouts[idToClear])
|
||||
|
||||
module.exports = TimeOverride
|
53
spec_integration/app-boot-spec.es6
Normal file
53
spec_integration/app-boot-spec.es6
Normal file
|
@ -0,0 +1,53 @@
|
|||
import {N1Launcher} from './integration-helper'
|
||||
|
||||
describe('Nylas Prod Bootup Tests', function() {
|
||||
beforeAll((done)=>{
|
||||
// Boot in dev mode with no arguments
|
||||
this.app = new N1Launcher([]);
|
||||
this.app.mainWindowReady().finally(done);
|
||||
});
|
||||
|
||||
afterAll((done)=> {
|
||||
if (this.app && this.app.isRunning()) {
|
||||
this.app.stop().then(done);
|
||||
} else {
|
||||
done()
|
||||
}
|
||||
});
|
||||
|
||||
it("has main window visible", (done)=> {
|
||||
this.app.client.isWindowVisible()
|
||||
.then((result)=>{ expect(result).toBe(true) })
|
||||
.finally(done)
|
||||
});
|
||||
|
||||
it("has main window focused", (done)=> {
|
||||
this.app.client.isWindowFocused()
|
||||
.then((result)=>{ expect(result).toBe(true) })
|
||||
.finally(done)
|
||||
});
|
||||
|
||||
it("isn't minimized", (done)=> {
|
||||
this.app.client.isWindowMinimized()
|
||||
.then((result)=>{ expect(result).toBe(false) })
|
||||
.finally(done)
|
||||
});
|
||||
|
||||
it("doesn't have the dev tools open", (done)=> {
|
||||
this.app.client.isWindowDevToolsOpened()
|
||||
.then((result)=>{ expect(result).toBe(false) })
|
||||
.finally(done)
|
||||
});
|
||||
|
||||
it("has width", (done)=> {
|
||||
this.app.client.getWindowWidth()
|
||||
.then((result)=>{ expect(result).toBeGreaterThan(0) })
|
||||
.finally(done)
|
||||
});
|
||||
|
||||
it("has height", (done)=> {
|
||||
this.app.client.getWindowHeight()
|
||||
.then((result)=>{ expect(result).toBeGreaterThan(0) })
|
||||
.finally(done)
|
||||
});
|
||||
});
|
12
spec_integration/bootstrap.js
vendored
Normal file
12
spec_integration/bootstrap.js
vendored
Normal file
|
@ -0,0 +1,12 @@
|
|||
// argv[0] = node
|
||||
// argv[1] = jasmine
|
||||
// argv[2] = JASMINE_CONFIG_PATH=./config.json
|
||||
// argv[3] = NYLAS_ROOT_PATH=/path/to/nylas/root
|
||||
|
||||
var babelOptions = require('../static/babelrc.json');
|
||||
require('babel-core/register')(babelOptions);
|
||||
|
||||
jasmine.NYLAS_ROOT_PATH = process.argv[3].split("NYLAS_ROOT_PATH=")[1]
|
||||
jasmine.UNIT_TEST_TIMEOUT = 120*1000;
|
||||
jasmine.BOOT_TIMEOUT = 30*1000;
|
||||
jasmine.DEFAULT_TIMEOUT_INTERVAL = 30*1000
|
36
spec_integration/integrated-unit-spec.es6
Normal file
36
spec_integration/integrated-unit-spec.es6
Normal file
|
@ -0,0 +1,36 @@
|
|||
import {N1Launcher} from './integration-helper'
|
||||
|
||||
// Some unit tests, such as the Contenteditable specs need to be run with
|
||||
// Spectron availble in the environment.
|
||||
fdescribe('Integrated Unit Tests', function() {
|
||||
beforeAll((done)=>{
|
||||
// Boot in dev mode with no arguments
|
||||
this.app = new N1Launcher(["--test=window"]);
|
||||
this.app.start().then(done).catch(done)
|
||||
this.originalTimeoutInterval = jasmine.DEFAULT_TIMEOUT_INTERVAL
|
||||
jasmine.DEFAULT_TIMEOUT_INTERVAL = 5*60*1000 // 5 minutes
|
||||
});
|
||||
|
||||
afterAll((done)=> {
|
||||
jasmine.DEFAULT_TIMEOUT_INTERVAL = this.originalTimeoutInterval
|
||||
if (this.app && this.app.isRunning()) {
|
||||
this.app.stop().then(done);
|
||||
} else {
|
||||
done()
|
||||
}
|
||||
});
|
||||
|
||||
it("Passes all integrated unit tests", (done)=> {
|
||||
var client = this.app.client
|
||||
client.waitForExist(".specs-complete", jasmine.UNIT_TEST_TIMEOUT)
|
||||
.then(()=>{ return client.getHTML(".specs-complete .message") })
|
||||
.then((results)=>{
|
||||
expect(results).toMatch(/0 failures/)
|
||||
}).then(()=>{ return client.getHTML(".plain-text-output") })
|
||||
.then((errorOutput)=>{
|
||||
expect(errorOutput).toBe('<pre class="plain-text-output"></pre>')
|
||||
done()
|
||||
}).catch(done)
|
||||
});
|
||||
|
||||
});
|
83
spec_integration/integration-helper.es6
Normal file
83
spec_integration/integration-helper.es6
Normal file
|
@ -0,0 +1,83 @@
|
|||
import path from 'path'
|
||||
import Promise from 'bluebird'
|
||||
import {Application} from 'spectron';
|
||||
|
||||
class N1Launcher extends Application {
|
||||
constructor(launchArgs = []) {
|
||||
super({
|
||||
path: N1Launcher.electronPath(),
|
||||
args: [jasmine.NYLAS_ROOT_PATH].concat(N1Launcher.defaultNylasArgs()).concat(launchArgs)
|
||||
})
|
||||
}
|
||||
|
||||
mainWindowReady() {
|
||||
// Wrap in a Bluebird promise so we have `.finally on the return`
|
||||
return Promise.resolve(this.start().then(()=>{
|
||||
return N1Launcher.waitUntilMainWindowLoaded(this.client).then((mainWindowId)=>{
|
||||
return this.client.window(mainWindowId)
|
||||
})
|
||||
}));
|
||||
}
|
||||
|
||||
static defaultNylasArgs() {
|
||||
return ["--enable-logging", `--resource-path=${jasmine.NYLAS_ROOT_PATH}`]
|
||||
}
|
||||
|
||||
static electronPath() {
|
||||
nylasRoot = jasmine.NYLAS_ROOT_PATH
|
||||
if (process.platform === "darwin") {
|
||||
return path.join(nylasRoot, "electron", "Electron.app", "Contents", "MacOS", "Electron")
|
||||
} else if (process.platform === "win32") {
|
||||
return path.join(nylasRoot, "electron", "electron.exe")
|
||||
}
|
||||
else if (process.platform === "linux") {
|
||||
return path.join(nylasRoot, "electron", "electron")
|
||||
}
|
||||
else {
|
||||
throw new Error(`Platform ${process.platform} is not supported`)
|
||||
}
|
||||
}
|
||||
|
||||
// We unfortunatley can't just Spectron's `waitUntilWindowLoaded` because
|
||||
// the first window that loads isn't necessarily the main render window (it
|
||||
// could be the work window or others), and once the window is "loaded"
|
||||
// it'll take a while for packages to load, etc. As such we periodically
|
||||
// poll the list of windows to find one that looks like the main loaded
|
||||
// window.
|
||||
//
|
||||
// Returns a promise that resolves with the main window's ID once it's
|
||||
// loaded.
|
||||
static waitUntilMainWindowLoaded(client, lastCheck=0) {
|
||||
var CHECK_EVERY = 1000
|
||||
return new Promise((resolve, reject) => {
|
||||
client.windowHandles().then(({value}) => {
|
||||
return Promise.mapSeries(value, (windowId)=>{
|
||||
return N1Launcher.switchAndCheckForMain(client, windowId)
|
||||
})
|
||||
}).then((mainChecks)=>{
|
||||
for (mainWindowId of mainChecks) {
|
||||
if (mainWindowId) {return resolve(mainWindowId)}
|
||||
}
|
||||
|
||||
var now = Date.now();
|
||||
var delay = Math.max(CHECK_EVERY - (now - lastCheck), 0)
|
||||
setTimeout(()=>{
|
||||
N1Launcher.waitUntilMainWindowLoaded(client, now).then(resolve)
|
||||
}, delay)
|
||||
}).catch((err) => {
|
||||
console.error(err);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Returns false or the window ID of the main window
|
||||
static switchAndCheckForMain(client, windowId) {
|
||||
return client.window(windowId).then(()=>{
|
||||
return client.isExisting(".main-window-loaded").then((exists)=>{
|
||||
if (exists) {return windowId} else {return false}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {N1Launcher}
|
|
@ -9,6 +9,7 @@
|
|||
"test": "jasmine JASMINE_CONFIG_PATH=./config.json"
|
||||
},
|
||||
"dependencies": {
|
||||
"bluebird": "^3.0.5",
|
||||
"babel-core": "^5.8.21",
|
||||
"jasmine": "^2.3.2",
|
||||
"spectron": "^0.34.1"
|
|
@ -1,25 +0,0 @@
|
|||
import {Application} from 'spectron';
|
||||
|
||||
describe('Nylas', ()=> {
|
||||
beforeAll((done)=>{
|
||||
this.app = new Application({
|
||||
path: jasmine.APP_PATH,
|
||||
args: jasmine.APP_ARGS,
|
||||
});
|
||||
this.app.start().then(()=> setTimeout(done, jasmine.BOOT_WAIT));
|
||||
});
|
||||
|
||||
afterEach((done)=> {
|
||||
if (this.app && this.app.isRunning()) {
|
||||
this.app.stop().then(done);
|
||||
}
|
||||
});
|
||||
|
||||
it('boots 4 windows on launch', (done)=> {
|
||||
this.app.client.getWindowCount().then((count)=> {
|
||||
expect(count).toEqual(jasmine.any(Number));
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
6
spectron/bootstrap.js
vendored
6
spectron/bootstrap.js
vendored
|
@ -1,6 +0,0 @@
|
|||
var babelOptions = require('../static/babelrc.json');
|
||||
require('babel-core/register')(babelOptions);
|
||||
jasmine.APP_PATH = process.argv.slice(3)[0].split('APP_PATH=')[1];
|
||||
jasmine.APP_ARGS = process.argv.slice(4)[0].split('APP_ARGS=')[1].split(',');
|
||||
jasmine.DEFAULT_TIMEOUT_INTERVAL = 30000;
|
||||
jasmine.BOOT_WAIT = 15000;
|
|
@ -172,8 +172,11 @@ DOMUtils =
|
|||
# https://developer.mozilla.org/en-US/docs/Web/API/Element/closest
|
||||
# Only Elements (not Text nodes) have the `closest` method
|
||||
closest: (node, selector) ->
|
||||
el = if node instanceof HTMLElement then node else node.parentElement
|
||||
return el.closest(selector)
|
||||
if node instanceof HTMLElement
|
||||
return node.closest(selector)
|
||||
else if node?.parentNode
|
||||
return DOMUtils.closest(node.parentNode, selector)
|
||||
else return null
|
||||
|
||||
closestAtCursor: (selector) ->
|
||||
selection = document.getSelection()
|
||||
|
|
|
@ -223,6 +223,52 @@ class NylasEnvConstructor extends Model
|
|||
window.onbeforeunload = => @_unloading()
|
||||
@_unloadCallbacks = []
|
||||
|
||||
# Some unit tests require access to the Selenium web driver APIs as exposed
|
||||
# by Spectron/Chromedriver. The app must be booted by Spectron as is done in the `run-integration-tests` task. Once Spectron boots the app, it will expose a RESTful API
|
||||
#
|
||||
# The Selenium API spec is here: https://code.google.com/p/selenium/wiki/JsonWireProtocol
|
||||
#
|
||||
# The Node wrapper for that API is provided by webdriver: http://webdriver.io/api.html
|
||||
#
|
||||
# Spectron wraps webdriver (in its `client` property) and adds additional methods: https://github.com/kevinsawicki/spectron
|
||||
#
|
||||
# Spectron requests that Selenium use Chromedriver
|
||||
# https://sites.google.com/a/chromium.org/chromedriver/home to interface
|
||||
# with the app, but points the binary at Electron.
|
||||
#
|
||||
# Since this code here is "inside" the booted process, we have no way of
|
||||
# directly accessing the client from the test runner. However, we can still
|
||||
# connect directly to the Selenium server to control ourself.
|
||||
#
|
||||
# We unfortunately can't create a new session with Selenium, because we
|
||||
# won't be connected to the correct process (us!). Instead we need to
|
||||
# inspect the existing sessions and use the existing one instead.
|
||||
#
|
||||
# We then manually setup the WebDriver session, and add in the extra
|
||||
# spectron APIs via `Spectron.Application::addCommands`.
|
||||
#
|
||||
# http://webdriver.io/api/protocol/windowHandles.html
|
||||
# https://code.google.com/p/selenium/wiki/JsonWireProtocol#/session/:sessionId/element/:id/value
|
||||
setupSpectron: ->
|
||||
options =
|
||||
host: "127.0.0.1"
|
||||
port: 9515
|
||||
SpectronApp = require('spectron').Application
|
||||
@spectron = new SpectronApp
|
||||
WebDriver = require('webdriverio')
|
||||
@spectron.client = new WebDriver.remote(options)
|
||||
@spectron.addCommands()
|
||||
@spectron.client.sessions().then ({value}) =>
|
||||
{sessionId, capabilities} = value[0]
|
||||
@spectron.client.requestHandler.sessionID = sessionId
|
||||
# https://github.com/webdriverio/webdriverio/blob/master/lib/protocol/init.js
|
||||
@spectron.client.sessionID = sessionId
|
||||
@spectron.client.capabilities = capabilities
|
||||
@spectron.client.desiredCapabilities = capabilities
|
||||
|
||||
inIntegrationSpecMode: ->
|
||||
@inSpecMode() and @spectron?.client?.sessionID
|
||||
|
||||
# Start our error reporting to the backend and attach error handlers
|
||||
# to the window and the Bluebird Promise library, converting things
|
||||
# back through the sourcemap as necessary.
|
||||
|
@ -239,7 +285,7 @@ class NylasEnvConstructor extends Model
|
|||
@lastUncaughtError = Array::slice.call(arguments)
|
||||
[message, url, line, column, originalError] = @lastUncaughtError
|
||||
|
||||
# {line, column} = mapSourcePosition({source: url, line, column})
|
||||
{line, column} = mapSourcePosition({source: url, line, column})
|
||||
|
||||
eventObject = {message, url, line, column, originalError}
|
||||
|
||||
|
@ -664,6 +710,7 @@ class NylasEnvConstructor extends Model
|
|||
showRootWindow: ->
|
||||
cover = document.getElementById("application-loading-cover")
|
||||
cover.classList.add('visible')
|
||||
document.body.classList.add("main-window-loaded")
|
||||
@restoreWindowDimensions()
|
||||
@getCurrentWindow().setMinimumSize(875, 500)
|
||||
|
||||
|
|
|
@ -14,6 +14,10 @@ body {
|
|||
line-height: 1.6em;
|
||||
color: #333;
|
||||
|
||||
.plain-text-output {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.list-unstyled {
|
||||
list-style: none;
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue