Skip to content

Commit b8c9da6

Browse files
Nathan Tatejelbourn
Nathan Tate
authored andcommitted
feat(youtube-player): initialize component and infrastructure (#16337)
1 parent 3ec531b commit b8c9da6

26 files changed

+622
-1
lines changed

.github/CODEOWNERS

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,9 @@
5757
/src/material/core/typography/** @crisbeto
5858
/src/material/core/util/** @jelbourn
5959

60+
# Miscellaneous components
61+
/src/youtube-player/** @nathantate
62+
6063
# CDK
6164
/src/cdk/* @jelbourn
6265
/src/cdk/a11y/** @jelbourn @devversion

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
"@angular/elements": "^8.1.0",
4646
"@angular/forms": "^8.1.0",
4747
"@angular/platform-browser": "^8.1.0",
48+
"@types/youtube": "^0.0.38",
4849
"@webcomponents/custom-elements": "^1.1.0",
4950
"core-js": "^2.6.1",
5051
"material-components-web": "^3.0.0",

packages.bzl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@ ROLLUP_GLOBALS = {
121121
"@angular/cdk-experimental": "ng.cdkExperimental",
122122
"@angular/material": "ng.material",
123123
"@angular/material-experimental": "ng.materialExperimental",
124+
"@angular/youtube-player": "ng.youtubePlayer",
124125
}
125126

126127
# Rollup globals for cdk subpackages in the form of, e.g., {"@angular/cdk/table": "ng.cdk.table"}

src/youtube-player/BUILD.bazel

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
package(default_visibility = ["//visibility:public"])
2+
3+
load("//:packages.bzl", "ROLLUP_GLOBALS")
4+
load(
5+
"//tools:defaults.bzl",
6+
"markdown_to_html",
7+
"ng_module",
8+
"ng_package",
9+
"ng_test_library",
10+
"ng_web_test_suite",
11+
)
12+
13+
ng_module(
14+
name = "youtube-player",
15+
srcs = glob(
16+
["**/*.ts"],
17+
exclude = [
18+
"**/*.spec.ts",
19+
"fake-youtube-player.ts",
20+
],
21+
),
22+
module_name = "@angular/youtube-player",
23+
deps = [
24+
"@npm//@angular/common",
25+
"@npm//@angular/core",
26+
"@npm//@types/youtube",
27+
"@npm//rxjs",
28+
],
29+
)
30+
31+
ng_package(
32+
name = "npm_package",
33+
srcs = ["package.json"],
34+
entry_point = ":public-api.ts",
35+
entry_point_name = "youtube-player",
36+
globals = ROLLUP_GLOBALS,
37+
deps = [":youtube-player"],
38+
)
39+
40+
ng_test_library(
41+
name = "unit_test_sources",
42+
srcs = ["fake-youtube-player.ts"] + glob(
43+
["**/*.spec.ts"],
44+
exclude = ["**/*.e2e.spec.ts"],
45+
),
46+
deps = [
47+
":youtube-player",
48+
"@npm//@angular/platform-browser",
49+
],
50+
)
51+
52+
ng_web_test_suite(
53+
name = "unit_tests",
54+
deps = [":unit_test_sources"],
55+
)
56+
57+
markdown_to_html(
58+
name = "overview",
59+
srcs = [":youtube-player.md"],
60+
)
61+
62+
filegroup(
63+
name = "source-files",
64+
srcs = glob(["**/*.ts"]),
65+
)

src/youtube-player/README.md

