Skip to content

Commit

Permalink
feat(youtube-player): initialize component and infrastructure (angula…
Browse files Browse the repository at this point in the history
  • Loading branch information
Nathan Tate authored and jelbourn committed Jul 31, 2019
1 parent 3ec531b commit b8c9da6
Show file tree
Hide file tree
Showing 26 changed files with 622 additions and 1 deletion.
3 changes: 3 additions & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,9 @@
/src/material/core/typography/** @crisbeto
/src/material/core/util/** @jelbourn

# Miscellaneous components
/src/youtube-player/** @nathantate

# CDK
/src/cdk/* @jelbourn
/src/cdk/a11y/** @jelbourn @devversion
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
"@angular/elements": "^8.1.0",
"@angular/forms": "^8.1.0",
"@angular/platform-browser": "^8.1.0",
"@types/youtube": "^0.0.38",
"@webcomponents/custom-elements": "^1.1.0",
"core-js": "^2.6.1",
"material-components-web": "^3.0.0",
Expand Down
1 change: 1 addition & 0 deletions packages.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ ROLLUP_GLOBALS = {
"@angular/cdk-experimental": "ng.cdkExperimental",
"@angular/material": "ng.material",
"@angular/material-experimental": "ng.materialExperimental",
"@angular/youtube-player": "ng.youtubePlayer",
}

# Rollup globals for cdk subpackages in the form of, e.g., {"@angular/cdk/table": "ng.cdk.table"}
Expand Down
65 changes: 65 additions & 0 deletions src/youtube-player/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package(default_visibility = ["//visibility:public"])

load("//:packages.bzl", "ROLLUP_GLOBALS")
load(
"//tools:defaults.bzl",
"markdown_to_html",
"ng_module",
"ng_package",
"ng_test_library",
"ng_web_test_suite",
)

ng_module(
name = "youtube-player",
srcs = glob(
["**/*.ts"],
exclude = [
"**/*.spec.ts",
"fake-youtube-player.ts",
],
),
module_name = "@angular/youtube-player",
deps = [
"@npm//@angular/common",
"@npm//@angular/core",
"@npm//@types/youtube",
"@npm//rxjs",
],
)

ng_package(
name = "npm_package",
srcs = ["package.json"],
entry_point = ":public-api.ts",
entry_point_name = "youtube-player",
globals = ROLLUP_GLOBALS,
deps = [":youtube-player"],
)

ng_test_library(
name = "unit_test_sources",
srcs = ["fake-youtube-player.ts"] + glob(
["**/*.spec.ts"],
exclude = ["**/*.e2e.spec.ts"],
),
deps = [
":youtube-player",
"@npm//@angular/platform-browser",
],
)

ng_web_test_suite(
name = "unit_tests",
deps = [":unit_test_sources"],
)

markdown_to_html(
name = "overview",
srcs = [":youtube-player.md"],
)

filegroup(
name = "source-files",
srcs = glob(["**/*.ts"]),
)
Empty file added src/youtube-player/README.md
Empty file.
61 changes: 61 additions & 0 deletions src/youtube-player/fake-youtube-player.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
// A re-creation of YT.PlayerState since enum values cannot be bound to the window
// object.
const playerState = {
UNSTARTED: -1,
ENDED: 0,
PLAYING: 1,
PAUSED: 2,
BUFFERING: 3,
CUED: 5,
};

interface FakeYtNamespace {
playerCtorSpy: jasmine.Spy;
playerSpy: jasmine.SpyObj<YT.Player>;
onPlayerReady: () => void;
namespace: typeof YT;
}

