diff --git a/test/declarations.d.ts b/test/declarations.d.ts index a0a45094..63c06435 100644 --- a/test/declarations.d.ts +++ b/test/declarations.d.ts @@ -1,4 +1,13 @@ -declare module "test/nbrowser/gristUtil-nbrowser"; +declare module "test/nbrowser/gristUtil-nbrowser" { + // TODO - tsc can now do nice type inference for most of this, except $, + // so could change how export is done. Right now it leads to a mess because + // of $. + export declare let $: any; + export declare let gu: any; + export declare let server: any; + export declare let test: any; +} + // Adds missing type declaration to chai declare namespace Chai { diff --git a/test/nbrowser/LinkingErrors.ts b/test/nbrowser/LinkingErrors.ts new file mode 100644 index 00000000..5d5d4aa6 --- /dev/null +++ b/test/nbrowser/LinkingErrors.ts @@ -0,0 +1,182 @@ +/** + * Test various error situations with linking page widgets. + */ +import {assert, driver, stackWrapFunc} from 'mocha-webdriver'; +import * as gu from 'test/nbrowser/gristUtils'; +import {setupTestSuite} from 'test/nbrowser/testUtils'; + +import {toTableDataAction} from 'app/common/DocActions'; +import {schema} from 'app/common/schema'; +import {TableData} from 'app/common/TableData'; +import {DocAPI, UserAPI} from 'app/common/UserAPI'; + +describe("LinkingErrors", function() { + this.timeout(20000); + const cleanup = setupTestSuite(); + let session: gu.Session; + let api: UserAPI; + let docId: string; + + afterEach(() => gu.checkForErrors()); + + it("should link correctly in the normal case", async function() { + session = await gu.session().teamSite.login(); + api = session.createHomeApi(); + docId = await session.tempNewDoc(cleanup, 'LinkingErrors1', {load: false}); + + // Make a table with some data. + await api.applyUserActions(docId, [ + ['AddTable', 'Planets', [{id: 'PlanetName'}]], + // Negative IDs allow referrnig to added records in the same action bundle. + ['AddRecord', 'Planets', -1, {PlanetName: 'Earth'}], + ['AddRecord', 'Planets', -2, {PlanetName: 'Mars'}], + ['AddTable', 'Moons', [{id: 'MoonName'}, {id: 'Planet', type: 'Ref:Planets'}]], + ['AddRecord', 'Moons', null, {MoonName: 'Phobos', Planet: -2}], + ['AddRecord', 'Moons', null, {MoonName: 'Deimos', Planet: -2}], + ['AddRecord', 'Moons', null, {MoonName: 'Moon', Planet: -1}], + ]); + + // Load the document. + await session.loadDoc(`/doc/${docId}`); + + await gu.getPageItem('Planets').click(); + await gu.waitForDocToLoad(); + await gu.addNewSection(/Table/, /Moons/, {selectBy: /PLANETS/}); + + await checkLinking(); + }); + + // Check that normal linking works. + const checkLinking = stackWrapFunc(async function() { + await gu.getCell({section: 'PLANETS', rowNum: 1, col: 'PlanetName'}).click(); + assert.deepEqual(await gu.getVisibleGridCells({section: 'MOONS', col: 'MoonName', rowNums: [1, 2]}), + ['Moon', '']); + await gu.getCell({section: 'PLANETS', rowNum: 2, col: 'PlanetName'}).click(); + assert.deepEqual(await gu.getVisibleGridCells({section: 'MOONS', col: 'MoonName', rowNums: [1, 2, 3]}), + ['Phobos', 'Deimos', '']); + }); + + it("should unset linking setting when changing the data table for a widget", async function() { + await gu.getCell({section: 'Moons', rowNum: 1, col: 'MoonName'}).click(); + await gu.toggleSidePanel('right', 'open'); + await driver.find('.test-right-tab-pagewidget').click(); + await driver.findContent('.test-right-panel button', /Change Widget/).click(); + + assert.equal(await driver.find('.test-wselect-table-label[class*=-selected]').getText(), 'Moons'); + await driver.findContent('.test-wselect-table', /Planets/).click(); + assert.match(await driver.find('.test-wselect-selectby').value(), /Select Widget/); + + await driver.find('.test-wselect-addBtn').doClick(); + await gu.waitForServer(); + + // Check that the two sections on the page are now for the same table, and link settings are + // cleared. + const tables = await getTableData(api.getDocAPI(docId), '_grist_Tables'); + const sections = await getTableData(api.getDocAPI(docId), '_grist_Views_section'); + const planetsTable = tables.filterRecords({tableId: 'Planets'})[0]; + assert.isOk(planetsTable); + const planetsSections = sections.filterRecords({tableRef: planetsTable.id}); + assert.lengthOf(planetsSections, 3); + assert.equal(planetsSections[0].parentId, planetsSections[2].parentId); + assert.deepEqual(planetsSections.map(s => s.linkTargetColRef), [0, 0, 0]); + assert.deepEqual(planetsSections.map(s => s.linkSrcSectionRef), [0, 0, 0]); + assert.deepEqual(planetsSections.map(s => s.linkSrcColRef), [0, 0, 0]); + + // Switch to another page and back and check that there are no errors. + await gu.getPageItem('Moons').click(); + await gu.checkForErrors(); + await gu.getPageItem('Planets').click(); + await gu.checkForErrors(); + + // Now undo. + await gu.undo(); + + // Still should have no errors, and linking should be restored. + await gu.checkForErrors(); + await checkLinking(); + }); + + it("should fail to link gracefully when linking settings are wrong", async function() { + // Fetch link settings, then mess them up. + const columns = await getTableData(api.getDocAPI(docId), '_grist_Tables_column'); + const sections = await getTableData(api.getDocAPI(docId), '_grist_Views_section'); + const planetRefCol = columns.filterRecords({colId: 'Planet'})[0]; // In table Moons + const planetNameCol = columns.filterRecords({colId: 'PlanetName'})[0]; // In table Planets + assert.isOk(planetRefCol); + assert.isOk(planetNameCol); + const planetSec = sections.filterRecords({linkTargetColRef: planetRefCol.id})[0]; + assert.isOk(planetSec); + + // Set invalid linking. The column we are setting is for the wrong table. It used to happen + // occasionally due to other bugs, here we check that we ignore such invalid settings. + await api.applyUserActions(docId, [ + ['UpdateRecord', '_grist_Views_section', planetSec.id, {linkTargetColRef: planetNameCol.id}] + ]); + + // Reload the page. + await driver.navigate().refresh(); + await gu.waitForDocToLoad(); + + // Expect no errors, and expect to see data, although it's no longer linked. + await gu.checkForErrors(); + await gu.getCell({section: 'PLANETS', rowNum: 1, col: 'PlanetName'}).click(); + assert.deepEqual(await gu.getVisibleGridCells({section: 'MOONS', col: 'MoonName', rowNums: [1, 2, 3, 4]}), + ['Phobos', 'Deimos', 'Moon', '']); + + // Reverting to correct settings should make the data linked again. + await api.applyUserActions(docId, [ + ['UpdateRecord', '_grist_Views_section', planetSec?.id, {linkTargetColRef: planetRefCol.id}] + ]); + await gu.checkForErrors(); + await checkLinking(); + }); + + it("should recreate page with undo", async function() { + const tempDoc = await session.tempNewDoc(cleanup, 'LinkingErrors2', {load: false}); + // This tests the bug: When removing page with linked sections and then undoing, there are two JS errors raised: + // - flexSize is not a function + // - getter is not a function + + // To recreate create a simple document: + // - Table1 with default columns + // - Table2 with A being reference to Table1 + // - For Table1 page add: + // -- Single card view selected by Table1 + // -- Table2 view selected by Table1 + // And make some updates that will cause a bug (without it undoing works). + // Modify the layout for page Table1, this makes the first JS bug (flexSize ...) when undoing. + // And add some records, which makes the second JS bug (getter is not a function). + const actions = [ + ['CreateViewSection', 1, 1, 'single', null, null], + ['AddEmptyTable', null], + ['UpdateRecord', '_grist_Tables_column', 6, {type: 'Ref:Table1'}], + ['CreateViewSection', 2, 1, 'record', null, null], + ['UpdateRecord', '_grist_Views_section', 3, {linkSrcSectionRef: 1, linkSrcColRef: 0, linkTargetColRef: 0}], + ['UpdateRecord', '_grist_Views_section', 6, {linkSrcSectionRef: 1, linkSrcColRef: 0, linkTargetColRef: 6}], + [ + 'UpdateRecord', + '_grist_Views', + 1, + {layoutSpec: '{"children":[{"children":[{"leaf":1},{"children":[{"leaf":2},{"leaf":4}]}]}]}'}, + ], + ['AddRecord', 'Table1', null, {A: '1'}], + ['AddRecord', 'Table2', null, {A: 1, B: '2'}], + ]; + await api.applyUserActions(tempDoc, actions); + // Load the document. + await session.loadDoc(`/doc/${tempDoc}`); + const revert = await gu.begin(); + // Remove the first page (and Table1). + await gu.removePage('Table1', {withData: true}); + await gu.waitForServer(); + // And do undo + await revert(); + await gu.checkForErrors(); + }); +}); + +async function getTableData(docApi: DocAPI, tableId: string): Promise { + const colValues = await docApi.getRows(tableId); + const tableAction = toTableDataAction(tableId, colValues); + return new TableData(tableId, tableAction, (schema as any)[tableId]); +} diff --git a/test/nbrowser/RawData.ts b/test/nbrowser/RawData.ts new file mode 100644 index 00000000..c1749f6c --- /dev/null +++ b/test/nbrowser/RawData.ts @@ -0,0 +1,701 @@ +import {UserAPI} from 'app/common/UserAPI'; +import {assert, driver, Key} from 'mocha-webdriver'; +import * as gu from 'test/nbrowser/gristUtils'; +import {server, setupTestSuite} from 'test/nbrowser/testUtils'; + +describe('RawData', function () { + this.timeout(30000); + let api: UserAPI; + let doc: string; + // We will stress undo here and will try to undo all tests that were using RAW DATA views. + // At the time of writing this test, undo was basically not possible and was throwing all sort + // of exceptions (related to summary tables). + let revertAll: () => Promise; + setupTestSuite(); + gu.bigScreen(); + afterEach(() => gu.checkForErrors()); + before(async function () { + await server.simulateLogin('Chimpy', 'chimpy@getgrist.com', 'nasa'); + const docInfo = await gu.importFixturesDoc('chimpy', 'nasa', 'Horizon', 'World.grist'); + doc = docInfo.id; + api = gu.createHomeApi('Chimpy', 'nasa'); + await openRawData(); + revertAll = await gu.begin(); + }); + + it('shows all tables', async function () { + const uiTables = await getRawTableIds(); + const data = await api.getTable(doc, '_grist_Tables'); + const tables: string[] = data.tableId as string[]; + tables.sort(); + uiTables.sort(); + assert.deepEqual(uiTables, tables); + }); + + it('shows row counts of all tables', async function () { + assert.deepEqual(await getRawTableRows(), [ + '4,079', + '239', + '984', + '4', + ]); + }); + + it('shows new table name', async function () { + await gu.renameTable('City', 'Town'); + const uiTables = await getRawTableIds(); + const data = await api.getTable(doc, '_grist_Tables'); + const tables: string[] = data.tableId as string[]; + tables.sort(); + uiTables.sort(); + assert.deepEqual(uiTables, tables); + }); + + it('shows table preview', async function () { + // Open modal with grid + await driver.findContent('.test-raw-data-table-title', 'Country').click(); + await gu.waitForServer(); + // Test that overlay is showed. + assert.isTrue(await driver.findWait('.test-raw-data-overlay', 100).isDisplayed()); + // Test proper table is selected. + assert.equal(await gu.getSectionTitle(), 'Country'); + // Test we have some data. + assert.deepEqual(await gu.getVisibleGridCells('Code', [1, 2], 'Country'), ['ABW', 'AFG']); + // Test we can close by button. + await gu.closeRawTable(); + assert.isFalse(await driver.find('.test-raw-data-overlay').isPresent()); + + // Test we can close by pressing escape. + await driver.findContent('.test-raw-data-table-title', 'Country').click(); + assert.isTrue(await driver.find('.test-raw-data-overlay').isDisplayed()); + await driver.sendKeys(Key.ESCAPE); + assert.isFalse(await driver.find('.test-raw-data-overlay').isPresent()); + + // Test we can't close by pressing escape when there is a selection, + await driver.findContent('.test-raw-data-table-title', 'Country').click(); + assert.isTrue(await driver.find('.test-raw-data-overlay').isDisplayed()); + await driver.find('.gridview_data_corner_overlay').doClick(); + await driver.sendKeys(Key.ESCAPE); + assert.isTrue(await driver.find('.test-raw-data-overlay').isDisplayed()); + // Press ESCAPE one more time to close. + await driver.sendKeys(Key.ESCAPE); + assert.isFalse(await driver.find('.test-raw-data-overlay').isPresent()); + + // Test we can close by clicking on overlay. + await driver.findContent('.test-raw-data-table-title', 'Country').click(); + assert.isTrue(await driver.find('.test-raw-data-overlay').isDisplayed()); + await driver.find('.test-raw-data-close-button').mouseMove(); + await driver.mouseMoveBy({y: 100}); // move 100px below (not negative value) + await driver.withActions(a => a.click()); + assert.isFalse(await driver.find('.test-raw-data-overlay').isPresent()); + }); + + it('should rename table from modal window', async function () { + // Open Country table. + await driver.findContent('.test-raw-data-table-title', 'Country').click(); + await gu.waitForServer(); + // Rename section to Empire + await gu.renameActiveTable('Empire'); + // Close and test that it was renamed + await gu.closeRawTable(); + const tables = await getRawTableIds(); + const titles = await driver.findAll('.test-raw-data-table-title', e => e.getText()); + tables.sort(); + titles.sort(); + // Title should also be renamed for now. In follow-up diff those + // two will be separate. + assert.deepEqual(titles, ['Town', 'Empire', 'CountryLanguage', 'Table1'].sort()); + assert.deepEqual(tables, ['Town', 'Empire', 'CountryLanguage', 'Table1'].sort()); + }); + + it('should remove table', async function () { + // Open menu for Town + await openMenu('Town'); + // Click delete. + await clickRemove(); + // Confirm. + await clickConfirm(); + await gu.waitForServer(); + const tables = await getRawTableIds(); + const titles = await driver.findAll('.test-raw-data-table-title', e => e.getText()); + tables.sort(); + titles.sort(); + // Title should also be renamed for now. In a follow-up diff those + // two will be separate. + assert.deepEqual(titles, ['Empire', 'CountryLanguage', 'Table1'].sort()); + assert.deepEqual(tables, ['Empire', 'CountryLanguage', 'Table1'].sort()); + }); + + it('should duplicate table', async function () { + await openMenu('Empire'); + await clickDuplicateTable(); + await driver.find('.test-duplicate-table-name').click(); + await gu.sendKeys('Empire Copy'); + + // Before clicking the Copy All Data checkbox, check that no warning about ACLs is shown. + assert.isFalse(await driver.find('.test-duplicate-table-acl-warning').isPresent()); + + // Now click the Copy All Data checkbox, and check that the warning is shown. + await driver.find('.test-duplicate-table-copy-all-data').click(); + assert.isTrue(await driver.find('.test-duplicate-table-acl-warning').isPresent()); + + await clickConfirm(); + await gu.waitForServer(); + assert.isTrue(await driver.findWait('.test-raw-data-overlay', 100).isDisplayed()); + assert.equal(await gu.getSectionTitle(), 'Empire Copy'); + assert.deepEqual(await gu.getVisibleGridCells('Code', [1, 2], 'Empire Copy'), ['ABW', 'AFG']); + + await driver.sendKeys(Key.ESCAPE); + const tables = await getRawTableIds(); + const titles = await driver.findAll('.test-raw-data-table-title', e => e.getText()); + tables.sort(); + titles.sort(); + assert.deepEqual(titles, ['Empire', 'Empire Copy', 'CountryLanguage', 'Table1'].sort()); + assert.deepEqual(tables, ['Empire', 'Empire_Copy', 'CountryLanguage', 'Table1'].sort()); + }); + + it('should restore position when browser is refreshed', async function () { + await driver.findContent('.test-raw-data-table-title', 'Empire').click(); + await gu.waitForServer(); + await gu.getCell(3, 2).click(); + await driver.navigate().refresh(); + await gu.waitForDocToLoad(); + assert.isTrue(await driver.findWait('.test-raw-data-overlay', 100).isDisplayed()); + assert.deepEqual(await gu.getCursorPosition(), {col: 3, rowNum: 2}); + // Close overlay. + await driver.sendKeys(Key.ESCAPE); + }); + + it('should restore last edit position when browser is refreshed', async function () { + await driver.findContent('.test-raw-data-table-title', 'Empire').click(); + await gu.waitForServer(); + await gu.getCell(2, 9).click(); + await driver.sendKeys('123456789'); + await gu.refreshDismiss(); + await gu.waitForDocToLoad(); + assert.isTrue(await driver.findWait('.test-raw-data-overlay', 100).isDisplayed()); + await gu.checkTextEditor(gu.exactMatch('123456789')); + // Close editor. + await driver.sendKeys(Key.ESCAPE); + assert.deepEqual(await gu.getCursorPosition(), {col: 2, rowNum: 9}); + // Close overlay. + await driver.sendKeys(Key.ESCAPE); + }); + + it('should copy anchor link and restore', async function () { + await driver.findContent('.test-raw-data-table-title', 'Empire').click(); + await gu.waitForServer(); + await (await gu.openRowMenu(10)).findContent('li', /Copy anchor link/).click(); + await driver.findContentWait('.test-notifier-toast-message', /Link copied to clipboard/, 2000); + await driver.find('.test-notifier-toast-close').click(); + const anchor = (await gu.getTestState()).clipboard!; + await gu.getCell(3, 2).click(); + await gu.onNewTab(async () => { + await driver.get(anchor); + await gu.waitForAnchor(); + assert.isTrue(await driver.findWait('.test-raw-data-overlay', 100).isDisplayed()); + assert.deepEqual(await gu.getCursorPosition(), {col: 0, rowNum: 10}); + }); + // Close overlay. + await driver.sendKeys(Key.ESCAPE); + }); + + it('should copy table name', async function () { + await driver.findContentWait('.test-raw-data-table-id', 'Empire', 1000).click(); + await gu.waitToPass(async () => { + assert.equal((await gu.getTestState()).clipboard, 'Empire'); + }, 500); + // Currently tooltip is not dismissible, so let's refresh the page. + await driver.navigate().refresh(); + await waitForRawData(); + }); + + it('shows summary tables under Raw Data Tables', async function () { + // Add a few summary tables: 1 with no group-by columns, and 2 that + // share the same group-by columns. + for (let i = 0; i <= 2; i++) { + await gu.addNewPage(/Table/, /CountryLanguage/, { + summarize: i === 0 ? [] : ['Country'] + }); + } + + // Check that the added summary tables are listed at the end. + await openRawData(); + assert.deepEqual(await getRawTableTitles(), [ + 'CountryLanguage', + 'Empire', + 'Empire Copy', + 'Table1', + 'CountryLanguage [Totals]', + 'CountryLanguage [by Country]', + ]); + assert.deepEqual(await getRawTableIds(), [ + 'CountryLanguage', + 'Empire', + 'Empire_Copy', + 'Table1', + 'CountryLanguage_summary', + 'CountryLanguage_summary_Country', + ]); + }); + + it('shows preview of summary table when clicked', async function () { + // Open a summary table. + await driver.findContent('.test-raw-data-table-title', 'CountryLanguage [by Country]').click(); + await gu.waitForServer(); + + // Check that an overlay is shown. + assert.isTrue(await driver.findWait('.test-raw-data-overlay', 100).isDisplayed()); + + // Check that the right section title is shown. + assert.equal(await gu.getSectionTitle(), 'COUNTRYLANGUAGE [by Country]'); + + // Make sure the data looks correct. + assert.deepEqual( + await gu.getVisibleGridCells('Country', [1, 2, 3, 4, 5], 'CountryLanguage [by Country]'), + ['ABW', 'AFG', 'AGO', 'AIA', 'ALB'], + ); + + // Close the overlay. + await gu.closeRawTable(); + assert.isFalse(await driver.find('.test-raw-data-overlay').isPresent()); + }); + + it('removes summary table when all sections referencing it are removed', async function () { + // CountryLanguage [Totals] and CountryLanguage [by Country] respectively. + await gu.removePage('New page'); + await gu.removePage('New page'); + + // Check that the table summarizing by country wasn't removed, since there is still + // one more view for it. + assert.deepEqual(await getRawTableTitles(), [ + 'CountryLanguage', + 'Empire', + 'Empire Copy', + 'Table1', + 'CountryLanguage [by Country]', + ]); + }); + + it('removes summary table when source table is removed', async function () { + await removeRawTable('CountryLanguage'); + assert.deepEqual(await getRawTableTitles(), [ + 'Empire', + 'Empire Copy', + 'Table1', + ]); + await gu.undo(); + assert.deepEqual(await getRawTableTitles(), [ + 'CountryLanguage', + 'Empire', + 'Empire Copy', + 'Table1', + 'CountryLanguage [by Country]', + ]); + }); + + it('removes summary table when "Remove" menu item is clicked', async function () { + const tableIds = await getRawTableIds(); + await removeRawTable(tableIds[tableIds.length - 1]); + + const titles = await getRawTableTitles(); + assert.deepEqual(titles, [ + 'CountryLanguage', + 'Empire', + 'Empire Copy', + 'Table1', + ]); + }); + + it('should stay on a page when undoing summary table', async function () { + // Undoing after converting a table to a summary table doesn't know + // where to navigate, as section is removed and recreated during navigation + // and it is not connected to any view for a brief moment - which makes that + // section look like a Raw Data View (section without a view). + + // This tests that the section is properly identified and Grist will not navigate + // to the Raw Data view. + await gu.addNewTable(); + const url = await driver.getCurrentUrl(); + assert.isTrue(url.endsWith('p/8')); + await convertToSummary(); + assert.equal(url, await driver.getCurrentUrl()); + await gu.undo(); + assert.equal(url, await driver.getCurrentUrl()); + await gu.redo(); + // Reverting actually went to a bare document url (without a page id) + // This was old buggy behavior that is now fixed. + + assert.equal(url, await driver.getCurrentUrl()); + assert.deepEqual(await gu.getCursorPosition(), {rowNum: 1, col: 0}); + + // Switching pages was producing error after undoing summary table. + await gu.openPage('Empire'); + await gu.checkForErrors(); + await gu.openPage('Table2'); + await gu.checkForErrors(); + }); + + it('should remove all tables except one (including referenced summary table)', async function () { + // First we will add a new summary table for CountryLanguage table. + // This table has a reference to the Country table, and Grist had a bug that + // didn't allow to delete those tables - so here we will test if this is fixed. + await gu.addNewPage('Table', 'CountryLanguage', { + summarize: ['Country'], + }); + + await openRawData(); + + const allTables = await getRawTableIds(); + + // Now we are ready to test deletion. + const beforeDeleteCheckpoint = await gu.begin(); + + // First remove that table without a raw section, to see if that works. + await removeRawTable('Table1'); + await gu.checkForErrors(); + assert.isFalse((await getRawTableIds()).includes('Table1')); + + // Now try to remove Country (now Empire) table - here we had a bug + await removeRawTable('Empire'); + await gu.checkForErrors(); + assert.isFalse((await getRawTableIds()).includes('Empire')); + + // Now revert and remove all until remove is disabled + await beforeDeleteCheckpoint(); + await openRawData(); + + while (allTables.length > 1) { + await removeRawTable(allTables.pop()!); + } + + // We should have only one table + assert.deepEqual(await getRawTableIds(), allTables); + + // The last table should have disabled remove button. + await openMenu(allTables[0]); + assert.isTrue(await driver.find('.test-raw-data-menu-remove.disabled').isDisplayed()); + await gu.sendKeys(Key.ESCAPE); + }); + + it('should allow removing GristHidden* pages', async () => { + // Add a table named GristHidden_test, to test when such tables are left over after an incomplete import. + + // Prepare two tables to test + await gu.addNewTable(); + // Remove last old table we have + await openRawData(); + await removeRawTable('CountryLanguage'); + + await gu.addNewTable(); + assert.deepEqual(await gu.getPageNames(), ['Table1', 'Table2']); + + // Rename Table2 page to GristHidden_test, it should be still visible, as the table + // id is diffrent (not hidden). + await gu.renamePage('Table1', 'GristHidden_test'); + assert.deepEqual(await gu.getPageNames(), ['GristHidden_test', 'Table2']); + // Make sure all pages can be removed + for (const page of await gu.getPageNames()) { + assert.isTrue(await gu.canRemovePage(page)); + } + await gu.removePage('Table2'); + assert.deepEqual(await gu.getPageNames(), ['GristHidden_test']); + assert.isFalse(await gu.canRemovePage('GristHidden_test')); + await gu.undo(); + + await gu.removePage('GristHidden_test'); + assert.deepEqual(await gu.getPageNames(), ['Table2']); + assert.isFalse(await gu.canRemovePage('Table2')); + await gu.undo(); + assert.deepEqual(await gu.getPageNames(), ['GristHidden_test', 'Table2']); + }); + + it('should allow removing hidden tables', async () => { + // Rename Table1 table to a simulate hidden table + await openRawData(); + await gu.renameRawTable("Table2", "GristHidden_import"); + assert.deepEqual(await getRawTableIds(), ['GristHidden_import', 'Table1']); + // Page should be hidden now + assert.deepEqual(await gu.getPageNames(), ['GristHidden_test']); + assert.isFalse(await gu.canRemovePage('GristHidden_test')); + // We should be able to remove hidden table, but not user table (as this can be last table that will + // be auto-removed). + assert.isTrue(await isRemovable('GristHidden_import')); + assert.isFalse(await isRemovable('Table1')); + + // Rename back + await gu.renameRawTable("GristHidden_import", "Table2"); + // Page should be visible again + assert.deepEqual(await gu.getPageNames(), ['GristHidden_test', 'Table2']); + for (const page of await gu.getPageNames()) { + assert.isTrue(await gu.canRemovePage(page)); + } + assert.isTrue(await isRemovable('Table2')); + assert.isTrue(await isRemovable('Table1')); + + // Rename once again and test if it can be actually removed. + await gu.renameRawTable("Table2", "GristHidden_import"); + assert.isTrue(await isRemovable('GristHidden_import')); + await removeRawTable("GristHidden_import"); + await gu.checkForErrors(); + assert.deepEqual(await getRawTableIds(), ['Table1']); + assert.isFalse(await isRemovable('Table1')); + }); + + it('should revert all without errors', async function () { + // Revert internally checks errors. + await revertAll(); + }); + + it('should open raw data as a popup', async () => { + // We are at City table, in first row/first cell. + // Send some keys, to make sure we have focus on active section. + // RawData popup is manipulating what section has focus, so we need to make sure that + // focus is properly restored. + assert.deepEqual(await gu.getCursorPosition(), {rowNum: 1, col: 0}); + await gu.getCell(0, 2).click(); + await gu.sendKeys("abc"); + await gu.checkTextEditor("abc"); + await gu.sendKeys(Key.ESCAPE); + await showRawData(); + assert.equal(await gu.getActiveSectionTitle(), 'City'); + assert.deepEqual(await gu.getCursorPosition(), {rowNum: 20, col: 0}); // raw popup is not sorted + await gu.sendKeys("abc"); + await gu.checkTextEditor("abc"); + await gu.sendKeys(Key.ESCAPE); + // Click on another cell, check page hasn't changed (there was a bug about that) + await gu.getCell({rowNum: 10, col: 1}).click(); + assert.deepEqual(await gu.getCursorPosition(), {rowNum: 10, col: 1}); + assert.equal(await gu.getCurrentPageName(), 'City'); + + // Close by hitting escape. + await gu.sendKeys(Key.ESCAPE); + await assertNoPopup(); + // Make sure we see CITY, and everything is where it should be. + assert.equal(await gu.getActiveSectionTitle(), 'CITY'); + assert.deepEqual(await gu.getCursorPosition(), {rowNum: 2, col: 0}); + await gu.sendKeys("abc"); + await gu.checkTextEditor("abc"); + await gu.sendKeys(Key.ESCAPE); + + // Now open popup again, but close it by clicking on the close button. + await showRawData(); + await gu.closeRawTable(); + await assertNoPopup(); + assert.equal(await gu.getActiveSectionTitle(), 'CITY'); + assert.deepEqual(await gu.getCursorPosition(), {rowNum: 2, col: 0}); + await gu.sendKeys("abc"); + await gu.checkTextEditor("abc"); + await gu.sendKeys(Key.ESCAPE); + + // Now do the same, but close by clicking on a diffrent page + await showRawData(); + await gu.getPageItem('Country').click(); + await assertNoPopup(); + assert.equal(await gu.getActiveSectionTitle(), 'COUNTRY'); + assert.deepEqual(await gu.getCursorPosition(), {rowNum: 1, col: 0}); + await gu.sendKeys("abc"); + await gu.checkTextEditor("abc"); + await gu.sendKeys(Key.ESCAPE); + + // Now make sure that raw data is available for card view. + await gu.selectSectionByTitle("COUNTRY Card List"); + assert.equal(await gu.getActiveSectionTitle(), 'COUNTRY Card List'); + await showRawData(); + assert.equal(await gu.getActiveSectionTitle(), 'Country'); + assert.deepEqual(await gu.getCursorPosition(), {rowNum: 1, col: 1}); + await gu.sendKeys("abc"); + await gu.checkTextEditor("abc"); + await gu.sendKeys(Key.ESCAPE); + // Make sure we see a grid + assert.isTrue(await driver.find(".grid_view_data").isDisplayed()); + await gu.sendKeys(Key.ESCAPE); + }); + + // This is not documented feature at this moment, and tailored for raw data + // view, but it should work for any kind of section. + it('should open detail section as a popup', async () => { + // We are at the Country page + await gu.getDetailCell('Code', 1).click(); + let anchorLink = replaceAnchor(await gu.getAnchor(), { a: '2' }); + const testResult = async () => { + await waitForAnchorPopup(anchorLink); + assert.equal(await gu.getActiveSectionTitle(), 'COUNTRY Card List'); + assert.deepEqual(await gu.getCursorPosition(), {rowNum: 1, col: 'Code'}); + await gu.sendKeys("abc"); + await gu.checkTextEditor("abc"); + await gu.sendKeys(Key.ESCAPE); + // Close by hitting escape. + await gu.sendKeys(Key.ESCAPE); + // Make sure we are on correct page + assert.equal(await gu.getCurrentPageName(), "City"); + }; + // Switch page and use only hash, otherwise it will just maximize the section on the current page. + await gu.getPageItem('City').click(); + anchorLink = (await driver.getCurrentUrl()) + '#' + anchorLink.split('#')[1]; + await testResult(); + }); + + it('should open chart section as a popup', gu.revertChanges(async () => { + // We are at the Country page + await gu.getPageItem('Country').click(); + await gu.selectSectionByTitle("COUNTRY Card List"); + await gu.getDetailCell('Code', 1).click(); + await gu.addNewSection(/Chart/, /CountryLanguage/); + // s19 is the new section id, we also strip row/column. + let chartLink = replaceAnchor(await gu.getAnchor(), {s: '19', a: '2'}); + await gu.getPageItem('City').click(); + chartLink = (await driver.getCurrentUrl()) + '#' + chartLink.split('#')[1]; + await waitForAnchorPopup(chartLink); + assert.isTrue(await driver.find(".test-raw-data-overlay .test-chart-container").isDisplayed()); + await gu.sendKeys(Key.ESCAPE); + })); + + it('should handle edge cases when table/section is removed', async () => { + await gu.getPageItem('Country').click(); + await gu.selectSectionByTitle("COUNTRY Card List"); + await gu.getDetailCell('Code', 1).click(); + let anchorLink = replaceAnchor(await gu.getAnchor(), { a: '2' }); + await gu.getPageItem('City').click(); + anchorLink = (await driver.getCurrentUrl()) + '#' + anchorLink.split('#')[1]; + await waitForAnchorPopup(anchorLink); + + assert.equal(await gu.getActiveSectionTitle(), 'COUNTRY Card List'); + // Now remove the section using api, popup should be closed. + const sectionId = parseInt(getAnchorParams(anchorLink).s); + await api.applyUserActions(doc, [[ + 'RemoveRecord', '_grist_Views_section', sectionId + ]]); + await gu.waitForServer(); + await gu.checkForErrors(); + await assertNoPopup(); + // Now open plain raw data for City table. + await gu.selectSectionByTitle("CITY"); + assert.equal(await gu.getActiveSectionTitle(), 'CITY'); // CITY is viewSection title + await showRawData(); + assert.equal(await gu.getActiveSectionTitle(), 'City'); // City is now a table title + // Now remove the table. + await api.applyUserActions(doc, [[ + 'RemoveTable', 'City' + ]]); + await gu.waitForServer(); + await gu.checkForErrors(); + await assertNoPopup(); + }); +}); + +const anchorRegex = /#a(\d+)\.s(\d+)\.r(\d+)\.c(\d+)/gm; + +function getAnchorParams(link: string) { + const match = anchorRegex.exec(link); + if (!match) { throw new Error("Invalid link"); } + const [, a, s, r, c] = match; + return { a, s, r, c }; +} + +function replaceAnchor(link: string, values: { + a?: string, + s?: string, + r?: string, + c?: string, +}) { + const { a, s, r, c } = getAnchorParams(link); + return link.replace(anchorRegex, `#a${values.a || a}.s${values.s || s}.r${values.r || r}.c${values.c || c}`); +} + +async function showRawData() { + await gu.openSectionMenu('viewLayout'); + await driver.find('.test-show-raw-data').click(); + await waitForPopup(); +} + +async function openRawData() { + await driver.find('.test-tools-raw').click(); + await waitForRawData(); +} + +async function clickConfirm() { + await driver.find('.test-modal-confirm').click(); +} + +async function clickDuplicateTable() { + await driver.find('.test-raw-data-menu-duplicate-table').click(); +} + +async function clickRemove() { + await driver.find('.test-raw-data-menu-remove').click(); +} + +async function removeRawTable(tableId: string) { + await openMenu(tableId); + await clickRemove(); + await clickConfirm(); + await gu.waitForServer(); +} + +async function convertToSummary(...groupByColumns: string[]) { + // Convert table to a summary table + await gu.toggleSidePanel('right', 'open'); + // Creator Panel > Table + await driver.find('.test-right-tab-pagewidget').click(); + // Tab [Data] + await driver.find('.test-config-data').click(); + // Edit Data Selection + await driver.find('.test-pwc-editDataSelection').click(); + // Σ + await driver.find('.test-wselect-pivot').click(); + // Select Group-By Columns + for (const c of groupByColumns) { + await driver.findContent('.test-wselect-column', c).click(); + } + // Save + await driver.find('.test-wselect-addBtn').click(); + await gu.waitForServer(); +} + +async function getRawTableTitles() { + return await driver.findAll('.test-raw-data-table-title', e => e.getText()); +} + +async function getRawTableIds() { + return await driver.findAll('.test-raw-data-table-id', e => e.getText()); +} + +async function getRawTableRows() { + return await driver.findAll('.test-raw-data-table-rows', e => e.getText()); +} + +async function openMenu(tableId: string) { + const allTables = await getRawTableIds(); + const tableIndex = allTables.indexOf(tableId); + assert.isTrue(tableIndex >= 0, `No raw table with id ${tableId}`); + const menus = await driver.findAll('.test-raw-data-table .test-raw-data-table-menu'); + assert.equal(menus.length, allTables.length); + await menus[tableIndex].click(); +} + +async function waitForRawData() { + await driver.findWait('.test-raw-data-list', 1000); + await gu.waitForServer(); +} + +async function isRemovable(tableId: string){ + await openMenu(tableId); + const disabledItems = await driver.findAll('.test-raw-data-menu-remove.disabled'); + await gu.sendKeys(Key.ESCAPE); + return disabledItems.length === 0; +} + +async function waitForPopup() { + assert.isTrue(await driver.findWait('.test-raw-data-overlay', 100).isDisplayed()); +} + +async function assertNoPopup() { + assert.isFalse(await driver.find('.test-raw-data-overlay').isPresent()); +} + +async function waitForAnchorPopup(link: string) { + await driver.get(link); + await gu.waitForAnchor(); + await waitForPopup(); +}