blob: 318d8d6b98aed53f1b8ab4f95b69acaad18c4160 [file] [log] [blame]
/*
* Copyright (c) 2010-2020 BSI Business Systems Integration AG.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* BSI Business Systems Integration AG - initial API and implementation
*/
import {Cell, keys, scout, StaticLookupCall, Status, TableRow, Widget} from '../../../src/index';
import {FormSpecHelper, TableSpecHelper} from '../../../src/testing/index';
describe('CellEditor', () => {
let session;
let helper;
let formHelper;
beforeEach(() => {
setFixtures(sandboxDesktop());
session = sandboxSession();
helper = new TableSpecHelper(session);
formHelper = new FormSpecHelper(session);
jasmine.Ajax.install();
jasmine.clock().install();
});
afterEach(() => {
session = null;
jasmine.Ajax.uninstall();
jasmine.clock().uninstall();
let popup = findPopup();
if (popup) {
popup.close();
}
});
class DummyLookupCall extends StaticLookupCall {
constructor() {
super();
}
_data() {
return [
['key0', 'Key 0'],
['key1', 'Key 1']
];
}
}
function createStringField() {
return scout.create('StringField', {
parent: session.desktop
});
}
function $findPopup() {
return $('.cell-editor-popup');
}
function findPopup() {
return $findPopup().data('popup');
}
function assertCellEditorIsOpen(table, column, row) {
let popup = table.cellEditorPopup;
expect(popup.cell.field.rendered).toBe(true);
expect(popup.column).toBe(column);
expect(popup.row).toBe(row);
let $popup = $findPopup();
expect($popup.length).toBe(1);
expect(popup.$container[0]).toBe($popup[0]);
expect($popup.find('.form-field').length).toBe(1);
}
describe('mouse click', () => {
let table, model, $rows, $cells0, $cells1, $cell0_0, $cell0_1, $cell1_0;
beforeEach(() => {
model = helper.createModelFixture(2, 2);
table = helper.createTable(model);
table.render();
helper.applyDisplayStyle(table);
$rows = table.$rows();
$cells0 = $rows.eq(0).find('.table-cell');
$cells1 = $rows.eq(1).find('.table-cell');
$cell0_0 = $cells0.eq(0);
$cell0_1 = $cells0.eq(1);
$cell1_0 = $cells1.eq(0);
});
it('starts cell edit if cell is editable', () => {
table.rows[0].cells[0].editable = true;
table.rows[1].cells[0].editable = false;
spyOn(table, 'prepareCellEdit');
$cell1_0.triggerClick();
expect(table.prepareCellEdit).not.toHaveBeenCalled();
$cell0_0.triggerClick();
expect(table.prepareCellEdit).toHaveBeenCalled();
});
it('does not start cell edit if cell is not editable', () => {
table.rows[0].cells[0].editable = false;
spyOn(table, 'prepareCellEdit');
$cell0_0.triggerClick();
expect(table.prepareCellEdit).not.toHaveBeenCalled();
});
it('does not start cell edit if row is disabled', () => {
table.rows[0].cells[0].editable = true;
table.rows[0].enabled = false;
spyOn(table, 'prepareCellEdit');
$cell0_0.triggerClick();
expect(table.prepareCellEdit).not.toHaveBeenCalled();
});
it('does not start cell edit if table is disabled', () => {
table.rows[0].cells[0].editable = true;
table.enabled = false;
table.recomputeEnabled();
spyOn(table, 'prepareCellEdit');
$cell0_0.triggerClick();
expect(table.prepareCellEdit).not.toHaveBeenCalled();
});
it('does not start cell edit if form is disabled', () => {
table.rows[0].cells[0].editable = true;
table.enabledComputed = false;
spyOn(table, 'prepareCellEdit');
$cell0_0.triggerClick();
expect(table.prepareCellEdit).not.toHaveBeenCalled();
});
it('does not start cell edit if mouse down and up happened on different cells', () => {
table.rows[0].cells[0].editable = true;
table.rows[0].cells[1].editable = true;
spyOn(table, 'prepareCellEdit');
$cell0_1.triggerMouseDown();
$cell0_0.triggerMouseUp();
expect(table.prepareCellEdit).not.toHaveBeenCalled();
});
it('does not start cell edit if right mouse button was pressed', () => {
table.rows[0].cells[0].editable = true;
spyOn(table, 'prepareCellEdit');
$cell0_0.triggerMouseDown({which: 3});
$cell0_0.triggerMouseUp({which: 3});
expect(table.prepareCellEdit).not.toHaveBeenCalled();
});
it('does not start cell edit if middle mouse button was pressed', () => {
table.rows[0].cells[0].editable = true;
spyOn(table, 'prepareCellEdit');
$cell0_0.triggerMouseDown({which: 2});
$cell0_0.triggerMouseUp({which: 2});
expect(table.prepareCellEdit).not.toHaveBeenCalled();
});
it('does not open cell editor if a ctrl or shift is pressed, because the user probably wants to do row selection rather than cell editing', () => {
table.rows[0].cells[0].editable = true;
table.rows[1].cells[0].editable = true;
spyOn(table, 'prepareCellEdit');
// row 0 is selected, user presses shift and clicks row 2
table.selectRows([table.rows[0]]);
$cell1_0.triggerClick({modifier: 'shift'});
expect(table.prepareCellEdit).not.toHaveBeenCalled();
$cell1_0.triggerClick({modifier: 'ctrl'});
expect(table.prepareCellEdit).not.toHaveBeenCalled();
});
});
describe('TAB key', () => {
let table, $rows, $cells0;
beforeEach(() => {
table = helper.createTable(helper.createModelFixture(2, 2));
table.render();
helper.applyDisplayStyle(table);
$rows = table.$rows();
$cells0 = $rows.eq(0).find('.table-cell');
});
it('starts the cell editor for the next editable cell', () => {
table.rows[0].cells[0].editable = true;
table.rows[1].cells[0].editable = true;
table.focusCell(table.columns[0], table.rows[0]);
jasmine.clock().tick(0);
assertCellEditorIsOpen(table, table.columns[0], table.rows[0]);
$(document.activeElement).triggerKeyInputCapture(keys.TAB);
jasmine.clock().tick(0);
jasmine.clock().tick(0);
assertCellEditorIsOpen(table, table.columns[0], table.rows[1]);
});
});
describe('prepareCellEdit', () => {
let table;
beforeEach(() => {
let model = helper.createModelFixture(2, 2);
table = helper.createTable(model);
table.render();
helper.applyDisplayStyle(table);
});
it('creates field and calls start', () => {
table.columns[0].setEditable(true);
spyOn(table, 'startCellEdit').and.callThrough();
table.prepareCellEdit(table.columns[0], table.rows[0]);
jasmine.clock().tick(0);
expect(table.startCellEdit).toHaveBeenCalled();
assertCellEditorIsOpen(table, table.columns[0], table.rows[0]);
});
it('copies the value to the field if cell was valid', () => {
let column = table.columns[0];
let row = table.rows[0];
column.setEditable(true);
column.setCellValue(row, 'valid value');
table.prepareCellEdit(column, row);
jasmine.clock().tick(0);
assertCellEditorIsOpen(table, column, row);
let field = table.cellEditorPopup.cell.field;
expect(field.value).toEqual('valid value');
expect(field.displayText).toEqual('valid value');
expect(field.errorStatus).toEqual(null);
});
it('copies the text and the error to the field if cell was invalid', () => {
let column = table.columns[0];
let row = table.rows[0];
column.setEditable(true);
column.setCellValue(row, 'valid value');
column.setCellText(row, 'invalid value');
column.setCellErrorStatus(row, Status.error('error'));
table.prepareCellEdit(column, row);
jasmine.clock().tick(0);
assertCellEditorIsOpen(table, column, row);
let field = table.cellEditorPopup.cell.field;
expect(field.value).toEqual(null);
expect(field.displayText).toEqual('invalid value');
expect(field.errorStatus.message).toEqual('error');
});
it('triggers prepareCellEdit event', () => {
let triggeredEvent;
table.columns[0].setEditable(true);
table.on('prepareCellEdit', event => {
triggeredEvent = event;
});
table.prepareCellEdit(table.columns[0], table.rows[0]);
jasmine.clock().tick(0);
expect(triggeredEvent.column).toBe(table.columns[0]);
expect(triggeredEvent.row).toBe(table.rows[0]);
});
});
describe('startCellEdit', () => {
let table;
beforeEach(() => {
let model = helper.createModelFixture(2, 2);
table = helper.createTable(model);
table.render();
helper.applyDisplayStyle(table);
});
it('opens popup with field', () => {
table.columns[0].setEditable(true);
let field = createStringField(table);
table.startCellEdit(table.columns[0], table.rows[0], field);
assertCellEditorIsOpen(table, table.columns[0], table.rows[0]);
expect(table.cellEditorPopup.cell.field).toBe(field);
});
it('triggers startCellEdit event', () => {
let triggeredEvent;
table.columns[0].setEditable(true);
table.on('startCellEdit', event => {
triggeredEvent = event;
});
table.prepareCellEdit(table.columns[0], table.rows[0]);
jasmine.clock().tick(0);
expect(triggeredEvent.row).toBe(table.rows[0]);
expect(triggeredEvent.column).toBe(table.columns[0]);
expect(triggeredEvent.field instanceof Widget).toBe(true);
});
});
describe('completeCellEdit', () => {
let table;
beforeEach(() => {
let model = helper.createModelFixture(2, 2);
table = helper.createTable(model);
table.render();
helper.applyDisplayStyle(table);
});
it('triggers completeCellEdit event', () => {
let triggeredEvent;
table.columns[0].setEditable(true);
table.prepareCellEdit(table.columns[0], table.rows[0]);
jasmine.clock().tick(0);
table.on('completeCellEdit', event => {
triggeredEvent = event;
});
table.completeCellEdit();
expect(triggeredEvent.column).toBe(table.columns[0]);
expect(triggeredEvent.row).toBe(table.rows[0]);
expect(triggeredEvent.field).toBe(table.rows[0].cells[0].field);
});
it('calls endCellEdit with saveEditorValue=true', () => {
table.columns[0].setEditable(true);
table.prepareCellEdit(table.columns[0], table.rows[0]);
jasmine.clock().tick(0);
spyOn(table, 'endCellEdit').and.callThrough();
let field = table.cellEditorPopup.cell.field;
table.completeCellEdit();
expect(table.endCellEdit).toHaveBeenCalledWith(field, true);
jasmine.clock().tick(0);
expect($findPopup().length).toBe(0);
});
it('saves editor value', () => {
table.columns[0].setEditable(true);
table.prepareCellEdit(table.columns[0], table.rows[0]);
jasmine.clock().tick(0);
table.cellEditorPopup.cell.field.setValue('my new value');
table.completeCellEdit();
expect(table.rows[0].cells[0].value).toBe('my new value');
});
it('copies the value to the cell if field was valid', () => {
table.columns[0].setEditable(true);
table.prepareCellEdit(table.columns[0], table.rows[0]);
jasmine.clock().tick(0);
table.cellEditorPopup.cell.field.setValue('my new value');
table.completeCellEdit();
let cell = table.rows[0].cells[0];
expect(cell.value).toBe('my new value');
expect(cell.text).toBe('my new value');
expect(cell.errorStatus).toBe(null);
expect($('.tooltip').length).toBe(0);
});
it('copies the text and error to the cell if field was invalid', () => {
let column = table.columns[0];
let row = table.rows[0];
let cell = row.cells[0];
expect($('.tooltip').length).toBe(0);
column.setEditable(true);
column.setCellValue(row, 'valid value');
table.prepareCellEdit(column, row);
jasmine.clock().tick(0);
let field = table.cellEditorPopup.cell.field;
field.setValidator(value => {
throw 'Validation failed';
});
field.setValue('invalid value');
expect(field.value).toBe('valid value');
expect(field.errorStatus.message).toBe('Validation failed');
expect(field.displayText).toBe('invalid value');
table.completeCellEdit();
expect(cell.value).toBe('valid value');
expect(cell.text).toBe('invalid value');
expect(cell.errorStatus.message).toBe('Validation failed');
expect($('.tooltip').length).toBe(1);
expect($('.tooltip')).toContainText('Validation failed');
});
it('clears the error if value is now valid', () => {
let column = table.columns[0];
let row = table.rows[0];
let cell = row.cells[0];
expect($('.tooltip').length).toBe(0);
column.setEditable(true);
column.setCellValue(row, 'valid value');
table.prepareCellEdit(column, row);
jasmine.clock().tick(0);
let field = table.cellEditorPopup.cell.field;
field.setValidator(value => {
throw 'Validation failed';
});
field.setValue('invalid value');
expect(field.value).toBe('valid value');
expect(field.errorStatus.message).toBe('Validation failed');
expect(field.displayText).toBe('invalid value');
table.completeCellEdit();
expect(cell.value).toBe('valid value');
expect(cell.text).toBe('invalid value');
expect(cell.errorStatus.message).toBe('Validation failed');
expect($('.tooltip').length).toBe(1);
expect($('.tooltip')).toContainText('Validation failed');
// Second time -> make it valid
table.prepareCellEdit(column, row);
jasmine.clock().tick(0);
field = table.cellEditorPopup.cell.field;
field.setValidator(null);
field.setValue('new valid value');
expect(field.value).toBe('new valid value');
expect(field.errorStatus).toBe(null);
expect(field.displayText).toBe('new valid value');
table.completeCellEdit();
expect(cell.value).toBe('new valid value');
expect(cell.text).toBe('new valid value');
expect(cell.errorStatus).toBe(null);
expect($('.tooltip').length).toBe(0);
});
it('clears the error if value is now valid even when changed to the original value', () => {
let column = table.columns[0];
let row = table.rows[0];
let cell = row.cells[0];
expect($('.tooltip').length).toBe(0);
column.setEditable(true);
column.setCellValue(row, 'valid value');
table.prepareCellEdit(column, row);
jasmine.clock().tick(0);
let field = table.cellEditorPopup.cell.field;
field.setValidator(value => {
throw 'Validation failed';
});
field.setValue('invalid value');
expect(field.value).toBe('valid value');
expect(field.errorStatus.message).toBe('Validation failed');
expect(field.displayText).toBe('invalid value');
table.completeCellEdit();
expect(cell.value).toBe('valid value');
expect(cell.text).toBe('invalid value');
expect(cell.errorStatus.message).toBe('Validation failed');
expect($('.tooltip').length).toBe(1);
expect($('.tooltip')).toContainText('Validation failed');
// Second time -> make it valid
table.prepareCellEdit(column, row);
jasmine.clock().tick(0);
field = table.cellEditorPopup.cell.field;
field.setValidator(null);
field.setValue('valid value'); // Same as at the beginning
expect(field.value).toBe('valid value');
expect(field.errorStatus).toBe(null);
expect(field.displayText).toBe('valid value');
table.completeCellEdit();
expect(cell.value).toBe('valid value');
expect(cell.text).toBe('valid value');
expect(cell.errorStatus).toBe(null);
expect($('.tooltip').length).toBe(0);
});
it('does not reopen the editor again', () => {
table.columns[0].setEditable(true);
table.prepareCellEdit(table.columns[0], table.rows[0]);
jasmine.clock().tick(0);
table.cellEditorPopup.cell.field.setValue('my new value');
let triggeredStartCellEditEvent = null;
table.on('startCellEdit', event => {
triggeredStartCellEditEvent = event;
});
table.completeCellEdit();
// CompleteCellEdit triggers updateRows which would reopen the editor -> this must not happen if the editor was closed
expect(triggeredStartCellEditEvent).toBe(null);
});
});
describe('completeCellEdit in SmartColumn', () => {
let table;
beforeEach(() => {
let lookupCall = new DummyLookupCall();
lookupCall.init({session: session});
table = helper.createTable({
columns: [{
objectType: 'SmartColumn',
lookupCall: lookupCall
}]
});
let cell = new Cell();
cell.init({value: 'key0', text: 'Key 0'});
table.insertRow({
cells: [cell]
});
table.render();
helper.applyDisplayStyle(table);
// Ensure texts are set and no updates are pending
expect(table.rows[0].cells[0].text).toEqual('Key 0');
expect(table.updateBuffer.promises.length).toBe(0);
});
it('does not fail when completing edit after removing a value', done => {
jasmine.clock().uninstall();
table.columns[0].setEditable(true);
table.sort(table.columns[0]); // Column needs to be sorted to force a rerendering of the rows at the end when rows are updated (_sortAfterUpdate)
table.prepareCellEdit(table.columns[0], table.rows[0], true).then(() => {
table.cellEditorPopup.cell.field.clear();
let triggeredStartCellEditEvent = null;
table.on('startCellEdit', event => {
triggeredStartCellEditEvent = event;
});
// Use completeEdit to simulate a mouse click (see CellEditorPopup._onMouseDownOutside)
// Compared to table.completeEdit it sets the flag _pendingCompleteCellEdit which delays the destruction of the popup (see _destroyCellEditorPopup)
table.cellEditorPopup.completeEdit().then(() => {
// CompleteCellEdit triggers setCellTextDeferred which adds the promise to the updateBuffer which eventually renders the viewport and would reopen the editor
// -> reopening must not happen if the editor was closed
expect(triggeredStartCellEditEvent).toBe(null);
done();
});
});
});
it('triggers update row event containing row with correct state', () => {
table.columns[0].setEditable(true);
table.markRowsAsNonChanged();
table.prepareCellEdit(table.columns[0], table.rows[0], true);
jasmine.clock().tick(300);
table.cellEditorPopup.cell.field.setValue('key1');
jasmine.clock().tick(300);
let updateRowCount = 0;
table.on('rowsUpdated', event => {
expect(event.rows[0].cells[0].value).toBe('key1');
expect(event.rows[0].cells[0].text).toBe('Key 1');
expect(event.rows[0].status).toBe(TableRow.Status.UPDATED);
updateRowCount++;
});
table.completeCellEdit();
jasmine.clock().tick(300);
expect(updateRowCount).toBe(1);
});
});
describe('cancelCellEdit', () => {
let table;
beforeEach(() => {
let model = helper.createModelFixture(2, 2);
table = helper.createTable(model);
table.render();
helper.applyDisplayStyle(table);
});
it('triggers cancelCellEdit event', () => {
let triggeredEvent;
table.columns[0].setEditable(true);
table.prepareCellEdit(table.columns[0], table.rows[0]);
jasmine.clock().tick(0);
table.on('cancelCellEdit', event => {
triggeredEvent = event;
});
table.cancelCellEdit();
expect(triggeredEvent.column).toBe(table.columns[0]);
expect(triggeredEvent.row).toBe(table.rows[0]);
expect(triggeredEvent.field).toBe(table.rows[0].cells[0].field);
});
it('calls endCellEdit with saveEditorValue=false', () => {
table.columns[0].setEditable(true);
table.prepareCellEdit(table.columns[0], table.rows[0]);
jasmine.clock().tick(0);
spyOn(table, 'endCellEdit').and.callThrough();
let field = table.cellEditorPopup.cell.field;
table.cancelCellEdit();
expect(table.endCellEdit).toHaveBeenCalledWith(field);
jasmine.clock().tick(0);
expect($findPopup().length).toBe(0);
});
it('does not save editor value', () => {
table.columns[0].setEditable(true);
table.prepareCellEdit(table.columns[0], table.rows[0]);
jasmine.clock().tick(0);
table.cellEditorPopup.cell.field.setValue('my new value');
table.cancelCellEdit();
expect(table.rows[0].cells[0].value).toBe('cell0_0');
});
});
describe('endCellEdit', () => {
let table;
beforeEach(() => {
let model = helper.createModelFixture(2, 2);
table = helper.createTable(model);
table.render();
helper.applyDisplayStyle(table);
});
it('destroys the field', () => {
table.prepareCellEdit(table.columns[0], table.rows[0]);
jasmine.clock().tick(0);
let popup = table.cellEditorPopup;
let field = popup.cell.field;
expect(field.destroyed).toBe(false);
table.endCellEdit(field);
expect(field.destroyed).toBe(true);
});
it('removes the cell editor popup', () => {
table.prepareCellEdit(table.columns[0], table.rows[0]);
jasmine.clock().tick(0);
let popup = table.cellEditorPopup;
let field = popup.cell.field;
expect(field.destroyed).toBe(false);
table.endCellEdit(field);
jasmine.clock().tick(0);
expect($findPopup().length).toBe(0);
expect($findPopup().find('.form-field').length).toBe(0);
expect(popup.rendered).toBe(false);
expect(popup.cell.field.rendered).toBe(false);
});
});
describe('validation', () => {
let table, model, cell0_0, $tooltip;
beforeEach(() => {
model = helper.createModelFixture(2, 2);
table = helper.createTable(model);
cell0_0 = table.rows[0].cells[0];
});
it('shows a tooltip if field has an error', () => {
cell0_0.editable = true;
cell0_0.errorStatus = 'Validation error';
$tooltip = $('.tooltip');
expect($tooltip.length).toBe(0);
table.render();
$tooltip = $('.tooltip');
expect($tooltip.length).toBe(1);
});
it('does not show a tooltip if field has no error', () => {
cell0_0.editable = true;
$tooltip = $('.tooltip');
expect($tooltip.length).toBe(0);
table.render();
$tooltip = $('.tooltip');
expect($tooltip.length).toBe(0);
});
});
describe('popup recovery', () => {
let model, table, row0, $cells0, $cell0_0;
beforeEach(() => {
model = helper.createModelFixture(2, 3);
table = helper.createTable(model);
row0 = table.rows[0];
});
it('reopens popup if row gets updated', () => {
row0.cells[0].editable = true;
table.render();
$cells0 = table.$cellsForRow(row0.$row);
$cell0_0 = $cells0.eq(0);
table.prepareCellEdit(table.columns[0], row0);
jasmine.clock().tick(0);
expect(table.cellEditorPopup.row).toBe(row0);
expect(table.cellEditorPopup.$anchor[0]).toBe($cell0_0[0]);
let updatedRows = helper.createModelRows(2, 1);
updatedRows[0].id = row0.id;
table.updateRows(updatedRows);
// Check if popup is correctly linked to updated row and new $cell
row0 = table.rows[0];
$cells0 = table.$cellsForRow(row0.$row);
$cell0_0 = $cells0.eq(0);
expect($findPopup().length).toBe(1);
expect(table.cellEditorPopup.row).toBe(row0);
expect(table.cellEditorPopup.$anchor[0]).toBe($cell0_0[0]);
});
it('closes popup if row gets deleted', () => {
row0.cells[0].editable = true;
table.render();
table.prepareCellEdit(table.columns[0], row0);
jasmine.clock().tick(0);
spyOn(table, 'cancelCellEdit');
table.deleteRows([row0]);
// Check if popup is closed
expect($findPopup().length).toBe(0);
// Check whether cancel edit has been called
expect(table.cancelCellEdit).toHaveBeenCalled();
});
it('closes popup if all rows get deleted', () => {
row0.cells[0].editable = true;
table.render();
table.prepareCellEdit(table.columns[0], row0);
jasmine.clock().tick(0);
spyOn(table, 'cancelCellEdit');
table.deleteAllRows();
// Check if popup is closed
expect($findPopup().length).toBe(0);
// Check whether cancel edit has been called
expect(table.cancelCellEdit).toHaveBeenCalled();
});
it('closes popup (before) table is removed', () => {
row0.cells[0].editable = true;
table.render();
table.prepareCellEdit(table.columns[0], row0);
jasmine.clock().tick(0);
expect(table.cellEditorPopup).toBeTruthy();
table.remove(); // called by parent.detach();
jasmine.clock().tick(0);
expect(table.cellEditorPopup).toBe(null);
});
it('closes popup when table is removed', () => {
row0.cells[0].editable = true;
table.render();
table.prepareCellEdit(table.columns[0], row0);
jasmine.clock().tick(0);
expect(table.cellEditorPopup).toBeTruthy();
table.remove();
jasmine.clock().tick(0);
expect(table.cellEditorPopup).toBe(null);
});
});
describe('tooltip recovery', () => {
let model, table, row0;
beforeEach(() => {
model = helper.createModelFixture(2, 3);
table = helper.createTable(model);
row0 = model.rows[0];
});
it('removes tooltip if row gets deleted', () => {
row0.cells[0].editable = true;
row0.cells[0].errorStatus = 'Validation error';
table.render();
expect($('.tooltip').length).toBe(1);
expect(table.tooltips.length).toBe(1);
table.deleteRows([row0]);
expect($('.tooltip').length).toBe(0);
expect(table.tooltips.length).toBe(0);
});
});
});