/** * Test the HomeIntro screen for empty orgs and the special rendering of Examples & Templates * page, both for anonymous and logged-in users. */ import {assert, driver, stackWrapFunc, WebElement} from 'mocha-webdriver'; import * as gu from 'test/nbrowser/gristUtils'; import {server, setupTestSuite} from 'test/nbrowser/testUtils'; describe('HomeIntro', function() { this.timeout(40000); setupTestSuite({samples: true}); gu.withEnvironmentSnapshot({'GRIST_TEMPLATE_ORG': 'templates'}); describe("Anonymous on merged-org", function() { it('should show welcome for anonymous user', async function() { // Sign out const session = await gu.session().personalSite.anon.login(); // Open doc-menu await session.loadDocMenu('/'); // Check message specific to anon assert.equal(await driver.find('.test-welcome-title').getText(), 'Welcome to Grist!'); assert.match(await driver.find('.test-welcome-text').getText(), /Sign up.*Visit our Help Center/); // Check the sign-up link. const signUp = await driver.findContent('.test-welcome-text a', 'Sign up'); assert.include(await signUp.getAttribute('href'), '/signin'); // Check that the link takes us to a Grist login page. await signUp.click(); await gu.checkLoginPage(); await driver.navigate().back(); await gu.waitForDocMenuToLoad(); }); it('should should intro screen for anon', () => testIntroScreen({team: false})); it('should not show Other Sites section', testOtherSitesSection); it('should allow create/import from intro screen', testCreateImport.bind(null, false)); it('should allow collapsing examples and remember the state', testExamplesCollapsing); it('should show examples workspace with the intro', testExamplesSection); it('should render selected Examples workspace specially', testSelectedExamplesPage); }); describe("Logged-in on merged-org", function() { it('should show welcome for logged-in user', async function() { // Sign in as a new user who has no docs. const session = gu.session().personalSite.user('user3'); await session.login({ isFirstLogin: false, freshAccount: true, }); // Open doc-menu and dismiss the welcome questions popup await session.loadDocMenu('/', 'skipWelcomeQuestions'); // Reload the doc-menu and dismiss the coaching call popup await session.loadDocMenu('/'); await gu.dismissCardPopups(); // Check message specific to logged-in user assert.match(await driver.find('.test-welcome-title').getText(), new RegExp(`Welcome.* ${session.name}`)); assert.match(await driver.find('.test-welcome-text').getText(), /Visit our Help Center/); assert.notMatch(await driver.find('.test-welcome-text').getText(), /sign up/i); }); it('should not show Other Sites section', testOtherSitesSection); it('should show intro screen for empty org', () => testIntroScreen({team: false})); it('should allow create/import from intro screen', testCreateImport.bind(null, true)); it('should allow collapsing examples and remember the state', testExamplesCollapsing); it('should show examples workspace with the intro', testExamplesSection); it('should allow copying examples', testCopyingExamples.bind(null, undefined)); it('should render selected Examples workspace specially', testSelectedExamplesPage); it('should show empty workspace info', testEmptyWorkspace.bind(null, {buttons: true})); }); describe("Logged-in on team site", function() { it('should show welcome for logged-in user', async function() { // Sign in as to a team that has no docs. const session = await gu.session().teamSite.user('user1').login(); await session.loadDocMenu('/'); await session.resetSite(); // Open doc-menu await session.loadDocMenu('/', 'skipWelcomeQuestions'); // Check message specific to logged-in user and an empty team site. assert.match(await driver.find('.test-welcome-title').getText(), new RegExp(`Welcome.* ${session.orgName}`)); assert.match(await driver.find('.test-welcome-text').getText(), /Learn more.*find an expert/); assert.notMatch(await driver.find('.test-welcome-text').getText(), /sign up/); }); it('should not show Other Sites section', testOtherSitesSection); it('should show intro screen for empty org', () => testIntroScreen({team: true})); it('should allow create/import from intro screen', testCreateImport.bind(null, true)); it('should show examples workspace with the intro', testExamplesSection); it('should allow copying examples', testCopyingExamples.bind(null, gu.session().teamSite.orgName)); it('should render selected Examples workspace specially', testSelectedExamplesPage); it('should show empty workspace info', testEmptyWorkspace); }); async function testOtherSitesSection() { // Check that the Other Sites section is not shown. assert.isFalse(await driver.find('.test-dm-other-sites-header').isPresent()); } async function testIntroScreen(options: {team: boolean}) { // TODO There is no longer a thumbnail + video link on an empty site, but it's a good place to // check for the presence and functionality of the planned links that open an intro video. // Check link to Help Center assert.include(await driver.findContent('.test-welcome-text a', /Help Center/).getAttribute('href'), 'support.getgrist.com'); if (options.team) { assert.equal(await driver.find('.test-intro-invite').getText(), 'Invite Team Members'); assert.equal(await driver.find('.test-topbar-manage-team').getText(), 'Manage Team'); } else { assert.equal(await driver.find('.test-intro-invite').isPresent(), false); assert.equal(await driver.find('.test-topbar-manage-team').isPresent(), false); assert.equal(await driver.find('.test-intro-templates').getText(), 'Browse Templates'); assert.include(await driver.find('.test-intro-templates').getAttribute('href'), '/p/templates'); } } async function testCreateImport(isLoggedIn: boolean) { await checkIntroButtons(isLoggedIn); // Check that add-new menu has enabled Create Empty and Import Doc items. await driver.find('.test-dm-add-new').doClick(); assert.equal(await driver.find('.test-dm-new-doc').matches('[class*=-disabled]'), false); assert.equal(await driver.find('.test-dm-import').matches('[class*=-disabled]'), false); // Create doc from add-new menu await driver.find('.test-dm-new-doc').doClick(); await checkDocAndRestore(isLoggedIn, async () => { await gu.dismissWelcomeTourIfNeeded(); assert.equal(await gu.getCell('A', 1).getText(), ''); if (!isLoggedIn) { assert.equal(await driver.find('.test-tb-share-action').getText(), 'Save Document'); await driver.find('.test-tb-share').click(); assert.equal(await driver.find('.test-save-copy').isPresent(), true); // There is no original of this document. assert.equal(await driver.find('.test-open-original').isPresent(), false); } else { assert.equal(await driver.find('.test-tb-share-action').isPresent(), false); } }); // Import doc from add-new menu await gu.docMenuImport('uploads/FileUploadData.csv'); await checkDocAndRestore(isLoggedIn, async () => assert.equal(await gu.getCell('fname', 1).getText(), 'george')); } // Wait for image to load (or fail), then check naturalWidth to ensure it loaded successfully. const checkImageLoaded = stackWrapFunc(async function(img: WebElement) { await driver.wait(() => img.getAttribute('complete'), 10000); assert.isAbove(Number(await img.getAttribute('naturalWidth')), 0); }); async function testExamplesCollapsing() { assert.equal(await driver.find('.test-dm-pinned-doc-name').isDisplayed(), true); // Collapse the templates section, check it's collapsed await driver.find('.test-dm-all-docs-templates-header').click(); assert.equal(await driver.find('.test-dm-pinned-doc-name').isPresent(), false); // Reload and check it's still collapsed. await driver.navigate().refresh(); await gu.waitForDocMenuToLoad(); assert.equal(await driver.find('.test-dm-pinned-doc-name').isPresent(), false); // Expand back, and check. await driver.find('.test-dm-all-docs-templates-header').click(); assert.equal(await driver.find('.test-dm-pinned-doc-name').isDisplayed(), true); // Reload and check it's still expanded. await driver.navigate().refresh(); await gu.waitForDocMenuToLoad(); assert.equal(await driver.find('.test-dm-pinned-doc-name').isDisplayed(), true); } async function testExamplesSection() { // Check rendering and functionality of the examples and templates section // Check titles. assert.includeMembers(await driver.findAll('.test-dm-pinned-doc-name', (el) => el.getText()), ['Lightweight CRM']); // Check the Discover More Templates button is shown. const discoverMoreButton = await driver.find('.test-dm-all-docs-templates-discover-more'); assert(await discoverMoreButton.isPresent()); assert.include(await discoverMoreButton.getAttribute('href'), '/p/templates'); // Check that the button takes us to the templates page, then go back. await discoverMoreButton.click(); await gu.waitForDocMenuToLoad(); assert(gu.testCurrentUrl(/p\/templates/)); await driver.navigate().back(); await gu.waitForDocMenuToLoad(); // Check images. const docItem = await driver.findContent('.test-dm-pinned-doc', /Lightweight CRM/); assert.equal(await docItem.find('img').isPresent(), true); await checkImageLoaded(docItem.find('img')); // Both the image and the doc title link to the doc. const imgHref = await docItem.find('img').findClosest('a').getAttribute('href'); const docHref = await docItem.find('.test-dm-pinned-doc-name').findClosest('a').getAttribute('href'); assert.match(docHref, /lightweight-crm/i); assert.equal(imgHref, docHref); // Open the example. await docItem.find('.test-dm-pinned-doc-name').click(); await gu.waitForDocToLoad(); assert.match(await gu.getCell('Company', 1).getText(), /Sporer/); assert.match(await driver.find('.test-bc-doc').value(), /Lightweight CRM/); await driver.navigate().back(); await gu.waitForDocMenuToLoad(); } async function testCopyingExamples(destination?: string) { // Open the example to copy it. Note that we no longer support copying examples from doc menu. // Make full copy of the example. await driver.findContent('.test-dm-pinned-doc-name', /Lightweight CRM/).click(); await gu.waitForDocToLoad(); await driver.findWait('.test-tb-share-action', 500).click(); await gu.completeCopy({destName: 'LCRM Copy', destOrg: destination ?? 'Personal'}); await checkDocAndRestore(true, async () => { assert.match(await gu.getCell('Company', 1).getText(), /Sporer/); assert.match(await driver.find('.test-bc-doc').value(), /LCRM Copy/); }, 2); // Make a template copy of the example. await driver.findContent('.test-dm-pinned-doc-name', /Lightweight CRM/).click(); await gu.waitForDocToLoad(); await driver.findWait('.test-tb-share-action', 500).click(); await driver.findWait('.test-save-as-template', 1000).click(); await gu.completeCopy({destName: 'LCRM Template Copy', destOrg: destination ?? 'Personal'}); await checkDocAndRestore(true, async () => { // No data, because the file was copied as a template. assert.equal(await gu.getCell(0, 1).getText(), ''); assert.match(await driver.find('.test-bc-doc').value(), /LCRM Template Copy/); }, 2); } async function testSelectedExamplesPage() { // Click Examples & Templates in left panel. await driver.find('.test-dm-templates-page').click(); await gu.waitForDocMenuToLoad(); // Check Featured Templates are shown at the top of the page. assert.equal(await driver.findWait('.test-dm-featured-templates-header', 500).getText(), 'Featured'); assert.includeMembers( await driver.findAll('.test-dm-pinned-doc-list .test-dm-pinned-doc-name', (el) => el.getText()), ['Lightweight CRM']); assert.includeMembers( await driver.findAll('.test-dm-pinned-doc-list .test-dm-pinned-doc-desc', (el) => el.getText()), ['CRM template and example for linking data, and creating productive layouts.'] ); // External servers may have additional templates beyond the 3 above, so stop here. if (server.isExternalServer()) { return; } // Check the CRM and Invoice sections are shown below Featured Templates. assert.includeMembers( await driver.findAll('.test-dm-templates-header', (el) => el.getText()), ['CRM', 'Other']); // Check that each section has the correct templates (title and description). const [crmSection, otherSection] = await driver.findAll('.test-dm-templates'); assert.includeMembers( await crmSection.findAll('.test-dm-pinned-doc-name', (el) => el.getText()), ['Lightweight CRM']); assert.includeMembers( await otherSection.findAll('.test-dm-pinned-doc-name', (el) => el.getText()), ['Afterschool Program', 'Investment Research']); assert.includeMembers( await crmSection.findAll('.test-dm-pinned-doc-desc', (el) => el.getText()), ['CRM template and example for linking data, and creating productive layouts.']); assert.includeMembers( await otherSection.findAll('.test-dm-pinned-doc-desc', (el) => el.getText()), [ 'Example for how to model business data, use formulas, and manage complexity.', 'Example for analyzing and visualizing with summary tables and linked charts.' ]); const docItem = await driver.findContent('.test-dm-pinned-doc', /Lightweight CRM/); assert.equal(await docItem.find('img').isPresent(), true); await checkImageLoaded(docItem.find('img')); } }); async function testEmptyWorkspace(options = { buttons: false }) { await gu.openWorkspace("Home"); assert.equal(await driver.findWait('.test-empty-workspace-info', 400).isDisplayed(), true); // Create doc and check it is created. await driver.find('.test-intro-create-doc').click(); await waitAndDismiss(); await emptyDockChecker(); // Check that we don't see empty info. await driver.navigate().back(); await gu.waitForDocMenuToLoad(); assert.equal(await driver.find('.test-empty-workspace-info').isPresent(), false); // Remove created document, it also checks that document is visible. await deleteFirstDoc(); assert.equal(await driver.findWait('.test-empty-workspace-info', 400).isDisplayed(), true); await checkImportDocButton(true); } // Wait for doc to load, check it, then return to home page, and remove the doc so that we // can see the intro again. async function checkDocAndRestore( isLoggedIn: boolean, docChecker: () => Promise, stepsBackToDocMenu: number = 1) { await waitAndDismiss(); await docChecker(); for (let i = 0; i < stepsBackToDocMenu; i++) { await driver.navigate().back(); if (await gu.isAlertShown()) { await gu.acceptAlert(); } } await gu.waitForDocMenuToLoad(); // If not logged in, we create docs "unsaved" and don't see them in doc-menu. if (isLoggedIn) { // Freshly-created users will see a tip for the Add New button; dismiss it. await gu.dismissBehavioralPrompts(); // Delete the first doc we find. We expect exactly one to exist. await deleteFirstDoc(); } assert.equal(await driver.find('.test-dm-doc').isPresent(), false); } async function waitAndDismiss() { await gu.waitForDocToLoad(); await gu.dismissWelcomeTourIfNeeded(); } async function deleteFirstDoc() { assert.equal(await driver.find('.test-dm-doc').isPresent(), true); await driver.find('.test-dm-doc').mouseMove().find('.test-dm-pinned-doc-options').click(); await driver.find('.test-dm-delete-doc').click(); await driver.find('.test-modal-confirm').click(); await driver.wait(async () => !(await driver.find('.test-modal-dialog').isPresent()), 3000); } async function checkIntroButtons(isLoggedIn: boolean) { // Create doc from intro button await checkCreateDocButton(isLoggedIn); // Import doc from intro button await checkImportDocButton(isLoggedIn); } async function checkImportDocButton(isLoggedIn: boolean) { await gu.fileDialogUpload('uploads/FileUploadData.csv', () => driver.find('.test-intro-import-doc').click()); await checkDocAndRestore(isLoggedIn, async () => assert.equal(await gu.getCell('fname', 1).getText(), 'george')); } async function checkCreateDocButton(isLoggedIn: boolean) { await driver.find('.test-intro-create-doc').click(); await checkDocAndRestore(isLoggedIn, emptyDockChecker); } async function emptyDockChecker() { assert.equal(await gu.getCell('A', 1).getText(), ''); }