diff --git a/web/src/multiview.test.ts b/web/src/multiview.test.ts index 36e2a09..a08bf79 100644 --- a/web/src/multiview.test.ts +++ b/web/src/multiview.test.ts @@ -202,3 +202,83 @@ describe("multiview openDraft (#62)", () => { expect(leaf.draft?.command).toBe("claude"); }); }); + +describe("multiview pane-drag swap (#73)", () => { + beforeEach(() => { + stubFetch(); + document.body.replaceChildren(); + }); + + afterEach(() => { + vi.unstubAllGlobals(); + vi.restoreAllMocks(); + }); + + /** Drag `sourcePane`'s title onto `targetPane`'s center, as the real pane + * drag handler does — modelled after the pointer sequence in runDrag()/ + * updateDock() in multiview.ts, since neither is exposed on MultiView. */ + async function dragPaneOntoCenter(sourcePane: HTMLElement, targetPane: HTMLElement): Promise { + // The real drag-start gate requires the pointerdown target to sit inside + // an `.attach-header` (attach.ts's session title bar). Draft panes mount + // no such element, so stand one in for the gate check itself. + const marker = document.createElement("div"); + marker.className = "attach-header"; + sourcePane.appendChild(marker); + + vi.spyOn(targetPane, "getBoundingClientRect").mockReturnValue({ + left: 100, + top: 0, + width: 100, + height: 100, + right: 200, + bottom: 100, + x: 100, + y: 0, + toJSON() {}, + } as DOMRect); + (document as unknown as { elementsFromPoint: (x: number, y: number) => Element[] }).elementsFromPoint = () => [ + targetPane, + ]; + + marker.dispatchEvent( + new PointerEvent("pointerdown", { clientX: 0, clientY: 0, bubbles: true, cancelable: true }), + ); + window.dispatchEvent( + new PointerEvent("pointermove", { clientX: 150, clientY: 50, bubbles: true, cancelable: true }), + ); + window.dispatchEvent(new PointerEvent("pointerup", { clientX: 150, clientY: 50, bubbles: true, cancelable: true })); + await flush(); + } + + it("moves each draft's form along with its data when two draft panes are swapped", async () => { + const layout = emptyLayout(); + const tab = layout.tabs[0]; + const leaf1 = splitLeaf(tab, "", null, "row", true); + const leaf2 = splitLeaf(tab, leaf1, null, "row", true); + const mv = makeMv(layout); + document.body.appendChild(mv.el); + mv.render(); + await flush(); + + const paneOf = (leafId: string) => mv.el.querySelector(`[data-leaf-id="${leafId}"]`) as HTMLElement; + const nameInputOf = (leafId: string) => paneOf(leafId).querySelector("input[name=name]") as HTMLInputElement; + + nameInputOf(leaf1).value = "west"; + nameInputOf(leaf1).dispatchEvent(new Event("input")); + nameInputOf(leaf2).value = "east"; + nameInputOf(leaf2).dispatchEvent(new Event("input")); + await flush(); + expect(leaves(tab.root).find((l) => l.id === leaf1)?.draft?.name).toBe("west"); + expect(leaves(tab.root).find((l) => l.id === leaf2)?.draft?.name).toBe("east"); + + await dragPaneOntoCenter(paneOf(leaf1), paneOf(leaf2)); + + // The leaf data swaps (already correct pre-fix)... + expect(leaves(tab.root).find((l) => l.id === leaf1)?.draft?.name).toBe("east"); + expect(leaves(tab.root).find((l) => l.id === leaf2)?.draft?.name).toBe("west"); + // ...and the mounted forms must follow, not stay behind showing the old + // pane's values (#73). + expect(nameInputOf(leaf1).value).toBe("east"); + expect(nameInputOf(leaf2).value).toBe("west"); + }); +}); diff --git a/web/src/multiview.ts b/web/src/multiview.ts index cfd00b9..3ea9a94 100644 --- a/web/src/multiview.ts +++ b/web/src/multiview.ts @@ -701,6 +701,16 @@ export function createMultiView(opts: MultiViewOptions): MultiView { const dragged = findLeaf(tab.root, self)!; [dragged.sessionId, target.sessionId] = [target.sessionId, dragged.sessionId]; [dragged.draft, target.draft] = [target.draft, dragged.draft]; + // The leaf data moved but each pane's mounted form is still keyed to + // its old leaf (#73) — tear both down like openDraft's reuse case + // (#72) so render() remounts fresh forms seeded from the swapped data. + for (const id of [dragged.id, target.id]) { + const stale = draftForms.get(id); + if (stale) { + stale.destroy(); + draftForms.delete(id); + } + } tab.focusedLeafId = target.id; } else if (sessionId) { const loc = locateSession(layout, sessionId);