532 lines
21 KiB
TypeScript
532 lines
21 KiB
TypeScript
import { provideHttpClient } from '@angular/common/http';
|
||
import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing';
|
||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||
import { signal } from '@angular/core';
|
||
import { AgentService, RecordingSource, VideoSegment } from '../../services/agent.service';
|
||
import { StepReplayFrame } from '../../core/models/stream.model';
|
||
import { FloatingVideoPlayerComponent } from './floating-video-player.component';
|
||
|
||
describe('FloatingVideoPlayerComponent video markers', () => {
|
||
let fixture: ComponentFixture<FloatingVideoPlayerComponent>;
|
||
let component: FloatingVideoPlayerComponent;
|
||
let video: HTMLVideoElement;
|
||
let paused: boolean;
|
||
let seeking: boolean;
|
||
let time: number;
|
||
let play: jasmine.Spy;
|
||
let agentService: ReturnType<typeof createAgentService>;
|
||
|
||
function createAgentService() {
|
||
const start = 1_800_000_000;
|
||
const frame: StepReplayFrame = {
|
||
index: 0, stepNumber: 1, title: '步骤 1: 点击设置', imageUrl: 'step.png',
|
||
actionText: '点击设置', timestamp: start + 5
|
||
};
|
||
const service = {
|
||
isVideoWindowOpen: signal(true),
|
||
isVideoMinimized: signal(false),
|
||
recordingPlaybackStatus: signal('ready'),
|
||
playerMode: signal('video'),
|
||
hasCurrentSessionStepFrames: signal(true),
|
||
activeVideoTitle: signal('测试录像'),
|
||
isVideoLoading: signal(false),
|
||
activeVideoUrl: signal('/test.mp4'),
|
||
activeVideoSegments: signal<VideoSegment[]>([{ url: '/test.mp4', start: 0, duration: 20, width: 400, height: 800 }]),
|
||
recordingSources: signal<RecordingSource[]>([]),
|
||
selectedRecordingSource: signal(''),
|
||
usesSessionTimeline: signal(false),
|
||
recordingSessionDuration: signal(0),
|
||
recordingPlaybackMessage: signal(''),
|
||
selectRecordingSource: jasmine.createSpy('selectRecordingSource'),
|
||
currentSessionStepFrames: signal([frame]),
|
||
currentSession: signal({ session_id: 'test', start_time: start }),
|
||
currentSessionId: signal('test'),
|
||
sessions: signal([{ session_id: 'test', status: 'completed' }]),
|
||
selectSession: jasmine.createSpy('selectSession'),
|
||
videoSeekRequest: signal<{ seconds: number; requestId: number; sourceId?: string } | null>(null),
|
||
stepSeekRequest: signal(null),
|
||
consumeVideoAutoplay: jasmine.createSpy('consumeVideoAutoplay').and.returnValue(false)
|
||
};
|
||
service.selectRecordingSource.and.callFake((id: string) => {
|
||
const source = service.recordingSources().find(item => item.id === id);
|
||
if (!source) return false;
|
||
if (service.selectedRecordingSource() === id) return true;
|
||
service.selectedRecordingSource.set(id);
|
||
service.activeVideoSegments.set(source.segments);
|
||
service.activeVideoUrl.set(source.url || source.segments[0]?.url || '');
|
||
return true;
|
||
});
|
||
return service;
|
||
}
|
||
|
||
beforeEach(() => {
|
||
agentService = createAgentService();
|
||
TestBed.configureTestingModule({
|
||
imports: [FloatingVideoPlayerComponent],
|
||
providers: [provideHttpClient(), provideHttpClientTesting(), { provide: AgentService, useValue: agentService }]
|
||
});
|
||
|
||
fixture = TestBed.createComponent(FloatingVideoPlayerComponent);
|
||
fixture.detectChanges();
|
||
TestBed.inject(HttpTestingController).expectOne('/api/task-extensions/test').flush({
|
||
session_id: 'test', initial_prompt: '', editable: true, plugins: []
|
||
});
|
||
component = fixture.componentInstance;
|
||
video = component.videoRef!.nativeElement;
|
||
paused = true;
|
||
seeking = false;
|
||
time = 0;
|
||
Object.defineProperties(video, {
|
||
paused: { configurable: true, get: () => paused },
|
||
seeking: { configurable: true, get: () => seeking },
|
||
ended: { configurable: true, get: () => false },
|
||
duration: { configurable: true, get: () => 20 },
|
||
readyState: { configurable: true, get: () => 1 },
|
||
currentTime: {
|
||
configurable: true, get: () => time,
|
||
set: (value: number) => { time = value; seeking = true; }
|
||
}
|
||
});
|
||
spyOn(video, 'pause').and.callFake(() => {
|
||
paused = true;
|
||
video.dispatchEvent(new Event('pause'));
|
||
});
|
||
play = spyOn(video, 'play').and.callFake(() => {
|
||
paused = false;
|
||
video.dispatchEvent(new Event('play'));
|
||
return Promise.resolve();
|
||
});
|
||
video.dispatchEvent(new Event('loadedmetadata'));
|
||
});
|
||
|
||
function finishSeek(): void {
|
||
seeking = false;
|
||
video.dispatchEvent(new Event('seeked'));
|
||
fixture.detectChanges();
|
||
}
|
||
|
||
for (const mode of ['video', 'steps']) {
|
||
it(`shows review controls in ${mode} mode even without a recording`, () => {
|
||
agentService.playerMode.set(mode);
|
||
agentService.recordingPlaybackStatus.set('unavailable');
|
||
component.extensionPanels()!.refresh(); fixture.detectChanges();
|
||
TestBed.inject(HttpTestingController).expectOne('/api/task-extensions/test').flush({
|
||
session_id: 'test', initial_prompt: '', editable: true, plugins: [{
|
||
plugin_id: 'review', name: '原始用例', version: '1.1.0', revision: 0, values: {}, history: [],
|
||
workspace: { fields: [], panels: [{ id: 'case', title: '原始用例', side: 'left', kind: 'original_case', width: {minimum: 240, preferred: 340, maximum: 480} }] }
|
||
}]
|
||
}); fixture.detectChanges();
|
||
const button: HTMLButtonElement = fixture.nativeElement.querySelector('button[aria-label="展开原始用例"]');
|
||
expect(button).not.toBeNull(); button.click(); fixture.detectChanges();
|
||
expect(fixture.nativeElement.querySelector('aside').textContent).toContain('未记录原始用例');
|
||
});
|
||
}
|
||
|
||
it('removes the live image while hidden and reconnects it on return', () => {
|
||
const hidden = spyOnProperty(document, 'hidden', 'get').and.returnValue(false);
|
||
agentService.recordingPlaybackStatus.set('live');
|
||
fixture.detectChanges();
|
||
expect(fixture.nativeElement.querySelector('img.live-screen-img')).not.toBeNull();
|
||
|
||
hidden.and.returnValue(true);
|
||
document.dispatchEvent(new Event('visibilitychange'));
|
||
fixture.detectChanges();
|
||
expect(fixture.nativeElement.querySelector('img.live-screen-img')).toBeNull();
|
||
|
||
hidden.and.returnValue(false);
|
||
document.dispatchEvent(new Event('visibilitychange'));
|
||
fixture.detectChanges();
|
||
expect(fixture.nativeElement.querySelector('img.live-screen-img')).not.toBeNull();
|
||
expect(component.liveStreamError()).toBeFalse();
|
||
});
|
||
|
||
function playButton(): HTMLButtonElement {
|
||
return fixture.nativeElement.querySelector('.play-btn');
|
||
}
|
||
|
||
it('shows a recorded step on the video timeline and seeks when clicked', () => {
|
||
const marker = fixture.nativeElement.querySelector('.video-marker') as HTMLButtonElement;
|
||
expect(marker).not.toBeNull();
|
||
expect(marker.getAttribute('aria-label')).toContain('步骤 1');
|
||
expect(marker.style.left).toBe('25%');
|
||
|
||
const seek = spyOn(fixture.componentInstance, 'seek');
|
||
marker.click();
|
||
expect(seek).toHaveBeenCalledOnceWith(5);
|
||
});
|
||
|
||
it('resumes a playing video after a marker seek finishes and synchronizes the button', async () => {
|
||
component.togglePlay();
|
||
await Promise.resolve();
|
||
play.calls.reset();
|
||
fixture.nativeElement.querySelector('.video-marker').click();
|
||
expect(time).toBe(5);
|
||
expect(play).not.toHaveBeenCalled();
|
||
finishSeek();
|
||
await Promise.resolve();
|
||
fixture.detectChanges();
|
||
expect(paused).toBeFalse();
|
||
expect(play).toHaveBeenCalledTimes(1);
|
||
expect(playButton().title).toBe('暂停');
|
||
});
|
||
|
||
it('keeps a paused video paused after a marker seek and resumes with one click', async () => {
|
||
fixture.nativeElement.querySelector('.video-marker').click();
|
||
finishSeek();
|
||
expect(paused).toBeTrue();
|
||
expect(play).not.toHaveBeenCalled();
|
||
expect(playButton().title).toBe('播放');
|
||
playButton().click();
|
||
await Promise.resolve();
|
||
fixture.detectChanges();
|
||
expect(paused).toBeFalse();
|
||
expect(playButton().title).toBe('暂停');
|
||
});
|
||
|
||
it('preserves playing intent across rapid consecutive marker seeks', () => {
|
||
component.togglePlay();
|
||
component.seek(5);
|
||
component.seek(8);
|
||
play.calls.reset();
|
||
finishSeek();
|
||
expect(time).toBe(8);
|
||
expect(play).toHaveBeenCalledTimes(1);
|
||
expect(paused).toBeFalse();
|
||
});
|
||
|
||
it('honors play and pause clicks while the destination is still seeking', () => {
|
||
component.togglePlay();
|
||
component.seek(5);
|
||
play.calls.reset();
|
||
fixture.detectChanges();
|
||
expect(playButton().title).toBe('播放');
|
||
playButton().click();
|
||
fixture.detectChanges();
|
||
expect(playButton().title).toBe('暂停');
|
||
expect(play).not.toHaveBeenCalled();
|
||
playButton().click();
|
||
finishSeek();
|
||
expect(paused).toBeTrue();
|
||
expect(play).not.toHaveBeenCalled();
|
||
expect(playButton().title).toBe('播放');
|
||
});
|
||
|
||
it('waits for the destination segment to seek before resuming playback', () => {
|
||
agentService.activeVideoSegments.set([
|
||
{ url: '/test.mp4', start: 0, duration: 20, width: 400, height: 800 },
|
||
{ url: '/second.mp4', start: 20, duration: 20, width: 400, height: 800 }
|
||
]);
|
||
fixture.detectChanges();
|
||
component.togglePlay();
|
||
component.seek(25);
|
||
play.calls.reset();
|
||
fixture.detectChanges();
|
||
video.dispatchEvent(new Event('loadedmetadata'));
|
||
expect(component.activeSegmentIndex()).toBe(1);
|
||
expect(time).toBe(5);
|
||
expect(play).not.toHaveBeenCalled();
|
||
finishSeek();
|
||
expect(play).toHaveBeenCalledTimes(1);
|
||
expect(paused).toBeFalse();
|
||
expect(component.currentTime()).toBe(25);
|
||
});
|
||
|
||
it('keeps a paused video paused when jumping to another segment', () => {
|
||
agentService.activeVideoSegments.set([
|
||
{ url: '/test.mp4', start: 0, duration: 20, width: 400, height: 800 },
|
||
{ url: '/second.mp4', start: 20, duration: 20, width: 400, height: 800 }
|
||
]);
|
||
fixture.detectChanges();
|
||
component.seek(25);
|
||
fixture.detectChanges();
|
||
video.dispatchEvent(new Event('loadedmetadata'));
|
||
finishSeek();
|
||
expect(time).toBe(5);
|
||
expect(paused).toBeTrue();
|
||
expect(play).not.toHaveBeenCalled();
|
||
expect(playButton().title).toBe('播放');
|
||
});
|
||
|
||
it('resumes after seeking a recording without a segment manifest', () => {
|
||
agentService.activeVideoSegments.set([]);
|
||
agentService.usesSessionTimeline.set(true);
|
||
fixture.detectChanges();
|
||
video.dispatchEvent(new Event('loadedmetadata'));
|
||
component.togglePlay();
|
||
component.seek(5);
|
||
play.calls.reset();
|
||
finishSeek();
|
||
expect(time).toBe(5);
|
||
expect(component.currentTime()).toBe(5);
|
||
expect(paused).toBeFalse();
|
||
expect(play).toHaveBeenCalledTimes(1);
|
||
});
|
||
|
||
function setSources(computerStart = 0): void {
|
||
agentService.recordingSources.set([
|
||
{ id: 'android', label: '安卓', kind: 'android', target_ids: ['android'], status: 'ready',
|
||
segments: [{ url: '/test.mp4', start: 0, duration: 20, width: 400, height: 800 }] },
|
||
{ id: 'screen', label: '电脑', kind: 'computer', target_ids: ['window-1'], status: 'ready',
|
||
segments: [{ url: '/computer.mp4', start: computerStart, duration: 20, width: 1280, height: 720 }] }
|
||
]);
|
||
agentService.usesSessionTimeline.set(true);
|
||
agentService.recordingSessionDuration.set(40);
|
||
agentService.selectRecordingSource('android');
|
||
fixture.detectChanges();
|
||
}
|
||
|
||
function setGappedRecording(): void {
|
||
agentService.usesSessionTimeline.set(true);
|
||
agentService.recordingSessionDuration.set(1040);
|
||
agentService.activeVideoSegments.set([
|
||
{ url: '/test.mp4', start: 2, offset_ms: 2000, duration: 20, width: 400, height: 800 },
|
||
{ url: '/resume.mp4', start: 1000, offset_ms: 1000000, duration: 20, width: 400, height: 800 }
|
||
]);
|
||
fixture.detectChanges();
|
||
video.dispatchEvent(new Event('loadedmetadata'));
|
||
}
|
||
|
||
it('automatically skips a long pause and plays the next clip after it loads', () => {
|
||
setGappedRecording();
|
||
component.togglePlay();
|
||
play.calls.reset();
|
||
time = 20;
|
||
paused = true;
|
||
component.onEnded();
|
||
expect(component.activeSegmentIndex()).toBe(1);
|
||
expect(component.currentTime()).toBe(1000);
|
||
expect(component.gapMessage()).toBe('');
|
||
expect(component.skippedInterval()).toContain('00:22–16:40');
|
||
expect(play).not.toHaveBeenCalled();
|
||
component.onTimeUpdate(); // Late timeupdate from the ended clip must not move the new timeline.
|
||
expect(component.currentTime()).toBe(1000);
|
||
fixture.detectChanges();
|
||
video.dispatchEvent(new Event('loadedmetadata'));
|
||
finishSeek();
|
||
expect(time).toBe(0);
|
||
expect(paused).toBeFalse();
|
||
expect(play).toHaveBeenCalledTimes(1);
|
||
});
|
||
|
||
it('restarts and loops at the first recorded frame, skipping leading and trailing gaps', () => {
|
||
setGappedRecording();
|
||
component.restart();
|
||
finishSeek();
|
||
expect(component.currentTime()).toBe(2);
|
||
expect(paused).toBeFalse();
|
||
component.seek(1010, true);
|
||
fixture.detectChanges();
|
||
video.dispatchEvent(new Event('loadedmetadata'));
|
||
finishSeek();
|
||
component.isLooping.set(true);
|
||
paused = true;
|
||
component.onEnded();
|
||
fixture.detectChanges();
|
||
video.dispatchEvent(new Event('loadedmetadata'));
|
||
finishSeek();
|
||
expect(component.activeSegmentIndex()).toBe(0);
|
||
expect(component.currentTime()).toBe(2);
|
||
expect(component.gapMessage()).toBe('');
|
||
expect(paused).toBeFalse();
|
||
});
|
||
|
||
for (const playing of [false, true]) {
|
||
it(`snaps a scrub in a gap forward while preserving ${playing ? 'playing' : 'paused'} intent`, () => {
|
||
setGappedRecording();
|
||
if (playing) component.togglePlay();
|
||
const input = fixture.nativeElement.querySelector('.scrubber-range') as HTMLInputElement;
|
||
input.value = '500';
|
||
input.dispatchEvent(new Event('input'));
|
||
fixture.detectChanges();
|
||
video.dispatchEvent(new Event('loadedmetadata'));
|
||
finishSeek();
|
||
expect(component.currentTime()).toBe(1000);
|
||
expect(component.skippedInterval()).toContain('08:20–16:40');
|
||
expect(paused).toBe(!playing);
|
||
});
|
||
}
|
||
|
||
it('does not substitute another time for an exact step seek in a gap', () => {
|
||
setGappedRecording();
|
||
component.seekToSessionTime(500, true);
|
||
expect(component.currentTime()).toBe(500);
|
||
expect(component.gapMessage()).toContain('未录制');
|
||
expect(paused).toBeTrue();
|
||
expect(play).not.toHaveBeenCalled();
|
||
});
|
||
|
||
it('ends at the last recorded frame without playing the trailing gap', () => {
|
||
setGappedRecording();
|
||
component.seek(1010, true);
|
||
fixture.detectChanges();
|
||
video.dispatchEvent(new Event('loadedmetadata'));
|
||
finishSeek();
|
||
play.calls.reset();
|
||
paused = true;
|
||
component.onEnded();
|
||
expect(component.currentTime()).toBe(1020);
|
||
expect(component.isPlaying()).toBeFalse();
|
||
expect(component.skippedInterval()).toContain('录像已结束');
|
||
expect(play).not.toHaveBeenCalled();
|
||
});
|
||
|
||
it('shows actual recorded duration and coverage separately from task duration', () => {
|
||
setGappedRecording();
|
||
fixture.detectChanges();
|
||
expect(fixture.nativeElement.querySelector('.recording-coverage-summary').textContent).toContain('00:40');
|
||
expect(fixture.nativeElement.querySelector('.time-display').textContent).toContain('17:20');
|
||
expect(fixture.nativeElement.querySelectorAll('.recording-coverage span').length).toBe(2);
|
||
expect(component.recordedCoverage()[1].left).toBeCloseTo(1000 / 1040 * 100);
|
||
});
|
||
|
||
it('explains missing computer footage without treating Android video as its evidence', () => {
|
||
agentService.currentSessionStepFrames.update(frames => [...frames,
|
||
{ ...frames[0], sourceId: 'plugin:open-computer-use', sourceLabel: '电脑', index: 1 }
|
||
]);
|
||
fixture.detectChanges();
|
||
expect(fixture.nativeElement.querySelector('.recording-notice').textContent).toContain('电脑步骤没有对应录像');
|
||
agentService.recordingSources.set([
|
||
{ id: 'pc', label: '电脑', kind: 'computer', target_ids: ['plugin:open-computer-use'],
|
||
status: 'ready', segments: agentService.activeVideoSegments() }
|
||
]);
|
||
fixture.detectChanges();
|
||
expect(component.missingRecordingLabels()).toEqual([]);
|
||
});
|
||
|
||
it('handles a seek request once without seeking back on play or pause', () => {
|
||
const seek = spyOn(component, 'seekToSessionTime').and.callThrough();
|
||
agentService.videoSeekRequest.set({ seconds: 5, requestId: 1 });
|
||
fixture.detectChanges();
|
||
finishSeek();
|
||
time = 9;
|
||
component.togglePlay();
|
||
fixture.detectChanges();
|
||
expect(time).toBe(9);
|
||
component.togglePlay();
|
||
fixture.detectChanges();
|
||
expect(time).toBe(9);
|
||
expect(seek).toHaveBeenCalledOnceWith(5, false);
|
||
});
|
||
|
||
it('defers a seek command until the recording is ready', () => {
|
||
const seek = spyOn(component, 'seekToSessionTime');
|
||
agentService.recordingPlaybackStatus.set('processing');
|
||
agentService.videoSeekRequest.set({ seconds: 5, requestId: 2 });
|
||
fixture.detectChanges();
|
||
expect(seek).not.toHaveBeenCalled();
|
||
agentService.recordingPlaybackStatus.set('ready');
|
||
fixture.detectChanges();
|
||
component.onLoadedMetadata();
|
||
expect(seek).toHaveBeenCalledOnceWith(5, false);
|
||
});
|
||
|
||
it('switches to the requested source, waits for seeked and allows later manual source changes', () => {
|
||
setSources(2);
|
||
component.togglePlay();
|
||
agentService.videoSeekRequest.set({ seconds: 7, requestId: 3, sourceId: 'window-1' });
|
||
fixture.detectChanges();
|
||
play.calls.reset();
|
||
video.dispatchEvent(new Event('loadedmetadata'));
|
||
expect(time).toBe(5);
|
||
expect(play).not.toHaveBeenCalled();
|
||
finishSeek();
|
||
expect(component.currentTime()).toBe(7);
|
||
expect(paused).toBeFalse();
|
||
expect(play).toHaveBeenCalledTimes(1);
|
||
component.selectSource('android');
|
||
fixture.detectChanges();
|
||
expect(agentService.selectedRecordingSource()).toBe('android');
|
||
video.dispatchEvent(new Event('loadedmetadata'));
|
||
finishSeek();
|
||
expect(time).toBe(7);
|
||
expect(agentService.selectedRecordingSource()).toBe('android');
|
||
});
|
||
|
||
it('preserves task time when switching into a gap and advances only on explicit play', () => {
|
||
setSources(20);
|
||
time = 12;
|
||
component.onTimeUpdate();
|
||
component.togglePlay();
|
||
component.selectSource('screen');
|
||
fixture.detectChanges();
|
||
play.calls.reset();
|
||
video.dispatchEvent(new Event('loadedmetadata'));
|
||
expect(component.currentTime()).toBe(12);
|
||
expect(component.gapMessage()).toContain('未录制');
|
||
expect(paused).toBeTrue();
|
||
expect(play).not.toHaveBeenCalled();
|
||
component.togglePlay();
|
||
finishSeek();
|
||
expect(component.currentTime()).toBe(20);
|
||
expect(component.gapMessage()).toBe('');
|
||
expect(paused).toBeFalse();
|
||
});
|
||
|
||
it('retains a paused source switch and resumes with one play click', () => {
|
||
setSources();
|
||
time = 6;
|
||
component.onTimeUpdate();
|
||
component.selectSource('screen');
|
||
fixture.detectChanges();
|
||
video.dispatchEvent(new Event('loadedmetadata'));
|
||
finishSeek();
|
||
expect(component.currentTime()).toBe(6);
|
||
expect(paused).toBeTrue();
|
||
expect(play).not.toHaveBeenCalled();
|
||
component.togglePlay();
|
||
expect(play).toHaveBeenCalledTimes(1);
|
||
expect(paused).toBeFalse();
|
||
});
|
||
|
||
it('does not play another source when the requested source has no recording', () => {
|
||
setSources();
|
||
agentService.videoSeekRequest.set({ seconds: 5, requestId: 4, sourceId: 'missing-window' });
|
||
fixture.detectChanges();
|
||
component.togglePlay();
|
||
expect(component.gapMessage()).toContain('来源没有录像');
|
||
expect(component.currentTime()).toBe(5);
|
||
expect(paused).toBeTrue();
|
||
expect(play).not.toHaveBeenCalled();
|
||
});
|
||
|
||
it('resets a rejected play request so a single subsequent click can resume', async () => {
|
||
play.and.callFake(() => {
|
||
paused = false;
|
||
video.dispatchEvent(new Event('play'));
|
||
return Promise.reject(new DOMException('Interrupted', 'AbortError'));
|
||
});
|
||
component.seek(5, true);
|
||
finishSeek();
|
||
await Promise.resolve();
|
||
await Promise.resolve();
|
||
fixture.detectChanges();
|
||
expect(paused).toBeTrue();
|
||
expect(playButton().title).toBe('播放');
|
||
play.and.callFake(() => {
|
||
paused = false;
|
||
video.dispatchEvent(new Event('play'));
|
||
return Promise.resolve();
|
||
});
|
||
playButton().click();
|
||
await Promise.resolve();
|
||
fixture.detectChanges();
|
||
expect(paused).toBeFalse();
|
||
expect(playButton().title).toBe('暂停');
|
||
});
|
||
|
||
it('ignores an older aborted play request after a newer seek resumes', async () => {
|
||
let rejectOldPlay!: (reason: unknown) => void;
|
||
play.and.returnValue(new Promise<void>((_, reject) => { rejectOldPlay = reject; }));
|
||
component.togglePlay();
|
||
paused = false;
|
||
component.seek(5);
|
||
play.and.callFake(() => { paused = false; return Promise.resolve(); });
|
||
finishSeek();
|
||
rejectOldPlay(new DOMException('Interrupted', 'AbortError'));
|
||
await Promise.resolve();
|
||
await Promise.resolve();
|
||
expect(paused).toBeFalse();
|
||
expect(component.isPlaying()).toBeTrue();
|
||
});
|
||
});
|