From e78974074f7b31a052915967b6628589f7c2d13d Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Mon, 24 Oct 2016 17:14:45 -0700 Subject: [PATCH] Init --- .editorconfig | 12 ++ .gitignore | 46 +++++ .npmignore | 57 ++++++ LICENSE | 50 +++++ demo/src/bundles/demo/demo.module.ts | 70 +++++++ demo/src/bundles/main.ts | 4 + demo/src/bundles/polyfills.ts | 4 + demo/src/index.ejs | 13 ++ demo/webpack.config.js | 73 ++++++++ gulpfile.js | 109 +++++++++++ package.json | 74 ++++++++ src/focus.service.ts | 8 + src/index.ts | 10 + src/input.service.ts | 268 +++++++++++++++++++++++++++ test/bin/system-config-spec.ts | 37 ++++ test/karma-shim.js | 56 ++++++ test/karma.conf.ts | 98 ++++++++++ test/karma.confloader.js | 28 +++ tsconfig.json | 30 +++ tslint.json | 64 +++++++ 20 files changed, 1111 insertions(+) create mode 100644 .editorconfig create mode 100644 .gitignore create mode 100644 .npmignore create mode 100644 LICENSE create mode 100644 demo/src/bundles/demo/demo.module.ts create mode 100644 demo/src/bundles/main.ts create mode 100644 demo/src/bundles/polyfills.ts create mode 100644 demo/src/index.ejs create mode 100644 demo/webpack.config.js create mode 100644 gulpfile.js create mode 100644 package.json create mode 100644 src/focus.service.ts create mode 100644 src/index.ts create mode 100644 src/input.service.ts create mode 100644 test/bin/system-config-spec.ts create mode 100644 test/karma-shim.js create mode 100644 test/karma.conf.ts create mode 100644 test/karma.confloader.js create mode 100644 tsconfig.json create mode 100644 tslint.json diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..9fa6fa0 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,12 @@ +# top-most EditorConfig file +root = true + +# Unix-style newlines with a newline ending every file +[*] +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true +charset = utf-8 +indent_style = space +indent_size = 4 + diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d3f21ff --- /dev/null +++ b/.gitignore @@ -0,0 +1,46 @@ +#### joe made this: http://goel.io/joe +#### node #### +# Logs +logs +*.log +npm-debug.log* + +# Runtime data +pids +*.pid +*.seed + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (http://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules +jspm_packages + +# Optional npm cache directory +.npm + +# Optional REPL history +.node_repl_history + +dist +.idea/ +.DS_Store + +# Editor +.vscode diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..1d8fe97 --- /dev/null +++ b/.npmignore @@ -0,0 +1,57 @@ +# Logs +logs +*.log + +# Runtime data +pids +*.pid +*.seed + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Compiled binary addons (http://nodejs.org/api/addons.html) +build/Release + +# Dependency directory +# Commenting this out is preferred by some people, see +# https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- +node_modules + +# Users Environment Variables +.lock-wscript +.tsdrc + +#IntelliJ configuration files +.idea + +dist +dev +docs +lib +test + +Thumbs.db +.DS_Store +*.yml +!*.d.ts +/src +*.spec.* +*.e2e.* +CONTRIBUTING.md +karma-shim.js +karma.conf.js +protractor.conf.js +tsconfig.json +tslint.json +typedoc.json +webpack.config.js +.travis.yml +.jshintrc +.editorconfig diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..6ec2d89 --- /dev/null +++ b/LICENSE @@ -0,0 +1,50 @@ +Much of the code and algorithms in this project are from the project at +https://git.io/vPxQQ, which is provided under the following license: + +The MIT License (MIT) + +Copyright (c) 2016 Microsoft. All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +------------------------------------------------------------------------------- + + +The original code in this project is available under the following license: + +MIT License + +Copyright (c) 2016 Beam Interactive, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/demo/src/bundles/demo/demo.module.ts b/demo/src/bundles/demo/demo.module.ts new file mode 100644 index 0000000..fb66638 --- /dev/null +++ b/demo/src/bundles/demo/demo.module.ts @@ -0,0 +1,70 @@ +import { ArcModule, InputService } from '../../../../src'; +import { Component, NgModule } from '@angular/core'; +import { BrowserModule } from '@angular/platform-browser'; + +@Component({ + selector: 'demo-app', + styles: [` + :host { + font-family: monospace; + } + + .area { + max-width: 960px; + border: 1px solid #000; + margin: 15px auto; + + .arc--selected { + border-color: #f00; + } + } + + .box { + width: 100px; + float: left; + margin: 15px; + background: #000; + color: #fff; + + .arc--selected { + background: #f00; + } + } + `], + template: ` +
+
+ {{ box }} +
+
+ `, +}) +export class DemoComponent { + public boxes: string[] = []; + + constructor(input: InputService) { + for (let i = 0; i < 100; i++) { + this.boxes.push(String(`Box ${i}`)); + } + + input.bootstrap(); + } +} + +@NgModule({ + imports: [ + BrowserModule, + ArcModule, + ], + providers: [ + InputService, + ], + declarations: [ + DemoComponent, + ], + bootstrap: [ + DemoComponent, + ], +}) +export class AppModule { +} diff --git a/demo/src/bundles/main.ts b/demo/src/bundles/main.ts new file mode 100644 index 0000000..f592b43 --- /dev/null +++ b/demo/src/bundles/main.ts @@ -0,0 +1,4 @@ +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; +import { AppModule } from './demo/demo.module'; + +platformBrowserDynamic().bootstrapModule(AppModule); \ No newline at end of file diff --git a/demo/src/bundles/polyfills.ts b/demo/src/bundles/polyfills.ts new file mode 100644 index 0000000..6ee83db --- /dev/null +++ b/demo/src/bundles/polyfills.ts @@ -0,0 +1,4 @@ +import 'ts-helpers'; + +import 'core-js/es7/reflect'; +import 'zone.js/dist/zone'; \ No newline at end of file diff --git a/demo/src/index.ejs b/demo/src/index.ejs new file mode 100644 index 0000000..4ab8c65 --- /dev/null +++ b/demo/src/index.ejs @@ -0,0 +1,13 @@ + + + + + + Arcade Machine + + + + + + + diff --git a/demo/webpack.config.js b/demo/webpack.config.js new file mode 100644 index 0000000..0383182 --- /dev/null +++ b/demo/webpack.config.js @@ -0,0 +1,73 @@ +const path = require('path'); +const webpack = require('webpack'); +const atl = require('awesome-typescript-loader'); +const CleanWebpackPlugin = require('clean-webpack-plugin'); +const HtmlWebpackPlugin = require('html-webpack-plugin'); + +const chunkOrder = ['inline', 'polyfill', 'main']; +const paths = {}; +paths.root = paths.root || path.resolve(__dirname); +paths.dist = path.resolve(__dirname, 'dist'); +paths.indexTemplate = path.resolve(__dirname, 'src/index.ejs'); +paths.tsconfig = path.resolve(__dirname, '../tsconfig.json'); + +module.exports = { + devtool: 'source-map', + context: paths.root, + entry: { + main: path.resolve(__dirname, 'src/bundles/main.ts'), + polyfills: path.resolve(__dirname, 'src/bundles/polyfills.ts') + }, + output: { + path: paths.dist, + filename: 'bundles/[name].bundle.js', + }, + resolve: { + extensions: ['.js', '.ts'], + }, + devServer: { + port: 8080, + historyApiFallback: true, + contentBase: 'demo/dist' + }, + module: { + loaders: [ + { + test: /\.ts$/, + loaders: [ + { + loader: 'awesome-typescript-loader', + query: { + useForkChecker: true, + tsconfig: paths.tsconfig + } + }, + ], + } + ] + }, + plugins: [ + new HtmlWebpackPlugin({ + template: paths.indexTemplate, + hash: true, + chunksSortMode: (a, b) => chunkOrder.indexOf(a.names[0]) > chunkOrder.indexOf(b.names[0]), + }), + + new atl.ForkCheckerPlugin(), + + // Fix for critical dependency warning due to System.import in angular. + // See https://github.com/angular/angular/issues/11580 + new webpack.ContextReplacementPlugin( + /angular(\\|\/)core(\\|\/)(esm(\\|\/)src|src)(\\|\/)linker/, + paths.app + ), + + ], + node: { + fs: 'empty', + crypto: 'empty', + module: false, + clearImmediate: false, + setImmediate: false + } +}; diff --git a/gulpfile.js b/gulpfile.js new file mode 100644 index 0000000..c0f47d0 --- /dev/null +++ b/gulpfile.js @@ -0,0 +1,109 @@ +const gulp = require('gulp'); +const gulpMerge = require('merge2'); +const gulpRunSequence = require('run-sequence'); +const gulpConcat = require('gulp-concat'); +const gulpAutoprefixer = require('gulp-autoprefixer'); + +const tsc = require('gulp-typescript'); +const karma = require('karma'); +const autoprefixer = require('autoprefixer'); +const gulpClean = require('gulp-clean'); +const path = require('path'); +const webpack = require('webpack'); + +let isTestRun = false; + +/** + * Inline the templates and styles, and the compile to javascript. + */ +gulp.task(':build:app', () => { + const tsProject = tsc.createProject('tsconfig.json', { + module: isTestRun ? 'commonjs' : 'es2015', + }); + + gulp.src(['./src/**/*.ts']) + .pipe(tsProject()) + .pipe(gulp.dest('./dist')); +}); + +/** + * Cleans the build folder + */ +gulp.task('clean', () => gulp.src('dist', { read: false }).pipe(gulpClean(null))); + +/** + * Builds the main framework to the build folder + */ +gulp.task('build', (cb) => gulpRunSequence( + 'clean', + [ + ':build:app', + ], + cb +)); + +/** + * Bundles vendor files for test access + */ +gulp.task(':test:vendor', function () { + const npmVendorFiles = [ + '@angular', 'core-js/client', 'systemjs/dist', 'zone.js/dist' + ]; + + return gulpMerge( + npmVendorFiles.map(function (root) { + const glob = path.join(root, '**/*.+(js|js.map)'); + + return gulp.src(path.join('node_modules', glob)) + .pipe(gulp.dest(path.join('dist/vendor', root))); + })); +}); + +/** + * Bundles systemjs files + */ +gulp.task(':test:system', () => { + gulp.src('test/bin/**.*') + .pipe(tsc()) + .pipe(gulp.dest('dist/bin')); +}); + +/** + * Pre-test setup task + */ +gulp.task(':test:deps', (cb) => { + isTestRun = true; + gulpRunSequence( + 'clean', + [ + ':test:system', + ':test:vendor', + ':build:app', + ], + cb + ); +}); + +/** + * Karma unit-testing + */ +gulp.task('test', [':test:deps'], (done) => { + new karma.Server({ + configFile: path.join(process.cwd(), 'test/karma.confloader.js') + }, done).start(); +}); + +gulp.task(':demo:clean', () => gulp.src('demo/dist', { read: false }).pipe(gulpClean(null))); + +gulp.task(':demo:build:ts', (cb) => { + webpack(require('./demo/webpack.config.js'), cb); +}); + +gulp.task('demo', (cb) => gulpRunSequence( + ':demo:clean', + [ + ':demo:build:ts', + ], + cb +)); + diff --git a/package.json b/package.json new file mode 100644 index 0000000..3ebf138 --- /dev/null +++ b/package.json @@ -0,0 +1,74 @@ +{ + "name": "arcade-machine", + "version": "0.1.0", + "scripts": { + "gulp": "gulp", + "build": "gulp build", + "prepublish": "npm run build", + "clean": "rimraf node_modules doc dist && npm cache clean", + "test:lint": "tslint 'src/**/*.ts' --project tsconfig.json", + "test:unit": "gulp test", + "test": "npm run lint && npm run test:unit", + "start": "npm run serve", + "serve": "webpack-dev-server --config demo/webpack.config.js" + }, + "main": "dist/index.js", + "typings": "dist/index.d.ts", + "license": "Proprietary", + "dependencies": { + "@angular/common": "^2.0.1", + "@angular/compiler": "^2.0.1", + "@angular/core": "^2.0.1", + "@angular/platform-browser": "^2.0.1", + "@angular/platform-browser-dynamic": "^2.0.1", + "@types/core-js": "^0.9.34", + "@types/node": "^6.0.40", + "core-js": "^2.4.1", + "systemjs": "0.19.38", + "zone.js": "^0.6.23" + }, + "devDependencies": { + "@types/jasmine": "^2.2.34", + "@types/karma": "^0.13.33", + "@types/lodash": "^4.14.36", + "@types/node": "^6.0.41", + "autoprefixer": "^6.5.0", + "awesome-typescript-loader": "^2.2.4", + "clean-webpack-plugin": "^0.1.10", + "codelyzer": "0.0.28", + "copy-webpack-plugin": "^3.0.0", + "extract-text-webpack-plugin": "^1.0.1", + "gulp": "^3.9.1", + "gulp-autoprefixer": "^3.1.1", + "gulp-clean": "^0.3.2", + "gulp-concat": "^2.6.0", + "gulp-typescript": "^3.0.1", + "html-webpack-plugin": "^2.22.0", + "istanbul-instrumenter-loader": "^0.2.0", + "jasmine-core": "^2.3.4", + "jasmine-spec-reporter": "^2.4.0", + "karma": "^1.1.1", + "karma-browserstack-launcher": "^1.0.1", + "karma-chrome-launcher": "^1.0.1", + "karma-coverage": "^1.1.1", + "karma-firefox-launcher": "^1.0.0", + "karma-jasmine": "^1.0.2", + "karma-mocha-reporter": "^2.2.0", + "karma-sauce-launcher": "^1.0.0", + "karma-sourcemap-loader": "^0.3.7", + "merge2": "^1.0.2", + "phantomjs-prebuilt": "^2.1.4", + "reflect-metadata": "^0.1.8", + "rimraf": "^2.5.1", + "run-sequence": "^1.2.2", + "sass-loader": "^4.0.2", + "ts-helpers": "^1.1.1", + "tslint": "^3.15.1", + "tslint-loader": "^2.1.0", + "tslint-microsoft-contrib": "^2.0.12", + "typedoc": "^0.3.12", + "typescript": "2.0.3", + "webpack": "^2.1.0-beta.25", + "webpack-dev-server": "^2.1.0-beta.7" + } +} diff --git a/src/focus.service.ts b/src/focus.service.ts new file mode 100644 index 0000000..b1bc89b --- /dev/null +++ b/src/focus.service.ts @@ -0,0 +1,8 @@ +export enum Direction { + LEFT, + RIGHT, + UP, + DOWN, + SUBMIT, + BACK, +} diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..d138235 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,10 @@ +import { NgModule } from '@angular/core'; + +export { InputService } from './input.service'; + +@NgModule({ + imports: [], + exports: [], +}) +export class ArcModule { +} diff --git a/src/input.service.ts b/src/input.service.ts new file mode 100644 index 0000000..2c5561b --- /dev/null +++ b/src/input.service.ts @@ -0,0 +1,268 @@ +import { Direction } from './focus.service'; +import { Injectable } from '@angular/core'; + +interface GamepadWrapper { + // Directional returns from the gamepad. They debounce themselves and + // trigger again after debounce times. + left(now: number): boolean; + right(now: number): boolean; + up(now: number): boolean; + down(now: number): boolean; + + /** + * Returns if the user is pressing the "back" button. + */ + back(now: number): boolean; + + /** + * Returns if the user is pressing the "submit" button. + */ + submit(now: number): boolean; + + /** + * Returns whether the gamepad is still connected; + */ + isConnected(): boolean; +} + +enum DebouncerStage { + IDLE, + HELD, + FAST, +} + +class DirectionalDebouncer { + + /** + * fn is a bound function that can be called to check if the key is held. + */ + public fn: (time: number) => boolean; + + /** + * Initial debounce after a joystick is pressed before beginning shorter + * press debouncded. + */ + public static JoystickInitialDebounce = 500; + + /** + * Fast debounce time for joysticks when they're being held in a direction. + */ + public static JoystickFastDebounce = 200; + + private heldAt = 0; + private stage = DebouncerStage.IDLE; + + constructor(private predicate: () => boolean) {} + + /** + * Returns whether the key should be registered as pressed. + */ + public attempt(now: number): boolean { + const result = this.predicate(); + if (!result) { + this.stage = DebouncerStage.IDLE; + return false; + } + + switch (this.stage) { + case DebouncerStage.IDLE: + this.stage = DebouncerStage.HELD; + return true; + + case DebouncerStage.HELD: + if (now - this.heldAt < DirectionalDebouncer.JoystickInitialDebounce) { + this.heldAt = now; + return false; + } + this.stage = DebouncerStage.FAST; + return true; + + case DebouncerStage.FAST: + if (now - this.heldAt < DirectionalDebouncer.JoystickFastDebounce) { + this.heldAt = now; + return false; + } + return true; + + default: + throw new Error(`Unknown debouncer stage ${this.stage}!`); + } + } +} +class XboxGamepadWrapper implements GamepadWrapper { + + public left: (now: number) => boolean; + public right: (now: number) => boolean; + public up: (now: number) => boolean; + public down: (now: number) => boolean; + public back: (now: number) => boolean; + public submit: (now: number) => boolean; + + constructor(private pad: Gamepad) { + const left = new DirectionalDebouncer(() => pad.axes[0] < -InputService.JoystickThreshold); + const right = new DirectionalDebouncer(() => pad.axes[0] > InputService.JoystickThreshold); + const up = new DirectionalDebouncer(() => pad.axes[1] < -InputService.JoystickThreshold); + const down = new DirectionalDebouncer(() => pad.axes[0] > InputService.JoystickThreshold); + const back = new DirectionalDebouncer(() => pad.buttons[1].pressed); + const submit = new DirectionalDebouncer(() => pad.buttons[0].pressed); + + this.left = now => left.attempt(now); + this.right = now => right.attempt(now); + this.up = now => up.attempt(now); + this.down = now => down.attempt(now); + this.back = now => back.attempt(now); + this.submit = now => submit.attempt(now); + } + + public isConnected() { + return this.pad.connected; + } +} + +/** + * InputService handles passing input from the external device (gamepad API + * or keyboard) to the arc internals. + */ +@Injectable() +export class InputService { + + /** + * Mangitude that joysticks have to go in one direction to be translated + * into a direction key press. + */ + public static JoystickThreshold = 0.5; + + /** + * DirectionCodes is a map of directions to key code names. + */ + public static DirectionCodes = new Map([ + [Direction.DOWN, []] + ]); + + private gamepads: GamepadWrapper[]; + private pollRaf: number; + + /** + * Bootstrap attaches event listeners from the service to the DOM. + */ + public bootstrap() { + // The gamepadInputEmulation is a string property that exists in + // JavaScript UWAs and in WebViews in UWAs. It won't exist in + // Win8.1 style apps or browsers. + if (typeof ( navigator).gamepadInputEmulation === "string") { + // We want the gamepad to provide gamepad VK keyboard events rather than moving a + // mouse like cursor. Set to "keyboard", the gamepad will provide such keyboard events + // and provide input to the DOM navigator.getGamepads API. + ( navigator).gamepadInputEmulation = "keyboard"; + } else if (typeof navigator.getGamepads === 'function') { + // Otherwise poll for connected gamepads and use that for input. + this.watchForGamepad(); + } + + this.addKeyboardListeners(); + } + /** + * Detects any connected gamepads and watches for new ones to start + * polling them. This is the entry point for gamepad input handling. + */ + private watchForGamepad() { + const addGamepads = () => { + // it's not an array, originally, and contains undefined elements. + this.gamepads = Array.from(navigator.getGamepads()) + .filter(pad => !!pad) + .map(pad => { + if (/xbox/i.test(pad.id)) { + return new XboxGamepadWrapper(pad); + } + + // We can try, at least ¯\_(ツ)_/¯ and this should + // usually be OK due to remapping. + return new XboxGamepadWrapper(pad); + }); + }; + + addGamepads(); + if (this.gamepads.length > 0) { + this.scheduleGamepadPoll(); + } + + addEventListener('gamepadconnected', () => { + addGamepads(); + cancelAnimationFrame(this.pollRaf); + this.scheduleGamepadPoll(); + }); + } + + /** + * Schedules a new gamepad poll at the next animation frame. + */ + private scheduleGamepadPoll() { + this.pollRaf = requestAnimationFrame(now => this.pollGamepad(now)); + } + + /** + * Checks for input provided by the gamepad and fires off events as + * necessary. It schedules itself again provided that there's still + * a connected gamepad somewhere. + */ + private pollGamepad(now: number) { + for (let i = 0; i < this.gamepads.length; i++) { + const gamepad = this.gamepads[i]; + if (!gamepad.isConnected()) { + this.gamepads.splice(i, 1); + i -= 1; + continue; + } + + if (gamepad.left(now)) { + this.handleDirection(Direction.LEFT); + } else if (gamepad.right(now)) { + this.handleDirection(Direction.RIGHT); + } else if (gamepad.down(now)) { + this.handleDirection(Direction.DOWN); + } else if (gamepad.up(now)) { + this.handleDirection(Direction.UP); + } else if (gamepad.submit(now)) { + this.handleDirection(Direction.SUBMIT); + } else if (gamepad.back(now)) { + this.handleDirection(Direction.BACK); + } + } + + if (this.gamepads.length > 0) { + this.scheduleGamepadPoll(); + } + } + + + private handleDirection(direction: Direction): boolean { + console.log('dir', direction); + return true; + } + + /** + * Handles a key down event, returns whether the event has resulted + * in a navigation and should be cancelled. + */ + private handleKeyDown(keyCode: number): boolean { + let result: boolean; + InputService.DirectionCodes.forEach((codes, direction) => { + if (result === undefined && codes.indexOf(keyCode) !== -1) { + result = this.handleDirection(direction); + } + }); + + return result; + } + + /** + * Adds listeners for keyboard events. + */ + private addKeyboardListeners() { + addEventListener('keydown', ev => { + if (!ev.defaultPrevented && this.handleKeyDown(ev.keyCode)) { + ev.preventDefault(); + } + }); + } +} diff --git a/test/bin/system-config-spec.ts b/test/bin/system-config-spec.ts new file mode 100644 index 0000000..6b77747 --- /dev/null +++ b/test/bin/system-config-spec.ts @@ -0,0 +1,37 @@ +declare var System: any; + +System.config({ + map: { + 'rxjs': 'vendor/rxjs', + + // Angular specific mappings. + '@angular/core': 'vendor/@angular/core/bundles/core.umd.js', + '@angular/core/testing': 'vendor/@angular/core/bundles/core-testing.umd.js', + '@angular/common': 'vendor/@angular/common/bundles/common.umd.js', + '@angular/common/testing': 'vendor/@angular/common/bundles/common-testing.umd.js', + '@angular/compiler': 'vendor/@angular/compiler/bundles/compiler.umd.js', + '@angular/compiler/testing': 'vendor/@angular/compiler/bundles/compiler-testing.umd.js', + '@angular/http': 'vendor/@angular/http/bundles/http.umd.js', + '@angular/http/testing': 'vendor/@angular/http/bundles/http-testing.umd.js', + '@angular/forms': 'vendor/@angular/forms/bundles/forms.umd.js', + '@angular/forms/testing': 'vendor/@angular/forms/bundles/forms-testing.umd.js', + '@angular/router': 'vendor/@angular/router/bundles/router.umd.js', + '@angular/router/testing': 'vendor/@angular/router/bundles/router-testing.umd.js', + '@angular/platform-browser': 'vendor/@angular/platform-browser/bundles/platform-browser.umd.js', + '@angular/platform-browser/testing': + 'vendor/@angular/platform-browser/bundles/platform-browser-testing.umd.js', + '@angular/platform-browser-dynamic': + 'vendor/@angular/platform-browser-dynamic/bundles/platform-browser-dynamic.umd.js', + '@angular/platform-browser-dynamic/testing': + 'vendor/@angular/platform-browser-dynamic/bundles/platform-browser-dynamic-testing.umd.js', + }, + packages: { + // Thirdparty barrels. + 'rxjs': { main: 'index' }, + // Set the default extension for the root package, because otherwise the demo-app can't + // be built within the production mode. Due to missing file extensions. + '.': { + defaultExtension: 'js' + } + } +}); \ No newline at end of file diff --git a/test/karma-shim.js b/test/karma-shim.js new file mode 100644 index 0000000..a893eea --- /dev/null +++ b/test/karma-shim.js @@ -0,0 +1,56 @@ +/*global jasmine, __karma__, window*/ +Error.stackTraceLimit = Infinity; +jasmine.DEFAULT_TIMEOUT_INTERVAL = 3000; + +__karma__.loaded = function () { +}; + +var distPath = '/base/dist/'; + +function isJsFile(path) { + return path.slice(-3) == '.js'; +} + +function isSpecFile(path) { + return path.slice(-8) == '.spec.js'; +} + +function isAppFile(path) { + return isJsFile(path) && path.indexOf('vendor') == -1; +} + +var allSpecFiles = Object.keys(window.__karma__.files) + .filter(isSpecFile) + .filter(isAppFile); + +// Load our SystemJS configuration. +System.config({ + baseURL: distPath, +}); + +// Load and configure the TestComponentBuilder. +System.import(distPath + 'bin/system-config-spec.js').then(function() { + // Load and configure the TestComponentBuilder. + return Promise.all([ + System.import('@angular/core/testing'), + System.import('@angular/platform-browser-dynamic/testing') + ]).then(function (providers) { + var testing = providers[0]; + var testingBrowser = providers[1]; + + jasmine.DEFAULT_TIMEOUT_INTERVAL = 10000; + + testing.TestBed.initTestEnvironment( + testingBrowser.BrowserDynamicTestingModule, + testingBrowser.platformBrowserDynamicTesting()); + }); +}).then(function() { + // Finally, load all spec files. + // This will run the tests directly. + return Promise.all( + allSpecFiles.map(function (moduleName) { + return System.import(moduleName).then(function(module) { + return module; + }); + })); +}).then(__karma__.start, __karma__.error); diff --git a/test/karma.conf.ts b/test/karma.conf.ts new file mode 100644 index 0000000..aedd3c0 --- /dev/null +++ b/test/karma.conf.ts @@ -0,0 +1,98 @@ +import path = require('path'); + +export function config(config: any) { + + config.set({ + + // base path that will be used to resolve all patterns (eg. files, exclude) + basePath: path.join(__dirname, '..'), + + failOnEmptyTestSuite: false, + + // frameworks to use + // available frameworks: https://npmjs.org/browse/keyword/karma-adapter + frameworks: ['jasmine'], + + plugins: [ + require('karma-jasmine'), + require('karma-mocha-reporter'), + require('karma-coverage'), + require('karma-chrome-launcher'), + require('karma-firefox-launcher'), + ], + + // list of files / patterns to load in the browser + files: [ + { pattern: 'dist/vendor/core-js/client/core.js', included: true, watched: false }, + { pattern: 'dist/vendor/systemjs/dist/system-polyfills.js', included: true, watched: false }, + { pattern: 'dist/vendor/systemjs/dist/system.src.js', included: true, watched: false }, + + { pattern: 'dist/vendor/zone.js/dist/zone.js', included: true, watched: false }, + { pattern: 'dist/vendor/zone.js/dist/sync-test.js', included: true, watched: false }, + { pattern: 'dist/vendor/zone.js/dist/async-test.js', included: true, watched: false }, + { pattern: 'dist/vendor/zone.js/dist/proxy.js', include: true, watched: false }, + { pattern: 'dist/vendor/zone.js/dist/fake-async-test.js', included: true, watched: false }, + { pattern: 'dist/vendor/zone.js/dist/long-stack-trace-zone.js', include: true, watched: false }, + { pattern: 'dist/vendor/zone.js/dist/jasmine-patch.js', included: true, watched: false }, + + { pattern: 'dist/vendor/hammerjs/hammer.min.js', included: true, watched: false }, + + { pattern: 'test/karma-shim.js', included: true, watched: false }, + + { pattern: 'dist/bin/system-config-spec.js', included: true, watched: false }, + + // paths loaded via module imports + { pattern: 'dist/**/*.js', included: false, watched: true }, + ], + + proxies: { + // required for component assets fetched by Angular's compiler + '/components/': '/base/dist/components/', + '/core/': '/base/dist/core/', + }, + + // list of files to exclude + exclude: [], + + coverageReporter: { + dir: 'coverage/', + reporters: [ + {type: 'text-summary'}, + {type: 'html'} + ] + }, + + // preprocess matching files before serving them to the browser + // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor + preprocessors: {}, + + // test results reporter to use + // possible values: 'dots', 'progress', 'mocha' + // available reporters: https://npmjs.org/browse/keyword/karma-reporter + reporters: ['mocha', 'coverage'], + + port: 9876, + colors: true, + + // level of logging + // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG + logLevel: config.LOG_INFO, + + + // enable / disable watching file and executing tests whenever any file changes + autoWatch: false, + + // start these browsers + // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher + browsers: ['Chrome'], + + browserDisconnectTimeout: 2000000, + browserNoActivityTimeout: 2400000, + captureTimeout: 12000000, + + // Continuous Integration mode + // if true, Karma captures browsers, runs the tests and exits + singleRun: true + }); + +}; diff --git a/test/karma.confloader.js b/test/karma.confloader.js new file mode 100644 index 0000000..e0a61ef --- /dev/null +++ b/test/karma.confloader.js @@ -0,0 +1,28 @@ +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.conf.ts').config; \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..71cbbec --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,30 @@ +{ + "compilerOptions": { + "forceConsistentCasingInFileNames": true, + "allowSyntheticDefaultImports": true, + "experimentalDecorators": true, + "emitDecoratorMetadata": true, + "moduleResolution": "node", + "noImplicitAny": true, + "strictNullChecks": false, + "module": "commonjs", + "sourceMap": true, + "target": "es5", + "pretty": true, + "declaration": true, + "lib": [ + "dom", + "es2015" + ], + "typeRoots": [ + "node_modules/@types" + ], + "types": [ + "jasmine", + "node" + ] + }, + "exclude": [ + "node_modules" + ] +} diff --git a/tslint.json b/tslint.json new file mode 100644 index 0000000..c419d5e --- /dev/null +++ b/tslint.json @@ -0,0 +1,64 @@ +{ + "extends": "tslint-microsoft-contrib", + "rulesDirectory": [ + "node_modules/codelyzer", + "node_modules/tslint-microsoft-contrib" + ], + "rules": { + "no-increment-decrement": false, + "no-multiline-string": false, + "no-for-in-array": false, + "restrict-plus-operands": false, + "no-constructor-vars": false, + "no-relative-imports": false, + "mocha-no-side-effect-code": false, + "missing-jsdoc": false, + "export-name": false, + "no-stateless-class": false, + "no-any": false, + "trailing-comma": [true, {"multiline": "always", "singleline": "never"}], + + // Angular + "directive-selector-name": [ + true, + "camelCase" + ], + "component-selector-name": [ + true, + "kebab-case" + ], + "directive-selector-type": [ + true, + "attribute" + ], + "component-selector-type": [ + true, + "element" + ], + "directive-selector-prefix": [ + true, + "bui" + ], + "component-selector-prefix": [ + true, + "bui" + ], + "use-input-property-decorator": true, + "use-output-property-decorator": true, + "use-host-property-decorator": true, + "no-attribute-parameter-decorator": true, + "no-input-rename": false, + "no-output-rename": false, + "no-forward-ref": false, + "use-life-cycle-interface": true, + "use-pipe-transform-interface": true, + "pipe-naming": [ + true, + "camelCase", + "bui" + ], + "component-class-suffix": true, + "directive-class-suffix": true, + "import-destructuring-spacing": true + } +}