2023-10-11 21:03:02 +00:00
|
|
|
/**
|
|
|
|
* Test for copy-pasting Grist data.
|
|
|
|
*
|
|
|
|
* TODO Most of the testing for copy-pasting lives in test/nbrowser/CopyPaste.ntest.js.
|
|
|
|
* This file just has some more recent additions to these test.
|
|
|
|
*/
|
|
|
|
import {arrayRepeat} from 'app/common/gutil';
|
|
|
|
import * as _ from 'lodash';
|
|
|
|
import {assert, driver, Key, WebElement} from 'mocha-webdriver';
|
|
|
|
import * as path from 'path';
|
|
|
|
import {serveStatic} from 'test/nbrowser/customUtil';
|
|
|
|
import * as gu from 'test/nbrowser/gristUtils';
|
|
|
|
import {setupTestSuite} from 'test/nbrowser/testUtils';
|
|
|
|
|
|
|
|
describe('CopyPaste', function() {
|
2023-10-17 19:38:19 +00:00
|
|
|
this.timeout(90000);
|
2023-10-11 21:03:02 +00:00
|
|
|
const cleanup = setupTestSuite();
|
|
|
|
const clipboard = gu.getLockableClipboard();
|
|
|
|
afterEach(() => gu.checkForErrors());
|
|
|
|
gu.bigScreen();
|
|
|
|
|
|
|
|
after(async function() {
|
|
|
|
await driver.executeScript(removeDummyTextArea);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should allow pasting merged cells', async function() {
|
|
|
|
// Test that we can paste uneven data, i.e. containing merged cells.
|
|
|
|
|
|
|
|
// Serve a static file with a page containing a table with some merged cells.
|
|
|
|
const serving = await serveStatic(path.join(gu.fixturesRoot, "sites/paste"));
|
|
|
|
await driver.get(`${serving.url}/paste.html`);
|
|
|
|
|
|
|
|
// Select everything in our little page.
|
|
|
|
await driver.executeScript(`
|
|
|
|
let range = document.createRange();
|
|
|
|
range.selectNodeContents(document.querySelector('table'));
|
|
|
|
let sel = window.getSelection();
|
|
|
|
sel.removeAllRanges();
|
|
|
|
sel.addRange(range);
|
|
|
|
`);
|
|
|
|
|
|
|
|
await clipboard.lockAndPerform(async (cb) => {
|
|
|
|
try {
|
|
|
|
await cb.copy();
|
|
|
|
} finally {
|
|
|
|
await serving?.shutdown();
|
|
|
|
}
|
|
|
|
|
|
|
|
const session = await gu.session().login();
|
|
|
|
await session.tempNewDoc(cleanup, 'CopyPaste');
|
|
|
|
|
|
|
|
await gu.getCell({col: 'A', rowNum: 1}).click();
|
|
|
|
await gu.waitAppFocus();
|
|
|
|
await cb.paste();
|
|
|
|
});
|
|
|
|
await gu.waitForServer();
|
|
|
|
|
|
|
|
await gu.checkForErrors();
|
|
|
|
assert.deepEqual(await gu.getVisibleGridCells({rowNums: [1, 2, 3, 4], cols: ['A', 'B']}), [
|
|
|
|
'a', 'b',
|
|
|
|
'c', '',
|
|
|
|
'd', 'e',
|
|
|
|
'f', '',
|
|
|
|
]);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should parse pasted numbers', async function() {
|
|
|
|
const session = await gu.session().teamSite.login();
|
|
|
|
await session.tempDoc(cleanup, 'PasteParsing.grist');
|
|
|
|
await driver.executeScript(createDummyTextArea);
|
|
|
|
|
2023-10-17 19:38:19 +00:00
|
|
|
await copyAndCheck(clipboard, [
|
|
|
|
'$1', '1',
|
|
|
|
'(2)', '-2',
|
|
|
|
'3e4', '30000',
|
|
|
|
'5,678.901', '5678.901',
|
|
|
|
'23%', '0.23',
|
|
|
|
'45 678', '45678',
|
|
|
|
|
|
|
|
// . is a decimal separator in this locale (USA) so this can't be parsed
|
|
|
|
'1.234.567', '1.234.567 INVALID',
|
|
|
|
|
|
|
|
// Doesn't match the default currency of the document, whereas $ above does
|
|
|
|
'€89', '€89 INVALID',
|
|
|
|
], true);
|
2023-10-11 21:03:02 +00:00
|
|
|
|
|
|
|
// Open the side panel for the numeric column.
|
|
|
|
await gu.toggleSidePanel('right', 'open');
|
|
|
|
await driver.find('.test-right-tab-field').click();
|
|
|
|
|
|
|
|
// Switch to currency mode, and check the result.
|
|
|
|
await driver.findContent('.test-numeric-mode .test-select-button', /\$/).click();
|
|
|
|
|
|
|
|
// Same data, just formatted differently
|
|
|
|
await checkGridCells([
|
|
|
|
'$1', '$1.00',
|
|
|
|
'(2)', '-$2.00',
|
|
|
|
'3e4', '$30,000.00',
|
|
|
|
'5,678.901', '$5,678.90',
|
|
|
|
'23%', '$0.23',
|
|
|
|
'45 678', '$45,678.00',
|
|
|
|
'1.234.567', '1.234.567 INVALID',
|
|
|
|
'€89', '€89 INVALID',
|
|
|
|
]);
|
|
|
|
|
|
|
|
// Check that currency is set to 'Default currency' by default (where the default is local currency).
|
|
|
|
assert.equal(await driver.find('.test-currency-autocomplete input').value(), 'Default currency (USD)');
|
|
|
|
|
|
|
|
// Change column setting for currency to Euros
|
|
|
|
await driver.findWait('.test-currency-autocomplete', 500).click();
|
|
|
|
await driver.sendKeys("eur", Key.ENTER);
|
|
|
|
await gu.waitForServer();
|
|
|
|
|
|
|
|
// Same data, just formatted differently
|
|
|
|
await checkGridCells([
|
|
|
|
'$1', '€1.00',
|
|
|
|
'(2)', '-€2.00',
|
|
|
|
'3e4', '€30,000.00',
|
|
|
|
'5,678.901', '€5,678.90',
|
|
|
|
'23%', '€0.23',
|
|
|
|
'45 678', '€45,678.00',
|
|
|
|
'1.234.567', '1.234.567 INVALID',
|
|
|
|
'€89', '€89 INVALID',
|
|
|
|
]);
|
|
|
|
|
|
|
|
// Copy the numbers column into itself.
|
|
|
|
// Values which were already parsed remain parsed since it copies the underlying numbers.
|
|
|
|
await clipboard.lockAndPerform(async (cb) => {
|
|
|
|
await copy(cb, 'Parsed');
|
|
|
|
});
|
|
|
|
await checkGridCells([
|
|
|
|
'$1', '€1.00',
|
|
|
|
'(2)', '-€2.00',
|
|
|
|
'3e4', '€30,000.00',
|
|
|
|
'5,678.901', '€5,678.90',
|
|
|
|
'23%', '€0.23',
|
|
|
|
'45 678', '€45,678.00',
|
|
|
|
'1.234.567', '1.234.567 INVALID',
|
|
|
|
|
|
|
|
// This was invalid before, so it was copied as text.
|
|
|
|
// This time it parsed successfully because the currency matches.
|
|
|
|
'€89', '€89.00',
|
|
|
|
]);
|
|
|
|
|
2023-10-17 19:38:19 +00:00
|
|
|
await copyAndCheck(clipboard, [
|
|
|
|
// Now we're copying from the text column so everything is parsed again.
|
|
|
|
// $ can no longer be parsed now the currency is euros.
|
|
|
|
'$1', '$1 INVALID',
|
|
|
|
|
|
|
|
'(2)', '-€2.00',
|
|
|
|
'3e4', '€30,000.00',
|
|
|
|
'5,678.901', '€5,678.90',
|
|
|
|
'23%', '€0.23',
|
|
|
|
'45 678', '€45,678.00',
|
|
|
|
'1.234.567', '1.234.567 INVALID',
|
|
|
|
'€89', '€89.00',
|
|
|
|
], true);
|
2023-10-11 21:03:02 +00:00
|
|
|
|
|
|
|
// Change the document locale
|
|
|
|
await gu.openDocumentSettings();
|
|
|
|
await driver.findWait('.test-locale-autocomplete', 500).click();
|
|
|
|
await driver.sendKeys("Germany", Key.ENTER);
|
|
|
|
await gu.waitForServer();
|
|
|
|
await driver.navigate().back();
|
|
|
|
|
|
|
|
// Same data, just formatted differently
|
|
|
|
// Currency sign has moved to the end
|
|
|
|
// Decimal separator is now ','
|
|
|
|
// Digit group separator is now '.'
|
|
|
|
await checkGridCells([
|
|
|
|
'$1', '$1 INVALID',
|
|
|
|
'(2)', '-2,00 €',
|
|
|
|
'3e4', '30.000,00 €',
|
|
|
|
'5,678.901', '5.678,90 €',
|
|
|
|
'23%', '0,23 €',
|
|
|
|
'45 678', '45.678,00 €',
|
|
|
|
'1.234.567', '1.234.567 INVALID',
|
|
|
|
'€89', '89,00 €',
|
|
|
|
]);
|
|
|
|
|
|
|
|
// Copy the numbers column into itself.
|
|
|
|
// Values which were already parsed don't change since it copies the underlying numbers.
|
|
|
|
await clipboard.lockAndPerform(async (cb) => {
|
|
|
|
await copy(cb, 'Parsed');
|
|
|
|
});
|
|
|
|
await checkGridCells([
|
|
|
|
'$1', '$1 INVALID',
|
|
|
|
'(2)', '-2,00 €',
|
|
|
|
'3e4', '30.000,00 €',
|
|
|
|
'5,678.901', '5.678,90 €',
|
|
|
|
'23%', '0,23 €',
|
|
|
|
'45 678', '45.678,00 €',
|
|
|
|
|
|
|
|
// This can be parsed for the first time now that '.'
|
|
|
|
// is seen as a digit group separator
|
|
|
|
'1.234.567', '1.234.567,00 €',
|
|
|
|
|
|
|
|
'€89', '89,00 €',
|
|
|
|
]);
|
|
|
|
|
2023-10-17 19:38:19 +00:00
|
|
|
await copyAndCheck(clipboard, [
|
|
|
|
'$1', '$1 INVALID',
|
|
|
|
'(2)', '-2,00 €',
|
|
|
|
'3e4', '30.000,00 €',
|
|
|
|
|
|
|
|
// Now we're copying from the text column so everything is parsed again.
|
|
|
|
// The result in this case is not good:
|
|
|
|
// '.' was simply removed because we don't check where it is
|
|
|
|
// ',' is the decimal separator
|
|
|
|
// So this is parsed as 5.678901
|
|
|
|
// which rounds to 5.68 to two decimal places for the currency format
|
|
|
|
'5,678.901', '5,68 €',
|
|
|
|
|
|
|
|
'23%', '0,23 €',
|
|
|
|
'45 678', '45.678,00 €',
|
|
|
|
'1.234.567', '1.234.567,00 €',
|
|
|
|
'€89', '89,00 €',
|
|
|
|
], true);
|
2023-10-11 21:03:02 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
it('should parse pasted dates', async function() {
|
|
|
|
await gu.getPageItem("Dates").click();
|
|
|
|
|
2023-10-17 19:38:19 +00:00
|
|
|
await copyAndCheck(clipboard, [
|
|
|
|
'01-02-03', '01-02-2003',
|
|
|
|
'01 02 2003', '01-02-2003',
|
|
|
|
'1/02/03', '01-02-2003',
|
|
|
|
'01/2/03', '01-02-2003',
|
|
|
|
'1/2/03', '01-02-2003',
|
|
|
|
'1/2/3', '1/2/3 INVALID',
|
|
|
|
'20/10/03', '20-10-2003',
|
|
|
|
'10/20/03', '10/20/03 INVALID',
|
|
|
|
]);
|
2023-10-11 21:03:02 +00:00
|
|
|
|
|
|
|
await gu.getCell({col: 'Parsed', rowNum: 1}).click();
|
|
|
|
assert.equal(await gu.getDateFormat(), "DD-MM-YYYY");
|
|
|
|
await gu.setDateFormat("MM-DD-YYYY");
|
|
|
|
|
|
|
|
// Same data, just formatted differently
|
|
|
|
await checkGridCells([
|
|
|
|
'01-02-03', '02-01-2003',
|
|
|
|
'01 02 2003', '02-01-2003',
|
|
|
|
'1/02/03', '02-01-2003',
|
|
|
|
'01/2/03', '02-01-2003',
|
|
|
|
'1/2/03', '02-01-2003',
|
|
|
|
'1/2/3', '1/2/3 INVALID',
|
|
|
|
'20/10/03', '10-20-2003',
|
|
|
|
'10/20/03', '10/20/03 INVALID',
|
|
|
|
]);
|
|
|
|
|
|
|
|
// Copy the parsed column into itself.
|
|
|
|
// Values which were already parsed don't change since it copies the underlying values.
|
|
|
|
await clipboard.lockAndPerform(async (cb) => {
|
|
|
|
await copy(cb, 'Parsed');
|
|
|
|
});
|
|
|
|
|
|
|
|
await checkGridCells([
|
|
|
|
'01-02-03', '02-01-2003',
|
|
|
|
'01 02 2003', '02-01-2003',
|
|
|
|
'1/02/03', '02-01-2003',
|
|
|
|
'01/2/03', '02-01-2003',
|
|
|
|
'1/2/03', '02-01-2003',
|
|
|
|
'1/2/3', '1/2/3 INVALID',
|
|
|
|
'20/10/03', '10-20-2003',
|
|
|
|
'10/20/03', '10-20-2003', // can be parsed now
|
|
|
|
]);
|
|
|
|
|
|
|
|
// Copy from the text column again, things get re-parsed
|
2023-10-17 19:38:19 +00:00
|
|
|
await copyAndCheck(clipboard, [
|
|
|
|
'01-02-03', '01-02-2003',
|
|
|
|
'01 02 2003', '01-02-2003',
|
|
|
|
'1/02/03', '01-02-2003',
|
|
|
|
'01/2/03', '01-02-2003',
|
|
|
|
'1/2/03', '01-02-2003',
|
|
|
|
'1/2/3', '1/2/3 INVALID',
|
|
|
|
'20/10/03', '20/10/03 INVALID', // newly invalid
|
|
|
|
'10/20/03', '10-20-2003',
|
|
|
|
]);
|
2023-10-11 21:03:02 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
// Note that these tests which reference other tables
|
|
|
|
// assume that the previous tests have run.
|
|
|
|
it('should parse pasted references', async function() {
|
|
|
|
await gu.getPageItem("References").click();
|
|
|
|
await gu.getCell({col: 'Parsed', rowNum: 1}).click();
|
|
|
|
assert.equal(await gu.getRefTable(), "Dates");
|
|
|
|
assert.equal(await gu.getRefShowColumn(), "Text");
|
|
|
|
|
|
|
|
// Initially the References.Parsed column is displaying Dates.Text
|
|
|
|
// No date parsing happens, we just see which strings exist in that column
|
2023-10-17 19:38:19 +00:00
|
|
|
await copyAndCheck(clipboard, [
|
|
|
|
'20/10/03', '20/10/03',
|
|
|
|
'10/20/03', '10/20/03',
|
|
|
|
'1/2/3', '1/2/3',
|
|
|
|
'foo', 'foo INVALID',
|
|
|
|
'3', '3 INVALID',
|
|
|
|
'-2', '-2 INVALID',
|
|
|
|
'$1', '$1 INVALID',
|
|
|
|
'€89', '€89 INVALID',
|
|
|
|
], true);
|
2023-10-11 21:03:02 +00:00
|
|
|
|
|
|
|
await gu.setRefShowColumn("Parsed");
|
|
|
|
|
|
|
|
// // Same data, just formatted differently
|
|
|
|
await checkGridCells([
|
|
|
|
// In the Parsed column, only the second value was parsed as an actual date
|
|
|
|
// The others look invalid in the Dates table, but here they're valid references
|
|
|
|
'20/10/03', '20/10/03',
|
|
|
|
'10/20/03', '10-20-2003',
|
|
|
|
'1/2/3', '1/2/3',
|
|
|
|
|
|
|
|
'foo', 'foo INVALID',
|
|
|
|
'3', '3 INVALID',
|
|
|
|
'-2', '-2 INVALID',
|
|
|
|
'$1', '$1 INVALID',
|
|
|
|
'€89', '€89 INVALID',
|
|
|
|
]);
|
|
|
|
|
2023-10-17 19:38:19 +00:00
|
|
|
await copyAndCheck(clipboard, [
|
|
|
|
'20/10/03', '20/10/03',
|
|
|
|
'10/20/03', '10-20-2003',
|
|
|
|
'1/2/3', '1/2/3',
|
|
|
|
'foo', 'foo INVALID',
|
|
|
|
'3', `3 INVALID`,
|
|
|
|
'-2', `-2 INVALID`,
|
|
|
|
'$1', `$1 INVALID`,
|
|
|
|
'€89', '€89 INVALID',
|
|
|
|
]);
|
2023-10-11 21:03:02 +00:00
|
|
|
|
|
|
|
await gu.setRefShowColumn("Row ID");
|
|
|
|
|
|
|
|
// Same data, just formatted differently
|
|
|
|
await checkGridCells([
|
|
|
|
'20/10/03', 'Dates[5]',
|
|
|
|
'10/20/03', 'Dates[6]',
|
|
|
|
'1/2/3', 'Dates[4]',
|
|
|
|
'foo', 'foo INVALID',
|
|
|
|
'3', `3 INVALID`,
|
|
|
|
'-2', `-2 INVALID`,
|
|
|
|
'$1', `$1 INVALID`,
|
|
|
|
'€89', '€89 INVALID',
|
|
|
|
]);
|
|
|
|
|
2023-10-17 19:38:19 +00:00
|
|
|
await copyAndCheck(clipboard, [
|
|
|
|
'20/10/03', '20/10/03 INVALID',
|
|
|
|
'10/20/03', '10/20/03 INVALID',
|
|
|
|
'1/2/3', '1/2/3 INVALID',
|
|
|
|
'foo', 'foo INVALID',
|
|
|
|
'3', 'Dates[3]', // 3 is the only valid Row ID
|
|
|
|
'-2', '-2 INVALID',
|
|
|
|
'$1', '$1 INVALID',
|
|
|
|
'€89', '€89 INVALID',
|
|
|
|
]);
|
2023-10-11 21:03:02 +00:00
|
|
|
|
|
|
|
await gu.setRefTable("Numbers");
|
|
|
|
|
|
|
|
// These checks run with References.Parsed as both a Reference and Reference List column.
|
|
|
|
async function checkRefsToNumbers() {
|
|
|
|
await gu.setRefShowColumn("Row ID");
|
|
|
|
|
2023-10-17 19:38:19 +00:00
|
|
|
await copyAndCheck(clipboard, [
|
|
|
|
'20/10/03', '20/10/03 INVALID',
|
|
|
|
'10/20/03', '10/20/03 INVALID',
|
|
|
|
'1/2/3', '1/2/3 INVALID',
|
|
|
|
'foo', 'foo INVALID',
|
|
|
|
'3', 'Numbers[3]',
|
|
|
|
'-2', '-2 INVALID',
|
|
|
|
'$1', '$1 INVALID',
|
|
|
|
'€89', '€89 INVALID',
|
|
|
|
], true);
|
2023-10-11 21:03:02 +00:00
|
|
|
|
|
|
|
await gu.setRefShowColumn("Text");
|
|
|
|
|
2023-10-17 19:38:19 +00:00
|
|
|
await copyAndCheck(clipboard, [
|
|
|
|
'20/10/03', '20/10/03 INVALID',
|
|
|
|
'10/20/03', '10/20/03 INVALID',
|
|
|
|
'1/2/3', '1/2/3 INVALID',
|
|
|
|
'foo', 'foo INVALID',
|
|
|
|
'3', '3 INVALID',
|
|
|
|
'-2', '-2 INVALID',
|
|
|
|
// These are the only strings that appear in Numbers.Text verbatim
|
|
|
|
'$1', '$1',
|
|
|
|
'€89', '€89',
|
|
|
|
]);
|
2023-10-11 21:03:02 +00:00
|
|
|
|
|
|
|
await gu.setRefShowColumn("Parsed");
|
|
|
|
|
|
|
|
// Same data, just formatted differently
|
|
|
|
await checkGridCells([
|
|
|
|
'20/10/03', '20/10/03 INVALID',
|
|
|
|
'10/20/03', '10/20/03 INVALID',
|
|
|
|
'1/2/3', '1/2/3 INVALID',
|
|
|
|
'foo', 'foo INVALID',
|
|
|
|
'3', '3 INVALID',
|
|
|
|
'-2', '-2 INVALID',
|
|
|
|
'$1', '$1',
|
|
|
|
'€89', '89,00 €',
|
|
|
|
]);
|
|
|
|
|
2023-10-17 19:38:19 +00:00
|
|
|
await copyAndCheck(clipboard, [
|
|
|
|
'20/10/03', '20/10/03 INVALID',
|
|
|
|
'10/20/03', '10/20/03 INVALID',
|
|
|
|
'1/2/3', '1/2/3 INVALID',
|
|
|
|
'foo', 'foo INVALID',
|
|
|
|
'3', '3 INVALID', // parsed, but not a valid reference
|
|
|
|
'-2', '-2,00 €',
|
|
|
|
'$1', '$1', // invalid in Numbers.parsed, but a valid reference
|
|
|
|
'€89', '89,00 €',
|
|
|
|
]);
|
2023-10-11 21:03:02 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
await checkRefsToNumbers();
|
|
|
|
|
|
|
|
// Copy the Parsed column into the same column in a forked document.
|
|
|
|
// Because it's a different document, it uses the display values instead of the raw values (row IDs)
|
|
|
|
// to avoid referencing the wrong rows.
|
|
|
|
await clipboard.lockAndPerform(async (cb) => {
|
|
|
|
await copy(cb, 'Parsed');
|
|
|
|
await driver.get(await driver.getCurrentUrl() + "/m/fork");
|
|
|
|
await gu.waitForDocToLoad();
|
|
|
|
await driver.executeScript(createDummyTextArea);
|
|
|
|
await gu.setRefShowColumn("Text");
|
|
|
|
await paste(cb);
|
|
|
|
});
|
|
|
|
await checkGridCells([
|
|
|
|
'20/10/03', '20/10/03 INVALID',
|
|
|
|
'10/20/03', '10/20/03 INVALID',
|
|
|
|
'1/2/3', '1/2/3 INVALID',
|
|
|
|
'foo', 'foo INVALID',
|
|
|
|
'3', '3 INVALID',
|
|
|
|
'-2', '-2,00 € INVALID',
|
|
|
|
'$1', '$1',
|
|
|
|
'€89', '89,00 € INVALID',
|
|
|
|
]);
|
|
|
|
|
|
|
|
// Test the main copies with the Numbers table data not loaded in the browser
|
|
|
|
// so the lookups get done in the data engine.
|
|
|
|
await checkRefsToNumbers();
|
|
|
|
|
|
|
|
// Now test that pasting the same values into a Reference List column
|
|
|
|
// produces the same result (reflists containing a single reference)
|
2023-10-17 19:38:19 +00:00
|
|
|
await gu.setType(/Reference List/, {apply: true});
|
2023-10-11 21:03:02 +00:00
|
|
|
|
|
|
|
// Clear the Parsed column. Make sure we don't edit the column header.
|
|
|
|
await gu.getCell({col: "Parsed", rowNum: 1}).click();
|
|
|
|
await gu.getColumnHeader({col: "Parsed"}).click();
|
|
|
|
await gu.sendKeys(Key.BACK_SPACE);
|
|
|
|
await gu.waitForServer();
|
|
|
|
|
|
|
|
await checkRefsToNumbers();
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should parse pasted reference lists containing multiple values', async function() {
|
|
|
|
async function checkMultiRefs() {
|
|
|
|
await gu.setRefShowColumn("Row ID");
|
|
|
|
|
2023-10-17 19:38:19 +00:00
|
|
|
await copyAndCheck(clipboard, [
|
|
|
|
'"(2)",$1', '"(2)",$1 INVALID',
|
|
|
|
'$1,(2),22', '$1,(2),22 INVALID',
|
|
|
|
'["$1",-2]', '["$1",-2] INVALID',
|
|
|
|
'1,-2', '1,-2 INVALID',
|
|
|
|
'3,5', 'Numbers[3]\nNumbers[5]', // only valid row IDs
|
|
|
|
'-2,30000', '-2,30000 INVALID',
|
|
|
|
'7,0', '7,0 INVALID', // 0 is not a valid row ID
|
|
|
|
'', '',
|
|
|
|
]);
|
2023-10-11 21:03:02 +00:00
|
|
|
|
|
|
|
await gu.setRefShowColumn("Text");
|
|
|
|
|
2023-10-17 19:38:19 +00:00
|
|
|
await copyAndCheck(clipboard, [
|
|
|
|
'"(2)",$1', '(2)\n$1', // only verbatim text
|
|
|
|
'$1,(2),22', '$1,(2),22 INVALID', // 22 is invalid so whole thing fails
|
|
|
|
'["$1",-2]', '["$1",-2] INVALID', // -2 is invalid because this is text, not parsed
|
|
|
|
'1,-2', '1,-2 INVALID',
|
|
|
|
'3,5', '3,5 INVALID',
|
|
|
|
'-2,30000', '-2,30000 INVALID',
|
|
|
|
'7,0', '7,0 INVALID',
|
|
|
|
'', '',
|
|
|
|
]);
|
2023-10-11 21:03:02 +00:00
|
|
|
|
|
|
|
await gu.setRefShowColumn("Parsed");
|
|
|
|
|
2023-10-17 19:38:19 +00:00
|
|
|
await copyAndCheck(clipboard, [
|
|
|
|
'"(2)",$1', '-2,00 €\n$1',
|
|
|
|
'$1,(2),22', '$1,(2),22 INVALID',
|
|
|
|
'["$1",-2]', '$1\n-2,00 €',
|
|
|
|
'1,-2', '1,-2 INVALID',
|
|
|
|
'3,5', '3,5 INVALID',
|
|
|
|
'-2,30000', '-2,00 €\n30.000,00 €',
|
|
|
|
'7,0', '7,0 INVALID',
|
|
|
|
'', '',
|
|
|
|
], true);
|
2023-10-11 21:03:02 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
await gu.getPageItem("Multi-References").click();
|
|
|
|
await gu.waitForServer();
|
|
|
|
await gu.getCell({col: 'Parsed', rowNum: 1}).click();
|
|
|
|
|
|
|
|
await checkMultiRefs();
|
|
|
|
|
|
|
|
// Load the Numbers table data in the browser and check again
|
|
|
|
await gu.getPageItem("Numbers").click();
|
|
|
|
await gu.getPageItem("Multi-References").click();
|
|
|
|
await gu.waitForServer();
|
|
|
|
await checkMultiRefs();
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should parse pasted choice lists', async function() {
|
|
|
|
await gu.getPageItem("ChoiceLists").click();
|
|
|
|
await gu.waitForServer();
|
|
|
|
|
2023-10-17 19:38:19 +00:00
|
|
|
await copyAndCheck(clipboard, [
|
|
|
|
'', '',
|
|
|
|
'a', 'a',
|
2023-10-11 21:03:02 +00:00
|
|
|
|
2023-10-17 19:38:19 +00:00
|
|
|
// On the left, \n in text affects parsing and separates choices
|
|
|
|
// On the right, \n is how choices are separated in .getText()
|
|
|
|
// So the newlines on the two sides match, but also "e,f" -> "e\nf"
|
|
|
|
'a b\nc d\ne,f', 'a b\nc d\ne\nf',
|
2023-10-11 21:03:02 +00:00
|
|
|
|
2023-10-17 19:38:19 +00:00
|
|
|
// CSVs
|
|
|
|
'a,b ', 'a\nb',
|
|
|
|
' "a ", b,"a,b " ', 'a\nb\na,b',
|
2023-10-11 21:03:02 +00:00
|
|
|
|
2023-10-17 19:38:19 +00:00
|
|
|
// JSON. Empty strings and null are removed
|
|
|
|
' ["a","b","a,b", null] ', 'a\nb\na,b',
|
2023-10-11 21:03:02 +00:00
|
|
|
|
2023-10-17 19:38:19 +00:00
|
|
|
// Nested JSON is formatted as JSON or CSV depending on nesting level
|
|
|
|
'["a","b",["a,b"], [["a,b"]], [["a", "b"], "c", "d"], "", " "]',
|
|
|
|
'a\nb\n"a,b"\n[["a,b"]]\n[["a", "b"], "c", "d"]',
|
2023-10-11 21:03:02 +00:00
|
|
|
|
2023-10-17 19:38:19 +00:00
|
|
|
'[]', '',
|
|
|
|
], true);
|
2023-10-11 21:03:02 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
it('should parse pasted datetimes', async function() {
|
|
|
|
await gu.getPageItem("DateTimes").click();
|
|
|
|
await gu.waitForServer();
|
|
|
|
|
2023-10-17 19:38:19 +00:00
|
|
|
await copyAndCheck(clipboard, [
|
|
|
|
'2021-11-12 22:57:17+03:00', '12-11-2021 21:57 SAST', // note the 1-hour difference
|
|
|
|
'2021-11-12 22:57:17+02:00', '12-11-2021 22:57 SAST',
|
|
|
|
'12-11-2021 22:57:17 SAST', '12-11-2021 22:57 SAST',
|
|
|
|
'12-11-2021 22:57:17', '12-11-2021 22:57 SAST',
|
|
|
|
'12-11-2021 22:57:17 UTC', '13-11-2021 00:57 SAST', // note the 2-hour difference
|
|
|
|
'12-11-2021 22:57:17 Z', '13-11-2021 00:57 SAST', // note the 2-hour difference
|
|
|
|
// EST doesn't match the current timezone so it's rejected
|
|
|
|
'12-11-2021 22:57:17 EST', '12-11-2021 22:57:17 EST INVALID',
|
|
|
|
// Date without time is allowed
|
|
|
|
'12-11-2021', '12-11-2021 00:00 SAST',
|
|
|
|
]);
|
2023-10-11 21:03:02 +00:00
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// mapper for getVisibleGridCells to get both text and whether the cell is invalid (pink).
|
|
|
|
// Invalid cells mean text that was not parsed to the column type.
|
|
|
|
async function mapper(el: WebElement) {
|
|
|
|
let text = await el.getText();
|
|
|
|
if (await el.find(".field_clip").matches(".invalid")) {
|
|
|
|
text += " INVALID";
|
|
|
|
}
|
|
|
|
return text;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Checks that the full grid is equal to the given argument
|
|
|
|
// The first column never changes, it's only included for readability of the test
|
|
|
|
async function checkGridCells(expected: string[]) {
|
|
|
|
const actual = await gu.getVisibleGridCells({rowNums: _.range(1, 9), cols: ['Text', 'Parsed'], mapper});
|
|
|
|
assert.deepEqual(actual, expected);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Paste whatever's in the clipboard into the Parsed column
|
|
|
|
async function paste(cb: gu.IClipboard) {
|
|
|
|
// Click the first cell rather than the column header so that it doesn't try renaming the column
|
|
|
|
await gu.getCell({col: 'Parsed', rowNum: 1}).click();
|
|
|
|
await cb.paste();
|
|
|
|
await gu.waitForServer();
|
|
|
|
await gu.checkForErrors();
|
|
|
|
}
|
|
|
|
|
|
|
|
// Copy the contents of fromCol into the Parsed column
|
|
|
|
async function copy(cb: gu.IClipboard, fromCol: 'Text' | 'Parsed') {
|
|
|
|
await gu.getColumnHeader({col: fromCol}).click();
|
|
|
|
await cb.copy();
|
|
|
|
await paste(cb);
|
|
|
|
}
|
|
|
|
|
2023-10-17 19:38:19 +00:00
|
|
|
async function copyAndCheck(
|
|
|
|
clipboard: gu.ILockableClipboard,
|
|
|
|
expected: string[],
|
|
|
|
extraChecks: boolean = false
|
|
|
|
) {
|
|
|
|
await clipboard.lockAndPerform(async (cb) => {
|
|
|
|
// Copy Text cells into the Parsed column
|
|
|
|
await copy(cb, 'Text');
|
|
|
|
await checkGridCells(expected);
|
|
|
|
|
|
|
|
// Tests some extra features of parsing that don't really depend on the column
|
|
|
|
// type and so don't need to be checked with every call to copyAndCheck
|
|
|
|
if (extraChecks) {
|
|
|
|
// With the text cells still in the clipboard, convert the clipboard from
|
|
|
|
// rich data (cells) to plain text and confirm that it gets parsed the same way.
|
|
|
|
// The cells are still selected, clear them all.
|
|
|
|
await gu.sendKeys(Key.BACK_SPACE);
|
|
|
|
await gu.waitForServer();
|
|
|
|
assert.deepEqual(
|
|
|
|
await gu.getVisibleGridCells({rowNums: _.range(1, 9), cols: ['Parsed']}),
|
|
|
|
arrayRepeat(8, ''),
|
|
|
|
);
|
|
|
|
|
|
|
|
// Paste the text cells to the dummy textarea.
|
|
|
|
await driver.find('#dummyText').click();
|
|
|
|
await gu.waitAppFocus(false);
|
|
|
|
await cb.paste();
|
|
|
|
}
|
|
|
|
});
|
2023-10-11 21:03:02 +00:00
|
|
|
|
|
|
|
if (extraChecks) {
|
|
|
|
await gu.sendKeys(await gu.selectAllKey());
|
2023-10-17 19:38:19 +00:00
|
|
|
await clipboard.lockAndPerform(async (cb) => {
|
|
|
|
await cb.copy();
|
|
|
|
await gu.sendKeys(Key.BACK_SPACE);
|
2023-10-11 21:03:02 +00:00
|
|
|
|
2023-10-17 19:38:19 +00:00
|
|
|
// Paste the now plain text and confirm that the resulting data is still the same.
|
|
|
|
await gu.getCell({col: 'Text', rowNum: 1}).click();
|
|
|
|
await gu.waitAppFocus();
|
|
|
|
await paste(cb);
|
|
|
|
});
|
2023-10-11 21:03:02 +00:00
|
|
|
await checkGridCells(expected);
|
|
|
|
|
|
|
|
// Check that copying from the Parsed column back into itself doesn't change anything.
|
2023-10-17 19:38:19 +00:00
|
|
|
await clipboard.lockAndPerform(async (cb) => {
|
|
|
|
await copy(cb, 'Parsed');
|
|
|
|
});
|
2023-10-11 21:03:02 +00:00
|
|
|
await checkGridCells(expected);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function createDummyTextArea() {
|
|
|
|
const textarea = document.createElement('textarea');
|
|
|
|
textarea.style.position = "absolute";
|
|
|
|
textarea.style.top = "0";
|
|
|
|
textarea.style.height = "2rem";
|
|
|
|
textarea.style.width = "16rem";
|
|
|
|
textarea.id = 'dummyText';
|
|
|
|
window.document.body.appendChild(textarea);
|
|
|
|
}
|
|
|
|
|
|
|
|
function removeDummyTextArea() {
|
|
|
|
const textarea = document.getElementById('dummyText');
|
|
|
|
if (textarea) {
|
|
|
|
window.document.body.removeChild(textarea);
|
|
|
|
}
|
|
|
|
}
|