mirror of
https://github.com/alexta69/metube.git
synced 2026-06-13 16:40:05 +00:00
implement tests
This commit is contained in:
+12
-12
@@ -23,14 +23,14 @@
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@angular/animations": "^21.2.4",
|
||||
"@angular/common": "^21.2.4",
|
||||
"@angular/compiler": "^21.2.4",
|
||||
"@angular/core": "^21.2.4",
|
||||
"@angular/forms": "^21.2.4",
|
||||
"@angular/platform-browser": "^21.2.4",
|
||||
"@angular/platform-browser-dynamic": "^21.2.4",
|
||||
"@angular/service-worker": "^21.2.4",
|
||||
"@angular/animations": "^21.2.5",
|
||||
"@angular/common": "^21.2.5",
|
||||
"@angular/compiler": "^21.2.5",
|
||||
"@angular/core": "^21.2.5",
|
||||
"@angular/forms": "^21.2.5",
|
||||
"@angular/platform-browser": "^21.2.5",
|
||||
"@angular/platform-browser-dynamic": "^21.2.5",
|
||||
"@angular/service-worker": "^21.2.5",
|
||||
"@fortawesome/angular-fontawesome": "~4.0.0",
|
||||
"@fortawesome/fontawesome-svg-core": "^7.2.0",
|
||||
"@fortawesome/free-brands-svg-icons": "^7.2.0",
|
||||
@@ -48,10 +48,10 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-eslint/builder": "21.1.0",
|
||||
"@angular/build": "^21.2.2",
|
||||
"@angular/cli": "^21.2.2",
|
||||
"@angular/compiler-cli": "^21.2.4",
|
||||
"@angular/localize": "^21.2.4",
|
||||
"@angular/build": "^21.2.3",
|
||||
"@angular/cli": "^21.2.3",
|
||||
"@angular/compiler-cli": "^21.2.5",
|
||||
"@angular/localize": "^21.2.5",
|
||||
"@eslint/js": "^9.39.4",
|
||||
"angular-eslint": "21.1.0",
|
||||
"eslint": "^9.39.4",
|
||||
|
||||
Generated
+313
-337
File diff suppressed because it is too large
Load Diff
+12
-17
@@ -1,24 +1,20 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { App } from './app';
|
||||
|
||||
vi.hoisted(() => {
|
||||
Object.defineProperty(window, "matchMedia", {
|
||||
writable: true,
|
||||
enumerable: true,
|
||||
value: vi.fn().mockImplementation((query) => ({
|
||||
matches: false,
|
||||
media: query,
|
||||
onchange: null,
|
||||
addEventListener: vi.fn(),
|
||||
removeEventListener: vi.fn(),
|
||||
dispatchEvent: vi.fn(),
|
||||
})),
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('App', () => {
|
||||
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(),
|
||||
})),
|
||||
});
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [App],
|
||||
}).compileComponents();
|
||||
@@ -29,5 +25,4 @@ describe('App', () => {
|
||||
const app = fixture.componentInstance;
|
||||
expect(app).toBeTruthy();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { SelectAllCheckboxComponent } from './master-checkbox.component';
|
||||
import { Checkable } from '../interfaces';
|
||||
|
||||
describe('SelectAllCheckboxComponent', () => {
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [SelectAllCheckboxComponent],
|
||||
}).compileComponents();
|
||||
});
|
||||
|
||||
it('clicked sets checked on all list items', () => {
|
||||
const fixture = TestBed.createComponent(SelectAllCheckboxComponent);
|
||||
const list = new Map<string, Checkable>();
|
||||
list.set('u1', { checked: false });
|
||||
fixture.componentRef.setInput('id', 'queue');
|
||||
fixture.componentRef.setInput('list', list);
|
||||
fixture.componentInstance.selected = true;
|
||||
fixture.detectChanges();
|
||||
fixture.componentInstance.clicked();
|
||||
expect(list.get('u1')?.checked).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,25 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { SelectAllCheckboxComponent } from './master-checkbox.component';
|
||||
import { ItemCheckboxComponent } from './slave-checkbox.component';
|
||||
|
||||
describe('ItemCheckboxComponent', () => {
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [ItemCheckboxComponent, SelectAllCheckboxComponent],
|
||||
}).compileComponents();
|
||||
});
|
||||
|
||||
it('creates with master and checkable inputs', () => {
|
||||
const masterFixture = TestBed.createComponent(SelectAllCheckboxComponent);
|
||||
masterFixture.componentRef.setInput('id', 'q');
|
||||
masterFixture.componentRef.setInput('list', new Map());
|
||||
masterFixture.detectChanges();
|
||||
|
||||
const itemFixture = TestBed.createComponent(ItemCheckboxComponent);
|
||||
itemFixture.componentRef.setInput('id', 'row1');
|
||||
itemFixture.componentRef.setInput('master', masterFixture.componentInstance);
|
||||
itemFixture.componentRef.setInput('checkable', { checked: false });
|
||||
itemFixture.detectChanges();
|
||||
expect(itemFixture.componentInstance).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,26 @@
|
||||
import { EtaPipe } from './eta.pipe';
|
||||
|
||||
describe('EtaPipe', () => {
|
||||
it('returns null for null input', () => {
|
||||
const pipe = new EtaPipe();
|
||||
expect(pipe.transform(null as unknown as number)).toBeNull();
|
||||
});
|
||||
|
||||
it('formats seconds under one minute', () => {
|
||||
const pipe = new EtaPipe();
|
||||
expect(pipe.transform(0)).toBe('0s');
|
||||
expect(pipe.transform(59)).toBe('59s');
|
||||
});
|
||||
|
||||
it('formats minutes and seconds', () => {
|
||||
const pipe = new EtaPipe();
|
||||
expect(pipe.transform(60)).toBe('1m 0s');
|
||||
expect(pipe.transform(90)).toBe('1m 30s');
|
||||
});
|
||||
|
||||
it('formats hours', () => {
|
||||
const pipe = new EtaPipe();
|
||||
expect(pipe.transform(3600)).toBe('1h 0m 0s');
|
||||
expect(pipe.transform(3661)).toBe('1h 1m 1s');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,24 @@
|
||||
import { FileSizePipe } from './file-size.pipe';
|
||||
|
||||
describe('FileSizePipe', () => {
|
||||
it('returns 0 Bytes for zero or NaN', () => {
|
||||
const pipe = new FileSizePipe();
|
||||
expect(pipe.transform(0)).toBe('0 Bytes');
|
||||
expect(pipe.transform(Number.NaN)).toBe('0 Bytes');
|
||||
});
|
||||
|
||||
it('formats bytes and larger units', () => {
|
||||
const pipe = new FileSizePipe();
|
||||
expect(pipe.transform(500)).toContain('Bytes');
|
||||
expect(pipe.transform(1000)).toContain('KB');
|
||||
expect(pipe.transform(1000 * 1000)).toContain('MB');
|
||||
expect(pipe.transform(1000 ** 3)).toContain('GB');
|
||||
});
|
||||
|
||||
it('handles boundaries between units', () => {
|
||||
const pipe = new FileSizePipe();
|
||||
expect(pipe.transform(999)).toContain('Bytes');
|
||||
expect(pipe.transform(1000)).toContain('KB');
|
||||
expect(pipe.transform(1001)).toContain('KB');
|
||||
});
|
||||
});
|
||||
@@ -12,4 +12,10 @@ describe('SpeedPipe', () => {
|
||||
expect(pipe.transform(1024)).toBe('1 KB/s');
|
||||
expect(pipe.transform(1536)).toBe('1.5 KB/s');
|
||||
});
|
||||
|
||||
it('formats MB/s and GB/s', () => {
|
||||
const pipe = new SpeedPipe();
|
||||
expect(pipe.transform(1024 * 1024)).toBe('1 MB/s');
|
||||
expect(pipe.transform(1024 * 1024 * 1024)).toBe('1 GB/s');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,279 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { provideHttpClient, HttpErrorResponse } from '@angular/common/http';
|
||||
import { provideHttpClientTesting, HttpTestingController } from '@angular/common/http/testing';
|
||||
import { Subject } from 'rxjs';
|
||||
import { DownloadsService, AddDownloadPayload } from './downloads.service';
|
||||
import { MeTubeSocket } from './metube-socket.service';
|
||||
import { Download } from '../interfaces';
|
||||
|
||||
class MeTubeSocketStub {
|
||||
private subjects: Record<string, Subject<string>> = {};
|
||||
|
||||
fromEvent(event: string) {
|
||||
if (!this.subjects[event]) {
|
||||
this.subjects[event] = new Subject<string>();
|
||||
}
|
||||
return this.subjects[event].asObservable();
|
||||
}
|
||||
|
||||
emit(event: string, data: string) {
|
||||
if (!this.subjects[event]) {
|
||||
this.subjects[event] = new Subject<string>();
|
||||
}
|
||||
this.subjects[event].next(data);
|
||||
}
|
||||
}
|
||||
|
||||
function basePayload(): AddDownloadPayload {
|
||||
return {
|
||||
url: 'https://example.com/v',
|
||||
downloadType: 'video',
|
||||
codec: 'auto',
|
||||
quality: 'best',
|
||||
format: 'any',
|
||||
folder: '',
|
||||
customNamePrefix: '',
|
||||
playlistItemLimit: 0,
|
||||
autoStart: true,
|
||||
splitByChapters: false,
|
||||
chapterTemplate: '',
|
||||
subtitleLanguage: 'en',
|
||||
subtitleMode: 'prefer_manual',
|
||||
};
|
||||
}
|
||||
|
||||
describe('DownloadsService', () => {
|
||||
let socket: MeTubeSocketStub;
|
||||
let httpMock: HttpTestingController;
|
||||
let service: DownloadsService;
|
||||
|
||||
beforeEach(async () => {
|
||||
socket = new MeTubeSocketStub();
|
||||
await TestBed.configureTestingModule({
|
||||
providers: [
|
||||
DownloadsService,
|
||||
provideHttpClient(),
|
||||
provideHttpClientTesting(),
|
||||
{ provide: MeTubeSocket, useValue: socket },
|
||||
],
|
||||
}).compileComponents();
|
||||
|
||||
service = TestBed.inject(DownloadsService);
|
||||
httpMock = TestBed.inject(HttpTestingController);
|
||||
});
|
||||
|
||||
it('add() posts snake_case fields matching backend', () => {
|
||||
service.add(basePayload()).subscribe();
|
||||
const req = httpMock.expectOne('add');
|
||||
expect(req.request.method).toBe('POST');
|
||||
expect(req.request.body).toEqual(
|
||||
expect.objectContaining({
|
||||
url: 'https://example.com/v',
|
||||
download_type: 'video',
|
||||
codec: 'auto',
|
||||
quality: 'best',
|
||||
format: 'any',
|
||||
playlist_item_limit: 0,
|
||||
auto_start: true,
|
||||
split_by_chapters: false,
|
||||
chapter_template: '',
|
||||
subtitle_language: 'en',
|
||||
subtitle_mode: 'prefer_manual',
|
||||
}),
|
||||
);
|
||||
req.flush({ status: 'ok' });
|
||||
});
|
||||
|
||||
it('cancelAdd posts to cancel-add', () => {
|
||||
service.cancelAdd().subscribe();
|
||||
const req = httpMock.expectOne('cancel-add');
|
||||
expect(req.request.method).toBe('POST');
|
||||
req.flush({ status: 'ok' });
|
||||
});
|
||||
|
||||
it('startById posts ids', () => {
|
||||
service.startById(['a', 'b']).subscribe();
|
||||
const req = httpMock.expectOne('start');
|
||||
expect(req.request.body).toEqual({ ids: ['a', 'b'] });
|
||||
req.flush({});
|
||||
});
|
||||
|
||||
it('delById marks items deleting and posts delete', () => {
|
||||
const dl: Download = {
|
||||
id: '1',
|
||||
title: 't',
|
||||
url: 'u1',
|
||||
download_type: 'video',
|
||||
quality: 'best',
|
||||
format: 'any',
|
||||
folder: '',
|
||||
custom_name_prefix: '',
|
||||
playlist_item_limit: 0,
|
||||
status: 'finished',
|
||||
msg: '',
|
||||
percent: 0,
|
||||
speed: 0,
|
||||
eta: 0,
|
||||
filename: '',
|
||||
checked: false,
|
||||
deleting: false,
|
||||
};
|
||||
service.queue.set('u1', dl);
|
||||
service.delById('queue', ['u1']).subscribe();
|
||||
expect(dl.deleting).toBe(true);
|
||||
const req = httpMock.expectOne('delete');
|
||||
expect(req.request.body).toEqual({ where: 'queue', ids: ['u1'] });
|
||||
req.flush({});
|
||||
});
|
||||
|
||||
it('handleHTTPError extracts msg from object body', async () => {
|
||||
const err = new HttpErrorResponse({
|
||||
error: { msg: 'bad' },
|
||||
status: 400,
|
||||
});
|
||||
const res = await new Promise((resolve) => {
|
||||
service.handleHTTPError(err).subscribe(resolve);
|
||||
});
|
||||
expect((res as { status: string }).status).toBe('error');
|
||||
expect((res as { msg?: string }).msg).toBe('bad');
|
||||
});
|
||||
|
||||
it('socket all updates queue and done', () => {
|
||||
const row: Download = {
|
||||
id: '1',
|
||||
title: 't',
|
||||
url: 'u1',
|
||||
download_type: 'video',
|
||||
quality: 'best',
|
||||
format: 'any',
|
||||
folder: '',
|
||||
custom_name_prefix: '',
|
||||
playlist_item_limit: 0,
|
||||
status: 'pending',
|
||||
msg: '',
|
||||
percent: 0,
|
||||
speed: 0,
|
||||
eta: 0,
|
||||
filename: '',
|
||||
checked: false,
|
||||
};
|
||||
const q: [string, Download][] = [['u1', row]];
|
||||
const d: [string, Download][] = [];
|
||||
socket.emit('all', JSON.stringify([q, d]));
|
||||
expect(service.loading).toBe(false);
|
||||
expect(service.queue.has('u1')).toBe(true);
|
||||
});
|
||||
|
||||
it('socket updated preserves checked and deleting', () => {
|
||||
service.queue.set('u1', {
|
||||
id: '1',
|
||||
title: 't',
|
||||
url: 'u1',
|
||||
download_type: 'video',
|
||||
quality: 'best',
|
||||
format: 'any',
|
||||
folder: '',
|
||||
custom_name_prefix: '',
|
||||
playlist_item_limit: 0,
|
||||
status: 'pending',
|
||||
msg: '',
|
||||
percent: 0,
|
||||
speed: 0,
|
||||
eta: 0,
|
||||
filename: '',
|
||||
checked: true,
|
||||
deleting: true,
|
||||
});
|
||||
socket.emit(
|
||||
'updated',
|
||||
JSON.stringify({ url: 'u1', title: 't', status: 'downloading' }),
|
||||
);
|
||||
const updated = service.queue.get('u1');
|
||||
expect(updated?.checked).toBe(true);
|
||||
expect(updated?.deleting).toBe(true);
|
||||
});
|
||||
|
||||
it('socket completed moves entry to done', () => {
|
||||
service.queue.set('u1', {
|
||||
id: '1',
|
||||
title: 't',
|
||||
url: 'u1',
|
||||
download_type: 'video',
|
||||
quality: 'best',
|
||||
format: 'any',
|
||||
folder: '',
|
||||
custom_name_prefix: '',
|
||||
playlist_item_limit: 0,
|
||||
status: 'pending',
|
||||
msg: '',
|
||||
percent: 0,
|
||||
speed: 0,
|
||||
eta: 0,
|
||||
filename: '',
|
||||
checked: false,
|
||||
});
|
||||
socket.emit('completed', JSON.stringify({ url: 'u1', title: 't', status: 'finished' }));
|
||||
expect(service.queue.has('u1')).toBe(false);
|
||||
expect(service.done.has('u1')).toBe(true);
|
||||
});
|
||||
|
||||
it('socket canceled removes from queue', () => {
|
||||
service.queue.set('u1', {
|
||||
id: '1',
|
||||
title: 't',
|
||||
url: 'u1',
|
||||
download_type: 'video',
|
||||
quality: 'best',
|
||||
format: 'any',
|
||||
folder: '',
|
||||
custom_name_prefix: '',
|
||||
playlist_item_limit: 0,
|
||||
status: 'pending',
|
||||
msg: '',
|
||||
percent: 0,
|
||||
speed: 0,
|
||||
eta: 0,
|
||||
filename: '',
|
||||
checked: false,
|
||||
});
|
||||
socket.emit('canceled', JSON.stringify('u1'));
|
||||
expect(service.queue.has('u1')).toBe(false);
|
||||
});
|
||||
|
||||
it('socket cleared removes from done', () => {
|
||||
service.done.set('u1', {
|
||||
id: '1',
|
||||
title: 't',
|
||||
url: 'u1',
|
||||
download_type: 'video',
|
||||
quality: 'best',
|
||||
format: 'any',
|
||||
folder: '',
|
||||
custom_name_prefix: '',
|
||||
playlist_item_limit: 0,
|
||||
status: 'finished',
|
||||
msg: '',
|
||||
percent: 0,
|
||||
speed: 0,
|
||||
eta: 0,
|
||||
filename: '',
|
||||
checked: false,
|
||||
});
|
||||
socket.emit('cleared', JSON.stringify('u1'));
|
||||
expect(service.done.has('u1')).toBe(false);
|
||||
});
|
||||
|
||||
it('socket configuration updates configuration', () => {
|
||||
socket.emit('configuration', JSON.stringify({ CUSTOM_DIRS: true }));
|
||||
expect(service.configuration['CUSTOM_DIRS']).toBe(true);
|
||||
});
|
||||
|
||||
it('socket custom_dirs updates customDirs', () => {
|
||||
socket.emit('custom_dirs', JSON.stringify({ download_dir: [''] }));
|
||||
expect(service.customDirs['download_dir']).toEqual(['']);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
httpMock.verify();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user