Files
NewTube/src/services/subscriptions.service.spec.ts
T
bruno 97e5de74f5
CI / build-and-test (push) Successful in 13m34s
feat(channel): page details chaine multi-providers avec onglets et infinite scroll
- Strategy Pattern front: ChannelProviderInterface + 6 providers (yt/dm/tw/pt/od/ru) + factory (ids longs/courts)
- ChannelContentService: cache par onglet/tri/recherche (TTL 5min), pagination nextPage
- ChannelPage: header (banniere/avatar/subs/description ...plus), tabs dynamiques par capabilities, grilles videos/shorts 9:16/playlists/live, recherche debounced, tri recent/populaire, infinite scroll IntersectionObserver, skeletons/empty/error
- Backend: GET /api/channels/:provider/:id/content (videos/shorts/playlists/live) natif par provider
2026-09-25 10:15:12 -04:00

173 lines
5.5 KiB
TypeScript

import 'zone.js/node';
import { TestBed } from '@angular/core/testing';
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { DOCUMENT } from '@angular/common';
import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing';
import { SubscriptionsService, type SubscriptionItem } from './subscriptions.service';
import type { ChannelMeta, ProviderId } from '../app/shared/models/channel.model';
const mockDocument = {
cookie: '',
createElement: () => ({
setAttribute() {},
}),
querySelector: () => null,
body: {
appendChild() {},
removeChild() {},
},
} as unknown as Document;
TestBed.resetTestEnvironment();
TestBed.initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting(), {
teardown: { destroyAfterEach: true, rethrowErrors: true },
});
function setup() {
TestBed.resetTestingModule();
TestBed.configureTestingModule({
imports: [HttpClientTestingModule],
providers: [
SubscriptionsService,
{ provide: DOCUMENT, useValue: mockDocument },
],
});
const service = TestBed.inject(SubscriptionsService);
const httpMock = TestBed.inject(HttpTestingController);
return { service, httpMock };
}
function assertEqual(actual: unknown, expected: unknown, message: string): void {
const a = JSON.stringify(actual);
const b = JSON.stringify(expected);
if (a !== b) {
throw new Error(`${message} (expected ${b}, got ${a})`);
}
}
function assert(condition: unknown, message: string): void {
if (!condition) {
throw new Error(message);
}
}
function makeChannel(provider: ProviderId, externalId: string, overrides: Partial<ChannelMeta> = {}): ChannelMeta {
return {
provider,
externalId,
id: overrides.id ?? 1,
title: overrides.title ?? 'Channel Title',
handle: overrides.handle ?? '@channel',
avatarUrl: overrides.avatarUrl ?? 'https://example.com/avatar.jpg',
url: overrides.url ?? 'https://example.com',
subsCount: overrides.subsCount ?? 1234,
verified: overrides.verified ?? true,
lastRefreshedAt: overrides.lastRefreshedAt ?? Date.now(),
};
}
function testListCachesResults(): void {
const { service, httpMock } = setup();
const channel = makeChannel('yt', 'abc');
const subscription: SubscriptionItem = {
subscriptionId: 101,
createdAt: Date.now(),
channel,
};
let received: SubscriptionItem[] | undefined;
service.list().subscribe(items => (received = items));
const req = httpMock.expectOne('/api/subscriptions');
assert(req.request.method === 'GET', 'list() should perform GET request');
req.flush({ items: [subscription], ttl: 600000 });
assertEqual(received, [subscription], 'list() should emit fetched subscriptions');
assertEqual(service.items(), [subscription], 'items signal should reflect fetched subscriptions');
httpMock.verify();
}
function testSubscribeAppendsToCache(): void {
const { service, httpMock } = setup();
const baseChannel = makeChannel('yt', 'abc');
const base: SubscriptionItem = {
subscriptionId: 1,
createdAt: Date.now() - 1000,
channel: baseChannel,
};
service.list().subscribe();
httpMock.expectOne('/api/subscriptions').flush({ items: [base] });
const newChannel = makeChannel('dm', 'xyz');
const newSubscription: SubscriptionItem = {
subscriptionId: 2,
createdAt: Date.now(),
channel: newChannel,
};
let subscribed: SubscriptionItem | null = null;
service.subscribe('dm', 'xyz').subscribe(result => (subscribed = result));
const req = httpMock.expectOne('/api/subscriptions');
assert(req.request.method === 'POST', 'subscribe() should perform POST request');
assertEqual(req.request.body, { provider: 'dm', externalId: 'xyz' }, 'subscribe() should send provider/externalId payload');
req.flush(newSubscription);
assertEqual(subscribed, newSubscription, 'subscribe() should emit created subscription');
assertEqual(
service.items().map(item => item.subscriptionId),
[newSubscription.subscriptionId, base.subscriptionId],
'subscribe() should prepend new subscription into cache'
);
httpMock.verify();
}
function testUnsubscribeRemovesFromCache(): void {
const { service, httpMock } = setup();
const channel = makeChannel('pt', 'foo');
const item: SubscriptionItem = {
subscriptionId: 44,
createdAt: Date.now(),
channel,
};
service.list().subscribe();
httpMock.expectOne('/api/subscriptions').flush({ items: [item] });
let result: boolean | null = null;
service.unsubscribe(item.subscriptionId).subscribe(ok => (result = ok));
const req = httpMock.expectOne(`/api/subscriptions/${item.subscriptionId}`);
assert(req.request.method === 'DELETE', 'unsubscribe() should perform DELETE request');
req.flush({}, { status: 204, statusText: 'No Content' });
assert(result === true, 'unsubscribe() should emit true when deletion succeeds');
assert(service.items().length === 0, 'unsubscribe() should remove subscription from cache');
httpMock.verify();
}
const tests: Array<[string, () => void]> = [
['list() caches fetched subscriptions', testListCachesResults],
['subscribe() appends to cache', testSubscribeAppendsToCache],
['unsubscribe() removes from cache', testUnsubscribeRemovesFromCache],
];
let failures = 0;
for (const [name, fn] of tests) {
try {
fn();
console.log(`✓ ${name}`);
} catch (error) {
failures += 1;
console.error(`✗ ${name}`);
console.error(error);
}
}
if (failures > 0) {
process.exitCode = 1;
}