Skip to content

Commit 1edfc0d

Browse files
authored
feat: add tutorial redirect modal (#584)
this modal should appear only to users that land on a lesson page from a search engine result.
1 parent e749f38 commit 1edfc0d

28 files changed

+543
-31
lines changed

cypress.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
22
"baseUrl": "http://localhost:3000",
3-
"defaultCommandTimeout": 45000,
3+
"defaultCommandTimeout": 60000,
44
"video": false
55
}
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
import tutorialsList, { getTutorialType } from '../../src/utils/tutorials'
2+
3+
function visit (url, { referrer, clearStorage = true } = {}) {
4+
cy.visit(url, {
5+
onBeforeLoad (window) {
6+
referrer &&
7+
Object.defineProperty(window.document, 'referrer', { get () { return referrer } })
8+
9+
clearStorage &&
10+
window.localStorage.clear()
11+
}
12+
})
13+
}
14+
15+
// Asserts
16+
17+
function assertModalShows () {
18+
cy.get('.modal-overlay', { timeout: 5e3 }).should('exist')
19+
cy.get('[data-cy="tutorial-redirect-modal"', { timeout: 5e3 }).should('be.visible')
20+
cy.get('.home', { timeout: 5e3 }).should('be.visible') // still visible behind the overlay
21+
}
22+
23+
function assertModalContent ({ tutorial, lessonId, context = 'start' }) {
24+
const lesson = tutorial && tutorial.lessons.find(lesson => lesson.id === lessonId)
25+
26+
cy.get('.modal-content')
27+
.should('contain.text', tutorial.title)
28+
.should('contain.text', lesson.title)
29+
30+
if (context === 'start') {
31+
// check content matches the start context
32+
cy.get('.modal-content', { timeout: 5e3 }).should('contain', 'start this tutorial')
33+
} else if (context === 'resume') {
34+
cy.get('.modal-content', { timeout: 5e3 }).should('contain', 'resume the tutorial')
35+
}
36+
}
37+
38+
function assertModalActions ({ action = 'start' } = {}) {
39+
if (action === 'start') {
40+
cy.get('[data-cy="tutorial-redirect-modal-action-start"]', { timeout: 5e3 }).should('exist')
41+
} else if (action === 'resume') {
42+
cy.get('[data-cy="tutorial-redirect-modal-action-resume"]', { timeout: 5e3 }).should('exist')
43+
}
44+
}
45+
46+
function assertModalDoesNotShow () {
47+
cy.get('[data-cy="tutorial-redirect-modal"', { timeout: 5e3 }).should('not.exist')
48+
cy.get('.modal-overlay', { timeout: 5e3 }).should('not.exist')
49+
cy.get('.home', { timeout: 5e3 }).should('be.visible')
50+
}
51+
52+
describe('TUTORIAL REDIRECT MODAL', () => {
53+
let tutorial
54+
55+
before(() => {
56+
// get first text based tutorial
57+
tutorial = tutorialsList[Object.keys(tutorialsList).find(tutorialFormattedId => getTutorialType(tutorialFormattedId) === 'text')]
58+
})
59+
60+
it('should show when landing in the middle of a tutorial from a search engine', () => {
61+
visit(`/${tutorial.url}/03`, { referrer: 'https://google.com/' })
62+
assertModalShows()
63+
assertModalContent({ tutorial, lessonId: 3 })
64+
65+
visit(`/${tutorial.url}/04/`, { referrer: 'https://bing.com/' })
66+
assertModalShows()
67+
assertModalContent({ tutorial, lessonId: 4 })
68+
})
69+
70+
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', () => {
71+
visit(`/${tutorial.url}/01`, { referrer: '' })
72+
cy.get('button[data-cy="next-lesson-text"]').click()
73+
cy.get('button[data-cy="next-lesson-text"]').click()
74+
visit(`/${tutorial.url}/05`, { referrer: 'https://google.com', clearStorage: false })
75+
assertModalShows()
76+
assertModalContent({ tutorial, lessonId: 5, context: 'resume' })
77+
assertModalActions({ action: 'resume' })
78+
})
79+
80+
it('should close when using close button', () => {
81+
visit(`/${tutorial.url}/02`, { referrer: 'https://google.com' })
82+
assertModalShows()
83+
assertModalContent({ tutorial, lessonId: 2 })
84+
cy.get('button.close').click()
85+
assertModalDoesNotShow()
86+
})
87+
88+
it('should close when using view lesson button', () => {
89+
visit(`/${tutorial.url}/02`, { referrer: 'https://google.com' })
90+
assertModalShows()
91+
assertModalContent({ tutorial, lessonId: 2 })
92+
cy.get('button[data-cy="tutorial-redirect-modal-view-lesson"').click()
93+
assertModalDoesNotShow()
94+
})
95+
96+
it('should not show when referrer is empty', () => {
97+
visit(`/${tutorial.url}/02`, { referrer: '' })
98+
assertModalDoesNotShow()
99+
})
100+
101+
it('should not show when opening lesson and the referrer is not one of the configured search engines', () => {
102+
visit(`/${tutorial.url}/03`, { referrer: 'https://searchengine.com/' })
103+
assertModalDoesNotShow()
104+
})
105+
106+
it('should not show when opening lesson from app link', () => {
107+
visit(`/${tutorial.url}`, { referrer: 'http://localhost:3000/' })
108+
cy.get(`a[href="/${tutorial.url}/02"][data-cy="lesson-link-standard"]`).click()
109+
assertModalDoesNotShow()
110+
})
111+
112+
it('should not show when opening the first lesson', () => {
113+
visit(`/${tutorial.url}/01`, { referrer: 'https://google.com/' })
114+
assertModalDoesNotShow()
115+
})
116+
117+
it('should not show when opening resources lesson', () => {
118+
visit(`/${tutorial.url}/resources`, { referrer: 'https://google.com/' })
119+
assertModalDoesNotShow()
120+
})
121+
122+
it('should not show when opening a lesson the user already passed', () => {
123+
visit(`/${tutorial.url}/02`, { referrer: 'https://google.com' })
124+
assertModalShows()
125+
assertModalContent({ tutorial, lessonId: 2 })
126+
cy.get('button.close').click()
127+
cy.get('button[data-cy="next-lesson-text"]').click()
128+
visit(`/${tutorial.url}/02`, { referrer: 'https://google.com', clearStorage: false })
129+
assertModalDoesNotShow()
130+
})
131+
132+
it('should not show a second time after starting a tutorial and passing the lesson', () => {
133+
visit(`/${tutorial.url}/01`, { referrer: 'https://google.com' })
134+
assertModalDoesNotShow()
135+
cy.get('button[data-cy="next-lesson-text"]').click()
136+
visit(`/${tutorial.url}/03`, { referrer: 'https://google.com', clearStorage: false })
137+
assertModalShows()
138+
assertModalContent({ tutorial, lessonId: 3, context: 'resume' })
139+
assertModalActions({ action: 'resume' })
140+
cy.get(`[data-cy="tutorial-redirect-modal-action-resume"]`).click()
141+
cy.location('pathname').should('eq', `/${tutorial.url}/02`)
142+
assertModalDoesNotShow()
143+
cy.get('button[data-cy="next-lesson-text"]').click()
144+
cy.location('pathname').should('eq', `/${tutorial.url}/03`)
145+
assertModalDoesNotShow()
146+
})
147+
148+
it('should always show when using the query parameter _forceShowRedirectModal=true', () => {
149+
visit(`/${tutorial.url}/01?_forceShowRedirectModal=true`)
150+
assertModalShows()
151+
assertModalContent({ tutorial, lessonId: 1, context: 'start' })
152+
assertModalActions({ action: 'start' })
153+
})
154+
})

