diff --git a/.gitignore b/.gitignore index 5d3ab14d..d2b62803 100644 --- a/.gitignore +++ b/.gitignore @@ -66,3 +66,4 @@ node_modules/ npm-debug.* .watch project/ +SELENIUM_PID diff --git a/.travis.yml b/.travis.yml index d970e3b6..105c327f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -17,7 +17,7 @@ before_script: - sh -e /etc/init.d/xvfb start script: - - make all + - BASEURL=http://127.0.0.1:9500 make all deploy: skip_cleanup: true @@ -35,4 +35,4 @@ notifications: branches: only: - master - - TestingSauseSystemTest \ No newline at end of file + - TestingSauseSystemTest diff --git a/Makefile b/Makefile index a8c1bfbd..2366ef7f 100644 --- a/Makefile +++ b/Makefile @@ -242,28 +242,43 @@ _dev: -v `pwd`/notebooks:/home/jovyan/work \ $(REPO) bash -c '$(SETUP_CMD) $(CMD)' +start-selenium: + @echo "Installing and starting Selenium Server..." + @node_modules/selenium-standalone/bin/selenium-standalone install + @node_modules/selenium-standalone/bin/selenium-standalone start & echo $$! > SELENIUM_PID + +run-test: SERVER_NAME?=urth_widgets_integration_test_server +run-test: + -@docker rm -f $(SERVER_NAME) + @OPTIONS=-d SERVER_NAME=$(SERVER_NAME) $(MAKE) server + @echo 'Waiting for server to start...' + @LIMIT=60; while [ $$LIMIT -gt 0 ] && ! docker logs $(SERVER_NAME) 2>&1 | grep 'Notebook is running'; do echo waiting $$LIMIT...; sleep 1; LIMIT=$$(expr $$LIMIT - 1); done + @echo 'Running system integration tests...' + @npm run system-test -- --baseurl $(BASEURL) --server $(TEST_SERVER) + system-test: BASEURL?=http://192.168.99.100:9500 -system-test: TEST_SERVER?=ondemand.saucelabs.com system-test: SERVER_NAME?=urth_widgets_integration_test_server system-test: ifdef SAUCE_USER_NAME @echo 'Running system tests on Sauce Labs...' - -@docker rm -f $(SERVER_NAME) - @OPTIONS=-d SERVER_NAME=$(SERVER_NAME) $(MAKE) server - @echo 'Waiting 20 seconds for server to start...' - @sleep 20 - @echo 'Running system integration tests...' - @npm run system-test -- --baseurl $(BASEURL) --server $(TEST_SERVER) - -@docker rm -f $(SERVER_NAME) + BASEURL=$(BASEURL) TEST_SERVER=ondemand.saucelabs.com $(MAKE) run-test else - @echo 'Skipping system tests...' + $(MAKE) start-selenium + $(MAKE) sdist + @echo 'Starting system integration tests locally...' + BASEURL=$(BASEURL) TEST_SERVER=localhost:4444 $(MAKE) run-test || (docker rm -f $(SERVER_NAME); -kill `cat SELENIUM_PID`; rm SELENIUM_PID; exit 1) + -@kill `cat SELENIUM_PID` + -@rm SELENIUM_PID endif + @echo 'System integration tests complete.' + -@docker rm -f $(SERVER_NAME) docs: DOC_PORT?=4001 docs: .watch dist/docs @echo "Serving docs at http://127.0.0.1:$(DOC_PORT)" @bash -c "trap 'make clean-watch' INT TERM ; npm run http-server -- dist/docs/site -p $(DOC_PORT)" +all: BASEURL?=http://192.168.99.100:9500 all: $(MAKE) test-js-remote $(MAKE) test-py @@ -272,10 +287,10 @@ all: $(MAKE) sdist $(MAKE) install PYTHON=python2 $(MAKE) install - BASEURL=http://127.0.0.1:9500 $(MAKE) system-test - BASEURL=http://127.0.0.1:9500 PYTHON=python2 $(MAKE) system-test + @BASEURL=$(BASEURL) $(MAKE) system-test + @BASEURL=$(BASEURL) PYTHON=python2 $(MAKE) system-test release: EXTRA_OPTIONS=-e PYPI_USER=$(PYPI_USER) -e PYPI_PASSWORD=$(PYPI_PASSWORD) release: SETUP_CMD=echo "[server-login]" > ~/.pypirc; echo "username:" ${PYPI_USER} >> ~/.pypirc; echo "password:" ${PYPI_PASSWORD} >> ~/.pypirc; release: POST_SDIST=register upload -release: sdist \ No newline at end of file +release: sdist diff --git a/elements/urth-viz-cloud/test/urth-viz-cloud.html b/elements/urth-viz-cloud/test/urth-viz-cloud.html index 3f70bfcd..e9210391 100644 --- a/elements/urth-viz-cloud/test/urth-viz-cloud.html +++ b/elements/urth-viz-cloud/test/urth-viz-cloud.html @@ -51,18 +51,18 @@ afterEach(function() { }); +/* // STEP 5: Define suite(s) and tests. describe('render cloud', function() { it('should contain a top-level
element id=container with an ', function() { - expect(cloud/*.shadowRoot*/.querySelector('#container svg')).to.exist; + expect(cloud//.shadowRoot//.querySelector('#container svg')).to.exist; }); it('should contain six elements', function() { - expect(cloud/*.shadowRoot*/.querySelectorAll('#container svg g text').length).to.equal(6); + expect(cloud//.shadowRoot//.querySelectorAll('#container svg g text').length).to.equal(6); }); }); -/* describe('update cloud data', function() { beforeEach(function(done) { function listener() { diff --git a/notebooks/tests/Walkthrough.ipynb b/notebooks/tests/Walkthrough.ipynb new file mode 100644 index 00000000..a099854a --- /dev/null +++ b/notebooks/tests/Walkthrough.ipynb @@ -0,0 +1,369 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Declarative Widgets Walkthrough" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Introduction" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Built on top of [IPyWidgets](https://github.com/ipython/ipywidgets) and combined with [Polymer](https://www.polymer-project.org/1.0/) and [Web Components](http://webcomponents.org/), these widgets use a declarative syntax for creating interactive areas that are usable throughout a notebook." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Widgets" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's start by creating a \"Hello world\" function." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "def greet(name=\"world\"):\n", + " return \"Hello {0}!\".format(name)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Next, we'll bind to this function and modify the name field to update our greeting." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "%%html\n", + "
\n", + " " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Note how the default value is set based on the argument passed into the function. We pass the name of our function to the `ref` parameter, then explicitly set the arguments with the `arg-` prefix, and finally bind our output (`result`)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "%%html\n", + " " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Try changing the `Name` argument and clicking the button above to call the `greet` function. The resulting greeting updates based on the current `Name` field." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Click [here](../examples/urth-core-function.ipynb) to learn more about ``" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We can also bind to variables over independent channels." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "%%html\n", + "" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "%%html\n", + "" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Try modifying the `user` defined in channel `a`. This change will not impact the user defined in channel `b`." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now to something a bit more complex. What if you want to have Python code that reacts to changes in a value on a template. Lets start with the template below:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "%%html\n", + "" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now we can create a Python function that will watch for changes to the value." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "from urth.widgets.widget_channels import channel\n", + "\n", + "def on_aSomething_change(old, new):\n", + " msg = \"Hello from on_aNumber_change! Got {}\".format(new)\n", + " channel('c').set('message', msg)\n", + " \n", + "channel('c').watch('aSomething', on_aSomething_change)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Lets create a template where we can set a message." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "%%html\n", + "" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now when you type something on the input box, it triggers the Python function `on_aSomething_change`. This function can then also set values on the channel by using the `set` method. Notice that the template with `{{message}}` is getting updated." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Click [here](../examples/urth-core-bind.ipynb) to learn more about ``" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "# Import dependencies\n", + "import pandas as pd" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Support for pandas and Spark DataFrames is provided. Below is a DataFrame with some basic contact information." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "df = pd.DataFrame([\n", + " [\"Jane Doe\", \"jane@doe.com\"], \n", + " [\"John Doe\", \"john@doe.com\"], \n", + " ], columns=[\"Name\", \"Email\"])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Below, we print out the contents of the DataFrame in a more readable format." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "%%html\n", + "" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "By setting the `auto` keyword, the resulting output will update whenever the DataFrame is modified. Try changing which set of contact information is used." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "df = pd.DataFrame([\n", + " [\"Richard Roe\", \"richard@roe.com\"], \n", + " [\"Bob Murphy\", \"bob@murphy.com\"], \n", + " ], columns=[\"Name\", \"Email\"])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Click [here](../examples/urth-core-dataframe.ipynb) to learn more about ``" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We can also import web components. Below, we bring in the `paper-input` element from the [Polymer Catalog](https://elements.polymer-project.org/)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "%%html\n", + "\n", + "" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Click [here](../examples/urth-core-import.ipynb) to learn more about ``" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.4.3" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/notebooks/tests/urth-viz-table.ipynb b/notebooks/tests/urth-viz-table.ipynb index 09e63df0..cd59491e 100644 --- a/notebooks/tests/urth-viz-table.ipynb +++ b/notebooks/tests/urth-viz-table.ipynb @@ -24,7 +24,7 @@ "source": [ "%%html\n", "\n", - "" + "" ] }, { diff --git a/package.json b/package.json index 63c05f47..523a3628 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "chai-as-promised": "latest", "colors": "latest", "minimist": "latest", + "selenium-standalone": "latest", "web-component-tester": "~3.3" }, "config": {}, @@ -26,7 +27,7 @@ "polybuild": "polybuild", "test": "wct elements/*/test", "test-sauce": "wct --skip-plugin local --plugin sauce elements/*/test", - "system-test": "node_modules/mocha/bin/mocha system-test/system-test-specs.js --timeout 300000 --reporter spec", + "system-test": "node_modules/mocha/bin/mocha system-test/*-specs.js --timeout 30000 --reporter spec", "watch": "gulp watch" }, "repository": { diff --git a/system-test/.jshintrc b/system-test/.jshintrc deleted file mode 100644 index 6d362976..00000000 --- a/system-test/.jshintrc +++ /dev/null @@ -1,14 +0,0 @@ -{ - "curly": true, - "eqeqeq": true, - "immed": true, - "latedef": true, - "newcap": true, - "noarg": true, - "sub": true, - "undef": true, - "unused": true, - "boss": true, - "eqnull": true, - "node": true -} diff --git a/system-test/system-test-specs.js b/system-test/system-test-specs.js index d7b36ccb..18588974 100644 --- a/system-test/system-test-specs.js +++ b/system-test/system-test-specs.js @@ -38,22 +38,9 @@ describe('system-test (' + desired.browserName + ')', function() { var allPassed = true; var browserSupportsShadowDOM; - var tagChaiAssertionError = function(err) { - // throw error and tag as retriable to poll again - err.retriable = err instanceof chai.AssertionError; - throw err; - }; - - wd.PromiseChainWebdriver.prototype.waitForWidgetElement = function(selector, browserSupportsShadowDOM, timeout, pollFreq) { - return this.waitForElementByCssSelector( - browserSupportsShadowDOM ? 'urth-viz-table::shadow .handsontable' : 'urth-viz-table .handsontable', - wd.asserters.isDisplayed, - timeout) - .catch(tagChaiAssertionError); - }; - before(function(done) { // http://user:apiKey@ondemand.saucelabs.com/wd/hub + console.log('http://' + (args.server || 'ondemand.saucelabs.com') + '/wd/hub'); browser = wd.promiseChainRemote('http://' + (args.server || 'ondemand.saucelabs.com') + '/wd/hub'); if (args.verbose) { @@ -68,7 +55,8 @@ describe('system-test (' + desired.browserName + ')', function() { browser .init(desired) - .get('/notebooks/examples/urth-viz-table.ipynb') + .get('/notebooks/tests/Walkthrough.ipynb') + .sleep(7000) .waitForElementByLinkText("Cell", wd.asserters.isDisplayed, 10000) .elementByLinkText("Cell") .click() @@ -89,17 +77,107 @@ describe('system-test (' + desired.browserName + ')', function() { after(function(done) { var result = browser .quit(); - if (result.sauceJobStatus) { + if (process.env.SAUCE_USER_NAME) { result = result.sauceJobStatus(allPassed); } result.nodeify(done); }); - it('should run all cells and find a handsontable in the 3rd output area', function(done) { + it('should prints the correct variable that is used for urth-core-function', function(done) { + browser + .sleep(5000) + .elementsByCssSelector('<', 'div.output_area').nth(2) + .elementByXPath('//button[text()="invoke"]').click() + .sleep(5000) + .elementsByCssSelector('<', 'div.output_area').nth(2) + .elementByXPath('//span[@id="test1"]') + .text().then(function(txt) { + console.log("span test1 is: ", txt); + txt.should.include('world'); + }) + .nodeify(done); + }); + + it('should bind variable to channel a', function(done) { + browser + .elementsByCssSelector('div.output_area').nth(3) + .elementByCssSelector('>', 'input') + .type('A') + .elementsByCssSelector('<', 'div.output_area').nth(3) + .elementByXPath('//span[@id="test2"]') + .text().then(function(txt) { + console.log("span test2 is: ", txt); + txt.should.include('A'); + }) + .nodeify(done); + }); + + it('should bind variable to channel b', function(done) { browser - .waitForElementsByCssSelector('div.output_area').nth(3) - .waitForWidgetElement("urth-viz-table", browserSupportsShadowDOM, 10000) + .elementsByCssSelector('div.output_area').nth(4) + .elementByCssSelector('>', 'input') + .type('B') + .elementsByCssSelector('<', 'div.output_area').nth(4) + .elementByXPath('//span[@id="test3"]') + .text().then(function(txt) { + console.log("span test3 is: ", txt); + txt.should.include('B'); + }) .nodeify(done); }); + it('should bind variables to channels independently', function(done) { + browser + .elementsByCssSelector('div.output_area').nth(3) + .elementByCssSelector('>', 'input') + .type('2') + .elementsByCssSelector('<', 'div.output_area').nth(3) + .elementByXPath('//span[@id="test2"]') + .text().then(function(txt) { + console.log("span test2 is: ", txt); + txt.should.include('A2'); + }) + .elementsByCssSelector('<', 'div.output_area').nth(4) + .elementByXPath('//span[@id="test3"]') + .text().then(function(txt) { + console.log("span test3 is: ", txt); + txt.should.include('B'); + }) + .nodeify(done); + }); + + it('should watch for changes in a watched variable', function(done) { + browser + .elementsByCssSelector('div.output_area').nth(5) + .elementByCssSelector('>', 'input') + .type('watched message') + .sleep(3000) + .elementsByCssSelector('<', 'div.output_area').nth(6) + .elementByXPath('//span[@id="test4"]') + .text().then(function(txt) { + console.log("span test4 is: ", txt); + txt.should.include('watched message'); + }) + .nodeify(done); + }); + + it('should update output when DataFrame is modified and set to auto', function(done) { + browser + .elementsByCssSelector('div.input').nth(10) + .click() + .sleep(3000) + .elementByLinkText("<", "Cell") + .click() + .waitForElementByLinkText("Run", wd.asserters.isDisplayed, 10000) + .elementByLinkText("Run") + .click() + .sleep(3000) + .elementsByCssSelector('<', 'div.output_area').nth(7) + .elementByXPath('//span[@class="test5"]') + .text().then(function(txt) { + console.log("span test5 is: ", txt); + txt.should.include('Jane Doe'); + }) + .nodeify(done); + }); }); diff --git a/system-test/urth-viz-table-specs.js b/system-test/urth-viz-table-specs.js new file mode 100644 index 00000000..d3b9f132 --- /dev/null +++ b/system-test/urth-viz-table-specs.js @@ -0,0 +1,108 @@ +//options: +// baseurl - base url for notebook server to test +// server - the selenium server, defaults to ondemand.saucelabs.com +// platform - defaults to 'OS X 10.10' +// browser - defaults to 'chrome' +// verbose + +var wd = require('wd'); +require('colors'); +var chai = require('chai'); +var chaiAsPromised = require('chai-as-promised'); +var parseArgs = require('minimist'); +var args = parseArgs(process.argv); + +chai.use(chaiAsPromised); +chai.should(); +chaiAsPromised.transferPromiseness = wd.transferPromiseness; + +// http configuration, not needed for simple runs +wd.configureHttp({ + timeout: 60000, + retryDelay: 15000, + retries: 5, + baseUrl: args.baseurl +}); + +var desired = {browserName: 'chrome', platform: 'OS X 10.10'}; +desired.platform = args.platform || desired.platform; +desired.browserName = args.browser || desired.browserName; +desired.name = 'Urth System Test with ' + desired.browserName; +desired.tags = ['tutorial']; + +var Asserter = wd.Asserter; + +describe('system-test (' + desired.browserName + ')', function() { + var browser; + var allPassed = true; + var browserSupportsShadowDOM; + + var tagChaiAssertionError = function(err) { + // throw error and tag as retriable to poll again + err.retriable = err instanceof chai.AssertionError; + throw err; + }; + + wd.PromiseChainWebdriver.prototype.waitForWidgetElement = function(selector, browserSupportsShadowDOM, timeout, pollFreq) { + return this.waitForElementByCssSelector( + browserSupportsShadowDOM ? 'urth-viz-table::shadow .handsontable' : 'urth-viz-table .handsontable', + wd.asserters.isDisplayed, + timeout) + .catch(tagChaiAssertionError); + }; + + before(function(done) { + // http://user:apiKey@ondemand.saucelabs.com/wd/hub + var auth = args['sauce-username'] && args['sauce-access-key'] ? + args['sauce-username'] + ':' + args['sauce-access-key'] + '@' : ''; + console.log('http://' + auth + (args.server || 'ondemand.saucelabs.com') + '/wd/hub'); + browser = wd.promiseChainRemote('http://' + auth + (args.server || 'ondemand.saucelabs.com') + '/wd/hub'); + + if (args.verbose) { + // optional logging + browser.on('status', function(info) { + console.log(info.cyan); + }); + browser.on('command', function(meth, path, data) { + console.log(' > ' + meth.yellow, path.grey, data || ''); + }); + } + + browser + .init(desired) + .get('/notebooks/tests/urth-viz-table.ipynb') + .sleep(5000) + .waitForElementByLinkText("Cell", wd.asserters.isDisplayed, 10000) + .elementByLinkText("Cell") + .click() + .waitForElementByLinkText("Run All", wd.asserters.isDisplayed, 10000) + .elementByLinkText("Run All") + .click() + .eval("!!document.body.createShadowRoot", function(err, value) { + browserSupportsShadowDOM = value; + done(); + }); + }); + + afterEach(function(done) { + allPassed = allPassed && (this.currentTest.state === 'passed'); + done(); + }); + + after(function(done) { + var result = browser + .quit(); + if (process.env.SAUCE_USER_NAME) { + result = result.sauceJobStatus(allPassed); + } + result.nodeify(done); + }); + + it('should run all cells and find a handsontable in the 3rd output area', function(done) { + browser + .waitForElementsByCssSelector('div.output_area').nth(3) + .waitForWidgetElement("urth-viz-table", browserSupportsShadowDOM, 10000) + .nodeify(done); + }); + +});