export function createFakeYtNamespace(): FakeYtNamespace {
const playerSpy: jasmine.SpyObj<YT.Player> = jasmine.createSpyObj('Player', [
'getPlayerState', 'destroy', 'cueVideoById', 'loadVideoById', 'pauseVideo', 'stopVideo',
'seekTo', 'isMuted', 'mute', 'unMute', 'getVolume', 'getPlaybackRate',
'getAvailablePlaybackRates', 'getVideoLoadedFraction', 'getPlayerState', 'getCurrentTime',
'getPlaybackQuality', 'getAvailableQualityLevels', 'getDuration', 'getVideoUrl',
'getVideoEmbedCode', 'playVideo', 'setSize', 'setVolume', 'setPlaybackQuality',
'setPlaybackRate', 'addEventListener', 'removeEventListener',
]);

let playerConfig: YT.PlayerOptions | undefined;
const playerCtorSpy = jasmine.createSpy('Player Constructor');
playerCtorSpy.and.callFake((_el: Element, config: YT.PlayerOptions) => {
playerConfig = config;
return playerSpy;
});

const onPlayerReady = () => {
if (!playerConfig) {
throw new Error('Player not initialized before onPlayerReady called');
}

if (playerConfig && playerConfig.events && playerConfig.events.onReady) {
playerConfig.events.onReady({target: playerSpy});
}

for (const [event, callback] of playerSpy.addEventListener.calls.allArgs()) {
if (event === 'onReady') {
callback({target: playerSpy});
}
}
};

return {
playerCtorSpy,
playerSpy,
onPlayerReady,
namespace: {
'Player': playerCtorSpy as unknown as typeof YT.Player,
'PlayerState': playerState,
} as typeof YT,
};
}
1 change: 1 addition & 0 deletions src/youtube-player/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './public-api';
31 changes: 31 additions & 0 deletions src/youtube-player/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
{
"name": "@angular/youtube-player",
"version": "0.0.0-PLACEHOLDER",
"description": "Angular YouTube Player",
"main": "./bundles/youtube-player.umd.js",
"module": "./esm5/youtube-player.es5.js",
"es2015": "./esm2015/youtube-player.js",
"typings": "./youtube-player.d.ts",
"repository": {
"type": "git",
"url": "https://github.com/angular/components.git"
},
"keywords": [
"angular",
"components",
"youtube"
],
"license": "MIT",
"bugs": {
"url": "https://github.com/angular/components/issues"
},
"homepage": "https://github.com/angular/components/tree/master/src/youtube-player#readme",
"dependencies": {
"@types/youtube": "^0.0.38"
},
"peerDependencies": {
"@angular/core": "0.0.0-NG",
"@angular/common": "0.0.0-NG"
},
"sideEffects": false
}
2 changes: 2 additions & 0 deletions src/youtube-player/public-api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './youtube-module';
export * from './youtube-player';
28 changes: 28 additions & 0 deletions src/youtube-player/tsconfig-build.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// TypeScript config file that is used to compile the Material package through Gulp. As the
// long term goal is to switch to Bazel, and we already want to run tests with Bazel, we need to
// ensure the TypeScript build options are the same for Gulp and Bazel. We achieve this by
// extending the generic Bazel build tsconfig which will be used for each entry-point.
{
"extends": "../bazel-tsconfig-build.json",
"compilerOptions": {
"baseUrl": ".",
"outDir": "../../dist/packages/youtube-player",
"rootDir": ".",
"rootDirs": [
".",
"../../dist/packages/youtube-player"
],
"types": ["youtube"]
},
"files": [
"public-api.ts"
],
"angularCompilerOptions": {
"annotateForClosureCompiler": true,
"strictMetadataEmit": true,
"flatModuleOutFile": "index.js",
"flatModuleId": "@angular/youtube-player",
"skipTemplateCodegen": true,
"fullTemplateTypeCheck": true
}
}
26 changes: 26 additions & 0 deletions src/youtube-player/tsconfig-tests.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// TypeScript config file that extends the default tsconfig file for the library. This config is
// used to compile the tests for Karma. Since the code will run inside of the browser, the target
// needs to be ES5. The format needs to be CommonJS since Karma only supports that module format.
{
"extends": "./tsconfig-build",
"compilerOptions": {
"importHelpers": false,
"module": "commonjs",
"target": "es5",
"types": ["jasmine", "youtube"]
},
"angularCompilerOptions": {
"strictMetadataEmit": true,
"skipTemplateCodegen": true,
"emitDecoratorMetadata": true,
"fullTemplateTypeCheck": true,

// Unset options inherited from tsconfig-build
"annotateForClosureCompiler": false,
"flatModuleOutFile": null,
"flatModuleId": null
},
"include": [
"*.ts"
]
}
11 changes: 11 additions & 0 deletions src/youtube-player/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// Configuration for IDEs only.
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"rootDir": "..",
"baseUrl": ".",
"paths": {},
"types": ["jasmine", "youtube"]
},
"include": ["*.ts"]
}
14 changes: 14 additions & 0 deletions src/youtube-player/youtube-module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import {CommonModule} from '@angular/common';
import {NgModule} from '@angular/core';

