mirror of
https://github.com/alexta69/metube.git
synced 2026-06-20 08:16:12 +00:00
280 lines
8.7 KiB
TypeScript
280 lines
8.7 KiB
TypeScript
import { TestBed } from '@angular/core/testing';
|
|
import { HttpClient } from '@angular/common/http';
|
|
import { Subject, of } from 'rxjs';
|
|
import { App } from './app';
|
|
import { DownloadsService } from './services/downloads.service';
|
|
import { SubscriptionsService } from './services/subscriptions.service';
|
|
import { ToastService } from './services/toast.service';
|
|
import { CookieService } from 'ngx-cookie-service';
|
|
|
|
class DownloadsServiceStub {
|
|
loading = false;
|
|
queue = new Map();
|
|
done = new Map();
|
|
configuration: Record<string, unknown> = { CUSTOM_DIRS: true, CREATE_CUSTOM_DIRS: true, ALLOW_YTDL_OPTIONS_OVERRIDES: false };
|
|
customDirs = { download_dir: [], audio_download_dir: [] };
|
|
queueChanged = new Subject<void>();
|
|
doneChanged = new Subject<void>();
|
|
configurationChanged = new Subject<Record<string, unknown>>();
|
|
customDirsChanged = new Subject<Record<string, string[]>>();
|
|
ytdlOptionsChanged = new Subject<Record<string, unknown>>();
|
|
updated = new Subject<void>();
|
|
|
|
getCookieStatus() {
|
|
return of({ status: 'ok', has_cookies: false });
|
|
}
|
|
|
|
getPresets() {
|
|
return of({ presets: ['Preset A'] });
|
|
}
|
|
|
|
add() {
|
|
return of({ status: 'ok' as const });
|
|
}
|
|
|
|
cancelAdd() {
|
|
return of({ status: 'ok' as const });
|
|
}
|
|
|
|
startById() {
|
|
return of({});
|
|
}
|
|
|
|
delById() {
|
|
return of({});
|
|
}
|
|
|
|
delByFilter() {
|
|
return of({});
|
|
}
|
|
|
|
startByFilter() {
|
|
return of({});
|
|
}
|
|
|
|
uploadCookies() {
|
|
return of({ status: 'ok' });
|
|
}
|
|
|
|
deleteCookies() {
|
|
return of({ status: 'ok' });
|
|
}
|
|
}
|
|
|
|
class SubscriptionsServiceStub {
|
|
subscriptions = new Map();
|
|
subscriptionsChanged = new Subject<void>();
|
|
subscribeCalls: unknown[] = [];
|
|
|
|
subscribe(payload: unknown) {
|
|
this.subscribeCalls.push(payload);
|
|
return of({ status: 'ok' as const });
|
|
}
|
|
|
|
delete() {
|
|
return of({});
|
|
}
|
|
|
|
update() {
|
|
return of({ status: 'ok' as const });
|
|
}
|
|
|
|
refreshList() {
|
|
return of([]);
|
|
}
|
|
}
|
|
|
|
class CookieServiceStub {
|
|
private cookies = new Map<string, string>();
|
|
|
|
get(name: string) {
|
|
return this.cookies.get(name) ?? '';
|
|
}
|
|
|
|
set(name: string, value: string) {
|
|
this.cookies.set(name, value);
|
|
}
|
|
|
|
check(name: string) {
|
|
return this.cookies.has(name);
|
|
}
|
|
}
|
|
|
|
describe('App', () => {
|
|
let downloads: DownloadsServiceStub;
|
|
|
|
beforeEach(async () => {
|
|
Object.defineProperty(window, 'matchMedia', {
|
|
writable: true,
|
|
enumerable: true,
|
|
value: vi.fn().mockImplementation((query: string) => ({
|
|
matches: false,
|
|
media: query,
|
|
onchange: null,
|
|
addEventListener: vi.fn(),
|
|
removeEventListener: vi.fn(),
|
|
dispatchEvent: vi.fn(),
|
|
})),
|
|
});
|
|
downloads = new DownloadsServiceStub();
|
|
await TestBed.configureTestingModule({
|
|
imports: [App],
|
|
providers: [
|
|
{ provide: DownloadsService, useValue: downloads },
|
|
{ provide: SubscriptionsService, useClass: SubscriptionsServiceStub },
|
|
{ provide: CookieService, useClass: CookieServiceStub },
|
|
{
|
|
provide: HttpClient,
|
|
useValue: {
|
|
get: vi.fn().mockReturnValue(of({ 'yt-dlp': 'test', version: 'test' })),
|
|
},
|
|
},
|
|
],
|
|
}).compileComponents();
|
|
});
|
|
|
|
it('should create the app', () => {
|
|
const fixture = TestBed.createComponent(App);
|
|
const app = fixture.componentInstance;
|
|
expect(app).toBeTruthy();
|
|
});
|
|
|
|
it('hides manual override input when disabled', () => {
|
|
const fixture = TestBed.createComponent(App);
|
|
fixture.componentInstance.isAdvancedOpen = true;
|
|
fixture.detectChanges();
|
|
|
|
const root = fixture.nativeElement as HTMLElement;
|
|
expect(root.querySelector('input[name="ytdlOptionsOverrides"]')).toBeNull();
|
|
|
|
const presetWrapper = root.querySelector('ng-select[name="ytdlOptionsPresets"]')?.closest('.col-12');
|
|
expect(presetWrapper?.classList.contains('col-md-6')).toBe(false);
|
|
|
|
const presetRow = root.querySelector('ng-select[name="ytdlOptionsPresets"]')?.closest('.row');
|
|
expect(presetRow?.querySelector('input[name="checkIntervalMinutes"]')).toBeNull();
|
|
});
|
|
|
|
it('shows manual override input when enabled', () => {
|
|
downloads.configuration['ALLOW_YTDL_OPTIONS_OVERRIDES'] = true;
|
|
|
|
const fixture = TestBed.createComponent(App);
|
|
fixture.componentInstance.isAdvancedOpen = true;
|
|
fixture.detectChanges();
|
|
|
|
const root = fixture.nativeElement as HTMLElement;
|
|
expect(root.querySelector('input[name="ytdlOptionsOverrides"]')).not.toBeNull();
|
|
|
|
const presetWrapper = root.querySelector('ng-select[name="ytdlOptionsPresets"]')?.closest('.col-12');
|
|
expect(presetWrapper?.classList.contains('col-md-6')).toBe(true);
|
|
|
|
const presetRow = root.querySelector('ng-select[name="ytdlOptionsPresets"]')?.closest('.row');
|
|
expect(presetRow?.querySelector('input[name="checkIntervalMinutes"]')).toBeNull();
|
|
expect(presetRow?.querySelector('input[name="ytdlOptionsOverrides"]')).not.toBeNull();
|
|
});
|
|
|
|
it('does not submit manual overrides when disabled', () => {
|
|
const fixture = TestBed.createComponent(App);
|
|
const app = fixture.componentInstance;
|
|
|
|
app.ytdlOptionsOverrides = '{"exec":"echo hi"}';
|
|
|
|
const payload = app['buildAddPayload']();
|
|
|
|
expect(payload.ytdlOptionsOverrides).toBe('');
|
|
});
|
|
|
|
it('shows waiting badge for scheduled live stream', () => {
|
|
downloads.queue.set('https://example.com/live', {
|
|
id: 'live1',
|
|
title: 'Upcoming Stream',
|
|
url: 'https://example.com/live',
|
|
download_type: 'video',
|
|
quality: 'best',
|
|
format: 'any',
|
|
folder: '',
|
|
custom_name_prefix: '',
|
|
playlist_item_limit: 0,
|
|
status: 'scheduled',
|
|
live_status: 'is_upcoming',
|
|
live_release_timestamp: Date.now() / 1000 + 3600,
|
|
msg: '',
|
|
percent: 0,
|
|
speed: 0,
|
|
eta: 0,
|
|
filename: '',
|
|
checked: false,
|
|
});
|
|
downloads.queueChanged.next();
|
|
|
|
const fixture = TestBed.createComponent(App);
|
|
fixture.detectChanges();
|
|
|
|
const root = fixture.nativeElement as HTMLElement;
|
|
expect(root.textContent).toContain('Waiting for stream');
|
|
expect(root.textContent).toContain('starts in');
|
|
});
|
|
|
|
it('includes titleRegex in subscribe payload', () => {
|
|
const fixture = TestBed.createComponent(App);
|
|
const app = fixture.componentInstance;
|
|
const subs = TestBed.inject(SubscriptionsService) as unknown as SubscriptionsServiceStub;
|
|
app.addUrl = 'https://example.com/channel';
|
|
app.titleRegex = 'EPISODE';
|
|
app.addSubscription();
|
|
expect(subs.subscribeCalls.length).toBe(1);
|
|
const payload = subs.subscribeCalls[0] as { titleRegex: string; skipSubscriberOnly: boolean };
|
|
expect(payload.titleRegex).toBe('EPISODE');
|
|
expect(payload.skipSubscriberOnly).toBe(false);
|
|
});
|
|
|
|
it('includes skipSubscriberOnly true when checked', () => {
|
|
const fixture = TestBed.createComponent(App);
|
|
const app = fixture.componentInstance;
|
|
const subs = TestBed.inject(SubscriptionsService) as unknown as SubscriptionsServiceStub;
|
|
app.addUrl = 'https://example.com/channel';
|
|
app.skipSubscriberOnly = true;
|
|
app.addSubscription();
|
|
expect(subs.subscribeCalls.length).toBe(1);
|
|
const payload = subs.subscribeCalls[0] as { skipSubscriberOnly: boolean };
|
|
expect(payload.skipSubscriberOnly).toBe(true);
|
|
});
|
|
|
|
it('omits clip fields from subscribe payload', () => {
|
|
const fixture = TestBed.createComponent(App);
|
|
const app = fixture.componentInstance;
|
|
const subs = TestBed.inject(SubscriptionsService) as unknown as SubscriptionsServiceStub;
|
|
app.addUrl = 'https://example.com/channel';
|
|
app.clipStart = '1:00';
|
|
app.clipEnd = '2:00';
|
|
app.addSubscription();
|
|
expect(subs.subscribeCalls.length).toBe(1);
|
|
const payload = subs.subscribeCalls[0] as Record<string, unknown>;
|
|
expect('clipStart' in payload).toBe(false);
|
|
expect('clipEnd' in payload).toBe(false);
|
|
});
|
|
|
|
it('buildAddPayload includes clip times', () => {
|
|
const fixture = TestBed.createComponent(App);
|
|
const app = fixture.componentInstance;
|
|
app.clipStart = '0:10';
|
|
app.clipEnd = '1:20';
|
|
const payload = app['buildAddPayload']();
|
|
expect(payload.clipStart).toBe('0:10');
|
|
expect(payload.clipEnd).toBe('1:20');
|
|
});
|
|
|
|
it('blocks subscribe with invalid title regex', () => {
|
|
const toasts = TestBed.inject(ToastService);
|
|
const errorSpy = vi.spyOn(toasts, 'error').mockImplementation(() => undefined);
|
|
const fixture = TestBed.createComponent(App);
|
|
const app = fixture.componentInstance;
|
|
const subs = TestBed.inject(SubscriptionsService) as unknown as SubscriptionsServiceStub;
|
|
app.addUrl = 'https://example.com/channel';
|
|
app.titleRegex = '[';
|
|
app.addSubscription();
|
|
expect(subs.subscribeCalls.length).toBe(0);
|
|
expect(errorSpy).toHaveBeenCalledWith('Invalid subscription title filter (regex)');
|
|
errorSpy.mockRestore();
|
|
});
|
|
});
|