Skip to content

Commit db7dd55

Browse files
authored
fix(canvas): show body menu on empty canvas right-click (CoreBunch#139)
1 parent 49de5d4 commit db7dd55

2 files changed

Lines changed: 129 additions & 1 deletion

File tree

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
import { afterEach, beforeEach, describe, expect, it } from 'bun:test'
2+
import React from 'react'
3+
import { act, cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react'
4+
import { DndContext } from '@dnd-kit/core'
5+
import { CanvasRoot } from '@site/canvas/CanvasRoot'
6+
import { useEditorStore } from '@site/store/store'
7+
import { makeNode, makePage, makeSite } from '../fixtures'
8+
import {
9+
waitForCanvasFrameDocument,
10+
waitForCanvasNodeInFrame,
11+
} from './iframeCanvasQuery'
12+
import '@modules/base'
13+
14+
afterEach(() => {
15+
cleanup()
16+
localStorage.clear()
17+
})
18+
19+
beforeEach(() => {
20+
localStorage.clear()
21+
useEditorStore.setState({
22+
site: null,
23+
activePageId: null,
24+
activeDocument: null,
25+
activeBreakpointId: 'desktop',
26+
canvasView: 'design',
27+
selectedNodeId: null,
28+
selectedNodeIds: [],
29+
hoveredNodeId: null,
30+
hoveredBreakpointId: null,
31+
clipboardEntry: null,
32+
previewClassAssignment: null,
33+
propertiesPanel: { collapsed: false, x: 0, y: 0, width: 360 },
34+
propertiesPanelMode: 'docked',
35+
_historyPast: [],
36+
_historyFuture: [],
37+
canUndo: false,
38+
canRedo: false,
39+
hasUnsavedChanges: false,
40+
} as Parameters<typeof useEditorStore.setState>[0])
41+
})
42+
43+
describe('canvas body context menu', () => {
44+
it('opens the root insert menu from an empty iframe body background', async () => {
45+
seedClipboardAndActivateEmptyPage()
46+
47+
render(
48+
<DndContext>
49+
<CanvasRoot />
50+
</DndContext>,
51+
)
52+
53+
const desktopDoc = await waitForCanvasFrameDocument('desktop')
54+
await waitForCanvasNodeInFrame('desktop', 'empty-root')
55+
56+
act(() => {
57+
fireEvent.contextMenu(desktopDoc.body, { clientX: 80, clientY: 96 })
58+
})
59+
60+
await waitFor(() => {
61+
expect(screen.getByRole('menu', { name: 'Node options' })).toBeDefined()
62+
})
63+
expect(screen.getByRole('menuitem', { name: /insert module here/i })).toBeDefined()
64+
const pasteItem = screen.getByRole('menuitem', { name: /^paste$/i })
65+
expect(screen.getByRole('menuitem', { name: /paste html here/i })).toBeDefined()
66+
67+
const state = useEditorStore.getState()
68+
expect(state.selectedNodeId).toBe('empty-root')
69+
expect(state.selectedNodeIds).toEqual(['empty-root'])
70+
71+
fireEvent.click(pasteItem)
72+
73+
const page = useEditorStore.getState().site?.pages.find((candidate) => candidate.id === 'page-empty')
74+
const root = page?.nodes['empty-root']
75+
expect(root?.children.length).toBe(1)
76+
const pastedId = root?.children[0]
77+
const pasted = pastedId ? page?.nodes[pastedId] : null
78+
expect(pasted?.moduleId).toBe('base.text')
79+
expect(pastedId).not.toBe('clip-text')
80+
})
81+
})
82+
83+
function seedClipboardAndActivateEmptyPage() {
84+
const sourcePage = makePage({
85+
id: 'page-source',
86+
title: 'Source',
87+
slug: 'source',
88+
rootNodeId: 'source-root',
89+
nodes: {
90+
'source-root': makeNode({ id: 'source-root', moduleId: 'base.body', children: ['clip-text'] }),
91+
'clip-text': makeNode({
92+
id: 'clip-text',
93+
moduleId: 'base.text',
94+
props: { text: 'Copied text', tag: 'p' },
95+
}),
96+
},
97+
})
98+
const emptyPage = makePage({
99+
id: 'page-empty',
100+
title: 'Empty',
101+
slug: 'empty',
102+
rootNodeId: 'empty-root',
103+
nodes: {
104+
'empty-root': makeNode({ id: 'empty-root', moduleId: 'base.body', children: [] }),
105+
},
106+
})
107+
108+
act(() => {
109+
useEditorStore.setState({
110+
site: makeSite({ pages: [sourcePage, emptyPage] }),
111+
activePageId: sourcePage.id,
112+
activeDocument: null,
113+
activeBreakpointId: 'desktop',
114+
selectedNodeId: null,
115+
selectedNodeIds: [],
116+
} as Parameters<typeof useEditorStore.setState>[0])
117+
})
118+
119+
expect(useEditorStore.getState().cutNode('clip-text')).toBe(true)
120+
act(() => {
121+
useEditorStore.setState({
122+
activePageId: emptyPage.id,
123+
selectedNodeId: null,
124+
selectedNodeIds: [],
125+
} as Parameters<typeof useEditorStore.setState>[0])
126+
})
127+
expect(useEditorStore.getState().clipboardEntry).not.toBeNull()
128+
}

src/admin/pages/site/canvas/CanvasRoot.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -282,7 +282,7 @@ export function CanvasRoot({ editable = true }: CanvasRootProps) {
282282
// `position: fixed` — it needs editor-document coordinates, which
283283
// `clientPointToEditorDoc` produces by adding the iframe's outer rect
284284
// (scaled by the canvas zoom).
285-
const point = clientPointToEditorDoc(e.nativeEvent)
285+
const point = clientPointToEditorDoc(e.nativeEvent ?? e)
286286
contextMenu.open({ x: point.x, y: point.y, nodeId })
287287
}
288288

0 commit comments

Comments
 (0)