diff --git a/script/utils/child-process-wrapper.js b/script/utils/child-process-wrapper.js index 3a05973d5..095d431d0 100644 --- a/script/utils/child-process-wrapper.js +++ b/script/utils/child-process-wrapper.js @@ -14,6 +14,7 @@ exports.safeExec = function(command, options, callback) { // The default is 200KB. options.maxBuffer = 1024 * 1024; + options.stdio = "inherit" var child = childProcess.exec(command, options, function(error, stdout, stderr) { if (error && !options.ignoreStderr) process.exit(error.code || 1); @@ -31,9 +32,8 @@ exports.safeSpawn = function(command, args, options, callback) { callback = options; options = {}; } + options.stdio = "inherit" var child = childProcess.spawn(command, args, options); - child.stderr.pipe(process.stderr); - child.stdout.pipe(process.stdout); child.on('error', function(error) { console.error('Command \'' + command + '\' failed: ' + error.message); }); diff --git a/spec_integration/bootstrap.js b/spec_integration/bootstrap.js index 73bffb797..df6fe59e7 100644 --- a/spec_integration/bootstrap.js +++ b/spec_integration/bootstrap.js @@ -2,11 +2,38 @@ // argv[1] = jasmine // argv[2] = JASMINE_CONFIG_PATH=./config.json // argv[3] = NYLAS_ROOT_PATH=/path/to/nylas/root - var babelOptions = require('../static/babelrc.json'); require('babel-core/register')(babelOptions); +var chalk = require('chalk') +var util = require('util') + +console.errorColor = function(err){ + if (typeof err === "string") { + console.error(chalk.red(err)); + } else { + console.error(chalk.red(util.inspect(err))); + } +} + +console.inspect = function(val) { + console.log(util.inspect(val, true, depth=7, colorize=true)); +} + jasmine.NYLAS_ROOT_PATH = process.argv[3].split("NYLAS_ROOT_PATH=")[1] jasmine.UNIT_TEST_TIMEOUT = 120*1000; jasmine.BOOT_TIMEOUT = 30*1000; jasmine.DEFAULT_TIMEOUT_INTERVAL = 30*1000 + +Promise = require('bluebird') +Promise.config({ + warnings: true, + longStackTraces: true, + cancellation: true +}) + +process.on("unhandledRejection", function(reason, promise) { + if (reason.stack) { console.errorColor(reason.stack); } + console.errorColor(promise); +}); + diff --git a/spec_integration/contenteditable-integration-spec.es6 b/spec_integration/contenteditable-integration-spec.es6 index 47c7e4157..d4db223d8 100644 --- a/spec_integration/contenteditable-integration-spec.es6 +++ b/spec_integration/contenteditable-integration-spec.es6 @@ -1,18 +1,14 @@ -import Promise from 'bluebird' import {N1Launcher} from './integration-helper' import ContenteditableTestHarness from './contenteditable-test-harness.es6' fdescribe('Contenteditable Integration Spec', function() { beforeAll((done)=>{ - console.log("----------- BEFORE ALL"); - // Boot in dev mode with no arguments this.app = new N1Launcher(["--dev"]); this.app.popoutComposerWindowReady().finally(done); }); beforeEach((done) => { - console.log("----------- BEFORE EACH"); - this.ce = new ContenteditableTestHarness(this.app.client, expect) + this.ce = new ContenteditableTestHarness(this.app.client) this.ce.init().finally(done); }); @@ -24,51 +20,120 @@ fdescribe('Contenteditable Integration Spec', function() { } }); - fit("Creates ordered lists", (done)=> { - console.log("RUNNING KEYS"); - this.app.client.keys(["1", ".", "Space"]).then(()=>{ - console.log("DONE FIRING KEYS"); - e1 = this.ce.expectHTML("
  1. WOOO
") - e2 = this.ce.expectSelection((dom) => { - return {node: dom.querySelectorAll("li")[0]} - }) - return Promise.all(e1,e2) - }).catch((err)=>{ console.log("XXXX ERROR"); console.log(err); }).finally(done) + + + describe('Manipulating Lists', () => { + it("Creates ordered lists", (done)=> { + this.ce.test({ + keys: ["1", ".", "Space"], + expectedHTML: "
", + expectedSelectionResolver: (dom) => { + return {node: dom.querySelectorAll('li')[0]} } + }).then(done).catch(done.fail) + }); + + it('Undoes ordered list creation with backspace', (done) => { + this.ce.test({ + keys: ["1", ".", "Space", "Back space"], + expectedHTML: "1. 
", + expectedSelectionResolver: (dom) => { + return {node: dom.childNodes[0], offset: 3} } + }).then(done).catch(done.fail) + }); + + it("Creates unordered lists with star", (done) => { + this.ce.test({ + keys: ['*', 'Space'], + expectedHTML: "", + expectedSelectionResolver: (dom) => { + return {node: dom.querySelectorAll("li")[0] } } + }).then(done).catch(done.fail) + }); + + it("Undoes unordered list creation with backspace", (done) => { + this.ce.test({ + keys: ['*', 'Space', 'Back space'], + expectedHTML: "* 
", + expectedSelectionResolver: (dom) => { + return {node: dom.childNodes[0], offset: 2} } + }).then(done).catch(done.fail) + }); + + it("Creates unordered lists with dash", (done) => { + this.ce.test({ + keys: ['-', 'Space'], + expectedHTML: "", + expectedSelectionResolver: (dom) => { + return {node: dom.querySelectorAll("li")[0] } } + }).then(done).catch(done.fail) + }); + + it("Undoes unordered list creation with backspace", (done) => { + this.ce.test({ + keys: ['-', 'Space', 'Back space'], + expectedHTML: "- 
", + expectedSelectionResolver: (dom) => { + return {node: dom.childNodes[0], offset: 2} } + }).then(done).catch(done.fail) + }); + + // it("create a single item then delete it with backspace", (done) => { + // this.ce.test({ + // keys: ['-', 'Space', 'a', 'Left arrow', 'Back space'], + // expectedHTML: "a
", + // expectedSelectionResolver: (dom) => { + // return {node: dom.childNodes[0], offset: 0} } + // }).then(done).catch(done.fail) + // }); + // + // it("create a single item then delete it with tab", (done) => { + // this.ce.test({ + // keys: ['-', 'Space', 'a', 'Shift', 'Tab'], + // expectedHTML: "a
", + // expectedSelectionResolver: (dom) => { + // return {node: dom.childNodes[0], offset: 1} } + // }).then(done).catch(done.fail) + // }); + }); - it("has main window visible", (done)=> { - this.app.client.isWindowVisible() - .then((result)=>{ expect(result).toBe(true) }) - .finally(done) - }); - it("has main window focused", (done)=> { - this.app.client.isWindowFocused() - .then((result)=>{ expect(result).toBe(true) }) - .finally(done) - }); - it("isn't minimized", (done)=> { - this.app.client.isWindowMinimized() - .then((result)=>{ expect(result).toBe(false) }) - .finally(done) - }); + describe('Ensuring popout composer window works', () => { + it("has main window visible", (done)=> { + this.app.client.isWindowVisible() + .then((result)=>{ expect(result).toBe(true) }) + .then(done).catch(done.fail) + }); - it("doesn't have the dev tools open", (done)=> { - this.app.client.isWindowDevToolsOpened() - .then((result)=>{ expect(result).toBe(false) }) - .finally(done) - }); + it("has main window focused", (done)=> { + this.app.client.isWindowFocused() + .then((result)=>{ expect(result).toBe(true) }) + .then(done).catch(done.fail) + }); - it("has width", (done)=> { - this.app.client.getWindowWidth() - .then((result)=>{ expect(result).toBeGreaterThan(0) }) - .finally(done) - }); + it("isn't minimized", (done)=> { + this.app.client.isWindowMinimized() + .then((result)=>{ expect(result).toBe(false) }) + .then(done).catch(done.fail) + }); - it("has height", (done)=> { - this.app.client.getWindowHeight() - .then((result)=>{ expect(result).toBeGreaterThan(0) }) - .finally(done) + it("doesn't have the dev tools open", (done)=> { + this.app.client.isWindowDevToolsOpened() + .then((result)=>{ expect(result).toBe(false) }) + .then(done).catch(done.fail) + }); + + it("has width", (done)=> { + this.app.client.getWindowWidth() + .then((result)=>{ expect(result).toBeGreaterThan(0) }) + .then(done).catch(done.fail) + }); + + it("has height", (done)=> { + this.app.client.getWindowHeight() + .then((result)=>{ expect(result).toBeGreaterThan(0) }) + .then(done).catch(done.fail) + }); }); }); diff --git a/spec_integration/contenteditable-test-harness.es6 b/spec_integration/contenteditable-test-harness.es6 index 3766b1203..1283b22b5 100644 --- a/spec_integration/contenteditable-test-harness.es6 +++ b/spec_integration/contenteditable-test-harness.es6 @@ -1,43 +1,40 @@ -import Promise from 'bluebird' - class ContenteditableTestHarness { - constructor(client, expect) { - this.expect = expect + constructor(client) { this.client = client; } init() { - console.log("INIT TEST HARNESS"); return this.client.execute(() => { ce = document.querySelector(".contenteditable") ce.innerHTML = "" ce.focus() - }).then(({value})=>{ - console.log(value); + }) + } + + test({keys, expectedHTML, expectedSelectionResolver}) { + return this.client.keys(keys).then(()=>{ + return this.expectHTML(expectedHTML) + }).then(()=>{ + return this.expectSelection(expectedSelectionResolver) }) } expectHTML(expectedHTML) { - console.log("EXPECTING HTML"); - console.log(expectedHTML); - return this.client.execute((expect, arg2) => { - console.log(expect); - console.log(arg2); - ce = document.querySelector(".contenteditable") - expect(ce.innerHTML).toBe(expectedHTML) - return ce.innerHTML - }, this.expect, "FOOO").then(({value})=>{ - console.log("GOT HTML VALUE"); - console.log(value); - }).catch((err)=>{ - console.log("XXXXXXXXXX GOT ERROR") - console.log(err); + return this.client.execute(() => { + return document.querySelector(".contenteditable").innerHTML + }).then(({value})=>{ + expect(value).toBe(expectedHTML) }) } expectSelection(callback) { - return this.client.execute(() => { + // Since `execute` fires parameters to Selenium via REST API, we can + // only pass strings. We serialize the callback so we can run it in + // the correct execution environment of the window instead of the + // Selenium wrapper. + return this.client.execute((callbackStr) => { + eval(`callback=${callbackStr}`); ce = document.querySelector(".contenteditable") expectSel = callback(ce) @@ -48,10 +45,51 @@ class ContenteditableTestHarness { selection = document.getSelection() - this.expect(selection.anchorNode).toBe(anchorNode) - this.expect(selection.focusNode).toBe(focusNode) - this.expect(selection.anchorOffset).toBe(anchorOffset) - this.expect(selection.focusOffset).toBe(focusOffset) + return { + anchorNodeMatch: selection.anchorNode === anchorNode, + focusNodeMatch: selection.focusNode === focusNode, + anchorOffsetMatch: selection.anchorOffset === anchorOffset, + focusOffsetMatch: selection.focusOffset === focusOffset, + expectedAnchorNode: anchorNode.outerHTML, + expectedFocusNode: focusNode.outerHTML, + expectedAnchorOffset: anchorOffset, + expectedFocusOffset: focusOffset, + actualAnchorNode: selection.anchorNode.outerHTML, + actualFocusNode: selection.focusNode.outerHTML, + actualAnchorOffset: selection.anchorOffset, + actualFocusOffset: selection.focusOffset, + } + + }, callback.toString()).then(({value}) => { + matchInfo = value + + allMatched = true; + if (!matchInfo.anchorNodeMatch) { + console.errorColor("\nAnchor nodes don't match") + console.errorColor(`Expected: "${matchInfo.actualAnchorNode}" to be "${matchInfo.expectedAnchorNode}"`); + allMatched = false; + } + if (!matchInfo.focusNodeMatch) { + console.errorColor("\nFocus nodes don't match") + console.errorColor(`Expected: "${matchInfo.actualFocusNode}" to be "${matchInfo.expectedFocusNode}"`); + allMatched = false; + } + if (!matchInfo.anchorOffsetMatch) { + console.errorColor("\nAnchor offsets don't match") + console.errorColor(`Expected: ${matchInfo.actualAnchorOffset} to be ${matchInfo.expectedAnchorOffset}`); + allMatched = false; + } + if (!matchInfo.focusOffsetMatch) { + console.errorColor("\nFocus offsets don't match") + console.errorColor(`Expected: ${matchInfo.actualFocusOffset} to be ${matchInfo.expectedFocusOffset}`); + allMatched = false; + } + + outMsgDescription = "matched. See discrepancies above" + if (allMatched) { outMsg = outMsgDescription + } else { outMsg = "Selection" } + // "Expected Selection to be matched. See discrepancies above" + expect(outMsg).toBe(outMsgDescription); }) } } diff --git a/spec_integration/integration-helper.es6 b/spec_integration/integration-helper.es6 index cc7f945a6..61ec67b80 100644 --- a/spec_integration/integration-helper.es6 +++ b/spec_integration/integration-helper.es6 @@ -1,5 +1,4 @@ import path from 'path' -import Promise from 'bluebird' import {Application} from 'spectron'; class N1Launcher extends Application { @@ -46,7 +45,7 @@ class N1Launcher extends Application { return NylasEnv.getLoadSettings().windowType; }).then(({value})=>{ if(value === "composer") { - return client.isWindowVisible() + return client.isExisting(".contenteditable") } else { return false } diff --git a/spec_integration/package.json b/spec_integration/package.json index 03e630d5b..ecde74ed7 100644 --- a/spec_integration/package.json +++ b/spec_integration/package.json @@ -12,6 +12,7 @@ "bluebird": "^3.0.5", "babel-core": "^5.8.21", "jasmine": "^2.3.2", - "spectron": "^0.34.1" + "spectron": "^0.34.1", + "chalk": "^1.1" } }