From 55fbe5a54e7dbae8d6ca8be35876c604e17e1f47 Mon Sep 17 00:00:00 2001 From: Sakana <15715093608@163.com> Date: Fri, 6 Nov 2026 22:03:04 +0800 Subject: [PATCH] fix(hub-ui): prevent nested docks in popup panels --- .../hub-ui/src/client/state/popup.test.ts | 83 +++++++++++++++++++ packages/hub-ui/src/client/state/popup.ts | 4 + 2 files changed, 87 insertions(+) create mode 100644 packages/hub-ui/src/client/state/popup.test.ts diff --git a/packages/hub-ui/src/client/state/popup.test.ts b/packages/hub-ui/src/client/state/popup.test.ts new file mode 100644 index 000000000..4e626d20b --- /dev/null +++ b/packages/hub-ui/src/client/state/popup.test.ts @@ -0,0 +1,83 @@ +import type { DocksContext } from '@devframes/hub/client' +import { CLIENT_CONTEXT_KEY } from '@devframes/hub/client' +import { afterEach, expect, it, vi } from 'vitest' +import { isInsideHub } from '../embedded/is-inside-hub' +import { closeDockPopup, requestDockPopupOpen, useIsDockPopupOpen } from './popup' + +vi.mock('./branding', () => ({ + applyDocumentHead: vi.fn(), + applyPrimaryColor: vi.fn(), + useBranding: () => ({ value: {} }), +})) +vi.mock('./color-mode', async () => { + const { ref } = await import('vue') + return { + isDark: ref(false), + } +}) + +const { onMount } = vi.hoisted(() => ({ + onMount: vi.fn(), +})) +vi.mock('../components/DockStandalone', () => ({ + DockStandalone: class { + constructor() { onMount() } + remove() {} + }, +})) + +afterEach(() => { + closeDockPopup() + vi.unstubAllGlobals() + vi.clearAllMocks() +}) + +it('identifies the popup as a Hub before mounting its iframe views', async () => { + const element = () => ({ + appendChild: vi.fn(), + style: { + setProperty: vi.fn(), + }, + }) + const popup = { + document: { + createElement: element, + documentElement: element(), + head: element(), + body: { + ...element(), + textContent: '', + }, + }, + addEventListener: vi.fn(), + removeEventListener: vi.fn(), + close: vi.fn(), + } + // The fixture supplies only the context fields used by popup initialization. + // eslint-disable-next-line slop/no-chained-type-assertions + const context = { + panel: { + store: { width: 80, height: 80 }, + }, + rpc: { connectionMeta: {} }, + docks: { + switchEntry: vi.fn(), + }, + } as unknown as DocksContext + vi.stubGlobal('window', { + innerWidth: 1000, + innerHeight: 800, + documentPictureInPicture: { + requestWindow: async () => popup, + }, + }) + let insideHubAtMount: boolean | undefined + onMount.mockImplementation(() => { + insideHubAtMount = isInsideHub({ parent: popup }) + }) + + requestDockPopupOpen(context) + await vi.waitFor(() => expect(useIsDockPopupOpen().value).toBe(true)) + expect(insideHubAtMount).toBe(true) + expect(Reflect.get(popup, CLIENT_CONTEXT_KEY)).toBe(context) +}) diff --git a/packages/hub-ui/src/client/state/popup.ts b/packages/hub-ui/src/client/state/popup.ts index a519f9204..f8abfdb40 100644 --- a/packages/hub-ui/src/client/state/popup.ts +++ b/packages/hub-ui/src/client/state/popup.ts @@ -1,4 +1,5 @@ import type { DocksContext } from '@devframes/hub/client' +import { CLIENT_CONTEXT_KEY } from '@devframes/hub/client' import { createEventEmitter } from 'devframe/utils/events' import { shallowRef, watch } from 'vue' import { applyDocumentHead, applyPrimaryColor, useBranding } from './branding' @@ -125,6 +126,9 @@ async function mountStandaloneApp(context: DocksContext, popup: Window) { ;(popup as Window & { [DEVFRAME_CONNECTION_META_KEY]?: unknown })[DEVFRAME_CONNECTION_META_KEY] = (globalThis as Record)[DEVFRAME_CONNECTION_META_KEY] ?? context.rpc.connectionMeta + // Let iframe bootstraps recognize the Hub before its views mount. + ;(popup as Window & { [CLIENT_CONTEXT_KEY]?: DocksContext })[CLIENT_CONTEXT_KEY] = context + const appRoot = popup.document.createElement('div') appRoot.id = 'devframes-popup-root' popup.document.body.appendChild(appRoot)