From fa98a3714dd0d19c182a6d0ee44a1caeeebb3d15 Mon Sep 17 00:00:00 2001 From: Hans Larsen Date: Thu, 14 Jan 2016 11:29:50 -0800 Subject: [PATCH] build(): First version of using SauceLabs and BrowserStack on travis. --- .travis.yml | 34 +- karma.conf.js | 46 --- package.json | 7 +- scripts/ci/build-and-test.sh | 2 +- test/browser-providers.ts | 349 ++++++++++++++++++ karma-test-shim.js => test/karma-test-shim.js | 0 test/karma.conf.js | 32 ++ test/karma.config.ts | 116 ++++++ 8 files changed, 536 insertions(+), 50 deletions(-) delete mode 100644 karma.conf.js create mode 100644 test/browser-providers.ts rename karma-test-shim.js => test/karma-test-shim.js (100%) create mode 100644 test/karma.conf.js create mode 100644 test/karma.config.ts diff --git a/.travis.yml b/.travis.yml index 2d5812c42bdb..fe97227f42f8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,9 +1,41 @@ -sudo: false +# TODO(hansl): When we're ready to test for Dart, readd every mention of Dart to this file. +# Base the Dart config on the main repo travis.yml file. + language: node_js +sudo: false node_js: - '4.2.3' +cache: + directories: + - node_modules + +env: + global: + - LOGS_DIR=/tmp/angular-material-build/logs + - SAUCE_USERNAME=angular-ci + - SAUCE_ACCESS_KEY=9b988f434ff8-fbca-8aa4-4ae3-35442987 + - BROWSER_STACK_USERNAME=angularteam1 + - BROWSER_STACK_ACCESS_KEY=BWCd4SynLzdDcv8xtzsB + - ARCH=linux-x64 + # Token for tsd to increase github rate limit + # See https://github.com/DefinitelyTyped/tsd#tsdrc + # This does not use http://docs.travis-ci.com/user/environment-variables/#Secure-Variables + # because those are not visible for pull requests, and those should also be reliable. + # This SSO token belongs to github account angular-github-ratelimit-token which has no access + # (password is in Valentine) + - TSDRC='{"token":"ef474500309daea53d5991b3079159a29520a40b"}' + # GITHUB_TOKEN_ANGULAR + - secure: "fq/U7VDMWO8O8SnAQkdbkoSe2X92PVqg4d044HmRYVmcf6YbO48+xeGJ8yOk0pCBwl3ISO4Q2ot0x546kxfiYBuHkZetlngZxZCtQiFT9kyId8ZKcYdXaIW9OVdw3Gh3tQyUwDucfkVhqcs52D6NZjyE2aWZ4/d1V4kWRO/LMgo=" + matrix: + # Order: a slower build first, so that we don't occupy an idle travis worker waiting for others to complete. + - MODE=saucelabs_required + - MODE=browserstack_required + - MODE=saucelabs_optional + - MODE=browserstack_optional + + addons: firefox: "latest" diff --git a/karma.conf.js b/karma.conf.js deleted file mode 100644 index 2858b845050a..000000000000 --- a/karma.conf.js +++ /dev/null @@ -1,46 +0,0 @@ -module.exports = function(config) { - config.set({ - basePath: '', - frameworks: ['jasmine'], - plugins: [ - require('karma-jasmine'), - require('karma-chrome-launcher'), - require('karma-firefox-launcher'), - ], - files: [ - {pattern: 'node_modules/angular2/bundles/angular2-polyfills.js', included: true, watched: true}, - {pattern: 'node_modules/systemjs/dist/system.src.js', included: true, watched: true}, - {pattern: 'node_modules/rxjs/bundles/Rx.js', included: true, watched: true}, - {pattern: 'node_modules/angular2/bundles/angular2.js', included: true, watched: true}, - {pattern: 'node_modules/angular2/bundles/testing.dev.js', included: true, watched: true}, - - {pattern: 'karma-test-shim.js', included: true, watched: true}, - - // paths loaded via module imports - {pattern: 'dist/**/*.js', included: false, watched: true}, - - // paths loaded via Angular's component compiler - // (these paths need to be rewritten, see proxies section) - {pattern: 'dist/**/*.html', included: false, watched: true}, - {pattern: 'dist/**/*.css', included: false, watched: true}, - - // paths to support debugging with source maps in dev tools - {pattern: 'dist/**/*.ts', included: false, watched: false}, - {pattern: 'dist/**/*.js.map', included: false, watched: false} - ], - proxies: { - // required for component assests fetched by Angular's compiler - "/demo-app/": "/base/dist/demo-app/", - "/components/": "/base/dist/components/", - }, - exclude: [], - preprocessors: {}, - reporters: ['dots'], - port: 9876, - colors: true, - logLevel: config.LOG_INFO, - autoWatch: true, - browsers: ['Firefox'], - singleRun: false - }); -}; diff --git a/package.json b/package.json index 689d9dd1ddc6..46959d8d1af8 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,8 @@ "url": "https://github.com/angular/material2.git" }, "scripts": { - "demo-app": "cd src && ng serve" + "demo-app": "cd src && ng serve", + "test": "karma start test/karma.conf.js" }, "version": "2.0.0-alpha.0", "license": "Apache-2.0", @@ -33,8 +34,10 @@ "ember-cli-inject-live-reload": "^1.3.0", "jasmine-core": "^2.3.4", "karma": "^0.13.15", + "karma-browserstack-launcher": "^0.1.7", "karma-chrome-launcher": "^0.2.1", "karma-firefox-launcher": "^0.1.7", - "karma-jasmine": "^0.3.6" + "karma-jasmine": "^0.3.6", + "karma-sauce-launcher": "^0.2.14" } } diff --git a/scripts/ci/build-and-test.sh b/scripts/ci/build-and-test.sh index 0c742110e53a..ef621cd37e1b 100755 --- a/scripts/ci/build-and-test.sh +++ b/scripts/ci/build-and-test.sh @@ -8,4 +8,4 @@ SCRIPT_DIR=$(dirname $0) cd ${SCRIPT_DIR}/../.. ng build -karma start --single-run --no-auto-watch --reporters='dots' +karma start test/karma.conf.js --single-run --no-auto-watch --reporters='dots' diff --git a/test/browser-providers.ts b/test/browser-providers.ts new file mode 100644 index 000000000000..9279f5939e92 --- /dev/null +++ b/test/browser-providers.ts @@ -0,0 +1,349 @@ +type ContextConfigurationInfo = { target: string, required: boolean }; +export interface ConfigurationInfo { + unitTest: ContextConfigurationInfo; + e2e: ContextConfigurationInfo; +}; + +export interface BrowserLauncherInfo { + base: string; + flags?: string[]; + version?: string; + platform?: string; + os?: string; + os_version?: string; +}; + +export type AliasMap = { [name: string]: string[] }; + + +// Unique place to configure the browsers which are used in the different CI jobs in Sauce Labs (SL) +// and BrowserStack (BS). +// If the target is set to null, then the browser is not run anywhere during CI. +// If a category becomes empty (e.g. BS and required), then the corresponding job must be commented +// out in Travis configuration. +const configuration: { [name: string]: ConfigurationInfo } = { + 'Chrome': { unitTest: {target: 'SL', required: true}, e2e: {target: null, required: true}}, + 'Firefox': { unitTest: {target: 'SL', required: true}, e2e: {target: null, required: true}}, + 'ChromeBeta': { unitTest: {target: 'SL', required: true}, e2e: {target: null, required: true}}, + 'FirefoxBeta': { unitTest: {target: 'SL', required: true}, e2e: {target: null, required: true}}, + 'ChromeDev': { unitTest: {target: null, required: true}, e2e: {target: null, required: true}}, + 'FirefoxDev': { unitTest: {target: null, required: true}, e2e: {target: null, required: true}}, + 'IE9': { unitTest: {target: null, required: false}, e2e: {target: null, required: true}}, + 'IE10': { unitTest: {target: null, required: true}, e2e: {target: null, required: true}}, + 'IE11': { unitTest: {target: 'SL', required: true}, e2e: {target: null, required: true}}, + 'Edge': { unitTest: {target: 'SL', required: true}, e2e: {target: null, required: true}}, + 'Android4.1': { unitTest: {target: 'SL', required: false}, e2e: {target: null, required: true}}, + 'Android4.2': { unitTest: {target: 'SL', required: false}, e2e: {target: null, required: true}}, + 'Android4.3': { unitTest: {target: 'SL', required: false}, e2e: {target: null, required: true}}, + 'Android4.4': { unitTest: {target: 'SL', required: false}, e2e: {target: null, required: true}}, + 'Android5': { unitTest: {target: 'SL', required: false}, e2e: {target: null, required: true}}, + 'Safari7': { unitTest: {target: null, required: false}, e2e: {target: null, required: true}}, + 'Safari8': { unitTest: {target: 'BS', required: false}, e2e: {target: null, required: true}}, + 'Safari9': { unitTest: {target: 'BS', required: false}, e2e: {target: null, required: true}}, + 'iOS7': { unitTest: {target: 'BS', required: true}, e2e: {target: null, required: true}}, + 'iOS8': { unitTest: {target: 'BS', required: false}, e2e: {target: null, required: true}}, + // TODO(mlaval): iOS9 deactivated as not reliable, reactivate after + // https://github.com/angular/angular/issues/5408 + 'iOS9': { unitTest: {target: null, required: false}, e2e: {target: null, required: true}}, + 'WindowsPhone': { unitTest: {target: 'BS', required: false}, e2e: {target: null, required: true}} +}; + +export const customLaunchers: { [name: string]: BrowserLauncherInfo } = { + 'DartiumWithWebPlatform': { + base: 'Dartium', + flags: ['--enable-experimental-web-platform-features'] }, + 'ChromeNoSandbox': { + base: 'Chrome', + flags: ['--no-sandbox'] }, + 'SL_CHROME': { + base: 'SauceLabs', + browserName: 'chrome', + version: '46' + }, + 'SL_CHROMEBETA': { + base: 'SauceLabs', + browserName: 'chrome', + version: 'beta' + }, + 'SL_CHROMEDEV': { + base: 'SauceLabs', + browserName: 'chrome', + version: 'dev' + }, + 'SL_FIREFOX': { + base: 'SauceLabs', + browserName: 'firefox', + version: '42' + }, + 'SL_FIREFOXBETA': { + base: 'SauceLabs', + browserName: 'firefox', + version: 'beta' + }, + 'SL_FIREFOXDEV': { + base: 'SauceLabs', + browserName: 'firefox', + version: 'dev' + }, + 'SL_SAFARI7': { + base: 'SauceLabs', + browserName: 'safari', + platform: 'OS X 10.9', + version: '7' + }, + 'SL_SAFARI8': { + base: 'SauceLabs', + browserName: 'safari', + platform: 'OS X 10.10', + version: '8' + }, + 'SL_SAFARI9': { + base: 'SauceLabs', + browserName: 'safari', + platform: 'OS X 10.11', + version: '9.0' + }, + 'SL_IOS7': { + base: 'SauceLabs', + browserName: 'iphone', + platform: 'OS X 10.10', + version: '7.1' + }, + 'SL_IOS8': { + base: 'SauceLabs', + browserName: 'iphone', + platform: 'OS X 10.10', + version: '8.4' + }, + 'SL_IOS9': { + base: 'SauceLabs', + browserName: 'iphone', + platform: 'OS X 10.10', + version: '9.1' + }, + 'SL_IE9': { + base: 'SauceLabs', + browserName: 'internet explorer', + platform: 'Windows 2008', + version: '9' + }, + 'SL_IE10': { + base: 'SauceLabs', + browserName: 'internet explorer', + platform: 'Windows 2012', + version: '10' + }, + 'SL_IE11': { + base: 'SauceLabs', + browserName: 'internet explorer', + platform: 'Windows 8.1', + version: '11' + }, + 'SL_EDGE': { + base: 'SauceLabs', + browserName: 'microsoftedge', + platform: 'Windows 10', + version: '20.10240' + }, + 'SL_ANDROID4.1': { + base: 'SauceLabs', + browserName: 'android', + platform: 'Linux', + version: '4.1' + }, + 'SL_ANDROID4.2': { + base: 'SauceLabs', + browserName: 'android', + platform: 'Linux', + version: '4.2' + }, + 'SL_ANDROID4.3': { + base: 'SauceLabs', + browserName: 'android', + platform: 'Linux', + version: '4.3' + }, + 'SL_ANDROID4.4': { + base: 'SauceLabs', + browserName: 'android', + platform: 'Linux', + version: '4.4' + }, + 'SL_ANDROID5': { + base: 'SauceLabs', + browserName: 'android', + platform: 'Linux', + version: '5.1' + }, + + 'BS_CHROME': { + base: 'BrowserStack', + browser: 'chrome', + os: 'OS X', + os_version: 'Yosemite' + }, + 'BS_FIREFOX': { + base: 'BrowserStack', + browser: 'firefox', + os: 'Windows', + os_version: '10' + }, + 'BS_SAFARI7': { + base: 'BrowserStack', + browser: 'safari', + os: 'OS X', + os_version: 'Mavericks' + }, + 'BS_SAFARI8': { + base: 'BrowserStack', + browser: 'safari', + os: 'OS X', + os_version: 'Yosemite' + }, + 'BS_SAFARI9': { + base: 'BrowserStack', + browser: 'safari', + os: 'OS X', + os_version: 'El Capitan' + }, + 'BS_IOS7': { + base: 'BrowserStack', + device: 'iPhone 5S', + os: 'ios', + os_version: '7.0' + }, + 'BS_IOS8': { + base: 'BrowserStack', + device: 'iPhone 6', + os: 'ios', + os_version: '8.3' + }, + 'BS_IOS9': { + base: 'BrowserStack', + device: 'iPhone 6S', + os: 'ios', + os_version: '9.0' + }, + 'BS_IE9': { + base: 'BrowserStack', + browser: 'ie', + browser_version: '9.0', + os: 'Windows', + os_version: '7' + }, + 'BS_IE10': { + base: 'BrowserStack', + browser: 'ie', + browser_version: '10.0', + os: 'Windows', + os_version: '8' + }, + 'BS_IE11': { + base: 'BrowserStack', + browser: 'ie', + browser_version: '11.0', + os: 'Windows', + os_version: '10' + }, + 'BS_EDGE': { + base: 'BrowserStack', + browser: 'edge', + os: 'Windows', + os_version: '10' + }, + 'BS_WINDOWSPHONE' : { + base: 'BrowserStack', + device: 'Nokia Lumia 930', + os: 'winphone', + os_version: '8.1' + }, + 'BS_ANDROID5': { + base: 'BrowserStack', + device: 'Google Nexus 5', + os: 'android', + os_version: '5.0' + }, + 'BS_ANDROID4.4': { + base: 'BrowserStack', + device: 'HTC One M8', + os: 'android', + os_version: '4.4' + }, + 'BS_ANDROID4.3': { + base: 'BrowserStack', + device: 'Samsung Galaxy S4', + os: 'android', + os_version: '4.3' + }, + 'BS_ANDROID4.2': { + base: 'BrowserStack', + device: 'Google Nexus 4', + os: 'android', + os_version: '4.2' + }, + 'BS_ANDROID4.1': { + base: 'BrowserStack', + device: 'Google Nexus 7', + os: 'android', + os_version: '4.1' + } +}; + +const sauceAliases: AliasMap = { + 'ALL': Object.keys(customLaunchers).filter(function(item) { + return customLaunchers[item].base == 'SauceLabs'; + }), + 'DESKTOP': ['SL_CHROME', 'SL_FIREFOX', 'SL_IE9', 'SL_IE10', 'SL_IE11', 'SL_EDGE', 'SL_SAFARI7', + 'SL_SAFARI8', 'SL_SAFARI9'], + 'MOBILE': ['SL_ANDROID4.1', 'SL_ANDROID4.2', 'SL_ANDROID4.3', 'SL_ANDROID4.4', 'SL_ANDROID5', + 'SL_IOS7', 'SL_IOS8', 'SL_IOS9'], + 'ANDROID': ['SL_ANDROID4.1', 'SL_ANDROID4.2', 'SL_ANDROID4.3', 'SL_ANDROID4.4', 'SL_ANDROID5'], + 'IE': ['SL_IE9', 'SL_IE10', 'SL_IE11'], + 'IOS': ['SL_IOS7', 'SL_IOS8', 'SL_IOS9'], + 'SAFARI': ['SL_SAFARI7', 'SL_SAFARI8', 'SL_SAFARI9'], + 'BETA': ['SL_CHROMEBETA', 'SL_FIREFOXBETA'], + 'DEV': ['SL_CHROMEDEV', 'SL_FIREFOXDEV'], + 'REQUIRED': buildConfiguration('unitTest', 'SL', true), + 'OPTIONAL': buildConfiguration('unitTest', 'SL', false) +}; + +const browserstackAliases: AliasMap = { + 'ALL': Object.keys(customLaunchers).filter(function(item) { + return customLaunchers[item].base == 'BrowserStack'; + }), + 'DESKTOP': ['BS_CHROME', 'BS_FIREFOX', 'BS_IE9', 'BS_IE10', 'BS_IE11', 'BS_EDGE', 'BS_SAFARI7', + 'BS_SAFARI8', 'BS_SAFARI9'], + 'MOBILE': ['BS_ANDROID4.3', 'BS_ANDROID4.4', 'BS_IOS7', 'BS_IOS8', 'BS_IOS9', 'BS_WINDOWSPHONE'], + 'ANDROID': ['BS_ANDROID4.3', 'BS_ANDROID4.4'], + 'IE': ['BS_IE9', 'BS_IE10', 'BS_IE11'], + 'IOS': ['BS_IOS7', 'BS_IOS8', 'BS_IOS9'], + 'SAFARI': ['BS_SAFARI7', 'BS_SAFARI8', 'BS_SAFARI9'], + 'REQUIRED': buildConfiguration('unitTest', 'BS', true), + 'OPTIONAL': buildConfiguration('unitTest', 'BS', false) +}; + +export const platformMap: { [name: string]: AliasMap } = { + 'saucelabs': sauceAliases, + 'browserstack': browserstackAliases, +}; + + +/** Decode the token for Travis to use. */ +function decode(str: string): string { + return (str || '').split('').reverse().join(''); +} + + +/** Setup the access keys */ +if (process.env.TRAVIS) { + process.env.SAUCE_ACCESS_KEY = decode(process.env.SAUCE_ACCESS_KEY); + process.env.BROWSER_STACK_ACCESS_KEY = decode(process.env.BROWSER_STACK_ACCESS_KEY); +} + +/** Build a configuration. */ +function buildConfiguration(type: string, target: string, required: boolean): string[] { + return Object.keys(configuration) + .map(item => [item, configuration[item][type]]) + .filter(([item, conf]) => conf.required == required && conf.target == target) + .map(([item, conf]) => `${target}_${item.toUpperCase()}`); +} diff --git a/karma-test-shim.js b/test/karma-test-shim.js similarity index 100% rename from karma-test-shim.js rename to test/karma-test-shim.js diff --git a/test/karma.conf.js b/test/karma.conf.js new file mode 100644 index 000000000000..ee2a38dd856f --- /dev/null +++ b/test/karma.conf.js @@ -0,0 +1,32 @@ +// This file only hook up on require calls to transpile the TypeScript. +// If you're looking at this file to see Karma configuration, you should look at +// karma.config.ts instead. + +const fs = require('fs'); +const ts = require('typescript'); + +const old = require.extensions['.ts']; + +require.extensions['.ts'] = function(m, filename) { + // If we're in node module, either call the old hook or simply compile the + // file without transpilation. We do not touch node_modules/**. + if (filename.match(/node_modules/)) { + if (old) { + return old(m, filename); + } + return m._compile(fs.readFileSync(filename), filename); + } + + // Node requires all require hooks to be sync. + const source = fs.readFileSync(filename).toString(); + const result = ts.transpile(source, { + target: ts.ScriptTarget.ES5, + module: ts.ModuleKind.CommonJs, + }); + + // Send it to node to execute. + return m._compile(result, filename); +}; + +// Import the TS once we know it's safe to require. +module.exports = require('./karma.config.ts').config; diff --git a/test/karma.config.ts b/test/karma.config.ts new file mode 100644 index 000000000000..ede00a5753c3 --- /dev/null +++ b/test/karma.config.ts @@ -0,0 +1,116 @@ +// This file is named differently than its JS bootstrapper to avoid the ts compiler to overwrite it. + +import path = require('path'); +import { + customLaunchers, + platformMap, +} from './browser-providers.ts'; + + +export function config(config) { + config.set({ + basePath: path.join(__dirname, '..'), + frameworks: ['jasmine'], + plugins: [ + require('karma-jasmine'), + require('karma-browserstack-launcher'), + require('karma-chrome-launcher'), + require('karma-firefox-launcher'), + require('karma-sauce-launcher'), + ], + files: [ + {pattern: 'node_modules/es6-shim/es6-shim.js', included: true, watched: true}, + {pattern: 'node_modules/angular2/bundles/angular2-polyfills.js', included: true, watched: true}, + {pattern: 'node_modules/systemjs/dist/system-polyfills.js', included: true, watched: true}, + {pattern: 'node_modules/systemjs/dist/system.src.js', included: true, watched: true}, + {pattern: 'node_modules/rxjs/bundles/Rx.js', included: true, watched: true}, + {pattern: 'node_modules/angular2/bundles/angular2.js', included: true, watched: true}, + {pattern: 'node_modules/angular2/bundles/testing.dev.js', included: true, watched: true}, + + {pattern: 'test/karma-test-shim.js', included: true, watched: true}, + + // paths loaded via module imports + {pattern: 'dist/**/*.js', included: false, watched: true}, + + // paths loaded via Angular's component compiler + // (these paths need to be rewritten, see proxies section) + {pattern: 'dist/**/*.html', included: false, watched: true}, + {pattern: 'dist/**/*.css', included: false, watched: true}, + + // paths to support debugging with source maps in dev tools + {pattern: 'dist/**/*.ts', included: false, watched: false}, + {pattern: 'dist/**/*.js.map', included: false, watched: false} + ], + proxies: { + // required for component assests fetched by Angular's compiler + "/demo-app/": "/base/dist/demo-app/", + "/components/": "/base/dist/components/", + }, + + customLaunchers: customLaunchers, + + exclude: [], + preprocessors: {}, + reporters: ['dots'], + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + autoWatch: true, + + sauceLabs: { + testName: 'material2', + startConnect: true, + recordVideo: false, + recordScreenshots: false, + options: { + 'selenium-version': '2.48.2', + 'command-timeout': 600, + 'idle-timeout': 600, + 'max-duration': 5400 + } + }, + + browserStack: { + project: 'material2', + startTunnel: false, + retryLimit: 1, + timeout: 600, + pollingTimeout: 10000 + }, + + browsers: ['Chrome'], + + singleRun: false + }); + + if (process.env['TRAVIS']) { + var buildId = `TRAVIS #${process.env.TRAVIS_BUILD_NUMBER} (${process.env.TRAVIS_BUILD_ID})`; + + // The MODE variable is the indicator of what row in the test matrix we're running. + // It will look like _, where platform is one of 'saucelabs' or 'browserstack', + // and alias is one of the keys in the CIconfiguration variable declared in + // browser-providers.ts. + var modeSplit = process.env.MODE.split('_'); + var platform = modeSplit[0]; + var alias = modeSplit[1] || 'all'; + + if (platform == 'saucelabs') { + config.sauceLabs.build = buildId; + config.sauceLabs.tunnelIdentifier = process.env.TRAVIS_JOB_NUMBER; + + // TODO(mlaval): remove once SauceLabs supports websockets. + // This speeds up the capturing a bit, as browsers don't even try to use websocket. + console.log('>>>> setting socket.io transport to polling <<<<'); + config.transports = ['polling']; + } + else if (platform == 'browserstack') { + config.browserStack.build = buildId; + config.browserStack.tunnelIdentifier = process.env.TRAVIS_JOB_NUMBER; + } + else { + throw new Error(`Platform "${platform}" unknown, but Travis specified. Exiting.`); + } + + config.browsers = platformMap[platform][alias.toUpperCase()]; + } +};