Skip to content
Draft
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
63 changes: 63 additions & 0 deletions __test__/hostOnlyListeners.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import postRobot from "post-robot";

import Asset from "../src/stack/api/asset";
import { IAppConfigInitData, LocationType } from "../src/types";
import UiLocation from "../src/uiLocation";

jest.mock("post-robot");

// MKT-22128: the host (app-extension-component) sends extensionEvent and
// uploadReady_<uid> from the window that holds the app iframe, i.e.
// window.parent. A listener without a window filter also accepts the same
// messages from sibling app frames, so both must be scoped to window.parent.
describe("host-only post-robot listeners (MKT-22128)", () => {
// jsdom's window.parent is window itself; a distinct parent proves the
// listener is bound to the parent and not to the app's own window.
const fakeParent = {} as Window;

beforeEach(() => {
jest.spyOn(window, "parent", "get").mockReturnValue(fakeParent);
(postRobot as any).on.mockReturnValue({ cancel: jest.fn() });
(postRobot as any).sendToParent.mockResolvedValue({ data: {} });
});

afterEach(() => {
jest.restoreAllMocks();
jest.clearAllMocks();
window["postRobot"] = undefined;
});

it("listens for extensionEvent from window.parent only", () => {
new UiLocation({
type: LocationType.APP_CONFIG_WIDGET,
app_id: "app_id",
installation_uid: "installation_uid",
extension_uid: "extension_uid",
region: "NA",
endpoints: { CMA: "", APP: "", DEVELOPER_HUB: "" },
stack: {} as any,
user: {} as any,
currentBranch: "main",
manifest: {} as any,
} as IAppConfigInitData);

const call = (postRobot as any).on.mock.calls.find(
([name]) => name === "extensionEvent"
);
expect(call).toBeDefined();
// compare by identity: printing a Window on failure exhausts memory
expect(call[1]?.window === fakeParent).toBe(true);
expect(typeof call[2]).toBe("function");
});

it("listens for uploadReady from window.parent only", async () => {
await Asset({}).uploadAsset([new Blob(["x"])]);

const call = (postRobot as any).on.mock.calls.find(([name]) =>
String(name).startsWith("uploadReady_")
);
expect(call).toBeDefined();
expect(call[1]?.window === fakeParent).toBe(true);
expect(typeof call[2]).toBe("function");
});
});
1 change: 1 addition & 0 deletions src/stack/api/asset/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,7 @@ class Asset extends Base {
try {
const uploadReadyListener = postRobot.on(
`uploadReady_${uid}`,
{ window: window.parent },
function () {
window.parent.postMessage(
{
Expand Down
6 changes: 5 additions & 1 deletion src/types/post-robot.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,11 @@ declare module "post-robot" {
cancel: () => void;
}

export function on(name: string, handler?: HandlerType): CancelableType;
export function on(
name: string,
options?: ServerOptionsType | HandlerType,
handler?: HandlerType
): CancelableType;

export function once(
name: string,
Expand Down
4 changes: 3 additions & 1 deletion src/uiLocation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -348,7 +348,9 @@ class UiLocation {
}

try {
postRobot.on("extensionEvent", async (event) => {
// Only the host (the window holding this iframe) sends events; ignore sibling frames.
const fromHost = { window: window.parent };
postRobot.on("extensionEvent", fromHost, async (event) => {
if (event.data.name === "entrySave") {
emitter.emitEvent("entrySave", [{ data: event.data.data }]);
emitter.emitEvent("updateFields", [
Expand Down
Loading