You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
gristlabs_grist-core/test/nbrowser/MultiColumn.ts

1408 lines
51 KiB

import {arrayRepeat} from 'app/plugin/gutil';
import * as gu from 'test/nbrowser/gristUtils';
import {ColumnType} from 'test/nbrowser/gristUtils';
import {setupTestSuite} from 'test/nbrowser/testUtils';
import {UserAPIImpl} from 'app/common/UserAPI';
import {assert, driver, Key} from 'mocha-webdriver';
let api: UserAPIImpl;
let doc: string;
const transparent = 'rgba(0, 0, 0, 0)';
const blue = '#0000FF';
const red = '#FF0000';
const types: Array<ColumnType> = [
'Any', 'Text', 'Integer', 'Numeric', 'Toggle', 'Date', 'DateTime', 'Choice', 'Choice List',
'Reference', 'Reference List', 'Attachment'
];
describe('MultiColumn', function() {
this.timeout(80000);
const cleanup = setupTestSuite();
before(async function() {
const session = await gu.session().login();
doc = await session.tempNewDoc(cleanup, "MultiColumn", {load: false});
api = session.createHomeApi();
await api.applyUserActions(doc, [
['BulkAddRecord', 'Table1', arrayRepeat(2, null), {}]
]);
// Leave only A column which will have AnyType. We don't need it, but
// table must have at least one column and we will be removing all columns
// that we test.
await api.applyUserActions(doc, [
['RemoveColumn', 'Table1', 'B'],
['RemoveColumn', 'Table1', 'C'],
]);
await session.loadDoc('/doc/' + doc);
await gu.toggleSidePanel('right', 'open');
await driver.find('.test-right-tab-field').click();
});
describe("behavior tests", function() {
let revertEach: () => Promise<void>;
let revertAll: () => Promise<void>;
let failed = false;
before(async function() {
revertAll = await gu.begin();
await addAnyColumn('Test1');
await addAnyColumn('Test2');
await addAnyColumn('Test3');
});
after(async function() {
if (!failed) {
await revertAll();
}
});
beforeEach(async () => {
revertEach = await gu.begin();
});
afterEach(async function() {
if (this.currentTest?.state !== 'failed') {
await revertEach();
} else {
failed = true;
}
});
it('should not work on card view', async () => {
await gu.changeWidget('Card');
await gu.openColumnPanel();
assert.notEqual(await gu.getType(), "Mixed types");
await gu.openColumnPanel();
// Should be able to change type.
await gu.getDetailCell('Test1', 1);
await gu.enterCell("aa");
await gu.setType("Integer", {apply: true});
assert.equal(await gu.getType(), "Integer");
});
it('should undo color change', async () => {
// This is test for a bug, colors were not saved when "click outside" was done by clicking
// one of the cells.
await selectColumns('Test1', 'Test2');
await gu.setType('Reference');
await gu.getCell('Test1', 1).click();
await gu.enterCell('Table1', Key.ENTER);
await gu.getCell('Test2', 3).click();
await gu.enterCell('Table1', Key.ENTER);
await selectColumns('Test1', 'Test2');
await gu.openCellColorPicker();
await gu.setFillColor(blue);
// Clicking on one of the cell caused that the color was not saved.
await gu.getCell('Test2', 1).click();
// Test if color is set.
await gu.assertFillColor(await gu.getCell('Test1', 1), blue);
await gu.assertFillColor(await gu.getCell('Test2', 1), blue);
// Press undo
await gu.undo();
await gu.assertFillColor(await gu.getCell('Test1', 1), transparent);
await gu.assertFillColor(await gu.getCell('Test2', 1), transparent);
});
for (const type of ['Choice', 'Text', 'Reference', 'Numeric'] as Array<ColumnType>) {
it(`should reset all columns to first column type for ${type}`, async () => {
// We start with empty columns, then we will change first one
// to a data column, select all and then change all other to the same type.
// This tests if creator panel is enabled properly, and we can change
// all columns to the type of the first selected columns (it was a bug).
await selectColumns('Test1');
await gu.setType(type);
await selectColumns('Test1', 'Test3');
assert.equal(await gu.getType(), "Mixed types");
await gu.setType(type);
assert.equal(await gu.getType(), type);
await selectColumns('Test1');
assert.equal(await gu.getType(), type);
await selectColumns('Test2');
assert.equal(await gu.getType(), type);
await selectColumns('Test3');
assert.equal(await gu.getType(), type);
await gu.undo();
await selectColumns('Test1');
assert.equal(await gu.getType(), type);
await selectColumns('Test2');
assert.equal(await gu.getType(), 'Any');
await selectColumns('Test3');
assert.equal(await gu.getType(), 'Any');
});
}
it('should show proper behavior label', async () => {
await selectColumns('Test1');
assert.equal(await gu.columnBehavior(), 'Empty Column');
await selectColumns('Test1', 'Test3');
assert.equal(await gu.columnBehavior(), 'Empty Columns');
// Change first to be data column.
await selectColumns('Test1');
await driver.find(".test-field-set-data").click();
await gu.waitForServer();
await selectColumns('Test1', 'Test3');
assert.equal(await gu.columnBehavior(), 'Mixed Behavior');
// Change second to be a data column
await selectColumns('Test2');
await driver.find(".test-field-set-data").click();
await gu.waitForServer();
await selectColumns('Test1', 'Test2');
assert.equal(await gu.columnBehavior(), 'Data Columns');
// Now make them all formulas
await gu.sendActions([
['ModifyColumn', 'Table1', 'Test1', {formula: '1', isFormula: true}],
['ModifyColumn', 'Table1', 'Test2', {formula: '1', isFormula: true}],
['ModifyColumn', 'Table1', 'Test3', {formula: '1', isFormula: true}],
]);
await selectColumns('Test1', 'Test3');
assert.equal(await gu.columnBehavior(), 'Formula Columns');
// Make one of them data column and test that the mix is recognized.
await selectColumns('Test1');
await gu.changeBehavior('Convert column to data');
await selectColumns('Test1', 'Test3');
assert.equal(await gu.columnBehavior(), 'Mixed Behavior');
});
it('should reset multiple columns', async () => {
// Now make them all formulas
await gu.sendActions([
['ModifyColumn', 'Table1', 'Test1', {formula: '1', isFormula: true}],
['ModifyColumn', 'Table1', 'Test2', {formula: '1', isFormula: true}],
['ModifyColumn', 'Table1', 'Test3', {formula: '1', isFormula: true}],
]);
await selectColumns('Test1', 'Test3');
assert.equal(await gu.columnBehavior(), 'Formula Columns');
await alignment('center');
assert.equal(await alignment(), 'center');
// Reset all of them
assert.deepEqual(await gu.availableBehaviorOptions(), ['Convert columns to data', 'Clear and reset']);
await gu.changeBehavior('Clear and reset');
assert.equal(await gu.columnBehavior(), 'Empty Columns');
assert.equal(await alignment(), 'left');
// Make them all data columns
await gu.getCell('Test1', 1).click(); await gu.enterCell('a');
await gu.getCell('Test2', 1).click(); await gu.enterCell('a');
await gu.getCell('Test3', 1).click(); await gu.enterCell('a');
await selectColumns('Test1', 'Test3');
assert.equal(await gu.columnBehavior(), 'Data Columns');
await selectColumns('Test1');
assert.equal(await gu.columnBehavior(), 'Data Column');
// Reset all of them
await selectColumns('Test1', 'Test3');
assert.deepEqual(await gu.availableBehaviorOptions(), ['Clear and reset']);
await gu.changeBehavior('Clear and reset');
assert.equal(await gu.columnBehavior(), 'Empty Columns');
await selectColumns('Test1');
assert.equal(await gu.columnBehavior(), 'Empty Column');
assert.equal(await gu.getCell('Test1', 1).getText(), '');
assert.equal(await gu.getCell('Test2', 1).getText(), '');
assert.equal(await gu.getCell('Test3', 1).getText(), '');
});
it('should convert to data multiple columns', async () => {
await selectColumns('Test1', 'Test3');
assert.equal(await gu.columnBehavior(), 'Empty Columns');
assert.deepEqual(await gu.availableBehaviorOptions(), ['Convert columns to data', 'Clear and reset']);
await gu.changeBehavior('Convert columns to data');
assert.equal(await gu.columnBehavior(), 'Data Columns');
await selectColumns('Test1');
assert.equal(await gu.columnBehavior(), 'Data Column');
// Now make them all formula columns
await gu.sendActions([
['ModifyColumn', 'Table1', 'Test1', {formula: '1', isFormula: true}],
['ModifyColumn', 'Table1', 'Test2', {formula: '2', isFormula: true}],
['ModifyColumn', 'Table1', 'Test3', {formula: '3', isFormula: true}],
]);
await selectColumns('Test1', 'Test3');
assert.equal(await gu.columnBehavior(), 'Formula Columns');
// Convert them to data
assert.deepEqual(await gu.availableBehaviorOptions(), ['Convert columns to data', 'Clear and reset']);
await gu.changeBehavior('Convert columns to data');
assert.equal(await gu.columnBehavior(), 'Data Columns');
await selectColumns('Test1');
assert.equal(await gu.columnBehavior(), 'Data Column');
// Test that data stays.
assert.equal(await gu.getCell('Test1', 1).getText(), '1');
assert.equal(await gu.getCell('Test2', 1).getText(), '2');
assert.equal(await gu.getCell('Test3', 1).getText(), '3');
});
it('should disable formula editor for multiple columns', async () => {
await gu.sendActions([
['ModifyColumn', 'Table1', 'Test1', {formula: '1', isFormula: true}],
]);
await selectColumns('Test1');
assert.isFalse(await formulaEditorDisabled());
await selectColumns('Test1', 'Test3');
assert.isTrue(await formulaEditorDisabled());
await selectColumns('Test1');
assert.isFalse(await formulaEditorDisabled());
});
it('should disable column id and other unique options', async () => {
await selectColumns('Test1', 'Test3');
assert.isTrue(await colIdDisabled());
assert.isTrue(await deriveDisabled());
assert.isTrue(await labelDisabled());
assert.isTrue(await transformSectionDisabled());
assert.isTrue(await setTriggerDisabled());
assert.isTrue(await setDataDisabled());
assert.isTrue(await setFormulaDisabled());
assert.isTrue(await addConditionDisabled());
assert.isFalse(await columnTypeDisabled());
await selectColumns('Test1');
assert.isTrue(await colIdDisabled());
assert.isFalse(await deriveDisabled());
assert.isFalse(await labelDisabled());
assert.isFalse(await setTriggerDisabled());
assert.isTrue(await transformSectionDisabled());
assert.isFalse(await addConditionDisabled());
assert.isFalse(await columnTypeDisabled());
// Make one column a data column, to disable type selector.
await selectColumns('Test1');
await gu.changeBehavior('Convert column to data');
assert.isFalse(await transformSectionDisabled());
await selectColumns('Test1', 'Test3');
assert.isTrue(await columnTypeDisabled());
// Make sure that a colId disabled state is not altered accidentally.
await selectColumns('Test1');
assert.isTrue(await colIdDisabled());
await toggleDerived();
assert.isFalse(await colIdDisabled());
await selectColumns('Test1', 'Test2');
assert.isTrue(await colIdDisabled());
await selectColumns('Test1');
assert.isFalse(await colIdDisabled());
await toggleDerived();
assert.isTrue(await colIdDisabled());
});
it('should change column type for mixed behaviors', async () => {
// For empty columns
await selectColumns('Test1', 'Test3');
assert.isFalse(await columnTypeDisabled());
// Check every column type
for (const type of types) {
await gu.setType(type);
await gu.checkForErrors();
await selectColumns('Test1');
assert.equal(await gu.getType(), type);
await selectColumns('Test1', 'Test3');
assert.equal(await gu.getType(), type);
}
// For mix of empty and formulas
await gu.sendActions([
['ModifyColumn', 'Table1', 'Test2', {formula: '2', isFormula: true}],
]);
await selectColumns('Test1', 'Test3');
assert.isFalse(await columnTypeDisabled());
for (const type of types) {
await gu.setType(type);
await gu.checkForErrors();
await selectColumns('Test1');
assert.equal(await gu.getType(), type);
await selectColumns('Test1', 'Test3');
assert.equal(await gu.getType(), type);
}
// For mix of empty and formulas and data
await gu.sendActions([
// We are changing first column, so the selection will start from data column.
['ModifyColumn', 'Table1', 'Test1', {type: 'Choice'}],
]);
await selectColumns('Test1', 'Test3');
assert.isFalse(await columnTypeDisabled());
for (const type of types) {
await gu.setType(type);
await gu.checkForErrors();
await selectColumns('Test1');
assert.equal(await gu.getType(), type);
await selectColumns('Test1', 'Test3');
assert.equal(await gu.getType(), type);
}
// Shows proper label for mixed types
await selectColumns('Test1');
await gu.setType('Numeric');
await selectColumns('Test2');
await gu.setType('Toggle');
await selectColumns('Test1', 'Test3');
assert.equal(await gu.getType(), 'Mixed types');
});
});
describe("color tests", function() {
before(async function() {
await addAnyColumn('Test1');
await addAnyColumn('Test2');
});
after(async function() {
await removeColumn('Test1');
await removeColumn('Test2');
});
it('should change cell background for multiple columns', async () => {
await selectColumns('Test1', 'Test2');
assert.equal(await cellColorLabel(), "Default cell style");
await gu.openCellColorPicker();
await gu.setFillColor(blue);
await gu.assertFillColor(await gu.getCell('Test1', 1).find(".field_clip"), blue);
await gu.assertFillColor(await gu.getCell('Test2', 1).find(".field_clip"), blue);
await driver.sendKeys(Key.ESCAPE);
await gu.assertFillColor(await gu.getCell('Test1', 1).find(".field_clip"), transparent);
await gu.assertFillColor(await gu.getCell('Test2', 1).find(".field_clip"), transparent);
assert.equal(await cellColorLabel(), "Default cell style");
// Change one cell to red
await selectColumns('Test1');
await gu.openCellColorPicker();
await gu.setFillColor(red);
await driver.sendKeys(Key.ENTER);
await gu.waitForServer();
await gu.assertFillColor(await gu.getCell('Test1', 1).find(".field_clip"), red);
await gu.assertFillColor(await gu.getCell('Test2', 1).find(".field_clip"), transparent);
// Check label and colors for multicolumn selection.
await selectColumns('Test1', 'Test2');
assert.equal(await cellColorLabel(), "Mixed style");
// Try to change to blue, but press escape.
await gu.openCellColorPicker();
await gu.setFillColor(blue);
await gu.assertFillColor(await gu.getCell('Test1', 1).find(".field_clip"), blue);
await gu.assertFillColor(await gu.getCell('Test2', 1).find(".field_clip"), blue);
await driver.sendKeys(Key.ESCAPE);
await gu.assertFillColor(await gu.getCell('Test1', 1).find(".field_clip"), red);
await gu.assertFillColor(await gu.getCell('Test2', 1).find(".field_clip"), transparent);
// Change both colors.
await gu.openCellColorPicker();
await gu.setFillColor(blue);
await driver.sendKeys(Key.ENTER);
await gu.waitForServer();
assert.equal(await cellColorLabel(), "Default cell style");
await gu.assertFillColor(await gu.getCell('Test1', 1).find(".field_clip"), blue);
await gu.assertFillColor(await gu.getCell('Test2', 1).find(".field_clip"), blue);
// Make sure they stick.
await driver.navigate().refresh();
await gu.waitForDocToLoad();
assert.equal(await cellColorLabel(), "Default cell style");
await gu.assertFillColor(await gu.getCell('Test1', 1).find(".field_clip"), blue);
await gu.assertFillColor(await gu.getCell('Test2', 1).find(".field_clip"), blue);
});
it('should change header background for multiple columns', async () => {
const defaultHeaderFillColor = 'rgba(247, 247, 247, 1)';
await selectColumns('Test1', 'Test2');
assert.equal(await headerColorLabel(), "Default header style");
await gu.openHeaderColorPicker();
await gu.setFillColor(blue);
await gu.assertHeaderFillColor('Test1', blue);
await gu.assertHeaderFillColor('Test2', blue);
await driver.sendKeys(Key.ESCAPE);
await gu.assertHeaderFillColor('Test1', defaultHeaderFillColor);
await gu.assertHeaderFillColor('Test2', defaultHeaderFillColor);
assert.equal(await headerColorLabel(), "Default header style");
// Change one header to red
await selectColumns('Test1');
await gu.openHeaderColorPicker();
await gu.setFillColor(red);
await driver.sendKeys(Key.ENTER);
await gu.waitForServer();
await gu.assertHeaderFillColor('Test1', red);
await gu.assertHeaderFillColor('Test2', defaultHeaderFillColor);
// Check label and colors for multicolumn selection.
await selectColumns('Test1', 'Test2');
assert.equal(await headerColorLabel(), "Mixed style");
// Try to change to blue, but press escape.
await gu.openHeaderColorPicker();
await gu.setFillColor(blue);
await gu.assertHeaderFillColor('Test1', blue);
await gu.assertHeaderFillColor('Test2', blue);
await driver.sendKeys(Key.ESCAPE);
await gu.assertHeaderFillColor('Test1', red);
await gu.assertHeaderFillColor('Test2', defaultHeaderFillColor);
// Change both colors.
await gu.openHeaderColorPicker();
await gu.setFillColor(blue);
await driver.sendKeys(Key.ENTER);
await gu.waitForServer();
assert.equal(await headerColorLabel(), "Default header style");
await gu.assertHeaderFillColor('Test1', blue);
await gu.assertHeaderFillColor('Test2', blue);
// Make sure they stick.
await driver.navigate().refresh();
await gu.waitForDocToLoad();
assert.equal(await headerColorLabel(), "Default header style");
await gu.assertHeaderFillColor('Test1', blue);
await gu.assertHeaderFillColor('Test2', blue);
});
});
describe(`test for Integer column`, function() {
beforeEach(async () => {
await gu.addColumn('Left', 'Integer');
});
afterEach(async function() {
if (this.currentTest?.state === "passed") {
await removeColumn('Left');
await removeColumn('Right');
}
});
for (const right of types) {
it(`should work with ${right} column`, async function() {
await gu.addColumn('Right', right);
await selectColumns('Left', 'Right');
if (['Toggle', 'Date', 'DateTime', 'Attachment'].includes(right)) {
assert.equal(await wrapDisabled(), true);
} else {
assert.equal(await wrapDisabled(), false);
assert.equal(await wrap(), false);
}
if (['Toggle', 'Attachment'].includes(right)) {
assert.equal(await alignmentDisabled(), true);
} else {
assert.equal(await alignmentDisabled(), false);
}
if (['Integer', 'Numeric'].includes(right)) {
assert.equal(await alignment(), 'right');
} else if (['Toggle', 'Attachment'].includes(right)) {
// With toggle, alignment is unset.
} else {
assert.equal(await alignment(), null);
}
if (['Toggle', 'Attachment'].includes(right)) {
// omit tests for alignment
} else {
await testAlignment();
}
if (['Toggle', 'Date', 'DateTime', 'Attachment'].includes(right)) {
// omit tests for wrap
} else if (['Choice'].includes(right)) {
// Choice column doesn't support wrapping.
await testSingleWrapping();
} else {
await testWrapping();
}
await selectColumns('Left', 'Right');
if (['Integer', 'Numeric'].includes(right)) {
// Test number formatting, be default nothing should be set.
assert.isFalse(await numberFormattingDisabled());
assert.isNull(await numMode());
for (const mode of ['decimal', 'currency', 'percent', 'exp']) {
await selectColumns('Left', 'Right');
await numMode(mode as any);
assert.equal(await numMode(), mode);
await selectColumns('Left');
assert.equal(await numMode(), mode);
await selectColumns('Right');
assert.equal(await numMode(), mode);
await selectColumns('Left', 'Right');
assert.equal(await numMode(), mode);
}
await selectColumns('Left', 'Right');
await numMode('decimal');
const decimalsProps = [minDecimals, maxDecimals];
for (const decimals of decimalsProps) {
await selectColumns('Left', 'Right');
await decimals(5);
assert.equal(await decimals(), 5);
await selectColumns('Left');
assert.equal(await decimals(), 5);
await selectColumns('Right');
assert.equal(await decimals(), 5);
// Set different decimals for left and right.
await selectColumns('Left');
await decimals(2);
await selectColumns('Right');
await decimals(4);
await selectColumns('Left', 'Right');
assert.isNaN(await decimals()); // default value that is empty
// Setting it will reset both.
await decimals(8);
await selectColumns('Left');
assert.equal(await decimals(), 8);
await selectColumns('Right');
assert.equal(await decimals(), 8);
}
// Clearing will clear both, but only for Numeric columns, Integer
// has a default value of 0, that will be set when element is cleared.
// TODO: This looks like a buggy behavior, and should be fixed.
await selectColumns('Left', 'Right');
await minDecimals(null);
await selectColumns('Left');
assert.equal(await minDecimals(), 0);
await selectColumns('Right');
if (right === 'Numeric') {
assert.isNaN(await minDecimals());
} else {
assert.equal(await minDecimals(), 0);
}
// Clearing max value works as expected.
await selectColumns('Left', 'Right');
await maxDecimals(null);
await selectColumns('Left');
assert.isNaN(await maxDecimals()); // default value that is empty
await selectColumns('Right');
assert.isNaN(await maxDecimals()); // default value that is empty
} else {
assert.isTrue(await numberFormattingDisabled());
}
});
}
});
for (const left of ['Choice', 'Choice List']) {
describe(`test for ${left} column`, function() {
beforeEach(async () => {
await gu.addColumn('Left', left);
});
afterEach(async function() {
if (this.currentTest?.state === "passed") {
await removeColumn('Left');
await removeColumn('Right');
}
});
for (const right of types) {
it(`should work with ${right} column`, async function() {
await gu.addColumn('Right', right);
await selectColumns('Left', 'Right');
if (['Choice', 'Choice List'].includes(right)) {
await testChoices();
} else {
assert.isTrue(await choiceEditorDisabled());
}
if (left === 'Choice List') {
if (['Toggle', 'Date', 'DateTime', 'Attachment'].includes(right)) {
assert.equal(await wrapDisabled(), true);
} else {
assert.equal(await wrapDisabled(), false);
assert.equal(await wrap(), false);
}
}
if (['Toggle', 'Attachment'].includes(right)) {
assert.equal(await alignmentDisabled(), true);
} else {
assert.equal(await alignmentDisabled(), false);
}
if (['Integer', 'Numeric'].includes(right)) {
assert.equal(await alignment(), null);
} else if (['Toggle', 'Attachment'].includes(right)) {
// With toggle, alignment is unset.
} else {
assert.equal(await alignment(), 'left');
}
if (['Toggle', 'Attachment'].includes(right)) {
// omit tests for alignment
} else {
await testAlignment();
}
// Choice doesn't support wrapping.
if (left === 'Choice List') {
if (['Toggle', 'Date', 'DateTime', 'Attachment'].includes(right)) {
// omit tests for wrap
} else if (['Choice'].includes(right)) {
// Choice column doesn't support wrapping.
await testSingleWrapping();
} else {
await testWrapping();
}
}
});
}
});
}
for (const left of ['Reference', 'Reference List']) {
describe(`test for ${left} column`, function() {
beforeEach(async () => {
await gu.addColumn('Left', left);
});
afterEach(async function() {
if (this.currentTest?.state === "passed") {
await removeColumn('Left');
await removeColumn('Right');
}
});
// Test for types that matter (have different set of defaults).
for (const right of ['Any', 'Reference', 'Reference List', 'Toggle', 'Integer']) {
it(`should work with ${right} column`, async function() {
await gu.addColumn('Right', right);
await selectColumns('Left', 'Right');
assert.isTrue(await refControlsDisabled(), "Reference controls should be disabled");
await commonTestsForAny(right);
});
}
});
}
describe(`test for Date column`, function() {
beforeEach(async () => {
await gu.addColumn('Left', 'Date');
});
afterEach(async function() {
if (this.currentTest?.state === "passed") {
await removeColumn('Left');
await removeColumn('Right');
}
});
for (const right of types) {
it(`should work with ${right} column`, async function() {
await gu.addColumn('Right', right);
await selectColumns('Left', 'Right');
if (['Date', 'DateTime'].includes(right)) {
assert.isFalse(await dateFormatDisabled());
} else {
assert.isTrue(await dateFormatDisabled());
}
if (['Toggle', 'Attachment'].includes(right)) {
assert.equal(await alignmentDisabled(), true);
} else {
assert.equal(await alignmentDisabled(), false);
}
if (['Integer', 'Numeric'].includes(right)) {
assert.equal(await alignment(), null);
} else if (['Toggle', 'Attachment'].includes(right)) {
// With toggle, alignment is unset.
} else {
assert.equal(await alignment(), 'left');
}
if (['Toggle', 'Attachment'].includes(right)) {
// omit tests for alignment
} else {
await testAlignment();
}
});
if (['Date', 'DateTime'].includes(right)) {
it(`should change format with ${right} column`, async function() {
await gu.addColumn('Right', right);
await selectColumns('Left', 'Right');
assert.isFalse(await dateFormatDisabled());
// Test for mixed format.
await selectColumns('Left');
await dateFormat('MM/DD/YY');
await selectColumns('Left', 'Right');
assert.equal(await dateFormat(), 'Mixed format');
// Test that both change when format is changed.
for (const mode of ['MM/DD/YY', 'DD-MM-YYYY']) {
await dateFormat(mode);
await selectColumns('Left');
assert.equal(await dateFormat(), mode);
await selectColumns('Right');
assert.equal(await dateFormat(), mode);
await selectColumns('Left', 'Right');
assert.equal(await dateFormat(), mode);
}
// Test that custom format works
await gu.setCustomDateFormat('MM');
await selectColumns('Left');
assert.equal(await gu.getDateFormat(), "MM");
await selectColumns('Right');
assert.equal(await gu.getDateFormat(), "MM");
await selectColumns('Left', 'Right');
assert.equal(await gu.getDateFormat(), "MM");
// Test that we can go back to normal format.
await gu.setDateFormat("MM/DD/YY");
assert.isFalse(await customDateFormatVisible());
await selectColumns('Left');
assert.isFalse(await customDateFormatVisible());
assert.equal(await gu.getDateFormat(), "MM/DD/YY");
await selectColumns('Right');
assert.isFalse(await customDateFormatVisible());
assert.equal(await gu.getDateFormat(), "MM/DD/YY");
});
}
}
});
describe(`test for Toggle column`, function() {
beforeEach(async () => {
await gu.addColumn('Left', 'Toggle');
});
afterEach(async function() {
if (this.currentTest?.state === "passed") {
await removeColumn('Left');
await removeColumn('Right');
}
});
for (const right of types) {
it(`should work with ${right} column`, async function() {
await gu.addColumn('Right', right);
// There is not match to test
if (right === 'Toggle') {
await selectColumns('Left', 'Right');
assert.isFalse(await widgetTypeDisabled());
// Test for mixed format.
await selectColumns('Left');
await gu.setFieldWidgetType('TextBox');
await selectColumns('Right');
await gu.setFieldWidgetType('CheckBox');
await selectColumns('Left', 'Right');
assert.equal(await gu.getFieldWidgetType(), 'Mixed format');
// Test that both change when format is changed.
for (const mode of ['TextBox', 'CheckBox', 'Switch']) {
await gu.setFieldWidgetType(mode);
await selectColumns('Left');
assert.equal(await gu.getFieldWidgetType(), mode);
await selectColumns('Right');
assert.equal(await gu.getFieldWidgetType(), mode);
await selectColumns('Left', 'Right');
assert.equal(await gu.getFieldWidgetType(), mode);
}
} else {
await selectColumns('Left', 'Right');
assert.isTrue(await widgetTypeDisabled());
}
});
}
});
// Any and Text column are identical in terms of formatting.
for (const left of ['Text', 'Any']) {
describe(`test for ${left} column`, function() {
beforeEach(async () => {
await gu.addColumn('Left', left);
});
afterEach(async function() {
if (this.currentTest?.state === "passed") {
await removeColumn('Left');
await removeColumn('Right');
}
});
for (const right of types) {
it(`should work with ${right} column`, async function() {
await gu.addColumn('Right', right);
await selectColumns('Left', 'Right');
if (left === 'Text') {
if (right === 'Text') {
assert.isFalse(await widgetTypeDisabled());
} else {
assert.isTrue(await widgetTypeDisabled());
}
}
await commonTestsForAny(right);
});
}
});
}
describe(`test for Attachment column`, function() {
beforeEach(async () => {
await gu.addColumn('Left', 'Attachment');
});
afterEach(async function() {
if (this.currentTest?.state === "passed") {
await removeColumn('Left');
await removeColumn('Right');
}
});
// Test for types that matter (have different set of defaults).
for (const right of ['Any', 'Attachment']) {
it(`should work with ${right} column`, async function() {
await gu.addColumn('Right', right);
await selectColumns('Left', 'Right');
if (right !== 'Attachment') {
assert.isTrue(await sliderDisabled());
} else {
assert.isFalse(await sliderDisabled());
// Test it works as expected
await slider(16); // min value
assert.equal(await slider(), 16);
await selectColumns('Left');
assert.equal(await slider(), 16);
await selectColumns('Right');
assert.equal(await slider(), 16);
// Set max for Right column, left still has minium
await slider(96); // max value
await selectColumns('Left', 'Right');
// When mixed, slider is in between.
assert.equal(await slider(), (96 - 16) / 2 + 16);
}
});
}
});
});
async function numModeDisabled() {
return await hasDisabledSuffix(".test-numeric-mode");
}
async function numSignDisabled() {
return await hasDisabledSuffix(".test-numeric-sign");
}
async function decimalsDisabled() {
const min = await hasDisabledSuffix(".test-numeric-min-decimals");
const max = await hasDisabledSuffix(".test-numeric-max-decimals");
return min && max;
}
async function numberFormattingDisabled() {
return (await numModeDisabled()) && (await numSignDisabled()) && (await decimalsDisabled());
}
async function testWrapping(colA: string = 'Left', colB: string = 'Right') {
await selectColumns(colA, colB);
await wrap(true);
assert.isTrue(await wrap());
assert.isTrue(await colWrap(colA), `${colA} should be wrapped`);
assert.isTrue(await colWrap(colB), `${colB} should be wrapped`);
await wrap(false);
assert.isFalse(await wrap());
assert.isFalse(await colWrap(colA), `${colA} should not be wrapped`);
assert.isFalse(await colWrap(colB), `${colB} should not be wrapped`);
// Test common wrapping.
await selectColumns(colA);
await wrap(true);
await selectColumns(colB);
await wrap(false);
await selectColumns(colA, colB);
assert.isFalse(await wrap());
await selectColumns(colB);
await wrap(true);
assert.isTrue(await wrap());
}
async function testSingleWrapping(colA: string = 'Left', colB: string = 'Right') {
await selectColumns(colA, colB);
await wrap(true);
assert.isTrue(await wrap());
assert.isTrue(await colWrap(colA), `${colA} should be wrapped`);
await wrap(false);
assert.isFalse(await wrap());
assert.isFalse(await colWrap(colA), `${colA} should not be wrapped`);
}
async function testChoices(colA: string = 'Left', colB: string = 'Right') {
await selectColumns(colA, colB);
assert.equal(await choiceEditor.label(), "No choices configured");
// Add two choices elements.
await choiceEditor.edit();
await choiceEditor.add("one");
await choiceEditor.add("two");
await choiceEditor.save();
// Check that both column have them.
await selectColumns(colA);
assert.deepEqual(await choiceEditor.read(), ['one', 'two']);
await selectColumns(colB);
assert.deepEqual(await choiceEditor.read(), ['one', 'two']);
// Check that they are shown normally and not as mixed.
await selectColumns(colA, colB);
assert.deepEqual(await choiceEditor.read(), ['one', 'two']);
// Modify only one.
await selectColumns(colA);
await choiceEditor.edit();
await choiceEditor.add("three");
await choiceEditor.save();
// Test that we now have a mix.
await selectColumns(colA, colB);
assert.equal(await choiceEditor.label(), "Mixed configuration");
// Edit them, but press cancel.
await choiceEditor.reset();
await choiceEditor.cancel();
// Test that we still have a mix.
assert.equal(await choiceEditor.label(), "Mixed configuration");
await selectColumns(colA);
assert.deepEqual(await choiceEditor.read(), ['one', 'two', 'three']);
await selectColumns(colB);
assert.deepEqual(await choiceEditor.read(), ['one', 'two']);
// Reset them back and add records to the table.
await selectColumns(colA, colB);
await choiceEditor.reset();
await choiceEditor.add("one");
await choiceEditor.add("two");
await choiceEditor.save();
await gu.getCell(colA, 1).click();
await gu.sendKeys("one", Key.ENTER);
// If this is choice list we need one more enter.
if (await getColumnType() === 'Choice List') {
await gu.sendKeys(Key.ENTER);
}
await gu.waitForServer();
await gu.getCell(colB, 1).click();
await gu.sendKeys("one", Key.ENTER);
if (await getColumnType() === 'Choice List') {
await gu.sendKeys(Key.ENTER);
}
await gu.waitForServer();
// Rename one of the choices.
await selectColumns(colA, colB);
const undo = await gu.begin();
await choiceEditor.edit();
await choiceEditor.rename("one", "one renamed");
await choiceEditor.save();
// Test if grid is ok.
assert.equal(await gu.getCell(colA, 1).getText(), 'one renamed');
assert.equal(await gu.getCell(colB, 1).getText(), 'one renamed');
await undo();
assert.equal(await gu.getCell(colA, 1).getText(), 'one');
assert.equal(await gu.getCell(colB, 1).getText(), 'one');
// Test that colors are also treated as different.
await selectColumns(colA, colB);
assert.deepEqual(await choiceEditor.read(), ['one', 'two']);
await selectColumns(colA);
await choiceEditor.edit();
await choiceEditor.color("one", red);
await choiceEditor.save();
await selectColumns(colA, colB);
assert.equal(await choiceEditor.label(), "Mixed configuration");
}
const choiceEditor = {
async hasReset() {
return (await driver.find(".test-choice-list-entry-edit").getText()) === "Reset";
},
async reset() {
await driver.find(".test-choice-list-entry-edit").click();
},
async label() {
return await driver.find(".test-choice-list-entry-row").getText();
},
async add(label: string) {
await driver.find(".test-tokenfield-input").click();
await driver.find(".test-tokenfield-input").clear();
await gu.sendKeys(label, Key.ENTER);
},
async rename(label: string, label2: string) {
const entry = await driver.findWait(`.test-choice-list-entry .test-token-label[value='${label}']`, 100);
await entry.click();
await gu.sendKeys(label2);
await gu.sendKeys(Key.ENTER);
},
async color(token: string, color: string) {
const label = await driver.findWait(`.test-choice-list-entry .test-token-label[value='${token}']`, 100);
await label.findClosest(".test-tokenfield-token").find(".test-color-button").click();
await gu.setFillColor(color);
await gu.sendKeys(Key.ENTER);
},
async read() {
return await driver.findAll(".test-choice-list-entry-label", e => e.getText());
},
async edit() {
await this.reset();
},
async save() {
await driver.find(".test-choice-list-entry-save").click();
await gu.waitForServer();
},
async cancel() {
await driver.find(".test-choice-list-entry-cancel").click();
}
};
async function testAlignment(colA: string = 'Left', colB: string = 'Right') {
await selectColumns(colA, colB);
await alignment('left');
assert.equal(await colAlignment(colA), 'left', `${colA} alignment should be left`);
assert.equal(await colAlignment(colB), 'left', `${colB} alignment should be left`);
assert.equal(await alignment(), 'left', 'Alignment should be left');
await alignment('center');
assert.equal(await colAlignment(colA), 'center', `${colA} alignment should be center`);
assert.equal(await colAlignment(colB), 'center', `${colB} alignment should be center`);
assert.equal(await alignment(), 'center', 'Alignment should be center');
await alignment('right');
assert.equal(await colAlignment(colA), 'right', `${colA} alignment should be right`);
assert.equal(await colAlignment(colB), 'right', `${colB} alignment should be right`);
assert.equal(await alignment(), 'right', 'Alignment should be right');
// Now align first column to left, and second to right.
await selectColumns(colA);
await alignment('left');
await selectColumns(colB);
await alignment('right');
// And test we don't have alignment set.
await selectColumns(colA, colB);
assert.isNull(await alignment());
// Now change alignment of first column to right, so that we have common alignment.
await selectColumns(colA);
await alignment('right');
await selectColumns(colA, colB);
assert.equal(await alignment(), 'right');
}
async function colWrap(col: string) {
const cell = await gu.getCell(col, 1).find(".field_clip");
let hasTextWrap = await cell.matches("[class*=text_wrapping]");
if (!hasTextWrap) {
// We can be in a choice column, where wrapping is done differently.
hasTextWrap = await cell.matches("[class*=-wrap]");
}
return hasTextWrap;
}
async function colAlignment(col: string) {
// TODO: unify how widgets are aligned.
let cell = await gu.getCell(col, 1).find(".field_clip");
let style = await cell.getAttribute('style');
if (!style) {
// We might have a choice column, use flex attribute of first child;
cell = await gu.getCell(col, 1).find(".field_clip > div");
style = await cell.getAttribute('style');
// Get justify-content style
const match = style.match(/justify-content: ([\w-]+)/);
if (!match) { return null; }
switch (match[1]) {
case 'left': return 'left';
case 'center': return 'center';
case 'flex-end': return 'right';
}
}
let match = style.match(/text-align: (\w+)/);
if (!match) {
// We might be in a choice list column, so check if we have a flex attribute.
match = style.match(/justify-content: ([\w-]+)/);
}
if (!match) { return null; }
return match[1] === 'flex-end' ? 'right' : match[1];
}
async function wrap(state?: boolean) {
const buttons = await driver.findAll(".test-tb-wrap-text .test-select-button");
if (buttons.length !== 1) {
assert.isUndefined(state, "Can't set wrap");
return undefined;
}
if (await buttons[0].matches('[class*=-selected]')) {
if (state === false) {
await buttons[0].click();
await gu.waitForServer();
return false;
}
return true;
}
if (state === true) {
await buttons[0].click();
await gu.waitForServer();
return true;
}
return false;
}
// Many controls works the same as any column for wrapping and alignment.
async function commonTestsForAny(right: string) {
await selectColumns('Left', 'Right');
if (['Toggle', 'Date', 'DateTime', 'Attachment'].includes(right)) {
assert.equal(await wrapDisabled(), true);
} else {
assert.equal(await wrapDisabled(), false);
assert.equal(await wrap(), false);
}
if (['Toggle', 'Attachment'].includes(right)) {
assert.equal(await alignmentDisabled(), true);
} else {
assert.equal(await alignmentDisabled(), false);
}
if (['Integer', 'Numeric'].includes(right)) {
assert.equal(await alignment(), null);
} else if (['Toggle', 'Attachment'].includes(right)) {
// With toggle, alignment is unset.
} else {
assert.equal(await alignment(), 'left');
}
if (['Toggle', 'Attachment'].includes(right)) {
// omit tests for alignment
} else {
await testAlignment();
}
if (['Toggle', 'Date', 'DateTime', 'Attachment'].includes(right)) {
// omit tests for wrap
} else if (['Choice'].includes(right)) {
// Choice column doesn't support wrapping.
await testSingleWrapping();
} else {
await testWrapping();
}
}
async function selectColumns(col1: string, col2?: string) {
// Clear selection in grid.
await driver.executeScript("gristDocPageModel.gristDoc.get().currentView.get().clearSelection();");
if (col2 === undefined) {
await gu.selectColumn(col1);
} else {
// First make sure we start with col1 selected.
await gu.selectColumnRange(col1, col2);
}
}
async function alignmentDisabled() {
return await hasDisabledSuffix(".test-alignment-select");
}
async function choiceEditorDisabled() {
return await hasDisabledSuffix(".test-choice-list-entry");
}
async function alignment(value?: 'left' | 'right' | 'center') {
const buttons = await driver.findAll(".test-alignment-select .test-select-button");
if (buttons.length !== 3) {
assert.isUndefined(value, "Can't set alignment");
return undefined;
}
if (value) {
if (value === 'left') {
await buttons[0].click();
}
if (value === 'center') {
await buttons[1].click();
}
if (value === 'right') {
await buttons[2].click();
}
await gu.waitForServer();
return;
}
if (await buttons[0].matches('[class*=-selected]')) {
return 'left';
}
if (await buttons[1].matches('[class*=-selected]')) {
return 'center';
}
if (await buttons[2].matches('[class*=-selected]')) {
return 'right';
}
return null;
}
async function dateFormatDisabled() {
const format = await driver.find('[data-test-id=Widget_dateFormat]');
return await format.matches(".disabled");
}
async function customDateFormatVisible() {
const control = driver.find('[data-test-id=Widget_dateCustomFormat]');
return await control.isPresent();
}
async function dateFormat(format?: string) {
if (!format) {
return await gu.getDateFormat();
}
await driver.find("[data-test-id=Widget_dateFormat]").click();
await driver.findContent('.test-select-menu li', gu.exactMatch(format)).click();
await gu.waitForServer();
}
async function widgetTypeDisabled() {
// Maybe we have selectbox
const selectbox = await driver.findAll(".test-fbuilder-widget-select .test-select-open");
if (selectbox.length === 1) {
return await selectbox[0].matches('.disabled');
}
const buttons = await driver.findAll(".test-fbuilder-widget-select > div");
const allDisabled = await Promise.all(buttons.map(button => button.matches('[class*=-disabled]')));
return allDisabled.every(disabled => disabled) && allDisabled.length > 0;
}
async function labelDisabled() {
return (await driver.find(".test-field-label").getAttribute('readonly')) === 'true';
}
async function colIdDisabled() {
return (await driver.find(".test-field-col-id").getAttribute('readonly')) === 'true';
}
async function hasDisabledSuffix(selector: string) {
return (await driver.find(selector).matches('[class*=-disabled]'));
}
async function hasDisabledClass(selector: string) {
return (await driver.find(selector).matches('.disabled'));
}
async function deriveDisabled() {
return await hasDisabledSuffix(".test-field-derive-id");
}
async function toggleDerived() {
await driver.find(".test-field-derive-id").click();
await gu.waitForServer();
}
async function wrapDisabled() {
return (await driver.find(".test-tb-wrap-text > div").matches('[class*=disabled]'));
}
async function columnTypeDisabled() {
return await hasDisabledClass(".test-fbuilder-type-select .test-select-open");
}
async function getColumnType() {
return await driver.find(".test-fbuilder-type-select").getText();
}
async function setFormulaDisabled() {
return (await driver.find(".test-field-set-formula").getAttribute('disabled')) === 'true';
}
async function formulaEditorDisabled() {
return await hasDisabledSuffix(".formula_field_sidepane");
}
async function setTriggerDisabled() {
return (await driver.find(".test-field-set-trigger").getAttribute('disabled')) === 'true';
}
async function refControlsDisabled() {
return (await hasDisabledClass(".test-fbuilder-ref-table-select .test-select-open")) &&
(await hasDisabledClass(".test-fbuilder-ref-col-select .test-select-open"));
}
async function setDataDisabled() {
return (await driver.find(".test-field-set-data").getAttribute('disabled')) === 'true';
}
async function transformSectionDisabled() {
return (await driver.find(".test-fbuilder-edit-transform").getAttribute('disabled')) === 'true';
}
async function addConditionDisabled() {
return (await driver.find(".test-widget-style-add-conditional-style").getAttribute('disabled')) === 'true';
}
async function addAnyColumn(name: string) {
await gu.sendActions([
['AddVisibleColumn', 'Table1', name, {}]
]);
await gu.waitForServer();
}
async function removeColumn(...names: string[]) {
await gu.sendActions([
...names.map(name => (['RemoveColumn', 'Table1', name]))
]);
await gu.waitForServer();
}
function maxDecimals(value?: number|null) {
return modDecimals(".test-numeric-max-decimals input", value);
}
function minDecimals(value?: number|null) {
return modDecimals(".test-numeric-min-decimals input", value);
}
async function modDecimals(selector: string, value?: number|null) {
const element = await driver.find(selector);
if (value === undefined) {
return parseInt(await element.value());
} else {
await element.click();
if (value !== null) {
await element.sendKeys(value.toString());
} else {
await element.doClear();
}
await driver.sendKeys(Key.ENTER);
await gu.waitForServer();
}
}
async function numMode(value?: 'currency' | 'percent' | 'exp' | 'decimal') {
const mode = await driver.findAll(".test-numeric-mode");
if (value !== undefined) {
if (mode.length === 0) {
assert.fail("No number format");
}
if (value === 'currency') {
if (await numMode() !== 'currency') {
await driver.findContent('.test-numeric-mode .test-select-button', /\$/).click();
}
} else if (value === 'percent') {
if (await numMode() !== 'percent') {
await driver.findContent('.test-numeric-mode .test-select-button', /%/).click();
}
} else if (value === 'decimal') {
if (await numMode() !== 'decimal') {
await driver.findContent('.test-numeric-mode .test-select-button', /,/).click();
}
} else if (value === 'exp') {
if (await numMode() !== 'exp') {
await driver.findContent('.test-numeric-mode .test-select-button', /Exp/).click();
}
}
await gu.waitForServer();
}
if (mode.length === 0) {
return undefined;
}
const curr = await driver.findContent('.test-numeric-mode .test-select-button', /\$/).matches('[class*=-selected]');
if (curr) {
return 'currency';
}
const decimal = await driver.findContent('.test-numeric-mode .test-select-button', /,/).matches('[class*=-selected]');
if (decimal) {
return 'decimal';
}
const percent = await driver.findContent('.test-numeric-mode .test-select-button', /%/).matches('[class*=-selected]');
if (percent) {
return 'percent';
}
const exp = await driver.findContent('.test-numeric-mode .test-select-button', /Exp/).matches('[class*=-selected]');
if (exp) {
return 'exp';
}
return null;
}
async function sliderDisabled() {
return (await driver.find(".test-pw-thumbnail-size").getAttribute('disabled')) === 'true';
}
async function slider(value?: number) {
if (value !== undefined) {
await driver.executeScript(`
document.querySelector('.test-pw-thumbnail-size').value = '${value}';
document.querySelector('.test-pw-thumbnail-size').dispatchEvent(new Event('change'));
`);
await gu.waitForServer();
}
return parseInt(await driver.find(".test-pw-thumbnail-size").getAttribute('value'));
}
async function cellColorLabel() {
// Text actually contains T symbol before.
const label = await driver.find(".test-cell-color-select .test-color-select").getText();
return label.replace(/^T/, '').trim();
}
async function headerColorLabel() {
// Text actually contains T symbol before.
const label = await driver.find(".test-header-color-select .test-color-select").getText();
return label.replace(/^T/, '').trim();
}