From fe220d576fefd1b3bbb867b35b33c734091c9b62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaros=C5=82aw=20Sadzi=C5=84ski?= Date: Wed, 3 Aug 2022 16:15:45 +0200 Subject: [PATCH] Adding tests for left panel --- test/fixtures/docs/Pages.grist | Bin 0 -> 200704 bytes test/nbrowser/Pages.ts | 475 +++++++++++++++++++++++++++++++++ 2 files changed, 475 insertions(+) create mode 100644 test/fixtures/docs/Pages.grist create mode 100644 test/nbrowser/Pages.ts diff --git a/test/fixtures/docs/Pages.grist b/test/fixtures/docs/Pages.grist new file mode 100644 index 0000000000000000000000000000000000000000..616e2fef7baa074a8c11896852074ed07244d405 GIT binary patch literal 200704 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 literal 0 HcmV?d00001 diff --git a/test/nbrowser/Pages.ts b/test/nbrowser/Pages.ts new file mode 100644 index 00000000..9237fa43 --- /dev/null +++ b/test/nbrowser/Pages.ts @@ -0,0 +1,475 @@ +import {DocCreationInfo} from 'app/common/DocListAPI'; +import {UserAPI} from 'app/common/UserAPI'; +import {assert, driver, Key} from 'mocha-webdriver'; +import {Session} from 'test/nbrowser/gristUtils'; +import * as gu from 'test/nbrowser/gristUtils'; +import {server, setupTestSuite} from 'test/nbrowser/testUtils'; +import values = require('lodash/values'); + +describe('Pages', function() { + this.timeout(20000); + setupTestSuite(); + let doc: DocCreationInfo; + let api: UserAPI; + let session: Session; + const cleanup = setupTestSuite({team: true}); + + before(async () => { + session = await gu.session().teamSite.login(); + doc = await session.tempDoc(cleanup, 'Pages.grist'); + api = session.createHomeApi(); + }); + + it('should list all pages in document', async () => { + + // check content of _girst_Pages and _grist_Views + assert.deepInclude(await api.getTable(doc.id, '_grist_Pages'), { + viewRef: [1, 2, 3, 4, 5], + pagePos: [1, 2, 1.5, 3, 4], + indentation: [0, 0, 1, 1, 1], + }); + assert.deepInclude(await api.getTable(doc.id, '_grist_Views'), { + name: ['Interactions', 'People', 'Documents', 'User & Leads', 'Overview'], + id: [1, 2, 3, 4, 5], + }); + + // load page and check all pages are listed + await driver.get(`${server.getHost()}/o/test-grist/doc/${doc.id}`); + await driver.findWait('.test-treeview-container', 1000); + assert.deepEqual(await gu.getPageNames(), ['Interactions', 'Documents', 'People', 'User & Leads', 'Overview']); + }); + + it('should select correct page if /p/ in the url', async () => { + + // show page with viewRef 2 + await gu.loadDoc(`/o/test-grist/doc/${doc.id}/p/2`); + assert.deepEqual(await gu.getPageNames(), ['Interactions', 'Documents', 'People', 'User & Leads', 'Overview']); + assert.match(await driver.find('.test-treeview-itemHeader.selected').getText(), /People/); + assert.match(await gu.getActiveSectionTitle(), /People/i); + }); + + it('should select first page if /p/ is omitted in the url', async () => { + + await driver.get(`${server.getHost()}/o/test-grist/doc/${doc.id}`); + await driver.findWait('.test-treeview-container', 1000); + assert.match(await driver.find('.test-treeview-itemHeader').getText(), /Interactions/); + assert.match(await driver.find('.test-treeview-itemHeader.selected').getText(), /Interactions/); + assert.match(await gu.getActiveSectionTitle(), /Interactions/i); + + // Check also that this did NOT cause a redirect to include /p/ in the URL. + assert.notMatch(await driver.getCurrentUrl(), /\/p\//); + }); + + it('clicking page should set /p/ in the url', async () => { + await driver.get(`${server.getHost()}/o/test-grist/doc/${doc.id}`); + + // Wait for data to load. + assert.equal(await driver.findWait('.viewsection_title', 3000).isDisplayed(), true); + await gu.waitForServer(); + + // Click on a page; check the URL, selected item, and the title of the view section. + await driver.findContent('.test-treeview-itemHeader', /Documents/).doClick(); + assert.match(await driver.getCurrentUrl(), /\/p\/3/); + assert.match(await driver.find('.test-treeview-itemHeader.selected').getText(), /Documents/); + assert.match(await gu.getActiveSectionTitle(), /Documents/i); + + // Click on another page; check the URL, selected item, and the title of the view section. + await driver.findContent('.test-treeview-itemHeader', /People/).doClick(); + assert.match(await driver.getCurrentUrl(), /\/p\/2/); + assert.match(await driver.find('.test-treeview-itemHeader.selected').getText(), /People/); + assert.match(await gu.getActiveSectionTitle(), /People/i); + + // TODO: Add a check that open-in-new-tab works too. + }); + + it('should allow renaming table', async () => { + + // open dots menu and click rename + await gu.openPageMenu('People'); + await driver.find('.test-docpage-rename').doClick(); + + // do rename + await driver.find('.test-docpage-editor').sendKeys('PeopleRenamed', Key.ENTER); + await gu.waitForServer(); + + assert.deepEqual( + await gu.getPageNames(), + ['Interactions', 'Documents', 'PeopleRenamed', 'User & Leads', 'Overview'] + ); + + // Test that we can delete after remove (there was a bug related to this). + await gu.removePage('PeopleRenamed'); + + assert.deepEqual(await gu.getPageNames(), ['Interactions', 'Documents', 'User & Leads', 'Overview']); + + // revert changes + await gu.undo(2); + assert.deepEqual(await gu.getPageNames(), ['Interactions', 'Documents', 'People', 'User & Leads', 'Overview']); + }); + + it('should not allow blank page name', async () => { + // Begin renaming of People page + await gu.openPageMenu('People'); + await driver.find('.test-docpage-rename').doClick(); + + // Delete page name and check editor's value equals '' + await driver.find('.test-docpage-editor').sendKeys(Key.DELETE); + assert.equal(await driver.find('.test-docpage-editor').value(), ''); + + // Save blank name + await driver.sendKeys(Key.ENTER); + await gu.waitForServer(); + + // Check name is still People + assert.include(await gu.getPageNames(), 'People'); + }); + + it('should not change page when clicking the input while renaming page', async () => { + // check that initially People is selected + assert.match(await driver.find('.test-treeview-itemHeader.selected').getText(), /People/); + + // start renaming Documents and click the input + await gu.openPageMenu('Documents'); + await driver.find('.test-docpage-rename').doClick(); + await driver.find('.test-docpage-editor').click(); + + // check that People is still the selected page. + assert.match(await driver.find('.test-treeview-itemHeader.selected').getText(), /People/); + + // abord renaming + await driver.find('.test-docpage-editor').sendKeys(Key.ESCAPE); + }); + + it('should allow moving pages', async () => { + + // check initial state + assert.deepEqual(await gu.getPageNames(), ['Interactions', 'Documents', 'People', 'User & Leads', 'Overview']); + + // move page + await movePage(/User & Leads/, {after: /Overview/}); + await gu.waitForServer(); + + assert.deepEqual(await gu.getPageNames(), ['Interactions', 'Documents', 'People', 'Overview', 'User & Leads']); + + // revert changes + await gu.undo(); + assert.deepEqual(await gu.getPageNames(), ['Interactions', 'Documents', 'People', 'User & Leads', 'Overview']); + + }); + + it('moving a page should not extend collapsed page', async () => { + /** + * Here what is really being tested is that TreeModelRecord correctly reuses TreeModelItem, + * because if it wasn't the case, TreeViewComponent would not be able to reuse dom and would + * rebuild dom for all pages causing all page to be expanded. + */ + + // let's collapse Interactions + await driver.findContent('.test-treeview-itemHeader', /Interactions/).find('.test-treeview-itemArrow').doClick(); + assert.deepEqual(await gu.getPageNames(), ['Interactions', '', 'People', 'User & Leads', 'Overview']); + + // let's move + await movePage(/User & Leads/, {after: /Overview/}); + await gu.waitForServer(); + + // check that pages has moved and Interactions remained collapsed + assert.deepEqual(await gu.getPageNames(), ['Interactions', '', 'People', 'Overview', 'User & Leads']); + + // revert changes + await gu.undo(); + await driver.findContent('.test-treeview-itemHeader', /Interactions/).find('.test-treeview-itemArrow').doClick(); + assert.deepEqual(await gu.getPageNames(), ['Interactions', 'Documents', 'People', 'User & Leads', 'Overview']); + }); + + it('should allow to cycle though pages using shortcuts', async () => { + + function nextPage() { + return driver.find('body').sendKeys(Key.chord(Key.ALT, Key.DOWN)); + } + + function prevPage() { + return driver.find('body').sendKeys(Key.chord(Key.ALT, Key.UP)); + } + + function selectedPage() { + return driver.find('.test-treeview-itemHeader.selected').getText(); + } + + // goto page 'Interactions' + await driver.findContent('.test-treeview-itemHeader', /Interactions/).doClick(); + + // check selected page + assert.match(await selectedPage(), /Interactions/); + + // prev page + await prevPage(); + + // check selecte page + assert.match(await selectedPage(), /Overview/); + + // prev page + await prevPage(); + + // check selecte page + assert.match(await selectedPage(), /User & Leads/); + + // next page + await nextPage(); + + // check selected page + assert.match(await selectedPage(), /Overview/); + + + // next page + await nextPage(); + + // check selected page + assert.match(await selectedPage(), /Interactions/); + + }); + + it('undo/redo should update url', async () => { + + // goto page 'Interactions' and send keys + await driver.findContent('.test-treeview-itemHeader', /Interactions/).doClick(); + await driver.findContentWait('.gridview_data_row_num', /1/, 2000); + await driver.sendKeys(Key.ENTER, 'Foo', Key.ENTER); + await gu.waitForServer(); + assert.deepEqual(await gu.getVisibleGridCells(0, [1]), ['Foo']); + + // goto page 'People' and click undo + await driver.findContent('.test-treeview-itemHeader', /People/).doClick(); + await gu.waitForDocToLoad(); + await gu.waitForUrl(/\/p\/2\b/); // check that url match p/2 + + await gu.undo(); + await gu.waitForDocToLoad(); + await gu.waitForUrl(/\/p\/1\b/); // check that url match p/1 + + // check that "Interactions" page is selected + assert.match(await driver.find('.test-treeview-itemHeader.selected').getText(), /Interactions/); + + // check that undo worked + assert.deepEqual(await gu.getVisibleGridCells(0, [1]), ['']); + }); + + it('Add new page should update url', async () => { + // goto page 'Interactions' and check that url updated + await driver.findContent('.test-treeview-itemHeader', /Interactions/).doClick(); + await gu.waitForUrl(/\/p\/1\b/); + + // Add new Page, check that url updated and page is selected + await gu.addNewPage(/Table/, /New Table/); + await gu.waitForUrl(/\/p\/6\b/); + assert.match(await driver.find('.test-treeview-itemHeader.selected').getText(), /Table1/); + + // goto page 'Interactions' and check that url updated and page selectd + await driver.findContent('.test-treeview-itemHeader', /Interactions/).doClick(); + await gu.waitForUrl(/\/p\/1\b/); + assert.match(await driver.find('.test-treeview-itemHeader.selected').getText(), /Interactions/); + }); + + it('Removing a page should work', async () => { + + // Create and open new document + const docId = await session.tempNewDoc(cleanup, "test-page-removal"); + await driver.get(`${server.getHost()}/o/test-grist/doc/${docId}`); + await gu.waitForUrl('test-page-removal'); + + // Add a new page using Table1 + await gu.addNewPage(/Table/, /Table1/); + assert.deepInclude(await api.getTable(docId, '_grist_Tables'), { + tableId: ['Table1'], + primaryViewId: [1], + }); + assert.deepInclude(await api.getTable(docId, '_grist_Views'), { + name: ['Table1', 'New page'], + id: [1, 2], + }); + assert.deepEqual(await gu.getPageNames(), ['Table1', 'New page']); + + // check that the new page is now selected + await gu.waitForUrl(/\/p\/2\b/); + assert.match(await driver.find('.test-treeview-itemHeader.selected').getText(), /New page/); + + // remove new page + await gu.removePage(/New page/); + + // check that url has no p/<...> and 'Table1' is now selected + await driver.wait(async () => !/\/p\//.test(await driver.getCurrentUrl()), 2000); + assert.match(await driver.find('.test-treeview-itemHeader.selected').getText(), /Table1/); + + // check that corresponding view is removed + assert.deepInclude(await api.getTable(docId, '_grist_Tables'), { + tableId: ['Table1'], + primaryViewId: [1], + }); + assert.deepInclude(await api.getTable(docId, '_grist_Views'), { + name: ['Table1'], + id: [1], + }); + assert.deepEqual(await gu.getPageNames(), ['Table1']); + + // create table Foo and 1 new page using Foo + await api.applyUserActions(docId, [['AddTable', 'Foo', [{id: null, isFormula: true}]]]); + await driver.findContentWait('.test-treeview-itemHeader', /Foo/, 2000); + await gu.addNewPage(/Table/, /Foo/); + assert.deepInclude(await api.getTable(docId, '_grist_Tables'), { + tableId: ['Table1', 'Foo'], + primaryViewId: [1, 2], + }); + assert.deepInclude(await api.getTable(docId, '_grist_Views'), { + name: ['Table1', 'Foo', 'New page'], + id: [1, 2, 3], + }); + assert.deepEqual(await gu.getPageNames(), ['Table1', 'Foo', 'New page']); + + // check that last page is now selected + await gu.waitForUrl(/\/p\/3\b/); + assert.match(await driver.find('.test-treeview-itemHeader.selected').getText(), /New page/); + + // remove table and make sure pages are also removed. + await gu.removeTable('Foo'); + + // check that Foo and page are removed + assert.deepInclude(await api.getTable(docId, '_grist_Tables'), { + tableId: ['Table1'], + primaryViewId: [1], + }); + assert.deepInclude(await api.getTable(docId, '_grist_Views'), { + name: ['Table1'], + id: [1], + }); + assert.deepEqual(await gu.getPageNames(), ['Table1']); + }); + + it('Remove should be disabled for last page', async () => { + // check that Remove is disabled on Table1 + assert.isFalse(await gu.canRemovePage('Table1')); + + // Adds a new page using Table1 + await gu.addNewPage(/Table/, /Table1/); + assert.deepEqual(await gu.getPageNames(), ['Table1', 'New page']); + + // Add a new table too. + await gu.addNewTable(); + assert.deepEqual(await gu.getPageNames(), ['Table1', 'New page', 'Table2']); + + // The "Remove" options should now be available on all three items. + assert.isTrue(await gu.canRemovePage('Table1')); + assert.isTrue(await gu.canRemovePage('New page')); + assert.isTrue(await gu.canRemovePage('Table2')); + + // Add Table2 to "New page" (so that it can remain after Table1 is removed below). + await gu.getPageItem('New page').click(); + await gu.addNewSection(/Table/, /Table2/); + + // Now remove Table1. + await gu.removeTable('Table1'); + assert.deepEqual(await gu.getPageNames(), ['New page', 'Table2']); + + // Both pages should be removable still. + assert.isTrue(await gu.canRemovePage('New page')); + assert.isTrue(await gu.canRemovePage('Table2')); + + // Remove New Page + await gu.removePage('New page'); + + // Now Table2 should not be removable (since it is the last page). + assert.isFalse(await gu.canRemovePage('Table2')); + }); + + it('should not throw JS errors when removing the current page without a slug', async () => { + // Create and open new document + const docId = await session.tempNewDoc(cleanup, "test-page-removal-js-error") + await driver.get(`${server.getHost()}/o/test-grist/doc/${docId}`); + await gu.waitForUrl('test-page-removal-js-error'); + + // Add two additional tables + await gu.addNewTable(); + await gu.addNewTable(); + assert.deepEqual(await gu.getPageNames(), ['Table1', 'Table2', 'Table3']); + + // Open the default page (no p/<...> in the URL) + await driver.get(`${server.getHost()}/o/test-grist/doc/${docId}`); + + // Check that Table1 is now selected + await driver.findContentWait('.test-treeview-itemHeader.selected', /Table1/, 2000); + assert.match(await driver.find('.test-treeview-itemHeader.selected').getText(), /Table1/); + + // Remove page Table1 + await gu.removePage('Table1'); + assert.deepEqual(await gu.getPageNames(), ['Table2', 'Table3']); + + // Now check that Table2 is selected + assert.match(await driver.find('.test-treeview-itemHeader.selected').getText(), /Table2/); + + // Remove page Table2 + await gu.removePage('Table2'); + assert.deepEqual(await gu.getPageNames(), ['Table3']); + + // Check that Table3 is the only page remaining + assert.deepInclude(await api.getTable(docId, '_grist_Views'), { + name: ['Table3'], + id: [3], + }); + + // Check that no JS errors were thrown + await gu.checkForErrors(); + }); + + it('should offer a way to delete last tables', async () => { + // Create and open new document + const docId = await session.tempNewDoc(cleanup, "prompts") + await driver.get(`${server.getHost()}/o/test-grist/doc/${docId}`); + await gu.waitForUrl('prompts'); + + // Add two additional tables, with custom names. + await gu.addNewTable('Table B'); + await gu.addNewTable('Table C'); + await gu.addNewTable('Table Last'); + assert.deepEqual(await gu.getPageNames(), ['Table1', 'Table B', 'Table C', 'Table Last']); + await gu.getPageItem('Table C').click(); + + // In Table C add Table D (a new one) and Table1 widget (existing); + await gu.addNewSection(/Table/, /New Table/, { tableName: "Table D"}); + await gu.addNewSection(/Table/, "Table1"); + // New table should not be added as a page + assert.deepEqual(await gu.getPageNames(), ['Table1', 'Table B', 'Table C', 'Table Last']); + // Make sure we see proper sections. + assert.deepEqual(await gu.getSectionTitles(), ['TABLE C', 'TABLE D', 'TABLE1']); + + const revert = await gu.begin(); + // Now removing Table1 page should be done without a prompt (since it is also on Table C) + await gu.removePage("Table1", { expectPrompt : false }); + assert.deepEqual(await gu.getPageNames(), ['Table B', 'Table C', 'Table Last']); + + // Removing Table B should show prompt (since it is last page) + await gu.removePage("Table B", { expectPrompt : true, tables: ['Table B'] }); + assert.deepEqual(await gu.getPageNames(), ['Table C', 'Table Last']); + + // Removing page Table C should also show prompt (it is last page for Table1,Table D and TableC) + await gu.getPageItem('Table C').click(); + assert.deepEqual(await gu.getSectionTitles(), ['TABLE C', 'TABLE D', 'TABLE1' ]); + await gu.removePage("Table C", { expectPrompt : true, tables: ['Table D', 'Table C', 'Table1'] }); + assert.deepEqual(await gu.getPageNames(), ['Table Last']); + await revert(); + + assert.deepEqual(await gu.getPageNames(), ['Table1', 'Table B', 'Table C', 'Table Last']); + assert.deepEqual(await gu.getSectionTitles(), ['TABLE C', 'TABLE D', 'TABLE1' ]); + }); + +}); + +async function movePage(page: RegExp, target: {before: RegExp}|{after: RegExp}) { + const targetReg = values(target)[0]; + await driver.withActions(actions => actions + .move({origin: driver.findContent('.test-treeview-itemHeader', page)}) + .move({origin: driver.findContent('.test-treeview-itemHeaderWrapper', page) + .find('.test-treeview-handle')}) + .press() + .move({origin: driver.findContent('.test-treeview-itemHeader', targetReg), + y: 'after' in target ? 1 : -1 + }) + .release()); +}