tests: add tests

This commit is contained in:
mathuo 2023-02-13 16:44:21 +07:00
parent bf350e404b
commit 321f78ec0e
No known key found for this signature in database
GPG Key ID: C6EEDEFD6CA07281
5 changed files with 546 additions and 24 deletions

View File

@ -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'; import { fireEvent } from '@testing-library/dom';
function createOffsetDragOverEvent(params: { function createOffsetDragOverEvent(params: {
@ -27,6 +35,17 @@ describe('droptarget', () => {
jest.spyOn(element, 'clientWidth', 'get').mockImplementation(() => 200); 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', () => { test('non-directional', () => {
let position: Position | undefined = undefined; let position: Position | undefined = undefined;
@ -197,4 +216,116 @@ describe('droptarget', () => {
viewQuery = element.querySelectorAll('.drop-target'); viewQuery = element.querySelectorAll('.drop-target');
expect(viewQuery.length).toBe(0); 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);
}
});
});
}); });

View File

@ -32,7 +32,7 @@ class PanelContentPartTest implements IContentRenderer {
isDisposed: boolean = false; 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}`); this.element.classList.add(`testpanel-${id}`);
} }
@ -53,7 +53,7 @@ class PanelContentPartTest implements IContentRenderer {
} }
toJSON(): object { toJSON(): object {
return { id: this.id }; return { id: this.component };
} }
focus(): void { 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 a default tab identifier when react default is present
// load a layout with invialid panel identifier // 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: {},
});
});
}); });

View File

@ -5,6 +5,10 @@ import { DragAndDropObserver } from './dnd';
import { clamp } from '../math'; import { clamp } from '../math';
import { Direction } from '../gridview/baseComponentGridview'; import { Direction } from '../gridview/baseComponentGridview';
function numberOrFallback(maybeNumber: any, fallback: number): number {
return typeof maybeNumber === 'number' ? maybeNumber : fallback;
}
export enum Position { export enum Position {
Top = 'Top', Top = 'Top',
Left = 'Left', Left = 'Left',
@ -26,15 +30,15 @@ export function directionToPosition(direction: Direction): Position {
case 'within': case 'within':
return Position.Center; return Position.Center;
default: default:
throw new Error(`invalid direction ${direction}`); throw new Error(`invalid direction '${direction}'`);
} }
} }
export type Quadrant = 'top' | 'bottom' | 'left' | 'right'; export type Quadrant = 'top' | 'bottom' | 'left' | 'right';
export interface DroptargetEvent { export interface DroptargetEvent {
position: Position; readonly position: Position;
nativeEvent: DragEvent; readonly nativeEvent: DragEvent;
} }
export type DropTargetDirections = export type DropTargetDirections =
@ -62,7 +66,7 @@ export class Droptarget extends CompositeDisposable {
private readonly _onDrop = new Emitter<DroptargetEvent>(); private readonly _onDrop = new Emitter<DroptargetEvent>();
readonly onDrop: Event<DroptargetEvent> = this._onDrop.event; readonly onDrop: Event<DroptargetEvent> = this._onDrop.event;
get state() { get state(): Position | undefined {
return this._state; return this._state;
} }
@ -172,7 +176,7 @@ export class Droptarget extends CompositeDisposable {
); );
} }
public dispose() { public dispose(): void {
this.removeDropTarget(); this.removeDropTarget();
} }
@ -180,7 +184,7 @@ export class Droptarget extends CompositeDisposable {
quadrant: Quadrant | null, quadrant: Quadrant | null,
width: number, width: number,
height: number height: number
) { ): void {
if (!this.overlay) { if (!this.overlay) {
return; return;
} }
@ -242,7 +246,7 @@ export class Droptarget extends CompositeDisposable {
toggleClass(this.overlay, 'small-bottom', isSmallY && isBottom); toggleClass(this.overlay, 'small-bottom', isSmallY && isBottom);
} }
private setState(quadrant: Quadrant | null) { private setState(quadrant: Quadrant | null): void {
switch (quadrant) { switch (quadrant) {
case 'top': case 'top':
this._state = Position.Top; this._state = Position.Top;
@ -273,13 +277,13 @@ export class Droptarget extends CompositeDisposable {
this.options.overlayModel?.activationSize === undefined || this.options.overlayModel?.activationSize === undefined ||
this.options.overlayModel?.activationSize?.type === 'percentage'; this.options.overlayModel?.activationSize?.type === 'percentage';
const value = const value = numberOrFallback(
typeof this.options.overlayModel?.activationSize?.value === 'number' this.options?.overlayModel?.activationSize?.value,
? this.options.overlayModel?.activationSize?.value 20
: 20; );
if (isPercentage) { if (isPercentage) {
return calculateQuadrant_Percentage( return calculateQuadrantAsPercentage(
overlayType, overlayType,
x, x,
y, y,
@ -289,7 +293,7 @@ export class Droptarget extends CompositeDisposable {
); );
} }
return calculateQuadrant_Pixels( return calculateQuadrantAsPixels(
overlayType, overlayType,
x, x,
y, y,
@ -310,7 +314,7 @@ export class Droptarget extends CompositeDisposable {
} }
} }
function calculateQuadrant_Percentage( export function calculateQuadrantAsPercentage(
overlayType: Set<DropTargetDirections>, overlayType: Set<DropTargetDirections>,
x: number, x: number,
y: number, y: number,
@ -341,7 +345,7 @@ function calculateQuadrant_Percentage(
return null; return null;
} }
function calculateQuadrant_Pixels( export function calculateQuadrantAsPixels(
overlayType: Set<DropTargetDirections>, overlayType: Set<DropTargetDirections>,
x: number, x: number,
y: number, y: number,
@ -358,7 +362,7 @@ function calculateQuadrant_Pixels(
if (overlayType.has('top') && y < threshold) { if (overlayType.has('top') && y < threshold) {
return 'top'; return 'top';
} }
if (overlayType.has('right') && y > height - threshold) { if (overlayType.has('bottom') && y > height - threshold) {
return 'bottom'; return 'bottom';
} }

View File

@ -47,8 +47,6 @@ import { GroupPanel, IGroupviewPanel } from '../groupview/groupviewPanel';
import { DefaultGroupPanelView } from './defaultGroupPanelView'; import { DefaultGroupPanelView } from './defaultGroupPanelView';
import { getPanelData } from '../dnd/dataTransfer'; import { getPanelData } from '../dnd/dataTransfer';
const nextGroupId = sequentialNumberGenerator();
export interface PanelReference { export interface PanelReference {
update: (event: { params: { [key: string]: any } }) => void; update: (event: { params: { [key: string]: any } }) => void;
remove: () => void; remove: () => void;
@ -89,6 +87,7 @@ export interface IDockviewComponent extends IBaseGrid<GroupPanel> {
readonly totalPanels: number; readonly totalPanels: number;
readonly panels: IDockviewPanel[]; readonly panels: IDockviewPanel[];
readonly onDidDrop: Event<DockviewDropEvent>; readonly onDidDrop: Event<DockviewDropEvent>;
readonly orientation: Orientation;
tabHeight: number | undefined; tabHeight: number | undefined;
deserializer: IPanelDeserializer | undefined; deserializer: IPanelDeserializer | undefined;
updateOptions(options: DockviewComponentUpdateOptions): void; updateOptions(options: DockviewComponentUpdateOptions): void;
@ -127,6 +126,7 @@ export class DockviewComponent
extends BaseGrid<GroupPanel> extends BaseGrid<GroupPanel>
implements IDockviewComponent implements IDockviewComponent
{ {
private readonly nextGroupId = sequentialNumberGenerator();
private _deserializer: IPanelDeserializer | undefined; private _deserializer: IPanelDeserializer | undefined;
private _api: DockviewApi; private _api: DockviewApi;
private _options: Exclude<DockviewComponentOptions, 'orientation'>; private _options: Exclude<DockviewComponentOptions, 'orientation'>;
@ -150,6 +150,10 @@ export class DockviewComponent
readonly onDidActivePanelChange: Event<IDockviewPanel | undefined> = readonly onDidActivePanelChange: Event<IDockviewPanel | undefined> =
this._onDidActivePanelChange.event; this._onDidActivePanelChange.event;
get orientation(): Orientation {
return this.gridview.orientation;
}
get totalPanels(): number { get totalPanels(): number {
return this.panels.length; return this.panels.length;
} }
@ -841,9 +845,9 @@ export class DockviewComponent
} }
if (!id) { if (!id) {
id = nextGroupId.next(); id = this.nextGroupId.next();
while (this._groups.has(id)) { while (this._groups.has(id)) {
id = nextGroupId.next(); id = this.nextGroupId.next();
} }
} }

View File

@ -434,7 +434,15 @@ export class Gridview implements IDisposable {
this.root.size 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); this.element.appendChild(this._root.element);