diff --git a/src/app/common/settings/base-settings.ts b/src/app/common/settings/base-settings.ts index 58611851..b504d746 100644 --- a/src/app/common/settings/base-settings.ts +++ b/src/app/common/settings/base-settings.ts @@ -66,4 +66,5 @@ export abstract class BaseSettings { public abstract showLove: boolean; public abstract enableMultimediaKeys: boolean; public abstract downloadArtistInformationFromLastFm: boolean; + public abstract isMuted: boolean; } diff --git a/src/app/common/settings/settings.ts b/src/app/common/settings/settings.ts index e3ffee56..8304b8d5 100644 --- a/src/app/common/settings/settings.ts +++ b/src/app/common/settings/settings.ts @@ -610,6 +610,15 @@ export class Settings implements BaseSettings { this.settings.set('downloadArtistInformationFromLastFm', v); } + // isMuted + public get isMuted(): boolean { + return this.settings.get('isMuted'); + } + + public set isMuted(v: boolean) { + this.settings.set('isMuted', v); + } + // Initialize private initialize(): void { if (!this.settings.has('language')) { @@ -871,5 +880,9 @@ export class Settings implements BaseSettings { if (!this.settings.has('downloadArtistInformationFromLastFm')) { this.settings.set('downloadArtistInformationFromLastFm', true); } + + if (!this.settings.has('isMuted')) { + this.settings.set('isMuted', false); + } } } diff --git a/src/app/components/collection/album-browser/album-browser.component.spec.ts b/src/app/components/collection/album-browser/album-browser.component.spec.ts index 1bd44d41..79e68df8 100644 --- a/src/app/components/collection/album-browser/album-browser.component.spec.ts +++ b/src/app/components/collection/album-browser/album-browser.component.spec.ts @@ -722,7 +722,7 @@ describe('AlbumBrowserComponent', () => { component.onAddToQueue(album1); // Assert - playbackServiceMock.verify((x) => x.addAlbumToQueue(album1), Times.once()); + playbackServiceMock.verify((x) => x.addAlbumToQueueAsync(album1), Times.once()); }); }); }); diff --git a/src/app/components/collection/album-browser/album-browser.component.ts b/src/app/components/collection/album-browser/album-browser.component.ts index d326f013..2f17fdef 100644 --- a/src/app/components/collection/album-browser/album-browser.component.ts +++ b/src/app/components/collection/album-browser/album-browser.component.ts @@ -182,6 +182,6 @@ export class AlbumBrowserComponent implements OnInit, AfterViewInit { } public onAddToQueue(album: AlbumModel): void { - this.playbackService.addAlbumToQueue(album); + this.playbackService.addAlbumToQueueAsync(album); } } diff --git a/src/app/components/slider/slider.component.ts b/src/app/components/slider/slider.component.ts index 31977226..60e1033d 100644 --- a/src/app/components/slider/slider.component.ts +++ b/src/app/components/slider/slider.component.ts @@ -46,6 +46,7 @@ export class SliderComponent implements AfterViewInit { @Input() public set value(v: number) { this._value = v; + this.applyPositionFromValue(v); } @Output() @@ -88,7 +89,7 @@ export class SliderComponent implements AfterViewInit { } public onSliderContainerMouseDown(e: MouseEvent): void { - this.applyPosition(this.getMouseXPositionRelativeToSlider(e.clientX)); + this.applyPositionAndValue(this.getMouseXPositionRelativeToSlider(e.clientX)); } @HostListener('document:mousedown', ['$event']) @@ -114,7 +115,7 @@ export class SliderComponent implements AfterViewInit { @HostListener('document:mousemove', ['$event']) public onDocumentMouseMove(e: MouseEvent): void { if (this.isSliderThumbDown) { - this.applyPosition(this.getMouseXPositionRelativeToSlider(e.clientX)); + this.applyPositionAndValue(this.getMouseXPositionRelativeToSlider(e.clientX)); } } @@ -122,7 +123,7 @@ export class SliderComponent implements AfterViewInit { public onDocumentTouchMove(e: TouchEvent): void { if (this.isSliderThumbDown) { const touch: Touch = e.touches[0] != undefined ? e.touches[0] : e.changedTouches[0]; - this.applyPosition(this.getMouseXPositionRelativeToSlider(touch.pageX)); + this.applyPositionAndValue(this.getMouseXPositionRelativeToSlider(touch.pageX)); } } @@ -132,7 +133,7 @@ export class SliderComponent implements AfterViewInit { if (event.deltaY > 0) { newPosition = this.sliderBarPosition - mouseStepConvertedToSliderScale; } - this.applyPosition(newPosition); + this.applyPositionAndValue(newPosition); } private getMouseStepConvertedToSliderScale(): number { @@ -142,24 +143,6 @@ export class SliderComponent implements AfterViewInit { return mouseStepUsingSliderScale; } - private applyPosition(position: number): void { - try { - const sliderWidth: number = this.nativeElementProxy.getElementWidth(this.sliderTrack); - - this.sliderBarPosition = this.mathExtensions.clamp(position, 0, sliderWidth); - - this.sliderThumbPosition = this.mathExtensions.clamp( - position - this.sliderThumbWidth / 2, - this.sliderThumbMargin - this.sliderThumbWidth / 2, - sliderWidth - this.sliderThumbMargin - this.sliderThumbWidth / 2 - ); - - this.calculateValue(); - } catch (e: unknown) { - this.logger.error(e, 'Could not apply position', 'SliderComponent', 'applyPosition'); - } - } - private getMouseXPositionRelativeToSlider(clientX: number): number { const rect: DOMRect | undefined = this.nativeElementProxy.getBoundingRectangle(this.sliderTrack); @@ -170,7 +153,19 @@ export class SliderComponent implements AfterViewInit { return clientX - rect.left; } - private calculateValue(): void { + private applyPosition(position: number): void { + const sliderWidth: number = this.nativeElementProxy.getElementWidth(this.sliderTrack); + + this.sliderBarPosition = this.mathExtensions.clamp(position, 0, sliderWidth); + + this.sliderThumbPosition = this.mathExtensions.clamp( + position - this.sliderThumbWidth / 2, + this.sliderThumbMargin - this.sliderThumbWidth / 2, + sliderWidth - this.sliderThumbMargin - this.sliderThumbWidth / 2 + ); + } + + private applyValue(): void { const sliderWidth: number = this.nativeElementProxy.getElementWidth(this.sliderTrack); const valueFraction: number = this.sliderBarPosition / sliderWidth; @@ -181,14 +176,27 @@ export class SliderComponent implements AfterViewInit { this.valueChange.emit(this._value); } + private applyPositionAndValue(position: number): void { + try { + this.applyPosition(position); + this.applyValue(); + } catch (e: unknown) { + this.logger.error(e, 'Could not apply position', 'SliderComponent', 'applyPosition'); + } + } + private applyPositionFromValue(value: number): void { - const sliderWidth: number = this.nativeElementProxy.getElementWidth(this.sliderTrack); - let position: number = 0; + try { + const sliderWidth: number = this.nativeElementProxy.getElementWidth(this.sliderTrack); + let position: number = 0; - if (this.maximum > 0) { - position = (value / this.maximum) * sliderWidth; - } + if (this.maximum > 0) { + position = (value / this.maximum) * sliderWidth; + } - this.applyPosition(position); + this.applyPosition(position); + } catch (e: unknown) { + this.logger.error(e, 'Could not apply position from value', 'SliderComponent', 'applyPositionFromValue'); + } } } diff --git a/src/app/components/volume-control/volume-control.component.html b/src/app/components/volume-control/volume-control.component.html index 5281b4d7..f3f640ce 100644 --- a/src/app/components/volume-control/volume-control.component.html +++ b/src/app/components/volume-control/volume-control.component.html @@ -1,7 +1,9 @@
- - - +
+ + + +
{ @@ -7,16 +9,13 @@ describe('VolumeControlComponent', () => { playbackServiceMock = { volume: 0 }; }); - function createVolumeControl(): VolumeControlComponent { - return new VolumeControlComponent(playbackServiceMock); - } - describe('constructor', () => { it('should create', () => { // Arrange + const playbackServiceMock = Mock.ofType(); // Act - const component: VolumeControlComponent = createVolumeControl(); + const component: VolumeControlComponent = new VolumeControlComponent(playbackServiceMock.object); // Assert expect(component).toBeDefined(); @@ -26,8 +25,8 @@ describe('VolumeControlComponent', () => { describe('volume', () => { it('should set playbackService.volume', () => { // Arrange - playbackServiceMock.volume = 50; - const component: VolumeControlComponent = createVolumeControl(); + const playbackServiceMock: any = { volume: 50 }; + const component: VolumeControlComponent = new VolumeControlComponent(playbackServiceMock); // Act component.volume = 20; @@ -38,11 +37,25 @@ describe('VolumeControlComponent', () => { it('should get playbackService.volume', () => { // Arrange - playbackServiceMock.volume = 40; - const component: VolumeControlComponent = createVolumeControl(); + const playbackServiceMock: any = { volume: 40 }; + const component: VolumeControlComponent = new VolumeControlComponent(playbackServiceMock); // Act & Assert expect(component.volume).toEqual(40); }); }); + + describe('toggleMute', () => { + it('should call playbackService.toggleMute()', () => { + // Arrange + const playbackServiceMock = Mock.ofType(); + const component: VolumeControlComponent = new VolumeControlComponent(playbackServiceMock.object); + + // Act + component.toggleMute(); + + // Assert + playbackServiceMock.verify((x) => x.toggleMute(), Times.once()); + }); + }); }); diff --git a/src/app/components/volume-control/volume-control.component.ts b/src/app/components/volume-control/volume-control.component.ts index 9686b6fb..eae4a80d 100644 --- a/src/app/components/volume-control/volume-control.component.ts +++ b/src/app/components/volume-control/volume-control.component.ts @@ -16,4 +16,8 @@ export class VolumeControlComponent { public set volume(v: number) { this.playbackService.volume = v; } + + public toggleMute(): void { + this.playbackService.toggleMute(); + } } diff --git a/src/app/services/playback/base-playback.service.ts b/src/app/services/playback/base-playback.service.ts index 5640fad2..28967d76 100644 --- a/src/app/services/playback/base-playback.service.ts +++ b/src/app/services/playback/base-playback.service.ts @@ -38,7 +38,7 @@ export abstract class BasePlaybackService { public abstract addTracksToQueueAsync(tracksToAdd: TrackModel[]): Promise; public abstract addArtistToQueueAsync(artistToAdd: ArtistModel, artistType: ArtistType): Promise; public abstract addGenreToQueueAsync(genreToAdd: GenreModel): Promise; - public abstract addAlbumToQueue(albumToAdd: AlbumModel): void; + public abstract addAlbumToQueueAsync(albumToAdd: AlbumModel): Promise; public abstract addPlaylistToQueueAsync(playlistToAdd: PlaylistModel): Promise; public abstract removeFromQueue(tracksToRemove: TrackModel[]): void; public abstract playQueuedTrack(trackToPlay: TrackModel): void; @@ -46,4 +46,7 @@ export abstract class BasePlaybackService { public abstract playNext(): void; public abstract skipByFractionOfTotalSeconds(fractionOfTotalSeconds: number): void; public abstract stopIfPlaying(track: TrackModel): void; + public abstract pause(): void; + public abstract resume(): void; + public abstract toggleMute(): void; } diff --git a/src/app/services/playback/playback.service.spec.ts b/src/app/services/playback/playback.service.spec.ts index d9792283..34d32fb8 100644 --- a/src/app/services/playback/playback.service.spec.ts +++ b/src/app/services/playback/playback.service.spec.ts @@ -18,6 +18,7 @@ import { TrackModel } from '../track/track-model'; import { TrackModels } from '../track/track-models'; import { BaseTranslatorService } from '../translator/base-translator.service'; import { BaseAudioPlayer } from './base-audio-player'; +import { BasePlaybackService } from './base-playback.service'; import { LoopMode } from './loop-mode'; import { PlaybackProgress } from './playback-progress'; import { PlaybackStarted } from './playback-started'; @@ -37,7 +38,6 @@ describe('PlaybackService', () => { let progressUpdaterMock: IMock; let mathExtensionsMock: IMock; let settingsStub: any; - let service: PlaybackService; let playbackFinished: Subject; let progressUpdaterProgressChanged: Subject; let subscription: Subscription; @@ -77,7 +77,7 @@ describe('PlaybackService', () => { queueMock = Mock.ofType(); progressUpdaterMock = Mock.ofType(); mathExtensionsMock = Mock.ofType(); - settingsStub = { volume: 0.6 }; + settingsStub = { volume: 0.6, isMuted: false }; playbackFinished = new Subject(); progressUpdaterProgressChanged = new Subject(); const playbackFinished$: Observable = playbackFinished.asObservable(); @@ -136,8 +136,14 @@ describe('PlaybackService', () => { trackServiceMock.setup((x) => x.getTracksForArtists(It.isAny(), It.isAny())).returns(() => tracks); trackServiceMock.setup((x) => x.getTracksForGenres(It.isAny())).returns(() => tracks); trackOrderingMock.setup((x) => x.getTracksOrderedByAlbum(tracks.tracks)).returns(() => orderedTrackModels); + }); + + afterEach(() => { + subscription.unsubscribe(); + }); - service = new PlaybackService( + function createService(): BasePlaybackService { + return new PlaybackService( trackServiceMock.object, playlistServiceMock.object, snackBarServiceMock.object, @@ -149,44 +155,36 @@ describe('PlaybackService', () => { settingsStub, loggerMock.object ); - }); - - afterEach(() => { - subscription.unsubscribe(); - }); + } describe('constructor', () => { it('should create', () => { - // Arrange - - // Act + // Arrange, Act + const service: BasePlaybackService = createService(); // Assert expect(service).toBeDefined(); }); it('should declare currentTrack', () => { - // Arrange - - // Act + // Arrange, Act + const service: BasePlaybackService = createService(); // Assert expect(service.currentTrack).toBeUndefined(); }); it('should define progressChanged$', () => { - // Arrange - - // Act + // Arrange, Act + const service: BasePlaybackService = createService(); // Assert expect(service.progressChanged$).toBeDefined(); }); it('should define progress with progressSeconds as 0 and totalSeconds as 0', () => { - // Arrange - - // Act + // Arrange, Act + const service: BasePlaybackService = createService(); // Assert expect(service.progress.progressSeconds).toEqual(0); @@ -194,125 +192,129 @@ describe('PlaybackService', () => { }); it('should define playbackStarted$', () => { - // Arrange - - // Act + // Arrange, Act + const service: BasePlaybackService = createService(); // Assert expect(service.playbackStarted$).toBeDefined(); }); it('should define playbackPaused$', () => { - // Arrange - - // Act + // Arrange, Act + const service: BasePlaybackService = createService(); // Assert expect(service.playbackPaused$).toBeDefined(); }); it('should define playbackResumed$', () => { - // Arrange - - // Act + // Arrange, Act + const service: BasePlaybackService = createService(); // Assert expect(service.playbackResumed$).toBeDefined(); }); it('should define playbackStopped$', () => { - // Arrange - - // Act + // Arrange, Act + const service: BasePlaybackService = createService(); // Assert expect(service.playbackStopped$).toBeDefined(); }); it('should define playbackSkipped$', () => { - // Arrange - - // Act + // Arrange, Act + const service: BasePlaybackService = createService(); // Assert expect(service.playbackSkipped$).toBeDefined(); }); it('should initialize loopMode as LoopMode.None', () => { - // Arrange - - // Act + // Arrange, Act + const service: BasePlaybackService = createService(); // Assert expect(service.loopMode).toEqual(LoopMode.None); }); it('should initialize isPlaying as false', () => { - // Arrange - - // Act + // Arrange, Act + const service: BasePlaybackService = createService(); // Assert expect(service.isShuffled).toBeFalsy(); }); it('should initialize isShuffled as false', () => { - // Arrange - - // Act + // Arrange, Act + const service: BasePlaybackService = createService(); // Assert expect(service.isShuffled).toBeFalsy(); }); it('should initialize canPause as false', () => { - // Arrange - - // Act + // Arrange, Act + const service: BasePlaybackService = createService(); // Assert expect(service.canPause).toBeFalsy(); }); it('should initialize canResume as true', () => { - // Arrange - - // Act + // Arrange, Act + const service: BasePlaybackService = createService(); // Assert expect(service.canResume).toBeTruthy(); }); - it('should apply volume from the settings', () => { - // Arrange - - // Act + it('should apply volume from the settings when not muted', () => { + // Arrange, Act + settingsStub.isMuted = false; + const service: BasePlaybackService = createService(); // Assert expect(service.volume).toEqual(0.6); audioPlayerMock.verify((x) => x.setVolume(0.6), Times.exactly(1)); }); + it('should apply zero volume when muted', () => { + // Arrange, Act + settingsStub.isMuted = true; + const service: BasePlaybackService = createService(); + + // Assert + expect(service.volume).toEqual(0); + audioPlayerMock.verify((x) => x.setVolume(0), Times.exactly(1)); + }); + it('should stop playback on playback finished if a next track is not found', () => { // Arrange + const service: BasePlaybackService = createService(); + queueMock.setup((x) => x.getNextTrack(It.isAny(), false)).returns(() => undefined); // Act playbackFinished.next(); // Assert - audioPlayerMock.verify((x) => x.stop(), Times.exactly(1)); + audioPlayerMock.verify((x) => x.stop(), Times.once()); expect(service.isPlaying).toBeFalsy(); expect(service.canResume).toBeTruthy(); expect(service.canPause).toBeFalsy(); expect(service.progress.progressSeconds).toEqual(0); expect(service.progress.totalSeconds).toEqual(0); expect(service.currentTrack).toBeUndefined(); - progressUpdaterMock.verify((x) => x.stopUpdatingProgress(), Times.exactly(1)); + progressUpdaterMock.verify((x) => x.stopUpdatingProgress(), Times.once()); }); it('should raise an event that playback is stopped on playback finished if a next track is not found', () => { // Arrange + const service: BasePlaybackService = createService(); + queueMock.setup((x) => x.getNextTrack(It.isAny(), false)).returns(() => undefined); let playbackIsStopped: boolean = false; @@ -331,6 +333,8 @@ describe('PlaybackService', () => { it('should set the current track to undefined before raising a playback stopped event', () => { // Arrange + const service: BasePlaybackService = createService(); + queueMock.setup((x) => x.getNextTrack(It.isAny(), false)).returns(() => undefined); queueMock.setup((x) => x.getFirstTrack()).returns(() => trackModel1); @@ -352,6 +356,8 @@ describe('PlaybackService', () => { it('should play the next track on playback finished if a next track is found', () => { // Arrange + const service: BasePlaybackService = createService(); + queueMock.setup((x) => x.getNextTrack(It.isObjectWith({ path: 'Path 1' }), false)).returns(() => trackModel2); queueMock.setup((x) => x.getFirstTrack()).returns(() => trackModel1); @@ -374,6 +380,8 @@ describe('PlaybackService', () => { it('should play the same track on playback finished if loopMode is One', () => { // Arrange + const service: BasePlaybackService = createService(); + while (service.loopMode !== LoopMode.One) { service.toggleLoopMode(); } @@ -392,6 +400,8 @@ describe('PlaybackService', () => { it('should not play the next track on playback finished if found and if loopMode is One', () => { // Arrange + const service: BasePlaybackService = createService(); + while (service.loopMode !== LoopMode.One) { service.toggleLoopMode(); } @@ -412,6 +422,8 @@ describe('PlaybackService', () => { it('should play the next track on playback finished if found and if loopMode is All', () => { // Arrange + const service: BasePlaybackService = createService(); + while (service.loopMode !== LoopMode.All) { service.toggleLoopMode(); } @@ -432,6 +444,8 @@ describe('PlaybackService', () => { it('should play the next track on playback finished if found and if loopMode is None', () => { // Arrange + const service: BasePlaybackService = createService(); + while (service.loopMode !== LoopMode.None) { service.toggleLoopMode(); } @@ -452,6 +466,7 @@ describe('PlaybackService', () => { it('should get the next track without wrap around on playback finished if loopMode is None', () => { // Arrange + createService(); // Act playbackFinished.next(); @@ -462,6 +477,8 @@ describe('PlaybackService', () => { it('should get the next track with wrap around on playback finished if loopMode is All', () => { // Arrange + const service: BasePlaybackService = createService(); + while (service.loopMode !== LoopMode.All) { service.toggleLoopMode(); } @@ -475,6 +492,8 @@ describe('PlaybackService', () => { it('should increase play count and date last played for the current track on playback finished', () => { // Arrange + const service: BasePlaybackService = createService(); + const trackModelMock: IMock = Mock.ofType(); queueMock.setup((x) => x.getFirstTrack()).returns(() => trackModelMock.object); service.enqueueAndPlayTracks([trackModelMock.object]); @@ -488,6 +507,8 @@ describe('PlaybackService', () => { it('should save play count and date last played for the current track on playback finished', () => { // Arrange + const service: BasePlaybackService = createService(); + const trackModelMock: IMock = Mock.ofType(); queueMock.setup((x) => x.getFirstTrack()).returns(() => trackModelMock.object); service.enqueueAndPlayTracks([trackModelMock.object]); @@ -501,6 +522,8 @@ describe('PlaybackService', () => { it('should raise an event, on playback finished, that playback has started, containing the current track and if a next track is being played.', () => { // Arrange + const service: BasePlaybackService = createService(); + queueMock.setup((x) => x.getFirstTrack()).returns(() => trackModel1); service.enqueueAndPlayTracks(trackModels); queueMock.setup((x) => x.getNextTrack(It.isObjectWith({ path: 'Path 1' }), false)).returns(() => trackModel2); @@ -524,6 +547,8 @@ describe('PlaybackService', () => { }); it('should listen to progress changes, set the progress in the service and publish progress changed.', () => { + const service: BasePlaybackService = createService(); + let subscribedProgress: PlaybackProgress | undefined; subscription.add( @@ -548,6 +573,8 @@ describe('PlaybackService', () => { describe('toggleLoopMode', () => { it('should set loopMode to All when loopMode is None', () => { // Arrange + const service: BasePlaybackService = createService(); + while (service.loopMode !== LoopMode.None) { service.toggleLoopMode(); } @@ -561,6 +588,8 @@ describe('PlaybackService', () => { it('should set loopMode to One when loopMode is All', () => { // Arrange + const service: BasePlaybackService = createService(); + while (service.loopMode !== LoopMode.All) { service.toggleLoopMode(); } @@ -574,6 +603,8 @@ describe('PlaybackService', () => { it('should set loopMode to None when loopMode is One', () => { // Arrange + const service: BasePlaybackService = createService(); + while (service.loopMode !== LoopMode.One) { service.toggleLoopMode(); } @@ -589,6 +620,8 @@ describe('PlaybackService', () => { describe('toggleIsShuffled', () => { it('should enable shuffle when shuffle is disabled', () => { // Arrange + const service: BasePlaybackService = createService(); + while (service.isShuffled !== false) { service.toggleIsShuffled(); } @@ -602,6 +635,8 @@ describe('PlaybackService', () => { it('should shuffle the queue when shuffle is disabled', () => { // Arrange + const service: BasePlaybackService = createService(); + while (service.isShuffled !== false) { service.toggleIsShuffled(); } @@ -615,6 +650,8 @@ describe('PlaybackService', () => { it('should not unshuffle the queue when shuffle is disabled', () => { // Arrange + const service: BasePlaybackService = createService(); + while (service.isShuffled !== false) { service.toggleIsShuffled(); } @@ -628,6 +665,8 @@ describe('PlaybackService', () => { it('should disable shuffle when shuffle is enabled', () => { // Arrange + const service: BasePlaybackService = createService(); + while (service.isShuffled !== true) { service.toggleIsShuffled(); } @@ -641,6 +680,8 @@ describe('PlaybackService', () => { it('should have shuffled the queue when shuffle is enabled', () => { // Arrange + const service: BasePlaybackService = createService(); + while (service.isShuffled !== true) { service.toggleIsShuffled(); } @@ -654,6 +695,8 @@ describe('PlaybackService', () => { it('should unshuffle the queue when shuffle is enabled', () => { // Arrange + const service: BasePlaybackService = createService(); + while (service.isShuffled !== true) { service.toggleIsShuffled(); } @@ -669,6 +712,8 @@ describe('PlaybackService', () => { describe('enqueueAndPlayTracks', () => { it('should not add tracks to the queue if tracks is empty', () => { // Arrange + const service: BasePlaybackService = createService(); + queueMock.setup((x) => x.getFirstTrack()).returns(() => trackModel1); // Act @@ -680,6 +725,8 @@ describe('PlaybackService', () => { it('should not start playback if tracks is empty', () => { // Arrange + const service: BasePlaybackService = createService(); + queueMock.setup((x) => x.getFirstTrack()).returns(() => trackModel1); // Act @@ -695,6 +742,8 @@ describe('PlaybackService', () => { it('should add tracks to the queue unshuffled if shuffle is disabled', () => { // Arrange + const service: BasePlaybackService = createService(); + queueMock.setup((x) => x.getFirstTrack()).returns(() => trackModel1); // Act @@ -706,6 +755,8 @@ describe('PlaybackService', () => { it('should add tracks to the queue shuffled if shuffle is enabled', () => { // Arrange + const service: BasePlaybackService = createService(); + service.toggleIsShuffled(); queueMock.setup((x) => x.getFirstTrack()).returns(() => trackModel1); @@ -718,6 +769,8 @@ describe('PlaybackService', () => { it('should start playback', () => { // Arrange + const service: BasePlaybackService = createService(); + audioPlayerMock.reset(); audioPlayerMock.setup((x) => x.stop()).verifiable(Times.once(), ExpectedCallType.InSequence); audioPlayerMock.setup((x) => x.play(trackModel1.path)).verifiable(Times.once(), ExpectedCallType.InSequence); @@ -737,6 +790,8 @@ describe('PlaybackService', () => { it('should raise an event that playback has started, containing the current track and if a next track is being played.', () => { // Arrange + const service: BasePlaybackService = createService(); + let receivedTrack: TrackModel | undefined; let isPlayingPreviousTrack: boolean = true; subscription.add( @@ -760,6 +815,7 @@ describe('PlaybackService', () => { describe('enqueueAndPlayTracksStartingFromGivenTrack', () => { it('should not add tracks to the queue if tracks is empty', () => { // Arrange + const service: BasePlaybackService = createService(); // Act service.enqueueAndPlayTracksStartingFromGivenTrack([], trackModel1); @@ -770,6 +826,7 @@ describe('PlaybackService', () => { it('should not start playback if tracks is empty', () => { // Arrange + const service: BasePlaybackService = createService(); // Act service.enqueueAndPlayTracksStartingFromGivenTrack([], trackModel1); @@ -784,6 +841,7 @@ describe('PlaybackService', () => { it('should add tracks to the queue unshuffled if shuffle is disabled', () => { // Arrange + const service: BasePlaybackService = createService(); // Act service.enqueueAndPlayTracksStartingFromGivenTrack(trackModels, trackModel1); @@ -794,6 +852,7 @@ describe('PlaybackService', () => { it('should add tracks to the queue shuffled if shuffle is enabled', () => { // Arrange + const service: BasePlaybackService = createService(); service.toggleIsShuffled(); // Act @@ -805,6 +864,8 @@ describe('PlaybackService', () => { it('should start playback', () => { // Arrange + const service: BasePlaybackService = createService(); + audioPlayerMock.reset(); audioPlayerMock.setup((x) => x.stop()).verifiable(Times.once(), ExpectedCallType.InSequence); audioPlayerMock.setup((x) => x.play(trackModel1.path)).verifiable(Times.once(), ExpectedCallType.InSequence); @@ -823,6 +884,8 @@ describe('PlaybackService', () => { it('should raise an event that playback has started, containing the current track and if a next track is being played.', () => { // Arrange + const service: BasePlaybackService = createService(); + let receivedTrack: TrackModel | undefined; let isPlayingPreviousTrack: boolean = true; subscription.add( @@ -844,6 +907,8 @@ describe('PlaybackService', () => { describe('enqueueAndPlayArtist', () => { it('should get tracks for the given artist', () => { // Arrange + const service: BasePlaybackService = createService(); + const artistToPlay: ArtistModel = new ArtistModel('artist1', translatorServiceMock.object); queueMock.setup((x) => x.getFirstTrack()).returns(() => trackModel1); @@ -856,6 +921,8 @@ describe('PlaybackService', () => { it('should order tracks for the artist byAlbum', () => { // Arrange + const service: BasePlaybackService = createService(); + const artistToPlay: ArtistModel = new ArtistModel('artist1', translatorServiceMock.object); queueMock.setup((x) => x.getFirstTrack()).returns(() => trackModel1); @@ -868,6 +935,8 @@ describe('PlaybackService', () => { it('should add tracks to the queue ordered by album', () => { // Arrange + const service: BasePlaybackService = createService(); + const artistToPlay: ArtistModel = new ArtistModel('artist1', translatorServiceMock.object); queueMock.setup((x) => x.getFirstTrack()).returns(() => trackModel1); @@ -880,6 +949,8 @@ describe('PlaybackService', () => { it('should start playback', () => { // Arrange + const service: BasePlaybackService = createService(); + const artistToPlay: ArtistModel = new ArtistModel('artist1', translatorServiceMock.object); audioPlayerMock.reset(); audioPlayerMock.setup((x) => x.stop()).verifiable(Times.once(), ExpectedCallType.InSequence); @@ -900,6 +971,8 @@ describe('PlaybackService', () => { it('should raise an event that playback has started, containing the current track and if a next track is being played.', () => { // Arrange + const service: BasePlaybackService = createService(); + const artistToPlay: ArtistModel = new ArtistModel('artist1', translatorServiceMock.object); let receivedTrack: TrackModel | undefined; let isPlayingPreviousTrack: boolean = true; @@ -924,6 +997,8 @@ describe('PlaybackService', () => { describe('enqueueAndPlayGenre', () => { it('should get tracks for the given genre', () => { // Arrange + const service: BasePlaybackService = createService(); + const genreToPlay: GenreModel = new GenreModel('genre1', translatorServiceMock.object); queueMock.setup((x) => x.getFirstTrack()).returns(() => trackModel1); @@ -936,6 +1011,8 @@ describe('PlaybackService', () => { it('should order tracks for the artist byAlbum', () => { // Arrange + const service: BasePlaybackService = createService(); + const genreToPlay: GenreModel = new GenreModel('genre1', translatorServiceMock.object); queueMock.setup((x) => x.getFirstTrack()).returns(() => trackModel1); @@ -948,6 +1025,8 @@ describe('PlaybackService', () => { it('should add tracks to the queue ordered by album', () => { // Arrange + const service: BasePlaybackService = createService(); + const genreToPlay: GenreModel = new GenreModel('genre1', translatorServiceMock.object); queueMock.setup((x) => x.getFirstTrack()).returns(() => trackModel1); @@ -960,6 +1039,8 @@ describe('PlaybackService', () => { it('should start playback', () => { // Arrange + const service: BasePlaybackService = createService(); + const genreToPlay: GenreModel = new GenreModel('genre1', translatorServiceMock.object); audioPlayerMock.reset(); @@ -981,6 +1062,8 @@ describe('PlaybackService', () => { it('should raise an event that playback has started, containing the current track and if a next track is being played.', () => { // Arrange + const service: BasePlaybackService = createService(); + const genreToPlay: GenreModel = new GenreModel('genre1', translatorServiceMock.object); audioPlayerMock.reset(); let receivedTrack: TrackModel | undefined; @@ -1005,6 +1088,8 @@ describe('PlaybackService', () => { describe('enqueueAndPlayAlbum', () => { it('should get tracks for the given album', () => { // Arrange + const service: BasePlaybackService = createService(); + queueMock.setup((x) => x.getFirstTrack()).returns(() => trackModel1); // Act @@ -1016,6 +1101,8 @@ describe('PlaybackService', () => { it('should order tracks for the album byAlbum', () => { // Arrange + const service: BasePlaybackService = createService(); + queueMock.setup((x) => x.getFirstTrack()).returns(() => trackModel1); // Act @@ -1027,6 +1114,8 @@ describe('PlaybackService', () => { it('should add tracks to the queue ordered by album', () => { // Arrange + const service: BasePlaybackService = createService(); + queueMock.setup((x) => x.getFirstTrack()).returns(() => trackModel1); // Act @@ -1038,6 +1127,8 @@ describe('PlaybackService', () => { it('should start playback', () => { // Arrange + const service: BasePlaybackService = createService(); + audioPlayerMock.reset(); audioPlayerMock.setup((x) => x.stop()).verifiable(Times.once(), ExpectedCallType.InSequence); audioPlayerMock.setup((x) => x.play(trackModel2.path)).verifiable(Times.once(), ExpectedCallType.InSequence); @@ -1057,6 +1148,8 @@ describe('PlaybackService', () => { it('should raise an event that playback has started, containing the current track and if a next track is being played.', () => { // Arrange + const service: BasePlaybackService = createService(); + let receivedTrack: TrackModel | undefined; let isPlayingPreviousTrack: boolean = true; subscription.add( @@ -1084,6 +1177,8 @@ describe('PlaybackService', () => { describe('pause', () => { it('should pause playback', () => { // Arrange + const service: BasePlaybackService = createService(); + queueMock.setup((x) => x.getFirstTrack()).returns(() => trackModel1); service.enqueueAndPlayTracks(trackModels); @@ -1100,6 +1195,8 @@ describe('PlaybackService', () => { it('should raise an event that playback is paused.', () => { // Arrange + const service: BasePlaybackService = createService(); + queueMock.setup((x) => x.getFirstTrack()).returns(() => trackModel1); service.enqueueAndPlayTracks(trackModels); let playbackIsPaused: boolean = false; @@ -1121,6 +1218,8 @@ describe('PlaybackService', () => { describe('resume', () => { it('should resume playback if playing', () => { // Arrange + const service: BasePlaybackService = createService(); + queueMock.setup((x) => x.getFirstTrack()).returns(() => trackModel1); service.enqueueAndPlayTracks(trackModels); audioPlayerMock.reset(); @@ -1139,6 +1238,7 @@ describe('PlaybackService', () => { it('should not resume playback if not playing', () => { // Arrange + const service: BasePlaybackService = createService(); // Act service.resume(); @@ -1153,6 +1253,8 @@ describe('PlaybackService', () => { it('should raise an event that playback is resumed if playing', () => { // Arrange + const service: BasePlaybackService = createService(); + queueMock.setup((x) => x.getFirstTrack()).returns(() => trackModel1); service.enqueueAndPlayTracks(trackModels); audioPlayerMock.reset(); @@ -1174,6 +1276,8 @@ describe('PlaybackService', () => { it('should raise an event that playback is resumed if not playing', () => { // Arrange + const service: BasePlaybackService = createService(); + let playbackIsResumed: boolean = false; subscription.add( @@ -1193,6 +1297,8 @@ describe('PlaybackService', () => { describe('skipByFractionOfTotalSeconds', () => { it('should skip to a fraction of the total seconds', () => { // Arrange + const service: BasePlaybackService = createService(); + audioPlayerMock.setup((x) => x.totalSeconds).returns(() => 60); // Act @@ -1204,6 +1310,8 @@ describe('PlaybackService', () => { it('should immediately set progress', () => { // Arrange + const service: BasePlaybackService = createService(); + audioPlayerMock.setup((x) => x.totalSeconds).returns(() => 60); const progress: PlaybackProgress = new PlaybackProgress(20, 200); progressUpdaterMock.setup((x) => x.getCurrentProgress()).returns(() => progress); @@ -1218,6 +1326,8 @@ describe('PlaybackService', () => { it('should raise an event that playback was skipped', () => { // Arrange + const service: BasePlaybackService = createService(); + audioPlayerMock.setup((x) => x.totalSeconds).returns(() => 60); progressUpdaterMock.setup((x) => x.getCurrentProgress()).returns(() => new PlaybackProgress(20, 200)); @@ -1239,6 +1349,8 @@ describe('PlaybackService', () => { describe('playPrevious', () => { it('should play the current track if there is a current track and playback lasted for more than 3 seconds', () => { // Arrange + const service: BasePlaybackService = createService(); + queueMock.setup((x) => x.getFirstTrack()).returns(() => trackModel1); service.enqueueAndPlayTracks(trackModels); audioPlayerMock.reset(); @@ -1258,6 +1370,8 @@ describe('PlaybackService', () => { it('should play the previous track if found and playback lasted for less then 3 seconds', () => { // Arrange + const service: BasePlaybackService = createService(); + queueMock.setup((x) => x.getFirstTrack()).returns(() => trackModel1); service.enqueueAndPlayTracks(trackModels); queueMock.setup((x) => x.getPreviousTrack(trackModel1, false)).returns(() => trackModel2); @@ -1278,6 +1392,8 @@ describe('PlaybackService', () => { it('should stop playback if a previous track was not found and playback lasted for less then 3 seconds', () => { // Arrange + const service: BasePlaybackService = createService(); + queueMock.setup((x) => x.getFirstTrack()).returns(() => trackModel1); service.enqueueAndPlayTracks(trackModels); queueMock.setup((x) => x.getPreviousTrack(trackModel1, false)).returns(() => undefined); @@ -1298,6 +1414,8 @@ describe('PlaybackService', () => { it('should raise an event that playback is stopped if a previous track was not found and playback lasted for less then 3 seconds.', () => { // Arrange + const service: BasePlaybackService = createService(); + queueMock.setup((x) => x.getFirstTrack()).returns(() => trackModel1); service.enqueueAndPlayTracks(trackModels); queueMock.setup((x) => x.getPreviousTrack(trackModel1, false)).returns(() => undefined); @@ -1320,6 +1438,8 @@ describe('PlaybackService', () => { it('should set the current track to undefined before raising a stop event', () => { // Arrange + const service: BasePlaybackService = createService(); + queueMock.setup((x) => x.getFirstTrack()).returns(() => trackModel1); service.enqueueAndPlayTracks(trackModels); queueMock.setup((x) => x.getNextTrack(It.isAny(), false)).returns(() => undefined); @@ -1340,6 +1460,8 @@ describe('PlaybackService', () => { it('should get the previous track without wrap around if loopMode is None', () => { // Arrange + const service: BasePlaybackService = createService(); + while (service.loopMode !== LoopMode.None) { service.toggleLoopMode(); } @@ -1353,6 +1475,8 @@ describe('PlaybackService', () => { it('should get the previous track with wrap around if loopMode is All', () => { // Arrange + const service: BasePlaybackService = createService(); + while (service.loopMode !== LoopMode.All) { service.toggleLoopMode(); } @@ -1366,6 +1490,8 @@ describe('PlaybackService', () => { it('should get the previous track with wrap around if loopMode is One', () => { // Arrange + const service: BasePlaybackService = createService(); + while (service.loopMode !== LoopMode.One) { service.toggleLoopMode(); } @@ -1379,6 +1505,8 @@ describe('PlaybackService', () => { it('should raise an event that playback has started, containing the current track and if a previous track is being played.', () => { // Arrange + const service: BasePlaybackService = createService(); + queueMock.setup((x) => x.getFirstTrack()).returns(() => trackModel1); service.enqueueAndPlayTracks(trackModels); queueMock.setup((x) => x.getPreviousTrack(trackModel1, false)).returns(() => trackModel2); @@ -1401,6 +1529,8 @@ describe('PlaybackService', () => { describe('playNext', () => { it('should stop playback if a next track is not found', () => { // Arrange + const service: BasePlaybackService = createService(); + queueMock.setup((x) => x.getNextTrack(It.isObjectWith({ path: 'Path 1' }), false)).returns(() => undefined); progressUpdaterMock.reset(); audioPlayerMock.reset(); @@ -1422,6 +1552,8 @@ describe('PlaybackService', () => { it('should raise an event that playback is stopped if a next track is not found', () => { // Arrange + const service: BasePlaybackService = createService(); + queueMock.setup((x) => x.getFirstTrack()).returns(() => trackModel1); service.enqueueAndPlayTracks(trackModels); queueMock.setup((x) => x.getNextTrack(It.isObjectWith({ path: 'Path 1' }), false)).returns(() => undefined); @@ -1444,6 +1576,8 @@ describe('PlaybackService', () => { it('should set the current track to undefined before raising a stop event', () => { // Arrange + const service: BasePlaybackService = createService(); + queueMock.setup((x) => x.getFirstTrack()).returns(() => trackModel1); service.enqueueAndPlayTracks(trackModels); queueMock.setup((x) => x.getNextTrack(It.isObjectWith({ path: 'Path 1' }), false)).returns(() => undefined); @@ -1466,6 +1600,8 @@ describe('PlaybackService', () => { it('should play the next track if found', () => { // Arrange + const service: BasePlaybackService = createService(); + queueMock.setup((x) => x.getFirstTrack()).returns(() => trackModel1); service.enqueueAndPlayTracks(trackModels); queueMock.setup((x) => x.getNextTrack(It.isObjectWith({ path: 'Path 1' }), false)).returns(() => trackModel2); @@ -1487,6 +1623,8 @@ describe('PlaybackService', () => { it('should get the next track without wrap around if loopMode is None', () => { // Arrange + const service: BasePlaybackService = createService(); + while (service.loopMode !== LoopMode.None) { service.toggleLoopMode(); } @@ -1499,6 +1637,8 @@ describe('PlaybackService', () => { }); it('should get the next track with wrap around if loopMode is All', () => { // Arrange + const service: BasePlaybackService = createService(); + while (service.loopMode !== LoopMode.All) { service.toggleLoopMode(); } @@ -1511,6 +1651,8 @@ describe('PlaybackService', () => { }); it('should get the next track with wrap around if loopMode is One', () => { // Arrange + const service: BasePlaybackService = createService(); + while (service.loopMode !== LoopMode.One) { service.toggleLoopMode(); } @@ -1524,6 +1666,8 @@ describe('PlaybackService', () => { it('should raise an event that playback has started, containing the current track and if a next track is being played.', () => { // Arrange + const service: BasePlaybackService = createService(); + queueMock.setup((x) => x.getFirstTrack()).returns(() => trackModel1); service.enqueueAndPlayTracks(trackModels); queueMock.setup((x) => x.getNextTrack(It.isObjectWith({ path: 'Path 1' }), false)).returns(() => trackModel2); @@ -1545,6 +1689,7 @@ describe('PlaybackService', () => { }); it('should increase play count and date last played for the current track if progress is more than 80%', () => { + const service: BasePlaybackService = createService(); const trackModelMock: IMock = Mock.ofType(); queueMock.setup((x) => x.getFirstTrack()).returns(() => trackModelMock.object); service.enqueueAndPlayTracks([trackModelMock.object]); @@ -1558,6 +1703,7 @@ describe('PlaybackService', () => { }); it('should save play count and date last played for the current track if progress is more than 80%', () => { + const service: BasePlaybackService = createService(); const trackModelMock: IMock = Mock.ofType(); queueMock.setup((x) => x.getFirstTrack()).returns(() => trackModelMock.object); service.enqueueAndPlayTracks([trackModelMock.object]); @@ -1571,6 +1717,7 @@ describe('PlaybackService', () => { }); it('should increase skip count for the current track if progress is less than 80%', () => { + const service: BasePlaybackService = createService(); const trackModelMock: IMock = Mock.ofType(); queueMock.setup((x) => x.getFirstTrack()).returns(() => trackModelMock.object); service.enqueueAndPlayTracks([trackModelMock.object]); @@ -1584,6 +1731,7 @@ describe('PlaybackService', () => { }); it('should save skip count for the current track if progress is less than 80%', () => { + const service: BasePlaybackService = createService(); const trackModelMock: IMock = Mock.ofType(); queueMock.setup((x) => x.getFirstTrack()).returns(() => trackModelMock.object); service.enqueueAndPlayTracks([trackModelMock.object]); @@ -1600,6 +1748,7 @@ describe('PlaybackService', () => { describe('volume', () => { it('should return the volume', () => { // Arrange + const service: BasePlaybackService = createService(); // Act const volume: number = service.volume; @@ -1610,6 +1759,8 @@ describe('PlaybackService', () => { it('should set the provided volume if a volume between 0 and 1 exclusive is provided', () => { // Arrange + const service: BasePlaybackService = createService(); + mathExtensionsMock.setup((x) => x.clamp(0.8, 0, 1)).returns(() => 0.8); // Act @@ -1621,6 +1772,8 @@ describe('PlaybackService', () => { it('should set the audio player volume to the provided volume if a volume between 0 and 1 exclusive is provided', () => { // Arrange + const service: BasePlaybackService = createService(); + mathExtensionsMock.setup((x) => x.clamp(0.8, 0, 1)).returns(() => 0.8); // Act @@ -1632,6 +1785,8 @@ describe('PlaybackService', () => { it('should save the provided volume in the settings if a volume between 0 and 1 exclusive is provided', () => { // Arrange + const service: BasePlaybackService = createService(); + mathExtensionsMock.setup((x) => x.clamp(0.8, 0, 1)).returns(() => 0.8); // Act @@ -1643,6 +1798,8 @@ describe('PlaybackService', () => { it('should set the provided volume if a volume of 0 is provided', () => { // Arrange + const service: BasePlaybackService = createService(); + mathExtensionsMock.setup((x) => x.clamp(0, 0, 1)).returns(() => 0); // Act @@ -1654,6 +1811,8 @@ describe('PlaybackService', () => { it('should set the audio player volume to the provided volume if a volume of 0 is provided', () => { // Arrange + const service: BasePlaybackService = createService(); + mathExtensionsMock.setup((x) => x.clamp(0, 0, 1)).returns(() => 0); // Act @@ -1665,6 +1824,8 @@ describe('PlaybackService', () => { it('should save the provided volume in the settings if a volume of 0 is provided', () => { // Arrange + const service: BasePlaybackService = createService(); + mathExtensionsMock.setup((x) => x.clamp(0, 0, 1)).returns(() => 0); // Act @@ -1676,6 +1837,8 @@ describe('PlaybackService', () => { it('should set the provided volume if a volume of 1 is provided', () => { // Arrange + const service: BasePlaybackService = createService(); + mathExtensionsMock.setup((x) => x.clamp(1, 0, 1)).returns(() => 1); // Act @@ -1687,6 +1850,8 @@ describe('PlaybackService', () => { it('should set the audio player volume to the provided volume if a volume of 1 is provided', () => { // Arrange + const service: BasePlaybackService = createService(); + mathExtensionsMock.setup((x) => x.clamp(1, 0, 1)).returns(() => 1); // Act @@ -1698,6 +1863,8 @@ describe('PlaybackService', () => { it('should save the provided volume in the settings if a volume of 1 is provided', () => { // Arrange + const service: BasePlaybackService = createService(); + mathExtensionsMock.setup((x) => x.clamp(1, 0, 1)).returns(() => 1); // Act @@ -1709,6 +1876,8 @@ describe('PlaybackService', () => { it('should set the volume to 0 if a volume smaller than 0 is provided', () => { // Arrange + const service: BasePlaybackService = createService(); + mathExtensionsMock.setup((x) => x.clamp(-0.5, 0, 1)).returns(() => 0); // Act @@ -1720,6 +1889,8 @@ describe('PlaybackService', () => { it('should set the audio player volume to 0 if a volume smaller than 0 is provided', () => { // Arrange + const service: BasePlaybackService = createService(); + mathExtensionsMock.setup((x) => x.clamp(-0.5, 0, 1)).returns(() => 0); // Act @@ -1731,6 +1902,8 @@ describe('PlaybackService', () => { it('should save a volume of 0 in the settings if a volume smaller than 0 is provided', () => { // Arrange + const service: BasePlaybackService = createService(); + mathExtensionsMock.setup((x) => x.clamp(-0.5, 0, 1)).returns(() => 0); // Act @@ -1742,6 +1915,8 @@ describe('PlaybackService', () => { it('should set the volume to 1 if a volume greater than 1 is provided', () => { // Arrange + const service: BasePlaybackService = createService(); + mathExtensionsMock.setup((x) => x.clamp(1.5, 0, 1)).returns(() => 1); // Act @@ -1753,6 +1928,8 @@ describe('PlaybackService', () => { it('should set the audio player volume to 1 if a volume greater than 1 is provided', () => { // Arrange + const service: BasePlaybackService = createService(); + mathExtensionsMock.setup((x) => x.clamp(1.5, 0, 1)).returns(() => 1); // Act @@ -1764,6 +1941,8 @@ describe('PlaybackService', () => { it('should save a volume of 1 in the settings if a volume greater than 1 is provided', () => { // Arrange + const service: BasePlaybackService = createService(); + mathExtensionsMock.setup((x) => x.clamp(1.5, 0, 1)).returns(() => 1); // Act @@ -1780,18 +1959,8 @@ describe('PlaybackService', () => { queueMock.reset(); queueMock.setup((x) => x.tracks).returns(() => []); queueMock.setup((x) => x.tracksInPlaybackOrder).returns(() => []); - service = new PlaybackService( - trackServiceMock.object, - playlistServiceMock.object, - snackBarServiceMock.object, - audioPlayerMock.object, - trackOrderingMock.object, - queueMock.object, - progressUpdaterMock.object, - mathExtensionsMock.object, - settingsStub, - loggerMock.object - ); + + const service: BasePlaybackService = createService(); // Act const queue: TrackModels = service.playbackQueue; @@ -1805,18 +1974,8 @@ describe('PlaybackService', () => { queueMock.reset(); queueMock.setup((x) => x.tracks).returns(() => tracks.tracks); queueMock.setup((x) => x.tracksInPlaybackOrder).returns(() => tracks.tracks); - service = new PlaybackService( - trackServiceMock.object, - playlistServiceMock.object, - snackBarServiceMock.object, - audioPlayerMock.object, - trackOrderingMock.object, - queueMock.object, - progressUpdaterMock.object, - mathExtensionsMock.object, - settingsStub, - loggerMock.object - ); + + const service: BasePlaybackService = createService(); // Act const queue: TrackModels = service.playbackQueue; @@ -1833,6 +1992,8 @@ describe('PlaybackService', () => { describe('playQueuedTrack', () => { it('should start playback', () => { // Arrange + const service: BasePlaybackService = createService(); + audioPlayerMock.reset(); audioPlayerMock.setup((x) => x.stop()).verifiable(Times.once(), ExpectedCallType.InSequence); audioPlayerMock.setup((x) => x.play(trackModel2.path)).verifiable(Times.once(), ExpectedCallType.InSequence); @@ -1851,6 +2012,8 @@ describe('PlaybackService', () => { it('should raise an event that playback has started, containing the current track and if a next track is being played.', () => { // Arrange + const service: BasePlaybackService = createService(); + let receivedTrack: TrackModel | undefined; let isPlayingPreviousTrack: boolean = true; subscription.add( @@ -1872,6 +2035,8 @@ describe('PlaybackService', () => { describe('togglePlayback', () => { it('should resume playback if paused', () => { // Arrange + const service: BasePlaybackService = createService(); + queueMock.setup((x) => x.getFirstTrack()).returns(() => trackModel1); service.enqueueAndPlayTracks(trackModels); audioPlayerMock.reset(); @@ -1888,6 +2053,8 @@ describe('PlaybackService', () => { it('should pause playback if playing', () => { // Arrange + const service: BasePlaybackService = createService(); + queueMock.setup((x) => x.getFirstTrack()).returns(() => trackModel1); service.enqueueAndPlayTracks(trackModels); audioPlayerMock.reset(); @@ -1905,6 +2072,7 @@ describe('PlaybackService', () => { describe('removeFromQueue', () => { it('should not remove tracks when tracksToRemove is empty', () => { // Arrange + const service: BasePlaybackService = createService(); // Act service.removeFromQueue([]); @@ -1915,6 +2083,7 @@ describe('PlaybackService', () => { it('should remove tracks when tracksToRemove has items', () => { // Arrange + const service: BasePlaybackService = createService(); // Act service.removeFromQueue([trackModel1]); @@ -1927,6 +2096,7 @@ describe('PlaybackService', () => { describe('addTracksToQueueAsync', () => { it('should not add tracks to the queue if tracksToAdd is empty', async () => { // Arrange + const service: BasePlaybackService = createService(); // Act await service.addTracksToQueueAsync([]); @@ -1939,6 +2109,7 @@ describe('PlaybackService', () => { it('should add tracks to the queue if tracksToAdd has tracks', async () => { // Arrange + const service: BasePlaybackService = createService(); // Act await service.addTracksToQueueAsync([trackModel1]); @@ -1952,6 +2123,7 @@ describe('PlaybackService', () => { describe('addArtistToQueueAsync', () => { it('should get tracks for the given artist and artistType', async () => { // Arrange + const service: BasePlaybackService = createService(); const artistToAdd: ArtistModel = new ArtistModel('artist1', translatorServiceMock.object); // Act @@ -1963,6 +2135,7 @@ describe('PlaybackService', () => { it('should order tracks for the artist byAlbum', async () => { // Arrange + const service: BasePlaybackService = createService(); const artistToAdd: ArtistModel = new ArtistModel('artist1', translatorServiceMock.object); // Act @@ -1974,6 +2147,7 @@ describe('PlaybackService', () => { it('should add tracks to the queue ordered by album', async () => { // Arrange + const service: BasePlaybackService = createService(); const artistToAdd: ArtistModel = new ArtistModel('artist1', translatorServiceMock.object); // Act @@ -1988,6 +2162,7 @@ describe('PlaybackService', () => { describe('addGenreToQueueAsync', () => { it('should get tracks for the given genre', async () => { // Arrange + const service: BasePlaybackService = createService(); const genreToAdd: GenreModel = new GenreModel('genre1', translatorServiceMock.object); // Act @@ -1999,6 +2174,7 @@ describe('PlaybackService', () => { it('should order tracks for the artist byAlbum', async () => { // Arrange + const service: BasePlaybackService = createService(); const genreToAdd: GenreModel = new GenreModel('genre1', translatorServiceMock.object); // Act @@ -2010,6 +2186,7 @@ describe('PlaybackService', () => { it('should add tracks to the queue ordered by album', async () => { // Arrange + const service: BasePlaybackService = createService(); const genreToAdd: GenreModel = new GenreModel('genre1', translatorServiceMock.object); // Act @@ -2024,9 +2201,10 @@ describe('PlaybackService', () => { describe('addAlbumToQueueAsync', () => { it('should get tracks for the given album', async () => { // Arrange + const service: BasePlaybackService = createService(); // Act - await service.addAlbumToQueue(album1); + await service.addAlbumToQueueAsync(album1); // Assert trackServiceMock.verify((x) => x.getTracksForAlbums([album1.albumKey]), Times.exactly(1)); @@ -2034,9 +2212,10 @@ describe('PlaybackService', () => { it('should order tracks for the album byAlbum', async () => { // Arrange + const service: BasePlaybackService = createService(); // Act - await service.addAlbumToQueue(album1); + await service.addAlbumToQueueAsync(album1); // Assert trackOrderingMock.verify((x) => x.getTracksOrderedByAlbum(tracks.tracks), Times.exactly(1)); @@ -2044,9 +2223,10 @@ describe('PlaybackService', () => { it('should add tracks to the queue ordered by album', async () => { // Arrange + const service: BasePlaybackService = createService(); // Act - await service.addAlbumToQueue(album1); + await service.addAlbumToQueueAsync(album1); // Assert queueMock.verify((x) => x.addTracks(orderedTrackModels), Times.exactly(1)); @@ -2061,6 +2241,7 @@ describe('PlaybackService', () => { describe('stopIfPlaying', () => { it('should not stop playback if there is no track playing', () => { // Arrange + const service: BasePlaybackService = createService(); // Act service.stopIfPlaying(trackModel2); @@ -2071,6 +2252,7 @@ describe('PlaybackService', () => { it('should not play the next track if there is no track playing', () => { // Arrange + const service: BasePlaybackService = createService(); // Act service.stopIfPlaying(trackModel2); @@ -2082,6 +2264,7 @@ describe('PlaybackService', () => { it('should not stop playback if the given track is not playing', () => { // Arrange + const service: BasePlaybackService = createService(); queueMock.setup((x) => x.getFirstTrack()).returns(() => trackModel1); service.enqueueAndPlayTracks(trackModels); audioPlayerMock.reset(); @@ -2095,6 +2278,7 @@ describe('PlaybackService', () => { it('should not play the next track if the given track is not playing', () => { // Arrange + const service: BasePlaybackService = createService(); queueMock.setup((x) => x.getFirstTrack()).returns(() => trackModel1); service.enqueueAndPlayTracks(trackModels); audioPlayerMock.reset(); @@ -2109,6 +2293,7 @@ describe('PlaybackService', () => { it('should stop playback if the given track is playing and it is the only track in the queue', () => { // Arrange + const service: BasePlaybackService = createService(); queueMock.setup((x) => x.getFirstTrack()).returns(() => trackModel1); service.enqueueAndPlayTracks([trackModel1]); audioPlayerMock.reset(); @@ -2124,6 +2309,7 @@ describe('PlaybackService', () => { it('should play the next track if the given track is playing and it not the only track in the queue', () => { // Arrange + const service: BasePlaybackService = createService(); queueMock.setup((x) => x.getFirstTrack()).returns(() => trackModel1); service.enqueueAndPlayTracks(trackModels); audioPlayerMock.reset(); @@ -2138,4 +2324,66 @@ describe('PlaybackService', () => { audioPlayerMock.verify((x) => x.play(trackModel2.path), Times.once()); }); }); + + describe('toggleMute', () => { + it('should apply a volume of 0 and save the muted state and volume to the settings when toggling to muted', () => { + // Arrange + settingsStub.isMuted = false; + settingsStub.volume = 0.9; + mathExtensionsMock.setup((x) => x.clamp(0, 0, 1)).returns(() => 0); + + const service: BasePlaybackService = createService(); + + // Act + service.toggleMute(); + + // Assert + expect(service.volume).toEqual(0); + audioPlayerMock.verify((x) => x.setVolume(0), Times.once()); + expect(settingsStub.isMuted).toBeTruthy(); + expect(settingsStub.volume).toEqual(0); + }); + + it('should apply the volume from before muting if it is not 0 and save the unmuted state and volume to the settings when toggling to unmuted', () => { + // Arrange + settingsStub.isMuted = false; + settingsStub.volume = 0.9; + mathExtensionsMock.setup((x) => x.clamp(0, 0, 1)).returns(() => 0); + mathExtensionsMock.setup((x) => x.clamp(0.9, 0, 1)).returns(() => 0.9); + + const service: BasePlaybackService = createService(); + service.toggleMute(); + audioPlayerMock.reset(); + + // Act + service.toggleMute(); + + // Assert + expect(service.volume).toEqual(0.9); + audioPlayerMock.verify((x) => x.setVolume(0.9), Times.once()); + expect(settingsStub.isMuted).toBeFalsy(); + expect(settingsStub.volume).toEqual(0.9); + }); + + it('should apply a volume of 0.5 if the volume before muting is 0 and save the unmuted state and volume to the settings when toggling to unmuted', () => { + // Arrange + settingsStub.isMuted = false; + settingsStub.volume = 0; + mathExtensionsMock.setup((x) => x.clamp(0, 0, 1)).returns(() => 0); + mathExtensionsMock.setup((x) => x.clamp(0.5, 0, 1)).returns(() => 0.5); + + const service: BasePlaybackService = createService(); + service.toggleMute(); + audioPlayerMock.reset(); + + // Act + service.toggleMute(); + + // Assert + expect(service.volume).toEqual(0.5); + audioPlayerMock.verify((x) => x.setVolume(0.5), Times.once()); + expect(settingsStub.isMuted).toBeFalsy(); + expect(settingsStub.volume).toEqual(0.5); + }); + }); }); diff --git a/src/app/services/playback/playback.service.ts b/src/app/services/playback/playback.service.ts index b9a99db5..757a8540 100644 --- a/src/app/services/playback/playback.service.ts +++ b/src/app/services/playback/playback.service.ts @@ -37,6 +37,8 @@ export class PlaybackService implements BasePlaybackService { private _isPlaying: boolean = false; private _canPause: boolean = false; private _canResume: boolean = true; + private _isMuted: boolean = false; + private _volumeBeforeMute: number = 0; private subscription: Subscription = new Subscription(); public constructor( @@ -73,10 +75,7 @@ export class PlaybackService implements BasePlaybackService { } public set volume(v: number) { - const volumeToSet: number = this.mathExtensions.clamp(v, 0, 1); - this._volume = volumeToSet; - this.settings.volume = volumeToSet; - this.audioPlayer.setVolume(volumeToSet); + this.applyVolume(v); } public get progress(): PlaybackProgress { @@ -190,7 +189,7 @@ export class PlaybackService implements BasePlaybackService { await this.notifyOfTracksAddedToPlaybackQueueAsync(orderedTracks.length); } - public async addAlbumToQueue(albumToAdd: AlbumModel): Promise { + public async addAlbumToQueueAsync(albumToAdd: AlbumModel): Promise { const tracksForAlbum: TrackModels = this.trackService.getTracksForAlbums([albumToAdd.albumKey]); const orderedTracks: TrackModel[] = this.trackOrdering.getTracksOrderedByAlbum(tracksForAlbum.tracks); this.queue.addTracks(orderedTracks); @@ -334,6 +333,18 @@ export class PlaybackService implements BasePlaybackService { } } + public toggleMute(): void { + if (this._isMuted) { + this.applyVolume(this._volumeBeforeMute > 0 ? this._volumeBeforeMute : 0.5); + } else { + this._volumeBeforeMute = this._volume; + this.applyVolume(0); + } + + this._isMuted = !this._isMuted; + this.settings.isMuted = this._isMuted; + } + private play(trackToPlay: TrackModel, isPlayingPreviousTrack: boolean): void { this.audioPlayer.stop(); this.audioPlayer.play(trackToPlay.path); @@ -441,10 +452,18 @@ export class PlaybackService implements BasePlaybackService { } private applyVolumeFromSettings(): void { - this._volume = this.settings.volume; + this._isMuted = this.settings.isMuted; + this._volume = this._isMuted ? 0 : this.settings.volume; this.audioPlayer.setVolume(this._volume); } + private applyVolume(volume: number): void { + const volumeToSet: number = this.mathExtensions.clamp(volume, 0, 1); + this._volume = volumeToSet; + this.settings.volume = volumeToSet; + this.audioPlayer.setVolume(volumeToSet); + } + private async notifyOfTracksAddedToPlaybackQueueAsync(numberOfAddedTracks: number): Promise { if (numberOfAddedTracks === 1) { await this.snackBarService.singleTrackAddedToPlaybackQueueAsync();