package-lock.json

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
"monaco-editor-webpack-plugin": "^1.9.0",
4646
"new-github-issue-url": "^0.2.1",
4747
"p-timeout": "^3.2.0",
48+
"portal-vue": "^2.1.7",
4849
"prerender-spa-plugin": "^3.4.0",
4950
"querystringify": "^2.1.1",
5051
"raw-loader": "^0.5.1",

src/App.vue

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
<template>
22
<div id="app">
33
<router-view :key="$route.path"></router-view>
4+
<portal-target name="modal"></portal-target>
45
</div>
56
</template>
67

src/api/debug.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
module.exports = (process.env.NODE_ENV === 'development' || process.env.NODE_ENV === 'test') && process.env.DEBUG

src/api/modules/lessons.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ const errorCode = require('err-code')
44
const marked = require('meta-marked')
55

66
const log = require('../logger')
7-
const debug = require('../../utils/debug')
7+
const debug = require('../debug')
88
const config = require('../config')
99

1010
const logGroup = log.createLogGroup('lessons')

src/api/modules/resources.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
const log = require('../logger')
2-
const debug = require('../../utils/debug')
2+
const debug = require('../debug')
33
const tutorialsApi = require('./tutorials')
44
const utils = require('../utils')
55

src/api/modules/tutorials.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ const _ = require('lodash')
1212
const del = require('del')
1313

1414
const log = require('../logger')
15-
const debug = require('../../utils/debug')
15+
const debug = require('../debug')
1616
const config = require('../config')
1717
const utils = require('../utils')
1818
const lessonsApi = require('./lessons')

src/components/Lesson.vue

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
<template>
22
<div :class="{'overflow-hidden': expandChallenge}">
3+
<TutorialRedirectModal :tutorial="tutorial" :lesson="lesson" />
34
<Header/>
45
<div class="container center-l mw7-l ph3">
56
<section class="mw7 center mt3 pt2">
@@ -11,7 +12,7 @@
1112
:lessonsInTutorial="lessonsInTutorial"
1213
:lessonPassed="lessonPassed" />
1314
<TypeIcon
14-
:lessonId="isResources? 'resources' : lessonId"
15+
:lessonId="isResources ? 'resources' : lessonId"
1516
:tutorialId="tutorial.formattedId"
1617
class="h2 ml3" />
1718
</div>
@@ -153,6 +154,7 @@ import Output from './Output.vue'
153154
import Info from './Info.vue'
154155
import Validator from './Validator.vue'
155156
import TutorialCompletionCallout from './callouts/TutorialCompletion.vue'
157+
import TutorialRedirectModal from './modals/TutorialRedirectModal.vue'
156158
import TypeIcon from './TypeIcon.vue'
157159
158160
const MAX_EXEC_TIMEOUT = isProduction ? 10000 : 60000
@@ -221,6 +223,7 @@ export default {
221223
Info,
222224
Validator,
223225
TutorialCompletionCallout,
226+
TutorialRedirectModal,
224227
TypeIcon
225228
},
226229
props: {

0 commit comments

Comments
 (0)