|
1 | 1 | import type { DevframeDefinition, DevframeNodeContext, DevframeRpcClientFunctions, DevframeRpcServerFunctions } from 'devframe/types' |
| 2 | +import type { DevframeDockPanelStateEvent } from '../../types/docks' |
2 | 3 | import { mkdtempSync, writeFileSync } from 'node:fs' |
3 | 4 | import { createServer } from 'node:http' |
4 | 5 | import { tmpdir } from 'node:os' |
5 | 6 | import { join } from 'node:path' |
6 | 7 | import { createRpcClient } from 'devframe/rpc/client' |
7 | 8 | import { createWsRpcChannel } from 'devframe/rpc/transports/ws-client' |
8 | 9 | import { getPort } from 'get-port-please' |
9 | | -import { describe, expect, it } from 'vitest' |
| 10 | +import { describe, expect, it, vi } from 'vitest' |
10 | 11 | import { DOCK_RENDERERS_STATE_KEY } from '../../constants' |
| 12 | +import { HUB_EVENTS } from '../../events' |
11 | 13 | import { DEVFRAMES_HUB_BASE, initHub } from '../initiate' |
12 | 14 |
|
13 | 15 | function makeDist(html: string): string { |
@@ -38,10 +40,12 @@ function makeFrame(id: string, distDir?: string): DevframeDefinition { |
38 | 40 | } |
39 | 41 |
|
40 | 42 | function connectWsClient(url: string) { |
41 | | - return createRpcClient<DevframeRpcServerFunctions, DevframeRpcClientFunctions>( |
| 43 | + const channel = createWsRpcChannel({ url }) |
| 44 | + const client = createRpcClient<DevframeRpcServerFunctions, DevframeRpcClientFunctions>( |
42 | 45 | {} as DevframeRpcClientFunctions, |
43 | | - { channel: createWsRpcChannel({ url }) }, |
| 46 | + { channel }, |
44 | 47 | ) |
| 48 | + return Object.assign(client, { close: channel.close }) |
45 | 49 | } |
46 | 50 |
|
47 | 51 | describe('initHub', () => { |
@@ -152,6 +156,63 @@ describe('initHub', () => { |
152 | 156 | } |
153 | 157 | }) |
154 | 158 |
|
| 159 | + it('tracks panel state by RPC connection and emits disconnect separately from close', async () => { |
| 160 | + expect.assertions(9) |
| 161 | + |
| 162 | + const host = '127.0.0.1' |
| 163 | + const port = await getPort({ port: 18215, host }) |
| 164 | + const hub = initHub({ |
| 165 | + base: DEVFRAMES_HUB_BASE, |
| 166 | + auth: false, |
| 167 | + host, |
| 168 | + ws: { port }, |
| 169 | + devframes: [makeFrame('alpha')], |
| 170 | + }) |
| 171 | + const clients: ReturnType<typeof connectWsClient>[] = [] |
| 172 | + |
| 173 | + try { |
| 174 | + await hub.ready |
| 175 | + const context = await hub.context |
| 176 | + const lifecycleEvents: DevframeDockPanelStateEvent[] = [] |
| 177 | + context.docks.events.on(HUB_EVENTS.bus.docksPanelState, event => lifecycleEvents.push(event)) |
| 178 | + |
| 179 | + const firstClient = connectWsClient(`ws://${host}:${port}/__ws`) |
| 180 | + const secondClient = connectWsClient(`ws://${host}:${port}/__ws`) |
| 181 | + clients.push(firstClient, secondClient) |
| 182 | + |
| 183 | + await firstClient.$call(HUB_EVENTS.rpc.docksPanelState, true) |
| 184 | + await firstClient.$call(HUB_EVENTS.rpc.docksPanelState, true) |
| 185 | + await firstClient.$call(HUB_EVENTS.rpc.docksPanelState, false) |
| 186 | + await secondClient.$call(HUB_EVENTS.rpc.docksPanelState, false) |
| 187 | + |
| 188 | + expect(lifecycleEvents).toHaveLength(3) |
| 189 | + expect(lifecycleEvents[0]).toMatchObject({ type: 'connected', open: true }) |
| 190 | + expect(typeof lifecycleEvents[0]!.sessionId).toBe('number') |
| 191 | + expect(lifecycleEvents[1]).toEqual({ type: 'changed', sessionId: lifecycleEvents[0]!.sessionId, open: false }) |
| 192 | + expect(lifecycleEvents[2]).toMatchObject({ type: 'connected', open: false }) |
| 193 | + expect(lifecycleEvents[2]!.sessionId).not.toBe(lifecycleEvents[0]!.sessionId) |
| 194 | + |
| 195 | + firstClient.close() |
| 196 | + await vi.waitFor(() => { |
| 197 | + if (lifecycleEvents.length !== 4) |
| 198 | + throw new Error('waiting for the first client to disconnect') |
| 199 | + }) |
| 200 | + expect(lifecycleEvents[3]).toEqual({ type: 'disconnected', sessionId: lifecycleEvents[0]!.sessionId }) |
| 201 | + |
| 202 | + const reconnectedClient = connectWsClient(`ws://${host}:${port}/__ws`) |
| 203 | + clients.push(reconnectedClient) |
| 204 | + await reconnectedClient.$call(HUB_EVENTS.rpc.docksPanelState, true) |
| 205 | + |
| 206 | + expect(lifecycleEvents[4]).toMatchObject({ type: 'connected', open: true }) |
| 207 | + expect([lifecycleEvents[0]!.sessionId, lifecycleEvents[2]!.sessionId]).not.toContain(lifecycleEvents[4]!.sessionId) |
| 208 | + } |
| 209 | + finally { |
| 210 | + for (const client of clients) |
| 211 | + client.close() |
| 212 | + await hub.close() |
| 213 | + } |
| 214 | + }) |
| 215 | + |
155 | 216 | it('ui slot: viewer owns the root, embedded.js serves the entry, discovery still wins', async () => { |
156 | 217 | const viewerDist = makeDist('<!doctype html><title>hub viewer</title>') |
157 | 218 | const embeddedDir = mkdtempSync(join(tmpdir(), 'hub-embedded-')) |
|
0 commit comments