Co-Study4Grid / frontend /src /hooks /useOverflowIframe.test.tsx
github-actions[bot]
Deploy 7688ef1
13d4e44
Raw
History Blame Contribute Delete
6.48 kB
// Copyright (c) 2025-2026, RTE (https://www.rte-france.com)
// This Source Code Form is subject to the terms of the Mozilla Public License, version 2.0.
// If a copy of the Mozilla Public License, version 2.0 was not distributed with this file,
// you can obtain one at http://mozilla.org/MPL/2.0/.
// SPDX-License-Identifier: MPL-2.0
// This file is part of Co-Study4Grid a Power Grid Study tool Assistant Interface to help solve contigencies for a grid state under study.
import { describe, it, expect, vi } from 'vitest';
import { renderHook, act } from '@testing-library/react';
import { useOverflowIframe } from './useOverflowIframe';
import type { OverflowPin } from '../utils/svg/overflowPinPayload';
/** Window-like stub backed by a real EventTarget so we can dispatch
* message events at it just like a popup window would receive them
* from `iframe.contentWindow.parent.postMessage(...)` when the
* overflow tab is detached into a secondary browser window. */
function makePopupWindow(): Window {
const target = new EventTarget();
return Object.assign(target, { closed: false }) as unknown as Window;
}
describe('useOverflowIframe', () => {
const noopArgs = {
pdfUrl: '/results/pdf/foo.html' as string | null | undefined,
overflowPinsEnabled: false,
overflowPins: [] as ReadonlyArray<OverflowPin>,
overviewFilters: undefined,
};
it('handshake on the MAIN window flips overlayReady so pins broadcast', () => {
const postMessage = vi.fn();
const { result, rerender } = renderHook(
({ enabled }: { enabled: boolean }) => useOverflowIframe({
...noopArgs,
overflowPinsEnabled: enabled,
}),
{ initialProps: { enabled: false } },
);
// Simulate the iframe ref being attached. Only `contentWindow.postMessage`
// is read, so a stub object satisfies the broadcast path.
act(() => {
(result.current.overflowIframeRef as { current: unknown }).current = {
contentWindow: { postMessage },
};
});
// Handshake on main window — listener registered there.
act(() => {
window.dispatchEvent(new MessageEvent('message', {
data: { type: 'cs4g:overlay-ready' },
}));
});
// Toggle enabled to trigger the broadcast effect.
rerender({ enabled: true });
expect(postMessage).toHaveBeenCalledWith(
expect.objectContaining({ type: 'cs4g:pins', visible: true, pins: [] }),
'*',
);
});
it('listens on detachedWindow too so the handshake from a popup-mounted iframe is received (regression)', () => {
// Regression: when the overflow tab is detached, the iframe's
// `window.parent.postMessage(...)` lands on the popup window.
// Without binding the listener there, overlay-ready was lost,
// overlayReady stayed false and the pin-toggle did nothing
// after a layout switch.
const popup = makePopupWindow();
const postMessage = vi.fn();
const { result, rerender } = renderHook(
({ enabled }: { enabled: boolean }) => useOverflowIframe({
...noopArgs,
overflowPinsEnabled: enabled,
detachedWindow: popup,
}),
{ initialProps: { enabled: false } },
);
act(() => {
(result.current.overflowIframeRef as { current: unknown }).current = {
contentWindow: { postMessage },
};
});
// Handshake arrives on the popup window — main window's
// listener would never see it, but the popup-bound listener
// must.
act(() => {
popup.dispatchEvent(new MessageEvent('message', {
data: { type: 'cs4g:overlay-ready' },
}));
});
rerender({ enabled: true });
expect(postMessage).toHaveBeenCalledWith(
expect.objectContaining({ type: 'cs4g:pins', visible: true }),
'*',
);
});
it('broadcasts cs4g:overflow-meta whenever overflowGraphTime changes after the handshake', () => {
const postMessage = vi.fn();
const { result, rerender } = renderHook(
({ time }: { time: number | null }) => useOverflowIframe({
...noopArgs,
overflowGraphTime: time,
}),
{ initialProps: { time: null as number | null } },
);
act(() => {
(result.current.overflowIframeRef as { current: unknown }).current = {
contentWindow: { postMessage },
};
});
act(() => {
window.dispatchEvent(new MessageEvent('message', {
data: { type: 'cs4g:overlay-ready' },
}));
});
// After the handshake, the initial null meta is forwarded so the
// iframe can wipe any prior subtitle.
expect(postMessage).toHaveBeenCalledWith(
{ type: 'cs4g:overflow-meta', overflowGraphTime: null },
'*',
);
// Update the time — the new value reaches the iframe.
postMessage.mockClear();
rerender({ time: 3.42 });
expect(postMessage).toHaveBeenCalledWith(
{ type: 'cs4g:overflow-meta', overflowGraphTime: 3.42 },
'*',
);
});
it('removes the popup-window listener on unmount and on detachedWindow change', () => {
const popupA = makePopupWindow();
const popupB = makePopupWindow();
const removeA = vi.spyOn(popupA, 'removeEventListener');
const removeB = vi.spyOn(popupB, 'removeEventListener');
const { rerender, unmount } = renderHook(
({ detached }: { detached: Window | null }) => useOverflowIframe({
...noopArgs,
detachedWindow: detached,
}),
{ initialProps: { detached: popupA as Window | null } },
);
// Switch from popupA to popupB — popupA's listener must come off.
rerender({ detached: popupB });
expect(removeA).toHaveBeenCalledWith('message', expect.any(Function));
// Reattach (detached → null) — popupB's listener must come off.
rerender({ detached: null });
expect(removeB).toHaveBeenCalledWith('message', expect.any(Function));
unmount();
});
});