Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions cypress/e2e/offering.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -343,11 +343,10 @@ describe('/my-offerings',{

const interceptors = (productSpec:any, productOfferingPOST:any, newCatalog:any, defaultCatalog: any, defaultCategory:any, offPricePOST:any) => {
const specResponse = [[productSpec], []]
const response = [[],[], [productOfferingPOST], []]
const sr = [[newCatalog],[],[newCatalog], []]
let call = 0
let specCall = 0
let scall = 0
let offeringCreated = false

cy.intercept({method: 'GET', url: 'http://proxy.docker:8004/catalog/catalog?*'}, (res)=>{
res.reply({
Expand All @@ -358,7 +357,7 @@ const interceptors = (productSpec:any, productOfferingPOST:any, newCatalog:any,
cy.intercept({method: 'GET', url: 'http://proxy.docker:8004/catalog/productOffering?*'}, (res)=>{
res.reply({
statusCode: 200,
body: response[call++]
body: offeringCreated ? [productOfferingPOST] : []
})
}).as('productOff')
cy.intercept({method: 'GET', url: 'http://proxy.docker:8004/catalog/productSpecification?*'}, (res)=>{
Expand All @@ -384,7 +383,10 @@ const interceptors = (productSpec:any, productOfferingPOST:any, newCatalog:any,
}).as('defaultCategory')


cy.intercept({method: 'POST', url: `http://proxy.docker:8004/catalog/catalog/${newCatalog.id}/productOffering`}, {statusCode: 201, body: productOfferingPOST}).as('offPOST')
cy.intercept({method: 'POST', url: `http://proxy.docker:8004/catalog/catalog/${newCatalog.id}/productOffering`}, (req)=>{
offeringCreated = true
req.reply({statusCode: 201, body: productOfferingPOST})
}).as('offPOST')
if (offPricePOST){
cy.intercept({method: 'GET', url: 'http://proxy.docker:8004//usage/usageSpecification?*'}, (res)=>{
res.reply({
Expand Down
3 changes: 1 addition & 2 deletions src/app/app-routing.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ const routes: Routes = [
component: DomeBlogComponent
},
{
path: 'blog/:id',
path: 'blog/:slugOrId',
component: BlogEntryDetailComponent
},
{
Expand Down Expand Up @@ -158,4 +158,3 @@ const routes: Routes = [
exports: [RouterModule]
})
export class AppRoutingModule { }

Original file line number Diff line number Diff line change
@@ -1,23 +1,5 @@
<div class="bg-secondary-50 dark:bg-secondary-200 flex min-h-full flex-col">
<section class="w-full bg-[#DDE6F6]">
<div
class="mx-auto w-full max-w-[1440px] px-6 py-10 sm:px-8 md:px-10 md:py-12 lg:px-16 xl:px-[160px]">
<div class="flex flex-row mx-auto max-w-screen-xl justify-between">
<div class="flex flex-col">
<h1
class="text-[clamp(2.75rem,2.1rem+2.1vw,3.75rem)] font-extrabold leading-[0.95] tracking-[0.02em] text-[#0B1528]">
{{entry.title}}
</h1>
<p class="mt-4 text-[clamp(1rem,0.92rem+0.35vw,1.25rem)] leading-[1.6] text-[#4C5A6B]">
Created by
<b class="text-[#2D58A7]">
{{entry.author}}
</b>
on {{entry.date | date:'EEEE, dd/MM/yy, HH:mm'}}.
</p>
</div>
</div>
</div>
<div class="pb-4">
<nav class="flex px-5 py-3 shadow-lg text-gray-700 border border-gray-200 bg-secondary-50 dark:bg-secondary-100 dark:border-gray-800 dark:text-white">
<ol class="inline-flex items-center space-x-1 md:space-x-2 rtl:space-x-reverse">
Expand Down Expand Up @@ -46,9 +28,64 @@
<div class="flex-1 mx-auto w-full max-w-[1440px] p-4 pb-16 sm:px-8 md:px-10 lg:px-16 xl:px-[160px]">
<div
class="mx-auto w-full max-w-screen-xl rounded-lg border border-gray-300 bg-white p-6 shadow-lg dark:border-secondary-300 dark:bg-secondary-100 md:p-8 lg:p-10">
<div class="mb-4 flex items-start justify-between gap-4">
<h1 class="text-3xl font-bold text-[#0B1528] dark:text-white break-words">{{entry.title}}</h1>
@if(canManageEntry()){
<div class="flex items-center gap-2">
<button type="button" (click)="goToUpdate()"
class="text-white bg-primary-100 hover:bg-primary-50 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-full text-sm p-2.5 text-center inline-flex items-center"
title="Edit entry">
<svg class="w-[18px] h-[18px] text-white" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 24 24">
<path fill-rule="evenodd" d="M14 4.182A4.136 4.136 0 0 1 16.9 3c1.087 0 2.13.425 2.899 1.182A4.01 4.01 0 0 1 21 7.037c0 1.068-.43 2.092-1.194 2.849L18.5 11.214l-5.8-5.71 1.287-1.31.012-.012Zm-2.717 2.763L6.186 12.13l2.175 2.141 5.063-5.218-2.141-2.108Zm-6.25 6.886-1.98 5.849a.992.992 0 0 0 .245 1.026 1.03 1.03 0 0 0 1.043.242L10.282 19l-5.25-5.168Zm6.954 4.01 5.096-5.186-2.218-2.183-5.063 5.218 2.185 2.15Z" clip-rule="evenodd"/>
</svg>
</button>
<button type="button" (click)="openDeleteDialog()" [disabled]="deleting"
class="text-white bg-red-700 hover:bg-red-800 focus:ring-4 focus:outline-none focus:ring-red-300 font-medium rounded-full text-sm p-2.5 text-center inline-flex items-center disabled:opacity-60"
title="Delete entry">
<svg class="w-[18px] h-[18px] text-white" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 7h12m-1 0-.9 12.1A2 2 0 0 1 14.11 21H9.89a2 2 0 0 1-1.99-1.9L7 7m3 0V5a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1v2"/>
</svg>
</button>
</div>
}
</div>
<p class="mb-6 text-[#4C5A6B] dark:text-secondary-50">
Created by
<b class="text-[#2D58A7] dark:text-primary-50">
{{entry.author}}
</b>
on {{entry.date | date:'EEEE, dd/MM/yy, HH:mm'}}.
</p>
@if(getEntryTags(); as tags){
@if(tags.length > 0){
<div class="mb-6 flex flex-wrap gap-2">
@for(tag of tags; track tag){
<span class="inline-flex items-center rounded-md border border-blue-200 bg-blue-50 px-2.5 py-1 text-xs font-medium text-blue-700 dark:border-blue-500/40 dark:bg-blue-900/30 dark:text-blue-200">
{{ tag }}
</span>
}
</div>
}
}
@if(getFeaturedImage(); as featuredImage){
<div class="mb-6 flex justify-center">
<img [src]="featuredImage" [alt]="entry.title + ' featured image'"
class="max-h-[300px] w-full max-w-3xl rounded-lg border border-gray-300 object-contain bg-gray-50 p-2 dark:border-gray-700 dark:bg-secondary-300">
</div>
}
<markdown class="text-gray-700 dark:text-secondary-50 whitespace-pre-line break-words"
[data]="entry.content">
</markdown>
</div>
</div>
</div>

<app-confirm-dialog
[isOpen]="showDeleteConfirm"
[title]="deleteConfirmTitle"
[message]="deleteConfirmMessage"
[confirmText]="deleteConfirmButtonText"
[confirmButtonClass]="deleteConfirmButtonClass"
(confirm)="confirmDeleteEntry()"
(cancel)="closeDeleteDialog()"
></app-confirm-dialog>
Original file line number Diff line number Diff line change
@@ -1,28 +1,109 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { NO_ERRORS_SCHEMA } from '@angular/core';
import { TranslateModule } from '@ngx-translate/core';
import { RouterTestingModule } from '@angular/router/testing';
import { HttpClientTestingModule } from '@angular/common/http/testing';
import { MarkdownModule } from 'ngx-markdown';

import { BlogEntryDetailComponent } from './blog-entry-detail.component';
import { convertToParamMap } from '@angular/router';

describe('BlogEntryDetailComponent', () => {
let component: BlogEntryDetailComponent;
let fixture: ComponentFixture<BlogEntryDetailComponent>;

beforeEach(async () => {
await TestBed.configureTestingModule({
schemas: [NO_ERRORS_SCHEMA],
imports: [BlogEntryDetailComponent, HttpClientTestingModule, RouterTestingModule, TranslateModule.forRoot(), MarkdownModule.forRoot()]
})
.compileComponents();

fixture = TestBed.createComponent(BlogEntryDetailComponent);
component = fixture.componentInstance;
});
const buildComponent = (slugOrId = 'entry-slug', serviceOverrides?: Partial<any>) => {
const route = {
snapshot: {
paramMap: convertToParamMap({ slugOrId })
}
} as any;
const router = { navigate: jasmine.createSpy('navigate') } as any;
const domeBlogService = {
getBlogEntries: jasmine.createSpy('getBlogEntries').and.resolveTo([
{ _id: 'entry-1', slug: 'entry-slug' }
]),
getBlogEntryById: jasmine.createSpy('getBlogEntryById').and.resolveTo({
_id: 'entry-1',
title: 'Entry title',
metaDescription: 'Meta description',
content: 'Body content'
}),
deleteBlogEntry: jasmine.createSpy('deleteBlogEntry').and.resolveTo({ ok: true }),
...serviceOverrides
} as any;

const localStorageService = {
getObject: jasmine.createSpy('getObject').and.returnValue({})
} as any;
const titleService = { setTitle: jasmine.createSpy('setTitle') } as any;
const metaService = { updateTag: jasmine.createSpy('updateTag') } as any;

const component = new BlogEntryDetailComponent(
route,
router,
domeBlogService,
localStorageService,
titleService,
metaService
);

return { component, router, domeBlogService, titleService, metaService };
};

it('should create', () => {
const { component } = buildComponent();
expect(component).toBeTruthy();
});

it('should resolve slug to entry and apply seo metadata on init', async () => {
const { component, domeBlogService, titleService, metaService } = buildComponent('entry-slug');

await component.ngOnInit();

expect(domeBlogService.getBlogEntries).toHaveBeenCalled();
expect(domeBlogService.getBlogEntryById).toHaveBeenCalledWith('entry-1');
expect(titleService.setTitle).toHaveBeenCalledWith('Entry title');
expect(metaService.updateTag).toHaveBeenCalledWith({ name: 'description', content: 'Meta description' });
});

it('should return featured image from string or object', () => {
const { component } = buildComponent();
component.entry = { featuredImage: ' https://cdn/test.png ' };
expect(component.getFeaturedImage()).toBe('https://cdn/test.png');

component.entry = { featuredImage: { url: ' https://cdn/obj.png ' } };
expect(component.getFeaturedImage()).toBe('https://cdn/obj.png');
});

it('should normalize tags from array and csv', () => {
const { component } = buildComponent();
component.entry = { tags: [' ai ', '', 'news'] };
expect(component.getEntryTags()).toEqual(['ai', 'news']);

component.entry = { tags: 'one, two, , three' };
expect(component.getEntryTags()).toEqual(['one', 'two', 'three']);
});

it('should open delete confirmation when entry id exists', () => {
const { component } = buildComponent();
component.entry = { _id: 'entry-2', title: 'Delete me' };

component.openDeleteDialog();

expect(component.showDeleteConfirm).toBeTrue();
expect(component.deleteConfirmMessage).toContain('Delete me');
});

it('should delete entry and navigate back', async () => {
const { component, domeBlogService, router } = buildComponent();
component.entry = { _id: 'entry-5', title: 'Post' };

await component.confirmDeleteEntry();

expect(domeBlogService.deleteBlogEntry).toHaveBeenCalledWith('entry-5');
expect(router.navigate).toHaveBeenCalledWith(['/blog']);
expect(component.deleting).toBeFalse();
});

it('should fallback to id lookup for object ids', async () => {
const objectId = '507f1f77bcf86cd799439011';
const { component, domeBlogService } = buildComponent(objectId);
domeBlogService.getBlogEntryById.and.resolveTo({ _id: objectId, title: 'By id' });

const result = await component.getEntryBySlugOrId(objectId);

expect(domeBlogService.getBlogEntryById).toHaveBeenCalledWith(objectId);
expect(result._id).toBe(objectId);
});
});
Loading
Loading