diff --git a/libs/portal/catalog/feature/jest.config.ts b/libs/portal/catalog/feature/jest.config.ts new file mode 100644 index 000000000..7f2920c30 --- /dev/null +++ b/libs/portal/catalog/feature/jest.config.ts @@ -0,0 +1,21 @@ +export default { + displayName: 'portal-catalog-feature', + preset: '../../../../jest.preset.js', + setupFilesAfterEnv: ['/src/test-setup.ts'], + coverageDirectory: '../../../../coverage/libs/portal/catalog/feature', + transform: { + '^.+\\.(ts|mjs|js|html)$': [ + 'jest-preset-angular', + { + tsconfig: '/tsconfig.spec.json', + stringifyContentPathRegex: '\\.(html|svg)$', + }, + ], + }, + transformIgnorePatterns: ['node_modules/(?!.*\\.mjs$)'], + snapshotSerializers: [ + 'jest-preset-angular/build/serializers/no-ng-attributes', + 'jest-preset-angular/build/serializers/ng-snapshot', + 'jest-preset-angular/build/serializers/html-comment', + ], +}; diff --git a/libs/portal/catalog/feature/project.json b/libs/portal/catalog/feature/project.json index b5eec6cb8..58afa7f34 100644 --- a/libs/portal/catalog/feature/project.json +++ b/libs/portal/catalog/feature/project.json @@ -6,6 +6,14 @@ "projectType": "library", "tags": ["scope:portal", "domain:portal-shared", "type:feature"], "targets": { + "test": { + "executor": "@nx/jest:jest", + "outputs": ["{workspaceRoot}/coverage/{projectRoot}"], + "options": { + "jestConfig": "libs/portal/catalog/feature/jest.config.ts", + "tsConfig": "libs/portal/catalog/feature/tsconfig.spec.json" + } + }, "lint": { "executor": "@nx/eslint:lint" } diff --git a/libs/portal/catalog/feature/src/lib/category-content-view/category-content-view.component.spec.ts b/libs/portal/catalog/feature/src/lib/category-content-view/category-content-view.component.spec.ts new file mode 100644 index 000000000..3efd82a4a --- /dev/null +++ b/libs/portal/catalog/feature/src/lib/category-content-view/category-content-view.component.spec.ts @@ -0,0 +1,175 @@ +import { Component, input, output, signal } from '@angular/core'; +import { NgComponentOutlet } from '@angular/common'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { MatIconButton } from '@angular/material/button'; +import { MatIcon } from '@angular/material/icon'; +import { MatMenuModule } from '@angular/material/menu'; +import { MatPaginatorModule } from '@angular/material/paginator'; +import { MatTooltip } from '@angular/material/tooltip'; +import { ActivatedRoute, Router } from '@angular/router'; +import { TranslatePipe, TranslateService } from '@ngx-translate/core'; +import { of } from 'rxjs'; +import { + PORTAL_CATALOG_DETAIL_COMPONENT, + PORTAL_CATALOG_FACADE, + PortalCatalogSortMode, +} from '@iptvnator/portal/shared/util'; +import { CategoryContentViewComponent } from './category-content-view.component'; + +@Component({ + selector: 'app-grid-list', + standalone: true, + template: '', +}) +class MockGridListComponent { + readonly isLoading = input(); + readonly items = input(); + readonly pageIndex = input(); + readonly totalPages = input(); + readonly limit = input(); + readonly pageSizeOptions = input(); + readonly showPaginator = input(true); + readonly itemClicked = output(); + readonly pageChange = output(); +} + +@Component({ + selector: 'app-playlist-error-view', + standalone: true, + template: '', +}) +class MockPlaylistErrorViewComponent { + readonly title = input(''); + readonly description = input(''); + readonly showActionButtons = input(true); + readonly viewType = input(''); +} + +@Component({ + standalone: true, + template: '', +}) +class MockDetailComponent {} + +describe('CategoryContentViewComponent', () => { + let fixture: ComponentFixture; + const isPaginatedContentLoading = signal(true); + const categoryItemCount = signal(0); + const contentSortMode = signal(null); + const catalog = { + provider: 'xtream' as const, + pageSizeOptions: [10, 25, 50], + contentType: signal('vod'), + limit: signal(25), + pageIndex: signal(0), + selectedCategory: signal({ id: 1 }), + paginatedContent: signal([]), + selectedCategoryTitle: signal('Movies'), + categoryItemCount, + selectedItem: signal(null), + totalPages: signal(0), + contentSortMode, + playlist: signal(null), + isPaginatedContentLoading, + initialize: jest.fn(), + clearSelectedItem: jest.fn(), + setPage: jest.fn(), + setLimit: jest.fn(), + setContentSortMode: jest.fn(), + selectItem: jest.fn().mockReturnValue(null), + getItemProgress: jest.fn().mockReturnValue({}), + }; + + beforeEach(async () => { + isPaginatedContentLoading.set(true); + categoryItemCount.set(0); + contentSortMode.set(null); + catalog.initialize.mockClear(); + + await TestBed.configureTestingModule({ + imports: [CategoryContentViewComponent, NoopAnimationsModule], + providers: [ + { + provide: TranslateService, + useValue: { + instant: (key: string) => + key === 'WORKSPACE.SHELL.XTREAM_IMPORT_LOADING' + ? 'Fetching playlist data from source...' + : key, + get: (key: string) => + of( + key === 'WORKSPACE.SHELL.XTREAM_IMPORT_LOADING' + ? 'Fetching playlist data from source...' + : key + ), + stream: (key: string) => + of( + key === 'WORKSPACE.SHELL.XTREAM_IMPORT_LOADING' + ? 'Fetching playlist data from source...' + : key + ), + onLangChange: of(null), + onTranslationChange: of(null), + onDefaultLangChange: of(null), + currentLang: 'en', + defaultLang: 'en', + }, + }, + { + provide: PORTAL_CATALOG_FACADE, + useValue: catalog, + }, + { + provide: PORTAL_CATALOG_DETAIL_COMPONENT, + useValue: MockDetailComponent, + }, + { + provide: ActivatedRoute, + useValue: { + snapshot: { + params: {}, + }, + }, + }, + { + provide: Router, + useValue: { + navigate: jest.fn(), + }, + }, + ], + }) + .overrideComponent(CategoryContentViewComponent, { + set: { + imports: [ + NgComponentOutlet, + MockGridListComponent, + MockPlaylistErrorViewComponent, + MatIcon, + MatIconButton, + MatMenuModule, + MatPaginatorModule, + MatTooltip, + TranslatePipe, + ], + }, + }) + .compileComponents(); + + fixture = TestBed.createComponent(CategoryContentViewComponent); + }); + + it('shows loading copy in the subtitle instead of 0 items while xtream content is still warming up', () => { + fixture.detectChanges(); + + const subtitle = fixture.nativeElement.querySelector( + '.category-subtitle' + ) as HTMLElement | null; + + expect(catalog.initialize).toHaveBeenCalledWith(null); + expect(subtitle?.textContent?.trim()).toBe( + 'Fetching playlist data from source...' + ); + }); +}); diff --git a/libs/portal/catalog/feature/src/lib/category-content-view/category-content-view.component.ts b/libs/portal/catalog/feature/src/lib/category-content-view/category-content-view.component.ts index 3b27f860b..7d17b2822 100644 --- a/libs/portal/catalog/feature/src/lib/category-content-view/category-content-view.component.ts +++ b/libs/portal/catalog/feature/src/lib/category-content-view/category-content-view.component.ts @@ -6,7 +6,7 @@ import { MatMenuModule } from '@angular/material/menu'; import { MatPaginatorModule, PageEvent } from '@angular/material/paginator'; import { MatTooltip } from '@angular/material/tooltip'; import { ActivatedRoute, Router } from '@angular/router'; -import { TranslatePipe } from '@ngx-translate/core'; +import { TranslatePipe, TranslateService } from '@ngx-translate/core'; import { GridListComponent, PlaylistErrorViewComponent, @@ -50,6 +50,7 @@ interface CategoryContentItem { export class CategoryContentViewComponent implements OnInit { private readonly activatedRoute = inject(ActivatedRoute); private readonly router = inject(Router); + private readonly translate = inject(TranslateService); private readonly catalog = inject(PORTAL_CATALOG_FACADE) as PortalCatalogFacade< CategoryContentItem, CategoryContentItem, @@ -70,7 +71,18 @@ export class CategoryContentViewComponent implements OnInit { readonly contentSortMode = this.catalog.contentSortMode; readonly isPaginatedContentLoading = this.catalog.isPaginatedContentLoading; + readonly isXtreamLoadingSubtitle = computed( + () => + this.catalog.provider === 'xtream' && + this.isPaginatedContentLoading() + ); readonly categoryItemSubtitle = computed(() => { + if (this.isXtreamLoadingSubtitle()) { + return this.translate.instant( + 'WORKSPACE.SHELL.XTREAM_IMPORT_LOADING' + ); + } + const itemCount = this.categoryItemCount(); return `${itemCount} ${itemCount === 1 ? 'item' : 'items'}`; }); diff --git a/libs/portal/catalog/feature/src/test-setup.ts b/libs/portal/catalog/feature/src/test-setup.ts new file mode 100644 index 000000000..0d88648e3 --- /dev/null +++ b/libs/portal/catalog/feature/src/test-setup.ts @@ -0,0 +1,6 @@ +import { setupZoneTestEnv } from 'jest-preset-angular/setup-env/zone'; + +setupZoneTestEnv({ + errorOnUnknownElements: true, + errorOnUnknownProperties: true, +}); diff --git a/libs/portal/catalog/feature/tsconfig.json b/libs/portal/catalog/feature/tsconfig.json new file mode 100644 index 000000000..7d33479d8 --- /dev/null +++ b/libs/portal/catalog/feature/tsconfig.json @@ -0,0 +1,28 @@ +{ + "extends": "../../../../tsconfig.base.json", + "compilerOptions": { + "isolatedModules": true, + "target": "es2022", + "moduleResolution": "bundler", + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "emitDecoratorMetadata": false, + "module": "preserve" + }, + "angularCompilerOptions": { + "enableI18nLegacyMessageIdFormat": false, + "strictInjectionParameters": true, + "strictInputAccessModifiers": true, + "strictTemplates": true + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + } + ] +} diff --git a/libs/portal/catalog/feature/tsconfig.lib.json b/libs/portal/catalog/feature/tsconfig.lib.json new file mode 100644 index 000000000..7fba372dc --- /dev/null +++ b/libs/portal/catalog/feature/tsconfig.lib.json @@ -0,0 +1,12 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../../dist/out-tsc", + "declaration": true, + "declarationMap": true, + "inlineSources": true, + "types": [] + }, + "include": ["src/**/*.ts"], + "exclude": ["src/**/*.spec.ts", "src/**/*.test.ts"] +} diff --git a/libs/portal/catalog/feature/tsconfig.spec.json b/libs/portal/catalog/feature/tsconfig.spec.json new file mode 100644 index 000000000..c0bb248be --- /dev/null +++ b/libs/portal/catalog/feature/tsconfig.spec.json @@ -0,0 +1,17 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../../dist/out-tsc", + "module": "commonjs", + "target": "es2016", + "types": ["jest", "node"], + "moduleResolution": "node10" + }, + "files": ["src/test-setup.ts"], + "include": [ + "jest.config.ts", + "src/**/*.test.ts", + "src/**/*.spec.ts", + "src/**/*.d.ts" + ] +}