├── .clang-format ├── .gitignore ├── .npmignore ├── .nvmrc ├── LICENSE ├── README.md ├── circle.yml ├── examples ├── README.md ├── e2e_test.py └── run_example.sh ├── gulpfile.js ├── lib ├── angular_wait_barrier.ts ├── bin.ts ├── blockingproxy.ts ├── client.ts ├── client_scripts │ ├── highlight.js │ └── wait.js ├── config.ts ├── highlight_delay_barrier.ts ├── index.ts ├── simple_webdriver_client.ts ├── webdriver_commands.ts ├── webdriver_logger.ts └── webdriver_proxy.ts ├── package.json ├── scripts ├── Dockerfile.sauceconnect └── set_up_selenium_hub.sh ├── spec ├── e2e │ ├── e2e_spec.ts │ ├── environment.ts │ ├── logging_spec.ts │ ├── ng1_async_spec.ts │ └── ng1_polling_spec.ts ├── helpers │ ├── jasmine-co.helper.js │ └── mock_selenium.ts ├── jasmine_e2e.json ├── jasmine_unit.json └── unit │ ├── client_spec.ts │ ├── config_spec.ts │ ├── highlight_delay_barrier_spec.ts │ ├── proxy_spec.ts │ ├── simple_webdriver_client_spec.ts │ ├── util.ts │ ├── webdriver_commands_spec.ts │ ├── webdriver_logger_spec.ts │ └── webdriver_proxy_spec.ts ├── testapp ├── app.css ├── hybrid │ ├── app │ │ ├── main.js │ │ ├── main.js.map │ │ ├── main.ts │ │ ├── myApp.js │ │ ├── myApp.js.map │ │ ├── myApp.ts │ │ ├── ng1.js │ │ ├── ng1.js.map │ │ ├── ng1.ts │ │ ├── ng2.js │ │ ├── ng2.js.map │ │ ├── ng2.ts │ │ ├── upgrader.js │ │ ├── upgrader.js.map │ │ └── upgrader.ts │ ├── html │ │ ├── myApp.html │ │ ├── ng1.html │ │ └── ng2.html │ ├── index.html │ ├── styles.css │ └── systemjs.config.js ├── index.html ├── ng1 │ ├── app.css │ ├── app.js │ ├── async │ │ ├── async.html │ │ └── async.js │ ├── components │ │ └── app-version.js │ ├── favicon.ico │ ├── index.html │ ├── interaction │ │ ├── interaction.html │ │ └── interaction.js │ ├── lib │ │ ├── angular_v1.5.0 │ │ │ ├── angular-animate.min.js │ │ │ ├── angular-animate.min.js.map │ │ │ ├── angular-aria.min.js │ │ │ ├── angular-aria.min.js.map │ │ │ ├── angular-route.min.js │ │ │ ├── angular-route.min.js.map │ │ │ ├── angular.min.js │ │ │ └── angular.min.js.map │ │ └── angular_version.js │ ├── login.html │ ├── polling │ │ ├── polling.html │ │ └── polling.js │ └── unstable │ │ ├── unstable.html │ │ └── unstable.js ├── ng2 │ ├── app │ │ ├── app-router.html │ │ ├── app.component.html │ │ ├── app.component.js │ │ ├── app.component.js.map │ │ ├── app.component.ts │ │ ├── app.module.js │ │ ├── app.module.js.map │ │ ├── app.module.ts │ │ ├── app.routes.js │ │ ├── app.routes.js.map │ │ ├── app.routes.ts │ │ ├── async │ │ │ ├── async-component.html │ │ │ ├── async.component.html │ │ │ ├── async.component.js │ │ │ ├── async.component.js.map │ │ │ └── async.component.ts │ │ ├── home │ │ │ ├── home.component.html │ │ │ ├── home.component.js │ │ │ ├── home.component.js.map │ │ │ └── home.component.ts │ │ ├── main.js │ │ ├── main.js.map │ │ └── main.ts │ ├── index.html │ ├── styles.css │ └── system-config.js ├── package.json ├── scripts │ └── web-server.js ├── tsconfig.json └── typings.json ├── tsconfig.json └── tslint.json /.clang-format: -------------------------------------------------------------------------------- 1 | Language: JavaScript 2 | BasedOnStyle: Google 3 | ColumnLimit: 100 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Idea configs 2 | .idea 3 | 4 | # TypeScript output 5 | built/ 6 | testapp/typings/ 7 | 8 | # Logs 9 | logs 10 | *.log 11 | npm-debug.log* 12 | chromedriver.log 13 | 14 | # Runtime data 15 | pids 16 | *.pid 17 | *.seed 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # node-waf configuration 32 | .lock-wscript 33 | 34 | # Compiled binary addons (http://nodejs.org/api/addons.html) 35 | build/Release 36 | 37 | # Dependency directories 38 | node_modules 39 | jspm_packages 40 | 41 | # Optional npm cache directory 42 | .npm 43 | 44 | # Optional REPL history 45 | .node_repl_history 46 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | docs/ 2 | lib/ 3 | scripts/ 4 | spec/ 5 | testapp/ 6 | built/spec/ 7 | 8 | .gitattributes 9 | .github/ 10 | .gitignore 11 | .jshintrc 12 | .npmignore 13 | .travis.yml 14 | chromedriver.log 15 | libpeerconnection.log 16 | npm-debug.log 17 | xmloutput* 18 | release.md 19 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 4.6 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Angular 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Blocking Proxy [![Build Status](https://circleci.com/gh/angular/blocking-proxy.svg?style=shield)](https://circleci.com/gh/angular/blocking-proxy) 2 | 3 | Blocking Proxy is a tool for adding functionality to WebDriver-based 4 | tests. It sits between your tests and the Selenium Server. For each 5 | WebDriver command, it runs a set of 'barriers' that will block 6 | forwarding the command to the Selenium server until some condition 7 | is met. 8 | 9 | Because it interacts with WebDriver at the network level, Blocking Proxy can be 10 | used regardless of which language your tests are written in. See [the example](https://github.com/angular/blocking-proxy/blob/master/examples/README.md) 11 | for a demonstration of using Blocking Proxy with WebDriver tests written in Python. 12 | 13 | Although Blocking Proxy can handle multiple WebDriver sessions, it can not yet handle 14 | multiple concurrent clients. Thus, it's recommended to start a separate instance 15 | for each test process. 16 | 17 | # Usage 18 | 19 | Blocking Proxy can be installed globally with `npm install -g blocking-proxy`. 20 | You can also use it by cloning this repo and running these commands: 21 | 22 | ``` 23 | npm install 24 | webdriver-manager update && webdriver-manager start (in another terminal) 25 | node ./built/lib/bin.js --seleniumAddress http://localhost:4444/wd/hub 26 | ``` 27 | 28 | # Features 29 | 30 | ## Wait for Angular 31 | 32 | When testing an Angular application, Blocking Proxy can block webdriver commands 33 | until Angular's change detection is finished, and thus make your tests less flaky. 34 | 35 | ## Highlight Delay 36 | 37 | If `--highlightDelay ` is specified, Blocking Proxy will wait for 38 | the specified delay (in milliseconds) before executing click commands or sending 39 | keys. It will also highlight the element that is the target of the command. 40 | 41 | Here's an example of highlight delay in action: 42 | 43 | ![Highlight Delay](http://i.giphy.com/jg7B2HHPIkwak.gif) 44 | 45 | ## WebDriver logging 46 | 47 | When `--logDir ` is set, Blocking Proxy will create a readable log of 48 | WebDriver commands at the specified path. The log will look something like this: 49 | 50 | ``` 51 | 20:08:14.830 | 834ms | 37f13c | NewSession 52 | {"browserName":"chrome"} 53 | 20:08:15.674 | 4ms | 37f13c | SetTimeouts 54 | 20:08:15.681 | 578ms | 37f13c | Go http://localhost:8081/ng1/#/interaction 55 | 20:08:16.300 | 438ms | 37f13c | FindElement 56 | Using css selector \'.invalid\' 57 | ERROR: no such element 58 | ``` 59 | Each line shows the command that was executed and how long it took. For some 60 | commands, extra data or the response from WebDriver will be shown on following 61 | lines. 62 | 63 | # Development 64 | 65 | ## Formatting and lint 66 | 67 | `gulp format` runs clang-format. `gulp lint` validates format and runs tslint. 68 | 69 | ## Running tests 70 | 71 | Unit tests live in `spec/unit` and can be run with `npm test`. Run `npm run test:auto` to automatically watch for changes and run unit tests. 72 | 73 | ## Running e2e tests 74 | 75 | Start webdriver 76 | 77 | webdriver-manager update 78 | webdriver-manager start 79 | 80 | in another terminal, start the testapp 81 | 82 | npm run testapp 83 | 84 | Start the proxy with 85 | 86 | npm start 87 | 88 | in yet another terminal, run the tests 89 | 90 | npm run test:e2e 91 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | machine: 2 | node: 3 | version: 6.9.1 4 | environment: 5 | # Fix issue with selenium-server in containers. 6 | # See http://github.com/SeleniumHQ/docker-selenium/issues/87 7 | DBUS_SESSION_BUS_ADDRESS: /dev/null 8 | 9 | dependencies: 10 | override: 11 | - npm update 12 | cache_directories: 13 | - testapp/node_modules 14 | post: 15 | # Install the latest Chrome 16 | - curl -L -o google-chrome.deb https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb 17 | - sudo dpkg -i google-chrome.deb 18 | - sudo sed -i 's|HERE/chrome\"|HERE/chrome\" --disable-setuid-sandbox|g' /opt/google/chrome/google-chrome 19 | - rm google-chrome.deb 20 | - npm run webdriver: 21 | background: true 22 | - cd testapp && npm update 23 | - npm run testapp: 24 | background: true 25 | test: 26 | override: 27 | - ./node_modules/.bin/gulp lint 28 | - npm test 29 | - npm run test:e2e 30 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | This is an example test that makes use of Blocking Proxy. 2 | 3 | ## Running the example 4 | 5 | The example requires python 2.7 and the python selenium module. Assuming you 6 | have pip and python, you can run the example like so: 7 | 8 | ``` 9 | pip install selenium 10 | ./run_example.sh 11 | ``` 12 | 13 | ## What it does 14 | 15 | The example test is a simple WebDriver test of angularjs.io. It starts a 16 | selenium server (using [WebDriver Manager](https://github.com/angular/webdriver-manager), 17 | starts a Blocking Proxy instance, then runs the test. 18 | 19 | Blocking Proxy is set to use a 3 second highlight delay, so you'll see elements 20 | highlighted before they're interacted with. It will also generate a log of WebDriver 21 | commands in the current directory, with the file like `webdriver_log_xxxxxxxx.txt`. 22 | 23 | -------------------------------------------------------------------------------- /examples/e2e_test.py: -------------------------------------------------------------------------------- 1 | from selenium import webdriver 2 | 3 | capabilities = webdriver.DesiredCapabilities.CHROME.copy() 4 | WD_URL = 'http://localhost:8001' 5 | 6 | driver = webdriver.Remote(desired_capabilities=capabilities, command_executor=WD_URL) 7 | 8 | print "Loading angularjs.org" 9 | driver.get('https://angularjs.org/') 10 | 11 | print "Testing hello app" 12 | sample_app = driver.find_element_by_css_selector("[app-run='hello.html']") 13 | sample_app.location_once_scrolled_into_view 14 | name_box = sample_app.find_element_by_css_selector('[ng-model="yourName"]') 15 | hello_box = sample_app.find_element_by_css_selector('h1') 16 | 17 | name_box.send_keys('Bob') 18 | 19 | assert "Hello Bob!" in hello_box.text 20 | 21 | print "Testing todo app" 22 | todo_app = driver.find_element_by_css_selector("[app-run='todo.html']") 23 | todo_app.location_once_scrolled_into_view 24 | todo_input = todo_app.find_element_by_css_selector('[ng-model="todoList.todoText"]') 25 | todo_list = todo_app.find_element_by_css_selector('ul') 26 | 27 | todo_input.send_keys('write some tests'); 28 | add_button = todo_app.find_element_by_css_selector('[value="add"]') 29 | add_button.click() 30 | 31 | assert 'write some tests' in todo_list.text 32 | -------------------------------------------------------------------------------- /examples/run_example.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | trap "kill -- -$$" EXIT 3 | 4 | cd .. 5 | npm install 6 | 7 | # Start Selenium Server 8 | ./node_modules/.bin/webdriver-manager update 9 | ./node_modules/.bin/webdriver-manager start &> /dev/null & 10 | 11 | # Start Blocking Proxy 12 | node ./built/lib/bin.js \ 13 | --seleniumAddress http://localhost:4444/wd/hub \ 14 | --port 8001 \ 15 | --highlightDelay 3000 \ 16 | --logDir examples/ &> /dev/null & 17 | 18 | # Wait a bit for things to come up 19 | sleep 2 20 | 21 | # Run the test 22 | python examples/e2e_test.py 23 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var gulp = require('gulp'); 4 | var runSequence = require('run-sequence'); 5 | var spawn = require('child_process').spawn; 6 | var tslint = require('gulp-tslint'); 7 | 8 | var runSpawn = function(done, task, opt_arg) { 9 | var child = spawn(task, opt_arg, {stdio: 'inherit'}); 10 | child.on('close', function() { 11 | done(); 12 | }); 13 | }; 14 | 15 | gulp.task('built:copy', function() { 16 | return gulp.src(['lib/**/*','!lib/**/*.ts']) 17 | .pipe(gulp.dest('built/lib/')); 18 | }); 19 | 20 | gulp.task('webdriver:update', function(done) { 21 | runSpawn(done, 'webdriver-manager', ['update']); 22 | }); 23 | 24 | gulp.task('tslint', function() { 25 | return gulp.src(['lib/**/*.ts', 'spec/**/*.ts']).pipe(tslint()).pipe(tslint.report()); 26 | }); 27 | 28 | gulp.task('format:enforce', () => { 29 | const format = require('gulp-clang-format'); 30 | const clangFormat = require('clang-format'); 31 | return gulp.src(['lib/**/*.ts', 'spec/**/*.ts']).pipe( 32 | format.checkFormat('file', clangFormat, {verbose: true, fail: true})); 33 | }); 34 | 35 | gulp.task('format', () => { 36 | const format = require('gulp-clang-format'); 37 | const clangFormat = require('clang-format'); 38 | return gulp.src(['lib/**/*.ts', 'spec/**/*.ts'], { base: '.' }).pipe( 39 | format.format('file', clangFormat)).pipe(gulp.dest('.')); 40 | }); 41 | 42 | gulp.task('tsc', function(done) { 43 | runSpawn(done, 'node', ['node_modules/typescript/bin/tsc']); 44 | }); 45 | 46 | gulp.task('lint', function(done) { 47 | runSequence('tslint', 'format:enforce', done); 48 | }); 49 | 50 | gulp.task('prepublish', function(done) { 51 | runSequence('lint' ,'tsc', 'built:copy', done); 52 | }); 53 | 54 | gulp.task('pretest', function(done) { 55 | runSequence( 56 | ['webdriver:update', 'tslint', 'clang'], 'tsc', 'built:copy', done); 57 | }); 58 | 59 | gulp.task('default', ['prepublish']); 60 | gulp.task('build', ['prepublish']); 61 | 62 | gulp.task('test:copy', function(done) { 63 | return gulp.src(['spec/**/*','!spec/**/*.ts']) 64 | .pipe(gulp.dest('built/spec/')); 65 | }); 66 | -------------------------------------------------------------------------------- /lib/angular_wait_barrier.ts: -------------------------------------------------------------------------------- 1 | import {SimpleWebDriverClient} from './simple_webdriver_client'; 2 | import {WebDriverCommand} from './webdriver_commands'; 3 | import {WebDriverLogger} from './webdriver_logger'; 4 | import {WebDriverBarrier} from './webdriver_proxy'; 5 | 6 | const angularWaits = require('./client_scripts/wait.js'); 7 | 8 | /** 9 | * A barrier that uses Angular's Testability API to block commands until the application is stable. 10 | */ 11 | export class AngularWaitBarrier implements WebDriverBarrier { 12 | // The ng-app root to use when waiting on the client. 13 | rootSelector: string; 14 | enabled: boolean; 15 | logger: WebDriverLogger; 16 | 17 | constructor(private client: SimpleWebDriverClient) { 18 | this.enabled = true; 19 | this.rootSelector = ''; 20 | } 21 | 22 | /** 23 | * A CSS Selector for a DOM element within your Angular application. 24 | * BlockingProxy will attempt to automatically find your application, but it is 25 | * necessary to set rootElement in certain cases. 26 | * 27 | * In Angular 1, BlockingProxy will use the element your app bootstrapped to by 28 | * default. If that doesn't work, it will then search for hooks in `body` or 29 | * `ng-app` elements (details here: https://git.io/v1b2r). 30 | * 31 | * In later versions of Angular, BlockingProxy will try to hook into all angular 32 | * apps on the page. Use rootElement to limit the scope of which apps 33 | * BlockingProxy waits for and searches within. 34 | * 35 | * @param rootSelector A selector for the root element of the Angular app. 36 | */ 37 | setRootSelector(selector: string) { 38 | this.rootSelector = selector; 39 | } 40 | 41 | private waitForAngularData() { 42 | return JSON.stringify({ 43 | script: 'return (' + angularWaits.NG_WAIT_FN + ').apply(null, arguments);', 44 | args: [this.rootSelector] 45 | }); 46 | } 47 | 48 | /** 49 | * Turn on WebDriver logging. 50 | * 51 | * @param logDir The directory to create logs in. 52 | */ 53 | enableLogging(logDir: string) { 54 | if (!this.logger) { 55 | this.logger = new WebDriverLogger(); 56 | } 57 | this.logger.setLogDir(logDir); 58 | } 59 | 60 | /** 61 | * Override the logger instance. Only used for testing. 62 | */ 63 | setLogger(logger: WebDriverLogger) { 64 | this.logger = logger; 65 | } 66 | 67 | private sendRequestToStabilize(command: WebDriverCommand): Promise { 68 | return this.client.executeAsync(command.sessionId, this.waitForAngularData()).then((value) => { 69 | // waitForAngular only returns a value if there was an error 70 | // in the browser. 71 | if (value) { 72 | throw new Error('Error from waitForAngular: ' + value); 73 | } 74 | }); 75 | } 76 | 77 | private shouldStabilize(command: WebDriverCommand) { 78 | const url = command.url; 79 | if (!this.enabled) { 80 | return false; 81 | } 82 | 83 | // TODO - should this implement some state, and be smart about whether 84 | // stabilization is necessary or not? Would that be as simple as GET/POST? 85 | // e.g. two gets in a row don't require a wait btwn. 86 | // 87 | // See https://code.google.com/p/selenium/wiki/JsonWireProtocol for 88 | // descriptions of the paths. 89 | // We shouldn't stabilize if we haven't loaded the page yet. 90 | const parts = url.split('/'); 91 | if (parts.length < 4) { 92 | return false; 93 | } 94 | 95 | const commandsToWaitFor = [ 96 | 'executeScript', 'screenshot', 'source', 'title', 'element', 'elements', 'execute', 'keys', 97 | 'moveto', 'click', 'buttondown', 'buttonup', 'doubleclick', 'touch', 'get' 98 | ]; 99 | 100 | if (commandsToWaitFor.indexOf(parts[3]) != -1) { 101 | return true; 102 | } 103 | return false; 104 | } 105 | 106 | onCommand(command: WebDriverCommand): Promise { 107 | if (this.logger) { 108 | command.on('data', () => { 109 | this.logger.logWebDriverCommand(command); 110 | }); 111 | } 112 | 113 | if (this.shouldStabilize(command)) { 114 | const started = Date.now(); 115 | return this.sendRequestToStabilize(command).then(() => { 116 | const ended = Date.now(); 117 | if (this.logger) { 118 | this.logger.logEvent('Waiting for Angular', command.sessionId, (ended - started)); 119 | } 120 | }); 121 | } 122 | return Promise.resolve(null); 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /lib/bin.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import {BlockingProxy} from './blockingproxy'; 4 | import {processArgs, printHelp} from './config'; 5 | 6 | /** 7 | * Starts up a proxy server which modifies calls between the test process 8 | * and the selenium server. 9 | */ 10 | 11 | const argv = processArgs(process.argv.slice(2)); 12 | 13 | if (argv.help) { 14 | printHelp(); 15 | process.exit(0); 16 | } 17 | 18 | const proxy = new BlockingProxy(argv.seleniumAddress, parseInt(argv.highlightDelay)); 19 | if (argv.logDir) { 20 | proxy.enableLogging(argv.logDir); 21 | } 22 | let port = proxy.listen(argv.port); 23 | console.log(`Listening on :${port}`); 24 | if (argv.fork) { 25 | process.send({ready: true, port: port}); 26 | process.on('disconnect', function() { 27 | console.log('parent exited, quitting'); 28 | process.exit(); 29 | }); 30 | } 31 | -------------------------------------------------------------------------------- /lib/blockingproxy.ts: -------------------------------------------------------------------------------- 1 | import * as http from 'http'; 2 | 3 | import {AngularWaitBarrier} from './angular_wait_barrier'; 4 | import {HighlightDelayBarrier} from './highlight_delay_barrier'; 5 | import {SimpleWebDriverClient} from './simple_webdriver_client'; 6 | import {WebDriverLogger} from './webdriver_logger'; 7 | import {WebDriverProxy} from './webdriver_proxy'; 8 | 9 | export const BP_PREFIX = 'bpproxy'; 10 | 11 | /** 12 | * The stability proxy is an http server responsible for intercepting 13 | * JSON webdriver commands. It keeps track of whether the page under test 14 | * needs to wait for page stability, and initiates a wait if so. 15 | */ 16 | export class BlockingProxy { 17 | server: http.Server; 18 | logger: WebDriverLogger; 19 | waitBarrier: AngularWaitBarrier; 20 | highlightBarrier: HighlightDelayBarrier; 21 | private proxy: WebDriverProxy; 22 | 23 | constructor(seleniumAddress: string, highlightDelay: number = null) { 24 | this.server = http.createServer(this.requestListener.bind(this)); 25 | this.proxy = new WebDriverProxy(seleniumAddress); 26 | 27 | let client = new SimpleWebDriverClient(seleniumAddress); 28 | this.waitBarrier = new AngularWaitBarrier(client); 29 | this.highlightBarrier = new HighlightDelayBarrier(client, highlightDelay); 30 | this.proxy.addBarrier(this.waitBarrier); 31 | this.proxy.addBarrier(this.highlightBarrier); 32 | } 33 | 34 | /** 35 | * This command is for the proxy server, not to be forwarded to Selenium. 36 | */ 37 | static isProxyCommand(commandPath: string) { 38 | return (commandPath.split('/')[1] === BP_PREFIX); 39 | } 40 | 41 | /** 42 | * Turn on WebDriver logging. 43 | * 44 | * @param logDir The directory to create logs in. 45 | */ 46 | enableLogging(logDir: string) { 47 | this.waitBarrier.enableLogging(logDir); 48 | } 49 | 50 | /** 51 | * Override the logger instance. Only used for testing. 52 | */ 53 | setLogger(logger: WebDriverLogger) { 54 | this.waitBarrier.setLogger(logger); 55 | } 56 | 57 | /** 58 | * Change the parameters used by the wait function. 59 | */ 60 | setWaitParams(rootEl) { 61 | this.waitBarrier.setRootSelector(rootEl); 62 | } 63 | 64 | handleProxyCommand(message, data, response) { 65 | let command = message.url.split('/')[2]; 66 | switch (command) { 67 | case 'waitEnabled': 68 | if (message.method === 'GET') { 69 | response.writeHead(200); 70 | response.write(JSON.stringify({value: this.waitBarrier.enabled})); 71 | response.end(); 72 | } else if (message.method === 'POST') { 73 | response.writeHead(200); 74 | this.waitBarrier.enabled = JSON.parse(data).value; 75 | response.end(); 76 | } else { 77 | response.writeHead(405); 78 | response.write('Invalid method'); 79 | response.end(); 80 | } 81 | break; 82 | case 'waitParams': 83 | if (message.method === 'GET') { 84 | response.writeHead(200); 85 | response.write(JSON.stringify({rootSelector: this.waitBarrier.rootSelector})); 86 | response.end(); 87 | } else if (message.method === 'POST') { 88 | response.writeHead(200); 89 | this.waitBarrier.rootSelector = JSON.parse(data).rootSelector; 90 | response.end(); 91 | } else { 92 | response.writeHead(405); 93 | response.write('Invalid method'); 94 | response.end(); 95 | } 96 | break; 97 | default: 98 | response.writeHead(404); 99 | response.write('Unknown stabilizer proxy command'); 100 | response.end(); 101 | } 102 | } 103 | 104 | requestListener(originalRequest: http.IncomingMessage, response: http.ServerResponse) { 105 | if (BlockingProxy.isProxyCommand(originalRequest.url)) { 106 | let commandData = ''; 107 | originalRequest.on('data', (d) => { 108 | commandData += d; 109 | }); 110 | originalRequest.on('end', () => { 111 | this.handleProxyCommand(originalRequest, commandData, response); 112 | }); 113 | return; 114 | } 115 | 116 | // OK to ignore the promise returned by this. 117 | this.proxy.handleRequest(originalRequest, response); 118 | } 119 | 120 | listen(port: number) { 121 | this.server.listen(port); 122 | let actualPort = this.server.address().port; 123 | return actualPort; 124 | } 125 | 126 | quit() { 127 | return new Promise((resolve) => { 128 | this.server.close(resolve); 129 | }); 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /lib/client.ts: -------------------------------------------------------------------------------- 1 | import * as http from 'http'; 2 | import * as url from 'url'; 3 | import {BP_PREFIX} from './blockingproxy'; 4 | 5 | export class BPClient { 6 | hostname: string; 7 | port: number; 8 | 9 | constructor(bpUrlValue: string) { 10 | let bpUrl = url.parse(bpUrlValue); 11 | this.hostname = bpUrl.hostname; 12 | this.port = parseInt(bpUrl.port); 13 | } 14 | 15 | /** 16 | * Toggle whether waiting for Angular is enabled. 17 | * 18 | * @param enabled Whether or not to enable waiting for angular. 19 | * @returns {Promise} 20 | */ 21 | setWaitEnabled(enabled: boolean): Promise { 22 | return new Promise((resolve, reject) => { 23 | let options = 24 | {host: this.hostname, port: this.port, method: 'POST', path: `/${BP_PREFIX}/waitEnabled`}; 25 | 26 | let request = http.request(options, (response) => { 27 | response.on('data', () => {}); 28 | response.on('error', (err) => reject(err)); 29 | response.on('end', () => { 30 | resolve(); 31 | }); 32 | }); 33 | request.write(JSON.stringify({value: enabled})); 34 | request.end(); 35 | }); 36 | } 37 | 38 | /** 39 | * Set the selector used to find the root element of the Angular application to wait for. See 40 | * AngularWaitBarrier for more details. 41 | * 42 | * @param selector A selector, or empty string to wait for all Angular apps. 43 | */ 44 | setWaitParams(rootSelector: string): Promise { 45 | return new Promise((resolve, reject) => { 46 | let options = 47 | {host: this.hostname, port: this.port, method: 'POST', path: `/${BP_PREFIX}/waitParams`}; 48 | 49 | let request = http.request(options, (response) => { 50 | response.on('data', () => {}); 51 | response.on('error', (err) => reject(err)); 52 | response.on('end', () => { 53 | resolve(); 54 | }); 55 | }); 56 | request.write(JSON.stringify({rootSelector: rootSelector})); 57 | request.end(); 58 | }); 59 | } 60 | 61 | isWaitEnabled() { 62 | return new Promise((res) => { 63 | let options = {host: this.hostname, port: this.port, path: `/${BP_PREFIX}/waitEnabled`}; 64 | 65 | http.get(options, (response) => { 66 | let body = ''; 67 | response.on('data', (data) => { 68 | body += data; 69 | }); 70 | response.on('end', () => { 71 | res(JSON.parse(body).value); 72 | }); 73 | }); 74 | }); 75 | } 76 | } -------------------------------------------------------------------------------- /lib/client_scripts/highlight.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Creates a floating translucent div at the specified location in order to highlight a particular 3 | * element. Note that these scripts are run directly in the browser under test, so they need to 4 | * be ES5 compatible and serializable. 5 | */ 6 | exports.HIGHLIGHT_FN = function(top, left, width, height) { 7 | console.log('Highlighting at ', top, left, width, height); 8 | var el = document.createElement('div'); 9 | el.id = 'BP_ELEMENT_HIGHLIGHT__'; 10 | document.body.appendChild(el); 11 | el.style['position'] = 'absolute'; 12 | el.style['background-color'] = 'lightblue'; 13 | el.style['opacity'] = '0.7'; 14 | el.style['top'] = top + 'px'; 15 | el.style['left'] = left + 'px'; 16 | el.style['width'] = width + 'px'; 17 | el.style['height'] = height + 'px'; 18 | }; 19 | 20 | /** 21 | * Removes the highlight 22 | */ 23 | exports.REMOVE_HIGHLIGHT_FN = function() { 24 | var el = document.getElementById('BP_ELEMENT_HIGHLIGHT__'); 25 | if (el) { 26 | el.parentElement.removeChild(el); 27 | } 28 | }; 29 | 30 | -------------------------------------------------------------------------------- /lib/client_scripts/wait.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copied from Protractor 5.2.0 3 | * 4 | * Wait until Angular has finished rendering and has 5 | * no outstanding $http calls before continuing. The specific Angular app 6 | * is determined by the rootSelector. 7 | * 8 | * Asynchronous. 9 | * 10 | * @param {string} rootSelector The selector housing an ng-app 11 | * @param {function(string)} callback callback. If a failure occurs, it will 12 | * be passed as a parameter. 13 | */ 14 | function waitForAngular(rootSelector, callback) { 15 | 16 | try { 17 | // Wait for both angular1 testability and angular2 testability. 18 | 19 | var testCallback = callback; 20 | 21 | // Wait for angular1 testability first and run waitForAngular2 as a callback 22 | var waitForAngular1 = function(callback) { 23 | 24 | if (window.angular) { 25 | var hooks = getNg1Hooks(rootSelector); 26 | if (!hooks){ 27 | callback(); // not an angular1 app 28 | } 29 | else{ 30 | if (hooks.$$testability) { 31 | hooks.$$testability.whenStable(callback); 32 | } else if (hooks.$injector) { 33 | hooks.$injector.get('$browser') 34 | .notifyWhenNoOutstandingRequests(callback); 35 | } else if (!!rootSelector) { 36 | throw new Error( 37 | 'Could not automatically find injector on page: "' + 38 | window.location.toString() + '". Consider using config.rootEl'); 39 | } else { 40 | throw new Error( 41 | 'root element (' + rootSelector + ') has no injector.' + 42 | ' this may mean it is not inside ng-app.'); 43 | } 44 | } 45 | } 46 | else {callback();} // not an angular1 app 47 | }; 48 | 49 | // Wait for Angular2 testability and then run test callback 50 | var waitForAngular2 = function() { 51 | if (window.getAngularTestability) { 52 | if (rootSelector) { 53 | var testability = null; 54 | var el = document.querySelector(rootSelector); 55 | try{ 56 | testability = window.getAngularTestability(el); 57 | } 58 | catch(e){} 59 | if (testability) { 60 | return testability.whenStable(testCallback); 61 | } 62 | } 63 | 64 | // Didn't specify root element or testability could not be found 65 | // by rootSelector. This may happen in a hybrid app, which could have 66 | // more than one root. 67 | var testabilities = window.getAllAngularTestabilities(); 68 | var count = testabilities.length; 69 | 70 | // No angular2 testability, this happens when 71 | // going to a hybrid page and going back to a pure angular1 page 72 | if (count === 0) { 73 | return testCallback(); 74 | } 75 | 76 | var decrement = function() { 77 | count--; 78 | if (count === 0) { 79 | testCallback(); 80 | } 81 | }; 82 | testabilities.forEach(function(testability) { 83 | testability.whenStable(decrement); 84 | }); 85 | 86 | } 87 | else {testCallback();} // not an angular2 app 88 | }; 89 | 90 | if (!(window.angular) && !(window.getAngularTestability)) { 91 | // no testability hook 92 | throw new Error( 93 | 'both angularJS testability and angular testability are undefined.' + 94 | ' This could be either ' + 95 | 'because this is a non-angular page or because your test involves ' + 96 | 'client-side navigation, which can interfere with Protractor\'s ' + 97 | 'bootstrapping. See http://git.io/v4gXM for details'); 98 | } else {waitForAngular1(waitForAngular2);} // Wait for angular1 and angular2 99 | // Testability hooks sequentially 100 | 101 | } catch (err) { 102 | callback(err.message); 103 | } 104 | 105 | }; 106 | 107 | /* Tries to find $$testability and possibly $injector for an ng1 app 108 | * 109 | * By default, doesn't care about $injector if it finds $$testability. However, 110 | * these priorities can be reversed. 111 | * 112 | * @param {string=} selector The selector for the element with the injector. If 113 | * falsy, tries a variety of methods to find an injector 114 | * @param {boolean=} injectorPlease Prioritize finding an injector 115 | * @return {$$testability?: Testability, $injector?: Injector} Returns whatever 116 | * ng1 app hooks it finds 117 | */ 118 | function getNg1Hooks(selector, injectorPlease) { 119 | function tryEl(el) { 120 | try { 121 | if (!injectorPlease && angular.getTestability) { 122 | var $$testability = angular.getTestability(el); 123 | if ($$testability) { 124 | return {$$testability: $$testability}; 125 | } 126 | } else { 127 | var $injector = angular.element(el).injector(); 128 | if ($injector) { 129 | return {$injector: $injector}; 130 | } 131 | } 132 | } catch(err) {} 133 | } 134 | function trySelector(selector) { 135 | var els = document.querySelectorAll(selector); 136 | for (var i = 0; i < els.length; i++) { 137 | var elHooks = tryEl(els[i]); 138 | if (elHooks) { 139 | return elHooks; 140 | } 141 | } 142 | } 143 | 144 | if (selector) { 145 | return trySelector(selector); 146 | } else if (window.__TESTABILITY__NG1_APP_ROOT_INJECTOR__) { 147 | var $injector = window.__TESTABILITY__NG1_APP_ROOT_INJECTOR__; 148 | var $$testability = null; 149 | try { 150 | $$testability = $injector.get('$$testability'); 151 | } catch (e) {} 152 | return {$injector: $injector, $$testability: $$testability}; 153 | } else { 154 | return tryEl(document.body) || 155 | trySelector('[ng-app]') || trySelector('[ng\\:app]') || 156 | trySelector('[ng-controller]') || trySelector('[ng\\:controller]'); 157 | } 158 | } 159 | 160 | /* Wraps a function up into a string with its helper functions so that it can 161 | * call those helper functions client side 162 | * 163 | * @param {function} fun The function to wrap up with its helpers 164 | * @param {...function} The helper functions. Each function must be named 165 | * 166 | * @return {string} The string which, when executed, will invoke fun in such a 167 | * way that it has access to its helper functions 168 | */ 169 | function wrapWithHelpers(fun) { 170 | var helpers = Array.prototype.slice.call(arguments, 1); 171 | if (!helpers.length) { 172 | return fun; 173 | } 174 | var FunClass = Function; // Get the linter to allow this eval 175 | return new FunClass( 176 | helpers.join(';') + String.fromCharCode(59) + 177 | ' return (' + fun.toString() + ').apply(this, arguments);'); 178 | } 179 | 180 | exports.NG_WAIT_FN = wrapWithHelpers(waitForAngular, getNg1Hooks); 181 | -------------------------------------------------------------------------------- /lib/config.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import * as minimist from 'minimist'; 4 | 5 | export interface Config { 6 | help?: boolean; 7 | fork?: boolean; 8 | highlightDelay?: string; 9 | seleniumAddress?: string; 10 | logDir?: string; 11 | port?: number; 12 | } 13 | 14 | const opts: minimist.Opts = { 15 | boolean: ['help', 'fork'], 16 | string: ['port', 'seleniumAddress', 'highlightDelay', 'logDir'], 17 | alias: { 18 | help: ['h'], 19 | port: ['p'], 20 | seleniumAddress: ['s'], 21 | }, 22 | default: { 23 | port: process.env.BP_PORT || 0, 24 | seleniumAddress: process.env.BP_SELENIUM_ADDRESS || 'http://localhost:4444/wd/hub', 25 | } 26 | }; 27 | 28 | export function processArgs(argv: string[]) { 29 | return minimist(argv, opts) as Config; 30 | } 31 | 32 | export function printHelp() { 33 | console.log(` 34 | Usage: blocking-proxy 35 | 36 | Options: 37 | --help, -h Show help. 38 | --port, -p The port to listen on. If unset, will choose a random free port. 39 | --fork Start in fork mode. BlockingProxy will use process.send() to communicate 40 | with the parent process. 41 | --selenumAddress, -s The address of the selenium remote server to proxy. 42 | --highlightDelay If specified, will highlight elements before interacting with them and 43 | wait the specified amount of time (in ms) before allowing WebDriver 44 | to continue. 45 | --logDir If specified, will create a log of WebDriver commands in this directory. 46 | --rootElement Element housing ng-app, if not html or body. 47 | `); 48 | } -------------------------------------------------------------------------------- /lib/highlight_delay_barrier.ts: -------------------------------------------------------------------------------- 1 | import {SimpleWebDriverClient} from './simple_webdriver_client'; 2 | import {CommandName, WebDriverCommand} from './webdriver_commands'; 3 | import {WebDriverBarrier} from './webdriver_proxy'; 4 | 5 | const HIGHLIGHT_COMMAND = 6 | [CommandName.ElementClick, CommandName.ElementSendKeys, CommandName.ElementClear]; 7 | 8 | let clientScripts = require('./client_scripts/highlight.js'); 9 | 10 | 11 | /** 12 | * A barrier that delays forwarding WebDriver commands that can affect the app (ie, clicks or 13 | * sending text) for a fixed amount of time. During the delay, the element that's the target 14 | * of the command will be highlighted by drawing a transparent div on top of it. 15 | */ 16 | export class HighlightDelayBarrier implements WebDriverBarrier { 17 | constructor(private client: SimpleWebDriverClient, public delay: number) {} 18 | 19 | private isHighlightCommand(command: WebDriverCommand) { 20 | return HIGHLIGHT_COMMAND.indexOf(command.commandName) !== -1; 21 | } 22 | 23 | private highlightData(top, left, width, height) { 24 | return JSON.stringify({ 25 | script: 'return (' + clientScripts.HIGHLIGHT_FN + ').apply(null, arguments);', 26 | args: [top, left, width, height] 27 | }); 28 | } 29 | 30 | private removeHighlightData() { 31 | return JSON.stringify({ 32 | script: 'return (' + clientScripts.REMOVE_HIGHLIGHT_FN + ').apply(null, arguments);', 33 | args: [] 34 | }); 35 | } 36 | 37 | // Simple promise-based sleep so we can use async/await 38 | private sleep(delay: number) { 39 | return new Promise((resolve) => { 40 | setTimeout(() => { 41 | resolve(); 42 | }, delay); 43 | }); 44 | } 45 | 46 | async onCommand(command: WebDriverCommand) { 47 | if (!this.isHighlightCommand(command) || !this.delay) { 48 | return; 49 | } 50 | const sessId = command.sessionId; 51 | const el = command.getParam('elementId'); 52 | 53 | // The W3C spec does have a 'getRect', but the standalone server doesn't support it yet. 54 | const loc = await this.client.getLocation(sessId, el); 55 | const size = await this.client.getSize(sessId, el); 56 | 57 | // Set the highlight 58 | await this.client.execute( 59 | sessId, this.highlightData(loc['y'], loc['x'], size['width'], size['height'])); 60 | 61 | // Wait 62 | await this.sleep(this.delay); 63 | 64 | // Clear the highlight 65 | await this.client.execute(sessId, this.removeHighlightData()); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /lib/index.ts: -------------------------------------------------------------------------------- 1 | export {BlockingProxy} from './blockingproxy'; 2 | export {BPClient} from './client'; 3 | -------------------------------------------------------------------------------- /lib/simple_webdriver_client.ts: -------------------------------------------------------------------------------- 1 | import * as http from 'http'; 2 | import * as url from 'url'; 3 | 4 | /** 5 | * Super dumb and simple WebDriver client. Works with selenium standalone, may or may not work yet 6 | * directly with other drivers. 7 | */ 8 | export class SimpleWebDriverClient { 9 | seleniumAddress: string; 10 | 11 | constructor(seleniumAddress: string) { 12 | this.seleniumAddress = seleniumAddress; 13 | } 14 | 15 | /** 16 | * Send an execute script command. 17 | * 18 | * @param sessionId 19 | * @param data A JSON blob with the script and arguments to execute. 20 | */ 21 | public execute(sessionId: string, data: string) { 22 | const url = ['session', sessionId, 'execute'].join('/'); 23 | return this.createSeleniumRequest('POST', url, data); 24 | } 25 | 26 | /** 27 | * Send an execute async script command. 28 | * 29 | * @param sessionId 30 | * @param data A JSON blob with the script and arguments to execute. 31 | */ 32 | public executeAsync(sessionId: string, data: string) { 33 | const url = ['session', sessionId, 'execute_async'].join('/'); 34 | return this.createSeleniumRequest('POST', url, data); 35 | } 36 | 37 | /** 38 | * Get the location of an element. 39 | * 40 | * @param sessionId 41 | * @param elementId 42 | * @returns Promise<{}> A promise that resolves with the x and y coordinates of the element. 43 | */ 44 | public getLocation(sessionId: string, elementId: string) { 45 | const url = ['session', sessionId, 'element', elementId, 'location'].join('/'); 46 | return this.createSeleniumRequest('GET', url); 47 | } 48 | 49 | /** 50 | * Get the size of an element. 51 | * 52 | * @param sessionId 53 | * @param elementId 54 | * @returns Promise<{}> A promise that resolves with the height and width of the element. 55 | */ 56 | public getSize(sessionId: string, elementId: string) { 57 | const url = ['session', sessionId, 'element', elementId, 'size'].join('/'); 58 | return this.createSeleniumRequest('GET', url); 59 | } 60 | 61 | private createSeleniumRequest(method, messageUrl, data?) { 62 | let parsedUrl = url.parse(this.seleniumAddress); 63 | let options: http.RequestOptions = {}; 64 | options['method'] = method; 65 | options['path'] = parsedUrl.path + '/' + messageUrl; 66 | options['hostname'] = parsedUrl.hostname; 67 | options['port'] = parseInt(parsedUrl.port); 68 | 69 | let request = http.request(options); 70 | 71 | return new Promise((resolve, reject) => { 72 | if (data) { 73 | request.write(data); 74 | } 75 | request.end(); 76 | 77 | request.on('response', (resp) => { 78 | let respData = ''; 79 | resp.on('data', (d) => { 80 | respData += d; 81 | }); 82 | resp.on('error', (err) => { 83 | reject(err); 84 | }); 85 | resp.on('end', () => { 86 | let response = JSON.parse(respData); 87 | // Selenium 3.5.x or greater 88 | if (response.status && response.status > 0) { 89 | console.error(`Got status ${response.status} from selenium`, response.value); 90 | reject(JSON.stringify(response.value)); 91 | } 92 | // Selenium 3.0.x 93 | if (response.state && response.state !== 'success') { 94 | console.error(`Got response ${response.state} from selenium`, response.value); 95 | reject(JSON.stringify(response.value)); 96 | } 97 | resolve(response.value); 98 | }); 99 | }); 100 | }); 101 | }; 102 | } 103 | -------------------------------------------------------------------------------- /lib/webdriver_commands.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Utilities for parsing WebDriver commands from HTTP Requests. 3 | */ 4 | import * as events from 'events'; 5 | 6 | export type HttpMethod = 'GET'|'POST'|'DELETE'; 7 | export type paramKey = 'sessionId'|'elementId'|'name'|'propertyName'; 8 | 9 | export enum CommandName { 10 | NewSession, 11 | DeleteSession, 12 | Status, 13 | GetTimeouts, 14 | SetTimeouts, 15 | Go, 16 | GetCurrentURL, 17 | Back, 18 | Forward, 19 | Refresh, 20 | GetTitle, 21 | FindElement, 22 | FindElements, 23 | FindElementFromElement, 24 | FindElementsFromElement, 25 | IsElementSelected, 26 | GetElementAttribute, 27 | GetElementProperty, 28 | GetElementCSSValue, 29 | GetElementText, 30 | GetElementTagName, 31 | GetElementRect, 32 | IsElementEnabled, 33 | ElementClick, 34 | ElementClear, 35 | ElementSendKeys, 36 | WireMoveTo, 37 | WireButtonDown, 38 | WireButtonUp, 39 | GetAlertText, 40 | AcceptAlert, 41 | DismissAlert, 42 | UNKNOWN 43 | } 44 | 45 | /** 46 | * Represents an endpoint in the WebDriver spec. Endpoints are defined by 47 | * the CommandName enum and the url pattern that they match. 48 | * 49 | * For example, the pattern 50 | * /session/:sessionId/element/:elementId/click 51 | * will match urls such as 52 | * /session/d9e52b96-9b6a-4cb3-b017-76e8b4236646/element/1c2855ba-213d-4466-ba16-b14a7e6c3699/click 53 | * 54 | * @param pattern The url pattern 55 | * @param method The HTTP method, ie GET, POST, DELETE 56 | * @param name The CommandName of this endpoint. 57 | */ 58 | class Endpoint { 59 | constructor(private pattern: string, private method: HttpMethod, public name: CommandName) {} 60 | 61 | /** 62 | * Tests whether a given url from a request matches this endpoint. 63 | * 64 | * @param url A url from a request to test against the endpoint. 65 | * @param method The HTTP method. 66 | * @returns {boolean} Whether the endpoint matches. 67 | */ 68 | matches(url, method) { 69 | let urlParts = url.split('/'); 70 | let patternParts = this.pattern.split('/'); 71 | 72 | if (method != this.method || urlParts.length != patternParts.length) { 73 | return false; 74 | } 75 | // TODO: Replace this naive search with better parsing. 76 | for (let idx in patternParts) { 77 | if (!patternParts[idx].startsWith(':') && patternParts[idx] != urlParts[idx]) { 78 | return false; 79 | } 80 | } 81 | return true; 82 | } 83 | 84 | /** 85 | * Given a url from a http request, create an object containing parameters from the URL. 86 | * 87 | * Parameters are the parts of the endpoint's pattern that start with ':'. The ':' is dropped 88 | * from the parameter key. 89 | * 90 | * @param url The url from the request. 91 | * @returns An object mapping parameter keys to values from the url. 92 | */ 93 | getParams(url) { 94 | let urlParts = url.split('/'); 95 | let patternParts = this.pattern.split('/'); 96 | 97 | let params = {}; 98 | for (let idx in patternParts) { 99 | if (patternParts[idx].startsWith(':')) { 100 | let paramName = patternParts[idx].slice(1); 101 | params[paramName] = urlParts[idx]; 102 | } 103 | } 104 | return params; 105 | } 106 | } 107 | 108 | /** 109 | * An instance of a WebDriver command, containing the params and data for that request. 110 | * 111 | * @param commandName The enum identifying the command. 112 | * @param params Parameters for the command taken from the request's url. 113 | * @param data Optional data included with the command, taken from the body of the request. 114 | */ 115 | export class WebDriverCommand extends events.EventEmitter { 116 | private params: {[key: string]: string}; 117 | data: any; 118 | responseStatus: number; 119 | responseData: any; 120 | 121 | // All WebDriver commands have a session Id, except for two. 122 | // NewSession will have a session Id in the data 123 | // Status just doesn't 124 | get sessionId(): string { 125 | if (!this.getParam('sessionId') && this.url.startsWith('/session')) { 126 | return this.url.split('/')[2]; 127 | } 128 | return this.getParam('sessionId'); 129 | } 130 | 131 | constructor( 132 | public commandName: CommandName, public readonly url: string, 133 | public readonly method: HttpMethod, params?) { 134 | super(); 135 | this.params = params; 136 | } 137 | 138 | public getParam(key: paramKey) { 139 | return this.params[key]; 140 | } 141 | 142 | public handleData(data?: any) { 143 | try { 144 | this.data = JSON.parse(data); 145 | } catch (err) { 146 | this.data = data; 147 | } 148 | this.emit('data'); 149 | } 150 | 151 | public handleResponse(statusCode: number, data?: any) { 152 | this.responseStatus = statusCode; 153 | try { 154 | this.responseData = JSON.parse(data); 155 | } catch (err) { 156 | this.responseData = data; 157 | } 158 | this.emit('response'); 159 | } 160 | } 161 | 162 | 163 | /** 164 | * The set of known endpoints. 165 | */ 166 | let endpoints: Endpoint[] = []; 167 | 168 | function addWebDriverCommand(command: CommandName, method: HttpMethod, pattern: string) { 169 | endpoints.push(new Endpoint(pattern, method, command)); 170 | } 171 | 172 | /** 173 | * Returns a new WebdriverCommand object for the resource at the given URL. 174 | */ 175 | export function parseWebDriverCommand(url, method) { 176 | for (let endpoint of endpoints) { 177 | if (endpoint.matches(url, method)) { 178 | let params = endpoint.getParams(url); 179 | return new WebDriverCommand(endpoint.name, url, method, params); 180 | } 181 | } 182 | 183 | return new WebDriverCommand(CommandName.UNKNOWN, url, method, {}); 184 | } 185 | 186 | let sessionPrefix = '/session/:sessionId'; 187 | addWebDriverCommand(CommandName.NewSession, 'POST', '/session'); 188 | addWebDriverCommand(CommandName.DeleteSession, 'DELETE', '/session/:sessionId'); 189 | addWebDriverCommand(CommandName.Status, 'GET', '/status'); 190 | addWebDriverCommand(CommandName.GetTimeouts, 'GET', sessionPrefix + '/timeouts'); 191 | addWebDriverCommand(CommandName.SetTimeouts, 'POST', sessionPrefix + '/timeouts'); 192 | addWebDriverCommand(CommandName.Go, 'POST', sessionPrefix + '/url'); 193 | addWebDriverCommand(CommandName.GetCurrentURL, 'GET', sessionPrefix + '/url'); 194 | addWebDriverCommand(CommandName.Back, 'POST', sessionPrefix + '/back'); 195 | addWebDriverCommand(CommandName.Forward, 'POST', sessionPrefix + '/forward'); 196 | addWebDriverCommand(CommandName.Refresh, 'POST', sessionPrefix + '/refresh'); 197 | addWebDriverCommand(CommandName.GetTitle, 'GET', sessionPrefix + '/title'); 198 | addWebDriverCommand(CommandName.FindElement, 'POST', sessionPrefix + '/element'); 199 | addWebDriverCommand(CommandName.FindElements, 'POST', sessionPrefix + '/elements'); 200 | addWebDriverCommand( 201 | CommandName.FindElementFromElement, 'POST', sessionPrefix + '/element/:elementId/element'); 202 | addWebDriverCommand( 203 | CommandName.FindElementsFromElement, 'POST', sessionPrefix + '/element/:elementId/elements'); 204 | addWebDriverCommand( 205 | CommandName.IsElementSelected, 'POST', sessionPrefix + '/element/:elementId/selected'); 206 | addWebDriverCommand( 207 | CommandName.GetElementAttribute, 'GET', 208 | sessionPrefix + '/element/:elementId/attribute/:attributeName'); 209 | addWebDriverCommand( 210 | CommandName.GetElementProperty, 'GET', 211 | sessionPrefix + '/element/:elementId/property/:propertyName'); 212 | addWebDriverCommand( 213 | CommandName.GetElementCSSValue, 'GET', 214 | sessionPrefix + '/element/:elementId/css/:cssPropertyName'); 215 | addWebDriverCommand(CommandName.GetElementText, 'GET', sessionPrefix + '/element/:elementId/text'); 216 | addWebDriverCommand( 217 | CommandName.GetElementTagName, 'GET', sessionPrefix + '/element/:elementId/name'); 218 | addWebDriverCommand(CommandName.GetElementRect, 'GET', sessionPrefix + '/element/:elementId/rect'); 219 | addWebDriverCommand(CommandName.GetElementRect, 'GET', sessionPrefix + '/element/:elementId/size'); 220 | addWebDriverCommand( 221 | CommandName.IsElementEnabled, 'GET', sessionPrefix + '/element/:elementId/enabled'); 222 | addWebDriverCommand(CommandName.ElementClick, 'POST', sessionPrefix + '/element/:elementId/click'); 223 | addWebDriverCommand(CommandName.ElementClear, 'POST', sessionPrefix + '/element/:elementId/clear'); 224 | addWebDriverCommand( 225 | CommandName.ElementSendKeys, 'POST', sessionPrefix + '/element/:elementId/value'); 226 | 227 | addWebDriverCommand(CommandName.GetAlertText, 'GET', sessionPrefix + '/alert_text'); 228 | addWebDriverCommand(CommandName.GetAlertText, 'GET', sessionPrefix + '/alert/text'); 229 | addWebDriverCommand(CommandName.AcceptAlert, 'POST', sessionPrefix + '/alert/accept'); 230 | addWebDriverCommand(CommandName.AcceptAlert, 'POST', sessionPrefix + '/accept_alert'); 231 | addWebDriverCommand(CommandName.DismissAlert, 'POST', sessionPrefix + '/alert/dismiss'); 232 | addWebDriverCommand(CommandName.DismissAlert, 'POST', sessionPrefix + '/dismiss_alert'); 233 | 234 | // These commands are part of the JSON protocol, and were replaced by Perform Actions in the W3C 235 | // spec 236 | addWebDriverCommand(CommandName.WireMoveTo, 'POST', sessionPrefix + '/moveto'); 237 | addWebDriverCommand(CommandName.WireButtonDown, 'POST', sessionPrefix + '/buttondown'); 238 | addWebDriverCommand(CommandName.WireButtonUp, 'POST', sessionPrefix + '/buttonup'); 239 | -------------------------------------------------------------------------------- /lib/webdriver_logger.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | import * as path from 'path'; 3 | import * as stream from 'stream'; 4 | 5 | import {CommandName, WebDriverCommand} from './webdriver_commands'; 6 | 7 | // Generate a random 8 character ID to avoid collisions. 8 | function getLogId() { 9 | return Math.floor(Math.random() * Number.MAX_SAFE_INTEGER).toString(36).slice(0, 8); 10 | } 11 | 12 | function leftPad(field: string): string { 13 | const fieldWidth = 6; 14 | let padding = fieldWidth - field.length; 15 | if (padding > 0) { 16 | return ' '.repeat(padding) + field; 17 | } 18 | return field; 19 | } 20 | 21 | 22 | const FINDERS = [ 23 | CommandName.FindElement, CommandName.FindElementFromElement, CommandName.FindElements, 24 | CommandName.FindElementsFromElement 25 | ]; 26 | const READERS = [ 27 | CommandName.GetElementTagName, CommandName.GetElementText, CommandName.GetElementAttribute, 28 | CommandName.GetElementProperty, CommandName.GetElementCSSValue, CommandName.GetElementRect 29 | ]; 30 | const PAD = ' '; 31 | 32 | /** 33 | * Logs WebDriver commands, transforming the command into a user-friendly description. 34 | */ 35 | export class WebDriverLogger { 36 | logStream: stream.Writable; 37 | readonly logName: string; 38 | 39 | constructor() { 40 | this.logName = `webdriver_log_${getLogId()}.txt`; 41 | } 42 | 43 | /** 44 | * Start logging to the specified directory. Will create a file named 45 | * 'webdriver_log_.txt' 46 | * 47 | * @param logDir The directory to create log files in. 48 | */ 49 | public setLogDir(logDir: string) { 50 | this.logStream = fs.createWriteStream(path.join(logDir, this.logName), {flags: 'a'}); 51 | } 52 | 53 | /** 54 | * Logs a webdriver command to the log file. 55 | * 56 | * @param command The command to log. 57 | */ 58 | public logWebDriverCommand(command: WebDriverCommand) { 59 | if (!this.logStream) { 60 | return; 61 | } 62 | 63 | let logLine: string; 64 | logLine = `${this.timestamp()} `; 65 | 66 | let started = Date.now(); 67 | command.on('response', () => { 68 | let done = Date.now(); 69 | let elapsed = leftPad((done - started) + ''); 70 | logLine += `| ${elapsed}ms `; 71 | 72 | if (command.getParam('sessionId')) { 73 | let session = command.getParam('sessionId').slice(0, 6); 74 | logLine += `| ${session} `; 75 | } else if (command.commandName == CommandName.NewSession) { 76 | // Only for new session commands, the sessionId is in the response. 77 | let session = command.responseData['sessionId'].slice(0, 6); 78 | logLine += `| ${session} `; 79 | } 80 | 81 | if (command.commandName == CommandName.UNKNOWN) { 82 | logLine += `| ${command.url}`; 83 | } else { 84 | logLine += `| ${CommandName[command.commandName]}`; 85 | } 86 | 87 | if (command.commandName == CommandName.Go) { 88 | logLine += ' ' + command.data['url']; 89 | } else if (command.getParam('elementId')) { 90 | logLine += ` (${command.getParam('elementId')})`; 91 | } 92 | logLine += '\n'; 93 | 94 | this.logStream.write(logLine); 95 | this.renderData(command); 96 | this.renderResponse(command); 97 | }); 98 | } 99 | 100 | /** 101 | * Log an arbitrary event to the log file. 102 | * 103 | * @param msg The message to log. 104 | * @param sessionId The session id associated with the event. 105 | * @param elapsedMs How long the event took, in ms. 106 | */ 107 | public logEvent(msg: string, sessionId: string, elapsedMs: number) { 108 | let elapsed = leftPad(elapsedMs.toString()); 109 | let logLine = `${this.timestamp()} | ${elapsed}ms | ${sessionId.slice(0, 6)} | ${msg}\n`; 110 | this.logStream.write(logLine); 111 | } 112 | 113 | private renderData(command: WebDriverCommand) { 114 | let dataLine = ''; 115 | if (command.commandName === CommandName.NewSession) { 116 | dataLine = JSON.stringify(command.data['desiredCapabilities']); 117 | 118 | } else if (command.commandName === CommandName.ElementSendKeys) { 119 | let value = command.data['value'].join(''); 120 | dataLine = `Send: ${value}`; 121 | 122 | } else if (FINDERS.indexOf(command.commandName) !== -1) { 123 | const using = command.data['using']; 124 | const value = command.data['value']; 125 | dataLine = `Using ${using} '${value}'`; 126 | } 127 | if (dataLine) { 128 | this.logStream.write(PAD + dataLine + '\n'); 129 | } 130 | } 131 | 132 | private renderResponse(command: WebDriverCommand) { 133 | let respLine = ''; 134 | const data = command.responseData; 135 | if (data['status'] > 0) { 136 | respLine = `ERROR ${data['status']}: ${data['value']['message']}`; 137 | } else if (FINDERS.indexOf(command.commandName) !== -1) { 138 | let els = command.responseData['value']; 139 | if (!Array.isArray(els)) { 140 | els = [els]; 141 | } 142 | els = els.map((e) => e['ELEMENT']); 143 | respLine = 'Elements: ' + els; 144 | } else if (READERS.indexOf(command.commandName) !== -1) { 145 | respLine = command.responseData['value']; 146 | if (typeof respLine == 'object') { 147 | respLine = JSON.stringify(respLine); 148 | } 149 | } 150 | if (respLine) { 151 | this.logStream.write(PAD + respLine + '\n'); 152 | } 153 | } 154 | 155 | private timestamp(): string { 156 | let d = new Date(); 157 | let hours = d.getHours() < 10 ? '0' + d.getHours() : d.getHours(); 158 | let minutes = d.getMinutes() < 10 ? '0' + d.getMinutes() : d.getMinutes(); 159 | let seconds = d.getSeconds() < 10 ? '0' + d.getSeconds() : d.getSeconds(); 160 | let millis = d.getMilliseconds().toString(); 161 | millis = '000'.slice(0, 3 - millis.length) + millis; 162 | return `${hours}:${minutes}:${seconds}.${millis}`; 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /lib/webdriver_proxy.ts: -------------------------------------------------------------------------------- 1 | import * as http from 'http'; 2 | import * as url from 'url'; 3 | 4 | import {parseWebDriverCommand, WebDriverCommand} from './webdriver_commands'; 5 | 6 | /** 7 | * A proxy that understands WebDriver commands. Users can add barriers (similar to middleware in 8 | * express) that will be called before forwarding the request to WebDriver. The proxy will wait for 9 | * each barrier to finish, calling them in the order in which they were added. 10 | */ 11 | export class WebDriverProxy { 12 | barriers: WebDriverBarrier[]; 13 | seleniumAddress: string; 14 | 15 | constructor(seleniumAddress: string) { 16 | this.barriers = []; 17 | this.seleniumAddress = seleniumAddress; 18 | } 19 | 20 | addBarrier(barrier: WebDriverBarrier) { 21 | this.barriers.push(barrier); 22 | } 23 | 24 | async handleRequest(originalRequest: http.IncomingMessage, response: http.ServerResponse) { 25 | let command = parseWebDriverCommand(originalRequest.url, originalRequest.method); 26 | 27 | let replyWithError = (err) => { 28 | response.writeHead(502); 29 | if (err && err.toString) { 30 | response.write(err.toString()); 31 | } 32 | response.end(); 33 | }; 34 | 35 | // Process barriers in order, one at a time. 36 | try { 37 | for (let barrier of this.barriers) { 38 | await barrier.onCommand(command); 39 | } 40 | } catch (err) { 41 | replyWithError(err); 42 | // Don't call through if a barrier fails. 43 | return; 44 | } 45 | 46 | let parsedUrl = url.parse(this.seleniumAddress); 47 | let options: http.RequestOptions = {}; 48 | options.method = originalRequest.method; 49 | options.path = parsedUrl.path + originalRequest.url; 50 | options.hostname = parsedUrl.hostname; 51 | options.port = parseInt(parsedUrl.port); 52 | options.headers = originalRequest.headers; 53 | 54 | if (options.headers) { 55 | // Remove original req host so http can insert the correct host to hit selenium server 56 | delete options.headers.host; 57 | } 58 | 59 | let forwardedRequest = http.request(options); 60 | 61 | // clang-format off 62 | let reqData = ''; 63 | originalRequest.on('data', (d) => { 64 | reqData += d; 65 | forwardedRequest.write(d); 66 | }).on('end', () => { 67 | command.handleData(reqData); 68 | forwardedRequest.end(); 69 | }).on('error', replyWithError); 70 | 71 | forwardedRequest.on('response', (seleniumResponse) => { 72 | response.writeHead(seleniumResponse.statusCode, seleniumResponse.headers); 73 | 74 | let respData = ''; 75 | seleniumResponse.on('data', (d) => { 76 | respData += d; 77 | response.write(d); 78 | }).on('end', () => { 79 | command.handleResponse(seleniumResponse.statusCode, respData); 80 | response.end(); 81 | }).on('error', replyWithError); 82 | 83 | }).on('error', replyWithError); 84 | // clang-format on 85 | } 86 | } 87 | 88 | /** 89 | * When the proxy receives a WebDriver command, it will call onCommand() for each of it's barriers. 90 | * Barriers may return a promise for the proxy to wait for before proceeding. If the promise is 91 | * rejected, the proxy will reply with an error code and the result of the promise and the command 92 | * will not be forwarded to Selenium. 93 | */ 94 | export interface WebDriverBarrier { 95 | onCommand(command: WebDriverCommand): Promise; 96 | } 97 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "blocking-proxy", 3 | "description": "WebDriver Proxy for testing rich clients. It block certain calls until Angular is done updating the page under test.", 4 | "homepage": "https://github.com/angular/blocking-proxy", 5 | "keywords": [ 6 | "test", 7 | "testing", 8 | "webdriver", 9 | "webdriverjs", 10 | "selenium" 11 | ], 12 | "contributors": [ 13 | "Michael Giambalvo ", 14 | "Julie Ralph " 15 | ], 16 | "dependencies": { 17 | "minimist": "^1.2.0" 18 | }, 19 | "devDependencies": { 20 | "@types/jasmine": "2.5.45", 21 | "@types/minimist": "^1.1.29", 22 | "@types/nock": "^8.2.0", 23 | "@types/node": "^6.0.45", 24 | "@types/rimraf": "0.0.28", 25 | "@types/selenium-webdriver": "^2.53.39", 26 | "body-parser": "1.14.2", 27 | "clang-format": "^1.0.34", 28 | "gulp": "^3.9.1", 29 | "gulp-clang-format": "^1.0.23", 30 | "gulp-tslint": "^7.0.1", 31 | "jasmine": "^2.3.2", 32 | "jasmine-co": "^1.2.2", 33 | "jasmine-ts": "^0.2.1", 34 | "jshint": "2.9.1", 35 | "nock": "^9.0.2", 36 | "rimraf": "^2.5.4", 37 | "run-sequence": "^1.2.2", 38 | "selenium-mock": "^0.1.5", 39 | "selenium-webdriver": "^3.6.0", 40 | "ts-node": "^2.1.2", 41 | "tslint": "^4.3.1", 42 | "tslint-eslint-rules": "^3.1.0", 43 | "typescript": "^2.1.5", 44 | "vrsource-tslint-rules": "^0.14.1", 45 | "webdriver-manager": "^12.0.6" 46 | }, 47 | "repository": { 48 | "type": "git", 49 | "url": "git://github.com/angular/jasminewd.git" 50 | }, 51 | "typings": "built/lib/index.d.ts", 52 | "main": "built/lib/index.js", 53 | "bin": { 54 | "blocking-proxy": "built/lib/bin.js" 55 | }, 56 | "scripts": { 57 | "prepublish": "gulp prepublish", 58 | "start": "node built/lib/bin.js", 59 | "webdriver": "webdriver-manager update && webdriver-manager start", 60 | "test": "JASMINE_CONFIG_PATH=spec/jasmine_unit.json jasmine-ts", 61 | "test:auto": "find lib spec | entr npm test", 62 | "testapp": "cd testapp && npm start", 63 | "test:e2e": "JASMINE_CONFIG_PATH=spec/jasmine_e2e.json jasmine-ts" 64 | }, 65 | "jshintConfig": { 66 | "esversion": 6 67 | }, 68 | "engines": { 69 | "node": ">=6.9.x" 70 | }, 71 | "license": "MIT", 72 | "version": "1.0.1" 73 | } 74 | -------------------------------------------------------------------------------- /scripts/Dockerfile.sauceconnect: -------------------------------------------------------------------------------- 1 | FROM java:8-jre 2 | 3 | ENV SAUCE_VERSION 4.3.15 4 | 5 | WORKDIR /usr/local/sauce-connect 6 | 7 | RUN apt-get update -qqy \ 8 | && apt-get install -qqy \ 9 | wget \ 10 | && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* 11 | 12 | RUN wget https://saucelabs.com/downloads/sc-$SAUCE_VERSION-linux.tar.gz -O - | tar -xz 13 | 14 | WORKDIR /usr/local/sauce-connect/sc-$SAUCE_VERSION-linux 15 | 16 | ENTRYPOINT ["/usr/local/sauce-connect/sc-4.3.15-linux/bin/sc"] 17 | 18 | EXPOSE 8032 19 | 20 | CMD ["--version"] 21 | -------------------------------------------------------------------------------- /scripts/set_up_selenium_hub.sh: -------------------------------------------------------------------------------- 1 | docker run -d -P --name selenium-hub selenium/hub 2 | 3 | docker run -d --link selenium-hub:hub selenium/node-chrome 4 | docker run -d --link selenium-hub:hub selenium/node-firefox 5 | -------------------------------------------------------------------------------- /spec/e2e/e2e_spec.ts: -------------------------------------------------------------------------------- 1 | import * as webdriver from 'selenium-webdriver'; 2 | import {getTestEnv} from './environment'; 3 | 4 | describe('blocking proxy', function() { 5 | let driver: webdriver.WebDriver; 6 | 7 | beforeAll(() => { 8 | ({driver} = getTestEnv()); 9 | }); 10 | 11 | it('should fail when angular is not available', function(done) { 12 | driver.manage().timeouts().setScriptTimeout(20000); 13 | driver.get('about:blank'); 14 | driver.executeScript('var x = 20') 15 | .then( 16 | function() { 17 | done.fail('expected driver.execute to fail, but it did not'); 18 | }, 19 | function(err) { 20 | expect(err).toMatch('Error from waitForAngular'); 21 | done(); 22 | }); 23 | }, 10000); 24 | }); 25 | -------------------------------------------------------------------------------- /spec/e2e/environment.ts: -------------------------------------------------------------------------------- 1 | import * as webdriver from 'selenium-webdriver'; 2 | 3 | import {BlockingProxy, BPClient} from '../../lib'; 4 | 5 | export const BP_PORT = 8111; 6 | export const BP_URL = `http://localhost:${BP_PORT}`; 7 | export const WD_URL = 'http://localhost:4444/wd/hub'; 8 | 9 | let driver: webdriver.WebDriver; 10 | let bp: BlockingProxy; 11 | let client: BPClient; 12 | 13 | jasmine.DEFAULT_TIMEOUT_INTERVAL = 15000; 14 | 15 | export function getTestEnv() { 16 | return {driver, bp, client}; 17 | } 18 | 19 | beforeAll(async () => { 20 | bp = new BlockingProxy(WD_URL, 250); 21 | bp.listen(BP_PORT); 22 | 23 | let capabilities = webdriver.Capabilities.chrome(); 24 | driver = new webdriver.Builder().usingServer(BP_URL).withCapabilities(capabilities).build(); 25 | await driver.manage().timeouts().setScriptTimeout(12000); 26 | 27 | client = new BPClient(BP_URL); 28 | }); 29 | 30 | afterAll((done) => { 31 | driver.quit().then(done, done.fail); 32 | }); 33 | -------------------------------------------------------------------------------- /spec/e2e/logging_spec.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | import * as path from 'path'; 3 | import * as readline from 'readline'; 4 | import * as rimraf from 'rimraf'; 5 | import * as webdriver from 'selenium-webdriver'; 6 | 7 | import {BlockingProxy} from '../../lib/blockingproxy'; 8 | 9 | import {BP_URL, getTestEnv} from './environment'; 10 | 11 | /* 12 | Example log of a test session 13 | 14 | 20:08:14.830 | 834ms | 37f13c | NewSession', 15 | {"browserName":"chrome"}', 16 | 20:08:15.674 | 4ms | 37f13c | SetTimeouts', 17 | 20:08:15.681 | 578ms | 37f13c | Go http://localhost:8081/ng1/#/interaction', 18 | 20:08:16.300 | 438ms | 37f13c | FindElement', 19 | Using css selector \'.none\'', 20 | ERROR: no such element' 21 | */ 22 | 23 | 24 | describe('Logger', () => { 25 | let driver: webdriver.WebDriver; 26 | let bp: BlockingProxy; 27 | let logDir: string; 28 | 29 | function logPath() { 30 | let logName = bp.waitBarrier.logger.logName; 31 | return path.join(logDir, logName); 32 | } 33 | 34 | function readLog(): Promise { 35 | const rl = readline.createInterface({input: fs.createReadStream(logPath())}); 36 | let lines = []; 37 | rl.on('line', (line) => { 38 | lines.push(line); 39 | }); 40 | return new Promise((resolve) => { 41 | rl.on('close', () => { 42 | resolve(lines); 43 | }); 44 | }); 45 | } 46 | 47 | beforeEach(() => { 48 | ({driver, bp} = getTestEnv()); 49 | logDir = fs.mkdtempSync('./tmp-'); 50 | bp.waitBarrier.enabled = false; 51 | bp.enableLogging(logDir); 52 | }); 53 | 54 | afterEach((done) => { 55 | rimraf(logDir, done); 56 | }); 57 | 58 | it('creates a log file', async () => { 59 | await driver.get('http://localhost:8081/ng1/#/async'); 60 | let session = await driver.getSession(); 61 | 62 | expect(session).not.toBeNull(); 63 | expect(fs.existsSync(logPath())).toBeTruthy(); 64 | }); 65 | 66 | it('logs multiple sessions to the same file', async () => { 67 | let capabilities = webdriver.Capabilities.chrome(); 68 | let otherDriver = 69 | new webdriver.Builder().usingServer(BP_URL).withCapabilities(capabilities).build(); 70 | 71 | await driver.get('http://localhost:8081/ng1/#/interaction'); 72 | await otherDriver.get('http://localhost:8081/ng1/#/async'); 73 | 74 | let session1 = await driver.getSession(); 75 | let session2 = await otherDriver.getSession(); 76 | expect(session1).not.toBeNull(); 77 | expect(session2).not.toBeNull(); 78 | 79 | let logLines = await readLog(); 80 | let sessionId1 = session1.getId().slice(0, 6); 81 | let sessionId2 = session2.getId().slice(0, 6); 82 | expect(logLines[2]).toContain(`Go http://localhost:8081/ng1/#/interaction`); 83 | expect(logLines[2]).toContain(sessionId1); 84 | expect(logLines[3]).toContain(`Go http://localhost:8081/ng1/#/async`); 85 | expect(logLines[3]).toContain(sessionId2); 86 | 87 | await otherDriver.quit(); 88 | }); 89 | 90 | it('logs information about element finders', async () => { 91 | await driver.get('http://localhost:8081/ng1/#/interaction'); 92 | let el = driver.findElement(webdriver.By.id('flux')); 93 | await el.click(); 94 | 95 | await el.getCssValue('fake-color'); 96 | await el.getAttribute('fake-attr'); 97 | await el.getTagName(); 98 | await el.getText(); 99 | await el.getSize(); 100 | 101 | let logLines = await readLog(); 102 | let expectedLog = [ 103 | 'Go http://localhost:8081/ng1/#/interaction', 'FindElement', 104 | /Using css selector '\*\[id="flux"\]'/, 'Elements: 0', /ElementClick \(0\.\d+-1\)/, 105 | /GetElementCSSValue \(0\.\d+-1\)/, /GetElementAttribute \(0\.\d+-1\)/, ' null', 106 | /GetElementTagName \(0\.\d+-1\)/, ' button', /GetElementText \(0\.\d+-1\)/, 107 | ' Status: fluxing', /GetElementRect \(0\.\d+-1\)/ 108 | ]; 109 | for (let line in expectedLog) { 110 | expect(logLines[line]).toMatch(expectedLog[line], `Expected line: ${line} to match`); 111 | } 112 | }); 113 | 114 | it('handles selenium errors', async () => { 115 | await driver.get('http://localhost:8081/ng1/#/interaction'); 116 | try { 117 | let el = driver.findElement(webdriver.By.css('.none')); 118 | await el.click(); 119 | } catch (e) { 120 | // Nothing to do. 121 | } 122 | 123 | let logLines = await readLog(); 124 | expect(logLines[3]).toContain('ERROR 7: no such element'); 125 | }); 126 | 127 | it('logs when waiting for Angular', async () => { 128 | bp.waitBarrier.enabled = true; 129 | 130 | await driver.get('http://localhost:8081/ng1/#/interaction'); 131 | let el = driver.findElement(webdriver.By.id('flux')); 132 | await el.click(); 133 | 134 | let logLines = await readLog(); 135 | let expectedLog = [ 136 | 'Go http://localhost:8081/ng1/#/interaction', 'Waiting for Angular', 'FindElement', 137 | /Using css selector '\*\[id="flux"\]'/, /Elements: 0\.\d+-1/, 'Waiting for Angular', 138 | /ElementClick \(0\.\d+-1\)/ 139 | ]; 140 | for (let line in expectedLog) { 141 | expect(logLines[line]).toMatch(expectedLog[line], `Expected line: ${line} to match`); 142 | } 143 | }); 144 | }); 145 | -------------------------------------------------------------------------------- /spec/e2e/ng1_async_spec.ts: -------------------------------------------------------------------------------- 1 | import * as webdriver from 'selenium-webdriver'; 2 | 3 | import {BlockingProxy} from '../../lib/blockingproxy'; 4 | 5 | import {getTestEnv} from './environment'; 6 | 7 | describe('ng1 synchronizing with slow pages', () => { 8 | let driver: webdriver.WebDriver; 9 | let bp: BlockingProxy; 10 | 11 | beforeAll(() => { 12 | ({driver, bp} = getTestEnv()); 13 | bp.waitBarrier.enabled = true; 14 | }); 15 | 16 | beforeEach((done) => { 17 | driver.get('http://localhost:8081/ng1/#/async').then(done); 18 | }); 19 | 20 | async function expectText(selector, expectedText) { 21 | let text = await driver.findElement(webdriver.By.css(selector)).getText(); 22 | expect(text).toEqual(expectedText); 23 | } 24 | 25 | async function clickElement(selector) { 26 | let el = await driver.findElement(webdriver.By.css(selector)); 27 | await el.click(); 28 | } 29 | 30 | it('waits for http calls', async () => { 31 | await expectText('[ng-bind="slowHttpStatus"]', 'not started'); 32 | 33 | await clickElement('[ng-click="slowHttp()"]'); 34 | 35 | await expectText('[ng-bind="slowHttpStatus"]', 'done'); 36 | }, 10000); 37 | 38 | it('waits for long javascript execution', async () => { 39 | await expectText('[ng-bind="slowFunctionStatus"]', 'not started'); 40 | 41 | await clickElement('[ng-click="slowFunction()"]'); 42 | 43 | await expectText('[ng-bind="slowFunctionStatus"]', 'done'); 44 | }, 10000); 45 | 46 | it('DOES NOT wait for timeout', async () => { 47 | await expectText('[ng-bind="slowTimeoutStatus"]', 'not started'); 48 | 49 | await clickElement('[ng-click="slowTimeout()"]'); 50 | 51 | await expectText('[ng-bind="slowTimeoutStatus"]', 'pending...'); 52 | }, 10000); 53 | 54 | it('waits for $timeout', async () => { 55 | await expectText('[ng-bind="slowAngularTimeoutStatus"]', 'not started'); 56 | 57 | await clickElement('[ng-click="slowAngularTimeout()"]'); 58 | 59 | await expectText('[ng-bind="slowAngularTimeoutStatus"]', 'done'); 60 | }, 10000); 61 | 62 | it('waits for $timeout then a promise', async () => { 63 | await expectText('[ng-bind="slowAngularTimeoutPromiseStatus"]', 'not started'); 64 | 65 | await clickElement('[ng-click="slowAngularTimeoutPromise()"]'); 66 | 67 | await expectText('[ng-bind="slowAngularTimeoutPromiseStatus"]', 'done'); 68 | }, 10000); 69 | 70 | it('waits for long http call then a promise', async () => { 71 | await expectText('[ng-bind="slowHttpPromiseStatus"]', 'not started'); 72 | 73 | await clickElement('[ng-click="slowHttpPromise()"]'); 74 | 75 | await expectText('[ng-bind="slowHttpPromiseStatus"]', 'done'); 76 | }, 10000); 77 | 78 | it('waits for slow routing changes', async () => { 79 | await expectText('[ng-bind="routingChangeStatus"]', 'not started'); 80 | 81 | await clickElement('[ng-click="routingChange()"]'); 82 | 83 | let source = await driver.getPageSource(); 84 | expect(source).toMatch('polling mechanism'); 85 | }, 10000); 86 | 87 | it('waits for slow ng-include templates to load', async () => { 88 | await expectText('.included', 'fast template contents'); 89 | 90 | await clickElement('[ng-click="changeTemplateUrl()"]'); 91 | 92 | await expectText('.included', 'slow template contents'); 93 | }, 10000); 94 | }); 95 | -------------------------------------------------------------------------------- /spec/e2e/ng1_polling_spec.ts: -------------------------------------------------------------------------------- 1 | import * as webdriver from 'selenium-webdriver'; 2 | 3 | import {BlockingProxy} from '../../lib/blockingproxy'; 4 | 5 | import {getTestEnv} from './environment'; 6 | 7 | const By = webdriver.By; 8 | 9 | describe('disabling waiting as needed', function() { 10 | let driver: webdriver.WebDriver; 11 | let bp: BlockingProxy; 12 | 13 | beforeAll(() => { 14 | ({driver, bp} = getTestEnv()); 15 | }); 16 | 17 | beforeEach(async () => { 18 | await driver.get('http://localhost:8081/ng1/#/polling'); 19 | }); 20 | 21 | it('avoids timeouts', async () => { 22 | bp.waitBarrier.enabled = true; 23 | 24 | let startButton = await driver.findElement(By.id('pollstarter')); 25 | 26 | let count = await driver.findElement(By.id('count')); 27 | expect(await count.getText()).toEqual('0'); 28 | 29 | await startButton.click(); 30 | 31 | bp.waitBarrier.enabled = false; 32 | 33 | expect(await count.getText()).toBeGreaterThan(-1); 34 | 35 | await driver.sleep(2100); 36 | 37 | expect(await count.getText()).toBeGreaterThan(1); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /spec/helpers/jasmine-co.helper.js: -------------------------------------------------------------------------------- 1 | // Makes using async/await in tests easier. 2 | require('jasmine-co').install(); 3 | 4 | -------------------------------------------------------------------------------- /spec/helpers/mock_selenium.ts: -------------------------------------------------------------------------------- 1 | import {Command, Server, Session as BasicSession} from 'selenium-mock'; 2 | 3 | export interface Session extends BasicSession { url: string; } 4 | 5 | // Set Timeout 6 | let setTimeouts = new Command('POST', 'timeouts', (session, params) => {}); 7 | 8 | // Go 9 | let setUrl = new Command('POST', 'url', (session, params) => { 10 | session.url = params['url']; 11 | }); 12 | 13 | // Get Current URL 14 | let getUrl = new Command('GET', 'url', (session, params) => { 15 | return session.url; 16 | }); 17 | 18 | // Back 19 | let forward = new Command('POST', 'back', (session, params) => {}); 20 | 21 | // Back 22 | let back = new Command('POST', 'forward', (session, params) => {}); 23 | 24 | // refresh 25 | let refresh = new Command('POST', 'refresh', (session, params) => {}); 26 | 27 | // refresh 28 | let title = new Command('GET', 'title', (session, params) => {}); 29 | 30 | // GetWindowHandle 31 | let getWindowHandle = new Command('GET', 'window_handle', (session, params) => { 32 | return 'main'; 33 | }); 34 | 35 | // Find Element 36 | let findElement = new Command('POST', 'element', (session, params) => { 37 | return {'ELEMENT': '0'}; 38 | }); 39 | 40 | // Find Elements 41 | let findElements = new Command('POST', 'elements', (session, params) => { 42 | return [{'ELEMENT': '0'}, {'ELEMENT': '1'}]; 43 | }); 44 | 45 | // Find Element From Element 46 | let findElementFromElement = 47 | new Command('POST', 'element/:elementId/element', (session, params) => { 48 | return {'ELEMENT': '0'}; 49 | }); 50 | 51 | // Find Elements From Element 52 | let findElementsFromElement = 53 | new Command('POST', 'element/:elementId/elements', (session, params) => { 54 | return [{'ELEMENT': '0'}, {'ELEMENT': '1'}]; 55 | }); 56 | 57 | // Is Element Selected 58 | let isElementSelected = 59 | new Command('POST', 'element/:elementId/selected', (session, params) => {}); 60 | 61 | // Get Element Attribute 62 | let getElementAttribute = new Command( 63 | 'GET', 'element/:elementId/attribute/:attributeName', (session, params) => { 64 | return 'null'; 65 | }); 66 | 67 | // Get Element Property 68 | let getElementProperty = 69 | new Command('GET', 'element/:elementId/property/:propertyName', (session, params) => { 70 | return 'Property'; 71 | }); 72 | 73 | // Get Element CSS Value 74 | let getElementCSSValue = 75 | new Command('GET', 'element/:elementId/css/:cssPropertyName', (session, params) => { 76 | return 'white'; 77 | }); 78 | 79 | // Get Element Text 80 | let getElementText = new Command('GET', 'element/:elementId/text', (session, params) => { 81 | return 'some text'; 82 | }); 83 | 84 | // Get Element Tag Name 85 | let getElementTagName = 86 | new Command('GET', 'element/:elementId/name', (session, params) => { 87 | return 'button'; 88 | }); 89 | 90 | // Get Element Rect 91 | let getElementRect = new Command('GET', 'element/:elementId/rect', (session, params) => { 92 | return {width: 88, hCode: 88, class: 'org.openqa.selenium.Dimension', height: 20}; 93 | }); 94 | 95 | // Get Element Rect from JSON Wire protocol (not W3C spec) 96 | let getElementRectWire = 97 | new Command('GET', 'element/:elementId/size', (session, params) => { 98 | return {width: 88, hCode: 88, class: 'org.openqa.selenium.Dimension', height: 20}; 99 | }); 100 | 101 | 102 | // Is Element Enabled 103 | let isElementEnabled = 104 | new Command('GET', 'element/:elementId/enabled', (session, params) => {}); 105 | 106 | // Element Click 107 | let elementClick = 108 | new Command('POST', 'element/:elementId/click', (session, params) => {}); 109 | 110 | // Element Clear 111 | let elementClear = 112 | new Command('POST', 'element/:elementId/clear', (session, params) => {}); 113 | 114 | // Element Send Keys 115 | let elementSendKeys = 116 | new Command('POST', 'element/:elementId/value', (session, params) => {}); 117 | 118 | // Get Alert Text 119 | let alertText = new Command('GET', 'alert_text', (session, params) => {}); 120 | 121 | // Accept Alert 122 | let acceptAlert = new Command('POST', 'accept_alert', (session, params) => {}); 123 | 124 | // Dismiss Alert 125 | let dismissAlert = new Command('POST', 'dismiss_alert', (session, params) => {}); 126 | 127 | // Actions 128 | let moveTo = new Command('POST', 'moveto', (session, params) => {}); 129 | 130 | // Button Down 131 | let buttonDown = new Command('POST', 'buttondown', (session, params) => {}); 132 | 133 | // Button Up 134 | let buttonUp = new Command('POST', 'buttonup', (session, params) => {}); 135 | 136 | export function getMockSelenium() { 137 | let server = new Server(0); 138 | server.addCommand(setTimeouts); 139 | server.addCommand(setUrl); 140 | server.addCommand(getUrl); 141 | server.addCommand(back); 142 | server.addCommand(forward); 143 | server.addCommand(refresh); 144 | server.addCommand(title); 145 | server.addCommand(getWindowHandle); 146 | server.addCommand(findElement); 147 | server.addCommand(findElements); 148 | server.addCommand(findElementFromElement); 149 | server.addCommand(findElementsFromElement); 150 | server.addCommand(isElementSelected); 151 | server.addCommand(getElementAttribute); 152 | server.addCommand(getElementProperty); 153 | server.addCommand(getElementCSSValue); 154 | server.addCommand(getElementText); 155 | server.addCommand(getElementTagName); 156 | server.addCommand(getElementRect); 157 | server.addCommand(getElementRectWire); 158 | server.addCommand(isElementEnabled); 159 | server.addCommand(elementClick); 160 | server.addCommand(elementClear); 161 | server.addCommand(elementSendKeys); 162 | server.addCommand(alertText); 163 | server.addCommand(acceptAlert); 164 | server.addCommand(dismissAlert); 165 | server.addCommand(moveTo); 166 | server.addCommand(buttonDown); 167 | server.addCommand(buttonUp); 168 | return server; 169 | } 170 | -------------------------------------------------------------------------------- /spec/jasmine_e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "spec_dir": "spec", 3 | "spec_files": [ 4 | "e2e/**/*[sS]pec.ts" 5 | ], 6 | "helpers": [ 7 | "helpers/jasmine-co.helper.js" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /spec/jasmine_unit.json: -------------------------------------------------------------------------------- 1 | { 2 | "spec_dir": "spec", 3 | "spec_files": [ 4 | "unit/**/*[sS]pec.ts" 5 | ], 6 | "helpers": [ 7 | "helpers/**/*.js" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /spec/unit/client_spec.ts: -------------------------------------------------------------------------------- 1 | import {BlockingProxy, BPClient} from '../../lib/'; 2 | 3 | describe('BlockingProxy Client', () => { 4 | let bp: BlockingProxy; 5 | let client: BPClient; 6 | 7 | beforeAll(() => { 8 | bp = new BlockingProxy('http://localhost:3111'); 9 | let bpPort = bp.listen(0); 10 | client = new BPClient(`http://localhost:${bpPort}`); 11 | }); 12 | 13 | it('should toggle waiting', async () => { 14 | expect(bp.waitBarrier.enabled).toBe(true); 15 | 16 | await client.setWaitEnabled(false); 17 | expect(bp.waitBarrier.enabled).toBe(false); 18 | }); 19 | 20 | it('can get whether wait is enabled', async () => { 21 | bp.waitBarrier.enabled = true; 22 | expect(await client.isWaitEnabled()).toBeTruthy(); 23 | bp.waitBarrier.enabled = false; 24 | expect(await client.isWaitEnabled()).toBeFalsy(); 25 | }); 26 | 27 | it('allows changing the root selector', async () => { 28 | bp.waitBarrier.rootSelector = ''; 29 | const newRoot = 'div#app'; 30 | 31 | await client.setWaitParams(newRoot); 32 | expect(bp.waitBarrier.rootSelector).toBe(newRoot); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /spec/unit/config_spec.ts: -------------------------------------------------------------------------------- 1 | import {processArgs} from '../../lib/config'; 2 | 3 | describe('cli launcher', () => { 4 | it('should read selenium address from commandline', () => { 5 | let argv = processArgs(['--seleniumAddress', 'http://test.com']); 6 | expect(argv.seleniumAddress).toBe('http://test.com'); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /spec/unit/highlight_delay_barrier_spec.ts: -------------------------------------------------------------------------------- 1 | import {HighlightDelayBarrier} from '../../lib/highlight_delay_barrier'; 2 | import {parseWebDriverCommand} from '../../lib/webdriver_commands'; 3 | 4 | describe('highlight delay barrier', () => { 5 | let highlight: HighlightDelayBarrier; 6 | let client: any; 7 | 8 | beforeEach(() => { 9 | client = jasmine.createSpyObj('client', ['getLocation', 'getSize', 'execute']); 10 | client.getLocation.and.returnValue(Promise.resolve({x: 10, y: 10})); 11 | client.getSize.and.returnValue(Promise.resolve({width: 20, height: 20})); 12 | client.execute.and.returnValue(Promise.resolve()); 13 | 14 | highlight = new HighlightDelayBarrier(client, 0); 15 | }); 16 | 17 | it('blocks for the set amount of time', async () => { 18 | highlight.delay = 200; 19 | let cmd = parseWebDriverCommand('/session/abcdef/element/0/click', 'POST'); 20 | 21 | // TODO Figure out how to use Jasmine's clock here. 22 | let start = Date.now(); 23 | await highlight.onCommand(cmd); 24 | let elapsed = Date.now() - start; 25 | 26 | expect(elapsed).toBeGreaterThanOrEqual(200); 27 | expect(client.getLocation).toHaveBeenCalled(); 28 | expect(client.getSize).toHaveBeenCalled(); 29 | expect(client.execute).toHaveBeenCalled(); 30 | }); 31 | 32 | it('doesn\'t do anything if delay isn\'t set', async () => { 33 | highlight.delay = 0; 34 | let cmd = parseWebDriverCommand('/session/abcdef/element/0/click', 'POST'); 35 | 36 | // TODO Figure out how to use Jasmine's clock here. 37 | let start = Date.now(); 38 | await highlight.onCommand(cmd); 39 | let elapsed = Date.now() - start; 40 | 41 | expect(elapsed).toBeLessThan(10); 42 | expect(client.getLocation).not.toHaveBeenCalled(); 43 | expect(client.getSize).not.toHaveBeenCalled(); 44 | expect(client.execute).not.toHaveBeenCalled(); 45 | 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /spec/unit/proxy_spec.ts: -------------------------------------------------------------------------------- 1 | import {BlockingProxy} from '../../lib/blockingproxy'; 2 | 3 | describe('BlockingProxy', () => { 4 | it('should wait for angular by default', () => { 5 | let proxy = new BlockingProxy('http://locahost:4444'); 6 | expect(proxy.waitBarrier.enabled).toBe(true); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /spec/unit/simple_webdriver_client_spec.ts: -------------------------------------------------------------------------------- 1 | import * as nock from 'nock'; 2 | 3 | import {SimpleWebDriverClient} from '../../lib/simple_webdriver_client'; 4 | 5 | describe('Simple WebDriver Client', () => { 6 | let client: SimpleWebDriverClient; 7 | let seleniumAddress = 'http://fake-address.com:4444/wd/hub'; 8 | const sessionId = 'abcde-fghij'; 9 | const fakeScript = 'function fakeScript() {}'; 10 | 11 | beforeEach(() => { 12 | client = new SimpleWebDriverClient(seleniumAddress); 13 | }); 14 | 15 | it('can make executeAsync calls', async () => { 16 | let scope = nock(seleniumAddress) 17 | .post(`/session/${sessionId}/execute_async`, fakeScript) 18 | .reply(200, {state: 'success', value: ''}); 19 | 20 | await client.executeAsync(sessionId, fakeScript); 21 | scope.done(); 22 | }); 23 | 24 | it('can make getLocation calls', async () => { 25 | const elementId = '0'; 26 | const fakeLoc = {x: 10, y: 10}; 27 | 28 | let scope = nock(seleniumAddress) 29 | .get(`/session/${sessionId}/element/${elementId}/location`) 30 | .reply(200, {state: 'success', value: fakeLoc}); 31 | 32 | const rect = await client.getLocation(sessionId, elementId); 33 | scope.done(); 34 | expect(rect).toEqual(fakeLoc); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /spec/unit/util.ts: -------------------------------------------------------------------------------- 1 | import * as stream from 'stream'; 2 | 3 | import {CommandName, WebDriverCommand} from '../../lib/webdriver_commands'; 4 | import {WebDriverBarrier} from '../../lib/webdriver_proxy'; 5 | 6 | /** 7 | * Fakes and helpers for testing. 8 | */ 9 | export class TestBarrier implements WebDriverBarrier { 10 | commands: WebDriverCommand[] = []; 11 | 12 | onCommand(command: WebDriverCommand): Promise { 13 | this.commands.push(command); 14 | return; 15 | } 16 | 17 | getCommands(): CommandName[] { 18 | return this.commands.map((c) => c.commandName); 19 | } 20 | 21 | getCommandNames(): string[] { 22 | return this.commands.map((c) => CommandName[c.commandName]); 23 | } 24 | } 25 | 26 | export class InMemoryWriter extends stream.Writable { 27 | content: string; 28 | doneCb: Function; 29 | 30 | constructor() { 31 | super({decodeStrings: true}); 32 | this.content = ''; 33 | } 34 | 35 | _write(chunk: Buffer, encoding?, callback?) { 36 | let data = chunk.toString(); 37 | this.content += data; 38 | callback(); 39 | } 40 | 41 | onEnd(cb: Function) { 42 | this.doneCb = cb; 43 | } 44 | 45 | end() { 46 | super.end(); 47 | if (this.doneCb) { 48 | this.doneCb(this.content); 49 | } 50 | } 51 | } 52 | 53 | export class InMemoryReader extends stream.Readable { 54 | content: string[]; 55 | idx: number; 56 | 57 | constructor() { 58 | super(); 59 | this.content = []; 60 | this.idx = 0; 61 | } 62 | 63 | _read() { 64 | if (this.idx < this.content.length) { 65 | this.push(this.content[this.idx++]); 66 | } else { 67 | this.push(null); 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /spec/unit/webdriver_commands_spec.ts: -------------------------------------------------------------------------------- 1 | import * as http from 'http'; 2 | import {Server} from 'selenium-mock'; 3 | import * as webdriver from 'selenium-webdriver'; 4 | 5 | import {CommandName} from '../../lib/webdriver_commands'; 6 | import {WebDriverProxy} from '../../lib/webdriver_proxy'; 7 | import {getMockSelenium, Session} from '../helpers/mock_selenium'; 8 | import {TestBarrier} from './util'; 9 | 10 | describe('WebDriver command parser', () => { 11 | let mockServer: Server; 12 | let driver: webdriver.WebDriver; 13 | let proxy: WebDriverProxy; 14 | let server: http.Server; 15 | let testBarrier: TestBarrier; 16 | let port: number; 17 | 18 | beforeEach(async () => { 19 | mockServer = getMockSelenium(); 20 | mockServer.start(); 21 | let mockPort = mockServer.handle.address().port; 22 | 23 | proxy = new WebDriverProxy(`http://localhost:${mockPort}/wd/hub`); 24 | testBarrier = new TestBarrier; 25 | proxy.addBarrier(testBarrier); 26 | server = http.createServer(proxy.handleRequest.bind(proxy)); 27 | server.listen(0); 28 | port = server.address().port; 29 | 30 | driver = new webdriver.Builder() 31 | .usingServer(`http://localhost:${port}`) 32 | .withCapabilities(webdriver.Capabilities.chrome()) 33 | .build(); 34 | 35 | // Ensure WebDriver client has created a session by waiting on a command. 36 | await driver.get('http://example.com'); 37 | }); 38 | 39 | it('parses session commands', async () => { 40 | let session = await driver.getSession(); 41 | let sessionId = session.getId(); 42 | await driver.quit(); 43 | 44 | let recentCommands = testBarrier.getCommands(); 45 | expect(recentCommands.length).toBe(3); 46 | expect(recentCommands).toEqual([ 47 | CommandName.NewSession, CommandName.Go, CommandName.DeleteSession 48 | ]); 49 | expect(testBarrier.commands[1].sessionId).toEqual(sessionId); 50 | }); 51 | 52 | it('parses url commands', async () => { 53 | await driver.getCurrentUrl(); 54 | await driver.navigate().back(); 55 | await driver.navigate().forward(); 56 | await driver.navigate().refresh(); 57 | await driver.getTitle(); 58 | 59 | let recentCommands = testBarrier.getCommands(); 60 | expect(recentCommands.length).toBe(7); 61 | expect(recentCommands).toEqual([ 62 | CommandName.NewSession, CommandName.Go, CommandName.GetCurrentURL, CommandName.Back, 63 | CommandName.Forward, CommandName.Refresh, CommandName.GetTitle 64 | ]); 65 | }); 66 | 67 | it('parses timeout commands', async () => { 68 | await driver.manage().timeouts().setScriptTimeout(2468); 69 | 70 | let recentCommands = testBarrier.getCommands(); 71 | expect(recentCommands[2]).toEqual(CommandName.SetTimeouts); 72 | let timeoutData = testBarrier.commands[2].data; 73 | expect(timeoutData['script']).toEqual(2468); 74 | }); 75 | 76 | it('parses element commands', async () => { 77 | let el = driver.findElement(webdriver.By.css('.test')); 78 | await el.click(); 79 | await el.getCssValue('fake-color'); 80 | await el.getAttribute('fake-attr'); 81 | await el.getTagName(); 82 | await el.getText(); 83 | await el.getSize(); 84 | await el.clear(); 85 | await el.sendKeys('test string'); 86 | 87 | let inner = el.findElement(webdriver.By.css('.inner_thing')); 88 | await inner.click(); 89 | 90 | await driver.findElements(webdriver.By.id('thing')); 91 | await el.findElements(webdriver.By.css('.inner_thing')); 92 | 93 | // let find = testBarrier.commands[2]; 94 | expect(testBarrier.getCommands()).toEqual([ 95 | CommandName.NewSession, 96 | CommandName.Go, 97 | CommandName.FindElement, 98 | CommandName.ElementClick, 99 | CommandName.GetElementCSSValue, 100 | CommandName.GetElementAttribute, 101 | CommandName.GetElementTagName, 102 | CommandName.GetElementText, 103 | CommandName.GetElementRect, 104 | CommandName.ElementClear, 105 | CommandName.ElementSendKeys, 106 | CommandName.FindElementFromElement, 107 | CommandName.ElementClick, 108 | CommandName.FindElements, 109 | CommandName.FindElementsFromElement, 110 | ]); 111 | }); 112 | 113 | it('parses actions', async () => { 114 | let el = driver.findElement(webdriver.By.css('.test')); 115 | 116 | await driver.actions().mouseMove({x: 10, y: 10}).dragAndDrop(el, {x: 20, y: 20}).perform(); 117 | 118 | expect(testBarrier.getCommands()).toEqual([ 119 | CommandName.NewSession, CommandName.Go, CommandName.FindElement, CommandName.WireMoveTo, 120 | CommandName.WireMoveTo, CommandName.WireButtonDown, CommandName.WireMoveTo, 121 | CommandName.WireButtonUp 122 | ]); 123 | expect(testBarrier.commands[3].data).toEqual({xoffset: 10, yoffset: 10}); 124 | }); 125 | 126 | it('parses alert commands', async () => { 127 | await driver.switchTo().alert().dismiss(); 128 | await driver.switchTo().alert().accept(); 129 | 130 | expect(testBarrier.getCommands()).toEqual([ 131 | CommandName.NewSession, CommandName.Go, CommandName.GetAlertText, CommandName.DismissAlert, 132 | CommandName.GetAlertText, CommandName.AcceptAlert 133 | ]); 134 | }); 135 | 136 | it('saves url and method for unknown commands', (done) => { 137 | const fakeUrl = '/session/abcdef/unknown'; 138 | let options: 139 | http.RequestOptions = {port: port, path: fakeUrl, hostname: 'localhost', method: 'GET'}; 140 | 141 | let req = http.request(options); 142 | req.end(); 143 | 144 | req.on('response', () => { 145 | let lastCommand = testBarrier.commands[2]; 146 | expect(lastCommand.commandName).toBe(CommandName.UNKNOWN); 147 | expect(lastCommand.url).toEqual(fakeUrl); 148 | expect(lastCommand.method).toEqual('GET'); 149 | done(); 150 | }); 151 | req.on('error', done.fail); 152 | }); 153 | 154 | afterEach(() => { 155 | server.close(); 156 | mockServer.stop(); 157 | }); 158 | }); 159 | -------------------------------------------------------------------------------- /spec/unit/webdriver_logger_spec.ts: -------------------------------------------------------------------------------- 1 | import {Server} from 'selenium-mock'; 2 | import * as webdriver from 'selenium-webdriver'; 3 | import * as stream from 'stream'; 4 | 5 | import {BlockingProxy} from '../../lib/blockingproxy'; 6 | import {parseWebDriverCommand} from '../../lib/webdriver_commands'; 7 | import {WebDriverLogger} from '../../lib/webdriver_logger'; 8 | import {getMockSelenium, Session} from '../helpers/mock_selenium'; 9 | 10 | const capabilities = webdriver.Capabilities.chrome(); 11 | 12 | /** 13 | * For testing purposes, create a logger that logs to an in-memory buffer, instead of to disk. 14 | */ 15 | class InMemoryLogger extends WebDriverLogger { 16 | logs: string[]; 17 | 18 | constructor() { 19 | super(); 20 | this.logs = []; 21 | } 22 | 23 | public setLogDir(logDir: string) { 24 | let self = this; 25 | this.logStream = new stream.Writable({ 26 | write(chunk, enc, next) { 27 | self.logs.push(chunk.toString()); 28 | next(); 29 | } 30 | }); 31 | } 32 | 33 | public reset() { 34 | this.logs = []; 35 | } 36 | 37 | public getLog() { 38 | return this.logs; 39 | } 40 | } 41 | 42 | describe('WebDriver logger', () => { 43 | let mockServer: Server; 44 | let driver: webdriver.WebDriver; 45 | let logger = new InMemoryLogger(); 46 | let proxy: BlockingProxy; 47 | let bpPort: number; 48 | let start = new Date('2017-01-26 22:05:34.000'); 49 | 50 | beforeAll(() => { 51 | mockServer = getMockSelenium(); 52 | mockServer.start(); 53 | let mockPort = mockServer.handle.address().port; 54 | 55 | proxy = new BlockingProxy(`http://localhost:${mockPort}/wd/hub`); 56 | proxy.waitBarrier.enabled = false; 57 | bpPort = proxy.listen(0); 58 | logger.setLogDir('.'); 59 | proxy.setLogger(logger); 60 | }); 61 | 62 | afterAll(() => { 63 | mockServer.stop(); 64 | }); 65 | 66 | beforeEach(async () => { 67 | jasmine.clock().install(); 68 | jasmine.clock().mockDate(start); 69 | 70 | driver = new webdriver.Builder() 71 | .usingServer(`http://localhost:${bpPort}`) 72 | .withCapabilities(capabilities) 73 | .build(); 74 | 75 | // Ensure WebDriver client has created a session by waiting on a command. 76 | await driver.get('http://example.com'); 77 | }); 78 | 79 | afterEach(() => { 80 | jasmine.clock().uninstall(); 81 | logger.reset(); 82 | }); 83 | 84 | it('creates logfiles with unique names', () => { 85 | let otherLogger = new InMemoryLogger(); 86 | 87 | expect(logger.logName).not.toEqual(otherLogger.logName); 88 | }); 89 | 90 | it('logs session commands', async () => { 91 | let session = await driver.getSession(); 92 | let shortSession = session.getId().slice(0, 6); 93 | await driver.quit(); 94 | 95 | let log = logger.getLog(); 96 | expect(log[0]).toContain('NewSession'); 97 | expect(log[0]).toContain(shortSession); 98 | expect(log[3]).toContain('DeleteSession'); 99 | expect(log[3]).toContain(shortSession); 100 | }); 101 | 102 | it('logs url commands', async () => { 103 | await driver.getCurrentUrl(); 104 | 105 | let log = logger.getLog(); 106 | expect(log[0]).toContain('NewSession'); 107 | expect(log[1]).toContain('chrome'); 108 | expect(log[2]).toContain('Go http://example.com'); 109 | }); 110 | 111 | it('parses commands that affect elements', async () => { 112 | let session = await driver.getSession(); 113 | let shortSession = session.getId().slice(0, 6); 114 | logger.reset(); 115 | 116 | let el = driver.findElement(webdriver.By.css('.test')); 117 | await el.click(); 118 | await el.clear(); 119 | await el.sendKeys('test string'); 120 | 121 | let inner = el.findElement(webdriver.By.css('.inner_thing')); 122 | await inner.click(); 123 | 124 | await driver.findElements(webdriver.By.id('thing')); 125 | await el.findElements(webdriver.By.css('.inner_thing')); 126 | 127 | let log = logger.getLog(); 128 | let expectedLog = [ 129 | `22:05:34.000 | 0ms | ${shortSession} | FindElement\n`, 130 | ` Using css selector '.test'\n`, 131 | ` Elements: 0\n`, 132 | `22:05:34.000 | 0ms | ${shortSession} | ElementClick (0)\n`, 133 | `22:05:34.000 | 0ms | ${shortSession} | ElementClear (0)\n`, 134 | `22:05:34.000 | 0ms | ${shortSession} | ElementSendKeys (0)\n`, 135 | ` Send: test string\n`, 136 | `22:05:34.000 | 0ms | ${shortSession} | FindElementFromElement (0)\n`, 137 | ` Using css selector '.inner_thing'\n`, 138 | ` Elements: 0\n`, 139 | `22:05:34.000 | 0ms | ${shortSession} | ElementClick (0)\n`, 140 | `22:05:34.000 | 0ms | ${shortSession} | FindElements\n`, 141 | ` Using css selector '*[id=\"thing\"]'\n`, 142 | ` Elements: 0,1\n`, 143 | `22:05:34.000 | 0ms | ${shortSession} | FindElementsFromElement (0)\n`, 144 | ` Using css selector '.inner_thing'\n`, 145 | ` Elements: 0,1\n`, 146 | ]; 147 | for (let line in expectedLog) { 148 | expect(log[line]).toEqual(expectedLog[line], `Expected line: ${line} to match`); 149 | } 150 | }); 151 | 152 | it('parses commands that read elements', async () => { 153 | logger.reset(); 154 | let session = await driver.getSession(); 155 | let shortSession = session.getId().slice(0, 6); 156 | 157 | let el = driver.findElement(webdriver.By.css('.test')); 158 | await el.getCssValue('color'); 159 | await el.getAttribute('id'); 160 | await el.getTagName(); 161 | await el.getText(); 162 | await el.getSize(); 163 | 164 | let log = logger.getLog(); 165 | 166 | let expectedLog = [ 167 | `22:05:34.000 | 0ms | ${shortSession} | FindElement\n`, 168 | ` Using css selector '.test'\n`, 169 | ` Elements: 0\n`, 170 | `22:05:34.000 | 0ms | ${shortSession} | GetElementCSSValue (0)\n`, 171 | ` white\n`, 172 | `22:05:34.000 | 0ms | ${shortSession} | GetElementAttribute (0)\n`, 173 | ` null\n`, 174 | `22:05:34.000 | 0ms | ${shortSession} | GetElementTagName (0)\n`, 175 | ` button\n`, 176 | `22:05:34.000 | 0ms | ${shortSession} | GetElementText (0)\n`, 177 | ` some text\n`, 178 | `22:05:34.000 | 0ms | ${shortSession} | GetElementRect (0)\n`, 179 | ` {"width":88,"hCode":88,"class":"org.openqa.selenium.Dimension","height":20}\n`, 180 | ]; 181 | for (let line in expectedLog) { 182 | expect(log[line]).toEqual(expectedLog[line], `Expected line: ${line} to match`); 183 | } 184 | }); 185 | 186 | it('logs response errors', () => { 187 | let cmd = parseWebDriverCommand('/session/abcdef/url', 'GET'); 188 | 189 | logger.logWebDriverCommand(cmd); 190 | cmd.handleResponse(200, {'status': 100, 'value': {'message': 'Fake Selenium Error'}}); 191 | 192 | let log = logger.getLog(); 193 | expect(log[4]).toContain('ERROR 100: Fake Selenium Error'); 194 | }); 195 | 196 | it('shows how long commands take', async () => { 197 | let cmd = parseWebDriverCommand('/session/abcdef/url', 'GET'); 198 | logger.logWebDriverCommand(cmd); 199 | 200 | let delay = new Promise((res) => setTimeout(() => { 201 | cmd.handleResponse(200, {}); 202 | res(); 203 | }, 1234)); 204 | 205 | jasmine.clock().tick(2000); 206 | await delay; 207 | 208 | let log = logger.getLog(); 209 | expect(log[3]).toContain('22:05:34.000 | 1234ms | abcdef | GetCurrentURL'); 210 | }); 211 | 212 | it('handles unknown commands', async () => { 213 | let session = await driver.getSession(); 214 | let shortSession = session.getId().slice(0, 6); 215 | 216 | let cmd = parseWebDriverCommand('/session/abcdef/not_a_command', 'GET'); 217 | logger.logWebDriverCommand(cmd); 218 | cmd.handleResponse(200, {}); 219 | 220 | let log = logger.getLog(); 221 | let expectedLog = [ 222 | `22:05:34.000 | 0ms | ${shortSession} | NewSession\n`, ` {"browserName":"chrome"}\n`, 223 | `22:05:34.000 | 0ms | ${shortSession} | Go http://example.com\n`, 224 | `22:05:34.000 | 0ms | /session/abcdef/not_a_command\n` 225 | ]; 226 | for (let line in expectedLog) { 227 | expect(log[line]).toEqual(expectedLog[line], `Expected line: ${line} to match`); 228 | } 229 | }); 230 | 231 | it('can log events', () => { 232 | logger.logEvent('test message', 'abcdef-hijkl', 50); 233 | 234 | let log = logger.getLog(); 235 | let expectedLog = `22:05:34.000 | 50ms | abcdef | test message\n`; 236 | expect(log[3]).toEqual(expectedLog); 237 | }); 238 | }); 239 | -------------------------------------------------------------------------------- /spec/unit/webdriver_proxy_spec.ts: -------------------------------------------------------------------------------- 1 | import * as nock from 'nock'; 2 | 3 | import {WebDriverCommand} from '../../lib/webdriver_commands'; 4 | import {WebDriverProxy} from '../../lib/webdriver_proxy'; 5 | 6 | import {InMemoryReader, InMemoryWriter, TestBarrier} from './util'; 7 | 8 | describe('WebDriver Proxy', () => { 9 | let proxy: WebDriverProxy; 10 | 11 | beforeEach(() => { 12 | proxy = new WebDriverProxy('http://test_webdriver_url/wd/hub'); 13 | }); 14 | 15 | it('proxies to WebDriver', (done) => { 16 | let req = new InMemoryReader() as any; 17 | let resp = new InMemoryWriter() as any; 18 | resp.writeHead = jasmine.createSpy('spy'); 19 | req.url = '/session/sessionId/get'; 20 | req.method = 'GET'; 21 | req.headers = {host: 'proxyHost'}; 22 | const responseData = {value: 'selenium response'}; 23 | 24 | let scope = nock(proxy.seleniumAddress).get('/session/sessionId/get').reply(function() { 25 | expect(this.req.headers.host).not.toEqual('proxyHost'); 26 | return [200, responseData]; 27 | }); 28 | 29 | proxy.handleRequest(req, resp); 30 | 31 | resp.onEnd((data) => { 32 | // Verify that all nock endpoints were called. 33 | expect(resp.writeHead.calls.first().args[0]).toBe(200); 34 | expect(data).toEqual(JSON.stringify(responseData)); 35 | scope.done(); 36 | done(); 37 | }); 38 | }); 39 | 40 | it('waits for barriers', (done) => { 41 | const WD_URL = '/session/sessionId/url'; 42 | 43 | let req = new InMemoryReader() as any; 44 | let resp = new InMemoryWriter() as any; 45 | resp.writeHead = jasmine.createSpy('spy'); 46 | req.url = WD_URL; 47 | req.method = 'POST'; 48 | 49 | let barrier = new TestBarrier(); 50 | let barrierDone = false; 51 | barrier.onCommand = (): Promise => { 52 | return new Promise((res) => { 53 | setTimeout(() => { 54 | barrierDone = true; 55 | res(); 56 | }, 250); 57 | }); 58 | }; 59 | 60 | let scope = nock(proxy.seleniumAddress).post(WD_URL).reply(() => { 61 | // Shouldn't see the command until the barrier is done. 62 | expect(barrierDone).toBeTruthy(); 63 | return [200]; 64 | }); 65 | 66 | proxy.addBarrier(barrier); 67 | proxy.handleRequest(req, resp); 68 | 69 | resp.onEnd(() => { 70 | expect(barrierDone).toBeTruthy(); 71 | scope.done(); 72 | done(); 73 | }); 74 | }); 75 | 76 | it('waits for multiple barriers in order', (done) => { 77 | const WD_URL = '/session/sessionId/url'; 78 | 79 | let req = new InMemoryReader() as any; 80 | let resp = new InMemoryWriter() as any; 81 | resp.writeHead = jasmine.createSpy('spy'); 82 | req.url = WD_URL; 83 | req.method = 'POST'; 84 | 85 | let barrier1 = new TestBarrier(); 86 | let barrier1Done = false; 87 | barrier1.onCommand = (): Promise => { 88 | return new Promise((res) => { 89 | setTimeout(() => { 90 | expect(barrier2Done).toBeFalsy(); 91 | barrier1Done = true; 92 | res(); 93 | }, 150); 94 | }); 95 | }; 96 | let barrier2 = new TestBarrier(); 97 | let barrier2Done = false; 98 | barrier2.onCommand = (): Promise => { 99 | return new Promise((res) => { 100 | setTimeout(() => { 101 | expect(barrier1Done).toBeTruthy(); 102 | barrier2Done = true; 103 | res(); 104 | }, 50); 105 | }); 106 | }; 107 | 108 | let scope = nock(proxy.seleniumAddress).post(WD_URL).reply(200); 109 | 110 | proxy.addBarrier(barrier1); 111 | proxy.addBarrier(barrier2); 112 | proxy.handleRequest(req, resp); 113 | 114 | resp.onEnd(() => { 115 | expect(barrier2Done).toBeTruthy(); 116 | scope.done(); 117 | done(); 118 | }); 119 | }); 120 | 121 | it('returns an error if a barrier fails', (done) => { 122 | const WD_URL = '/session/sessionId/url'; 123 | 124 | let req = new InMemoryReader() as any; 125 | let resp = new InMemoryWriter() as any; 126 | resp.writeHead = jasmine.createSpy('spy'); 127 | req.url = WD_URL; 128 | req.method = 'GET'; 129 | 130 | let barrier = new TestBarrier(); 131 | barrier.onCommand = (): Promise => { 132 | return new Promise((res, rej) => { 133 | rej('Barrier failed'); 134 | }); 135 | }; 136 | 137 | let scope = nock(proxy.seleniumAddress).get(WD_URL).reply(200); 138 | 139 | proxy.addBarrier(barrier); 140 | proxy.handleRequest(req, resp); 141 | 142 | resp.onEnd((respData) => { 143 | expect(resp.writeHead.calls.first().args[0]).toBe(502); 144 | expect(respData).toEqual('Barrier failed'); 145 | 146 | // Should not call the selenium server. 147 | expect(scope.isDone()).toBeFalsy(); 148 | nock.cleanAll(); 149 | done(); 150 | }); 151 | }); 152 | 153 | it('barriers get selenium responses', (done) => { 154 | const WD_URL = '/session/sessionId/url'; 155 | const RESPONSE = {url: 'http://example.com'}; 156 | 157 | let req = new InMemoryReader() as any; 158 | let resp = new InMemoryWriter() as any; 159 | resp.writeHead = jasmine.createSpy('spy'); 160 | req.url = WD_URL; 161 | req.method = 'GET'; 162 | 163 | let scope = nock(proxy.seleniumAddress).get(WD_URL).reply(200, RESPONSE); 164 | 165 | let barrier = new TestBarrier(); 166 | barrier.onCommand = (command: WebDriverCommand): Promise => { 167 | command.on('response', () => { 168 | expect(command.responseData['url']).toEqual(RESPONSE.url); 169 | scope.done(); 170 | done(); 171 | }); 172 | return undefined; 173 | }; 174 | proxy.addBarrier(barrier); 175 | proxy.handleRequest(req, resp); 176 | }); 177 | 178 | it('propagates http errors', (done) => { 179 | const WD_URL = '/session/'; 180 | const ERR = new Error('HTTP error'); 181 | 182 | let req = new InMemoryReader() as any; 183 | let resp = new InMemoryWriter() as any; 184 | resp.writeHead = jasmine.createSpy('spy'); 185 | req.url = WD_URL; 186 | req.method = 'POST'; 187 | 188 | let scope = nock(proxy.seleniumAddress).post(WD_URL).replyWithError(ERR); 189 | 190 | proxy.handleRequest(req, resp); 191 | 192 | resp.onEnd((data) => { 193 | expect(resp.writeHead.calls.first().args[0]).toBe(502); 194 | expect(data).toEqual(ERR.toString()); 195 | scope.done(); 196 | done(); 197 | }); 198 | }); 199 | }); 200 | -------------------------------------------------------------------------------- /testapp/app.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: Helvetica, Arial, san-sarif; 3 | font-size: 2em; 4 | } 5 | 6 | h1 { 7 | text-align: center; 8 | } 9 | 10 | ol { 11 | position: relative; 12 | padding: 0; 13 | } 14 | 15 | li { 16 | width: 25%; 17 | padding: 4%; 18 | display: block; 19 | text-align: center; 20 | position: absolute; 21 | top: 0; 22 | margin: auto; 23 | } 24 | 25 | li.left, li.mid { 26 | left: 0; 27 | } 28 | 29 | li.right, li.mid { 30 | right: 0; 31 | } 32 | -------------------------------------------------------------------------------- /testapp/hybrid/app/main.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var myApp_1 = require('./myApp'); 3 | var ng2_1 = require('./ng2'); 4 | var ng1_1 = require('./ng1'); 5 | var upgrader_1 = require('./upgrader'); 6 | var ng1module = angular.module('hybridApp', []); 7 | ng1module.directive('myApp', myApp_1.myApp); 8 | ng1module.directive('ng2', upgrader_1.adapter.downgradeNg2Component(ng2_1.Ng2Component)); 9 | ng1module.directive('ng1', ng1_1.ng1Dir); 10 | upgrader_1.adapter.bootstrap(document.body, ['hybridApp']); 11 | //# sourceMappingURL=main.js.map -------------------------------------------------------------------------------- /testapp/hybrid/app/main.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"main.js","sourceRoot":"","sources":["main.ts"],"names":[],"mappings":";AAAA,sBAAoB,SACpB,CAAC,CAD4B;AAC7B,oBAA2B,OAC3B,CAAC,CADiC;AAClC,oBAAqB,OACrB,CAAC,CAD2B;AAC5B,yBAAsB,YAAY,CAAC,CAAA;AAInC,IAAI,SAAS,GAAG,OAAO,CAAC,MAAM,CAAC,WAAW,EAAE,EAAE,CAAC,CAAC;AAEhD,SAAS,CAAC,SAAS,CAAC,OAAO,EAAE,aAAK,CAAC,CAAC;AACpC,SAAS,CAAC,SAAS,CAAC,KAAK,EAAE,kBAAO,CAAC,qBAAqB,CAAC,kBAAY,CAAC,CAAC,CAAC;AACxE,SAAS,CAAC,SAAS,CAAC,KAAK,EAAE,YAAM,CAAC,CAAC;AAEnC,kBAAO,CAAC,SAAS,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC,WAAW,CAAC,CAAC,CAAC"} -------------------------------------------------------------------------------- /testapp/hybrid/app/main.ts: -------------------------------------------------------------------------------- 1 | import {myApp} from './myApp' 2 | import {Ng2Component} from './ng2' 3 | import {ng1Dir} from './ng1' 4 | import {adapter} from './upgrader'; 5 | 6 | declare var angular; 7 | 8 | var ng1module = angular.module('hybridApp', []); 9 | 10 | ng1module.directive('myApp', myApp); 11 | ng1module.directive('ng2', adapter.downgradeNg2Component(Ng2Component)); 12 | ng1module.directive('ng1', ng1Dir); 13 | 14 | adapter.bootstrap(document.body, ['hybridApp']); 15 | -------------------------------------------------------------------------------- /testapp/hybrid/app/myApp.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | function ctrl($scope, $timeout) { 3 | $scope.callCount = 0; 4 | $scope.clickButton = function () { 5 | $timeout(function () { 6 | $scope.callCount++; 7 | }, 1000); 8 | }; 9 | } 10 | function myApp() { 11 | return { 12 | scope: {}, 13 | templateUrl: './html/myApp.html', 14 | controller: ctrl, 15 | controllerAs: 'ctrl' 16 | }; 17 | } 18 | exports.myApp = myApp; 19 | //# sourceMappingURL=myApp.js.map -------------------------------------------------------------------------------- /testapp/hybrid/app/myApp.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"myApp.js","sourceRoot":"","sources":["myApp.ts"],"names":[],"mappings":";AAAA,cAAc,MAAW,EAAE,QAAa;IACtC,MAAM,CAAC,SAAS,GAAG,CAAC,CAAC;IAErB,MAAM,CAAC,WAAW,GAAG;QACnB,QAAQ,CAAC;YACP,MAAM,CAAC,SAAS,EAAE,CAAC;QACrB,CAAC,EAAE,IAAI,CAAC,CAAC;IACX,CAAC,CAAC;AACJ,CAAC;AAED;IACE,MAAM,CAAC;QACL,KAAK,EAAE,EAAE;QACT,WAAW,EAAE,mBAAmB;QAChC,UAAU,EAAE,IAAI;QAChB,YAAY,EAAE,MAAM;KACrB,CAAC;AACJ,CAAC;AAPe,aAAK,QAOpB,CAAA"} -------------------------------------------------------------------------------- /testapp/hybrid/app/myApp.ts: -------------------------------------------------------------------------------- 1 | function ctrl($scope: any, $timeout: any) { 2 | $scope.callCount = 0; 3 | 4 | $scope.clickButton = function() { 5 | $timeout(() => { 6 | $scope.callCount++; 7 | }, 1000); 8 | }; 9 | } 10 | 11 | export function myApp() { 12 | return { 13 | scope: {}, 14 | templateUrl: './html/myApp.html', 15 | controller: ctrl, 16 | controllerAs: 'ctrl' 17 | }; 18 | } 19 | -------------------------------------------------------------------------------- /testapp/hybrid/app/ng1.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | function ctrl($scope, $timeout) { 3 | $scope.callCount = 0; 4 | $scope.clickButton = function () { 5 | $timeout(function () { 6 | $scope.callCount++; 7 | }, 1000); 8 | }; 9 | } 10 | function ng1Dir() { 11 | return { 12 | scope: {}, 13 | templateUrl: './html/ng1.html', 14 | controller: ctrl, 15 | controllerAs: 'ctrl' 16 | }; 17 | } 18 | exports.ng1Dir = ng1Dir; 19 | //# sourceMappingURL=ng1.js.map -------------------------------------------------------------------------------- /testapp/hybrid/app/ng1.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"ng1.js","sourceRoot":"","sources":["ng1.ts"],"names":[],"mappings":";AAAA,cAAc,MAAW,EAAE,QAAa;IACtC,MAAM,CAAC,SAAS,GAAG,CAAC,CAAC;IAErB,MAAM,CAAC,WAAW,GAAG;QACnB,QAAQ,CAAC;YACP,MAAM,CAAC,SAAS,EAAE,CAAC;QACrB,CAAC,EAAE,IAAI,CAAC,CAAC;IACX,CAAC,CAAC;AACJ,CAAC;AAED;IACE,MAAM,CAAC;QACL,KAAK,EAAE,EAAE;QACT,WAAW,EAAE,iBAAiB;QAC9B,UAAU,EAAE,IAAI;QAChB,YAAY,EAAE,MAAM;KACrB,CAAC;AACJ,CAAC;AAPe,cAAM,SAOrB,CAAA"} -------------------------------------------------------------------------------- /testapp/hybrid/app/ng1.ts: -------------------------------------------------------------------------------- 1 | function ctrl($scope: any, $timeout: any) { 2 | $scope.callCount = 0; 3 | 4 | $scope.clickButton = function() { 5 | $timeout(() => { 6 | $scope.callCount++; 7 | }, 1000); 8 | }; 9 | } 10 | 11 | export function ng1Dir() { 12 | return { 13 | scope: {}, 14 | templateUrl: './html/ng1.html', 15 | controller: ctrl, 16 | controllerAs: 'ctrl' 17 | }; 18 | } 19 | -------------------------------------------------------------------------------- /testapp/hybrid/app/ng2.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { 3 | var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; 4 | if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); 5 | else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; 6 | return c > 3 && r && Object.defineProperty(target, key, r), r; 7 | }; 8 | var __metadata = (this && this.__metadata) || function (k, v) { 9 | if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v); 10 | }; 11 | var core_1 = require('@angular/core'); 12 | var Ng2Component = (function () { 13 | function Ng2Component() { 14 | var _this = this; 15 | this.callCount = 0; 16 | this.clickButton = function () { 17 | setTimeout(function () { 18 | _this.callCount++; 19 | }, 1000); 20 | }; 21 | } 22 | Ng2Component = __decorate([ 23 | core_1.Component({ 24 | selector: 'ng2', 25 | templateUrl: './html/ng2.html' 26 | }), 27 | __metadata('design:paramtypes', []) 28 | ], Ng2Component); 29 | return Ng2Component; 30 | }()); 31 | exports.Ng2Component = Ng2Component; 32 | //# sourceMappingURL=ng2.js.map -------------------------------------------------------------------------------- /testapp/hybrid/app/ng2.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"ng2.js","sourceRoot":"","sources":["ng2.ts"],"names":[],"mappings":";;;;;;;;;;AAAA,qBAAwB,eAAe,CAAC,CAAA;AAOxC;IAAA;QAAA,iBAOC;QANC,cAAS,GAAW,CAAC,CAAC;QACtB,gBAAW,GAAG;YACZ,UAAU,CAAC;gBACT,KAAI,CAAC,SAAS,EAAE,CAAC;YACnB,CAAC,EAAE,IAAI,CAAC,CAAC;QACX,CAAC,CAAC;IACJ,CAAC;IAXD;QAAC,gBAAS,CAAC;YACT,QAAQ,EAAE,KAAK;YACf,WAAW,EAAE,iBAAiB;SAC/B,CAAC;;oBAAA;IAQF,mBAAC;AAAD,CAAC,AAPD,IAOC;AAPY,oBAAY,eAOxB,CAAA"} -------------------------------------------------------------------------------- /testapp/hybrid/app/ng2.ts: -------------------------------------------------------------------------------- 1 | import {Component} from '@angular/core'; 2 | import {adapter} from './upgrader'; 3 | 4 | @Component({ 5 | selector: 'ng2', 6 | templateUrl: './html/ng2.html' 7 | }) 8 | export class Ng2Component { 9 | callCount: number = 0; 10 | clickButton = () => { 11 | setTimeout(() => { 12 | this.callCount++; 13 | }, 1000); 14 | }; 15 | } 16 | -------------------------------------------------------------------------------- /testapp/hybrid/app/upgrader.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { 3 | var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; 4 | if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); 5 | else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; 6 | return c > 3 && r && Object.defineProperty(target, key, r), r; 7 | }; 8 | var __metadata = (this && this.__metadata) || function (k, v) { 9 | if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v); 10 | }; 11 | var upgrade_1 = require('@angular/upgrade'); 12 | var platform_browser_1 = require('@angular/platform-browser'); 13 | var core_1 = require('@angular/core'); 14 | var ng2_1 = require('./ng2'); 15 | exports.adapter = new upgrade_1.UpgradeAdapter(core_1.forwardRef(function () { return Ng2Module; })); 16 | var Ng2Module = (function () { 17 | function Ng2Module() { 18 | } 19 | Ng2Module = __decorate([ 20 | core_1.NgModule({ 21 | imports: [ 22 | platform_browser_1.BrowserModule 23 | ], 24 | declarations: [ 25 | ng2_1.Ng2Component, exports.adapter.upgradeNg1Component('ng1') 26 | ], 27 | }), 28 | __metadata('design:paramtypes', []) 29 | ], Ng2Module); 30 | return Ng2Module; 31 | }()); 32 | exports.Ng2Module = Ng2Module; 33 | //# sourceMappingURL=upgrader.js.map -------------------------------------------------------------------------------- /testapp/hybrid/app/upgrader.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"upgrader.js","sourceRoot":"","sources":["upgrader.ts"],"names":[],"mappings":";;;;;;;;;;AAAA,wBAA+B,kBAAkB,CAAC,CAAA;AAClD,iCAA8B,2BAA2B,CAAC,CAAA;AAC1D,qBAAqC,eAAe,CAAC,CAAA;AACrD,oBAA6B,OAG7B,CAAC,CAHmC;AAGvB,eAAO,GAAmB,IAAI,wBAAc,CAAC,iBAAU,CAAC,cAAM,OAAA,SAAS,EAAT,CAAS,CAAC,CAAC,CAAC;AAWvF;IAAA;IAAwB,CAAC;IATzB;QAAC,eAAQ,CAAC;YACR,OAAO,EAAE;gBACP,gCAAa;aACd;YACD,YAAY,EAAE;gBACZ,kBAAY,EAAE,eAAO,CAAC,mBAAmB,CAAC,KAAK,CAAC;aACjD;SAEF,CAAC;;iBAAA;IACsB,gBAAC;AAAD,CAAC,AAAzB,IAAyB;AAAZ,iBAAS,YAAG,CAAA"} -------------------------------------------------------------------------------- /testapp/hybrid/app/upgrader.ts: -------------------------------------------------------------------------------- 1 | import { UpgradeAdapter } from '@angular/upgrade'; 2 | import { BrowserModule } from '@angular/platform-browser'; 3 | import { forwardRef, NgModule } from '@angular/core'; 4 | import { Ng2Component } from './ng2' 5 | 6 | 7 | export const adapter: UpgradeAdapter = new UpgradeAdapter(forwardRef(() => Ng2Module)); 8 | 9 | @NgModule({ 10 | imports: [ 11 | BrowserModule 12 | ], 13 | declarations: [ 14 | Ng2Component, adapter.upgradeNg1Component('ng1') 15 | ], 16 | 17 | }) 18 | export class Ng2Module {} 19 | -------------------------------------------------------------------------------- /testapp/hybrid/html/myApp.html: -------------------------------------------------------------------------------- 1 |

