From 166f6b1b1e73a066c506ff45a9f76726594f7c53 Mon Sep 17 00:00:00 2001 From: Oren Farhi Date: Mon, 8 Jan 2018 21:09:08 +0200 Subject: [PATCH] [RELEASE] v0.1.0 --- .gitignore | 3 +- .travis.yml | 2 +- CHANGELOG.md | 5 + LICENSE | 2 +- build.js | 110 +++++++++-------- license-banner.txt | 23 ++++ ngx-youtube-player-0.0.52.tgz | Bin 0 -> 22788 bytes index.ts => ngx-youtube-player.ts | 0 package.json | 84 +++++++------ public_api.ts | 2 +- rollup.config.js | 55 ++++++--- rollup.es.config.js | 23 ++++ scripts/map-sources.js | 9 -- spec.bundle.js | 37 +++--- src/modules/youtube-player.component.ts | 4 +- src/services/youtube-player.service.ts | 82 +++++-------- tests/services/youtube-player.service.spec.ts | 114 +++++++++++++++++- tsconfig-build.json | 60 +++++---- tslint.json | 9 +- 19 files changed, 404 insertions(+), 220 deletions(-) create mode 100644 license-banner.txt create mode 100644 ngx-youtube-player-0.0.52.tgz rename index.ts => ngx-youtube-player.ts (100%) create mode 100644 rollup.es.config.js delete mode 100644 scripts/map-sources.js diff --git a/.gitignore b/.gitignore index 2178ea4..3f8d0cc 100644 --- a/.gitignore +++ b/.gitignore @@ -52,4 +52,5 @@ src/ngFactory *.metadata.json dist -examples/webpack/node_modules \ No newline at end of file +examples/webpack/node_modules +examples/ \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index 0430783..d2aec17 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,7 +8,7 @@ addons: - google-chrome-stable language: node_js node_js: - - stable + - "8" before_install: - npm i npm@^4 -g install: diff --git a/CHANGELOG.md b/CHANGELOG.md index 634a66d..bcbfd28 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +## v 0.1.0 (2018/01/08) +* [UPGRADE] - official support for Angular 5 +* [UPDATE] - updated repo to ngx-youtube-player +* [REFACTOR] - added more unit tests and increased coverage + ## v 0.0.51 (2017/12/29) * [FIX] - fixes #27 - youtube player iframe api loaded with each instance diff --git a/LICENSE b/LICENSE index a116064..57e43e3 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2017 Oren Farhi +Copyright (c) 2018 Oren Farhi Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/build.js b/build.js index 0eadc00..cff8408 100644 --- a/build.js +++ b/build.js @@ -1,61 +1,73 @@ "use strict"; -require('shelljs/global'); +const shell = require('shelljs'); const chalk = require('chalk'); + const PACKAGE = `ngx-youtube-player`; -module.exports.PACKAGE = PACKAGE; const NPM_DIR = `dist`; -const MODULES_DIR = `${NPM_DIR}/modules`; +const ESM2015_DIR = `${NPM_DIR}/esm2015`; +const ESM5_DIR = `${NPM_DIR}/esm5`; const BUNDLES_DIR = `${NPM_DIR}/bundles`; +const OUT_DIR_ESM5 = `${NPM_DIR}/package/esm5`; -echo('Start building...'); +shell.echo(`Start building...`); -rm(`-Rf`, `${NPM_DIR}/*`); -mkdir(`-p`, `./${MODULES_DIR}`); -mkdir(`-p`, `./${BUNDLES_DIR}`); +shell.rm(`-Rf`, `${NPM_DIR}/*`); +shell.mkdir(`-p`, `./${ESM2015_DIR}`); +shell.mkdir(`-p`, `./${ESM5_DIR}`); +shell.mkdir(`-p`, `./${BUNDLES_DIR}`); /* TSLint with Codelyzer */ // https://github.com/palantir/tslint/blob/master/src/configs/recommended.ts // https://github.com/mgechev/codelyzer -echo(`Start TSLint`); -exec(`tslint --project ./tsconfig.json --type-check ./src/**/*.ts`); -echo(chalk.green(`TSLint completed`)); - -/* Aot compilation: ES2015 sources */ -echo(`Start AoT compilation`); -exec(`ngc -p tsconfig-build.json`); -echo(chalk.green(`AoT compilation completed`)); - -/* Creates bundles: ESM/ES5 and UMD bundles */ -echo(`Start bundling`); -echo(`Rollup package`); -exec(`rollup -i ${NPM_DIR}/${PACKAGE}.js -o ${MODULES_DIR}/${PACKAGE}.js --sourcemap`, {silent: true}); -exec(`node scripts/map-sources -f ${MODULES_DIR}/${PACKAGE}.js`); - -echo(`Downleveling ES2015 to ESM/ES5`); -cp(`${MODULES_DIR}/${PACKAGE}.js`, `${MODULES_DIR}/${PACKAGE}.es5.ts`); -exec(`tsc ${MODULES_DIR}/${PACKAGE}.es5.ts --target es5 --module es2015 --noLib --sourceMap`, {silent: true}); -exec(`node scripts/map-sources -f ${MODULES_DIR}/${PACKAGE}.es5.js`); -rm(`-f`, `${MODULES_DIR}/${PACKAGE}.es5.ts`); - -echo(`Run Rollup conversion on package`); -exec(`rollup -c rollup.config.js --sourcemap`, {silent: true}); -exec(`node scripts/map-sources -f ${BUNDLES_DIR}/${PACKAGE}.umd.js`); - -echo(`Minifying`); -cd(`${BUNDLES_DIR}`); -exec(`uglifyjs -c --screw-ie8 --comments -o ${PACKAGE}.umd.min.js --source-map ${PACKAGE}.umd.min.js.map --source-map-include-sources ${PACKAGE}.umd.js`, {silent: true}); -exec(`node ../../scripts/map-sources -f ${PACKAGE}.umd.min.js`); -cd(`..`); -cd(`..`); - -echo(chalk.green(`Bundling completed`)); - -rm(`-Rf`, `${NPM_DIR}/*.js`); -rm(`-Rf`, `${NPM_DIR}/*.js.map`); -rm(`-Rf`, `${NPM_DIR}/src/**/*.js`); -rm(`-Rf`, `${NPM_DIR}/src/**/*.js.map`); - -cp(`-Rf`, [`package.json`, `LICENSE`, `README.md`], `${NPM_DIR}`); - -echo(chalk.green(`End building`)); +shell.echo(`Start TSLint`); +shell.exec(`tslint -c tslint.json -t stylish src/**/*.ts`); +shell.echo(chalk.green(`TSLint completed`)); + +/* AoT compilation */ +shell.echo(`Start AoT compilation`); +if (shell.exec(`ngc -p tsconfig-build.json`).code !== 0) { + shell.echo(chalk.red(`Error: AoT compilation failed`)); + shell.exit(1); +} +shell.echo(chalk.green(`AoT compilation completed`)); + +/* BUNDLING PACKAGE */ +shell.echo(`Start bundling`); +shell.echo(`Rollup package`); +if (shell.exec(`rollup -c rollup.es.config.js -i ${NPM_DIR}/${PACKAGE}.js -o ${ESM2015_DIR}/${PACKAGE}.js`).code !== 0) { + shell.echo(chalk.red(`Error: Rollup package failed`)); + shell.exit(1); +} + +shell.echo(`Produce ESM5 version`); +shell.exec(`ngc -p tsconfig-build.json --target es5 -d false --outDir ${OUT_DIR_ESM5} --importHelpers true --sourceMap`); +if (shell.exec(`rollup -c rollup.es.config.js -i ${OUT_DIR_ESM5}/${PACKAGE}.js -o ${ESM5_DIR}/${PACKAGE}.js`).code !== 0) { + shell.echo(chalk.red(`Error: ESM5 version failed`)); + shell.exit(1); +} + +shell.echo(`Run Rollup conversion on package`); +if (shell.exec(`rollup -c rollup.config.js -i ${ESM5_DIR}/${PACKAGE}.js -o ${BUNDLES_DIR}/${PACKAGE}.umd.js`).code !== 0) { + shell.echo(chalk.red(`Error: Rollup conversion failed`)); + shell.exit(1); +} + +shell.echo(`Minifying`); +shell.cd(`${BUNDLES_DIR}`); +shell.exec(`uglifyjs ${PACKAGE}.umd.js -c --comments -o ${PACKAGE}.umd.min.js --source-map "filename='${PACKAGE}.umd.min.js.map', includeSources"`); +shell.cd(`..`); +shell.cd(`..`); + +shell.echo(chalk.green(`Bundling completed`)); + +shell.rm(`-Rf`, `${NPM_DIR}/package`); +shell.rm(`-Rf`, `${NPM_DIR}/node_modules`); +shell.rm(`-Rf`, `${NPM_DIR}/*.js`); +shell.rm(`-Rf`, `${NPM_DIR}/*.js.map`); +shell.rm(`-Rf`, `${NPM_DIR}/src/**/*.js`); +shell.rm(`-Rf`, `${NPM_DIR}/src/**/*.js.map`); + +shell.cp(`-Rf`, [`package.json`, `LICENSE`, `README.md`], `${NPM_DIR}`); + +shell.echo(chalk.green(`End building`)); \ No newline at end of file diff --git a/license-banner.txt b/license-banner.txt new file mode 100644 index 0000000..eff3c15 --- /dev/null +++ b/license-banner.txt @@ -0,0 +1,23 @@ +/** + * @license ngx-youtube-library + * Copyright (c) 2018 Oren Farhi + * MIT license + + 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. + */ \ No newline at end of file diff --git a/ngx-youtube-player-0.0.52.tgz b/ngx-youtube-player-0.0.52.tgz new file mode 100644 index 0000000000000000000000000000000000000000..8aa93e0ffa75d81536e92f851ee0318b5b1924a4 GIT binary patch literal 22788 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 literal 0 HcmV?d00001 diff --git a/index.ts b/ngx-youtube-player.ts similarity index 100% rename from index.ts rename to ngx-youtube-player.ts diff --git a/package.json b/package.json index 4e03329..a2de135 100644 --- a/package.json +++ b/package.json @@ -1,13 +1,14 @@ { "name": "ngx-youtube-player", - "version": "0.0.51", + "version": "0.1.0", "description": "A Powerful Youtube Player Component for Angular", "main": "./bundles/ngx-youtube-player.umd.js", - "module": "./modules/ngx-youtube-player.es5.js", - "es2015": "./modules/ngx-youtube-player.js", + "module": "./esm5/ngx-youtube-player.js", + "es2015": "./esm2015/ngx-youtube-player.js", "scripts": { "build": "node build.js", "test": "karma start", + "test:watch": "karma start --single-run=false", "pack-lib": "npm pack ./dist", "publish-lib": "npm publish ./dist", "publish-lib:next": "npm publish --tag next ./dist", @@ -18,12 +19,12 @@ "author": "Oren Farhi (orizens.com)", "repository": { "type": "git", - "url": "https://github.com/orizens/ng2-youtube-player.git" + "url": "https://github.com/orizens/ngx-youtube-player.git" }, "bugs": { - "url": "https://github.com/orizens/ng2-youtube-player/issues" + "url": "https://github.com/orizens/ngx-youtube-player/issues" }, - "homepage": "https://github.com/orizens/ng2-youtube-player", + "homepage": "https://github.com/orizens/ngx-youtube-player", "keywords": [ "angular", "javascript", @@ -32,46 +33,51 @@ "youtube player" ], "license": "MIT", + "dependencies": { + "tslib": "^1.7.1" + }, "peerDependencies": { - "@angular/common": ">= 4.0.0", - "@angular/core": ">= 4.0.0" + "@angular/common": ">= 5.0.0", + "@angular/core": ">= 5.0.0" }, "devDependencies": { - "@angular/animations": "^4.0.0", - "@angular/common": "^4.0.0", - "@angular/compiler": "^4.0.0", - "@angular/compiler-cli": "^4.0.0", - "@angular/core": "^4.0.0", - "@angular/http": "^4.0.0", - "@angular/platform-browser": "^4.0.0", - "@angular/platform-browser-dynamic": "^4.0.0", - "@angular/platform-server": "^4.0.0", - "@types/jasmine": "2.5.46", - "@types/node": "7.0.10", + "@angular/animations": "5.0.0", + "@angular/common": "5.0.0", + "@angular/compiler": "5.0.0", + "@angular/compiler-cli": "5.0.0", + "@angular/core": "5.0.0", + "@angular/platform-browser": "5.0.0", + "@angular/platform-browser-dynamic": "5.0.0", + "@angular/platform-server": "5.0.0", + "@types/jasmine": "2.6.1", + "@types/node": "8.0.47", "@types/youtube": "0.0.29", - "chalk": "1.1.3", - "codelyzer": "3.0.0-beta.4", + "chalk": "2.3.0", + "codelyzer": "4.0.0", "compodoc": "0.0.41", - "core-js": "2.4.1", - "jasmine-core": "2.5.2", - "karma": "1.5.0", - "karma-chrome-launcher": "2.0.0", + "core-js": "2.5.1", + "jasmine-core": "2.8.0", + "karma": "1.7.1", + "karma-chrome-launcher": "2.2.0", "karma-jasmine": "1.1.0", "karma-sourcemap-loader": "0.3.7", - "karma-spec-reporter": "0.0.30", - "karma-webpack": "2.0.3", + "karma-spec-reporter": "0.0.31", + "karma-webpack": "2.0.5", + "karma-coverage-istanbul-reporter": "1.3.0", + "istanbul-instrumenter-loader": "3.0.0", "reflect-metadata": "0.1.10", - "rollup": "0.41.6", - "rxjs": "5.2.0", - "shelljs": "0.7.7", - "sorcery": "0.10.0", - "ts-helpers": "1.1.2", - "ts-loader": "2.0.3", - "tslint": "4.5.1", - "typescript": "^2.1.5", - "uglify-js": "2.8.15", - "webpack": "2.3.1", - "yargs": "7.0.2", - "zone.js": "0.8.5" + "rollup": "0.50.0", + "rollup-plugin-node-resolve": "3.0.0", + "rollup-plugin-sourcemaps": "0.4.2", + "rollup-plugin-license": "0.5.0", + "rxjs": "5.5.2", + "shelljs": "0.7.8", + "source-map-loader": "0.2.3", + "ts-loader": "3.1.1", + "tslint": "5.8.0", + "typescript": "2.4.2", + "uglify-js": "3.1.6", + "webpack": "3.8.1", + "zone.js": "0.8.18" } } \ No newline at end of file diff --git a/public_api.ts b/public_api.ts index 6be2915..252fdda 100644 --- a/public_api.ts +++ b/public_api.ts @@ -1,7 +1,7 @@ /** * Angular library starter. * Build an Angular library compatible with AoT compilation & Tree shaking. - * Written by Roberto Simonetti. + * Copyright Roberto Simonetti. * MIT license. * https://github.com/robisim74/angular-library-starter */ diff --git a/rollup.config.js b/rollup.config.js index 5546cb1..2d273f1 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -1,20 +1,39 @@ +import resolve from 'rollup-plugin-node-resolve'; +import sourcemaps from 'rollup-plugin-sourcemaps'; + +/** + * Add here external dependencies that actually you use. + * + * About RxJS + * Each RxJS functionality that you use in the library must be added as external dependency. + * - For main classes use 'Rx': + * e.g. import { Observable } from 'rxjs/Observable'; => 'rxjs/Observable': 'Rx' + * - For observable methods use 'Rx.Observable': + * e.g. import 'rxjs/add/observable/merge'; => 'rxjs/add/observable/merge': 'Rx.Observable' + * or for lettable operators: + * e.g. import { merge } from 'rxjs/observable/merge'; => 'rxjs/observable/merge': 'Rx.Observable' + * - For operators use 'Rx.Observable.prototype': + * e.g. import 'rxjs/add/operator/map'; => 'rxjs/add/operator/map': 'Rx.Observable.prototype' + * or for lettable operators: + * e.g. import { map } from 'rxjs/operators'; => 'rxjs/operators': 'Rx.Observable.prototype' + */ +const globals = { + '@angular/core': 'ng.core', + '@angular/common': 'ng.common', + 'rxjs/Observable': 'Rx', + 'rxjs/ReplaySubject': 'Rx', + 'rxjs/Observer': 'Rx' +}; + export default { - entry: './dist/modules/ngx-youtube-player.es5.js', - dest: './dist/bundles/ngx-youtube-player.umd.js', - format: 'umd', - exports: 'named', - moduleName: 'ng.ngxYoutubePlayer', - external: [ - '@angular/core', - '@angular/common', - 'rxjs/Observable', - 'rxjs/Observer' - ], - globals: { - '@angular/core': 'ng.core', - '@angular/common': 'ng.common', - 'rxjs/Observable': 'Rx', - 'rxjs/Observer': 'Rx' - }, - onwarn: () => { return } + external: Object.keys(globals), + plugins: [resolve(), sourcemaps()], + onwarn: () => { return }, + output: { + format: 'umd', + name: 'ng.ngxYoutubePlayer', + globals: globals, + sourcemap: true, + exports: 'named' + } } \ No newline at end of file diff --git a/rollup.es.config.js b/rollup.es.config.js new file mode 100644 index 0000000..ac263fe --- /dev/null +++ b/rollup.es.config.js @@ -0,0 +1,23 @@ +import sourcemaps from 'rollup-plugin-sourcemaps'; +import license from 'rollup-plugin-license'; + +const path = require('path'); + +export default { + output: { + format: 'es', + sourcemap: true + }, + plugins: [ + sourcemaps(), + license({ + sourceMap: true, + + banner: { + file: path.join(__dirname, 'license-banner.txt'), + encoding: 'utf-8', + } + }) + ], + onwarn: () => { return } +} \ No newline at end of file diff --git a/scripts/map-sources.js b/scripts/map-sources.js deleted file mode 100644 index 1d53166..0000000 --- a/scripts/map-sources.js +++ /dev/null @@ -1,9 +0,0 @@ -const sorcery = require('sorcery'); - -var argv = require('yargs') - .alias('f', 'file') - .argv; - -sorcery.load(argv.file).then(function(chain) { - chain.write(); -}); diff --git a/spec.bundle.js b/spec.bundle.js index b1890cf..3bb2285 100644 --- a/spec.bundle.js +++ b/spec.bundle.js @@ -1,28 +1,29 @@ -Error.stackTraceLimit = Infinity; +import 'core-js'; +import 'zone.js/dist/zone'; +import 'zone.js/dist/long-stack-trace-zone'; +import 'zone.js/dist/proxy.js'; +import 'zone.js/dist/sync-test'; +import 'zone.js/dist/jasmine-patch'; +import 'zone.js/dist/async-test'; +import 'zone.js/dist/fake-async-test'; -require('core-js'); -require('ts-helpers'); -require('zone.js/dist/zone'); -require('zone.js/dist/long-stack-trace-zone'); -require('zone.js/dist/proxy'); -require('zone.js/dist/sync-test'); -require('zone.js/dist/jasmine-patch'); -require('zone.js/dist/async-test'); -require('zone.js/dist/fake-async-test'); -require('rxjs/Rx'); +import { getTestBed } from '@angular/core/testing'; +import { + BrowserDynamicTestingModule, + platformBrowserDynamicTesting +} from '@angular/platform-browser-dynamic/testing'; -var testing = require('@angular/core/testing'); -var browser = require('@angular/platform-browser-dynamic/testing'); +import 'rxjs'; -testing.TestBed.initTestEnvironment( - browser.BrowserDynamicTestingModule, - browser.platformBrowserDynamicTesting() +getTestBed().initTestEnvironment( + BrowserDynamicTestingModule, + platformBrowserDynamicTesting() ); -var testContext = require.context('./tests', true, /\.spec\.ts/); +const testContext = require.context('./tests', true, /\.spec\.ts/); function requireAll(requireContext) { return requireContext.keys().map(requireContext); } -var modules = requireAll(testContext); +const modules = requireAll(testContext); \ No newline at end of file diff --git a/src/modules/youtube-player.component.ts b/src/modules/youtube-player.component.ts index 3463651..9de4f94 100644 --- a/src/modules/youtube-player.component.ts +++ b/src/modules/youtube-player.component.ts @@ -40,9 +40,9 @@ export class YoutubePlayerComponent implements AfterContentInit { public playerService: YoutubePlayerService, private elementRef: ElementRef, private renderer: Renderer2 - ) {} + ) { } - ngAfterContentInit () { + ngAfterContentInit() { const htmlId = this.playerService.generateUniqueId(); const playerSize = { height: this.height, width: this.width }; const container = this.renderer.selectRootElement('#yt-player-ngx-component'); diff --git a/src/services/youtube-player.service.ts b/src/services/youtube-player.service.ts index d3f7683..8c41a4e 100644 --- a/src/services/youtube-player.service.ts +++ b/src/services/youtube-player.service.ts @@ -1,40 +1,39 @@ -import { Http, URLSearchParams, Response } from '@angular/http'; import { Injectable, NgZone, EventEmitter } from '@angular/core'; import { ReplaySubject } from 'rxjs/ReplaySubject'; import { IPlayerApiScriptOptions, IPlayerOutputs, IPlayerSize } from '../models'; -@Injectable() -export class YoutubePlayerService { - static get win() { - return window; - } +export function win() { + return window; +} - static get YT() { - return YoutubePlayerService.win['YT']; - } +export function YT() { + return win()['YT']; +} - static get Player() { - return YoutubePlayerService.YT.Player; - } +export function Player() { + return YT().Player; +} + +export const defaultSizes = { + height: 270, + width: 367 +}; +@Injectable() +export class YoutubePlayerService { api: ReplaySubject; private ytApiLoaded = false; - private isFullscreen: boolean = false; - private defaultSizes = { - height: 270, - width: 367 - }; constructor(private zone: NgZone) { this.createApi(); } loadPlayerApi(options: IPlayerApiScriptOptions) { - const doc = YoutubePlayerService.win.document; + const doc = win().document; if (!this.ytApiLoaded) { this.ytApiLoaded = true; - let playerApiScript = doc.createElement("script"); + const playerApiScript = doc.createElement("script"); playerApiScript.type = "text/javascript"; playerApiScript.src = `${options.protocol}://www.youtube.com/iframe_api`; doc.body.appendChild(playerApiScript); @@ -45,7 +44,7 @@ export class YoutubePlayerService { elementId: string, outputs: IPlayerOutputs, sizes: IPlayerSize, videoId = '', playerVars: YT.PlayerVars) { const createPlayer = () => { - if (YoutubePlayerService.Player) { + if (Player) { this.createPlayer(elementId, outputs, sizes, videoId, playerVars); } }; @@ -71,7 +70,7 @@ export class YoutubePlayerService { const isPlayerReady: any = player && player.getPlayerState; const playerState = isPlayerReady ? player.getPlayerState() : {}; const isPlayerPlaying = isPlayerReady - ? playerState !== YT.PlayerState.ENDED && playerState !== YT.PlayerState.PAUSED + ? playerState !== YT().PlayerState.ENDED && playerState !== YT().PlayerState.PAUSED : false; return isPlayerPlaying; } @@ -79,53 +78,34 @@ export class YoutubePlayerService { createPlayer( elementId: string, outputs: IPlayerOutputs, sizes: IPlayerSize, videoId = '', playerVars: YT.PlayerVars = {}) { - const service = this; const playerSize = { - height: sizes.height || this.defaultSizes.height, - width: sizes.width || this.defaultSizes.width + height: sizes.height || defaultSizes.height, + width: sizes.width || defaultSizes.width }; - return new YoutubePlayerService.Player(elementId, Object.assign({}, playerSize, { + const ytPlayer = Player(); + return new ytPlayer(elementId, { + ...playerSize, events: { onReady: (ev: YT.PlayerEvent) => { this.zone.run(() => outputs.ready && outputs.ready.next(ev.target)); }, onStateChange: (ev: YT.PlayerEvent) => { this.zone.run(() => outputs.change && outputs.change.next(ev)); - // this.zone.run(() => onPlayerStateChange(ev)); } }, - videoId, playerVars, - })); - - // TODO: DEPRECATE? - // function onPlayerStateChange (event: any) { - // const state = event.data; - // const PlayerState = YoutubePlayerService.YT.PlayerState; - // // play the next song if its not the end of the playlist - // // should add a "repeat" feature - // if (state === PlayerState.ENDED) { - - // } - - // if (state === PlayerState.PAUSED) { - // // service.playerState = PlayerState.PAUSED; - // } - // if (state === PlayerState.PLAYING) { - // // service.playerState = PlayerState.PLAYING; - // } - // } + videoId, + }); } toggleFullScreen(player: YT.Player, isFullScreen: boolean | null | undefined) { - let { height, width } = this.defaultSizes; + let { height, width } = defaultSizes; if (!isFullScreen) { height = window.innerHeight; width = window.innerWidth; } player.setSize(width, height); - // TODO: dispatch event } // adpoted from uid @@ -137,10 +117,10 @@ export class YoutubePlayerService { private createApi() { this.api = new ReplaySubject(1); const onYouTubeIframeAPIReady = () => { - if (YoutubePlayerService.win) { - this.api.next( YoutubePlayerService.YT); + if (win()) { + this.api.next(YT()); } }; - YoutubePlayerService.win['onYouTubeIframeAPIReady'] = onYouTubeIframeAPIReady; + win()['onYouTubeIframeAPIReady'] = onYouTubeIframeAPIReady; } } diff --git a/tests/services/youtube-player.service.spec.ts b/tests/services/youtube-player.service.spec.ts index 5226951..1252657 100644 --- a/tests/services/youtube-player.service.spec.ts +++ b/tests/services/youtube-player.service.spec.ts @@ -3,12 +3,16 @@ import { inject } from '@angular/core/testing'; -import { YoutubePlayerService } from '../../src/services/youtube-player.service'; +import { YoutubePlayerService, defaultSizes, win, Player, YT } from '../../src/services/youtube-player.service'; import { ReplaySubject } from 'rxjs/ReplaySubject'; const zone = jasmine.createSpyObj('zone', ['run']); describe('YoutubePlayerService', () => { + global['YT'] = { + Player: jasmine.createSpy('YTPlayer').and.callFake((id: string, params: any) => params), + PlayerState: 1 + } it('should create an instance of YoutubePlayerService', () => { const service = new YoutubePlayerService(zone); @@ -23,4 +27,112 @@ describe('YoutubePlayerService', () => { const expected = jasmine.any(ReplaySubject); expect(actual).toEqual(expected); }); + + it('should emit the YT player when youtube iframe api is ready', () => { + const service = new YoutubePlayerService(zone); + const actual = service.api; + spyOn(service.api, 'next'); + win()['onYouTubeIframeAPIReady'](); + expect(service.api.next).toHaveBeenCalledWith(global['YT']); + }); + + it('should generate a unique id', () => { + const service = new YoutubePlayerService(zone); + const actual = service.generateUniqueId(); + expect(actual).toBeDefined(); + expect(actual.length).toBeGreaterThan(1); + }); + + describe('YT Player Creation', () => { + let params, service, actual; + + beforeEach(() => { + params = { + id: 'testing-id', + outputs: {}, + playerVars: {}, + sizes: { + height: 1000, + width: 2000 + }, + videoId: '', + }; + service = new YoutubePlayerService(zone); + actual = service.createPlayer(params.id, params.outputs, params.sizes, params.videoId, params.playerVars); + }); + + it('should create a player using YT Api', () => { + const expected = actual; + expect(Player()).toHaveBeenCalledWith(params.id, expected); + }); + + it('should create a player with given sizes', () => { + const expected = params.sizes; + expect(actual.height).toBeDefined(expected.height); + expect(actual.width).toBeDefined(expected.width); + }); + + it('should create a player with default sizes', () => { + const expected = defaultSizes; + expect(actual.height).toBeDefined(expected.height); + expect(actual.width).toBeDefined(expected.width); + }); + }); + + describe('YT Player functionality exposure', () => { + let player; + + beforeEach(() => { + player = jasmine.createSpyObj('ytplayer', [ + 'playVideo', + 'pauseVideo', + 'loadVideoById', + 'getPlayerState', + 'setSize' + ]); + }) + it('should play the video', () => { + const service = new YoutubePlayerService(zone); + service.play(player); + expect(player.playVideo).toHaveBeenCalledTimes(1); + }); + it('should pause the video', () => { + const service = new YoutubePlayerService(zone); + service.pause(player); + expect(player.pauseVideo).toHaveBeenCalledTimes(1); + }); + it('should tell a video is playing', () => { + const service = new YoutubePlayerService(zone); + const media = { id: 'testing' } + service.playVideo(media, player); + expect(player.loadVideoById).toHaveBeenCalledTimes(1); + expect(player.playVideo).toHaveBeenCalledTimes(1); + }); + it('should tell a video is playing using the player state', () => { + const service = new YoutubePlayerService(zone); + const media = { id: 'testing' } + service.isPlaying(player); + expect(player.getPlayerState).toHaveBeenCalledTimes(1); + }); + it('should tell a video is NOT playing', () => { + const service = new YoutubePlayerService(zone); + const media = { id: 'testing' } + const actual = service.isPlaying({} as YT.Player); + const expected = false; + expect(player.getPlayerState).not.toHaveBeenCalled(); + expect(actual).toBe(expected); + }); + it('should setSize to defaults when in fullscreen', () => { + const service = new YoutubePlayerService(zone); + const actual = service.toggleFullScreen(player, true); + expect(player.setSize).toHaveBeenCalledTimes(1); + }); + it('should setSize to fullscreen when NOT in fullscreen', () => { + const service = new YoutubePlayerService(zone); + global['innerHeight'] = 1000; + global['innerWidth'] = 2000; + const actual = service.toggleFullScreen(player, false); + expect(player.setSize).toHaveBeenCalledWith(global['innerWidth'], global['innerHeight']); + }); + }); }); diff --git a/tsconfig-build.json b/tsconfig-build.json index 926ee7e..c4008f5 100644 --- a/tsconfig-build.json +++ b/tsconfig-build.json @@ -1,29 +1,39 @@ { - "compilerOptions": { - "baseUrl": ".", - "declaration": true, - "experimentalDecorators": true, - "module": "es2015", - "moduleResolution": "node", - "outDir": "dist", - "rootDir": ".", - "sourceMap": true, - "inlineSources": true, - "target": "es2015", - "skipLibCheck": true, - "lib": [ - "es2015", - "dom" - ] + "compilerOptions": { + "rootDir": ".", + "baseUrl": ".", + "paths": { + "@angular/*": [ + "node_modules/@angular/*" + ] }, - "files": [ - "public_api.ts" + "outDir": "dist", + "declaration": true, + "strict": true, + "noImplicitAny": false, + "moduleResolution": "node", + "module": "es2015", + "target": "es2015", + "lib": [ + "es2015", + "dom" ], - "angularCompilerOptions": { - "skipTemplateCodegen": true, - "annotateForClosureCompiler": true, - "strictMetadataEmit": true, - "flatModuleOutFile": "ngx-youtube-player.js", - "flatModuleId": "ngx-youtube-player" - } + "skipLibCheck": true, + "types": [], + "experimentalDecorators": true, + "emitDecoratorMetadata": true, + "sourceMap": true, + "inlineSources": true + }, + "files": [ + "public_api.ts", + "node_modules/zone.js/dist/zone.js.d.ts" + ], + "angularCompilerOptions": { + "skipTemplateCodegen": true, + "annotateForClosureCompiler": true, + "strictMetadataEmit": true, + "flatModuleOutFile": "ngx-youtube-player.js", + "flatModuleId": "ngx-youtube-player" + } } \ No newline at end of file diff --git a/tslint.json b/tslint.json index 7973ddb..08bbe1c 100644 --- a/tslint.json +++ b/tslint.json @@ -31,6 +31,7 @@ "use-output-property-decorator": true, "use-host-property-decorator": true, "no-attribute-parameter-decorator": true, + "no-any": false, "no-input-rename": true, "no-output-rename": true, "no-forward-ref": true, @@ -41,9 +42,6 @@ true, "Component" ], - "templates-use-public": true, - "no-access-missing-member": true, - "invoke-injectable": true, "ordered-imports": [ false ], @@ -53,6 +51,9 @@ "trailing-comma": [ false ], - "member-access": [false, "check-accessor"] + "member-access": [ + false, + "check-accessor" + ] } } \ No newline at end of file