gristlabs_grist-core/test/nbrowser/ChoiceList.ts
George Gevoian 0cadb93d25 (core) Update dependencies
Summary:
Changes the minimum version of Node to 18, and updates the Docker images and GitHub workflows to build Grist with Node 18.

Also updates various dependencies and scripts to support building running tests with arm64 builds of Node.

Test Plan: Existing tests.

Reviewers: paulfitz

Reviewed By: paulfitz

Differential Revision: https://phab.getgrist.com/D3968
2023-10-11 17:36:58 -04:00

1011 lines
39 KiB
TypeScript

import {assert, driver, Key, stackWrapFunc, WebElement} from 'mocha-webdriver';
import * as gu from 'test/nbrowser/gristUtils';
import {setupTestSuite} from 'test/nbrowser/testUtils';
const normalFont = { bold: false, underline: false, italic: false, strikethrough: false};
const bold = true;
const underline = true;
const italic = true;
const strikethrough = true;
function getEditorTokens() {
return driver.findAll('.cell_editor .test-tokenfield .test-tokenfield-token', el => el.getText());
}
function getEditorTokenStyles() {
return driver.findAll(
'.cell_editor .test-tokenfield .test-tokenfield-token',
async el => {
const classList = await el.getAttribute("class");
return {
fillColor: await el.getCssValue('background-color'),
textColor: await el.getCssValue('color'),
boxShadow: await el.getCssValue('box-shadow'),
bold: classList.includes("font-bold"),
italic: classList.includes("font-italic"),
underline: classList.includes("font-underline"),
strikethrough: classList.includes("font-strikethrough"),
};
}
);
}
function getCellTokens(cell: WebElement) {
return cell.getText();
}
function getCellTokenStyles(cell: WebElement) {
return cell.findAll(
'.test-choice-list-cell-token',
async el => {
const classList = await el.getAttribute("class");
return {
fillColor: await el.getCssValue('background-color'),
textColor: await el.getCssValue('color'),
boxShadow: await el.getCssValue('box-shadow'),
bold: classList.includes("font-bold"),
italic: classList.includes("font-italic"),
underline: classList.includes("font-underline"),
strikethrough: classList.includes("font-strikethrough"),
};
}
);
}
function getChoiceLabels() {
return driver.findAll('.test-right-panel .test-choice-list-entry-label', el => el.getText());
}
function getChoiceColors() {
return driver.findAll(
'.test-right-panel .test-choice-list-entry-color',
el => el.getCssValue('background-color')
);
}
function getEditModeChoiceLabels() {
return driver.findAll('.test-right-panel .test-tokenfield-token input', el => el.value());
}
function getEditModeFillColors() {
return driver.findAll(
'.test-right-panel .test-tokenfield-token .test-color-button',
el => el.getCssValue('background-color')
);
}
function getEditModeTextColors() {
return driver.findAll(
'.test-right-panel .test-tokenfield-token .test-color-button',
el => el.getCssValue('color')
);
}
function getEditModeFontOptions() {
return driver.findAll(
'.test-right-panel .test-tokenfield-token .test-color-button',
async el => {
const classes = await el.getAttribute("class");
const options: any = {};
if (classes.includes("font-bold")) {
options['bold'] = true;
}
if (classes.includes("font-underline")) {
options['underline'] = true;
}
if (classes.includes("font-italic")) {
options['italic'] = true;
}
if (classes.includes("font-strikethrough")) {
options['strikethrough'] = true;
}
return options;
}
);
}
function getEditorTokensIsInvalid() {
return driver.findAll('.cell_editor .test-tokenfield .test-tokenfield-token', el => el.matches('[class*=-invalid]'));
}
function getEditorInput() {
return driver.find('.cell_editor .test-tokenfield .test-tokenfield-input');
}
async function editChoiceEntries() {
await driver.find(".test-choice-list-entry").click();
await gu.waitAppFocus(false);
}
async function renameEntry(from: string, to: string) {
await clickEntry(from);
await gu.sendKeys(to);
await gu.sendKeys(Key.ENTER);
}
async function clickEntry(label: string) {
const entry = await driver.findWait(`.test-choice-list-entry .test-token-label[value='${label}']`, 100);
await entry.click();
}
async function saveChoiceEntries() {
await driver.find(".test-choice-list-entry-save").click();
await gu.waitForServer();
}
describe('ChoiceList', function() {
this.timeout(20000);
const cleanup = setupTestSuite();
const clipboard = gu.getLockableClipboard();
const WHITE_FILL = 'rgba(255, 255, 255, 1)';
const UNSET_FILL = WHITE_FILL;
const INVALID_FILL = WHITE_FILL;
const DEFAULT_FILL = 'rgba(232, 232, 232, 1)';
const GREEN_FILL = 'rgba(225, 254, 222, 1)';
const DARK_GREEN_FILL = 'rgba(18, 110, 14, 1)';
const BLUE_FILL = 'rgba(204, 254, 254, 1)';
const BLACK_FILL = 'rgba(0, 0, 0, 1)';
const BLACK_TEXT = 'rgba(0, 0, 0, 1)';
const WHITE_TEXT = 'rgba(255, 255, 255, 1)';
const APRICOT_FILL = 'rgba(254, 204, 129, 1)';
const APRICOT_TEXT = 'rgba(70, 13, 129, 1)';
const DEFAULT_TEXT = BLACK_TEXT;
const INVALID_TEXT = BLACK_TEXT;
const VALID_CHOICE = {
boxShadow: 'none',
...normalFont,
};
const INVALID_CHOICE = {
fillColor: INVALID_FILL,
textColor: INVALID_TEXT,
boxShadow: 'rgb(208, 2, 27) 0px 0px 0px 1px inset',
...normalFont,
};
afterEach(() => gu.checkForErrors());
it('should support basic editing', async () => {
const mainSession = await gu.session().teamSite.login();
const api = mainSession.createHomeApi();
const docId = await mainSession.tempNewDoc(cleanup, 'FormulaCounts', {load: true});
// Make a ChoiceList column and add some data.
await api.applyUserActions(docId, [
['ModifyColumn', 'Table1', 'B', {
type: 'ChoiceList',
widgetOptions: JSON.stringify({
choices: ['Green', 'Blue', 'Black'],
choiceOptions: {
'Green': {
fillColor: '#e1fede',
textColor: '#000000',
fontBold: true
},
'Blue': {
fillColor: '#ccfefe',
textColor: '#000000'
},
'Black': {
fillColor: '#000000',
textColor: '#ffffff'
}
}
})
}],
['BulkAddRecord', 'Table1', [null, null, null], {}],
]);
// Enter by typing into an empty cell: valid value, invalue value, then check the editor.
await gu.getCell({rowNum: 1, col: 'B'}).click();
await driver.sendKeys('Gre', Key.ENTER);
await driver.sendKeys('fake', Key.ENTER);
assert.deepEqual(await getEditorTokens(), ['Green', 'fake']);
assert.deepEqual(await getEditorTokensIsInvalid(), [false, true]);
assert.deepEqual(
await getEditorTokenStyles(),
[
{fillColor: GREEN_FILL, textColor: BLACK_TEXT, ...VALID_CHOICE, bold},
INVALID_CHOICE,
]
);
// Escape to cancel; check nothing got saved.
await driver.sendKeys(Key.ESCAPE);
await gu.waitForServer();
assert.equal(await driver.find('.cell_editor').isPresent(), false);
assert.equal(await gu.getCell({rowNum: 1, col: 'B'}).getText(), '');
// Type invalid value, then select from dropdown valid
await gu.getCell({rowNum: 1, col: 'B'}).click();
await driver.sendKeys('fake', Key.ENTER);
await getEditorInput().click();
const blueChoice = await driver.findContent('.test-autocomplete li', /Blue/);
assert.equal(
await blueChoice.find('.test-choice-list-editor-item-label').getCssValue('background-color'),
BLUE_FILL
);
assert.equal(
await blueChoice.find('.test-choice-list-editor-item-label').getCssValue('color'),
BLACK_TEXT
);
await blueChoice.click();
// Type another valid, check what's in editor
await driver.sendKeys('black', Key.ENTER);
assert.deepEqual(await getEditorTokens(), ['fake', 'Blue', 'Black']);
assert.deepEqual(await getEditorTokensIsInvalid(), [true, false, false]);
assert.deepEqual(
await getEditorTokenStyles(),
[
INVALID_CHOICE,
{fillColor: BLUE_FILL, textColor: BLACK_TEXT, ...VALID_CHOICE},
{fillColor: BLACK_FILL, textColor: WHITE_TEXT, ...VALID_CHOICE}
]
);
// Enter to save; check values got saved.
await driver.sendKeys(Key.ENTER);
await gu.waitForServer();
assert.equal(await driver.find('.cell_editor').isPresent(), false);
assert.equal(await getCellTokens(await gu.getCell({rowNum: 1, col: 'B'})), 'fake\nBlue\nBlack');
assert.deepEqual(
await getCellTokenStyles(await gu.getCell({rowNum: 1, col: 'B'})),
[
INVALID_CHOICE,
{fillColor: BLUE_FILL, textColor: BLACK_TEXT, ...VALID_CHOICE},
{fillColor: BLACK_FILL, textColor: WHITE_TEXT, ...VALID_CHOICE}
]
);
// Enter to edit. Enter token, remove two tokens, with a key and with an x-click.
await gu.getCell({rowNum: 1, col: 'B'}).click();
await driver.sendKeys(Key.ENTER);
assert.deepEqual(await getEditorTokens(), ['fake', 'Blue', 'Black']);
await driver.sendKeys('Gre', Key.TAB);
assert.deepEqual(await getEditorTokens(), ['fake', 'Blue', 'Black', 'Green']);
await driver.sendKeys(Key.LEFT, Key.LEFT, Key.BACK_SPACE);
assert.deepEqual(await getEditorTokens(), ['fake', 'Blue', 'Green']);
const tok1 = driver.findContent('.cell_editor .test-tokenfield .test-tokenfield-token', /fake/);
await tok1.mouseMove();
await tok1.find('.test-tokenfield-delete').click();
assert.deepEqual(await getEditorTokens(), ['Blue', 'Green']);
// Enter to save; check values got saved.
await driver.sendKeys(Key.ENTER);
await gu.waitForServer();
assert.equal(await driver.find('.cell_editor').isPresent(), false);
assert.equal(await getCellTokens(gu.getCell({rowNum: 1, col: 'B'})), 'Blue\nGreen');
// Start typing to replace content with a token; check values.
await gu.getCell({rowNum: 1, col: 'B'}).click();
await driver.sendKeys('foo');
assert.deepEqual(await getEditorTokens(), []);
assert.equal(await getEditorInput().value(), 'foo');
await driver.sendKeys(Key.TAB);
assert.deepEqual(await getEditorTokens(), ['foo']);
// Escape to cancel; check nothing got saved.
await driver.sendKeys(Key.ESCAPE);
await gu.waitForServer();
assert.equal(await driver.find('.cell_editor').isPresent(), false);
assert.equal(await gu.getCell({rowNum: 1, col: 'B'}).getText(), 'Blue\nGreen');
// Double-click to open dropdown and select a token.
await driver.withActions(a => a.doubleClick(gu.getCell({rowNum: 1, col: 'B'})));
await driver.findContent('.test-autocomplete li', /Black/).click();
assert.deepEqual(await getEditorTokens(), ['Blue', 'Green', 'Black']);
// Click away to save: new token should be added.
await gu.getCell({rowNum: 2, col: 'B'}).click();
await gu.waitForServer();
assert.equal(await driver.find('.cell_editor').isPresent(), false);
assert.equal(await gu.getCell({rowNum: 1, col: 'B'}).getText(), 'Blue\nGreen\nBlack');
// Starting to type names without accents should match the actual choices
await gu.addColumn("Accents");
await api.applyUserActions(docId, [
['ModifyColumn', 'Table1', 'Accents', {
type: 'ChoiceList',
widgetOptions: JSON.stringify({
choices: ['Adélaïde', 'Adèle', 'Agnès', 'Amélie'],
})
}],
]);
await gu.getCell({rowNum: 1, col: 'Accents'}).click();
await driver.sendKeys('Ade', Key.ENTER);
await driver.sendKeys('Agne', Key.ENTER);
await driver.sendKeys('Ame', Key.ENTER);
assert.deepEqual(await getEditorTokens(), ['Adélaïde', 'Agnès', 'Amélie']);
});
it('should be visible in formulas', async () => {
// Add a formula that returns tokens reversed
await gu.getCell({rowNum: 1, col: 'C'}).click();
await gu.enterFormula('":".join($B)');
// Check value
assert.deepEqual(await gu.getVisibleGridCells({rowNums: [1, 2, 3], cols: ['B', 'C']}), [
'Blue\nGreen\nBlack', 'Blue:Green:Black',
'', '',
'', '',
]);
// Hit enter, click to delete a token, save.
await gu.getCell({rowNum: 1, col: 'B'}).click();
await driver.sendKeys(Key.ENTER);
await driver.sendKeys(Key.BACK_SPACE);
await driver.sendKeys(Key.ENTER);
await gu.waitForServer();
// Check formula got updated
assert.deepEqual(await gu.getVisibleGridCells({rowNums: [1], cols: ['B', 'C']}), [
'Blue\nGreen', 'Blue:Green',
]);
// Type a couple new tokens.
await gu.getCell({rowNum: 1, col: 'B'}).click();
await driver.sendKeys(Key.ENTER);
await driver.sendKeys('fake', Key.TAB, 'Bla', Key.TAB);
// Enter to save; check formula got updated
await driver.sendKeys(Key.ENTER);
await gu.waitForServer();
assert.deepEqual(await gu.getVisibleGridCells({rowNums: [1], cols: ['B', 'C']}), [
'Blue\nGreen\nfake\nBlack', 'Blue:Green:fake:Black',
]);
// Hit delete. ChoiceList cell and formula should clear.
await gu.getCell({rowNum: 1, col: 'B'}).click();
await driver.sendKeys(Key.DELETE);
await gu.waitForServer();
assert.deepEqual(await gu.getVisibleGridCells({rowNums: [1, 2, 3], cols: ['B', 'C']}), [
'', '',
'', '',
'', '',
]);
});
it('should allow adding new values', async () => {
// Check what choices are configured.
await gu.toggleSidePanel('right', 'open');
await driver.find('.test-right-tab-field').click();
assert.deepEqual(await getChoiceLabels(), ['Green', 'Blue', 'Black']);
assert.deepEqual(
await getChoiceColors(),
[GREEN_FILL, BLUE_FILL, BLACK_FILL]
);
// Select a token from autocomplete
const cell = gu.getCell({rowNum: 1, col: 'B'});
assert.equal(await cell.getText(), '');
await cell.click();
await driver.sendKeys(Key.ENTER);
await driver.findContent('.test-autocomplete li', /Green/).click();
assert.deepEqual(await getEditorTokens(), ['Green']);
// Type token that's not in autocomplete
await driver.sendKeys("Orange");
// Enter should add invalid token.
await driver.sendKeys(Key.ENTER);
assert.deepEqual(await getEditorTokens(), ['Green', 'Orange']);
assert.deepEqual(await getEditorTokensIsInvalid(), [false, true]);
// Type another token, and click the "+" button in autocomplete. New token should be valid.
await driver.sendKeys("Apricot");
const newChoice = await driver.find('.test-autocomplete .test-choice-list-editor-new-item');
assert.equal(await newChoice.getText(), 'Apricot');
assert.equal(
await newChoice.find('.test-choice-list-editor-item-label').getCssValue('background-color'),
DEFAULT_FILL
);
assert.equal(
await newChoice.find('.test-choice-list-editor-item-label').getCssValue('color'),
DEFAULT_TEXT
);
await driver.find('.test-autocomplete .test-choice-list-editor-new-item').click();
assert.deepEqual(await getEditorTokens(), ['Green', 'Orange', 'Apricot']);
assert.deepEqual(await getEditorTokensIsInvalid(), [false, true, false]);
assert.deepEqual(
await getEditorTokenStyles(),
[
{fillColor: GREEN_FILL, textColor: BLACK_TEXT, ...VALID_CHOICE, bold},
INVALID_CHOICE,
{fillColor: DEFAULT_FILL, textColor: DEFAULT_TEXT, ...VALID_CHOICE}
]
);
// Save: check tokens
await driver.sendKeys(Key.ENTER);
await gu.waitForServer();
assert.equal(await getCellTokens(gu.getCell({rowNum: 1, col: 'B'})), 'Green\nOrange\nApricot');
assert.deepEqual(
await getCellTokenStyles(gu.getCell({rowNum: 1, col: 'B'})),
[
{fillColor: GREEN_FILL, textColor: BLACK_TEXT, ...VALID_CHOICE, bold},
INVALID_CHOICE,
{fillColor: DEFAULT_FILL, textColor: DEFAULT_TEXT, ...VALID_CHOICE}
]
);
// New option should be listed in config.
assert.deepEqual(await getChoiceLabels(), ['Green', 'Blue', 'Black', 'Apricot']);
assert.deepEqual(
await getChoiceColors(),
[GREEN_FILL, BLUE_FILL, BLACK_FILL, UNSET_FILL]
);
});
const convertColumn = stackWrapFunc(async function(typeRe: RegExp) {
await gu.setType(typeRe);
await gu.waitForServer();
await driver.findContent('.test-type-transform-apply', /Apply/).click();
await gu.waitForServer();
});
it('should allow reasonable conversions between ChoiceList and other types', async function() {
await gu.enterGridRows({rowNum: 1, col: 'A'},
[['Hello'], ['World'], [' Foo,Bar;Baz!,"Qux, quux corge", "80\'s",']]);
await testTextChoiceListConversions();
});
it('should allow ChoiceList conversions for column used in summary', async function() {
// Add a widget with a summary on column A.
await gu.addNewSection(/Table/, /Table1/, {summarize: [/^A$/]});
await testTextChoiceListConversions();
await gu.undo();
});
async function testTextChoiceListConversions() {
await gu.getCell({section: 'TABLE1', rowNum: 3, col: 'A'}).click();
// Convert this text column to ChoiceList.
await gu.toggleSidePanel('right', 'open');
await driver.find('.test-right-tab-field').click();
await convertColumn(/Choice List/);
// Check that choices got populated.
await driver.find('.test-right-tab-field').click();
assert.deepEqual(await getChoiceLabels(), ['Hello', 'World', 'Foo', 'Bar;Baz!', 'Qux, quux corge', '80\'s']);
assert.deepEqual(
await getChoiceColors(),
[UNSET_FILL, UNSET_FILL, UNSET_FILL, UNSET_FILL, UNSET_FILL, UNSET_FILL]
);
// Check that the result contains the right tags.
assert.deepEqual(await gu.getVisibleGridCells({rowNums: [1, 2, 3], cols: ['A']}), [
'Hello',
'World',
'Foo\nBar;Baz!\nQux, quux corge\n80\'s'
]);
await gu.checkForErrors();
// Check that the result contains the right colors.
for (const rowNum of [1, 2]) {
assert.deepEqual(
await getCellTokenStyles(await gu.getCell({rowNum, col: 'A'})),
[{fillColor: DEFAULT_FILL, textColor: DEFAULT_TEXT, ...VALID_CHOICE}]
);
}
assert.deepEqual(
await getCellTokenStyles(await gu.getCell({rowNum: 3, col: 'A'})),
[
{fillColor: DEFAULT_FILL, textColor: DEFAULT_TEXT, ...VALID_CHOICE},
{fillColor: DEFAULT_FILL, textColor: DEFAULT_TEXT, ...VALID_CHOICE},
{fillColor: DEFAULT_FILL, textColor: DEFAULT_TEXT, ...VALID_CHOICE},
{fillColor: DEFAULT_FILL, textColor: DEFAULT_TEXT, ...VALID_CHOICE},
]
);
// Open a cell to see the actual tags.
await gu.getCell({rowNum: 3, col: 'A'}).click();
await driver.sendKeys(Key.ENTER);
assert.deepEqual(await getEditorTokens(), ['Foo', 'Bar;Baz!', 'Qux, quux corge', '80\'s']);
assert.deepEqual(await getEditorTokensIsInvalid(), [ false, false, false, false ]);
assert.deepEqual(
await getEditorTokenStyles(),
[
{fillColor: DEFAULT_FILL, textColor: DEFAULT_TEXT, ...VALID_CHOICE},
{fillColor: DEFAULT_FILL, textColor: DEFAULT_TEXT, ...VALID_CHOICE},
{fillColor: DEFAULT_FILL, textColor: DEFAULT_TEXT, ...VALID_CHOICE},
{fillColor: DEFAULT_FILL, textColor: DEFAULT_TEXT, ...VALID_CHOICE}
]
);
await driver.sendKeys("hooray", Key.TAB, Key.ENTER);
await gu.waitForServer();
await gu.checkForErrors();
// Convert back to text.
await convertColumn(/Text/);
// Check that values turn into comma-separated values.
assert.deepEqual(await gu.getVisibleGridCells({rowNums: [1, 2, 3], cols: ['A']}), [
'Hello',
'World',
'Foo, Bar;Baz!, "Qux, quux corge", 80\'s, hooray'
]);
// Undo the cell change and both conversions (back to ChoiceList, back to Text), and check
// that UNDO also works correctly and without errors.
await gu.undo(3);
assert.deepEqual(await gu.getVisibleGridCells({rowNums: [1, 2, 3], cols: ['A']}), [
'Hello',
'World',
' Foo,Bar;Baz!,"Qux, quux corge", "80\'s",', // That's the text originally entered into this Text cell.
]);
}
it('should keep choices when converting between Choice and ChoiceList', async function() {
// Column B starts off as ChoiceList with the following choices.
await gu.getCell({rowNum: 1, col: 'B'}).click();
await driver.find('.test-right-tab-field').click();
assert.deepEqual(await getChoiceLabels(), ['Green', 'Blue', 'Black', 'Apricot']);
// Add some more values to this columm.
await gu.getCell({rowNum: 2, col: 'B'}).click();
await driver.sendKeys('Black', Key.ENTER, Key.ENTER);
await gu.waitForServer();
await driver.sendKeys('Green', Key.ENTER, Key.ENTER);
await gu.waitForServer();
assert.deepEqual(await gu.getVisibleGridCells({rowNums: [1, 2, 3], cols: ['B']}), [
"Green\nOrange\nApricot",
"Black",
"Green",
]);
// Convert to Choice. Configured Choices should stay the same.
await convertColumn(/^Choice$/);
assert.deepEqual(await getChoiceLabels(), ['Green', 'Blue', 'Black', 'Apricot']);
assert.deepEqual(await getChoiceColors(), [GREEN_FILL, BLUE_FILL, BLACK_FILL, UNSET_FILL]);
// Cells which contain multiple choices become CSVs.
assert.deepEqual(await gu.getVisibleGridCells({rowNums: [1, 2, 3], cols: ['B']}), [
"Green, Orange, Apricot",
"Black",
"Green",
]);
const cell1 = gu.getCell('B', 1);
assert.equal(await cell1.find('.field_clip').matches('.invalid'), false);
assert.equal(await cell1.find('.test-choice-token').getCssValue('background-color'), INVALID_FILL);
assert.equal(await cell1.find('.test-choice-token').getCssValue('color'), INVALID_TEXT);
const cell2 = gu.getCell('B', 2);
assert.equal(await cell2.find('.field_clip').matches('.invalid'), false);
assert.equal(await cell2.find('.test-choice-token').getCssValue('background-color'), BLACK_FILL);
assert.equal(await cell2.find('.test-choice-token').getCssValue('color'), WHITE_TEXT);
// Convert back to ChoiceList. Choices should stay the same.
await convertColumn(/Choice List/);
assert.deepEqual(await getChoiceLabels(), ['Green', 'Blue', 'Black', 'Apricot']);
assert.deepEqual(await getChoiceColors(), [GREEN_FILL, BLUE_FILL, BLACK_FILL, UNSET_FILL]);
// Cell and editor data should be restored too.
assert.deepEqual(await gu.getVisibleGridCells({rowNums: [1, 2, 3], cols: ['B']}), [
"Green\nOrange\nApricot",
"Black",
"Green",
]);
assert.deepEqual(
await getCellTokenStyles(await gu.getCell({rowNum: 1, col: 'B'})),
[
{fillColor: GREEN_FILL, textColor: BLACK_TEXT, ...VALID_CHOICE, bold},
INVALID_CHOICE,
{fillColor: DEFAULT_FILL, textColor: DEFAULT_TEXT, ...VALID_CHOICE},
]
);
await gu.getCell({rowNum: 1, col: 'B'}).click();
await driver.sendKeys(Key.ENTER);
assert.deepEqual(await getEditorTokens(), ['Green', 'Orange', 'Apricot']);
assert.deepEqual(await getEditorTokensIsInvalid(), [false, true, false]);
assert.deepEqual(
await getEditorTokenStyles(),
[
{fillColor: GREEN_FILL, textColor: BLACK_TEXT, ...VALID_CHOICE, bold},
INVALID_CHOICE,
{fillColor: DEFAULT_FILL, textColor: DEFAULT_TEXT, ...VALID_CHOICE},
]
);
await driver.sendKeys(Key.ESCAPE);
});
it('should allow setting choice style', async function() {
// Open the choice editor.
await driver.find('.test-choice-list-entry').click();
await gu.waitAppFocus(false);
// Change 'Apricot' to a light shade of orange with purple text.
const [greenColorBtn, , , apricotColorBtn] = await driver
.findAll('.test-tokenfield .test-color-button');
await apricotColorBtn.click();
await gu.setColor(driver.find('.test-fill-input'), 'rgb(254, 204, 129)');
await gu.setColor(driver.find('.test-text-input'), 'rgb(70, 13, 129)');
await gu.setFont('bold', true);
await gu.setFont('italic', true);
await driver.sendKeys(Key.ENTER);
// Change 'Green' to a darker shade with white text.
await greenColorBtn.click();
await gu.setColor(driver.find('.test-fill-input'), 'rgb(18, 110, 14)');
await gu.setColor(driver.find('.test-text-input'), 'rgb(255, 255, 255)');
await gu.setFont('strikethrough', true);
await gu.setFont('underline', true);
await driver.sendKeys(Key.ENTER);
// Check that the old colors are still being used in the grid
assert.deepEqual(
await getCellTokenStyles(await gu.getCell({rowNum: 1, col: 'B'})),
[
{fillColor: GREEN_FILL, textColor: BLACK_TEXT, ...VALID_CHOICE, bold},
INVALID_CHOICE,
{fillColor: DEFAULT_FILL, textColor: DEFAULT_TEXT, ...VALID_CHOICE}
]
);
assert.deepEqual(
await getCellTokenStyles(await gu.getCell({rowNum: 3, col: 'B'})),
[
{fillColor: GREEN_FILL, textColor: BLACK_TEXT, ...VALID_CHOICE, bold}
]
);
// Click save, and check that the new colors are now used in the grid
await driver.find('.test-choice-list-entry-save').click();
await gu.waitForServer();
assert.deepEqual(
await getCellTokenStyles(await gu.getCell({rowNum: 1, col: 'B'})),
[
{fillColor: DARK_GREEN_FILL, textColor: WHITE_TEXT, ...VALID_CHOICE, strikethrough, underline, bold},
INVALID_CHOICE,
{fillColor: APRICOT_FILL, textColor: APRICOT_TEXT, ...VALID_CHOICE, bold, italic}
]
);
assert.deepEqual(
await getCellTokenStyles(await gu.getCell({rowNum: 3, col: 'B'})),
[
{fillColor: DARK_GREEN_FILL, textColor: WHITE_TEXT, ...VALID_CHOICE,
strikethrough, underline, bold}
]
);
});
it('should discard changes on cancel', async function() {
for (const method of ['button', 'shortcut']) {
// Open the editor.
await driver.find('.test-choice-list-entry').click();
await gu.waitAppFocus(false);
// Delete 'Apricot', then cancel the change.
await gu.sendKeys(Key.BACK_SPACE);
assert.deepEqual(await getEditModeChoiceLabels(), ['Green', 'Blue', 'Black']);
if (method === 'button') {
await driver.find('.test-choice-list-entry-cancel').click();
} else {
await gu.sendKeys(Key.ESCAPE);
}
// Check that 'Apricot' is still there and the change wasn't saved.
assert.deepEqual(await getChoiceLabels(), ['Green', 'Blue', 'Black', 'Apricot']);
}
});
it('should support undo/redo shortcuts in the choice config editor', async function() {
// Open the choice editor.
await driver.find('.test-choice-list-entry').click();
await gu.waitAppFocus(false);
// Add a few choices.
await driver.sendKeys('Foo', Key.ENTER, 'Bar', Key.ENTER, 'Baz', Key.ENTER);
assert.deepEqual(await getEditModeChoiceLabels(), ['Green', 'Blue', 'Black', 'Apricot', 'Foo', 'Bar', 'Baz']);
// Undo, verifying the contents of the choice config editor are correct after each invocation.
const modKey = await gu.modKey();
await gu.sendKeys(Key.chord(modKey, 'z'));
assert.deepEqual(await getEditModeChoiceLabels(), ['Green', 'Blue', 'Black', 'Apricot', 'Foo', 'Bar']);
await gu.sendKeys(Key.chord(modKey, 'z'));
assert.deepEqual(await getEditModeChoiceLabels(), ['Green', 'Blue', 'Black', 'Apricot', 'Foo']);
await gu.sendKeys(Key.chord(modKey, 'z'));
assert.deepEqual(await getEditModeChoiceLabels(), ['Green', 'Blue', 'Black', 'Apricot']);
// Redo, then undo, verifying at each step.
await gu.sendKeys(Key.chord(Key.CONTROL, 'y'));
assert.deepEqual(await getEditModeChoiceLabels(), ['Green', 'Blue', 'Black', 'Apricot', 'Foo']);
await gu.sendKeys(Key.chord(modKey, 'z'));
assert.deepEqual(await getEditModeChoiceLabels(), ['Green', 'Blue', 'Black', 'Apricot']);
// Change the color of 'Apricot' to white with black text, and modify font options
const [, , , apricotColorBtn] = await driver
.findAll('.test-tokenfield .test-color-button');
await apricotColorBtn.click();
await gu.setColor(driver.find('.test-fill-input'), 'rgb(255, 255, 255)');
await gu.setColor(driver.find('.test-text-input'), 'rgb(0, 0, 0)');
await gu.setFont('bold', false);
await gu.setFont('italic', false);
await gu.setFont('underline', true);
await driver.sendKeys(Key.ENTER);
assert.deepEqual(await getEditModeFillColors(), [DARK_GREEN_FILL, BLUE_FILL, BLACK_FILL, WHITE_FILL]);
assert.deepEqual(await getEditModeTextColors(), [WHITE_TEXT, BLACK_TEXT, WHITE_TEXT, BLACK_TEXT]);
assert.deepEqual(await getEditModeFontOptions(), [{bold, underline, strikethrough}, {}, {}, {underline}]);
// Undo, then re-do, verifying after each invocation
await driver.find('.test-choice-list-entry .test-tokenfield .test-tokenfield-input').click();
await gu.sendKeys(Key.chord(modKey, 'z'));
assert.deepEqual(await getEditModeFillColors(), [DARK_GREEN_FILL, BLUE_FILL, BLACK_FILL, APRICOT_FILL]);
assert.deepEqual(await getEditModeTextColors(), [WHITE_TEXT, BLACK_TEXT, WHITE_TEXT, APRICOT_TEXT]);
assert.deepEqual(await getEditModeFontOptions(), [{bold, underline, strikethrough}, {}, {}, {bold, italic}]);
await gu.sendKeys(Key.chord(Key.CONTROL, 'y'));
assert.deepEqual(await getEditModeFillColors(), [DARK_GREEN_FILL, BLUE_FILL, BLACK_FILL, WHITE_FILL]);
assert.deepEqual(await getEditModeTextColors(), [WHITE_TEXT, BLACK_TEXT, WHITE_TEXT, BLACK_TEXT]);
assert.deepEqual(await getEditModeFontOptions(), [{bold, underline, strikethrough}, {}, {}, {underline}]);
});
it('should support rich copy/paste in the choice config editor', async function() {
// Remove all choices
const modKey = await gu.modKey();
await gu.sendKeys(Key.chord(modKey, 'a'), Key.BACK_SPACE);
// Add a few new choices
await gu.sendKeys('Choice 1', Key.ENTER, 'Choice 2', Key.ENTER, 'Choice 3', Key.ENTER);
// Copy all the choices
await gu.sendKeys(await gu.selectAllKey());
await clipboard.lockAndPerform(async (cb) => {
await cb.copy();
// Delete all the choices, then paste them back
await driver.sendKeys(Key.BACK_SPACE);
assert.deepEqual(await getEditModeChoiceLabels(), []);
await cb.paste();
});
// Verify no data was lost
assert.deepEqual(await getEditModeChoiceLabels(), ['Choice 1', 'Choice 2', 'Choice 3']);
// In Jenkins, clipboard contents are pasted from the system clipboard, which only copies
// choices as newline-separated labels. For this reason, we can't check that the color
// information also got pasted, because the data is stored elsewhere. In actual use, the
// workflow above would copy all the choice data as well, and use it for pasting in the editor.
});
it('should save and close the choice config editor on focusout', async function() {
// Click outside of the editor.
await driver.find('.test-gristdoc').click();
await gu.waitAppFocus(true);
// Check that the changes were saved.
assert.deepEqual(await getChoiceLabels(), ['Choice 1', 'Choice 2', 'Choice 3']);
await gu.undo();
});
it('should add a new element on a fresh ChoiceList column', async function() {
await gu.addColumn("ChoiceList");
await gu.setType(gu.exactMatch("Choice List"));
const cell = await gu.getCell("ChoiceList", 1);
await cell.click();
await gu.sendKeys("foo");
const plus = await driver.findWait(".test-choice-list-editor-new-item", 100);
await plus.click();
await gu.sendKeys(Key.ENTER);
await gu.waitForServer();
assert.equal(await cell.getText(), "foo");
});
it('should add a new element on a fresh Choice column', async function() {
await gu.addColumn("Choice");
await gu.setType(gu.exactMatch("Choice"));
const cell = await gu.getCell("Choice", 1);
await cell.click();
await gu.sendKeys("foo");
const plus = await driver.findWait(".test-choice-editor-new-item", 100);
await plus.click();
await gu.waitForServer();
assert.equal(await cell.getText(), "foo");
});
for (const columnName of ["ChoiceList", "Choice"]) {
it(`should allow renaming tokens on ${columnName} column`, gu.revertChanges(async function () {
// Helper that converts ChoiceList to choice-list
const editorDashedName = columnName.toLowerCase().replace(/list/, "-list");
// Add two new options: one, two.
await gu.getCell(columnName, 2).click();
await gu.sendKeys("one");
await driver.findWait(`.test-${editorDashedName}-editor-new-item`, 300).click();
if (columnName === "ChoiceList") {
await gu.sendKeys(Key.ENTER);
}
await gu.waitForServer();
await gu.getCell(columnName, 3).click();
await gu.sendKeys("two");
await driver.findWait(`.test-${editorDashedName}-editor-new-item`, 300).click();
if (columnName === "ChoiceList") {
await gu.sendKeys(Key.ENTER);
}
await gu.waitForServer();
// Make sure right panel is open and has right focus.
await gu.toggleSidePanel("right", "open");
await driver.find(".test-right-tab-field").click();
// Rename one to three.
await editChoiceEntries();
await renameEntry("one", "three");
await saveChoiceEntries();
assert.deepEqual(await gu.getVisibleGridCells({ rowNums: [1, 2, 3], cols: [columnName] }), [
"foo",
"three",
"two",
]);
// Rename foo to bar, two to four, and three to eight, press undo 3 times
// and make sure nothing changes.
await editChoiceEntries();
await renameEntry("foo", "bar");
await renameEntry("three", "eight");
await renameEntry("two", "four");
assert.deepEqual(await getEditModeChoiceLabels(), ["bar", "eight", "four"]);
const undoKey = Key.chord(await gu.modKey(), "z");
await gu.sendKeys(undoKey);
await gu.sendKeys(undoKey);
await gu.sendKeys(undoKey);
assert.deepEqual(await getEditModeChoiceLabels(), ["foo", "three", "two"]);
// Make sure we can copy and paste without adding new item
await clickEntry('foo');
await clipboard.lockAndPerform(async (cb) => {
await cb.cut();
await cb.paste();
await cb.paste();
});
await gu.sendKeys(Key.ENTER);
await clickEntry('two');
await clipboard.lockAndPerform(async (cb) => {
await cb.copy();
await gu.sendKeys(Key.ARROW_RIGHT);
await cb.paste();
});
await gu.sendKeys(Key.ENTER);
assert.deepEqual(await getEditModeChoiceLabels(), ["foofoo", "three", "twotwo"]);
await saveChoiceEntries();
assert.deepEqual(await gu.getVisibleGridCells({ rowNums: [1, 2, 3], cols: [columnName] }), [
"foofoo",
"three",
"twotwo",
]);
// Rename to bar, four and eight and do the change.
await editChoiceEntries();
await renameEntry("foofoo", "bar");
await renameEntry("twotwo", "four");
await renameEntry("three", "eight");
await saveChoiceEntries();
assert.deepEqual(await gu.getVisibleGridCells({ rowNums: [1, 2, 3], cols: [columnName] }), [
"bar",
"eight",
"four",
]);
// Add color to bar, save it, change to foo and make sure the color is still there.
await editChoiceEntries();
const [barColor] = await driver.findAll(".test-tokenfield .test-color-button");
await barColor.click();
await gu.setColor(driver.find(".test-text-input"), "rgb(70, 13, 129)");
await driver.sendKeys(Key.ENTER);
await renameEntry("bar", "foo");
await saveChoiceEntries();
await editChoiceEntries();
const [fooColorText] = await getEditModeTextColors();
assert.equal(fooColorText, "rgba(70, 13, 129, 1)");
// Start renaming, but cancel out of the editor with two presses of the Escape key;
// a previous bug caused focus to be lost after the first Escape, making it impossible
// to close the editor with a subsequent press of Escape.
await editChoiceEntries();
await clickEntry("foo");
await gu.sendKeys("food");
await gu.sendKeys(Key.ESCAPE, Key.ESCAPE);
assert.isFalse(await driver.find(".test-choice-list-entry-save").isPresent());
},
// Test if the column is reverted to state before the test
() => gu.getVisibleGridCells({rowNums: [1, 2, 3], cols: [columnName]})));
}
it('should allow renaming multiple tokens on ChoiceList', gu.revertChanges(async function() {
// Work on ChoiceList column, add one new option "one"
await gu.getCell("ChoiceList", 2).click();
await gu.sendKeys("one");
await driver.findWait(`.test-choice-list-editor-new-item`, 300).click();
await gu.sendKeys(Key.ENTER);
await gu.waitForServer();
await gu.getCell("ChoiceList", 3).click();
await gu.sendKeys("one", Key.ENTER, "foo", Key.ENTER);
await gu.waitForServer();
// Make sure right panel is open and has right focus.
await gu.toggleSidePanel('right', 'open');
await driver.find('.test-right-tab-field').click();
// rename one to three
await editChoiceEntries();
await renameEntry("one", "three");
await renameEntry("foo", "four");
await saveChoiceEntries();
assert.deepEqual(await gu.getVisibleGridCells({rowNums: [1, 2, 3], cols: ['ChoiceList']}), [
'four',
'three',
'three\nfour',
]);
},
// Test if the column is reverted to state before the test
() => gu.getVisibleGridCells({rowNums: [1, 2, 3], cols: ['ChoiceList']})));
it('should rename saved filters', gu.revertChanges(async function() {
// Make sure right panel is open and has focus.
await gu.toggleSidePanel('right', 'open');
await driver.find('.test-right-tab-field').click();
// Work on ChoiceList column, add two new options: one, two
await gu.getCell("ChoiceList", 2).click();
await gu.sendKeys("one");
await driver.findWait(`.test-choice-list-editor-new-item`, 300).click();
await gu.sendKeys(Key.ENTER);
await gu.getCell("ChoiceList", 3).click();
await gu.sendKeys("one", Key.ENTER, "foo", Key.ENTER, Key.ENTER);
await gu.waitForServer();
// Make sure column looks like this:
assert.deepEqual(await gu.getVisibleGridCells({rowNums: [1, 2, 3, 4], cols: ['ChoiceList']}), [
'foo',
'one',
'one\nfoo',
'' // add row
]);
// Filter by single value and save.
await gu.filterBy('ChoiceList', true, [/one/]);
// Duplicate page, to make sure filters are also renamed in a new section.
await gu.openPageMenu('Table1');
await driver.find('.test-docpage-duplicate').click();
await driver.find('.test-modal-confirm').click();
await driver.findContentWait('.test-docpage-label', /copy/, 2000);
await gu.waitForServer();
// Go back to Table1
await gu.getPageItem('Table1').click();
// Make sure grid is filtered
assert.deepEqual(await gu.getVisibleGridCells({rowNums: [1, 2, 3], cols: ['ChoiceList']}), [
'one',
'one\nfoo',
'' // new row
]);
// Rename one to five, foo to bar
await editChoiceEntries();
await renameEntry("one", "five");
await renameEntry("foo", "bar");
await saveChoiceEntries();
// Make sure that there are still two records - filter should be changed to new values.
assert.deepEqual(await gu.getVisibleGridCells({rowNums: [1, 2, 3], cols: ['ChoiceList']}), [
'five',
'five\nbar',
'' // new row
]);
// Make sure that it also renamed filters in diffrent section.
await gu.getPageItem('Table1 (copy)').click();
assert.deepEqual(await gu.getVisibleGridCells({rowNums: [1, 2, 3], cols: ['ChoiceList']}), [
'five',
'five\nbar',
'' // new row
]);
// Go back to previous names, filter still should work.
await gu.undo();
assert.deepEqual(await gu.getVisibleGridCells({rowNums: [1, 2, 3], cols: ['ChoiceList']}), [
'one',
'one\nfoo',
'' // new row
]);
},
// Test if the column is reverted to state before the test
() => gu.getVisibleGridCells({rowNums: [1, 2, 3], cols: ['ChoiceList']})));
});