My App

2 | 3 | 4 | -------------------------------------------------------------------------------- /testapp/hybrid/html/ng1.html: -------------------------------------------------------------------------------- 1 |

ng1

2 | 3 | -------------------------------------------------------------------------------- /testapp/hybrid/html/ng2.html: -------------------------------------------------------------------------------- 1 |

ng2

2 | 3 | 4 | -------------------------------------------------------------------------------- /testapp/hybrid/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Test ngUpgrade App 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 30 | 31 | 32 | 33 | Loading... 34 | 35 | 36 | 37 | 38 | 39 | 44 | -------------------------------------------------------------------------------- /testapp/hybrid/styles.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family:"Roboto","Helvetica Neue Light","Helvetica Neue",Helvetica,Arial,"Lucida Grande",sans-serif; 3 | font-size:14px;color:#1a2326 4 | } 5 | .container { 6 | padding-left: 20px; 7 | } 8 | .nav { 9 | padding-bottom: 20px; 10 | } 11 | -------------------------------------------------------------------------------- /testapp/hybrid/systemjs.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * System configuration for Angular 2 samples 3 | * Adjust as necessary for your application needs. 4 | */ 5 | (function (global) { 6 | System.config({ 7 | paths: { 8 | // paths serve as alias 9 | 'npm:': '/node_modules/' 10 | }, 11 | // map tells the System loader where to look for things 12 | map: { 13 | // our app is within the app folder 14 | app: 'app', 15 | 16 | // angular bundles 17 | '@angular/core': 'npm:@angular/core/bundles/core.umd.js', 18 | '@angular/common': 'npm:@angular/common/bundles/common.umd.js', 19 | '@angular/compiler': 'npm:@angular/compiler/bundles/compiler.umd.js', 20 | '@angular/platform-browser': 'npm:@angular/platform-browser/bundles/platform-browser.umd.js', 21 | '@angular/platform-browser-dynamic': 'npm:@angular/platform-browser-dynamic/bundles/platform-browser-dynamic.umd.js', 22 | '@angular/http': 'npm:@angular/http/bundles/http.umd.js', 23 | '@angular/router': 'npm:@angular/router/bundles/router.umd.js', 24 | '@angular/forms': 'npm:@angular/forms/bundles/forms.umd.js', 25 | '@angular/upgrade': 'npm:@angular/upgrade/bundles/upgrade.umd.js', 26 | 27 | // other libraries 28 | 'rxjs': 'npm:rxjs', 29 | }, 30 | // packages tells the System loader how to load when no filename and/or no extension 31 | packages: { 32 | app: { 33 | main: './main.js', 34 | defaultExtension: 'js' 35 | }, 36 | rxjs: { 37 | defaultExtension: 'js' 38 | }, 39 | 'angular2-in-memory-web-api': { 40 | main: './index.js', 41 | defaultExtension: 'js' 42 | } 43 | } 44 | }); 45 | })(this); 46 | -------------------------------------------------------------------------------- /testapp/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | My AngularJS App 6 | 7 | 8 | 9 |

