Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(UVE): Ensure Window Object Re-Initialization in UVE iframe #30989

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
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
class="iframe-wrapper">
<iframe
(load)="onIframePageLoad()"
[src]="$editorProps().iframe.src | safeUrl"
[src]="uveStore.$iframeURL() | safeUrl"
[title]="host"
[ngStyle]="{
pointerEvents: $editorProps().iframe.pointerEvents,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,14 @@ const messagesMock = {

const IFRAME_MOCK = {
nativeElement: {
addEventListener: function (_event, cb) {
this.registerListener.push(cb);
},
dispatchEvent: function (event) {
this.registerListener.forEach((cb) => cb(event));
},
registerListener: [],
contentWindow: document.defaultView,
contentDocument: {
getElementsByTagName: () => [],
querySelectorAll: () => [],
Expand Down Expand Up @@ -2614,7 +2622,7 @@ describe('EditEmaEditorComponent', () => {
const iframe = spectator.debugElement.query(By.css('[data-testId="iframe"]'));

expect(iframe.nativeElement.src).toBe(
'http://localhost:3000/index?clientHost=http%3A%2F%2Flocalhost%3A3000&language_id=1&com.dotmarketing.persona.id=modes.persona.no.persona&variantName=DEFAULT'
'http://localhost:3000/page-one?clientHost=http%3A%2F%2Flocalhost%3A3000&language_id=1&com.dotmarketing.persona.id=modes.persona.no.persona&variantName=DEFAULT'
);
});

Expand All @@ -2639,6 +2647,9 @@ describe('EditEmaEditorComponent', () => {
By.css('[data-testId="iframe"]')
);

iframe.nativeElement.dispatchEvent(new Event('load'));
spectator.detectChanges();

expect(iframe.nativeElement.contentDocument.body.innerHTML).toContain(
'<div>hello world</div>'
);
Expand All @@ -2665,11 +2676,14 @@ describe('EditEmaEditorComponent', () => {
language_id: '4',
'com.dotmarketing.persona.id': DEFAULT_PERSONA.identifier
});
spectator.detectChanges();

spectator.detectChanges();
jest.runOnlyPendingTimers();

expect(iframe.nativeElement.src).toBe('http://localhost/'); //When dont have src, the src is the same as the current page
iframe.nativeElement.dispatchEvent(new Event('load'));
spectator.detectChanges();

expect(iframe.nativeElement.src).toContain('about:blank'); //When dont have src, the src is the same as the current page
expect(iframe.nativeElement.contentDocument.body.innerHTML).toContain(
'<div>New Content - Hello World</div>'
);
Expand Down Expand Up @@ -2809,24 +2823,23 @@ describe('EditEmaEditorComponent', () => {

describe('script and styles injection', () => {
let iframeDocument: Document;
let iframeElement: HTMLIFrameElement;
let spy: jest.SpyInstance;

beforeEach(() => {
jest.spyOn(window, 'requestAnimationFrame').mockImplementation((cb) => {
cb(0); // Pass a dummy value to satisfy the expected argument count

return 0;
});

// eslint-disable-next-line @typescript-eslint/no-explicit-any
spectator.component.iframe = IFRAME_MOCK as any;
iframeDocument = spectator.component.iframe.nativeElement.contentDocument;
iframeElement = spectator.component.iframe.nativeElement;
iframeDocument = iframeElement.contentDocument;
spy = jest.spyOn(iframeDocument, 'write');
});

it('should add script and styles to iframe', () => {
spectator.component.setIframeContent(`<head></head></body></body>`);

iframeElement.dispatchEvent(new Event('load'));
spectator.detectChanges();

expect(spy).toHaveBeenCalled();
expect(iframeDocument.body.innerHTML).toContain(
`<script src="${SDK_EDITOR_SCRIPT_SOURCE}"></script>`
Expand All @@ -2842,6 +2855,9 @@ describe('EditEmaEditorComponent', () => {
it('should add script and styles to iframe for advance templates', () => {
spectator.component.setIframeContent(`<div>Advanced Template</div>`);

iframeElement.dispatchEvent(new Event('load'));
spectator.detectChanges();

expect(spy).toHaveBeenCalled();
expect(iframeDocument.body.innerHTML).toContain(
`<script src="${SDK_EDITOR_SCRIPT_SOURCE}"></script>`
Expand All @@ -2854,10 +2870,6 @@ describe('EditEmaEditorComponent', () => {
'[data-dot-object="contentlet"].empty-contentlet'
);
});

afterEach(() => {
(window.requestAnimationFrame as jest.Mock).mockRestore();
});
});
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -192,27 +192,14 @@ export class EditEmaEditorComponent implements OnInit, OnDestroy {
return;
}

this.setIframeContent(code);

requestAnimationFrame(() => {
/**
* The status of isClientReady is changed outside of editor
* so we need to set it to true here to avoid the editor to be in a loading state
* This is only for traditional pages. For Headless, the isClientReady is set from the client application
*/
this.uveStore.setIsClientReady(true);
const win = this.contentWindow;
if (enableInlineEdit) {
this.inlineEditingService.injectInlineEdit(this.iframe);
} else {
this.inlineEditingService.removeInlineEdit(this.iframe);
}
this.setIframeContent(code, enableInlineEdit);

fromEvent(win, 'click').subscribe((e: MouseEvent) => {
this.handleInternalNav(e);
this.handleInlineEditing(e); // If inline editing is not active this will do nothing
});
});
/**
* The status of isClientReady is changed outside of editor
* so we need to set it to true here to avoid the editor to be in a loading state
* This is only for traditional pages. For Headless, the isClientReady is set from the client application
*/
this.uveStore.setIsClientReady(true);

return;
},
Expand Down Expand Up @@ -682,23 +669,53 @@ export class EditEmaEditorComponent implements OnInit, OnDestroy {
* @param code - The code to be added to the iframe.
* @memberof EditEmaEditorComponent
*/
setIframeContent(code: string) {
requestAnimationFrame(() => {
const doc = this.iframe?.nativeElement.contentDocument;
setIframeContent(code: string, enableInlineEdit = false): void {
const iframeElement = this.iframe?.nativeElement;

if (doc) {
const newFile = this.inyectCodeToVTL(code);
if (!iframeElement) {
return;
}

doc.open();
doc.write(newFile);
doc.close();
iframeElement.addEventListener('load', () => {
const doc = iframeElement.contentDocument;
const newDoc = this.inyectCodeToVTL(code);

this.uveStore.setOgTags(this.dotSeoMetaTagsUtilService.getMetaTags(doc));
this.ogTagsResults$ = this.dotSeoMetaTagsService
.getMetaTagsResults(doc)
.pipe(take(1));
if (!doc) {
return;
}

doc.open();
doc.write(newDoc);
doc.close();

this.uveStore.setOgTags(this.dotSeoMetaTagsUtilService.getMetaTags(doc));
this.ogTagsResults$ = this.dotSeoMetaTagsService.getMetaTagsResults(doc).pipe(take(1));
this.handleInlineScripts(enableInlineEdit);
});
}

/**
* Handle the Injection and removal of the inline editing scripts
*
* @param {boolean} enableInlineEdit
* @return {*}
* @memberof EditEmaEditorComponent
*/
handleInlineScripts(enableInlineEdit: boolean) {
const win = this.contentWindow;

fromEvent(win, 'click').subscribe((e: MouseEvent) => {
this.handleInternalNav(e);
this.handleInlineEditing(e); // If inline editing is not active this will do nothing
});

if (enableInlineEdit) {
this.inlineEditingService.injectInlineEdit(this.iframe);

return;
}

this.inlineEditingService.removeInlineEdit(this.iframe);
}

protected handleNgEvent({ event, actionPayload, clientAction }: DialogAction) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,6 @@ export interface EditorProps {
width: string;
height: string;
};
src: string;
pointerEvents: string;
opacity: string;
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -642,6 +642,23 @@ describe('withEditor', () => {
});
});

describe('$iframeURL', () => {
it("should return the iframe's URL", () => {
expect(store.$iframeURL()).toBe(
'http://localhost:3000/test-url?language_id=1&com.dotmarketing.persona.id=dot%3Apersona&variantName=DEFAULT&clientHost=http%3A%2F%2Flocalhost%3A3000'
);
});

it('should contain `about:blanck` in src when the page is traditional', () => {
patchState(store, {
pageAPIResponse: MOCK_RESPONSE_VTL,
isTraditionalPage: true
});

expect(store.$iframeURL()).toContain('about:blank');
});
});

describe('$editorProps', () => {
it('should return the expected data on init', () => {
expect(store.$editorProps()).toEqual({
Expand All @@ -651,7 +668,6 @@ describe('withEditor', () => {
iframe: {
opacity: '0.5',
pointerEvents: 'auto',
src: 'http://localhost:3000/test-url?language_id=1&com.dotmarketing.persona.id=dot%3Apersona&variantName=DEFAULT&clientHost=http%3A%2F%2Flocalhost%3A3000',
wrapper: null
},
progressBar: true,
Expand Down Expand Up @@ -720,15 +736,6 @@ describe('withEditor', () => {
expect(store.$editorProps().iframe.pointerEvents).toBe('none');
});

it('should have src as empty when the page is traditional', () => {
patchState(store, {
pageAPIResponse: MOCK_RESPONSE_VTL,
isTraditionalPage: true
});

expect(store.$editorProps().iframe.src).toBe('');
});

it('should have a wrapper when a device is present', () => {
const device = mockDotDevices[0] as DotDeviceWithIcon;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,16 +37,29 @@ import {
PositionPayload
} from '../../../shared/models';
import {
sanitizeURL,
createPageApiUrlWithQueryParams,
mapContainerStructureToArrayOfContainers,
getPersonalization,
areContainersEquals,
getEditorStates
getEditorStates,
createPageApiUrlWithQueryParams,
sanitizeURL
} from '../../../utils';
import { UVEState } from '../../models';
import { withClient } from '../client/withClient';

const buildIframeURL = ({ pageURI, params, isTraditionalPage }) => {
if (isTraditionalPage) {
// Force iframe reload on every page load to avoid caching issues and window dirty state
return `about:blank?t=${Date.now()}`;
}

const pageAPIQueryParams = createPageApiUrlWithQueryParams(pageURI, params);
const origin = params.clientHost || window.location.origin;
const url = new URL(pageAPIQueryParams, origin);

return sanitizeURL(url.toString());
};

const initialState: EditorState = {
bounds: [],
state: EDITOR_STATE.IDLE,
Expand Down Expand Up @@ -122,10 +135,6 @@ export function withEditor() {

const { dragIsActive, isScrolling } = getEditorStates(state);

const url = sanitizeURL(params?.url);

const pageAPIQueryParams = createPageApiUrlWithQueryParams(url, params);

const showDialogs = canEditPage && isEditState;
const showBlockEditorSidebar = canEditPage && isEditState && isEnterprise;

Expand All @@ -142,8 +151,6 @@ export function withEditor() {
const shouldShowSeoResults = socialMedia && ogTags;

const iframeOpacity = isLoading || !isPageReady ? '0.5' : '1';
const origin = params.clientHost || window.location.origin;
const iframeURL = new URL(pageAPIQueryParams, origin);

return {
showDialogs,
Expand All @@ -152,7 +159,6 @@ export function withEditor() {
iframe: {
opacity: iframeOpacity,
pointerEvents: dragIsActive ? 'none' : 'auto',
src: !isTraditionalPage ? iframeURL.href : '',
wrapper: device
? {
width: `${device.cssWidth}${BASE_IFRAME_MEASURE_UNIT}`,
Expand Down Expand Up @@ -189,6 +195,16 @@ export function withEditor() {
}
: null
};
}),
$iframeURL: computed<string>(() => {
rjvelazco marked this conversation as resolved.
Show resolved Hide resolved
const page = store.pageAPIResponse().page;
const url = buildIframeURL({
pageURI: page?.pageURI,
params: store.pageParams(),
isTraditionalPage: untracked(() => store.isTraditionalPage())
});

return url;
})
};
}),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,6 @@ export function withLoad() {
pageAsset?.page,
currentUser
);

const isTraditionalPage = !pageParams.clientHost;

patchState(store, {
Expand Down