gristlabs_grist-core/test/nbrowser/GridViewNewColumnMenu.ts
Jakub Serafin c04f61738c (core) "add column with type" and "add formula column" options in new column menu
Summary: New Column menu was enhanced by "add column with type" and "add formula column" options. First one allow user to chose the type of newly created column, to save time for selecting this option in creator menu. "Add formula column" opens formula editor in popup state right after creating the column. In this case, renaming column popup is ignored to not overburden user with to many popup at once.

Test Plan: new nbrowser test was added to check validity of menu items, and output of menu action - if columns have given types or if formula editor popup is opened and functionin, accordingly.

Reviewers: georgegevoian

Differential Revision: https://phab.getgrist.com/D4113
2023-11-23 14:31:27 +01:00

1357 lines
54 KiB
TypeScript

import {driver, Key} from "mocha-webdriver";
import {assert} from "chai";
import * as gu from "./gristUtils";
import {setupTestSuite} from "./testUtils";
import {UserAPIImpl} from 'app/common/UserAPI';
describe('GridViewNewColumnMenu', function () {
this.timeout('2m');
const cleanup = setupTestSuite();
gu.bigScreen();
let api: UserAPIImpl;
let docId: string;
let session: gu.Session;
before(async function () {
session = await gu.session().login({showTips:true});
api = session.createHomeApi();
docId = await session.tempNewDoc(cleanup, 'ColumnMenu');
// Add a table that will be used for lookups.
await gu.sendActions([
['AddTable', 'Person', [
{id: "Name"},
{id: "Age", type: 'Numeric'},
{id: 'Hobby', type: 'ChoiceList', widgetOptions: JSON.stringify({choices: ['Books', 'Cars']})},
{id: 'Employee', type: 'Choice', widgetOptions: JSON.stringify({choices: ['Y', 'N']})},
{id: "Birthday date", type: 'Date', label: 'Birthday date'},
{id: "Member", type: 'Bool'},
{id: "SeenAt", type: 'DateTime:UTC'},
{id: "Photo", type: 'Attachments'},
{id: "Fun", type: 'Any', formula: '44'},
{id: 'Parent', type: 'Ref:Person'},
{id: 'Children', type: 'RefList:Person'},
]],
['AddRecord', 'Person', null, {Name: "Bob", Age: 12}],
['AddRecord', 'Person', null, {Name: "Robert", Age: 34, Parent: 1}],
]);
});
describe('sections', function () {
revertEach();
it('looks ok for an empty document', async function () {
await clickAddColumn();
await hasAddNewColumMenu();
await hasShortcuts();
await closeAddColumnMenu();
});
it('has lookup columns', async function () {
await gu.sendActions([
// Create a table that we can reference to.
['AddTable', 'Reference', [
{id: "Name"},
{id: "Age"},
{id: "City"}
]],
// Add some data to the table.
['AddRecord', 'Reference', null, {Name: "Bob", Age: 12, City: "New York"}],
['AddRecord', 'Reference', null, {Name: "Robert", Age: 34, City: "Łódź"}],
// And a Ref column in the main table to that table.
['AddColumn', 'Table1', 'Reference', {type: 'Ref:Reference'}],
]);
await clickAddColumn();
await hasAddNewColumMenu();
await hasShortcuts();
await hasLookupMenu('Reference');
await closeAddColumnMenu();
});
});
describe('column creation', function () {
revertEach();
it('should show rename menu after a new column click', async function () {
await clickAddColumn();
await driver.findWait('.test-new-columns-menu-add-new', 100).click();
await driver.findWait('.test-column-title-popup', 100, 'rename menu is not present');
await closeAddColumnMenu();
});
it('should create a new column', async function () {
await clickAddColumn();
await driver.findWait('.test-new-columns-menu-add-new', 100).click();
//discard rename menu
await driver.findWait('.test-column-title-close', 100).click();
//check if new column is present
const columns = await gu.getColumnNames();
assert.include(columns, 'D', 'new column is not present');
assert.lengthOf(columns, 4, 'wrong number of columns');
// check that single undo removes new column
await gu.undo();
const columns2 = await gu.getColumnNames();
assert.notInclude(columns2, 'D', 'new column is still present');
assert.lengthOf(columns2, 3, 'wrong number of columns');
});
it('should support inserting before selected column', async function () {
await gu.openColumnMenu('A', 'Insert column to the left');
await driver.findWait(".test-new-columns-menu", 100);
await gu.sendKeys(Key.ENTER);
await gu.waitForServer();
await driver.findWait('.test-column-title-close', 100).click();
const columns = await gu.getColumnNames();
assert.deepEqual(columns, ['D', 'A', 'B', 'C']);
});
it('should support inserting after selected column', async function () {
await gu.openColumnMenu('A', 'Insert column to the right');
await driver.findWait(".test-new-columns-menu", 100);
await gu.sendKeys(Key.ENTER);
await gu.waitForServer();
await driver.findWait('.test-column-title-close', 100).click();
const columns = await gu.getColumnNames();
assert.deepEqual(columns, ['A', 'D', 'B', 'C']);
});
it('should support inserting after the last visible column', async function () {
await gu.openColumnMenu('C', 'Insert column to the right');
await driver.findWait(".test-new-columns-menu", 100);
await gu.sendKeys(Key.ENTER);
await gu.waitForServer();
await driver.findWait('.test-column-title-close', 100).click();
const columns = await gu.getColumnNames();
assert.deepEqual(columns, ['A', 'B', 'C', 'D']);
});
it('should skip showing menu when inserting with keyboard shortcuts', async function () {
await gu.sendKeys(Key.chord(Key.ALT, '='));
await gu.waitForServer();
assert.isFalse(await driver.find('.test-new-columns-menu').isPresent());
await gu.sendKeys(Key.ENTER);
let columns = await gu.getColumnNames();
assert.deepEqual(columns, ['A', 'B', 'C', 'D']);
await gu.sendKeys(Key.chord(Key.SHIFT, Key.ALT, '='));
await gu.waitForServer();
assert.isFalse(await driver.find('.test-new-columns-menu').isPresent());
await gu.sendKeys(Key.ENTER);
columns = await gu.getColumnNames();
assert.deepEqual(columns, ['A', 'B', 'C', 'E', 'D']);
});
});
describe('create column with type', function () {
revertThis();
it('should show "Add Column With type" option', async function () {
// open add new colum menu
await clickAddColumn();
// check if "Add Column With type" option is persent
const addWithType = await driver.findWait(
'.test-new-columns-menu-add-with-type',
100,
'Add Column With Type is not present');
assert.equal(await addWithType.getText(), 'Add column with type');
});
it('should display reference column popup when opened for the first time', async function(){
// open add new colum menu
await clickAddColumn();
// select "Add Column With type" option
await driver.findWait('.test-new-columns-menu-add-with-type', 100).click();
// wait for submenu to appear
await driver.findWait('.test-new-columns-menu-add-with-type-submenu', 100);
// check if popup is showed
await driver.findWait('.test-behavioral-prompt', 100, 'Reference column popup is not present');
// close popup
await gu.dismissBehavioralPrompts();
// close menu
await closeAddColumnMenu();
// open it again
await clickAddColumn();
await driver.findWait('.test-new-columns-menu-add-with-type', 100).click();
await driver.findWait('.test-new-columns-menu-add-with-type-submenu', 100);
// popup should not be showed
assert.isFalse(await driver.find('.test-behavioral-prompt').isPresent());
await gu.disableTips(session.email);
await closeAddColumnMenu();
});
const optionsToBeDisplayed = [
"Text",
"Numeric",
"Integer",
"Toggle",
"Date",
"DateTime",
"Choice",
"Choice List",
"Reference",
"Reference List",
"Attachment",
].map((option) => ({type:option, testClass: option.toLowerCase().replace(' ', '-')}));
for (const option of optionsToBeDisplayed) {
it(`should allow to select column type ${option.type}`, async function () {
// open add new colum menu
await clickAddColumn();
// select "Add Column With type" option
await driver.findWait('.test-new-columns-menu-add-with-type', 100).click();
// wait for submenu to appear
await driver.findWait('.test-new-columns-menu-add-with-type-submenu', 100);
// check if it is present in the menu
const element = await driver.findWait(
`.test-new-columns-menu-add-${option.testClass}`.toLowerCase(),
100,
`${option.type} option is not present`);
// click on the option and check if column is added with a proper type
await element.click();
//discard rename menu
await driver.findWait('.test-column-title-close', 100).click();
//check if new column is present
await gu.waitForServer();
await gu.selectColumn('D');
await gu.openColumnPanel();
const type = await gu.getType();
assert.equal(type, option.type);
await gu.undo(1);
});
}
});
describe('create formula column', function(){
revertThis();
it('should show "create formula column" option with tooltip', async function () {
// open add new colum menu
await clickAddColumn();
// check if "create formula column" option is present
const addWithType = await driver.findWait('.test-new-columns-menu-add-formula', 100,
'Add formula column is not present');
// check if it has a tooltip button
const tooltip = await addWithType.findWait('.test-info-tooltip', 100,
'Tooltip button is not present');
// check if tooltip is show after hovering
await tooltip.mouseMove();
const tooltipContainer = await driver.findWait('.test-info-tooltip-popup',
100,
'Tooltip is not shown');
// check if tooltip is showing valid message
const tooltipText = await tooltipContainer.getText();
assert.include(tooltipText,
'Formulas support many Excel functions, full Python syntax, and include a helpful AI Assistant.',
'Tooltip is showing wrong message');
// check if link in tooltip has a proper href
const hrefAddress = await tooltipContainer.findWait('a',
100,
'Tooltip link is not present');
assert.equal(await hrefAddress.getText(), 'Learn more.');
assert.equal(await hrefAddress.getAttribute('href'),
'https://support.getgrist.com/formulas',
'Tooltip link has wrong href');
});
it('should allow to select formula column', async function () {
// open column panel - we will need it later
await gu.openColumnPanel();
// open add new colum menu
await clickAddColumn();
// select "create formula column" option
await driver.findWait('.test-new-columns-menu-add-formula', 100).click();
// there should not be a rename poup
assert.isFalse(await driver.find('test-column-title-popup').isPresent());
//check if new column is present
await gu.waitForServer();
// check if editor popup is opened
await driver.findWait('.test-floating-editor-popup', 200, 'Editor popup is not present');
// write some formula
await gu.sendKeys('1+1');
await driver.find('.test-formula-editor-save-button').click();
await gu.waitForServer();
// check if column is created with a proper type
const type = await gu.columnBehavior();
assert.equal(type, 'Formula Column');
});
});
describe('hidden columns', function () {
revertThis();
it('hides hidden column section from < 5 columns', async function () {
await gu.sendActions([
['AddVisibleColumn', 'Table1', 'New1', {type: 'Any'}],
['AddVisibleColumn', 'Table1', 'New2', {type: 'Any'}],
['AddVisibleColumn', 'Table1', 'New3', {type: 'Any'}],
]);
await gu.openWidgetPanel();
await clickAddColumn();
assert.isFalse(await driver.find(".new-columns-menu-hidden-columns").isPresent(), 'hidden section is present');
await closeAddColumnMenu();
});
describe('inline menu section', function () {
revertEach();
it('shows hidden section as inlined for 1 to 5 hidden columns', async function () {
// Check that the hidden section is present and has the expected columns.
const checkSection = async (...columns: string[]) => {
await clickAddColumn();
await driver.findWait(".test-new-columns-menu-hidden-columns-header", 100, 'hidden section is not present');
for (const column of columns) {
assert.isTrue(
await driver.findContent('.test-new-columns-menu-hidden-column-inlined', column).isPresent(),
`column ${column} is not present`
);
}
await closeAddColumnMenu();
};
await gu.moveToHidden('A');
await checkSection('A');
await gu.moveToHidden('B');
await gu.moveToHidden('C');
await gu.moveToHidden('New1');
await gu.moveToHidden('New2');
await checkSection('A', 'B', 'C', 'New1', 'New2');
});
it('should add hidden column at the end', async function () {
let columns = await gu.getColumnNames();
assert.deepEqual(columns, ['A', 'B', 'C', 'New1', 'New2', 'New3']);
// Hide 'A' and add it back.
await gu.moveToHidden('A');
await clickAddColumn();
await driver.findContent('.test-new-columns-menu-hidden-column-inlined', 'A').click();
await gu.waitForServer();
// Now check that the column was added at the end.
columns = await gu.getColumnNames();
assert.deepEqual(columns, ['B', 'C', 'New1', 'New2', 'New3', 'A']);
});
});
describe('submenu section', function () {
before(async function () {
await gu.sendActions([
['AddVisibleColumn', 'Table1', 'New4', {type: 'Any'}],
]);
});
after(async function () {
await gu.sendActions([
['RemoveColumn', 'Table1', 'New4'],
]);
});
it('more than 5 hidden columns, section should be in submenu', async function () {
// Hide all columns except A.
const columns = await gu.getColumnNames();
for (const column of columns.slice(1)) {
await gu.moveToHidden(column);
}
// Make sure they are hidden.
assert.deepEqual(await gu.getColumnNames(), ['A']);
// Now make sure we see all of them in the submenu.
await clickAddColumn();
await driver.findWait(".test-new-columns-menu-hidden-columns-menu", 100, 'hidden section is not present');
assert.isFalse(await driver.find(".test-new-columns-menu-hidden-columns-header").isPresent());
// We don't see any hidden columns in the main menu.
assert.isFalse(await driver.find(".test-new-columns-menu-hidden-column-inlined").isPresent());
// Now expand the submenu and check that we see all the hidden columns.
await driver.find(".test-new-columns-menu-hidden-columns-menu").click();
// And we should see all the hidden columns.
for (const column of columns.slice(1)) {
assert.isTrue(
await driver.findContentWait('.test-new-columns-menu-hidden-column-collapsed', column, 100).isDisplayed(),
`column ${column} is not present`
);
}
// Add B column.
await driver.findContent('.test-new-columns-menu-hidden-column-collapsed', 'B').click();
await gu.waitForServer();
// Now check that the column was added at the end.
const columns2 = await gu.getColumnNames();
assert.deepEqual(columns2, ['A', 'B']);
// Hide it again.
await gu.undo();
});
it('submenu should be searchable', async function () {
await clickAddColumn();
await driver.find(".test-new-columns-menu-hidden-columns-menu").click();
await driver.findWait('.test-searchable-menu-input', 100).click();
await gu.sendKeys('New');
await checkResult(['New1', 'New2', 'New3', 'New4']);
await gu.sendKeys('2');
await checkResult(['New2']);
await gu.sendKeys('dummy');
await checkResult([]);
await gu.clearInput();
await checkResult(['B', 'C', 'New1', 'New2', 'New3', 'New4']);
await gu.sendKeys(Key.ESCAPE);
assert.isFalse(await isMenuPresent());
// Show it once again and add B and C.
await clickAddColumn();
await driver.find(".test-new-columns-menu-hidden-columns-menu").click();
await driver.findContentWait('.test-new-columns-menu-hidden-column-collapsed', 'B', 100).click();
await gu.waitForServer();
await clickAddColumn();
// Now this column is inlined.
await driver.findContentWait(".test-new-columns-menu-hidden-column-inlined", 'C', 100).click();
await gu.waitForServer();
// Make sure they are added at the end.
const columns = await gu.getColumnNames();
assert.deepEqual(columns, ['A', 'B', 'C']);
async function checkResult(cols: string[]) {
await gu.waitToPass(async () => {
assert.deepEqual(
await collapsedHiddenColumns(),
cols
);
}, 250);
}
});
});
});
const COLUMN_LABELS = [
"Name", "Age", "Hobby", "Employee", "Birthday date", "Member", "SeenAt", "Photo", "Fun", "Parent", "Children"
];
describe('lookups from Reference columns', function () {
revertThis();
before(async function () {
await gu.sendActions([
['AddVisibleColumn', 'Table1', 'Person', {type: 'Ref:Person'}],
['AddVisibleColumn', 'Table1', 'Employees', {type: 'RefList:Person'}],
]);
await gu.openColumnPanel();
// Add color to the name column to make sure it is not added to the lookup menu.
await gu.openPage('Person');
await gu.getCell('Name', 1).click();
await gu.openCellColorPicker();
await gu.setFillColor('#FD8182');
await driver.find('.test-colors-save').click();
await gu.waitForServer();
// And add conditional rule here. We will test if style rules are not copied over.
await gu.addInitialStyleRule();
await gu.openStyleRuleFormula(0);
await gu.sendKeys('True');
await gu.sendKeys(Key.ENTER);
await gu.waitForServer();
await gu.openCellColorPicker(0);
await gu.setFillColor('#FD8182');
await driver.find('.test-colors-save').click();
await gu.waitForServer();
await gu.openPage('Table1');
});
it('should show only 2 reference columns', async function () {
await clickAddColumn();
await gu.waitToPass(async () => {
const labels = await driver.findAll('.test-new-columns-menu-lookup', (el) => el.getText());
assert.deepEqual(
labels,
['Person [Person]', 'Person [Employees]'],
);
});
await closeAddColumnMenu();
});
it('should suggest to add every column from a reference', async function () {
await clickAddColumn();
await driver.findWait('.test-new-columns-menu-lookup-Person', 100).click();
await gu.waitToPass(async () => {
const allColumns = await driver.findAll('.test-new-columns-menu-lookup-column', (el) => el.getText());
assert.deepEqual(allColumns, COLUMN_LABELS);
});
await closeAddColumnMenu();
});
// Now add each column and make sure it is added with a proper name.
for(const column of COLUMN_LABELS) {
it(`should insert ${column} with a proper name and type from a Ref column`, async function () {
const revert = await gu.begin();
await clickAddColumn();
await driver.findWait('.test-new-columns-menu-lookup-Person', 100).click();
await driver.findContentWait(`.test-new-columns-menu-lookup-column`, column, 100).click();
await gu.waitForServer();
const columns = await gu.getColumnNames();
assert.deepEqual(columns, ['A', 'B', 'C', 'Person', 'Employees', `Person_${column}`]);
// This should be a formula column.
assert.equal(await gu.columnBehavior(), "Formula Column");
// And the formula should be correct.
await driver.find('.formula_field_sidepane').click();
assert.equal(await gu.getFormulaText(), `$Person.${column.replace(" ", "_")}`);
await gu.sendKeys(Key.ESCAPE);
switch (column) {
case "Name":
// This should be a text column.
assert.equal(await gu.getType(), 'Text');
// We should have color but no rules.
await gu.openCellColorPicker();
assert.equal(await driver.find(".test-fill-hex").value(), '#FD8182');
await driver.find('.test-colors-cancel').click();
assert.equal(0, await gu.styleRulesCount());
break;
case "Age":
// This should be a numeric column.
assert.equal(await gu.getType(), 'Numeric');
break;
case "Hobby": {
// This should be a choice column.
assert.equal(await gu.getType(), 'Choice List');
// And the choices should be correct.
const labels = await driver.findAll('.test-choice-list-entry-label', el => el.getText());
assert.deepEqual(labels, ['Books', 'Cars']);
break;
}
case "Employee": {
// This should be a choice column.
assert.equal(await gu.getType(), 'Choice');
// And the choices should be correct.
const labels = await driver.findAll('.test-choice-list-entry-label', el => el.getText());
assert.deepEqual(labels, ['Y', 'N']);
break;
}
case "Birthday date":
// This should be a date column.
assert.equal(await gu.getType(), 'Date');
break;
case "Member":
// This should be a boolean column.
assert.equal(await gu.getType(), 'Toggle');
break;
case "SeenAt":
// This should be a datetime column.
assert.equal(await gu.getType(), 'DateTime');
assert.equal(await driver.find(".test-tz-autocomplete input").value(), 'UTC');
break;
case "Photo":
// This should be an attachment column.
assert.equal(await gu.getType(), 'Attachment');
break;
case "Fun":
// This should be an any column.
assert.equal(await gu.getType(), 'Any');
break;
case "Parent":
// This should be a ref column.
assert.equal(await gu.getType(), 'Reference');
// With a proper table.
assert.equal(await gu.getRefTable(), 'Person');
// And a proper column.
assert.equal(await gu.getRefShowColumn(), 'Row ID');
break;
case "Children":
// This should be a ref list column.
assert.equal(await gu.getType(), 'Reference List');
// With a proper table.
assert.equal(await gu.getRefTable(), 'Person');
// And a proper column.
assert.equal(await gu.getRefShowColumn(), 'Row ID');
break;
}
await revert();
});
}
it('should suggest aggregations for RefList column', async function () {
await clickAddColumn();
await driver.findWait('.test-new-columns-menu-lookup-Employees', 100).click();
// Wait for the menu to appear.
await driver.findWait('.test-new-columns-menu-lookup-column', 100);
// First check items (so columns we can add which don't have menu)
const items = await driver.findAll('.test-new-columns-menu-lookup-column', (el) => el.getText());
assert.deepEqual(items, [
'Name\nlist',
'Hobby\nlist',
'Employee\nlist',
'Photo\nlist',
'Fun\nlist',
'Parent\nlist',
'Children\nlist'
]);
const menus = await driver.findAll('.test-new-columns-menu-lookup-submenu', (el) => el.getText());
assert.deepEqual(menus, [
'Age\nsum',
'Birthday date\nlist',
'Member\ncount',
'SeenAt\nlist'
]);
// Make sure that clicking on a column adds it with a default aggregation.
await driver.find('.test-new-columns-menu-lookup-column-Name').click();
await gu.waitForServer();
const columns = await gu.getColumnNames();
assert.deepEqual(columns, ['A', 'B', 'C', 'Person', 'Employees', 'Employees_Name']);
await checkTypeAndFormula('Any', `$Employees.Name`);
await gu.undo();
});
// Now test each aggregation.
for(const column of ['Age', 'Member', 'Birthday date', 'SeenAt']) {
it(`should insert ${column} with a proper name and type from a RefList column`, async function () {
const colId = column.replace(" ", "_");
await clickAddColumn();
await driver.findWait('.test-new-columns-menu-lookup-Employees', 100).click();
await driver.findWait(`.test-new-columns-menu-lookup-submenu-${colId}`, 100).mouseMove();
// Wait for the menu to show up.
await driver.findWait('.test-new-columns-menu-lookup-submenu-function', 100);
// Make sure the list of function is accurate.
const suggestedFunctions =
await driver.findAll('.test-new-columns-menu-lookup-submenu-function', (el) => el.getText());
switch(column) {
case "Age":
assert.deepEqual(suggestedFunctions, ['sum', 'average', 'min', 'max']);
break;
case "Birthday date":
assert.deepEqual(suggestedFunctions, ['list', 'min', 'max']);
break;
case "Member":
assert.deepEqual(suggestedFunctions, ['count', 'percent']);
break;
case "SeenAt":
assert.deepEqual(suggestedFunctions, ['list', 'min', 'max']);
break;
}
// Now pick the default function.
await driver.findWait(`.test-new-columns-menu-lookup-submenu-${colId}`, 100).click();
await gu.waitForServer();
const columns = await gu.getColumnNames();
assert.deepEqual(columns, ['A', 'B', 'C', 'Person', 'Employees', `Employees_${column}`]);
// This should be a formula column.
assert.equal(await gu.columnBehavior(), "Formula Column");
// And the formula should be correct.
switch(column) {
case "Age":
await checkTypeAndFormula('Numeric', `SUM($Employees.${colId})`);
// For this column test other aggregations as well.
await gu.undo();
await addRefListLookup('Employees', column, 'average');
await checkTypeAndFormula('Numeric', `AVERAGE($Employees.${colId})`);
await gu.undo();
await addRefListLookup('Employees', column, 'min');
await checkTypeAndFormula('Numeric', `MIN($Employees.${colId})`);
await gu.undo();
await addRefListLookup('Employees', column, 'max');
await checkTypeAndFormula('Numeric', `MAX($Employees.${colId})`);
break;
case "Member":
await checkTypeAndFormula('Integer', `SUM($Employees.${colId})`);
// Here we also test that the formula is correct for percent.
await gu.undo();
await addRefListLookup('Employees', column, 'percent');
await checkTypeAndFormula('Numeric', `AVERAGE(map(int, $Employees.Member)) if $Employees else None`);
assert.isTrue(
await driver.findContent('.test-numeric-mode .test-select-button', /%/).matches('[class*=-selected]'));
break;
case "SeenAt":
await checkTypeAndFormula('Any', `$Employees.${colId}`);
await gu.undo();
await addRefListLookup('Employees', column, 'min');
await checkTypeAndFormula('DateTime', `MIN($Employees.${colId})`);
assert.equal(await driver.find(".test-tz-autocomplete input").value(), 'UTC');
await gu.undo();
await addRefListLookup('Employees', column, 'max');
await checkTypeAndFormula('DateTime', `MAX($Employees.${colId})`);
assert.equal(await driver.find(".test-tz-autocomplete input").value(), 'UTC');
break;
default:
await checkTypeAndFormula('Any', `$Employees.${colId}`);
break;
}
await gu.undo();
});
}
});
describe('reverse lookups', function () {
revertThis();
before(async function () {
// Reference the Person table once more
await gu.sendActions([
['AddVisibleColumn', 'Person', 'Item', {type: 'Ref:Table1'}],
['AddVisibleColumn', 'Person', 'Items', {type: 'RefList:Table1'}],
]);
});
it('should show reverse lookups in the menu', async function () {
await clickAddColumn();
// Wait for any menu to show up.
await driver.findWait('.test-new-columns-menu-revlookup', 100);
// We should see two rev lookups.
assert.deepEqual(await driver.findAll('.test-new-columns-menu-revlookup', (el) => el.getText()), [
'Person [← Item]',
'Person [← Items]',
]);
});
it('should show same list from Ref and RefList', async function () {
await driver.findContent('.test-new-columns-menu-revlookup', 'Person [← Item]').mouseMove();
// Wait for any menu to show up.
await driver.findWait('.test-new-columns-menu-revlookup-column', 100);
const columns = await driver.findAll('.test-new-columns-menu-revlookup-column', (el) => el.getText());
const submenus = await driver.findAll('.test-new-columns-menu-revlookup-submenu', (el) => el.getText());
// Now open the other submenu and make sure list is the same.
await driver.findContent('.test-new-columns-menu-revlookup', 'Person [← Items]').mouseMove();
// Wait for any menu to show up.
await driver.findWait('.test-new-columns-menu-revlookup-column', 100);
const columns2 = await driver.findAll('.test-new-columns-menu-revlookup-column', (el) => el.getText());
const submenus2 = await driver.findAll('.test-new-columns-menu-revlookup-submenu', (el) => el.getText());
assert.deepEqual(columns, columns2);
assert.deepEqual(submenus, submenus2);
assert.deepEqual(columns, [
'Name\nlist',
'Hobby\nlist',
'Employee\nlist',
'Photo\nlist',
'Fun\nlist',
'Parent\nlist',
'Children\nlist',
'Item\nlist',
'Items\nlist'
]);
assert.deepEqual(submenus, [
'Age\nsum',
'Birthday date\nlist',
'Member\ncount',
'SeenAt\nlist'
]);
// Make sure that clicking one of the columns adds it with a default aggregation.
await driver.findContent('.test-new-columns-menu-revlookup-column', 'Name').click();
await gu.waitForServer();
const columns3 = await gu.getColumnNames();
assert.deepEqual(columns3, ['A', 'B', 'C', 'Person_Name']);
assert.equal(await gu.columnBehavior(), "Formula Column");
assert.equal(await gu.getType(), 'Any');
await driver.find('.formula_field_sidepane').click();
assert.equal(await gu.getFormulaText(), `Person.lookupRecords(Items=CONTAINS($id)).Name`);
await gu.sendKeys(Key.ESCAPE);
await gu.undo();
});
describe('reverse lookups from Ref column', function () {
for(const column of ['Age', 'Member', 'Birthday date', 'SeenAt']) {
it(`should properly add reverse lookup for ${column}`, async function () {
await clickAddColumn();
await driver.findContentWait('.test-new-columns-menu-revlookup', 'Person [← Item]', 100).mouseMove();
// This is submenu so expand it.
await driver.findContentWait('.test-new-columns-menu-revlookup-submenu', new RegExp("^" + column), 100)
.mouseMove();
// Wait for any function to appear.
await driver.findWait('.test-new-columns-menu-revlookup-column-function', 100);
// Make sure we see proper list.
const functions = await driver.findAll('.test-new-columns-menu-revlookup-column-function',
(el) => el.getText());
switch(column) {
case "Age":
assert.deepEqual(functions, ['sum', 'average', 'min', 'max']);
break;
case "Member":
assert.deepEqual(functions, ['count', 'percent']);
break;
case "Birthday date":
case "SeenAt":
assert.deepEqual(functions, ['list', 'min', 'max']);
break;
}
// Now add each function and make sure it is added with a proper name.
await gu.sendKeys(Key.ESCAPE);
switch(column) {
case "Age":
await addRevLookup('sum');
await checkTypeAndFormula('Numeric', `SUM(Person.lookupRecords(Item=$id).Age)`);
assert.deepEqual(await gu.getColumnNames(),
['A', 'B', 'C', 'Person_Age']);
await gu.undo();
await addRevLookup('average');
await checkTypeAndFormula('Numeric', `AVERAGE(Person.lookupRecords(Item=$id).Age)`);
await gu.undo();
await addRevLookup('min');
await checkTypeAndFormula('Numeric', `MIN(Person.lookupRecords(Item=$id).Age)`);
await gu.undo();
await addRevLookup('max');
await checkTypeAndFormula('Numeric', `MAX(Person.lookupRecords(Item=$id).Age)`);
break;
case "Member":
await addRevLookup('count');
await checkTypeAndFormula('Integer', `SUM(Person.lookupRecords(Item=$id).Member)`);
await gu.undo();
await addRevLookup('percent');
await checkTypeAndFormula('Numeric',
`AVERAGE(map(int, Person.lookupRecords(Item=$id).Member))` +
` if Person.lookupRecords(Item=$id) else None`);
break;
case "Birthday date":
await addRevLookup('list');
await checkTypeAndFormula('Any', `Person.lookupRecords(Item=$id).Birthday_date`);
await gu.undo();
await addRevLookup('min');
await checkTypeAndFormula('Date', `MIN(Person.lookupRecords(Item=$id).Birthday_date)`);
await gu.undo();
await addRevLookup('max');
await checkTypeAndFormula('Date', `MAX(Person.lookupRecords(Item=$id).Birthday_date)`);
assert.deepEqual(await gu.getColumnNames(),
['A', 'B', 'C', 'Person_Birthday date']);
break;
case "SeenAt":
await addRevLookup('max');
await checkTypeAndFormula('DateTime', `MAX(Person.lookupRecords(Item=$id).SeenAt)`);
// Here check the timezone.
assert.equal(await driver.find(".test-tz-autocomplete input").value(), 'UTC');
break;
}
await gu.undo();
async function addRevLookup(func: string) {
await clickAddColumn();
await driver.findContentWait('.test-new-columns-menu-revlookup', 'Person [← Item]', 100).mouseMove();
await driver.findContentWait('.test-new-columns-menu-revlookup-submenu', new RegExp("^" + column), 100)
.mouseMove();
await driver.findContentWait('.test-new-columns-menu-revlookup-column-function', func, 100).click();
await gu.waitForServer();
}
});
}
});
describe('reverse lookups from RefList column', function () {
for(const column of ['Age', 'Member', 'Birthday date', 'SeenAt']) {
it(`should properly add reverse lookup for ${column}`, async function () {
await clickAddColumn();
await driver.findContentWait('.test-new-columns-menu-revlookup', 'Person [← Items]', 100).mouseMove();
// This is submenu so expand it.
await driver.findContentWait('.test-new-columns-menu-revlookup-submenu', new RegExp("^" + column), 100)
.mouseMove();
// Wait for any function to appear.
await driver.findWait('.test-new-columns-menu-revlookup-column-function', 100);
// Make sure we see proper list.
const functions = await driver.findAll('.test-new-columns-menu-revlookup-column-function',
(el) => el.getText());
switch(column) {
case "Age":
assert.deepEqual(functions, ['sum', 'average', 'min', 'max']);
break;
case "Member":
assert.deepEqual(functions, ['count', 'percent']);
break;
case "Birthday date":
case "SeenAt":
assert.deepEqual(functions, ['list', 'min', 'max']);
break;
}
// Now add each function and make sure it is added with a proper name.
await gu.sendKeys(Key.ESCAPE);
switch(column) {
case "Age":
await addRevLookup('sum');
await checkTypeAndFormula('Numeric', `SUM(Person.lookupRecords(Items=CONTAINS($id)).Age)`);
await gu.undo();
await addRevLookup('average');
await checkTypeAndFormula('Numeric', `AVERAGE(Person.lookupRecords(Items=CONTAINS($id)).Age)`);
await gu.undo();
await addRevLookup('min');
await checkTypeAndFormula('Numeric', `MIN(Person.lookupRecords(Items=CONTAINS($id)).Age)`);
await gu.undo();
await addRevLookup('max');
await checkTypeAndFormula('Numeric', `MAX(Person.lookupRecords(Items=CONTAINS($id)).Age)`);
break;
case "Member":
await addRevLookup('count');
await checkTypeAndFormula('Integer', `SUM(Person.lookupRecords(Items=CONTAINS($id)).Member)`);
await gu.undo();
await addRevLookup('percent');
await checkTypeAndFormula('Numeric',
`AVERAGE(map(int, Person.lookupRecords(Items=CONTAINS($id)).Member))` +
` if Person.lookupRecords(Items=CONTAINS($id)) else None`);
break;
case "Birthday date":
await addRevLookup('list');
await checkTypeAndFormula('Any', `Person.lookupRecords(Items=CONTAINS($id)).Birthday_date`);
await gu.undo();
await addRevLookup('min');
await checkTypeAndFormula('Date', `MIN(Person.lookupRecords(Items=CONTAINS($id)).Birthday_date)`);
await gu.undo();
await addRevLookup('max');
await checkTypeAndFormula('Date', `MAX(Person.lookupRecords(Items=CONTAINS($id)).Birthday_date)`);
break;
case "SeenAt":
await addRevLookup('max');
await checkTypeAndFormula('DateTime', `MAX(Person.lookupRecords(Items=CONTAINS($id)).SeenAt)`);
// Here check the timezone.
assert.equal(await driver.find(".test-tz-autocomplete input").value(), 'UTC');
break;
}
await gu.undo();
async function addRevLookup(func: string) {
await clickAddColumn();
await driver.findContentWait('.test-new-columns-menu-revlookup', 'Person [← Items]', 100).mouseMove();
await driver.findContentWait('.test-new-columns-menu-revlookup-submenu', new RegExp("^" + column), 100)
.mouseMove();
await driver.findContentWait('.test-new-columns-menu-revlookup-column-function', func, 100).click();
await gu.waitForServer();
}
});
}
});
});
describe('shortcuts', function () {
describe('Timestamp', function () {
revertEach();
it('created at - should create new column with date triggered on create', async function () {
await gu.openColumnPanel();
await clickAddColumn();
await driver.findWait('.test-new-columns-menu-shortcuts-timestamp', 100).mouseMove();
await driver.findWait('.test-new-columns-menu-shortcuts-timestamp-new', 100).click();
await gu.waitForServer();
// Make sure we have Created At column at the end.
const columns = await gu.getColumnNames();
assert.deepEqual(columns, ['A', 'B', 'C', 'Created At']);
// Make sure this is the column that is selected.
assert.equal(await driver.find('.test-field-label').value(), 'Created At');
assert.equal(await driver.find('.test-field-col-id').value(), '$Created_At');
// Check behavior - this is trigger formula
assert.equal(await gu.columnBehavior(), "Data Column");
assert.isTrue(await driver.findContent('div', 'TRIGGER FORMULA').isDisplayed());
// It applies to new records only.
assert.equal(await driver.find('.test-field-formula-apply-to-new').getAttribute('checked'), 'true');
assert.equal(await driver.find('.test-field-formula-apply-on-changes').getAttribute('checked'), null);
// Make sure type and formula are correct.
await checkTypeAndFormula('DateTime', 'NOW()');
assert.isNotEmpty(await driver.find(".test-tz-autocomplete input").value());
});
it('modified at - should create new column with date triggered on change', async function () {
await clickAddColumn();
await driver.findWait('.test-new-columns-menu-shortcuts-timestamp', 100).mouseMove();
await driver.findWait('.test-new-columns-menu-shortcuts-timestamp-change', 100).click();
await gu.waitForServer();
// Make sure we have this column at the end.
const columns = await gu.getColumnNames();
assert.deepEqual(columns, ['A', 'B', 'C', 'Last Updated At']);
// Make sure this is the column that is selected.
assert.equal(await driver.find('.test-field-label').value(), 'Last Updated At');
assert.equal(await driver.find('.test-field-col-id').value(), '$Last_Updated_At');
// Check behavior - this is trigger formula
assert.equal(await gu.columnBehavior(), "Data Column");
assert.isTrue(await driver.findContent('div', 'TRIGGER FORMULA').isDisplayed());
// It applies to new records only and if anything changes.
assert.equal(await driver.find('.test-field-formula-apply-to-new').getAttribute('checked'), 'true');
assert.equal(await driver.find('.test-field-formula-apply-on-changes').getAttribute('checked'), 'true');
assert.equal(await driver.find('.test-field-triggers-select').getText(), 'Any field');
// Make sure type and formula are correct.
await checkTypeAndFormula('DateTime', 'NOW()');
assert.isNotEmpty(await driver.find(".test-tz-autocomplete input").value());
});
});
describe('Authorship', function () {
revertEach();
it('created by - should create new column with author name triggered on create', async function () {
await gu.openColumnPanel();
await clickAddColumn();
await driver.findWait('.test-new-columns-menu-shortcuts-author', 100).mouseMove();
await driver.findWait('.test-new-columns-menu-shortcuts-author-new', 100).click();
await gu.waitForServer();
// Make sure we have this column at the end.
const columns = await gu.getColumnNames();
assert.deepEqual(columns, ['A', 'B', 'C', 'Created By']);
// Make sure this is the column that is selected.
assert.equal(await driver.find('.test-field-label').value(), 'Created By');
assert.equal(await driver.find('.test-field-col-id').value(), '$Created_By');
// Check behavior - this is trigger formula
assert.equal(await gu.columnBehavior(), "Data Column");
assert.isTrue(await driver.findContent('div', 'TRIGGER FORMULA').isDisplayed());
// It applies to new records only.
assert.equal(await driver.find('.test-field-formula-apply-to-new').getAttribute('checked'), 'true');
assert.equal(await driver.find('.test-field-formula-apply-on-changes').getAttribute('checked'), null);
// Make sure type and formula are correct.
await checkTypeAndFormula('Text', 'user.Name');
});
it('modified by - should create new column with author name triggered on change', async function () {
await gu.openColumnPanel();
await clickAddColumn();
await driver.findWait('.test-new-columns-menu-shortcuts-author', 100).mouseMove();
await driver.findWait('.test-new-columns-menu-shortcuts-author-change', 100).click();
await gu.waitForServer();
// Make sure we have this column at the end.
const columns = await gu.getColumnNames();
assert.deepEqual(columns, ['A', 'B', 'C', 'Last Updated By']);
// Make sure this is the column that is selected.
assert.equal(await driver.find('.test-field-label').value(), 'Last Updated By');
assert.equal(await driver.find('.test-field-col-id').value(), '$Last_Updated_By');
// Check behavior - this is trigger formula
assert.equal(await gu.columnBehavior(), "Data Column");
assert.isTrue(await driver.findContent('div', 'TRIGGER FORMULA').isDisplayed());
// It applies to new records only and if anything changes.
assert.equal(await driver.find('.test-field-formula-apply-to-new').getAttribute('checked'), 'true');
assert.equal(await driver.find('.test-field-formula-apply-on-changes').getAttribute('checked'), 'true');
assert.equal(await driver.find('.test-field-triggers-select').getText(), 'Any field');
// Make sure type and formula are correct.
await checkTypeAndFormula('Text', 'user.Name');
});
});
describe('Detect Duplicates in...', function () {
it('should show columns in a searchable sub-menu', async function () {
await clickAddColumn();
await driver.findWait('.test-new-columns-menu-shortcuts-duplicates', 100).mouseMove();
await gu.waitToPass(async () => {
assert.deepEqual(
await driver.findAll('.test-searchable-menu li', (el) => el.getText()),
['A', 'B', 'C']
);
}, 500);
await driver.find('.test-searchable-menu-input').click();
await gu.sendKeys('A');
await gu.waitToPass(async () => {
assert.deepEqual(
await driver.findAll('.test-searchable-menu li', (el) => el.getText()),
['A']
);
}, 250);
await gu.sendKeys('BC');
await gu.waitToPass(async () => {
assert.deepEqual(
await driver.findAll('.test-searchable-menu li', (el) => el.getText()),
[]
);
}, 250);
await gu.clearInput();
await gu.waitToPass(async () => {
assert.deepEqual(
await driver.findAll('.test-searchable-menu li', (el) => el.getText()),
['A', 'B', 'C']
);
}, 250);
});
it('should create new column that checks for duplicates in the specified column', async function () {
await clickAddColumn();
await driver.findWait('.test-new-columns-menu-shortcuts-duplicates', 100).mouseMove();
await driver.findContentWait('.test-searchable-menu li', 'A', 500).click();
await gu.waitForServer();
await gu.sendKeys(Key.ENTER);
// Just checking the formula looks plausible - correctness is best left to a python test.
assert.equal(
await driver.find('.test-formula-editor').getText(),
'$A != "" and $A is not None and len(Table1.lookupRecords(A=$A)) > 1'
);
await gu.sendKeys(Key.ESCAPE);
let columns = await gu.getColumnNames();
assert.deepEqual(columns, ['A', 'B', 'C', 'Duplicate in A']);
await gu.undo();
// Try it with list-based columns; the formula should look a little different.
for (const [label, type] of [['Choice', 'Choice List'], ['Ref', 'Reference List']]) {
await gu.addColumn(label, type);
await clickAddColumn();
await driver.findWait('.test-new-columns-menu-shortcuts-duplicates', 100).mouseMove();
await driver.findContentWait('.test-searchable-menu li', label, 500).click();
await gu.waitForServer();
await gu.sendKeys(Key.ENTER);
assert.equal(
await driver.find('.test-formula-editor').getText(),
`any([len(Table1.lookupRecords(${label}=CONTAINS(x))) > 1 for x in $${label}])`
);
await gu.sendKeys(Key.ESCAPE);
columns = await gu.getColumnNames();
assert.deepEqual(columns, ['A', 'B', 'C', label, `Duplicate in ${label}`]);
await gu.undo(4);
}
});
});
describe('UUID', function () {
it('should create new column that generates a UUID on new record', async function () {
await gu.getCell(2, 1).click();
await gu.sendKeys('A', Key.ENTER);
await gu.waitForServer();
await clickAddColumn();
await driver.findWait('.test-new-columns-menu-shortcuts-uuid', 100).click();
await gu.waitForServer();
const cells1 = await gu.getVisibleGridCells({col: 'UUID', rowNums: [1, 2]});
assert.match(cells1[0], /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/);
assert.equal(cells1[1], '');
await gu.getCell(2, 2).click();
await gu.sendKeys('B', Key.ENTER);
await gu.waitForServer();
const cells2 = await gu.getVisibleGridCells({col: 'UUID', rowNums: [1, 2, 3]});
assert.match(cells2[0], /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/);
assert.match(cells2[1], /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/);
assert.equal(cells2[2], '');
assert.equal(cells1[0], cells2[0]);
await gu.undo(3);
});
});
});
it("should not show hidden Ref columns", async function () {
// Duplicate current tab and start transforming a column.
const mainTab = await gu.myTab();
const transformTab = await gu.duplicateTab();
// Start transforming a column.
await gu.sendActions([
['AddRecord', 'Table1', null, {A: 1, B: 2, C: 3}],
]);
await gu.getCell('A', 1).click();
await gu.setType('Reference', {apply: false});
await gu.waitForServer();
await gu.setRefTable('Person');
await gu.waitForServer();
// Now we have two hidden columns present.
let columns = await api.getTable(docId, 'Table1');
assert.includeMembers(Object.keys(columns), [
'gristHelper_Converted',
'gristHelper_Transform'
]);
// Now on the main tab, make sure we don't see those references in lookup menu.
await mainTab.open();
await clickAddColumn();
await driver.findWait('.test-new-columns-menu-lookups-none', 100);
// Now test RefList columns.
await transformTab.open();
await driver.find('.test-type-transform-cancel').click();
await gu.waitForServer();
// Make sure hidden columns are removed.
columns = await api.getTable(docId, 'Table1');
assert.notIncludeMembers(Object.keys(columns), [
'gristHelper_Converted',
'gristHelper_Transform'
]);
await gu.setType('Reference List', {apply: false});
await gu.setRefTable('Person');
await gu.waitForServer();
columns = await api.getTable(docId, 'Table1');
assert.includeMembers(Object.keys(columns), [
'gristHelper_Converted',
'gristHelper_Transform'
]);
// Now on the main make sure we still don't see those references in lookup menu.
await mainTab.open();
await clickAddColumn();
await driver.findWait('.test-new-columns-menu-lookups-none', 100);
// Now test reverse lookups.
await gu.openPage('Person');
await clickAddColumn();
// Wait for any menu to show up.
await driver.findWait('.test-new-columns-menu-lookup', 100);
// Now make sure we don't have helper columns
assert.isEmpty(await driver.findAll('.test-new-columns-menu-revlookup', e => e.getText()));
await gu.sendKeys(Key.ESCAPE);
await gu.scrollActiveView(-1000, 0);
await transformTab.open();
await driver.find('.test-type-transform-cancel').click();
await gu.waitForServer();
await transformTab.close();
await mainTab.open();
await gu.openPage('Table1');
});
async function clickAddColumn() {
const isMenuPresent = await driver.find(".test-new-columns-menu").isPresent();
if (!isMenuPresent) {
await driver.findWait(".mod-add-column", 100).click();
}
await driver.findWait(".test-new-columns-menu", 100);
}
async function isMenuPresent() {
return await driver.find(".test-new-columns-menu").isPresent();
}
async function closeAddColumnMenu() {
await driver.sendKeys(Key.ESCAPE);
assert.isFalse(await isMenuPresent(), 'menu is still present');
}
async function hasAddNewColumMenu() {
await isDisplayed('.test-new-columns-menu-add-new', 'add new column menu is not present');
}
async function isDisplayed(selector: string, message: string) {
assert.isTrue(await driver.findWait(selector, 100, message).isDisplayed(), message);
}
async function hasShortcuts() {
await isDisplayed('.test-new-columns-menu-shortcuts', 'shortcuts section is not present');
await isDisplayed('.test-new-columns-menu-shortcuts-timestamp', 'timestamp shortcuts section is not present');
await isDisplayed('.test-new-columns-menu-shortcuts-author', 'authorship shortcuts section is not present');
}
async function hasLookupMenu(colId: string) {
await isDisplayed('.test-new-columns-menu-lookup', 'lookup section is not present');
await isDisplayed(`.test-new-columns-menu-lookup-${colId}`, `lookup section for ${colId} is not present`);
}
async function collapsedHiddenColumns() {
return await driver.findAll('.test-new-columns-menu-hidden-column-collapsed', (el) => el.getText());
}
function revertEach() {
let revert: () => Promise<void>;
beforeEach(async function () {
revert = await gu.begin();
});
gu.afterEachCleanup(async function () {
if (await isMenuPresent()) {
await closeAddColumnMenu();
}
await revert();
});
}
function revertThis() {
let revert: () => Promise<void>;
before(async function () {
revert = await gu.begin();
});
gu.afterCleanup(async function () {
if (await isMenuPresent()) {
await closeAddColumnMenu();
}
await revert();
});
}
async function addRefListLookup(refListId: string, colId: string, func: string) {
await clickAddColumn();
await driver.findWait(`.test-new-columns-menu-lookup-${refListId}`, 100).click();
await driver.findWait(`.test-new-columns-menu-lookup-submenu-${colId}`, 100).mouseMove();
await driver.findWait(`.test-new-columns-menu-lookup-submenu-function-${func}`, 100).click();
await gu.waitForServer();
}
async function checkTypeAndFormula(type: string, formula: string) {
assert.equal(await gu.getType(), type);
await driver.find('.formula_field_sidepane').click();
assert.equal(await gu.getFormulaText(false).then(s => s.trim()), formula);
await gu.sendKeys(Key.ESCAPE);
}
});