From 321f78ec0eb68346e61bc0cb2882866bc6567e95 Mon Sep 17 00:00:00 2001 From: mathuo <6710312+mathuo@users.noreply.github.com> Date: Mon, 13 Feb 2023 16:44:21 +0700 Subject: [PATCH] tests: add tests --- .../src/__tests__/dnd/droptarget.spec.ts | 133 +++++- .../dockview/dockviewComponent.spec.ts | 379 +++++++++++++++++- packages/dockview/src/dnd/droptarget.ts | 36 +- .../src/dockview/dockviewComponent.ts | 12 +- packages/dockview/src/gridview/gridview.ts | 10 +- 5 files changed, 546 insertions(+), 24 deletions(-) diff --git a/packages/dockview/src/__tests__/dnd/droptarget.spec.ts b/packages/dockview/src/__tests__/dnd/droptarget.spec.ts index 8e21fe445..3f79aa220 100644 --- a/packages/dockview/src/__tests__/dnd/droptarget.spec.ts +++ b/packages/dockview/src/__tests__/dnd/droptarget.spec.ts @@ -1,4 +1,12 @@ -import { Droptarget, Position } from '../../dnd/droptarget'; +import { + calculateQuadrantAsPercentage, + calculateQuadrantAsPixels, + directionToPosition, + Droptarget, + DropTargetDirections, + Position, + Quadrant, +} from '../../dnd/droptarget'; import { fireEvent } from '@testing-library/dom'; function createOffsetDragOverEvent(params: { @@ -27,6 +35,17 @@ describe('droptarget', () => { jest.spyOn(element, 'clientWidth', 'get').mockImplementation(() => 200); }); + test('directionToPosition', () => { + expect(directionToPosition('above')).toBe(Position.Top); + expect(directionToPosition('below')).toBe(Position.Bottom); + expect(directionToPosition('left')).toBe(Position.Left); + expect(directionToPosition('right')).toBe(Position.Right); + expect(directionToPosition('within')).toBe(Position.Center); + expect(() => directionToPosition('bad_input' as any)).toThrow( + "invalid direction 'bad_input'" + ); + }); + test('non-directional', () => { let position: Position | undefined = undefined; @@ -197,4 +216,116 @@ describe('droptarget', () => { viewQuery = element.querySelectorAll('.drop-target'); expect(viewQuery.length).toBe(0); }); + + describe('calculateQuadrantAsPercentage', () => { + test('variety of cases', () => { + const inputs: Array<{ + directions: DropTargetDirections[]; + x: number; + y: number; + result: Quadrant | null | undefined; + }> = [ + { directions: ['left', 'right'], x: 19, y: 50, result: 'left' }, + { + directions: ['left', 'right'], + x: 81, + y: 50, + result: 'right', + }, + { + directions: ['top', 'bottom'], + x: 50, + y: 19, + result: 'top', + }, + { + directions: ['top', 'bottom'], + x: 50, + y: 81, + result: 'bottom', + }, + { + directions: ['left', 'right', 'top', 'bottom', 'center'], + x: 50, + y: 50, + result: null, + }, + { + directions: ['left', 'right', 'top', 'bottom'], + x: 50, + y: 50, + result: undefined, + }, + ]; + + for (const input of inputs) { + expect( + calculateQuadrantAsPercentage( + new Set(input.directions), + input.x, + input.y, + 100, + 100, + 20 + ) + ).toBe(input.result); + } + }); + }); + + describe('calculateQuadrantAsPixels', () => { + test('variety of cases', () => { + const inputs: Array<{ + directions: DropTargetDirections[]; + x: number; + y: number; + result: Quadrant | null | undefined; + }> = [ + { directions: ['left', 'right'], x: 19, y: 50, result: 'left' }, + { + directions: ['left', 'right'], + x: 81, + y: 50, + result: 'right', + }, + { + directions: ['top', 'bottom'], + x: 50, + y: 19, + result: 'top', + }, + { + directions: ['top', 'bottom'], + x: 50, + y: 81, + result: 'bottom', + }, + { + directions: ['left', 'right', 'top', 'bottom', 'center'], + x: 50, + y: 50, + result: null, + }, + { + directions: ['left', 'right', 'top', 'bottom'], + x: 50, + y: 50, + result: undefined, + }, + ]; + + for (const input of inputs) { + expect( + calculateQuadrantAsPixels( + new Set(input.directions), + input.x, + input.y, + 100, + 100, + 20 + ) + ).toBe(input.result); + } + }); + }); }); diff --git a/packages/dockview/src/__tests__/dockview/dockviewComponent.spec.ts b/packages/dockview/src/__tests__/dockview/dockviewComponent.spec.ts index db0f66522..d7ed07d0d 100644 --- a/packages/dockview/src/__tests__/dockview/dockviewComponent.spec.ts +++ b/packages/dockview/src/__tests__/dockview/dockviewComponent.spec.ts @@ -32,7 +32,7 @@ class PanelContentPartTest implements IContentRenderer { isDisposed: boolean = false; - constructor(public readonly id: string, component: string) { + constructor(public readonly id: string, public readonly component: string) { this.element.classList.add(`testpanel-${id}`); } @@ -53,7 +53,7 @@ class PanelContentPartTest implements IContentRenderer { } toJSON(): object { - return { id: this.id }; + return { id: this.component }; } focus(): void { @@ -2017,4 +2017,379 @@ describe('dockviewComponent', () => { // load a layout with a default tab identifier when react default is present // load a layout with invialid panel identifier + + test('orthogonal realigment #1', () => { + const container = document.createElement('div'); + + const dockview = new DockviewComponent(container, { + components: { + default: PanelContentPartTest, + }, + tabComponents: { + test_tab_id: PanelTabPartTest, + }, + orientation: Orientation.HORIZONTAL, + }); + dockview.deserializer = new ReactPanelDeserialzier(dockview); + + expect(dockview.orientation).toBe(Orientation.HORIZONTAL); + + dockview.fromJSON({ + activeGroup: 'group-1', + grid: { + root: { + type: 'branch', + data: [ + { + type: 'leaf', + data: { + views: ['panel1'], + id: 'group-1', + activeView: 'panel1', + }, + size: 500, + }, + ], + size: 1000, + }, + height: 1000, + width: 1000, + orientation: Orientation.VERTICAL, + }, + panels: { + panel1: { + id: 'panel1', + view: { content: { id: 'default' } }, + title: 'panel1', + }, + }, + }); + + expect(dockview.orientation).toBe(Orientation.VERTICAL); + + dockview.addPanel({ + id: 'panel2', + component: 'default', + position: { + direction: 'left', + }, + }); + + expect(dockview.orientation).toBe(Orientation.HORIZONTAL); + + expect(JSON.parse(JSON.stringify(dockview.toJSON()))).toEqual({ + activeGroup: '1', + grid: { + root: { + type: 'branch', + data: [ + { + type: 'leaf', + data: { + views: ['panel2'], + id: '1', + activeView: 'panel2', + }, + size: 500, + }, + { + type: 'leaf', + data: { + views: ['panel1'], + id: 'group-1', + activeView: 'panel1', + }, + size: 1000, + }, + ], + size: 1000, + }, + height: 1000, + width: 1000, + orientation: Orientation.HORIZONTAL, + }, + panels: { + panel1: { + id: 'panel1', + view: { content: { id: 'default' } }, + title: 'panel1', + }, + panel2: { + id: 'panel2', + view: { content: { id: 'default' } }, + title: 'panel2', + }, + }, + options: {}, + }); + }); + + test('orthogonal realigment #2', () => { + const container = document.createElement('div'); + + const dockview = new DockviewComponent(container, { + components: { + default: PanelContentPartTest, + }, + tabComponents: { + test_tab_id: PanelTabPartTest, + }, + orientation: Orientation.HORIZONTAL, + }); + dockview.deserializer = new ReactPanelDeserialzier(dockview); + + expect(dockview.orientation).toBe(Orientation.HORIZONTAL); + + dockview.fromJSON({ + activeGroup: 'group-1', + grid: { + root: { + type: 'branch', + data: [ + { + type: 'leaf', + data: { + views: ['panel1'], + id: 'group-1', + activeView: 'panel1', + }, + size: 500, + }, + { + type: 'leaf', + data: { + views: ['panel2'], + id: 'group-2', + activeView: 'panel2', + }, + size: 500, + }, + ], + size: 1000, + }, + height: 1000, + width: 1000, + orientation: Orientation.VERTICAL, + }, + panels: { + panel1: { + id: 'panel1', + view: { content: { id: 'default' } }, + title: 'panel1', + }, + panel2: { + id: 'panel2', + view: { content: { id: 'default' } }, + title: 'panel2', + }, + }, + }); + + expect(dockview.orientation).toBe(Orientation.VERTICAL); + + dockview.addPanel({ + id: 'panel3', + component: 'default', + position: { + direction: 'left', + }, + }); + + expect(dockview.orientation).toBe(Orientation.HORIZONTAL); + + expect(JSON.parse(JSON.stringify(dockview.toJSON()))).toEqual({ + activeGroup: '1', + grid: { + root: { + type: 'branch', + data: [ + { + type: 'leaf', + data: { + views: ['panel3'], + id: '1', + activeView: 'panel3', + }, + size: 500, + }, + { + type: 'branch', + data: [ + { + type: 'leaf', + data: { + views: ['panel1'], + id: 'group-1', + activeView: 'panel1', + }, + size: 500, + }, + { + type: 'leaf', + data: { + views: ['panel2'], + id: 'group-2', + activeView: 'panel2', + }, + size: 500, + }, + ], + size: 500, + }, + ], + size: 1000, + }, + height: 1000, + width: 1000, + orientation: Orientation.HORIZONTAL, + }, + panels: { + panel1: { + id: 'panel1', + view: { content: { id: 'default' } }, + title: 'panel1', + }, + + panel2: { + id: 'panel2', + view: { content: { id: 'default' } }, + title: 'panel2', + }, + panel3: { + id: 'panel3', + view: { content: { id: 'default' } }, + title: 'panel3', + }, + }, + options: {}, + }); + }); + + test('orthogonal realigment #3', () => { + const container = document.createElement('div'); + + const dockview = new DockviewComponent(container, { + components: { + default: PanelContentPartTest, + }, + tabComponents: { + test_tab_id: PanelTabPartTest, + }, + orientation: Orientation.HORIZONTAL, + }); + dockview.deserializer = new ReactPanelDeserialzier(dockview); + + expect(dockview.orientation).toBe(Orientation.HORIZONTAL); + + dockview.fromJSON({ + activeGroup: 'group-1', + grid: { + root: { + type: 'branch', + data: [ + { + type: 'leaf', + data: { + views: ['panel1'], + id: 'group-1', + activeView: 'panel1', + }, + size: 500, + }, + ], + size: 1000, + }, + height: 1000, + width: 1000, + orientation: Orientation.VERTICAL, + }, + panels: { + panel1: { + id: 'panel1', + view: { content: { id: 'default' } }, + title: 'panel1', + }, + }, + }); + + expect(dockview.orientation).toBe(Orientation.VERTICAL); + + dockview.addPanel({ + id: 'panel2', + component: 'default', + position: { + direction: 'above', + }, + }); + + dockview.addPanel({ + id: 'panel3', + component: 'default', + position: { + direction: 'below', + }, + }); + + expect(dockview.orientation).toBe(Orientation.VERTICAL); + + expect(JSON.parse(JSON.stringify(dockview.toJSON()))).toEqual({ + activeGroup: '2', + grid: { + root: { + type: 'branch', + data: [ + { + type: 'leaf', + data: { + views: ['panel2'], + id: '1', + activeView: 'panel2', + }, + size: 333, + }, + { + type: 'leaf', + data: { + views: ['panel1'], + id: 'group-1', + activeView: 'panel1', + }, + size: 333, + }, + { + type: 'leaf', + data: { + views: ['panel3'], + id: '2', + activeView: 'panel3', + }, + size: 334, + }, + ], + size: 1000, + }, + height: 1000, + width: 1000, + orientation: Orientation.VERTICAL, + }, + panels: { + panel1: { + id: 'panel1', + view: { content: { id: 'default' } }, + title: 'panel1', + }, + panel2: { + id: 'panel2', + view: { content: { id: 'default' } }, + title: 'panel2', + }, + panel3: { + id: 'panel3', + view: { content: { id: 'default' } }, + title: 'panel3', + }, + }, + options: {}, + }); + }); }); diff --git a/packages/dockview/src/dnd/droptarget.ts b/packages/dockview/src/dnd/droptarget.ts index cbb4b699e..774c7c5a5 100644 --- a/packages/dockview/src/dnd/droptarget.ts +++ b/packages/dockview/src/dnd/droptarget.ts @@ -5,6 +5,10 @@ import { DragAndDropObserver } from './dnd'; import { clamp } from '../math'; import { Direction } from '../gridview/baseComponentGridview'; +function numberOrFallback(maybeNumber: any, fallback: number): number { + return typeof maybeNumber === 'number' ? maybeNumber : fallback; +} + export enum Position { Top = 'Top', Left = 'Left', @@ -26,15 +30,15 @@ export function directionToPosition(direction: Direction): Position { case 'within': return Position.Center; default: - throw new Error(`invalid direction ${direction}`); + throw new Error(`invalid direction '${direction}'`); } } export type Quadrant = 'top' | 'bottom' | 'left' | 'right'; export interface DroptargetEvent { - position: Position; - nativeEvent: DragEvent; + readonly position: Position; + readonly nativeEvent: DragEvent; } export type DropTargetDirections = @@ -62,7 +66,7 @@ export class Droptarget extends CompositeDisposable { private readonly _onDrop = new Emitter(); readonly onDrop: Event = this._onDrop.event; - get state() { + get state(): Position | undefined { return this._state; } @@ -172,7 +176,7 @@ export class Droptarget extends CompositeDisposable { ); } - public dispose() { + public dispose(): void { this.removeDropTarget(); } @@ -180,7 +184,7 @@ export class Droptarget extends CompositeDisposable { quadrant: Quadrant | null, width: number, height: number - ) { + ): void { if (!this.overlay) { return; } @@ -242,7 +246,7 @@ export class Droptarget extends CompositeDisposable { toggleClass(this.overlay, 'small-bottom', isSmallY && isBottom); } - private setState(quadrant: Quadrant | null) { + private setState(quadrant: Quadrant | null): void { switch (quadrant) { case 'top': this._state = Position.Top; @@ -273,13 +277,13 @@ export class Droptarget extends CompositeDisposable { this.options.overlayModel?.activationSize === undefined || this.options.overlayModel?.activationSize?.type === 'percentage'; - const value = - typeof this.options.overlayModel?.activationSize?.value === 'number' - ? this.options.overlayModel?.activationSize?.value - : 20; + const value = numberOrFallback( + this.options?.overlayModel?.activationSize?.value, + 20 + ); if (isPercentage) { - return calculateQuadrant_Percentage( + return calculateQuadrantAsPercentage( overlayType, x, y, @@ -289,7 +293,7 @@ export class Droptarget extends CompositeDisposable { ); } - return calculateQuadrant_Pixels( + return calculateQuadrantAsPixels( overlayType, x, y, @@ -310,7 +314,7 @@ export class Droptarget extends CompositeDisposable { } } -function calculateQuadrant_Percentage( +export function calculateQuadrantAsPercentage( overlayType: Set, x: number, y: number, @@ -341,7 +345,7 @@ function calculateQuadrant_Percentage( return null; } -function calculateQuadrant_Pixels( +export function calculateQuadrantAsPixels( overlayType: Set, x: number, y: number, @@ -358,7 +362,7 @@ function calculateQuadrant_Pixels( if (overlayType.has('top') && y < threshold) { return 'top'; } - if (overlayType.has('right') && y > height - threshold) { + if (overlayType.has('bottom') && y > height - threshold) { return 'bottom'; } diff --git a/packages/dockview/src/dockview/dockviewComponent.ts b/packages/dockview/src/dockview/dockviewComponent.ts index 7089a6294..6f7f2c869 100644 --- a/packages/dockview/src/dockview/dockviewComponent.ts +++ b/packages/dockview/src/dockview/dockviewComponent.ts @@ -47,8 +47,6 @@ import { GroupPanel, IGroupviewPanel } from '../groupview/groupviewPanel'; import { DefaultGroupPanelView } from './defaultGroupPanelView'; import { getPanelData } from '../dnd/dataTransfer'; -const nextGroupId = sequentialNumberGenerator(); - export interface PanelReference { update: (event: { params: { [key: string]: any } }) => void; remove: () => void; @@ -89,6 +87,7 @@ export interface IDockviewComponent extends IBaseGrid { readonly totalPanels: number; readonly panels: IDockviewPanel[]; readonly onDidDrop: Event; + readonly orientation: Orientation; tabHeight: number | undefined; deserializer: IPanelDeserializer | undefined; updateOptions(options: DockviewComponentUpdateOptions): void; @@ -127,6 +126,7 @@ export class DockviewComponent extends BaseGrid implements IDockviewComponent { + private readonly nextGroupId = sequentialNumberGenerator(); private _deserializer: IPanelDeserializer | undefined; private _api: DockviewApi; private _options: Exclude; @@ -150,6 +150,10 @@ export class DockviewComponent readonly onDidActivePanelChange: Event = this._onDidActivePanelChange.event; + get orientation(): Orientation { + return this.gridview.orientation; + } + get totalPanels(): number { return this.panels.length; } @@ -841,9 +845,9 @@ export class DockviewComponent } if (!id) { - id = nextGroupId.next(); + id = this.nextGroupId.next(); while (this._groups.has(id)) { - id = nextGroupId.next(); + id = this.nextGroupId.next(); } } diff --git a/packages/dockview/src/gridview/gridview.ts b/packages/dockview/src/gridview/gridview.ts index 691808b79..c47dca887 100644 --- a/packages/dockview/src/gridview/gridview.ts +++ b/packages/dockview/src/gridview/gridview.ts @@ -434,7 +434,15 @@ export class Gridview implements IDisposable { this.root.size ); - this._root.addChild(oldRoot, Sizing.Distribute, 0); + if (oldRoot.children.length === 1) { + // can remove one level of redundant branching if there is only a single child + const childReference = oldRoot.children[0]; + oldRoot.removeChild(0); // remove to prevent disposal when disposing of unwanted root + oldRoot.dispose(); + this._root.addChild(childReference, Sizing.Distribute, 0); + } else { + this._root.addChild(oldRoot, Sizing.Distribute, 0); + } this.element.appendChild(this._root.element);