test: add comprehensive unit tests for disableDnd functionality

- Add unit tests for Tab draggable attribute with disableDnd option
- Add unit tests for VoidContainer draggable attribute with disableDnd option
- Add unit tests for updateDragAndDropState methods in all components
- Add integration tests for updateOptions with disableDnd changes
- Fix existing test mocks to include required options property

Tests cover both initial state and dynamic updates when option changes.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
mathuo 2025-07-16 22:33:21 +01:00
parent ad9f884847
commit ac6154196e
No known key found for this signature in database
GPG Key ID: C6EEDEFD6CA07281
5 changed files with 335 additions and 94 deletions

View File

@ -12,12 +12,14 @@ import { fromPartial } from '@total-typescript/shoehorn';
describe('tab', () => {
test('that empty tab has inactive-tab class', () => {
const accessorMock = jest.fn();
const accessor = fromPartial<DockviewComponent>({
options: {}
});
const groupMock = jest.fn();
const cut = new Tab(
{ id: 'panelId' } as IDockviewPanel,
new accessorMock(),
accessor,
new groupMock()
);
@ -25,12 +27,14 @@ describe('tab', () => {
});
test('that active tab has active-tab class', () => {
const accessorMock = jest.fn();
const accessor = fromPartial<DockviewComponent>({
options: {}
});
const groupMock = jest.fn();
const cut = new Tab(
{ id: 'panelId' } as IDockviewPanel,
new accessorMock(),
accessor,
new groupMock()
);
@ -42,26 +46,20 @@ describe('tab', () => {
});
test('that an external event does not render a drop target and calls through to the group model', () => {
const accessorMock = jest.fn<Partial<DockviewComponent>, []>(() => {
return {
id: 'testcomponentid',
};
const accessor = fromPartial<DockviewComponent>({
id: 'testcomponentid',
options: {}
});
const groupView = fromPartial<DockviewGroupPanelModel>({
canDisplayOverlay: jest.fn(),
});
const groupPanelMock = jest.fn<Partial<DockviewGroupPanel>, []>(() => {
return {
id: 'testgroupid',
model: groupView,
};
const groupPanel = fromPartial<DockviewGroupPanel>({
id: 'testgroupid',
model: groupView,
});
const accessor = new accessorMock() as DockviewComponent;
const groupPanel = new groupPanelMock() as DockviewGroupPanel;
const cut = new Tab(
{ id: 'panelId' } as IDockviewPanel,
accessor,
@ -86,26 +84,20 @@ describe('tab', () => {
});
test('that if you drag over yourself a drop target is shown', () => {
const accessorMock = jest.fn<Partial<DockviewComponent>, []>(() => {
return {
id: 'testcomponentid',
};
const accessor = fromPartial<DockviewComponent>({
id: 'testcomponentid',
options: {}
});
const groupView = fromPartial<DockviewGroupPanelModel>({
canDisplayOverlay: jest.fn(),
});
const groupPanelMock = jest.fn<Partial<DockviewGroupPanel>, []>(() => {
return {
id: 'testgroupid',
model: groupView,
};
const groupPanel = fromPartial<DockviewGroupPanel>({
id: 'testgroupid',
model: groupView,
});
const accessor = new accessorMock() as DockviewComponent;
const groupPanel = new groupPanelMock() as DockviewGroupPanel;
const cut = new Tab(
{ id: 'panel1' } as IDockviewPanel,
accessor,
@ -135,30 +127,19 @@ describe('tab', () => {
});
test('that if you drag over another tab a drop target is shown', () => {
const accessorMock = jest.fn<Partial<DockviewComponent>, []>(() => {
return {
id: 'testcomponentid',
};
});
const groupviewMock = jest.fn<Partial<DockviewGroupPanelModel>, []>(
() => {
return {
canDisplayOverlay: jest.fn(),
};
}
);
const groupView = new groupviewMock() as DockviewGroupPanelModel;
const groupPanelMock = jest.fn<Partial<DockviewGroupPanel>, []>(() => {
return {
id: 'testgroupid',
model: groupView,
};
const accessor = fromPartial<DockviewComponent>({
id: 'testcomponentid',
options: {}
});
const accessor = new accessorMock() as DockviewComponent;
const groupPanel = new groupPanelMock() as DockviewGroupPanel;
const groupView = fromPartial<DockviewGroupPanelModel>({
canDisplayOverlay: jest.fn(),
});
const groupPanel = fromPartial<DockviewGroupPanel>({
id: 'testgroupid',
model: groupView,
});
const cut = new Tab(
{ id: 'panel1' } as IDockviewPanel,
@ -189,30 +170,19 @@ describe('tab', () => {
});
test('that dropping on a tab with the same id but from a different component should not render a drop over and call through to the group model', () => {
const accessorMock = jest.fn<Partial<DockviewComponent>, []>(() => {
return {
id: 'testcomponentid',
};
});
const groupviewMock = jest.fn<Partial<DockviewGroupPanelModel>, []>(
() => {
return {
canDisplayOverlay: jest.fn(),
};
}
);
const groupView = new groupviewMock() as DockviewGroupPanelModel;
const groupPanelMock = jest.fn<Partial<DockviewGroupPanel>, []>(() => {
return {
id: 'testgroupid',
model: groupView,
};
const accessor = fromPartial<DockviewComponent>({
id: 'testcomponentid',
options: {}
});
const accessor = new accessorMock() as DockviewComponent;
const groupPanel = new groupPanelMock() as DockviewGroupPanel;
const groupView = fromPartial<DockviewGroupPanelModel>({
canDisplayOverlay: jest.fn(),
});
const groupPanel = fromPartial<DockviewGroupPanel>({
id: 'testgroupid',
model: groupView,
});
const cut = new Tab(
{ id: 'panel1' } as IDockviewPanel,
@ -249,30 +219,19 @@ describe('tab', () => {
});
test('that dropping on a tab from a different component should not render a drop over and call through to the group model', () => {
const accessorMock = jest.fn<Partial<DockviewComponent>, []>(() => {
return {
id: 'testcomponentid',
};
});
const groupviewMock = jest.fn<Partial<DockviewGroupPanelModel>, []>(
() => {
return {
canDisplayOverlay: jest.fn(),
};
}
);
const groupView = new groupviewMock() as DockviewGroupPanelModel;
const groupPanelMock = jest.fn<Partial<DockviewGroupPanel>, []>(() => {
return {
id: 'testgroupid',
model: groupView,
};
const accessor = fromPartial<DockviewComponent>({
id: 'testcomponentid',
options: {}
});
const accessor = new accessorMock() as DockviewComponent;
const groupPanel = new groupPanelMock() as DockviewGroupPanel;
const groupView = fromPartial<DockviewGroupPanelModel>({
canDisplayOverlay: jest.fn(),
});
const groupPanel = fromPartial<DockviewGroupPanel>({
id: 'testgroupid',
model: groupView,
});
const cut = new Tab(
{ id: 'panel1' } as IDockviewPanel,
@ -307,4 +266,77 @@ describe('tab', () => {
cut.element.getElementsByClassName('dv-drop-target-dropzone').length
).toBe(0);
});
describe('disableDnd option', () => {
test('that tab is draggable by default (disableDnd not set)', () => {
const accessor = fromPartial<DockviewComponent>({
options: {}
});
const groupMock = jest.fn();
const cut = new Tab(
{ id: 'panelId' } as IDockviewPanel,
accessor,
new groupMock()
);
expect(cut.element.draggable).toBe(true);
});
test('that tab is draggable when disableDnd is false', () => {
const accessor = fromPartial<DockviewComponent>({
options: { disableDnd: false }
});
const groupMock = jest.fn();
const cut = new Tab(
{ id: 'panelId' } as IDockviewPanel,
accessor,
new groupMock()
);
expect(cut.element.draggable).toBe(true);
});
test('that tab is not draggable when disableDnd is true', () => {
const accessor = fromPartial<DockviewComponent>({
options: { disableDnd: true }
});
const groupMock = jest.fn();
const cut = new Tab(
{ id: 'panelId' } as IDockviewPanel,
accessor,
new groupMock()
);
expect(cut.element.draggable).toBe(false);
});
test('that updateDragAndDropState updates draggable attribute based on disableDnd option', () => {
const options = { disableDnd: false };
const accessor = fromPartial<DockviewComponent>({
options
});
const groupMock = jest.fn();
const cut = new Tab(
{ id: 'panelId' } as IDockviewPanel,
accessor,
new groupMock()
);
expect(cut.element.draggable).toBe(true);
// Simulate option change
options.disableDnd = true;
cut.updateDragAndDropState();
expect(cut.element.draggable).toBe(false);
// Change back
options.disableDnd = false;
cut.updateDragAndDropState();
expect(cut.element.draggable).toBe(true);
});
});
});

View File

@ -63,4 +63,33 @@ describe('tabs', () => {
).toBe(0);
});
});
describe('updateDragAndDropState', () => {
test('that updateDragAndDropState calls updateDragAndDropState on all tabs', () => {
const cut = new Tabs(
fromPartial<DockviewGroupPanel>({}),
fromPartial<DockviewComponent>({
options: {},
}),
{
showTabsOverflowControl: true,
}
);
// Mock tab to verify the method is called
const mockTab1 = { updateDragAndDropState: jest.fn() };
const mockTab2 = { updateDragAndDropState: jest.fn() };
// Add mock tabs to the internal tabs array
(cut as any)._tabs = [
{ value: mockTab1 },
{ value: mockTab2 }
];
cut.updateDragAndDropState();
expect(mockTab1.updateDragAndDropState).toHaveBeenCalledTimes(1);
expect(mockTab2.updateDragAndDropState).toHaveBeenCalledTimes(1);
});
});
});

View File

@ -864,4 +864,34 @@ describe('tabsContainer', () => {
cut.closePanel(panel2);
expect(cut.element.classList.contains('dv-single-tab')).toBeFalsy();
});
describe('updateDragAndDropState', () => {
test('that updateDragAndDropState calls updateDragAndDropState on tabs and voidContainer', () => {
const accessor = fromPartial<DockviewComponent>({
onDidAddPanel: jest.fn(),
onDidRemovePanel: jest.fn(),
options: {},
onDidOptionsChange: jest.fn(),
});
const groupPanel = fromPartial<DockviewGroupPanel>({
id: 'testgroupid',
model: fromPartial<DockviewGroupPanelModel>({}),
});
const cut = new TabsContainer(accessor, groupPanel);
// Mock the tabs and voidContainer to verify methods are called
const mockTabs = { updateDragAndDropState: jest.fn() };
const mockVoidContainer = { updateDragAndDropState: jest.fn() };
(cut as any).tabs = mockTabs;
(cut as any).voidContainer = mockVoidContainer;
cut.updateDragAndDropState();
expect(mockTabs.updateDragAndDropState).toHaveBeenCalledTimes(1);
expect(mockVoidContainer.updateDragAndDropState).toHaveBeenCalledTimes(1);
});
});
});

View File

@ -8,6 +8,7 @@ describe('voidContainer', () => {
test('that `pointerDown` triggers activation', () => {
const accessor = fromPartial<DockviewComponent>({
doSetGroupActive: jest.fn(),
options: {}
});
const group = fromPartial<DockviewGroupPanel>({});
const cut = new VoidContainer(accessor, group);
@ -17,4 +18,57 @@ describe('voidContainer', () => {
fireEvent.pointerDown(cut.element);
expect(accessor.doSetGroupActive).toHaveBeenCalledWith(group);
});
describe('disableDnd option', () => {
test('that void container is draggable by default (disableDnd not set)', () => {
const accessor = fromPartial<DockviewComponent>({
options: {}
});
const group = fromPartial<DockviewGroupPanel>({});
const cut = new VoidContainer(accessor, group);
expect(cut.element.draggable).toBe(true);
});
test('that void container is draggable when disableDnd is false', () => {
const accessor = fromPartial<DockviewComponent>({
options: { disableDnd: false }
});
const group = fromPartial<DockviewGroupPanel>({});
const cut = new VoidContainer(accessor, group);
expect(cut.element.draggable).toBe(true);
});
test('that void container is not draggable when disableDnd is true', () => {
const accessor = fromPartial<DockviewComponent>({
options: { disableDnd: true }
});
const group = fromPartial<DockviewGroupPanel>({});
const cut = new VoidContainer(accessor, group);
expect(cut.element.draggable).toBe(false);
});
test('that updateDragAndDropState updates draggable attribute based on disableDnd option', () => {
const options = { disableDnd: false };
const accessor = fromPartial<DockviewComponent>({
options
});
const group = fromPartial<DockviewGroupPanel>({});
const cut = new VoidContainer(accessor, group);
expect(cut.element.draggable).toBe(true);
// Simulate option change
options.disableDnd = true;
cut.updateDragAndDropState();
expect(cut.element.draggable).toBe(false);
// Change back
options.disableDnd = false;
cut.updateDragAndDropState();
expect(cut.element.draggable).toBe(true);
});
});
});

View File

@ -144,6 +144,102 @@ describe('dockviewComponent', () => {
);
});
describe('disableDnd option integration', () => {
test('that updateOptions with disableDnd updates all tabs and void containers', () => {
dockview = new DockviewComponent(container, {
createComponent(options) {
switch (options.name) {
case 'default':
return new PanelContentPartTest(
options.id,
options.name
);
default:
throw new Error(`unsupported`);
}
},
disableDnd: false,
});
// Add some panels to create tabs
const panel1 = dockview.addPanel({
id: 'panel1',
component: 'default',
});
const panel2 = dockview.addPanel({
id: 'panel2',
component: 'default',
});
// Get all tab elements and void containers
const tabElements = Array.from(dockview.element.querySelectorAll('.dv-tab')) as HTMLElement[];
const voidContainers = Array.from(dockview.element.querySelectorAll('.dv-void-container')) as HTMLElement[];
// Initially tabs should be draggable (disableDnd: false)
tabElements.forEach(tab => {
expect(tab.draggable).toBe(true);
});
voidContainers.forEach(container => {
expect(container.draggable).toBe(true);
});
// Update options to disable DnD
dockview.updateOptions({ disableDnd: true });
// Now tabs should not be draggable
tabElements.forEach(tab => {
expect(tab.draggable).toBe(false);
});
voidContainers.forEach(container => {
expect(container.draggable).toBe(false);
});
// Update options to enable DnD again
dockview.updateOptions({ disableDnd: false });
// Tabs should be draggable again
tabElements.forEach(tab => {
expect(tab.draggable).toBe(true);
});
voidContainers.forEach(container => {
expect(container.draggable).toBe(true);
});
});
test('that new tabs respect current disableDnd option when added after option change', () => {
dockview = new DockviewComponent(container, {
createComponent(options) {
switch (options.name) {
case 'default':
return new PanelContentPartTest(
options.id,
options.name
);
default:
throw new Error(`unsupported`);
}
},
disableDnd: false,
});
// Set disableDnd to true
dockview.updateOptions({ disableDnd: true });
// Add a panel after the option change
const panel = dockview.addPanel({
id: 'panel1',
component: 'default',
});
// New tab should not be draggable
const tabElement = dockview.element.querySelector('.dv-tab') as HTMLElement;
const voidContainer = dockview.element.querySelector('.dv-void-container') as HTMLElement;
expect(tabElement.draggable).toBe(false);
expect(voidContainer.draggable).toBe(false);
});
});
describe('memory leakage', () => {
beforeEach(() => {
window.open = () => setupMockWindow();