feat(channel): page details chaine multi-providers avec onglets et infinite scroll
CI / build-and-test (push) Successful in 13m34s
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
This commit is contained in:
@@ -0,0 +1,172 @@
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user