diff --git a/.github/workflows/editor-tests.yml b/.github/workflows/editor-tests.yml index c40264941f..760d1b0e2a 100644 --- a/.github/workflows/editor-tests.yml +++ b/.github/workflows/editor-tests.yml @@ -42,8 +42,8 @@ jobs: - name: Run Tests if: runner.os != 'Linux' - run: node script/run-tests.js spec + run: yarn test:editor - name: Run Tests with xvfb-run (Linux) if: runner.os == 'Linux' - run: xvfb-run --auto-servernum node script/run-tests.js spec + run: xvfb-run --auto-servernum yarn test:editor diff --git a/.github/workflows/package-tests-linux.yml b/.github/workflows/package-tests-linux.yml index db872336c4..35060cdf66 100644 --- a/.github/workflows/package-tests-linux.yml +++ b/.github/workflows/package-tests-linux.yml @@ -201,4 +201,3 @@ jobs: - name: Run ${{ matrix.package }} Tests run: Xvfb :1 & cd node_modules/${{ matrix.package }} && if test -d spec; then DISPLAY=:1 pulsar --test spec; fi - # run: node -e "require('./script/run-package-tests')(/${{ matrix.package }}/)" diff --git a/package.json b/package.json index 4daa0cd7c7..9bbeb48fa2 100644 --- a/package.json +++ b/package.json @@ -86,8 +86,8 @@ "image-view": "file:packages/image-view", "incompatible-packages": "file:packages/incompatible-packages", "jasmine-json": "~0.0", - "jasmine-reporters": "1.1.0", "jasmine-tagged": "^1.1.4", + "jasmine": "2.5.3", "key-path-helpers": "^0.4.0", "keybinding-resolver": "file:./packages/keybinding-resolver", "language-c": "file:packages/language-c", @@ -293,7 +293,9 @@ "start": "electron --no-sandbox --enable-logging . -f", "dist": "node script/electron-builder.js", "js-docs": "jsdoc2md --files src --configure docs/.jsdoc.json > ./docs/Pulsar-API-Documentation.md", - "private-js-docs": "jsdoc2md --private --files src --configure docs/.jsdoc.json > ./docs/Source-Code-Documentation.md" + "private-js-docs": "jsdoc2md --private --files src --configure docs/.jsdoc.json > ./docs/Source-Code-Documentation.md", + "test:editor": "yarn test:only spec", + "test:only": "yarn start --test" }, "devDependencies": { "@electron/notarize": "^1.2.3", diff --git a/packages/archive-view/package.json b/packages/archive-view/package.json index 64389cb8b5..20f728d7a8 100644 --- a/packages/archive-view/package.json +++ b/packages/archive-view/package.json @@ -14,6 +14,7 @@ "engines": { "atom": "*" }, + "atomTestRunner": "runners/jasmine2-test-runner", "deserializers": { "ArchiveEditor": "deserialize", "ArchiveEditorView": "deserialize" diff --git a/packages/archive-view/spec/archive-editor-spec.js b/packages/archive-view/spec/archive-editor-spec.js index 7b0bea958c..1ab995be05 100644 --- a/packages/archive-view/spec/archive-editor-spec.js +++ b/packages/archive-view/spec/archive-editor-spec.js @@ -1,5 +1,3 @@ -const {it, fit, ffit, fffit, beforeEach, afterEach, conditionPromise} = require('./async-spec-helpers') // eslint-disable-line no-unused-vars - const path = require('path') const ArchiveEditor = require('../lib/archive-editor') const ArchiveEditorView = require('../lib/archive-editor-view') diff --git a/packages/archive-view/spec/archive-editor-view-spec.js b/packages/archive-view/spec/archive-editor-view-spec.js index 4ded3e5a8e..38d2848703 100644 --- a/packages/archive-view/spec/archive-editor-view-spec.js +++ b/packages/archive-view/spec/archive-editor-view-spec.js @@ -1,6 +1,6 @@ const {Disposable, File} = require('atom') const getIconServices = require('../lib/get-icon-services') -const {it, fit, ffit, fffit, beforeEach, afterEach, conditionPromise} = require('./async-spec-helpers') // eslint-disable-line no-unused-vars +const {conditionPromise} = require('./async-spec-helpers') // eslint-disable-line no-unused-vars async function condition (handler) { if (jasmine.isSpy(window.setTimeout)) { @@ -13,21 +13,21 @@ describe('ArchiveEditorView', () => { let archiveEditorView, onDidChangeCallback, onDidRenameCallback, onDidDeleteCallback beforeEach(async () => { - spyOn(File.prototype, 'onDidChange').andCallFake(function (callback) { + spyOn(File.prototype, 'onDidChange').and.callFake(function (callback) { if (/\.tar$/.test(this.getPath())) { onDidChangeCallback = callback } return new Disposable() }) - spyOn(File.prototype, 'onDidRename').andCallFake(function (callback) { + spyOn(File.prototype, 'onDidRename').and.callFake(function (callback) { if (/\.tar$/.test(this.getPath())) { onDidRenameCallback = callback } return new Disposable() }) - spyOn(File.prototype, 'onDidDelete').andCallFake(function (callback) { + spyOn(File.prototype, 'onDidDelete').and.callFake(function (callback) { if (/\.tar$/.test(this.getPath())) { onDidDeleteCallback = callback } @@ -150,9 +150,9 @@ describe('ArchiveEditorView', () => { describe('when the file is renamed', () => { it('refreshes the view and updates the title', async () => { - spyOn(File.prototype, 'getPath').andReturn('nested-renamed.tar') + spyOn(File.prototype, 'getPath').and.returnValue('nested-renamed.tar') await condition(() => archiveEditorView.element.querySelectorAll('.entry').length > 0) - spyOn(archiveEditorView, 'refresh').andCallThrough() + spyOn(archiveEditorView, 'refresh').and.callThrough() spyOn(archiveEditorView, 'getTitle') onDidRenameCallback() expect(archiveEditorView.refresh).toHaveBeenCalled() diff --git a/packages/archive-view/spec/async-spec-helpers.js b/packages/archive-view/spec/async-spec-helpers.js index 73002c049a..019902ce82 100644 --- a/packages/archive-view/spec/async-spec-helpers.js +++ b/packages/archive-view/spec/async-spec-helpers.js @@ -1,39 +1,5 @@ /** @babel */ -export function beforeEach (fn) { - global.beforeEach(function () { - const result = fn() - if (result instanceof Promise) { - waitsForPromise(() => result) - } - }) -} - -export function afterEach (fn) { - global.afterEach(function () { - const result = fn() - if (result instanceof Promise) { - waitsForPromise(() => result) - } - }) -} - -['it', 'fit', 'ffit', 'fffit'].forEach(function (name) { - module.exports[name] = function (description, fn) { - if (fn === undefined) { - global[name](description) - return - } - - global[name](description, function () { - const result = fn() - if (result instanceof Promise) { - waitsForPromise(() => result) - } - }) - } -}) - export async function conditionPromise (condition, description = 'anonymous condition') { const startTime = Date.now() @@ -50,54 +16,8 @@ export async function conditionPromise (condition, description = 'anonymous cond } } -export function timeoutPromise (timeout) { +function timeoutPromise (timeout) { return new Promise(function (resolve) { global.setTimeout(resolve, timeout) }) } - -function waitsForPromise (fn) { - const promise = fn() - global.waitsFor('spec promise to resolve', function (done) { - promise.then(done, function (error) { - jasmine.getEnv().currentSpec.fail(error) - done() - }) - }) -} - -export function emitterEventPromise (emitter, event, timeout = 15000) { - return new Promise((resolve, reject) => { - const timeoutHandle = setTimeout(() => { - reject(new Error(`Timed out waiting for '${event}' event`)) - }, timeout) - emitter.once(event, () => { - clearTimeout(timeoutHandle) - resolve() - }) - }) -} - -export function promisify (original) { - return function (...args) { - return new Promise((resolve, reject) => { - args.push((err, ...results) => { - if (err) { - reject(err) - } else { - resolve(...results) - } - }) - - return original(...args) - }) - } -} - -export function promisifySome (obj, fnNames) { - const result = {} - for (const fnName of fnNames) { - result[fnName] = promisify(obj[fnName]) - } - return result -} diff --git a/script/electron-builder.js b/script/electron-builder.js index 32718fdce7..7b4390dc7d 100644 --- a/script/electron-builder.js +++ b/script/electron-builder.js @@ -61,11 +61,8 @@ let options = { // Core Repo Test Inclusions "spec/jasmine-test-runner.js", - "spec/spec-helper.js", - "spec/jasmine-junit-reporter.js", - "spec/spec-helper-functions.js", - "spec/atom-reporter.js", - "spec/jasmine-list-reporter.js", + "spec/helpers/**/*", + "spec/runners/**/*", // --- Exclusions --- // Core Repo Exclusions diff --git a/script/run-package-tests.js b/script/run-package-tests.js deleted file mode 100644 index 558332ccbc..0000000000 --- a/script/run-package-tests.js +++ /dev/null @@ -1,20 +0,0 @@ -const fs = require('fs') -const path = require('path') -const packJson = require('../package.json') -const runAllSpecs = require('./run-tests') - -module.exports = function(filter) { - let packagePath = [] - - for(let pack in packJson.packageDependencies) { - if(pack.match(filter)) { - let basePath = path.join('node_modules', pack) - let testPath = path.join(basePath, 'test') - let specPath = path.join(basePath, 'spec') - if(fs.existsSync(testPath)) packagePath.push(testPath) - if(fs.existsSync(specPath)) packagePath.push(specPath) - } - } - - runAllSpecs(packagePath) -} diff --git a/script/run-tests.js b/script/run-tests.js deleted file mode 100644 index f6fff0c3c8..0000000000 --- a/script/run-tests.js +++ /dev/null @@ -1,76 +0,0 @@ -const cp = require('child_process') - -function runAllSpecs(files) { - runSpecs(files, []) -} - -function runSpecs(files, retries) { - let env = process.env - env.ATOM_JASMINE_REPORTER='list' - if(retries.length > 0) { - // Escape possible tests that can generate a regexp that will not match... - const escaped = retries.map(str => str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')); - env.SPEC_FILTER = escaped.join("|") - } - const res = cp.spawn('yarn', ['start', '--test', ...files], { - cwd: process.cwd(), - env: env - }) - - let out; - res.stdout.on('data', data => { - process.stdout.write(data.toString()); - }); - - res.stderr.on('data', data => { - const strData = data.toString(); - process.stderr.write(strData); - - if(strData.match(/ALL TESTS THAT FAILED:/)) { - out = ''; - } else if(out !== undefined) { - out += strData; - } - }); - - res.on('close', code => { - if(code !== 0 && retries.length === 0) { - const failed = filterSpecs(out) - - console.log(`********************* -Tests failed. Retrying failed tests... -********************* - -`) - runSpecs(files, failed) - } else { - process.exit(code) - } - }); -} - -function filterSpecs(output) { - if(!output) return '' - let descriptions = [] - let start = true - for(let out of output.split("\n")) { - if(start) { - if(out !== '') { - start = false - descriptions.push(out) - } - } else if(out !== '') { - descriptions.push(out) - } else { - return descriptions - } - } -} - -if(process.argv[0] === __filename) { - runAllSpecs(process.argv.splice(1)) -} else if(process.argv[1] === __filename) { - runAllSpecs(process.argv.splice(2)) -} else { - module.exports = runAllSpecs -} diff --git a/spec/atom-environment-spec.js b/spec/atom-environment-spec.js index 2a44b24042..b6b076dd6b 100644 --- a/spec/atom-environment-spec.js +++ b/spec/atom-environment-spec.js @@ -1,4 +1,4 @@ -const { conditionPromise } = require('./async-spec-helpers'); +const { conditionPromise } = require('./helpers/async-spec-helpers'); const fs = require('fs'); const path = require('path'); const temp = require('temp').track(); diff --git a/spec/async-spec-helpers.js b/spec/helpers/async-spec-helpers.js similarity index 100% rename from spec/async-spec-helpers.js rename to spec/helpers/async-spec-helpers.js diff --git a/spec/helpers/attach-to-dom.js b/spec/helpers/attach-to-dom.js new file mode 100644 index 0000000000..cb8575b033 --- /dev/null +++ b/spec/helpers/attach-to-dom.js @@ -0,0 +1,4 @@ +jasmine.attachToDOM = function(element) { + const jasmineContent = document.querySelector('#jasmine-content'); + if (!jasmineContent.contains(element)) { jasmineContent.appendChild(element); } +}; diff --git a/spec/helpers/build-atom-environment.js b/spec/helpers/build-atom-environment.js new file mode 100644 index 0000000000..43f08863a0 --- /dev/null +++ b/spec/helpers/build-atom-environment.js @@ -0,0 +1,28 @@ +const fs = require('fs-plus'); +const temp = require('temp'); +const path = require('path'); + +const userHome = process.env.ATOM_HOME || path.join(fs.getHomeDirectory(), '.atom'); +const atomHome = temp.mkdirSync({prefix: 'atom-test-home-'}); +if (process.env.APM_TEST_PACKAGES) { + const testPackages = process.env.APM_TEST_PACKAGES.split(/\s+/); + fs.makeTreeSync(path.join(atomHome, 'packages')); + for (let packName of Array.from(testPackages)) { + const userPack = path.join(userHome, 'packages', packName); + const loadablePack = path.join(atomHome, 'packages', packName); + + try { + fs.symlinkSync(userPack, loadablePack, 'dir'); + } catch (error) { + fs.copySync(userPack, loadablePack); + } + } +} + +const ApplicationDelegate = require('../../src/application-delegate'); +const applicationDelegate = new ApplicationDelegate(); +applicationDelegate.setRepresentedFilename = function () {}; +applicationDelegate.setWindowDocumentEdited = function () {}; + +exports.atomHome = atomHome +exports.applicationDelegate = applicationDelegate; diff --git a/spec/helpers/default-timeout.js b/spec/helpers/default-timeout.js new file mode 100644 index 0000000000..e2a5901e0b --- /dev/null +++ b/spec/helpers/default-timeout.js @@ -0,0 +1,5 @@ +if (process.env.CI) { + jasmine.DEFAULT_TIMEOUT_INTERVAL = 120000; +} else { + jasmine.DEFAULT_TIMEOUT_INTERVAL = 5000; +} diff --git a/spec/helpers/deprecation-snapshots.js b/spec/helpers/deprecation-snapshots.js new file mode 100644 index 0000000000..d8329f2de5 --- /dev/null +++ b/spec/helpers/deprecation-snapshots.js @@ -0,0 +1,15 @@ +const _ = require("underscore-plus"); +const Grim = require("grim"); + +let grimDeprecationsSnapshot = null; +let stylesDeprecationsSnapshot = null; + +jasmine.snapshotDeprecations = function() { + grimDeprecationsSnapshot = _.clone(Grim.deprecations); + return stylesDeprecationsSnapshot = _.clone(atom.styles.deprecationsBySourcePath); +}; + +jasmine.restoreDeprecationsSnapshot = function() { + Grim.deprecations = grimDeprecationsSnapshot; + return atom.styles.deprecationsBySourcePath = stylesDeprecationsSnapshot; +}; diff --git a/spec/helpers/document-title.js b/spec/helpers/document-title.js new file mode 100644 index 0000000000..63098a13db --- /dev/null +++ b/spec/helpers/document-title.js @@ -0,0 +1,12 @@ +// Allow document.title to be assigned in specs without screwing up spec window title +let documentTitle = null; + +Object.defineProperty(document, 'title', { + get() { + return documentTitle; + }, + set(title) { + return documentTitle = title; + } + } +); diff --git a/spec/helpers/fixture-packages.js b/spec/helpers/fixture-packages.js new file mode 100644 index 0000000000..242d3586e1 --- /dev/null +++ b/spec/helpers/fixture-packages.js @@ -0,0 +1,4 @@ +const path = require('path') + +const fixturePackagesPath = path.resolve(__dirname, '../fixtures/packages'); +atom.packages.packageDirPaths.unshift(fixturePackagesPath); diff --git a/spec/jasmine-list-reporter.js b/spec/helpers/jasmine-list-reporter.js similarity index 100% rename from spec/jasmine-list-reporter.js rename to spec/helpers/jasmine-list-reporter.js diff --git a/spec/helpers/jasmine-singleton.js b/spec/helpers/jasmine-singleton.js new file mode 100644 index 0000000000..f871baa00c --- /dev/null +++ b/spec/helpers/jasmine-singleton.js @@ -0,0 +1,16 @@ +let jasmine; + +jasmineVendor = require('../../vendor/jasmine'); +for (let key in jasmineVendor) { window[key] = jasmineVendor[key]; } + +jasmine = jasmineVendor.jasmine; + +require('jasmine-json'); + +if ( !jasmine.TerminalReporter ) { + const { jasmineNode} = require('jasmine-node/lib/jasmine-node/reporter'); + + jasmine.TerminalReporter = jasmineNode.TerminalReporter; +} + +module.exports = jasmine; diff --git a/spec/atom-reporter.js b/spec/helpers/jasmine1-atom-reporter.js similarity index 99% rename from spec/atom-reporter.js rename to spec/helpers/jasmine1-atom-reporter.js index d67d2a305a..553229b4ad 100644 --- a/spec/atom-reporter.js +++ b/spec/helpers/jasmine1-atom-reporter.js @@ -12,8 +12,8 @@ const path = require('path'); const process = require('process'); const _ = require('underscore-plus'); const grim = require('grim'); -const listen = require('../src/delegated-listener'); -const ipcHelpers = require('../src/ipc-helpers'); +const listen = require('../../src/delegated-listener'); +const ipcHelpers = require('../../src/ipc-helpers'); const formatStackTrace = function(spec, message, stackTrace) { if (message == null) { message = ''; } diff --git a/spec/spec-helper.js b/spec/helpers/jasmine1-spec-helper.js similarity index 96% rename from spec/spec-helper.js rename to spec/helpers/jasmine1-spec-helper.js index 6917c55301..16ef5fc6a7 100644 --- a/spec/spec-helper.js +++ b/spec/helpers/jasmine1-spec-helper.js @@ -8,9 +8,9 @@ * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md */ let specDirectory, specPackageName, specPackagePath, specProjectPath; -require('jasmine-json'); -require('../src/window'); -require('../vendor/jasmine-jquery'); +require('./jasmine-singleton'); +require('../../src/window'); +require('../../vendor/jasmine-jquery'); const path = require('path'); const _ = require('underscore-plus'); const fs = require('fs-plus'); @@ -19,18 +19,18 @@ const pathwatcher = require('pathwatcher'); const FindParentDir = require('find-parent-dir'); const {CompositeDisposable} = require('event-kit'); -const TextEditor = require('../src/text-editor'); -const TextEditorElement = require('../src/text-editor-element'); -const TextMateLanguageMode = require('../src/text-mate-language-mode'); -const TreeSitterLanguageMode = require('../src/tree-sitter-language-mode'); +const TextEditor = require('../../src/text-editor'); +const TextEditorElement = require('../../src/text-editor-element'); +const TextMateLanguageMode = require('../../src/text-mate-language-mode'); +const TreeSitterLanguageMode = require('../../src/tree-sitter-language-mode'); const {clipboard} = require('electron'); -const {mockDebounce} = require("./spec-helper-functions.js"); +const {mockDebounce} = require("./mock-debounce.js"); const jasmineStyle = document.createElement('style'); jasmineStyle.textContent = atom.themes.loadStylesheet(atom.themes.resolveStylesheet('../static/jasmine')); document.head.appendChild(jasmineStyle); -const fixturePackagesPath = path.resolve(__dirname, './fixtures/packages'); +const fixturePackagesPath = path.resolve(__dirname, '../fixtures/packages'); atom.packages.packageDirPaths.unshift(fixturePackagesPath); document.querySelector('html').style.overflow = 'auto'; diff --git a/spec/helpers/jasmine2-atom-reporter.js b/spec/helpers/jasmine2-atom-reporter.js new file mode 100644 index 0000000000..7b2e84853a --- /dev/null +++ b/spec/helpers/jasmine2-atom-reporter.js @@ -0,0 +1,371 @@ +const path = require('path'); +const process = require('process'); +const listen = require('../../src/delegated-listener'); +const ipcHelpers = require('../../src/ipc-helpers'); + +function formatStackTrace(spec, message = '', stackTrace) { + if (!stackTrace) { return stackTrace; } + + // at ... (.../jasmine.js:1:2) + const jasminePattern = /^\s*at\s+.*\(?.*[/\\]jasmine(-[^/\\]*)?\.js:\d+:\d+\)?\s*$/; + // at jasmine.Something... (.../jasmine.js:1:2) + const firstJasmineLinePattern = /^\s*at\s+jasmine\.[A-Z][^\s]*\s+\(?.*[/\\]jasmine(-[^/\\]*)?\.js:\d+:\d+\)?\s*$/; + let lines = []; + for (let line of stackTrace.split('\n')) { + if (firstJasmineLinePattern.test(line)) { break; } + if (!jasminePattern.test(line)) { lines.push(line); } + } + + // Remove first line of stack when it is the same as the error message + const errorMatch = lines[0]?.match(/^Error: (.*)/); + if (message.trim() === errorMatch?.[1]?.trim()) { lines.shift(); } + + lines = lines.map(function (line) { + // Only format actual stacktrace lines + if (/^\s*at\s/.test(line)) { + // Needs to occur before path relativization + if ((process.platform === 'win32') && /file:\/\/\//.test(line)) { + // file:///C:/some/file -> C:\some\file + line = line + .replace('file:///', '') + .replace(new RegExp(`${path.posix.sep}`, 'g'), path.win32.sep); + } + + line = line.trim() + // at jasmine.Spec. (path:1:2) -> at path:1:2 + .replace(/^at jasmine\.Spec\. \(([^)]+)\)/, 'at $1') + // at jasmine.Spec.it (path:1:2) -> at path:1:2 + .replace(/^at jasmine\.Spec\.f*it \(([^)]+)\)/, 'at $1') + // at it (path:1:2) -> at path:1:2 + .replace(/^at f*it \(([^)]+)\)/, 'at $1') + // at spec/file-test.js -> at file-test.js + .replace(spec.specDirectory + path.sep, ''); + } + + return line; + }); + + return lines.join('\n').trim(); +} + +// Spec objects in the reporter lifecycle don't have all the metadata we need. +// We'll store the full objects in this map, then look them up as needed by ID. +const REGISTRY = new Map(); + +class AtomReporter { + constructor() { + this.startedAt = null; + this.runningSpecCount = 0; + this.completeSpecCount = 0; + this.passedCount = 0; + this.failedCount = 0; + this.skippedCount = 0; + this.totalSpecCount = 0; + this.deprecationCount = 0; + this.timeoutId = 0; + this.element = document.createElement('div'); + this.element.classList.add('spec-reporter-container'); + this.element.innerHTML = `\ +
+
+ +
+
+
+
    +
    +
    +
    +
    +
    +
    +
    + +
    +
    \ +`; + + for (let element of Array.from(this.element.querySelectorAll('[outlet]'))) { + this[element.getAttribute('outlet')] = element; + } + } + + jasmineStarted(_suiteInfo) { + let topSuite = jasmine.getEnv().topSuite(); + topSuite._isTopSuite = true; + this.specs = this.getSpecs(topSuite); + + this.handleEvents(); + this.startedAt = Date.now(); + this.totalSpecCount = Object.keys(this.specs).length; + + // Create summary dots for each test. + for (let spec of Object.values(this.specs)) { + const symbol = document.createElement('li'); + symbol.setAttribute('id', `spec-summary-${spec.id}`); + symbol.setAttribute('title', this.specTitle(spec)); + symbol.className = "spec-summary pending"; + this.userSummary.appendChild(symbol); + } + + document.body.appendChild(this.element); + } + + getSpecs(suite, specs = {}) { + for (const child of suite.children) { + if (child.children) { + specs = this.getSpecs(child, specs); + } else { + REGISTRY.set(child.id, child); + child.suite = suite; + child.suites = this.specSuites(child, suite); + child.title = this.specTitle(child); + specs[child.id] = child; + } + } + return specs; + } + + specSuites(spec, parentSuite) { + const suites = []; + spec.suite ??= parentSuite; + + let { suite } = spec; + while (suite.parentSuite) { + suites.unshift({ + id: suite.id, + description: suite.result.description + }); + suite = suite.parentSuite; + } + return suites; + } + + suiteStarted(_result) {} + + specStarted(_spec) { + this.runningSpecCount++; + } + + jasmineDone() { + this.updateSpecCounts(); + if (this.failedCount === 0) { + this.status.classList.add('alert-success'); + this.status.classList.remove('alert-info'); + } + + if (this.failedCount === 1) { + this.message.textContent = `${this.failedCount} failure`; + } else { + this.message.textContent = `${this.failedCount} failures`; + } + } + + handleEvents() { + listen(document, 'click', '.spec-toggle', function (event) { + const specFailures = event.currentTarget.parentElement.querySelector('.spec-failures'); + + if (specFailures.style.display === 'none') { + specFailures.style.display = ''; + event.currentTarget.classList.remove('folded'); + } else { + specFailures.style.display = 'none'; + event.currentTarget.classList.add('folded'); + } + + event.preventDefault(); + }); + + listen(document, 'click', '.deprecation-list', function (event) { + const deprecationList = event.currentTarget.parentElement.querySelector('.deprecation-list'); + + if (deprecationList.style.display === 'none') { + deprecationList.style.display = ''; + event.currentTarget.classList.remove('folded'); + } else { + deprecationList.style.display = 'none'; + event.currentTarget.classList.add('folded'); + } + + event.preventDefault(); + }); + + listen(document, 'click', '.stack-trace', event => event.currentTarget.classList.toggle('expanded')); + + this.reloadButton.addEventListener('click', () => ipcHelpers.call('window-method', 'reload')); + } + + updateSpecCounts() { + let specCount; + if (this.skippedCount) { + specCount = `${this.completeSpecCount - this.skippedCount}/${this.totalSpecCount - this.skippedCount} (${this.skippedCount} skipped)`; + } else { + specCount = `${this.completeSpecCount}/${this.totalSpecCount}`; + } + this.specCount.textContent = specCount; + } + + updateStatusView(spec) { + if (this.failedCount > 0) { + this.status.classList.add('alert-danger'); + this.status.classList.remove('alert-info'); + } + let fullSpec = REGISTRY.get(spec.id); + + this.updateSpecCounts(); + + let rootSuite = fullSpec.suite; + while (rootSuite.parentSuite) { + if (rootSuite.parentSuite._isTopSuite) break; + rootSuite = rootSuite.parentSuite; + } + this.message.textContent = rootSuite.description; + + let time = `${Math.round((spec.endedAt - this.startedAt) / 10)}`; + if (time.length < 3) { time = `0${time}`; } + this.time.textContent = `${time.slice(0, -2)}.${time.slice(-2)}s`; + } + + specTitle(spec) { + const parentDescs = []; + let s = spec.suite; + while (s && !s._isTopSuite) { + parentDescs.unshift(s.description); + s = s.parentSuite; + } + + let suiteString = ""; + let indent = ""; + for (let desc of parentDescs) { + suiteString += indent + desc + "\n"; + indent += " "; + } + + return `${suiteString} ${indent} it ${spec.description}`; + } + + suiteDone(_suite) {} + + specDone(spec) { + const specSummaryElement = document.getElementById(`spec-summary-${spec.id}`); + if (!specSummaryElement) { + console.warn(`Does not exist:`, spec.id); + return; + } + specSummaryElement.classList.remove('pending'); + switch (spec.status) { + case 'disabled': + specSummaryElement.classList.add('skipped'); + this.skippedCount++; + break; + case 'failed': { + specSummaryElement.classList.add('failed'); + const specView = new SpecResultView(spec); + specView.attach(); + this.failedCount++; + break; + } + case 'passed': + specSummaryElement.classList.add('passed'); + this.passedCount++; + break; + default: + // no-op + } + + this.completeSpecCount++; + spec.endedAt = Date.now(); + if (spec.status !== 'disabled') { + this.updateStatusView(spec); + } + } +} + +module.exports = AtomReporter; + +class SuiteResultView { + constructor(suite) { + this.suite = suite; + this.element = document.createElement('div'); + this.element.className = 'suite'; + this.element.setAttribute('id', `suite-view-${this.suite.id}`); + this.description = document.createElement('div'); + this.description.className = 'description'; + this.description.textContent = this.suite.description; + this.element.appendChild(this.description); + } + + attach() { + (this.parentSuiteView() || document.querySelector('.results')).appendChild(this.element); + } + + parentSuiteView() { + let suiteViewElement; + if (!this.suite.parentSuite || this.suite.parentSuite._isTopSuite) { return; } + + if (!(suiteViewElement = document.querySelector(`#suite-view-${this.suite.parentSuite.id}`))) { + const suiteView = new SuiteResultView(this.suite.parentSuite); + suiteView.attach(); + suiteViewElement = suiteView.element; + } + + return suiteViewElement; + } +} + +class SpecResultView { + constructor(spec) { + this.spec = spec; + this.element = document.createElement('div'); + this.element.className = 'spec'; + this.element.innerHTML = `\ +
    +
    +
    \ +`; + this.description = this.element.querySelector('[outlet="description"]'); + this.specFailures = this.element.querySelector('[outlet="specFailures"]'); + + this.element.classList.add(`spec-view-${this.spec.id}`); + + let { + description + } = this.spec; + if (description.indexOf('it ') !== 0) { description = `it ${description}`; } + this.description.textContent = description; + + for (let result of this.spec.failedExpectations) { + let stackTrace = formatStackTrace(this.spec, result.message, result.stack); + const resultElement = document.createElement('div'); + resultElement.className = 'result-message fail'; + resultElement.textContent = result.message; + this.specFailures.appendChild(resultElement); + + if (stackTrace) { + const traceElement = document.createElement('pre'); + traceElement.className = 'stack-trace padded'; + traceElement.textContent = stackTrace; + this.specFailures.appendChild(traceElement); + } + } + } + + attach() { + this.parentSuiteView().appendChild(this.element); + } + + parentSuiteView() { + let suiteViewElement; + let fullSpec = REGISTRY.get(this.spec.id) + if (!(suiteViewElement = document.querySelector(`#suite-view-${fullSpec.suite.id}`))) { + const suiteView = new SuiteResultView(fullSpec.suite); + suiteView.attach(); + suiteViewElement = suiteView.element; + } + + return suiteViewElement; + } +} diff --git a/spec/helpers/jasmine2-custom-matchers.js b/spec/helpers/jasmine2-custom-matchers.js new file mode 100644 index 0000000000..68f1b93453 --- /dev/null +++ b/spec/helpers/jasmine2-custom-matchers.js @@ -0,0 +1,167 @@ +const _ = require("underscore-plus"); +const fs = require("fs-plus"); +const path = require("path"); + +exports.register = (jasmineEnv) => { + jasmineEnv.beforeEach(function () { + jasmineEnv.addCustomEqualityTester(function (a, b) { + // Match jasmine.any's equality matching logic + if ((a != null ? a.jasmineMatches : undefined) != null) { + return a.jasmineMatches(b); + } + if ((b != null ? b.jasmineMatches : undefined) != null) { + return b.jasmineMatches(a); + } + + // Use underscore's definition of equality for toEqual assertions + return _.isEqual(a, b); + }); + + jasmineEnv.addMatchers({ + toHaveLength: function (util, customEqualityTesters) { + return { + compare: function (actual, expected) { + if (actual == null) { + return { + pass: false, + message: `Expected object ${actual} has no length method`, + }; + } else { + return { + pass: actual.length === expected, + message: `Expected object with length ${actual.length} to have length ${expected}`, + }; + } + }, + } + }, + + toExistOnDisk: function (util, customEqualityTesters) { + return { + compare: function (actual) { + return { + pass: fs.existsSync(actual), + message: `Expected path '${actual}' to exist.`, + }; + }, + } + }, + + toHaveFocus: function (util, customEqualityTesters) { + return { + compare: function (actual) { + if (!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."); + } + + let element = actual; + if (element.jquery) { + element = element.get(0); + } + + return { + pass: (element === document.activeElement) || element.contains(document.activeElement), + message: `Expected element '${actual}' or its descendants to have focus.`, + }; + }, + } + }, + + toShow: function (util, customEqualityTesters) { + return { + compare: function (actual) { + let element = actual; + if (element.jquery) { + element = element.get(0); + } + const computedStyle = getComputedStyle(element); + + return { + pass: (computedStyle.display !== 'none') && (computedStyle.visibility === 'visible') && !element.hidden, + message: `Expected element '${element}' or its descendants to show.`, + }; + }, + } + }, + + toEqualPath: function (util, customEqualityTesters) { + return { + compare: function (actual, expected) { + const actualPath = path.normalize(actual); + const expectedPath = path.normalize(expected); + + return { + pass: actualPath === expectedPath, + message: `Expected path '${actualPath}' to be equal to '${expectedPath}'.`, + }; + }, + } + }, + + toBeNear: function (util, customEqualityTesters) { + return { + compare: function (actual, expected) { + let acceptedError = 1; + + return { + pass: ((expected - acceptedError) <= actual) && (actual <= (expected + acceptedError)), + message: `Expected '${actual}' to be near to '${expected}'.`, + }; + }, + } + }, + + toHaveNearPixels: function (util, customEqualityTesters) { + return { + compare: function (actual, expected) { + let acceptedError = 1; + + const expectedNumber = parseFloat(expected); + const actualNumber = parseFloat(actual); + + return { + pass: (expected.indexOf('px') >= 1) && (actual.indexOf('px') >= 1) && ((expectedNumber - acceptedError) <= actualNumber) && (actualNumber <= (expectedNumber + acceptedError)), + message: `Expected '${actual}' to have near pixels to '${expected}'.`, + } + } + } + }, + + toHaveClass: function (util, customEqualityTesters) { + return { + compare: function (actual, expected) { + return { + pass: actual instanceof HTMLElement && actual.classList.contains(expected), + message: `Expected '${actual}' to have '${expected}' class` + } + } + } + }, + + toHaveText: function (util, customEqualityTesters) { + return { + compare: function (actual, expected) { + return { + pass: actual instanceof HTMLElement && actual.textContent == expected, + message: `Expected '${actual}' to have text: '${expected}'` + } + } + } + }, + + toExist: function (util, customEqualityTesters) { + return { + compare: function (actual) { + if (actual instanceof HTMLElement) { + return {pass: true} + } else if (actual) { + return {pass: actual.size() > 0} + } else { + return {pass: false} + } + } + } + } + }); + }); +} diff --git a/spec/helpers/jasmine2-singleton.js b/spec/helpers/jasmine2-singleton.js new file mode 100644 index 0000000000..12b1f0576f --- /dev/null +++ b/spec/helpers/jasmine2-singleton.js @@ -0,0 +1,6 @@ +let Jasmine = require('jasmine'); +let jasmine = new Jasmine(); + +window['jasmine'] = jasmine.jasmine + +module.exports = jasmine; diff --git a/spec/helpers/jasmine2-spies.js b/spec/helpers/jasmine2-spies.js new file mode 100644 index 0000000000..f2b5abf17a --- /dev/null +++ b/spec/helpers/jasmine2-spies.js @@ -0,0 +1,99 @@ +const FindParentDir = require("find-parent-dir"); +const path = require("path"); +const _ = require("underscore-plus"); +const TextEditorElement = require("../../src/text-editor-element"); +const pathwatcher = require("pathwatcher"); +const TextEditor = require("../../src/text-editor"); +const TextMateLanguageMode = require("../../src/text-mate-language-mode"); +const TreeSitterLanguageMode = require("../../src/tree-sitter-language-mode"); +const {CompositeDisposable} = require("event-kit"); +const {clipboard} = require("electron"); + +const {testPaths} = atom.getLoadSettings(); +let specPackagePath = FindParentDir.sync(testPaths[0], 'package.json') + +let specPackageName; +if (specPackagePath) { + const packageMetadata = require(path.join(specPackagePath, 'package.json')); + specPackageName = packageMetadata.name; +} + +let specDirectory = FindParentDir.sync(testPaths[0], 'fixtures'); +let specProjectPath; +if (specDirectory) { + specProjectPath = path.join(specDirectory, 'fixtures'); +} else { + specProjectPath = require('os').tmpdir(); +} + +exports.register = (jasmineEnv) => { + jasmineEnv.beforeEach(function () { + // Do not clobber recent project history + spyOn(Object.getPrototypeOf(atom.history), 'saveState').and.returnValue(Promise.resolve()); + + atom.project.setPaths([specProjectPath]); + + atom.packages._originalResolvePackagePath = atom.packages.resolvePackagePath; + const spy = spyOn(atom.packages, 'resolvePackagePath') + spy.and.callFake(function (packageName) { + if (specPackageName && (packageName === specPackageName)) { + return atom.packages._originalResolvePackagePath(specPackagePath); + } else { + return atom.packages._originalResolvePackagePath(packageName); + } + }); + + // 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.calls.reset(); + + // make editor display updates synchronous + TextEditorElement.prototype.setUpdatedSynchronously(true); + + spyOn(pathwatcher.File.prototype, "detectResurrectionAfterDelay").and.callFake(function () { + return this.detectResurrection(); + }); + spyOn(TextEditor.prototype, "shouldPromptToSave").and.returnValue(false); + + // make tokenization synchronous + TextMateLanguageMode.prototype.chunkSize = Infinity; + TreeSitterLanguageMode.prototype.syncTimeoutMicros = Infinity; + spyOn(TextMateLanguageMode.prototype, "tokenizeInBackground").and.callFake(function () { + return this.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").and.callFake(function (callback) { + return new CompositeDisposable( + this.emitter.on("did-tokenize", callback), + this.onDidChangeGrammar(() => { + const languageMode = this.buffer.getLanguageMode(); + if (languageMode.tokenizeInBackground != null ? languageMode.tokenizeInBackground.originalValue : undefined) { + return callback(); + } + }) + ); + }); + + let clipboardContent = 'initial clipboard content'; + spyOn(clipboard, 'writeText').and.callFake(text => clipboardContent = text); + spyOn(clipboard, 'readText').and.callFake(() => clipboardContent); + }); +} + +jasmine.unspy = function(object, methodName) { + object[methodName].and.callThrough(); +}; diff --git a/spec/helpers/jasmine2-time.js b/spec/helpers/jasmine2-time.js new file mode 100644 index 0000000000..e61cf1c933 --- /dev/null +++ b/spec/helpers/jasmine2-time.js @@ -0,0 +1,91 @@ +const _ = require("underscore-plus"); +const { mockDebounce } = require("../helpers/mock-debounce"); + +jasmine.useRealClock = function() { + jasmine.unspy(window, 'setTimeout'); + jasmine.unspy(window, 'clearTimeout'); + jasmine.unspy(window, 'setInterval'); + jasmine.unspy(window, 'clearInterval'); + jasmine.unspy(_._, 'now'); + jasmine.unspy(Date, 'now'); +}; + +let now; +let timeoutCount; +let intervalCount; +let timeouts; +let intervalTimeouts; + +const resetTimeouts = function() { + now = 0; + timeoutCount = 0; + intervalCount = 0; + timeouts = []; + intervalTimeouts = {}; +}; + +const fakeSetTimeout = function(callback, ms) { + if (ms == null) { ms = 0; } + const id = ++timeoutCount; + timeouts.push([id, now + ms, callback]); + return id; +}; + +const fakeClearTimeout = (idToClear) => { + timeouts = timeouts.filter(function (...args) { + const [id] = Array.from(args[0]); + return id !== idToClear; + }); +} + +const fakeSetInterval = function(callback, ms) { + const id = ++intervalCount; + var action = function() { + callback(); + return intervalTimeouts[id] = fakeSetTimeout(action, ms); + }; + intervalTimeouts[id] = fakeSetTimeout(action, ms); + return id; +}; + +fakeClearInterval = function(idToClear) { + fakeClearTimeout(intervalTimeouts[idToClear]); +}; + +window.advanceClock = function(delta) { + if (delta == null) { delta = 1; } + now += delta; + const callbacks = []; + + timeouts = timeouts.filter(function(...args) { + let id, strikeTime; + let callback; + [id, strikeTime, callback] = Array.from(args[0]); + if (strikeTime <= now) { + callbacks.push(callback); + return false; + } else { + return true; + } + }); + + return (() => { + const result = []; + for (let callback of Array.from(callbacks)) { result.push(callback()); + } + return result; + })(); +}; + +exports.register = (jasmineEnv) => { + jasmineEnv.beforeEach(() => { + resetTimeouts(); + spyOn(_._, "now").and.callFake(() => now); + spyOn(Date, 'now').and.callFake(() => now); + spyOn(window, "setTimeout").and.callFake(fakeSetTimeout); + spyOn(window, "clearTimeout").and.callFake(fakeClearTimeout); + spyOn(window, 'setInterval').and.callFake(fakeSetInterval); + spyOn(window, 'clearInterval').and.callFake(fakeClearInterval); + spyOn(_, "debounce").and.callFake(mockDebounce); + }) +} diff --git a/spec/helpers/jasmine2-warnings.js b/spec/helpers/jasmine2-warnings.js new file mode 100644 index 0000000000..24e100896b --- /dev/null +++ b/spec/helpers/jasmine2-warnings.js @@ -0,0 +1,21 @@ +const { + ensureNoDeprecatedFunctionCalls, + ensureNoDeprecatedStylesheets, + warnIfLeakingPathSubscriptions +} = require('./warnings') + +exports.register = (jasmineEnv) => { + jasmineEnv.afterEach(async (done) => { + ensureNoDeprecatedFunctionCalls(); + ensureNoDeprecatedStylesheets(); + + await atom.reset(); + + if (!window.debugContent) { + document.getElementById('jasmine-content').innerHTML = ''; + } + warnIfLeakingPathSubscriptions(); + + done(); + }); +} diff --git a/spec/helpers/load-jasmine-stylesheet.js b/spec/helpers/load-jasmine-stylesheet.js new file mode 100644 index 0000000000..0977c4c372 --- /dev/null +++ b/spec/helpers/load-jasmine-stylesheet.js @@ -0,0 +1,6 @@ +const jasmineStyle = document.createElement('style'); +jasmineStyle.textContent = atom.themes.loadStylesheet(atom.themes.resolveStylesheet('../static/jasmine')); +document.head.appendChild(jasmineStyle); + +document.querySelector('html').style.overflow = 'auto'; +document.body.style.overflow = 'auto'; diff --git a/spec/spec-helper-functions.js b/spec/helpers/mock-debounce.js similarity index 100% rename from spec/spec-helper-functions.js rename to spec/helpers/mock-debounce.js diff --git a/spec/helpers/mock-local-storage.js b/spec/helpers/mock-local-storage.js new file mode 100644 index 0000000000..30a8f03c1f --- /dev/null +++ b/spec/helpers/mock-local-storage.js @@ -0,0 +1,6 @@ +exports.mockLocalStorage = function() { + const items = {}; + spyOn(global.localStorage, 'setItem').and.callFake(function(key, item) { items[key] = item.toString(); return undefined; }); + spyOn(global.localStorage, 'getItem').and.callFake(key => items[key] != null ? items[key] : null); + return spyOn(global.localStorage, 'removeItem').and.callFake(function(key) { delete items[key]; return undefined; }); +}; diff --git a/spec/helpers/normalize-comments.js b/spec/helpers/normalize-comments.js new file mode 100644 index 0000000000..919c35d45c --- /dev/null +++ b/spec/helpers/normalize-comments.js @@ -0,0 +1,141 @@ +// This will normalize the comments for the special format of grammar tests +// that TextMate and Tree-Sitter do +// +// Basically, receiving a text editor and the regex that probably defines +// what a comment is, it'll return an object with `expect` - that is what was +// expected to pass the test, like a scope description for example, and two +// Point-compatible fields - `editorPosition`, that is basically in what +// position of the editor `expect` should be satisfied, and `testPosition`, that +// is where in file the test actually happened. This makes it easier for us +// to construct an error showing where EXACTLY was the assertion that failed +function normalizeTreeSitterTextData(editor, commentRegex) { + let allMatches = [], lastNonComment = 0 + const checkAssert = new RegExp('^\\s*' + commentRegex.source + '\\s*[\\<\\-|\\^]') + editor.getBuffer().getLines().forEach((row, i) => { + const m = row.match(commentRegex) + if(m) { + // const scope = editor.scopeDescriptorForBufferPosition([i, m.index]) + // FIXME: use editor.scopeDescriptorForBufferPosition when it works + const scope = editor.tokensForScreenRow(i) + const scopes = scope.flatMap(e => e.scopes) + if(scopes.find(s => s.match(/comment/)) && row.match(checkAssert)) { + allMatches.push({row: lastNonComment, text: row, col: m.index, testRow: i}) + return + } + } + lastNonComment = i + }) + return allMatches.map(({text, row, col, testRow}) => { + const exactPos = text.match(/\^\s+(.*)/) + if(exactPos) { + const expected = exactPos[1] + return { + expected, + editorPosition: {row, column: exactPos.index}, + testPosition: {row: testRow, column: col} + } + } else { + const pos = text.match(/\<-\s+(.*)/) + if(!pos) throw new Error(`Can't match ${text}`) + return { + expected: pos[1], + editorPosition: {row, column: col}, + testPosition: {row: testRow, column: col} + } + } + }) +} +exports.normalizeTreeSitterTextData = normalizeTreeSitterTextData; +window.normalizeTreeSitterTextData = normalizeTreeSitterTextData + +async function openDocument(fullPath) { + const editor = await atom.workspace.open(fullPath); + await editor.languageMode.ready; + return editor; +} + +async function runGrammarTests(fullPath, commentRegex) { + const editor = await openDocument(fullPath); + + const normalized = normalizeTreeSitterTextData(editor, commentRegex) + expect(normalized.length).toSatisfy((n, reason) => { + reason("Tokenizer didn't run correctly - could not find any comment") + return n > 0 + }) + normalized.forEach(({expected, editorPosition, testPosition}) => { + expect(editor.scopeDescriptorForBufferPosition(editorPosition).scopes).toSatisfy((scopes, reason) => { + const dontFindScope = expected.startsWith("!"); + expected = expected.replace(/^!/, "") + if(dontFindScope) { + reason(`Expected to NOT find scope "${expected}" but found it\n` + + ` at ${fullPath}:${testPosition.row+1}:${testPosition.column+1}` + ); + } else { + reason(`Expected to find scope "${expected}" but found "${scopes}"\n` + + ` at ${fullPath}:${testPosition.row+1}:${testPosition.column+1}` + ); + } + const normalized = expected.replace(/([\.\-])/g, '\\$1'); + const scopeRegex = new RegExp('^' + normalized + '(\\..+)?$'); + let result = scopes.find(e => e.match(scopeRegex)) !== undefined; + if(dontFindScope) result = !result; + return result + }) + }) +} +exports.runGrammarTests = runGrammarTests; +window.runGrammarTests = runGrammarTests; + +async function runFoldsTests(fullPath, commentRegex) { + const editor = await openDocument(fullPath); + let grouped = {} + const normalized = normalizeTreeSitterTextData(editor, commentRegex).forEach(test => { + const [kind, id] = test.expected.split('.') + if(!kind || !id) { + throw new Error(`Folds must be in the format fold_end.some-id\n` + + ` at ${test.testPosition.row+1}:${test.testPosition.column+1}`) + } + grouped[id] ||= {} + grouped[id][kind] = test + }) + for(const k in grouped) { + const v = grouped[k] + const keys = Object.keys(v) + if(keys.indexOf('fold_begin') === -1) + throw new Error(`Fold ${k} must contain fold_begin`) + if(keys.indexOf('fold_end') === -1) + throw new Error(`Fold ${k} must contain fold_end`) + if(keys.indexOf('fold_new_position') === -1) + throw new Error(`Fold ${k} must contain fold_new_position`) + } + + for(const k in grouped) { + const fold = grouped[k] + const begin = fold['fold_begin'] + const end = fold['fold_end'] + const newPos = fold['fold_new_position'] + + expect(editor.isFoldableAtBufferRow(begin.editorPosition.row)) + .toSatisfy((foldable, reason) => { + reason(`Editor is not foldable at row ${begin.editorPosition.row+1}\n` + + ` at ${fullPath}:${begin.testPosition.row+1}:${begin.testPosition.column+1}`) + return foldable + }) + editor.foldBufferRow(begin.editorPosition.row) + + expect(editor.screenPositionForBufferPosition(end.editorPosition)) + .toSatisfy((screenPosition, reason) => { + const {row,column} = newPos.editorPosition + reason(`At row ${begin.editorPosition.row+1}, editor should fold ` + + `up to the ${end.editorPosition.row+1}:${end.editorPosition.column+1}\n` + + ` into the new position ${row+1}:${column+1}\n`+ + ` but folded to position ${screenPosition.row+1}:${screenPosition.column+1}\n`+ + ` at ${fullPath}:${newPos.testPosition.row+1}:${newPos.testPosition.column+1}\n` + + ` at ${fullPath}:${end.testPosition.row+1}:${end.testPosition.column+1}`) + return row === screenPosition.row && column === screenPosition.column + }) + editor.unfoldAll() + } +} +exports.runFoldsTests = runFoldsTests; +window.runFoldsTests = runFoldsTests; diff --git a/spec/helpers/platform-filter.js b/spec/helpers/platform-filter.js new file mode 100644 index 0000000000..6a9fc8a1d0 --- /dev/null +++ b/spec/helpers/platform-filter.js @@ -0,0 +1,11 @@ +jasmine.filterByPlatform = ({only, except}, done) => { + if (only && !only.includes(process.platform)) { + done(); + pending(); + } + + if (except && except.includes(process.platform)) { + done(); + pending(); + } +} diff --git a/spec/spec-helper-platform.js b/spec/helpers/platform.js similarity index 92% rename from spec/spec-helper-platform.js rename to spec/helpers/platform.js index b552ec30ee..f992d40611 100644 --- a/spec/spec-helper-platform.js +++ b/spec/helpers/platform.js @@ -14,7 +14,7 @@ module.exports = { // Returns nothing. generateEvilFiles() { let filenames; - const evilFilesPath = path.join(__dirname, 'fixtures', 'evil-files'); + const evilFilesPath = path.join(__dirname, '..', 'fixtures', 'evil-files'); if (fs.existsSync(evilFilesPath)) { fs.removeSync(evilFilesPath); } diff --git a/spec/helpers/set-prototype-extensions.js b/spec/helpers/set-prototype-extensions.js new file mode 100644 index 0000000000..01955d5b25 --- /dev/null +++ b/spec/helpers/set-prototype-extensions.js @@ -0,0 +1,17 @@ +Set.prototype.jasmineToString = function() { + return `Set {${[...this.values()].join(', ')}}`; +}; + +Set.prototype.isEqual = function(other) { + if (other instanceof Set) { + let next; + if (this.size !== other.size) { return false; } + const values = this.values(); + while (!(next = values.next()).done) { + if (!other.has(next.value)) { return false; } + } + return true; + } else { + return false; + } +}; diff --git a/spec/helpers/warnings.js b/spec/helpers/warnings.js new file mode 100644 index 0000000000..0ce07ff26d --- /dev/null +++ b/spec/helpers/warnings.js @@ -0,0 +1,56 @@ +const pathwatcher = require("pathwatcher"); +const _ = require("underscore-plus"); +const Grim = require("grim"); + +exports.warnIfLeakingPathSubscriptions = function() { + const watchedPaths = pathwatcher.getWatchedPaths(); + if (watchedPaths.length > 0) { + console.error("WARNING: Leaking subscriptions for paths: " + watchedPaths.join(", ")); + } + return pathwatcher.closeAllWatchers(); +}; + +exports.ensureNoDeprecatedFunctionCalls = function() { + const deprecations = _.clone(Grim.getDeprecations()); + Grim.clearDeprecations(); + if (deprecations.length > 0) { + const originalPrepareStackTrace = Error.prepareStackTrace; + Error.prepareStackTrace = function(error, stack) { + const output = []; + for (let deprecation of Array.from(deprecations)) { + output.push(`${deprecation.originName} is deprecated. ${deprecation.message}`); + output.push(_.multiplyString("-", output[output.length - 1].length)); + for (stack of Array.from(deprecation.getStacks())) { + for (let {functionName, location} of Array.from(stack)) { + output.push(`${functionName} -- ${location}`); + } + } + output.push(""); + } + return output.join("\n"); + }; + + const error = new Error(`Deprecated function(s) ${deprecations.map(({originName}) => originName).join(', ')}) were called.`); + error.stack; + Error.prepareStackTrace = originalPrepareStackTrace; + throw error; + } +}; + +exports.ensureNoDeprecatedStylesheets = function() { + const deprecations = _.clone(atom.styles.getDeprecations()); + atom.styles.clearDeprecations(); + return (() => { + const result = []; + for (let sourcePath in deprecations) { + const deprecation = deprecations[sourcePath]; + const title = + sourcePath !== 'undefined' ? + `Deprecated stylesheet at '${sourcePath}':` + : + "Deprecated stylesheet:"; + throw new Error(`${title}\n${deprecation.message}`); + } + return result; + })(); +}; diff --git a/spec/jasmine-junit-reporter.js b/spec/jasmine-junit-reporter.js deleted file mode 100644 index 26c3231653..0000000000 --- a/spec/jasmine-junit-reporter.js +++ /dev/null @@ -1,21 +0,0 @@ -require('jasmine-reporters'); - -class JasmineJUnitReporter extends jasmine.JUnitXmlReporter { - fullDescription(spec) { - let fullDescription = spec.description; - let currentSuite = spec.suite; - while (currentSuite) { - fullDescription = currentSuite.description + ' ' + fullDescription; - currentSuite = currentSuite.parentSuite; - } - - return fullDescription; - } - - reportSpecResults(spec) { - spec.description = this.fullDescription(spec); - return super.reportSpecResults(spec); - } -} - -module.exports = { JasmineJUnitReporter }; diff --git a/spec/jasmine-test-runner.js b/spec/jasmine-test-runner.js index 5c91fb7c81..a77d95f117 100644 --- a/spec/jasmine-test-runner.js +++ b/spec/jasmine-test-runner.js @@ -1,222 +1 @@ -/* - * decaffeinate suggestions: - * DS101: Remove unnecessary use of Array.from - * DS102: Remove unnecessary code created because of implicit returns - * DS202: Simplify dynamic range loops - * DS205: Consider reworking code to avoid use of IIFEs - * DS207: Consider shorter variations of null checks - * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md - */ -const Grim = require('grim'); -const fs = require('fs-plus'); -const temp = require('temp'); -const path = require('path'); -const {ipcRenderer} = require('electron'); - -temp.track(); - -module.exports = function({logFile, headless, testPaths, buildAtomEnvironment}) { - const object = require('../vendor/jasmine'); - for (let key in object) { const value = object[key]; window[key] = value; } - - require('jasmine-tagged'); - - // Rewrite global jasmine functions to have support for async tests. - // This way packages can create async specs without having to import these from the - // async-spec-helpers file. - global.it = asyncifyJasmineFn(global.it, 1); - global.fit = asyncifyJasmineFn(global.fit, 1); - global.ffit = asyncifyJasmineFn(global.ffit, 1); - global.fffit = asyncifyJasmineFn(global.fffit, 1); - global.beforeEach = asyncifyJasmineFn(global.beforeEach, 0); - global.afterEach = asyncifyJasmineFn(global.afterEach, 0); - - // Allow document.title to be assigned in specs without screwing up spec window title - let documentTitle = null; - Object.defineProperty(document, 'title', { - get() { return documentTitle; }, - set(title) { return documentTitle = title; } - } - ); - - const userHome = process.env.ATOM_HOME || path.join(fs.getHomeDirectory(), '.atom'); - const atomHome = temp.mkdirSync({prefix: 'atom-test-home-'}); - if (process.env.APM_TEST_PACKAGES) { - const testPackages = process.env.APM_TEST_PACKAGES.split(/\s+/); - fs.makeTreeSync(path.join(atomHome, 'packages')); - for (let packName of Array.from(testPackages)) { - const userPack = path.join(userHome, 'packages', packName); - const loadablePack = path.join(atomHome, 'packages', packName); - - try { - fs.symlinkSync(userPack, loadablePack, 'dir'); - } catch (error) { - fs.copySync(userPack, loadablePack); - } - } - } - - const ApplicationDelegate = require('../src/application-delegate'); - const applicationDelegate = new ApplicationDelegate(); - applicationDelegate.setRepresentedFilename = function() {}; - applicationDelegate.setWindowDocumentEdited = function() {}; - window.atom = buildAtomEnvironment({ - applicationDelegate, window, document, - configDirPath: atomHome, - enablePersistence: false - }); - - require('./spec-helper'); - if (process.env.JANKY_SHA1 || process.env.CI) { disableFocusMethods(); } - for (let testPath of Array.from(testPaths)) { requireSpecs(testPath); } - - setSpecType('user'); - - let resolveWithExitCode = null; - const promise = new Promise((resolve, reject) => resolveWithExitCode = resolve); - const jasmineEnv = jasmine.getEnv(); - jasmineEnv.addReporter(buildReporter({logFile, headless, resolveWithExitCode})); - - if(process.env.SPEC_FILTER) { - const {getFullDescription} = require('./jasmine-list-reporter'); - const regex = new RegExp(process.env.SPEC_FILTER) - jasmineEnv.specFilter = (spec) => getFullDescription(spec, false).match(regex) - } - - if (process.env.TEST_JUNIT_XML_PATH) { - const {JasmineJUnitReporter} = require('./jasmine-junit-reporter'); - process.stdout.write(`Outputting JUnit XML to <${process.env.TEST_JUNIT_XML_PATH}>\n`); - const outputDir = path.dirname(process.env.TEST_JUNIT_XML_PATH); - const fileBase = path.basename(process.env.TEST_JUNIT_XML_PATH, '.xml'); - - jasmineEnv.addReporter(new JasmineJUnitReporter(outputDir, true, false, fileBase, true)); - } - - jasmineEnv.setIncludedTags([process.platform]); - - const jasmineContent = document.createElement('div'); - jasmineContent.setAttribute('id', 'jasmine-content'); - - document.body.appendChild(jasmineContent); - - jasmineEnv.execute(); - return promise; -}; - -var asyncifyJasmineFn = (fn, callbackPosition) => (function(...args) { - if (typeof args[callbackPosition] === 'function') { - const callback = args[callbackPosition]; - - args[callbackPosition] = function(...args) { - const result = callback.apply(this, args); - if (result instanceof Promise) { - return waitsForPromise(() => result); - } - }; - } - - return fn.apply(this, args); -}); - -var waitsForPromise = function(fn) { - const promise = fn(); - - return global.waitsFor('spec promise to resolve', done => promise.then(done, function(error) { - jasmine.getEnv().currentSpec.fail(error); - return done(); - })); -}; - -var disableFocusMethods = () => ['fdescribe', 'ffdescribe', 'fffdescribe', 'fit', 'ffit', 'fffit'].forEach(function(methodName) { - const focusMethod = window[methodName]; - return window[methodName] = function(description) { - const error = new Error('Focused spec is running on CI'); - return focusMethod(description, function() { throw error; }); - }; -}); - -var requireSpecs = function(testPath, specType) { - if (fs.isDirectorySync(testPath)) { - return (() => { - const result = []; - for (let testFilePath of Array.from(fs.listTreeSync(testPath))) { - if (/-spec\.(coffee|js)$/.test(testFilePath)) { - require(testFilePath); - // Set spec directory on spec for setting up the project in spec-helper - result.push(setSpecDirectory(testPath)); - } - } - return result; - })(); - } else { - require(testPath); - return setSpecDirectory(path.dirname(testPath)); - } -}; - -const setSpecField = function(name, value) { - const specs = jasmine.getEnv().currentRunner().specs(); - if (specs.length === 0) { return; } - return (() => { - const result = []; - for (let start = specs.length-1, index = start, asc = start <= 0; asc ? index <= 0 : index >= 0; asc ? index++ : index--) { - if (specs[index][name] != null) { break; } - result.push(specs[index][name] = value); - } - return result; - })(); -}; - -var setSpecType = specType => setSpecField('specType', specType); - -var setSpecDirectory = specDirectory => setSpecField('specDirectory', specDirectory); - -var buildReporter = function({logFile, headless, resolveWithExitCode}) { - if (headless) { - return buildTerminalReporter(logFile, resolveWithExitCode); - } else { - let reporter; - const AtomReporter = require('./atom-reporter.js'); - return reporter = new AtomReporter(); - } -}; - -var buildTerminalReporter = function(logFile, resolveWithExitCode) { - let logStream; - if (logFile != null) { logStream = fs.openSync(logFile, 'w'); } - const log = function(str) { - if (logStream != null) { - return fs.writeSync(logStream, str); - } else { - return ipcRenderer.send('write-to-stderr', str); - } - }; - - const options = { - print(str) { - return log(str); - }, - onComplete(runner) { - if (logStream != null) { fs.closeSync(logStream); } - if (Grim.getDeprecationsLength() > 0) { - Grim.logDeprecations(); - resolveWithExitCode(1); - return; - } - - if (runner.results().failedCount > 0) { - return resolveWithExitCode(1); - } else { - return resolveWithExitCode(0); - } - } - }; - - if (process.env.ATOM_JASMINE_REPORTER === 'list') { - const {JasmineListReporter} = require('./jasmine-list-reporter'); - return new JasmineListReporter(options); - } else { - const {TerminalReporter} = require('jasmine-tagged'); - return new TerminalReporter(options); - } -}; +module.exports = require('./runners/jasmine1-test-runner'); diff --git a/spec/main-process/atom-application.test.js b/spec/main-process/atom-application.test.js index 1aba2382b1..b67a0b9038 100644 --- a/spec/main-process/atom-application.test.js +++ b/spec/main-process/atom-application.test.js @@ -12,7 +12,7 @@ const parseCommandLine = require('../../src/main-process/parse-command-line'); const { emitterEventPromise, conditionPromise -} = require('../async-spec-helpers'); +} = require('../helpers/async-spec-helpers'); // These tests use a utility class called LaunchScenario, defined below, to manipulate AtomApplication instances that // (1) are stubbed to only simulate AtomWindow creation and (2) allow you to use a shorthand notation to assert the diff --git a/spec/main-process/atom-window.test.js b/spec/main-process/atom-window.test.js index 7b6ecbfc60..9dd71403c0 100644 --- a/spec/main-process/atom-window.test.js +++ b/spec/main-process/atom-window.test.js @@ -9,7 +9,7 @@ const sandbox = require('sinon').createSandbox(); const dedent = require('dedent'); const AtomWindow = require('../../src/main-process/atom-window'); -const { emitterEventPromise } = require('../async-spec-helpers'); +const { emitterEventPromise } = require('../helpers/async-spec-helpers'); describe('AtomWindow', function() { let sinon, app, service; diff --git a/spec/main-process/mocha-test-runner.js b/spec/main-process/mocha-test-runner.js index 2c3a28ded1..03eec1edf6 100644 --- a/spec/main-process/mocha-test-runner.js +++ b/spec/main-process/mocha-test-runner.js @@ -9,15 +9,6 @@ module.exports = function(testPaths) { reporterEnabled: 'list' }; - if (process.env.TEST_JUNIT_XML_PATH) { - reporterOptions = { - reporterEnabled: 'list, mocha-junit-reporter', - mochaJunitReporterReporterOptions: { - mochaFile: process.env.TEST_JUNIT_XML_PATH - } - }; - } - const mocha = new Mocha({ reporter: 'mocha-multi-reporters', reporterOptions diff --git a/spec/package-manager-spec.js b/spec/package-manager-spec.js index 8b7da3ad32..bd97eac9fc 100644 --- a/spec/package-manager-spec.js +++ b/spec/package-manager-spec.js @@ -6,7 +6,7 @@ const temp = require('temp').track(); const fs = require('fs-plus'); const { Disposable } = require('atom'); const { buildKeydownEvent } = require('../src/keymap-extensions'); -const { mockLocalStorage } = require('./spec-helper'); +const { mockLocalStorage } = require('./helpers/mock-local-storage'); const ModuleCache = require('../src/module-cache'); describe('PackageManager', () => { diff --git a/spec/package-spec.js b/spec/package-spec.js index a235c4e919..9d7b4a959d 100644 --- a/spec/package-spec.js +++ b/spec/package-spec.js @@ -1,7 +1,7 @@ const path = require('path'); const Package = require('../src/package'); const ThemePackage = require('../src/theme-package'); -const { mockLocalStorage } = require('./spec-helper'); +const { mockLocalStorage } = require('./helpers/mock-local-storage'); describe('Package', function() { const build = (constructor, packagePath) => diff --git a/spec/pane-spec.js b/spec/pane-spec.js index 7482f9662d..81e6cb76d3 100644 --- a/spec/pane-spec.js +++ b/spec/pane-spec.js @@ -3,7 +3,7 @@ const { Emitter } = require('event-kit'); const Grim = require('grim'); const Pane = require('../src/pane'); const PaneContainer = require('../src/pane-container'); -const { conditionPromise, timeoutPromise } = require('./async-spec-helpers'); +const { conditionPromise, timeoutPromise } = require('./helpers/async-spec-helpers'); describe('Pane', () => { let confirm, showSaveDialog, deserializerDisposable; diff --git a/spec/runners/jasmine1-test-runner.js b/spec/runners/jasmine1-test-runner.js new file mode 100644 index 0000000000..2771d890b8 --- /dev/null +++ b/spec/runners/jasmine1-test-runner.js @@ -0,0 +1,215 @@ +/* + * decaffeinate suggestions: + * DS101: Remove unnecessary use of Array.from + * DS102: Remove unnecessary code created because of implicit returns + * DS202: Simplify dynamic range loops + * DS205: Consider reworking code to avoid use of IIFEs + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md + */ +const Grim = require('grim'); +const fs = require('fs-plus'); +const temp = require('temp'); +const path = require('path'); +const {ipcRenderer} = require('electron'); + +temp.track(); + +module.exports = function({logFile, headless, testPaths, buildAtomEnvironment}) { + require('../helpers/jasmine-singleton'); + + const normalizeComments = require('../helpers/normalize-comments'); + for (let key in normalizeComments) { window[key] = normalizeComments[key]; } + + // Rewrite global jasmine functions to have support for async tests. + // This way packages can create async specs without having to import these from the + // async-spec-helpers file. + global.it = asyncifyJasmineFn(global.it, 1); + global.fit = asyncifyJasmineFn(global.fit, 1); + global.ffit = asyncifyJasmineFn(global.ffit, 1); + global.fffit = asyncifyJasmineFn(global.fffit, 1); + global.beforeEach = asyncifyJasmineFn(global.beforeEach, 0); + global.afterEach = asyncifyJasmineFn(global.afterEach, 0); + + // Allow document.title to be assigned in specs without screwing up spec window title + let documentTitle = null; + Object.defineProperty(document, 'title', { + get() { return documentTitle; }, + set(title) { return documentTitle = title; } + } + ); + + const userHome = process.env.ATOM_HOME || path.join(fs.getHomeDirectory(), '.atom'); + const atomHome = temp.mkdirSync({prefix: 'atom-test-home-'}); + if (process.env.APM_TEST_PACKAGES) { + const testPackages = process.env.APM_TEST_PACKAGES.split(/\s+/); + fs.makeTreeSync(path.join(atomHome, 'packages')); + for (let packName of Array.from(testPackages)) { + const userPack = path.join(userHome, 'packages', packName); + const loadablePack = path.join(atomHome, 'packages', packName); + + try { + fs.symlinkSync(userPack, loadablePack, 'dir'); + } catch (error) { + fs.copySync(userPack, loadablePack); + } + } + } + + const ApplicationDelegate = require('../../src/application-delegate'); + const applicationDelegate = new ApplicationDelegate(); + applicationDelegate.setRepresentedFilename = function() {}; + applicationDelegate.setWindowDocumentEdited = function() {}; + window.atom = buildAtomEnvironment({ + applicationDelegate, window, document, + configDirPath: atomHome, + enablePersistence: false + }); + + require('../helpers/jasmine1-spec-helper'); + if (process.env.JANKY_SHA1 || process.env.CI) { disableFocusMethods(); } + for (let testPath of Array.from(testPaths)) { requireSpecs(testPath); } + + setSpecType('user'); + + let resolveWithExitCode = null; + const promise = new Promise((resolve, reject) => resolveWithExitCode = resolve); + const jasmineEnv = jasmine.getEnv(); + jasmineEnv.addReporter(buildReporter({logFile, headless, resolveWithExitCode})); + + if(process.env.SPEC_FILTER) { + const {getFullDescription} = require('../helpers/jasmine-list-reporter'); + const regex = new RegExp(process.env.SPEC_FILTER) + jasmineEnv.specFilter = (spec) => getFullDescription(spec, false).match(regex) + } + + if (jasmineEnv.setIncludedTags) { + jasmineEnv.setIncludedTags([process.platform]); + } + + const jasmineContent = document.createElement('div'); + jasmineContent.setAttribute('id', 'jasmine-content'); + + document.body.appendChild(jasmineContent); + + jasmineEnv.execute(); + return promise; +}; + +var asyncifyJasmineFn = (fn, callbackPosition) => (function(...args) { + if (typeof args[callbackPosition] === 'function') { + const callback = args[callbackPosition]; + + args[callbackPosition] = function(...args) { + const result = callback.apply(this, args); + if (result instanceof Promise) { + return waitsForPromise(() => result); + } + }; + } + + return fn.apply(this, args); +}); + +var waitsForPromise = function(fn) { + const promise = fn(); + + return global.waitsFor('spec promise to resolve', done => promise.then(done, function(error) { + jasmine.getEnv().currentSpec.fail(error); + return done(); + })); +}; + +var disableFocusMethods = () => ['fdescribe', 'ffdescribe', 'fffdescribe', 'fit', 'ffit', 'fffit'].forEach(function(methodName) { + const focusMethod = window[methodName]; + return window[methodName] = function(description) { + const error = new Error('Focused spec is running on CI'); + return focusMethod(description, function() { throw error; }); + }; +}); + +var requireSpecs = function(testPath, specType) { + if (fs.isDirectorySync(testPath)) { + return (() => { + const result = []; + for (let testFilePath of Array.from(fs.listTreeSync(testPath))) { + if (/-spec\.(coffee|js)$/.test(testFilePath)) { + require(testFilePath); + // Set spec directory on spec for setting up the project in spec-helper + result.push(setSpecDirectory(testPath)); + } + } + return result; + })(); + } else { + require(testPath); + return setSpecDirectory(path.dirname(testPath)); + } +}; + +const setSpecField = function(name, value) { + const specs = jasmine.getEnv().currentRunner().specs(); + if (specs.length === 0) { return; } + return (() => { + const result = []; + for (let start = specs.length-1, index = start, asc = start <= 0; asc ? index <= 0 : index >= 0; asc ? index++ : index--) { + if (specs[index][name] != null) { break; } + result.push(specs[index][name] = value); + } + return result; + })(); +}; + +var setSpecType = specType => setSpecField('specType', specType); + +var setSpecDirectory = specDirectory => setSpecField('specDirectory', specDirectory); + +var buildReporter = function({logFile, headless, resolveWithExitCode}) { + if (headless) { + return buildTerminalReporter(logFile, resolveWithExitCode); + } else { + let reporter; + const AtomReporter = require('../helpers/jasmine1-atom-reporter.js'); + return reporter = new AtomReporter(); + } +}; + +var buildTerminalReporter = function(logFile, resolveWithExitCode) { + let logStream; + if (logFile != null) { logStream = fs.openSync(logFile, 'w'); } + const log = function(str) { + if (logStream != null) { + return fs.writeSync(logStream, str); + } else { + return ipcRenderer.send('write-to-stderr', str); + } + }; + + const options = { + print(str) { + return log(str); + }, + onComplete(runner) { + if (logStream != null) { fs.closeSync(logStream); } + if (Grim.getDeprecationsLength() > 0) { + Grim.logDeprecations(); + resolveWithExitCode(1); + return; + } + + if (runner.results().failedCount > 0) { + return resolveWithExitCode(1); + } else { + return resolveWithExitCode(0); + } + } + }; + + if (process.env.ATOM_JASMINE_REPORTER === 'list') { + const {JasmineListReporter} = require('../helpers/jasmine-list-reporter'); + return new JasmineListReporter(options); + } else { + const {TerminalReporter} = require('jasmine-tagged'); + return new TerminalReporter(options); + } +}; diff --git a/spec/runners/jasmine2-test-runner.js b/spec/runners/jasmine2-test-runner.js new file mode 100644 index 0000000000..7df2526f09 --- /dev/null +++ b/spec/runners/jasmine2-test-runner.js @@ -0,0 +1,240 @@ +/* + * decaffeinate suggestions: + * DS101: Remove unnecessary use of Array.from + * DS102: Remove unnecessary code created because of implicit returns + * DS202: Simplify dynamic range loops + * DS205: Consider reworking code to avoid use of IIFEs + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md + */ +const Grim = require('grim'); +const fs = require('fs-plus'); +const temp = require('temp'); +const path = require('path'); +const {ipcRenderer} = require('electron'); + +temp.track(); + +module.exports = function({logFile, headless, testPaths, buildAtomEnvironment}) { + // Load Jasmine 2.x + require('../helpers/jasmine2-singleton'); + defineJasmineHelpersOnWindow(jasmine.getEnv()) + + // Build Atom Environment + const { atomHome, applicationDelegate } = require('../helpers/build-atom-environment'); + window.atom = buildAtomEnvironment({ + applicationDelegate, window, document, + configDirPath: atomHome, + enablePersistence: false + }); + + // Load general helpers + require('../../src/window'); + require('../helpers/normalize-comments'); + require('../helpers/document-title'); + require('../helpers/load-jasmine-stylesheet'); + require('../helpers/fixture-packages'); + require('../helpers/set-prototype-extensions'); + require('../helpers/default-timeout'); + require('../helpers/attach-to-dom'); + require('../helpers/deprecation-snapshots'); + require('../helpers/platform-filter'); + + const jasmineContent = document.createElement('div'); + jasmineContent.setAttribute('id', 'jasmine-content'); + document.body.appendChild(jasmineContent); + + return loadSpecsAndRunThem(logFile, headless, testPaths) + .then((result) => { + // Retrying failures only really makes sense in headless mode, + // otherwise the whole HTML view is replaced before the user can inspect the details of the failures + if (!headless) return result; + // All specs passed, don't need to rerun any of them - pass the results to handle possible Grim deprecations + if (result.failedSpecs.length === 0) return result; + + console.log('\n', '\n', `Retrying ${result.failedSpecs.length} spec(s)`, '\n', '\n'); + + // Gather the full names of the failed specs - this is the closest to be a unique identifier for all specs + const fullNamesOfFailedSpecs = result.failedSpecs.map((spec) => { + return spec.fullName; + }) + + // Force-delete the current env - this way Jasmine will reset and we'll be able to re-run failed specs only. The next time the code calls getEnv(), it'll generate a new environment + jasmine.currentEnv_ = null; + + // As all the jasmine helpers (it, describe, etc..) were registered to the previous environment, we need to re-set them on window + defineJasmineHelpersOnWindow(jasmine.getEnv()); + + // Set up a specFilter to disable all passing spec and re-run only the flaky ones + jasmine.getEnv().specFilter = (spec) => { + return fullNamesOfFailedSpecs.includes(spec.result.fullName); + }; + + // Run the specs again - due to the spec filter, only the failed specs will run this time + return loadSpecsAndRunThem(logFile, headless, testPaths); + }).then((result) => { + // Some of the specs failed, we should return with a non-zero exit code + if (result.failedSpecs.length !== 0) return 1; + + // Some of the tests had deprecation warnings, we should log them and return with a non-zero exit code + if (result.hasDeprecations) { + Grim.logDeprecations(); + return 1; + } + + // Everything went good, time to return with a zero exit code + return 0; + }) +}; + +const defineJasmineHelpersOnWindow = (jasmineEnv) => { + for (let key in jasmineEnv) { + window[key] = jasmineEnv[key]; + } + + ['it', 'fit', 'xit'].forEach((key) => { + window[key] = (name, originalFn) => { + jasmineEnv[key](name, async (done) => { + if(originalFn.length === 0) { + await originalFn() + done(); + } else { + originalFn(done); + } + }); + } + }); + + ['beforeEach', 'afterEach'].forEach((key) => { + window[key] = (originalFn) => { + jasmineEnv[key](async (done) => { + if(originalFn.length === 0) { + await originalFn() + done(); + } else { + originalFn(done); + } + }) + } + }); +} + +const loadSpecsAndRunThem = (logFile, headless, testPaths) => { + return new Promise((resolve) => { + const jasmineEnv = jasmine.getEnv(); + + // Load before and after hooks, custom matchers + require('../helpers/jasmine2-custom-matchers').register(jasmineEnv); + require('../helpers/jasmine2-spies').register(jasmineEnv); + require('../helpers/jasmine2-time').register(jasmineEnv); + require('../helpers/jasmine2-warnings').register(jasmineEnv); + + // Load specs and set spec type + for (let testPath of Array.from(testPaths)) { requireSpecs(testPath); } + setSpecType('user'); + + // Add the reporter and register the promise resolve as a callback + jasmineEnv.addReporter(buildReporter({logFile, headless})); + jasmineEnv.addReporter(buildRetryReporter(resolve)); + + // And finally execute the tests + jasmineEnv.execute(); + }) +} + +// This is a helper function to remove a file from the require cache. +// We are using this to force a re-evaluation of the test files when we need to re-run some flaky tests +const unrequire = (requiredPath) => { + for (const path in require.cache) { + if (path === requiredPath) { + delete require.cache[path]; + } + } +} + +const requireSpecs = (testPath) => { + if (fs.isDirectorySync(testPath)) { + for (let testFilePath of fs.listTreeSync(testPath)) { + if (/-spec\.js$/.test(testFilePath)) { + unrequire(testFilePath); + require(testFilePath); + // Set spec directory on spec for setting up the project in spec-helper + setSpecDirectory(testPath); + } + } + } else { + unrequire(testPath); + require(testPath); + setSpecDirectory(path.dirname(testPath)); + } +}; + +const setSpecField = (name, value) => { + const specs = (new jasmine.JsApiReporter({})).specs(); + if (specs.length === 0) { return; } + + for (let index = specs.length - 1; index >= 0; index--) { + if (specs[index][name] != null) { break; } + specs[index][name] = value; + } +}; + +const setSpecType = specType => setSpecField('specType', specType); + +const setSpecDirectory = specDirectory => setSpecField('specDirectory', specDirectory); + +const buildReporter = ({logFile, headless}) => { + if (headless) { + return buildConsoleReporter(logFile); + } else { + const AtomReporter = require('../helpers/jasmine2-atom-reporter.js'); + return new AtomReporter(); + } +}; + +const buildRetryReporter = (onCompleteCallback) => { + const failedSpecs = []; + + return { + jasmineStarted: () => {}, + suiteStarted: () => {}, + specStarted: () => {}, + suiteDone: () => {}, + + specDone: (spec) => { + if (spec.status === 'failed') { + failedSpecs.push(spec); + } + }, + + jasmineDone: () => { + onCompleteCallback({failedSpecs, hasDeprecations: Grim.getDeprecationsLength() > 0}); + } + }; +} + +const buildConsoleReporter = (logFile) => { + let logStream; + if (logFile != null) { logStream = fs.openSync(logFile, 'w'); } + const log = function(str) { + if (logStream != null) { + fs.writeSync(logStream, str); + } else { + ipcRenderer.send('write-to-stderr', str); + } + }; + + const options = { + print(str) { + log(str); + }, + onComplete() { + if (logStream != null) { fs.closeSync(logStream); } + }, + printDeprecation: (msg) => { + console.log(msg) + } + }; + + return new jasmine.ConsoleReporter(options); +}; diff --git a/spec/text-editor-component-spec.js b/spec/text-editor-component-spec.js index 487d055cec..a5b9d04730 100644 --- a/spec/text-editor-component-spec.js +++ b/spec/text-editor-component-spec.js @@ -1,4 +1,4 @@ -const { conditionPromise } = require('./async-spec-helpers'); +const { conditionPromise } = require('./helpers/async-spec-helpers'); const Random = require('random-seed'); const { getRandomBufferRange, buildRandomLines } = require('./helpers/random'); diff --git a/spec/window-event-handler-spec.js b/spec/window-event-handler-spec.js index 51a3ca47bb..ce9f70e351 100644 --- a/spec/window-event-handler-spec.js +++ b/spec/window-event-handler-spec.js @@ -1,6 +1,6 @@ const KeymapManager = require('atom-keymap'); const WindowEventHandler = require('../src/window-event-handler'); -const { conditionPromise } = require('./async-spec-helpers'); +const { conditionPromise } = require('./helpers/async-spec-helpers'); describe('WindowEventHandler', () => { let windowEventHandler; diff --git a/spec/workspace-spec.js b/spec/workspace-spec.js index c8e0c451ec..5c2a3d5cfa 100644 --- a/spec/workspace-spec.js +++ b/spec/workspace-spec.js @@ -5,12 +5,12 @@ const TextBuffer = require('text-buffer'); const TextEditor = require('../src/text-editor'); const Workspace = require('../src/workspace'); const Project = require('../src/project'); -const platform = require('./spec-helper-platform'); +const platform = require('./helpers/platform'); const _ = require('underscore-plus'); const fstream = require('fstream'); const fs = require('fs-plus'); const AtomEnvironment = require('../src/atom-environment'); -const { conditionPromise } = require('./async-spec-helpers'); +const { conditionPromise } = require('./helpers/async-spec-helpers'); describe('Workspace', () => { let workspace; diff --git a/src/main-process/atom-application.js b/src/main-process/atom-application.js index 93eae815b1..16daacc03d 100644 --- a/src/main-process/atom-application.js +++ b/src/main-process/atom-application.js @@ -1750,37 +1750,60 @@ module.exports = class AtomApplication extends EventEmitter { } resolveTestRunnerPath(testPath) { - let packageRoot; - if (FindParentDir == null) { - FindParentDir = require('find-parent-dir'); + FindParentDir ||= require('find-parent-dir'); + + let packageRoot = FindParentDir.sync(testPath, 'package.json'); + + if (!packageRoot) { + process.stderr.write('Error: Could not find root directory'); + process.exit(1); } - if ((packageRoot = FindParentDir.sync(testPath, 'package.json'))) { - const packageMetadata = require(path.join(packageRoot, 'package.json')); - if (packageMetadata.atomTestRunner) { - let testRunnerPath; - if (Resolve == null) { - Resolve = require('resolve'); - } - if ( - (testRunnerPath = Resolve.sync(packageMetadata.atomTestRunner, { - basedir: packageRoot, - extensions: Object.keys(require.extensions) - })) - ) { - return testRunnerPath; - } else { - process.stderr.write( - `Error: Could not resolve test runner path '${ - packageMetadata.atomTestRunner - }'` - ); - process.exit(1); - } + const packageMetadata = require(path.join(packageRoot, 'package.json')); + let atomTestRunner = packageMetadata.atomTestRunner; + + if (!atomTestRunner) { + process.stdout.write('atomTestRunner was not defined, using the deprecated runners/jasmine1-test-runner.'); + atomTestRunner = 'runners/jasmine1-test-runner'; + } + + let testRunnerPath; + Resolve ||= require('resolve'); + + // First try to run with local runners (e.g: `./test/runner.js`) or packages (e.g.: `atom-mocha-test-runner`) + try { + testRunnerPath = Resolve.sync(atomTestRunner, { + basedir: packageRoot, + extensions: Object.keys(require.extensions) + }); + + if (testRunnerPath) { + return testRunnerPath; + } + } catch { + // Nothing to do, try the next strategy + } + + // Then try to use one of the runners defined in Pulsar + try { + testRunnerPath = Resolve.sync(`./spec/${atomTestRunner}`, { + basedir: this.devResourcePath, + extensions: Object.keys(require.extensions) + }); + + if (testRunnerPath) { + return testRunnerPath; } + } catch { + // Nothing to do, try the next strategy } - return this.resolveLegacyTestRunnerPath(); + process.stderr.write( + `Error: Could not resolve test runner path '${ + packageMetadata.atomTestRunner + }'` + ); + process.exit(1); } resolveLegacyTestRunnerPath() { diff --git a/src/main-process/atom-window.js b/src/main-process/atom-window.js index f5e242da51..bdf6ad83ab 100644 --- a/src/main-process/atom-window.js +++ b/src/main-process/atom-window.js @@ -60,7 +60,6 @@ module.exports = class AtomWindow extends EventEmitter { disableBlinkFeatures: 'Auxclick', nodeIntegration: true, contextIsolation: false, - enableRemoteModule: true, webviewTag: true, // TodoElectronIssue: remote module is deprecated https://www.electronjs.org/docs/breaking-changes#default-changed-enableremotemodule-defaults-to-false diff --git a/vendor/jasmine.js b/vendor/jasmine.js index d69b33d2d8..43c68d77f5 100644 --- a/vendor/jasmine.js +++ b/vendor/jasmine.js @@ -2705,142 +2705,3 @@ jasmine.Matchers.prototype.toSatisfy = function(fn) { } return fn(this.actual, msgFun) }; - -// This will normalize the comments for the special format of grammar tests -// that TextMate and Tree-Sitter do -// -// Basically, receiving a text editor and the regex that probably defines -// what a comment is, it'll return an object with `expect` - that is what was -// expected to pass the test, like a scope description for example, and two -// Point-compatible fields - `editorPosition`, that is basically in what -// position of the editor `expect` should be satisfied, and `testPosition`, that -// is where in file the test actually happened. This makes it easier for us -// to construct an error showing where EXACTLY was the assertion that failed -function normalizeTreeSitterTextData(editor, commentRegex) { - let allMatches = [], lastNonComment = 0 - const checkAssert = new RegExp('^\\s*' + commentRegex.source + '\\s*[\\<\\-|\\^]') - editor.getBuffer().getLines().forEach((row, i) => { - const m = row.match(commentRegex) - if(m) { - // const scope = editor.scopeDescriptorForBufferPosition([i, m.index]) - // FIXME: use editor.scopeDescriptorForBufferPosition when it works - const scope = editor.tokensForScreenRow(i) - const scopes = scope.flatMap(e => e.scopes) - if(scopes.find(s => s.match(/comment/)) && row.match(checkAssert)) { - allMatches.push({row: lastNonComment, text: row, col: m.index, testRow: i}) - return - } - } - lastNonComment = i - }) - return allMatches.map(({text, row, col, testRow}) => { - const exactPos = text.match(/\^\s+(.*)/) - if(exactPos) { - const expected = exactPos[1] - return { - expected, - editorPosition: {row, column: exactPos.index}, - testPosition: {row: testRow, column: col} - } - } else { - const pos = text.match(/\<-\s+(.*)/) - if(!pos) throw new Error(`Can't match ${text}`) - return { - expected: pos[1], - editorPosition: {row, column: col}, - testPosition: {row: testRow, column: col} - } - } - }) -} -if (isCommonJS) exports.normalizeTreeSitterTextData = normalizeTreeSitterTextData; - -async function openDocument(fullPath) { - const editor = await atom.workspace.open(fullPath); - await editor.languageMode.ready; - return editor; -} - -async function runGrammarTests(fullPath, commentRegex) { - const editor = await openDocument(fullPath); - - const normalized = normalizeTreeSitterTextData(editor, commentRegex) - expect(normalized.length).toSatisfy((n, reason) => { - reason("Tokenizer didn't run correctly - could not find any comment") - return n > 0 - }) - normalized.forEach(({expected, editorPosition, testPosition}) => { - expect(editor.scopeDescriptorForBufferPosition(editorPosition).scopes).toSatisfy((scopes, reason) => { - const dontFindScope = expected.startsWith("!"); - expected = expected.replace(/^!/, "") - if(dontFindScope) { - reason(`Expected to NOT find scope "${expected}" but found it\n` + - ` at ${fullPath}:${testPosition.row+1}:${testPosition.column+1}` - ); - } else { - reason(`Expected to find scope "${expected}" but found "${scopes}"\n` + - ` at ${fullPath}:${testPosition.row+1}:${testPosition.column+1}` - ); - } - const normalized = expected.replace(/([\.\-])/g, '\\$1'); - const scopeRegex = new RegExp('^' + normalized + '(\\..+)?$'); - let result = scopes.find(e => e.match(scopeRegex)) !== undefined; - if(dontFindScope) result = !result; - return result - }) - }) -} -if (isCommonJS) exports.runGrammarTests = runGrammarTests; - -async function runFoldsTests(fullPath, commentRegex) { - const editor = await openDocument(fullPath); - let grouped = {} - const normalized = normalizeTreeSitterTextData(editor, commentRegex).forEach(test => { - const [kind, id] = test.expected.split('.') - if(!kind || !id) { - throw new Error(`Folds must be in the format fold_end.some-id\n` + - ` at ${test.testPosition.row+1}:${test.testPosition.column+1}`) - } - grouped[id] ||= {} - grouped[id][kind] = test - }) - for(const k in grouped) { - const v = grouped[k] - const keys = Object.keys(v) - if(keys.indexOf('fold_begin') === -1) - throw new Error(`Fold ${k} must contain fold_begin`) - if(keys.indexOf('fold_end') === -1) - throw new Error(`Fold ${k} must contain fold_end`) - if(keys.indexOf('fold_new_position') === -1) - throw new Error(`Fold ${k} must contain fold_new_position`) - } - - for(const k in grouped) { - const fold = grouped[k] - const begin = fold['fold_begin'] - const end = fold['fold_end'] - const newPos = fold['fold_new_position'] - - expect(editor.isFoldableAtBufferRow(begin.editorPosition.row)) - .toSatisfy((foldable, reason) => { - reason(`Editor is not foldable at row ${begin.editorPosition.row+1}\n` + - ` at ${fullPath}:${begin.testPosition.row+1}:${begin.testPosition.column+1}`) - return foldable - }) - editor.foldBufferRow(begin.editorPosition.row) - - expect(editor.screenPositionForBufferPosition(end.editorPosition)) - .toSatisfy((screenPosition, reason) => { - const {row,column} = newPos.editorPosition - reason(`At row ${begin.editorPosition.row+1}, editor should fold ` + - `up to the ${end.editorPosition.row+1}:${end.editorPosition.column+1}\n` + - ` into the new position ${row+1}:${column+1}\n`+ - ` but folded to position ${screenPosition.row+1}:${screenPosition.column+1}\n`+ - ` at ${fullPath}:${newPos.testPosition.row+1}:${newPos.testPosition.column+1}\n` + - ` at ${fullPath}:${end.testPosition.row+1}:${end.testPosition.column+1}`) - return row === screenPosition.row && column === screenPosition.column - }) - editor.unfoldAll() - } -} -if (isCommonJS) exports.runFoldsTests = runFoldsTests; diff --git a/yarn.lock b/yarn.lock index 4505c010e9..d74a5ae956 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4380,6 +4380,11 @@ event-stream@~3.1.0: stack-trace "0.0.9" underscore-plus "^1.7.0" +exit@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/exit/-/exit-0.1.2.tgz#0632638f8d877cc82107d30a0fff1a17cba1cd0c" + integrity sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ== + expand-template@^2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/expand-template/-/expand-template-2.0.3.tgz#6e14b3fcee0f3a6340ecb57d2e8918692052a47c" @@ -4986,7 +4991,7 @@ glob@7.1.3: once "^1.3.0" path-is-absolute "^1.0.0" -glob@^7.0.0, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6, glob@^7.2.0: +glob@^7.0.0, glob@^7.0.6, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6, glob@^7.2.0: version "7.2.3" resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== @@ -5804,6 +5809,11 @@ jake@^10.8.5: filelist "^1.0.1" minimatch "^3.0.4" +jasmine-core@~2.5.2: + version "2.5.2" + resolved "https://registry.yarnpkg.com/jasmine-core/-/jasmine-core-2.5.2.tgz#6f61bd79061e27f43e6f9355e44b3c6cab6ff297" + integrity sha512-kpf1e8MlD9rnurToeGLjfmsSSAupkgZM+ky9BmXFKbFnr/MgNnLokfGkrZg+dqtbsQF7hv/1s67vFVLS3iOVxA== + jasmine-focused@^1.0.7: version "1.0.7" resolved "https://registry.yarnpkg.com/jasmine-focused/-/jasmine-focused-1.0.7.tgz#b83c757c800e68e1d6efc1a3a1a13ff39ff6dcd2" @@ -5831,13 +5841,6 @@ jasmine-json@~0.0: underscore ">= 1.3.1" walkdir ">= 0.0.1" -jasmine-reporters@1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/jasmine-reporters/-/jasmine-reporters-1.1.0.tgz#f3350885890c9edaad12a087c62f2cc19dcf66c0" - integrity sha512-y0sNPC0/emtTk9eDCXp57JqOAEfBkOCSF/p+d1Zd4dv2tLdfmvAm2PtEUpI/j1Y5qYsxnoWO1M3VZ4YoZIPoTg== - dependencies: - mkdirp "~0.3.5" - jasmine-reporters@>=0.2.0: version "2.5.2" resolved "https://registry.yarnpkg.com/jasmine-reporters/-/jasmine-reporters-2.5.2.tgz#b5dfa1d9c40b8020c5225e0e1e2b9953d66a4d69" @@ -5853,6 +5856,15 @@ jasmine-tagged@^1.1.4: dependencies: jasmine-focused "^1.0.7" +jasmine@2.5.3: + version "2.5.3" + resolved "https://registry.yarnpkg.com/jasmine/-/jasmine-2.5.3.tgz#5441f254e1fc2269deb1dfd93e0e57d565ff4d22" + integrity sha512-gJeubNIl+BgKUQ/YgTX3yH4iNGa1HicbZl81v5VSZSInjj8LhkzuwTd3CjiDUxQL09f02sR3BlmQQcfrT6qTSw== + dependencies: + exit "^0.1.2" + glob "^7.0.6" + jasmine-core "~2.5.2" + js-sdsl@^4.1.4: version "4.1.5" resolved "https://registry.yarnpkg.com/js-sdsl/-/js-sdsl-4.1.5.tgz#1ff1645e6b4d1b028cd3f862db88c9d887f26e2a"