diff --git a/build/resources/asar-ordering-hint.txt b/build/resources/asar-ordering-hint.txt index fcf891f41..40c896989 100644 --- a/build/resources/asar-ordering-hint.txt +++ b/build/resources/asar-ordering-hint.txt @@ -3094,7 +3094,6 @@ 2998273: node_modules/theorist/lib/sequence.js 3004878: node_modules/coffeestack/index.js 3010047: src/window-event-handler.js -3033233: node_modules/space-pen/lib/space-pen.js 3054355: src/styles-element.js 1995661: src/flux/models/utils.js 3061775: src/flux/errors.js @@ -4562,7 +4561,6 @@ 2998273: node_modules/theorist/lib/sequence.js 3004878: node_modules/coffeestack/index.js 3010047: src/window-event-handler.js -3033233: node_modules/space-pen/lib/space-pen.js 3054355: src/styles-element.js 1995661: src/flux/models/utils.js 3061775: src/flux/errors.js diff --git a/build/tasks/docs-build-task.coffee b/build/tasks/docs-build-task.coffee index 91efeb608..aa5305230 100644 --- a/build/tasks/docs-build-task.coffee +++ b/build/tasks/docs-build-task.coffee @@ -8,10 +8,6 @@ _ = require 'underscore' donna = require 'donna' tello = require 'tello' -moduleBlacklist = [ - 'space-pen' -] - module.exports = (grunt) -> {cp, mkdir, rm} = require('./task-helpers')(grunt) @@ -23,7 +19,6 @@ module.exports = (grunt) -> return false if modulePath.match(/node_modules/g) # Don't traverse blacklisted packages (that have docs, but we don't want to include) - return false if path.basename(modulePath) in moduleBlacklist return true unless path.basename(modulePath) is 'package.json' return true unless fs.isFileSync(modulePath) diff --git a/package.json b/package.json index a048912fc..e43f577bd 100644 --- a/package.json +++ b/package.json @@ -67,7 +67,6 @@ "season": "^5.1", "semver": "^4.2", "source-map-support": "^0.3.2", - "space-pen": "3.8.2", "spellchecker": "3.2.3", "temp": "^0.8", "underscore": "^1.8", diff --git a/spec/jasmine-helper.coffee b/spec/jasmine-helper.coffee index 9612c154e..97c397f9b 100644 --- a/spec/jasmine-helper.coffee +++ b/spec/jasmine-helper.coffee @@ -2,8 +2,6 @@ fs = require 'fs' {remote} = require 'electron' module.exports.runSpecSuite = (specSuite, logFile, logErrors=true) -> - {$, $$} = require '../src/space-pen-extensions' - window[key] = value for key, value of require './jasmine' {TerminalReporter} = require 'jasmine-tagged' @@ -21,8 +19,7 @@ module.exports.runSpecSuite = (specSuite, logFile, logErrors=true) -> remote.process.stdout.write(str) if NylasEnv.getLoadSettings().showSpecsInWindow - N1SpecReporter = require './n1-spec-reporter' - reporter = new N1SpecReporter() + reporter = require './n1-spec-reporter' else if NylasEnv.getLoadSettings().exitWhenDone reporter = new TerminalReporter color: true @@ -38,8 +35,7 @@ module.exports.runSpecSuite = (specSuite, logFile, logErrors=true) -> else NylasEnv.exit(0) else - N1SpecReporter = require './n1-spec-reporter' - reporter = new N1SpecReporter() + reporter = require './n1-spec-reporter' NylasEnv.initialize() @@ -50,7 +46,9 @@ module.exports.runSpecSuite = (specSuite, logFile, logErrors=true) -> jasmineEnv.addReporter(timeReporter) jasmineEnv.setIncludedTags([process.platform]) - $('body').append $$ -> @div id: 'jasmine-content' + div = document.createElement('div') + div.id = 'jasmine-content' + document.body.appendChild(div) jasmineEnv.execute() diff --git a/spec/jasmine-jquery.js b/spec/jasmine-jquery.js deleted file mode 100644 index ef2731784..000000000 --- a/spec/jasmine-jquery.js +++ /dev/null @@ -1,181 +0,0 @@ -(function(jQuery) { - -jasmine.JQuery = function() {}; - -jasmine.JQuery.browserTagCaseIndependentHtml = function(html) { - return jQuery('
').append(html).html(); -}; - -jasmine.JQuery.elementToString = function(element) { - return jQuery('
').append(element.clone()).html(); -}; - -jasmine.JQuery.matchersClass = {}; - -(function(namespace) { - var data = { - spiedEvents: {}, - handlers: [] - }; - - namespace.events = { - spyOn: function(selector, eventName) { - var handler = function(e) { - data.spiedEvents[[selector, eventName]] = e; - }; - jQuery(selector).bind(eventName, handler); - data.handlers.push(handler); - }, - - wasTriggered: function(selector, eventName) { - return !!(data.spiedEvents[[selector, eventName]]); - }, - - cleanUp: function() { - data.spiedEvents = {}; - data.handlers = []; - } - } -})(jasmine.JQuery); - -(function(){ - var jQueryMatchers = { - toHaveClass: function(className) { - return this.actual.hasClass(className); - }, - - toBeVisible: function() { - return this.actual.is(':visible'); - }, - - toBeHidden: function() { - return this.actual.is(':hidden'); - }, - - toBeSelected: function() { - return this.actual.is(':selected'); - }, - - toBeChecked: function() { - return this.actual.is(':checked'); - }, - - toBeEmpty: function() { - return this.actual.is(':empty'); - }, - - toExist: function() { - return this.actual.size() > 0; - }, - - toHaveAttr: function(attributeName, expectedAttributeValue) { - return hasProperty(this.actual.attr(attributeName), expectedAttributeValue); - }, - - toHaveId: function(id) { - return this.actual.attr('id') == id; - }, - - toHaveHtml: function(html) { - return this.actual.html() == jasmine.JQuery.browserTagCaseIndependentHtml(html); - }, - - toHaveText: function(text) { - if (text && jQuery.isFunction(text.test)) { - return text.test(this.actual.text()); - } else { - return this.actual.text() == text; - } - }, - - toHaveValue: function(value) { - return this.actual.val() == value; - }, - - toHaveData: function(key, expectedValue) { - return hasProperty(this.actual.data(key), expectedValue); - }, - - toMatchSelector: function(selector) { - return this.actual.is(selector); - }, - - toContain: function(selector) { - return this.actual.find(selector).size() > 0; - }, - - toBeDisabled: function(selector){ - return this.actual.is(':disabled'); - }, - - // tests the existence of a specific event binding - toHandle: function(eventName) { - var events = this.actual.data("events"); - return events && events[eventName].length > 0; - }, - - // tests the existence of a specific event binding + handler - toHandleWith: function(eventName, eventHandler) { - var stack = this.actual.data("events")[eventName]; - var i; - for (i = 0; i < stack.length; i++) { - if (stack[i].handler == eventHandler) { - return true; - } - } - return false; - } - }; - - var hasProperty = function(actualValue, expectedValue) { - if (expectedValue === undefined) { - return actualValue !== undefined; - } - return actualValue == expectedValue; - }; - - var bindMatcher = function(methodName) { - var builtInMatcher = jasmine.Matchers.prototype[methodName]; - - jasmine.JQuery.matchersClass[methodName] = function() { - if (this.actual instanceof HTMLElement) { - this.actual = jQuery(this.actual); - } - if (this.actual && this.actual.jquery) { - var result = jQueryMatchers[methodName].apply(this, arguments); - this.actual = jasmine.JQuery.elementToString(this.actual); - return result; - } - - if (builtInMatcher) { - return builtInMatcher.apply(this, arguments); - } - - return false; - }; - }; - - for(var methodName in jQueryMatchers) { - bindMatcher(methodName); - } -})(); - -beforeEach(function() { - this.addMatchers(jasmine.JQuery.matchersClass); - this.addMatchers({ - toHaveBeenTriggeredOn: function(selector) { - this.message = function() { - return [ - "Expected event " + this.actual + " to have been triggered on" + selector, - "Expected event " + this.actual + " not to have been triggered on" + selector - ]; - }; - return jasmine.JQuery.events.wasTriggered(selector, this.actual); - } - }) -}); - -afterEach(function() { - jasmine.JQuery.events.cleanUp(); -}); -})(require('../src/space-pen-extensions').jQuery); diff --git a/spec/n1-spec-reporter.cjsx b/spec/n1-spec-reporter.cjsx new file mode 100644 index 000000000..b0a63b52f --- /dev/null +++ b/spec/n1-spec-reporter.cjsx @@ -0,0 +1,315 @@ +path = require 'path' +_ = require 'underscore' +_str = require 'underscore.string' +{convertStackTrace} = require 'coffeestack' +React = require 'react' +ReactDOM = require 'react-dom' +grim = require 'grim' +marked = require 'marked' + +sourceMaps = {} +formatStackTrace = (spec, message='', stackTrace, indent="") -> + return stackTrace unless stackTrace + + jasminePattern = /^\s*at\s+.*\(?.*[/\\]jasmine(-[^/\\]*)?\.js:\d+:\d+\)?\s*$/ + firstJasmineLinePattern = /^\s*at [/\\].*[/\\]jasmine(-[^/\\]*)?\.js:\d+:\d+\)?\s*$/ + convertedLines = [] + for line in stackTrace.split('\n') + convertedLines.push(line) unless jasminePattern.test(line) + break if firstJasmineLinePattern.test(line) + + stackTrace = convertStackTrace(convertedLines.join('\n'), sourceMaps) + lines = stackTrace.split('\n') + + # Remove first line of stack when it is the same as the error message + errorMatch = lines[0]?.match(/^Error: (.*)/) + lines.shift() if message.trim() is errorMatch?[1]?.trim() + + for line, index in lines + # Remove prefix of lines matching: at [object Object]. (path:1:2) + prefixMatch = line.match(/at \[object Object\]\. \(([^)]+)\)/) + line = "at #{prefixMatch[1]}" if prefixMatch + + # Relativize locations to spec directory + lines[index] = line.replace("at #{spec.specDirectory}#{path.sep}", 'at ') + + lines = lines.map (line) -> indent + line.trim() + lines.join('\n') + + +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") + + +class N1SpecReporter extends React.Component + constructor: (@props) -> + + render: -> +
+
+ +
+
+
Core
+
{@_renderSpecsOfType('core')}
+
+
+
Bundled
+
{@_renderSpecsOfType('bundled')}
+
+
+
User
+
{@_renderSpecsOfType('user')}
+
+ {@_renderStatus()} +
+ {@_renderFailures()} +
+ {@_renderDeprecations()} +
+ {@props.plainTextOutput} +
+
+ + _renderSpecsOfType: (type) => + items = [] + @props.specs.forEach (spec, idx) => + return unless spec.specType is type + statusClass = "pending" + title = undefined + results = spec.results() + if results + if results.skipped + statusClass = "skipped" + else if results.failedCount > 0 + statusClass = "failed" + title = spec.getFullName() + else if spec.endedAt + statusClass = "passed" + + items.push
  • + + items + + _renderFailures: => + # We have an array of specs with `suite` and potentially N `parentSuite` from there. + # Create a tree instead. + topLevelSuites = [] + + failedSpecs = @props.specs.filter (spec) -> + spec.endedAt and spec.results().failedCount > 0 + + for spec in failedSpecs + suite = spec.suite + suite = suite.parentSuite while suite.parentSuite + if topLevelSuites.indexOf(suite) is -1 + topLevelSuites.push(suite) + + topLevelSuites.map (suite, idx) => + + + _renderDeprecations: => + return if @props.deprecations.length is 0 + + if @props.deprecations.length is 1 + label = "1 deprecation" + else + label = "#{@props.deprecations.length} deprecations" + +
    + {label} +
    +
    + {@_renderDeprecationList()} +
    +
    +
    + + _renderDeprecationList: => + @props.deprecations.map (deprecation) => +
    +
    + {deprecation.message} + { + deprecation.getStacks().map (stack) => + fullStack = stack.map ({functionName, location}) -> + if functionName is '' + " at #{location}" + else + " at #{functionName} (#{location})" +
    +                {formatStackTrace(deprecation.spec, deprecation.message, fullStack.join('\n'))}
    +              
    + } +
    +
    + + _renderStatus: => + failedCount = 0 + skippedCount = 0 + completeCount = 0 + for spec in @props.specs + results = spec.results() + continue unless spec.endedAt + failedCount += 1 if results.failedCount > 0 + skippedCount += 1 if results.skipped + completeCount += 1 if results.passedCount > 0 and results.failedCount is 0 + + if failedCount is 1 + message = "#{failedCount} failure" + else + message = "#{failedCount} failures" + + if skippedCount + specCount = "#{completeCount - skippedCount}/#{@props.specs.length - skippedCount} (#{skippedCount} skipped)" + else + specCount = "#{completeCount}/#{@props.specs.length}" + +
    +
    +
    {specCount}
    +
    {message}
    +
    + + onReloadSpecs: => + require('electron').ipcRenderer.send('call-webcontents-method', 'reload') + + +class SuiteResultView extends React.Component + @propTypes: -> + suite: React.PropTypes.object + allSpecs: React.PropTypes.array + + render: -> + items = [] + subsuites = [] + + @props.allSpecs.forEach (spec) => + if spec.suite is @props.suite + items.push(spec) + else + suite = spec.suite + while suite.parentSuite + if suite.parentSuite is @props.suite + subsuites.push(suite) + return + suite = suite.parentSuite + + items = items.map (spec, idx) => + + + subsuites = subsuites.map (suite, idx) => + + +
    +
    {@props.suite.description}
    +
    + {items} + {subsuites} +
    +
    + +class SpecResultView extends React.Component + @propTypes: -> + spec: React.PropTypes.object + + render: -> + description = @props.spec.description + resultItems = @props.spec.results().getItems() + description = "it #{description}" if description.indexOf('it ') isnt 0 + + failures = [] + for result, idx in resultItems + continue if result.passed() + stackTrace = formatStackTrace(@props.spec, result.message, result.trace.stack) + failures.push( +
    +
    {result.message}
    +
    {stackTrace}
    +
    + ) + +
    +
    {description}
    +
    {failures}
    +
    + + + +el = document.createElement('div') +document.body.appendChild(el) + +startedAt = null +specs = [] +deprecations = [] +plainTextOutput = "" + +update = => + component = + ReactDOM.render(component, el) + +updateSoon = _.debounce(update, 125) + +module.exports = + reportRunnerStarting: (runner) -> + specs = runner.specs() + startedAt = Date.now() + updateSoon() + + reportRunnerResults: (runner) -> + updateSoon() + + reportSuiteResults: (suite) -> + + reportSpecResults: (spec) -> + spec.endedAt = Date.now() + specDeprecations = grim.getDeprecations() + d.spec = spec for d in specDeprecations + deprecations = deprecations.concat(specDeprecations) + grim.clearDeprecations() + updateSoon() + + 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 = plainTextOutput + str + updateSoon() + + reportSpecStarting: (spec) -> + updateSoon() diff --git a/spec/n1-spec-reporter.coffee b/spec/n1-spec-reporter.coffee deleted file mode 100644 index 8666de3af..000000000 --- a/spec/n1-spec-reporter.coffee +++ /dev/null @@ -1,317 +0,0 @@ -path = require 'path' -_ = require 'underscore' -_str = require 'underscore.string' -{convertStackTrace} = require 'coffeestack' -{View, $, $$} = require '../src/space-pen-extensions' -grim = require 'grim' -marked = require 'marked' - -sourceMaps = {} -formatStackTrace = (spec, message='', stackTrace, indent="") -> - return stackTrace unless stackTrace - - jasminePattern = /^\s*at\s+.*\(?.*[/\\]jasmine(-[^/\\]*)?\.js:\d+:\d+\)?\s*$/ - firstJasmineLinePattern = /^\s*at [/\\].*[/\\]jasmine(-[^/\\]*)?\.js:\d+:\d+\)?\s*$/ - convertedLines = [] - for line in stackTrace.split('\n') - convertedLines.push(line) unless jasminePattern.test(line) - break if firstJasmineLinePattern.test(line) - - stackTrace = convertStackTrace(convertedLines.join('\n'), sourceMaps) - lines = stackTrace.split('\n') - - # Remove first line of stack when it is the same as the error message - errorMatch = lines[0]?.match(/^Error: (.*)/) - lines.shift() if message.trim() is errorMatch?[1]?.trim() - - for line, index in lines - # Remove prefix of lines matching: at [object Object]. (path:1:2) - prefixMatch = line.match(/at \[object Object\]\. \(([^)]+)\)/) - line = "at #{prefixMatch[1]}" if prefixMatch - - # Relativize locations to spec directory - lines[index] = line.replace("at #{spec.specDirectory}#{path.sep}", 'at ') - - lines = lines.map (line) -> indent + line.trim() - lines.join('\n') - -module.exports = -class N1SpecReporter extends View - @content: -> - @div class: 'spec-reporter', => - @div class: 'padded pull-right', => - @button outlet: 'reloadButton', class: 'btn reload-button', 'Reload Specs' - @div outlet: 'coreArea', class: 'symbol-area', => - @div outlet: 'coreHeader', class: 'symbol-header' - @ul outlet: 'coreSummary', class: 'symbol-summary list-unstyled' - @div outlet: 'bundledArea', class: 'symbol-area', => - @div outlet: 'bundledHeader', class: 'symbol-header' - @ul outlet: 'bundledSummary', class: 'symbol-summary list-unstyled' - @div outlet: 'userArea', class: 'symbol-area', => - @div outlet: 'userHeader', class: 'symbol-header' - @ul outlet: 'userSummary', class: 'symbol-summary list-unstyled' - @div outlet: "status", class: 'status alert alert-info', => - @div outlet: "time", class: 'time' - @div outlet: "specCount", class: 'spec-count' - @div outlet: "message", class: 'message' - @div outlet: "results", class: 'results' - - @div outlet: "deprecations", class: 'status alert alert-warning', style: 'display: none', => - @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 - completeSpecCount: 0 - passedCount: 0 - failedCount: 0 - skippedCount: 0 - totalSpecCount: 0 - deprecationCount: 0 - @timeoutId: 0 - - reportRunnerStarting: (runner) -> - @handleEvents() - @startedAt = Date.now() - specs = runner.specs() - @totalSpecCount = specs.length - @addSpecs(specs) - $(document.body).append this - - @on 'click', '.stack-trace', -> - $(this).toggleClass('expanded') - - @reloadButton.on 'click', -> require('electron').ipcRenderer.send('call-webcontents-method', 'reload') - - reportRunnerResults: (runner) -> - @updateSpecCounts() - @status.addClass('alert-success').removeClass('alert-info') if @failedCount is 0 - if @failedCount is 1 - @message.text "#{@failedCount} failure" - else - @message.text "#{@failedCount} failures" - - @status.addClass("specs-complete") - - reportSuiteResults: (suite) -> - - reportSpecResults: (spec) -> - @completeSpecCount++ - 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) - - addDeprecations: (spec) -> - deprecations = grim.getDeprecations() - @deprecationCount += deprecations.length - @deprecations.show() if @deprecationCount > 0 - if @deprecationCount is 1 - @deprecationStatus.text("1 deprecation") - else - @deprecationStatus.text("#{@deprecationCount} deprecations") - - for deprecation in deprecations - @deprecationList.append $$ -> - @div class: 'padded', => - @div class: 'result-message fail deprecation-message', => - @raw marked(deprecation.message) - - for stack in deprecation.getStacks() - fullStack = stack.map ({functionName, location}) -> - if functionName is '' - " at #{location}" - else - " at #{functionName} (#{location})" - @pre class: 'stack-trace padded', formatStackTrace(spec, deprecation.message, fullStack.join('\n')) - grim.clearDeprecations() - - handleEvents: -> - $(document).on "click", ".spec-toggle", ({currentTarget}) -> - element = $(currentTarget) - specFailures = element.parent().find('.spec-failures') - specFailures.toggle() - element.toggleClass('folded') - false - - $(document).on "click", ".deprecation-toggle", ({currentTarget}) -> - element = $(currentTarget) - deprecationList = $(document).find('.deprecation-list') - deprecationList.toggle() - element.toggleClass('folded') - false - - updateSpecCounts: -> - if @skippedCount - specCount = "#{@completeSpecCount - @skippedCount}/#{@totalSpecCount - @skippedCount} (#{@skippedCount} skipped)" - else - specCount = "#{@completeSpecCount}/#{@totalSpecCount}" - @specCount[0].textContent = specCount - - updateStatusView: (spec) -> - if @failedCount > 0 - @status.addClass('alert-danger').removeClass('alert-info') - - @updateSpecCounts() - - rootSuite = spec.suite - rootSuite = rootSuite.parentSuite while rootSuite.parentSuite - @message.text rootSuite.description - - time = "#{Math.round((spec.endedAt - @startedAt) / 10)}" - time = "0#{time}" if time.length < 3 - @time[0].textContent = "#{time[0...-2]}.#{time[-2..]}s" - - addSpecs: (specs) -> - coreSpecs = 0 - bundledPackageSpecs = 0 - userPackageSpecs = 0 - for spec in specs - symbol = $$ -> @li id: "spec-summary-#{spec.id}", class: "spec-summary pending" - switch spec.specType - when 'core' - coreSpecs++ - @coreSummary.append symbol - when 'bundled' - bundledPackageSpecs++ - @bundledSummary.append symbol - when 'user' - userPackageSpecs++ - @userSummary.append symbol - - if coreSpecs > 0 - @coreHeader.text("Core Specs (#{coreSpecs})") - else - @coreArea.hide() - if bundledPackageSpecs > 0 - @bundledHeader.text("Bundled Package Specs (#{bundledPackageSpecs})") - else - @bundledArea.hide() - if userPackageSpecs > 0 - if coreSpecs is 0 and bundledPackageSpecs is 0 - # Package specs being run, show a more descriptive label - {specDirectory} = specs[0] - packageFolderName = path.basename(path.dirname(specDirectory)) - packageName = _str.humanize(packageFolderName) - @userHeader.text("#{packageName} Specs") - else - @userHeader.text("User Package Specs (#{userPackageSpecs})") - else - @userArea.hide() - - specStarted: (spec) -> - @runningSpecCount++ - - specComplete: (spec) -> - specSummaryElement = $("#spec-summary-#{spec.id}") - specSummaryElement.removeClass('pending') - specSummaryElement.attr('title', spec.getFullName()) - - results = spec.results() - if results.skipped - specSummaryElement.addClass("skipped") - @skippedCount++ - else if results.passed() - specSummaryElement.addClass("passed") - @passedCount++ - else - specSummaryElement.addClass("failed") - - specView = new SpecResultView(spec) - specView.attach() - @failedCount++ - @addDeprecations(spec) - -class SuiteResultView extends View - @content: -> - @div class: 'suite', => - @div outlet: 'description', class: 'description' - - initialize: (@suite) -> - @attr('id', "suite-view-#{@suite.id}") - @description.text(@suite.description) - - attach: -> - (@parentSuiteView() or $('.results')).append this - - parentSuiteView: -> - return unless @suite.parentSuite - - if not suiteView = $("#suite-view-#{@suite.parentSuite.id}").view() - suiteView = new SuiteResultView(@suite.parentSuite) - suiteView.attach() - - suiteView - -class SpecResultView extends View - @content: -> - @div class: 'spec', => - @div class: 'spec-toggle' - @div outlet: 'description', class: 'description' - @div outlet: 'specFailures', class: 'spec-failures' - - initialize: (@spec) -> - @addClass("spec-view-#{@spec.id}") - - description = @spec.description - description = "it #{description}" if description.indexOf('it ') isnt 0 - @description.text(description) - - for result in @spec.results().getItems() when not result.passed() - stackTrace = formatStackTrace(@spec, result.message, result.trace.stack) - @specFailures.append $$ -> - @div result.message, class: 'result-message fail' - @pre stackTrace, class: 'stack-trace padded' if stackTrace - - attach: -> - @parentSuiteView().append this - - parentSuiteView: -> - if not suiteView = $("#suite-view-#{@spec.suite.id}").view() - suiteView = new SuiteResultView(@spec.suite) - suiteView.attach() - - suiteView diff --git a/spec/package-manager-spec.coffee b/spec/package-manager-spec.coffee index 3438edfb8..066cd6869 100644 --- a/spec/package-manager-spec.coffee +++ b/spec/package-manager-spec.coffee @@ -456,9 +456,9 @@ describe "PackageManager", -> one = require.resolve("./fixtures/packages/package-with-style-sheets-manifest/styles/1.css") two = require.resolve("./fixtures/packages/package-with-style-sheets-manifest/styles/2.less") three = require.resolve("./fixtures/packages/package-with-style-sheets-manifest/styles/3.css") - expect(NylasEnv.themes.stylesheetElementForId(one)).not.toExist() - expect(NylasEnv.themes.stylesheetElementForId(two)).not.toExist() - expect(NylasEnv.themes.stylesheetElementForId(three)).not.toExist() + expect(NylasEnv.themes.stylesheetElementForId(one)).toBe(null) + expect(NylasEnv.themes.stylesheetElementForId(two)).toBe(null) + expect(NylasEnv.themes.stylesheetElementForId(three)).toBe(null) it "invokes ::onDidDeactivatePackage listeners with the deactivated package", -> waitsForPromise -> diff --git a/spec/package-spec.coffee b/spec/package-spec.coffee index 333480721..952c0e09b 100644 --- a/spec/package-spec.coffee +++ b/spec/package-spec.coffee @@ -1,4 +1,3 @@ -{$} = require '../src/space-pen-extensions' path = require 'path' Package = require '../src/package' ThemePackage = require '../src/theme-package' @@ -41,51 +40,57 @@ describe "Package", -> theme = null beforeEach -> - $("#jasmine-content").append $("") + @wrap = document.createElement('nylas-theme-wrap') + document.getElementById("jasmine-content").appendChild(@wrap) afterEach -> theme.deactivate() if theme? describe "when the theme contains a single style file", -> it "loads and applies css", -> - expect($("nylas-theme-wrap").css("padding-bottom")).not.toBe "1234px" + + expect(window.getComputedStyle(@wrap)['padding-bottom']).not.toBe "1234px" themePath = resolveFixturePath('theme-with-index-css') theme = new ThemePackage(themePath) theme.activate() - expect($("nylas-theme-wrap").css("padding-top")).toBe "1234px" + expect(window.getComputedStyle(@wrap)['padding-top']).toBe "1234px" it "parses, loads and applies less", -> - expect($("nylas-theme-wrap").css("padding-bottom")).not.toBe "1234px" + expect(window.getComputedStyle(@wrap)['padding-bottom']).not.toBe "1234px" themePath = resolveFixturePath('theme-with-index-less') theme = new ThemePackage(themePath) theme.activate() - expect($("nylas-theme-wrap").css("padding-top")).toBe "4321px" + expect(window.getComputedStyle(@wrap)['padding-top']).toBe "4321px" describe "when the theme contains a package.json file", -> it "loads and applies stylesheets from package.json in the correct order", -> - expect($("nylas-theme-wrap").css("padding-top")).not.toBe("101px") - expect($("nylas-theme-wrap").css("padding-right")).not.toBe("102px") - expect($("nylas-theme-wrap").css("padding-bottom")).not.toBe("103px") + styles = window.getComputedStyle(@wrap) + expect(styles["padding-top"]).not.toBe("101px") + expect(styles["padding-right"]).not.toBe("102px") + expect(styles["padding-bottom"]).not.toBe("103px") themePath = resolveFixturePath('theme-with-package-file') theme = new ThemePackage(themePath) theme.activate() - expect($("nylas-theme-wrap").css("padding-top")).toBe("101px") - expect($("nylas-theme-wrap").css("padding-right")).toBe("102px") - expect($("nylas-theme-wrap").css("padding-bottom")).toBe("103px") + styles = window.getComputedStyle(@wrap) + expect(styles["padding-top"]).toBe("101px") + expect(styles["padding-right"]).toBe("102px") + expect(styles["padding-bottom"]).toBe("103px") describe "when the theme does not contain a package.json file and is a directory", -> it "loads all stylesheet files in the directory", -> - expect($("nylas-theme-wrap").css("padding-top")).not.toBe "10px" - expect($("nylas-theme-wrap").css("padding-right")).not.toBe "20px" - expect($("nylas-theme-wrap").css("padding-bottom")).not.toBe "30px" + styles = window.getComputedStyle(@wrap) + expect(styles["padding-top"]).not.toBe("10px") + expect(styles["padding-right"]).not.toBe("20px") + expect(styles["padding-bottom"]).not.toBe("30px") themePath = resolveFixturePath('theme-without-package-file') theme = new ThemePackage(themePath) theme.activate() - expect($("nylas-theme-wrap").css("padding-top")).toBe "10px" - expect($("nylas-theme-wrap").css("padding-right")).toBe "20px" - expect($("nylas-theme-wrap").css("padding-bottom")).toBe "30px" + styles = window.getComputedStyle(@wrap) + expect(styles["padding-top"]).toBe("10px") + expect(styles["padding-right"]).toBe("20px") + expect(styles["padding-bottom"]).toBe("30px") describe "reloading a theme", -> beforeEach -> diff --git a/spec/spec-helper.coffee b/spec/spec-helper.coffee index b33d38912..a04c47a1b 100644 --- a/spec/spec-helper.coffee +++ b/spec/spec-helper.coffee @@ -7,15 +7,11 @@ 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' pathwatcher = require 'pathwatcher' {clipboard} = require 'electron' @@ -40,7 +36,9 @@ window.addEventListener 'core:close', -> window.close() window.addEventListener 'beforeunload', -> NylasEnv.storeWindowDimensions() NylasEnv.saveSync() -$('html,body').css('overflow', 'auto') + +document.querySelector('html').style.overflow = 'initial' +document.querySelector('body').style.overflow = 'initial' # Allow document.title to be assigned in specs without screwing up spec window title documentTitle = null @@ -142,7 +140,6 @@ beforeEach -> TaskQueue._completed = [] TaskQueue._onlineStatus = true - $.fx.off = true documentTitle = null NylasEnv.styles.restoreSnapshot(styleElementsToRestore) NylasEnv.workspaceViewParentSelector = '#jasmine-content' @@ -243,8 +240,8 @@ afterEach -> delete NylasEnv.state?.packageStates - $('#jasmine-content').empty() unless window.debugContent - + unless window.debugContent + document.getElementById('jasmine-content').innerHTML = '' ReactTestUtils.unmountAll() jasmine.unspy(NylasEnv, 'saveSync') @@ -354,8 +351,8 @@ window.keydownEvent = (key, properties={}) -> originalEventProperties.target = properties.target?[0] ? properties.target originalEventProperties.which = properties.which originalEvent = KeymapManager.keydownEvent(key, originalEventProperties) - properties = $.extend({originalEvent}, properties) - $.Event("keydown", properties) + properties = _.extend({originalEvent}, properties) + new CustomEvent('keydown', properties) window.mouseEvent = (type, properties) -> if properties.point @@ -364,7 +361,7 @@ window.mouseEvent = (type, properties) -> properties.pageX = left + 1 properties.pageY = top + 1 properties.originalEvent ?= {detail: 1} - $.Event type, properties + new CustomEvent(type, properties) window.clickEvent = (properties={}) -> window.mouseEvent("click", properties) diff --git a/spec/theme-manager-spec.coffee b/spec/theme-manager-spec.coffee index c213ee8cd..5d99a4d52 100644 --- a/spec/theme-manager-spec.coffee +++ b/spec/theme-manager-spec.coffee @@ -1,6 +1,5 @@ path = require 'path' -{$, $$} = require '../src/space-pen-extensions' fs = require 'fs-plus' temp = require 'temp' @@ -101,7 +100,7 @@ describe "ThemeManager", -> runs -> didChangeActiveThemesHandler.reset() - expect($('style.theme')).toHaveLength 0 + expect(document.querySelectorAll('style.theme')).toHaveLength 0 NylasEnv.config.set('core.themes', ['ui-dark']) waitsFor -> @@ -109,8 +108,9 @@ describe "ThemeManager", -> runs -> didChangeActiveThemesHandler.reset() - expect($('style[priority=1]')).toHaveLength 1 - expect($('style[priority=1]:eq(0)').attr('source-path')).toMatch /ui-dark/ + sheets = Array.from(document.querySelectorAll('style[priority="1"]')) + expect(sheets).toHaveLength 1 + expect(sheets[0].getAttribute('source-path')).toMatch /ui-dark/ NylasEnv.config.set('core.themes', ['ui-light', 'ui-dark']) waitsFor -> @@ -118,9 +118,10 @@ describe "ThemeManager", -> runs -> didChangeActiveThemesHandler.reset() - expect($('style[priority=1]')).toHaveLength 2 - expect($('style[priority=1]:eq(0)').attr('source-path')).toMatch /ui-dark/ - expect($('style[priority=1]:eq(1)').attr('source-path')).toMatch /ui-light/ + sheets = Array.from(document.querySelectorAll('style[priority="1"]')) + expect(sheets).toHaveLength 2 + expect(sheets[0].getAttribute('source-path')).toMatch /ui-dark/ + expect(sheets[1].getAttribute('source-path')).toMatch /ui-light/ NylasEnv.config.set('core.themes', []) waitsFor -> @@ -128,7 +129,8 @@ describe "ThemeManager", -> runs -> didChangeActiveThemesHandler.reset() - expect($('style[priority=1]')).toHaveLength(1) + sheets = Array.from(document.querySelectorAll('style[priority="1"]')) + expect(sheets).toHaveLength(1) # ui-dark has an directory path, the syntax one doesn't NylasEnv.config.set('core.themes', ['theme-with-index-less', 'ui-light']) @@ -136,7 +138,8 @@ describe "ThemeManager", -> didChangeActiveThemesHandler.callCount == 1 runs -> - expect($('style[priority=1]')).toHaveLength 2 + sheets = Array.from(document.querySelectorAll('style[priority="1"]')) + expect(sheets).toHaveLength 2 importPaths = themeManager.getImportPaths() expect(importPaths.length).toBe 1 expect(importPaths[0]).toContain 'ui-light' @@ -151,7 +154,7 @@ describe "ThemeManager", -> themeManager.activateThemes() runs -> - expect(workspaceElement).toHaveClass 'theme-ui-light' + expect(workspaceElement.classList.contains('theme-ui-light')).toBe(true) themeManager.onDidChangeActiveThemes didChangeActiveThemesHandler = jasmine.createSpy() NylasEnv.config.set('core.themes', ['theme-with-ui-variables']) @@ -161,8 +164,8 @@ describe "ThemeManager", -> runs -> # `theme-` twice as it prefixes the name with `theme-` - expect(workspaceElement).toHaveClass 'theme-theme-with-ui-variables' - expect(workspaceElement).not.toHaveClass 'theme-ui-dark' + expect(workspaceElement.classList.contains('theme-theme-with-ui-variables')).toBe(true) + expect(workspaceElement.classList.contains('theme-ui-dark')).toBe(false) describe "when a theme fails to load", -> it "logs a warning", -> @@ -183,37 +186,37 @@ describe "ThemeManager", -> themeManager.onDidAddStylesheet stylesheetAddedHandler = jasmine.createSpy("stylesheetAddedHandler") cssPath = path.join(__dirname, 'fixtures', 'css.css') - lengthBefore = $('head style').length + lengthBefore = document.querySelectorAll('head style').length themeManager.requireStylesheet(cssPath) - expect($('head style').length).toBe lengthBefore + 1 + expect(document.querySelectorAll('head style').length).toBe lengthBefore + 1 expect(styleElementAddedHandler).toHaveBeenCalled() expect(stylesheetAddedHandler).toHaveBeenCalled() expect(stylesheetsChangedHandler).toHaveBeenCalled() - element = $('head style[source-path*="css.css"]') - expect(element.attr('source-path')).toBe themeManager.stringToId(cssPath) - expect(element.text()).toBe fs.readFileSync(cssPath, 'utf8') - expect(element[0].sheet).toBe stylesheetAddedHandler.argsForCall[0][0] + element = document.querySelector('head style[source-path*="css.css"]') + expect(element.getAttribute('source-path')).toBe themeManager.stringToId(cssPath) + expect(element.textContent).toBe fs.readFileSync(cssPath, 'utf8') # doesn't append twice styleElementAddedHandler.reset() themeManager.requireStylesheet(cssPath) - expect($('head style').length).toBe lengthBefore + 1 + expect(document.querySelectorAll('head style').length).toBe lengthBefore + 1 expect(styleElementAddedHandler).not.toHaveBeenCalled() - $('head style[id*="css.css"]').remove() + element .remove() it "synchronously loads and parses less files at the given path and installs a style tag for it in the head", -> lessPath = path.join(__dirname, 'fixtures', 'sample.less') - lengthBefore = $('head style').length + lengthBefore = document.querySelectorAll('head style').length themeManager.requireStylesheet(lessPath) - expect($('head style').length).toBe lengthBefore + 1 + lengthAfter = document.querySelectorAll('head style').length + expect(lengthAfter).toBe lengthBefore + 1 - element = $('head style[source-path*="sample.less"]') - expect(element.attr('source-path')).toBe themeManager.stringToId(lessPath) - expect(element.text()).toBe """ + element = document.querySelector('head style[source-path*="sample.less"]') + expect(element.getAttribute('source-path')).toBe themeManager.stringToId(lessPath) + expect(element.textContent).toBe """ #header { color: #4d926f; } @@ -225,24 +228,24 @@ describe "ThemeManager", -> # doesn't append twice themeManager.requireStylesheet(lessPath) - expect($('head style').length).toBe lengthBefore + 1 - $('head style[id*="sample.less"]').remove() + expect(document.querySelectorAll('head style').length).toBe lengthBefore + 1 + element.remove() it "supports requiring css and less stylesheets without an explicit extension", -> themeManager.requireStylesheet path.join(__dirname, 'fixtures', 'css') - expect($('head style[source-path*="css.css"]').attr('source-path')).toBe themeManager.stringToId(path.join(__dirname, 'fixtures', 'css.css')) + expect(document.querySelector('head style[source-path*="css.css"]').getAttribute('source-path')).toBe themeManager.stringToId(path.join(__dirname, 'fixtures', 'css.css')) themeManager.requireStylesheet path.join(__dirname, 'fixtures', 'sample') - expect($('head style[source-path*="sample.less"]').attr('source-path')).toBe themeManager.stringToId(path.join(__dirname, 'fixtures', 'sample.less')) + expect(document.querySelector('head style[source-path*="sample.less"]').getAttribute('source-path')).toBe themeManager.stringToId(path.join(__dirname, 'fixtures', 'sample.less')) - $('head style[id*="css.css"]').remove() - $('head style[id*="sample.less"]').remove() + document.querySelector('head style[source-path*="css.css"]').remove() + document.querySelector('head style[source-path*="sample.less"]').remove() it "returns a disposable allowing styles applied by the given path to be removed", -> cssPath = require.resolve('./fixtures/css.css') - expect($(document.body).css('font-weight')).not.toBe("bold") + expect(window.getComputedStyle(document.body)['font-weight']).not.toBe("bold") disposable = themeManager.requireStylesheet(cssPath) - expect($(document.body).css('font-weight')).toBe("bold") + expect(window.getComputedStyle(document.body)['font-weight']).toBe("bold") NylasEnv.styles.onDidRemoveStyleElement styleElementRemovedHandler = jasmine.createSpy("styleElementRemovedHandler") themeManager.onDidRemoveStylesheet stylesheetRemovedHandler = jasmine.createSpy("stylesheetRemovedHandler") @@ -250,7 +253,7 @@ describe "ThemeManager", -> disposable.dispose() - expect($(document.body).css('font-weight')).not.toBe("bold") + expect(window.getComputedStyle(document.body)['font-weight']).not.toBe("bold") expect(styleElementRemovedHandler).toHaveBeenCalled() expect(stylesheetRemovedHandler).toHaveBeenCalled() @@ -291,9 +294,11 @@ describe "ThemeManager", -> expect(getComputedStyle(workspaceElement)["background-color"]).toBe "rgb(0, 0, 255)" # a value that is not overridden in the theme - expect($("nylas-theme-wrap").css("padding-top")).toBe "150px" - expect($("nylas-theme-wrap").css("padding-right")).toBe "150px" - expect($("nylas-theme-wrap").css("padding-bottom")).toBe "150px" + node = document.querySelector('nylas-theme-wrap') + nodeStyle = window.getComputedStyle(node) + expect(nodeStyle['padding-top']).toBe "150px" + expect(nodeStyle['padding-right']).toBe "150px" + expect(nodeStyle['padding-bottom']).toBe "150px" describe "when there is a theme with incomplete variables", -> it "loads the correct values from the fallback ui-variables", -> @@ -304,8 +309,10 @@ describe "ThemeManager", -> # an override loaded in the base css of theme-with-incomplete-ui-variables expect(getComputedStyle(workspaceElement)["background-color"]).toBe "rgb(0, 0, 255)" - # a value that is not overridden in the theme - expect($("nylas-theme-wrap").css("background-color")).toBe "rgb(152, 123, 0)" + # a value that is not overridden in the theme + node = document.querySelector('nylas-theme-wrap') + nodeStyle = window.getComputedStyle(node) + expect(nodeStyle['background-color']).toBe "rgb(152, 123, 0)" describe "user stylesheet", -> userStylesheetPath = null @@ -337,14 +344,16 @@ describe "ThemeManager", -> themeManager.onDidAddStylesheet stylesheetAddedHandler = jasmine.createSpy("stylesheetAddedHandler") spyOn(themeManager, 'loadUserStylesheet').andCallThrough() - expect($(document.body).css('border-style')).toBe 'dotted' + bodyStyle = window.getComputedStyle(document.body) + expect(bodyStyle['border-style']).toBe "dotted" fs.writeFileSync(userStylesheetPath, 'body {border-style: dashed}') waitsFor -> themeManager.loadUserStylesheet.callCount is 1 runs -> - expect($(document.body).css('border-style')).toBe 'dashed' + bodyStyle = window.getComputedStyle(document.body) + expect(bodyStyle['border-style']).toBe "dashed" expect(styleElementRemovedHandler).toHaveBeenCalled() expect(styleElementRemovedHandler.argsForCall[0][0].textContent).toContain 'dotted' @@ -382,7 +391,9 @@ describe "ThemeManager", -> match = null for rule in stylesheetRemovedHandler.argsForCall[0][0].cssRules match = rule if rule.selectorText is 'body' - match.style.border is 'dashed' and $(document.body).css('border-style') is 'none' + + bodyStyle = window.getComputedStyle(document.body) + match.style.border is 'dashed' and bodyStyle['border-style'] is 'none' runs -> expect(stylesheetsChangedHandler).toHaveBeenCalled() diff --git a/src/space-pen-extensions.coffee b/src/space-pen-extensions.coffee deleted file mode 100644 index 0f2d5c0df..000000000 --- a/src/space-pen-extensions.coffee +++ /dev/null @@ -1,109 +0,0 @@ -_ = require 'underscore' -_ = _.extend(_, require('./space-pen-utils')) -SpacePen = require 'space-pen' -{Subscriber} = require 'emissary' - -### -Edgehill Note: - -I want this file goneā€”plan is to slowly delete stuff below as we clean spacepen -and it's helpers out of the project. - -- Ben -### - - -Subscriber.includeInto(SpacePen.View) - -jQuery = SpacePen.jQuery -JQueryCleanData = jQuery.cleanData -jQuery.cleanData = (elements) -> - jQuery(element).view()?.unsubscribe?() for element in elements - JQueryCleanData(elements) - -SpacePenCallRemoveHooks = SpacePen.callRemoveHooks -SpacePen.callRemoveHooks = (element) -> - view.unsubscribe?() for view in SpacePen.viewsForElement(element) - SpacePenCallRemoveHooks(element) - -NativeEventNames = new Set -NativeEventNames.add(nativeEvent) for nativeEvent in ["blur", "focus", "focusin", -"focusout", "load", "resize", "scroll", "unload", "click", "dblclick", "mousedown", -"mouseup", "mousemove", "mouseover", "mouseout", "mouseenter", "mouseleave", "change", -"select", "submit", "keydown", "keypress", "keyup", "error", "contextmenu", "textInput", -"textinput", "beforeunload"] - -JQueryTrigger = jQuery.fn.trigger -jQuery.fn.trigger = (eventName, data) -> - if NativeEventNames.has(eventName) or typeof eventName is 'object' - JQueryTrigger.call(this, eventName, data) - else - data ?= {} - data.jQueryTrigger = true - - for element in this - NylasEnv.commands.dispatch(eventName, data) - this - -HandlersByOriginalHandler = new WeakMap -CommandDisposablesByElement = new WeakMap - -AddEventListener = (element, type, listener) -> - if NativeEventNames.has(type) - element.addEventListener(type, listener) - else - disposable = NylasEnv.commands.add(element, type, listener) - - unless disposablesByType = CommandDisposablesByElement.get(element) - disposablesByType = {} - CommandDisposablesByElement.set(element, disposablesByType) - - unless disposablesByListener = disposablesByType[type] - disposablesByListener = new WeakMap - disposablesByType[type] = disposablesByListener - - disposablesByListener.set(listener, disposable) - -RemoveEventListener = (element, type, listener) -> - if NativeEventNames.has(type) - element.removeEventListener(type, listener) - else - CommandDisposablesByElement.get(element)?[type]?.get(listener)?.dispose() - -JQueryEventAdd = jQuery.event.add -jQuery.event.add = (elem, types, originalHandler, data, selector) -> - handler = (event) -> - if arguments.length is 1 and event.originalEvent?.detail? - {detail} = event.originalEvent - if Array.isArray(detail) - originalHandler.apply(this, [event].concat(detail)) - else - originalHandler.call(this, event, detail) - else - originalHandler.apply(this, arguments) - - HandlersByOriginalHandler.set(originalHandler, handler) - - JQueryEventAdd.call(this, elem, types, handler, data, selector, AddEventListener if NylasEnv?.commands?) - -JQueryEventRemove = jQuery.event.remove -jQuery.event.remove = (elem, types, originalHandler, selector, mappedTypes) -> - if originalHandler? - handler = HandlersByOriginalHandler.get(originalHandler) ? originalHandler - JQueryEventRemove(elem, types, handler, selector, mappedTypes, RemoveEventListener if NylasEnv?.commands?) - -JQueryContains = jQuery.contains - -jQuery.contains = (a, b) -> - shadowRoot = null - currentNode = b - while currentNode - if currentNode instanceof ShadowRoot and a.contains(currentNode.host) - return true - currentNode = currentNode.parentNode - - JQueryContains.call(this, a, b) - -Object.defineProperty jQuery.fn, 'element', get: -> @[0] - -module.exports = SpacePen