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
126 changes: 126 additions & 0 deletions editor/src/app/preview/preview.component.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import { fakeAsync, tick } from '@angular/core/testing';
import { Store } from '@ngxs/store';
import { of } from 'rxjs';

import { PreviewComponent } from './preview.component';

/**
* Covers `waitFullLoad`, which decides when a loaded preview document is
* ready for the editor services to attach to. Only the store's `isSetup`
* selection is used, so every other dependency is an empty stub.
*/
describe('PreviewComponent (waitFullLoad)', () => {
const EDITOR_URL = 'https://example.com/engine/editor/?section=works';

let component: PreviewComponent;

beforeEach(() => {
const store = { select: () => of(false) } as unknown as Store;
component = new PreviewComponent(
{} as any,
store,
{} as any,
{} as any,
{} as any,
{} as any,
{} as any,
{} as any,
{} as any,
{} as any,
);
});

function fakeDocument(href: string, bodyClass: string | null): Document {
return {
location: { href },
body: bodyClass === null ? null : { className: bodyClass },
} as unknown as Document;
}

function fakeIframe(doc: Document | null) {
return { contentDocument: doc } as { contentDocument: Document | null };
}

function watch(iframe: { contentDocument: Document | null }) {
const result = {
emitted: [] as unknown[],
completed: false,
error: undefined as unknown,
};

(component as any).waitFullLoad(iframe).subscribe({
next: (value: unknown) => result.emitted.push(value),
error: (error: unknown) => (result.error = error),
complete: () => (result.completed = true),
});

return result;
}

it('emits the iframe once the body has Berta classes', fakeAsync(() => {
const iframe = fakeIframe(fakeDocument(EDITOR_URL, 'xContent-works'));
const result = watch(iframe);

tick(100);

expect(result.emitted).toEqual([iframe]);
expect(result.completed).toBeTrue();
}));

it('emits right away for the login page', fakeAsync(() => {
const iframe = fakeIframe(
fakeDocument('https://example.com/engine/', null),
);
const result = watch(iframe);

tick(100);

expect(result.emitted).toEqual([iframe]);
}));

it('keeps waiting while the document has no body yet', fakeAsync(() => {
const doc = fakeDocument(EDITOR_URL, null);
const iframe = fakeIframe(doc);
const result = watch(iframe);

tick(500);

expect(result.emitted).toEqual([]);
expect(result.completed).toBeFalse();

(doc as any).body = { className: 'xSectionType-default' };
tick(100);

expect(result.emitted).toEqual([iframe]);
}));

it('errors once the body never appears', fakeAsync(() => {
const result = watch(fakeIframe(fakeDocument(EDITOR_URL, null)));

tick(12000);

expect(result.emitted).toEqual([]);
expect(result.error).toBeDefined();
}));

it('completes without emitting once a newer navigation replaces the document', fakeAsync(() => {
const iframe = fakeIframe(fakeDocument('about:blank', ''));
const result = watch(iframe);

tick(100);
expect(result.completed).toBeFalse();

// A freshly committed document: nothing parsed yet.
iframe.contentDocument = fakeDocument(EDITOR_URL, null);
tick(100);

expect(result.emitted).toEqual([]);
expect(result.completed).toBeTrue();

// Even once the new document is ready, it's left to its own `load`.
(iframe.contentDocument as any).body = { className: 'xContent-works' };
tick(500);

expect(result.emitted).toEqual([]);
}));
});
22 changes: 17 additions & 5 deletions editor/src/app/preview/preview.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -410,7 +410,19 @@ export class PreviewComponent implements OnInit {
observer.complete();
});

// The document this `load` event is for. A later navigation replaces it
// with a new document that fires its own `load` (and wait), so this one
// must not resolve against it — the new document may not even have an
// <html> element parsed yet.
const loadedDocument = iframe.contentDocument;

