734 lines
20 KiB
TypeScript

import HotTable from '../src/HotTable.vue';
import BaseEditorComponent from '../src/BaseEditorComponent.vue';
import { HOT_DESTROYED_WARNING } from '../src/helpers';
import { mount } from '@vue/test-utils';
import {
createDomContainer,
createSampleData,
mockClientDimensions
} from './_helpers';
import { LRUMap } from "../src/lib/lru/lru";
import Vue from 'vue';
describe('hotInit', () => {
it('should initialize Handsontable and assign it to the `hotInstace` property of the provided object', () => {
let testWrapper = mount(HotTable, {
propsData: {
data: createSampleData(1, 1),
licenseKey: 'non-commercial-and-evaluation'
}
});
expect(typeof testWrapper.vm.hotInstance).toEqual('object');
expect(testWrapper.vm.hotInstance.getDataAtCell(0, 0)).toEqual('0-0');
});
});
describe('Updating the Handsontable settings', () => {
it('should update the previously initialized Handsontable instance with a single changed property', async() => {
let updateSettingsCalls = 0;
const testWrapper = mount(HotTable, {
propsData: {
data: createSampleData(1, 1),
licenseKey: 'non-commercial-and-evaluation',
rowHeaders: true,
afterUpdateSettings: function () {
updateSettingsCalls++;
}
}
});
expect(testWrapper.vm.hotInstance.getSettings().rowHeaders).toEqual(true);
testWrapper.setProps({
rowHeaders: false
});
await Vue.nextTick();
expect(updateSettingsCalls).toEqual(1);
expect(testWrapper.vm.hotInstance.getSettings().rowHeaders).toEqual(false);
});
it('should update the previously initialized Handsontable instance only once with multiple changed properties', async() => {
const initialAfterChangeHook = () => { /* initial hook */ };
const modifiedAfterChangeHook = () => { /* modified hook */ };
const App = Vue.extend({
data: function () {
return {
rowHeaders: true,
colHeaders: true,
readOnly: true,
afterChange: initialAfterChangeHook
}
},
methods: {
updateData: function () {
this.rowHeaders = false;
this.colHeaders = false;
this.readOnly = false;
this.afterChange = modifiedAfterChangeHook;
}
},
render(h) {
// HotTable
return h(HotTable, {
ref: 'hotInstance',
props: {
rowHeaders: this.rowHeaders,
colHeaders: this.colHeaders,
readOnly: this.readOnly,
afterChange: this.afterChange,
afterUpdateSettings: function () {
updateSettingsCalls++;
}
}
})
}
});
let testWrapper = mount(App, {
sync: false
});
let updateSettingsCalls = 0;
const hotTableComponent = testWrapper.vm.$children[0];
expect(hotTableComponent.hotInstance.getSettings().rowHeaders).toEqual(true);
expect(hotTableComponent.hotInstance.getSettings().colHeaders).toEqual(true);
expect(hotTableComponent.hotInstance.getSettings().readOnly).toEqual(true);
const hooks = hotTableComponent.hotInstance.pluginHookBucket.getHooks('afterChange');
expect(hooks.filter(hookEntry => hookEntry.callback === initialAfterChangeHook).length).toBe(1);
expect(hooks.filter(hookEntry => hookEntry.callback === modifiedAfterChangeHook).length).toBe(0);
testWrapper.vm.updateData();
await Vue.nextTick();
expect(updateSettingsCalls).toEqual(1);
expect(hotTableComponent.hotInstance.getSettings().rowHeaders).toEqual(false);
expect(hotTableComponent.hotInstance.getSettings().colHeaders).toEqual(false);
expect(hotTableComponent.hotInstance.getSettings().readOnly).toEqual(false);
expect(hooks.filter(hookEntry => hookEntry.callback === initialAfterChangeHook).length).toBe(0);
expect(hooks.filter(hookEntry => hookEntry.callback === modifiedAfterChangeHook).length).toBe(1);
});
it('should update the previously initialized Handsontable instance with only the options that are passed to the' +
' component as props and actually changed', async() => {
let newHotSettings = null;
let App = Vue.extend({
data: function () {
return {
rowHeaders: true,
colHeaders: true,
readOnly: true,
}
},
methods: {
updateData: function () {
this.rowHeaders = false;
this.colHeaders = false;
this.readOnly = false;
}
},
render(h) {
// HotTable
return h(HotTable, {
ref: 'hotInstance',
props: {
rowHeaders: this.rowHeaders,
colHeaders: this.colHeaders,
readOnly: this.readOnly,
minSpareRows: 4,
afterUpdateSettings: function (newSettings) {
newHotSettings = newSettings
}
}
})
}
});
let testWrapper = mount(App, {
sync: false
});
testWrapper.vm.updateData();
await Vue.nextTick();
expect(Object.keys(newHotSettings).length).toBe(3)
});
it('should not call Handsontable\'s `updateSettings` method, when the table data was changed by reference, and the' +
' dataset is an array of arrays', async() => {
let newHotSettings = null;
let App = Vue.extend({
data: function () {
return {
data: [[1, 2, 3]],
}
},
methods: {
modifyFirstRow: function () {
Vue.set(this.data, 0, [22, 32, 42]);
},
addRow: function () {
this.data.push([2, 3, 4]);
},
removeRow: function () {
this.data.pop()
},
},
render(h) {
// HotTable
return h(HotTable, {
ref: 'hotInstance',
props: {
data: this.data,
afterUpdateSettings: function (newSettings) {
newHotSettings = newSettings
}
}
})
}
});
let testWrapper = mount(App, {
sync: false
});
testWrapper.vm.addRow();
await Vue.nextTick();
expect(testWrapper.vm.$children[0].hotInstance.getData()).toEqual([[1, 2, 3], [2, 3, 4]]);
expect(newHotSettings).toBe(null);
testWrapper.vm.removeRow();
await Vue.nextTick();
expect(testWrapper.vm.$children[0].hotInstance.getData()).toEqual([[1, 2, 3]]);
expect(newHotSettings).toBe(null);
testWrapper.vm.modifyFirstRow();
await Vue.nextTick();
expect(testWrapper.vm.$children[0].hotInstance.getData()).toEqual([[22, 32, 42]]);
expect(newHotSettings).toBe(null);
testWrapper.vm.removeRow();
await Vue.nextTick();
expect(testWrapper.vm.$children[0].hotInstance.getData()).toEqual([]);
expect(newHotSettings).toBe(null);
});
it('should call Handsontable\'s `updateSettings` method, when the table data was changed by reference while the' +
' dataset is an array of object and property number changed', async() => {
let newHotSettings = null;
let App = Vue.extend({
data: function () {
return {
data: [{a: 1, b: 2, c: 3}],
}
},
methods: {
updateData: function (changedRow) {
Vue.set(this.data, 0, Object.assign({}, changedRow));
}
},
render(h) {
// HotTable
return h(HotTable, {
ref: 'hotInstance',
props: {
data: this.data,
afterUpdateSettings: function (newSettings) {
newHotSettings = newSettings
}
}
})
}
});
let testWrapper = mount(App, {
sync: false
});
testWrapper.vm.updateData({a: 1, b: 2, c: 3, d: 4});
await Vue.nextTick();
expect(testWrapper.vm.$children[0].hotInstance.getData()).toEqual([[1, 2, 3, 4]]);
expect(JSON.stringify(newHotSettings)).toBe(JSON.stringify({
data: [{a: 1, b: 2, c: 3, d: 4}]
}));
testWrapper.vm.updateData({a: 1});
await Vue.nextTick();
expect(testWrapper.vm.$children[0].hotInstance.getData()).toEqual([[1]]);
expect(JSON.stringify(newHotSettings)).toBe(JSON.stringify({
data: [{a: 1}]
}));
});
it('should NOT call Handsontable\'s `updateSettings` method, when the table data was changed by reference while the' +
' dataset is an array of object and property number DID NOT change', async() => {
let newHotSettings = null;
let App = Vue.extend({
data: function () {
return {
data: [{a: 1, b: 2, c: 3}],
}
},
methods: {
addRow: function () {
this.data.push({a: 12, b: 22, c: 32})
},
removeRow: function () {
this.data.pop()
}
},
render(h) {
// HotTable
return h(HotTable, {
ref: 'hotInstance',
props: {
data: this.data,
afterUpdateSettings: function (newSettings) {
newHotSettings = newSettings
}
}
})
}
});
let testWrapper = mount(App, {
sync: false
});
testWrapper.vm.addRow();
await Vue.nextTick();
expect(testWrapper.vm.$children[0].hotInstance.getData()).toEqual([[1, 2, 3], [12, 22, 32]]);
expect(newHotSettings).toBe(null);
testWrapper.vm.removeRow();
await Vue.nextTick();
expect(testWrapper.vm.$children[0].hotInstance.getData()).toEqual([[1, 2, 3]]);
expect(newHotSettings).toBe(null);
});
});
describe('getRendererWrapper', () => {
it('should create the wrapper function for the provided renderer child component', () => {
// mocks
const mockVNode = {
componentOptions: {
Ctor: class {
$mount() {
return {
$data: {},
$el: document.createElement('TD')
};
}
}
}
};
const mockComponent = {
editorCache: new Map(),
rendererCache: new LRUMap(100),
$parent: {}
};
const getRendererWrapper = (HotTable as any).methods.getRendererWrapper;
const mockTD = document.createElement('TD');
expect(typeof getRendererWrapper.call(mockComponent, mockVNode, mockComponent)).toEqual('function');
expect(getRendererWrapper.call(mockComponent, mockVNode, mockComponent)({}, mockTD, 0, 0, 0, '', {})).toEqual(mockTD);
});
});
describe('getEditorClass', () => {
it('should create a fresh class to be used as an editor, based on the editor component provided.', () => {
// mocks
const mockVNode = {
componentOptions: {
Ctor: class {
static get options() {
return {
name: 'name'
};
}
$mount() {
return {
$data: {
hotCustomEditorClass: class B {
prepare() {
return 'not-undefined';
}
}
},
$el: document.createElement('TD')
};
}
}
}
};
const mockComponent = {
editorCache: new Map(),
rendererCache: new LRUMap(100),
$parent: {}
};
const getEditorClass = (HotTable as any).methods.getEditorClass;
const editorClass = getEditorClass.call(mockComponent, mockVNode, mockComponent);
expect(editorClass.constructor).not.toEqual(void 0);
expect(editorClass.prototype.prepare).not.toEqual(void 0);
});
});
describe('Global editors and renderers', () => {
it('should allow defining renderer and editor components to work globally on the entire table', () => {
const dummyHtmlElement = document.createElement('DIV');
dummyHtmlElement.id = 'dummy';
const dummyEditorComponent = Vue.component('renderer-component', {
name: 'EditorComponent',
extends: BaseEditorComponent,
render: function (h) {
return h('div', {
'attrs': {
'id': 'dummy-editor'
}
});
}
});
const dummyRendererComponent = Vue.component('renderer-component', {
name: 'RendererComponent',
render: function (h) {
return h('div', {
'attrs': {
'id': 'dummy-renderer'
}
});
}
});
let App = Vue.extend({
render(h) {
// HotTable
return h(HotTable, {
props: {
data: createSampleData(50, 2),
autoRowSize: false,
autoColumnSize: false,
width: 400,
height: 400,
init: function () {
mockClientDimensions(this.rootElement, 400, 400);
}
}
}, [
h(dummyRendererComponent, {
attrs: {
'hot-renderer': true
}
}),
h(dummyEditorComponent, {
attrs: {
'hot-editor': true
}
})
])
}
});
let testWrapper = mount(App, {
attachTo: createDomContainer()
});
const hotTableComponent = testWrapper.vm.$children[0];
const globalEditor = hotTableComponent.hotInstance.getSettings().editor;
const globalEditorInstance = new globalEditor(hotTableComponent.hotInstance);
expect(globalEditorInstance._fullEditMode).toEqual(false);
expect(globalEditorInstance.hot).toEqual(hotTableComponent.hotInstance);
expect(hotTableComponent.hotInstance.getSettings().renderer(hotTableComponent.hotInstance, document.createElement('DIV'), 555, 0, 0, '0', {}).childNodes[0].id).toEqual('dummy-renderer');
testWrapper.destroy();
});
});
it('should inject an `isRenderer` and `isEditor` properties to renderer/editor components', () => {
const dummyEditorComponent = Vue.component('renderer-component', {
name: 'EditorComponent',
extends: BaseEditorComponent,
render: function (h) {
return h('div', {
'attrs': {
'id': 'dummy-editor'
}
});
}
});
const dummyRendererComponent = Vue.component('renderer-component', {
name: 'RendererComponent',
render: function (h) {
return h('div', {
'attrs': {
'id': 'dummy-renderer'
}
});
}
});
let App = Vue.extend({
render(h) {
// HotTable
return h(HotTable, {
props: {
data: createSampleData(50, 2),
licenseKey: 'non-commercial-and-evaluation',
autoRowSize: false,
autoColumnSize: false
}
}, [
h(dummyRendererComponent, {
attrs: {
'hot-renderer': true
}
}),
h(dummyEditorComponent, {
attrs: {
'hot-editor': true
}
})
])
}
});
let testWrapper = mount(App, {
attachTo: createDomContainer()
});
const hotTableComponent = testWrapper.vm.$children[0];
expect(hotTableComponent.$data.rendererCache.get('0-0').component.$data.isRenderer).toEqual(true);
expect(hotTableComponent.$data.editorCache.get('EditorComponent').$data.isEditor).toEqual(true);
testWrapper.destroy();
});
it('should be possible to access the `hotInstance` property of the HotTable instance from a parent-component', () => {
let hotInstanceFromRef = 'not-set';
let App = Vue.extend({
data: function () {
return {
rowHeaders: true,
colHeaders: true,
readOnly: true,
}
},
methods: {
cellsCallback: function() {
if (hotInstanceFromRef === 'not-set') {
hotInstanceFromRef = this.$refs.hTable.hotInstance;
}
}
},
render(h) {
// HotTable
return h(HotTable, {
ref: 'hTable',
props: {
rowHeaders: this.rowHeaders,
colHeaders: this.colHeaders,
readOnly: this.readOnly,
cells: this.cellsCallback
}
})
}
});
let testWrapper = mount(App, {
attachTo: createDomContainer()
});
expect(['not-set', null].includes(hotInstanceFromRef)).toBe(false);
testWrapper.destroy();
});
it('should be possible to pass props to the editor and renderer components', () => {
const dummyEditorComponent = Vue.component('renderer-component', {
name: 'EditorComponent',
extends: BaseEditorComponent,
props: ['test-prop'],
render: function (h) {
return h('div', {});
}
});
const dummyRendererComponent = Vue.component('renderer-component', {
name: 'RendererComponent',
props: ['test-prop'],
render: function (h) {
return h('div', {});
}
});
let App = Vue.extend({
render(h) {
// HotTable
return h(HotTable, {
props: {
data: createSampleData(1, 1),
licenseKey: 'non-commercial-and-evaluation',
}
}, [
h(dummyRendererComponent, {
attrs: {
'hot-renderer': true,
'test-prop': 'test-prop-value'
}
}),
h(dummyEditorComponent, {
attrs: {
'hot-editor': true,
'test-prop': 'test-prop-value'
}
})
])
}
});
let testWrapper = mount(App, {
attachTo: createDomContainer()
});
const hotTableComponent = testWrapper.vm.$children[0];
expect(hotTableComponent.rendererCache.get('0-0').component.$props.testProp).toEqual('test-prop-value');
expect(hotTableComponent.editorCache.get('EditorComponent').$props.testProp).toEqual('test-prop-value');
testWrapper.destroy();
});
it('should display a warning and not throw any errors, when the underlying Handsontable instance ' +
'has been destroyed', () => {
const warnFunc = console.warn;
const testWrapper = mount(HotTable, {
propsData: {
data: [[1]],
licenseKey: 'non-commercial-and-evaluation',
}
});
const warnCalls = [];
console.warn = (warningMessage) => {
warnCalls.push(warningMessage);
}
expect(testWrapper.vm.hotInstance.isDestroyed).toEqual(false);
testWrapper.vm.hotInstance.destroy();
expect(testWrapper.vm.hotInstance).toEqual(null);
expect(warnCalls.length).toBeGreaterThan(0);
warnCalls.forEach((message) => {
expect(message).toEqual(HOT_DESTROYED_WARNING);
});
testWrapper.destroy();
console.warn = warnFunc;
});
describe('HOT-based CRUD actions', () => {
it('should should not add/remove any additional rows when calling `alter` on the HOT instance', async() => {
const testWrapper = mount(HotTable, {
propsData: {
data: createSampleData(4, 4),
rowHeaders: true,
colHeaders: true,
}
});
const hotInstance = testWrapper.vm.hotInstance;
hotInstance.alter('insert_row_above', 2, 2);
hotInstance.alter('insert_col_end', 2, 2);
await Vue.nextTick();
expect(hotInstance.countRows()).toEqual(6);
expect(hotInstance.countSourceRows()).toEqual(6);
expect(hotInstance.countCols()).toEqual(6);
expect(hotInstance.countSourceCols()).toEqual(6);
expect(hotInstance.getSourceData().length).toEqual(6);
expect(hotInstance.getSourceData()[0].length).toEqual(6);
hotInstance.alter('remove_row', 2, 2);
hotInstance.alter('remove_col', 2, 2);
expect(hotInstance.countRows()).toEqual(4);
expect(hotInstance.countSourceRows()).toEqual(4);
expect(hotInstance.countCols()).toEqual(4);
expect(hotInstance.countSourceCols()).toEqual(4);
expect(hotInstance.getSourceData().length).toEqual(4);
expect(hotInstance.getSourceData()[0].length).toEqual(4);
});
});
describe('Non-HOT based CRUD actions', () => {
it('should should not add/remove any additional rows when modifying a data array passed to the wrapper', async() => {
const externalData = createSampleData(4, 4);
const testWrapper = mount(HotTable, {
propsData: {
data: externalData,
rowHeaders: true,
colHeaders: true,
}
});
const hotInstance = testWrapper.vm.hotInstance;
externalData.push(externalData[0], externalData[0]);
externalData[0].push('test', 'test');
await Vue.nextTick();
expect(hotInstance.countRows()).toEqual(6);
expect(hotInstance.countSourceRows()).toEqual(6);
expect(hotInstance.countCols()).toEqual(6);
expect(hotInstance.countSourceCols()).toEqual(6);
expect(hotInstance.getSourceData().length).toEqual(6);
expect(hotInstance.getSourceData()[0].length).toEqual(6);
externalData.pop();
externalData.pop();
externalData[0].pop();
externalData[0].pop();
await Vue.nextTick();
expect(hotInstance.countRows()).toEqual(4);
expect(hotInstance.countSourceRows()).toEqual(4);
expect(hotInstance.countCols()).toEqual(4);
expect(hotInstance.countSourceCols()).toEqual(4);
expect(hotInstance.getSourceData().length).toEqual(4);
expect(hotInstance.getSourceData()[0].length).toEqual(4);
});
});