gristlabs_grist-core/test/nbrowser/MultiColumn.ts
Jarosław Sadziński 8be920dd25 (core) Multi-column configuration
Summary:
Creator panel allows now to edit multiple columns at once
for some options that are common for them. Options that
are not common are disabled.

List of options that can be edited for multiple columns:
- Column behavior (but limited to empty/formula columns)
- Alignment and wrapping
- Default style
- Number options (for numeric columns)
- Column types (but only for empty/formula columns)

If multiple columns of the same type are selected, most of
the options are available to change, except formula, trigger formula
and conditional styles.

Editing column label or column id is disabled by default for multiple
selection.

Not related: some tests were fixed due to the change in the column label
and id widget in grist-core (disabled attribute was replaced by readonly).

Test Plan: Updated and new tests.

Reviewers: georgegevoian

Reviewed By: georgegevoian

Differential Revision: https://phab.getgrist.com/D3598
2022-10-17 09:51:19 +02:00

1313 lines
47 KiB
TypeScript

import {arrayRepeat} from 'app/plugin/gutil';
import * as gu 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 = ['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;
}
});
for (const type of ['Choice', 'Text', 'Reference', 'Numeric']) {
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 columnBehavior(), 'Empty Column');
await selectColumns('Test1', 'Test3');
assert.equal(await 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 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 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 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 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 columnBehavior(), 'Formula Columns');
// 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 columnBehavior(), 'Empty Columns');
// 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 columnBehavior(), 'Data Columns');
await selectColumns('Test1');
assert.equal(await 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 columnBehavior(), 'Empty Columns');
await selectColumns('Test1');
assert.equal(await 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 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 columnBehavior(), 'Data Columns');
await selectColumns('Test1');
assert.equal(await 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 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 columnBehavior(), 'Data Columns');
await selectColumns('Test1');
assert.equal(await 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.isFalse(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');
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 background for multiple columns', async () => {
await selectColumns('Test1', 'Test2');
assert.equal(await colorLabel(), "Default cell style");
await gu.openColorPicker();
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 colorLabel(), "Default cell style");
// Change one cell to red
await selectColumns('Test1');
await gu.openColorPicker();
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 colorLabel(), "Mixed style");
// Try to change to blue, but press escape.
await gu.openColorPicker();
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.openColorPicker();
await gu.setFillColor(blue);
await driver.sendKeys(Key.ENTER);
await gu.waitForServer();
assert.equal(await colorLabel(), "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 colorLabel(), "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);
});
});
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 widgetType('TextBox');
await selectColumns('Right');
await widgetType('CheckBox');
await selectColumns('Left', 'Right');
assert.equal(await widgetType(), 'Mixed format');
// Test that both change when format is changed.
for (const mode of ['TextBox', 'CheckBox', 'Switch']) {
await widgetType(mode);
await selectColumns('Left');
assert.equal(await widgetType(), mode);
await selectColumns('Right');
assert.equal(await widgetType(), mode);
await selectColumns('Left', 'Right');
assert.equal(await widgetType(), 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.getCell(colB, 1).click();
await gu.sendKeys("one", Key.ENTER);
if (await getColumnType() === 'Choice List') {
await gu.sendKeys(Key.ENTER);
}
// 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();
},
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();
return false;
}
return true;
}
if (state === true) {
await buttons[0].click();
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();
}
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 widgetType(type?: string) {
if (!type) {
return await driver.find(".test-fbuilder-widget-select").getText();
}
await driver.find(".test-fbuilder-widget-select").click();
await driver.findContent('.test-select-menu li', gu.exactMatch(type)).click();
await gu.waitForServer();
}
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 columnBehavior() {
return (await driver.find(".test-field-behaviour").getText());
}
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() {
const elements = await driver.findAll(".test-panel-transform .test-panel-disabled-section");
return elements.length === 1;
}
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();
}
}
}
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 colorLabel() {
// Text actually contains T symbol before.
const label = await driver.find(".test-color-select").getText();
return label.replace(/^T/, '').trim();
}