From 7c3a159b64bcab9108191d10909de7f1f2c0eadc Mon Sep 17 00:00:00 2001 From: Corinne PAULVE Date: Fri, 13 Oct 2023 10:36:06 +0200 Subject: [PATCH] feat: post message events upon placeholder --- .../placeholder-template.selectors.ts | 32 +++++-- .../placeholder/placeholder.component.ts | 44 +++++---- .../placeholder/placeholder.interface.ts | 27 ++++++ .../src/tools/placeholder/placeholder.spec.ts | 91 ++++++++++++++++--- 4 files changed, 159 insertions(+), 35 deletions(-) create mode 100644 packages/@o3r/components/src/tools/placeholder/placeholder.interface.ts diff --git a/packages/@o3r/components/src/stores/placeholder-template/placeholder-template.selectors.ts b/packages/@o3r/components/src/stores/placeholder-template/placeholder-template.selectors.ts index 534c10aff9..71a55cebb9 100644 --- a/packages/@o3r/components/src/stores/placeholder-template/placeholder-template.selectors.ts +++ b/packages/@o3r/components/src/stores/placeholder-template/placeholder-template.selectors.ts @@ -19,13 +19,13 @@ export const selectPlaceholderTemplateEntity = (placeholderId: string) => createSelector(selectPlaceholderTemplateState, (state) => state?.entities[placeholderId]); /** - * Select the ordered rendered templates for a given placeholderId + * Select the ordered rendered placeholder template full data (url, priority etc.) for a given placeholderId * Return undefined if the placeholder is not found * Returns {orderedRenderedTemplates: undefined, isPending: true} if any of the request is still pending * * @param placeholderId */ -export const selectPlaceholderRenderedTemplates = (placeholderId: string) => createSelector( +export const selectSortedTemplates = (placeholderId: string) => createSelector( selectPlaceholderTemplateEntity(placeholderId), selectPlaceholderRequestState, (placeholderTemplate, placeholderRequestState) => { @@ -52,13 +52,31 @@ export const selectPlaceholderRenderedTemplates = (placeholderId: string) => cre }); // No need to perform sorting if still pending if (isPending) { - return {orderedRenderedTemplates: undefined, isPending}; + return {orderedTemplates: undefined, isPending}; } // Sort templates by priority - const orderedRenderedTemplates = templates.sort((template1, template2) => { + const orderedTemplates = templates.sort((template1, template2) => { return (template2.priority - template1.priority) || 1; - }).map(template => template.renderedTemplate) - .filter(renderedTemplate => !!renderedTemplate); + }).filter(templateData => !!templateData.renderedTemplate); + + return {orderedTemplates, isPending}; + }); - return {orderedRenderedTemplates, isPending}; +/** + * Select the ordered rendered templates for a given placeholderId + * Return undefined if the placeholder is not found + * Returns {orderedRenderedTemplates: undefined, isPending: true} if any of the request is still pending + * + * @param placeholderId + */ +export const selectPlaceholderRenderedTemplates = (placeholderId: string) => createSelector( + selectSortedTemplates(placeholderId), + (placeholderData) => { + if (!placeholderData) { + return; + } + return { + orderedRenderedTemplates: placeholderData.orderedTemplates?.map(placeholder => placeholder.renderedTemplate), + isPending: placeholderData.isPending + }; }); diff --git a/packages/@o3r/components/src/tools/placeholder/placeholder.component.ts b/packages/@o3r/components/src/tools/placeholder/placeholder.component.ts index a6297a19e2..860d459d7b 100644 --- a/packages/@o3r/components/src/tools/placeholder/placeholder.component.ts +++ b/packages/@o3r/components/src/tools/placeholder/placeholder.component.ts @@ -8,14 +8,15 @@ import { ViewEncapsulation } from '@angular/core'; import {Store} from '@ngrx/store'; +import {sendOtterMessage} from '@o3r/core'; import {ReplaySubject, Subscription} from 'rxjs'; -import {distinctUntilChanged, switchMap} from 'rxjs/operators'; -import {PlaceholderTemplateStore, selectPlaceholderRenderedTemplates} from '../../stores/placeholder-template'; +import {distinctUntilChanged, map, switchMap} from 'rxjs/operators'; +import {PlaceholderTemplateStore, selectSortedTemplates} from '../../stores/placeholder-template'; +import {PlaceholderLoadingStatus, PlaceholderLoadingStatusMessage} from './placeholder.interface'; /** * Placeholder component that is bind to the PlaceholderTemplateStore to display a template based on its ID * A loading indication can be provided via projection - * * @example * Is loading ... */ @@ -47,27 +48,38 @@ export class PlaceholderComponent implements OnInit, OnDestroy { constructor(private store: Store, private cd: ChangeDetectorRef) { } + private sendMessage(data: PlaceholderLoadingStatus) { + sendOtterMessage('placeholder-loading-status', data, false); + } + /** @inheritdoc */ public ngOnInit() { this.subscription.add( this.id$.pipe( distinctUntilChanged(), - switchMap((id) => this.store.select(selectPlaceholderRenderedTemplates(id))) - ).subscribe((templates) => { - if (templates) { - this.isPending = templates.isPending; - const orderedRenderedTemplates = templates.orderedRenderedTemplates; - if (!orderedRenderedTemplates || !orderedRenderedTemplates.length) { - this.template = undefined; - } else { - // Concatenates the list of templates - this.template = orderedRenderedTemplates.join(''); - } - } else { - this.isPending = false; + switchMap((id) => + this.store.select(selectSortedTemplates(id)).pipe( + map((placeholders) => ({ + id, + orderedTemplates: placeholders?.orderedTemplates, + isPending: placeholders?.isPending + })) + ) + ) + ).subscribe(({id, orderedTemplates, isPending}) => { + this.isPending = isPending; + if (!orderedTemplates?.length) { this.template = undefined; + } else { + // Concatenates the list of templates + this.template = orderedTemplates.map(placeholder => placeholder.renderedTemplate).join(''); } this.cd.markForCheck(); + this.sendMessage({ + status: this.isPending ? 'loading' : 'loaded', + templateIds: orderedTemplates?.map(placeholder => placeholder.rawUrl), + placeholderId: id || 'unknown' + }); }) ); } diff --git a/packages/@o3r/components/src/tools/placeholder/placeholder.interface.ts b/packages/@o3r/components/src/tools/placeholder/placeholder.interface.ts new file mode 100644 index 0000000000..315545aaf4 --- /dev/null +++ b/packages/@o3r/components/src/tools/placeholder/placeholder.interface.ts @@ -0,0 +1,27 @@ +import {OtterMessageContent} from '@o3r/core'; + +/** + * Payload of the {@see PlaceholderLoadingStatusMessage} + * + * Describe the state of an identified placeholder: what template is being loaded and whether the load is done + */ +export interface PlaceholderLoadingStatus { + /** + * Loading state + */ + status: 'loading' | 'loaded'; + /** + * Ids of the template to be loaded in the placeholder + */ + templateIds?: string[]; + /** + * Identify the placeholder under consideration + */ + placeholderId?: string; +} + +/** + * Message to describe a placeholder's loading status: the templates to be loaded and the pending status. + */ +export interface PlaceholderLoadingStatusMessage extends PlaceholderLoadingStatus, + OtterMessageContent<'placeholder-loading-status'> {} diff --git a/packages/@o3r/components/src/tools/placeholder/placeholder.spec.ts b/packages/@o3r/components/src/tools/placeholder/placeholder.spec.ts index d3349f3506..2e213be7e7 100644 --- a/packages/@o3r/components/src/tools/placeholder/placeholder.spec.ts +++ b/packages/@o3r/components/src/tools/placeholder/placeholder.spec.ts @@ -4,7 +4,7 @@ import {ComponentFixture, getTestBed, TestBed} from '@angular/core/testing'; import {By} from '@angular/platform-browser'; import {BrowserDynamicTestingModule, platformBrowserDynamicTesting} from '@angular/platform-browser-dynamic/testing'; import {Store} from '@ngrx/store'; -import {BehaviorSubject, Subject} from 'rxjs'; +import {ReplaySubject, Subject} from 'rxjs'; import {PlaceholderComponent} from './placeholder.component'; /** @@ -29,18 +29,24 @@ describe('Placeholder component', () => { })); let placeholderComponent: ComponentFixture; - type TemplatesFromStore = { orderedRenderedTemplates: (string | undefined)[]; isPending: boolean }; + type TemplatesFromStore = { + orderedTemplates: { + renderedTemplate: (string | undefined); + rawUrl: string; + }[]; + isPending: boolean; + }; let storeContent: Subject; let mockStore: { dispatch: jest.Mock; - pipe: jest.Mock; select: jest.Mock; }; + const postMessageMock = jest.spyOn(window, 'postMessage'); + beforeEach(async () => { - storeContent = new BehaviorSubject({orderedRenderedTemplates: [''], isPending: false}); + storeContent = new ReplaySubject(1); mockStore = { dispatch: jest.fn(), - pipe: jest.fn().mockReturnValue(storeContent), select: jest.fn().mockReturnValue(storeContent) }; await TestBed.configureTestingModule({ @@ -57,11 +63,19 @@ describe('Placeholder component', () => { }).compileComponents(); }); + afterEach(() => { + jest.clearAllMocks(); + postMessageMock.mockReset(); + }); + it('should render the template', () => { placeholderComponent = TestBed.createComponent(PlaceholderComponent); storeContent.next({ - orderedRenderedTemplates: ['
test template
'], + orderedTemplates: [{ + rawUrl: '/test.json', + renderedTemplate: '
test template
' + }], isPending: false }); @@ -70,13 +84,28 @@ describe('Placeholder component', () => { expect(placeholderComponent.nativeElement.children[0].tagName).toBe('DIV'); expect(placeholderComponent.nativeElement.children[0].innerHTML).toEqual('
test template
'); + expect(postMessageMock).toHaveBeenCalledWith({ + type: 'otter', + content: { + dataType: 'placeholder-loading-status', + status: 'loaded', + templateIds: ['/test.json'], + placeholderId: 'testPlaceholder' + } + }, '*'); }); it('should render the templates', () => { placeholderComponent = TestBed.createComponent(PlaceholderComponent); storeContent.next({ - orderedRenderedTemplates: ['
test template
', '
test template 2
'], + orderedTemplates: [{ + renderedTemplate: '
test template
', + rawUrl: 'assets/test.json' + }, { + renderedTemplate: '
test template 2
', + rawUrl: 'assets/test2.json' + }], isPending: false }); @@ -85,6 +114,15 @@ describe('Placeholder component', () => { expect(placeholderComponent.nativeElement.children[0].tagName).toBe('DIV'); expect(placeholderComponent.nativeElement.children[0].innerHTML).toEqual('
test template
test template 2
'); + expect(postMessageMock).toHaveBeenCalledWith({ + type: 'otter', + content: { + dataType: 'placeholder-loading-status', + status: 'loaded', + templateIds: ['assets/test.json', 'assets/test2.json'], + placeholderId: 'testPlaceholder' + } + }, '*'); }); it('should retrieve new template on ID change', () => { @@ -97,6 +135,7 @@ describe('Placeholder component', () => { placeholderComponent.detectChanges(true); expect(mockStore.select).toHaveBeenCalledTimes(2); + expect(postMessageMock).not.toHaveBeenCalled(); }); it('isPending status of the placeholder should display the ng-content', () => { @@ -111,15 +150,43 @@ describe('Placeholder component', () => { expect(contentDisplayed.query(By.css('span'))).toBe(null); // Simulate a call to an url to retrieve a placeholder, Loading... should be displayed - storeContent.next({orderedRenderedTemplates: [''], isPending: true}); + storeContent.next({ + orderedTemplates: [{ + renderedTemplate: '', + rawUrl: '/test-empty.json' + }], + isPending: true + }); testComponentFixture.detectChanges(); - - expect(contentDisplayed.query(By.css('span')).nativeElement.innerHTML).toBe('Loading...'); + expect(postMessageMock).toHaveBeenCalledWith({ + type: 'otter', + content: { + dataType: 'placeholder-loading-status', + status: 'loading', + templateIds: ['/test-empty.json'], + placeholderId: 'placeholder1' + } + }, '*'); + expect(testComponentFixture.debugElement.query(By.css('span')).nativeElement.innerHTML).toBe('Loading...'); // Simulate the result from the call, setting isPending to false, renderedTemplate should be displayed - storeContent.next({orderedRenderedTemplates: ['This is the rendered template'], isPending: false}); + storeContent.next({ + orderedTemplates: [{ + renderedTemplate: 'This is the rendered template', + rawUrl: '/test.json' + }], + isPending: false + }); testComponentFixture.detectChanges(); - + expect(postMessageMock).toHaveBeenCalledWith({ + type: 'otter', + content: { + dataType: 'placeholder-loading-status', + status: 'loaded', + templateIds: ['/test.json'], + placeholderId: 'placeholder1' + } + }, '*'); expect(contentDisplayed.query(By.css('div')).nativeElement.innerHTML).toBe('This is the rendered template'); }); });