Whitespace-only changes.
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
// A re-creation of YT.PlayerState since enum values cannot be bound to the window
2+
// object.
3+
const playerState = {
4+
UNSTARTED: -1,
5+
ENDED: 0,
6+
PLAYING: 1,
7+
PAUSED: 2,
8+
BUFFERING: 3,
9+
CUED: 5,
10+
};
11+
12+
interface FakeYtNamespace {
13+
playerCtorSpy: jasmine.Spy;
14+
playerSpy: jasmine.SpyObj<YT.Player>;
15+
onPlayerReady: () => void;
16+
namespace: typeof YT;
17+
}
18+
19+
export function createFakeYtNamespace(): FakeYtNamespace {
20+
const playerSpy: jasmine.SpyObj<YT.Player> = jasmine.createSpyObj('Player', [
21+
'getPlayerState', 'destroy', 'cueVideoById', 'loadVideoById', 'pauseVideo', 'stopVideo',
22+
'seekTo', 'isMuted', 'mute', 'unMute', 'getVolume', 'getPlaybackRate',
23+
'getAvailablePlaybackRates', 'getVideoLoadedFraction', 'getPlayerState', 'getCurrentTime',
24+
'getPlaybackQuality', 'getAvailableQualityLevels', 'getDuration', 'getVideoUrl',
25+
'getVideoEmbedCode', 'playVideo', 'setSize', 'setVolume', 'setPlaybackQuality',
26+
'setPlaybackRate', 'addEventListener', 'removeEventListener',
27+
]);
28+
29+
let playerConfig: YT.PlayerOptions | undefined;
30+
const playerCtorSpy = jasmine.createSpy('Player Constructor');
31+
playerCtorSpy.and.callFake((_el: Element, config: YT.PlayerOptions) => {
32+
playerConfig = config;
33+
return playerSpy;
34+
});
35+
36+
const onPlayerReady = () => {
37+
if (!playerConfig) {
38+
throw new Error('Player not initialized before onPlayerReady called');
39+
}
40+
41+
if (playerConfig && playerConfig.events && playerConfig.events.onReady) {
42+
playerConfig.events.onReady({target: playerSpy});
43+
}
44+
45+
for (const [event, callback] of playerSpy.addEventListener.calls.allArgs()) {
46+
if (event === 'onReady') {
47+
callback({target: playerSpy});
48+
}
49+
}
50+
};
51+
52+
return {
53+
playerCtorSpy,
54+
playerSpy,
55+
onPlayerReady,
56+
namespace: {
57+
'Player': playerCtorSpy as unknown as typeof YT.Player,
58+
'PlayerState': playerState,
59+
} as typeof YT,
60+
};
61+
}

src/youtube-player/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './public-api';

src/youtube-player/package.json

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
{
2+
"name": "@angular/youtube-player",
3+
"version": "0.0.0-PLACEHOLDER",
4+
"description": "Angular YouTube Player",
5+
"main": "./bundles/youtube-player.umd.js",
6+
"module": "./esm5/youtube-player.es5.js",
7+
"es2015": "./esm2015/youtube-player.js",
8+
"typings": "./youtube-player.d.ts",
9+
"repository": {
10+
"type": "git",
11+
"url": "https://github.com/angular/components.git"
12+
},
13+
"keywords": [
14+
"angular",
15+
"components",
16+
"youtube"
17+
],
18+
"license": "MIT",
19+
"bugs": {
20+
"url": "https://github.com/angular/components/issues"
21+
},
22+
"homepage": "https://github.com/angular/components/tree/master/src/youtube-player#readme",
23+
"dependencies": {
24+
"@types/youtube": "^0.0.38"
25+
},
26+
"peerDependencies": {
27+
"@angular/core": "0.0.0-NG",
28+
"@angular/common": "0.0.0-NG"
29+
},
30+
"sideEffects": false
31+
}

