From 872064e12e97fb01df7d4be558894473099f5be4 Mon Sep 17 00:00:00 2001 From: Jay Traband Date: Sun, 20 Dec 2015 13:17:16 -0800 Subject: [PATCH] feat(e2e testing): testing framework + initial tests close #604 --- .gitignore | 1 + gulpfile.js | 185 ++++++++++++++++-- package.json | 7 +- public/docs/_examples/.gitignore | 3 +- .../docs/_examples/architecture/e2e-spec.js | 60 ++++++ .../attribute-directives/e2e-spec.js | 26 +++ .../_examples/displaying-data/e2e-spec.js | 21 ++ public/docs/_examples/forms/e2e-spec.js | 61 ++++++ .../e2e-spec.js | 55 ++++++ .../_examples/lifecycle-hooks/e2e-spec.js | 154 +++++++++++++++ public/docs/_examples/package.json | 18 +- public/docs/_examples/pipes/e2e-spec.js | 78 ++++++++ ...tractor-conf.js => protractor-conf.old.js} | 0 public/docs/_examples/protractor.config.js | 161 +++++++++++++++ public/docs/_examples/quickstart/e2e-spec.js | 26 +-- .../structural-directives/e2e-spec.js | 64 ++++++ .../_examples/styleguide/example-config.json | 0 .../_examples/template-syntax/e2e-spec.js | 29 +++ public/docs/_examples/tutorial/e2e-spec.js | 46 +++++ 19 files changed, 950 insertions(+), 45 deletions(-) create mode 100644 public/docs/_examples/architecture/e2e-spec.js create mode 100644 public/docs/_examples/attribute-directives/e2e-spec.js create mode 100644 public/docs/_examples/displaying-data/e2e-spec.js create mode 100644 public/docs/_examples/forms/e2e-spec.js create mode 100644 public/docs/_examples/hierarchical-dependency-injection/e2e-spec.js create mode 100644 public/docs/_examples/lifecycle-hooks/e2e-spec.js create mode 100644 public/docs/_examples/pipes/e2e-spec.js rename public/docs/_examples/{protractor-conf.js => protractor-conf.old.js} (100%) create mode 100644 public/docs/_examples/protractor.config.js create mode 100644 public/docs/_examples/structural-directives/e2e-spec.js delete mode 100644 public/docs/_examples/styleguide/example-config.json create mode 100644 public/docs/_examples/template-syntax/e2e-spec.js create mode 100644 public/docs/_examples/tutorial/e2e-spec.js diff --git a/.gitignore b/.gitignore index 3bb82b4110..451f2be222 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,4 @@ npm-debug.log.* plnkr.html *plnkr.no-link.html public/docs/*/latest/guide/cheatsheet.json +protractor-results.txt diff --git a/gulpfile.js b/gulpfile.js index 9daf60b7e2..46f9753d05 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -15,8 +15,13 @@ var fsExtra = require('fs-extra'); var fs = fsExtra; var exec = require('child_process').exec; var execPromise = Q.denodeify(exec); +// cross platform version of spawn that also works on windows. +var xSpawn = require('cross-spawn'); var prompt = require('prompt'); var globby = require("globby"); +// Ugh... replacement needed to kill processes on any OS +// - because childProcess.kill does not work properly on windows +var treeKill = require("tree-kill"); // TODO: // 1. Think about using runSequence @@ -56,6 +61,120 @@ var _excludeMatchers = _excludePatterns.map(function(excludePattern){ var _exampleBoilerplateFiles = ['package.json', 'tsconfig.json', 'karma.conf.js', 'karma-test-shim.js' ] +// --filter may be passed in to filter/select _example app subdir names +// i.e. gulp run-e2e-tests --filter=foo ; would select all example apps with +// 'foo' in their folder names. +gulp.task('run-e2e-tests', function() { + var spawnInfo = spawnExt('npm', ['install'], { cwd: EXAMPLES_PATH}); + return spawnInfo.promise.then(function() { + copyExampleBoilerplate(); + var exePath = path.join(process.cwd(), "./node_modules/.bin/"); + spawnInfo = spawnExt('webdriver-manager', ['update'], {cwd: exePath}); + return spawnInfo.promise; + }).then(function(x) { + return findAndRunE2eTests(argv.filter); + }).fail(function(e) { + return e; + }); +}); + +// finds all of the *e2e-spec.tests under the _examples folder along +// with the corresponding apps that they should run under. Then run +// each app/spec collection sequentially. +function findAndRunE2eTests(filter) { + // create an output file with header. + var outputFile = path.join(process.cwd(), 'protractor-results.txt'); + var header = "Protractor example results for: " + (new Date()).toLocaleString() + "\n\n"; + if (filter) { + header += ' Filter: ' + filter.toString() + '\n\n'; + } + fs.writeFileSync(outputFile, header); + + // create an array of combos where each + // combo consists of { examplePath: ... , protractorConfigFilename: ... } + var exeConfigs = []; + var e2eSpecPaths = getE2eSpecPaths(EXAMPLES_PATH); + var srcConfig = path.join(EXAMPLES_PATH, 'protractor.config.js'); + e2eSpecPaths.forEach(function(specPath) { + var destConfig = path.join(specPath, 'protractor.config.js'); + fsExtra.copySync(srcConfig, destConfig); + // get all of the examples under each dir where a pcFilename is found + examplePaths = getExamplePaths(specPath, true); + if (filter) { + examplePaths = examplePaths.filter(function (fn) { + return fn.match(filter) != null; + }) + } + examplePaths.forEach(function(exPath) { + exeConfigs.push( { examplePath: exPath, protractorConfigFilename: destConfig }); + }) + }); + + // run the tests sequentially + return exeConfigs.reduce(function (promise, combo) { + return promise.then(function () { + return runE2eTests(combo.examplePath, combo.protractorConfigFilename, outputFile); + }); + }, Q.resolve()); +} + +// start the example in appDir; then run protractor with the specified +// fileName; then shut down the example. All protractor output is appended +// to the outputFile. +function runE2eTests(appDir, protractorConfigFilename, outputFile ) { + // start the app + var appRunSpawnInfo = spawnExt('npm',['run','http-server', '--', '-s' ], { cwd: appDir }); + + // start protractor + var pcFilename = path.resolve(protractorConfigFilename); // need to resolve because we are going to be running from a different dir + var exePath = path.join(process.cwd(), "./node_modules/.bin/"); + var spawnInfo = spawnExt('protractor', + [ pcFilename, '--params.appDir=' + appDir, '--params.outputFile=' + outputFile], { cwd: exePath }); + return spawnInfo.promise.then(function(data) { + // kill the app now that protractor has completed. + treeKill(appRunSpawnInfo.proc.pid); + // Ugh... proc.kill does not work properly on windows with child processes. + // appRun.proc.kill(); + return data; + }).fail(function(err) { + // Ugh... proc.kill does not work properly on windows with child processes. + // appRun.proc.kill(); + treeKill(appRunSpawnInfo.proc.pid) + return err; + }); +} + +// returns both a promise and the spawned process so that it can be killed if needed. +function spawnExt(command, args, options) { + var deferred = Q.defer(); + var descr = command + " " + args.join(' '); + var proc; + gutil.log('running: ' + descr); + try { + proc = xSpawn.spawn(command, args, options); + } catch(e) { + gutil.log(e); + deferred.reject(e); + return { proc: null, promise: deferred.promise }; + } + proc.stdout.on('data', function (data) { + gutil.log(data.toString()); + }); + proc.stderr.on('data', function (data) { + gutil.log(data.toString()); + }); + proc.on('close', function (data) { + gutil.log('completed: ' + descr); + deferred.resolve(data); + }); + proc.on('error', function (data) { + gutil.log('completed with error:' + descr); + gutil.log(data.toString()); + deferred.reject(data); + }); + return { proc: proc, promise: deferred.promise }; +} + // Public tasks gulp.task('default', ['help']); @@ -69,7 +188,7 @@ gulp.task('help', taskListing.withFilters(function(taskName) { })); // requires admin access -gulp.task('add-example-boilerplate', function() { +gulp.task('add-example-boilerplate', function() { var realPath = path.join(EXAMPLES_PATH, '/node_modules'); var nodeModulesPaths = getNodeModulesPaths(EXAMPLES_PATH); @@ -77,12 +196,25 @@ gulp.task('add-example-boilerplate', function() { gutil.log("symlinking " + linkPath + ' -> ' + realPath) fsUtils.addSymlink(realPath, linkPath); }); + copyExampleBoilerplate(); +}); + +// copies boilerplate files to locations +// where an example app is found +// also copies protractor.config.js file +function copyExampleBoilerplate() { var sourceFiles = _exampleBoilerplateFiles.map(function(fn) { return path.join(EXAMPLES_PATH, fn); }); var examplePaths = getExamplePaths(EXAMPLES_PATH); - return copyFiles(sourceFiles, examplePaths ); -}); + // copies protractor.config.js from _examples dir to each subdir that + // contains a e2e-spec file. + return copyFiles(sourceFiles, examplePaths).then(function() { + var sourceFiles = [ path.join(EXAMPLES_PATH, 'protractor.config.js') ]; + var e2eSpecPaths = getE2eSpecPaths(EXAMPLES_PATH); + return copyFiles(sourceFiles, e2eSpecPaths); + }); +} gulp.task('remove-example-boilerplate', function() { var nodeModulesPaths = getNodeModulesPaths(EXAMPLES_PATH); @@ -90,7 +222,10 @@ gulp.task('remove-example-boilerplate', function() { fsUtils.removeSymlink(linkPath); }); var examplePaths = getExamplePaths(EXAMPLES_PATH); - return deleteFiles(_exampleBoilerplateFiles, examplePaths ); + return deleteFiles(_exampleBoilerplateFiles, examplePaths).then(function() { + var e2eSpecPaths = getE2eSpecPaths(EXAMPLES_PATH); + return deleteFiles(['protractor.config.js'], e2eSpecPaths); + }) }); gulp.task('serve-and-sync', ['build-docs'], function (cb) { @@ -264,18 +399,11 @@ function deleteFiles(baseFileNames, destPaths) { return Q.all(delPromises); } -function getExamplePaths(basePath) { - var jsonPattern = path.join(basePath, "**/example-config.json"); - // ignore (skip) the top level version. - var exceptJsonPattern = "!" + path.join(basePath, "/example-config.json"); - var nmPattern = path.join(basePath, "**/node_modules/**"); - var fileNames = globby.sync( [ jsonPattern, exceptJsonPattern ], { ignore: [nmPattern] } ); - // same as above but perf can differ. - // var fileNames = globby.sync( [jsonPattern, "!" + nmPattern]); - var paths = fileNames.map(function(fileName) { - return path.dirname(fileName); - }); - return paths; +// TODO: filter out all paths that are subdirs of another +// path in the result. +function getE2eSpecPaths(basePath) { + var paths = getPaths(basePath, '*e2e-spec.js', true); + return _.uniq(paths); } function getNodeModulesPaths(basePath) { @@ -285,6 +413,31 @@ function getNodeModulesPaths(basePath) { return paths; } +function getExamplePaths(basePath, includeBase) { + // includeBase defaults to false + return getPaths(basePath, "example-config.json", includeBase) +} + +function getPaths(basePath, filename, includeBase) { + var filenames = getFilenames(basePath, filename, includeBase); + var paths = filenames.map(function(fileName) { + return path.dirname(fileName); + }); + return paths; +} + +function getFilenames(basePath, filename, includeBase) { + // includeBase defaults to false + var includePatterns = [path.join(basePath, "**/" + filename)]; + if (!includeBase) { + // ignore (skip) the top level version. + includePatterns.push("!" + path.join(basePath, "/" + filename)); + } + var nmPattern = path.join(basePath, "**/node_modules/**"); + var filenames = globby.sync(includePatterns, {ignore: [nmPattern]}); + return filenames; +} + function watchAndSync(options, cb) { execCommands(['npm run harp -- server .'], {}, cb); diff --git a/package.json b/package.json index d43b938d64..207324642e 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,9 @@ "test": "echo \"Error: no test specified\" && exit 1", "harp": "harp", "live-server": "live-server", - "test-api-builder": "jasmine-node tools/api-builder" + "test-api-builder": "jasmine-node tools/api-builder", + + "protractor": "protractor" }, "repository": { "type": "git", @@ -28,6 +30,7 @@ "assert-plus": "^0.1.5", "browser-sync": "^2.9.3", "canonical-path": "0.0.2", + "cross-spawn": "^2.1.0", "del": "^1.2.0", "dgeni": "^0.4.0", "dgeni-packages": "^0.11.1", @@ -58,7 +61,9 @@ "nodegit": "0.5.0", "path": "^0.11.14", "prompt": "^0.2.14", + "protractor": "^3.0.0", "q": "^1.4.1", + "tree-kill": "^1.0.0", "typescript": "1.7.3", "yargs": "^3.23.0" }, diff --git a/public/docs/_examples/.gitignore b/public/docs/_examples/.gitignore index 02d898bf04..816d48cb07 100644 --- a/public/docs/_examples/.gitignore +++ b/public/docs/_examples/.gitignore @@ -4,4 +4,5 @@ package.json karma.conf.js karma-test-shim.js tsconfig.json -npm-debug*.log +npm-debug*. +**/protractor.config.js diff --git a/public/docs/_examples/architecture/e2e-spec.js b/public/docs/_examples/architecture/e2e-spec.js new file mode 100644 index 0000000000..1be545fa6c --- /dev/null +++ b/public/docs/_examples/architecture/e2e-spec.js @@ -0,0 +1,60 @@ +describe('Architecture', function () { + + var _title = "Hero List"; + + beforeAll(function () { + browser.get(''); + }); + + function itReset(name, func) { + it(name, function() { + browser.get('').then(func); + }); + } + + it('should display correct title: ' + _title, function () { + expect(element(by.css('h2')).getText()).toEqual(_title); + }); + + it('should display correct detail after selection', function() { + var detailView = element(by.css('hero-detail')); + expect(detailView.isPresent()).toBe(false); + // select the 2nd element + var selectEle = element.all(by.css('hero-list > div')).get(1); + selectEle.click().then(function() { + return selectEle.getText(); + }).then(function(selectedHeroName) { + // works but too specific if we change the app + // expect(selectedHeroName).toEqual('Mr. Nice'); + expect(detailView.isDisplayed()).toBe(true); + var detailTitleEle = element(by.css('hero-detail > h4')); + expect(detailTitleEle.getText()).toContain(selectedHeroName); + }); + }) + + itReset('should display correct detail after modification', function() { + var detailView = element(by.css('hero-detail')); + expect(detailView.isPresent()).toBe(false); + // select the 2nd element + var selectEle = element.all(by.css('hero-list > div')).get(1); + selectEle.click().then(function () { + return selectEle.getText(); + }).then(function (selectedHeroName) { + var detailTitleEle = element(by.css('hero-detail > h4')); + expect(detailTitleEle.getText()).toContain(selectedHeroName); + var heroNameEle = element.all(by.css('hero-detail input')).get(0); + + // check that both the initial selected item and the detail title reflect changes + // made to the input box. + heroNameEle.sendKeys('foo'); + expect(detailTitleEle.getText()).toContain('foo'); + expect(selectEle.getText()).toContain('foo'); + + // getText on an input element always returns null + // http://stackoverflow.com/questions/20310442/how-to-gettext-on-an-input-in-protractor + // expect(heroNameEle.getText()).toEqual(selectedHeroName); + expect(heroNameEle.getAttribute('value')).toEqual(selectedHeroName + 'foo'); + }); + }) + +}); diff --git a/public/docs/_examples/attribute-directives/e2e-spec.js b/public/docs/_examples/attribute-directives/e2e-spec.js new file mode 100644 index 0000000000..16fa374c75 --- /dev/null +++ b/public/docs/_examples/attribute-directives/e2e-spec.js @@ -0,0 +1,26 @@ +describe('Attribute directives', function () { + + var _title = "My First Attribute Directive"; + + beforeAll(function () { + browser.get(''); + }); + + it('should display correct title: ' + _title, function () { + expect(element(by.css('h1')).getText()).toEqual(_title); + }); + + it('should be able to select green highlight', function () { + var highlightedEle = element(by.cssContainingText('p', 'Highlight me')); + var lightGreen = "rgba(144, 238, 144, 1)"; + + expect(highlightedEle.getCssValue('background-color')).not.toEqual(lightGreen); + // var greenRb = element(by.cssContainingText('input', 'Green')); + var greenRb = element.all(by.css('input')).get(0); + greenRb.click().then(function() { + browser.actions().mouseMove(highlightedEle).perform(); + expect(highlightedEle.getCssValue('background-color')).toEqual(lightGreen); + }); + + }); +}); diff --git a/public/docs/_examples/displaying-data/e2e-spec.js b/public/docs/_examples/displaying-data/e2e-spec.js new file mode 100644 index 0000000000..4768050501 --- /dev/null +++ b/public/docs/_examples/displaying-data/e2e-spec.js @@ -0,0 +1,21 @@ +describe('Displaying Data Tests', function () { + + var _title = "Tour of Heroes"; + var _defaultHero = 'Windstorm' + + beforeAll(function () { + browser.get(''); + }); + + it('should display correct title: ' + _title, function () { + expect(element(by.css('h1')).getText()).toEqual(_title); + }); + + it('should have correct default hero: ' + _defaultHero, function () { + expect(element(by.css('h2')).getText()).toContain(_defaultHero); + }); + + it('should have many heroes', function () { + expect(element(by.css('ul ~ p')).getText()).toContain('There are many heroes!'); + }); +}); diff --git a/public/docs/_examples/forms/e2e-spec.js b/public/docs/_examples/forms/e2e-spec.js new file mode 100644 index 0000000000..093b170688 --- /dev/null +++ b/public/docs/_examples/forms/e2e-spec.js @@ -0,0 +1,61 @@ +describeIf(browser.appIsTs, 'Forms Tests', function () { + + beforeEach(function () { + browser.get(''); + }); + + it('should display correct title', function () { + expect(element.all(by.css('h1')).get(0).getText()).toEqual('Hero Form'); + }); + + + it('should not display message before submit', function () { + var ele = element(by.css('h2')); + expect(ele.isDisplayed()).toBe(false); + }); + + it('should hide form after submit', function () { + var ele = element.all(by.css('h1')).get(0); + expect(ele.isDisplayed()).toBe(true); + var b = element.all(by.css('button[type=submit]')).get(0); + b.click().then(function() { + expect(ele.isDisplayed()).toBe(false); + }); + }); + + it('should display message after submit', function () { + var b = element.all(by.css('button[type=submit]')).get(0); + b.click().then(function() { + expect(element(by.css('h2')).getText()).toContain('You submitted the following'); + }); + }); + + it('should hide form after submit', function () { + var alterEgoEle = element.all(by.css('input[ngcontrol=alterEgo]')).get(0); + expect(alterEgoEle.isDisplayed()).toBe(true); + var submitButtonEle = element.all(by.css('button[type=submit]')).get(0); + submitButtonEle.click().then(function() { + expect(alterEgoEle.isDisplayed()).toBe(false); + }) + }); + + it('should reflect submitted data after submit', function () { + var test = 'testing 1 2 3'; + var newValue; + var alterEgoEle = element.all(by.css('input[ngcontrol=alterEgo]')).get(0); + alterEgoEle.getAttribute('value').then(function(value) { + alterEgoEle.sendKeys(test); + newValue = value + test; + expect(alterEgoEle.getAttribute('value')).toEqual(newValue); + }).then(function() { + var b = element.all(by.css('button[type=submit]')).get(0); + return b.click(); + }).then(function() { + var alterEgoTextEle = element(by.cssContainingText('div', 'Alter Ego')); + expect(alterEgoTextEle.isPresent()).toBe(true, 'cannot locate "Alter Ego" label'); + var divEle = element(by.cssContainingText('div', newValue)); + expect(divEle.isPresent()).toBe(true, 'cannot locate div with this text: ' + newValue); + }); + }); +}); + diff --git a/public/docs/_examples/hierarchical-dependency-injection/e2e-spec.js b/public/docs/_examples/hierarchical-dependency-injection/e2e-spec.js new file mode 100644 index 0000000000..df867c8a62 --- /dev/null +++ b/public/docs/_examples/hierarchical-dependency-injection/e2e-spec.js @@ -0,0 +1,55 @@ +describe('Hierarchical dependency injection', function () { + + beforeEach(function () { + browser.get(''); + }); + + it('should open with a card view', function () { + expect(element.all(by.cssContainingText('button','edit')).get(0).isDisplayed()).toBe(true, + "edit button should be displayed"); + }); + + it('should have multiple heros listed', function () { + expect(element.all(by.css('heroes-list li')).count()).toBeGreaterThan(1); + }); + + it('should change to editor view after selection', function () { + var editButtonEle = element.all(by.cssContainingText('button','edit')).get(0); + editButtonEle.click().then(function() { + expect(editButtonEle.isDisplayed()).toBe(false, "edit button should be hidden after selection"); + }) + }); + + it('should be able to save editor change', function () { + testEdit(true); + }); + + it('should be able to cancel editor change', function () { + testEdit(false); + }); + + function testEdit(shouldSave) { + var inputEle; + // select 2nd ele + var heroEle = element.all(by.css('heroes-list li')).get(1); + // get the 2nd span which is the name of the hero + var heroNameEle = heroEle.all(by.css('hero-card span')).get(1); + var editButtonEle = heroEle.element(by.cssContainingText('button','edit')); + editButtonEle.click().then(function() { + inputEle = heroEle.element(by.css('hero-editor input')); + return inputEle.sendKeys("foo"); + }).then(function() { + buttonName = shouldSave ? 'save' : 'cancel'; + var buttonEle = heroEle.element(by.cssContainingText('button', buttonName)); + return buttonEle.click(); + }).then(function() { + if (shouldSave) { + expect(heroNameEle.getText()).toContain('foo'); + } else { + expect(heroNameEle.getText()).not.toContain('foo'); + } + }) + } + + +}); diff --git a/public/docs/_examples/lifecycle-hooks/e2e-spec.js b/public/docs/_examples/lifecycle-hooks/e2e-spec.js new file mode 100644 index 0000000000..13c0421737 --- /dev/null +++ b/public/docs/_examples/lifecycle-hooks/e2e-spec.js @@ -0,0 +1,154 @@ +describe('Lifecycle hooks', function () { + + beforeAll(function () { + browser.get(''); + }); + + it('should open correctly', function () { + expect(element.all(by.css('h2')).get(0).getText()).toEqual('Peek-A-Boo'); + }); + + it('should be able to drive peek-a-boo button', function () { + var pabComp = element(by.css('peek-a-boo-parent peek-a-boo')); + expect(pabComp.isPresent()).toBe(false, "should not be able to find the 'peek-a-boo' component"); + var pabButton = element.all(by.css('peek-a-boo-parent button')).get(0); + var updateHeroButton = element.all(by.css('peek-a-boo-parent button')).get(1); + expect(pabButton.getText()).toContain('Create Peek'); + pabButton.click().then(function () { + expect(pabButton.getText()).toContain('Destroy Peek'); + expect(pabComp.isDisplayed()).toBe(true, "should be able to see the 'peek-a-boo' component"); + expect(pabComp.getText()).toContain('Windstorm'); + expect(pabComp.getText()).not.toContain('Windstorm!'); + expect(updateHeroButton.isPresent()).toBe(true, "should be able to see the update hero button"); + return updateHeroButton.click(); + }).then(function () { + expect(pabComp.getText()).toContain('Windstorm!'); + return pabButton.click(); + }).then(function () { + expect(pabComp.isPresent()).toBe(false, "should no longer be able to find the 'peek-a-boo' component"); + }); + }); + + it('should be able to trigger onChanges', function () { + var onChangesViewEle = element.all(by.css('on-changes-parent my-hero div')).get(0); + var inputEles = element.all(by.css('on-changes-parent input')); + var heroNameInputEle = inputEles.get(0); + var powerInputEle = inputEles.get(1); + var titleEle = onChangesViewEle.element(by.css('p')); + expect(titleEle.getText()).toContain('Windstorm can sing'); + var changeLogEles = onChangesViewEle.all(by.css('div')); + expect(changeLogEles.count()).toEqual(3, "should start with 3 messages"); + heroNameInputEle.sendKeys('-foo-').then(function () { + expect(titleEle.getText()).toContain('Windstorm-foo- can sing'); + expect(changeLogEles.count()).toEqual(3, "should still have 3 messages"); + // protractor bug with sendKeys means that line below does not work. + // return powerInputEle.sendKeys('-bar-'); + return sendKeys(powerInputEle, '-bar-'); + }).then(function () { + expect(titleEle.getText()).toContain('Windstorm-foo- can sing-bar-'); + // 8 == 3 previously + length of '-bar-' + expect(changeLogEles.count()).toEqual(8, "should have 8 messages now"); + }); + }); + + it('should support after-view hooks', function () { + var inputEle = element(by.css('after-view-parent input')); + var buttonEle = element(by.css('after-view-parent button')); + var logEles = element.all(by.css('after-view-parent h4 ~ div')); + var childViewTextEle = element(by.css('after-view-parent my-child .child')); + expect(childViewTextEle.getText()).toContain('Magneta is my hero'); + expect(logEles.count()).toBeGreaterThan(2); + var logCount; + logEles.count().then(function(count) { + logCount = count; + return sendKeys(inputEle, "-test-"); + }).then(function() { + expect(childViewTextEle.getText()).toContain('-test-'); + return logEles.count(); + }).then(function(count) { + expect(logCount + 6).toEqual(count, "6 additional log messages should have been added"); + logCount = count; + return buttonEle.click(); + }).then(function() { + expect(childViewTextEle.isPresent()).toBe(false,"child view should no longer be part of the DOM"); + sendKeys(inputEle, "-foo-"); + expect(logEles.count()).toEqual(logCount, "no additional log messages should have been added"); + }); + }); + + it('should support after-content hooks', function () { + var inputEle = element(by.css('after-content-parent input')); + var buttonEle = element(by.css('after-content-parent button')); + var logEles = element.all(by.css('after-content-parent h4 ~ div')); + var childViewTextEle = element(by.css('after-content-parent my-child .child')); + expect(childViewTextEle.getText()).toContain('Magneta is my hero'); + expect(logEles.count()).toBeGreaterThan(2); + var logCount; + logEles.count().then(function(count) { + logCount = count; + return sendKeys(inputEle, "-test-"); + }).then(function() { + expect(childViewTextEle.getText()).toContain('-test-'); + return logEles.count(); + }).then(function(count) { + expect(logCount + 6).toEqual(count, "6 additional log messages should have been added"); + logCount = count; + return buttonEle.click(); + }).then(function() { + expect(childViewTextEle.isPresent()).toBe(false,"child view should no longer be part of the DOM"); + sendKeys(inputEle, "-foo-"); + expect(logEles.count()).toEqual(logCount, "no additional log messages should have been added"); + }); + }); + + it('should support "spy" hooks', function () { + var inputEle = element(by.css('spy-parent input')); + var addHeroButtonEle = element(by.cssContainingText('spy-parent button','Add Hero')); + var resetHeroesButtonEle = element(by.cssContainingText('spy-parent button','Reset Heroes')); + var heroEles = element.all(by.css('spy-parent div[my-spy')); + var logEles = element.all(by.css('spy-parent h4 ~ div')); + expect(heroEles.count()).toBe(2, 'should have two heroes displayed'); + expect(logEles.count()).toBe(2, 'should have two log entries'); + sendKeys(inputEle, "-test-").then(function() { + return addHeroButtonEle.click(); + }).then(function() { + expect(heroEles.count()).toBe(3, 'should have added one hero'); + expect(heroEles.get(2).getText()).toContain('-test-'); + expect(logEles.count()).toBe(3, 'should now have 3 log entries'); + return resetHeroesButtonEle.click(); + }).then(function() { + expect(heroEles.count()).toBe(0, 'should no longer have any heroes'); + expect(logEles.count()).toBe(7, 'should now have 7 log entries - 3 orig + 1 reset + 3 removeall'); + }) + }); + + it('should support "spy counter" hooks', function () { + var updateCounterButtonEle = element(by.cssContainingText('counter-parent button','Update')); + var resetCounterButtonEle = element(by.cssContainingText('counter-parent button','Reset')); + var textEle = element(by.css('counter-parent my-counter > div')); + var logEles = element.all(by.css('counter-parent h4 ~ div')); + expect(textEle.getText()).toContain('Counter = 0'); + expect(logEles.count()).toBe(2, 'should start with two log entries'); + updateCounterButtonEle.click().then(function() { + expect(textEle.getText()).toContain('Counter = 1'); + expect(logEles.count()).toBe(3, 'should now have 3 log entries'); + return resetCounterButtonEle.click(); + }).then(function() { + expect(textEle.getText()).toContain('Counter = 0'); + expect(logEles.count()).toBe(7, 'should now have 7 log entries - 3 prev + 1 reset + 2 destroy + 1 init'); + }) + }); + + + // Hack - because of bug with send keys + function sendKeys(element, str) { + return str.split('').reduce(function (promise, char) { + return promise.then(function () { + return element.sendKeys(char); + }); + }, element.getAttribute('value')); + // better to create a resolved promise here but ... don't know how with protractor; + } + + +}); diff --git a/public/docs/_examples/package.json b/public/docs/_examples/package.json index c76ab2026e..a5336a34fc 100644 --- a/public/docs/_examples/package.json +++ b/public/docs/_examples/package.json @@ -10,7 +10,8 @@ "live": "live-server", "start": "concurrent \"npm run tsc:w\" \"npm run lite\" ", "test": "karma start karma.conf.js", - "build-and-test": "npm run tsc && npm run test" + "build-and-test": "npm run tsc && npm run test", + "http-server": "tsc && http-server" }, "keywords": [], "author": "", @@ -27,15 +28,16 @@ }, "devDependencies": { "concurrently": "^1.0.0", - "lite-server": "^1.3.1", - "live-server": "^0.8.2", - "typescript": "^1.7.3", - "jasmine-core":"~2.1.0", + "http-server": "^0.8.5", + "jasmine-core": "~2.1.0", "karma": "^0.12.23", "karma-chrome-launcher": "^0.1.4", "karma-cli": "^0.0.4", "karma-jasmine": "^0.3.6", - "rimraf": "^2.4.3" - } - + "lite-server": "^1.3.1", + "live-server": "^0.8.2", + "rimraf": "^2.4.3", + "typescript": "^1.7.3" + }, + "repository": { } } diff --git a/public/docs/_examples/pipes/e2e-spec.js b/public/docs/_examples/pipes/e2e-spec.js new file mode 100644 index 0000000000..2a4318d162 --- /dev/null +++ b/public/docs/_examples/pipes/e2e-spec.js @@ -0,0 +1,78 @@ +describe('Pipes', function () { + + beforeAll(function () { + browser.get(''); + }); + + it('should open correctly', function () { + expect(element.all(by.css('h4')).get(0).getText()).toEqual('Hero Birthday v.1'); + expect(element(by.css('body > hero-birthday p')).getText()).toEqual("The hero's birthday is Apr 15, 1988"); + }); + + it('should show delayed message', function () { + expect(element.all(by.css('hero-message')).get(0).getText()).toEqual('Message: You are my Hero!'); + }); + + it('should show 4 heroes', function () { + expect(element.all(by.css('hero-list div')).count()).toEqual(4); + }); + + it('should show 4 heroes in json', function () { + expect(element(by.cssContainingText('hero-list p', 'Heroes as JSON')).getText()).toContain('Bombasto'); + }); + + it('should show alternate birthday formats', function () { + expect(element(by.cssContainingText('my-app > p', "The hero's birthday is Apr 15, 1988")).isDisplayed()).toBe(true); + expect(element(by.cssContainingText('my-app > p', "The hero's birthday is 04/15/88")).isDisplayed()).toBe(true); + }); + + it('should be able to toggle birthday formats', function () { + var birthDayEle = element(by.css('my-app > hero-birthday > p')); + expect(birthDayEle.getText()).toEqual("The hero's birthday is 4/15/1988"); + var buttonEle = element(by.cssContainingText('my-app > hero-birthday > button', "Toggle Format")); + expect(buttonEle.isDisplayed()).toBe(true); + buttonEle.click().then(function() { + expect(birthDayEle.getText()).toEqual("The hero's birthday is Friday, April 15, 1988"); + }); + }); + + it('should be able to chain and compose pipes', function () { + var chainedPipeEles = element.all(by.cssContainingText('my-app p', "The chained hero's")); + expect(chainedPipeEles.count()).toBe(3, "should have 3 chained pipe examples"); + expect(chainedPipeEles.get(0).getText()).toContain('APR 15, 1988'); + expect(chainedPipeEles.get(1).getText()).toContain('FRIDAY, APRIL 15, 1988'); + expect(chainedPipeEles.get(2).getText()).toContain('FRIDAY, APRIL 15, 1988'); + }); + + it('should be able to use ExponentialStrengthPipe pipe', function () { + var ele = element(by.css('power-booster p')); + expect(ele.getText()).toContain('Super power boost: 1024'); + }); + + it('should be able to use the exponential calculator', function () { + var eles = element.all(by.css('power-boost-calculator input')); + var baseInputEle = eles.get(0); + var factorInputEle = eles.get(1); + var outputEle = element(by.css('power-boost-calculator p')); + baseInputEle.clear().then(function() { + return sendKeys(baseInputEle, "7"); + }).then(function() { + return factorInputEle.clear(); + }).then(function() { + return sendKeys(factorInputEle, "3"); + }).then(function() { + expect(outputEle.getText()).toContain("343"); + }); + }); + + // Hack - because of bug with send keys + function sendKeys(element, str) { + return str.split('').reduce(function (promise, char) { + return promise.then(function () { + return element.sendKeys(char); + }); + }, element.getAttribute('value')); + // better to create a resolved promise here but ... don't know how with protractor; + } + +}); diff --git a/public/docs/_examples/protractor-conf.js b/public/docs/_examples/protractor-conf.old.js similarity index 100% rename from public/docs/_examples/protractor-conf.js rename to public/docs/_examples/protractor-conf.old.js diff --git a/public/docs/_examples/protractor.config.js b/public/docs/_examples/protractor.config.js new file mode 100644 index 0000000000..217c3a08e2 --- /dev/null +++ b/public/docs/_examples/protractor.config.js @@ -0,0 +1,161 @@ +// TO RUN THE TESTS +// +// The first time, run: +// ./node_modules/.bin/webdriver-manager update +// Make sure the test server is running. Then do. +// ./node_modules/.bin/protractor protractor.config.js + +var fs = require('fs'); +var path = require('canonical-path'); + + +exports.config = { + directConnect: true, + + // Capabilities to be passed to the webdriver instance. + capabilities: { + 'browserName': 'chrome' + }, + + // Framework to use. Jasmine is recommended. + framework: 'jasmine', + + // Spec patterns are relative to this config file + specs: ['**/*e2e-spec.js' ], + + + // For angular2 tests + useAllAngular2AppRoots: true, + + baseUrl: 'http://localhost:8080', + + // doesn't seem to work. + // resultJsonOutputFile: "foo.json", + + onPrepare: function() { + // SpecReporter + //var SpecReporter = require('jasmine-spec-reporter'); + //// add jasmine spec reporter + //jasmine.getEnv().addReporter(new SpecReporter({displayStacktrace: 'none'})); + // jasmine.getEnv().addReporter(new SpecReporter({displayStacktrace: 'all'})); + + // debugging + console.log('browser.params:' + JSON.stringify(browser.params)); + var appDir = browser.params.appDir; + if (appDir) { + if (appDir.match('/ts') != null) { + browser.appIsTs = true; + } else if (appDir.match('/js') != null) { + browser.appIsJs = true; + } else if (appDir.match('/dart') != null) { + browser.appIsDart = true; + } else { + browser.appIsUnknown = true; + } + } else { + browser.appIsUnknown = true; + } + jasmine.getEnv().addReporter(new Reporter( browser.params )) ; + global.describeIf = describeIf; + global.itIf = itIf; + }, + + jasmineNodeOpts: { + // defaultTimeoutInterval: 60000, + defaultTimeoutInterval: 10000, + showTiming: true, + print: function() {} + } +}; + +function describeIf(cond, name, func) { + if (cond) { + describe(name, func); + } else { + xdescribe('*** Skipped *** - ' + name, func); + } +} + +function itIf(cond, name, func) { + if (cond) { + it(name, func); + } else { + xit('*** Skipped *** - ' + name, func); + } +} + + + +function Reporter(options) { + this.defaultOutputFile = path.resolve(process.cwd(), "../../", 'protractor-results.txt'); + var _output = []; + var _logDepth = 0; + var __pad = ' '; + var _currentSuite; + options.outputFile = options.outputFile || this.defaultOutputFile; + log('AppDir: ' + options.appDir); + + this.suiteStarted = function(suite) { + _logDepth++; + log('Suite: ' + suite.description); + // debugging info + // log('Suite stringify:' + JSON.stringify(suite)); + // log('argv stringify:' + JSON.stringify(process.argv)); + _currentSuite = suite; + _currentSuite.ok = true; + }; + + this.suiteDone = function(suite) { + var status = _currentSuite.ok ? 'Suite passed: ' : 'Suite failed: '; + log(status + suite.description); + _logDepth--; + }; + + this.specStarted = function(spec) { + _logDepth++; + }; + + this.specDone = function(spec) { + log(spec.status + ": " + spec.description); + logFailedSpec(spec); + _logDepth--; + }; + + this.jasmineDone = function() { + outputFile = options.outputFile; + // console.log('Completed - appending output to: ' + outputFile); + fs.appendFileSync(outputFile, _output.join('\n')+ '\n\n'); + }; + + function logFailedSpec(spec) { + if (spec.failedExpectations.length == 0) return; + _logDepth++; + spec.failedExpectations.forEach(function(exp) { + log('detail: ' + exp.message); + _currentSuite.ok = false; + }); + _logDepth--; + } + + function log(msg) { + msg = lpad(msg, 3 * _logDepth); + console.log(msg); + _output.push(msg); + } + + function lpad(value, length) { + return __pad.substr(0,length) + value; + } + +}; + +// To be copied into e2e-tests experiencing sendKeys bug. +//// Hack - because of bug with send keys +//function sendKeys(element, str) { +// return str.split('').reduce(function (promise, char) { +// return promise.then(function () { +// return element.sendKeys(char); +// }); +// }, element.getAttribute('value')); +// // better to create a resolved promise here but ... don't know how with protractor; +//} \ No newline at end of file diff --git a/public/docs/_examples/quickstart/e2e-spec.js b/public/docs/_examples/quickstart/e2e-spec.js index c113291c91..52bb8ec7af 100644 --- a/public/docs/_examples/quickstart/e2e-spec.js +++ b/public/docs/_examples/quickstart/e2e-spec.js @@ -1,27 +1,15 @@ -/*global browser, element, by */ + describe('QuickStart E2E Tests', function () { - // #docregion shared var expectedMsg = 'My First Angular 2 App'; - // tests shared across languages - function sharedTests(basePath) { - beforeEach(function () { - browser.get(basePath + 'index.html'); - }); - it('should display: ' + expectedMsg, function () { - expect(element(by.css('h1')).getText()).toEqual(expectedMsg); - }); - } - // #enddocregion + beforeEach(function () { + browser.get(''); + }); - describe('QuickStart in JavaScript', function () { - sharedTests('quickstart/js/'); - }); - - describe('QuickStart in TypeScript', function () { - sharedTests('quickstart/ts/'); - }); + it('should display: ' + expectedMsg, function () { + expect(element(by.css('h1')).getText()).toEqual(expectedMsg); + }); }); diff --git a/public/docs/_examples/structural-directives/e2e-spec.js b/public/docs/_examples/structural-directives/e2e-spec.js new file mode 100644 index 0000000000..a2ed0455b9 --- /dev/null +++ b/public/docs/_examples/structural-directives/e2e-spec.js @@ -0,0 +1,64 @@ +describe('Structural Directives', function () { + + // tests interact - so we need beforeEach instead of beforeAll + beforeEach(function () { + browser.get(''); + }); + + it('should be able to use ngFor, ngIf and ngWhen together', function () { + var allDivEles = element.all(by.css('structural-directives > div')); + expect(allDivEles.get(0).getText()).toEqual('Mr. Nice'); + expect(allDivEles.get(1).getText()).toEqual('Mr. Nice'); + expect(allDivEles.get(4).getText()).toEqual('Ready'); + }); + + it('should be able to toggle ngIf with a button', function () { + var setConditionButtonEle = element.all(by.css('button')).get(0); + var conditionTrueEles = element.all(by.cssContainingText('p', 'condition is true')); + var conditionFalseEles = element.all(by.cssContainingText('p', 'condition is false')); + expect(conditionTrueEles.count()).toBe(2, 'should be two condition true elements'); + expect(conditionFalseEles.count()).toBe(0, 'should be no condition false elements'); + setConditionButtonEle.click().then(function() { + expect(conditionTrueEles.count()).toBe(0, 'should be no condition true elements'); + expect(conditionFalseEles.count()).toBe(2, 'should be two condition false elements'); + }); + }); + + it('should be able to compare use of ngIf with changing css visibility', function () { + var setConditionButtonEle = element.all(by.css('button')).get(0); + var ngIfButtonEle = element(by.cssContainingText('button', 'if | !if')); + var ngIfParentEle = ngIfButtonEle.element(by.xpath('..')); + var ngIfSiblingEle = ngIfParentEle.element(by.css('heavy-loader')); + var cssButtonEle = element(by.cssContainingText('button', 'show | hide')); + var cssSiblingEle = cssButtonEle.element(by.xpath('..')).element(by.css('heavy-loader')); + var setConditionText; + setConditionButtonEle.getText().then(function(text) { + setConditionText = text; + expect(ngIfButtonEle.isPresent()).toBe(true, 'should be able to find ngIfButton'); + expect(cssButtonEle.isPresent()).toBe(true, 'should be able to find cssButton'); + expect(ngIfParentEle.isPresent()).toBe(true, 'should be able to find ngIfButton parent'); + expect(ngIfSiblingEle.isPresent()).toBe(true, 'should be able to find ngIfButton sibling'); + expect(cssSiblingEle.isPresent()).toBe(true, 'should be able to find cssButton sibling'); + return ngIfButtonEle.click(); + }).then(function() { + expect(ngIfSiblingEle.isPresent()).toBe(false, 'now should NOT be able to find ngIfButton sibling'); + expect(setConditionButtonEle.getText()).not.toEqual(setConditionText); + return cssButtonEle.click(); + }).then(function() { + expect(cssSiblingEle.isPresent()).toBe(true, 'now should still be able to find cssButton sibling'); + expect(cssSiblingEle.isDisplayed()).toBe(false, 'now cssButton sibling should NOT be visible'); + return ngIfButtonEle.click(); + }).then(function() { + expect(setConditionButtonEle.getText()).toEqual(setConditionText); + }); + }); + + it('should be able to use *ngIf ', function () { + var setConditionButtonEle = element.all(by.css('button')).get(0); + var displayEles = element.all(by.cssContainingText('p', 'Our heroes are true!')); + expect(displayEles.count()).toBe(2, "should be displaying two ngIf elements"); + setConditionButtonEle.click().then(function() { + expect(displayEles.count()).toBe(0, "should nog longer be displaying ngIf elements"); + }); + }); +}); diff --git a/public/docs/_examples/styleguide/example-config.json b/public/docs/_examples/styleguide/example-config.json deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/public/docs/_examples/template-syntax/e2e-spec.js b/public/docs/_examples/template-syntax/e2e-spec.js new file mode 100644 index 0000000000..8aa140eb5c --- /dev/null +++ b/public/docs/_examples/template-syntax/e2e-spec.js @@ -0,0 +1,29 @@ +// Not yet complete +describe('Template Syntax', function () { + + beforeAll(function () { + browser.get(''); + }); + + it('should be able to use interpolation with a hero', function () { + var heroInterEle = element.all(by.css('h2+p')).get(0); + expect(heroInterEle.getText()).toEqual('My current hero is Hercules'); + }); + + it('should be able to use interpolation with a calculation', function () { + var theSumEles = element.all(by.cssContainingText('h3~p','The sum of')); + expect(theSumEles.count()).toBe(2); + expect(theSumEles.get(0).getText()).toEqual('The sum of 1 + 1 is 2'); + expect(theSumEles.get(1).getText()).toEqual('The sum of 1 + 1 is not 4'); + }); + + it('should be able to use class binding syntax', function () { + var specialEle = element(by.cssContainingText('div','Special')); + expect(specialEle.getAttribute('class')).toMatch('special'); + }); + + it('should be able to use style binding syntax', function () { + var specialButtonEle = element(by.cssContainingText('div.special~button', 'button')); + expect(specialButtonEle.getAttribute('style')).toMatch('color: red'); + }); +}); diff --git a/public/docs/_examples/tutorial/e2e-spec.js b/public/docs/_examples/tutorial/e2e-spec.js new file mode 100644 index 0000000000..eec022ed23 --- /dev/null +++ b/public/docs/_examples/tutorial/e2e-spec.js @@ -0,0 +1,46 @@ +// Not yet complete +describe('Tutorial', function () { + + beforeAll(function () { + browser.get(''); + init(); + }); + + + var _hrefEles, _tohDashboardHrefEle, _tohHeroesHrefEle; + + function init() { + _hrefEles = element.all(by.css('my-app a')); + + _tohDashboardHrefEle = _hrefEles.get(0); + _tohHeroesHrefEle = _hrefEles.get(1); + + } + + it('should be able to see the start screen', function () { + expect(_hrefEles.count()).toEqual(2, 'should be two dashboard choices'); + expect(_tohDashboardHrefEle.getText()).toEqual("Dashboard"); + expect(_tohHeroesHrefEle.getText()).toEqual("Heroes"); + }); + + it('should be able to see dashboard choices', function () { + var dashboardHeroEles = element.all(by.css('my-app my-dashboard .module.hero')); + expect(dashboardHeroEles.count()).toBe(4, "should be 4 dashboard hero choices"); + }); + + it('should be able to toggle the views', function () { + var dashboardEle = element(by.css('my-app my-dashboard')); + expect(dashboardEle.element(by.css('h3')).getText()).toEqual('Top Heroes'); + _tohHeroesHrefEle.click().then(function() { + expect(dashboardEle.isPresent()).toBe(false, 'should no longer see dashboard element'); + var heroEles = element.all(by.css('my-app my-heroes li')); + expect(heroEles.count()).toBeGreaterThan(4, "should be more than 4 heroes shown"); + return _tohDashboardHrefEle.click(); + }).then(function() { + expect(dashboardEle.isPresent()).toBe(true, 'should once again see the dashboard element'); + }); + + }); + + +});