mirror of
https://github.com/Foundry376/Mailspring.git
synced 2024-11-12 12:40:08 +08:00
c3ecca2692
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`
370 lines
13 KiB
CoffeeScript
370 lines
13 KiB
CoffeeScript
_ = require 'underscore'
|
|
_str = require 'underscore.string'
|
|
fs = require 'fs-plus'
|
|
path = require 'path'
|
|
|
|
require '../src/window'
|
|
NylasEnv.restoreWindowDimensions()
|
|
|
|
require 'jasmine-json'
|
|
require './jasmine-jquery'
|
|
|
|
Grim = require 'grim'
|
|
TimeOverride = require './time-override'
|
|
KeymapManager = require '../src/keymap-manager'
|
|
|
|
# FIXME: Remove jquery from this
|
|
{$} = require '../src/space-pen-extensions'
|
|
|
|
Config = require '../src/config'
|
|
ServiceHub = require 'service-hub'
|
|
pathwatcher = require 'pathwatcher'
|
|
{clipboard} = require 'electron'
|
|
|
|
Account = require "../src/flux/models/account"
|
|
AccountStore = require "../src/flux/stores/account-store"
|
|
Contact = require '../src/flux/models/contact'
|
|
{TaskQueue, ComponentRegistry} = require "nylas-exports"
|
|
|
|
NylasEnv.themes.loadBaseStylesheets()
|
|
NylasEnv.themes.requireStylesheet '../static/jasmine'
|
|
NylasEnv.themes.initialLoadComplete = true
|
|
|
|
NylasEnv.keymaps.loadBundledKeymaps()
|
|
keyBindingsToRestore = NylasEnv.keymaps.getKeyBindings()
|
|
commandsToRestore = NylasEnv.commands.getSnapshot()
|
|
styleElementsToRestore = NylasEnv.styles.getSnapshot()
|
|
|
|
window.addEventListener 'core:close', -> window.close()
|
|
window.addEventListener 'beforeunload', ->
|
|
NylasEnv.storeWindowDimensions()
|
|
NylasEnv.saveSync()
|
|
$('html,body').css('overflow', 'auto')
|
|
|
|
# Allow document.title to be assigned in specs without screwing up spec window title
|
|
documentTitle = null
|
|
Object.defineProperty document, 'title',
|
|
get: -> documentTitle
|
|
set: (title) -> documentTitle = title
|
|
|
|
jasmine.getEnv().addEqualityTester(_.isEqual) # Use underscore's definition of equality for toEqual assertions
|
|
|
|
if process.env.JANKY_SHA1 and process.platform is 'win32'
|
|
jasmine.getEnv().defaultTimeoutInterval = 60000
|
|
else
|
|
jasmine.getEnv().defaultTimeoutInterval = 10000
|
|
|
|
specPackageName = null
|
|
specPackagePath = null
|
|
isCoreSpec = false
|
|
|
|
{specDirectory, resourcePath} = NylasEnv.getLoadSettings()
|
|
|
|
if specDirectory
|
|
specPackagePath = path.resolve(specDirectory, '..')
|
|
try
|
|
specPackageName = JSON.parse(fs.readFileSync(path.join(specPackagePath, 'package.json')))?.name
|
|
|
|
isCoreSpec = specDirectory == fs.realpathSync(__dirname)
|
|
|
|
# Override React.addons.TestUtils.renderIntoDocument so that
|
|
# we can remove all the created elements after the test completes.
|
|
React = require "react/addons"
|
|
ReactTestUtils = React.addons.TestUtils
|
|
ReactTestUtils.scryRenderedComponentsWithTypeAndProps = (root, type, props) ->
|
|
if not root then throw new Error("Must supply a root to scryRenderedComponentsWithTypeAndProps")
|
|
_.compact _.map ReactTestUtils.scryRenderedComponentsWithType(root, type), (el) ->
|
|
if _.isEqual(_.pick(el.props, Object.keys(props)), props)
|
|
return el
|
|
else
|
|
return false
|
|
|
|
ReactTestUtils.scryRenderedDOMComponentsWithAttr = (root, attrName, attrValue) ->
|
|
ReactTestUtils.findAllInRenderedTree root, (inst) ->
|
|
inst.props[attrName] and (!attrValue or inst.props[attrName] is attrValue)
|
|
|
|
ReactTestUtils.findRenderedDOMComponentWithAttr = (root, attrName, attrValue) ->
|
|
all = ReactTestUtils.scryRenderedDOMComponentsWithAttr(root, attrName, attrValue)
|
|
if all.length is not 1
|
|
throw new Error("Did not find exactly one match for data attribute: #{attrName} with value: #{attrValue}")
|
|
all[0]
|
|
|
|
ReactElementContainers = []
|
|
ReactTestUtils.renderIntoDocument = (element) ->
|
|
container = document.createElement('div')
|
|
ReactElementContainers.push(container)
|
|
React.render(element, container)
|
|
|
|
ReactTestUtils.unmountAll = ->
|
|
for container in ReactElementContainers
|
|
React.unmountComponentAtNode(container)
|
|
ReactElementContainers = []
|
|
|
|
# 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"
|
|
window.TEST_ACCOUNT_EMAIL = "tester@nylas.com"
|
|
window.TEST_ACCOUNT_NAME = "Nylas Test"
|
|
|
|
beforeEach ->
|
|
NylasEnv.testOrganizationUnit = null
|
|
Grim.clearDeprecations() if isCoreSpec
|
|
ComponentRegistry._clear()
|
|
global.localStorage.clear()
|
|
|
|
TaskQueue._queue = []
|
|
TaskQueue._completed = []
|
|
TaskQueue._onlineStatus = true
|
|
|
|
$.fx.off = true
|
|
documentTitle = null
|
|
NylasEnv.packages.serviceHub = new ServiceHub
|
|
NylasEnv.keymaps.keyBindings = _.clone(keyBindingsToRestore)
|
|
NylasEnv.commands.restoreSnapshot(commandsToRestore)
|
|
NylasEnv.styles.restoreSnapshot(styleElementsToRestore)
|
|
NylasEnv.workspaceViewParentSelector = '#jasmine-content'
|
|
|
|
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)
|
|
else
|
|
resolvePackagePath(packageName)
|
|
resolvePackagePath = _.bind(spy.originalValue, NylasEnv.packages)
|
|
|
|
# prevent specs from modifying N1's menus
|
|
spyOn(NylasEnv.menu, 'sendToBrowserProcess')
|
|
|
|
# Log in a fake user
|
|
spyOn(AccountStore, 'current').andCallFake ->
|
|
new Account
|
|
provider: "gmail"
|
|
name: TEST_ACCOUNT_NAME
|
|
emailAddress: TEST_ACCOUNT_EMAIL
|
|
organizationUnit: NylasEnv.testOrganizationUnit
|
|
clientId: TEST_ACCOUNT_CLIENT_ID
|
|
serverId: TEST_ACCOUNT_ID
|
|
|
|
# reset config before each spec; don't load or save from/to `config.json`
|
|
spyOn(Config::, 'load')
|
|
spyOn(Config::, 'save')
|
|
config = new Config({resourcePath, configDirPath: NylasEnv.getConfigDirPath()})
|
|
NylasEnv.config = config
|
|
NylasEnv.loadConfig()
|
|
config.set "core.destroyEmptyPanes", false
|
|
config.set "editor.fontFamily", "Courier"
|
|
config.set "editor.fontSize", 16
|
|
config.set "editor.autoIndent", false
|
|
config.set "core.disabledPackages", ["package-that-throws-an-exception",
|
|
"package-with-broken-package-json", "package-with-broken-keymap"]
|
|
config.set "editor.useShadowDOM", true
|
|
advanceClock(1000)
|
|
config.load.reset()
|
|
config.save.reset()
|
|
|
|
spyOn(pathwatcher.File.prototype, "detectResurrectionAfterDelay").andCallFake -> @detectResurrection()
|
|
|
|
clipboardContent = 'initial clipboard content'
|
|
spyOn(clipboard, 'writeText').andCallFake (text) -> clipboardContent = text
|
|
spyOn(clipboard, 'readText').andCallFake -> clipboardContent
|
|
|
|
addCustomMatchers(this)
|
|
|
|
TimeOverride.resetSpyData()
|
|
|
|
original_log = console.log
|
|
original_warn = console.warn
|
|
original_error = console.error
|
|
|
|
afterEach ->
|
|
|
|
if console.log isnt original_log
|
|
console.log = original_log
|
|
if console.warn isnt original_warn
|
|
console.warn = original_warn
|
|
if console.error isnt original_error
|
|
console.error = original_error
|
|
|
|
NylasEnv.packages.deactivatePackages()
|
|
NylasEnv.menu.template = []
|
|
|
|
NylasEnv.themes.removeStylesheet('global-editor-styles')
|
|
|
|
delete NylasEnv.state?.packageStates
|
|
|
|
$('#jasmine-content').empty() unless window.debugContent
|
|
|
|
ReactTestUtils.unmountAll()
|
|
|
|
jasmine.unspy(NylasEnv, 'saveSync')
|
|
ensureNoPathSubscriptions()
|
|
waits(0) # yield to ui thread to make screen update more frequently
|
|
|
|
ensureNoPathSubscriptions = ->
|
|
watchedPaths = pathwatcher.getWatchedPaths()
|
|
pathwatcher.closeAllWatchers()
|
|
if watchedPaths.length > 0
|
|
throw new Error("Leaking subscriptions for paths: " + watchedPaths.join(", "))
|
|
|
|
ensureNoDeprecatedFunctionsCalled = ->
|
|
deprecations = Grim.getDeprecations()
|
|
if deprecations.length > 0
|
|
originalPrepareStackTrace = Error.prepareStackTrace
|
|
Error.prepareStackTrace = (error, stack) ->
|
|
output = []
|
|
for deprecation in deprecations
|
|
output.push "#{deprecation.originName} is deprecated. #{deprecation.message}"
|
|
output.push _str.repeat("-", output[output.length - 1].length)
|
|
for stack in deprecation.getStacks()
|
|
for {functionName, location} in stack
|
|
output.push "#{functionName} -- #{location}"
|
|
output.push ""
|
|
output.join("\n")
|
|
|
|
error = new Error("Deprecated function(s) #{deprecations.map(({originName}) -> originName).join ', '}) were called.")
|
|
error.stack
|
|
Error.prepareStackTrace = originalPrepareStackTrace
|
|
|
|
throw error
|
|
|
|
emitObject = jasmine.StringPrettyPrinter.prototype.emitObject
|
|
jasmine.StringPrettyPrinter.prototype.emitObject = (obj) ->
|
|
if obj.inspect
|
|
@append obj.inspect()
|
|
else
|
|
emitObject.call(this, obj)
|
|
|
|
jasmine.unspy = (object, methodName) ->
|
|
throw new Error("Not a spy") unless object[methodName].hasOwnProperty('originalValue')
|
|
object[methodName] = object[methodName].originalValue
|
|
|
|
jasmine.attachToDOM = (element) ->
|
|
jasmineContent = document.querySelector('#jasmine-content')
|
|
jasmineContent.appendChild(element) unless jasmineContent.contains(element)
|
|
|
|
deprecationsSnapshot = null
|
|
jasmine.snapshotDeprecations = ->
|
|
deprecationsSnapshot = _.clone(Grim.deprecations)
|
|
|
|
jasmine.restoreDeprecationsSnapshot = ->
|
|
Grim.deprecations = deprecationsSnapshot
|
|
|
|
addCustomMatchers = (spec) ->
|
|
spec.addMatchers
|
|
toBeInstanceOf: (expected) ->
|
|
notText = if @isNot then " not" else ""
|
|
this.message = => "Expected #{jasmine.pp(@actual)} to#{notText} be instance of #{expected.name} class"
|
|
@actual instanceof expected
|
|
|
|
toHaveLength: (expected) ->
|
|
if not @actual?
|
|
this.message = => "Expected object #{@actual} has no length method"
|
|
false
|
|
else
|
|
notText = if @isNot then " not" else ""
|
|
this.message = => "Expected object with length #{@actual.length} to#{notText} have length #{expected}"
|
|
@actual.length == expected
|
|
|
|
toExistOnDisk: (expected) ->
|
|
notText = this.isNot and " not" or ""
|
|
@message = -> return "Expected path '" + @actual + "'" + notText + " to exist."
|
|
fs.existsSync(@actual)
|
|
|
|
toHaveFocus: ->
|
|
notText = this.isNot and " not" or ""
|
|
if not document.hasFocus()
|
|
console.error "Specs will fail because the Dev Tools have focus. To fix this close the Dev Tools or click the spec runner."
|
|
|
|
@message = -> return "Expected element '" + @actual + "' or its descendants" + notText + " to have focus."
|
|
element = @actual
|
|
element = element.get(0) if element.jquery
|
|
element is document.activeElement or element.contains(document.activeElement)
|
|
|
|
toShow: ->
|
|
notText = if @isNot then " not" else ""
|
|
element = @actual
|
|
element = element.get(0) if element.jquery
|
|
@message = -> return "Expected element '#{element}' or its descendants#{notText} to show."
|
|
element.style.display in ['block', 'inline-block', 'static', 'fixed']
|
|
|
|
window.keyIdentifierForKey = (key) ->
|
|
if key.length > 1 # named key
|
|
key
|
|
else
|
|
charCode = key.toUpperCase().charCodeAt(0)
|
|
"U+00" + charCode.toString(16)
|
|
|
|
window.keydownEvent = (key, properties={}) ->
|
|
originalEventProperties = {}
|
|
originalEventProperties.ctrl = properties.ctrlKey
|
|
originalEventProperties.alt = properties.altKey
|
|
originalEventProperties.shift = properties.shiftKey
|
|
originalEventProperties.cmd = properties.metaKey
|
|
originalEventProperties.target = properties.target?[0] ? properties.target
|
|
originalEventProperties.which = properties.which
|
|
originalEvent = KeymapManager.keydownEvent(key, originalEventProperties)
|
|
properties = $.extend({originalEvent}, properties)
|
|
$.Event("keydown", properties)
|
|
|
|
window.mouseEvent = (type, properties) ->
|
|
if properties.point
|
|
{point, editorView} = properties
|
|
{top, left} = @pagePixelPositionForPoint(editorView, point)
|
|
properties.pageX = left + 1
|
|
properties.pageY = top + 1
|
|
properties.originalEvent ?= {detail: 1}
|
|
$.Event type, properties
|
|
|
|
window.clickEvent = (properties={}) ->
|
|
window.mouseEvent("click", properties)
|
|
|
|
window.mousedownEvent = (properties={}) ->
|
|
window.mouseEvent('mousedown', properties)
|
|
|
|
window.mousemoveEvent = (properties={}) ->
|
|
window.mouseEvent('mousemove', properties)
|
|
|
|
# See docs/writing-specs.md
|
|
window.waitsForPromise = (args...) ->
|
|
if args.length > 1
|
|
{ shouldReject, timeout } = args[0]
|
|
else
|
|
shouldReject = false
|
|
fn = _.last(args)
|
|
|
|
window.waitsFor timeout, (moveOn) ->
|
|
promise = fn()
|
|
# Keep in mind we can't check `promise instanceof Promise` because parts of
|
|
# the app still use other Promise libraries Just see if it looks
|
|
# promise-like.
|
|
if not promise or not promise.then
|
|
jasmine.getEnv().currentSpec.fail("Expected callback to return a promise-like object, but it returned #{promise}")
|
|
moveOn()
|
|
else if shouldReject
|
|
promise.catch(moveOn)
|
|
promise.then ->
|
|
jasmine.getEnv().currentSpec.fail("Expected promise to be rejected, but it was resolved")
|
|
moveOn()
|
|
else
|
|
promise.then(moveOn)
|
|
promise.catch (error) ->
|
|
# I don't know what `pp` does, but for standard `new Error` objects,
|
|
# it sometimes returns "{ }". Catch this case and fall through to toString()
|
|
msg = jasmine.pp(error)
|
|
msg = error.toString() if msg is "{ }"
|
|
jasmine.getEnv().currentSpec.fail("Expected promise to be resolved, but it was rejected with #{msg}")
|
|
moveOn()
|
|
|
|
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 }
|