diff --git a/npm/ng-packs/guides/CMS_KIT_ANGULAR_STRUCTURE.md b/npm/ng-packs/guides/CMS_KIT_ANGULAR_STRUCTURE.md index a6da9d5eba..bb29a43622 100644 --- a/npm/ng-packs/guides/CMS_KIT_ANGULAR_STRUCTURE.md +++ b/npm/ng-packs/guides/CMS_KIT_ANGULAR_STRUCTURE.md @@ -1221,9 +1221,9 @@ export enum eCmsKitAdminRouteNames { ### Phase 8: Admin - Global Resources Feature -- [ ] Create GlobalResourceListComponent -- [ ] Create default extension points -- [ ] Add routes and providers +- [x] Create GlobalResourceListComponent +- [x] Create default extension points +- [x] Add routes and providers ### Phase 9: Public - Pages Feature @@ -1254,8 +1254,6 @@ export enum eCmsKitAdminRouteNames { - [ ] Write unit tests for services - [ ] Write unit tests for components - [ ] Write integration tests -- [ ] Update README documentation -- [ ] Create usage examples ## Best Practices diff --git a/npm/ng-packs/packages/cms-kit/admin/src/services/blog-post-form.service.ts b/npm/ng-packs/packages/cms-kit/admin/src/services/blog-post-form.service.ts index 1ab899da3e..37d971ca93 100644 --- a/npm/ng-packs/packages/cms-kit/admin/src/services/blog-post-form.service.ts +++ b/npm/ng-packs/packages/cms-kit/admin/src/services/blog-post-form.service.ts @@ -9,7 +9,6 @@ import { CreateBlogPostDto, UpdateBlogPostDto, BlogPostDto, - BlogPostStatus, } from '@abp/ng.cms-kit/proxy'; @Injectable({ diff --git a/npm/ng-packs/packages/cms-kit/admin/src/tests/blog-post-form.service.spec.ts b/npm/ng-packs/packages/cms-kit/admin/src/tests/blog-post-form.service.spec.ts new file mode 100644 index 0000000000..de63cc346d --- /dev/null +++ b/npm/ng-packs/packages/cms-kit/admin/src/tests/blog-post-form.service.spec.ts @@ -0,0 +1,141 @@ +/* eslint-disable */ +import { describe, it, expect, beforeEach, jest } from '@jest/globals'; +import { FormGroup } from '@angular/forms'; +import { Router } from '@angular/router'; +import { TestBed } from '@angular/core/testing'; +import { of } from 'rxjs'; +// @ts-ignore - test types are resolved only in the library build context +import { ToasterService } from '@abp/ng.theme.shared'; +// @ts-ignore - test types are resolved only in the library build context +import { BlogPostAdminService } from '@abp/ng.cms-kit/proxy'; +import { BlogPostFormService } from '../services'; + +describe('BlogPostFormService', () => { + let service: BlogPostFormService; + let blogPostAdminService: any; + let toasterService: any; + let router: any; + + beforeEach(() => { + blogPostAdminService = { + create: jest.fn().mockReturnValue(of({})), + createAndPublish: jest.fn().mockReturnValue(of({})), + createAndSendToReview: jest.fn().mockReturnValue(of({})), + update: jest.fn().mockReturnValue(of({})), + }; + + toasterService = { + success: jest.fn(), + }; + + router = { + navigate: jest.fn(), + }; + + TestBed.configureTestingModule({ + providers: [ + BlogPostFormService, + { provide: BlogPostAdminService, useValue: blogPostAdminService }, + { provide: ToasterService, useValue: toasterService }, + { provide: Router, useValue: router }, + ], + }); + + service = TestBed.inject(BlogPostFormService); + }); + + function createValidForm(): FormGroup { + // We don't rely on any specific controls, only on form.value and validity. + return new FormGroup({}); + } + + function createInvalidForm(): FormGroup { + const form = new FormGroup({}); + form.setErrors({ invalid: true }); + return form; + } + + it('should throw when creating with invalid form', () => { + const form = createInvalidForm(); + + expect(() => service.create(form)).toThrowError('Form is invalid'); + }); + + it('should call BlogPostAdminService.create and navigate on create', done => { + const form = createValidForm(); + + service.create(form).subscribe({ + next: () => { + expect(blogPostAdminService.create).toHaveBeenCalledWith(form.value); + expect(toasterService.success).toHaveBeenCalledWith('AbpUi::SavedSuccessfully'); + expect(router.navigate).toHaveBeenCalledWith(['/cms/blog-posts']); + done(); + }, + error: err => done(err as any), + }); + }); + + it('should call BlogPostAdminService.create on createAsDraft', done => { + const form = createValidForm(); + + service.createAsDraft(form).subscribe({ + next: () => { + expect(blogPostAdminService.create).toHaveBeenCalledWith(form.value); + done(); + }, + error: err => done(err as any), + }); + }); + + it('should call BlogPostAdminService.createAndPublish on createAndPublish', done => { + const form = createValidForm(); + + service.createAndPublish(form).subscribe({ + next: () => { + expect(blogPostAdminService.createAndPublish).toHaveBeenCalledWith(form.value); + done(); + }, + error: err => done(err as any), + }); + }); + + it('should call BlogPostAdminService.createAndSendToReview on createAndSendToReview', done => { + const form = createValidForm(); + + service.createAndSendToReview(form).subscribe({ + next: () => { + expect(blogPostAdminService.createAndSendToReview).toHaveBeenCalledWith(form.value); + done(); + }, + error: err => done(err as any), + }); + }); + + it('should throw when updating with invalid form or missing blog post', () => { + const form = createInvalidForm(); + + expect(() => service.update('id', form, {} as any)).toThrowError( + 'Form is invalid or blog post is missing', + ); + + const validForm = createValidForm(); + expect(() => service.update('id', validForm, null as any)).toThrowError( + 'Form is invalid or blog post is missing', + ); + }); + + it('should call BlogPostAdminService.update and navigate on update', done => { + const form = createValidForm(); + const blogPost = { id: '1', title: 't' }; + + service.update('1', form, blogPost).subscribe({ + next: () => { + expect(blogPostAdminService.update).toHaveBeenCalled(); + expect(toasterService.success).toHaveBeenCalledWith('AbpUi::SavedSuccessfully'); + expect(router.navigate).toHaveBeenCalledWith(['/cms/blog-posts']); + done(); + }, + error: err => done(err as any), + }); + }); +}); diff --git a/npm/ng-packs/packages/cms-kit/admin/src/tests/cms-kit-admin.routes.spec.ts b/npm/ng-packs/packages/cms-kit/admin/src/tests/cms-kit-admin.routes.spec.ts new file mode 100644 index 0000000000..8248491fd0 --- /dev/null +++ b/npm/ng-packs/packages/cms-kit/admin/src/tests/cms-kit-admin.routes.spec.ts @@ -0,0 +1,64 @@ +/* eslint-disable */ +import { describe, it, expect } from '@jest/globals'; +import { Routes } from '@angular/router'; +import { createRoutes } from '../cms-kit-admin.routes'; +import { CmsKitAdminConfigOptions } from '../models'; + +describe('cms-kit-admin routes', () => { + function findRoute(routes: Routes, path: string): any { + for (const route of routes) { + if (route.path === path) { + return route; + } + if (route.children) { + const found = findRoute(route.children, path); + if (found) { + return found; + } + } + } + return null; + } + + it('should create base route with children', () => { + const routes = createRoutes(); + + expect(Array.isArray(routes)).toBe(true); + const root = routes[0]; + expect(root.path).toBe(''); + expect(root.children?.length).toBeGreaterThan(0); + }); + + it('should contain expected admin routes with required policies', () => { + const routes = createRoutes(); + + const comments = findRoute(routes, 'comments'); + const pages = findRoute(routes, 'pages'); + const blogs = findRoute(routes, 'blogs'); + const blogPosts = findRoute(routes, 'blog-posts'); + const menus = findRoute(routes, 'menus'); + const globalResources = findRoute(routes, 'global-resources'); + + expect(comments?.data?.requiredPolicy).toBe('CmsKit.Comments'); + expect(pages?.data?.requiredPolicy).toBe('CmsKit.Pages'); + expect(blogs?.data?.requiredPolicy).toBe('CmsKit.Blogs'); + expect(blogPosts?.data?.requiredPolicy).toBe('CmsKit.BlogPosts'); + expect(menus?.data?.requiredPolicy).toBe('CmsKit.Menus'); + expect(globalResources?.data?.requiredPolicy).toBe('CmsKit.GlobalResources'); + }); + + it('should propagate contributors from config options', () => { + const options: CmsKitAdminConfigOptions = { + entityActionContributors: {}, + entityPropContributors: {}, + toolbarActionContributors: {}, + createFormPropContributors: {}, + editFormPropContributors: {}, + }; + + const routes = createRoutes(options); + const root = routes[0]; + + expect(root.providers).toBeDefined(); + }); +}); diff --git a/npm/ng-packs/packages/cms-kit/admin/src/tests/comment-entity.service.spec.ts b/npm/ng-packs/packages/cms-kit/admin/src/tests/comment-entity.service.spec.ts new file mode 100644 index 0000000000..cedd3badfa --- /dev/null +++ b/npm/ng-packs/packages/cms-kit/admin/src/tests/comment-entity.service.spec.ts @@ -0,0 +1,102 @@ +/* eslint-disable */ +import { describe, it, expect, beforeEach, jest } from '@jest/globals'; +import { Router } from '@angular/router'; +import { TestBed } from '@angular/core/testing'; +import { of, Subject } from 'rxjs'; +// @ts-ignore - test types are resolved only in the library build context +import { ConfigStateService, ListService } from '@abp/ng.core'; +// @ts-ignore - test types are resolved only in the library build context +import { Confirmation, ConfirmationService, ToasterService } from '@abp/ng.theme.shared'; +// @ts-ignore - proxy module types are resolved only in the library build context +import { CommentAdminService, CommentGetListInput } from '@abp/ng.cms-kit/proxy'; +import { CommentEntityService } from '../services'; + +describe('CommentEntityService', () => { + let service: CommentEntityService; + let commentAdminService: any; + let toasterService: any; + let confirmationService: any; + let configStateService: any; + let router: any; + + beforeEach(() => { + commentAdminService = { + updateApprovalStatus: jest.fn().mockReturnValue(of(void 0)), + delete: jest.fn().mockReturnValue(of(void 0)), + }; + + toasterService = { + success: jest.fn(), + }; + + confirmationService = { + warn: jest.fn(), + }; + + configStateService = { + getSetting: jest.fn(), + }; + + router = { + url: '/cms/comments/123', + }; + + TestBed.configureTestingModule({ + providers: [ + CommentEntityService, + { provide: CommentAdminService, useValue: commentAdminService }, + { provide: ToasterService, useValue: toasterService }, + { provide: ConfirmationService, useValue: confirmationService }, + { provide: ConfigStateService, useValue: configStateService }, + { provide: Router, useValue: router }, + ], + }); + + service = TestBed.inject(CommentEntityService); + }); + + it('should return requireApprovement based on setting', () => { + configStateService.getSetting.mockReturnValue('true'); + expect(service.requireApprovement).toBe(true); + + configStateService.getSetting.mockReturnValue('false'); + expect(service.requireApprovement).toBe(false); + }); + + it('should detect comment reply from router url', () => { + expect(service.isCommentReply('123')).toBe(true); + expect(service.isCommentReply('456')).toBe(false); + expect(service.isCommentReply(undefined)).toBe(false); + }); + + it('should update approval status and refresh list', () => { + const list = { + get: jest.fn(), + } as unknown as ListService; + + service.updateApprovalStatus('1', true, list); + + expect(commentAdminService.updateApprovalStatus).toHaveBeenCalledWith('1', { + isApproved: true, + }); + expect(list.get).toHaveBeenCalled(); + expect(toasterService.success).toHaveBeenCalledWith('CmsKit::ApprovedSuccessfully'); + }); + + it('should show confirmation and delete comment when confirmed', () => { + const subject = new Subject(); + (confirmationService.warn as jest.Mock).mockReturnValue(subject.asObservable()); + + const list = { + get: jest.fn(), + } as unknown as ListService; + + service.delete('1', list); + + subject.next(Confirmation.Status.confirm); + subject.complete(); + + expect(commentAdminService.delete).toHaveBeenCalledWith('1'); + expect(list.get).toHaveBeenCalled(); + }); +}); diff --git a/npm/ng-packs/packages/cms-kit/admin/src/tests/page-form.service.spec.ts b/npm/ng-packs/packages/cms-kit/admin/src/tests/page-form.service.spec.ts new file mode 100644 index 0000000000..e49a5ea839 --- /dev/null +++ b/npm/ng-packs/packages/cms-kit/admin/src/tests/page-form.service.spec.ts @@ -0,0 +1,156 @@ +/* eslint-disable */ +import { describe, it, expect, beforeEach, jest } from '@jest/globals'; +import { FormGroup } from '@angular/forms'; +import { Router } from '@angular/router'; +import { TestBed } from '@angular/core/testing'; +import { of } from 'rxjs'; +// @ts-ignore - test types are resolved only in the library build context +import { ToasterService } from '@abp/ng.theme.shared'; +// @ts-ignore - proxy module types are resolved only in the library build context +import { PageAdminService, PageDto } from '@abp/ng.cms-kit/proxy'; +import { PageFormService } from '../services'; + +describe('PageFormService', () => { + let service: PageFormService; + let pageAdminService: any; + let toasterService: any; + let router: any; + + beforeEach(() => { + pageAdminService = { + create: jest.fn().mockReturnValue(of({})), + update: jest.fn().mockReturnValue(of({})), + setAsHomePage: jest.fn(), + delete: jest.fn(), + get: jest.fn(), + getList: jest.fn(), + }; + + toasterService = { + success: jest.fn(), + }; + + router = { + navigate: jest.fn(), + }; + + TestBed.configureTestingModule({ + providers: [ + PageFormService, + { provide: PageAdminService, useValue: pageAdminService }, + { provide: ToasterService, useValue: toasterService }, + { provide: Router, useValue: router }, + ], + }); + + service = TestBed.inject(PageFormService); + }); + + function createValidForm(): FormGroup { + return new FormGroup({}); + } + + function createInvalidForm(): FormGroup { + const form = new FormGroup({}); + form.setErrors({ invalid: true }); + return form; + } + + it('should throw when creating with invalid form', () => { + const form = createInvalidForm(); + + expect(() => service.create(form)).toThrowError('Form is invalid'); + }); + + it('should call PageAdminService.create and navigate on create', done => { + const form = createValidForm(); + + service.create(form).subscribe({ + next: () => { + expect(pageAdminService.create).toHaveBeenCalledWith(form.value); + expect(toasterService.success).toHaveBeenCalledWith('AbpUi::SavedSuccessfully'); + expect(router.navigate).toHaveBeenCalledWith(['/cms/pages']); + done(); + }, + error: err => done(err as any), + }); + }); + + it('should call PageAdminService.create on createAsDraft', done => { + const form = createValidForm(); + + service.createAsDraft(form).subscribe({ + next: () => { + expect(pageAdminService.create).toHaveBeenCalled(); + done(); + }, + error: err => done(err as any), + }); + }); + + it('should call PageAdminService.create on publish', done => { + const form = createValidForm(); + + service.publish(form).subscribe({ + next: () => { + expect(pageAdminService.create).toHaveBeenCalled(); + done(); + }, + error: err => done(err as any), + }); + }); + + it('should throw when updating with invalid form or missing page', () => { + const form = createInvalidForm(); + + expect(() => service.update('id', form, {} as any)).toThrowError( + 'Form is invalid or page is missing', + ); + + const validForm = createValidForm(); + expect(() => service.update('id', validForm, null as any)).toThrowError( + 'Form is invalid or page is missing', + ); + }); + + it('should call PageAdminService.update on update', done => { + const form = createValidForm(); + const page = { id: '1', name: 'test', isHomePage: false }; + + service.update('1', form, page).subscribe({ + next: () => { + expect(pageAdminService.update).toHaveBeenCalledWith('1', expect.objectContaining(page)); + done(); + }, + error: err => done(err as any), + }); + }); + + it('should set status Draft on updateAsDraft', done => { + const form = createValidForm(); + const page = { id: '1', name: 'test', isHomePage: false }; + + service.updateAsDraft('1', form, page).subscribe({ + next: () => { + const arg = pageAdminService.update.mock.calls[0][1]; + expect(arg).toMatchObject(page); + done(); + }, + error: err => done(err as any), + }); + }); + + it('should set status Publish on updateAndPublish', done => { + const form = createValidForm(); + const page = { id: '1', name: 'test', isHomePage: false }; + + service.updateAndPublish('1', form, page).subscribe({ + next: () => { + const arg = pageAdminService.update.mock.calls[0][1]; + expect(arg).toMatchObject(page); + done(); + }, + error: err => done(err as any), + }); + }); +}); diff --git a/npm/ng-packs/packages/cms-kit/src/test-setup.ts b/npm/ng-packs/packages/cms-kit/src/test-setup.ts index 1100b3e8a6..ff49dc4906 100644 --- a/npm/ng-packs/packages/cms-kit/src/test-setup.ts +++ b/npm/ng-packs/packages/cms-kit/src/test-setup.ts @@ -1 +1,15 @@ -import 'jest-preset-angular/setup-jest'; +import { setupZoneTestEnv } from 'jest-preset-angular/setup-env/zone'; + +setupZoneTestEnv(); + +// Optional: align with core package behavior and provide a stable window.location +Object.defineProperty(window, 'location', { + value: { + href: 'http://localhost:4200', + origin: 'http://localhost:4200', + pathname: '/', + search: '', + hash: '', + }, + writable: true, +});