Choose Version

10 |
    11 |
  1. 12 | Angular 1 13 |
  2. 14 |
  3. 15 | Hybrid 16 |
  4. 17 |
  5. 18 | Angular 2 19 |
  6. 20 |
21 | 22 | 23 | -------------------------------------------------------------------------------- /testapp/ng1/app.css: -------------------------------------------------------------------------------- 1 | /* app css stylesheet */ 2 | 3 | .menu { 4 | list-style: none; 5 | border-bottom: 0.1em solid black; 6 | margin-bottom: 2em; 7 | padding: 0 0 0.5em; 8 | } 9 | 10 | .menu:before { 11 | content: "["; 12 | } 13 | 14 | .menu:after { 15 | content: "]"; 16 | } 17 | 18 | .menu > li { 19 | display: inline; 20 | } 21 | 22 | .menu > li:before { 23 | content: "|"; 24 | padding-right: 0.3em; 25 | } 26 | 27 | .menu > li:nth-child(1):before { 28 | content: ""; 29 | padding: 0; 30 | } 31 | 32 | .ng-scope { 33 | background-color: rgba(0,0,0,.05); 34 | } 35 | 36 | .ng-binding { 37 | border: 1px solid rgba(50, 200, 50, .8); 38 | } 39 | 40 | #chat-box { 41 | width: 300px; 42 | height: 200px; 43 | padding: 25px; 44 | border: 2px solid; 45 | margin: 25px; 46 | overflow: scroll; 47 | } 48 | -------------------------------------------------------------------------------- /testapp/ng1/app.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | // Declare app level module which depends on filters, and services 5 | angular.module('myApp', ['ngRoute', 'myApp.appVersion']). 6 | config(['$routeProvider', function($routeProvider) { 7 | $routeProvider.when('/async', {templateUrl: 'async/async.html', controller: AsyncCtrl}); 8 | $routeProvider.when('/polling', {templateUrl: 'polling/polling.html', controller: PollingCtrl}); 9 | $routeProvider.when('/interaction', 10 | {templateUrl: 'interaction/interaction.html', controller: InteractionCtrl}); 11 | $routeProvider.when('/slowloader', { 12 | templateUrl: 'polling/polling.html', 13 | controller: PollingCtrl, 14 | resolve: { 15 | slow: function($timeout) { 16 | return $timeout(function() {}, 5000); 17 | } 18 | } 19 | }); 20 | $routeProvider.otherwise({redirectTo: '/async'}); 21 | }]); 22 | -------------------------------------------------------------------------------- /testapp/ng1/async/async.html: -------------------------------------------------------------------------------- 1 | Slow things that can happen: 2 |
    3 |
  • 4 | 5 | 6 |
  • 7 |
  • 8 | 9 | 10 |
  • 11 |
  • 12 | 13 | 14 |
  • 15 |
  • 16 | 17 | 18 |
  • 19 |
  • 20 | 21 | 22 |
  • 23 |
  • 24 | 25 | 26 |
  • 27 |
  • 28 | 29 | 30 |
  • 31 |
  • 32 | 33 |
    34 |
  • 35 |
