156 lines
5.3 KiB
TypeScript
156 lines
5.3 KiB
TypeScript
|
|
import { TestBed } from '@angular/core/testing';
|
||
|
|
import { of, throwError } from 'rxjs';
|
||
|
|
import { AdminModerationFacade } from './admin-moderation.facade';
|
||
|
|
import { ADMIN_MODERATION_GATEWAY } from '../services/admin-moderation-gateway.token';
|
||
|
|
import { AdminModerationGateway } from '../services/admin-moderation-gateway.interface';
|
||
|
|
import { AdminReview } from '../models/admin-review.model';
|
||
|
|
import { LocalStorageService } from '../../../../core/storage/local-storage.service';
|
||
|
|
|
||
|
|
function review(overrides: Partial<AdminReview> = {}): AdminReview {
|
||
|
|
return {
|
||
|
|
id: 'r1',
|
||
|
|
productId: 'p1',
|
||
|
|
productName: 'Widget',
|
||
|
|
customerName: 'Buyer',
|
||
|
|
customerEmail: 'b@example.com',
|
||
|
|
rating: 5,
|
||
|
|
text: 'Great',
|
||
|
|
photos: [],
|
||
|
|
status: 'pending',
|
||
|
|
visible: true,
|
||
|
|
pinned: false,
|
||
|
|
featured: false,
|
||
|
|
reportCount: 0,
|
||
|
|
moderatorNotes: '',
|
||
|
|
timeline: [],
|
||
|
|
createdAt: new Date().toISOString(),
|
||
|
|
updatedAt: new Date().toISOString(),
|
||
|
|
...overrides,
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
describe('AdminModerationFacade', () => {
|
||
|
|
let facade: AdminModerationFacade;
|
||
|
|
let gateway: jasmine.SpyObj<AdminModerationGateway>;
|
||
|
|
|
||
|
|
beforeEach(() => {
|
||
|
|
gateway = jasmine.createSpyObj<AdminModerationGateway>('AdminModerationGateway', [
|
||
|
|
'loadReviews', 'loadReview', 'setReviewStatus', 'setReviewVisible', 'setReviewPinned',
|
||
|
|
'setReviewFeatured', 'addModeratorNote', 'deleteReview', 'loadReports', 'setReportStatus',
|
||
|
|
]);
|
||
|
|
|
||
|
|
TestBed.configureTestingModule({
|
||
|
|
providers: [
|
||
|
|
{ provide: ADMIN_MODERATION_GATEWAY, useValue: gateway },
|
||
|
|
{ provide: LocalStorageService, useValue: { getItem: () => null, setItem: () => {}, getJSON: () => null, setJSON: () => {} } },
|
||
|
|
],
|
||
|
|
});
|
||
|
|
|
||
|
|
facade = TestBed.inject(AdminModerationFacade);
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('health', () => {
|
||
|
|
it('scores 100% when every check passes', () => {
|
||
|
|
const result = facade.health(review({ rating: 5, text: 'x', photos: ['a'], status: 'approved', reportCount: 0, visible: true }));
|
||
|
|
|
||
|
|
expect(result.completionPercent).toBe(100);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('scores 0% when every check fails', () => {
|
||
|
|
const result = facade.health(review({ rating: 0, text: '', photos: [], status: 'pending', reportCount: 1, visible: false }));
|
||
|
|
|
||
|
|
expect(result.completionPercent).toBe(0);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('treats whitespace-only text as no text', () => {
|
||
|
|
const result = facade.health(review({ text: ' ' }));
|
||
|
|
|
||
|
|
expect(result.hasText).toBeFalse();
|
||
|
|
});
|
||
|
|
|
||
|
|
it('any status other than pending counts as moderated', () => {
|
||
|
|
expect(facade.health(review({ status: 'approved' })).isModerated).toBeTrue();
|
||
|
|
expect(facade.health(review({ status: 'rejected' })).isModerated).toBeTrue();
|
||
|
|
expect(facade.health(review({ status: 'pending' })).isModerated).toBeFalse();
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('loadDashboardStats', () => {
|
||
|
|
it('computes averageRating only from reviews that have a rating', () => {
|
||
|
|
gateway.loadReviews.and.returnValue(of({
|
||
|
|
items: [review({ rating: 4 }), review({ rating: 0 }), review({ rating: 5 })],
|
||
|
|
total: 3, page: 1, pageSize: 100000,
|
||
|
|
}));
|
||
|
|
|
||
|
|
facade.loadDashboardStats();
|
||
|
|
|
||
|
|
// (4 + 5) / 2 = 4.5 - the zero-rating review must not drag the average down.
|
||
|
|
expect(facade.dashboardStats()?.averageRating).toBe(4.5);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('averageRating is null when no review has a rating, not 0', () => {
|
||
|
|
gateway.loadReviews.and.returnValue(of({
|
||
|
|
items: [review({ rating: 0 })],
|
||
|
|
total: 1, page: 1, pageSize: 100000,
|
||
|
|
}));
|
||
|
|
|
||
|
|
facade.loadDashboardStats();
|
||
|
|
|
||
|
|
expect(facade.dashboardStats()?.averageRating).toBeNull();
|
||
|
|
});
|
||
|
|
|
||
|
|
it('moderationHealthPercent is 100 for an empty queue, not a divide-by-zero artifact', () => {
|
||
|
|
gateway.loadReviews.and.returnValue(of({ items: [], total: 0, page: 1, pageSize: 100000 }));
|
||
|
|
|
||
|
|
facade.loadDashboardStats();
|
||
|
|
|
||
|
|
expect(facade.dashboardStats()?.moderationHealthPercent).toBe(100);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('counts reported reviews independently of their moderation status', () => {
|
||
|
|
gateway.loadReviews.and.returnValue(of({
|
||
|
|
items: [review({ status: 'approved', reportCount: 2 }), review({ status: 'pending', reportCount: 0 })],
|
||
|
|
total: 2, page: 1, pageSize: 100000,
|
||
|
|
}));
|
||
|
|
|
||
|
|
facade.loadDashboardStats();
|
||
|
|
|
||
|
|
expect(facade.dashboardStats()?.reported).toBe(1);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('setReportStatus', () => {
|
||
|
|
it('reloads the reports list after a successful status change', () => {
|
||
|
|
gateway.setReportStatus.and.returnValue(of(null));
|
||
|
|
gateway.loadReports.and.returnValue(of([]));
|
||
|
|
|
||
|
|
facade.setReportStatus('rep1', 'resolved');
|
||
|
|
|
||
|
|
expect(gateway.loadReports).toHaveBeenCalled();
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('loadReports error handling', () => {
|
||
|
|
it('clears reports and sets reportsError on failure', () => {
|
||
|
|
gateway.loadReports.and.returnValue(throwError(() => new Error('x')));
|
||
|
|
|
||
|
|
facade.loadReports();
|
||
|
|
|
||
|
|
expect(facade.reports()).toEqual([]);
|
||
|
|
expect(facade.reportsError()).toBeTrue();
|
||
|
|
expect(facade.reportsLoading()).toBeFalse();
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('selection', () => {
|
||
|
|
it('toggleAll selects every loaded review id', () => {
|
||
|
|
gateway.loadReviews.and.returnValue(of({ items: [review({ id: 'a' }), review({ id: 'b' })], total: 2, page: 1, pageSize: 10 }));
|
||
|
|
facade.loadList();
|
||
|
|
|
||
|
|
facade.toggleAll(true);
|
||
|
|
|
||
|
|
expect(facade.selectedIds()).toEqual(['a', 'b']);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
});
|