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()); }