import {YouTubePlayer} from './youtube-player';

const COMPONENTS = [YouTubePlayer];

@NgModule({
declarations: COMPONENTS,
exports: COMPONENTS,
imports: [CommonModule],
})
export class YouTubePlayerModule {
}
Empty file.
101 changes: 101 additions & 0 deletions src/youtube-player/youtube-player.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
import {Component, ViewChild} from '@angular/core';
import {YouTubePlayerModule} from './index';
import {YouTubePlayer} from './youtube-player';
import {createFakeYtNamespace} from './fake-youtube-player';

const VIDEO_ID = 'a12345';

declare global {
interface Window { YT: typeof YT | undefined; }
}

describe('YoutubePlayer', () => {
let playerCtorSpy: jasmine.Spy;
let playerSpy: jasmine.SpyObj<YT.Player>;
let onPlayerReady: () => void;
let fixture: ComponentFixture<TestApp>;
let testComponent: TestApp;

beforeEach(async(() => {
const fake = createFakeYtNamespace();
playerCtorSpy = fake.playerCtorSpy;
playerSpy = fake.playerSpy;
onPlayerReady = fake.onPlayerReady;
window.YT = fake.namespace;

TestBed.configureTestingModule({
imports: [YouTubePlayerModule],
declarations: [TestApp],
});

TestBed.compileComponents();
}));

beforeEach(() => {
fixture = TestBed.createComponent(TestApp);
testComponent = fixture.debugElement.componentInstance;
fixture.detectChanges();
});

afterEach(() => {
window.YT = undefined;
});

it('initializes a youtube player', () => {
let containerElement = fixture.nativeElement.querySelector('div');

expect(playerCtorSpy).toHaveBeenCalledWith(
containerElement, jasmine.objectContaining({
videoId: VIDEO_ID,
}));
});

it('destroys the iframe when the component is destroyed', () => {
onPlayerReady();

testComponent.visible = false;
fixture.detectChanges();

expect(playerSpy.destroy).toHaveBeenCalled();
});

it('responds to changes in video id', () => {
let containerElement = fixture.nativeElement.querySelector('div');

testComponent.videoId = 'otherId';
fixture.detectChanges();

expect(playerSpy.cueVideoById).not.toHaveBeenCalled();

onPlayerReady();

expect(playerSpy.cueVideoById).toHaveBeenCalledWith(
jasmine.objectContaining({videoId: 'otherId'}));

testComponent.videoId = undefined;
fixture.detectChanges();

expect(playerSpy.destroy).toHaveBeenCalled();

testComponent.videoId = 'otherId2';
fixture.detectChanges();

expect(playerCtorSpy).toHaveBeenCalledWith(
containerElement, jasmine.objectContaining({videoId: 'otherId2'}));
});
});

/** Test component that contains a YouTubePlayer. */
@Component({
selector: 'test-app',
template: `
<youtube-player #player [videoId]="videoId" *ngIf="visible">
</youtube-player>
`
})
class TestApp {
videoId: string | undefined = VIDEO_ID;
visible = true;
@ViewChild('player', {static: true}) youtubePlayer: YouTubePlayer;
}
Loading

0 comments on commit b8c9da6

Please sign in to comment.