gristlabs_grist-core/test/nbrowser/GridViewNewColumnMenu.ts
2024-02-01 10:45:18 -05:00

1358 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();
await gu.waitForServer();
//discard rename menu
await driver.find('.test-column-title-close').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();
await gu.waitForServer();
//discard rename menu
await driver.findWait('.test-column-title-close', 100).click();
//check if new column is present
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);
}
});