require 'jasmine-json' require '../src/window' require '../vendor/jasmine-jquery' path = require 'path' _ = require 'underscore-plus' fs = require 'fs-plus' Grim = require 'grim' pathwatcher = require 'pathwatcher' FindParentDir = require 'find-parent-dir' {CompositeDisposable} = require 'event-kit' TextEditor = require '../src/text-editor' TextEditorElement = require '../src/text-editor-element' TextMateLanguageMode = require '../src/text-mate-language-mode' {clipboard} = require 'electron' jasmineStyle = document.createElement('style') jasmineStyle.textContent = atom.themes.loadStylesheet(atom.themes.resolveStylesheet('../static/jasmine')) document.head.appendChild(jasmineStyle) fixturePackagesPath = path.resolve(__dirname, './fixtures/packages') atom.packages.packageDirPaths.unshift(fixturePackagesPath) document.querySelector('html').style.overflow = 'auto' document.body.style.overflow = 'auto' Set.prototype.jasmineToString = -> result = "Set {" first = true @forEach (element) -> result += ", " unless first result += element.toString() first = false result + "}" Set.prototype.isEqual = (other) -> if other instanceof Set return false if @size isnt other.size values = @values() until (next = values.next()).done return false unless other.has(next.value) true else false jasmine.getEnv().addEqualityTester(_.isEqual) # Use underscore's definition of equality for toEqual assertions if process.env.CI jasmine.getEnv().defaultTimeoutInterval = 60000 else jasmine.getEnv().defaultTimeoutInterval = 5000 {testPaths} = atom.getLoadSettings() if specPackagePath = FindParentDir.sync(testPaths[0], 'package.json') packageMetadata = require(path.join(specPackagePath, 'package.json')) specPackageName = packageMetadata.name if specDirectory = FindParentDir.sync(testPaths[0], 'fixtures') specProjectPath = path.join(specDirectory, 'fixtures') else specProjectPath = require('os').tmpdir() beforeEach -> # Do not clobber recent project history spyOn(Object.getPrototypeOf(atom.history), 'saveState').andReturn(Promise.resolve()) atom.project.setPaths([specProjectPath]) window.resetTimeouts() spyOn(_._, "now").andCallFake -> window.now spyOn(Date, 'now').andCallFake(-> window.now) spyOn(window, "setTimeout").andCallFake window.fakeSetTimeout spyOn(window, "clearTimeout").andCallFake window.fakeClearTimeout spy = spyOn(atom.packages, 'resolvePackagePath').andCallFake (packageName) -> if specPackageName and packageName is specPackageName resolvePackagePath(specPackagePath) else resolvePackagePath(packageName) resolvePackagePath = _.bind(spy.originalValue, atom.packages) # prevent specs from modifying Atom's menus spyOn(atom.menu, 'sendToBrowserProcess') # reset config before each spec atom.config.set "core.destroyEmptyPanes", false atom.config.set "editor.fontFamily", "Courier" atom.config.set "editor.fontSize", 16 atom.config.set "editor.autoIndent", false atom.config.set "core.disabledPackages", ["package-that-throws-an-exception", "package-with-broken-package-json", "package-with-broken-keymap"] advanceClock(1000) window.setTimeout.reset() # make editor display updates synchronous TextEditorElement::setUpdatedSynchronously(true) spyOn(pathwatcher.File.prototype, "detectResurrectionAfterDelay").andCallFake -> @detectResurrection() spyOn(TextEditor.prototype, "shouldPromptToSave").andReturn false # make tokenization synchronous TextMateLanguageMode.prototype.chunkSize = Infinity spyOn(TextMateLanguageMode.prototype, "tokenizeInBackground").andCallFake -> @tokenizeNextChunk() # Without this spy, TextEditor.onDidTokenize callbacks would not be called # after the buffer's language mode changed, because by the time the editor # called its new language mode's onDidTokenize method, the language mode # would already be fully tokenized. spyOn(TextEditor.prototype, "onDidTokenize").andCallFake (callback) -> new CompositeDisposable( @emitter.on("did-tokenize", callback), @onDidChangeGrammar => languageMode = @buffer.getLanguageMode() if languageMode.tokenizeInBackground?.originalValue callback() ) clipboardContent = 'initial clipboard content' spyOn(clipboard, 'writeText').andCallFake (text) -> clipboardContent = text spyOn(clipboard, 'readText').andCallFake -> clipboardContent addCustomMatchers(this) afterEach -> ensureNoDeprecatedFunctionCalls() ensureNoDeprecatedStylesheets() waitsForPromise -> atom.reset() runs -> document.getElementById('jasmine-content').innerHTML = '' unless window.debugContent warnIfLeakingPathSubscriptions() waits(0) # yield to ui thread to make screen update more frequently warnIfLeakingPathSubscriptions = -> watchedPaths = pathwatcher.getWatchedPaths() if watchedPaths.length > 0 console.error("WARNING: Leaking subscriptions for paths: " + watchedPaths.join(", ")) pathwatcher.closeAllWatchers() ensureNoDeprecatedFunctionCalls = -> deprecations = _.clone(Grim.getDeprecations()) Grim.clearDeprecations() 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 _.multiplyString("-", 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 ensureNoDeprecatedStylesheets = -> deprecations = _.clone(atom.styles.getDeprecations()) atom.styles.clearDeprecations() for sourcePath, deprecation of deprecations title = if sourcePath isnt 'undefined' "Deprecated stylesheet at '#{sourcePath}':" else "Deprecated stylesheet:" throw new Error("#{title}\n#{deprecation.message}") 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) grimDeprecationsSnapshot = null stylesDeprecationsSnapshot = null jasmine.snapshotDeprecations = -> grimDeprecationsSnapshot = _.clone(Grim.deprecations) stylesDeprecationsSnapshot = _.clone(atom.styles.deprecationsBySourcePath) jasmine.restoreDeprecationsSnapshot = -> Grim.deprecations = grimDeprecationsSnapshot atom.styles.deprecationsBySourcePath = stylesDeprecationsSnapshot jasmine.useRealClock = -> jasmine.unspy(window, 'setTimeout') jasmine.unspy(window, 'clearTimeout') jasmine.unspy(_._, 'now') jasmine.unspy(Date, 'now') # The clock is halfway mocked now in a sad and terrible way... only setTimeout # and clearTimeout are included. This method will also include setInterval. We # would do this everywhere if didn't cause us to break a bunch of package tests. jasmine.useMockClock = -> spyOn(window, 'setInterval').andCallFake(fakeSetInterval) spyOn(window, 'clearInterval').andCallFake(fakeClearInterval) addCustomMatchers = (spec) -> spec.addMatchers toBeInstanceOf: (expected) -> beOrNotBe = if @isNot then "not be" else "be" this.message = => "Expected #{jasmine.pp(@actual)} to #{beOrNotBe} instance of #{expected.name} class" @actual instanceof expected toHaveLength: (expected) -> if not @actual? this.message = => "Expected object #{@actual} has no length method" false else haveOrNotHave = if @isNot then "not have" else "have" this.message = => "Expected object with length #{@actual.length} to #{haveOrNotHave} length #{expected}" @actual.length is expected toExistOnDisk: (expected) -> toOrNotTo = this.isNot and "not to" or "to" @message = -> return "Expected path '#{@actual}' #{toOrNotTo} exist." fs.existsSync(@actual) toHaveFocus: -> toOrNotTo = this.isNot and "not to" or "to" 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 #{toOrNotTo} have focus." element = @actual element = element.get(0) if element.jquery element is document.activeElement or element.contains(document.activeElement) toShow: -> toOrNotTo = this.isNot and "not to" or "to" element = @actual element = element.get(0) if element.jquery @message = -> return "Expected element '#{element}' or its descendants #{toOrNotTo} show." element.style.display in ['block', 'inline-block', 'static', 'fixed'] toEqualPath: (expected) -> actualPath = path.normalize(@actual) expectedPath = path.normalize(expected) @message = -> return "Expected path '#{actualPath}' to be equal to '#{expectedPath}'." actualPath is expectedPath window.waitsForPromise = (args...) -> label = null if args.length > 1 {shouldReject, timeout, label} = args[0] else shouldReject = false label ?= 'promise to be resolved or rejected' fn = _.last(args) window.waitsFor label, timeout, (moveOn) -> promise = fn() if shouldReject promise.catch.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.call promise, (error) -> jasmine.getEnv().currentSpec.fail("Expected promise to be resolved, but it was rejected with: #{error?.message} #{jasmine.pp(error)}") moveOn() window.resetTimeouts = -> window.now = 0 window.timeoutCount = 0 window.intervalCount = 0 window.timeouts = [] window.intervalTimeouts = {} window.fakeSetTimeout = (callback, ms=0) -> id = ++window.timeoutCount window.timeouts.push([id, window.now + ms, callback]) id window.fakeClearTimeout = (idToClear) -> window.timeouts = window.timeouts.filter ([id]) -> id isnt 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.filter ([id, strikeTime, callback]) -> if strikeTime <= window.now callbacks.push(callback) false else true callback() for callback in callbacks exports.mockLocalStorage = -> items = {} spyOn(global.localStorage, 'setItem').andCallFake (key, item) -> items[key] = item.toString(); undefined spyOn(global.localStorage, 'getItem').andCallFake (key) -> items[key] ? null spyOn(global.localStorage, 'removeItem').andCallFake (key) -> delete items[key]; undefined