diff --git a/app/client/widgets/ChoiceListEditor.ts b/app/client/widgets/ChoiceListEditor.ts index 143d793b..753b202d 100644 --- a/app/client/widgets/ChoiceListEditor.ts +++ b/app/client/widgets/ChoiceListEditor.ts @@ -66,7 +66,7 @@ export class ChoiceListEditor extends NewBaseEditor { // If starting to edit by typing in a string, ignore previous tokens. const cellValue = decodeObject(options.cellValue); - const startLabels: unknown[] = options.editValue || !Array.isArray(cellValue) ? [] : cellValue; + const startLabels: unknown[] = options.editValue !== undefined || !Array.isArray(cellValue) ? [] : cellValue; const startTokens = startLabels.map(label => new ChoiceItem(String(label), !choiceSet.has(String(label)))); this._tokenField = TokenField.ctor().create(this, { diff --git a/app/client/widgets/ReferenceListEditor.ts b/app/client/widgets/ReferenceListEditor.ts index 2fcaad86..2fe98377 100644 --- a/app/client/widgets/ReferenceListEditor.ts +++ b/app/client/widgets/ReferenceListEditor.ts @@ -73,7 +73,7 @@ export class ReferenceListEditor extends NewBaseEditor { // If starting to edit by typing in a string, ignore previous tokens. const cellValue = decodeObject(options.cellValue); - const startRowIds: unknown[] = options.editValue || !Array.isArray(cellValue) ? [] : cellValue; + const startRowIds: unknown[] = options.editValue !== undefined || !Array.isArray(cellValue) ? [] : cellValue; // If referenced table hasn't loaded yet, hold off on initializing tokens. const needReload = (options.editValue === undefined && !this._utils.tableData.isLoaded); diff --git a/test/nbrowser/MultiColumn.ts b/test/nbrowser/MultiColumn.ts index 10470679..bc77e8f6 100644 --- a/test/nbrowser/MultiColumn.ts +++ b/test/nbrowser/MultiColumn.ts @@ -1,5 +1,6 @@ import {arrayRepeat} from 'app/plugin/gutil'; import * as gu from 'test/nbrowser/gristUtils'; +import {ColumnType} from 'test/nbrowser/gristUtils'; import {setupTestSuite} from 'test/nbrowser/testUtils'; import {UserAPIImpl} from 'app/common/UserAPI'; import {assert, driver, Key} from 'mocha-webdriver'; @@ -9,8 +10,10 @@ let doc: string; const transparent = 'rgba(0, 0, 0, 0)'; const blue = '#0000FF'; const red = '#FF0000'; -const types = ['Any', 'Text', 'Integer', 'Numeric', 'Toggle', 'Date', 'DateTime', 'Choice', 'Choice List', - 'Reference', 'Reference List', 'Attachment']; +const types: Array = [ + 'Any', 'Text', 'Integer', 'Numeric', 'Toggle', 'Date', 'DateTime', 'Choice', 'Choice List', + 'Reference', 'Reference List', 'Attachment' +]; describe('MultiColumn', function() { this.timeout(80000); @@ -96,7 +99,7 @@ describe('MultiColumn', function() { await gu.assertFillColor(await gu.getCell('Test2', 1), transparent); }); - for (const type of ['Choice', 'Text', 'Reference', 'Numeric']) { + for (const type of ['Choice', 'Text', 'Reference', 'Numeric'] as Array) { it(`should reset all columns to first column type for ${type}`, async () => { // We start with empty columns, then we will change first one // to a data column, select all and then change all other to the same type. diff --git a/test/nbrowser/TokenField.ts b/test/nbrowser/TokenField.ts new file mode 100644 index 00000000..5fd95bbd --- /dev/null +++ b/test/nbrowser/TokenField.ts @@ -0,0 +1,123 @@ +import {assert, Key} from 'mocha-webdriver'; +import * as gu from 'test/nbrowser/gristUtils'; +import {setupTestSuite} from 'test/nbrowser/testUtils'; + +describe('TokenField', function() { + this.timeout(20000); + const cleanup = setupTestSuite(); + let session: gu.Session; + + before(async function() { + session = await gu.session().login(); + await session.tempDoc(cleanup, "Favorite_Films.grist"); + await gu.toggleSidePanel('right', 'open'); + + // Prepare test table, as a base for following tests. + await gu.addNewPage('Table', 'New Table'); + await gu.sendKeys('one', Key.ENTER); + await gu.waitForServer(); + await gu.sendKeys('two', Key.ENTER); + await gu.waitForServer(); + await gu.sendKeys('three', Key.ENTER); + await gu.waitForServer(); + }); + + it('should clear choice list on card view', async function() { + // Test for a bug. In Card or Card List widgets, if the cursor is on a Ref List or Choice List field and you + // hit Backspace or Delete , the behavior is the same as hitting enter (pulls up list of references/choices to add + // one more, does not clear cell). + + // The bug was there because choice list editor and ref list editor didn't handle empty string as an edit value, + // which is a signal to clear the value. In a grid view, DELETE and BACKSPACE were handled by the grid itself. + + const revert = await gu.begin(); + await gu.getCell('A', 1).click(); + + // Convert A column to Choice List. + await gu.openColumnPanel(); + await gu.setType('Choice List', {apply: true}); + + // Add a second value to the first row. + await gu.getCell('A', 1).click(); + await gu.sendKeys(Key.ENTER); + await gu.sendKeys('two', Key.ENTER); + await gu.sendKeys(Key.ENTER); + await gu.waitForServer(); + await gu.getCell('A', 1).click(); + assert.equal(await gu.getCell('A', 1).getText(), 'one\ntwo'); + + // Change it to card view. + await gu.changeWidget('Card'); + + // Test that DELETE opens the editor and clears the value. + // Clicking on the cell twice will put it in edit mode, so we will first click other cell. + await gu.getDetailCell('B', 1).click(); + await gu.getDetailCell('A', 1).click(); + await gu.sendKeys(Key.DELETE); + await gu.checkTokenEditor(''); + await gu.sendKeys(Key.ESCAPE); + + // Now test BACKSPACE. + await gu.getDetailCell('B', 1).click(); + await gu.getDetailCell('A', 1).click(); + await gu.sendKeys(Key.BACK_SPACE); + await gu.checkTokenEditor(''); + await gu.sendKeys(Key.ESCAPE); + + // Value should still be there. + assert.equal(await gu.getDetailCell('A', 1).getText(), 'one\ntwo'); + // But ENTER works fine, it just opens the editor. + await gu.sendKeys(Key.ENTER); + await gu.checkTokenEditor('one\ntwo'); + await gu.sendKeys(Key.ESCAPE); + // Any other key also works + await gu.sendKeys('a'); + await gu.checkTokenEditor('a'); + await gu.sendKeys(Key.ESCAPE); + await revert(); + }); + + it('should clear ref list on card view', async function() { + await gu.getCell(0, 1).click(); + await gu.changeBehavior('Clear and reset'); + // This is an empty column, so no transformation is needed. + await gu.setType('Reference List', {apply: false}); + await gu.waitForServer(); + await gu.setRefTable('Films'); + await gu.waitForServer(); + await gu.setRefShowColumn('Title'); + await gu.waitForServer(); + + // Add two films. + await gu.sendKeys(Key.ENTER); + await gu.sendKeys('Toy', Key.ENTER); + await gu.sendKeys('Alien', Key.ENTER); + // Save. + await gu.sendKeys(Key.ENTER); + await gu.waitForServer(); + + // Make sure it works in Grid view. + await gu.getCell(0, 1).click(); + await gu.sendKeys(Key.DELETE); + await gu.waitForServer(); + assert.equal(await gu.getCell(0, 1).getText(), ''); + await gu.undo(); + await gu.sendKeys(Key.BACK_SPACE); + await gu.waitForServer(); + assert.equal(await gu.getCell(0, 1).getText(), ''); + await gu.undo(); + + // Now make sure it works in Card view. + await gu.changeWidget('Card'); + assert.equal(await gu.getDetailCell('A', 1).getText(), 'Toy Story\nAlien'); + await gu.sendKeys(Key.DELETE); + await gu.checkTokenEditor(''); + await gu.sendKeys(Key.ESCAPE); + await gu.sendKeys(Key.BACK_SPACE); + await gu.checkTokenEditor(''); + await gu.sendKeys(Key.ESCAPE); + await gu.waitForServer(); + // Nothing should have changed. + assert.equal(await gu.getDetailCell('A', 1).getText(), 'Toy Story\nAlien'); + }); +}); diff --git a/test/nbrowser/gristUtils.ts b/test/nbrowser/gristUtils.ts index e9b9250d..651236bf 100644 --- a/test/nbrowser/gristUtils.ts +++ b/test/nbrowser/gristUtils.ts @@ -654,6 +654,20 @@ export async function checkTextEditor(value: RegExp|string) { assert.match(await driver.find('.celleditor_text_editor').value(), valueRe); } +/** + * Checks that token editor in a cell has a correct value. Converts all tokens to text including the input field + * and joins them with newlines. + */ +export async function checkTokenEditor(value: RegExp|string) { + assert.equal(await driver.findWait('.test-widget-text-editor', 500).isDisplayed(), true); + const valueRe = typeof value === 'string' ? exactMatch(value) : value; + const allTokens = await driver.findAll( + '.test-widget-text-editor .test-tokenfield .test-tokenfield-token', e => e.getText()); + const inputToken = await driver.find('.test-widget-text-editor .test-tokenfield .test-tokenfield-input').value(); + const combined = [...allTokens, inputToken].join('\n').trim(); + assert.match(combined, valueRe); +} + /** * Enter rows of values into a GridView, starting at the given cell. Values are specified as a * list of rows, for examples `[['foo'], ['bar']]` will enter two rows, with one value in each. @@ -1181,7 +1195,10 @@ export async function selectWidget( await waitForServer(); } -export async function changeWidget(type: string) { +export type WidgetType = 'Table' | 'Card' | 'Card List' | 'Chart' | 'Custom'; + + +export async function changeWidget(type: WidgetType) { await openWidgetPanel(); await driver.findContent('.test-right-panel button', /Change Widget/).click(); await selectWidget(type); @@ -1406,6 +1423,7 @@ export async function waitForSidePanel() { export async function openWidgetPanel() { await toggleSidePanel('right', 'open'); await driver.find('.test-right-tab-pagewidget').click(); + await driver.find(".test-config-widget").click(); } /** @@ -1585,11 +1603,15 @@ export async function deleteColumn(col: IColHeader|string) { await waitForServer(); } +export type ColumnType = + 'Any' | 'Text' | 'Numeric' | 'Integer' | 'Toggle' | 'Date' | 'DateTime' | + 'Choice' | 'Choice List' | 'Reference' | 'Reference List' | 'Attachment'; + /** * Sets the type of the currently selected field to value. */ export async function setType( - type: RegExp|string, + type: RegExp|ColumnType, options: {skipWait?: boolean, apply?: boolean} = {} ) { const {skipWait, apply} = options; @@ -2957,10 +2979,13 @@ export async function setWidgetUrl(url: string) { await waitForServer(); } +type BehaviorActions = 'Clear and reset' | 'Convert column to data' | 'Clear and make into formula' | + 'Convert columns to data'; /** * Opens a behavior menu and clicks one of the option. */ -export async function changeBehavior(option: string|RegExp) { +export async function changeBehavior(option: BehaviorActions|RegExp) { + await openColumnPanel(); await driver.find('.test-field-behaviour').click(); await driver.findContent('.grist-floating-menu li', option).click(); await waitForServer();