From 3049d4d6d93fc7b92260ea462811ec15ba99a36e Mon Sep 17 00:00:00 2001 From: Ze Bateira Date: Wed, 27 Jan 2021 15:24:25 +0000 Subject: [PATCH] feat: add tutorial redirect modal this modal should appear only to users that land on a lesson page from a search engine result. --- cypress.json | 2 +- .../tutorial-redirect-modal.spec.js | 154 ++++++++++++++++ package-lock.json | 5 + package.json | 1 + src/App.vue | 1 + src/api/debug.js | 1 + src/api/modules/lessons.js | 2 +- src/api/modules/resources.js | 2 +- src/api/modules/tutorials.js | 2 +- src/components/Lesson.vue | 5 +- src/components/buttons/Button.vue | 7 +- src/components/buttons/ButtonClose.vue | 9 +- src/components/buttons/ButtonLink.vue | 10 +- src/components/modals/BaseModal.vue | 85 +++++++++ .../modals/TutorialRedirectModal.vue | 174 ++++++++++++++++++ src/config.js | 19 +- src/main.js | 9 +- src/pages/Home.vue | 1 + src/pages/Lesson.vue | 2 +- src/routes.js | 3 +- src/state/init.js | 5 + src/state/modals.js | 5 + src/static/translations/en.json | 22 +++ src/styles/media-queries.css | 13 ++ src/utils/countly.js | 24 ++- src/utils/debug.js | 6 +- src/utils/env.js | 3 +- src/utils/tutorials.js | 2 +- 28 files changed, 543 insertions(+), 31 deletions(-) create mode 100644 cypress/integration/tutorial-redirect-modal.spec.js create mode 100644 src/api/debug.js create mode 100644 src/components/modals/BaseModal.vue create mode 100644 src/components/modals/TutorialRedirectModal.vue create mode 100644 src/state/init.js create mode 100644 src/state/modals.js create mode 100644 src/styles/media-queries.css diff --git a/cypress.json b/cypress.json index 07e06d861..d9660f94c 100644 --- a/cypress.json +++ b/cypress.json @@ -1,5 +1,5 @@ { "baseUrl": "http://localhost:3000", - "defaultCommandTimeout": 45000, + "defaultCommandTimeout": 60000, "video": false } diff --git a/cypress/integration/tutorial-redirect-modal.spec.js b/cypress/integration/tutorial-redirect-modal.spec.js new file mode 100644 index 000000000..197a37906 --- /dev/null +++ b/cypress/integration/tutorial-redirect-modal.spec.js @@ -0,0 +1,154 @@ +import tutorialsList, { getTutorialType } from '../../src/utils/tutorials' + +function visit (url, { referrer, clearStorage = true } = {}) { + cy.visit(url, { + onBeforeLoad (window) { + referrer && + Object.defineProperty(window.document, 'referrer', { get () { return referrer } }) + + clearStorage && + window.localStorage.clear() + } + }) +} + +// Asserts + +function assertModalShows () { + cy.get('.modal-overlay', { timeout: 5e3 }).should('exist') + cy.get('[data-cy="tutorial-redirect-modal"', { timeout: 5e3 }).should('be.visible') + cy.get('.home', { timeout: 5e3 }).should('be.visible') // still visible behind the overlay +} + +function assertModalContent ({ tutorial, lessonId, context = 'start' }) { + const lesson = tutorial && tutorial.lessons.find(lesson => lesson.id === lessonId) + + cy.get('.modal-content') + .should('contain.text', tutorial.title) + .should('contain.text', lesson.title) + + if (context === 'start') { + // check content matches the start context + cy.get('.modal-content', { timeout: 5e3 }).should('contain', 'start this tutorial') + } else if (context === 'resume') { + cy.get('.modal-content', { timeout: 5e3 }).should('contain', 'resume the tutorial') + } +} + +function assertModalActions ({ action = 'start' } = {}) { + if (action === 'start') { + cy.get('[data-cy="tutorial-redirect-modal-action-start"]', { timeout: 5e3 }).should('exist') + } else if (action === 'resume') { + cy.get('[data-cy="tutorial-redirect-modal-action-resume"]', { timeout: 5e3 }).should('exist') + } +} + +function assertModalDoesNotShow () { + cy.get('[data-cy="tutorial-redirect-modal"', { timeout: 5e3 }).should('not.exist') + cy.get('.modal-overlay', { timeout: 5e3 }).should('not.exist') + cy.get('.home', { timeout: 5e3 }).should('be.visible') +} + +describe('TUTORIAL REDIRECT MODAL', () => { + let tutorial + + before(() => { + // get first text based tutorial + tutorial = tutorialsList[Object.keys(tutorialsList).find(tutorialFormattedId => getTutorialType(tutorialFormattedId) === 'text')] + }) + + it('should show when landing in the middle of a tutorial from a search engine', () => { + visit(`/${tutorial.url}/03`, { referrer: 'https://google.com/' }) + assertModalShows() + assertModalContent({ tutorial, lessonId: 3 }) + + visit(`/${tutorial.url}/04/`, { referrer: 'https://bing.com/' }) + assertModalShows() + assertModalContent({ tutorial, lessonId: 4 }) + }) + + it('should show when landing in the middle of a tutorial from a search engine and show the resume option when some past lessons have been passed', () => { + visit(`/${tutorial.url}/01`, { referrer: '' }) + cy.get('button[data-cy="next-lesson-text"]').click() + cy.get('button[data-cy="next-lesson-text"]').click() + visit(`/${tutorial.url}/05`, { referrer: 'https://google.com', clearStorage: false }) + assertModalShows() + assertModalContent({ tutorial, lessonId: 5, context: 'resume' }) + assertModalActions({ action: 'resume' }) + }) + + it('should close when using close button', () => { + visit(`/${tutorial.url}/02`, { referrer: 'https://google.com' }) + assertModalShows() + assertModalContent({ tutorial, lessonId: 2 }) + cy.get('button.close').click() + assertModalDoesNotShow() + }) + + it('should close when using view lesson button', () => { + visit(`/${tutorial.url}/02`, { referrer: 'https://google.com' }) + assertModalShows() + assertModalContent({ tutorial, lessonId: 2 }) + cy.get('button[data-cy="tutorial-redirect-modal-view-lesson"').click() + assertModalDoesNotShow() + }) + + it('should not show when referrer is empty', () => { + visit(`/${tutorial.url}/02`, { referrer: '' }) + assertModalDoesNotShow() + }) + + it('should not show when opening lesson and the referrer is not one of the configured search engines', () => { + visit(`/${tutorial.url}/03`, { referrer: 'https://searchengine.com/' }) + assertModalDoesNotShow() + }) + + it('should not show when opening lesson from app link', () => { + visit(`/${tutorial.url}`, { referrer: 'http://localhost:3000/' }) + cy.get(`a[href="/${tutorial.url}/02"][data-cy="lesson-link-standard"]`).click() + assertModalDoesNotShow() + }) + + it('should not show when opening the first lesson', () => { + visit(`/${tutorial.url}/01`, { referrer: 'https://google.com/' }) + assertModalDoesNotShow() + }) + + it('should not show when opening resources lesson', () => { + visit(`/${tutorial.url}/resources`, { referrer: 'https://google.com/' }) + assertModalDoesNotShow() + }) + + it('should not show when opening a lesson the user already passed', () => { + visit(`/${tutorial.url}/02`, { referrer: 'https://google.com' }) + assertModalShows() + assertModalContent({ tutorial, lessonId: 2 }) + cy.get('button.close').click() + cy.get('button[data-cy="next-lesson-text"]').click() + visit(`/${tutorial.url}/02`, { referrer: 'https://google.com', clearStorage: false }) + assertModalDoesNotShow() + }) + + it('should not show a second time after starting a tutorial and passing the lesson', () => { + visit(`/${tutorial.url}/01`, { referrer: 'https://google.com' }) + assertModalDoesNotShow() + cy.get('button[data-cy="next-lesson-text"]').click() + visit(`/${tutorial.url}/03`, { referrer: 'https://google.com', clearStorage: false }) + assertModalShows() + assertModalContent({ tutorial, lessonId: 3, context: 'resume' }) + assertModalActions({ action: 'resume' }) + cy.get(`[data-cy="tutorial-redirect-modal-action-resume"]`).click() + cy.location('pathname').should('eq', `/${tutorial.url}/02`) + assertModalDoesNotShow() + cy.get('button[data-cy="next-lesson-text"]').click() + cy.location('pathname').should('eq', `/${tutorial.url}/03`) + assertModalDoesNotShow() + }) + + it('should always show when using the query parameter _forceShowRedirectModal=true', () => { + visit(`/${tutorial.url}/01?_forceShowRedirectModal=true`) + assertModalShows() + assertModalContent({ tutorial, lessonId: 1, context: 'start' }) + assertModalActions({ action: 'start' }) + }) +}) diff --git a/package-lock.json b/package-lock.json index 2a6e0b7f8..1dd58da3a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20277,6 +20277,11 @@ "resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.16.1.tgz", "integrity": "sha512-Wb4p1J4zyFTbM+u6WuO4XstYx4Ky9Cewe4DWrel7B0w6VVICvPwdOpotjzcf6eD8TsckVnIMNONQyPIUFOUbCQ==" }, + "portal-vue": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/portal-vue/-/portal-vue-2.1.7.tgz", + "integrity": "sha512-+yCno2oB3xA7irTt0EU5Ezw22L2J51uKAacE/6hMPMoO/mx3h4rXFkkBkT4GFsMDv/vEe8TNKC3ujJJ0PTwb6g==" + }, "portfinder": { "version": "1.0.26", "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.26.tgz", diff --git a/package.json b/package.json index 2e93a26ee..1ec0f08e6 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,7 @@ "monaco-editor-webpack-plugin": "^1.9.0", "new-github-issue-url": "^0.2.1", "p-timeout": "^3.2.0", + "portal-vue": "^2.1.7", "prerender-spa-plugin": "^3.4.0", "querystringify": "^2.1.1", "raw-loader": "^0.5.1", diff --git a/src/App.vue b/src/App.vue index d59b23a41..13701dac1 100644 --- a/src/App.vue +++ b/src/App.vue @@ -1,6 +1,7 @@ diff --git a/src/api/debug.js b/src/api/debug.js new file mode 100644 index 000000000..5e590cfc3 --- /dev/null +++ b/src/api/debug.js @@ -0,0 +1 @@ +module.exports = (process.env.NODE_ENV === 'development' || process.env.NODE_ENV === 'test') && process.env.DEBUG diff --git a/src/api/modules/lessons.js b/src/api/modules/lessons.js index d618edc4c..c3ed09f0e 100644 --- a/src/api/modules/lessons.js +++ b/src/api/modules/lessons.js @@ -4,7 +4,7 @@ const errorCode = require('err-code') const marked = require('meta-marked') const log = require('../logger') -const debug = require('../../utils/debug') +const debug = require('../debug') const config = require('../config') const logGroup = log.createLogGroup('lessons') diff --git a/src/api/modules/resources.js b/src/api/modules/resources.js index 76ce408c6..a5a7bd0d2 100644 --- a/src/api/modules/resources.js +++ b/src/api/modules/resources.js @@ -1,5 +1,5 @@ const log = require('../logger') -const debug = require('../../utils/debug') +const debug = require('../debug') const tutorialsApi = require('./tutorials') const utils = require('../utils') diff --git a/src/api/modules/tutorials.js b/src/api/modules/tutorials.js index eb7618081..344c17a9a 100644 --- a/src/api/modules/tutorials.js +++ b/src/api/modules/tutorials.js @@ -12,7 +12,7 @@ const _ = require('lodash') const del = require('del') const log = require('../logger') -const debug = require('../../utils/debug') +const debug = require('../debug') const config = require('../config') const utils = require('../utils') const lessonsApi = require('./lessons') diff --git a/src/components/Lesson.vue b/src/components/Lesson.vue index abd440b78..f6b2a760a 100644 --- a/src/components/Lesson.vue +++ b/src/components/Lesson.vue @@ -1,5 +1,6 @@