-
Notifications
You must be signed in to change notification settings - Fork 433
Sync node help with selection and add watcher tests #7105
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
a436c94
4c3718f
82e13d4
00aaa14
19e1ede
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,9 @@ | ||
| import { flushPromises, mount } from '@vue/test-utils' | ||
| import type { VueWrapper } from '@vue/test-utils' | ||
| import { createPinia, setActivePinia } from 'pinia' | ||
| import { beforeEach, describe, expect, test, vi } from 'vitest' | ||
| import { type Ref, ref } from 'vue' | ||
| import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest' | ||
| import { ref } from 'vue' | ||
| import type { Ref } from 'vue' | ||
|
|
||
| import { useSelectionState } from '@/composables/graph/useSelectionState' | ||
| import { useNodeLibrarySidebarTab } from '@/composables/sidebarTabs/useNodeLibrarySidebarTab' | ||
|
|
@@ -78,9 +81,50 @@ const mockComment = { type: 'comment', isNode: false } | |
| const mockConnection = { type: 'connection', isNode: false } | ||
|
|
||
| describe('useSelectionState', () => { | ||
| const mountedWrappers: VueWrapper[] = [] | ||
| // Mock store instances | ||
| let mockSelectedItems: Ref<MockedItem[]> | ||
|
|
||
| const mountSelectionStateComposable = () => { | ||
| let selectionState: ReturnType<typeof useSelectionState> | ||
| const wrapper = mount({ | ||
| template: '<div />', | ||
| setup() { | ||
| selectionState = useSelectionState() | ||
| return {} | ||
| } | ||
| }) | ||
| mountedWrappers.push(wrapper) | ||
| return { selectionState: selectionState! } | ||
| } | ||
|
|
||
| const mountHelpSyncHarness = () => { | ||
| const nodeA = createTestNode({ type: 'NodeA' }) | ||
| const nodeB = createTestNode({ type: 'NodeB' }) | ||
|
|
||
| const wrapper = mount({ | ||
| template: ` | ||
| <div> | ||
| <button data-test="select-a" @click="select(nodeA)">A</button> | ||
| <button data-test="select-b" @click="select(nodeB)">B</button> | ||
| </div> | ||
| `, | ||
| setup() { | ||
| const select = (node: TestNode) => { | ||
| mockSelectedItems.value = [node] | ||
| } | ||
|
|
||
| useSelectionState() | ||
|
|
||
| return { select, nodeA, nodeB } | ||
| } | ||
| }) | ||
|
|
||
| mountedWrappers.push(wrapper) | ||
|
|
||
| return { wrapper, nodeA, nodeB } | ||
| } | ||
|
|
||
| beforeEach(() => { | ||
| vi.clearAllMocks() | ||
| setActivePinia(createPinia()) | ||
|
|
@@ -181,10 +225,14 @@ describe('useSelectionState', () => { | |
| ) | ||
| }) | ||
|
|
||
| afterEach(() => { | ||
| mountedWrappers.splice(0).forEach((wrapper) => wrapper.unmount()) | ||
| }) | ||
|
|
||
| describe('Selection Detection', () => { | ||
| test('should return false when nothing selected', () => { | ||
| const { hasAnySelection } = useSelectionState() | ||
| expect(hasAnySelection.value).toBe(false) | ||
| const { selectionState } = mountSelectionStateComposable() | ||
| expect(selectionState.hasAnySelection.value).toBe(false) | ||
| }) | ||
|
|
||
| test('should return true when items selected', () => { | ||
|
|
@@ -193,8 +241,8 @@ describe('useSelectionState', () => { | |
| const node2 = createTestNode() | ||
| mockSelectedItems.value = [node1, node2] | ||
|
|
||
| const { hasAnySelection } = useSelectionState() | ||
| expect(hasAnySelection.value).toBe(true) | ||
| const { selectionState } = mountSelectionStateComposable() | ||
| expect(selectionState.hasAnySelection.value).toBe(true) | ||
| }) | ||
| }) | ||
|
|
||
|
|
@@ -204,9 +252,9 @@ describe('useSelectionState', () => { | |
| const graphNode = createTestNode() | ||
| mockSelectedItems.value = [graphNode, mockComment, mockConnection] | ||
|
|
||
| const { selectedNodes } = useSelectionState() | ||
| expect(selectedNodes.value).toHaveLength(1) | ||
| expect(selectedNodes.value[0]).toEqual(graphNode) | ||
| const { selectionState } = mountSelectionStateComposable() | ||
| expect(selectionState.selectedNodes.value).toHaveLength(1) | ||
| expect(selectionState.selectedNodes.value[0]).toEqual(graphNode) | ||
| }) | ||
| }) | ||
|
|
||
|
|
@@ -216,8 +264,8 @@ describe('useSelectionState', () => { | |
| const bypassedNode = createTestNode({ mode: LGraphEventMode.BYPASS }) | ||
| mockSelectedItems.value = [bypassedNode] | ||
|
|
||
| const { selectedNodes } = useSelectionState() | ||
| const isBypassed = selectedNodes.value.some( | ||
| const { selectionState } = mountSelectionStateComposable() | ||
| const isBypassed = selectionState.selectedNodes.value.some( | ||
| (n) => n.mode === LGraphEventMode.BYPASS | ||
| ) | ||
| expect(isBypassed).toBe(true) | ||
|
|
@@ -229,42 +277,93 @@ describe('useSelectionState', () => { | |
| const collapsedNode = createTestNode({ flags: { collapsed: true } }) | ||
| mockSelectedItems.value = [pinnedNode, collapsedNode] | ||
|
|
||
| const { selectedNodes } = useSelectionState() | ||
| const isPinned = selectedNodes.value.some((n) => n.pinned === true) | ||
| const isCollapsed = selectedNodes.value.some( | ||
| const { selectionState } = mountSelectionStateComposable() | ||
| const isPinned = selectionState.selectedNodes.value.some( | ||
| (n) => n.pinned === true | ||
| ) | ||
| const isCollapsed = selectionState.selectedNodes.value.some( | ||
| (n) => n.flags?.collapsed === true | ||
| ) | ||
| const isBypassed = selectedNodes.value.some( | ||
| const isBypassed = selectionState.selectedNodes.value.some( | ||
| (n) => n.mode === LGraphEventMode.BYPASS | ||
| ) | ||
| expect(isPinned).toBe(true) | ||
| expect(isCollapsed).toBe(true) | ||
| expect(isBypassed).toBe(false) | ||
| }) | ||
|
|
||
| test('should provide non-reactive state computation', () => { | ||
| test('should provide non-reactive state computation', async () => { | ||
| // Update the mock data before creating the composable | ||
| const node = createTestNode({ pinned: true }) | ||
| mockSelectedItems.value = [node] | ||
|
|
||
| const { selectedNodes } = useSelectionState() | ||
| const isPinned = selectedNodes.value.some((n) => n.pinned === true) | ||
| const isCollapsed = selectedNodes.value.some( | ||
| const { selectionState } = mountSelectionStateComposable() | ||
| const isPinned = selectionState.selectedNodes.value.some( | ||
| (n) => n.pinned === true | ||
| ) | ||
| const isCollapsed = selectionState.selectedNodes.value.some( | ||
| (n) => n.flags?.collapsed === true | ||
| ) | ||
| const isBypassed = selectedNodes.value.some( | ||
| const isBypassed = selectionState.selectedNodes.value.some( | ||
| (n) => n.mode === LGraphEventMode.BYPASS | ||
| ) | ||
|
|
||
| expect(isPinned).toBe(true) | ||
| expect(isCollapsed).toBe(false) | ||
| expect(isBypassed).toBe(false) | ||
|
|
||
| // Test with empty selection using new composable instance | ||
| // Test with empty selection using updated selection | ||
| mockSelectedItems.value = [] | ||
| const { selectedNodes: newSelectedNodes } = useSelectionState() | ||
| const newIsPinned = newSelectedNodes.value.some((n) => n.pinned === true) | ||
| await flushPromises() | ||
|
|
||
| const newIsPinned = selectionState.selectedNodes.value.some( | ||
| (n) => n.pinned === true | ||
| ) | ||
| expect(newIsPinned).toBe(false) | ||
| }) | ||
| }) | ||
|
|
||
| describe('Help Sync', () => { | ||
| beforeEach(() => { | ||
| const nodeDefStore = useNodeDefStore() as any | ||
| nodeDefStore.fromLGraphNode.mockImplementation((node: TestNode) => ({ | ||
| nodePath: node.type | ||
| })) | ||
| }) | ||
|
|
||
| test('opens help for newly selected node when help is open', async () => { | ||
| const nodeHelpStore = useNodeHelpStore() as any | ||
| nodeHelpStore.isHelpOpen = true | ||
| nodeHelpStore.currentHelpNode = { nodePath: 'NodeA' } | ||
|
|
||
| const { wrapper } = mountHelpSyncHarness() | ||
|
|
||
| await wrapper.find('[data-test="select-a"]').trigger('click') | ||
| await wrapper.find('[data-test="select-b"]').trigger('click') | ||
| await flushPromises() | ||
|
|
||
| expect(nodeHelpStore.openHelp).toHaveBeenCalledWith({ | ||
| nodePath: 'NodeB' | ||
| }) | ||
| }) | ||
|
|
||
| test('does not reopen help when selection is unchanged or closed', async () => { | ||
| const nodeHelpStore = useNodeHelpStore() as any | ||
|
|
||
| const { wrapper } = mountHelpSyncHarness() | ||
|
|
||
| // Help closed -> no call | ||
| nodeHelpStore.isHelpOpen = false | ||
| await wrapper.find('[data-test="select-a"]').trigger('click') | ||
| await flushPromises() | ||
| expect(nodeHelpStore.openHelp).not.toHaveBeenCalled() | ||
|
|
||
| // Help open but same node -> no call | ||
| nodeHelpStore.isHelpOpen = true | ||
| nodeHelpStore.currentHelpNode = { nodePath: 'NodeA' } | ||
| await wrapper.find('[data-test="select-a"]').trigger('click') | ||
| await flushPromises() | ||
| expect(nodeHelpStore.openHelp).not.toHaveBeenCalled() | ||
| }) | ||
|
Comment on lines
+350
to
+367
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Major: Test logic doesn't properly verify the guard condition. The second scenario (lines 362-366) doesn't properly test the guard condition. It clicks To properly test the guard condition, the selection should change and then return to the same node: test('does not reopen help when selection is unchanged or closed', async () => {
const nodeHelpStore = useNodeHelpStore() as any
const { wrapper } = mountHelpSyncHarness()
// Help closed -> no call
- nodeHelpStore.isHelpOpen = false
+ mockIsHelpOpen.value = false
await wrapper.find('[data-test="select-a"]').trigger('click')
await flushPromises()
expect(nodeHelpStore.openHelp).not.toHaveBeenCalled()
- // Help open but same node -> no call
- nodeHelpStore.isHelpOpen = true
- nodeHelpStore.currentHelpNode = { nodePath: 'NodeA' }
+ // Help open, select different node, then reselect same node -> no call for reselection
+ mockIsHelpOpen.value = true
+ mockCurrentHelpNode.value = { nodePath: 'NodeA' }
await wrapper.find('[data-test="select-a"]').trigger('click')
+ await flushPromises()
+
+ // Clear previous calls
+ nodeHelpStore.openHelp.mockClear()
+
+ // Select different node, then back to NodeA - guard should prevent second call
+ await wrapper.find('[data-test="select-b"]').trigger('click')
+ await flushPromises()
+ mockCurrentHelpNode.value = { nodePath: 'NodeB' }
+
+ await wrapper.find('[data-test="select-a"]').trigger('click')
await flushPromises()
expect(nodeHelpStore.openHelp).not.toHaveBeenCalled()
})
|
||
| }) | ||
|
Comment on lines
+326
to
+368
|
||
| }) | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The watcher callback can receive
nullas thedefparameter when help is open and the selection changes to multiple nodes or no nodes (sincenodeDef.valuereturnsnullin those cases). This would result in callingnodeHelpStore.openHelp(null), which is likely incorrect.Add a null check before calling
openHelp: