added messages list, new client form, logic for client Apps plus others

This commit is contained in:
Kwesi Banson Jnr
2026-03-22 22:29:28 +00:00
parent c68c007945
commit 4ab0fda326
858 changed files with 242393 additions and 337 deletions

View File

@@ -0,0 +1,161 @@
import Tabulator from '../../../src/js/core/Tabulator.js';
import TabulatorFull from '../../../src/js/core/TabulatorFull.js';
import Accessor from '../../../src/js/modules/Accessor/Accessor.js';
describe('Accessor', function(){
/** @type {Tabulator} */
let table;
/** @type {Accessor} */
let accessor;
let tableData = [
{id:1, name:"John", age:20},
{id:2, name:"Jane", age:25},
{id:3, name:"Steve", age:30}
];
let tableColumns = [
{title:"ID", field:"id"},
{title:"Name", field:"name", accessor:function(value){ return value; }},
{title:"Age", field:"age", accessor:function(value, data, type){
return value + " years";
}},
{title:"Custom", field:"custom", accessorDownload:function(value){ return value; }}
];
// Mock accessor function
beforeAll(function(){
// Add a test accessor to the static accessors
Accessor.accessors.test = function(value) { return value + "-test"; };
});
beforeEach(function(){
let element = document.createElement("div");
table = new TabulatorFull(element, {
data:tableData,
columns:tableColumns
});
accessor = table.module("accessor");
});
afterEach(function(){
table.destroy();
});
test('module is initialized', function(){
expect(accessor).toBeDefined();
expect(accessor.allowedTypes).toContain("data");
expect(accessor.allowedTypes).toContain("download");
expect(accessor.allowedTypes).toContain("clipboard");
});
test('lookupAccessor returns accessor function from string name', function(){
const testAccessor = accessor.lookupAccessor("test");
expect(typeof testAccessor).toBe("function");
expect(testAccessor("value")).toBe("value-test");
});
test('lookupAccessor returns function directly', function(){
const customFunc = function(value){ return value; };
const result = accessor.lookupAccessor(customFunc);
expect(result).toBe(customFunc);
});
test('lookupAccessor warns on invalid accessor name', function(){
const consoleSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
const result = accessor.lookupAccessor("invalid");
expect(consoleSpy).toHaveBeenCalled();
expect(result).toBe(false);
consoleSpy.mockRestore();
});
// Create mock table for testing accessor functionality
test('initializeColumn sets up accessors on columns', function() {
// Create a mock column
const mockColumn = {
definition: {
accessor: "test",
accessorParams: { test: true }
},
modules: {}
};
// Initialize the column
accessor.initializeColumn(mockColumn);
// The nested structure: modules.accessor.accessor.accessor represents
// Container -> Type -> Function
expect(mockColumn.modules.accessor).toBeDefined();
expect(mockColumn.modules.accessor.accessor).toBeDefined();
expect(mockColumn.modules.accessor.accessor.accessor).toBeDefined();
expect(typeof mockColumn.modules.accessor.accessor.accessor).toBe('function');
});
test('transformRow applies accessors to data', function(){
// Create mock column with accessor
const mockColumn = {
field: "age",
modules: {
accessor: {
accessor: {
// This is the actual accessor function in the nested structure
accessor: function(value) { return value + " years"; },
params: {}
}
}
},
getFieldValue: function(data) { return data.age; },
setFieldValue: function(data, value) { data.age = value; },
getComponent: function() { return {}; }
};
// Create mock row
const mockRow = {
data: { id: 1, name: "John", age: 20 },
getComponent: function() { return {}; }
};
// Simplified implementation of transformRow process
const transformedData = { ...mockRow.data };
const value = mockColumn.getFieldValue(transformedData);
const newValue = mockColumn.modules.accessor.accessor.accessor(value);
mockColumn.setFieldValue(transformedData, newValue);
// Verify accessor was applied
expect(transformedData.age).toBe("20 years");
});
test('transformRow applies different accessors based on type', function(){
// Create mock column with download accessor
const mockColumn = {
field: "custom",
modules: {
accessor: {
// Using accessorDownload instead of accessor for download-specific transformations
accessorDownload: {
accessor: function(value) { return "downloaded-" + value; },
params: {}
}
}
},
getFieldValue: function(data) { return data.custom || ""; },
setFieldValue: function(data, value) { data.custom = value; },
getComponent: function() { return {}; }
};
// Create mock row
const mockRow = {
data: { id: 1, name: "John", age: 20, custom: "test" },
getComponent: function() { return {}; }
};
// Using the type-specific accessor (accessorDownload)
const transformedData = { ...mockRow.data };
const value = mockColumn.getFieldValue(transformedData);
const newValue = mockColumn.modules.accessor.accessorDownload.accessor(value);
mockColumn.setFieldValue(transformedData, newValue);
// Verify download accessor was applied
expect(transformedData.custom).toBe("downloaded-test");
});
});

View File

@@ -0,0 +1,160 @@
import TabulatorFull from '../../../src/js/core/TabulatorFull.js';
describe('Ajax', function(){
let table, ajax;
beforeEach(function(){
let element = document.createElement("div");
table = new TabulatorFull(element, {
ajaxURL: 'fake-data-url.json'
});
ajax = table.module("ajax");
// Need to manually set URL since the ajax module doesn't seem to initialize from options
ajax.setUrl('fake-data-url.json');
});
afterEach(function(){
table.destroy();
});
test('module is initialized', function(){
expect(ajax).toBeDefined();
expect(ajax.url).toBe('fake-data-url.json');
});
test('getUrl returns the correct URL', function(){
expect(ajax.getUrl()).toBe('fake-data-url.json');
});
test('setUrl updates the URL', function(){
ajax.setUrl('new-url.json');
expect(ajax.url).toBe('new-url.json');
expect(ajax.getUrl()).toBe('new-url.json');
});
test('setDefaultConfig handles string config', function(){
ajax.setDefaultConfig('post');
expect(ajax.config.method).toBe('post');
});
test('setDefaultConfig handles object config', function(){
const config = {
method: 'put',
headers: {'Content-Type': 'application/json'}
};
ajax.setDefaultConfig(config);
expect(ajax.config.method).toBe('put');
expect(ajax.config.headers).toEqual({'Content-Type': 'application/json'});
});
test('generateConfig with string parameter', function(){
const result = ajax.generateConfig('delete');
expect(result.method).toBe('delete');
});
test('generateConfig with object parameter', function(){
const config = {
method: 'patch',
timeout: 5000
};
const result = ajax.generateConfig(config);
expect(result.method).toBe('patch');
expect(result.timeout).toBe(5000);
});
test('requestParams merges parameters correctly', function(){
// Set up a table with ajaxParams
let element = document.createElement("div");
let customTable = new TabulatorFull(element, {
ajaxURL: 'test.json',
ajaxParams: {page: 1, size: 10}
});
let customAjax = customTable.module("ajax");
let result = customAjax.requestParams(null, null, false, {filter: 'active'});
expect(result).toEqual({page: 1, size: 10, filter: 'active'});
customTable.destroy();
});
test('requestParams handles function parameters', function(){
// Set up a table with function ajaxParams
let element = document.createElement("div");
let customTable = new TabulatorFull(element, {
ajaxURL: 'test.json',
ajaxParams: function() {
return {dynamic: true, timestamp: 12345};
}
});
let customAjax = customTable.module("ajax");
let result = customAjax.requestParams(null, null, false, {sort: 'name'});
expect(result).toEqual({dynamic: true, timestamp: 12345, sort: 'name'});
customTable.destroy();
});
test('requestDataCheck returns true for string data with url', function(){
expect(ajax.requestDataCheck('stringData')).toBe(true);
});
test('requestDataCheck returns false for object data', function(){
expect(ajax.requestDataCheck({id: 1, name: 'test'})).toBe(false);
});
// This test needs special handling since the requestDataCheck behavior depends on ajax.url
test('requestDataCheck returns true for null data with url', function(){
// Ensure url is set
ajax.url = 'fake-data-url.json';
expect(ajax.requestDataCheck(null)).toBe(true);
});
test('sendRequest calls ajaxRequesting callback', function(){
// Create a table with custom ajaxRequesting callback
let requestingCalled = false;
let element = document.createElement("div");
let customTable = new TabulatorFull(element, {
ajaxURL: 'test.json',
ajaxRequesting: function() {
requestingCalled = true;
return true; // Allow request to proceed
}
});
let customAjax = customTable.module("ajax");
customAjax.loaderPromise = jest.fn().mockResolvedValue({data: []});
customAjax.sendRequest('test.json', {}, {});
expect(requestingCalled).toBe(true);
expect(customAjax.loaderPromise).toHaveBeenCalled();
customTable.destroy();
});
test('sendRequest handles canceled requests', function(done){
// Create a table with ajaxRequesting that cancels requests
let element = document.createElement("div");
let customTable = new TabulatorFull(element, {
ajaxURL: 'test.json',
ajaxRequesting: function() {
return false; // Cancel the request
}
});
let customAjax = customTable.module("ajax");
customAjax.loaderPromise = jest.fn();
// Using Promise catch to handle the rejection
customAjax.sendRequest('test.json', {}, {})
.catch(() => {
// Verify the loader was not called
expect(customAjax.loaderPromise).not.toHaveBeenCalled();
customTable.destroy();
done();
});
});
});

View File

@@ -0,0 +1,184 @@
import TabulatorFull from '../../../src/js/core/TabulatorFull.js';
describe('Clipboard', function(){
let table, clipboard;
let tableData = [
{id:1, name:"John", age:20},
{id:2, name:"Jane", age:25},
{id:3, name:"Steve", age:30}
];
let tableColumns = [
{title:"ID", field:"id"},
{title:"Name", field:"name"},
{title:"Age", field:"age"}
];
beforeEach(function(){
let element = document.createElement("div");
table = new TabulatorFull(element, {
data:tableData,
columns:tableColumns,
clipboard:true
});
clipboard = table.module("clipboard");
});
afterEach(function(){
table.destroy();
});
test('module is initialized', function(){
expect(clipboard).toBeDefined();
expect(clipboard.mode).toBe(true);
});
test('setPasteParser accepts string parameter', function(){
const consoleSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
clipboard.setPasteParser('table');
expect(typeof clipboard.pasteParser).toBe('function');
expect(consoleSpy).not.toHaveBeenCalled();
consoleSpy.mockRestore();
});
test('setPasteParser warns on invalid parser name', function(){
const consoleSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
clipboard.setPasteParser('invalid');
expect(consoleSpy).toHaveBeenCalled();
consoleSpy.mockRestore();
});
test('setPasteParser accepts function parameter', function(){
const customParser = function(clipboard){
return [{id:99, name:"Test", age:99}];
};
clipboard.setPasteParser(customParser);
expect(clipboard.pasteParser).toBe(customParser);
});
test('setPasteAction accepts string parameter', function(){
const consoleSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
clipboard.setPasteAction('insert');
expect(typeof clipboard.pasteAction).toBe('function');
expect(consoleSpy).not.toHaveBeenCalled();
consoleSpy.mockRestore();
});
test('setPasteAction warns on invalid action name', function(){
const consoleSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
clipboard.setPasteAction('invalid');
expect(consoleSpy).toHaveBeenCalled();
consoleSpy.mockRestore();
});
test('setPasteAction accepts function parameter', function(){
const customAction = function(rows){
return rows;
};
clipboard.setPasteAction(customAction);
expect(clipboard.pasteAction).toBe(customAction);
});
test('generatePlainContent creates tab-delimited text', function(){
const testData = [
{columns: [{value: "A1"}, {value: "B1"}, {value: "C1"}]},
{columns: [{value: "A2"}, {value: "B2"}, {value: "C2"}]}
];
const result = clipboard.generatePlainContent(testData);
expect(result).toBe("A1\tB1\tC1\nA2\tB2\tC2");
});
test('generatePlainContent handles different value types', function(){
const testData = [
{columns: [
{value: "text"},
{value: 123},
{value: null},
{value: undefined},
{value: {test: "object"}}
]}
];
const result = clipboard.generatePlainContent(testData);
expect(result).toBe("text\t123\t\t\t{\"test\":\"object\"}");
});
test('reset clears custom selection and blocks copying', function(){
clipboard.blocked = false;
clipboard.customSelection = "test";
clipboard.reset();
expect(clipboard.blocked).toBe(true);
expect(clipboard.customSelection).toBe(false);
});
test('mutateData transforms row data', function(){
// Mock mutator module
table.modules.mutator = {
transformRow: jest.fn(row => ({ ...row, transformed: true }))
};
const testData = [
{id: 1, name: "Test"}
];
const result = clipboard.mutateData(testData);
expect(table.modules.mutator.transformRow).toHaveBeenCalledWith(
{id: 1, name: "Test"},
"clipboard"
);
expect(result[0].transformed).toBe(true);
});
test('checkPasteOrigin validates paste targets', function(){
const divTarget = {target: {tagName: "DIV"}};
const spanTarget = {target: {tagName: "SPAN"}};
const invalidTarget = {target: {tagName: "INPUT"}};
// Mock confirm method
clipboard.confirm = jest.fn(() => false);
expect(clipboard.checkPasteOrigin(divTarget)).toBe(true);
expect(clipboard.checkPasteOrigin(spanTarget)).toBe(true);
expect(clipboard.checkPasteOrigin(invalidTarget)).toBe(false);
// Test when blocked by confirm
clipboard.confirm = jest.fn(() => true);
expect(clipboard.checkPasteOrigin(divTarget)).toBe(false);
});
test('getPasteData extracts clipboard text', function(){
// Test with clipboardData
const event1 = {
clipboardData: {
getData: jest.fn().mockReturnValue("test data")
}
};
expect(clipboard.getPasteData(event1)).toBe("test data");
expect(event1.clipboardData.getData).toHaveBeenCalledWith("text/plain");
// Test with originalEvent
const event2 = {
originalEvent: {
clipboardData: {
getData: jest.fn().mockReturnValue("original data")
}
}
};
expect(clipboard.getPasteData(event2)).toBe("original data");
});
});

View File

@@ -0,0 +1,269 @@
import Module from '../../../src/js/core/Module.js';
import ColumnCalcs from '../../../src/js/modules/ColumnCalcs/ColumnCalcs.js';
// Override the Module methods that interact with the table to avoid dependency issues
const originalRegisterTableOption = Module.prototype.registerTableOption;
Module.prototype.registerTableOption = function() {};
const originalRegisterColumnOption = Module.prototype.registerColumnOption;
Module.prototype.registerColumnOption = function() {};
// Define test calculation functions
const testCalcs = {
'avg': function(values, data){
var output = 0;
var count = 0;
if(!values.length){
return 0;
}
values.forEach(function(value){
output += Number(value);
count++;
});
return output / count;
},
'max': function(values, data){
var output = null;
values.forEach(function(value){
if(value > output || output === null){
output = value;
}
});
return output;
},
'min': function(values, data){
var output = null;
values.forEach(function(value){
if(value < output || output === null){
output = value;
}
});
return output;
},
'sum': function(values, data){
var output = 0;
values.forEach(function(value){
output += Number(value);
});
return output;
}
};
describe('ColumnCalcs', function(){
beforeAll(function() {
// Ensure calculations are available for tests
ColumnCalcs.calculations = testCalcs;
});
// Test direct functionality without a complete table instance
describe('Functionality tests', function() {
test('initializeColumn sets up calculations correctly', function(){
// Create a mock column
const mockColumn = {
definition: {
topCalc: "avg",
topCalcParams: { test: true },
bottomCalc: "max"
},
modules: {}
};
// Create a mock context with required properties
const mockContext = {
table: {options: {}},
topCalcs: [], // Initialize the required array
botCalcs: [], // Initialize the required array
initializeTopRow: jest.fn(), // Mock the initialization function
initializeBottomRow: jest.fn() // Mock the initialization function
};
// Call the function directly with the mock context
ColumnCalcs.prototype.initializeColumn.call(mockContext, mockColumn);
// Check the results
expect(mockColumn.modules.columnCalcs).toBeDefined();
expect(mockColumn.modules.columnCalcs.topCalc).toBeDefined();
expect(mockColumn.modules.columnCalcs.botCalc).toBeDefined();
expect(typeof mockColumn.modules.columnCalcs.topCalc).toBe('function');
expect(typeof mockColumn.modules.columnCalcs.botCalc).toBe('function');
});
test('initializeColumn warns on invalid calculation', function(){
const consoleSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
// Create a column with invalid calculation type
const testColumn = {
definition: {
topCalc: "invalid"
},
modules: {}
};
// Create a mock context with required properties
const mockContext = {
table: {options: {}},
topCalcs: [], // Initialize the required array
botCalcs: [], // Initialize the required array
initializeTopRow: jest.fn(), // Mock the initialization function
initializeBottomRow: jest.fn() // Mock the initialization function
};
// Call the function directly
ColumnCalcs.prototype.initializeColumn.call(mockContext, testColumn);
expect(consoleSpy).toHaveBeenCalled();
consoleSpy.mockRestore();
});
test('rowsToData extracts data from rows', function(){
// Create mock rows
const rows = [
{ getData: function() { return {id:1, name:"John", age:20}; } },
{ getData: function() { return {id:2, name:"Jane", age:25}; } }
];
// Mock table options needed for the function
const mockThis = {
table: {
options: {
dataTree: false,
dataTreeChildColumnCalcs: false
},
modules: {}
}
};
const data = ColumnCalcs.prototype.rowsToData.call(mockThis, rows);
expect(data.length).toBe(rows.length);
expect(data[0].id).toBe(1);
expect(data[1].name).toBe("Jane");
});
test('generateRowData creates calculation results', function(){
// Create mock data
const data = [
{age: 20, total: 100},
{age: 25, total: 200},
{age: 30, total: 300},
{age: 35, total: 400}
];
// Create mock columns
const ageColumn = {
modules: {
columnCalcs: {
topCalc: testCalcs.avg,
botCalc: testCalcs.max,
topCalcParams: {},
botCalcParams: {}
}
},
getFieldValue: function(row) { return row.age; },
setFieldValue: function(row, value) { row.age = value; }
};
const totalColumn = {
modules: {
columnCalcs: {
topCalc: testCalcs.sum,
botCalc: testCalcs.sum,
topCalcParams: {},
botCalcParams: {}
}
},
getFieldValue: function(row) { return row.total; },
setFieldValue: function(row, value) { row.total = value; }
};
// Create a mock context object with the needed properties
const mockThis = {
topCalcs: [ageColumn, totalColumn],
botCalcs: [ageColumn, totalColumn],
table: {}
};
// Test top calculations
const topData = ColumnCalcs.prototype.generateRowData.call(mockThis, "top", data);
expect(topData.age).toBe(27.5); // avg of [20,25,30,35]
expect(topData.total).toBe(1000); // sum of [100,200,300,400]
// Test bottom calculations
const botData = ColumnCalcs.prototype.generateRowData.call(mockThis, "bottom", data);
expect(botData.age).toBe(35); // max of [20,25,30,35]
expect(botData.total).toBe(1000); // sum of [100,200,300,400]
});
test('blockCheck manages blocking state', function(){
// Create a mock object with the properties needed by blockCheck
const mockObj = {
blocked: false,
recalcAfterBlock: false
};
// Initially not blocked
expect(ColumnCalcs.prototype.blockCheck.call(mockObj)).toBe(false);
expect(mockObj.recalcAfterBlock).toBe(false);
// Set to blocked
mockObj.blocked = true;
expect(ColumnCalcs.prototype.blockCheck.call(mockObj)).toBe(true);
expect(mockObj.recalcAfterBlock).toBe(true);
// Restore from blocked
mockObj.blocked = false;
expect(ColumnCalcs.prototype.blockCheck.call(mockObj)).toBe(false);
});
test('cellValueChanged triggers recalc for relevant columns', function(){
// Create mock object for 'this' context
const mockThis = {
recalcActiveRows: jest.fn(),
recalcRowGroup: jest.fn(),
table: { options: { groupBy: false } }
};
// Create a cell in a column with calcs
const cell = {
column: {
definition: {
topCalc: "avg",
bottomCalc: "max"
}
},
row: { /* mock row */ }
};
// Test with calc column
ColumnCalcs.prototype.cellValueChanged.call(mockThis, cell);
expect(mockThis.recalcActiveRows).toHaveBeenCalled();
// Reset mock
mockThis.recalcActiveRows.mockReset();
// Create a cell in a column without calcs
const cell2 = {
column: {
definition: { /* no calcs */ }
},
row: { /* mock row */ }
};
ColumnCalcs.prototype.cellValueChanged.call(mockThis, cell2);
expect(mockThis.recalcActiveRows).not.toHaveBeenCalled();
});
});
});
// Restore original Module methods after all tests
afterAll(() => {
Module.prototype.registerTableOption = originalRegisterTableOption;
Module.prototype.registerColumnOption = originalRegisterColumnOption;
});

View File

@@ -0,0 +1,89 @@
import Module from '../../../src/js/core/Module.js';
import Comms from '../../../src/js/modules/Comms/Comms.js';
// Override the Module.prototype.registerTableFunction to avoid dependency on the full module system
const originalRegisterTableFunction = Module.prototype.registerTableFunction;
Module.prototype.registerTableFunction = function(name, func) {
this.table[name] = func.bind(this);
};
// This test file focuses only on the essential functionality of the Comms module
describe('Comms', function(){
let comms;
let mockTable;
beforeEach(function(){
// Create mock element
const element = document.createElement("div");
element.id = "table1";
document.body.appendChild(element);
// Create mock table with only what we need for Comms
mockTable = {
element: element,
modExists: jest.fn(),
modules: {
testModule: {
commsReceived: jest.fn()
}
},
initGuard: jest.fn() // Add mock initGuard
};
// Create the module to test
comms = new Comms(mockTable);
// Manually assign receive function without initialization
mockTable.tableComms = function(sourceTable, module, action, data) {
return comms.receive(sourceTable, module, action, data);
};
});
afterEach(function(){
document.body.removeChild(document.getElementById("table1"));
});
test('module can be created', function(){
expect(comms).toBeDefined();
expect(comms.table).toBe(mockTable);
});
test('receive calls module commsReceived method', function(){
// Mock the modExists method to return true
mockTable.modExists.mockReturnValue(true);
// Mock sourceTable
const mockSourceTable = document.createElement("div");
// Call receive
comms.receive(mockSourceTable, "testModule", "testAction", {test: "data"});
// Verify the module's commsReceived was called correctly
expect(mockTable.modExists).toHaveBeenCalledWith("testModule");
expect(mockTable.modules.testModule.commsReceived).toHaveBeenCalledWith(
mockSourceTable,
"testAction",
{test: "data"}
);
});
test('receive warns if module does not exist', function(){
const consoleSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
// Mock the modExists method to return false
mockTable.modExists.mockReturnValue(false);
// Mock sourceTable
const mockSourceTable = document.createElement("div");
comms.receive(mockSourceTable, "nonexistentModule", "testAction", {test: "data"});
expect(consoleSpy).toHaveBeenCalled();
consoleSpy.mockRestore();
});
});
// Restore the original method after tests
afterAll(() => {
Module.prototype.registerTableFunction = originalRegisterTableFunction;
});

View File

@@ -0,0 +1,473 @@
import Module from '../../../src/js/core/Module.js';
import DataTree from '../../../src/js/modules/DataTree/DataTree.js';
// Override the Module methods that interact with the table to avoid dependency issues
const originalRegisterTableOption = Module.prototype.registerTableOption;
Module.prototype.registerTableOption = function() {};
const originalRegisterColumnOption = Module.prototype.registerColumnOption;
Module.prototype.registerColumnOption = function() {};
const originalRegisterComponentFunction = Module.prototype.registerComponentFunction;
Module.prototype.registerComponentFunction = function() {};
// Mock CoreFeature methods
DataTree.prototype.subscribe = jest.fn();
DataTree.prototype.registerDisplayHandler = jest.fn();
DataTree.prototype.dispatchExternal = jest.fn();
describe('DataTree', function(){
// Restore original Module methods after all tests
afterAll(() => {
Module.prototype.registerTableOption = originalRegisterTableOption;
Module.prototype.registerColumnOption = originalRegisterColumnOption;
Module.prototype.registerComponentFunction = originalRegisterComponentFunction;
});
// Test direct functionality without a complete table instance
describe('Functionality tests', function() {
test('initialize sets properties based on options', function(){
// Mock document methods for DOM element creation
const mockElement = {
classList: {
add: jest.fn()
},
innerHTML: '',
appendChild: jest.fn(),
tabIndex: 0
};
const originalCreateElement = document.createElement;
document.createElement = jest.fn().mockReturnValue({...mockElement});
// Create mock table with options
const mockTable = {
options: {
dataTree: true,
dataTreeChildField: "children",
dataTreeChildIndent: 15,
dataTreeBranchElement: true,
dataTreeStartExpanded: false,
movableRows: false
},
columnManager: {
getFirstVisibleColumn: jest.fn().mockReturnValue({
field: "name"
})
}
};
// Create the module instance
const dataTree = new DataTree(mockTable);
// Call initialize
dataTree.initialize();
// Check that properties were set correctly
expect(dataTree.field).toBe("children");
expect(dataTree.indent).toBe(15);
expect(dataTree.subscribe).toHaveBeenCalled();
expect(dataTree.registerDisplayHandler).toHaveBeenCalled();
// Restore original createElement
document.createElement = originalCreateElement;
});
test('startOpen function is correctly configured', function(){
// Test with boolean option
const mockTableBoolean = {
options: {
dataTree: true,
dataTreeStartExpanded: true
}
};
const dataTreeBoolean = new DataTree(mockTableBoolean);
dataTreeBoolean.options = jest.fn().mockReturnValue(false); // Mock options to prevent other dependency issues
// Mock the initialize method to not require full initialization
const originalInitialize = dataTreeBoolean.initialize;
dataTreeBoolean.initialize = function() {
// Extract just the startOpen configuration part
switch(typeof this.table.options.dataTreeStartExpanded){
case "boolean":
this.startOpen = function(row, index){
return this.table.options.dataTreeStartExpanded;
};
break;
}
};
dataTreeBoolean.initialize();
// Should return true for any row
expect(dataTreeBoolean.startOpen({}, 0)).toBe(true);
// Restore original method
dataTreeBoolean.initialize = originalInitialize;
// Test with function option
const customFn = jest.fn().mockImplementation((row, index) => index > 1);
const mockTableFunction = {
options: {
dataTree: true,
dataTreeStartExpanded: customFn
}
};
const dataTreeFunction = new DataTree(mockTableFunction);
dataTreeFunction.options = jest.fn().mockReturnValue(false);
// Mock the initialize method
dataTreeFunction.initialize = function() {
switch(typeof this.table.options.dataTreeStartExpanded){
case "function":
this.startOpen = this.table.options.dataTreeStartExpanded;
break;
}
};
dataTreeFunction.initialize();
// Should use the provided function
expect(dataTreeFunction.startOpen).toBe(customFn);
expect(dataTreeFunction.startOpen({}, 2)).toBe(true);
expect(dataTreeFunction.startOpen({}, 0)).toBe(false);
// Test with array option
const mockTableArray = {
options: {
dataTree: true,
dataTreeStartExpanded: [true, false, true]
}
};
const dataTreeArray = new DataTree(mockTableArray);
dataTreeArray.options = jest.fn().mockReturnValue(false);
// Mock the initialize method
dataTreeArray.initialize = function() {
// This is the relevant part from the actual initialize method
switch(typeof this.table.options.dataTreeStartExpanded){
default:
this.startOpen = function(row, index){
return this.table.options.dataTreeStartExpanded[index];
};
break;
}
};
dataTreeArray.initialize();
// Should return value from array based on index
expect(dataTreeArray.startOpen({}, 0)).toBe(true);
expect(dataTreeArray.startOpen({}, 1)).toBe(false);
expect(dataTreeArray.startOpen({}, 2)).toBe(true);
});
test('initializeRow sets up row.modules.dataTree correctly', function(){
// Create a dataTree instance with minimal options
const dataTree = new DataTree({
options: {dataTree: true, dataTreeChildField: "children"}
});
// Set up required properties manually
dataTree.field = "children";
// Mock the startOpen function to avoid dependencies
dataTree.startOpen = jest.fn().mockReturnValue(true);
// Case 1: Row with children as array
const rowWithChildren = {
getData: jest.fn().mockReturnValue({
children: [{id: 1}, {id: 2}]
}),
modules: {},
getComponent: jest.fn().mockReturnValue({})
};
dataTree.initializeRow(rowWithChildren);
expect(rowWithChildren.modules.dataTree).toBeDefined();
expect(rowWithChildren.modules.dataTree.children).toBe(true);
expect(rowWithChildren.modules.dataTree.open).toBe(true);
// Case 2: Row with children as object
const rowWithObjectChildren = {
getData: jest.fn().mockReturnValue({
children: {id: 1, name: "Child"}
}),
modules: {},
getComponent: jest.fn().mockReturnValue({})
};
dataTree.initializeRow(rowWithObjectChildren);
expect(rowWithObjectChildren.modules.dataTree).toBeDefined();
expect(rowWithObjectChildren.modules.dataTree.children).toBe(true);
// Case 3: Row without children
const rowWithoutChildren = {
getData: jest.fn().mockReturnValue({
name: "No children"
}),
modules: {},
getComponent: jest.fn().mockReturnValue({})
};
dataTree.initializeRow(rowWithoutChildren);
expect(rowWithoutChildren.modules.dataTree).toBeDefined();
expect(rowWithoutChildren.modules.dataTree.children).toBe(false);
});
test('expandRow sets open to true and refreshes data', function(){
// Create a dataTree instance with minimal options
const dataTree = new DataTree({
options: {dataTree: true}
});
// Mock methods
dataTree.refreshData = jest.fn();
dataTree.dispatchExternal = jest.fn();
// Create a row with children
const row = {
modules: {
dataTree: {
children: true,
open: false,
index: 1
}
},
reinitialize: jest.fn(),
getComponent: jest.fn().mockReturnValue({})
};
dataTree.expandRow(row);
expect(row.modules.dataTree.open).toBe(true);
expect(row.reinitialize).toHaveBeenCalled();
expect(dataTree.refreshData).toHaveBeenCalledWith(true);
expect(dataTree.dispatchExternal).toHaveBeenCalledWith("dataTreeRowExpanded", expect.anything(), 1);
});
test('collapseRow sets open to false and refreshes data', function(){
// Create a dataTree instance with minimal options
const dataTree = new DataTree({
options: {dataTree: true}
});
// Mock methods
dataTree.refreshData = jest.fn();
dataTree.dispatchExternal = jest.fn();
// Create a row with children
const row = {
modules: {
dataTree: {
children: true,
open: true,
index: 1
}
},
reinitialize: jest.fn(),
getComponent: jest.fn().mockReturnValue({})
};
dataTree.collapseRow(row);
expect(row.modules.dataTree.open).toBe(false);
expect(row.reinitialize).toHaveBeenCalled();
expect(dataTree.refreshData).toHaveBeenCalledWith(true);
expect(dataTree.dispatchExternal).toHaveBeenCalledWith("dataTreeRowCollapsed", expect.anything(), 1);
});
test('toggleRow calls appropriate function based on open state', function(){
// Create a dataTree instance with minimal options
const dataTree = new DataTree({
options: {dataTree: true}
});
// Mock methods
dataTree.expandRow = jest.fn();
dataTree.collapseRow = jest.fn();
// Test with closed row
const closedRow = {
modules: {
dataTree: {
children: true,
open: false
}
}
};
dataTree.toggleRow(closedRow);
expect(dataTree.expandRow).toHaveBeenCalledWith(closedRow);
expect(dataTree.collapseRow).not.toHaveBeenCalled();
dataTree.expandRow.mockClear();
dataTree.collapseRow.mockClear();
// Test with open row
const openRow = {
modules: {
dataTree: {
children: true,
open: true
}
}
};
dataTree.toggleRow(openRow);
expect(dataTree.collapseRow).toHaveBeenCalledWith(openRow);
expect(dataTree.expandRow).not.toHaveBeenCalled();
});
test('isRowExpanded returns correct state', function(){
// Create a dataTree instance with minimal options
const dataTree = new DataTree({
options: {dataTree: true}
});
const openRow = {
modules: {
dataTree: {
open: true
}
}
};
const closedRow = {
modules: {
dataTree: {
open: false
}
}
};
expect(dataTree.isRowExpanded(openRow)).toBe(true);
expect(dataTree.isRowExpanded(closedRow)).toBe(false);
});
test('getTreeParent returns parent component', function(){
// Create a dataTree instance with minimal options
const dataTree = new DataTree({
options: {dataTree: true}
});
const parentComponent = {};
const childRow = {
modules: {
dataTree: {
parent: {
getComponent: jest.fn().mockReturnValue(parentComponent)
}
}
}
};
const orphanRow = {
modules: {
dataTree: {
parent: false
}
}
};
expect(dataTree.getTreeParent(childRow)).toBe(parentComponent);
expect(dataTree.getTreeParent(orphanRow)).toBe(false);
});
test('getTreeChildren returns children correctly', function(){
// Create a dataTree instance with minimal options
const dataTree = new DataTree({
options: {dataTree: true}
});
// In this modified approach, we'll directly mock the implementation of getTreeChildren
// instead of trying to modify Symbol.hasInstance which is read-only
const originalGetTreeChildren = dataTree.getTreeChildren;
dataTree.getTreeChildren = jest.fn((row, component, recurse) => {
if (row === rowWithChildren) {
if (component) {
return [{ component: 1 }, { component: 2 }];
} else {
return [mockRow1, mockRow2];
}
}
return [];
});
const mockRow1 = { id: 1 };
const mockRow2 = { id: 2 };
// Row with pre-generated children
const rowWithChildren = {
modules: {
dataTree: {
children: [mockRow1, mockRow2]
}
}
};
// Get children without component conversion
const childrenAsRows = dataTree.getTreeChildren(rowWithChildren, false, false);
expect(childrenAsRows.length).toBe(2);
expect(childrenAsRows[0]).toBe(mockRow1);
expect(childrenAsRows[1]).toBe(mockRow2);
// Get children with component conversion
const childrenAsComponents = dataTree.getTreeChildren(rowWithChildren, true, false);
expect(childrenAsComponents.length).toBe(2);
expect(childrenAsComponents[0]).toEqual({ component: 1 });
expect(childrenAsComponents[1]).toEqual({ component: 2 });
// Restore original method
dataTree.getTreeChildren = originalGetTreeChildren;
});
test('addTreeChildRow adds a child row correctly', function(){
// Create a dataTree instance with minimal options
const dataTree = new DataTree({
options: {dataTree: true}
});
dataTree.field = "children";
dataTree.startOpen = jest.fn().mockReturnValue(true);
dataTree.initializeRow = jest.fn();
dataTree.layoutRow = jest.fn();
dataTree.refreshData = jest.fn();
// Create a parent row without children initially
const row = {
data: {},
modules: {
dataTree: {
index: 1
}
},
getComponent: jest.fn().mockReturnValue({})
};
// Add a child to the top
dataTree.addTreeChildRow(row, {id: 1, name: "Child 1"}, true);
expect(row.data.children).toBeDefined();
expect(row.data.children.length).toBe(1);
expect(row.data.children[0].id).toBe(1);
expect(dataTree.initializeRow).toHaveBeenCalledWith(row);
expect(dataTree.layoutRow).toHaveBeenCalledWith(row);
expect(dataTree.refreshData).toHaveBeenCalledWith(true);
// Add another child to the bottom
dataTree.addTreeChildRow(row, {id: 2, name: "Child 2"}, false);
expect(row.data.children.length).toBe(2);
expect(row.data.children[1].id).toBe(2);
});
});
});

View File

@@ -0,0 +1,332 @@
import Module from '../../../src/js/core/Module.js';
import Download from '../../../src/js/modules/Download/Download.js';
// Override the Module methods that interact with the table to avoid dependency issues
const originalRegisterTableOption = Module.prototype.registerTableOption;
Module.prototype.registerTableOption = function() {};
const originalRegisterColumnOption = Module.prototype.registerColumnOption;
Module.prototype.registerColumnOption = function() {};
const originalRegisterTableFunction = Module.prototype.registerTableFunction;
Module.prototype.registerTableFunction = function() {};
describe('Download', function(){
// Restore original Module methods after all tests
afterAll(() => {
Module.prototype.registerTableOption = originalRegisterTableOption;
Module.prototype.registerColumnOption = originalRegisterColumnOption;
Module.prototype.registerTableFunction = originalRegisterTableFunction;
});
// Test direct functionality without a complete table instance
describe('Functionality tests', function() {
test('initialize registers table functions', function(){
// Create mock table
const mockTable = {
options: {}
};
// Create a download instance
const download = new Download(mockTable);
// Mock the register function
download.registerTableFunction = jest.fn();
download.deprecatedOptionsCheck = jest.fn();
// Initialize the module
download.initialize();
// Check that the table functions were registered
expect(download.deprecatedOptionsCheck).toHaveBeenCalled();
expect(download.registerTableFunction).toHaveBeenCalledWith("download", expect.any(Function));
expect(download.registerTableFunction).toHaveBeenCalledWith("downloadToTab", expect.any(Function));
});
test('downloadToTab calls download with correct parameters', function(){
// Create mock table
const mockTable = {
options: {}
};
// Create a download instance
const download = new Download(mockTable);
// Mock the download method
download.download = jest.fn();
// Call downloadToTab
download.downloadToTab("csv", "test.csv", {delimiter: ","}, "active");
// Check that download was called with the correct parameters
expect(download.download).toHaveBeenCalledWith("csv", "test.csv", {delimiter: ","}, "active", true);
});
test('download warns when invalid type is provided', function(){
// Create mock table with export module and options
const mockTable = {
modules: {
export: {
generateExportList: jest.fn().mockReturnValue([])
}
},
options: {
downloadConfig: {},
downloadRowRange: "active"
}
};
// Create a download instance
const download = new Download(mockTable);
download.table = mockTable;
// Mock console.warn
const consoleSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
// Call download with invalid type
download.download("invalidType", "test.file");
// Check that warning was shown
expect(consoleSpy).toHaveBeenCalledWith("Download Error - No such download type found: ", "invalidType");
consoleSpy.mockRestore();
});
test('download calls downloader function with correct parameters', function(){
// Create mock downloader function
const mockDownloader = jest.fn();
Download.downloaders = {
csv: mockDownloader
};
// Create mock export list
const mockExportList = [{type: "data", columns: []}];
// Create mock table with export module
const mockTable = {
modules: {
export: {
generateExportList: jest.fn().mockReturnValue(mockExportList)
}
},
options: {
downloadConfig: {},
downloadRowRange: "active"
}
};
// Create a download instance
const download = new Download(mockTable);
download.table = mockTable;
// Call download
const options = {delimiter: ","};
download.download("csv", "test.csv", options);
// Check that generateExportList was called
expect(mockTable.modules.export.generateExportList).toHaveBeenCalledWith(
{}, false, "active", "download"
);
// Check that downloader was called with correct parameters
expect(mockDownloader).toHaveBeenCalledWith(
mockExportList,
options,
expect.any(Function)
);
});
test('download accepts a custom function as a downloader', function(){
// Create mock custom downloader function
const customDownloader = jest.fn();
// Create mock export list
const mockExportList = [{type: "data", columns: []}];
// Create mock table with export module
const mockTable = {
modules: {
export: {
generateExportList: jest.fn().mockReturnValue(mockExportList)
}
},
options: {
downloadConfig: {},
downloadRowRange: "active"
}
};
// Create a download instance
const download = new Download(mockTable);
download.table = mockTable;
// Call download with custom function
const options = {custom: true};
download.download(customDownloader, "test.file", options);
// Check that the custom downloader was called with correct parameters
expect(customDownloader).toHaveBeenCalledWith(
mockExportList,
options,
expect.any(Function)
);
});
test('generateExportList handles group headers correctly', function(){
// Create mock export list with a group row
const mockComponent = {
_group: {
getRowCount: jest.fn().mockReturnValue(5),
getData: jest.fn().mockReturnValue({id: "group1"})
}
};
const mockExportList = [
{
type: "group",
indent: 0,
columns: [{value: "Group 1"}],
component: mockComponent
}
];
// Create mock table with export module and group header formatter
const groupHeaderFormatter = jest.fn().mockReturnValue("Custom Group Header");
const mockTable = {
modules: {
export: {
generateExportList: jest.fn().mockReturnValue(mockExportList)
}
},
options: {
downloadConfig: {},
downloadRowRange: "active",
groupHeaderDownload: groupHeaderFormatter
}
};
// Create a download instance
const download = new Download(mockTable);
download.table = mockTable;
// Call generateExportList
const result = download.generateExportList();
// Check that the group header formatter was called
expect(groupHeaderFormatter).toHaveBeenCalledWith(
"Group 1",
5,
{id: "group1"},
mockComponent
);
// Check that the group value was updated
expect(result[0].columns[0].value).toBe("Custom Group Header");
});
test('triggerDownload creates and triggers download element', function(){
// Mock document methods
const mockElement = {
setAttribute: jest.fn(),
style: {},
click: jest.fn()
};
document.createElement = jest.fn().mockReturnValue(mockElement);
document.body.appendChild = jest.fn();
document.body.removeChild = jest.fn();
// Mock URL.createObjectURL
const mockURL = "blob:url";
window.URL.createObjectURL = jest.fn().mockReturnValue(mockURL);
// Create mock table with downloadEncoder
const mockBlob = new Blob(["test data"], {type: "text/csv"});
const mockTable = {
options: {
downloadEncoder: jest.fn().mockReturnValue(mockBlob)
}
};
// Create a download instance
const download = new Download(mockTable);
download.table = mockTable;
download.dispatchExternal = jest.fn();
// Call triggerDownload
download.triggerDownload("test data", "text/csv", "csv", "test.csv");
// Check that downloadEncoder was called
expect(mockTable.options.downloadEncoder).toHaveBeenCalledWith("test data", "text/csv");
// Check that URL.createObjectURL was called with the blob
expect(window.URL.createObjectURL).toHaveBeenCalledWith(mockBlob);
// Check that the element was properly configured
expect(mockElement.setAttribute).toHaveBeenCalledWith("href", mockURL);
expect(mockElement.setAttribute).toHaveBeenCalledWith("download", "test.csv");
expect(mockElement.style.display).toBe("none");
// Check that the element was added, clicked, and removed
expect(document.body.appendChild).toHaveBeenCalledWith(mockElement);
expect(mockElement.click).toHaveBeenCalled();
expect(document.body.removeChild).toHaveBeenCalledWith(mockElement);
// Check that external event was dispatched
expect(download.dispatchExternal).toHaveBeenCalledWith("downloadComplete");
});
test('triggerDownload opens in new tab when newTab is true', function(){
// Mock window.open
window.open = jest.fn();
// Mock URL.createObjectURL
const mockURL = "blob:url";
window.URL.createObjectURL = jest.fn().mockReturnValue(mockURL);
// Create mock table with downloadEncoder
const mockBlob = new Blob(["test data"], {type: "text/csv"});
const mockTable = {
options: {
downloadEncoder: jest.fn().mockReturnValue(mockBlob)
}
};
// Create a download instance
const download = new Download(mockTable);
download.table = mockTable;
download.dispatchExternal = jest.fn();
// Call triggerDownload with newTab = true
download.triggerDownload("test data", "text/csv", "csv", "test.csv", true);
// Check that window.open was called with the blob URL
expect(window.open).toHaveBeenCalledWith(mockURL);
// Check that external event was dispatched
expect(download.dispatchExternal).toHaveBeenCalledWith("downloadComplete");
});
test('commsReceived handles intercept action correctly', function(){
// Create a download instance
const download = new Download({});
// Mock the download method
download.download = jest.fn();
// Call commsReceived with intercept action
download.commsReceived(null, "intercept", {
type: "csv",
options: {delimiter: ","},
active: true,
intercept: true
});
// Check that download was called with the correct parameters
expect(download.download).toHaveBeenCalledWith(
"csv", "", {delimiter: ","}, true, true
);
});
});
});

View File

@@ -0,0 +1,128 @@
import TabulatorFull from '../../../src/js/core/TabulatorFull.js';
import Edit from '../../../src/js/modules/Edit/Edit.js';
describe("Edit module", () => {
let table;
// Helper function to create a table and wait for it to be built
const setupTable = async (tableOptions = {}) => {
document.body.innerHTML = '<div id="test-table"></div>';
const defaultOptions = {
data: [
{ id: 1, name: "John", age: 25 },
{ id: 2, name: "Jane", age: 30 },
{ id: 3, name: "Bob", age: 35 }
],
columns: [
{ title: "ID", field: "id" },
{ title: "Name", field: "name", editor: "input" },
{ title: "Age", field: "age", editor: "number" }
]
};
// Create table with merged options
const options = { ...defaultOptions, ...tableOptions };
const newTable = new TabulatorFull("#test-table", options);
// Wait for table to be built
await new Promise(resolve => {
newTable.on("tableBuilt", resolve);
});
return newTable;
};
beforeEach(async () => {
table = await setupTable();
});
afterEach(() => {
if(table) {
table.destroy();
}
});
it("should have edit module", () => {
const edit = table.module("edit");
expect(edit).toBeInstanceOf(Edit);
});
it("should have predefined editors", () => {
// Check that various editors are defined
const edit = table.module("edit");
expect(edit.editors).toBeDefined();
expect(typeof edit.editors.input).toBe("function");
expect(typeof edit.editors.textarea).toBe("function");
expect(typeof edit.editors.number).toBe("function");
});
it("should update cell values", async () => {
// Get a cell
const row = table.getRows()[0];
const cell = row.getCell("name");
// Initial value
const initialValue = cell.getValue();
expect(initialValue).toBe("John");
// Update value and check it was changed
const newValue = "Updated Name";
cell.setValue(newValue);
expect(cell.getValue()).toBe(newValue);
expect(row.getData().name).toBe(newValue);
});
it("should emit cellEdited event when value changes", async () => {
// Listen for the cellEdited event
const editPromise = new Promise(resolve => {
table.on("cellEdited", function(cell) {
// Verify the changed cell
expect(cell.getValue()).toBe("Updated via Event");
expect(cell.getField()).toBe("name");
resolve();
});
});
// Change a cell value to trigger the event
const cell = table.getRows()[0].getCell("name");
cell.setValue("Updated via Event");
// Wait for the event
await editPromise;
});
it("should add editors to editor types object", () => {
const edit = table.module("edit");
const initialEditorCount = Object.keys(edit.editors).length;
// Add a new editor
const customEditor = function(cell, onRendered, success, cancel) {
return document.createElement("input");
};
edit.editors.custom = customEditor;
// Check the editor was added
expect(Object.keys(edit.editors).length).toBe(initialEditorCount + 1);
expect(edit.editors.custom).toBe(customEditor);
});
it("should track edited events", async () => {
// Create a spy for the cellEdited event
const cellEditedSpy = jest.fn();
table.on("cellEdited", cellEditedSpy);
// Change a cell value
const cell = table.getRows()[0].getCell("name");
cell.setValue("New Value");
// Wait a bit for event processing
await new Promise(resolve => setTimeout(resolve, 100));
// Should have received the cellEdited event
expect(cellEditedSpy).toHaveBeenCalled();
});
});

View File

@@ -0,0 +1,462 @@
import Module from '../../../src/js/core/Module.js';
import Export from '../../../src/js/modules/Export/Export.js';
import ExportRow from '../../../src/js/modules/Export/ExportRow.js';
import ExportColumn from '../../../src/js/modules/Export/ExportColumn.js';
// Override the Module methods that interact with the table to avoid dependency issues
const originalRegisterTableOption = Module.prototype.registerTableOption;
Module.prototype.registerTableOption = function() {};
const originalRegisterColumnOption = Module.prototype.registerColumnOption;
Module.prototype.registerColumnOption = function() {};
const originalRegisterTableFunction = Module.prototype.registerTableFunction;
Module.prototype.registerTableFunction = function() {};
describe('Export', function(){
// Restore original Module methods after all tests
afterAll(() => {
Module.prototype.registerTableOption = originalRegisterTableOption;
Module.prototype.registerColumnOption = originalRegisterColumnOption;
Module.prototype.registerTableFunction = originalRegisterTableFunction;
});
// Test direct functionality without a complete table instance
describe('Functionality tests', function() {
test('initialize registers table functions', function(){
// Create mock table
const mockTable = {
options: {}
};
// Create an Export instance
const exportModule = new Export(mockTable);
// Mock the register function
exportModule.registerTableFunction = jest.fn();
// Initialize the module
exportModule.initialize();
// Check that the table functions were registered
expect(exportModule.registerTableFunction).toHaveBeenCalledWith("getHtml", expect.any(Function));
});
test('columnVisCheck properly evaluates column visibility', function(){
// Create a mock Export instance
const exportModule = new Export({});
exportModule.colVisProp = "htmlOutput";
exportModule.config = {};
// Test with explicit visible setting
const visibleColumn = {
definition: {
htmlOutput: true
},
visible: true,
field: "name"
};
expect(exportModule.columnVisCheck(visibleColumn)).toBe(true);
// Test with explicit hidden setting
const hiddenColumn = {
definition: {
htmlOutput: false
},
visible: true,
field: "name"
};
expect(exportModule.columnVisCheck(hiddenColumn)).toBe(false);
// Test with function
const mockFn = jest.fn().mockReturnValue(true);
const funcColumn = {
definition: {
htmlOutput: mockFn
},
visible: true,
field: "name",
getComponent: jest.fn().mockReturnValue({})
};
expect(exportModule.columnVisCheck(funcColumn)).toBe(true);
expect(mockFn).toHaveBeenCalled();
// Fix this test case for default column visibility
const defaultColumn = {
definition: {},
visible: true,
field: "name"
};
// This should return column.visible && column.field according to the function
expect(exportModule.columnVisCheck(defaultColumn)).toBe(defaultColumn.visible && defaultColumn.field);
// Test with row header when config.rowHeaders is false
exportModule.config.rowHeaders = false;
const rowHeaderColumn = {
definition: {},
visible: true,
field: "name",
isRowHeader: true
};
expect(exportModule.columnVisCheck(rowHeaderColumn)).toBe(false);
});
test('rowLookup returns correct rows based on range', function(){
// Define mock rows and mock table
const mockRows = [
{id: 1, name: "Row 1"},
{id: 2, name: "Row 2"},
{id: 3, name: "Row 3"}
];
// Create mock table with rowManager
const mockTable = {
rowManager: {
findRow: jest.fn(row => {
// Mock findRow to return the row if it exists in mockRows
const foundRow = mockRows.find(r => r.id === row);
return foundRow || false;
})
}
};
// Create a mock Export instance
const exportModule = new Export(mockTable);
exportModule.table = mockTable;
// Mock Export.rowLookups
Export.rowLookups = {
"active": jest.fn().mockReturnValue(mockRows),
"visible": jest.fn().mockReturnValue([mockRows[0], mockRows[1]])
};
// Test with string range
const activeRows = exportModule.rowLookup("active");
expect(activeRows).toEqual(mockRows);
expect(Export.rowLookups.active).toHaveBeenCalled();
const visibleRows = exportModule.rowLookup("visible");
expect(visibleRows).toEqual([mockRows[0], mockRows[1]]);
expect(Export.rowLookups.visible).toHaveBeenCalled();
// Test with function range
const customRange = jest.fn().mockReturnValue([1, 3]);
const customRows = exportModule.rowLookup(customRange);
expect(customRange).toHaveBeenCalled();
expect(mockTable.rowManager.findRow).toHaveBeenCalledWith(1);
expect(mockTable.rowManager.findRow).toHaveBeenCalledWith(3);
expect(customRows).toEqual([mockRows[0], mockRows[2]]);
});
test('generateExportList correctly assembles the list', function(){
// Create mock columns and table
const mockColumns = [
{
definition: { title: "Name" },
field: "name",
visible: true,
getComponent: jest.fn().mockReturnValue({_column: {getFieldValue: jest.fn()}})
},
{
definition: { title: "Age" },
field: "age",
visible: true,
getComponent: jest.fn().mockReturnValue({_column: {getFieldValue: jest.fn()}})
}
];
const mockTable = {
columnManager: {
columns: mockColumns,
columnsByIndex: mockColumns
},
options: {}
};
// Create mock Export instance with custom processColumnGroup
const exportModule = new Export(mockTable);
exportModule.table = mockTable;
// Mock required methods to avoid deep dependencies
exportModule.columnVisCheck = jest.fn().mockReturnValue(true);
exportModule.headersToExportRows = jest.fn().mockReturnValue(["header1", "header2"]);
exportModule.bodyToExportRows = jest.fn().mockReturnValue(["row1", "row2"]);
exportModule.rowLookup = jest.fn().mockReturnValue(["rowData1", "rowData2"]);
exportModule.generateColumnGroupHeaders = jest.fn().mockReturnValue(["group1", "group2"]);
// Mock Export.columnLookups
Export.columnLookups = {
"active": jest.fn().mockReturnValue(mockColumns)
};
// Call generateExportList
const result = exportModule.generateExportList({}, true, "active", "htmlOutput");
// Check that the result is correct
expect(result).toEqual(["header1", "header2", "row1", "row2"]);
expect(exportModule.cloneTableStyle).toBe(true);
expect(exportModule.config).toEqual({});
expect(exportModule.colVisProp).toBe("htmlOutput");
expect(exportModule.colVisPropAttach).toBe("HtmlOutput");
expect(Export.columnLookups.active).toHaveBeenCalled();
expect(exportModule.columnVisCheck).toHaveBeenCalled();
expect(exportModule.headersToExportRows).toHaveBeenCalled();
expect(exportModule.bodyToExportRows).toHaveBeenCalled();
});
test('generateHTMLTable creates a HTML table', function(){
// Create a mock DOM structure
const mockTable = document.createElement('table');
mockTable.innerHTML = '<tr><td>Test</td></tr>';
// Create mock Export instance
const exportModule = new Export({});
// Mock required methods
exportModule.generateTableElement = jest.fn().mockReturnValue(mockTable);
// Call generateHTMLTable
const result = exportModule.generateHTMLTable(["list item"]);
// Check that generateTableElement was called with the list
expect(exportModule.generateTableElement).toHaveBeenCalledWith(["list item"]);
// Check that the result is the HTML string representation of the table
expect(result).toContain("<table");
expect(result).toContain("<tr><td>Test</td></tr>");
});
test('getHtml generates HTML with correct parameters', function(){
// Create mock table
const mockTable = {
options: {
htmlOutputConfig: { custom: true }
}
};
// Create mock Export instance
const exportModule = new Export(mockTable);
exportModule.table = mockTable;
// Mock required methods
exportModule.generateExportList = jest.fn().mockReturnValue(["mock list"]);
exportModule.generateHTMLTable = jest.fn().mockReturnValue("<table>Mock HTML</table>");
// Call getHtml with default parameters
const result = exportModule.getHtml(true);
// Check that the methods were called with correct parameters
expect(exportModule.generateExportList).toHaveBeenCalledWith(
{ custom: true }, // config from table.options.htmlOutputConfig
undefined, // style
true, // visible
"htmlOutput" // colVisProp default
);
expect(exportModule.generateHTMLTable).toHaveBeenCalledWith(["mock list"]);
expect(result).toBe("<table>Mock HTML</table>");
// Call getHtml with custom parameters
exportModule.generateExportList.mockClear();
exportModule.generateHTMLTable.mockClear();
const customResult = exportModule.getHtml("active", true, { custom: false }, "downloadOutput");
// Check that the methods were called with custom parameters
expect(exportModule.generateExportList).toHaveBeenCalledWith(
{ custom: false }, // custom config
true, // custom style
"active", // custom visible
"downloadOutput" // custom colVisProp
);
expect(exportModule.generateHTMLTable).toHaveBeenCalledWith(["mock list"]);
expect(customResult).toBe("<table>Mock HTML</table>");
});
test('mapElementStyles maps styles from one element to another', function(){
// Create mock elements
const fromEl = document.createElement('div');
const toEl = document.createElement('div');
// Set inline styles on fromEl
fromEl.style.backgroundColor = 'red';
fromEl.style.color = 'blue';
fromEl.style.fontSize = '16px';
// Create a mock window.getComputedStyle
const originalGetComputedStyle = window.getComputedStyle;
window.getComputedStyle = jest.fn().mockReturnValue({
getPropertyValue: (prop) => {
switch(prop) {
case 'background-color': return 'red';
case 'color': return 'blue';
case 'font-size': return '16px';
default: return '';
}
}
});
// Create mock Export instance
const exportModule = new Export({});
exportModule.cloneTableStyle = true;
// Call mapElementStyles
exportModule.mapElementStyles(
fromEl,
toEl,
['background-color', 'color', 'font-size']
);
// Check that styles were copied
expect(toEl.style.backgroundColor).toBe('red');
expect(toEl.style.fontColor).toBe('blue');
expect(toEl.style.fontSize).toBe('16px');
// Restore original getComputedStyle
window.getComputedStyle = originalGetComputedStyle;
});
test('processColumnGroup correctly processes column groups', function(){
// Create mock Export instance
const exportModule = new Export({});
exportModule.colVisProp = "htmlOutput";
exportModule.colVisPropAttach = "HtmlOutput";
// Mock columnVisCheck to make testing more predictable
exportModule.columnVisCheck = jest.fn();
// Leaf column with no subgroups
const leafColumn = {
definition: { title: "Name", titleHtmlOutput: "Custom Name" },
columns: [], // Important: This needs to be defined for the test
visible: true,
field: "name"
};
// For simple leaf column that should be visible
exportModule.columnVisCheck.mockReturnValueOnce(true);
const leafResult = exportModule.processColumnGroup(leafColumn);
expect(leafResult).toEqual({
title: "Custom Name", // Should use titleHtmlOutput
column: leafColumn,
depth: 1,
width: 1
});
// For a column group with a visible child
const childLeafColumn = {
definition: { title: "Child" },
columns: [],
visible: true,
field: "child"
};
const columnGroupWithChild = {
definition: { title: "Group" },
columns: [childLeafColumn], // Important: needs to be defined
visible: true
};
// Mock to make the child visible
exportModule.columnVisCheck.mockReturnValueOnce(true);
// Create a custom processColumnGroup that handles recursion for the test
const originalProcessColumnGroup = exportModule.processColumnGroup;
exportModule.processColumnGroup = jest.fn(column => {
if (column === childLeafColumn) {
return {
title: "Child",
column: childLeafColumn,
depth: 1,
width: 1
};
}
return originalProcessColumnGroup.call(exportModule, column);
});
const groupResult = exportModule.processColumnGroup(columnGroupWithChild);
expect(groupResult).toEqual({
title: "Group",
column: columnGroupWithChild,
depth: 2,
width: 1,
subGroups: [{
title: "Child",
column: childLeafColumn,
depth: 1,
width: 1
}]
});
// For a column group with no visible children
const hiddenChildColumn = {
definition: { title: "Hidden" },
columns: [],
visible: false,
field: "hidden"
};
const emptyColumnGroup = {
definition: { title: "Empty Group" },
columns: [hiddenChildColumn],
visible: true
};
// Reset our mocks for this test case
exportModule.processColumnGroup = originalProcessColumnGroup; // Reset to original
// We need special handling for this test
// We'll mock processColumnGroup to return false for hiddenChildColumn
exportModule.processColumnGroup = jest.fn((column) => {
if (column === hiddenChildColumn) {
return false; // This simulates columnVisCheck returning false and resulting in no width
}
if (column === emptyColumnGroup) {
return originalProcessColumnGroup.call(exportModule, column);
}
});
const emptyGroupResult = exportModule.processColumnGroup(emptyColumnGroup);
expect(emptyGroupResult).toBe(false);
});
});
// Test ExportRow and ExportColumn
describe('ExportRow and ExportColumn', function() {
test('ExportRow initializes correctly', function() {
const columns = ["col1", "col2"];
const component = { getComponent: jest.fn() };
const indent = 2;
const row = new ExportRow("group", columns, component, indent);
expect(row.type).toBe("group");
expect(row.columns).toBe(columns);
expect(row.component).toBe(component);
expect(row.indent).toBe(indent);
});
test('ExportColumn initializes correctly', function() {
const value = "Cell Value";
const component = { getComponent: jest.fn() };
const width = 2;
const height = 3;
const depth = 1;
const column = new ExportColumn(value, component, width, height, depth);
expect(column.value).toBe(value);
expect(column.component).toBe(component);
expect(column.width).toBe(width);
expect(column.height).toBe(height);
expect(column.depth).toBe(depth);
});
});
});

View File

@@ -0,0 +1,161 @@
import TabulatorFull from "../../../src/js/core/TabulatorFull";
import Filter from "../../../src/js/modules/Filter/Filter";
describe("Filter module", () => {
/** @type {TabulatorFull} */
let tabulator;
/** @type {Filter} */
let filterMod;
let tableData = [
{ id: 1, name: "John", age: 30, location: "New York" },
{ id: 2, name: "Jane", age: 25, location: "London" },
{ id: 3, name: "Bob", age: 35, location: "Paris" },
{ id: 4, name: "Alice", age: 28, location: "New York" },
{ id: 5, name: "Mark", age: 40, location: "Tokyo" }
];
let tableColumns = [
{ title: "ID", field: "id" },
{ title: "Name", field: "name" },
{ title: "Age", field: "age" },
{ title: "Location", field: "location" }
];
beforeEach(async () => {
const el = document.createElement("div");
el.id = "tabulator";
document.body.appendChild(el);
tabulator = new TabulatorFull("#tabulator", {
data: tableData,
columns: tableColumns
});
filterMod = tabulator.module("filter");
return new Promise((resolve) => {
tabulator.on("tableBuilt", () => {
resolve();
});
});
});
afterEach(() => {
tabulator.destroy();
document.getElementById("tabulator")?.remove();
});
it("should initialize with no filters", () => {
const filters = filterMod.getFilters();
expect(filters.length).toBe(0);
});
it("should add a single filter", () => {
filterMod.addFilter("name", "=", "John");
const filters = filterMod.getFilters();
expect(filters.length).toBe(1);
expect(filters[0].field).toBe("name");
expect(filters[0].type).toBe("=");
expect(filters[0].value).toBe("John");
});
it("should remove a filter", () => {
filterMod.addFilter("name", "=", "John");
expect(filterMod.getFilters().length).toBe(1);
filterMod.removeFilter("name", "=", "John");
expect(filterMod.getFilters().length).toBe(0);
});
it("should clear all filters", () => {
filterMod.addFilter("name", "=", "John");
filterMod.addFilter("age", ">", 25);
expect(filterMod.getFilters().length).toBe(2);
filterMod.clearFilter();
expect(filterMod.getFilters().length).toBe(0);
});
it("should filter data with equals operator", () => {
// Apply filter for name = "John"
filterMod.addFilter("name", "=", "John");
// Filter the data
const filtered = filterMod.filter(tabulator.rowManager.activeRows);
// Should return only one row with name "John"
expect(filtered.length).toBe(1);
expect(filtered[0].data.name).toBe("John");
});
it("should filter data with greater than operator", () => {
// Apply filter for age > 30
filterMod.addFilter("age", ">", 30);
// Filter the data
const filtered = filterMod.filter(tabulator.rowManager.activeRows);
// Should return 2 rows: Bob (35) and Mark (40)
expect(filtered.length).toBe(2);
// Check the filtered data contains the expected names
const names = filtered.map(row => row.data.name);
expect(names).toContain("Bob");
expect(names).toContain("Mark");
});
it("should filter data with less than operator", () => {
// Apply filter for age < 30
filterMod.addFilter("age", "<", 30);
// Filter the data
const filtered = filterMod.filter(tabulator.rowManager.activeRows);
// Should return 2 rows: Jane (25) and Alice (28)
expect(filtered.length).toBe(2);
// Check the filtered data contains the expected names
const names = filtered.map(row => row.data.name);
expect(names).toContain("Jane");
expect(names).toContain("Alice");
});
it("should filter data with like operator", () => {
// Apply filter for location like "New" (should match "New York")
filterMod.addFilter("location", "like", "New");
// Filter the data
const filtered = filterMod.filter(tabulator.rowManager.activeRows);
// Should return 2 rows with location "New York"
expect(filtered.length).toBe(2);
// Check all filtered rows have location containing "New"
filtered.forEach(row => {
expect(row.data.location).toContain("New");
});
});
it("should apply multiple filters with AND logic", () => {
// Apply two filters: location = "New York" AND age < 30
filterMod.addFilter("location", "=", "New York");
filterMod.addFilter("age", "<", 30);
// Filter the data
const filtered = filterMod.filter(tabulator.rowManager.activeRows);
// Should return 1 row: Alice (age 28, location "New York")
expect(filtered.length).toBe(1);
expect(filtered[0].data.name).toBe("Alice");
});
it("should search data and return matching rows", () => {
// Search for rows with age > 30
const results = filterMod.searchData("age", ">", 30);
// Should find 2 rows
expect(results.length).toBe(2);
// Check results have the expected data
const names = results.map(row => row.name);
expect(names).toContain("Bob");
expect(names).toContain("Mark");
});
});

View File

@@ -0,0 +1,115 @@
import TabulatorFull from "../../../src/js/core/TabulatorFull";
import Format from "../../../src/js/modules/Format/Format";
describe("Format module", () => {
/** @type {TabulatorFull} */
let tabulator;
/** @type {Format} */
let formatMod;
let tableData = [
{ id: 1, name: "John", active: true, balance: 1250.75, date: "2021-05-10" },
{ id: 2, name: "Jane", active: false, balance: 750.50, date: "2022-03-15" },
{ id: 3, name: "Bob", active: true, balance: 325.20, date: "2020-12-01" }
];
let tableColumns = [
{ title: "ID", field: "id" },
{ title: "Name", field: "name" },
{ title: "Active", field: "active", formatter: "tickCross" },
{ title: "Balance", field: "balance", formatter: "money", formatterParams: { symbol: "$" } },
{ title: "Date", field: "date", formatter: "datetime", formatterParams: { outputFormat: "DD/MM/YYYY" } }
];
beforeEach(async () => {
const el = document.createElement("div");
el.id = "tabulator";
document.body.appendChild(el);
tabulator = new TabulatorFull("#tabulator", {
data: tableData,
columns: tableColumns
});
formatMod = tabulator.module("format");
return new Promise((resolve) => {
tabulator.on("tableBuilt", () => {
resolve();
});
});
});
afterEach(() => {
tabulator.destroy();
document.getElementById("tabulator")?.remove();
});
it("should initialize formatters for columns", () => {
const columns = tabulator.columnManager.getColumns();
// Check if formatters are properly set
expect(columns[0].modules.format.formatter).toBeDefined();
expect(columns[2].modules.format.formatter).toBeDefined();
expect(columns[3].modules.format.formatter).toBeDefined();
expect(columns[4].modules.format.formatter).toBeDefined();
});
it("should sanitize HTML properly", () => {
// Test with HTML content
const html = "<script>alert('test')</script><b>Hello</b>";
const sanitized = formatMod.sanitizeHTML(html);
// Check XSS is prevented
expect(sanitized).not.toContain("<script>");
expect(sanitized).not.toContain("</script>");
expect(sanitized).not.toContain("<b>");
expect(sanitized).not.toContain("</b>");
// Check correct HTML entities are used
expect(sanitized).toContain("&lt;script&gt;");
expect(sanitized).toContain("&lt;b&gt;");
});
it("should convert empty values to space", () => {
expect(formatMod.emptyToSpace(null)).toBe("&nbsp;");
expect(formatMod.emptyToSpace(undefined)).toBe("&nbsp;");
expect(formatMod.emptyToSpace("")).toBe("&nbsp;");
expect(formatMod.emptyToSpace("test")).toBe("test");
expect(formatMod.emptyToSpace(0)).toBe(0);
});
it("should handle lookupFormatter", () => {
// Test with a string formatter name
const stringFormatter = formatMod.lookupFormatter("plaintext");
expect(typeof stringFormatter).toBe("function");
// Test with a function formatter
const funcFormatter = function(cell) { return cell; };
const returnedFuncFormatter = formatMod.lookupFormatter(funcFormatter);
expect(returnedFuncFormatter).toBe(funcFormatter);
// Test with non-existent formatter name
const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation();
const defaultFormatter = formatMod.lookupFormatter("nonExistentFormatter");
expect(consoleWarnSpy).toHaveBeenCalled();
expect(typeof defaultFormatter).toBe("function");
// Clean up
consoleWarnSpy.mockRestore();
});
it("should create proper type formatters", () => {
const column = {
definition: {
formatter: "money",
formatterParams: { symbol: "$" },
formatterPrint: "plaintext",
formatterPrintParams: { prefix: "Print-" }
}
};
const config = formatMod.lookupTypeFormatter(column, "");
expect(config.formatter).toBeDefined();
expect(config.params).toEqual({ symbol: "$" });
const printConfig = formatMod.lookupTypeFormatter(column, "Print");
expect(printConfig.formatter).toBeDefined();
expect(printConfig.params).toEqual({ prefix: "Print-" });
});
});

View File

@@ -0,0 +1,382 @@
import Module from '../../../src/js/core/Module.js';
import FrozenColumns from '../../../src/js/modules/FrozenColumns/FrozenColumns.js';
// Override the Module methods that interact with the table to avoid dependency issues
const originalRegisterTableOption = Module.prototype.registerTableOption;
Module.prototype.registerTableOption = function() {};
const originalRegisterColumnOption = Module.prototype.registerColumnOption;
Module.prototype.registerColumnOption = function() {};
describe('FrozenColumns', function(){
// Restore original Module methods after all tests
afterAll(() => {
Module.prototype.registerTableOption = originalRegisterTableOption;
Module.prototype.registerColumnOption = originalRegisterColumnOption;
});
// Test direct functionality without a complete table instance
describe('Functionality tests', function() {
test('initialize sets up listeners', function(){
// Create a mock table
const mockTable = {};
// Create a FrozenColumns instance
const frozenColumns = new FrozenColumns(mockTable);
// Mock the subscribe method
frozenColumns.subscribe = jest.fn();
// Call initialize
frozenColumns.initialize();
// Check that the required events are subscribed to
expect(frozenColumns.subscribe).toHaveBeenCalledWith("cell-layout", expect.any(Function));
expect(frozenColumns.subscribe).toHaveBeenCalledWith("column-init", expect.any(Function));
expect(frozenColumns.subscribe).toHaveBeenCalledWith("column-width", expect.any(Function));
expect(frozenColumns.subscribe).toHaveBeenCalledWith("row-layout-after", expect.any(Function));
expect(frozenColumns.subscribe).toHaveBeenCalledWith("table-layout", expect.any(Function));
expect(frozenColumns.subscribe).toHaveBeenCalledWith("columns-loading", expect.any(Function));
expect(frozenColumns.subscribe).toHaveBeenCalledWith("column-add", expect.any(Function));
expect(frozenColumns.subscribe).toHaveBeenCalledWith("column-deleted", expect.any(Function));
expect(frozenColumns.subscribe).toHaveBeenCalledWith("column-hide", expect.any(Function));
expect(frozenColumns.subscribe).toHaveBeenCalledWith("column-show", expect.any(Function));
expect(frozenColumns.subscribe).toHaveBeenCalledWith("columns-loaded", expect.any(Function));
expect(frozenColumns.subscribe).toHaveBeenCalledWith("table-redraw", expect.any(Function));
expect(frozenColumns.subscribe).toHaveBeenCalledWith("layout-refreshing", expect.any(Function));
expect(frozenColumns.subscribe).toHaveBeenCalledWith("layout-refreshed", expect.any(Function));
expect(frozenColumns.subscribe).toHaveBeenCalledWith("scrollbar-vertical", expect.any(Function));
});
test('reset restores initial state', function(){
// Create a mock table
const mockTable = {};
// Create a FrozenColumns instance
const frozenColumns = new FrozenColumns(mockTable);
// Set some values
frozenColumns.leftColumns = ["col1", "col2"];
frozenColumns.rightColumns = ["col3", "col4"];
frozenColumns.initializationMode = "right";
frozenColumns.active = true;
// Call reset
frozenColumns.reset();
// Check that values are reset to initial state
expect(frozenColumns.leftColumns).toEqual([]);
expect(frozenColumns.rightColumns).toEqual([]);
expect(frozenColumns.initializationMode).toBe("left");
expect(frozenColumns.active).toBe(false);
});
test('blockLayout and unblockLayout control blocked state', function(){
// Create a mock table
const mockTable = {};
// Create a FrozenColumns instance
const frozenColumns = new FrozenColumns(mockTable);
// Initial state should be blocked
expect(frozenColumns.blocked).toBe(true);
// Call unblockLayout
frozenColumns.unblockLayout();
// Should be unblocked
expect(frozenColumns.blocked).toBe(false);
// Call blockLayout
frozenColumns.blockLayout();
// Should be blocked again
expect(frozenColumns.blocked).toBe(true);
});
test('initializeColumn assigns frozen properties to column', function(){
// Create a mock table
const mockTable = {};
// Create a FrozenColumns instance
const frozenColumns = new FrozenColumns(mockTable);
// Mock frozenCheck method to return true
frozenColumns.frozenCheck = jest.fn().mockReturnValue(true);
// Create a mock column
const mockColumn = {
isGroup: false,
definition: { frozen: true },
parent: { isGroup: false },
modules: {}
};
// Call initializeColumn
frozenColumns.initializeColumn(mockColumn);
// Check that column is properly configured
expect(mockColumn.modules.frozen).toBeDefined();
expect(mockColumn.modules.frozen.position).toBe("left");
expect(frozenColumns.leftColumns).toContain(mockColumn);
expect(frozenColumns.active).toBe(true);
// Reset
frozenColumns.reset();
frozenColumns.frozenCheck.mockClear();
// Set up a non-frozen column
const nonFrozenColumn = {
isGroup: false,
definition: { frozen: false },
parent: { isGroup: false },
modules: {}
};
// Mock frozenCheck to return false
frozenColumns.frozenCheck = jest.fn().mockReturnValue(false);
// Call initializeColumn
frozenColumns.initializeColumn(nonFrozenColumn);
// Check that initialization mode is switched to right
expect(frozenColumns.initializationMode).toBe("right");
expect(frozenColumns.leftColumns).not.toContain(nonFrozenColumn);
expect(frozenColumns.rightColumns).not.toContain(nonFrozenColumn);
// Create a right frozen column
const rightFrozenColumn = {
isGroup: false,
definition: { frozen: true },
parent: { isGroup: false },
modules: {}
};
// Mock frozenCheck to return true again
frozenColumns.frozenCheck = jest.fn().mockReturnValue(true);
// Call initializeColumn
frozenColumns.initializeColumn(rightFrozenColumn);
// Check that column is added to rightColumns
expect(rightFrozenColumn.modules.frozen.position).toBe("right");
expect(frozenColumns.rightColumns).toContain(rightFrozenColumn);
});
test('frozenCheck determines if column should be frozen', function(){
// Create a mock table
const mockTable = {};
// Create a FrozenColumns instance
const frozenColumns = new FrozenColumns(mockTable);
// Mock console.warn
const originalWarn = console.warn;
console.warn = jest.fn();
// Test a simple column that is frozen
const frozenColumn = {
parent: { isGroup: false },
definition: { frozen: true }
};
expect(frozenColumns.frozenCheck(frozenColumn)).toBe(true);
// Test a simple column that is not frozen
const notFrozenColumn = {
parent: { isGroup: false },
definition: { frozen: false }
};
expect(frozenColumns.frozenCheck(notFrozenColumn)).toBe(false);
// Test a column in a group - need to implement a specific test for this case
const parentColumn = {
parent: { isGroup: false },
definition: { frozen: true },
isGroup: true
};
const groupChild = {
parent: parentColumn,
definition: { frozen: false }
};
// Create a custom implementation that directly returns the parent's frozen status
frozenColumns.frozenCheck = function(column) {
if(column.parent.isGroup) {
// For this test, we'll just return a fixed result
return true;
} else {
return column.definition.frozen;
}
};
expect(frozenColumns.frozenCheck(groupChild)).toBe(true);
// Test warning for invalid frozen configuration
frozenColumns.frozenCheck = function(column) {
if(column.parent.isGroup && column.definition.frozen){
console.warn("Frozen Column Error - Parent column group must be frozen, not individual columns or sub column groups");
}
if(column.parent.isGroup){
return this.frozenCheck(column.parent);
}else{
return column.definition.frozen;
}
};
const invalidColumn = {
parent: {
isGroup: true,
parent: { isGroup: false },
definition: { frozen: false }
},
definition: { frozen: true }
};
frozenColumns.frozenCheck(invalidColumn);
expect(console.warn).toHaveBeenCalledWith("Frozen Column Error - Parent column group must be frozen, not individual columns or sub column groups");
// Restore console.warn
console.warn = originalWarn;
});
test('layoutElement applies correct styles to element', function(){
// Create a mock table
const mockTable = {
rtl: false
};
// Create a FrozenColumns instance
const frozenColumns = new FrozenColumns(mockTable);
// Create a mock element
const element = document.createElement('div');
// Create a mock left frozen column
const leftColumn = {
modules: {
frozen: {
position: "left",
margin: "50px",
edge: true
}
}
};
// Layout the element
frozenColumns.layoutElement(element, leftColumn);
// Check that the correct styles are applied
expect(element.style.position).toBe("sticky");
expect(element.style.left).toBe("50px");
expect(element.classList.contains("tabulator-frozen")).toBe(true);
expect(element.classList.contains("tabulator-frozen-left")).toBe(true);
expect(element.classList.contains("tabulator-frozen-right")).toBe(false);
// Test with right column
const rightColumn = {
modules: {
frozen: {
position: "right",
margin: "100px",
edge: true
}
}
};
// Reset element
element.className = "";
element.style.position = "";
element.style.left = "";
element.style.right = "";
// Layout the element
frozenColumns.layoutElement(element, rightColumn);
// Check that the correct styles are applied
expect(element.style.position).toBe("sticky");
expect(element.style.right).toBe("100px");
expect(element.classList.contains("tabulator-frozen")).toBe(true);
expect(element.classList.contains("tabulator-frozen-left")).toBe(false);
expect(element.classList.contains("tabulator-frozen-right")).toBe(true);
// Test with RTL enabled
mockTable.rtl = true;
// Reset element
element.className = "";
element.style.position = "";
element.style.left = "";
element.style.right = "";
// Layout the element with left column
frozenColumns.layoutElement(element, leftColumn);
// In RTL mode, left position becomes right
expect(element.style.position).toBe("sticky");
expect(element.style.right).toBe("50px");
expect(element.classList.contains("tabulator-frozen")).toBe(true);
expect(element.classList.contains("tabulator-frozen-left")).toBe(true);
expect(element.classList.contains("tabulator-frozen-right")).toBe(false);
});
test('getFrozenColumns returns all frozen columns', function(){
// Create a mock table
const mockTable = {};
// Create a FrozenColumns instance
const frozenColumns = new FrozenColumns(mockTable);
// Create mock columns
const leftCol1 = { id: "left1" };
const leftCol2 = { id: "left2" };
const rightCol1 = { id: "right1" };
const rightCol2 = { id: "right2" };
// Set up the columns
frozenColumns.leftColumns = [leftCol1, leftCol2];
frozenColumns.rightColumns = [rightCol1, rightCol2];
// Get frozen columns
const result = frozenColumns.getFrozenColumns();
// Should contain all columns from both left and right
expect(result).toHaveLength(4);
expect(result).toContain(leftCol1);
expect(result).toContain(leftCol2);
expect(result).toContain(rightCol1);
expect(result).toContain(rightCol2);
});
test('_calcSpace calculates width of columns', function(){
// Create a mock table
const mockTable = {};
// Create a FrozenColumns instance
const frozenColumns = new FrozenColumns(mockTable);
// Create mock columns
const columns = [
{ visible: true, getWidth: () => 100 },
{ visible: false, getWidth: () => 50 }, // Not visible, shouldn't be counted
{ visible: true, getWidth: () => 150 },
{ visible: true, getWidth: () => 200 }
];
// Calculate space up to index 1
let space = frozenColumns._calcSpace(columns, 1);
expect(space).toBe(100); // Only first column
// Calculate space up to index 3
space = frozenColumns._calcSpace(columns, 3);
expect(space).toBe(250); // First and third columns (second is not visible)
// Calculate space for all columns
space = frozenColumns._calcSpace(columns, 4);
expect(space).toBe(450); // First, third, and fourth columns
});
});
});

View File

@@ -0,0 +1,398 @@
import Module from '../../../src/js/core/Module.js';
import FrozenRows from '../../../src/js/modules/FrozenRows/FrozenRows.js';
// Override the Module methods that interact with the table to avoid dependency issues
const originalRegisterTableOption = Module.prototype.registerTableOption;
Module.prototype.registerTableOption = function() {};
const originalRegisterColumnOption = Module.prototype.registerColumnOption;
Module.prototype.registerColumnOption = function() {};
const originalRegisterComponentFunction = Module.prototype.registerComponentFunction;
Module.prototype.registerComponentFunction = function() {};
describe('FrozenRows', function(){
// Restore original Module methods after all tests
afterAll(() => {
Module.prototype.registerTableOption = originalRegisterTableOption;
Module.prototype.registerColumnOption = originalRegisterColumnOption;
Module.prototype.registerComponentFunction = originalRegisterComponentFunction;
});
// Test direct functionality without a complete table instance
describe('Functionality tests', function() {
test('freezeRow adds row to frozen rows', function(){
// Create mock elements
document.createDocumentFragment = jest.fn().mockReturnValue({
appendChild: jest.fn()
});
// Create a mock table
const mockTable = {
rowManager: {
adjustTableSize: jest.fn(),
styleRow: jest.fn()
},
columnManager: {
getContentsElement: jest.fn().mockReturnValue({
insertBefore: jest.fn()
}),
headersElement: {
nextSibling: null,
offsetWidth: 500
}
},
options: {}
};
// Create a FrozenRows instance
const frozenRows = new FrozenRows(mockTable);
// Mock the refreshData method
frozenRows.refreshData = jest.fn();
frozenRows.styleRows = jest.fn();
// Create a mock row element
const mockElement = document.createElement('div');
// Create a spy for appendChild
const appendChildSpy = jest.spyOn(frozenRows.topElement, 'appendChild');
// Create a mock row
const mockRow = {
modules: {},
getElement: jest.fn().mockReturnValue(mockElement),
initialize: jest.fn(),
normalizeHeight: jest.fn()
};
// Mock console.warn
const originalWarn = console.warn;
console.warn = jest.fn();
// Freeze the row
frozenRows.freezeRow(mockRow);
// Check that the row was properly frozen
expect(mockRow.modules.frozen).toBe(true);
expect(frozenRows.rows).toContain(mockRow);
expect(appendChildSpy).toHaveBeenCalledWith(mockElement);
expect(mockRow.initialize).toHaveBeenCalled();
expect(mockRow.normalizeHeight).toHaveBeenCalled();
expect(frozenRows.refreshData).toHaveBeenCalledWith(false, "display");
expect(mockTable.rowManager.adjustTableSize).toHaveBeenCalled();
expect(frozenRows.styleRows).toHaveBeenCalled();
// Clean up spy
appendChildSpy.mockRestore();
// Try to freeze already frozen row
frozenRows.freezeRow(mockRow);
// Should show warning
expect(console.warn).toHaveBeenCalledWith("Freeze Error - Row is already frozen");
// Restore console.warn
console.warn = originalWarn;
});
test('unfreezeRow removes row from frozen rows', function(){
// Create mock elements
document.createDocumentFragment = jest.fn().mockReturnValue({
appendChild: jest.fn()
});
// Create a mock table
const mockTable = {
rowManager: {
adjustTableSize: jest.fn(),
styleRow: jest.fn()
},
columnManager: {
getContentsElement: jest.fn().mockReturnValue({
insertBefore: jest.fn()
}),
headersElement: {
nextSibling: null,
offsetWidth: 500
}
},
options: {}
};
// Create a FrozenRows instance
const frozenRows = new FrozenRows(mockTable);
// Mock the refreshData method
frozenRows.refreshData = jest.fn();
// Create a mock row element with parent node
const mockRowElement = document.createElement('div');
const parentNode = document.createElement('div');
parentNode.appendChild(mockRowElement);
// Create a mock row that is already frozen
const mockRow = {
modules: { frozen: true },
getElement: jest.fn().mockReturnValue(mockRowElement)
};
// Clear the rows array so we don't have any frozen rows after detaching
frozenRows.rows = [];
// Mock console.warn
const originalWarn = console.warn;
console.warn = jest.fn();
// Mock the detachRow method to empty the rows array
frozenRows.detachRow = jest.fn().mockImplementation(() => {
frozenRows.rows = []; // Simulate row removal
});
frozenRows.styleRows = jest.fn();
// Add the row to be unfrozen
frozenRows.rows = [mockRow];
// Unfreeze the row
frozenRows.unfreezeRow(mockRow);
// Check that the row was properly unfrozen
expect(mockRow.modules.frozen).toBe(false);
expect(frozenRows.detachRow).toHaveBeenCalledWith(mockRow);
expect(frozenRows.refreshData).toHaveBeenCalledWith(false, "display");
expect(mockTable.rowManager.adjustTableSize).toHaveBeenCalled();
// Since rows array is empty after detaching, styleRows should not be called
expect(frozenRows.styleRows).not.toHaveBeenCalled();
// Try to unfreeze a row that's not frozen
const notFrozenRow = {
modules: { frozen: false }
};
frozenRows.unfreezeRow(notFrozenRow);
// Should show warning
expect(console.warn).toHaveBeenCalledWith("Freeze Error - Row is already unfrozen");
// Add back a frozen row and test styleRows is called
const anotherRow = {
modules: { frozen: true },
getElement: jest.fn().mockReturnValue(document.createElement('div'))
};
// Mock the rows array to be non-empty after detaching
frozenRows.detachRow.mockImplementationOnce(() => {
frozenRows.rows = [{ id: "dummy" }]; // Leave a row in the array
});
frozenRows.rows.push(anotherRow);
frozenRows.styleRows.mockClear();
frozenRows.unfreezeRow(anotherRow);
// With remaining frozen rows, styleRows should have been called
expect(frozenRows.styleRows).toHaveBeenCalled();
// Restore console.warn
console.warn = originalWarn;
});
test('detachRow removes row from frozen rows array', function(){
// Create a FrozenRows instance with minimal mock
const frozenRows = new FrozenRows({});
// Create a mock row element with parent
const mockRowElement = document.createElement('div');
const parentNode = document.createElement('div');
parentNode.appendChild(mockRowElement);
// Create a mock row
const mockRow = {
getElement: jest.fn().mockReturnValue(mockRowElement)
};
// Add the row to the frozen rows
frozenRows.rows = [mockRow];
// Call detachRow
frozenRows.detachRow(mockRow);
// Row should be removed from rows array
expect(frozenRows.rows).not.toContain(mockRow);
expect(frozenRows.rows.length).toBe(0);
// Element should be removed from DOM
expect(mockRowElement.parentNode).toBeNull();
// Test with row not in array
const notIncludedRow = {
getElement: jest.fn().mockReturnValue(document.createElement('div'))
};
frozenRows.detachRow(notIncludedRow);
// Should not throw error, no change to rows array
expect(frozenRows.rows.length).toBe(0);
});
test('isRowFrozen checks if row is in frozen rows array', function(){
// Create a FrozenRows instance with minimal mock
const frozenRows = new FrozenRows({});
// Create mock rows
const frozenRow = { id: 1 };
const notFrozenRow = { id: 2 };
// Add one row to the frozen rows
frozenRows.rows = [frozenRow];
// Check frozen row
expect(frozenRows.isRowFrozen(frozenRow)).toBe(true);
// Check non-frozen row
expect(frozenRows.isRowFrozen(notFrozenRow)).toBe(false);
});
test('isFrozen checks if there are any frozen rows', function(){
// Create a FrozenRows instance with minimal mock
const frozenRows = new FrozenRows({});
// No frozen rows
frozenRows.rows = [];
expect(frozenRows.isFrozen()).toBe(false);
// With frozen rows
frozenRows.rows = [{ id: 1 }];
expect(frozenRows.isFrozen()).toBe(true);
});
test('getRows filters frozen rows from input array', function(){
// Create a FrozenRows instance with minimal mock
const frozenRows = new FrozenRows({});
// Create mock rows
const row1 = { id: 1 };
const row2 = { id: 2 };
const row3 = { id: 3 };
const row4 = { id: 4 };
// Set frozen rows
frozenRows.rows = [row2, row4];
// Test filtering
const inputRows = [row1, row2, row3, row4];
const result = frozenRows.getRows(inputRows);
// Should contain only non-frozen rows
expect(result).toEqual([row1, row3]);
// Original array should not be modified
expect(inputRows).toEqual([row1, row2, row3, row4]);
});
test('visibleRows adds frozen rows to visible rows array', function(){
// Create a FrozenRows instance with minimal mock
const frozenRows = new FrozenRows({});
// Create mock rows
const frozenRow1 = { id: 1 };
const frozenRow2 = { id: 2 };
const normalRow = { id: 3 };
// Set frozen rows
frozenRows.rows = [frozenRow1, frozenRow2];
// Test adding to visible rows
const visibleRows = [normalRow];
const result = frozenRows.visibleRows(true, visibleRows);
// Should contain both frozen and normal rows
expect(result).toEqual([normalRow, frozenRow1, frozenRow2]);
// Original array should be modified (this is expected behavior of the method)
expect(visibleRows).toEqual([normalRow, frozenRow1, frozenRow2]);
});
test('initializeRow correctly handles different frozenRows options', function(){
// Create a mock table
const mockTable = {
options: {
frozenRows: 2 // Number option
}
};
// Create a FrozenRows instance
const frozenRows = new FrozenRows(mockTable);
frozenRows.freezeRow = jest.fn();
frozenRows.options = jest.fn().mockReturnValue("id");
// Test with numeric frozenRows option
// Row position <= frozenRows value
const row1 = {
getPosition: jest.fn().mockReturnValue(1),
getComponent: jest.fn()
};
frozenRows.initializeRow(row1);
expect(frozenRows.freezeRow).toHaveBeenCalledWith(row1);
frozenRows.freezeRow.mockClear();
// Row position > frozenRows value
const row3 = {
getPosition: jest.fn().mockReturnValue(3),
getComponent: jest.fn()
};
frozenRows.initializeRow(row3);
expect(frozenRows.freezeRow).not.toHaveBeenCalled();
// Test with function frozenRows option
mockTable.options.frozenRows = jest.fn().mockImplementation(row => row.id === 2);
const row2 = {
id: 2,
getComponent: jest.fn().mockReturnValue({ id: 2 })
};
frozenRows.initializeRow(row2);
expect(mockTable.options.frozenRows).toHaveBeenCalledWith(row2.getComponent());
expect(frozenRows.freezeRow).toHaveBeenCalledWith(row2);
frozenRows.freezeRow.mockClear();
mockTable.options.frozenRows.mockClear();
// Row that doesn't match function condition
const row4 = {
id: 4,
getComponent: jest.fn().mockReturnValue({ id: 4 })
};
frozenRows.initializeRow(row4);
expect(mockTable.options.frozenRows).toHaveBeenCalledWith(row4.getComponent());
expect(frozenRows.freezeRow).not.toHaveBeenCalled();
// Test with array frozenRows option
mockTable.options.frozenRows = [1, 5, 7];
// Row with matching ID
const rowMatch = {
data: { id: 5 }
};
frozenRows.initializeRow(rowMatch);
expect(frozenRows.freezeRow).toHaveBeenCalledWith(rowMatch);
frozenRows.freezeRow.mockClear();
// Row with non-matching ID
const rowNoMatch = {
data: { id: 2 }
};
frozenRows.initializeRow(rowNoMatch);
expect(frozenRows.freezeRow).not.toHaveBeenCalled();
});
});
});

View File

@@ -0,0 +1,97 @@
import Module from '../../../src/js/core/Module.js';
import GroupRows from '../../../src/js/modules/GroupRows/GroupRows.js';
// Override the Module methods that interact with the table to avoid dependency issues
const originalRegisterTableOption = Module.prototype.registerTableOption;
Module.prototype.registerTableOption = function() {};
const originalRegisterColumnOption = Module.prototype.registerColumnOption;
Module.prototype.registerColumnOption = function() {};
const originalRegisterTableFunction = Module.prototype.registerTableFunction;
Module.prototype.registerTableFunction = function() {};
const originalRegisterComponentFunction = Module.prototype.registerComponentFunction;
Module.prototype.registerComponentFunction = function() {};
describe('GroupRows', function(){
// Restore original Module methods after all tests
afterAll(() => {
Module.prototype.registerTableOption = originalRegisterTableOption;
Module.prototype.registerColumnOption = originalRegisterColumnOption;
Module.prototype.registerTableFunction = originalRegisterTableFunction;
Module.prototype.registerComponentFunction = originalRegisterComponentFunction;
});
// Test direct functionality without a complete table instance
describe('Initialization', function() {
test('initializes with proper defaults', function() {
// Create a GroupRows instance
const groupRows = new GroupRows({});
// Check default properties
expect(groupRows.groupIDLookups).toBe(false);
expect(Array.isArray(groupRows.startOpen)).toBe(true);
expect(Array.isArray(groupRows.headerGenerator)).toBe(true);
expect(Array.isArray(groupRows.groupList)).toBe(true);
expect(typeof groupRows.displayHandler).toBe("function");
expect(groupRows.blockRedraw).toBe(false);
});
});
describe('Group Management', function() {
test('_blockRedrawing and _restore_redrawing manage block state', function() {
// Create a GroupRows instance
const groupRows = new GroupRows({});
// Initial state
expect(groupRows.blockRedraw).toBe(false);
// Block redrawing
groupRows._blockRedrawing();
expect(groupRows.blockRedraw).toBe(true);
// Restore redrawing
groupRows._restore_redrawing();
expect(groupRows.blockRedraw).toBe(false);
});
});
describe('Core methods with direct testing', function() {
test('userGetGroups processes group structures', function() {
// Create a GroupRows instance with mocked table
const groupRows = new GroupRows({});
// Setup mock group data
const group1 = { getComponent: jest.fn().mockReturnValue('component1') };
const group2 = { getComponent: jest.fn().mockReturnValue('component2') };
// Direct implementation test without checking original method result
const groups = { group1, group2 };
const components = Object.values(groups).map(group => group.getComponent());
// Checking our mock groups work as expected
expect(components).toEqual(['component1', 'component2']);
expect(group1.getComponent).toHaveBeenCalled();
expect(group2.getComponent).toHaveBeenCalled();
});
test('can create custom header generators', function() {
// Create a custom header generator function
const customGenerator = (value, count, data) => {
return `${value || ''} (${count} items)`;
};
// Call the function directly
let result = customGenerator('test', 5, {});
// Check format includes value and count
expect(result).toContain('test');
expect(result).toContain('(5 items)');
// Test with undefined value
result = customGenerator(undefined, 3, {});
expect(result).toContain('(3 items)');
});
});
});

View File

@@ -0,0 +1,200 @@
import TabulatorFull from "../../../src/js/core/TabulatorFull";
import History from "../../../src/js/modules/History/History";
describe("History module", () => {
/** @type {TabulatorFull} */
let tabulator;
/** @type {History} */
let historyMod;
let tableData = [
{ id: 1, name: "John", age: 30 },
{ id: 2, name: "Jane", age: 25 },
{ id: 3, name: "Bob", age: 35 }
];
let tableColumns = [
{ title: "ID", field: "id" },
{ title: "Name", field: "name", editor: "input" },
{ title: "Age", field: "age", editor: "number" }
];
beforeEach(async () => {
const el = document.createElement("div");
el.id = "tabulator";
document.body.appendChild(el);
tabulator = new TabulatorFull("#tabulator", {
data: tableData,
columns: tableColumns,
history: true
});
historyMod = tabulator.module("history");
return new Promise((resolve) => {
tabulator.on("tableBuilt", () => {
resolve();
});
});
});
afterEach(() => {
// Clean up DOM without destroying tabulator to avoid dispatch errors
if (document.getElementById("tabulator")) {
document.getElementById("tabulator").remove();
}
});
it("should initialize with empty history", () => {
expect(historyMod.getHistoryUndoSize()).toBe(0);
expect(historyMod.getHistoryRedoSize()).toBe(0);
});
it("should track cell edits", () => {
// Get a cell and update its value
const cell = tabulator.rowManager.rows[0].getCells()[1]; // name cell
const oldValue = cell.getValue();
const newValue = "Updated Name";
// Update cell value
cell.setValue(newValue);
// Check that history recorded the action
expect(historyMod.getHistoryUndoSize()).toBe(1);
expect(historyMod.getHistoryRedoSize()).toBe(0);
// Undo the action
expect(historyMod.undo()).toBe(true);
// Check cell value reverted
expect(cell.getValue()).toBe(oldValue);
// Check history state updated
expect(historyMod.getHistoryUndoSize()).toBe(0);
expect(historyMod.getHistoryRedoSize()).toBe(1);
// Redo the action
expect(historyMod.redo()).toBe(true);
// Check cell value restored
expect(cell.getValue()).toBe(newValue);
});
it("should have rowDeleted method", () => {
// Verify rowDeleted method exists
expect(typeof historyMod.rowDeleted).toBe('function');
});
it("should have action method to record history", () => {
// Test action method directly
expect(typeof historyMod.action).toBe('function');
// Verify initial history state
expect(historyMod.history.length).toBe(0);
expect(historyMod.index).toBe(-1);
// Call action directly with test parameters
const testComponent = {};
const testData = { foo: "bar" };
historyMod.action("testAction", testComponent, testData);
// Verify history was updated
expect(historyMod.history.length).toBe(1);
expect(historyMod.index).toBe(0);
expect(historyMod.history[0].type).toBe("testAction");
expect(historyMod.history[0].component).toBe(testComponent);
expect(historyMod.history[0].data).toBe(testData);
});
it("should track row addition", () => {
// Directly manipulate history to simulate row add
const newRowData = { id: 4, name: "Alice", age: 28 };
const newRow = tabulator.rowManager.addRow(newRowData);
// Manually add to history
historyMod.action("rowAdd", newRow, {data: newRowData, pos: true, index: false});
// Check history state
expect(historyMod.getHistoryUndoSize()).toBe(1);
// Reset for next test
historyMod.clear();
});
it("should clear history", () => {
// Make some changes to build history
const cell = tabulator.rowManager.rows[0].getCells()[1];
cell.setValue("New Value 1");
cell.setValue("New Value 2");
// Check history state
expect(historyMod.getHistoryUndoSize()).toBe(2);
// Clear history
historyMod.clear();
// Check history is empty
expect(historyMod.getHistoryUndoSize()).toBe(0);
expect(historyMod.getHistoryRedoSize()).toBe(0);
});
it("should handle multiple undo and redo operations", () => {
// Make several changes
const cell = tabulator.rowManager.rows[0].getCells()[1];
const originalValue = cell.getValue();
cell.setValue("Change 1");
cell.setValue("Change 2");
cell.setValue("Change 3");
// Undo all changes
historyMod.undo(); // Undo Change 3
historyMod.undo(); // Undo Change 2
historyMod.undo(); // Undo Change 1
// Check value is back to original
expect(cell.getValue()).toBe(originalValue);
// Check history state
expect(historyMod.getHistoryUndoSize()).toBe(0);
expect(historyMod.getHistoryRedoSize()).toBe(3);
// Redo changes
historyMod.redo(); // Redo Change 1
expect(cell.getValue()).toBe("Change 1");
historyMod.redo(); // Redo Change 2
expect(cell.getValue()).toBe("Change 2");
historyMod.redo(); // Redo Change 3
expect(cell.getValue()).toBe("Change 3");
});
it("should warn when undoing with no history", () => {
// Mock console.warn
const originalWarn = console.warn;
console.warn = jest.fn();
// Try to undo with no history
const result = historyMod.undo();
// Check result and warning
expect(result).toBe(false);
expect(console.warn).toHaveBeenCalled();
// Restore console.warn
console.warn = originalWarn;
});
it("should warn when redoing with no further history", () => {
// Mock console.warn
const originalWarn = console.warn;
console.warn = jest.fn();
// Try to redo with no future history
const result = historyMod.redo();
// Check result and warning
expect(result).toBe(false);
expect(console.warn).toHaveBeenCalled();
// Restore console.warn
console.warn = originalWarn;
});
});

View File

@@ -0,0 +1,450 @@
import HtmlTableImport from "../../../src/js/modules/HtmlTableImport/HtmlTableImport";
describe("HtmlTableImport module", () => {
/** @type {HtmlTableImport} */
let htmlTableImport;
let mockTable;
beforeEach(() => {
// Create mock optionsList
const mockOptionsList = {
register: jest.fn(),
generate: jest.fn(),
registeredDefaults: {
title: {},
field: {},
width: {}
}
};
// Create mock eventBus
const mockEventBus = {
subscribe: jest.fn(),
dispatch: jest.fn(),
};
// Create mock externalEvents
const mockExternalEvents = {
dispatch: jest.fn(),
};
// Create mock columnManager
const mockColumnManager = {
optionsList: mockOptionsList
};
// Create a simplified mock of the table
mockTable = {
options: {
columns: [],
index: "id",
data: null
},
columnManager: mockColumnManager,
optionsList: mockOptionsList,
eventBus: mockEventBus,
externalEvents: mockExternalEvents,
originalElement: null
};
// Mock methods in the HtmlTableImport prototype
jest.spyOn(HtmlTableImport.prototype, 'dispatchExternal').mockImplementation(function(event, ...args) {
this.table.externalEvents.dispatch(event, ...args);
});
// Create an instance of the HtmlTableImport module with the mock table
htmlTableImport = new HtmlTableImport(mockTable);
});
afterEach(() => {
jest.clearAllMocks();
jest.restoreAllMocks();
// Clean up document
if (document.body.firstChild) {
document.body.removeChild(document.body.firstChild);
}
});
it("should skip import when not initialized on a table element", () => {
// Set up div as the original element
const divElement = document.createElement('div');
mockTable.originalElement = divElement;
// Mock console.warn to check for warnings
const warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
// Initialize the module
htmlTableImport.initialize();
// Should not throw any errors
expect(mockTable.options.data).toBeNull();
// Should not have dispatched any events
expect(mockTable.externalEvents.dispatch).not.toHaveBeenCalled();
// Should not have logged a warning about empty table
expect(warnSpy).not.toHaveBeenCalled();
});
it("should warn when initialized on an empty table", () => {
// Set up empty table as original element
const tableElement = document.createElement('table');
mockTable.originalElement = tableElement;
// Mock console.warn to check for warnings
const warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
// Initialize the module
htmlTableImport.initialize();
// Should have logged a warning about empty table
expect(warnSpy).toHaveBeenCalledWith(
"Unable to parse data from empty table tag, Tabulator should be initialized on a div tag unless importing data from a table element."
);
// Should not have dispatched any events
expect(mockTable.externalEvents.dispatch).not.toHaveBeenCalled();
});
it("should import data from a table with headers", () => {
// Create a table with headers and data
const tableHTML = `
<table>
<thead>
<tr>
<th>Name</th>
<th>Age</th>
<th>City</th>
</tr>
</thead>
<tbody>
<tr>
<td>John</td>
<td>25</td>
<td>New York</td>
</tr>
<tr>
<td>Jane</td>
<td>30</td>
<td>London</td>
</tr>
</tbody>
</table>
`;
// Add the table to the document
document.body.innerHTML = tableHTML;
// Set the table as the original element
mockTable.originalElement = document.querySelector('table');
// Initialize the module
htmlTableImport.initialize();
// Check if events were dispatched
expect(mockTable.externalEvents.dispatch).toHaveBeenCalledWith("htmlImporting");
expect(mockTable.externalEvents.dispatch).toHaveBeenCalledWith("htmlImported");
// Check the columns were correctly parsed
expect(mockTable.options.columns.length).toBe(3);
expect(mockTable.options.columns[0].title).toBe("Name");
expect(mockTable.options.columns[0].field).toBe("name");
expect(mockTable.options.columns[1].title).toBe("Age");
expect(mockTable.options.columns[1].field).toBe("age");
expect(mockTable.options.columns[2].title).toBe("City");
expect(mockTable.options.columns[2].field).toBe("city");
// Check the data was correctly parsed
expect(mockTable.options.data.length).toBe(2);
expect(mockTable.options.data[0].name).toBe("John");
expect(mockTable.options.data[0].age).toBe("25");
expect(mockTable.options.data[0].city).toBe("New York");
expect(mockTable.options.data[1].name).toBe("Jane");
expect(mockTable.options.data[1].age).toBe("30");
expect(mockTable.options.data[1].city).toBe("London");
});
it("should generate default headers when none are provided", () => {
// Create a table with no headers but with data
const tableHTML = `
<table>
<tbody>
<tr>
<td>John</td>
<td>25</td>
<td>New York</td>
</tr>
<tr>
<td>Jane</td>
<td>30</td>
<td>London</td>
</tr>
</tbody>
</table>
`;
// Add the table to the document
document.body.innerHTML = tableHTML;
// Set the table as the original element
mockTable.originalElement = document.querySelector('table');
// Mock the parseTable method directly since we're testing specific behavior
jest.spyOn(htmlTableImport, 'parseTable').mockImplementation(() => {
// Create field index
htmlTableImport.hasIndex = false;
htmlTableImport.fieldIndex = ["col0", "col1", "col2"];
// Create columns
mockTable.options.columns = [
{ title: "", field: "col0" },
{ title: "", field: "col1" },
{ title: "", field: "col2" }
];
// Create data
mockTable.options.data = [
{ id: 0, col0: "John", col1: "25", col2: "New York" },
{ id: 1, col0: "Jane", col1: "30", col2: "London" }
];
// Dispatch events
htmlTableImport.dispatchExternal("htmlImporting");
htmlTableImport.dispatchExternal("htmlImported");
});
// Create array of TDs for the rows
const cells = document.querySelectorAll('td');
// Mock getElementsByTagName to control what's returned
jest.spyOn(mockTable.originalElement, 'getElementsByTagName').mockImplementation((tag) => {
if (tag === "th") {
return []; // No header elements
} else if (tag === "tbody") {
return [document.querySelector('tbody')];
} else if (tag === "tr") {
return document.querySelectorAll('tr');
} else if (tag === "td") {
return cells;
}
return [];
});
// Initialize the module
htmlTableImport.initialize();
// Verify that parseTable was called
expect(htmlTableImport.parseTable).toHaveBeenCalled();
// Verify events were dispatched
expect(mockTable.externalEvents.dispatch).toHaveBeenCalledWith("htmlImporting");
expect(mockTable.externalEvents.dispatch).toHaveBeenCalledWith("htmlImported");
// Verify columns were generated
expect(mockTable.options.columns.length).toBe(3);
expect(mockTable.options.columns[0].field).toBe("col0");
expect(mockTable.options.columns[1].field).toBe("col1");
expect(mockTable.options.columns[2].field).toBe("col2");
// Data should have been processed using our mock columns
expect(mockTable.options.data.length).toBe(2);
expect(mockTable.options.data[0]).toHaveProperty("id", 0); // Auto-generated index
});
it("should extract tabulator-specific attributes", () => {
// Add the layout option to the mockTable.options so that it can be extracted
mockTable.options.layout = undefined;
mockTable.options.height = undefined;
// Create a table with Tabulator-specific attributes on headers
const tableHTML = `
<table tabulator-layout="fitColumns" tabulator-height="300px">
<thead>
<tr>
<th tabulator-width="150" tabulator-headerFilter="true">Name</th>
<th tabulator-formatter="number">Age</th>
<th>City</th>
</tr>
</thead>
<tbody>
<tr>
<td>John</td>
<td>25</td>
<td>New York</td>
</tr>
</tbody>
</table>
`;
// Add the table to the document
document.body.innerHTML = tableHTML;
// Set the table as the original element
mockTable.originalElement = document.querySelector('table');
// Add layout and height to available options list
mockTable.optionsList.generate.mockImplementation((defaults, options) => {
return { ...defaults, ...options };
});
// Mock _extractOptions to record the attributes
const originalExtractOptions = htmlTableImport._extractOptions;
htmlTableImport._extractOptions = jest.fn((element, options, defaultOptions) => {
if(element.hasAttribute("tabulator-layout")) {
options.layout = element.getAttribute("tabulator-layout");
}
if(element.hasAttribute("tabulator-height")) {
options.height = element.getAttribute("tabulator-height");
}
// Call the actual method for column options
if(element.tagName === "TH") {
originalExtractOptions.call(htmlTableImport, element, options, defaultOptions);
}
});
// Initialize the module
htmlTableImport.initialize();
// Check if table options were extracted
expect(mockTable.options.layout).toBe("fitColumns");
expect(mockTable.options.height).toBe("300px");
// Column options should still be processed with original method
expect(mockTable.options.columns.length).toBe(3);
expect(mockTable.options.columns[0].title).toBe("Name");
// Restore the original method
htmlTableImport._extractOptions = originalExtractOptions;
});
it("should handle attribute values correctly", () => {
// Test the _attribValue method
expect(htmlTableImport._attribValue("true")).toBe(true);
expect(htmlTableImport._attribValue("false")).toBe(false);
expect(htmlTableImport._attribValue("123")).toBe("123");
expect(htmlTableImport._attribValue("string")).toBe("string");
});
it("should handle tables with column widths", () => {
// Create a table with width attributes on headers
const tableHTML = `
<table>
<thead>
<tr>
<th width="200">Name</th>
<th width="100">Age</th>
<th width="300">City</th>
</tr>
</thead>
<tbody>
<tr>
<td>John</td>
<td>25</td>
<td>New York</td>
</tr>
</tbody>
</table>
`;
// Add the table to the document
document.body.innerHTML = tableHTML;
// Set the table as the original element
mockTable.originalElement = document.querySelector('table');
// Initialize the module
htmlTableImport.initialize();
// Check if column widths were extracted
expect(mockTable.options.columns[0].width).toBe("200");
expect(mockTable.options.columns[1].width).toBe("100");
expect(mockTable.options.columns[2].width).toBe("300");
});
it("should check if a field matches the index", () => {
// Create a table with headers including the index field
const tableHTML = `
<table>
<thead>
<tr>
<th>ID</th>
<th>Name</th>
</tr>
</thead>
<tbody>
<tr>
<td>1</td>
<td>John</td>
</tr>
</tbody>
</table>
`;
// Add the table to the document
document.body.innerHTML = tableHTML;
// Set the table as the original element and set the index field
mockTable.originalElement = document.querySelector('table');
mockTable.options.index = "id";
// Initialize the module
htmlTableImport.initialize();
// Check if the module recognized the index field
expect(htmlTableImport.hasIndex).toBe(true);
// Check that no auto-generated index was added to the data
expect(mockTable.options.data[0]).not.toHaveProperty("id", 0);
expect(mockTable.options.data[0].id).toBe("1");
});
it("should use existing column definitions if they match", () => {
// Create existing column definitions
mockTable.options.columns = [
{ title: "Name", field: "name", formatter: "text" },
{ title: "Age", field: "custom_age" }
];
// Create a table with headers
const tableHTML = `
<table>
<thead>
<tr>
<th>Name</th>
<th>Age</th>
<th>City</th>
</tr>
</thead>
<tbody>
<tr>
<td>John</td>
<td>25</td>
<td>New York</td>
</tr>
</tbody>
</table>
`;
// Add the table to the document
document.body.innerHTML = tableHTML;
// Set the table as the original element
mockTable.originalElement = document.querySelector('table');
// Initialize the module
htmlTableImport.initialize();
// Check that the existing column definition was used
expect(mockTable.options.columns.length).toBe(3);
expect(mockTable.options.columns[0].formatter).toBe("text");
expect(mockTable.options.columns[1].field).toBe("custom_age"); // Should keep the custom field name
expect(mockTable.options.columns[2].field).toBe("city"); // New column added
// Check that data was mapped to the correct fields
expect(mockTable.options.data[0].name).toBe("John");
expect(mockTable.options.data[0].custom_age).toBe("25");
expect(mockTable.options.data[0].city).toBe("New York");
});
});

View File

@@ -0,0 +1,392 @@
import Import from "../../../src/js/modules/Import/Import";
import defaultImporters from "../../../src/js/modules/Import/defaults/importers";
describe("Import module", () => {
/** @type {Import} */
let importMod;
let mockTable;
beforeEach(() => {
// Create mock optionsList
const mockOptionsList = {
register: jest.fn(),
generate: jest.fn().mockImplementation((defaults, options) => {
return { ...defaults, ...options };
})
};
// Create mock eventBus
const mockEventBus = {
subscribe: jest.fn(),
dispatch: jest.fn(),
};
// Create mock externalEvents
const mockExternalEvents = {
dispatch: jest.fn(),
};
// Create mock dataLoader
const mockDataLoader = {
alertLoader: jest.fn(),
alertError: jest.fn(),
clearAlert: jest.fn()
};
// Create column mock
const mockColumnField = "columnField";
const mockColumn = {
getField: jest.fn().mockReturnValue(mockColumnField),
getDefinition: jest.fn().mockReturnValue({ title: "Column" })
};
// Create mock module manager
const mockModuleManager = {
mutator: {
transformRow: jest.fn(row => row)
}
};
// Create a simplified mock of the table
mockTable = {
options: {
importFormat: null,
importReader: "text",
importHeaderTransform: null,
importValueTransform: null
},
modules: mockModuleManager,
dataLoader: mockDataLoader,
eventBus: mockEventBus,
externalEvents: mockExternalEvents,
optionsList: mockOptionsList,
getColumns: jest.fn().mockReturnValue([mockColumn]),
setData: jest.fn().mockResolvedValue(),
};
// Mock methods in the Import prototype
jest.spyOn(Import.prototype, 'registerTableOption').mockImplementation(function(key, value) {
this.table.optionsList.register(key, value);
});
jest.spyOn(Import.prototype, 'registerTableFunction').mockImplementation(function(name, callback) {
this.table[name] = callback;
});
jest.spyOn(Import.prototype, 'subscribe').mockImplementation(function(key, callback, priority) {
this.table.eventBus.subscribe(key, callback, priority);
});
jest.spyOn(Import.prototype, 'dispatch').mockImplementation(function(event, ...args) {
this.table.eventBus.dispatch(event, ...args);
});
jest.spyOn(Import.prototype, 'dispatchExternal').mockImplementation(function(event, ...args) {
this.table.externalEvents.dispatch(event, ...args);
});
// Create an instance of the Import module with the mock table
importMod = new Import(mockTable);
// Initialize the module
importMod.initialize();
});
afterEach(() => {
jest.clearAllMocks();
jest.restoreAllMocks();
});
it("should register table options", () => {
// Verify that the correct options were registered
expect(mockTable.optionsList.register).toHaveBeenCalledWith("importFormat", undefined);
expect(mockTable.optionsList.register).toHaveBeenCalledWith("importReader", "text");
expect(mockTable.optionsList.register).toHaveBeenCalledWith("importHeaderTransform", undefined);
expect(mockTable.optionsList.register).toHaveBeenCalledWith("importValueTransform", undefined);
expect(mockTable.optionsList.register).toHaveBeenCalledWith("importDataValidator", undefined);
expect(mockTable.optionsList.register).toHaveBeenCalledWith("importFileValidator", undefined);
});
it("should register table functions", () => {
// Verify that the import function was registered
expect(mockTable.import).toBeDefined();
expect(typeof mockTable.import).toBe("function");
});
it("should subscribe to data loading events when importFormat is set", () => {
// Set importFormat
mockTable.options.importFormat = "csv";
// Re-initialize the module
importMod.initialize();
// Verify that the module subscribed to data events
expect(mockTable.eventBus.subscribe).toHaveBeenCalledWith("data-loading", expect.any(Function), 10);
expect(mockTable.eventBus.subscribe).toHaveBeenCalledWith("data-load", expect.any(Function), 10);
});
it("should properly check if data should be loaded through import", () => {
// Test with string data and importFormat set
mockTable.options.importFormat = "csv";
expect(importMod.loadDataCheck("some csv data")).toBe(true);
// Test with array data and importFormat set
expect(importMod.loadDataCheck([["header1", "header2"], ["data1", "data2"]])).toBe(true);
// Test with object data (should return false)
expect(importMod.loadDataCheck({ key: "value" })).toBe(false);
// Test with importFormat not set
mockTable.options.importFormat = null;
// Should simply evaluate as falsy, doesn't matter if specifically false
expect(Boolean(importMod.loadDataCheck("some csv data"))).toBe(false);
});
it("should lookup importers correctly", () => {
// Mock console.error to suppress error messages
const originalConsoleError = console.error;
console.error = jest.fn();
// Test looking up built-in importers
mockTable.options.importFormat = "csv";
expect(importMod.lookupImporter()).toBe(defaultImporters.csv);
mockTable.options.importFormat = "json";
expect(importMod.lookupImporter()).toBe(defaultImporters.json);
// Test looking up custom importer function
const customImporter = () => {};
mockTable.options.importFormat = customImporter;
expect(importMod.lookupImporter()).toBe(customImporter);
// Test with non-existent importer
mockTable.options.importFormat = "nonexistent";
expect(importMod.lookupImporter()).toBeUndefined();
expect(console.error).toHaveBeenCalled();
// Restore console.error
console.error = originalConsoleError;
});
it("should transform headers using importHeaderTransform option", () => {
// Set up importHeaderTransform option
mockTable.options.importHeaderTransform = jest.fn((value) => value.toUpperCase());
// Test header transformation
const headers = ["name", "age", "city"];
const transformed = importMod.transformHeader(headers);
// Verify transformation
expect(transformed).toEqual(["NAME", "AGE", "CITY"]);
expect(mockTable.options.importHeaderTransform).toHaveBeenCalledTimes(3);
});
it("should transform data using importValueTransform option", () => {
// Set up importValueTransform option
mockTable.options.importValueTransform = jest.fn((value) => {
if (typeof value === 'string') {
return value.trim();
}
return value;
});
// Test data transformation
const rowData = [" John ", 25, " New York "];
const transformed = importMod.transformData(rowData);
// Verify transformation
expect(transformed).toEqual(["John", 25, "New York"]);
expect(mockTable.options.importValueTransform).toHaveBeenCalledTimes(3);
});
it("should structure array data to object format", () => {
// Mock the transform functions
jest.spyOn(importMod, 'transformHeader').mockImplementation(headers => headers);
jest.spyOn(importMod, 'transformData').mockImplementation(data => data);
// Test data
const arrayData = [
["name", "age", "city"],
["John", 25, "New York"],
["Jane", 30, "London"]
];
// Structure data
const result = importMod.structureArrayToObject(arrayData);
// Verify structured data
expect(result).toEqual([
{ name: "John", age: 25, city: "New York" },
{ name: "Jane", age: 30, city: "London" }
]);
// Verify transform functions were called
expect(importMod.transformHeader).toHaveBeenCalledWith(["name", "age", "city"]);
expect(importMod.transformData).toHaveBeenCalledWith(["John", 25, "New York"]);
expect(importMod.transformData).toHaveBeenCalledWith(["Jane", 30, "London"]);
});
it("should structure array data to column format", () => {
// Mock the transform functions
jest.spyOn(importMod, 'transformHeader').mockImplementation(headers => headers);
jest.spyOn(importMod, 'transformData').mockImplementation(data => data);
// Test data without header row
const arrayData = [
["Data1", 25, "Location1"],
["Data2", 30, "Location2"]
];
// Structure data
const result = importMod.structureArrayToColumns(arrayData);
// Verify structured data (should match columns)
expect(result).toEqual([
{ columnField: "Data1" },
{ columnField: "Data2" }
]);
// Test data with header row matching column title
const arrayDataWithHeader = [
["Column", "Age", "City"], // Header row
["Data1", 25, "Location1"],
["Data2", 30, "Location2"]
];
// Structure data
const resultWithHeader = importMod.structureArrayToColumns(arrayDataWithHeader);
// Verify structured data (header should be skipped)
expect(resultWithHeader).toEqual([
{ columnField: "Data1" },
{ columnField: "Data2" }
]);
});
it("should validate file with custom validator", () => {
// Set up custom file validator
mockTable.options.importFileValidator = jest.fn(file => {
if (file.size > 1000000) {
return "File is too large";
}
return true;
});
// Test with valid file
const validFile = { name: "data.csv", size: 500000 };
expect(importMod.validateFile(validFile)).toBe(true);
// Test with invalid file
const invalidFile = { name: "data.csv", size: 2000000 };
expect(importMod.validateFile(invalidFile)).toBe("File is too large");
// Verify validator was called
expect(mockTable.options.importFileValidator).toHaveBeenCalledTimes(2);
});
it("should validate data with custom validator", async () => {
// Set up custom data validator
mockTable.options.importDataValidator = jest.fn(data => {
if (data.length === 0) {
return "Data cannot be empty";
}
return true;
});
// Test with valid data
const validData = [{ name: "John" }];
const validResult = await importMod.validateData(validData);
expect(validResult).toEqual(validData);
// Test with invalid data
const invalidData = [];
await expect(importMod.validateData(invalidData)).rejects.toBe("Data cannot be empty");
// Verify validator was called
expect(mockTable.options.importDataValidator).toHaveBeenCalledTimes(2);
});
it("should mutate data using the mutator module", () => {
// Mock the mutator transform function
mockTable.modules.mutator.transformRow.mockImplementation(row => {
return { ...row, transformed: true };
});
// Test data
const data = [
{ name: "John" },
{ name: "Jane" }
];
// Mutate data
const result = importMod.mutateData(data);
// Verify mutated data
expect(result).toEqual([
{ name: "John", transformed: true },
{ name: "Jane", transformed: true }
]);
// Verify mutator was called
expect(mockTable.modules.mutator.transformRow).toHaveBeenCalledTimes(2);
expect(mockTable.modules.mutator.transformRow).toHaveBeenCalledWith({ name: "John" }, "import");
expect(mockTable.modules.mutator.transformRow).toHaveBeenCalledWith({ name: "Jane" }, "import");
});
it("should set data and dispatch events", async () => {
// Test data
const data = [{ name: "John" }];
// Set data
await importMod.setData(data);
// Verify events were dispatched
expect(mockTable.eventBus.dispatch).toHaveBeenCalledWith("import-imported", data);
expect(mockTable.externalEvents.dispatch).toHaveBeenCalledWith("importImported", data);
// Verify dataLoader was called
expect(mockTable.dataLoader.clearAlert).toHaveBeenCalled();
// Verify setData was called
expect(mockTable.setData).toHaveBeenCalledWith(data);
});
it("should process data based on format type", () => {
// Mock autoColumns option
mockTable.options.autoColumns = true;
// Mock structureArrayToObject to return expected data
jest.spyOn(importMod, 'structureArrayToObject').mockImplementation(() => {
return [{ name: "John", age: 25 }];
});
// Test with array data
const arrayData = [["name", "age"], ["John", 25]];
expect(importMod.structureData(arrayData)).toEqual([{ name: "John", age: 25 }]);
// Test with non-array data
const objectData = { key: "value" };
expect(importMod.structureData(objectData)).toEqual(objectData);
});
it("should load data through import when triggered by data-load event", async () => {
// Mock the required functions
jest.spyOn(importMod, 'lookupImporter').mockReturnValue(() => {
return [{ name: "Imported" }];
});
jest.spyOn(importMod, 'structureData').mockImplementation(data => data);
// Set importFormat
mockTable.options.importFormat = "csv";
// Trigger data load
const result = await importMod.loadData("some csv data");
// Verify functions were called
expect(importMod.lookupImporter).toHaveBeenCalled();
expect(importMod.structureData).toHaveBeenCalledWith([{ name: "Imported" }]);
// Verify result
expect(result).toEqual([{ name: "Imported" }]);
});
});

View File

@@ -0,0 +1,394 @@
import Interaction from "../../../src/js/modules/Interaction/Interaction";
describe("Interaction module", () => {
/** @type {Interaction} */
let interaction;
let mockTable;
beforeEach(() => {
// Create mock optionsList
const mockOptionsList = {
register: jest.fn(),
};
// Create mock columnManager
const mockColumnManager = {
optionsList: mockOptionsList
};
// Create mock eventBus
const mockEventBus = {
subscribe: jest.fn(),
unsubscribe: jest.fn(),
};
// Create mock externalEvents
const mockExternalEvents = {
dispatch: jest.fn(),
subscribed: jest.fn(),
subscriptionChange: jest.fn(),
};
// Create a simplified mock of the table
mockTable = {
modExists: jest.fn(() => false),
modules: {},
columnManager: mockColumnManager,
options: {},
eventBus: mockEventBus,
externalEvents: mockExternalEvents,
};
// Mock methods in the Interaction prototype
jest.spyOn(Interaction.prototype, 'registerColumnOption').mockImplementation(function(key) {
this.table.columnManager.optionsList.register(key);
});
jest.spyOn(Interaction.prototype, 'subscribe').mockImplementation(function(key, callback) {
return this.table.eventBus.subscribe(key, callback);
});
jest.spyOn(Interaction.prototype, 'unsubscribe').mockImplementation(function(key, callback) {
return this.table.eventBus.unsubscribe(key, callback);
});
jest.spyOn(Interaction.prototype, 'dispatchExternal').mockImplementation(function(event, ...args) {
this.table.externalEvents.dispatch(event, ...args);
});
jest.spyOn(Interaction.prototype, 'subscriptionChangeExternal').mockImplementation(function(event, callback) {
this.table.externalEvents.subscriptionChange(event, callback);
});
jest.spyOn(Interaction.prototype, 'subscribedExternal').mockImplementation(function(event) {
return this.table.externalEvents.subscribed(event);
});
// Create an instance of the Interaction module with the mock table
interaction = new Interaction(mockTable);
});
afterEach(() => {
jest.clearAllMocks();
jest.restoreAllMocks();
});
it("should register all interaction-related column options", () => {
// Check that all header events are registered
expect(mockTable.columnManager.optionsList.register).toHaveBeenCalledWith("headerClick");
expect(mockTable.columnManager.optionsList.register).toHaveBeenCalledWith("headerDblClick");
expect(mockTable.columnManager.optionsList.register).toHaveBeenCalledWith("headerContext");
expect(mockTable.columnManager.optionsList.register).toHaveBeenCalledWith("headerMouseEnter");
expect(mockTable.columnManager.optionsList.register).toHaveBeenCalledWith("headerMouseLeave");
expect(mockTable.columnManager.optionsList.register).toHaveBeenCalledWith("headerMouseOver");
expect(mockTable.columnManager.optionsList.register).toHaveBeenCalledWith("headerMouseOut");
expect(mockTable.columnManager.optionsList.register).toHaveBeenCalledWith("headerMouseMove");
expect(mockTable.columnManager.optionsList.register).toHaveBeenCalledWith("headerMouseDown");
expect(mockTable.columnManager.optionsList.register).toHaveBeenCalledWith("headerMouseUp");
expect(mockTable.columnManager.optionsList.register).toHaveBeenCalledWith("headerTap");
expect(mockTable.columnManager.optionsList.register).toHaveBeenCalledWith("headerDblTap");
expect(mockTable.columnManager.optionsList.register).toHaveBeenCalledWith("headerTapHold");
// Check that all cell events are registered
expect(mockTable.columnManager.optionsList.register).toHaveBeenCalledWith("cellClick");
expect(mockTable.columnManager.optionsList.register).toHaveBeenCalledWith("cellDblClick");
expect(mockTable.columnManager.optionsList.register).toHaveBeenCalledWith("cellContext");
expect(mockTable.columnManager.optionsList.register).toHaveBeenCalledWith("cellMouseEnter");
expect(mockTable.columnManager.optionsList.register).toHaveBeenCalledWith("cellMouseLeave");
expect(mockTable.columnManager.optionsList.register).toHaveBeenCalledWith("cellMouseOver");
expect(mockTable.columnManager.optionsList.register).toHaveBeenCalledWith("cellMouseOut");
expect(mockTable.columnManager.optionsList.register).toHaveBeenCalledWith("cellMouseMove");
expect(mockTable.columnManager.optionsList.register).toHaveBeenCalledWith("cellMouseDown");
expect(mockTable.columnManager.optionsList.register).toHaveBeenCalledWith("cellMouseUp");
expect(mockTable.columnManager.optionsList.register).toHaveBeenCalledWith("cellTap");
expect(mockTable.columnManager.optionsList.register).toHaveBeenCalledWith("cellDblTap");
expect(mockTable.columnManager.optionsList.register).toHaveBeenCalledWith("cellTapHold");
});
it("should initialize and subscribe to events", () => {
// Run initialize
interaction.initialize();
// Verify subscriptions
expect(mockTable.eventBus.subscribe).toHaveBeenCalledWith("column-init", expect.any(Function));
expect(mockTable.eventBus.subscribe).toHaveBeenCalledWith("cell-dblclick", expect.any(Function));
expect(mockTable.eventBus.subscribe).toHaveBeenCalledWith("scroll-horizontal", expect.any(Function));
expect(mockTable.eventBus.subscribe).toHaveBeenCalledWith("scroll-vertical", expect.any(Function));
// Verify external event subscriptions are initialized
expect(mockTable.externalEvents.subscriptionChange).toHaveBeenCalled();
});
it("should clear touch watchers", () => {
// Setup some watch values
interaction.touchWatchers.row.tap = {};
interaction.touchWatchers.cell.tapHold = {};
// Call clear method
interaction.clearTouchWatchers();
// Verify all watchers are cleared
expect(interaction.touchWatchers.row.tap).toBeNull();
expect(interaction.touchWatchers.row.tapDbl).toBeNull();
expect(interaction.touchWatchers.row.tapHold).toBeNull();
expect(interaction.touchWatchers.cell.tap).toBeNull();
expect(interaction.touchWatchers.cell.tapDbl).toBeNull();
expect(interaction.touchWatchers.cell.tapHold).toBeNull();
expect(interaction.touchWatchers.column.tap).toBeNull();
expect(interaction.touchWatchers.column.tapDbl).toBeNull();
expect(interaction.touchWatchers.column.tapHold).toBeNull();
expect(interaction.touchWatchers.group.tap).toBeNull();
expect(interaction.touchWatchers.group.tapDbl).toBeNull();
expect(interaction.touchWatchers.group.tapHold).toBeNull();
});
it("should handle subscription changes for regular events", () => {
// Mock the subscribe method
jest.spyOn(interaction, 'subscribe');
// Test subscribing to a regular event (with dash in name)
interaction.subscriptionChanged("cellClick", true);
// Verify that the internal event was subscribed
expect(interaction.subscribers.cellClick).toBeDefined();
expect(interaction.subscribe).toHaveBeenCalledWith("cell-click", interaction.subscribers.cellClick);
// Test unsubscribing
jest.spyOn(interaction, 'unsubscribe');
jest.spyOn(interaction, 'subscribedExternal').mockReturnValue(false);
// Store the subscriber function before unsubscribing
const subscriberFunc = interaction.subscribers.cellClick;
interaction.subscriptionChanged("cellClick", false);
// Verify that the internal event was unsubscribed
expect(interaction.unsubscribe).toHaveBeenCalledWith("cell-click", subscriberFunc);
expect(interaction.subscribers.cellClick).toBeUndefined();
});
it("should handle subscription changes for touch events", () => {
// Mock the subscribeTouchEvents and unsubscribeTouchEvents methods
jest.spyOn(interaction, 'subscribeTouchEvents');
jest.spyOn(interaction, 'unsubscribeTouchEvents');
// Test subscribing to a touch event (without dash in name)
interaction.subscriptionChanged("cellTap", true);
// Verify that the touch events were subscribed
expect(interaction.subscribeTouchEvents).toHaveBeenCalledWith("cellTap");
// Test unsubscribing
interaction.subscriptionChanged("cellTap", false);
// Verify that the touch events were unsubscribed
expect(interaction.unsubscribeTouchEvents).toHaveBeenCalledWith("cellTap");
});
it("should subscribe to touch events", () => {
// Mock the subscribe method
jest.spyOn(interaction, 'subscribe');
// Subscribe to touch events
interaction.subscribeTouchEvents("cellTap");
// Verify that the touchstart and touchend events were subscribed
expect(interaction.subscribe).toHaveBeenCalledWith("cell-touchstart", expect.any(Function));
expect(interaction.subscribe).toHaveBeenCalledWith("cell-touchend", expect.any(Function));
// Verify that the subscribers flag is set
expect(interaction.subscribers.cellTap).toBe(true);
// Verify that the touch subscribers were created
expect(interaction.touchSubscribers["cell-touchstart"]).toBeDefined();
expect(interaction.touchSubscribers["cell-touchend"]).toBeDefined();
});
it("should unsubscribe from touch events", () => {
// Setup for unsubscribe test
interaction.subscribers.cellTap = true;
interaction.touchSubscribers["cell-touchstart"] = function() {};
interaction.touchSubscribers["cell-touchend"] = function() {};
// Mock methods
jest.spyOn(interaction, 'unsubscribe');
jest.spyOn(interaction, 'subscribedExternal').mockReturnValue(false);
// Unsubscribe from touch events
interaction.unsubscribeTouchEvents("cellTap");
// We've already verified the unsubscribeTouchEvents method was called
// We just need to check side effects of the call
// Verify that the subscribers flag is removed
expect(interaction.subscribers.cellTap).toBeUndefined();
// Verify that the touch subscribers were removed
expect(interaction.touchSubscribers["cell-touchstart"]).toBeUndefined();
expect(interaction.touchSubscribers["cell-touchend"]).toBeUndefined();
});
it("should initialize a column with interaction handlers", () => {
// Create a mock column with interaction handlers in definition
const mockColumn = {
definition: {
cellClick: jest.fn(),
headerMouseEnter: jest.fn()
},
getComponent: jest.fn().mockReturnValue({})
};
// Mock the subscriptionChanged method
jest.spyOn(interaction, 'subscriptionChanged');
// Initialize the column
interaction.initializeColumn(mockColumn);
// Verify that subscriptionChanged was called for each interaction handler
expect(interaction.subscriptionChanged).toHaveBeenCalledWith("cellClick", true);
expect(interaction.subscriptionChanged).toHaveBeenCalledWith("headerMouseEnter", true);
// Verify that the column was added to columnSubscribers
expect(interaction.columnSubscribers.cellClick).toContain(mockColumn);
expect(interaction.columnSubscribers.headerMouseEnter).toContain(mockColumn);
});
it("should handle events and dispatch them", () => {
// Mock the dispatchEvent method
jest.spyOn(interaction, 'dispatchEvent');
// Create a mock event and component
const mockEvent = { type: "click" };
const mockComponent = { getComponent: jest.fn().mockReturnValue({}) };
// Handle an event
interaction.handle("cellClick", mockEvent, mockComponent);
// Verify dispatchEvent was called with the correct parameters
expect(interaction.dispatchEvent).toHaveBeenCalledWith("cellClick", mockEvent, mockComponent);
});
it("should handle touch events", () => {
// Mock the dispatchEvent method
jest.spyOn(interaction, 'dispatchEvent');
// Create a mock event and component
const mockEvent = { type: "touchstart" };
const mockComponent = { getComponent: jest.fn().mockReturnValue({}) };
// Handle a touchstart event
interaction.handleTouch("cell", "start", mockEvent, mockComponent);
// Check that the tap flag is set
expect(interaction.touchWatchers.cell.tap).toBe(true);
// Handle a touchend event
interaction.handleTouch("cell", "end", mockEvent, mockComponent);
// Verify dispatchEvent was called with the correct parameters for tap
expect(interaction.dispatchEvent).toHaveBeenCalledWith("cellTap", mockEvent, mockComponent);
// Check that the tap flag is cleared
expect(interaction.touchWatchers.cell.tap).toBeNull();
// Check that the tapDbl timer is set
expect(interaction.touchWatchers.cell.tapDbl).not.toBeNull();
// Fast forward time to test double tap
jest.useFakeTimers();
// Handle a second touchstart/end within the double tap interval
interaction.handleTouch("cell", "start", mockEvent, mockComponent);
interaction.handleTouch("cell", "end", mockEvent, mockComponent);
// Verify dispatchEvent was called with the correct parameters for double tap
expect(interaction.dispatchEvent).toHaveBeenCalledWith("cellDblTap", mockEvent, mockComponent);
// Reset timer and interaction watchers
jest.useRealTimers();
interaction.clearTouchWatchers();
});
it("should dispatch events externally", () => {
// Create a mock component
const mockComponent = { comp: true };
// Mock dispatchExternal
jest.spyOn(interaction, 'dispatchExternal');
// Create a mock event
const mockEvent = { type: "click" };
// Dispatch the external event directly
interaction.dispatchExternal("cellClick", mockEvent, mockComponent);
// Verify dispatchExternal was called with the correct parameters
expect(interaction.dispatchExternal).toHaveBeenCalledWith("cellClick", mockEvent, mockComponent);
});
it("should handle cell contents selection on double click", () => {
// Mock document functions
const mockRange = {
selectNode: jest.fn(),
moveToElementText: jest.fn(),
select: jest.fn()
};
document.createRange = jest.fn().mockReturnValue(mockRange);
window.getSelection = jest.fn().mockReturnValue({
removeAllRanges: jest.fn(),
addRange: jest.fn()
});
// Create mock event
const mockEvent = {
preventDefault: jest.fn()
};
// Create mock cell
const mockCell = {
getElement: jest.fn().mockReturnValue(document.createElement('div'))
};
// Set edit module to not exist
mockTable.modExists.mockReturnValue(false);
// Call the cell contents selection fixer
interaction.cellContentsSelectionFixer(mockEvent, mockCell);
// Verify event was prevented
expect(mockEvent.preventDefault).toHaveBeenCalled();
// Verify selection functions were called
expect(document.createRange).toHaveBeenCalled();
expect(mockRange.selectNode).toHaveBeenCalledWith(mockCell.getElement());
expect(window.getSelection().removeAllRanges).toHaveBeenCalled();
expect(window.getSelection().addRange).toHaveBeenCalledWith(mockRange);
});
it("should not select cell contents if cell is being edited", () => {
// Mock event
const mockEvent = {
preventDefault: jest.fn()
};
// Create mock cell
const mockCell = {
getElement: jest.fn()
};
// Set up edit module
mockTable.modExists.mockReturnValue(true);
mockTable.modules.edit = {
currentCell: mockCell
};
// Call the cell contents selection fixer
interaction.cellContentsSelectionFixer(mockEvent, mockCell);
// Verify event was not prevented (early return)
expect(mockEvent.preventDefault).not.toHaveBeenCalled();
});
});

View File

@@ -0,0 +1,325 @@
import Keybindings from "../../../src/js/modules/Keybindings/Keybindings";
describe("Keybindings module", () => {
/** @type {Keybindings} */
let keybindingsMod;
let mockTable;
beforeEach(() => {
// Create mock optionsList
const mockOptionsList = {
register: jest.fn(),
generate: jest.fn().mockImplementation((defaults, options) => {
return { ...defaults, ...options };
})
};
// Create mock eventBus
const mockEventBus = {
subscribe: jest.fn(),
unsubscribe: jest.fn(),
subscribed: jest.fn(),
subscriptionChange: jest.fn(),
dispatch: jest.fn(),
chain: jest.fn(),
confirm: jest.fn()
};
// Create mock externalEvents
const mockExternalEvents = {
dispatch: jest.fn(),
subscribed: jest.fn(),
subscriptionChange: jest.fn()
};
// Create a simplified mock of the table
mockTable = {
options: {
keybindings: false // Disable default keybindings for most tests
},
element: {
addEventListener: jest.fn(),
removeEventListener: jest.fn(),
focus: jest.fn()
},
rowManager: {
element: {
clientHeight: 200,
scrollHeight: 600,
scrollTop: 200
},
scrollToRow: jest.fn(),
getDisplayRows: jest.fn().mockReturnValue([
{ id: 1 },
{ id: 2 },
{ id: 3 }
]),
displayRowsCount: 3
},
columnManager: {
optionsList: mockOptionsList
},
optionsList: mockOptionsList,
eventBus: mockEventBus,
externalEvents: mockExternalEvents,
registerTableFunction: jest.fn(),
initGuard: jest.fn()
};
// Mock the prototype methods of the Module class
jest.spyOn(Keybindings.prototype, 'registerTableOption').mockImplementation(function(key, value) {
this.table.optionsList.register(key, value);
});
jest.spyOn(Keybindings.prototype, 'registerColumnOption').mockImplementation(function(key, value) {
this.table.columnManager.optionsList.register(key, value);
});
// Mock subscribe method which is used in initialize
jest.spyOn(Keybindings.prototype, 'subscribe').mockImplementation(function(key, callback) {
return this.table.eventBus.subscribe(key, callback);
});
// Create an instance of the Keybindings module with the mock table
keybindingsMod = new Keybindings(mockTable);
// Mock the dispatch method
keybindingsMod.dispatch = jest.fn();
// Mock registerTableFunction
keybindingsMod.registerTableFunction = function(name, callback) {
mockTable.registerTableFunction(name, callback);
};
// Initialize the module
keybindingsMod.initialize();
});
afterEach(() => {
jest.clearAllMocks();
jest.restoreAllMocks();
});
it("should register keybindings table option during construction", () => {
const testActionFunc = jest.fn();
const mod = new Keybindings({
...mockTable,
options: {
keybindings: {
testAction: "ctrl + q"
}
},
});
Object.assign(Keybindings.actions, {
testAction: testActionFunc,
})
mod.initialize();
mod.keydownBinding(new KeyboardEvent("keydown", { key: "q", ctrlKey: true }));
mod.keyupBinding(new KeyboardEvent("keyup", { key: "q", ctrlKey: true }));
expect(testActionFunc).toHaveBeenCalled();
})
it("should initialize watchKeys and pressedKeys as empty when keybindings disabled", () => {
// With keybindings:false, the module should initialize empty structures
expect(keybindingsMod.watchKeys).toEqual({});
expect(keybindingsMod.pressedKeys).toEqual([]);
});
it("should map default bindings when keybindings is enabled", () => {
// Create a new instance with enabled keybindings
mockTable.options.keybindings = {};
keybindingsMod = new Keybindings(mockTable);
keybindingsMod.dispatch = jest.fn();
// Mock the mapBinding method to inspect what it's called with
keybindingsMod.mapBinding = jest.fn();
keybindingsMod.initialize();
// Verify that mapBinding was called at least once
expect(keybindingsMod.mapBinding).toHaveBeenCalled();
});
it("should map bindings through the public API", () => {
// Create a separate instance to ensure clean state
const instance = new Keybindings({...mockTable, options: {keybindings: false}});
// Initialize the instance to set up watchKeys
instance.initialize();
// Add a spy to track key binding objects
let capturedBindings = [];
const originalMapBinding = instance.mapBinding;
instance.mapBinding = jest.fn((action, binding) => {
capturedBindings.push({action, binding});
return originalMapBinding.call(instance, action, binding);
});
// Map a simple binding
const bindings = {
scrollToStart: "ctrl + home"
};
// Call the method
instance.mapBindings(bindings);
// Verify mapBinding was called with expected parameters
expect(instance.mapBinding).toHaveBeenCalledWith("scrollToStart", "ctrl + home");
expect(capturedBindings.length).toBeGreaterThan(0);
// Verify the first binding was for scrollToStart
const capturedBinding = capturedBindings[0];
expect(capturedBinding.action).toBe("scrollToStart");
expect(capturedBinding.binding).toBe("ctrl + home");
});
it("should handle key combinations correctly", () => {
// Mock a key binding
const binding = {
action: jest.fn(),
keys: [38], // UP arrow
ctrl: true,
shift: false,
meta: false
};
// Create a mock event
const event = {
ctrlKey: true,
shiftKey: false,
metaKey: false
};
// Set pressed keys
keybindingsMod.pressedKeys = [38];
// Test binding check - should match
const result = keybindingsMod.checkBinding(event, binding);
expect(result).toBe(true);
expect(binding.action).toHaveBeenCalled();
// Test with non-matching modifiers
event.shiftKey = true;
const nonMatchResult = keybindingsMod.checkBinding(event, binding);
expect(nonMatchResult).toBe(false);
});
it("should handle clearing bindings", () => {
// Set up the binding references
keybindingsMod.keyupBinding = jest.fn();
keybindingsMod.keydownBinding = jest.fn();
// Call clear bindings
keybindingsMod.clearBindings();
// Check that event listeners were removed
expect(keybindingsMod.table.element.removeEventListener).toHaveBeenCalledWith("keydown", keybindingsMod.keyupBinding);
expect(keybindingsMod.table.element.removeEventListener).toHaveBeenCalledWith("keyup", keybindingsMod.keydownBinding);
});
it("should execute scrollPageUp action by calling scrollToRow for first row when scroll would be negative", () => {
// Create mock event
const event = {
preventDefault: jest.fn()
};
// Change scrollTop to a small value to trigger the "else" branch
keybindingsMod.table.rowManager.element.scrollTop = 100;
keybindingsMod.table.rowManager.element.clientHeight = 200;
// Execute the action directly to test the scrollToRow path
Keybindings.actions.scrollPageUp.call(keybindingsMod, event);
// Check scrollToRow was called with first row
expect(keybindingsMod.table.rowManager.scrollToRow).toHaveBeenCalledWith({ id: 1 });
expect(event.preventDefault).toHaveBeenCalled();
expect(keybindingsMod.table.element.focus).toHaveBeenCalled();
});
it("should execute scrollPageDown action by calling scrollToRow for last row", () => {
// Create mock event
const event = {
preventDefault: jest.fn()
};
// Set scrollTop and scrollHeight to trigger the "else" branch
keybindingsMod.table.rowManager.element.scrollTop = 500;
keybindingsMod.table.rowManager.element.clientHeight = 200;
keybindingsMod.table.rowManager.element.scrollHeight = 600;
// Execute the action directly
Keybindings.actions.scrollPageDown.call(keybindingsMod, event);
// Check that the right methods were called
expect(event.preventDefault).toHaveBeenCalled();
expect(keybindingsMod.table.element.focus).toHaveBeenCalled();
// Check scrollToRow was called with last row since we're at the bottom
expect(keybindingsMod.table.rowManager.scrollToRow).toHaveBeenCalledWith({ id: 3 });
});
it("should execute scrollToStart action correctly", () => {
// Create mock event
const event = {
preventDefault: jest.fn()
};
// Execute the action
Keybindings.actions.scrollToStart.call(keybindingsMod, event);
// Check that event was prevented
expect(event.preventDefault).toHaveBeenCalled();
// Check that focus was called
expect(keybindingsMod.table.element.focus).toHaveBeenCalled();
// Check scrollToRow was called with first row
expect(keybindingsMod.table.rowManager.scrollToRow).toHaveBeenCalledWith({ id: 1 });
});
it("should execute scrollToEnd action correctly", () => {
// Create mock event
const event = {
preventDefault: jest.fn()
};
// Execute the action
Keybindings.actions.scrollToEnd.call(keybindingsMod, event);
// Check that event was prevented
expect(event.preventDefault).toHaveBeenCalled();
// Check that focus was called
expect(keybindingsMod.table.element.focus).toHaveBeenCalled();
// Check scrollToRow was called with last row
expect(keybindingsMod.table.rowManager.scrollToRow).toHaveBeenCalledWith({ id: 3 });
});
it("should execute keyBlock action correctly", () => {
// Create mock event
const event = {
stopPropagation: jest.fn(),
preventDefault: jest.fn()
};
// Execute the action
Keybindings.actions.keyBlock.call(keybindingsMod, event);
// Check that event methods were called
expect(event.stopPropagation).toHaveBeenCalled();
expect(event.preventDefault).toHaveBeenCalled();
});
it("should execute navigation actions correctly", () => {
// Test all navigation actions
const actions = ["navPrev", "navNext", "navUp", "navDown", "navLeft", "navRight"];
const events = {};
actions.forEach(action => {
events[action] = { };
Keybindings.actions[action].call(keybindingsMod, events[action]);
expect(keybindingsMod.dispatch).toHaveBeenCalledWith(`keybinding-${action.replace("nav", "nav-").toLowerCase()}`, events[action]);
});
});
});

View File

@@ -0,0 +1,230 @@
import Layout from "../../../src/js/modules/Layout/Layout";
import defaultModes from "../../../src/js/modules/Layout/defaults/modes";
describe("Layout module", () => {
/** @type {Layout} */
let layout;
let mockTable;
let mockColumnManager;
beforeEach(() => {
// Mock renderer for columnManager
const mockRenderer = {
reinitializeColumnWidths: jest.fn()
};
// Create mock columnManager
mockColumnManager = {
renderer: mockRenderer,
columnsByIndex: []
};
// Create mock element
const mockElement = {
setAttribute: jest.fn()
};
// Create mock eventBus
const mockEventBus = {
subscribe: jest.fn(),
dispatch: jest.fn()
};
// Create mock optionsList
const mockOptionsList = {
register: jest.fn()
};
// Create a simplified mock of the table
mockTable = {
modExists: jest.fn(() => false),
modules: {},
element: mockElement,
columnManager: mockColumnManager,
options: {
layout: "fitData"
},
rowManager: {
element: {
getBoundingClientRect: jest.fn(() => ({ width: 800 })),
scrollHeight: 500,
clientHeight: 400,
offsetWidth: 820,
clientWidth: 800
},
normalizeHeight: jest.fn()
},
eventBus: mockEventBus,
optionsList: mockOptionsList
};
// Mock methods in the Layout prototype
jest.spyOn(Layout.prototype, 'registerTableOption').mockImplementation(function(key, value) {
this.table.options[key] = this.table.options[key] || value;
});
jest.spyOn(Layout.prototype, 'registerColumnOption').mockImplementation(function(key) {
this.table.optionsList.register(key);
});
jest.spyOn(Layout.prototype, 'subscribe').mockImplementation(function(key, callback) {
return this.table.eventBus.subscribe(key, callback);
});
jest.spyOn(Layout.prototype, 'dispatch').mockImplementation(function(event, ...args) {
return this.table.eventBus.dispatch(event, ...args);
});
// Create an instance of the Layout module with the mock table
layout = new Layout(mockTable);
});
afterEach(() => {
jest.clearAllMocks();
jest.restoreAllMocks();
});
it("should register layout options during construction", () => {
// Verify table options are registered
expect(mockTable.options.layout).toBe("fitData");
expect(mockTable.options.layoutColumnsOnNewData).toBe(false);
// Verify column options are registered
expect(mockTable.optionsList.register).toHaveBeenCalledWith("widthGrow");
expect(mockTable.optionsList.register).toHaveBeenCalledWith("widthShrink");
});
it("should initialize with valid mode from options", () => {
// Simulate having modes loaded
Layout.modes = defaultModes;
// Run initialize
layout.initialize();
// Verify mode is set correctly
expect(layout.mode).toBe("fitData");
expect(mockTable.element.setAttribute).toHaveBeenCalledWith("tabulator-layout", "fitData");
expect(layout.table.eventBus.subscribe).toHaveBeenCalledWith("column-init", expect.any(Function));
});
it("should warn and default to fitData when an invalid mode is provided", () => {
// Mock console.warn
const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
// Simulate having modes loaded
Layout.modes = defaultModes;
// Set invalid layout mode
mockTable.options.layout = "invalidMode";
// Run initialize
layout.initialize();
// Verify default mode is set and warning is issued
expect(layout.mode).toBe("fitData");
expect(consoleWarnSpy).toHaveBeenCalledWith("Layout Error - invalid mode set, defaulting to 'fitData' : invalidMode");
expect(mockTable.element.setAttribute).toHaveBeenCalledWith("tabulator-layout", "fitData");
});
it("should properly initialize a column with number values", () => {
// Create mock column
const mockColumn = {
definition: {
widthGrow: "2",
widthShrink: "1.5"
}
};
// Initialize column
layout.initializeColumn(mockColumn);
// Verify values are converted to numbers
expect(mockColumn.definition.widthGrow).toBe(2);
expect(mockColumn.definition.widthShrink).toBe(1.5);
});
it("should return the current layout mode", () => {
// Set a mode
layout.mode = "fitColumns";
// Get the mode
const result = layout.getMode();
// Verify result
expect(result).toBe("fitColumns");
});
it("should trigger layout refresh with the selected mode", () => {
// Prepare a column with variable height
const mockColumn = { definition: { variableHeight: true } };
mockColumnManager.columnsByIndex = [mockColumn];
// Set up the layout mode
layout.mode = "fitData";
// Mock the mode function
Layout.modes = {
fitData: jest.fn()
};
// Call layout method
layout.layout(true);
// Verify dispatch events
expect(mockTable.eventBus.dispatch).toHaveBeenCalledWith("layout-refreshing");
expect(mockTable.eventBus.dispatch).toHaveBeenCalledWith("layout-refreshed");
// Verify the layout mode function was called
expect(Layout.modes.fitData).toHaveBeenCalledWith(mockColumnManager.columnsByIndex, true);
// Verify row height normalization was called (due to variable height column)
expect(mockTable.rowManager.normalizeHeight).toHaveBeenCalledWith(true);
});
it("should not normalize row heights if no variable height columns exist", () => {
// Prepare columns without variable height
const mockColumn = { definition: { variableHeight: false, formatter: "text" } };
mockColumnManager.columnsByIndex = [mockColumn];
// Set up the layout mode
layout.mode = "fitData";
// Mock the mode function
Layout.modes = {
fitData: jest.fn()
};
// Call layout method
layout.layout(false);
// Verify dispatch events
expect(mockTable.eventBus.dispatch).toHaveBeenCalledWith("layout-refreshing");
expect(mockTable.eventBus.dispatch).toHaveBeenCalledWith("layout-refreshed");
// Verify the layout mode function was called
expect(Layout.modes.fitData).toHaveBeenCalledWith(mockColumnManager.columnsByIndex, false);
// Verify row height normalization was not called (no variable height columns)
expect(mockTable.rowManager.normalizeHeight).not.toHaveBeenCalled();
});
it("should recognize textarea formatter as variable height column", () => {
// Prepare a column with textarea formatter
const mockColumn = { definition: { formatter: "textarea" } };
mockColumnManager.columnsByIndex = [mockColumn];
// Set up the layout mode
layout.mode = "fitData";
// Mock the mode function
Layout.modes = {
fitData: jest.fn()
};
// Call layout method
layout.layout(false);
// Verify row height normalization was called (due to textarea formatter)
expect(mockTable.rowManager.normalizeHeight).toHaveBeenCalledWith(true);
});
});

View File

@@ -0,0 +1,325 @@
import Localize from "../../../src/js/modules/Localize/Localize";
describe("Localize module", () => {
/** @type {Localize} */
let localizeMod;
let mockTable;
let mockOptionsList;
beforeEach(() => {
// Create mock optionsList
mockOptionsList = {
register: jest.fn(),
generate: jest.fn().mockImplementation((defaults, options) => {
return { ...defaults, ...options };
})
};
// Create a simplified mock of the table
mockTable = {
options: {
locale: false,
langs: {
"es": {
"groups": {
"item": "artículo",
"items": "artículos"
},
"pagination": {
"page_size": "Tamaño de página"
}
},
"fr": {
"groups": {
"item": "élément",
"items": "éléments"
}
}
},
columnDefaults: {
headerFilterPlaceholder: false
}
},
optionsList: mockOptionsList,
columnManager: {
optionsList: mockOptionsList
},
registerTableFunction: jest.fn()
};
// Mock the prototype methods of the Module class
jest.spyOn(Localize.prototype, 'registerTableOption').mockImplementation(function(key, value) {
this.table.optionsList.register(key, value);
});
// Mock console warn
jest.spyOn(console, 'warn').mockImplementation(() => {});
// Create Localize module instance
localizeMod = new Localize(mockTable);
// Mock dispatchExternal
localizeMod.dispatchExternal = jest.fn();
// Mock registerTableFunction
const originalRegisterTableFunction = localizeMod.registerTableFunction;
localizeMod.registerTableFunction = function(name, callback) {
mockTable.registerTableFunction(name, callback);
};
// Initialize the module
localizeMod.initialize();
});
afterEach(() => {
jest.clearAllMocks();
jest.restoreAllMocks();
});
it("should initialize with default locale", () => {
expect(localizeMod.locale).toBe("default");
expect(localizeMod.lang).toBeDefined();
expect(localizeMod.bindings).toEqual({});
// Check table function registration
expect(mockTable.registerTableFunction).toHaveBeenCalledWith("setLocale", expect.any(Function));
expect(mockTable.registerTableFunction).toHaveBeenCalledWith("getLocale", expect.any(Function));
expect(mockTable.registerTableFunction).toHaveBeenCalledWith("getLang", expect.any(Function));
});
it("should register table options during construction", () => {
// Verify table options are registered
expect(mockTable.optionsList.register).toHaveBeenCalledWith("locale", false);
expect(mockTable.optionsList.register).toHaveBeenCalledWith("langs", {});
});
it("should install langs from table options", () => {
// Check that Spanish lang is installed
expect(localizeMod.langList.es).toBeDefined();
expect(localizeMod.langList.es.groups.item).toBe("artículo");
// Check that French lang is installed
expect(localizeMod.langList.fr).toBeDefined();
expect(localizeMod.langList.fr.groups.item).toBe("élément");
});
it("should set header filter placeholder", () => {
const placeholder = "Type to filter...";
localizeMod.setHeaderFilterPlaceholder(placeholder);
expect(localizeMod.langList.default.headerFilters.default).toBe(placeholder);
});
it("should install new language", () => {
// Install a new language
const germanLang = {
"groups": {
"item": "Element",
"items": "Elemente"
}
};
localizeMod.installLang("de", germanLang);
// Check that language was installed
expect(localizeMod.langList.de).toBeDefined();
expect(localizeMod.langList.de.groups.item).toBe("Element");
});
it("should update existing language", () => {
// Update existing Spanish language
const updatedSpanish = {
"data": {
"loading": "Cargando"
}
};
localizeMod.installLang("es", updatedSpanish);
// Check that language was updated
expect(localizeMod.langList.es.groups.item).toBe("artículo"); // Original value retained
expect(localizeMod.langList.es.data.loading).toBe("Cargando"); // New value added
});
it("should set locale and update language", () => {
// Set locale to Spanish
localizeMod.setLocale("es");
// Check locale is set
expect(localizeMod.locale).toBe("es");
// Check language values are correct
expect(localizeMod.lang.groups.item).toBe("artículo");
expect(localizeMod.lang.pagination.page_size).toBe("Tamaño de página");
// Check that default values are inherited
expect(localizeMod.lang.data.loading).toBe("Loading");
// Check dispatch is called
expect(localizeMod.dispatchExternal).toHaveBeenCalledWith("localized", "es", localizeMod.lang);
});
it("should handle setting locale with browser language", () => {
// Mock navigator language
const originalNavigator = global.navigator;
const mockNavigator = { language: "es-ES" };
Object.defineProperty(global, 'navigator', { value: mockNavigator, writable: true });
// Mock traverseLang function
const originalSetLocale = localizeMod.setLocale;
jest.spyOn(localizeMod, 'setLocale').mockImplementation(function(desiredLocale) {
if (desiredLocale === true) {
desiredLocale = navigator.language.toLowerCase();
}
this.locale = "es"; // Force to spanish for this test
// Simulate dispatch
this.dispatchExternal("localized", this.locale, this.lang);
});
// Set locale to true to use browser language
localizeMod.setLocale(true);
// Check locale is set to es
expect(localizeMod.locale).toBe("es");
// Restore original function
localizeMod.setLocale.mockRestore();
// Restore navigator
Object.defineProperty(global, 'navigator', { value: originalNavigator, writable: true });
});
it("should fall back to language prefix if exact match not found", () => {
// Add Spanish to langList to test fallback
localizeMod.langList = {
"default": localizeMod.langList.default,
"es": localizeMod.langList.es
};
// Set locale to Spanish variant
localizeMod.setLocale("es-MX");
// Should fall back to es
expect(localizeMod.locale).toBe("es");
expect(console.warn).toHaveBeenCalled();
});
it("should fall back to default if no matching language found", () => {
// Set locale to an unsupported language
localizeMod.setLocale("ja");
// Should fall back to default
expect(localizeMod.locale).toBe("default");
expect(console.warn).toHaveBeenCalled();
});
it("should get current locale", () => {
// Set locale to Spanish
localizeMod.setLocale("es");
// Check getLocale returns correct value
expect(localizeMod.getLocale()).toBe("es");
});
it("should get language for specified locale", () => {
// Get Spanish language
const spanishLang = localizeMod.getLang("es");
// Check language is returned
expect(spanishLang).toBe(localizeMod.langList.es);
});
it("should get current language if no locale specified", () => {
// Set locale to Spanish
localizeMod.setLocale("es");
// Get current language
const currentLang = localizeMod.getLang();
// Check language is returned
expect(currentLang).toBe(localizeMod.lang);
});
it("should get text for current locale", () => {
// Set locale to Spanish
localizeMod.setLocale("es");
// Get text
const itemText = localizeMod.getText("groups|item");
// Check text is returned
expect(itemText).toBe("artículo");
});
it("should get text using simplified path", () => {
// Set locale to Spanish
localizeMod.setLocale("es");
// Get text with path and value
const itemText = localizeMod.getText("groups", "item");
// Check text is returned
expect(itemText).toBe("artículo");
});
it("should return empty string if text not found", () => {
// Get text for non-existent path
const nonExistentText = localizeMod.getText("nonexistent|path");
// Check empty string is returned
expect(nonExistentText).toBe("");
});
it("should bind callback to text path", () => {
// Create mock callback
const callback = jest.fn();
// Bind callback to path
localizeMod.bind("groups|item", callback);
// Check callback was called with current text
expect(callback).toHaveBeenCalledWith(localizeMod.getText("groups|item"), localizeMod.lang);
// Check binding was stored
expect(localizeMod.bindings["groups|item"]).toContain(callback);
});
it("should execute bindings when locale changes", () => {
// Create mock callback
const callback = jest.fn();
// Bind callback to path
localizeMod.bind("groups|item", callback);
// Reset mock to clear initial call
callback.mockReset();
// Change locale
localizeMod.setLocale("es");
// Check callback was called with updated text
expect(callback).toHaveBeenCalledWith("artículo", localizeMod.lang);
});
it("should handle multiple bindings to same path", () => {
// Create mock callbacks
const callback1 = jest.fn();
const callback2 = jest.fn();
// Bind callbacks to same path
localizeMod.bind("groups|item", callback1);
localizeMod.bind("groups|item", callback2);
// Reset mocks to clear initial calls
callback1.mockReset();
callback2.mockReset();
// Change locale
localizeMod.setLocale("es");
// Check both callbacks were called
expect(callback1).toHaveBeenCalledWith("artículo", localizeMod.lang);
expect(callback2).toHaveBeenCalledWith("artículo", localizeMod.lang);
});
});

View File

@@ -0,0 +1,357 @@
import TabulatorFull from "../../../src/js/core/TabulatorFull";
import Menu from "../../../src/js/modules/Menu/Menu";
describe("Menu module", () => {
/** @type {TabulatorFull} */
let tabulator;
/** @type {Menu} */
let menuMod;
let tableData = [
{ id: 1, name: "John", age: 30 },
{ id: 2, name: "Jane", age: 25 },
{ id: 3, name: "Bob", age: 35 }
];
let tableColumns = [
{
title: "ID",
field: "id",
headerMenu: [
{
label: "Header Action",
action: jest.fn()
}
]
},
{
title: "Name",
field: "name",
contextMenu: [
{
label: "Cell Action",
action: jest.fn()
}
]
},
{
title: "Age",
field: "age",
headerContextMenu: [
{
label: "Header Context Action",
action: jest.fn()
}
]
}
];
beforeEach(async () => {
const el = document.createElement("div");
el.id = "tabulator";
document.body.appendChild(el);
tabulator = new TabulatorFull("#tabulator", {
data: tableData,
columns: tableColumns,
rowContextMenu: [
{
label: "Row Action",
action: jest.fn()
}
]
});
menuMod = tabulator.module("menu");
// Mock dispatch function
menuMod.dispatchExternal = jest.fn();
menuMod.dispatch = jest.fn();
// Mock popup function
menuMod.popup = jest.fn().mockImplementation(() => {
return {
hide: jest.fn(),
show: jest.fn().mockReturnThis(),
hideOnBlur: jest.fn(),
child: jest.fn().mockReturnValue({
hide: jest.fn(),
show: jest.fn().mockReturnThis(),
})
};
});
return new Promise((resolve) => {
tabulator.on("tableBuilt", () => {
resolve();
});
});
});
afterEach(() => {
tabulator.destroy();
document.getElementById("tabulator")?.remove();
jest.clearAllMocks();
});
it("should initialize with menu options", () => {
// Check row context menu option
expect(Array.isArray(tabulator.options.rowContextMenu)).toBe(true);
expect(tabulator.options.rowContextMenu[0].label).toBe("Row Action");
});
it("should handle row context menu", () => {
// Get a row
const row = tabulator.rowManager.rows[0];
// Create mock event
const mockEvent = new MouseEvent("contextmenu", {
bubbles: true,
cancelable: true
});
// Trigger context menu event
menuMod.loadMenuEvent(tabulator.options.rowContextMenu, mockEvent, row);
// Check popup was called
expect(menuMod.popup).toHaveBeenCalled();
// Check menu element creation
const menuEl = menuMod.popup.mock.calls[0][0];
expect(menuEl.classList.contains("tabulator-menu")).toBe(true);
// Check menu items
const menuItems = menuEl.querySelectorAll(".tabulator-menu-item");
expect(menuItems.length).toBe(1);
expect(menuItems[0].innerHTML).toBe("Row Action");
// Check events were dispatched
expect(menuMod.dispatch).toHaveBeenCalledWith("menu-opened", expect.anything(), expect.anything());
expect(menuMod.dispatchExternal).toHaveBeenCalledWith("menuOpened", expect.anything());
});
it("should handle cell context menu", () => {
// Get a cell with context menu
const nameCell = tabulator.rowManager.rows[0].getCells()[1];
// Create mock event
const mockEvent = new MouseEvent("contextmenu", {
bubbles: true,
cancelable: true
});
// Trigger context menu event
menuMod.loadMenuTableCellEvent("contextMenu", mockEvent, nameCell);
// Check popup was called
expect(menuMod.popup).toHaveBeenCalled();
// Check menu element creation
const menuEl = menuMod.popup.mock.calls[0][0];
// Check menu items
const menuItems = menuEl.querySelectorAll(".tabulator-menu-item");
expect(menuItems.length).toBe(1);
expect(menuItems[0].innerHTML).toBe("Cell Action");
});
it("should handle header context menu", () => {
// Get a column with header context menu
const ageCol = tabulator.columnManager.findColumn("age");
// Create mock event
const mockEvent = new MouseEvent("contextmenu", {
bubbles: true,
cancelable: true
});
// Trigger context menu event
menuMod.loadMenuTableColumnEvent("headerContextMenu", mockEvent, ageCol);
// Check popup was called
expect(menuMod.popup).toHaveBeenCalled();
// Check menu element creation
const menuEl = menuMod.popup.mock.calls[0][0];
// Check menu items
const menuItems = menuEl.querySelectorAll(".tabulator-menu-item");
expect(menuItems.length).toBe(1);
expect(menuItems[0].innerHTML).toBe("Header Context Action");
});
it("should handle menu items with separators", () => {
// Create menu with separator
const menuWithSeparator = [
{
label: "Action 1",
action: jest.fn()
},
{
separator: true
},
{
label: "Action 2",
action: jest.fn()
}
];
// Get a row
const row = tabulator.rowManager.rows[0];
// Create mock event
const mockEvent = new MouseEvent("click");
// Trigger menu event
menuMod.loadMenuEvent(menuWithSeparator, mockEvent, row);
// Check menu element creation
const menuEl = menuMod.popup.mock.calls[0][0];
// Check menu items and separator
const menuItems = menuEl.querySelectorAll(".tabulator-menu-item");
const separators = menuEl.querySelectorAll(".tabulator-menu-separator");
expect(menuItems.length).toBe(2);
expect(separators.length).toBe(1);
});
it("should handle disabled menu items", () => {
// Create menu with disabled item
const menuWithDisabled = [
{
label: "Enabled Action",
action: jest.fn()
},
{
label: "Disabled Action",
action: jest.fn(),
disabled: true
}
];
// Get a row
const row = tabulator.rowManager.rows[0];
// Create mock event
const mockEvent = new MouseEvent("click");
// Trigger menu event
menuMod.loadMenuEvent(menuWithDisabled, mockEvent, row);
// Check menu element creation
const menuEl = menuMod.popup.mock.calls[0][0];
// Check enabled and disabled menu items
const menuItems = menuEl.querySelectorAll(".tabulator-menu-item");
const disabledItems = menuEl.querySelectorAll(".tabulator-menu-item-disabled");
expect(menuItems.length).toBe(2);
expect(disabledItems.length).toBe(1);
expect(disabledItems[0].innerHTML).toBe("Disabled Action");
});
it("should create menu items with submenu class", () => {
// Create menu with submenu
const menuWithSubmenu = [
{
label: "Main Action",
action: jest.fn()
},
{
label: "Submenu",
menu: [
{
label: "Submenu Action",
action: jest.fn()
}
]
}
];
// Get a row
const row = tabulator.rowManager.rows[0];
// Create mock event
const mockEvent = new MouseEvent("click");
// Create rootPopup mock
menuMod.rootPopup = {
hide: jest.fn()
};
// Trigger menu event
menuMod.loadMenuEvent(menuWithSubmenu, mockEvent, row);
// Check menu element creation
const menuEl = menuMod.popup.mock.calls[0][0];
// Check menu items
const menuItems = menuEl.querySelectorAll(".tabulator-menu-item");
const submenuItems = menuEl.querySelectorAll(".tabulator-menu-item-submenu");
expect(menuItems.length).toBe(2);
expect(submenuItems.length).toBe(1);
expect(submenuItems[0].innerHTML).toBe("Submenu");
});
it("should handle menu click to hide", () => {
// Create menu
const menu = [
{
label: "Action",
action: jest.fn()
}
];
// Get a row
const row = tabulator.rowManager.rows[0];
// Create mock event
const mockEvent = new MouseEvent("click");
// Create rootPopup mock
menuMod.rootPopup = {
hide: jest.fn()
};
// Trigger menu event
menuMod.loadMenuEvent(menu, mockEvent, row);
// Get menu element
const menuEl = menuMod.popup.mock.calls[0][0];
// Click on menu element (menu background)
menuEl.click();
// Check rootPopup hide was called
expect(menuMod.rootPopup.hide).toHaveBeenCalled();
});
it("should track currentComponent", () => {
// Create menu
const menu = [
{
label: "Action",
action: jest.fn()
}
];
// Get a row
const row = tabulator.rowManager.rows[0];
// Create mock event
const mockEvent = new MouseEvent("click");
// Setup rootPopup mock
menuMod.rootPopup = null;
// Trigger menu event
menuMod.loadMenuEvent(menu, mockEvent, row);
// Verify currentComponent was set
expect(menuMod.currentComponent).not.toBeNull();
// Reset currentComponent for next test
menuMod.currentComponent = null;
// Verify currentComponent was reset
expect(menuMod.currentComponent).toBeNull();
});
});

View File

@@ -0,0 +1,729 @@
import MoveColumns from "../../../src/js/modules/MoveColumns/MoveColumns";
import Helpers from "../../../src/js/core/tools/Helpers";
describe("MoveColumns module", () => {
/** @type {MoveColumns} */
let moveColumns;
let mockTable;
let mockColumnManager;
beforeEach(() => {
// Mock DOM methods
document.createElement = jest.fn().mockImplementation((tagName) => {
const element = {
tagName: tagName.toUpperCase(),
classList: {
add: jest.fn(),
remove: jest.fn(),
contains: jest.fn()
},
style: {},
parentNode: {
insertBefore: jest.fn(),
removeChild: jest.fn()
},
appendChild: jest.fn(),
cloneNode: jest.fn().mockImplementation(() => ({
classList: {
add: jest.fn(),
remove: jest.fn()
},
style: {}
})),
addEventListener: jest.fn(),
removeEventListener: jest.fn(),
nextSibling: null,
};
return element;
});
// Mock column elements
const createMockColumnElement = () => ({
style: {},
parentNode: {
insertBefore: jest.fn(),
removeChild: jest.fn()
},
appendChild: jest.fn(),
classList: {
add: jest.fn(),
remove: jest.fn()
},
addEventListener: jest.fn(),
removeEventListener: jest.fn(),
nextSibling: null
});
// Create mock columns with modules.moveColumn initialized
const mockColumn1 = {
getElement: jest.fn().mockReturnValue(createMockColumnElement()),
getCells: jest.fn().mockReturnValue([]),
getWidth: jest.fn().mockReturnValue(100),
getHeight: jest.fn().mockReturnValue(30),
parent: "row", // For parent comparison in column movement
isGroup: false,
isRowHeader: false,
nextColumn: jest.fn(),
prevColumn: jest.fn(),
modules: {
moveColumn: {} // Initialize the moveColumn property
}
};
const mockColumn2 = {
getElement: jest.fn().mockReturnValue(createMockColumnElement()),
getCells: jest.fn().mockReturnValue([]),
getWidth: jest.fn().mockReturnValue(150),
getHeight: jest.fn().mockReturnValue(30),
parent: "row", // For parent comparison in column movement
isGroup: false,
isRowHeader: false,
nextColumn: jest.fn(),
prevColumn: jest.fn(),
modules: {
moveColumn: {} // Initialize the moveColumn property
}
};
// Mock frozen column
const mockFrozenColumn = {
getElement: jest.fn().mockReturnValue(createMockColumnElement()),
getCells: jest.fn().mockReturnValue([]),
parent: "row",
isGroup: false,
isRowHeader: false,
modules: {
frozen: {},
moveColumn: {} // Initialize the moveColumn property
}
};
// Create mock contents element
const mockContentsElement = {
scrollLeft: 0,
clientWidth: 1000,
appendChild: jest.fn(),
clientHeight: 50
};
// Create mock headers element
const mockHeadersElement = {
offsetHeight: 40
};
// Create mock columnManager
mockColumnManager = {
columnsByIndex: [mockColumn1, mockColumn2, mockFrozenColumn],
moveColumnActual: jest.fn(),
getContentsElement: jest.fn().mockReturnValue(mockContentsElement),
getHeadersElement: jest.fn().mockReturnValue(mockHeadersElement)
};
// Create mock row manager
const mockRowManager = {
getElement: jest.fn().mockReturnValue({
scrollLeft: 0
}),
element: {
scrollLeft: 0
}
};
// Create mock element
const mockElement = {
classList: {
add: jest.fn(),
remove: jest.fn()
}
};
// Create mock eventBus
const mockEventBus = {
subscribe: jest.fn(),
dispatch: jest.fn()
};
// Create a simplified mock of the table
mockTable = {
modExists: jest.fn((name) => name === "selectRange" ? true : false),
modules: {
selectRange: {
columnSelection: false,
mousedown: false,
selecting: null
}
},
columnManager: mockColumnManager,
rowManager: mockRowManager,
element: mockElement,
options: {
movableColumns: true
},
eventBus: mockEventBus
};
// Mock Helpers.elOffset
jest.spyOn(Helpers, 'elOffset').mockImplementation(() => ({
left: 50,
top: 20
}));
// Mock methods in the MoveColumns prototype
jest.spyOn(MoveColumns.prototype, 'registerTableOption').mockImplementation(function(key, value) {
this.table.options[key] = this.table.options[key] || value;
});
jest.spyOn(MoveColumns.prototype, 'subscribe').mockImplementation(function(key, callback) {
return this.table.eventBus.subscribe(key, callback);
});
jest.spyOn(MoveColumns.prototype, 'dispatch').mockImplementation(function(event, ...args) {
return this.table.eventBus.dispatch(event, ...args);
});
// Create an instance of the MoveColumns module with the mock table
moveColumns = new MoveColumns(mockTable);
// Mock setTimeout
jest.useFakeTimers();
});
afterEach(() => {
jest.clearAllMocks();
jest.restoreAllMocks();
jest.useRealTimers();
});
it("should register movableColumns table option during construction", () => {
// Verify table option is registered
expect(mockTable.options.movableColumns).toBe(true);
// Verify placeholder element is created
expect(document.createElement).toHaveBeenCalledWith("div");
expect(moveColumns.placeholderElement.classList.add).toHaveBeenCalledWith("tabulator-col");
expect(moveColumns.placeholderElement.classList.add).toHaveBeenCalledWith("tabulator-col-placeholder");
});
it("should initialize and subscribe to events if movableColumns is enabled", () => {
// Set movableColumns option
mockTable.options.movableColumns = true;
// Run initialize
moveColumns.initialize();
// Verify event subscriptions
expect(mockTable.eventBus.subscribe).toHaveBeenCalledWith("column-init", expect.any(Function));
expect(mockTable.eventBus.subscribe).toHaveBeenCalledWith("alert-show", expect.any(Function));
});
it("should not subscribe to events if movableColumns is disabled", () => {
// Set movableColumns option to false
mockTable.options.movableColumns = false;
// Run initialize
moveColumns.initialize();
// Verify no subscriptions were made
expect(mockTable.eventBus.subscribe).not.toHaveBeenCalled();
});
it("should abort any move operation when alert is shown", () => {
// Mock clearTimeout
jest.spyOn(global, 'clearTimeout');
// Set timeout
moveColumns.checkTimeout = setTimeout(() => {}, 1000);
// Call abortMove method
moveColumns.abortMove();
// Verify timeout was cleared
expect(clearTimeout).toHaveBeenCalledWith(moveColumns.checkTimeout);
});
it("should initialize a regular column for movement", () => {
// Create mock column
const mockColumn = {
parent: null,
modules: {},
isGroup: false,
isRowHeader: false,
getElement: jest.fn().mockReturnValue({
addEventListener: jest.fn(),
parentNode: {
insertBefore: jest.fn()
}
}),
nextColumn: jest.fn(),
prevColumn: jest.fn()
};
// Spy on bindTouchEvents
jest.spyOn(moveColumns, 'bindTouchEvents');
// Initialize column
moveColumns.initializeColumn(mockColumn);
// Verify event listeners were added
expect(mockColumn.getElement().addEventListener).toHaveBeenCalledWith("mousedown", expect.any(Function));
expect(mockColumn.getElement().addEventListener).toHaveBeenCalledWith("mouseup", expect.any(Function));
// Verify touch events were bound
expect(moveColumns.bindTouchEvents).toHaveBeenCalledWith(mockColumn);
// Verify the column has mousemove configuration
expect(mockColumn.modules.moveColumn).toBeDefined();
expect(mockColumn.modules.moveColumn.mousemove).toBeDefined();
});
it("should not initialize a frozen column for movement", () => {
// Create mock frozen column
const mockColumn = {
parent: null,
modules: {
frozen: {}
},
isGroup: false,
isRowHeader: false,
getElement: jest.fn()
};
// Spy on bindTouchEvents
jest.spyOn(moveColumns, 'bindTouchEvents');
// Initialize column
moveColumns.initializeColumn(mockColumn);
// Verify no event listeners were added
expect(mockColumn.getElement).not.toHaveBeenCalled();
// Verify touch events were not bound
expect(moveColumns.bindTouchEvents).not.toHaveBeenCalled();
// Verify the column has a moveColumn configuration
expect(mockColumn.modules.moveColumn).toBeDefined();
});
it("should not initialize a group column for movement", () => {
// Create mock group column
const mockColumn = {
parent: null,
modules: {},
isGroup: true,
isRowHeader: false,
getElement: jest.fn()
};
// Initialize column
moveColumns.initializeColumn(mockColumn);
// Verify no event listeners were added
expect(mockColumn.getElement).not.toHaveBeenCalled();
});
it("should not initialize a row header column for movement", () => {
// Create mock row header column
const mockColumn = {
parent: null,
modules: {},
isGroup: false,
isRowHeader: true,
getElement: jest.fn()
};
// Initialize column
moveColumns.initializeColumn(mockColumn);
// Verify no event listeners were added
expect(mockColumn.getElement).not.toHaveBeenCalled();
});
it("should start a move operation on mouse down after timeout", () => {
// Create mock column
const mockColumn = {
parent: null,
modules: {},
isGroup: false,
isRowHeader: false,
getElement: jest.fn().mockReturnValue({
addEventListener: jest.fn(),
parentNode: {
insertBefore: jest.fn(),
removeChild: jest.fn()
},
cloneNode: jest.fn().mockReturnValue({
classList: {
add: jest.fn()
},
style: {}
})
}),
getWidth: jest.fn().mockReturnValue(100),
getHeight: jest.fn().mockReturnValue(30),
getCells: jest.fn().mockReturnValue([])
};
// Mock event
const mockEvent = {
which: 1,
pageX: 100
};
// Initialize the column first to add event listeners
moveColumns.initializeColumn(mockColumn);
// Get the mousedown handler
const mousedownHandler = mockColumn.getElement().addEventListener.mock.calls.find(call => call[0] === "mousedown")[1];
// Spy on startMove
jest.spyOn(moveColumns, 'startMove').mockImplementation(() => {});
// Call mousedown handler
mousedownHandler(mockEvent);
// Fast forward time to trigger the timeout
jest.advanceTimersByTime(moveColumns.checkPeriod + 10);
// Verify startMove was called
expect(moveColumns.startMove).toHaveBeenCalledWith(mockEvent, mockColumn);
});
it("should bind touch events to a column", () => {
// Create mock column element
const mockColEl = {
addEventListener: jest.fn()
};
// Create mock column
const mockColumn = {
getElement: jest.fn().mockReturnValue(mockColEl),
nextColumn: jest.fn(),
prevColumn: jest.fn()
};
// Bind touch events
moveColumns.bindTouchEvents(mockColumn);
// Verify touch event listeners were added
expect(mockColEl.addEventListener).toHaveBeenCalledWith("touchstart", expect.any(Function), {passive: true});
expect(mockColEl.addEventListener).toHaveBeenCalledWith("touchmove", expect.any(Function), {passive: true});
expect(mockColEl.addEventListener).toHaveBeenCalledWith("touchend", expect.any(Function));
});
it("should not start move if range selection is active", () => {
// Set up selectRange module to be active
mockTable.modules.selectRange.mousedown = true;
mockTable.modules.selectRange.selecting = "column";
mockTable.modules.selectRange.columnSelection = true;
// Create mock column
const mockColumn = {
getElement: jest.fn()
};
// Create mock event
const mockEvent = {
pageX: 100
};
// Spy on methods that should not be called
jest.spyOn(moveColumns, '_bindMouseMove');
// Start move operation
moveColumns.startMove(mockEvent, mockColumn);
// Verify that early return happened and no move operation started
expect(moveColumns._bindMouseMove).not.toHaveBeenCalled();
expect(moveColumns.moving).toBeFalsy();
});
it("should bind mouse move handlers to all columns", () => {
// Update mock columnsByIndex to have proper modules.moveColumn structure
mockTable.columnManager.columnsByIndex.forEach(column => {
column.modules.moveColumn = { mousemove: jest.fn() };
});
// Bind mouse move handlers
moveColumns._bindMouseMove();
// Verify event listeners were added to all columns
mockTable.columnManager.columnsByIndex.forEach(column => {
expect(column.getElement().addEventListener).toHaveBeenCalledWith(
"mousemove",
column.modules.moveColumn.mousemove
);
});
});
it("should unbind mouse move handlers from all columns", () => {
// Update mock columnsByIndex to have proper modules.moveColumn structure
mockTable.columnManager.columnsByIndex.forEach(column => {
column.modules.moveColumn = { mousemove: jest.fn() };
});
// Unbind mouse move handlers
moveColumns._unbindMouseMove();
// Verify event listeners were removed from all columns
mockTable.columnManager.columnsByIndex.forEach(column => {
expect(column.getElement().removeEventListener).toHaveBeenCalledWith(
"mousemove",
column.modules.moveColumn.mousemove
);
});
});
it("should move column cells after target column", () => {
// Create mock cells
const mockCellEl1 = {
parentNode: {
insertBefore: jest.fn()
},
nextSibling: "nextSibling1"
};
const mockCellEl2 = {
parentNode: {
insertBefore: jest.fn()
},
nextSibling: "nextSibling2"
};
// Create mock source and target columns
const mockSourceCell1 = {
getElement: jest.fn().mockReturnValue("sourceCell1")
};
const mockSourceCell2 = {
getElement: jest.fn().mockReturnValue("sourceCell2")
};
const mockSourceColumn = {
getCells: jest.fn().mockReturnValue([mockSourceCell1, mockSourceCell2])
};
const mockTargetCell1 = {
getElement: jest.fn(true).mockReturnValue(mockCellEl1)
};
const mockTargetCell2 = {
getElement: jest.fn(true).mockReturnValue(mockCellEl2)
};
const mockTargetColumn = {
getCells: jest.fn().mockReturnValue([mockTargetCell1, mockTargetCell2])
};
// Set up the moving column
moveColumns.moving = mockSourceColumn;
// Move column after target
moveColumns.moveColumn(mockTargetColumn, true);
// Verify cells are moved correctly
expect(moveColumns.toCol).toBe(mockTargetColumn);
expect(moveColumns.toColAfter).toBe(true);
expect(mockCellEl1.parentNode.insertBefore).toHaveBeenCalledWith("sourceCell1", "nextSibling1");
expect(mockCellEl2.parentNode.insertBefore).toHaveBeenCalledWith("sourceCell2", "nextSibling2");
});
it("should move column cells before target column", () => {
// Create mock cells
const mockCellEl1 = {
parentNode: {
insertBefore: jest.fn()
}
};
const mockCellEl2 = {
parentNode: {
insertBefore: jest.fn()
}
};
// Create mock source and target columns
const mockSourceCell1 = {
getElement: jest.fn().mockReturnValue("sourceCell1")
};
const mockSourceCell2 = {
getElement: jest.fn().mockReturnValue("sourceCell2")
};
const mockSourceColumn = {
getCells: jest.fn().mockReturnValue([mockSourceCell1, mockSourceCell2])
};
const mockTargetCell1 = {
getElement: jest.fn(true).mockReturnValue(mockCellEl1)
};
const mockTargetCell2 = {
getElement: jest.fn(true).mockReturnValue(mockCellEl2)
};
const mockTargetColumn = {
getCells: jest.fn().mockReturnValue([mockTargetCell1, mockTargetCell2])
};
// Set up the moving column
moveColumns.moving = mockSourceColumn;
// Move column before target
moveColumns.moveColumn(mockTargetColumn, false);
// Verify cells are moved correctly
expect(moveColumns.toCol).toBe(mockTargetColumn);
expect(moveColumns.toColAfter).toBe(false);
expect(mockCellEl1.parentNode.insertBefore).toHaveBeenCalledWith("sourceCell1", mockCellEl1);
expect(mockCellEl2.parentNode.insertBefore).toHaveBeenCalledWith("sourceCell2", mockCellEl2);
});
it("should finalize column move on mouse up", () => {
// Create mock column elements
const mockElement = {
parentNode: {
insertBefore: jest.fn(),
removeChild: jest.fn()
},
nextSibling: "nextSibling"
};
// Set up the moving column state
moveColumns.moving = {
getElement: jest.fn().mockReturnValue("columnElement")
};
moveColumns.placeholderElement = {
parentNode: {
insertBefore: jest.fn(),
removeChild: jest.fn()
},
nextSibling: "nextSibling"
};
moveColumns.hoverElement = {
parentNode: {
removeChild: jest.fn()
}
};
moveColumns.toCol = "targetColumn";
moveColumns.toColAfter = true;
// Create mock event
const mockEvent = {
which: 1
};
// Mock _unbindMouseMove to avoid the issues with column.modules.moveColumn
jest.spyOn(moveColumns, '_unbindMouseMove').mockImplementation(() => {});
// Mock document body event listener removal
jest.spyOn(document.body, 'removeEventListener').mockImplementation(jest.fn());
// Call endMove
moveColumns.endMove(mockEvent);
// Verify DOM cleanup
expect(moveColumns._unbindMouseMove).toHaveBeenCalled();
expect(moveColumns.placeholderElement.parentNode.insertBefore).toHaveBeenCalledWith(
"columnElement",
"nextSibling"
);
expect(moveColumns.placeholderElement.parentNode.removeChild).toHaveBeenCalledWith(
moveColumns.placeholderElement
);
expect(moveColumns.hoverElement.parentNode.removeChild).toHaveBeenCalledWith(
moveColumns.hoverElement
);
// Verify table class cleanup
expect(mockTable.element.classList.remove).toHaveBeenCalledWith("tabulator-block-select");
// Verify column manager was called to move the column
expect(mockTable.columnManager.moveColumnActual).toHaveBeenCalledWith(
expect.anything(), // The moving column (just checking it was passed)
"targetColumn",
true
);
// Verify state reset
expect(moveColumns.moving).toBe(false);
expect(moveColumns.toCol).toBe(false);
expect(moveColumns.toColAfter).toBe(false);
});
it("should update hover element position on moveHover", () => {
// Set up the hoverElement
moveColumns.hoverElement = {
style: {}
};
moveColumns.startX = 20;
// Create mock event and column holder
const mockEvent = {
pageX: 100
};
// Call moveHover
moveColumns.moveHover(mockEvent);
// Verify hover element position is updated
expect(moveColumns.hoverElement.style.left).toBe("30px");
});
it("should trigger auto scroll when near left edge", () => {
// Set up the hoverElement
moveColumns.hoverElement = {
style: {}
};
moveColumns.startX = 20;
// Create mock event near left edge
const mockEvent = {
pageX: 70 // Will calculate to xPos=20, which is less than autoScrollMargin (40)
};
// Mock rowManager getElement to return an object we can directly update
const scrollObj = { scrollLeft: 0 };
mockTable.rowManager.getElement.mockReturnValue(scrollObj);
// Call moveHover
moveColumns.moveHover(mockEvent);
// Fast forward to trigger auto scroll
jest.advanceTimersByTime(5);
// Verify auto scroll was triggered (scrollLeft should remain at 0 since we're at the edge)
expect(scrollObj.scrollLeft).toBe(0);
});
it("should attempt to set auto scroll when near right edge", () => {
// Mock setTimeout to track if it's called
const setTimeoutSpy = jest.spyOn(global, 'setTimeout');
// Set up the hoverElement
moveColumns.hoverElement = {
style: {}
};
moveColumns.startX = 20;
// Create mock event near right edge
const mockEvent = {
pageX: 1000 // Will calculate to xPos=950, which is within autoScrollMargin of right edge
};
// Mock columnHolder with larger scrollLeft value
const mockContentsElement = {
scrollLeft: 10,
clientWidth: 50 // Small width to ensure we're near the right edge
};
mockTable.columnManager.getContentsElement.mockReturnValue(mockContentsElement);
// Call moveHover
moveColumns.moveHover(mockEvent);
// Verify setTimeout was called (indicating the auto scroll code was executed)
expect(setTimeoutSpy).toHaveBeenCalled();
});
});

View File

@@ -0,0 +1,698 @@
import MoveRows from "../../../src/js/modules/MoveRows/MoveRows";
describe("MoveRows module", () => {
/** @type {MoveRows} */
let moveRowsMod;
let mockTable;
let mockRows;
beforeEach(() => {
// Create mock rows with necessary methods
mockRows = [
{
id: 1,
data: { id: 1, name: "John", age: 30 },
getElement: jest.fn().mockReturnValue({
classList: {
add: jest.fn(),
remove: jest.fn(),
contains: jest.fn().mockReturnValue(false)
},
getBoundingClientRect: jest.fn().mockReturnValue({ top: 100, left: 100 }),
parentNode: {
insertBefore: jest.fn(),
removeChild: jest.fn()
},
cloneNode: jest.fn().mockReturnValue({
classList: {
add: jest.fn(),
remove: jest.fn()
},
style: {}
}),
nextSibling: {}
}),
getHeight: jest.fn().mockReturnValue(30),
getWidth: jest.fn().mockReturnValue(100),
getComponent: jest.fn().mockReturnValue({ id: 1 }),
delete: jest.fn(),
update: jest.fn(),
modules: {},
getData: jest.fn().mockReturnValue({ id: 1, name: "John", age: 30 })
},
{
id: 2,
data: { id: 2, name: "Jane", age: 25 },
getElement: jest.fn().mockReturnValue({
classList: {
add: jest.fn(),
remove: jest.fn(),
contains: jest.fn().mockReturnValue(false)
},
getBoundingClientRect: jest.fn().mockReturnValue({ top: 100, left: 100 }),
parentNode: {
insertBefore: jest.fn(),
removeChild: jest.fn()
},
cloneNode: jest.fn().mockReturnValue({
classList: {
add: jest.fn(),
remove: jest.fn()
},
style: {}
}),
nextSibling: {}
}),
getHeight: jest.fn().mockReturnValue(30),
getWidth: jest.fn().mockReturnValue(100),
getComponent: jest.fn().mockReturnValue({ id: 2 }),
delete: jest.fn(),
update: jest.fn(),
modules: {},
getData: jest.fn().mockReturnValue({ id: 2, name: "Jane", age: 25 })
},
{
id: 3,
data: { id: 3, name: "Bob", age: 35 },
getElement: jest.fn().mockReturnValue({
classList: {
add: jest.fn(),
remove: jest.fn(),
contains: jest.fn().mockReturnValue(false)
},
getBoundingClientRect: jest.fn().mockReturnValue({ top: 100, left: 100 }),
parentNode: {
insertBefore: jest.fn(),
removeChild: jest.fn()
},
cloneNode: jest.fn().mockReturnValue({
classList: {
add: jest.fn(),
remove: jest.fn()
},
style: {}
}),
nextSibling: {}
}),
getHeight: jest.fn().mockReturnValue(30),
getWidth: jest.fn().mockReturnValue(100),
getComponent: jest.fn().mockReturnValue({ id: 3 }),
delete: jest.fn(),
update: jest.fn(),
modules: {},
getData: jest.fn().mockReturnValue({ id: 3, name: "Bob", age: 35 })
}
];
// Create DOM element mock with all needed methods
const mockElement = {
classList: {
add: jest.fn(),
remove: jest.fn(),
contains: jest.fn().mockReturnValue(false)
},
appendChild: jest.fn(),
removeChild: jest.fn(),
clientWidth: 800,
getBoundingClientRect: jest.fn().mockReturnValue({
top: 0,
left: 0
})
};
// Create mock optionsList
const mockOptionsList = {
register: jest.fn(),
generate: jest.fn().mockImplementation((defaults, options) => {
return { ...defaults, ...options };
})
};
// Create a mock table with all required properties and methods
mockTable = {
element: mockElement,
options: {
movableRows: true,
movableRowsReceiver: "insert"
},
optionsList: mockOptionsList,
rowManager: {
rows: mockRows,
getElement: jest.fn().mockReturnValue(mockElement),
getTableElement: jest.fn().mockReturnValue(mockElement),
moveRow: jest.fn(),
getDisplayRows: jest.fn().mockReturnValue(mockRows),
element: {
scrollTop: 0,
scrollHeight: 500,
getBoundingClientRect: jest.fn().mockReturnValue({
top: 100,
left: 100
}),
appendChild: jest.fn(),
}
},
columnManager: {
columns: [],
optionsList: mockOptionsList
},
registerTableOption: jest.fn(),
registerColumnOption: jest.fn(),
modules: {},
addRow: jest.fn(),
initGuard: jest.fn()
};
// Mock document.body
document.body.appendChild = jest.fn();
document.body.addEventListener = jest.fn();
document.body.removeEventListener = jest.fn();
// Mock document.createElement to return our custom mock elements
jest.spyOn(document, 'createElement').mockImplementation(() => {
return {
classList: {
add: jest.fn(),
remove: jest.fn(),
contains: jest.fn().mockReturnValue(true)
},
style: {},
parentNode: {
insertBefore: jest.fn(),
removeChild: jest.fn()
},
appendChild: jest.fn(),
getBoundingClientRect: jest.fn().mockReturnValue({
top: 0,
left: 0
}),
nextSibling: {}
};
});
// Mock the prototype methods of the Module class
jest.spyOn(MoveRows.prototype, 'registerTableOption').mockImplementation(function(key, value) {
this.table.optionsList.register(key, value);
});
jest.spyOn(MoveRows.prototype, 'registerColumnOption').mockImplementation(function(key, value) {
this.table.columnManager.optionsList.register(key, value);
});
// Create MoveRows module instance
moveRowsMod = new MoveRows(mockTable);
// Mock module methods
moveRowsMod.dispatchExternal = jest.fn();
moveRowsMod.commsSend = jest.fn();
moveRowsMod.subscribe = jest.fn();
moveRowsMod._bindMouseMove = jest.fn();
moveRowsMod._unbindMouseMove = jest.fn();
moveRowsMod.setStartPosition = jest.fn();
// Initialize the module
moveRowsMod.initialize();
// Add the placeholder element to the DOM (fake it)
moveRowsMod.placeholderElement.parentNode = {
insertBefore: jest.fn(),
removeChild: jest.fn()
};
moveRowsMod.placeholderElement.nextSibling = {};
// Add extra helper method for testing
moveRowsMod.getRow = (id) => {
return mockRows.find(row => row.data.id === id);
};
// Mock connection property
moveRowsMod.connection = false;
});
afterEach(() => {
jest.clearAllMocks();
jest.restoreAllMocks();
});
it("should initialize with movableRows enabled", () => {
expect(moveRowsMod.placeholderElement).toBeDefined();
expect(moveRowsMod.hasHandle).toBe(false);
expect(moveRowsMod.moving).toBe(false);
// Verify subscription to events
expect(moveRowsMod.subscribe).toHaveBeenCalledWith("cell-init", expect.any(Function));
expect(moveRowsMod.subscribe).toHaveBeenCalledWith("column-init", expect.any(Function));
expect(moveRowsMod.subscribe).toHaveBeenCalledWith("row-init", expect.any(Function));
});
it("should create proper placeholder element", () => {
const placeholder = moveRowsMod.placeholderElement;
expect(placeholder.classList.contains("tabulator-row")).toBe(true);
expect(placeholder.classList.contains("tabulator-row-placeholder")).toBe(true);
});
it("should handle row movement", () => {
// Get two rows
const row1 = moveRowsMod.getRow(1);
const row2 = moveRowsMod.getRow(2);
// Call moveRow to set target
moveRowsMod.moveRow(row2, true);
// Check target row and position is set correctly
expect(moveRowsMod.toRow).toBe(row2);
expect(moveRowsMod.toRowAfter).toBe(true);
});
it("should handle starting a move operation", () => {
// Mock methods that directly interact with DOM
jest.spyOn(moveRowsMod, 'startMove').mockImplementation((event, row) => {
// Just set the necessary state without DOM operations
moveRowsMod.moving = row;
moveRowsMod.placeholderElement.style.width = row.getWidth() + "px";
moveRowsMod.placeholderElement.style.height = row.getHeight() + "px";
moveRowsMod.hoverElement = {
classList: {
add: jest.fn()
},
style: {}
};
// Mock event listener attachment
document.body.addEventListener("mousemove", moveRowsMod.moveHover);
document.body.addEventListener("mouseup", moveRowsMod.endMove);
// Dispatch the external event
moveRowsMod.dispatchExternal("rowMoving", row.getComponent());
});
// Create mock event
const event = {
preventDefault: jest.fn(),
which: 1,
pageX: 150,
pageY: 150
};
// Get a row
const row = moveRowsMod.getRow(1);
// Call startMove
moveRowsMod.startMove(event, row);
// Check settings are correct
expect(moveRowsMod.moving).toBe(row);
expect(moveRowsMod.placeholderElement.style.width).toBe("100px");
expect(moveRowsMod.placeholderElement.style.height).toBe("30px");
expect(moveRowsMod.hoverElement).toBeDefined();
// Check event listeners are attached
expect(document.body.addEventListener).toHaveBeenCalledWith("mousemove", moveRowsMod.moveHover);
expect(document.body.addEventListener).toHaveBeenCalledWith("mouseup", moveRowsMod.endMove);
// Check external event is dispatched
expect(moveRowsMod.dispatchExternal).toHaveBeenCalledWith("rowMoving", expect.anything());
});
it("should handle end move operation", () => {
// Mock endMove for testing
jest.spyOn(moveRowsMod, 'endMove').mockImplementation((event) => {
// Skip DOM operations but keep the logic
document.body.removeEventListener("mousemove", moveRowsMod.moveHover);
document.body.removeEventListener("mouseup", moveRowsMod.endMove);
if (moveRowsMod.toRow) {
mockTable.rowManager.moveRow(moveRowsMod.moving, moveRowsMod.toRow, moveRowsMod.toRowAfter);
} else {
moveRowsMod.dispatchExternal("rowMoveCancelled", moveRowsMod.moving.getComponent());
}
// Reset state
moveRowsMod.moving = false;
moveRowsMod.toRow = false;
moveRowsMod.toRowAfter = false;
// Reset styles
mockTable.element.classList.remove("tabulator-block-select");
});
// Create mock event
const event = {
preventDefault: jest.fn(),
which: 1
};
// Get rows
const row1 = moveRowsMod.getRow(1);
const row2 = moveRowsMod.getRow(2);
// Set up the state for end move
moveRowsMod.moving = row1;
moveRowsMod.toRow = row2;
moveRowsMod.toRowAfter = true;
// End move
moveRowsMod.endMove(event);
// Check event listeners are removed
expect(document.body.removeEventListener).toHaveBeenCalledWith("mousemove", moveRowsMod.moveHover);
expect(document.body.removeEventListener).toHaveBeenCalledWith("mouseup", moveRowsMod.endMove);
// Check row movement was triggered
expect(mockTable.rowManager.moveRow).toHaveBeenCalledWith(row1, row2, true);
// Check settings are reset
expect(moveRowsMod.moving).toBe(false);
expect(moveRowsMod.toRow).toBe(false);
expect(moveRowsMod.toRowAfter).toBe(false);
expect(mockTable.element.classList.remove).toHaveBeenCalledWith("tabulator-block-select");
});
it("should handle movement cancellation", () => {
// Mock endMove for testing
jest.spyOn(moveRowsMod, 'endMove').mockImplementation((event) => {
// Skip DOM operations but keep the logic
document.body.removeEventListener("mousemove", moveRowsMod.moveHover);
document.body.removeEventListener("mouseup", moveRowsMod.endMove);
if (moveRowsMod.toRow) {
mockTable.rowManager.moveRow(moveRowsMod.moving, moveRowsMod.toRow, moveRowsMod.toRowAfter);
} else {
moveRowsMod.dispatchExternal("rowMoveCancelled", moveRowsMod.moving.getComponent());
}
// Reset state
moveRowsMod.moving = false;
moveRowsMod.toRow = false;
moveRowsMod.toRowAfter = false;
// Reset styles
mockTable.element.classList.remove("tabulator-block-select");
});
// Create mock event
const event = {
preventDefault: jest.fn(),
which: 1
};
// Get row
const row = moveRowsMod.getRow(1);
// Set up the state for cancelled move
moveRowsMod.moving = row;
moveRowsMod.toRow = false; // No target = cancelled
// End move
moveRowsMod.endMove(event);
// Check event listeners are removed
expect(document.body.removeEventListener).toHaveBeenCalledWith("mousemove", moveRowsMod.moveHover);
expect(document.body.removeEventListener).toHaveBeenCalledWith("mouseup", moveRowsMod.endMove);
// Check rowMoveCancelled event was dispatched
expect(moveRowsMod.dispatchExternal).toHaveBeenCalledWith("rowMoveCancelled", expect.anything());
// Check settings are reset
expect(moveRowsMod.moving).toBe(false);
expect(moveRowsMod.toRow).toBe(false);
expect(moveRowsMod.toRowAfter).toBe(false);
});
it("should test the insert receiver", () => {
// Test insert receiver
const insertReceiver = MoveRows.receivers.insert;
const fromRow = { getData: jest.fn().mockReturnValue({ id: 4 }) };
const toRow = { getData: jest.fn() };
mockTable.addRow = jest.fn();
// Call receiver
const result = insertReceiver.call(moveRowsMod, fromRow, toRow);
// Check result
expect(result).toBe(true);
expect(mockTable.addRow).toHaveBeenCalledWith(fromRow.getData(), undefined, toRow);
});
it("should test the update receiver", () => {
// Test update receiver
const updateReceiver = MoveRows.receivers.update;
const fromRow = { getData: jest.fn().mockReturnValue({ id: 4 }) };
const toRow = { update: jest.fn() };
// Call receiver with valid target row
let result = updateReceiver.call(moveRowsMod, fromRow, toRow);
// Check result
expect(result).toBe(true);
expect(toRow.update).toHaveBeenCalledWith(fromRow.getData());
// Call receiver with no target row
result = updateReceiver.call(moveRowsMod, fromRow, null);
// Check result
expect(result).toBe(false);
});
it("should test the replace receiver", () => {
// Test replace receiver
const replaceReceiver = MoveRows.receivers.replace;
const fromRow = { getData: jest.fn().mockReturnValue({ id: 4 }) };
const toRow = { delete: jest.fn() };
mockTable.addRow = jest.fn();
// Call receiver with valid target row
let result = replaceReceiver.call(moveRowsMod, fromRow, toRow);
// Check result
expect(result).toBe(true);
expect(mockTable.addRow).toHaveBeenCalledWith(fromRow.getData(), undefined, toRow);
expect(toRow.delete).toHaveBeenCalled();
// Call receiver with no target row
result = replaceReceiver.call(moveRowsMod, fromRow, null);
// Check result
expect(result).toBe(false);
});
it("should test the default delete sender", () => {
// Test delete sender
const deleteSender = MoveRows.senders.delete;
const fromRow = { delete: jest.fn() };
const toRow = {};
// Call sender
deleteSender.call(moveRowsMod, fromRow, toRow);
// Check result
expect(fromRow.delete).toHaveBeenCalled();
});
it("should handle column with rowHandle option", () => {
// Mock column initialization
const column = {
definition: {
rowHandle: true
}
};
// Call initializeColumn with rowHandle column
moveRowsMod.initializeColumn(column);
// Check hasHandle property is set
expect(moveRowsMod.hasHandle).toBe(true);
});
it("should handle table row drop event", () => {
// Mock tableRowDrop method
jest.spyOn(moveRowsMod, 'tableRowDrop').mockImplementation((event, toRow) => {
// Stop event propagation
event.stopImmediatePropagation();
// Call receiver directly to avoid complex receiver logic
mockTable.addRow(moveRowsMod.connectedRow.getData(), undefined, toRow);
// Dispatch event
moveRowsMod.dispatchExternal("movableRowsReceived", moveRowsMod.connectedRow.getComponent(), toRow ? toRow.getComponent() : undefined, moveRowsMod.connectedTable);
// Send comms
moveRowsMod.commsSend(moveRowsMod.connectedTable, "moveRow", "dropcomplete", {
row: toRow,
success: true
});
});
// Set up a connected table scenario
const fromRow = {
getComponent: jest.fn().mockReturnValue({ id: 4 }),
getData: jest.fn().mockReturnValue({ id: 4, name: "Test", age: 40 })
};
const toRow = {
getComponent: jest.fn().mockReturnValue({ id: 2 })
};
const connectedTable = {};
// Mock properties and methods
moveRowsMod.connectedRow = fromRow;
moveRowsMod.connectedTable = connectedTable;
// Mock event
const event = {
stopImmediatePropagation: jest.fn()
};
// Call tableRowDrop
moveRowsMod.tableRowDrop(event, toRow);
// Check event was stopped
expect(event.stopImmediatePropagation).toHaveBeenCalled();
// Check receiver was called
expect(mockTable.addRow).toHaveBeenCalled();
// Check external event was dispatched
expect(moveRowsMod.dispatchExternal).toHaveBeenCalledWith("movableRowsReceived", expect.anything(), expect.anything(), connectedTable);
// Check comms were sent
expect(moveRowsMod.commsSend).toHaveBeenCalledWith(connectedTable, "moveRow", "dropcomplete", {
row: toRow,
success: true
});
});
it("should handle communications received", () => {
// Mock methods
moveRowsMod.connect = jest.fn().mockReturnValue(true);
moveRowsMod.disconnect = jest.fn();
moveRowsMod.dropComplete = jest.fn();
const table = {};
const data = {
row: mockRows[0],
success: true
};
// Test connect action
expect(moveRowsMod.commsReceived(table, "connect", data)).toBe(true);
expect(moveRowsMod.connect).toHaveBeenCalledWith(table, data.row);
// Test disconnect action
moveRowsMod.commsReceived(table, "disconnect", {});
expect(moveRowsMod.disconnect).toHaveBeenCalledWith(table);
// Test dropcomplete action
moveRowsMod.commsReceived(table, "dropcomplete", data);
expect(moveRowsMod.dropComplete).toHaveBeenCalledWith(table, data.row, data.success);
});
it("should connect to external table", () => {
// Mock connect
jest.spyOn(moveRowsMod, 'connect').mockImplementation((table, row) => {
// Set connected table and row
moveRowsMod.connectedTable = table;
moveRowsMod.connectedRow = row;
// Add class
mockTable.element.classList.add("tabulator-movingrow-receiving");
// Dispatch event
moveRowsMod.dispatchExternal("movableRowsReceivingStart", row, table);
return true;
});
// Set up parameters
const table = {};
const row = mockRows[0];
// Test connect
const result = moveRowsMod.connect(table, row);
// Check result
expect(result).toBe(true);
expect(moveRowsMod.connectedTable).toBe(table);
expect(moveRowsMod.connectedRow).toBe(row);
expect(mockTable.element.classList.add).toHaveBeenCalledWith("tabulator-movingrow-receiving");
expect(moveRowsMod.dispatchExternal).toHaveBeenCalledWith("movableRowsReceivingStart", row, table);
});
it("should disconnect from external table", () => {
// Mock disconnect
jest.spyOn(moveRowsMod, 'disconnect').mockImplementation((table) => {
// Reset connected table and row
moveRowsMod.connectedTable = false;
moveRowsMod.connectedRow = false;
// Remove class
mockTable.element.classList.remove("tabulator-movingrow-receiving");
// Dispatch event
moveRowsMod.dispatchExternal("movableRowsReceivingStop", table);
});
// Set up the connection state
const table = {};
moveRowsMod.connectedTable = table;
moveRowsMod.tableRowDropEvent = jest.fn();
// Test disconnect
moveRowsMod.disconnect(table);
// Check result
expect(moveRowsMod.connectedTable).toBe(false);
expect(moveRowsMod.connectedRow).toBe(false);
expect(mockTable.element.classList.remove).toHaveBeenCalledWith("tabulator-movingrow-receiving");
expect(moveRowsMod.dispatchExternal).toHaveBeenCalledWith("movableRowsReceivingStop", table);
});
it("should handle drop completion", () => {
// Mock drop complete
jest.spyOn(moveRowsMod, 'dropComplete').mockImplementation((table, row, success) => {
if (success) {
const sender = mockTable.options.movableRowsSender;
if (typeof sender === 'function') {
sender.call(moveRowsMod, moveRowsMod.moving.getComponent(), row.getComponent(), table);
}
moveRowsMod.dispatchExternal("movableRowsSent", moveRowsMod.moving.getComponent(), row.getComponent(), table);
} else {
moveRowsMod.dispatchExternal("movableRowsSentFailed", moveRowsMod.moving.getComponent(), row.getComponent(), table);
}
moveRowsMod.endMove();
});
// Set up test
const table = {};
const row = mockRows[0];
const fromRow = moveRowsMod.getRow(1);
// Mock sender function
const senderFunc = jest.fn();
mockTable.options.movableRowsSender = senderFunc;
moveRowsMod.moving = fromRow;
moveRowsMod.endMove = jest.fn();
// Test successful drop
moveRowsMod.dropComplete(table, row, true);
// Check results
expect(senderFunc).toHaveBeenCalledWith(fromRow.getComponent(), row.getComponent(), table);
expect(moveRowsMod.dispatchExternal).toHaveBeenCalledWith("movableRowsSent", fromRow.getComponent(), row.getComponent(), table);
expect(moveRowsMod.endMove).toHaveBeenCalled();
// Reset mocks
jest.clearAllMocks();
// Test failed drop
moveRowsMod.dropComplete(table, row, false);
// Check results
expect(moveRowsMod.dispatchExternal).toHaveBeenCalledWith("movableRowsSentFailed", fromRow.getComponent(), row.getComponent(), table);
expect(moveRowsMod.endMove).toHaveBeenCalled();
});
});

View File

@@ -0,0 +1,422 @@
import Mutator from "../../../src/js/modules/Mutator/Mutator";
import defaultMutators from "../../../src/js/modules/Mutator/defaults/mutators";
describe("Mutator module", () => {
/** @type {Mutator} */
let mutator;
let mockTable;
// Store original mutators
let originalMutators;
beforeEach(() => {
// Save original mutators
originalMutators = { ...Mutator.mutators };
// Create mock columnManager
const mockColumnManager = {
traverse: jest.fn((callback) => {
mockColumns.forEach(callback);
})
};
// Create mock eventBus
const mockEventBus = {
subscribe: jest.fn()
};
// Create mock optionsList
const mockOptionsList = {
register: jest.fn()
};
// Create mock table
mockTable = {
columnManager: mockColumnManager,
options: {},
eventBus: mockEventBus,
optionsList: mockOptionsList
};
// Mock columns for testing
const mockColumns = [
createMockColumn("col1", undefined),
createMockColumn("col2", undefined),
createMockColumn("col3", undefined)
];
function createMockColumn(field, mutator) {
return {
definition: {
field: field,
mutator: mutator,
mutatorParams: { param1: "test" }
},
getFieldValue: jest.fn((data) => data[field]),
setFieldValue: jest.fn((data, value) => {
data[field] = value;
return data;
}),
getComponent: jest.fn(() => ({ column: true }))
};
}
// Mock methods in the Mutator prototype
jest.spyOn(Mutator.prototype, 'registerColumnOption').mockImplementation(function(key) {
this.table.optionsList.register(key);
});
jest.spyOn(Mutator.prototype, 'subscribe').mockImplementation(function(key, callback) {
return this.table.eventBus.subscribe(key, callback);
});
// Create test mutator functions
Mutator.mutators = {
...defaultMutators,
uppercase: (value) => typeof value === 'string' ? value.toUpperCase() : value,
addPrefix: (value, data, type, params) => `${params.prefix || 'prefix_'}${value}`
};
// Create an instance of the Mutator module with the mock table
mutator = new Mutator(mockTable);
});
afterEach(() => {
jest.clearAllMocks();
jest.restoreAllMocks();
// Restore original mutators
Mutator.mutators = originalMutators;
});
it("should register all column options during construction", () => {
// Verify column options are registered
expect(mockTable.optionsList.register).toHaveBeenCalledWith("mutator");
expect(mockTable.optionsList.register).toHaveBeenCalledWith("mutatorParams");
expect(mockTable.optionsList.register).toHaveBeenCalledWith("mutatorData");
expect(mockTable.optionsList.register).toHaveBeenCalledWith("mutatorDataParams");
expect(mockTable.optionsList.register).toHaveBeenCalledWith("mutatorEdit");
expect(mockTable.optionsList.register).toHaveBeenCalledWith("mutatorEditParams");
expect(mockTable.optionsList.register).toHaveBeenCalledWith("mutatorClipboard");
expect(mockTable.optionsList.register).toHaveBeenCalledWith("mutatorClipboardParams");
expect(mockTable.optionsList.register).toHaveBeenCalledWith("mutatorImport");
expect(mockTable.optionsList.register).toHaveBeenCalledWith("mutatorImportParams");
expect(mockTable.optionsList.register).toHaveBeenCalledWith("mutateLink");
});
it("should subscribe to required events during initialization", () => {
// Initialize the mutator
mutator.initialize();
// Verify event subscriptions
expect(mockTable.eventBus.subscribe).toHaveBeenCalledWith("cell-value-changing", expect.any(Function));
expect(mockTable.eventBus.subscribe).toHaveBeenCalledWith("cell-value-changed", expect.any(Function));
expect(mockTable.eventBus.subscribe).toHaveBeenCalledWith("column-layout", expect.any(Function));
expect(mockTable.eventBus.subscribe).toHaveBeenCalledWith("row-data-init-before", expect.any(Function));
expect(mockTable.eventBus.subscribe).toHaveBeenCalledWith("row-data-changing", expect.any(Function));
});
it("should initialize a column with string mutator correctly", () => {
// Create a mock column with string mutator
const mockColumn = {
definition: {
field: "name",
mutator: "uppercase",
mutatorParams: { test: true }
},
modules: {}
};
// Initialize column
mutator.initializeColumn(mockColumn);
// Verify mutator is set correctly
expect(mockColumn.modules.mutate).toBeDefined();
expect(mockColumn.modules.mutate.mutator.mutator).toBe(Mutator.mutators.uppercase);
expect(mockColumn.modules.mutate.mutator.params).toEqual({ test: true });
});
it("should initialize a column with function mutator correctly", () => {
// Create a mock column with function mutator
const mutatorFunction = jest.fn();
const mockColumn = {
definition: {
field: "name",
mutator: mutatorFunction,
mutatorParams: { test: true }
},
modules: {}
};
// Initialize column
mutator.initializeColumn(mockColumn);
// Verify mutator is set correctly
expect(mockColumn.modules.mutate).toBeDefined();
expect(mockColumn.modules.mutate.mutator.mutator).toBe(mutatorFunction);
expect(mockColumn.modules.mutate.mutator.params).toEqual({ test: true });
});
it("should initialize a column with data mutator correctly", () => {
// Create a mock column with data mutator
const mockColumn = {
definition: {
field: "name",
mutatorData: "uppercase",
mutatorDataParams: { test: true }
},
modules: {}
};
// Initialize column
mutator.initializeColumn(mockColumn);
// Verify mutator is set correctly
expect(mockColumn.modules.mutate).toBeDefined();
expect(mockColumn.modules.mutate.mutatorData.mutator).toBe(Mutator.mutators.uppercase);
expect(mockColumn.modules.mutate.mutatorData.params).toEqual({ test: true });
});
it("should warn if mutator is not found", () => {
// Spy on console.warn
const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
// Create a mock column with invalid mutator
const mockColumn = {
definition: {
field: "name",
mutator: "nonExistentMutator"
},
modules: {}
};
// Initialize column
mutator.initializeColumn(mockColumn);
// Verify warning is issued
expect(consoleWarnSpy).toHaveBeenCalledWith(
"Mutator Error - No such mutator found, ignoring: ",
"nonExistentMutator"
);
});
it("should transform row data correctly with mutator", () => {
// Create mock columns with mutators
const mockColumn1 = {
definition: {
field: "name"
},
modules: {
mutate: {
mutatorData: {
mutator: Mutator.mutators.uppercase,
params: {}
}
}
},
getFieldValue: jest.fn(data => data.name),
setFieldValue: jest.fn((data, value) => {
data.name = value;
return data;
}),
getComponent: jest.fn(() => ({ column: true }))
};
const mockColumn2 = {
definition: {
field: "id"
},
modules: {
mutate: {
mutatorData: {
mutator: Mutator.mutators.addPrefix,
params: { prefix: "ID_" }
}
}
},
getFieldValue: jest.fn(data => data.id),
setFieldValue: jest.fn((data, value) => {
data.id = value;
return data;
}),
getComponent: jest.fn(() => ({ column: true }))
};
// Mock the column traversal
mockTable.columnManager.traverse.mockImplementation(callback => {
[mockColumn1, mockColumn2].forEach(callback);
});
// Setup test data
const testData = {
name: "john doe",
id: "123"
};
// Transform the data
const result = mutator.transformRow(testData, "data");
// Verify the data was transformed
expect(result.name).toBe("JOHN DOE");
expect(result.id).toBe("ID_123");
// Verify the column methods were called
expect(mockColumn1.getFieldValue).toHaveBeenCalledWith(testData);
expect(mockColumn1.setFieldValue).toHaveBeenCalledWith(testData, "JOHN DOE");
expect(mockColumn2.getFieldValue).toHaveBeenCalledWith(testData);
expect(mockColumn2.setFieldValue).toHaveBeenCalledWith(testData, "ID_123");
});
it("should transform cell value correctly with mutator", () => {
// Create mock column with edit mutator
const mockColumn = {
modules: {
mutate: {
mutatorEdit: {
mutator: Mutator.mutators.uppercase,
params: {}
}
}
},
setFieldValue: jest.fn((data, value) => {
data.name = value;
return data;
})
};
// Create mock cell
const mockCell = {
column: mockColumn,
row: {
getData: jest.fn(() => ({ name: "john doe", id: "123" }))
},
getComponent: jest.fn(() => ({ cell: true }))
};
// Transform the cell value
const result = mutator.transformCell(mockCell, "john doe");
// Verify the value was transformed
expect(result).toBe("JOHN DOE");
});
it("should handle mutateLink property", () => {
// Create mock linked cell
const mockLinkedCell = {
setValue: jest.fn(),
getValue: jest.fn().mockReturnValue("test value")
};
// Create mock cell
const mockCell = {
column: {
definition: {
mutateLink: "linkedColumn"
}
},
row: {
getCell: jest.fn().mockReturnValue(mockLinkedCell)
}
};
// Call mutateLink
mutator.mutateLink(mockCell);
// Verify linked cell was updated
expect(mockCell.row.getCell).toHaveBeenCalledWith("linkedColumn");
expect(mockLinkedCell.getValue).toHaveBeenCalled();
expect(mockLinkedCell.setValue).toHaveBeenCalledWith("test value", true, true);
});
it("should handle array of mutateLink properties", () => {
// Create mock linked cells
const mockLinkedCell1 = {
setValue: jest.fn(),
getValue: jest.fn().mockReturnValue("value1")
};
const mockLinkedCell2 = {
setValue: jest.fn(),
getValue: jest.fn().mockReturnValue("value2")
};
// Create mock cell
const mockCell = {
column: {
definition: {
mutateLink: ["linkedColumn1", "linkedColumn2"]
}
},
row: {
getCell: jest.fn((link) => {
if (link === "linkedColumn1") return mockLinkedCell1;
if (link === "linkedColumn2") return mockLinkedCell2;
return null;
})
}
};
// Call mutateLink
mutator.mutateLink(mockCell);
// Verify linked cells were updated
expect(mockCell.row.getCell).toHaveBeenCalledWith("linkedColumn1");
expect(mockCell.row.getCell).toHaveBeenCalledWith("linkedColumn2");
expect(mockLinkedCell1.getValue).toHaveBeenCalled();
expect(mockLinkedCell2.getValue).toHaveBeenCalled();
expect(mockLinkedCell1.setValue).toHaveBeenCalledWith("value1", true, true);
expect(mockLinkedCell2.setValue).toHaveBeenCalledWith("value2", true, true);
});
it("should handle enable and disable methods", () => {
// Initially enabled
expect(mutator.enabled).toBe(true);
// Disable
mutator.disable();
expect(mutator.enabled).toBe(false);
// Enable
mutator.enable();
expect(mutator.enabled).toBe(true);
});
it("should not apply mutations when disabled", () => {
// Create mock column with mutator
const mockColumn = {
definition: {
field: "name"
},
modules: {
mutate: {
mutatorData: {
mutator: Mutator.mutators.uppercase,
params: {}
}
}
},
getFieldValue: jest.fn(data => data.name),
setFieldValue: jest.fn(),
getComponent: jest.fn(() => ({ column: true }))
};
// Mock the column traversal
mockTable.columnManager.traverse.mockImplementation(callback => {
callback(mockColumn);
});
// Setup test data
const testData = {
name: "john doe"
};
// Disable the mutator
mutator.disable();
// Transform the data
const result = mutator.transformRow(testData, "data");
// Verify the data was not transformed
expect(result).toEqual(testData);
expect(mockColumn.setFieldValue).not.toHaveBeenCalled();
});
});

View File

@@ -0,0 +1,154 @@
import TabulatorFull from "../../../src/js/core/TabulatorFull";
import Page from "../../../src/js/modules/Page/Page";
describe("Page module", () => {
/** @type {TabulatorFull} */
let tabulator;
/** @type {Page} */
let pageMod;
const tableData = Array(100).fill().map((_, index) => ({
id: index + 1,
name: `Name ${index + 1}`,
age: Math.floor(Math.random() * 50) + 20,
country: ["USA", "UK", "Canada", "Australia", "Germany"][Math.floor(Math.random() * 5)]
}));
const tableColumns = [
{ title: "ID", field: "id" },
{ title: "Name", field: "name" },
{ title: "Age", field: "age" },
{ title: "Country", field: "country" }
];
beforeEach(async () => {
const el = document.createElement("div");
el.id = "tabulator";
document.body.appendChild(el);
tabulator = new TabulatorFull("#tabulator", {
data: tableData,
columns: tableColumns,
pagination: true,
paginationSize: 10, // 10 rows per page
paginationInitialPage: 1
});
pageMod = tabulator.module("page");
return new Promise((resolve) => {
tabulator.on("tableBuilt", () => {
resolve();
});
});
});
afterEach(() => {
tabulator.destroy();
document.getElementById("tabulator")?.remove();
});
it("should initialize with the correct pagination settings", () => {
expect(pageMod.getPage()).toBe(1);
expect(pageMod.getPageSize()).toBe(10);
expect(pageMod.getPageMax()).toBe(10); // 100 rows / 10 per page = 10 pages
});
it("should change page when setPage is called", async () => {
// Mock console.warn to prevent test output noise
const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation();
try {
await pageMod.setPage(3);
expect(pageMod.getPage()).toBe(3);
} finally {
consoleWarnSpy.mockRestore();
}
});
it("should handle invalid page numbers appropriately", () => {
// Mock console.warn to prevent test output noise
const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation();
try {
// Current implementation appears to reject invalid page numbers
// without changing the current page
expect(pageMod.getPage()).toBe(1);
// Test setting page beyond max
expect(pageMod.setPage(15)).rejects.toEqual(undefined);
// Test setting page below 1
expect(pageMod.setPage(0)).rejects.toEqual(undefined);
// Page should still be 1
expect(pageMod.getPage()).toBe(1);
} finally {
consoleWarnSpy.mockRestore();
}
});
it("should navigate to next and previous pages", async () => {
// Start at page 1
expect(pageMod.getPage()).toBe(1);
// Go to next page
await pageMod.nextPage();
expect(pageMod.getPage()).toBe(2);
// Go to previous page
await pageMod.previousPage();
expect(pageMod.getPage()).toBe(1);
});
it("should change page size and update max pages", async () => {
// Initial size and max pages
expect(pageMod.getPageSize()).toBe(10);
expect(pageMod.getPageMax()).toBe(10);
// Change page size to 20
await pageMod.setPageSize(20);
// Check page size updated
expect(pageMod.getPageSize()).toBe(20);
// Max pages should be recalculated
await new Promise(resolve => setTimeout(resolve, 100)); // Allow for async updates
// Reset the page size to 10
await pageMod.setPageSize(10);
expect(pageMod.getPageSize()).toBe(10);
});
it("should add data and recalculate pages", async () => {
// Initially 100 rows / 10 per page = 10 pages
expect(pageMod.getPageMax()).toBe(10);
// Add 10 more rows (110 total)
const newData = Array(10).fill().map((_, index) => ({
id: tableData.length + index + 1,
name: `New ${index + 1}`,
age: 30,
country: "Canada"
}));
// Add data
tabulator.addData(newData);
// Force table to process the data change
await new Promise(resolve => setTimeout(resolve, 100));
// Check the number of rows
const rowCount = tabulator.getRows().length;
expect(rowCount).toBeGreaterThan(100);
});
it("should correctly handle page navigation methods", async () => {
// These methods should function correctly even if setPage behavior
// is adjusted in the implementation
// Test getPage
expect(typeof pageMod.getPage()).toBe("number");
// Test getPageMax
expect(typeof pageMod.getPageMax()).toBe("number");
// Test getPageSize
expect(typeof pageMod.getPageSize()).toBe("number");
});
});

View File

@@ -0,0 +1,638 @@
import Persistence from "../../../src/js/modules/Persistence/Persistence";
import defaultReaders from "../../../src/js/modules/Persistence/defaults/readers";
import defaultWriters from "../../../src/js/modules/Persistence/defaults/writers";
describe("Persistence module", () => {
/** @type {Persistence} */
let persistence;
let mockTable;
beforeEach(() => {
// Mock DOM elements and localStorage
global.localStorage = {
getItem: jest.fn(),
setItem: jest.fn(),
removeItem: jest.fn()
};
document.cookie = "";
Object.defineProperty(document, 'cookie', {
writable: true
});
// Create mock element
const mockElement = {
getAttribute: jest.fn().mockReturnValue("test-table")
};
// Create mock columnManager
const mockColumnManager = {
getColumns: jest.fn().mockReturnValue([]),
setColumns: jest.fn()
};
// Create mock modules
const mockFilter = {
getFilters: jest.fn().mockReturnValue([]),
getHeaderFilters: jest.fn().mockReturnValue([])
};
const mockSort = {
getSort: jest.fn().mockReturnValue([])
};
const mockPage = {
getPageSize: jest.fn().mockReturnValue(10),
getPage: jest.fn().mockReturnValue(1)
};
// Create mock eventBus
const mockEventBus = {
subscribe: jest.fn()
};
// Create mock optionsList
const mockOptionsList = {
register: jest.fn()
};
// Create mock table functions registry
const mockFunctionRegistry = {
getColumnLayout: jest.fn(),
setColumnLayout: jest.fn()
};
// Create a simplified mock of the table
mockTable = {
element: mockElement,
columnManager: mockColumnManager,
options: {
persistence: false,
persistenceID: "",
persistenceMode: true,
persistenceReaderFunc: false,
persistenceWriterFunc: false
},
modules: {
filter: mockFilter,
sort: mockSort,
page: mockPage
},
eventBus: mockEventBus,
optionsList: mockOptionsList,
functionRegistry: mockFunctionRegistry
};
// Mock methods in the Persistence prototype
jest.spyOn(Persistence.prototype, 'registerTableOption').mockImplementation(function(key, value) {
this.table.options[key] = this.table.options[key] || value;
});
jest.spyOn(Persistence.prototype, 'registerTableFunction').mockImplementation(function(key, callback) {
this.table.functionRegistry[key] = callback;
});
jest.spyOn(Persistence.prototype, 'subscribe').mockImplementation(function(key, callback) {
return this.table.eventBus.subscribe(key, callback);
});
jest.spyOn(Persistence.prototype, 'localStorageTest').mockReturnValue(true);
// Create an instance of the Persistence module with the mock table
persistence = new Persistence(mockTable);
});
afterEach(() => {
jest.clearAllMocks();
jest.restoreAllMocks();
delete global.localStorage;
});
it("should register table options during construction", () => {
// Verify table options are registered
expect(mockTable.options.persistence).toBe(false);
expect(mockTable.options.persistenceID).toBe("");
expect(mockTable.options.persistenceMode).toBe(true);
expect(mockTable.options.persistenceReaderFunc).toBe(false);
expect(mockTable.options.persistenceWriterFunc).toBe(false);
});
it("should register table functions during initialization", () => {
// Initialize persistence
persistence.initialize();
// Verify table functions are registered
expect(mockTable.functionRegistry.getColumnLayout).toBeDefined();
expect(mockTable.functionRegistry.setColumnLayout).toBeDefined();
});
it("should not set up persistence if not enabled", () => {
// Set persistence option to false
mockTable.options.persistence = false;
// Initialize persistence
persistence.initialize();
// Verify that event subscriptions are not made
expect(mockTable.eventBus.subscribe).not.toHaveBeenCalledWith("column-init", expect.any(Function));
expect(mockTable.eventBus.subscribe).not.toHaveBeenCalledWith("column-show", expect.any(Function));
expect(mockTable.eventBus.subscribe).not.toHaveBeenCalledWith("column-hide", expect.any(Function));
});
it("should set up localStorage persistence mode when enabled", () => {
// Set persistence option to true with localStorage available
mockTable.options.persistence = true;
persistence.localStorageTest.mockReturnValue(true);
// Initialize persistence
persistence.initialize();
// Verify that the mode is set to 'local'
expect(persistence.mode).toBe("local");
expect(persistence.readFunc).toBe(defaultReaders.local);
expect(persistence.writeFunc).toBe(defaultWriters.local);
});
it("should set up cookie persistence mode when localStorage is not available", () => {
// Set persistence option to true with localStorage not available
mockTable.options.persistence = true;
persistence.localStorageTest.mockReturnValue(false);
// Initialize persistence
persistence.initialize();
// Verify that the mode is set to 'cookie'
expect(persistence.mode).toBe("cookie");
expect(persistence.readFunc).toBe(defaultReaders.cookie);
expect(persistence.writeFunc).toBe(defaultWriters.cookie);
});
it("should use custom reader/writer functions when provided", () => {
// Set up custom reader and writer functions
const customReader = jest.fn();
const customWriter = jest.fn();
mockTable.options.persistence = true;
mockTable.options.persistenceReaderFunc = customReader;
mockTable.options.persistenceWriterFunc = customWriter;
// Initialize persistence
persistence.initialize();
// Verify that the custom functions are used
expect(persistence.readFunc).toBe(customReader);
expect(persistence.writeFunc).toBe(customWriter);
});
it("should look up reader/writer functions by name when provided as strings", () => {
// Set up named reader and writer functions
mockTable.options.persistence = true;
mockTable.options.persistenceReaderFunc = "local";
mockTable.options.persistenceWriterFunc = "local";
// Initialize persistence
persistence.initialize();
// Verify that the named functions are looked up
expect(persistence.readFunc).toBe(defaultReaders.local);
expect(persistence.writeFunc).toBe(defaultWriters.local);
});
it("should warn if invalid reader/writer functions are provided", () => {
// Spy on console.warn
const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
// Set up invalid reader and writer function names
mockTable.options.persistence = true;
mockTable.options.persistenceReaderFunc = "invalidReader";
mockTable.options.persistenceWriterFunc = "invalidWriter";
// Initialize persistence
persistence.initialize();
// Verify warnings are issued
expect(consoleWarnSpy).toHaveBeenNthCalledWith(
1,
"Persistence Read Error - invalid reader set",
"invalidReader"
);
expect(consoleWarnSpy).toHaveBeenNthCalledWith(
2,
"Persistence Write Error - invalid reader set",
"invalidWriter"
);
});
it("should generate the correct persistence ID", () => {
// Test with provided persistenceID
mockTable.options.persistence = true;
mockTable.options.persistenceID = "custom-id";
// Initialize persistence
persistence.initialize();
// Verify the ID is generated correctly
expect(persistence.id).toBe("tabulator-custom-id");
// Test with table element ID
mockTable.options.persistenceID = "";
mockTable.element.getAttribute.mockReturnValue("element-id");
// Initialize persistence again
persistence.initialize();
// Verify the ID is generated correctly
expect(persistence.id).toBe("tabulator-element-id");
});
it("should set up the correct config options based on persistence setting", () => {
// Test with persistence = true
mockTable.options.persistence = true;
// Initialize persistence
persistence.initialize();
// Verify config options
expect(persistence.config.sort).toBe(true);
expect(persistence.config.filter).toBe(true);
expect(persistence.config.headerFilter).toBe(true);
expect(persistence.config.group).toBe(true);
expect(persistence.config.page).toBe(true);
expect(persistence.config.columns).toEqual(["title", "width", "visible"]);
// Test with specific persistence options
mockTable.options.persistence = {
sort: true,
filter: false,
headerFilter: true,
group: false,
page: true,
columns: ["field", "visible"]
};
// Initialize persistence again
persistence.initialize();
// Verify config options match specific settings
expect(persistence.config.sort).toBe(true);
expect(persistence.config.filter).toBe(false);
expect(persistence.config.headerFilter).toBe(true);
expect(persistence.config.group).toBe(false);
expect(persistence.config.page).toBe(true);
expect(persistence.config.columns).toEqual(["field", "visible"]);
});
it("should load pagination data from persistence", () => {
// Set up mock persistence data
const pageData = {
paginationSize: 25,
paginationInitialPage: 3
};
// Enable persistence with page option
mockTable.options.persistence = {
page: true
};
// Mock the retrieveData method
jest.spyOn(persistence, 'retrieveData').mockImplementation((type) => {
if (type === "page") return pageData;
return null;
});
// Initialize persistence
persistence.initialize();
// Verify pagination options are set
expect(mockTable.options.paginationSize).toBe(25);
expect(mockTable.options.paginationInitialPage).toBe(3);
});
it("should load group data from persistence", () => {
// Set up mock persistence data
const groupData = {
groupBy: "name",
groupStartOpen: false,
groupHeader: jest.fn()
};
// Enable persistence with group option
mockTable.options.persistence = {
group: true
};
// Mock the retrieveData method
jest.spyOn(persistence, 'retrieveData').mockImplementation((type) => {
if (type === "group") return groupData;
return null;
});
// Initialize persistence
persistence.initialize();
// Verify group options are set
expect(mockTable.options.groupBy).toBe("name");
expect(mockTable.options.groupStartOpen).toBe(false);
expect(mockTable.options.groupHeader).toBe(groupData.groupHeader);
});
it("should subscribe to column events when column persistence is enabled", () => {
// Enable persistence with columns option
mockTable.options.persistence = {
columns: true
};
// Mock the load method
jest.spyOn(persistence, 'load').mockReturnValue([]);
// Initialize persistence
persistence.initialize();
// Verify column-related subscriptions
expect(mockTable.eventBus.subscribe).toHaveBeenCalledWith("column-init", expect.any(Function));
expect(mockTable.eventBus.subscribe).toHaveBeenCalledWith("column-show", expect.any(Function));
expect(mockTable.eventBus.subscribe).toHaveBeenCalledWith("column-hide", expect.any(Function));
expect(mockTable.eventBus.subscribe).toHaveBeenCalledWith("column-moved", expect.any(Function));
});
it("should subscribe to general events when persistence is enabled", () => {
// Enable persistence
mockTable.options.persistence = true;
// Initialize persistence
persistence.initialize();
// Verify specific event subscriptions we care about in this test
// Instead of checking exact number of calls, just check that it was called
expect(mockTable.eventBus.subscribe).toHaveBeenCalled();
// Let's verify a few specific important event types are included
const subscriptionCalls = mockTable.eventBus.subscribe.mock.calls.map(call => call[0]);
expect(subscriptionCalls).toContain("filter-changed");
expect(subscriptionCalls).toContain("sort-changed");
expect(subscriptionCalls).toContain("page-changed");
});
it("should save data when eventSave is called with enabled config", () => {
// Set up persistence config
persistence.config = {
sort: true,
filter: false
};
// Spy on save method
jest.spyOn(persistence, 'save').mockImplementation(() => {});
// Call eventSave for enabled type
persistence.eventSave("sort");
// Verify save was called
expect(persistence.save).toHaveBeenCalledWith("sort");
// Call eventSave for disabled type
persistence.eventSave("filter");
// Verify save was not called again
expect(persistence.save).toHaveBeenCalledTimes(1);
});
it("should set initial sort/filter/headerFilter options during tableBuilt", () => {
// Set up mock persistence data
const sortData = [{ column: "name", dir: "asc" }];
const filterData = [{ field: "age", type: ">=", value: 18 }];
const headerFilterData = [{ field: "name", type: "like", value: "John" }];
// Enable all persistence options
mockTable.options.persistence = true;
// Mock the load method
jest.spyOn(persistence, 'load').mockImplementation((type) => {
if (type === "sort") return sortData;
if (type === "filter") return filterData;
if (type === "headerFilter") return headerFilterData;
return null;
});
// Call tableBuilt
persistence.tableBuilt();
// Mock for the tableBuilt function
mockTable.options.initialSort = sortData;
mockTable.options.initialFilter = filterData;
mockTable.options.initialHeaderFilter = headerFilterData;
// Now verify they match
expect(mockTable.options.initialSort).toEqual(sortData);
expect(mockTable.options.initialFilter).toEqual(filterData);
expect(mockTable.options.initialHeaderFilter).toEqual(headerFilterData);
});
it("should call tableRedraw to save columns if force is true", () => {
// Set up persistence config
persistence.config = {
columns: true
};
// Spy on save method
jest.spyOn(persistence, 'save').mockImplementation(() => {});
// Call tableRedraw with force = true
persistence.tableRedraw(true);
// Verify save was called
expect(persistence.save).toHaveBeenCalledWith("columns");
// Call tableRedraw with force = false
persistence.tableRedraw(false);
// Verify save was not called again
expect(persistence.save).toHaveBeenCalledTimes(1);
});
it("should return column layout with getColumnLayout method", () => {
// Set up mock columns
const mockColumns = [
{ definition: { field: "name", width: 100, visible: true } },
{ definition: { field: "age", width: 80, visible: false } }
];
// Mock the parseColumns method
const mockColumnsResult = [
{ field: "name", width: 100, visible: true },
{ field: "age", width: 80, visible: false }
];
jest.spyOn(persistence, 'parseColumns').mockReturnValue(mockColumnsResult);
// Mock columnManager.getColumns
mockTable.columnManager.getColumns.mockReturnValue(mockColumns);
// Call getColumnLayout
const result = persistence.getColumnLayout();
// Verify results
expect(result).toBe(mockColumnsResult);
expect(persistence.parseColumns).toHaveBeenCalledWith(mockColumns);
});
it("should set column layout with setColumnLayout method", () => {
// Set up mock layout
const mockLayout = [
{ field: "name", width: 100, visible: true },
{ field: "age", width: 80, visible: false }
];
// Set up mock merged definitions
const mockMerged = [
{ field: "name", width: 100, visible: true, title: "Name" },
{ field: "age", width: 80, visible: false, title: "Age" }
];
// Mock the mergeDefinition method
jest.spyOn(persistence, 'mergeDefinition').mockReturnValue(mockMerged);
// Call setColumnLayout
const result = persistence.setColumnLayout(mockLayout);
// Verify results
expect(result).toBe(true);
expect(persistence.mergeDefinition).toHaveBeenCalledWith(
mockTable.options.columns,
mockLayout,
true
);
expect(mockTable.columnManager.setColumns).toHaveBeenCalledWith(mockMerged);
});
it("should save data with writeFunc when save is called", () => {
// Set up mock data for different types
const mockColumnData = [{ field: "name", width: 100 }];
const mockFilterData = [{ field: "age", type: ">=", value: 18 }];
const mockHeaderFilterData = [{ field: "name", type: "like", value: "John" }];
const mockSortData = [{ column: "name", dir: "asc" }];
const mockGroupData = { groupBy: "name" };
const mockPageData = { paginationSize: 25 };
// Mock the various methods that return data
jest.spyOn(persistence, 'parseColumns').mockReturnValue(mockColumnData);
jest.spyOn(persistence, 'validateSorters').mockReturnValue(mockSortData);
jest.spyOn(persistence, 'getGroupConfig').mockReturnValue(mockGroupData);
jest.spyOn(persistence, 'getPageConfig').mockReturnValue(mockPageData);
mockTable.modules.filter.getFilters.mockReturnValue(mockFilterData);
mockTable.modules.filter.getHeaderFilters.mockReturnValue(mockHeaderFilterData);
// Set up persistence with writeFunc
persistence.id = "test-id";
persistence.writeFunc = jest.fn();
// Call save for each type
persistence.save("columns");
persistence.save("filter");
persistence.save("headerFilter");
persistence.save("sort");
persistence.save("group");
persistence.save("page");
// Verify writeFunc was called for each type with correct data
expect(persistence.writeFunc).toHaveBeenCalledWith("test-id", "columns", mockColumnData);
expect(persistence.writeFunc).toHaveBeenCalledWith("test-id", "filter", mockFilterData);
expect(persistence.writeFunc).toHaveBeenCalledWith("test-id", "headerFilter", mockHeaderFilterData);
expect(persistence.writeFunc).toHaveBeenCalledWith("test-id", "sort", mockSortData);
expect(persistence.writeFunc).toHaveBeenCalledWith("test-id", "group", mockGroupData);
expect(persistence.writeFunc).toHaveBeenCalledWith("test-id", "page", mockPageData);
});
it("should correctly transform sorters with validateSorters", () => {
// Set up mock sorters
const mockSorters = [
{ field: "name", dir: "asc" },
{ field: "age", dir: "desc" }
];
// Call validateSorters
const result = persistence.validateSorters(mockSorters);
// Verify results
expect(result).toEqual([
{ column: "name", dir: "asc" },
{ column: "age", dir: "desc" }
]);
});
it("should return group config with getGroupConfig", () => {
// Set up mock table options
mockTable.options.groupBy = "name";
mockTable.options.groupStartOpen = false;
mockTable.options.groupHeader = () => {};
// Test with config.group = true
persistence.config = {
group: true
};
// Call getGroupConfig
const result = persistence.getGroupConfig();
// Verify results
expect(result).toEqual({
groupBy: "name",
groupStartOpen: false,
groupHeader: mockTable.options.groupHeader
});
// Test with specific config.group options
persistence.config = {
group: {
groupBy: true,
groupStartOpen: false,
groupHeader: true
}
};
// Call getGroupConfig
const result2 = persistence.getGroupConfig();
// Verify results - only checking for groupBy and groupHeader which are essential
expect(result2).toEqual({
groupBy: "name",
groupHeader: mockTable.options.groupHeader
});
});
it("should return page config with getPageConfig", () => {
// Set up mock page module methods
mockTable.modules.page.getPageSize.mockReturnValue(25);
mockTable.modules.page.getPage.mockReturnValue(3);
// Test with config.page = true
persistence.config = {
page: true
};
// Call getPageConfig
const result = persistence.getPageConfig();
// Verify results
expect(result).toEqual({
paginationSize: 25,
paginationInitialPage: 3
});
// Test with specific config.page options
persistence.config = {
page: {
size: true,
page: false
}
};
// Call getPageConfig
const result2 = persistence.getPageConfig();
// Verify results
expect(result2).toEqual({
paginationSize: 25
});
});
});

View File

@@ -0,0 +1,735 @@
import Popup from "../../../src/js/modules/Popup/Popup";
describe("Popup module", () => {
/** @type {Popup} */
let popup;
let mockTable;
beforeEach(() => {
// Create mock element
document.createElement = jest.fn().mockImplementation((tagName) => {
const element = {
tagName: tagName.toUpperCase(),
classList: {
add: jest.fn()
},
addEventListener: jest.fn(),
insertBefore: jest.fn(),
innerHTML: "",
appendChild: jest.fn(),
firstChild: {}
};
return element;
});
// Create mock column
const mockColumn = {
definition: {},
titleElement: {
insertBefore: jest.fn()
}
};
// Mock implementation of the core popup function
const mockPopupMethods = {
renderCallback: jest.fn(),
show: jest.fn(),
hideOnBlur: jest.fn()
};
const mockPopupFunc = jest.fn().mockImplementation(() => {
return mockPopupMethods;
});
// Create mock eventBus
const mockEventBus = {
subscribe: jest.fn()
};
// Create mock externalEventBus
const mockExternalEventBus = {
dispatch: jest.fn()
};
// Create mock optionsList
const mockOptionsList = {
register: jest.fn()
};
// Create mock componentFunctionBinder
const mockComponentFunctionBinder = {
bind: jest.fn()
};
// Create a simplified mock of the table
mockTable = {
columnManager: {
columns: [mockColumn]
},
options: {
rowContextPopup: false,
rowClickPopup: false,
rowDblClickPopup: false,
groupContextPopup: false,
groupClickPopup: false,
groupDblClickPopup: false
},
eventBus: mockEventBus,
externalEvents: mockExternalEventBus,
optionsList: mockOptionsList,
componentFunctionBinder: mockComponentFunctionBinder,
popup: mockPopupFunc,
on: jest.fn()
};
// Mock methods in the Popup prototype
jest.spyOn(Popup.prototype, 'registerTableOption').mockImplementation(function(key, value) {
this.table.options[key] = this.table.options[key] || value;
});
jest.spyOn(Popup.prototype, 'registerColumnOption').mockImplementation(function(key) {
this.table.optionsList.register(key);
});
jest.spyOn(Popup.prototype, 'registerComponentFunction').mockImplementation(function(component, name, callback) {
this.table.componentFunctionBinder.bind(component, name, callback);
});
jest.spyOn(Popup.prototype, 'subscribe').mockImplementation(function(key, callback) {
return this.table.eventBus.subscribe(key, callback);
});
jest.spyOn(Popup.prototype, 'dispatchExternal').mockImplementation(function(event, component) {
this.table.externalEvents.dispatch(event, component);
});
// Create an instance of the Popup module with the mock table
popup = new Popup(mockTable);
});
afterEach(() => {
jest.clearAllMocks();
jest.restoreAllMocks();
});
it("should register all table options during construction", () => {
// Verify table options are registered
expect(mockTable.options.rowContextPopup).toBe(false);
expect(mockTable.options.rowClickPopup).toBe(false);
expect(mockTable.options.rowDblClickPopup).toBe(false);
expect(mockTable.options.groupContextPopup).toBe(false);
expect(mockTable.options.groupClickPopup).toBe(false);
expect(mockTable.options.groupDblClickPopup).toBe(false);
});
it("should register all column options during construction", () => {
// Verify column options are registered
expect(mockTable.optionsList.register).toHaveBeenCalledWith("headerContextPopup");
expect(mockTable.optionsList.register).toHaveBeenCalledWith("headerClickPopup");
expect(mockTable.optionsList.register).toHaveBeenCalledWith("headerDblClickPopup");
expect(mockTable.optionsList.register).toHaveBeenCalledWith("headerPopup");
expect(mockTable.optionsList.register).toHaveBeenCalledWith("headerPopupIcon");
expect(mockTable.optionsList.register).toHaveBeenCalledWith("contextPopup");
expect(mockTable.optionsList.register).toHaveBeenCalledWith("clickPopup");
expect(mockTable.optionsList.register).toHaveBeenCalledWith("dblClickPopup");
});
it("should register component functions during construction", () => {
// Verify component functions are registered
expect(mockTable.componentFunctionBinder.bind).toHaveBeenCalledWith("cell", "popup", expect.any(Function));
expect(mockTable.componentFunctionBinder.bind).toHaveBeenCalledWith("column", "popup", expect.any(Function));
expect(mockTable.componentFunctionBinder.bind).toHaveBeenCalledWith("row", "popup", expect.any(Function));
expect(mockTable.componentFunctionBinder.bind).toHaveBeenCalledWith("group", "popup", expect.any(Function));
});
it("should initialize and subscribe to events", () => {
// Run initialize
popup.initialize();
// Verify subscriptions
expect(mockTable.eventBus.subscribe).toHaveBeenCalledWith("column-init", expect.any(Function));
});
it("should set up row watchers when row popup options are enabled", () => {
// Spy on subscribe and loadPopupEvent methods
jest.spyOn(popup, 'loadPopupEvent');
// Set row popup options
mockTable.options.rowContextPopup = () => {};
mockTable.options.rowClickPopup = () => {};
mockTable.options.rowDblClickPopup = () => {};
// Initialize row watchers
popup.initializeRowWatchers();
// Verify subscriptions for row events
expect(mockTable.eventBus.subscribe).toHaveBeenCalledWith("row-contextmenu", expect.any(Function));
expect(mockTable.on).toHaveBeenCalledWith("rowTapHold", expect.any(Function));
expect(mockTable.eventBus.subscribe).toHaveBeenCalledWith("row-click", expect.any(Function));
expect(mockTable.eventBus.subscribe).toHaveBeenCalledWith("row-dblclick", expect.any(Function));
});
it("should set up group watchers when group popup options are enabled", () => {
// Spy on subscribe and loadPopupEvent methods
jest.spyOn(popup, 'loadPopupEvent');
// Set group popup options
mockTable.options.groupContextPopup = () => {};
mockTable.options.groupClickPopup = () => {};
mockTable.options.groupDblClickPopup = () => {};
// Initialize group watchers
popup.initializeGroupWatchers();
// Verify subscriptions for group events
expect(mockTable.eventBus.subscribe).toHaveBeenCalledWith("group-contextmenu", expect.any(Function));
expect(mockTable.on).toHaveBeenCalledWith("groupTapHold", expect.any(Function));
expect(mockTable.eventBus.subscribe).toHaveBeenCalledWith("group-click", expect.any(Function));
expect(mockTable.eventBus.subscribe).toHaveBeenCalledWith("group-dblclick", expect.any(Function));
});
it("should initialize column header popup features", () => {
// Set up mock column with headerPopup
const mockColumn = {
definition: {
headerPopup: () => {},
headerPopupIcon: "<i>Icon</i>"
},
titleElement: {
insertBefore: jest.fn(),
firstChild: {}
},
getComponent: jest.fn().mockReturnValue({ column: true })
};
// Create a mock for document.createElement with span
let headerPopupEl;
const origCreateElement = document.createElement;
document.createElement = jest.fn().mockImplementation((tagName) => {
if (tagName === "span") {
headerPopupEl = {
tagName: "SPAN",
classList: {
add: jest.fn()
},
addEventListener: jest.fn(),
insertBefore: jest.fn(),
appendChild: jest.fn(),
innerHTML: ""
};
return headerPopupEl;
}
return origCreateElement(tagName);
});
// Initialize column header popup
popup.initializeColumnHeaderPopup(mockColumn);
// Verify the popup button was created
expect(headerPopupEl.classList.add).toHaveBeenCalledWith("tabulator-header-popup-button");
expect(headerPopupEl.innerHTML).toBe("<i>Icon</i>");
expect(headerPopupEl.addEventListener).toHaveBeenCalledWith("click", expect.any(Function));
expect(mockColumn.titleElement.insertBefore).toHaveBeenCalledWith(headerPopupEl, mockColumn.titleElement.firstChild);
});
it("should use default popup icon if none provided", () => {
// Set up mock column with headerPopup but no icon
const mockColumn = {
definition: {
headerPopup: () => {}
},
titleElement: {
insertBefore: jest.fn(),
firstChild: {}
}
};
// Create a mock for document.createElement with span
let headerPopupEl;
const origCreateElement = document.createElement;
document.createElement = jest.fn().mockImplementation((tagName) => {
if (tagName === "span") {
headerPopupEl = {
tagName: "SPAN",
classList: {
add: jest.fn()
},
addEventListener: jest.fn(),
insertBefore: jest.fn(),
appendChild: jest.fn(),
innerHTML: ""
};
return headerPopupEl;
}
return origCreateElement(tagName);
});
// Initialize column header popup
popup.initializeColumnHeaderPopup(mockColumn);
// Verify default icon was used
expect(headerPopupEl.innerHTML).toBe("&vellip;");
});
it("should handle function that returns HTML element as icon", () => {
// Mock the actual implementation of initializeColumnHeaderPopup
const originalInitializeColumnHeaderPopup = Popup.prototype.initializeColumnHeaderPopup;
// Custom mock to test HTMLElement handling
Popup.prototype.initializeColumnHeaderPopup = jest.fn().mockImplementation(function(column) {
// Create popup element
const headerPopupEl = {
classList: { add: jest.fn() },
appendChild: jest.fn(),
addEventListener: jest.fn(),
innerHTML: ""
};
// Get icon from column definition
let icon = column.definition.headerPopupIcon;
if (icon) {
if (typeof icon === "function") {
icon = icon(column.getComponent());
}
// For testing purposes, check if the icon has a tagName property
// which would indicate it's an HTML element
if (icon && icon.tagName) {
headerPopupEl.appendChild(icon);
} else {
headerPopupEl.innerHTML = icon;
}
} else {
headerPopupEl.innerHTML = "&vellip;";
}
// Add event listener
headerPopupEl.addEventListener("click", jest.fn());
// Insert into DOM
column.titleElement.insertBefore(headerPopupEl, column.titleElement.firstChild);
return headerPopupEl;
});
// Create mock HTML element with tagName property to simulate HTMLElement
const iconElement = {
tagName: "I"
};
// Set up mock column with headerPopup and function icon
const mockColumn = {
definition: {
headerPopup: () => {},
headerPopupIcon: jest.fn().mockReturnValue(iconElement)
},
titleElement: {
insertBefore: jest.fn(),
firstChild: {}
},
getComponent: jest.fn().mockReturnValue({ column: true })
};
// Initialize column header popup
const headerPopupEl = popup.initializeColumnHeaderPopup(mockColumn);
// Verify icon function was called and element was appended
expect(mockColumn.definition.headerPopupIcon).toHaveBeenCalledWith({ column: true });
expect(headerPopupEl.appendChild).toHaveBeenCalledWith(iconElement);
// Restore original method
Popup.prototype.initializeColumnHeaderPopup = originalInitializeColumnHeaderPopup;
});
it("should initialize column with event handlers", () => {
// Set up mock column with popup options
const mockColumn = {
definition: {
headerContextPopup: () => {},
headerClickPopup: () => {},
headerDblClickPopup: () => {},
headerPopup: () => {},
contextPopup: () => {},
clickPopup: () => {},
dblClickPopup: () => {}
}
};
// Spy on initializeColumnHeaderPopup
jest.spyOn(popup, 'initializeColumnHeaderPopup').mockImplementation(() => {});
// Initialize column
popup.initializeColumn(mockColumn);
// Verify event subscriptions
expect(mockTable.eventBus.subscribe).toHaveBeenCalledWith("column-contextmenu", expect.any(Function));
expect(mockTable.eventBus.subscribe).toHaveBeenCalledWith("column-click", expect.any(Function));
expect(mockTable.eventBus.subscribe).toHaveBeenCalledWith("column-dblclick", expect.any(Function));
expect(mockTable.eventBus.subscribe).toHaveBeenCalledWith("cell-contextmenu", expect.any(Function));
expect(mockTable.eventBus.subscribe).toHaveBeenCalledWith("cell-click", expect.any(Function));
expect(mockTable.on).toHaveBeenCalledWith("headerTapHold", expect.any(Function));
expect(mockTable.on).toHaveBeenCalledWith("cellTapHold", expect.any(Function));
// Verify header popup was initialized
expect(popup.initializeColumnHeaderPopup).toHaveBeenCalledWith(mockColumn);
});
it("should load popup event for table cell", () => {
// Spy on loadPopupEvent
jest.spyOn(popup, 'loadPopupEvent').mockImplementation(() => {});
// Create mock cell with column definition
const mockCell = {
_cell: {
column: {
definition: {
contextPopup: () => {}
}
}
}
};
// Mock event
const mockEvent = { type: "contextmenu" };
// Load popup for cell
popup.loadPopupTableCellEvent("contextPopup", mockEvent, mockCell);
// Verify loadPopupEvent was called
expect(popup.loadPopupEvent).toHaveBeenCalledWith(
mockCell._cell.column.definition.contextPopup,
mockEvent,
mockCell._cell
);
});
it("should load popup event for table column", () => {
// Spy on loadPopupEvent
jest.spyOn(popup, 'loadPopupEvent').mockImplementation(() => {});
// Create mock column with definition
const mockColumn = {
_column: {
definition: {
headerContextPopup: () => {}
}
}
};
// Mock event
const mockEvent = { type: "contextmenu" };
// Load popup for column
popup.loadPopupTableColumnEvent("headerContextPopup", mockEvent, mockColumn);
// Verify loadPopupEvent was called
expect(popup.loadPopupEvent).toHaveBeenCalledWith(
mockColumn._column.definition.headerContextPopup,
mockEvent,
mockColumn._column
);
});
it("should load popup with function content", () => {
// Spy on loadPopup
jest.spyOn(popup, 'loadPopup').mockImplementation(() => {});
// Create mock component
const mockComponent = {
getComponent: jest.fn().mockReturnValue({ component: true })
};
// Create mock content function
const mockContentFunc = jest.fn().mockReturnValue("Popup Content");
// Mock event
const mockEvent = { type: "click" };
// Load popup event
popup.loadPopupEvent(mockContentFunc, mockEvent, mockComponent);
// Verify content function was called and loadPopup was called
expect(mockContentFunc).toHaveBeenCalledWith(mockEvent, { component: true }, expect.any(Function));
expect(popup.loadPopup).toHaveBeenCalledWith(
mockEvent,
mockComponent,
"Popup Content",
undefined,
undefined
);
});
it("should load popup with string content", () => {
// Spy on loadPopup
jest.spyOn(popup, 'loadPopup').mockImplementation(() => {});
// Create mock component
const mockComponent = {
getComponent: jest.fn().mockReturnValue({ component: true })
};
// Create mock content string
const mockContent = "Static Popup Content";
// Mock event
const mockEvent = { type: "click" };
// Load popup event
popup.loadPopupEvent(mockContent, mockEvent, mockComponent);
// Verify loadPopup was called with string content
expect(popup.loadPopup).toHaveBeenCalledWith(
mockEvent,
mockComponent,
mockContent,
undefined,
undefined
);
});
it("should unwrap _group and _row components", () => {
// Spy on loadPopup
jest.spyOn(popup, 'loadPopup').mockImplementation(() => {});
// Create mock group component
const mockGroup = {
_group: {
getComponent: jest.fn().mockReturnValue({ group: true })
}
};
// Create mock row component
const mockRow = {
_row: {
getComponent: jest.fn().mockReturnValue({ row: true })
}
};
// Create mock content
const mockContent = "Popup Content";
// Mock event
const mockEvent = { type: "click" };
// Load popup event for group
popup.loadPopupEvent(mockContent, mockEvent, mockGroup);
// Verify loadPopup was called with unwrapped group
expect(popup.loadPopup).toHaveBeenCalledWith(
mockEvent,
mockGroup._group,
mockContent,
undefined,
undefined
);
// Load popup event for row
popup.loadPopupEvent(mockContent, mockEvent, mockRow);
// Verify loadPopup was called with unwrapped row
expect(popup.loadPopup).toHaveBeenCalledWith(
mockEvent,
mockRow._row,
mockContent,
undefined,
undefined
);
});
it("should create popup with HTML content", () => {
// Create mock for the Popup module's actual implementation
const originalLoadPopup = Popup.prototype.loadPopup;
Popup.prototype.loadPopup = jest.fn();
// Create mock HTML element
const mockHtmlElement = document.createElement("div");
// Create mock component
const mockComponent = {
getElement: jest.fn().mockReturnValue({}),
getComponent: jest.fn().mockReturnValue({ component: true })
};
// Mock event
const mockEvent = {
preventDefault: jest.fn()
};
// Now call loadPopupEvent which will call our mocked loadPopup
popup.loadPopupEvent(mockHtmlElement, mockEvent, mockComponent);
// Verify the loadPopup was called with the right params
expect(Popup.prototype.loadPopup).toHaveBeenCalledWith(
mockEvent,
mockComponent,
mockHtmlElement,
undefined,
undefined
);
// Restore the original method
Popup.prototype.loadPopup = originalLoadPopup;
});
it("should create popup with string content", () => {
// Create mock for the Popup module's actual implementation
const originalLoadPopup = Popup.prototype.loadPopup;
Popup.prototype.loadPopup = jest.fn();
// Create mock component
const mockComponent = {
getElement: jest.fn().mockReturnValue({}),
getComponent: jest.fn().mockReturnValue({ component: true })
};
// Mock event
const mockEvent = {
preventDefault: jest.fn()
};
// String content
const content = "String Content";
// Now call loadPopupEvent which will call our mocked loadPopup
popup.loadPopupEvent(content, mockEvent, mockComponent);
// Verify the loadPopup was called with the right params
expect(Popup.prototype.loadPopup).toHaveBeenCalledWith(
mockEvent,
mockComponent,
content,
undefined,
undefined
);
// Restore the original method
Popup.prototype.loadPopup = originalLoadPopup;
});
it("should handle custom position when no event is provided", () => {
// Create mock for the Popup module's actual implementation
const originalLoadPopup = Popup.prototype.loadPopup;
Popup.prototype.loadPopup = jest.fn();
// Create mock component
const mockComponent = {
getElement: jest.fn().mockReturnValue({}),
getComponent: jest.fn().mockReturnValue({ component: true })
};
// Load popup with a position parameter directly
const content = "Content";
const position = "bottom";
// Call the component popup function which will use our mock
popup._componentPopupCall(mockComponent, content, position);
// Verify loadPopup was called with the correct position
expect(Popup.prototype.loadPopup).toHaveBeenCalledWith(
null,
mockComponent,
content,
undefined,
position
);
// Restore the original method
Popup.prototype.loadPopup = originalLoadPopup;
});
it("should call rendered callback if provided", () => {
// Create mock for the Popup module's actual implementation
const originalLoadPopup = Popup.prototype.loadPopup;
Popup.prototype.loadPopup = jest.fn();
// Create mock component
const mockComponent = {
getElement: jest.fn().mockReturnValue({}),
getComponent: jest.fn().mockReturnValue({ component: true })
};
// Mock event
const mockEvent = { preventDefault: jest.fn() };
// Create rendered callback and content
const content = "Content";
let renderedCallback;
// Create a content function that provides the callback
const contentFunction = jest.fn().mockImplementation((e, component, onRendered) => {
onRendered(jest.fn());
return content;
});
// Call loadPopupEvent with our content function
popup.loadPopupEvent(contentFunction, mockEvent, mockComponent);
// Verify loadPopup was called with a callback parameter
expect(Popup.prototype.loadPopup).toHaveBeenCalledWith(
mockEvent,
mockComponent,
content,
expect.any(Function),
undefined
);
// Restore the original method
Popup.prototype.loadPopup = originalLoadPopup;
});
it("should dispatch popupClosed event when popup closes", () => {
// Create mock for the hideOnBlur method
const mockHideOnBlur = jest.fn().mockImplementation(callback => {
// Store the callback for later use
mockHideOnBlur.callback = callback;
});
// Create mock popup object with hideOnBlur method
const mockPopupObj = {
renderCallback: jest.fn(),
show: jest.fn(),
hideOnBlur: mockHideOnBlur
};
// Replace the popup method to return our mock
const originalPopup = mockTable.popup;
mockTable.popup = jest.fn().mockReturnValue(mockPopupObj);
// Spy on dispatchExternal
jest.spyOn(popup, 'dispatchExternal');
// Create mock component using simplified structure
const mockComponent = {
getElement: jest.fn().mockReturnValue({}),
getComponent: jest.fn().mockReturnValue({ component: true })
};
// Create a mock event using our helper
const mockEvent = createMockEvent('click');
// Mock the loadPopup method with a simpler approach
const originalLoadPopup = Popup.prototype.loadPopup;
Popup.prototype.loadPopup = jest.fn().mockImplementation(function(e, component, contents) {
const popupObj = this.table.popup(contents);
popupObj.show(e);
popupObj.hideOnBlur(() => {
this.dispatchExternal("popupClosed", component.getComponent());
});
this.dispatchExternal("popupOpened", component.getComponent());
});
// Call loadPopup
popup.loadPopup(mockEvent, mockComponent, "Content");
// Simulate the hideOnBlur callback being called
mockHideOnBlur.callback();
// Verify popupClosed event was dispatched
expect(popup.dispatchExternal).toHaveBeenCalledWith("popupClosed", { component: true });
// Restore mocks
mockTable.popup = originalPopup;
Popup.prototype.loadPopup = originalLoadPopup;
});
});

View File

@@ -0,0 +1,396 @@
import Print from "../../../src/js/modules/Print/Print";
describe("Print module", () => {
/** @type {Print} */
let print;
let mockTable;
let addEventListenerSpy;
let removeEventListenerSpy;
let originalWindowPrint;
let originalScrollTo;
beforeEach(() => {
// Save original window methods
originalWindowPrint = window.print;
originalScrollTo = window.scrollTo;
// Mock window methods
window.print = jest.fn();
window.scrollTo = jest.fn();
window.scrollX = 100;
window.scrollY = 100;
// Spy on window event listeners
addEventListenerSpy = jest.spyOn(window, 'addEventListener');
removeEventListenerSpy = jest.spyOn(window, 'removeEventListener');
// Mock document methods and elements
document.createElement = jest.fn().mockImplementation((tagName) => {
const element = {
tagName: tagName.toUpperCase(),
classList: {
add: jest.fn()
},
style: {},
appendChild: jest.fn(),
parentNode: {
insertBefore: jest.fn(),
removeChild: jest.fn()
},
innerHTML: ""
};
return element;
});
// Instead of replacing document.body, just spy on its methods
if (document.body) {
jest.spyOn(document.body.classList, 'add').mockImplementation(() => {});
jest.spyOn(document.body.classList, 'remove').mockImplementation(() => {});
jest.spyOn(document.body, 'appendChild').mockImplementation(() => {});
}
// Create mock element
const mockElement = {
style: {},
parentNode: {
insertBefore: jest.fn()
}
};
// Create mock export module
const mockExportModule = {
generateTable: jest.fn().mockReturnValue({
table: true
})
};
// Create mock eventBus
const mockEventBus = {
subscribe: jest.fn()
};
// Create mock optionsList
const mockOptionsList = {
register: jest.fn()
};
// Create mock functionRegistry
const mockFunctionRegistry = {};
// Create a simplified mock of the table
mockTable = {
element: mockElement,
modules: {
export: mockExportModule
},
options: {
printAsHtml: false,
printFormatter: false,
printHeader: false,
printFooter: false,
printStyled: true,
printRowRange: "visible",
printConfig: {}
},
eventBus: mockEventBus,
optionsList: mockOptionsList,
functionRegistry: mockFunctionRegistry
};
// Mock methods in the Print prototype
jest.spyOn(Print.prototype, 'registerTableOption').mockImplementation(function(key, value) {
this.table.options[key] = this.table.options[key] || value;
});
jest.spyOn(Print.prototype, 'registerColumnOption').mockImplementation(function(key) {
this.table.optionsList.register(key);
});
jest.spyOn(Print.prototype, 'registerTableFunction').mockImplementation(function(key, callback) {
this.table.functionRegistry[key] = callback;
});
jest.spyOn(Print.prototype, 'subscribe').mockImplementation(function(key, callback) {
return this.table.eventBus.subscribe(key, callback);
});
// Create an instance of the Print module with the mock table
print = new Print(mockTable);
});
afterEach(() => {
jest.clearAllMocks();
jest.restoreAllMocks();
// Restore original window methods
window.print = originalWindowPrint;
window.scrollTo = originalScrollTo;
});
it("should register all table options during construction", () => {
// Verify table options are registered
expect(mockTable.options.printAsHtml).toBe(false);
expect(mockTable.options.printFormatter).toBe(false);
expect(mockTable.options.printHeader).toBe(false);
expect(mockTable.options.printFooter).toBe(false);
expect(mockTable.options.printStyled).toBe(true);
expect(mockTable.options.printRowRange).toBe("visible");
expect(mockTable.options.printConfig).toEqual({});
});
it("should register column options during construction", () => {
// Verify column options are registered
expect(mockTable.optionsList.register).toHaveBeenCalledWith("print");
expect(mockTable.optionsList.register).toHaveBeenCalledWith("titlePrint");
});
it("should register table print function during initialization", () => {
// Initialize print module
print.initialize();
// Verify print function is registered
expect(mockTable.functionRegistry.print).toBeDefined();
});
it("should not set up event listeners when printAsHtml is false", () => {
// Initialize print module with printAsHtml = false
mockTable.options.printAsHtml = false;
print.initialize();
// Verify event listeners are not added
expect(addEventListenerSpy).not.toHaveBeenCalledWith("beforeprint", expect.any(Function));
expect(addEventListenerSpy).not.toHaveBeenCalledWith("afterprint", expect.any(Function));
});
it("should set up event listeners when printAsHtml is true", () => {
// Initialize print module with printAsHtml = true
mockTable.options.printAsHtml = true;
print.initialize();
// Verify event listeners are added
expect(addEventListenerSpy).toHaveBeenCalledWith("beforeprint", expect.any(Function));
expect(addEventListenerSpy).toHaveBeenCalledWith("afterprint", expect.any(Function));
expect(mockTable.eventBus.subscribe).toHaveBeenCalledWith("table-destroy", expect.any(Function));
});
it("should clean up event listeners on destroy", () => {
// Initialize print module with printAsHtml = true
mockTable.options.printAsHtml = true;
print.initialize();
// Destroy print module
print.destroy();
// Verify event listeners are removed
expect(removeEventListenerSpy).toHaveBeenCalledWith("beforeprint", expect.any(Function));
expect(removeEventListenerSpy).toHaveBeenCalledWith("afterprint", expect.any(Function));
});
it("should replace table with print version on beforeprint event", () => {
// Initialize print module with printAsHtml = true
mockTable.options.printAsHtml = true;
print.initialize();
// Call replaceTable (this would be triggered by beforeprint event)
print.replaceTable();
// Verify table is replaced
expect(document.createElement).toHaveBeenCalledWith("div");
expect(print.element.classList.add).toHaveBeenCalledWith("tabulator-print-table");
expect(mockTable.modules.export.generateTable).toHaveBeenCalledWith(
mockTable.options.printConfig,
mockTable.options.printStyled,
mockTable.options.printRowRange,
"print"
);
expect(mockTable.element.style.display).toBe("none");
expect(mockTable.element.parentNode.insertBefore).toHaveBeenCalledWith(print.element, mockTable.element);
});
it("should clean up after printing", () => {
// Set up element and call cleanup
print.element = document.createElement("div");
mockTable.element.style.display = "none";
// Call cleanup (this would be triggered by afterprint event)
print.cleanup();
// Verify cleanup
expect(document.body.classList.remove).toHaveBeenCalledWith("tabulator-print-fullscreen-hide");
expect(print.element.parentNode.removeChild).toHaveBeenCalledWith(print.element);
expect(mockTable.element.style.display).toBe("");
});
it("should print table in fullscreen mode", () => {
// Initialize print module
print.initialize();
// Call printFullscreen
print.printFullscreen();
// Verify fullscreen print setup
expect(document.createElement).toHaveBeenCalledWith("div");
expect(print.element.classList.add).toHaveBeenCalledWith("tabulator-print-fullscreen");
expect(mockTable.modules.export.generateTable).toHaveBeenCalledWith(
mockTable.options.printConfig,
mockTable.options.printStyled,
mockTable.options.printRowRange,
"print"
);
expect(document.body.classList.add).toHaveBeenCalledWith("tabulator-print-fullscreen-hide");
expect(document.body.appendChild).toHaveBeenCalledWith(print.element);
expect(window.print).toHaveBeenCalled();
expect(window.scrollTo).toHaveBeenCalledWith(100, 100);
});
it("should print table with custom visible, style, and config parameters", () => {
// Initialize print module
print.initialize();
// Call printFullscreen with custom parameters
print.printFullscreen(true, false, { custom: "config" });
// Verify parameters are passed to generateTable
expect(mockTable.modules.export.generateTable).toHaveBeenCalledWith(
{ custom: "config" },
false,
true,
"print"
);
});
it("should add header to print output if printHeader is provided as string", () => {
// Set header option as string
mockTable.options.printHeader = "Test Header";
// Initialize and print
print.initialize();
print.printFullscreen();
// Verify header is created and added
expect(document.createElement).toHaveBeenCalledWith("div");
expect(print.element.appendChild).toHaveBeenCalledTimes(2); // header and table
// Get the header element (first created div)
const headerEl = document.createElement.mock.results[0].value;
// Verify header setup
expect(headerEl.classList.add).toHaveBeenCalledWith("tabulator-print-header");
expect(headerEl.innerHTML).toBe("Test Header");
});
it("should add header to print output if printHeader is provided as function", () => {
// Create header element
const headerContent = document.createElement("h1");
// Spy on document.createElement to capture calls
const createElementSpy = jest.spyOn(document, 'createElement');
// Set header option as function
mockTable.options.printHeader = jest.fn().mockReturnValue(headerContent);
// Initialize and print
print.initialize();
print.printFullscreen();
// Verify header function is called
expect(mockTable.options.printHeader).toHaveBeenCalled();
// Find the header element by looking at all created elements and finding the one
// that had the tabulator-print-header class added to it
let headerEl = null;
createElementSpy.mock.results.forEach(result => {
const el = result.value;
if (el.classList.add.mock.calls.some(call => call[0] === "tabulator-print-header")) {
headerEl = el;
}
});
// Verify header setup
expect(headerEl).not.toBeNull();
expect(headerEl.classList.add).toHaveBeenCalledWith("tabulator-print-header");
expect(headerEl.appendChild).toHaveBeenCalledWith(headerContent);
});
it("should add footer to print output if printFooter is provided as string", () => {
// Set footer option as string
mockTable.options.printFooter = "Test Footer";
// Initialize and print
print.initialize();
print.printFullscreen();
// Verify footer is created and added
expect(document.createElement).toHaveBeenCalledWith("div");
expect(print.element.appendChild).toHaveBeenCalledTimes(2); // table and footer
// Get the footer element (second created div)
const footerEl = document.createElement.mock.results[1].value;
// Verify footer setup
expect(footerEl.classList.add).toHaveBeenCalledWith("tabulator-print-footer");
expect(footerEl.innerHTML).toBe("Test Footer");
});
it("should add footer to print output if printFooter is provided as function", () => {
// Create footer element
const footerContent = document.createElement("div");
// Spy on document.createElement to capture calls
const createElementSpy = jest.spyOn(document, 'createElement');
// Set footer option as function
mockTable.options.printFooter = jest.fn().mockReturnValue(footerContent);
// Initialize and print
print.initialize();
print.printFullscreen();
// Verify footer function is called
expect(mockTable.options.printFooter).toHaveBeenCalled();
// Find the footer element by looking at all created elements and finding the one
// that had the tabulator-print-footer class added to it
let footerEl = null;
createElementSpy.mock.results.forEach(result => {
const el = result.value;
if (el.classList.add.mock.calls.some(call => call[0] === "tabulator-print-footer")) {
footerEl = el;
}
});
// Verify footer setup
expect(footerEl).not.toBeNull();
expect(footerEl.classList.add).toHaveBeenCalledWith("tabulator-print-footer");
expect(footerEl.appendChild).toHaveBeenCalledWith(footerContent);
});
it("should call printFormatter if provided", () => {
// Set formatter function
mockTable.options.printFormatter = jest.fn();
// Initialize and print
print.initialize();
print.printFullscreen();
// Verify formatter is called
expect(mockTable.options.printFormatter).toHaveBeenCalledWith(
print.element,
{ table: true } // mock table element returned by generateTable
);
});
it("should not replace table during window.print if manual block is active", () => {
// Initialize print module with printAsHtml = true
mockTable.options.printAsHtml = true;
print.initialize();
// Set manual block and call replaceTable
print.manualBlock = true;
print.replaceTable();
// Verify table is not replaced
expect(document.createElement).not.toHaveBeenCalled();
expect(mockTable.modules.export.generateTable).not.toHaveBeenCalled();
});
});

View File

@@ -0,0 +1,560 @@
import ReactiveData from "../../../src/js/modules/ReactiveData/ReactiveData";
describe("ReactiveData module", () => {
/** @type {ReactiveData} */
let reactiveData;
let mockTable;
beforeEach(() => {
// Create mock rowManager
const mockRowManager = {
addRowActual: jest.fn(),
getRowFromDataObject: jest.fn(),
reRenderInPosition: jest.fn(),
refreshActiveData: jest.fn()
};
// Create mock dataTree module
const mockDataTree = {
initializeRow: jest.fn(),
layoutRow: jest.fn()
};
// Create mock eventBus
const mockEventBus = {
subscribe: jest.fn()
};
// Create a simplified mock of the table
mockTable = {
rowManager: mockRowManager,
modules: {
dataTree: mockDataTree
},
options: {
reactiveData: false,
dataTree: false,
dataTreeChildField: "children"
},
eventBus: mockEventBus
};
// Mock methods in the ReactiveData prototype
jest.spyOn(ReactiveData.prototype, 'registerTableOption').mockImplementation(function(key, value) {
this.table.options[key] = this.table.options[key] || value;
});
jest.spyOn(ReactiveData.prototype, 'subscribe').mockImplementation(function(key, callback) {
return this.table.eventBus.subscribe(key, callback);
});
// Create an instance of the ReactiveData module with the mock table
reactiveData = new ReactiveData(mockTable);
});
afterEach(() => {
jest.clearAllMocks();
jest.restoreAllMocks();
});
it("should register reactiveData table option during construction", () => {
// Verify table option is registered
expect(mockTable.options.reactiveData).toBe(false);
});
it("should not subscribe to events if reactiveData is disabled", () => {
// Initialize module with reactiveData = false
mockTable.options.reactiveData = false;
reactiveData.initialize();
// Verify no events are subscribed
expect(mockTable.eventBus.subscribe).not.toHaveBeenCalled();
});
it("should subscribe to events when reactiveData is enabled", () => {
// Initialize module with reactiveData = true
mockTable.options.reactiveData = true;
reactiveData.initialize();
// Verify events are subscribed
expect(mockTable.eventBus.subscribe).toHaveBeenCalledWith("cell-value-save-before", expect.any(Function));
expect(mockTable.eventBus.subscribe).toHaveBeenCalledWith("cell-value-save-after", expect.any(Function));
expect(mockTable.eventBus.subscribe).toHaveBeenCalledWith("row-data-save-before", expect.any(Function));
expect(mockTable.eventBus.subscribe).toHaveBeenCalledWith("row-data-save-after", expect.any(Function));
expect(mockTable.eventBus.subscribe).toHaveBeenCalledWith("row-data-init-after", expect.any(Function));
expect(mockTable.eventBus.subscribe).toHaveBeenCalledWith("data-processing", expect.any(Function));
expect(mockTable.eventBus.subscribe).toHaveBeenCalledWith("table-destroy", expect.any(Function));
});
it("should block and unblock reactivity", () => {
// Initially not blocked
expect(reactiveData.blocked).toBe(false);
// Block reactivity
reactiveData.block("test");
expect(reactiveData.blocked).toBe("test");
// Try to block with another key
reactiveData.block("another");
expect(reactiveData.blocked).toBe("test"); // Still blocked by first key
// Unblock with wrong key
reactiveData.unblock("wrong");
expect(reactiveData.blocked).toBe("test"); // Still blocked
// Unblock with correct key
reactiveData.unblock("test");
expect(reactiveData.blocked).toBe(false); // Unblocked
});
it("should watch a row and its data properties", () => {
// Create a mock row
const mockRow = {
getData: jest.fn().mockReturnValue({
id: 1,
name: "John",
age: 30
}),
updateData: jest.fn()
};
// Spy on watchKey
jest.spyOn(reactiveData, 'watchKey');
// Watch row
reactiveData.watchRow(mockRow);
// Verify watchKey was called for each property
expect(reactiveData.watchKey).toHaveBeenCalledWith(mockRow, mockRow.getData(), "id");
expect(reactiveData.watchKey).toHaveBeenCalledWith(mockRow, mockRow.getData(), "name");
expect(reactiveData.watchKey).toHaveBeenCalledWith(mockRow, mockRow.getData(), "age");
});
it("should watch tree children if dataTree is enabled", () => {
// Enable dataTree
mockTable.options.dataTree = true;
// Create a mock row with children
const mockRow = {
getData: jest.fn().mockReturnValue({
id: 1,
name: "John",
children: [
{ id: 2, name: "Jane" }
]
}),
updateData: jest.fn()
};
// Spy on watchTreeChildren
jest.spyOn(reactiveData, 'watchTreeChildren');
// Watch row
reactiveData.watchRow(mockRow);
// Verify watchTreeChildren was called
expect(reactiveData.watchTreeChildren).toHaveBeenCalledWith(mockRow);
});
it("should watch a data property and trigger updates when it changes", () => {
// Create a mock row
const mockRow = {
getData: jest.fn().mockReturnValue({
id: 1,
name: "John"
}),
updateData: jest.fn()
};
// Watch the name property
reactiveData.watchKey(mockRow, mockRow.getData(), "name");
// Change the property value
mockRow.getData().name = "Jane";
// Verify updateData was called with the new value
expect(mockRow.updateData).toHaveBeenCalledWith({ name: "Jane" });
});
it("should not trigger updates when property changes if reactivity is blocked", () => {
// Create a mock row
const mockRow = {
getData: jest.fn().mockReturnValue({
id: 1,
name: "John"
}),
updateData: jest.fn()
};
// Watch the name property
reactiveData.watchKey(mockRow, mockRow.getData(), "name");
// Block reactivity
reactiveData.block("test");
// Change the property value
mockRow.getData().name = "Jane";
// Verify updateData was not called
expect(mockRow.updateData).not.toHaveBeenCalled();
});
it("should watch and react to tree children array manipulations", () => {
// Create a mock row with children array
const children = [
{ id: 2, name: "Jane" },
{ id: 3, name: "Bob" }
];
const mockRow = {
getData: jest.fn().mockReturnValue({
id: 1,
name: "John",
children: children
})
};
// Spy on rebuildTree
jest.spyOn(reactiveData, 'rebuildTree');
// Watch children array
reactiveData.watchTreeChildren(mockRow);
// Test push
children.push({ id: 4, name: "Alice" });
expect(reactiveData.rebuildTree).toHaveBeenCalledWith(mockRow);
// Reset spy
reactiveData.rebuildTree.mockClear();
// Test unshift
children.unshift({ id: 5, name: "Tom" });
expect(reactiveData.rebuildTree).toHaveBeenCalledWith(mockRow);
// Reset spy
reactiveData.rebuildTree.mockClear();
// Test pop
children.pop();
expect(reactiveData.rebuildTree).toHaveBeenCalledWith(mockRow);
// Reset spy
reactiveData.rebuildTree.mockClear();
// Test shift
children.shift();
expect(reactiveData.rebuildTree).toHaveBeenCalledWith(mockRow);
// Reset spy
reactiveData.rebuildTree.mockClear();
// Test splice
children.splice(0, 1, { id: 6, name: "Sam" });
expect(reactiveData.rebuildTree).toHaveBeenCalledWith(mockRow);
});
it("should not trigger tree rebuilds when array manipulations happen while blocked", () => {
// Create a mock row with children array
const children = [
{ id: 2, name: "Jane" },
{ id: 3, name: "Bob" }
];
const mockRow = {
getData: jest.fn().mockReturnValue({
id: 1,
name: "John",
children: children
})
};
// Spy on rebuildTree
jest.spyOn(reactiveData, 'rebuildTree');
// Watch children array
reactiveData.watchTreeChildren(mockRow);
// Block reactivity
reactiveData.block("test");
// Test various operations
children.push({ id: 4, name: "Alice" });
children.unshift({ id: 5, name: "Tom" });
children.pop();
children.shift();
children.splice(0, 1, { id: 6, name: "Sam" });
// Verify rebuildTree was not called
expect(reactiveData.rebuildTree).not.toHaveBeenCalled();
});
it("should invoke required methods when rebuilding a tree", () => {
// Create a mock row
const mockRow = {
id: 1,
name: "John"
};
// Call rebuildTree
reactiveData.rebuildTree(mockRow);
// Verify dataTree methods were called
expect(mockTable.modules.dataTree.initializeRow).toHaveBeenCalledWith(mockRow);
expect(mockTable.modules.dataTree.layoutRow).toHaveBeenCalledWith(mockRow);
expect(mockTable.rowManager.refreshActiveData).toHaveBeenCalledWith("tree", false, true);
});
it("should watch data array and override array methods", () => {
// Create a test data array
const testData = [
{ id: 1, name: "John" },
{ id: 2, name: "Jane" }
];
// Store original array methods
const origPush = testData.push;
const origUnshift = testData.unshift;
const origShift = testData.shift;
const origPop = testData.pop;
const origSplice = testData.splice;
// Watch data
reactiveData.watchData(testData);
// Verify methods were overridden
expect(testData.push).not.toBe(origPush);
expect(testData.unshift).not.toBe(origUnshift);
expect(testData.shift).not.toBe(origShift);
expect(testData.pop).not.toBe(origPop);
expect(testData.splice).not.toBe(origSplice);
// Verify data was stored
expect(reactiveData.data).toBe(testData);
expect(reactiveData.origFuncs.push).toBe(origPush);
expect(reactiveData.origFuncs.unshift).toBe(origUnshift);
expect(reactiveData.origFuncs.shift).toBe(origShift);
expect(reactiveData.origFuncs.pop).toBe(origPop);
expect(reactiveData.origFuncs.splice).toBe(origSplice);
});
it("should handle push operations on the data array", () => {
// Create a test data array
const testData = [
{ id: 1, name: "John" },
{ id: 2, name: "Jane" }
];
// Create a new row object
const newRow = { id: 3, name: "Bob" };
// Watch data
reactiveData.watchData(testData);
// Call push
testData.push(newRow);
// Verify row was added
expect(mockTable.rowManager.addRowActual).toHaveBeenCalledWith(newRow, false);
});
it("should handle unshift operations on the data array", () => {
// Create a test data array
const testData = [
{ id: 1, name: "John" },
{ id: 2, name: "Jane" }
];
// Create a new row object
const newRow = { id: 3, name: "Bob" };
// Watch data
reactiveData.watchData(testData);
// Call unshift
testData.unshift(newRow);
// Verify row was added
expect(mockTable.rowManager.addRowActual).toHaveBeenCalledWith(newRow, true);
});
it("should handle shift operations on the data array", () => {
// Create a test data array
const testData = [
{ id: 1, name: "John" },
{ id: 2, name: "Jane" }
];
// Create a mock row with a delete method
const mockRow = {
deleteActual: jest.fn()
};
// Store reference to first item before shift
const firstItem = testData[0];
// Configure rowManager to return the mock row
mockTable.rowManager.getRowFromDataObject.mockReturnValue(mockRow);
// Watch data
reactiveData.watchData(testData);
// Call shift
testData.shift();
// Verify the row was retrieved and deleted
expect(mockTable.rowManager.getRowFromDataObject).toHaveBeenCalledWith(firstItem);
expect(mockRow.deleteActual).toHaveBeenCalled();
});
it("should handle pop operations on the data array", () => {
// Create a test data array
const testData = [
{ id: 1, name: "John" },
{ id: 2, name: "Jane" }
];
// Create a mock row with a delete method
const mockRow = {
deleteActual: jest.fn()
};
// Store reference to last item before pop
const lastItem = testData[testData.length - 1];
// Configure rowManager to return the mock row
mockTable.rowManager.getRowFromDataObject.mockReturnValue(mockRow);
// Watch data
reactiveData.watchData(testData);
// Call pop
testData.pop();
// Verify the row was retrieved and deleted
expect(mockTable.rowManager.getRowFromDataObject).toHaveBeenCalledWith(lastItem);
expect(mockRow.deleteActual).toHaveBeenCalled();
});
it("should handle splice operations on the data array", () => {
// Create a test data array
const testData = [
{ id: 1, name: "John" },
{ id: 2, name: "Jane" },
{ id: 3, name: "Bob" }
];
// Create a new row and a mock row
const newRow = { id: 4, name: "Alice" };
const mockRow = {
deleteActual: jest.fn()
};
// Configure rowManager to return the mock row for the removed row
mockTable.rowManager.getRowFromDataObject.mockReturnValue(mockRow);
// Watch data
reactiveData.watchData(testData);
// Call splice to remove one row and add a new one
testData.splice(1, 1, newRow);
// Verify rows were added and removed appropriately
expect(mockTable.rowManager.addRowActual).toHaveBeenCalled();
expect(mockTable.rowManager.getRowFromDataObject).toHaveBeenCalled();
expect(mockRow.deleteActual).toHaveBeenCalled();
expect(mockTable.rowManager.reRenderInPosition).toHaveBeenCalled();
});
it("should not trigger data operations when reactivity is blocked", () => {
// Create a test data array
const testData = [
{ id: 1, name: "John" },
{ id: 2, name: "Jane" }
];
// Watch data
reactiveData.watchData(testData);
// Block reactivity
reactiveData.block("test");
// Call various operations
testData.push({ id: 3, name: "Bob" });
testData.unshift({ id: 4, name: "Alice" });
testData.pop();
testData.shift();
testData.splice(0, 1, { id: 5, name: "Sam" });
// Verify no changes were processed
expect(mockTable.rowManager.addRowActual).not.toHaveBeenCalled();
expect(mockTable.rowManager.getRowFromDataObject).not.toHaveBeenCalled();
expect(mockTable.rowManager.reRenderInPosition).not.toHaveBeenCalled();
});
it("should unwatchData and restore original array methods", () => {
// Create a test data array
const testData = [
{ id: 1, name: "John" },
{ id: 2, name: "Jane" }
];
// Store original methods
const origPush = testData.push;
// Watch data
reactiveData.watchData(testData);
// Verify methods were changed
expect(testData.push).not.toBe(origPush);
// Spy on Object.defineProperty
jest.spyOn(Object, 'defineProperty');
// Unwatch data
reactiveData.unwatchData();
// Verify Object.defineProperty was called to restore methods
expect(Object.defineProperty).toHaveBeenCalledWith(testData, "push", expect.any(Object));
expect(Object.defineProperty).toHaveBeenCalledWith(testData, "unshift", expect.any(Object));
expect(Object.defineProperty).toHaveBeenCalledWith(testData, "shift", expect.any(Object));
expect(Object.defineProperty).toHaveBeenCalledWith(testData, "pop", expect.any(Object));
expect(Object.defineProperty).toHaveBeenCalledWith(testData, "splice", expect.any(Object));
});
it("should unwatchRow and restore original property definitions", () => {
// Create a mock row
const mockRow = {
getData: jest.fn().mockReturnValue({
id: 1,
name: "John"
})
};
// Spy on Object.defineProperty
jest.spyOn(Object, 'defineProperty');
// Unwatch row
reactiveData.unwatchRow(mockRow);
// Verify Object.defineProperty was called for each property
expect(Object.defineProperty).toHaveBeenCalledWith(mockRow.getData(), "id", expect.any(Object));
expect(Object.defineProperty).toHaveBeenCalledWith(mockRow.getData(), "name", expect.any(Object));
});
it("should update currentVersion when watching new data", () => {
// Initial version should be 0
expect(reactiveData.currentVersion).toBe(0);
// Watch data
reactiveData.watchData([]);
// Version should be incremented
expect(reactiveData.currentVersion).toBe(1);
// Watch another data array
reactiveData.watchData([]);
// Version should be incremented again
expect(reactiveData.currentVersion).toBe(2);
});
});

View File

@@ -0,0 +1,148 @@
import TabulatorFull from "../../../src/js/core/TabulatorFull";
import ResizeColumns from "../../../src/js/modules/ResizeColumns/ResizeColumns";
describe("ResizeColumns module", () => {
/** @type {TabulatorFull} */
let tabulator;
/** @type {ResizeColumns} */
let resizeColumnsMod;
let tableData = [
{ id: 1, name: "John", active: true },
{ id: 2, name: "Jane", active: false },
{ id: 3, name: "Bob", active: true }
];
let tableColumns = [
{ title: "ID", field: "id", resizable: true },
{ title: "Name", field: "name", resizable: true },
{ title: "Active", field: "active", resizable: false }
];
beforeEach(async () => {
const el = document.createElement("div");
el.id = "tabulator";
document.body.appendChild(el);
tabulator = new TabulatorFull("#tabulator", {
data: tableData,
columns: tableColumns
});
resizeColumnsMod = tabulator.module("resizeColumns");
return new Promise((resolve) => {
tabulator.on("tableBuilt", () => {
resolve();
});
});
});
afterEach(() => {
tabulator.destroy();
document.getElementById("tabulator")?.remove();
});
it("should initialize ResizeColumns module", () => {
expect(resizeColumnsMod).toBeDefined();
expect(typeof resizeColumnsMod.initializeColumn).toBe('function');
expect(typeof resizeColumnsMod.resize).toBe('function');
expect(typeof resizeColumnsMod._checkResizability).toBe('function');
});
it("should check column resizability correctly", () => {
// Create mock columns with resizable definition
const resizableColumn = { definition: { resizable: true } };
const nonResizableColumn = { definition: { resizable: false } };
expect(resizeColumnsMod._checkResizability(resizableColumn)).toBe(true);
expect(resizeColumnsMod._checkResizability(nonResizableColumn)).toBe(false);
});
it("should dispatch events when columns are resized", () => {
// Create a spy for dispatchExternal
const dispatchSpy = jest.spyOn(resizeColumnsMod, 'dispatchExternal');
// Create mock column with needed methods
const mockColumn = {
getComponent: () => ({}),
getWidth: () => 100,
setWidth: jest.fn(),
modules: {}
};
// Set initial values
resizeColumnsMod.startX = 100;
resizeColumnsMod.startWidth = 100;
resizeColumnsMod.latestX = 100;
// Call resize method
resizeColumnsMod.resize({ clientX: 150 }, mockColumn);
// Verify column width was set
expect(mockColumn.setWidth).toHaveBeenCalledWith(150); // 100 + (150 - 100)
// Clean up
dispatchSpy.mockRestore();
});
it("should calculate guide position correctly", () => {
// Enable guide
tabulator.options.resizableColumnGuide = true;
// Create mock objects
const mockColumn = {
element: {
getBoundingClientRect: () => ({ left: 50 })
},
minWidth: 30,
maxWidth: 300
};
const mockHandle = {
getBoundingClientRect: () => ({ x: 120 })
};
// Mock table element
resizeColumnsMod.table = {
element: {
getBoundingClientRect: () => ({ x: 20 }),
classList: { add: jest.fn() }
}
};
// Set initial values
resizeColumnsMod.startX = 100;
// Calculate guide position
const mockEvent = { clientX: 150 }; // 50px difference
const position = resizeColumnsMod.calcGuidePosition(mockEvent, mockColumn, mockHandle);
// Guide position should be handle position + mouse movement
// handleX: 120 - 20 = 100, mouseDiff: 150 - 100 = 50, total: 150
expect(position).toBe(150);
});
it("should set column width in resize method", () => {
// Create a mock column with width methods
const mockColumn = {
width: 100,
minWidth: 50,
maxWidth: 200,
setWidth: jest.fn(),
getWidth: () => 100,
modules: {}
};
// Create mock for the calculation without using resize() directly
const startWidth = 100;
const startX = 100;
const clientX = 120; // 20px difference
const startDiff = clientX - startX;
// Calculate expected new width
const expectedWidth = startWidth + startDiff; // 100 + 20 = 120
// Directly test the column width setting behavior
mockColumn.setWidth(expectedWidth);
// Verify column width was set correctly
expect(mockColumn.setWidth).toHaveBeenCalledWith(120);
});
});

View File

@@ -0,0 +1,131 @@
import TabulatorFull from "../../../src/js/core/TabulatorFull";
import ResizeRows from "../../../src/js/modules/ResizeRows/ResizeRows";
describe("ResizeRows module", () => {
/** @type {TabulatorFull} */
let tabulator;
/** @type {ResizeRows} */
let resizeRowsMod;
let tableData = [
{ id: 1, name: "John", active: true },
{ id: 2, name: "Jane", active: false },
{ id: 3, name: "Bob", active: true }
];
let tableColumns = [
{ title: "ID", field: "id" },
{ title: "Name", field: "name" },
{ title: "Active", field: "active" }
];
beforeEach(async () => {
const el = document.createElement("div");
el.id = "tabulator";
document.body.appendChild(el);
tabulator = new TabulatorFull("#tabulator", {
data: tableData,
columns: tableColumns,
resizableRows: true
});
resizeRowsMod = tabulator.module("resizeRows");
return new Promise((resolve) => {
tabulator.on("tableBuilt", () => {
resolve();
});
});
});
afterEach(() => {
// Manually clean up DOM element instead of destroying tabulator,
// which can cause issues with missing event listeners
if (document.getElementById("tabulator")) {
document.getElementById("tabulator").remove();
}
});
it("should initialize resizable rows module", () => {
// Check if module is properly initialized
expect(resizeRowsMod).toBeDefined();
expect(typeof resizeRowsMod.initializeRow).toBe('function');
expect(typeof resizeRowsMod.resize).toBe('function');
expect(typeof resizeRowsMod._mouseDown).toBe('function');
});
it("should dispatch external events when row is resized", () => {
// Mock the row and dispatchExternal method
const mockRow = {
getComponent: () => ({}),
getElement: () => document.createElement('div'),
getHeight: () => 30,
setHeight: jest.fn()
};
// Create a spy for the dispatchExternal method
const dispatchSpy = jest.spyOn(resizeRowsMod, 'dispatchExternal');
// Mock the event and handle
const mockEvent = { screenY: 100 };
const mockHandle = document.createElement('div');
// Trigger resize directly
resizeRowsMod.startY = 80;
resizeRowsMod.startHeight = 30;
resizeRowsMod.resize(mockEvent, mockRow);
// Clean up
dispatchSpy.mockRestore();
// Verify the row's height was set
expect(mockRow.setHeight).toHaveBeenCalledWith(50); // 30 + (100 - 80)
});
it("should calculate guide position correctly", () => {
// Enable resizableRowGuide
tabulator.options.resizableRowGuide = true;
// Mock row, handle, element and objects
const mockRow = {
element: {
getBoundingClientRect: () => ({ top: 100 })
}
};
const mockHandle = {
getBoundingClientRect: () => ({ y: 120 })
};
// Set table element
resizeRowsMod.table.element = {
getBoundingClientRect: () => ({ y: 50 })
};
// Set initial values
resizeRowsMod.startY = 150;
// Calculate guide position
const mockEvent = { screenY: 180 }; // 30px difference from startY
const position = resizeRowsMod.calcGuidePosition(mockEvent, mockRow, mockHandle);
// Check that position is correct
// handleY (120 - 50 = 70) + mouseDiff (180 - 150 = 30) = 100
expect(position).toBe(100);
});
it("should calculate new height correctly in resize method", () => {
// Mock row with setHeight method
const mockRow = {
setHeight: jest.fn()
};
// Set initial values
resizeRowsMod.startHeight = 30;
resizeRowsMod.startY = 100;
// Call resize with mock event
const mockEvent = { screenY: 120 }; // 20px difference
resizeRowsMod.resize(mockEvent, mockRow);
// Check setHeight was called with expected value
expect(mockRow.setHeight).toHaveBeenCalledWith(50); // 30 + (120 - 100)
});
});

View File

@@ -0,0 +1,138 @@
import TabulatorFull from "../../../src/js/core/TabulatorFull";
import ResizeTable from "../../../src/js/modules/ResizeTable/ResizeTable";
describe("ResizeTable module", () => {
/** @type {TabulatorFull} */
let tabulator;
/** @type {ResizeTable} */
let resizeTableMod;
let tableData = [
{ id: 1, name: "John", active: true },
{ id: 2, name: "Jane", active: false },
{ id: 3, name: "Bob", active: true }
];
let tableColumns = [
{ title: "ID", field: "id" },
{ title: "Name", field: "name" },
{ title: "Active", field: "active" }
];
beforeEach(async () => {
const el = document.createElement("div");
el.id = "tabulator";
document.body.appendChild(el);
tabulator = new TabulatorFull("#tabulator", {
data: tableData,
columns: tableColumns,
autoResize: true
});
resizeTableMod = tabulator.module("resizeTable");
return new Promise((resolve) => {
tabulator.on("tableBuilt", () => {
resolve();
});
});
});
afterEach(() => {
tabulator.destroy();
document.getElementById("tabulator")?.remove();
});
it("should initialize with proper default values", () => {
// Initialize may not have completed yet or test environment doesn't have proper dimensions
expect(resizeTableMod.tableHeight).toBeDefined();
expect(resizeTableMod.tableWidth).toBeDefined();
expect(resizeTableMod.containerHeight).toBeDefined();
expect(resizeTableMod.containerWidth).toBeDefined();
expect(resizeTableMod.autoResize).toBeDefined();
});
it("should have method to clear bindings", () => {
// Verify the clearBindings method exists
expect(typeof resizeTableMod.clearBindings).toBe('function');
// Test if resizeObserver and visibilityObserver are properly defined
if (resizeTableMod.resizeObserver) {
expect(typeof resizeTableMod.resizeObserver.unobserve).toBe('function');
}
if (resizeTableMod.visibilityObserver) {
expect(typeof resizeTableMod.visibilityObserver.unobserve).toBe('function');
}
});
it("should handle table redraw", () => {
// Mock redraw method to detect if it's called
const originalRedraw = tabulator.rowManager.redraw;
let wasCalled = false;
tabulator.rowManager.redraw = function() {
wasCalled = true;
return originalRedraw.apply(this, arguments);
};
// Call tableResized
resizeTableMod.tableResized();
// Restore original function
tabulator.rowManager.redraw = originalRedraw;
// Test if our mock was called
expect(wasCalled).toBe(true);
});
it("should not redraw if not visible", () => {
// Set visible to false
resizeTableMod.visible = false;
resizeTableMod.initialized = true;
// Spy on table redraw
const redrawSpy = jest.spyOn(tabulator, 'redraw');
// Call redrawTable
resizeTableMod.redrawTable();
// Check that redraw was not called
expect(redrawSpy).not.toHaveBeenCalled();
// Clean up
redrawSpy.mockRestore();
});
it('should determine whether to redraw table or not using latest visibility state from batched IntersectionObserver entries', () => {
let observedElements = [];
let observerCallback;
let redrawTableCalled = false;
// mock intersectionObserver
global.IntersectionObserver = jest.fn((callback) => {
observerCallback = callback;
return {
observe: jest.fn((el) => observedElements.push(el)),
unobserve: jest.fn(),
disconnect: jest.fn()
};
});
// mock redrawTable
resizeTableMod.redrawTable = function() {
redrawTableCalled = true;
};
resizeTableMod.initializeVisibilityObserver();
// reproduce IntersectionObserver being called when table rendered for the first time
observerCallback([
{ target: tabulator.element, isIntersecting: true },
]);
observerCallback([
{ target: tabulator.element, isIntersecting: false },
{ target: tabulator.element, isIntersecting: true },
]);
expect(redrawTableCalled).toBe(true);
});
});

View File

@@ -0,0 +1,147 @@
import TabulatorFull from "../../../src/js/core/TabulatorFull";
import ResponsiveLayout from "../../../src/js/modules/ResponsiveLayout/ResponsiveLayout";
describe("ResponsiveLayout module", () => {
/** @type {TabulatorFull} */
let tabulator;
/** @type {ResponsiveLayout} */
let responsiveLayoutMod;
let tableData = [
{ id: 1, name: "John", age: 30, gender: "Male", city: "New York" },
{ id: 2, name: "Jane", age: 25, gender: "Female", city: "Boston" },
{ id: 3, name: "Bob", age: 40, gender: "Male", city: "Chicago" }
];
let tableColumns = [
{ title: "ID", field: "id", responsive: 0 },
{ title: "Name", field: "name", responsive: 1 },
{ title: "Age", field: "age", responsive: 2 },
{ title: "Gender", field: "gender", responsive: 3 },
{ title: "City", field: "city", responsive: 4 }
];
beforeEach(async () => {
const el = document.createElement("div");
el.style.width = "500px"; // Set fixed width for testing
el.id = "tabulator";
document.body.appendChild(el);
});
afterEach(() => {
// Manually remove element without calling destroy() to avoid event listener errors
if (document.getElementById("tabulator")) {
document.getElementById("tabulator").remove();
}
});
describe("collapse mode", () => {
beforeEach(async () => {
tabulator = new TabulatorFull("#tabulator", {
data: tableData,
columns: tableColumns,
responsiveLayout: "collapse"
});
responsiveLayoutMod = tabulator.module("responsiveLayout");
return new Promise((resolve) => {
tabulator.on("tableBuilt", () => {
resolve();
});
});
});
it("should initialize with collapse mode", () => {
expect(responsiveLayoutMod.mode).toBe("collapse");
expect(Array.isArray(responsiveLayoutMod.columns)).toBe(true);
});
it("should have methods for generating collapsed content", () => {
// Verify methods exist
expect(typeof responsiveLayoutMod.generateCollapsedContent).toBe('function');
expect(typeof responsiveLayoutMod.generateCollapsedRowContent).toBe('function');
expect(typeof responsiveLayoutMod.generateCollapsedRowData).toBe('function');
expect(typeof responsiveLayoutMod.formatCollapsedData).toBe('function');
});
it("should add column to hiddenColumns when hidden", () => {
const column = tabulator.columnManager.findColumn("gender");
const initialHiddenCount = responsiveLayoutMod.hiddenColumns.length;
// Hide column
responsiveLayoutMod.hideColumn(column);
// Check if column was added to hiddenColumns
expect(responsiveLayoutMod.hiddenColumns.length).toBe(initialHiddenCount + 1);
expect(responsiveLayoutMod.hiddenColumns.includes(column)).toBe(true);
// Check if column is actually hidden
expect(column.visible).toBe(false);
});
it("should remove column from hiddenColumns when shown", () => {
// First hide a column
const column = tabulator.columnManager.findColumn("city");
responsiveLayoutMod.hideColumn(column);
// Then show it
responsiveLayoutMod.showColumn(column);
// Check if column was removed from hiddenColumns
expect(responsiveLayoutMod.hiddenColumns.includes(column)).toBe(false);
// Check if column is actually visible
expect(column.visible).toBe(true);
});
});
describe("hide mode", () => {
beforeEach(async () => {
tabulator = new TabulatorFull("#tabulator", {
data: tableData,
columns: tableColumns,
responsiveLayout: "hide"
});
responsiveLayoutMod = tabulator.module("responsiveLayout");
return new Promise((resolve) => {
tabulator.on("tableBuilt", () => {
resolve();
});
});
});
it("should initialize with hide mode", () => {
expect(responsiveLayoutMod.mode).toBe("hide");
expect(Array.isArray(responsiveLayoutMod.columns)).toBe(true);
});
it("should have update method for responsivity", () => {
// Verify update method exists
expect(typeof responsiveLayoutMod.update).toBe('function');
// Mock required methods
responsiveLayoutMod.hideColumn = jest.fn();
responsiveLayoutMod.showColumn = jest.fn();
// Mock a column with responsive order
const mockColumn = { visible: true };
responsiveLayoutMod.columns = [mockColumn];
responsiveLayoutMod.index = 0;
// Force an update with a negative width difference (simulate too narrow table)
tabulator.modules.layout = { getMode: () => 'fitData' };
tabulator.columnManager.getWidth = () => 1000;
tabulator.columnManager.element = { clientWidth: 800 };
tabulator.element = { clientWidth: 800 };
tabulator.options.headerVisible = true;
tabulator.rowManager = { activeRowsCount: 1, renderEmptyScroll: jest.fn() };
// Run update
responsiveLayoutMod.update();
// Verify hideColumn was called when table is too narrow
expect(responsiveLayoutMod.hideColumn).toHaveBeenCalled();
});
});
});

View File

@@ -0,0 +1,148 @@
import TabulatorFull from "../../../src/js/core/TabulatorFull";
import SelectRange from "../../../src/js/modules/SelectRange/SelectRange";
describe("SelectRange module", () => {
/** @type {TabulatorFull} */
let tabulator;
/** @type {SelectRange} */
let selectRangeMod;
let tableData = [
{ id: 1, name: "John", age: 30, position: "Manager" },
{ id: 2, name: "Jane", age: 25, position: "Developer" },
{ id: 3, name: "Bob", age: 35, position: "Designer" }
];
let tableColumns = [
{ title: "ID", field: "id" },
{ title: "Name", field: "name" },
{ title: "Age", field: "age" },
{ title: "Position", field: "position" }
];
beforeEach(async () => {
const el = document.createElement("div");
el.id = "tabulator";
document.body.appendChild(el);
tabulator = new TabulatorFull("#tabulator", {
data: tableData,
columns: tableColumns,
selectableRange: true,
});
selectRangeMod = tabulator.module("selectRange");
return new Promise((resolve) => {
tabulator.on("tableBuilt", () => {
resolve();
});
});
});
afterEach(() => {
tabulator.destroy();
document.getElementById("tabulator")?.remove();
});
it("should have one range initially at the top left", () => {
const range = selectRangeMod.getRanges()[0]._range;
expect(range.top).toBe(0);
expect(range.left).toBe(0);
expect(range.bottom).toBe(0);
expect(range.right).toBe(0);
});
it("should add a new range when addRange is called", () => {
const initialRangesCount = selectRangeMod.getRanges().length;
selectRangeMod.addRange();
expect(selectRangeMod.getRanges().length).toBe(initialRangesCount + 1);
});
it("should reset ranges when resetRanges is called", () => {
// Add multiple ranges
selectRangeMod.addRange();
selectRangeMod.addRange();
// Reset ranges
const resetRange = selectRangeMod.resetRanges();
// Should have only one range after reset
expect(selectRangeMod.getRanges().length).toBe(1);
expect(resetRange).toBe(selectRangeMod.getRanges()[0]._range);
});
it("should have correct structure in RangeComponent", () => {
const rangeComponent = selectRangeMod.getRanges()[0];
// Test component properties
expect(rangeComponent._range).toBeDefined();
expect(typeof rangeComponent.getElement).toBe("function");
expect(typeof rangeComponent.getData).toBe("function");
expect(typeof rangeComponent.getCells).toBe("function");
expect(typeof rangeComponent.getRows).toBe("function");
expect(typeof rangeComponent.getColumns).toBe("function");
});
it("should have correct min/max values", () => {
const range = selectRangeMod.getRanges()[0]._range;
// The min/max values should match the start/end values after they're set
range.setStart(1, 2);
range.setEnd(3, 4);
expect(range.top).toBe(1);
expect(range.bottom).toBe(3);
expect(range.left).toBe(2);
expect(range.right).toBe(4);
});
it("should handle Range setStart and setEnd", () => {
const range = selectRangeMod.getRanges()[0]._range;
// Initial values
expect(range.start.row).toBeUndefined();
expect(range.start.col).toBeUndefined();
expect(range.end.row).toBeUndefined();
expect(range.end.col).toBeUndefined();
// Set start
range.setStart(1, 2);
expect(range.start.row).toBe(1);
expect(range.start.col).toBe(2);
// Set end
range.setEnd(3, 4);
expect(range.end.row).toBe(3);
expect(range.end.col).toBe(4);
});
it("should detect overlaps correctly", () => {
const range = selectRangeMod.getRanges()[0]._range;
// Setup range bounds
range.top = 1;
range.bottom = 3;
range.left = 2;
range.right = 4;
// Test overlapping case
expect(range.overlaps(1, 1, 5, 5)).toBe(true);
expect(range.overlaps(3, 3, 5, 5)).toBe(true);
// Test non-overlapping cases
expect(range.overlaps(5, 5, 7, 7)).toBe(false);
expect(range.overlaps(0, 0, 0, 0)).toBe(false);
});
it("should handle destroyedGuard", () => {
const range = selectRangeMod.getRanges()[0]._range;
// Should return true when not destroyed
expect(range.destroyedGuard("testFunction")).toBe(true);
// Test warning message when destroyed
const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation();
range.destroyed = true;
expect(range.destroyedGuard("testFunction")).toBe(false);
expect(consoleWarnSpy).toHaveBeenCalled();
// Clean up
consoleWarnSpy.mockRestore();
});
});

View File

@@ -0,0 +1,425 @@
import SelectRow from "../../../src/js/modules/SelectRow/SelectRow";
describe("SelectRow module", () => {
/** @type {SelectRow} */
let selectRowMod;
let mockTable;
let mockRows;
beforeEach(() => {
// Create mock optionsList
const mockOptionsList = {
register: jest.fn(),
generate: jest.fn().mockImplementation((defaults, options) => {
return { ...defaults, ...options };
})
};
// Create mock eventBus
const mockEventBus = {
subscribe: jest.fn(),
unsubscribe: jest.fn(),
subscribed: jest.fn(),
subscriptionChange: jest.fn(),
dispatch: jest.fn(),
chain: jest.fn(),
confirm: jest.fn()
};
// Create mock externalEvents
const mockExternalEvents = {
dispatch: jest.fn(),
subscribed: jest.fn(),
subscriptionChange: jest.fn()
};
// Create mock rows
mockRows = [
createMockRow(1),
createMockRow(2),
createMockRow(3)
];
// Create mock row manager
const mockRowManager = {
rows: mockRows,
findRow: jest.fn((id) => {
if (typeof id === 'number') {
return mockRows.find(row => row.data.id === id);
} else if (typeof id === 'object' && id !== null) {
return id;
}
return null;
}),
getRows: jest.fn(() => mockRows),
getDisplayRows: jest.fn(() => mockRows),
getDisplayRowIndex: jest.fn((row) => {
return mockRows.indexOf(row);
})
};
// Create mock modules object
const mockModules = {
dataTree: {
getChildren: jest.fn(() => [])
}
};
// Create a simplified mock of the table
mockTable = {
options: {
selectableRows: "highlight",
selectableRowsRangeMode: "drag",
selectableRowsRollingSelection: true,
selectableRowsPersistence: true,
selectableRowsCheck: jest.fn().mockReturnValue(true),
dataTreeSelectPropagate: false
},
rowManager: mockRowManager,
columnManager: {
optionsList: mockOptionsList
},
optionsList: mockOptionsList,
eventBus: mockEventBus,
externalEvents: mockExternalEvents,
_clearSelection: jest.fn(),
registerTableFunction: jest.fn(),
initGuard: jest.fn(),
modExists: jest.fn(() => true),
modules: mockModules
};
// Mock methods in the SelectRow prototype
jest.spyOn(SelectRow.prototype, 'registerTableOption').mockImplementation(function(key, value) {
this.table.optionsList.register(key, value);
});
jest.spyOn(SelectRow.prototype, 'registerTableFunction').mockImplementation(function(name, callback) {
this.table.registerTableFunction(name, callback);
});
jest.spyOn(SelectRow.prototype, 'registerComponentFunction').mockImplementation(function(component, name, callback) {
// Mock component registration
});
jest.spyOn(SelectRow.prototype, 'subscribe').mockImplementation(function(key, callback) {
return this.table.eventBus.subscribe(key, callback);
});
jest.spyOn(SelectRow.prototype, 'dispatchExternal').mockImplementation(function(event, ...args) {
this.table.externalEvents.dispatch(event, ...args);
});
// Create an instance of the SelectRow module with the mock table
selectRowMod = new SelectRow(mockTable);
// Initialize the module
selectRowMod.initialize();
});
afterEach(() => {
jest.clearAllMocks();
jest.restoreAllMocks();
});
// Helper function to create mock row objects
function createMockRow(id) {
const element = document.createElement('div');
const mockComponent = {
getData: jest.fn(() => ({ id: id, name: `Row ${id}` }))
};
const row = {
type: "row",
data: { id: id, name: `Row ${id}` },
modules: {
select: {
selected: false
}
},
element: element,
_row: {
modules: {}
},
getElement: jest.fn(() => element),
getComponent: jest.fn(() => mockComponent),
getData: jest.fn(() => ({ id: id, name: `Row ${id}` }))
};
return row;
}
it("should initialize with empty selection", () => {
// Check initial state
expect(selectRowMod.selectedRows).toEqual([]);
expect(selectRowMod.selecting).toBe(false);
expect(selectRowMod.lastClickedRow).toBe(false);
expect(selectRowMod.selectPrev).toEqual([]);
});
it("should register required table options", () => {
// Verify that the correct options were registered
expect(mockTable.optionsList.register).toHaveBeenCalledWith("selectableRows", "highlight");
expect(mockTable.optionsList.register).toHaveBeenCalledWith("selectableRowsRangeMode", "drag");
expect(mockTable.optionsList.register).toHaveBeenCalledWith("selectableRowsRollingSelection", true);
expect(mockTable.optionsList.register).toHaveBeenCalledWith("selectableRowsPersistence", true);
expect(mockTable.optionsList.register).toHaveBeenCalledWith("selectableRowsCheck", expect.any(Function));
});
it("should register required table functions", () => {
// Verify that the correct table functions were registered
expect(mockTable.registerTableFunction).toHaveBeenCalledWith("selectRow", expect.any(Function));
expect(mockTable.registerTableFunction).toHaveBeenCalledWith("deselectRow", expect.any(Function));
expect(mockTable.registerTableFunction).toHaveBeenCalledWith("toggleSelectRow", expect.any(Function));
expect(mockTable.registerTableFunction).toHaveBeenCalledWith("getSelectedRows", expect.any(Function));
expect(mockTable.registerTableFunction).toHaveBeenCalledWith("getSelectedData", expect.any(Function));
});
it("should subscribe to row events when selectableRows is not false", () => {
// Verify that the correct events were subscribed to
expect(mockTable.eventBus.subscribe).toHaveBeenCalledWith("row-init", expect.any(Function));
expect(mockTable.eventBus.subscribe).toHaveBeenCalledWith("row-deleting", expect.any(Function));
expect(mockTable.eventBus.subscribe).toHaveBeenCalledWith("rows-wipe", expect.any(Function));
expect(mockTable.eventBus.subscribe).toHaveBeenCalledWith("rows-retrieve", expect.any(Function));
});
it("should check row selectability", () => {
const row = mockRows[0];
// Test the selectableRowsCheck function
const result = selectRowMod.checkRowSelectability(row);
// Check if the function was called with the row component
expect(mockTable.options.selectableRowsCheck).toHaveBeenCalled();
// Verify the result is as expected
expect(result).toBe(true);
// Test with non-row object
const nonRowResult = selectRowMod.checkRowSelectability({type: "header"});
expect(nonRowResult).toBe(false);
});
it("should be able to directly select and deselect a row", () => {
const row = mockRows[0];
// Mock findRow to return the actual row
mockTable.rowManager.findRow.mockReturnValueOnce(row);
// Select the row
selectRowMod._selectRow(row);
// Verify row is selected
expect(row.modules.select.selected).toBe(true);
expect(selectRowMod.selectedRows).toContain(row);
expect(row.getElement().classList.contains("tabulator-selected")).toBe(true);
// Verify event was dispatched
expect(mockTable.externalEvents.dispatch).toHaveBeenCalledWith("rowSelected", row.getComponent());
// Clear mocks for the next test
jest.clearAllMocks();
mockTable.rowManager.findRow.mockReturnValueOnce(row);
// Deselect the row
selectRowMod._deselectRow(row);
// Verify row is deselected
expect(row.modules.select.selected).toBe(false);
expect(selectRowMod.selectedRows).not.toContain(row);
expect(row.getElement().classList.contains("tabulator-selected")).toBe(false);
// Verify event was dispatched
expect(mockTable.externalEvents.dispatch).toHaveBeenCalledWith("rowDeselected", row.getComponent());
});
it("should toggle row selection", () => {
const row = mockRows[0];
// Set up initial state
row.modules.select.selected = false;
selectRowMod.selectedRows = [];
// Mock row manager to return the row
mockTable.rowManager.findRow.mockReturnValue(row);
// Toggle selection ON
selectRowMod.toggleRow(row);
// Verify row is now selected
expect(row.modules.select.selected).toBe(true);
expect(selectRowMod.selectedRows).toContain(row);
// Toggle selection OFF
selectRowMod.toggleRow(row);
// Verify row is now deselected
expect(row.modules.select.selected).toBe(false);
expect(selectRowMod.selectedRows).not.toContain(row);
});
it("should select rows by ID", () => {
const row = mockRows[1]; // row with ID 2
// Mock row manager's findRow to return our row
mockTable.rowManager.findRow.mockReturnValue(row);
// Select row with ID 2
selectRowMod.selectRows(2);
// Verify the row was selected
expect(row.modules.select.selected).toBe(true);
expect(selectRowMod.selectedRows).toContain(row);
// Verify external event was dispatched
expect(mockTable.externalEvents.dispatch).toHaveBeenCalledWith("rowSelected", row.getComponent());
});
it("should select multiple rows as an array", () => {
// Select multiple rows
mockTable.rowManager.findRow
.mockReturnValueOnce(mockRows[0])
.mockReturnValueOnce(mockRows[2]);
selectRowMod.selectRows([mockRows[0], mockRows[2]]);
// Verify the rows were selected
expect(mockRows[0].modules.select.selected).toBe(true);
expect(mockRows[2].modules.select.selected).toBe(true);
expect(selectRowMod.selectedRows).toContain(mockRows[0]);
expect(selectRowMod.selectedRows).toContain(mockRows[2]);
// Verify the elements have the selected class
expect(mockRows[0].getElement().classList.contains("tabulator-selected")).toBe(true);
expect(mockRows[2].getElement().classList.contains("tabulator-selected")).toBe(true);
});
it("should report selected data correctly", () => {
// First select some rows
mockTable.rowManager.findRow
.mockReturnValueOnce(mockRows[0])
.mockReturnValueOnce(mockRows[2]);
selectRowMod.selectRows([mockRows[0], mockRows[2]]);
selectRowMod.selectedRows = [mockRows[0], mockRows[2]];
// Get the selected data
const data = selectRowMod.getSelectedData();
// Verify the correct data is returned
expect(data.length).toBe(2);
expect(data[0]).toEqual({id: 1, name: "Row 1"});
expect(data[1]).toEqual({id: 3, name: "Row 3"});
});
it("should report selected row components correctly", () => {
// First select some rows
mockTable.rowManager.findRow
.mockReturnValueOnce(mockRows[0])
.mockReturnValueOnce(mockRows[2]);
selectRowMod.selectRows([mockRows[0], mockRows[2]]);
selectRowMod.selectedRows = [mockRows[0], mockRows[2]];
// Reset mocks for clean expectations
jest.clearAllMocks();
// Get the selected rows
const rows = selectRowMod.getSelectedRows();
// Verify getComponent was called for each row
expect(mockRows[0].getComponent).toHaveBeenCalled();
expect(mockRows[2].getComponent).toHaveBeenCalled();
// Verify the correct number of components
expect(rows.length).toBe(2);
});
it("should handle row deletion", () => {
// First select all rows
mockRows.forEach(row => {
mockTable.rowManager.findRow.mockReturnValueOnce(row);
});
selectRowMod.selectRows(mockRows);
selectRowMod.selectedRows = [...mockRows];
// Reset mocks for clean expectations
jest.clearAllMocks();
mockTable.rowManager.findRow.mockReturnValueOnce(mockRows[1]);
// Trigger row deletion
selectRowMod.rowDeleted(mockRows[1]);
// Verify the row was deselected
expect(selectRowMod.selectedRows).not.toContain(mockRows[1]);
});
it("should limit selected rows based on selectableRows option", () => {
// Set maximum of 2 selectable rows
mockTable.options.selectableRows = 2;
// Start with a clean slate
selectRowMod.selectedRows = [];
// Mock the find function to return our mock rows
mockTable.rowManager.findRow
.mockReturnValueOnce(mockRows[0])
.mockReturnValueOnce(mockRows[1])
.mockReturnValueOnce(mockRows[2]);
// Select first two rows
selectRowMod.selectRows([mockRows[0], mockRows[1]]);
// Verify only 2 rows were selected
expect(selectRowMod.selectedRows.length).toBe(2);
// Clear the mock calls and reset mock implementation
jest.clearAllMocks();
// Verify selectable rows limit is enforced
mockTable.options.selectableRowsRollingSelection = false;
mockTable.rowManager.findRow.mockReturnValueOnce(mockRows[2]);
// Try to select a third row when limit is 2 and rolling selection is off
const result = selectRowMod._selectRow(mockRows[2], false, false);
// Should return false when limit is reached and rolling selection is disabled
expect(result).toBe(false);
});
it("should handle rolling selection when max rows is reached", () => {
// Set maximum of 2 selectable rows with rolling selection
mockTable.options.selectableRows = 2;
mockTable.options.selectableRowsRollingSelection = true;
// Start with a clean slate
selectRowMod.selectedRows = [];
// First select two rows
mockTable.rowManager.findRow
.mockReturnValueOnce(mockRows[0])
.mockReturnValueOnce(mockRows[1]);
selectRowMod.selectRows([mockRows[0], mockRows[1]]);
// Verify the first two rows are selected
expect(selectRowMod.selectedRows.length).toBe(2);
expect(selectRowMod.selectedRows).toContain(mockRows[0]);
expect(selectRowMod.selectedRows).toContain(mockRows[1]);
// Reset mocks
jest.clearAllMocks();
// Directly modify selectedRows to simulate the module's behavior
// (we're bypassing some of the module's internal logic to focus on the test)
selectRowMod.selectedRows = [mockRows[1], mockRows[2]];
// Verify the expected outcome with rolling selection:
// First row is deselected, second and third are selected
expect(selectRowMod.selectedRows.length).toBe(2);
expect(selectRowMod.selectedRows).not.toContain(mockRows[0]);
expect(selectRowMod.selectedRows).toContain(mockRows[1]);
expect(selectRowMod.selectedRows).toContain(mockRows[2]);
});
});

View File

@@ -0,0 +1,157 @@
import TabulatorFull from "../../../src/js/core/TabulatorFull";
import Sort from "../../../src/js/modules/Sort/Sort";
describe("Sort module", () => {
/** @type {TabulatorFull} */
let tabulator;
/** @type {Sort} */
let sortMod;
let tableData = [
{ id: 2, name: "Jane", age: 25, score: 85.2 },
{ id: 1, name: "John", age: 30, score: 78.4 },
{ id: 3, name: "Bob", age: 35, score: 92.1 }
];
let tableColumns = [
{ title: "ID", field: "id", sorter: "number" },
{ title: "Name", field: "name", sorter: "string" },
{ title: "Age", field: "age", sorter: "number" },
{ title: "Score", field: "score", sorter: "number" }
];
beforeEach(async () => {
const el = document.createElement("div");
el.id = "tabulator";
document.body.appendChild(el);
tabulator = new TabulatorFull("#tabulator", {
data: tableData,
columns: tableColumns
});
sortMod = tabulator.module("sort");
return new Promise((resolve) => {
tabulator.on("tableBuilt", () => {
resolve();
});
});
});
afterEach(() => {
tabulator.destroy();
document.getElementById("tabulator")?.remove();
});
it("should initialize with no sorting", () => {
const sorters = sortMod.getSort();
expect(sorters.length).toBe(0);
});
it("should set a sort on a single column", () => {
const column = tabulator.columnManager.findColumn("name");
sortMod.setSort(column, "asc");
const sorters = sortMod.getSort();
expect(sorters.length).toBe(1);
expect(sorters[0].field).toBe("name");
expect(sorters[0].dir).toBe("asc");
});
it("should clear sort when calling clearSort", () => {
const column = tabulator.columnManager.findColumn("name");
sortMod.setSort(column, "asc");
expect(sortMod.getSort().length).toBe(1);
sortMod.clear();
expect(sortMod.getSort().length).toBe(0);
});
it("should set multiple column sorting", () => {
const nameColumn = tabulator.columnManager.findColumn("name");
const ageColumn = tabulator.columnManager.findColumn("age");
sortMod.setSort([
{ column: nameColumn, dir: "asc" },
{ column: ageColumn, dir: "desc" }
]);
const sorters = sortMod.getSort();
expect(sorters.length).toBe(2);
expect(sorters[0].field).toBe("name");
expect(sorters[0].dir).toBe("asc");
expect(sorters[1].field).toBe("age");
expect(sorters[1].dir).toBe("desc");
});
it("should sort data by string column", () => {
// Apply sort
const column = tabulator.columnManager.findColumn("name");
sortMod.setSort(column, "asc");
// Trigger sort
const sorted = sortMod.sort(tabulator.rowManager.activeRows);
// Check order is correct
expect(sorted[0].data.name).toBe("Bob");
expect(sorted[1].data.name).toBe("Jane");
expect(sorted[2].data.name).toBe("John");
});
it("should sort data by number column", () => {
// Apply sort
const column = tabulator.columnManager.findColumn("age");
sortMod.setSort(column, "asc");
// Trigger sort
const sorted = sortMod.sort(tabulator.rowManager.activeRows);
// Check order is correct
expect(sorted[0].data.age).toBe(25);
expect(sorted[1].data.age).toBe(30);
expect(sorted[2].data.age).toBe(35);
});
it("should sort data by multiple columns", () => {
// Create test data with same ages but different names
const testData = [
{ id: 1, name: "John", age: 30 },
{ id: 2, name: "Jane", age: 30 },
{ id: 3, name: "Bob", age: 25 }
];
// Update table data
tabulator.setData(testData);
// Apply multi-column sort: first by age ascending, then by name ascending
const ageColumn = tabulator.columnManager.findColumn("age");
const nameColumn = tabulator.columnManager.findColumn("name");
sortMod.setSort([
{ column: ageColumn, dir: "asc" },
{ column: nameColumn, dir: "asc" }
]);
// Trigger sort
const sorted = sortMod.sort(tabulator.rowManager.activeRows);
// First should be Bob (age 25)
expect(sorted[0].data.name).toBe("Bob");
// Then Jane (age 30) before John (age 30) due to alphabetical sort
expect(sorted[1].data.name).toBe("Jane");
expect(sorted[2].data.name).toBe("John");
});
it("should reverse sort direction", () => {
// Apply sort
const column = tabulator.columnManager.findColumn("name");
sortMod.setSort(column, "desc");
// Trigger sort
const sorted = sortMod.sort(tabulator.rowManager.activeRows);
// Check order is reversed
expect(sorted[0].data.name).toBe("John");
expect(sorted[1].data.name).toBe("Jane");
expect(sorted[2].data.name).toBe("Bob");
});
});

View File

@@ -0,0 +1,200 @@
import TabulatorFull from "../../../src/js/core/TabulatorFull";
import Spreadsheet from "../../../src/js/modules/Spreadsheet/Spreadsheet";
import SheetComponent from "../../../src/js/modules/Spreadsheet/SheetComponent";
describe("Spreadsheet module", () => {
/** @type {TabulatorFull} */
let tabulator;
/** @type {Spreadsheet} */
let spreadsheetMod;
beforeEach(async () => {
const el = document.createElement("div");
el.id = "tabulator";
document.body.appendChild(el);
});
afterEach(() => {
if (tabulator) {
tabulator.destroy();
}
document.getElementById("tabulator")?.remove();
});
describe("basic functionality", () => {
beforeEach(async () => {
tabulator = new TabulatorFull("#tabulator", {
spreadsheet: true,
spreadsheetRows: 10,
spreadsheetColumns: 10
});
spreadsheetMod = tabulator.module("spreadsheet");
return new Promise((resolve) => {
tabulator.on("tableBuilt", () => {
resolve();
});
});
});
it("should initialize properly", () => {
expect(spreadsheetMod).toBeDefined();
expect(Array.isArray(spreadsheetMod.sheets)).toBe(true);
// In test environment, sheets might not be auto-created
// until data is explicitly loaded
});
it("should add new sheets", () => {
const initialSheetCount = spreadsheetMod.sheets.length;
// Add a new sheet
const sheetComponent = spreadsheetMod.addSheet({
title: "Test Sheet",
rows: 5,
columns: 5
});
// Check sheet was added
expect(spreadsheetMod.sheets.length).toBe(initialSheetCount + 1);
expect(sheetComponent instanceof SheetComponent).toBe(true);
expect(spreadsheetMod.sheets[spreadsheetMod.sheets.length - 1].title).toBe("Test Sheet");
});
it("should get sheet definitions", () => {
// Add a sheet with some data
spreadsheetMod.addSheet({
title: "Data Sheet",
rows: 3,
columns: 3,
data: [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9]
]
});
const definitions = spreadsheetMod.getSheetDefinitions();
// Check definitions format
expect(Array.isArray(definitions)).toBe(true);
expect(definitions.length).toBe(spreadsheetMod.sheets.length);
// Check data sheet definition
const dataSheetDef = definitions.find(def => def.title === "Data Sheet");
expect(dataSheetDef).toBeDefined();
expect(dataSheetDef.data).toEqual([
[1, 2, 3],
[4, 5, 6],
[7, 8, 9]
]);
expect(dataSheetDef.rows).toBe(3);
expect(dataSheetDef.columns).toBe(3);
});
it("should switch active sheet", () => {
// Add second sheet
const sheetComponent = spreadsheetMod.addSheet({
title: "Active Test"
});
// Get initial active sheet
const initialActiveSheet = spreadsheetMod.activeSheet;
// Activate the new sheet
spreadsheetMod.activeSheetFunc(sheetComponent);
// Check that active sheet changed
expect(spreadsheetMod.activeSheet).not.toBe(initialActiveSheet);
expect(spreadsheetMod.activeSheet.title).toBe("Active Test");
});
it("should manage sheets", () => {
// Create new sheet with data
const testData = [
["A", "B", "C"],
[1, 2, 3],
[4, 5, 6]
];
// First create a sheet
const sheet = spreadsheetMod.addSheet({
title: "Test Sheet",
data: testData
});
expect(sheet).toBeDefined();
expect(typeof sheet.getData).toBe('function');
expect(typeof sheet.setData).toBe('function');
});
it("should remove sheets", () => {
// Add several sheets
const sheet1 = spreadsheetMod.addSheet({ title: "Sheet 1" });
const sheet2 = spreadsheetMod.addSheet({ title: "Sheet 2" });
const sheet3 = spreadsheetMod.addSheet({ title: "Sheet 3" });
const initialCount = spreadsheetMod.sheets.length;
// Remove a sheet
spreadsheetMod.removeSheetFunc(sheet2);
// Check sheet was removed
expect(spreadsheetMod.sheets.length).toBe(initialCount - 1);
expect(spreadsheetMod.sheets.some(s => s.title === "Sheet 2")).toBe(false);
// Check we can't remove last sheet
const spy = jest.spyOn(console, 'warn').mockImplementation(() => {});
while (spreadsheetMod.sheets.length > 1) {
spreadsheetMod.removeSheetFunc(spreadsheetMod.sheets[0]);
}
// Try to remove last sheet
spreadsheetMod.removeSheetFunc(spreadsheetMod.sheets[0]);
// Should still have one sheet and warning should be logged
expect(spreadsheetMod.sheets.length).toBe(1);
expect(spy).toHaveBeenCalled();
spy.mockRestore();
});
});
describe("with initial data", () => {
const initialData = [
["Name", "Age", "City"],
["John", 30, "New York"],
["Jane", 25, "Boston"],
["Bob", 40, "Chicago"]
];
beforeEach(async () => {
tabulator = new TabulatorFull("#tabulator", {
spreadsheet: true,
spreadsheetData: initialData
});
spreadsheetMod = tabulator.module("spreadsheet");
return new Promise((resolve) => {
tabulator.on("tableBuilt", () => {
resolve();
});
});
});
it("should load initial data", () => {
const data = spreadsheetMod.getSheetData();
expect(data).toEqual(initialData);
});
it("should clear sheet data", () => {
// Clear the sheet
spreadsheetMod.clearSheet();
// Get data and verify it's empty
const data = spreadsheetMod.getSheetData();
expect(data).toEqual([]);
});
});
});

View File

@@ -0,0 +1,192 @@
import TabulatorFull from "../../../src/js/core/TabulatorFull";
import Tooltip from "../../../src/js/modules/Tooltip/Tooltip";
describe("Tooltip module", () => {
/** @type {TabulatorFull} */
let tabulator;
/** @type {Tooltip} */
let tooltipMod;
let tableData = [
{ id: 1, name: "John", age: 30, notes: "This is a note about John" },
{ id: 2, name: "Jane", age: 25, notes: "This is a note about Jane" },
{ id: 3, name: "Bob", age: 35, notes: "This is a note about Bob" }
];
let tableColumns = [
{ title: "ID", field: "id" },
{ title: "Name", field: "name", tooltip: true },
{ title: "Age", field: "age", tooltip: function(e, cell) { return "Age: " + cell.getValue(); } },
{ title: "Notes", field: "notes", headerTooltip: "Additional information" }
];
beforeEach(async () => {
const el = document.createElement("div");
el.id = "tabulator";
document.body.appendChild(el);
tabulator = new TabulatorFull("#tabulator", {
data: tableData,
columns: tableColumns,
tooltipDelay: 10 // Set short delay for testing
});
tooltipMod = tabulator.module("tooltip");
// Mock popup and dispatch functions
tooltipMod.popup = jest.fn().mockImplementation(() => {
return {
hide: jest.fn(),
show: jest.fn().mockReturnThis(),
hideOnBlur: jest.fn(),
renderCallback: jest.fn(),
containerEventCoords: jest.fn().mockReturnValue({ x: 0, y: 0 })
};
});
tooltipMod.dispatchExternal = jest.fn();
return new Promise((resolve) => {
tabulator.on("tableBuilt", () => {
resolve();
});
});
});
afterEach(() => {
tabulator.destroy();
document.getElementById("tabulator")?.remove();
jest.clearAllMocks();
});
it("should initialize with tooltip delay option", () => {
// Check tooltip delay option
expect(tabulator.options.tooltipDelay).toBe(10);
});
it("should show tooltip for cell with simple tooltip", async () => {
// Get a cell with simple tooltip (name column)
const nameCell = tabulator.rowManager.rows[0].getCells()[1];
// Create mock event
const mockEvent = new MouseEvent("mousemove");
// Trigger mousemove event on cell
tooltipMod.mousemoveCheck("tooltip", mockEvent, nameCell);
// Wait for tooltip delay
await new Promise(resolve => setTimeout(resolve, 20));
// Check popup was called
expect(tooltipMod.popup).toHaveBeenCalled();
// Check tooltip content
const popupCall = tooltipMod.popup.mock.calls[0][0];
expect(popupCall.classList.contains("tabulator-tooltip")).toBe(true);
expect(popupCall.innerHTML).toBe("John"); // Cell value
});
it("should show tooltip for cell with function tooltip", async () => {
// Get a cell with function tooltip (age column)
const ageCell = tabulator.rowManager.rows[0].getCells()[2];
// Create mock event
const mockEvent = new MouseEvent("mousemove");
// Trigger mousemove event on cell
tooltipMod.mousemoveCheck("tooltip", mockEvent, ageCell);
// Wait for tooltip delay
await new Promise(resolve => setTimeout(resolve, 20));
// Check popup was called
expect(tooltipMod.popup).toHaveBeenCalled();
// Check tooltip content
const popupCall = tooltipMod.popup.mock.calls[0][0];
expect(popupCall.classList.contains("tabulator-tooltip")).toBe(true);
expect(popupCall.innerHTML).toBe("Age: 30"); // Function result
});
it("should show tooltip for column header", async () => {
// Get a column with header tooltip
const notesCol = tabulator.columnManager.findColumn("notes");
// Create mock event
const mockEvent = new MouseEvent("mousemove");
// Trigger mousemove event on column header
tooltipMod.mousemoveCheck("headerTooltip", mockEvent, notesCol);
// Wait for tooltip delay
await new Promise(resolve => setTimeout(resolve, 20));
// Check popup was called
expect(tooltipMod.popup).toHaveBeenCalled();
// Check tooltip content
const popupCall = tooltipMod.popup.mock.calls[0][0];
expect(popupCall.classList.contains("tabulator-tooltip")).toBe(true);
expect(popupCall.innerHTML).toBe("Additional information");
});
it("should clear tooltip on mouseout", async () => {
// Get a cell with tooltip
const nameCell = tabulator.rowManager.rows[0].getCells()[1];
// Create mock event
const mockEvent = new MouseEvent("mousemove");
// Trigger mousemove event on cell
tooltipMod.mousemoveCheck("tooltip", mockEvent, nameCell);
// Trigger mouseout event
tooltipMod.mouseoutCheck("tooltip", mockEvent, nameCell);
// Check timeout was cleared
expect(tooltipMod.timeout).toBe(null);
});
it("should clear existing tooltip when showing a new one", async () => {
// Create spy for clearPopup
const clearSpy = jest.spyOn(tooltipMod, 'clearPopup');
// Get cells
const nameCell = tabulator.rowManager.rows[0].getCells()[1];
const ageCell = tabulator.rowManager.rows[0].getCells()[2];
// Create mock event
const mockEvent = new MouseEvent("mousemove");
// Trigger mousemove on first cell
tooltipMod.mousemoveCheck("tooltip", mockEvent, nameCell);
// Wait for tooltip delay
await new Promise(resolve => setTimeout(resolve, 20));
// Trigger mousemove on second cell
tooltipMod.mousemoveCheck("tooltip", mockEvent, ageCell);
// Check clearPopup was called
expect(clearSpy).toHaveBeenCalled();
});
it("should handle popupInstance tracking", async () => {
// Get a cell with tooltip
const nameCell = tabulator.rowManager.rows[0].getCells()[1];
// Create mock event
const mockEvent = new MouseEvent("mousemove");
// Trigger mousemove event on cell
tooltipMod.mousemoveCheck("tooltip", mockEvent, nameCell);
// Wait for tooltip delay
await new Promise(resolve => setTimeout(resolve, 20));
// Set popupInstance for test - this simulates what the module does
tooltipMod.popupInstance = tooltipMod.popup();
// Set popupInstance to null - this simulates what happens during hideOnBlur
tooltipMod.popupInstance = null;
// Check popupInstance was reset
expect(tooltipMod.popupInstance).toBe(null);
});
});

View File

@@ -0,0 +1,215 @@
import TabulatorFull from "../../../src/js/core/TabulatorFull";
import Validate from "../../../src/js/modules/Validate/Validate";
describe("Validate module", () => {
/** @type {TabulatorFull} */
let tabulator;
/** @type {Validate} */
let validateMod;
let tableData = [
{ id: 1, name: "John", age: 30, email: "john@example.com", status: "active" },
{ id: 2, name: "Jane", age: 25, email: "jane@example.com", status: "pending" },
{ id: 3, name: "Bob", age: 35, email: "bob@example.com", status: "inactive" }
];
let tableColumns = [
{ title: "ID", field: "id", validator: "integer" },
{ title: "Name", field: "name", editor: "input", validator: "required" },
{ title: "Age", field: "age", editor: "number", validator: ["min:18", "max:100"] },
{ title: "Email", field: "email", editor: "input", validator: "regex:^\\S+@\\S+\\.\\S+$" },
{ title: "Status", field: "status", editor: "input", validator: "in:active|pending|inactive" }
];
beforeEach(async () => {
const el = document.createElement("div");
el.id = "tabulator";
document.body.appendChild(el);
tabulator = new TabulatorFull("#tabulator", {
data: tableData,
columns: tableColumns
});
validateMod = tabulator.module("validate");
return new Promise((resolve) => {
tabulator.on("tableBuilt", () => {
resolve();
});
});
});
afterEach(() => {
// Clean up DOM without destroying tabulator to avoid dispatch errors
if (document.getElementById("tabulator")) {
document.getElementById("tabulator").remove();
}
});
it("should initialize with no invalid cells", () => {
const invalidCells = validateMod.getInvalidCells();
expect(invalidCells.length).toBe(0);
});
it("should validate a cell with integer validator", () => {
const idCell = tabulator.rowManager.rows[0].getCells()[0]; // ID cell
// Valid test
let result = validateMod.cellValidate(idCell);
expect(result).toBe(true);
// Invalid test - set to non-integer
idCell.setValue("abc");
result = validateMod.cellValidate(idCell);
expect(Array.isArray(result)).toBe(true);
expect(result[0].type).toBe("integer");
// Check that the cell is marked as invalid
expect(idCell.getElement().classList.contains("tabulator-validation-fail")).toBe(true);
// Cell should be in invalid cells list
const invalidCells = validateMod.getInvalidCells();
expect(invalidCells.length).toBe(1);
});
it("should validate a cell with required validator", () => {
const nameCell = tabulator.rowManager.rows[0].getCells()[1]; // Name cell
// Valid test
let result = validateMod.cellValidate(nameCell);
expect(result).toBe(true);
// Invalid test - set to empty
nameCell.setValue("");
result = validateMod.cellValidate(nameCell);
expect(Array.isArray(result)).toBe(true);
expect(result[0].type).toBe("required");
});
it("should validate a cell with multiple validators", () => {
const ageCell = tabulator.rowManager.rows[0].getCells()[2]; // Age cell
// Valid test
let result = validateMod.cellValidate(ageCell);
expect(result).toBe(true);
// Invalid test - below minimum
ageCell.setValue(15);
result = validateMod.cellValidate(ageCell);
expect(Array.isArray(result)).toBe(true);
expect(result[0].type).toBe("min");
// Invalid test - above maximum
ageCell.setValue(150);
result = validateMod.cellValidate(ageCell);
expect(Array.isArray(result)).toBe(true);
expect(result[0].type).toBe("max");
});
it("should validate a cell with regex validator", () => {
const emailCell = tabulator.rowManager.rows[0].getCells()[3]; // Email cell
// Valid test
let result = validateMod.cellValidate(emailCell);
expect(result).toBe(true);
// Invalid test - not an email
emailCell.setValue("not-an-email");
result = validateMod.cellValidate(emailCell);
expect(Array.isArray(result)).toBe(true);
expect(result[0].type).toBe("regex");
});
it("should validate a cell with in validator", () => {
const statusCell = tabulator.rowManager.rows[0].getCells()[4]; // Status cell
// Valid test
let result = validateMod.cellValidate(statusCell);
expect(result).toBe(true);
// Invalid test - not in list
statusCell.setValue("unknown");
result = validateMod.cellValidate(statusCell);
expect(Array.isArray(result)).toBe(true);
expect(result[0].type).toBe("in");
});
it("should validate an entire column", () => {
// All cells are initially valid
const nameColumn = tabulator.columnManager.findColumn("name");
let result = validateMod.columnValidate(nameColumn);
expect(result).toBe(true);
// Make one cell invalid
tabulator.rowManager.rows[1].getCells()[1].setValue(""); // Set Jane's name to empty
result = validateMod.columnValidate(nameColumn);
// Result should be an array with one invalid cell
expect(Array.isArray(result)).toBe(true);
expect(result.length).toBe(1);
});
it("should have row validation method", () => {
const row = tabulator.rowManager.rows[0];
// Make sure row component has a validate function
expect(typeof row.getComponent().validate).toBe('function');
// Verify the rowValidate method exists
expect(typeof validateMod.rowValidate).toBe('function');
});
it("should have table validation method", () => {
// Verify table validation methods exist
expect(typeof validateMod.userValidate).toBe('function');
});
it("should track invalid cells", () => {
// Verify the invalidCells array exists
expect(Array.isArray(validateMod.invalidCells)).toBe(true);
expect(validateMod.invalidCells.length).toBe(0);
// Verify getInvalidCells method exists
expect(typeof validateMod.getInvalidCells).toBe('function');
// In the real implementation, the invalidCells array contains cell components
// that already have getComponent defined, but we're not testing that here
// We just want to test that getInvalidCells returns the current invalid cells
const emptyResult = validateMod.getInvalidCells();
expect(Array.isArray(emptyResult)).toBe(true);
expect(emptyResult.length).toBe(0);
});
it("should manually clear validation for a cell", () => {
// Make a cell invalid
const idCell = tabulator.rowManager.rows[0].getCells()[0]; // ID cell
idCell.setValue("abc");
validateMod.cellValidate(idCell);
// Check cell is invalid
expect(validateMod.getInvalidCells().length).toBe(1);
// Clear validation
validateMod.clearValidation(idCell);
// Check cell is no longer marked as invalid
expect(idCell.getElement().classList.contains("tabulator-validation-fail")).toBe(false);
expect(validateMod.getInvalidCells().length).toBe(0);
});
it("should clear validation for all invalid cells", () => {
// Make multiple cells invalid
tabulator.rowManager.rows[0].getCells()[1].setValue(""); // Set John's name to empty
tabulator.rowManager.rows[1].getCells()[2].setValue(15); // Set Jane's age below minimum
// Validate cells manually
validateMod.cellValidate(tabulator.rowManager.rows[0].getCells()[1]);
validateMod.cellValidate(tabulator.rowManager.rows[1].getCells()[2]);
// Check cells are invalid
expect(validateMod.getInvalidCells().length).toBe(2);
// Clear all validation
validateMod.userClearCellValidation();
// Check all cells are now valid
expect(validateMod.getInvalidCells().length).toBe(0);
});
});

View File

@@ -0,0 +1,56 @@
/**
* JSDOM test setup helpers
*
* This file provides helper functions for common DOM element mocking
* needs in Tabulator tests.
*/
// Create an element with automatic spy functions on common methods
global.createSpyElement = (tagName) => {
const element = document.createElement(tagName);
// Add spies to common DOM methods
element.appendChild = jest.fn().mockImplementation(element.appendChild);
element.addEventListener = jest.fn().mockImplementation(element.addEventListener);
element.classList.add = jest.fn().mockImplementation(element.classList.add);
element.classList.remove = jest.fn().mockImplementation(element.classList.remove);
return element;
};
// Helper method to create mock events with required properties
global.createMockEvent = (type, props = {}) => {
// Create appropriate event type
let event;
if (type === 'click' || type === 'mousedown' || type === 'mouseup') {
event = new MouseEvent(type, { bubbles: true, cancelable: true, ...props });
} else {
event = new Event(type, { bubbles: true, cancelable: true, ...props });
}
// Add any additional properties
Object.entries(props).forEach(([key, value]) => {
if (!event[key]) {
event[key] = value;
}
});
// Add spy on preventDefault
const originalPreventDefault = event.preventDefault;
event.preventDefault = jest.fn().mockImplementation(() => originalPreventDefault.call(event));
return event;
};
// Helper for column creation
global.createMockColumn = (definition = {}) => {
return {
definition,
titleElement: {
insertBefore: jest.fn(),
firstChild: {}
},
getComponent: jest.fn().mockReturnValue({ column: true })
};
};