checkInterval = setInterval(() => {
if (iframe.contentDocument !== loadedDocument) {
clearInterval(checkInterval);
observer.complete();
return;
}

const lastUrlPart = iframe.contentDocument?.location.href
.replace(/\/$/, '')
.split('/')
Expand Down Expand Up @@ -438,12 +450,12 @@ export class PreviewComponent implements OnInit {
return;
}

// No body yet means the page is still being parsed.
const body = iframe.contentDocument.body;

if (
iframe.contentDocument.body &&
(iframe.contentDocument.body.classList.length === 0 ||
!/(xLoginPageBody|xContent-|xSectionType-)/.test(
iframe.contentDocument.body.className,
))
!body ||
!/(xLoginPageBody|xContent-|xSectionType-)/.test(body.className)
) {
lastError =
'Berta classes `xLoginPageBody` or `xContent-[]` or `xSectionType-` are missing from body element';
Expand Down
7 changes: 2 additions & 5 deletions editor/src/app/rerender/common/page-layout.service.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
import { Injectable } from '@angular/core';
import {
removeExtraAddBtnAndAddListeners,
replaceContent,
} from '../utilities/content';
import { notifyEntriesRerendered, replaceContent } from '../utilities/content';

@Injectable({
providedIn: 'root',
Expand Down Expand Up @@ -36,7 +33,7 @@ export class PageLayoutService {

replaceContent(dom, 'pageEntries', viewData.entries);
replaceContent(dom, 'portfolioThumbnails', viewData.portfolioThumbnails);
removeExtraAddBtnAndAddListeners(iframe);
notifyEntriesRerendered(iframe);
}

protected static handleIsResponsiveSetting(
Expand Down
9 changes: 3 additions & 6 deletions editor/src/app/rerender/default-template-rerender.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,7 @@ import { UpdateNavigationSiteSettingsAction } from '../sites/settings/site-setti
import { HandleSiteTemplateSettingsAction } from '../sites/template-settings/site-template-settings.actions';
import { TemplateRerenderService } from './template-rerender.service';
import { SiteSettingChildrenHandler } from './types/components';
import {
replaceContent,
removeExtraAddBtnAndAddListeners,
} from './utilities/content';
import { notifyEntriesRerendered, replaceContent } from './utilities/content';

@Injectable({
providedIn: 'root',
Expand Down Expand Up @@ -108,12 +105,12 @@ export class DefaultTemplateRerenderService extends TemplateRerenderService {
replaceContent(dom, 'siteBanners', viewData.siteBanners);
replaceContent(dom, 'sectionFooter', viewData.sectionFooter);

removeExtraAddBtnAndAddListeners(iframe);
notifyEntriesRerendered(iframe);
} else if (action.settingGroup === 'pageHeading') {
replaceContent(dom, 'siteHeader', viewData.siteHeader);
} else if (action.settingGroup === 'entryLayout') {
replaceContent(dom, 'pageEntries', viewData.entries);
removeExtraAddBtnAndAddListeners(iframe);
notifyEntriesRerendered(iframe);
} else if (action.settingGroup === 'css') {
DefaultTemplateRerenderService.handleCssDesignSettingChange(
dom,
Expand Down
4 changes: 2 additions & 2 deletions editor/src/app/rerender/mashup/page-layout.service.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Injectable } from '@angular/core';
import { reloadBackendJs, replaceContent } from '../utilities/content';
import { notifyEntriesRerendered, replaceContent } from '../utilities/content';
import { createEl } from '../utilities/element';
import { PageLayoutService as MainPageLayoutService } from '../common/page-layout.service';

Expand Down Expand Up @@ -50,7 +50,7 @@ export class PageLayoutService extends MainPageLayoutService {
PageLayoutService.replaceCommonContent(dom, viewData);

replaceContent(dom, 'mashupEntries', viewData.mashupEntries);
reloadBackendJs(iframe);
notifyEntriesRerendered(iframe);
}

private static handleMainColumn(
Expand Down
92 changes: 92 additions & 0 deletions editor/src/app/rerender/template-rerender.service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { Actions, ActionStatus } from '@ngxs/store';
import { Subject } from 'rxjs';

import { TemplateRerenderService } from './template-rerender.service';
import { TemplateRenderService } from '../render/template-render.service';
import { AddSectionEntryFromSyncAction } from '../sites/sections/entries/entries-state/section-entries.actions';
import { HandleSiteSettingsChildrenChangesAction } from '../sites/settings/site-settings.actions';
import { SiteSettingChildrenHandler } from './types/components';

describe('TemplateRerenderService', () => {
const ENTRY =
'<li class="xEntry"><a class="xCreateNewEntry" href="#"></a>entry</li>';

let doc: Document;
let iframe: HTMLIFrameElement;
let dispatchEvent: jasmine.Spy;
let actions$: Subject<{ action: unknown; status: ActionStatus }>;
let viewData: Record<string, string>;
let service: TemplateRerenderService;

function succeed(action: unknown) {
actions$.next({ action, status: ActionStatus.Successful });
}

function dispatchedEvents(): string[] {
return dispatchEvent.calls.allArgs().map(([event]) => event.type);
}

beforeEach(() => {
doc = document.implementation.createHTMLDocument('');
dispatchEvent = jasmine.createSpy('dispatchEvent');
iframe = {
contentDocument: doc,
contentWindow: { dispatchEvent },
} as unknown as HTMLIFrameElement;
actions$ = new Subject();
viewData = {};
service = new TemplateRerenderService(
{ getViewData: () => viewData } as unknown as TemplateRenderService,
actions$ as unknown as Actions,
);
});

describe('entry creation', () => {
const created = new AddSectionEntryFromSyncAction('', 'works', {
tag: null,
before_entry: null,
});

beforeEach(() => {
viewData.entries = ENTRY + ENTRY;
viewData.portfolioThumbnails = '';
service.handleEntryCreationRerender(iframe);
});

it('rerenders a page that has no "create new entry" link yet', () => {
doc.body.innerHTML = '<ol id="pageEntries"></ol>';

succeed(created);

expect(doc.querySelectorAll('#pageEntries .xEntry').length).toBe(2);
expect(dispatchedEvents()).toEqual(['entriesRerendered']);
});

it('keeps the "create new entry" link of every entry', () => {
doc.body.innerHTML = '<ol id="pageEntries"></ol>';

succeed(created);

expect(doc.querySelectorAll('.xEntry .xCreateNewEntry').length).toBe(2);
});

it('rerenders a page without an entries list', () => {
succeed(created);

expect(dispatchedEvents()).toEqual(['entriesRerendered']);
});
});

it('rerenders site settings on a page without entries', () => {
doc.body.innerHTML = '<div id="siteBanners"></div>';
viewData.siteBanners = '<div class="banner"></div>';
service.handleSiteSettingChildrenHandleRerender(iframe, {
banners: { id: 'siteBanners', dataKey: 'siteBanners' },
} as SiteSettingChildrenHandler);

succeed(new HandleSiteSettingsChildrenChangesAction('banners'));

expect(doc.querySelector('#siteBanners .banner')).not.toBeNull();
expect(dispatchedEvents()).toEqual(['entriesRerendered']);
});
});
13 changes: 5 additions & 8 deletions editor/src/app/rerender/template-rerender.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,7 @@ import {
UpdateSiteSettingsAction,
} from '../sites/settings/site-settings.actions';
import { Component, SiteSettingChildrenHandler } from './types/components';
import {
removeExtraAddBtnAndAddListeners,
replaceContent,
} from './utilities/content';
import { notifyEntriesRerendered, replaceContent } from './utilities/content';
import {
HandleSiteTemplateSettingsAction,
ResetToDefaultsSiteTemplateSettingsAction,
Expand Down Expand Up @@ -260,7 +257,7 @@ export class TemplateRerenderService {
viewData.portfolioThumbnails,
);

removeExtraAddBtnAndAddListeners(iframe);
notifyEntriesRerendered(iframe);
});
}

Expand All @@ -272,7 +269,7 @@ export class TemplateRerenderService {

replaceContent(iframe.contentDocument, 'pageEntries', viewData.entries);

removeExtraAddBtnAndAddListeners(iframe);
notifyEntriesRerendered(iframe);
});
}

Expand Down Expand Up @@ -319,7 +316,7 @@ export class TemplateRerenderService {
viewData.portfolioThumbnails,
);

removeExtraAddBtnAndAddListeners(iframe);
notifyEntriesRerendered(iframe);
});
}

Expand Down Expand Up @@ -350,7 +347,7 @@ export class TemplateRerenderService {
replaceContent(iframe.contentDocument, comp.id, viewData[comp.dataKey]),
);

removeExtraAddBtnAndAddListeners(iframe);
notifyEntriesRerendered(iframe);
}

public unsubscribe(win: Window, subList: Subscription[]): void {
Expand Down
19 changes: 5 additions & 14 deletions editor/src/app/rerender/utilities/content.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,9 @@ export function replaceContent(
element.appendChild(dom.createRange().createContextualFragment(sectionHtml));
}

export function removeExtraAddBtnAndAddListeners(
iframe: HTMLIFrameElement,
): void {
// remove extra 'create entry' button due to backend js reload
const createEntriesList =
iframe.contentDocument.getElementsByClassName('xCreateNewEntry');
createEntriesList[createEntriesList.length - 1].remove();

reloadBackendJs(iframe);
}

export function reloadBackendJs(iframe: HTMLIFrameElement) {
// reload backend js
iframe.contentWindow.dispatchEvent(new Event('addEntry'));
export function notifyEntriesRerendered(iframe: HTMLIFrameElement) {
// The preview scripts re-init the replaced entries: BertaEditor re-binds the
// entry listeners and replaces the "create new entry" link after the list,
// BertaPortfolio re-binds the thumbnail links
iframe.contentWindow.dispatchEvent(new Event('entriesRerendered'));
}
Loading
Loading