36 | -------------------------------------------------------------------------------- /testapp/ng1/async/async.js: -------------------------------------------------------------------------------- 1 | function AsyncCtrl($scope, $http, $timeout, $location) { 2 | $scope.slowHttpStatus = 'not started'; 3 | $scope.slowFunctionStatus = 'not started'; 4 | $scope.slowTimeoutStatus = 'not started'; 5 | $scope.slowAngularTimeoutStatus = 'not started'; 6 | $scope.slowAngularTimeoutPromiseStatus = 'not started'; 7 | $scope.slowHttpPromiseStatus = 'not started'; 8 | $scope.routingChangeStatus = 'not started'; 9 | $scope.templateUrl = 'fastTemplateUrl'; 10 | 11 | $scope.slowHttp = function() { 12 | $scope.slowHttpStatus = 'pending...'; 13 | $http({method: 'GET', url: 'slowcall'}).success(function() { 14 | $scope.slowHttpStatus = 'done'; 15 | }); 16 | }; 17 | 18 | $scope.slowFunction = function() { 19 | $scope.slowFunctionStatus = 'pending...'; 20 | for (var i = 0, t = 0; i < 500000000; ++i) { 21 | t++; 22 | } 23 | $scope.slowFunctionStatus = 'done'; 24 | }; 25 | 26 | $scope.slowTimeout = function() { 27 | $scope.slowTimeoutStatus = 'pending...'; 28 | window.setTimeout(function() { 29 | $scope.$apply(function() { 30 | $scope.slowTimeoutStatus = 'done'; 31 | }); 32 | }, 3000); 33 | }; 34 | 35 | $scope.slowAngularTimeout = function() { 36 | $scope.slowAngularTimeoutStatus = 'pending...'; 37 | $timeout(function() { 38 | $scope.slowAngularTimeoutStatus = 'done'; 39 | }, 3000); 40 | }; 41 | 42 | $scope.slowAngularTimeoutPromise = function() { 43 | $scope.slowAngularTimeoutPromiseStatus = 'pending...'; 44 | $timeout(function() { 45 | // intentionally empty 46 | }, 3000).then(function() { 47 | $scope.slowAngularTimeoutPromiseStatus = 'done'; 48 | }); 49 | }; 50 | 51 | $scope.slowHttpPromise = function() { 52 | $scope.slowHttpPromiseStatus = 'pending...'; 53 | $http({method: 'GET', url: 'slowcall'}).success(function() { 54 | // intentionally empty 55 | }).then(function() { 56 | $scope.slowHttpPromiseStatus = 'done'; 57 | }); 58 | }; 59 | 60 | $scope.routingChange = function() { 61 | $scope.routingChangeStatus = 'pending...'; 62 | $location.url('slowloader'); 63 | }; 64 | 65 | $scope.changeTemplateUrl = function() { 66 | $scope.templateUrl = 'slowTemplateUrl'; 67 | }; 68 | } 69 | 70 | AsyncCtrl.$inject = ['$scope', '$http', '$timeout', '$location']; 71 | -------------------------------------------------------------------------------- /testapp/ng1/components/app-version.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular.module('myApp.appVersion', []). 4 | value('version', '0.1'). 5 | directive('appVersion', ['version', function(version) { 6 | return function(scope, elm, attrs) { 7 | elm.text(version); 8 | }; 9 | }]); 10 | -------------------------------------------------------------------------------- /testapp/ng1/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular/blocking-proxy/481bbf78a21dc303eda0bec115fcfff9ac82310d/testapp/ng1/favicon.ico -------------------------------------------------------------------------------- /testapp/ng1/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | My AngularJS App 6 | 7 | 8 | 9 | 14 | 15 |
16 | 17 |
Angular seed app: v
18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /testapp/ng1/interaction/interaction.html: -------------------------------------------------------------------------------- 1 |

Different things we can interact with

2 | 3 |
4 | Example text 5 |
6 | 7 |
8 |

Buttons

9 | 10 | 11 |
12 | 13 |
14 |

Input boxes

15 | 16 | 17 | 18 | 19 |
20 | 21 |
22 |

Drag and drop

23 | 24 |
{{points}}
25 |
26 | 27 |
28 |

Alert trigger

29 | 30 |
31 | -------------------------------------------------------------------------------- /testapp/ng1/interaction/interaction.js: -------------------------------------------------------------------------------- 1 | function InteractionCtrl($scope, $window) { 2 | $scope.points = 1; 3 | $scope.fluxCapacitor = "off"; 4 | 5 | $scope.toggleFlux = function() { 6 | if ($scope.fluxCapacitor == "off") { 7 | $scope.fluxCapacitor = "fluxing"; 8 | } else { 9 | $scope.fluxCapacitor = "off"; 10 | } 11 | }; 12 | 13 | $scope.doAlert = function() { 14 | $window.alert('Hello'); 15 | }; 16 | } 17 | 18 | -------------------------------------------------------------------------------- /testapp/ng1/lib/angular_v1.5.0/angular-animate.min.js: -------------------------------------------------------------------------------- 1 | /* 2 | AngularJS v1.5.0 3 | (c) 2010-2016 Google, Inc. http://angularjs.org 4 | License: MIT 5 | */ 6 | (function(D,r,Va){'use strict';function ya(a,b,c){if(!a)throw Ka("areq",b||"?",c||"required");return a}function za(a,b){if(!a&&!b)return"";if(!a)return b;if(!b)return a;ba(a)&&(a=a.join(" "));ba(b)&&(b=b.join(" "));return a+" "+b}function La(a){var b={};a&&(a.to||a.from)&&(b.to=a.to,b.from=a.from);return b}function X(a,b,c){var d="";a=ba(a)?a:a&&R(a)&&a.length?a.split(/\s+/):[];s(a,function(a,g){a&&0=a&&(a=e,e=0,b.push(t),t=[]);t.push(g.fn);g.children.forEach(function(a){e++; 28 | c.push(a)});a--}t.length&&b.push(t);return b}(c)}var M=[],r=U(a);return function(u,A,v){function z(a){a=a.hasAttribute("ng-animate-ref")?[a]:a.querySelectorAll("[ng-animate-ref]");var b=[];s(a,function(a){var c=a.getAttribute("ng-animate-ref");c&&c.length&&b.push(a)});return b}function K(a){var b=[],c={};s(a,function(a,f){var d=G(a.element),h=0<=["enter","move"].indexOf(a.event),d=a.structural?z(d):[];if(d.length){var e=h?"to":"from";s(d,function(a){var b=a.getAttribute("ng-animate-ref");c[b]=c[b]|| 29 | {};c[b][e]={animationID:f,element:I(a)}})}else b.push(a)});var d={},h={};s(c,function(c,e){var l=c.from,t=c.to;if(l&&t){var g=a[l.animationID],E=a[t.animationID],k=l.animationID.toString();if(!h[k]){var z=h[k]={structural:!0,beforeStart:function(){g.beforeStart();E.beforeStart()},close:function(){g.close();E.close()},classes:J(g.classes,E.classes),from:g,to:E,anchors:[]};z.classes.length?b.push(z):(b.push(g),b.push(E))}h[k].anchors.push({out:l.element,"in":t.element})}else l=l?l.animationID:t.animationID, 30 | t=l.toString(),d[t]||(d[t]=!0,b.push(a[l]))});return b}function J(a,b){a=a.split(" ");b=b.split(" ");for(var c=[],d=0;d=P&&b>=O&&(wa=!0,q())}function L(){function b(){if(!A){t(!1);s(m, 37 | function(a){l.style[a[0]]=a[1]});z(a,f);e.addClass(a,ca);if(p.recalculateTimingStyles){ja=l.className+" "+da;ga=r(l,ja);F=v(l,ja,ga);$=F.maxDelay;n=Math.max($,0);O=F.maxDuration;if(0===O){q();return}p.hasTransitions=0B.expectedEndTime)?H.cancel(B.timer):g.push(q)}L&&(k=H(c,k,!1),g[0]={timer:k,expectedEndTime:d},g.push(q),a.data("$$animateCss",g));if(ea.length)a.on(ea.join(" "),E);f.to&&(f.cleanupStyles&&Ga(x,l,Object.keys(f.to)),Ba(a, 39 | f))}}function c(){var b=a.data("$$animateCss");if(b){for(var d=1;d 2 | 3 | Test Application Login 4 | 18 | 19 | 20 |
Login
21 |
22 |
23 |
24 | 25 | 26 | -------------------------------------------------------------------------------- /testapp/ng1/polling/polling.html: -------------------------------------------------------------------------------- 1 |
This view shows a controller which uses a polling mechanism to 2 | contact the server. It is constantly using angular's $timeout.
3 | 4 |
{{count}}
5 | -------------------------------------------------------------------------------- /testapp/ng1/polling/polling.js: -------------------------------------------------------------------------------- 1 | function PollingCtrl($scope, $timeout) { 2 | $scope.count = 0; 3 | 4 | $scope.startPolling = function() { 5 | function poll() { 6 | $timeout(function() { 7 | $scope.count++; 8 | poll(); 9 | }, 1000); 10 | }; 11 | 12 | poll(); 13 | }; 14 | } 15 | PollingCtrl.$inject = ['$scope', '$timeout']; 16 | -------------------------------------------------------------------------------- /testapp/ng1/unstable/unstable.html: -------------------------------------------------------------------------------- 1 |
This view shows a controller which uses a polling mechanism to 2 | contact the server. It is constantly using angular's $timeout.
3 |
{{slowHttpPromiseStatus}}
4 |
{{routeChangeStatus}}
5 | -------------------------------------------------------------------------------- /testapp/ng1/unstable/unstable.js: -------------------------------------------------------------------------------- 1 | function UnstableCtrl($scope, $rootScope, $timeout, $http) { 2 | $scope.count = 0; 3 | $scope.slowHttpPromiseStatus = 'fast http pending'; 4 | $scope.routeChangeStatus = 'route change pending'; 5 | $rootScope.$on('$routeChangeSuccess', function() { 6 | $scope.routeChangeStatus = 'route change done'; 7 | }); 8 | 9 | 10 | $http({method: 'GET', url: 'slowcall'}).success(function() { 11 | // intentionally empty 12 | }).then(function() { 13 | $scope.slowHttpPromiseStatus = 'fast http done'; 14 | }); 15 | 16 | } 17 | UnstableCtrl.$inject = ['$scope', '$rootScope', '$timeout', '$http']; 18 | -------------------------------------------------------------------------------- /testapp/ng2/app/app-router.html: -------------------------------------------------------------------------------- 1 |
2 |

Test App for Angular 2

3 | 7 | 8 |
9 | -------------------------------------------------------------------------------- /testapp/ng2/app/app.component.html: -------------------------------------------------------------------------------- 1 |
2 |

Test App for Angular 2

3 | 7 | 8 |
9 | -------------------------------------------------------------------------------- /testapp/ng2/app/app.component.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { 3 | var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; 4 | if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); 5 | else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; 6 | return c > 3 && r && Object.defineProperty(target, key, r), r; 7 | }; 8 | var __metadata = (this && this.__metadata) || function (k, v) { 9 | if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v); 10 | }; 11 | var core_1 = require('@angular/core'); 12 | var AppComponent = (function () { 13 | function AppComponent() { 14 | } 15 | AppComponent = __decorate([ 16 | core_1.Component({ 17 | selector: 'my-app', 18 | templateUrl: 'app/app.component.html' 19 | }), 20 | __metadata('design:paramtypes', []) 21 | ], AppComponent); 22 | return AppComponent; 23 | }()); 24 | exports.AppComponent = AppComponent; 25 | //# sourceMappingURL=app.component.js.map -------------------------------------------------------------------------------- /testapp/ng2/app/app.component.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"app.component.js","sourceRoot":"","sources":["app.component.ts"],"names":[],"mappings":";;;;;;;;;;AAAA,qBAA0B,eAAe,CAAC,CAAA;AAM1C;IAAA;IAEA,CAAC;IAND;QAAC,gBAAS,CAAC;YACT,QAAQ,EAAE,QAAQ;YAClB,WAAW,EAAE,wBAAwB;SACtC,CAAC;;oBAAA;IAGF,mBAAC;AAAD,CAAC,AAFD,IAEC;AAFY,oBAAY,eAExB,CAAA"} -------------------------------------------------------------------------------- /testapp/ng2/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'my-app', 5 | templateUrl: 'app/app.component.html' 6 | }) 7 | export class AppComponent { 8 | 9 | } 10 | -------------------------------------------------------------------------------- /testapp/ng2/app/app.module.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { 3 | var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; 4 | if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); 5 | else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; 6 | return c > 3 && r && Object.defineProperty(target, key, r), r; 7 | }; 8 | var __metadata = (this && this.__metadata) || function (k, v) { 9 | if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v); 10 | }; 11 | var core_1 = require('@angular/core'); 12 | var platform_browser_1 = require('@angular/platform-browser'); 13 | var app_routes_1 = require('./app.routes'); 14 | var app_component_1 = require('./app.component'); 15 | var home_component_1 = require('./home/home.component'); 16 | var async_component_1 = require('./async/async.component'); 17 | var AppModule = (function () { 18 | function AppModule() { 19 | } 20 | AppModule = __decorate([ 21 | core_1.NgModule({ 22 | imports: [ 23 | platform_browser_1.BrowserModule, 24 | app_routes_1.appRouting 25 | ], 26 | declarations: [ 27 | app_component_1.AppComponent, 28 | home_component_1.HomeComponent, 29 | async_component_1.AsyncComponent, 30 | ], 31 | bootstrap: [app_component_1.AppComponent] 32 | }), 33 | __metadata('design:paramtypes', []) 34 | ], AppModule); 35 | return AppModule; 36 | }()); 37 | exports.AppModule = AppModule; 38 | //# sourceMappingURL=app.module.js.map -------------------------------------------------------------------------------- /testapp/ng2/app/app.module.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"app.module.js","sourceRoot":"","sources":["app.module.ts"],"names":[],"mappings":";;;;;;;;;;AAAA,qBAAyB,eAAe,CAAC,CAAA;AACzC,iCAA8B,2BAA2B,CAAC,CAAA;AAC1D,2BAA2B,cAAc,CAAC,CAAA;AAC1C,8BAA6B,iBAAiB,CAAC,CAAA;AAC/C,+BAA8B,uBAAuB,CAAC,CAAA;AACtD,gCAA+B,yBAAyB,CAAC,CAAA;AAczD;IAAA;IAAwB,CAAC;IAZzB;QAAC,eAAQ,CAAC;YACR,OAAO,EAAE;gBACP,gCAAa;gBACb,uBAAU;aACX;YACD,YAAY,EAAE;gBACZ,4BAAY;gBACZ,8BAAa;gBACb,gCAAc;aACf;YACD,SAAS,EAAE,CAAC,4BAAY,CAAC;SAC1B,CAAC;;iBAAA;IACsB,gBAAC;AAAD,CAAC,AAAzB,IAAyB;AAAZ,iBAAS,YAAG,CAAA"} -------------------------------------------------------------------------------- /testapp/ng2/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { BrowserModule } from '@angular/platform-browser'; 3 | import { appRouting } from './app.routes'; 4 | import { AppComponent } from './app.component'; 5 | import { HomeComponent } from './home/home.component'; 6 | import { AsyncComponent } from './async/async.component'; 7 | 8 | @NgModule({ 9 | imports: [ 10 | BrowserModule, 11 | appRouting 12 | ], 13 | declarations: [ 14 | AppComponent, 15 | HomeComponent, 16 | AsyncComponent, 17 | ], 18 | bootstrap: [AppComponent] 19 | }) 20 | export class AppModule {} 21 | -------------------------------------------------------------------------------- /testapp/ng2/app/app.routes.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var router_1 = require('@angular/router'); 3 | var home_component_1 = require('./home/home.component'); 4 | var async_component_1 = require('./async/async.component'); 5 | exports.routes = [ 6 | { path: '', component: home_component_1.HomeComponent }, 7 | { path: 'async', component: async_component_1.AsyncComponent } 8 | ]; 9 | exports.appRouting = router_1.RouterModule.forRoot(exports.routes, { useHash: true }); 10 | //# sourceMappingURL=app.routes.js.map -------------------------------------------------------------------------------- /testapp/ng2/app/app.routes.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"app.routes.js","sourceRoot":"","sources":["app.routes.ts"],"names":[],"mappings":";AAAA,uBAAqC,iBAAiB,CAAC,CAAA;AACvD,+BAA8B,uBAAuB,CAAC,CAAA;AACtD,gCAA+B,yBAAyB,CAAC,CAAA;AAE5C,cAAM,GAAW;IAC5B,EAAE,IAAI,EAAE,EAAE,EAAE,SAAS,EAAE,8BAAa,EAAE;IACtC,EAAE,IAAI,EAAE,OAAO,EAAE,SAAS,EAAE,gCAAc,EAAE;CAC7C,CAAC;AAEW,kBAAU,GAAG,qBAAY,CAAC,OAAO,CAAC,cAAM,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC"} -------------------------------------------------------------------------------- /testapp/ng2/app/app.routes.ts: -------------------------------------------------------------------------------- 1 | import { Routes, RouterModule } from '@angular/router'; 2 | import { HomeComponent } from './home/home.component'; 3 | import { AsyncComponent } from './async/async.component'; 4 | 5 | export const routes: Routes = [ 6 | { path: '', component: HomeComponent }, 7 | { path: 'async', component: AsyncComponent } 8 | ]; 9 | 10 | export const appRouting = RouterModule.forRoot(routes, { useHash: true }); 11 | -------------------------------------------------------------------------------- /testapp/ng2/app/async/async-component.html: -------------------------------------------------------------------------------- 1 |
2 | {{val1}} 3 | 4 |
5 |
6 | {{val2}} 7 | 8 | 10 |
11 |
12 | {{val3}} 13 | 15 | 17 |
18 |
19 | {{val4}} 20 | 22 | 24 |
25 | -------------------------------------------------------------------------------- /testapp/ng2/app/async/async.component.html: -------------------------------------------------------------------------------- 1 |
2 | {{val1}} 3 | 4 |
5 |
6 | {{val2}} 7 | 8 | 10 |
11 |
12 | {{val3}} 13 | 15 | 17 |
18 |
19 | {{val4}} 20 | 22 | 24 |
25 |
26 | {{val5}} 27 | 29 | 31 |
32 | -------------------------------------------------------------------------------- /testapp/ng2/app/async/async.component.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { 3 | var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; 4 | if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); 5 | else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; 6 | return c > 3 && r && Object.defineProperty(target, key, r), r; 7 | }; 8 | var __metadata = (this && this.__metadata) || function (k, v) { 9 | if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v); 10 | }; 11 | var core_1 = require('@angular/core'); 12 | var AsyncComponent = (function () { 13 | function AsyncComponent(_ngZone) { 14 | this._ngZone = _ngZone; 15 | this.val1 = 0; 16 | this.val2 = 0; 17 | this.val3 = 0; 18 | this.val4 = 0; 19 | this.val5 = 0; 20 | this.timeoutId = null; 21 | this.chainedTimeoutId = null; 22 | this.intervalId = null; 23 | this.intervalId_unzoned = null; 24 | } 25 | ; 26 | AsyncComponent.prototype.increment = function () { this.val1++; }; 27 | ; 28 | AsyncComponent.prototype.delayedIncrement = function () { 29 | var _this = this; 30 | this.cancelDelayedIncrement(); 31 | this.timeoutId = setTimeout(function () { 32 | _this.val2++; 33 | _this.timeoutId = null; 34 | }, 2000); 35 | }; 36 | ; 37 | AsyncComponent.prototype.chainedDelayedIncrements = function (i) { 38 | this.cancelChainedDelayedIncrements(); 39 | var self = this; 40 | function helper(_i) { 41 | if (_i <= 0) { 42 | self.chainedTimeoutId = null; 43 | return; 44 | } 45 | self.chainedTimeoutId = setTimeout(function () { 46 | self.val3++; 47 | helper(_i - 1); 48 | }, 500); 49 | } 50 | helper(i); 51 | }; 52 | ; 53 | AsyncComponent.prototype.periodicIncrement = function () { 54 | var _this = this; 55 | this.cancelPeriodicIncrement(); 56 | this.intervalId = setInterval(function () { _this.val4++; }, 2000); 57 | }; 58 | ; 59 | AsyncComponent.prototype.periodicIncrement_unzoned = function () { 60 | var _this = this; 61 | this.cancelPeriodicIncrement_unzoned(); 62 | this._ngZone.runOutsideAngular(function () { 63 | _this.intervalId_unzoned = setInterval(function () { 64 | _this._ngZone.run(function () { 65 | _this.val5++; 66 | }); 67 | }, 2000); 68 | }); 69 | }; 70 | ; 71 | AsyncComponent.prototype.cancelDelayedIncrement = function () { 72 | if (this.timeoutId != null) { 73 | clearTimeout(this.timeoutId); 74 | this.timeoutId = null; 75 | } 76 | }; 77 | ; 78 | AsyncComponent.prototype.cancelChainedDelayedIncrements = function () { 79 | if (this.chainedTimeoutId != null) { 80 | clearTimeout(this.chainedTimeoutId); 81 | this.chainedTimeoutId = null; 82 | } 83 | }; 84 | ; 85 | AsyncComponent.prototype.cancelPeriodicIncrement = function () { 86 | if (this.intervalId != null) { 87 | clearInterval(this.intervalId); 88 | this.intervalId = null; 89 | } 90 | }; 91 | ; 92 | AsyncComponent.prototype.cancelPeriodicIncrement_unzoned = function () { 93 | if (this.intervalId_unzoned != null) { 94 | clearInterval(this.intervalId_unzoned); 95 | this.intervalId_unzoned = null; 96 | } 97 | }; 98 | ; 99 | AsyncComponent = __decorate([ 100 | core_1.Component({ 101 | templateUrl: 'app/async/async.component.html', 102 | }), 103 | __metadata('design:paramtypes', [core_1.NgZone]) 104 | ], AsyncComponent); 105 | return AsyncComponent; 106 | }()); 107 | exports.AsyncComponent = AsyncComponent; 108 | //# sourceMappingURL=async.component.js.map -------------------------------------------------------------------------------- /testapp/ng2/app/async/async.component.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"async.component.js","sourceRoot":"","sources":["async.component.ts"],"names":[],"mappings":";;;;;;;;;;AAAA,qBAAkC,eAAe,CAAC,CAAA;AAKlD;IAWE,wBAAoB,OAAe;QAAf,YAAO,GAAP,OAAO,CAAQ;QAVnC,SAAI,GAAW,CAAC,CAAC;QACjB,SAAI,GAAW,CAAC,CAAC;QACjB,SAAI,GAAW,CAAC,CAAC;QACjB,SAAI,GAAW,CAAC,CAAC;QACjB,SAAI,GAAW,CAAC,CAAC;QACjB,cAAS,GAAG,IAAI,CAAC;QACjB,qBAAgB,GAAG,IAAI,CAAC;QACxB,eAAU,GAAG,IAAI,CAAC;QAClB,uBAAkB,GAAG,IAAI,CAAC;IAEY,CAAC;;IAEvC,kCAAS,GAAT,cAAoB,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC;;IAElC,yCAAgB,GAAhB;QAAA,iBAMC;QALC,IAAI,CAAC,sBAAsB,EAAE,CAAC;QAC9B,IAAI,CAAC,SAAS,GAAG,UAAU,CAAC;YAC1B,KAAI,CAAC,IAAI,EAAE,CAAC;YACZ,KAAI,CAAC,SAAS,GAAG,IAAI,CAAC;QACxB,CAAC,EAAE,IAAI,CAAC,CAAC;IACX,CAAC;;IAED,iDAAwB,GAAxB,UAAyB,CAAS;QAChC,IAAI,CAAC,8BAA8B,EAAE,CAAC;QAEtC,IAAI,IAAI,GAAG,IAAI,CAAC;QAChB,gBAAgB,EAAE;YAChB,EAAE,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC;gBACZ,IAAI,CAAC,gBAAgB,GAAG,IAAI,CAAC;gBAC7B,MAAM,CAAC;YACT,CAAC;YAED,IAAI,CAAC,gBAAgB,GAAG,UAAU,CAAC;gBACjC,IAAI,CAAC,IAAI,EAAE,CAAC;gBACZ,MAAM,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC;YACjB,CAAC,EAAE,GAAG,CAAC,CAAC;QACV,CAAC;QACD,MAAM,CAAC,CAAC,CAAC,CAAC;IACZ,CAAC;;IAED,0CAAiB,GAAjB;QAAA,iBAGC;QAFC,IAAI,CAAC,uBAAuB,EAAE,CAAC;QAC/B,IAAI,CAAC,UAAU,GAAG,WAAW,CAAC,cAAQ,KAAI,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,CAAC,CAAA;IAC7D,CAAC;;IAED,kDAAyB,GAAzB;QAAA,iBASC;QARC,IAAI,CAAC,+BAA+B,EAAE,CAAC;QACvC,IAAI,CAAC,OAAO,CAAC,iBAAiB,CAAC;YAC7B,KAAI,CAAC,kBAAkB,GAAG,WAAW,CAAC;gBACpC,KAAI,CAAC,OAAO,CAAC,GAAG,CAAC;oBACf,KAAI,CAAC,IAAI,EAAE,CAAC;gBACd,CAAC,CAAC,CAAC;YACL,CAAC,EAAE,IAAI,CAAC,CAAA;QACV,CAAC,CAAC,CAAC;IACL,CAAC;;IAED,+CAAsB,GAAtB;QACE,EAAE,CAAC,CAAC,IAAI,CAAC,SAAS,IAAI,IAAI,CAAC,CAAC,CAAC;YAC3B,YAAY,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;YAC7B,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC;QACxB,CAAC;IACH,CAAC;;IAED,uDAA8B,GAA9B;QACE,EAAE,CAAC,CAAC,IAAI,CAAC,gBAAgB,IAAI,IAAI,CAAC,CAAC,CAAC;YAClC,YAAY,CAAC,IAAI,CAAC,gBAAgB,CAAC,CAAC;YACpC,IAAI,CAAC,gBAAgB,GAAG,IAAI,CAAC;QAC/B,CAAC;IACH,CAAC;;IAED,gDAAuB,GAAvB;QACE,EAAE,CAAC,CAAC,IAAI,CAAC,UAAU,IAAI,IAAI,CAAC,CAAC,CAAC;YAC5B,aAAa,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;YAC/B,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC;QACzB,CAAC;IACH,CAAC;;IAED,wDAA+B,GAA/B;QACE,EAAE,CAAC,CAAC,IAAI,CAAC,kBAAkB,IAAI,IAAI,CAAC,CAAC,CAAC;YACpC,aAAa,CAAC,IAAI,CAAC,kBAAkB,CAAC,CAAC;YACvC,IAAI,CAAC,kBAAkB,GAAG,IAAI,CAAC;QACjC,CAAC;IACH,CAAC;;IAtFH;QAAC,gBAAS,CAAC;YACT,WAAW,EAAE,gCAAgC;SAC9C,CAAC;;sBAAA;IAqFF,qBAAC;AAAD,CAAC,AApFD,IAoFC;AApFY,sBAAc,iBAoF1B,CAAA"} -------------------------------------------------------------------------------- /testapp/ng2/app/async/async.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, NgZone } from '@angular/core'; 2 | 3 | @Component({ 4 | templateUrl: 'app/async/async.component.html', 5 | }) 6 | export class AsyncComponent { 7 | val1: number = 0; 8 | val2: number = 0; 9 | val3: number = 0; 10 | val4: number = 0; 11 | val5: number = 0; 12 | timeoutId = null; 13 | chainedTimeoutId = null; 14 | intervalId = null; 15 | intervalId_unzoned = null; 16 | 17 | constructor(private _ngZone: NgZone) {}; 18 | 19 | increment(): void { this.val1++; }; 20 | 21 | delayedIncrement(): void { 22 | this.cancelDelayedIncrement(); 23 | this.timeoutId = setTimeout(() => { 24 | this.val2++; 25 | this.timeoutId = null; 26 | }, 2000); 27 | }; 28 | 29 | chainedDelayedIncrements(i: number): void { 30 | this.cancelChainedDelayedIncrements(); 31 | 32 | var self = this; 33 | function helper(_i) { 34 | if (_i <= 0) { 35 | self.chainedTimeoutId = null; 36 | return; 37 | } 38 | 39 | self.chainedTimeoutId = setTimeout(() => { 40 | self.val3++; 41 | helper(_i - 1); 42 | }, 500); 43 | } 44 | helper(i); 45 | }; 46 | 47 | periodicIncrement(): void { 48 | this.cancelPeriodicIncrement(); 49 | this.intervalId = setInterval(() => { this.val4++; }, 2000) 50 | }; 51 | 52 | periodicIncrement_unzoned(): void { 53 | this.cancelPeriodicIncrement_unzoned(); 54 | this._ngZone.runOutsideAngular(() => { 55 | this.intervalId_unzoned = setInterval(() => { 56 | this._ngZone.run(() => { 57 | this.val5++; 58 | }); 59 | }, 2000) 60 | }); 61 | }; 62 | 63 | cancelDelayedIncrement(): void { 64 | if (this.timeoutId != null) { 65 | clearTimeout(this.timeoutId); 66 | this.timeoutId = null; 67 | } 68 | }; 69 | 70 | cancelChainedDelayedIncrements(): void { 71 | if (this.chainedTimeoutId != null) { 72 | clearTimeout(this.chainedTimeoutId); 73 | this.chainedTimeoutId = null; 74 | } 75 | }; 76 | 77 | cancelPeriodicIncrement(): void { 78 | if (this.intervalId != null) { 79 | clearInterval(this.intervalId); 80 | this.intervalId = null; 81 | } 82 | }; 83 | 84 | cancelPeriodicIncrement_unzoned(): void { 85 | if (this.intervalId_unzoned != null) { 86 | clearInterval(this.intervalId_unzoned); 87 | this.intervalId_unzoned = null; 88 | } 89 | }; 90 | } 91 | -------------------------------------------------------------------------------- /testapp/ng2/app/home/home.component.html: -------------------------------------------------------------------------------- 1 | this is the home 2 | -------------------------------------------------------------------------------- /testapp/ng2/app/home/home.component.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { 3 | var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; 4 | if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); 5 | else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; 6 | return c > 3 && r && Object.defineProperty(target, key, r), r; 7 | }; 8 | var __metadata = (this && this.__metadata) || function (k, v) { 9 | if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v); 10 | }; 11 | var core_1 = require('@angular/core'); 12 | var HomeComponent = (function () { 13 | function HomeComponent() { 14 | } 15 | HomeComponent = __decorate([ 16 | core_1.Component({ 17 | templateUrl: 'app/home/home.component.html' 18 | }), 19 | __metadata('design:paramtypes', []) 20 | ], HomeComponent); 21 | return HomeComponent; 22 | }()); 23 | exports.HomeComponent = HomeComponent; 24 | //# sourceMappingURL=home.component.js.map -------------------------------------------------------------------------------- /testapp/ng2/app/home/home.component.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"home.component.js","sourceRoot":"","sources":["home.component.ts"],"names":[],"mappings":";;;;;;;;;;AAAA,qBAA0B,eAAe,CAAC,CAAA;AAK1C;IAAA;IAEA,CAAC;IALD;QAAC,gBAAS,CAAC;YACT,WAAW,EAAE,8BAA8B;SAC5C,CAAC;;qBAAA;IAGF,oBAAC;AAAD,CAAC,AAFD,IAEC;AAFY,qBAAa,gBAEzB,CAAA"} -------------------------------------------------------------------------------- /testapp/ng2/app/home/home.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | templateUrl: 'app/home/home.component.html' 5 | }) 6 | export class HomeComponent { 7 | 8 | } 9 | -------------------------------------------------------------------------------- /testapp/ng2/app/main.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var platform_browser_dynamic_1 = require('@angular/platform-browser-dynamic'); 3 | var app_module_1 = require('./app.module'); 4 | platform_browser_dynamic_1.platformBrowserDynamic().bootstrapModule(app_module_1.AppModule); 5 | //# sourceMappingURL=main.js.map -------------------------------------------------------------------------------- /testapp/ng2/app/main.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"main.js","sourceRoot":"","sources":["main.ts"],"names":[],"mappings":";AAAA,yCAAuC,mCAAmC,CAAC,CAAA;AAC3E,2BAA0B,cAAc,CAAC,CAAA;AAEzC,iDAAsB,EAAE,CAAC,eAAe,CAAC,sBAAS,CAAC,CAAC"} -------------------------------------------------------------------------------- /testapp/ng2/app/main.ts: -------------------------------------------------------------------------------- 1 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 2 | import { AppModule } from './app.module'; 3 | 4 | platformBrowserDynamic().bootstrapModule(AppModule); 5 | -------------------------------------------------------------------------------- /testapp/ng2/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Test App 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 26 | 27 | 28 | 29 | loading... 30 | 31 | 32 | 33 | 34 | 35 | 40 | -------------------------------------------------------------------------------- /testapp/ng2/styles.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family:"Roboto","Helvetica Neue Light","Helvetica Neue",Helvetica,Arial,"Lucida Grande",sans-serif; 3 | font-size:14px;color:#1a2326 4 | } 5 | .container { 6 | padding-left: 20px; 7 | } 8 | .nav { 9 | padding-bottom: 20px; 10 | } 11 | -------------------------------------------------------------------------------- /testapp/ng2/system-config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * System configuration for Angular 2 samples 3 | * Adjust as necessary for your application needs. 4 | */ 5 | (function (global) { 6 | System.config({ 7 | paths: { 8 | // paths serve as alias 9 | 'npm:': '/node_modules/' 10 | }, 11 | // map tells the System loader where to look for things 12 | map: { 13 | // our app is within the app folder 14 | app: 'app', 15 | 16 | // angular bundles 17 | '@angular/core': 'npm:@angular/core/bundles/core.umd.js', 18 | '@angular/common': 'npm:@angular/common/bundles/common.umd.js', 19 | '@angular/compiler': 'npm:@angular/compiler/bundles/compiler.umd.js', 20 | '@angular/platform-browser': 'npm:@angular/platform-browser/bundles/platform-browser.umd.js', 21 | '@angular/platform-browser-dynamic': 'npm:@angular/platform-browser-dynamic/bundles/platform-browser-dynamic.umd.js', 22 | '@angular/http': 'npm:@angular/http/bundles/http.umd.js', 23 | '@angular/router': 'npm:@angular/router/bundles/router.umd.js', 24 | '@angular/forms': 'npm:@angular/forms/bundles/forms.umd.js', 25 | 26 | // other libraries 27 | 'rxjs': 'npm:rxjs', 28 | }, 29 | // packages tells the System loader how to load when no filename and/or no extension 30 | packages: { 31 | app: { 32 | main: './main.js', 33 | defaultExtension: 'js' 34 | }, 35 | rxjs: { 36 | defaultExtension: 'js' 37 | }, 38 | 'angular2-in-memory-web-api': { 39 | main: './index.js', 40 | defaultExtension: 'js' 41 | } 42 | } 43 | }); 44 | })(this); 45 | -------------------------------------------------------------------------------- /testapp/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "testapp", 3 | "version": "0.0.1", 4 | "scripts": { 5 | "start": "scripts/web-server.js", 6 | "tsc": "tsc", 7 | "tsc:w": "tsc -w", 8 | "lite": "lite-server", 9 | "typings": "typings", 10 | "postinstall": "typings install" 11 | }, 12 | "dependencies": { 13 | "@angular/common": "2.0.0", 14 | "@angular/compiler": "2.0.0", 15 | "@angular/core": "2.0.0", 16 | "@angular/http": "2.0.0", 17 | "@angular/platform-browser": "2.0.0", 18 | "@angular/platform-browser-dynamic": "2.0.0", 19 | "@angular/router": "3.0.0", 20 | "@angular/upgrade": "2.0.0", 21 | "core-js": "2.4.1", 22 | "optimist": "^0.6.1", 23 | "reflect-metadata": "0.1.3", 24 | "rxjs": "5.0.0-beta.12", 25 | "systemjs": "0.19.27", 26 | "zone.js": "0.6.25" 27 | }, 28 | "devDependencies": { 29 | "concurrently": "2.2.0", 30 | "lite-server": "2.2.0", 31 | "typescript": "1.8.10", 32 | "typings": "1.0.4", 33 | "express": "4.13.3" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /testapp/scripts/web-server.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var express = require('express'); 4 | var bodyParser = require('body-parser') 5 | var optimist = require('optimist'); 6 | var util = require('util'); 7 | var path = require('path'); 8 | 9 | var testApp = express(); 10 | var DEFAULT_PORT = process.env.HTTP_PORT | 8081; 11 | var testAppDir = path.resolve(__dirname, '..'); 12 | var defaultAngular = require(path.join(testAppDir, 'ng1/lib/angular_version.js')); 13 | 14 | var argv = optimist.describe('port', 'port'). 15 | default('port', DEFAULT_PORT). 16 | describe('ngversion', 'version of AngularJS to use'). 17 | default('ngversion', defaultAngular). 18 | argv; 19 | 20 | var angularDir = path.join(testAppDir, 'ng1/lib/angular_v' + argv.ngversion); 21 | 22 | var main = function() { 23 | var port = argv.port; 24 | testApp.use('/ng1/lib/angular', express.static(angularDir)); 25 | testApp.use(express.static(testAppDir)); 26 | testApp.use(bodyParser.json()); 27 | testApp.use(testMiddleware); 28 | testApp.listen(port); 29 | util.puts(["Starting express web server in", testAppDir ,"on port", port]. 30 | join(" ")); 31 | }; 32 | 33 | var storage = {}; 34 | var testMiddleware = function(req, res, next) { 35 | if (/ng[1-2]\/fastcall/.test(req.path)) { 36 | res.status(200).send('done'); 37 | } else if (/ng[1-2]\/slowcall/.test(req.path)) { 38 | setTimeout(function() { 39 | res.status(200).send('finally done'); 40 | }, 5000); 41 | } else if (/ng[1-2]\/fastTemplateUrl/.test(req.path)) { 42 | res.status(200).send('fast template contents'); 43 | } else if (/ng[1-2]\/slowTemplateUrl/.test(req.path)) { 44 | setTimeout(function() { 45 | res.status(200).send('slow template contents'); 46 | }, 5000); 47 | } else if (/ng[1-2]\/chat/.test(req.path)) { 48 | if (req.method === 'GET') { 49 | var value; 50 | if (req.query.q) { 51 | value = storage[req.query.q]; 52 | res.status(200).send(value); 53 | } else { 54 | res.status(400).send('must specify query'); 55 | } 56 | } else if (req.method === 'POST') { 57 | if (req.body.key == 'newChatMessage') { 58 | if (!storage['chatMessages']) { 59 | storage['chatMessages'] = []; 60 | } 61 | storage['chatMessages'].push(req.body.value); 62 | res.sendStatus(200); 63 | } else if (req.body.key == 'clearChatMessages') { 64 | storage['chatMessages'] = []; 65 | res.sendStatus(200); 66 | } else { 67 | res.status(400).send('Unknown command'); 68 | } 69 | } else { 70 | res.status(400).send('only accepts GET/POST'); 71 | } 72 | } else { 73 | return next(); 74 | } 75 | }; 76 | 77 | main(); 78 | -------------------------------------------------------------------------------- /testapp/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "commonjs", 5 | "moduleResolution": "node", 6 | "sourceMap": true, 7 | "emitDecoratorMetadata": true, 8 | "experimentalDecorators": true, 9 | "removeComments": false, 10 | "noImplicitAny": false, 11 | "suppressImplicitAnyIndexErrors": true 12 | }, 13 | "exclude": [ 14 | "typings/globals", 15 | "node_modules" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /testapp/typings.json: -------------------------------------------------------------------------------- 1 | { 2 | "ambientDependencies": { 3 | "es6-shim": "github:DefinitelyTyped/DefinitelyTyped/es6-shim/es6-shim.d.ts#4de74cb527395c13ba20b438c3a7a419ad931f1c", 4 | "jasmine": "github:DefinitelyTyped/DefinitelyTyped/jasmine/jasmine.d.ts#bc92442c075929849ec41d28ab618892ba493504" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "module": "commonjs", 5 | "moduleResolution": "node", 6 | "noUnusedLocals": true, 7 | "sourceMap": true, 8 | "declaration": true, 9 | "removeComments": false, 10 | "noImplicitAny": false, 11 | "outDir": "built/" 12 | }, 13 | "exclude": [ 14 | "built", 15 | "node_modules", 16 | "selenium", 17 | "testapp", 18 | "typings/browser", 19 | "typings/browser.d.ts" 20 | ], 21 | "filesGlob": [ 22 | "**/*.ts" 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rulesDirectory": [ 3 | "node_modules/vrsource-tslint-rules/rules", 4 | "node_modules/tslint-eslint-rules/dist/rules" 5 | ], 6 | "rules": { 7 | "no-duplicate-imports": true, 8 | "no-duplicate-variable": true, 9 | "no-jasmine-focus": true, 10 | "no-var-keyword": true, 11 | "semicolon": [true], 12 | "variable-name": [true, "ban-keywords"], 13 | "no-inner-declarations": [true, "function"] 14 | } 15 | } 16 | --------------------------------------------------------------------------------