CI / build-and-test (push) Successful in 13m34s
- 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
173 lines
5.5 KiB
TypeScript
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;
|
|
}
|