src/youtube-player/public-api.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export * from './youtube-module';
2+
export * from './youtube-player';
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
// TypeScript config file that is used to compile the Material package through Gulp. As the
2+
// long term goal is to switch to Bazel, and we already want to run tests with Bazel, we need to
3+
// ensure the TypeScript build options are the same for Gulp and Bazel. We achieve this by
4+
// extending the generic Bazel build tsconfig which will be used for each entry-point.
5+
{
6+
"extends": "../bazel-tsconfig-build.json",
7+
"compilerOptions": {
8+
"baseUrl": ".",
9+
"outDir": "../../dist/packages/youtube-player",
10+
"rootDir": ".",
11+
"rootDirs": [
12+
".",
13+
"../../dist/packages/youtube-player"
14+
],
15+
"types": ["youtube"]
16+
},
17+
"files": [
18+
"public-api.ts"
19+
],
20+
"angularCompilerOptions": {
21+
"annotateForClosureCompiler": true,
22+
"strictMetadataEmit": true,
23+
"flatModuleOutFile": "index.js",
24+
"flatModuleId": "@angular/youtube-player",
25+
"skipTemplateCodegen": true,
26+
"fullTemplateTypeCheck": true
27+
}
28+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
// TypeScript config file that extends the default tsconfig file for the library. This config is
2+
// used to compile the tests for Karma. Since the code will run inside of the browser, the target
3+
// needs to be ES5. The format needs to be CommonJS since Karma only supports that module format.
4+
{
5+
"extends": "./tsconfig-build",
6+
"compilerOptions": {
7+
"importHelpers": false,
8+
"module": "commonjs",
9+
"target": "es5",
10+
"types": ["jasmine", "youtube"]
11+
},
12+
"angularCompilerOptions": {
13+
"strictMetadataEmit": true,
14+
"skipTemplateCodegen": true,
15+
"emitDecoratorMetadata": true,
16+
"fullTemplateTypeCheck": true,
17+
18+
// Unset options inherited from tsconfig-build
19+
"annotateForClosureCompiler": false,
20+
"flatModuleOutFile": null,
21+
"flatModuleId": null
22+
},
23+
"include": [
24+
"*.ts"
25+
]
26+
}

src/youtube-player/tsconfig.json

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
// Configuration for IDEs only.
2+
{
3+
"extends": "../../tsconfig.json",
4+
"compilerOptions": {
5+
"rootDir": "..",
6+
"baseUrl": ".",
7+
"paths": {},
8+
"types": ["jasmine", "youtube"]
9+
},
10+
"include": ["*.ts"]
11+
}

src/youtube-player/youtube-module.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import {CommonModule} from '@angular/common';
2+
import {NgModule} from '@angular/core';
3+
4+
import {YouTubePlayer} from './youtube-player';
5+
6+
const COMPONENTS = [YouTubePlayer];
7+
8+
@NgModule({
9+
declarations: COMPONENTS,
10+
exports: COMPONENTS,
11+
imports: [CommonModule],
12+
})
13+
export class YouTubePlayerModule {
14+
}

src/youtube-player/youtube-player.md

Whitespace-only changes.
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
2+
import {Component, ViewChild} from '@angular/core';
3+
import {YouTubePlayerModule} from './index';
4+
import {YouTubePlayer} from './youtube-player';
5+
import {createFakeYtNamespace} from './fake-youtube-player';
6+
7+
const VIDEO_ID = 'a12345';
8+
9+
declare global {
10+
interface Window { YT: typeof YT | undefined; }
11+
}
12+
13+
describe('YoutubePlayer', () => {
14+
let playerCtorSpy: jasmine.Spy;
15+
let playerSpy: jasmine.SpyObj<YT.Player>;
16+
let onPlayerReady: () => void;
17+
let fixture: ComponentFixture<TestApp>;
18+
let testComponent: TestApp;
19+
20+
beforeEach(async(() => {
21+
const fake = createFakeYtNamespace();
22+
playerCtorSpy = fake.playerCtorSpy;
23+
playerSpy = fake.playerSpy;
24+
onPlayerReady = fake.onPlayerReady;
25+
window.YT = fake.namespace;
26+
27+
TestBed.configureTestingModule({
28+
imports: [YouTubePlayerModule],
29+
declarations: [TestApp],
30+
});
31+
32+
TestBed.compileComponents();
33+
}));
34+
35+
beforeEach(() => {
36+
fixture = TestBed.createComponent(TestApp);
37+
testComponent = fixture.debugElement.componentInstance;
38+
fixture.detectChanges();
39+
});
40+
41+
afterEach(() => {
42+
window.YT = undefined;
43+
});
44+
45+
it('initializes a youtube player', () => {
46+
let containerElement = fixture.nativeElement.querySelector('div');
47+
48+
expect(playerCtorSpy).toHaveBeenCalledWith(
49+
containerElement, jasmine.objectContaining({
50+
videoId: VIDEO_ID,
51+
}));
52+
});
53+
54+
it('destroys the iframe when the component is destroyed', () => {
55+
onPlayerReady();
56+
57+
testComponent.visible = false;
58+
fixture.detectChanges();
59+
60+
expect(playerSpy.destroy).toHaveBeenCalled();
61+
});
62+
63+
it('responds to changes in video id', () => {
64+
let containerElement = fixture.nativeElement.querySelector('div');
65+
66+
testComponent.videoId = 'otherId';
67+
fixture.detectChanges();
68+
69+
expect(playerSpy.cueVideoById).not.toHaveBeenCalled();
70+
71+
onPlayerReady();
72+
73+
expect(playerSpy.cueVideoById).toHaveBeenCalledWith(
74+
jasmine.objectContaining({videoId: 'otherId'}));
75+
76+
testComponent.videoId = undefined;
77+
fixture.detectChanges();
78+
79+
expect(playerSpy.destroy).toHaveBeenCalled();
80+
81+
testComponent.videoId = 'otherId2';
82+
fixture.detectChanges();
83+
84+
expect(playerCtorSpy).toHaveBeenCalledWith(
85+
containerElement, jasmine.objectContaining({videoId: 'otherId2'}));
86+
});
87+
});
88+
89+
/** Test component that contains a YouTubePlayer. */
90+
@Component({
91+
selector: 'test-app',
92+
template: `
93+
<youtube-player #player [videoId]="videoId" *ngIf="visible">
94+
</youtube-player>
95+
`
96+
})
97+
class TestApp {
98+
videoId: string | undefined = VIDEO_ID;
99+
visible = true;
100+
@ViewChild('player', {static: true}) youtubePlayer: YouTubePlayer;
101+
}

0 commit comments

Comments
 (0)