mirror of
https://github.com/hectorqin/reader.git
synced 2026-11-11 21:41:02 +08:00
114 lines
10 KiB
TypeScript
114 lines
10 KiB
TypeScript
// @vitest-environment jsdom
|
|
import { afterEach, expect, it, vi } from 'vitest';
|
|
import { MediaPlayer } from '../src/features/media/services/player.ts';
|
|
import type { MediaApi, Playback } from '../src/features/media/api/media-api.ts';
|
|
|
|
afterEach(()=>{delete window.ReaderMedia;vi.restoreAllMocks();});
|
|
it('uses discovered clipped duration when scan duration is absent, and bounds seeks without retaining the previous item duration',async()=>{
|
|
const messages:Array<Record<string,unknown>>=[];
|
|
const port={postMessage:(text:string)=>messages.push(JSON.parse(text)),onmessage:null as ((event:{data:string})=>void)|null};window.ReaderMedia=port;
|
|
vi.spyOn(HTMLMediaElement.prototype,'pause').mockImplementation(()=>{});vi.spyOn(HTMLMediaElement.prototype,'load').mockImplementation(()=>{});
|
|
const player=new MediaPlayer({nativeCredentials:()=>({userId:'user',baseUrl:'https://server',accessToken:'token'})} as unknown as MediaApi);
|
|
player.reconnectNative();
|
|
const state={sessionId:'first',queueId:'queue',userId:'user',baseUrl:'https://server',partId:'a',start:30,end:null,position:45,paused:true,error:''};
|
|
port.onmessage!({data:JSON.stringify(state)});
|
|
const seek=player.element.querySelector<HTMLInputElement>('[aria-label="当前章节播放位置"]')!;
|
|
expect(seek.disabled).toBe(true);
|
|
port.onmessage!({data:JSON.stringify({...state,duration:90})});
|
|
expect(player.element.querySelector('.media-mini-time')!.textContent).toBe('00:15 / 01:30');
|
|
expect(seek.disabled).toBe(false);expect(seek.max).toBe('90');
|
|
seek.value='60';seek.dispatchEvent(new Event('change'));expect(messages.at(-1)).toEqual({action:'seek',position:90});
|
|
player.seek(200);expect(messages.at(-1)).toEqual({action:'seek',position:120});
|
|
port.onmessage!({data:JSON.stringify({...state,sessionId:'second',partId:'b',start:0,position:0,duration:-9223372036854776})});
|
|
expect(seek.disabled).toBe(true);expect(player.element.querySelector('.media-mini-time')!.textContent).toBe('00:00 / 时长未知');
|
|
await player.stop();
|
|
});
|
|
it.each([false,true])('rebinds to the matching account without creating a session or changing reading visibility (video=%s)',async(video)=>{
|
|
const messages:Array<Record<string,unknown>>=[];
|
|
const port={postMessage:(text:string)=>messages.push(JSON.parse(text)),onmessage:null as ((event:{data:string})=>void)|null};
|
|
window.ReaderMedia=port;
|
|
vi.spyOn(HTMLMediaElement.prototype,'pause').mockImplementation(()=>{});
|
|
vi.spyOn(HTMLMediaElement.prototype,'load').mockImplementation(()=>{});
|
|
const playback=vi.fn(),request=vi.fn();
|
|
const api={nativeCredentials:()=>({userId:'user',baseUrl:'https://server',accessToken:'new-token'}),playback,request} as unknown as MediaApi;
|
|
const player=new MediaPlayer(api);player.setVisible(false);player.reconnectNative();
|
|
port.onmessage!({data:JSON.stringify({video,sessionId:'existing',userId:'user',baseUrl:'https://server',queueId:'queue',partId:'part',title:'正在听的书',start:30,end:120,position:75,paused:false,playing:true,ended:false,error:''})});
|
|
expect(player.canOpenNativeVideo).toBe(video);
|
|
expect(player.element.classList.contains('media-player-native-video')).toBe(video);
|
|
expect(player.element.querySelector<HTMLElement>('.media-audio-controls')!.hidden).toBe(false);
|
|
expect(player.element.querySelector('[aria-label="播放位置"]')).toBeNull();
|
|
expect(player.active).toBe(true);expect(player.title).toBe('正在听的书');expect(player.paused).toBe(false);
|
|
const chapterSeek=player.element.querySelector<HTMLInputElement>('[aria-label="当前章节播放位置"]')!;
|
|
expect(chapterSeek.max).toBe('90');expect(chapterSeek.value).toBe('45');
|
|
chapterSeek.value='60';chapterSeek.dispatchEvent(new Event('change'));
|
|
expect(messages.at(-1)).toEqual({action:'seek',position:90});
|
|
port.onmessage!({data:JSON.stringify({sessionId:'existing',queueId:'queue',queueIndex:0,currentQueue:[{title:'第一章'},{title:'第二章'},{title:'第三章'}],position:75,paused:false,error:''})});
|
|
expect(player.currentPlaylist).toHaveLength(3);await player.selectPlaylist(2);
|
|
expect(messages.at(-1)).toEqual({action:'selectQueue',sessionId:'existing',index:2});expect(playback).not.toHaveBeenCalled();
|
|
port.onmessage!({data:JSON.stringify({sessionId:'existing',queueId:'queue',queueIndex:0,queueRevision:7,position:75,paused:false,error:''})});
|
|
player.editPlaylist(2,0);
|
|
expect(messages.at(-1)).toEqual({action:'editQueue',sessionId:'existing',revision:7,index:2,direction:0});
|
|
expect(player.currentPlaylist).toHaveLength(3);
|
|
port.onmessage!({data:JSON.stringify({sessionId:'existing',queueId:'queue',queueIndex:0,position:76,paused:false,error:''})});
|
|
expect(player.currentPlaylist).toHaveLength(3);
|
|
port.onmessage!({data:JSON.stringify({sessionId:'existing',queueId:'queue',queueIndex:0,currentQueue:[null],position:77,paused:false,error:''})});
|
|
expect(player.currentPlaylist).toHaveLength(3);
|
|
const audioTracks=[{id:'0:0',groupId:'g',label:'国语',supported:true,selected:true},{id:'0:1',groupId:'g',label:'英语',supported:true,selected:false},{id:'1:0',groupId:'other',label:'其他',supported:false,selected:false}];
|
|
port.onmessage!({data:JSON.stringify({sessionId:'existing',queueId:'queue',position:75,paused:false,audioTracks,tracksVersion:4,error:''})});
|
|
const trackSelect=player.element.querySelector<HTMLSelectElement>('select[aria-label="原生音轨"]')!;
|
|
expect(trackSelect.options).toHaveLength(3);expect(trackSelect.options[2]!.disabled).toBe(true);
|
|
trackSelect.value='0:1';trackSelect.dispatchEvent(new Event('change'));
|
|
expect(messages.at(-1)).toEqual({action:'audioTrack',sessionId:'existing',id:'0:1',groupId:'g',tracksVersion:4});
|
|
const messageCount=messages.length;trackSelect.value='1:0';trackSelect.dispatchEvent(new Event('change'));expect(messages).toHaveLength(messageCount);
|
|
expect(playback).not.toHaveBeenCalled();expect(player.element.hidden).toBe(true);
|
|
port.onmessage!({data:JSON.stringify({sessionId:'existing',queueId:'queue',position:75,paused:false,canPrevious:true,canNext:true,error:''})});
|
|
expect(player.canPrevious).toBe(true);expect(player.canNext).toBe(true);
|
|
await player.move(-1);expect(messages.at(-1)).toEqual({action:'previous'});
|
|
await player.move(1);expect(messages.at(-1)).toEqual({action:'next'});
|
|
port.onmessage!({data:JSON.stringify({sessionId:'existing',queueId:'queue',position:75,paused:true,canPrevious:false,canNext:false,queueSwitching:true,error:''})});
|
|
expect(player.canNext).toBe(false);expect(player.hasQueueNavigation).toBe(true);
|
|
expect(player.element.hidden).toBe(true);expect(playback).not.toHaveBeenCalled();await player.flush();expect(request).not.toHaveBeenCalled();
|
|
await player.stop();
|
|
player.reconnectNative();await player.stop();
|
|
port.onmessage!({data:JSON.stringify({sessionId:'existing',userId:'user',baseUrl:'https://server',queueId:'queue',partId:'part',start:30,end:120,position:75,paused:false})});
|
|
expect(player.active).toBe(false);
|
|
});
|
|
it('rejects a native session belonging to another account',async()=>{
|
|
const messages:Array<Record<string,unknown>>=[];
|
|
const port={postMessage:(text:string)=>messages.push(JSON.parse(text)),onmessage:null as ((event:{data:string})=>void)|null};
|
|
window.ReaderMedia=port;
|
|
const player=new MediaPlayer({nativeCredentials:()=>({userId:'new-user',baseUrl:'https://server',accessToken:'token'})} as unknown as MediaApi);
|
|
player.reconnectNative();port.onmessage!({data:JSON.stringify({sessionId:'old',userId:'old-user',baseUrl:'https://server',position:20,paused:false})});
|
|
expect(player.active).toBe(false);expect(messages.at(-1)).toEqual({action:'stop'});
|
|
});
|
|
it.each([false,true])('delegates media and persistence to Android (video=%s), retaining controls and stopping on logout',async(video)=>{
|
|
const messages:Array<Record<string,unknown>>=[];
|
|
const port={postMessage:(text:string)=>messages.push(JSON.parse(text)),onmessage:null as ((event:{data:string})=>void)|null};
|
|
window.ReaderMedia=port;
|
|
const play=vi.spyOn(HTMLMediaElement.prototype,'play').mockResolvedValue();
|
|
vi.spyOn(HTMLMediaElement.prototype,'pause').mockImplementation(()=>{});
|
|
vi.spyOn(HTMLMediaElement.prototype,'load').mockImplementation(()=>{});
|
|
const plan:Playback={id:'native-session',partId:'part',streamUrl:'/api/v1/media/streams/native-session?ticket=fixture',contentType:'audio/mpeg',expiresAt:Date.now()+21600000,start:0,end:90,position:10,revision:2};
|
|
const request=vi.fn();
|
|
const api={preferenceScope:()=>'native-test',playback:async()=>plan,streamUrl:()=> 'http://localhost'+plan.streamUrl,nativePlan:()=>({...plan,baseUrl:'http://localhost',accessToken:'fixture'}),request} as unknown as MediaApi;
|
|
const player=new MediaPlayer(api);
|
|
await player.play([{part:{id:'part',assetId:'asset',title:'Track',start:0,end:90,available:true},title:'Track',video}],0,video);
|
|
expect(play).not.toHaveBeenCalled();
|
|
expect(messages[0]).toMatchObject({action:'play',sessionId:'native-session',position:10});
|
|
expect(messages[0]!.plan).toMatchObject({video});
|
|
expect(player.video.hidden).toBe(true);
|
|
expect(player.canOpenNativeVideo).toBe(video);
|
|
if(video){await player.openNativeVideo();expect(messages.at(-1)).toEqual({action:'showVideo'});}
|
|
const nativePlan=messages[0]!.plan as {queueId:string;queue:Array<{partId:string}>};
|
|
expect(nativePlan.queue).toEqual([{partId:'part',title:'Track'}]);
|
|
port.onmessage!({data:JSON.stringify({sessionId:'native-session',position:23,paused:false,playing:true,ended:false,error:''})});
|
|
await player.flush();expect(request).not.toHaveBeenCalled();expect(player.paused).toBe(false);
|
|
port.onmessage!({data:JSON.stringify({sessionId:'next-session',queueId:nativePlan.queueId,queueIndex:1,title:'Next track',partId:'next-part',start:0,end:120,position:2,paused:false,playing:true,ended:false,error:''})});
|
|
expect(player.title).toBe('Next track');
|
|
port.onmessage!({data:JSON.stringify({sessionId:'next-session',queueId:nativePlan.queueId,position:120,paused:false,playing:false,ended:true,error:''})});
|
|
expect(messages.filter(message=>message.action==='play')).toHaveLength(1);
|
|
player.speed(1.5);player.sleep(30);player.pause();
|
|
expect(messages).toContainEqual({action:'speed',value:1.5});expect(messages).toContainEqual({action:'sleep',minutes:30});
|
|
await player.stop();expect(messages.at(-1)).toEqual({action:'stop'});
|
|
});
|