103 |
104 |
105 |
106 | )
107 | }
108 |
--------------------------------------------------------------------------------
/components/Mobile/MobileProgress.tsx:
--------------------------------------------------------------------------------
1 | import { CheckIcon } from "@heroicons/react/solid"
2 | import { isLessonCompleted } from "../../utils/machineUtils"
3 |
4 | function classNames(...classes) {
5 | return classes.filter(Boolean).join(" ")
6 | }
7 |
8 | export default function LessonSteps({ course, content, progressService }) {
9 | return (
10 |
79 | )
80 | }
81 |
--------------------------------------------------------------------------------
/components/Progress/CompletedLesson.tsx:
--------------------------------------------------------------------------------
1 | import { CheckIcon } from "@heroicons/react/solid"
2 |
3 | type Props = {
4 | index: number
5 | }
6 |
7 | export default function CompletedLesson({ index }: Props) {
8 | return (
9 | <>
10 |
11 |
15 |
16 |
17 |
18 | >
19 | )
20 | }
21 |
--------------------------------------------------------------------------------
/components/Progress/IncompleteLesson.tsx:
--------------------------------------------------------------------------------
1 | type Props = {
2 | index: number
3 | }
4 |
5 | export default function IncompleteLesson({ index }: Props) {
6 | return (
7 | <>
8 |
9 |
13 |
14 |
15 |
16 | >
17 | )
18 | }
19 |
--------------------------------------------------------------------------------
/components/Progress/ProgressLine.tsx:
--------------------------------------------------------------------------------
1 | type Props = {
2 | index: number
3 | isCompleted: boolean
4 | lessons: []
5 | }
6 |
7 | export default function ProgressLine({ index, isCompleted, lessons }: Props) {
8 | return (
9 | <>
10 | {index !== lessons.length - 1 ? (
11 |
17 | ) : null}
18 | >
19 | )
20 | }
21 |
--------------------------------------------------------------------------------
/components/Subscribe.tsx:
--------------------------------------------------------------------------------
1 | import React from "react"
2 | import { useForm, SubmitHandler } from "react-hook-form"
3 |
4 | type Inputs = {
5 | email: string
6 | }
7 |
8 | export default function Subscribe() {
9 | const {
10 | register,
11 | handleSubmit,
12 | formState,
13 | formState: { errors },
14 | } = useForm()
15 |
16 | const [isSubmitted, setIsSubmitted] = React.useState("")
17 |
18 | const onSubmit: SubmitHandler = async (data, event) => {
19 | event.target.reset()
20 |
21 | const subscribe = await fetch("/api/subscribe", {
22 | method: "POST",
23 | headers: {
24 | "Content-Type": "application/json",
25 | },
26 | body: JSON.stringify(data),
27 | })
28 | const response = await subscribe.json()
29 |
30 | setIsSubmitted(response.message)
31 | }
32 |
33 | return (
34 |
75 | )
76 | }
77 |
--------------------------------------------------------------------------------
/content/courses/cypress-fundamentals/command-chaining.mdx:
--------------------------------------------------------------------------------
1 | # Command Chaining
2 |
3 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec mattis lobortis maximus. Quisque egestas, sem fringilla egestas aliquet, turpis nisl placerat justo, vitae feugiat ligula urna in justo. Curabitur porta risus libero, tempus imperdiet arcu auctor sed. Praesent eget luctus tortor, vel hendrerit sapien. Phasellus quis ex ligula. Curabitur vehicula, lacus imperdiet ornare pulvinar, sem massa vulputate sem, eget mattis ex diam a lorem. Cras ac ex gravida magna vestibulum volutpat et et elit. Nullam vitae porttitor nulla, sed volutpat nisi.
4 |
5 | ## Section 1
6 |
7 | Sed elementum ut nulla nec sagittis. Sed orci neque, cursus id lacinia id, suscipit eget ligula. Etiam mollis dolor et libero dapibus facilisis. Phasellus erat ex, efficitur egestas lorem vitae, rhoncus convallis quam. Aliquam non laoreet ipsum. Nullam luctus, felis ut imperdiet mattis, ligula ligula ultricies orci, at dignissim erat odio id ligula. Praesent placerat sapien eu urna hendrerit pulvinar. Integer eget diam enim. Cras id massa porta, feugiat mi ac, euismod dui. Ut sed justo augue. Quisque viverra enim id nisi placerat, nec tincidunt urna facilisis. Nam vitae vestibulum ex, quis malesuada orci. Nulla a mi nec sem consectetur luctus non placerat urna.
8 |
9 | ## Section 2
10 |
11 | ```js
12 | export async function getStaticProps(context) {
13 | const res = await fetch(`https://.../data`)
14 | const data = await res.json()
15 |
16 | if (!data) {
17 | return {
18 | notFound: true,
19 | }
20 | }
21 |
22 | return {
23 | props: { data }, // will be passed to the page component as props
24 | }
25 | }
26 | ```
27 |
28 | Vestibulum congue consectetur quam in mattis. Maecenas condimentum gravida arcu eu blandit. Aenean lorem risus, imperdiet eu bibendum vitae, imperdiet in diam. Phasellus quis risus ac leo condimentum egestas quis in tortor. Pellentesque at aliquet urna. Proin quis est tincidunt, molestie sem sit amet, aliquam leo. Suspendisse egestas lacinia dignissim.
29 |
30 | ## Section 3
31 |
32 | Aenean eu lobortis nisi. Donec rutrum fringilla orci, vitae fermentum nisi porta eu. Nam sit amet odio lacus. Nulla eu fermentum metus. Suspendisse eu aliquet lectus, at fringilla massa. Pellentesque hendrerit iaculis metus id aliquet. Curabitur urna libero, molestie in lorem porta, imperdiet ultricies ex. Cras elementum aliquam lorem, non semper enim fermentum sed. Nullam egestas, elit at vulputate pretium, dui enim ornare erat, a tristique nulla risus id neque.
33 |
--------------------------------------------------------------------------------
/content/courses/cypress-fundamentals/cypress-runs-in-the-browser.mdx:
--------------------------------------------------------------------------------
1 | # Cypress runs in the browser
2 |
3 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec mattis lobortis maximus. Quisque egestas, sem fringilla egestas aliquet, turpis nisl placerat justo, vitae feugiat ligula urna in justo. Curabitur porta risus libero, tempus imperdiet arcu auctor sed. Praesent eget luctus tortor, vel hendrerit sapien. Phasellus quis ex ligula. Curabitur vehicula, lacus imperdiet ornare pulvinar, sem massa vulputate sem, eget mattis ex diam a lorem. Cras ac ex gravida magna vestibulum volutpat et et elit. Nullam vitae porttitor nulla, sed volutpat nisi.
4 |
5 | ## Section 1
6 |
7 | ```js
8 | describe("Next Lesson Button on Course Pages", () => {
9 | beforeEach(() => {
10 | // @ts-ignore
11 | cy.restoreLocalStorage()
12 | })
13 |
14 | afterEach(() => {
15 | // @ts-ignore
16 | cy.saveLocalStorage()
17 | })
18 |
19 | it("says 'Start Course' and links to the first lesson if none of the lessons have been completed", () => {
20 | cy.visit(`/${sectionSlug}`)
21 | cy.getBySel("next-lesson-button").then(($btn) => {
22 | // @ts-ignore
23 | const text = $btn.text()
24 | // @ts-ignore
25 | const href = $btn.attr("href")
26 |
27 | expect(text).to.eq("Start Course")
28 | expect(href).to.eq(
29 | `/${sectionSlug}/${coursesJson[`${sectionSlug}`].lessons[0].slug}`
30 | )
31 | })
32 | })
33 | ```
34 |
35 | Sed elementum ut nulla nec sagittis. Sed orci neque, cursus id lacinia id, suscipit eget ligula. Etiam mollis dolor et libero dapibus facilisis. Phasellus erat ex, efficitur egestas lorem vitae, rhoncus convallis quam. Aliquam non laoreet ipsum. Nullam luctus, felis ut imperdiet mattis, ligula ligula ultricies orci, at dignissim erat odio id ligula. Praesent placerat sapien eu urna hendrerit pulvinar. Integer eget diam enim. Cras id massa porta, feugiat mi ac, euismod dui. Ut sed justo augue. Quisque viverra enim id nisi placerat, nec tincidunt urna facilisis. Nam vitae vestibulum ex, quis malesuada orci. Nulla a mi nec sem consectetur luctus non placerat urna.
36 |
37 | ## Section 2
38 |
39 | Vestibulum congue consectetur quam in mattis. Maecenas condimentum gravida arcu eu blandit. Aenean lorem risus, imperdiet eu bibendum vitae, imperdiet in diam. Phasellus quis risus ac leo condimentum egestas quis in tortor. Pellentesque at aliquet urna. Proin quis est tincidunt, molestie sem sit amet, aliquam leo. Suspendisse egestas lacinia dignissim.
40 |
--------------------------------------------------------------------------------
/content/courses/cypress-fundamentals/how-to-write-a-test.mdx:
--------------------------------------------------------------------------------
1 | # How to Write a Test
2 |
3 | ```js
4 | // posts will be populated at build time by getStaticProps()
5 | function Blog({ posts }) {
6 | return (
7 |
8 | {posts.map((post) => (
9 |
{post.title}
10 | ))}
11 |
12 | )
13 | }
14 |
15 | // This function gets called at build time on server-side.
16 | // It won't be called on client-side, so you can even do
17 | // direct database queries. See the "Technical details" section.
18 | export async function getStaticProps() {
19 | // Call an external API endpoint to get posts.
20 | // You can use any data fetching library
21 | const res = await fetch("https://.../posts")
22 | const posts = await res.json()
23 |
24 | // By returning { props: { posts } }, the Blog component
25 | // will receive `posts` as a prop at build time
26 | return {
27 | props: {
28 | posts,
29 | },
30 | }
31 | }
32 |
33 | export default Blog
34 | ```
35 |
36 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec mattis lobortis maximus. Quisque egestas, sem fringilla egestas aliquet, turpis nisl placerat justo, vitae feugiat ligula urna in justo. Curabitur porta risus libero, tempus imperdiet arcu auctor sed. Praesent eget luctus tortor, vel hendrerit sapien. Phasellus quis ex ligula. Curabitur vehicula, lacus imperdiet ornare pulvinar, sem massa vulputate sem, eget mattis ex diam a lorem. Cras ac ex gravida magna vestibulum volutpat et et elit. Nullam vitae porttitor nulla, sed volutpat nisi.
37 |
38 | ## Section 1
39 |
40 | Sed elementum ut nulla nec sagittis. Sed orci neque, cursus id lacinia id, suscipit eget ligula. Etiam mollis dolor et libero dapibus facilisis. Phasellus erat ex, efficitur egestas lorem vitae, rhoncus convallis quam. Aliquam non laoreet ipsum. Nullam luctus, felis ut imperdiet mattis, ligula ligula ultricies orci, at dignissim erat odio id ligula. Praesent placerat sapien eu urna hendrerit pulvinar. Integer eget diam enim. Cras id massa porta, feugiat mi ac, euismod dui. Ut sed justo augue. Quisque viverra enim id nisi placerat, nec tincidunt urna facilisis. Nam vitae vestibulum ex, quis malesuada orci. Nulla a mi nec sem consectetur luctus non placerat urna.
41 |
42 | ## Section 2
43 |
44 | Vestibulum congue consectetur quam in mattis. Maecenas condimentum gravida arcu eu blandit. Aenean lorem risus, imperdiet eu bibendum vitae, imperdiet in diam. Phasellus quis risus ac leo condimentum egestas quis in tortor. Pellentesque at aliquet urna. Proin quis est tincidunt, molestie sem sit amet, aliquam leo. Suspendisse egestas lacinia dignissim.
45 |
46 | ## Section 3
47 |
48 | Aenean eu lobortis nisi. Donec rutrum fringilla orci, vitae fermentum nisi porta eu. Nam sit amet odio lacus. Nulla eu fermentum metus. Suspendisse eu aliquet lectus, at fringilla massa. Pellentesque hendrerit iaculis metus id aliquet. Curabitur urna libero, molestie in lorem porta, imperdiet ultricies ex. Cras elementum aliquam lorem, non semper enim fermentum sed. Nullam egestas, elit at vulputate pretium, dui enim ornare erat, a tristique nulla risus id neque.
49 |
50 | ## Section 4
51 |
52 | Suspendisse vel massa elementum, porta odio pharetra, fringilla nunc. Proin vitae porttitor ex. Phasellus suscipit lorem augue, a pretium mauris porttitor ut. Interdum et malesuada fames ac ante ipsum primis in faucibus. Etiam interdum sollicitudin arcu et tristique. Ut consequat ullamcorper dolor, quis laoreet velit fermentum vitae. Aliquam erat volutpat.
53 |
--------------------------------------------------------------------------------
/content/courses/testing-foundations/knowing-what-to-test.mdx:
--------------------------------------------------------------------------------
1 | # Knowing What to Test
2 |
3 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec mattis lobortis maximus. Quisque egestas, sem fringilla egestas aliquet, turpis nisl placerat justo, vitae feugiat ligula urna in justo. Curabitur porta risus libero, tempus imperdiet arcu auctor sed. Praesent eget luctus tortor, vel hendrerit sapien. Phasellus quis ex ligula. Curabitur vehicula, lacus imperdiet ornare pulvinar, sem massa vulputate sem, eget mattis ex diam a lorem. Cras ac ex gravida magna vestibulum volutpat et et elit. Nullam vitae porttitor nulla, sed volutpat nisi.
4 |
5 | ## Section 1
6 |
7 | Sed elementum ut nulla nec sagittis. Sed orci neque, cursus id lacinia id, suscipit eget ligula. Etiam mollis dolor et libero dapibus facilisis. Phasellus erat ex, efficitur egestas lorem vitae, rhoncus convallis quam. Aliquam non laoreet ipsum. Nullam luctus, felis ut imperdiet mattis, ligula ligula ultricies orci, at dignissim erat odio id ligula. Praesent placerat sapien eu urna hendrerit pulvinar. Integer eget diam enim. Cras id massa porta, feugiat mi ac, euismod dui. Ut sed justo augue. Quisque viverra enim id nisi placerat, nec tincidunt urna facilisis. Nam vitae vestibulum ex, quis malesuada orci. Nulla a mi nec sem consectetur luctus non placerat urna.
8 |
9 | ## Section 2
10 |
11 | ```js
12 | export async function getStaticProps(context) {
13 | const res = await fetch(`https://.../data`)
14 | const data = await res.json()
15 |
16 | if (!data) {
17 | return {
18 | notFound: true,
19 | }
20 | }
21 |
22 | return {
23 | props: { data }, // will be passed to the page component as props
24 | }
25 | }
26 | ```
27 |
28 | Vestibulum congue consectetur quam in mattis. Maecenas condimentum gravida arcu eu blandit. Aenean lorem risus, imperdiet eu bibendum vitae, imperdiet in diam. Phasellus quis risus ac leo condimentum egestas quis in tortor. Pellentesque at aliquet urna. Proin quis est tincidunt, molestie sem sit amet, aliquam leo. Suspendisse egestas lacinia dignissim.
29 |
30 | ## Section 3
31 |
32 | Aenean eu lobortis nisi. Donec rutrum fringilla orci, vitae fermentum nisi porta eu. Nam sit amet odio lacus. Nulla eu fermentum metus. Suspendisse eu aliquet lectus, at fringilla massa. Pellentesque hendrerit iaculis metus id aliquet. Curabitur urna libero, molestie in lorem porta, imperdiet ultricies ex. Cras elementum aliquam lorem, non semper enim fermentum sed. Nullam egestas, elit at vulputate pretium, dui enim ornare erat, a tristique nulla risus id neque.
33 |
34 | ## Section 4
35 |
36 | Suspendisse vel massa elementum, porta odio pharetra, fringilla nunc. Proin vitae porttitor ex. Phasellus suscipit lorem augue, a pretium mauris porttitor ut. Interdum et malesuada fames ac ante ipsum primis in faucibus. Etiam interdum sollicitudin arcu et tristique. Ut consequat ullamcorper dolor, quis laoreet velit fermentum vitae. Aliquam erat volutpat.
37 |
--------------------------------------------------------------------------------
/content/courses/testing-foundations/manual-vs-automated-testing.mdx:
--------------------------------------------------------------------------------
1 | # Manual vs Automated Testing
2 |
3 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec mattis lobortis maximus. Quisque egestas, sem fringilla egestas aliquet, turpis nisl placerat justo, vitae feugiat ligula urna in justo. Curabitur porta risus libero, tempus imperdiet arcu auctor sed. Praesent eget luctus tortor, vel hendrerit sapien. Phasellus quis ex ligula. Curabitur vehicula, lacus imperdiet ornare pulvinar, sem massa vulputate sem, eget mattis ex diam a lorem. Cras ac ex gravida magna vestibulum volutpat et et elit. Nullam vitae porttitor nulla, sed volutpat nisi.
4 |
5 | ## Section 1
6 |
7 | Sed elementum ut nulla nec sagittis. Sed orci neque, cursus id lacinia id, suscipit eget ligula. Etiam mollis dolor et libero dapibus facilisis. Phasellus erat ex, efficitur egestas lorem vitae, rhoncus convallis quam. Aliquam non laoreet ipsum. Nullam luctus, felis ut imperdiet mattis, ligula ligula ultricies orci, at dignissim erat odio id ligula. Praesent placerat sapien eu urna hendrerit pulvinar. Integer eget diam enim. Cras id massa porta, feugiat mi ac, euismod dui. Ut sed justo augue. Quisque viverra enim id nisi placerat, nec tincidunt urna facilisis. Nam vitae vestibulum ex, quis malesuada orci. Nulla a mi nec sem consectetur luctus non placerat urna.
8 |
--------------------------------------------------------------------------------
/content/courses/testing-foundations/testing-is-a-mindset.mdx:
--------------------------------------------------------------------------------
1 | # Testing is a Mindset
2 |
3 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec mattis lobortis maximus. Quisque egestas, sem fringilla egestas aliquet, turpis nisl placerat justo, vitae feugiat ligula urna in justo. Curabitur porta risus libero, tempus imperdiet arcu auctor sed. Praesent eget luctus tortor, vel hendrerit sapien. Phasellus quis ex ligula. Curabitur vehicula, lacus imperdiet ornare pulvinar, sem massa vulputate sem, eget mattis ex diam a lorem. Cras ac ex gravida magna vestibulum volutpat et et elit. Nullam vitae porttitor nulla, sed volutpat nisi.
4 |
5 | ## Section 1
6 |
7 | Sed elementum ut nulla nec sagittis. Sed orci neque, cursus id lacinia id, suscipit eget ligula. Etiam mollis dolor et libero dapibus facilisis. Phasellus erat ex, efficitur egestas lorem vitae, rhoncus convallis quam. Aliquam non laoreet ipsum. Nullam luctus, felis ut imperdiet mattis, ligula ligula ultricies orci, at dignissim erat odio id ligula. Praesent placerat sapien eu urna hendrerit pulvinar. Integer eget diam enim. Cras id massa porta, feugiat mi ac, euismod dui. Ut sed justo augue. Quisque viverra enim id nisi placerat, nec tincidunt urna facilisis. Nam vitae vestibulum ex, quis malesuada orci. Nulla a mi nec sem consectetur luctus non placerat urna.
8 |
9 | ## Section 2
10 |
11 | Vestibulum congue consectetur quam in mattis. Maecenas condimentum gravida arcu eu blandit. Aenean lorem risus, imperdiet eu bibendum vitae, imperdiet in diam. Phasellus quis risus ac leo condimentum egestas quis in tortor. Pellentesque at aliquet urna. Proin quis est tincidunt, molestie sem sit amet, aliquam leo. Suspendisse egestas lacinia dignissim.
12 |
--------------------------------------------------------------------------------
/content/courses/testing-your-first-application/app-install-and-overview.mdx:
--------------------------------------------------------------------------------
1 | # App Install & Overview
2 |
3 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec mattis lobortis maximus. Quisque egestas, sem fringilla egestas aliquet, turpis nisl placerat justo, vitae feugiat ligula urna in justo. Curabitur porta risus libero, tempus imperdiet arcu auctor sed. Praesent eget luctus tortor, vel hendrerit sapien. Phasellus quis ex ligula. Curabitur vehicula, lacus imperdiet ornare pulvinar, sem massa vulputate sem, eget mattis ex diam a lorem. Cras ac ex gravida magna vestibulum volutpat et et elit. Nullam vitae porttitor nulla, sed volutpat nisi.
4 |
5 | ## Section 1
6 |
7 | ```js
8 | export async function getStaticProps(context) {
9 | const res = await fetch(`https://.../data`)
10 | const data = await res.json()
11 |
12 | if (!data) {
13 | return {
14 | notFound: true,
15 | }
16 | }
17 |
18 | return {
19 | props: { data }, // will be passed to the page component as props
20 | }
21 | }
22 | ```
23 |
24 | Sed elementum ut nulla nec sagittis. Sed orci neque, cursus id lacinia id, suscipit eget ligula. Etiam mollis dolor et libero dapibus facilisis. Phasellus erat ex, efficitur egestas lorem vitae, rhoncus convallis quam. Aliquam non laoreet ipsum. Nullam luctus, felis ut imperdiet mattis, ligula ligula ultricies orci, at dignissim erat odio id ligula. Praesent placerat sapien eu urna hendrerit pulvinar. Integer eget diam enim. Cras id massa porta, feugiat mi ac, euismod dui. Ut sed justo augue. Quisque viverra enim id nisi placerat, nec tincidunt urna facilisis. Nam vitae vestibulum ex, quis malesuada orci. Nulla a mi nec sem consectetur luctus non placerat urna.
25 |
26 | ## Section 2
27 |
28 | Vestibulum congue consectetur quam in mattis. Maecenas condimentum gravida arcu eu blandit. Aenean lorem risus, imperdiet eu bibendum vitae, imperdiet in diam. Phasellus quis risus ac leo condimentum egestas quis in tortor. Pellentesque at aliquet urna. Proin quis est tincidunt, molestie sem sit amet, aliquam leo. Suspendisse egestas lacinia dignissim.
29 |
30 | Vestibulum congue consectetur quam in mattis. Maecenas condimentum gravida arcu eu blandit. Aenean lorem risus, imperdiet eu bibendum vitae, imperdiet in diam. Phasellus quis risus ac leo condimentum egestas quis in tortor. Pellentesque at aliquet urna. Proin quis est tincidunt, molestie sem sit amet, aliquam leo. Suspendisse egestas lacinia dignissim.
31 |
32 | Vestibulum congue consectetur quam in mattis. Maecenas condimentum gravida arcu eu blandit. Aenean lorem risus, imperdiet eu bibendum vitae, imperdiet in diam. Phasellus quis risus ac leo condimentum egestas quis in tortor. Pellentesque at aliquet urna. Proin quis est tincidunt, molestie sem sit amet, aliquam leo. Suspendisse egestas lacinia dignissim.
33 |
34 | Vestibulum congue consectetur quam in mattis. Maecenas condimentum gravida arcu eu blandit. Aenean lorem risus, imperdiet eu bibendum vitae, imperdiet in diam. Phasellus quis risus ac leo condimentum egestas quis in tortor. Pellentesque at aliquet urna. Proin quis est tincidunt, molestie sem sit amet, aliquam leo. Suspendisse egestas lacinia dignissim.
35 |
--------------------------------------------------------------------------------
/content/courses/testing-your-first-application/installing-cypress-and-writing-our-first-test.mdx:
--------------------------------------------------------------------------------
1 | # Installing Cypress and writing our first test
2 |
3 | ```js
4 | describe("Next Lesson Button on Course Pages", () => {
5 | beforeEach(() => {
6 | // @ts-ignore
7 | cy.restoreLocalStorage()
8 | })
9 |
10 | afterEach(() => {
11 | // @ts-ignore
12 | cy.saveLocalStorage()
13 | })
14 |
15 | it("says 'Start Course' and links to the first lesson if none of the lessons have been completed", () => {
16 | cy.visit(`/${sectionSlug}`)
17 | cy.getBySel("next-lesson-button").then(($btn) => {
18 | // @ts-ignore
19 | const text = $btn.text()
20 | // @ts-ignore
21 | const href = $btn.attr("href")
22 |
23 | expect(text).to.eq("Start Course")
24 | expect(href).to.eq(
25 | `/${sectionSlug}/${coursesJson[`${sectionSlug}`].lessons[0].slug}`
26 | )
27 | })
28 | })
29 | ```
30 |
31 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec mattis lobortis maximus. Quisque egestas, sem fringilla egestas aliquet, turpis nisl placerat justo, vitae feugiat ligula urna in justo. Curabitur porta risus libero, tempus imperdiet arcu auctor sed. Praesent eget luctus tortor, vel hendrerit sapien. Phasellus quis ex ligula. Curabitur vehicula, lacus imperdiet ornare pulvinar, sem massa vulputate sem, eget mattis ex diam a lorem. Cras ac ex gravida magna vestibulum volutpat et et elit. Nullam vitae porttitor nulla, sed volutpat nisi.
32 |
33 | ## Section 1
34 |
35 | Sed elementum ut nulla nec sagittis. Sed orci neque, cursus id lacinia id, suscipit eget ligula. Etiam mollis dolor et libero dapibus facilisis. Phasellus erat ex, efficitur egestas lorem vitae, rhoncus convallis quam. Aliquam non laoreet ipsum. Nullam luctus, felis ut imperdiet mattis, ligula ligula ultricies orci, at dignissim erat odio id ligula. Praesent placerat sapien eu urna hendrerit pulvinar. Integer eget diam enim. Cras id massa porta, feugiat mi ac, euismod dui. Ut sed justo augue. Quisque viverra enim id nisi placerat, nec tincidunt urna facilisis. Nam vitae vestibulum ex, quis malesuada orci. Nulla a mi nec sem consectetur luctus non placerat urna.
36 |
37 | ## Section 2
38 |
39 | Vestibulum congue consectetur quam in mattis. Maecenas condimentum gravida arcu eu blandit. Aenean lorem risus, imperdiet eu bibendum vitae, imperdiet in diam. Phasellus quis risus ac leo condimentum egestas quis in tortor. Pellentesque at aliquet urna. Proin quis est tincidunt, molestie sem sit amet, aliquam leo. Suspendisse egestas lacinia dignissim.
40 |
--------------------------------------------------------------------------------
/content/courses/testing-your-first-application/setting-up-data-before-each-test.mdx:
--------------------------------------------------------------------------------
1 | # Setting up Data Before Each Test
2 |
3 | ```js
4 | // posts will be populated at build time by getStaticProps()
5 | function Blog({ posts }) {
6 | return (
7 |
8 | {posts.map((post) => (
9 |
{post.title}
10 | ))}
11 |
12 | )
13 | }
14 |
15 | // This function gets called at build time on server-side.
16 | // It won't be called on client-side, so you can even do
17 | // direct database queries. See the "Technical details" section.
18 | export async function getStaticProps() {
19 | // Call an external API endpoint to get posts.
20 | // You can use any data fetching library
21 | const res = await fetch("https://.../posts")
22 | const posts = await res.json()
23 |
24 | // By returning { props: { posts } }, the Blog component
25 | // will receive `posts` as a prop at build time
26 | return {
27 | props: {
28 | posts,
29 | },
30 | }
31 | }
32 |
33 | export default Blog
34 | ```
35 |
36 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec mattis lobortis maximus. Quisque egestas, sem fringilla egestas aliquet, turpis nisl placerat justo, vitae feugiat ligula urna in justo. Curabitur porta risus libero, tempus imperdiet arcu auctor sed. Praesent eget luctus tortor, vel hendrerit sapien. Phasellus quis ex ligula. Curabitur vehicula, lacus imperdiet ornare pulvinar, sem massa vulputate sem, eget mattis ex diam a lorem. Cras ac ex gravida magna vestibulum volutpat et et elit. Nullam vitae porttitor nulla, sed volutpat nisi.
37 |
38 | ## Section 1
39 |
40 | Sed elementum ut nulla nec sagittis. Sed orci neque, cursus id lacinia id, suscipit eget ligula. Etiam mollis dolor et libero dapibus facilisis. Phasellus erat ex, efficitur egestas lorem vitae, rhoncus convallis quam. Aliquam non laoreet ipsum. Nullam luctus, felis ut imperdiet mattis, ligula ligula ultricies orci, at dignissim erat odio id ligula. Praesent placerat sapien eu urna hendrerit pulvinar. Integer eget diam enim. Cras id massa porta, feugiat mi ac, euismod dui. Ut sed justo augue. Quisque viverra enim id nisi placerat, nec tincidunt urna facilisis. Nam vitae vestibulum ex, quis malesuada orci. Nulla a mi nec sem consectetur luctus non placerat urna.
41 |
42 | ## Section 2
43 |
44 | Vestibulum congue consectetur quam in mattis. Maecenas condimentum gravida arcu eu blandit. Aenean lorem risus, imperdiet eu bibendum vitae, imperdiet in diam. Phasellus quis risus ac leo condimentum egestas quis in tortor. Pellentesque at aliquet urna. Proin quis est tincidunt, molestie sem sit amet, aliquam leo. Suspendisse egestas lacinia dignissim.
45 |
46 | ## Section 3
47 |
48 | Aenean eu lobortis nisi. Donec rutrum fringilla orci, vitae fermentum nisi porta eu. Nam sit amet odio lacus. Nulla eu fermentum metus. Suspendisse eu aliquet lectus, at fringilla massa. Pellentesque hendrerit iaculis metus id aliquet. Curabitur urna libero, molestie in lorem porta, imperdiet ultricies ex. Cras elementum aliquam lorem, non semper enim fermentum sed. Nullam egestas, elit at vulputate pretium, dui enim ornare erat, a tristique nulla risus id neque.
49 |
--------------------------------------------------------------------------------
/data/courses.json:
--------------------------------------------------------------------------------
1 | {
2 | "testing-your-first-application": {
3 | "title": "Testing Your First Next.js Application",
4 | "slug": "testing-your-first-application",
5 | "description": "How to test a Next.js e-commerce app with Cypress.",
6 | "image": "/images/courses/store.png",
7 | "learnFeatures": [
8 | "Lorem ipsum dolor sit amet",
9 | "Lorem ipsum dolor sit amet",
10 | "Lorem ipsum dolor sit amet"
11 | ],
12 | "lessons": [
13 | {
14 | "title": "App Install and Overview",
15 | "slug": "app-install-and-overview",
16 | "description": "How to install the TodoMVC application and get it up and running.",
17 | "challenges": [
18 | {
19 | "challengeType": "multiple-choice",
20 | "question": "Vestibulum congue consectetur quam in mattis?",
21 | "answers": ["True", "False"],
22 | "correctAnswerIndex": 0
23 | }
24 | ]
25 | },
26 | {
27 | "title": "Installing Cypress and writing our first test",
28 | "slug": "installing-cypress-and-writing-our-first-test",
29 | "description": "How to install Cypress and write your first end to end test.",
30 | "challenges": [
31 | {
32 | "challengeType": "multiple-choice",
33 | "question": "Vestibulum congue consectetur quam in mattis?",
34 | "answers": ["True", "False"],
35 | "correctAnswerIndex": 0
36 | }
37 | ]
38 | },
39 | {
40 | "title": "Setting up Data Before Each Test",
41 | "slug": "setting-up-data-before-each-test",
42 | "description": "How to utilize the beforeEach hook to setup data before each test.",
43 | "challenges": [
44 | {
45 | "challengeType": "multiple-choice",
46 | "question": "Vestibulum congue consectetur quam in mattis?",
47 | "answers": ["True", "False"],
48 | "correctAnswerIndex": 0
49 | }
50 | ]
51 | }
52 | ]
53 | },
54 | "testing-foundations": {
55 | "title": "Testing Foundations",
56 | "slug": "testing-foundations",
57 | "description": "The fundamentals you need to write great tests.",
58 | "image": "https://tailwindui.com/img/component-images/inbox-app-screenshot-2.jpg",
59 | "learnFeatures": [
60 | "Lorem ipsum dolor sit amet",
61 | "Lorem ipsum dolor sit amet",
62 | "Lorem ipsum dolor sit amet"
63 | ],
64 | "lessons": [
65 | {
66 | "title": "Testing is a Mindset",
67 | "slug": "testing-is-a-mindset",
68 | "description": "Understand the testing mindset that is necessary before you begin writing tests.",
69 | "challenges": [
70 | {
71 | "challengeType": "multiple-choice",
72 | "question": "Vestibulum congue consectetur quam in mattis?",
73 | "answers": ["True", "False"],
74 | "correctAnswerIndex": 0
75 | }
76 | ]
77 | },
78 | {
79 | "title": "Knowing What to Test",
80 | "slug": "knowing-what-to-test",
81 | "description": "How to test user journeys, new features and bugs within your application.",
82 | "challenges": [
83 | {
84 | "challengeType": "multiple-choice",
85 | "question": "Vestibulum congue consectetur quam in mattis?",
86 | "answers": ["True", "False"],
87 | "correctAnswerIndex": 0
88 | }
89 | ]
90 | },
91 | {
92 | "title": "Manual vs Automated Testing",
93 | "slug": "manual-vs-automated-testing",
94 | "description": "The differences between automated testing and manual testing.",
95 | "challenges": [
96 | {
97 | "challengeType": "multiple-choice",
98 | "question": "Vestibulum congue consectetur quam in mattis?",
99 | "answers": ["True", "False"],
100 | "correctAnswerIndex": 0
101 | }
102 | ]
103 | }
104 | ]
105 | },
106 | "cypress-fundamentals": {
107 | "title": "Cypress Fundamentals",
108 | "slug": "cypress-fundamentals",
109 | "description": "The aspects of Cypress you must know.",
110 | "image": "/images/courses/cypress-rwa.png",
111 | "learnFeatures": [
112 | "Lorem ipsum dolor sit amet",
113 | "Lorem ipsum dolor sit amet",
114 | "Lorem ipsum dolor sit amet"
115 | ],
116 | "lessons": [
117 | {
118 | "title": "How to Write a Test",
119 | "slug": "how-to-write-a-test",
120 | "description": "How to write tests using the Arrange, Act and Assert pattern.",
121 | "challenges": [
122 | {
123 | "challengeType": "multiple-choice",
124 | "question": "Vestibulum congue consectetur quam in mattis?",
125 | "answers": ["True", "False"],
126 | "correctAnswerIndex": 0
127 | }
128 | ]
129 | },
130 | {
131 | "title": "Cypress Runs in the Browser",
132 | "slug": "cypress-runs-in-the-browser",
133 | "description": "Cypress's architecture, unlike most other testing tools, runs inside of the browser. This means that your tests are being executed in the same environment as your application.",
134 | "challenges": [
135 | {
136 | "challengeType": "multiple-choice",
137 | "question": "Vestibulum congue consectetur quam in mattis?",
138 | "answers": ["True", "False"],
139 | "correctAnswerIndex": 0
140 | }
141 | ]
142 | },
143 | {
144 | "title": "Command Chaining",
145 | "slug": "command-chaining",
146 | "description": "It's important to understand the mechanism Cypress uses to chain commands together. It manages a Promise chain on your behalf...",
147 | "challenges": [
148 | {
149 | "challengeType": "multiple-choice",
150 | "question": "Vestibulum congue consectetur quam in mattis?",
151 | "answers": ["True", "False"],
152 | "correctAnswerIndex": 0
153 | }
154 | ]
155 | }
156 | ]
157 | }
158 | }
159 |
--------------------------------------------------------------------------------
/docs/.vuepress/config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | title: "Real World Testing with Cypress Docs",
3 | description:
4 | "Documentation for the Real World Testing with Cypress Next.js app",
5 | themeConfig: {
6 | nav: [
7 | { text: "Home", link: "/" },
8 | {
9 | text: "GitHub Repo",
10 | link: "https://github.com/cypress-io/cypress-realworld-testing",
11 | },
12 | ],
13 | sidebar: [
14 | {
15 | title: "Directory Structure",
16 | path: "/directory-structure/",
17 | },
18 | {
19 | title: "Pages",
20 | path: "/pages/",
21 | },
22 | {
23 | title: "Machines",
24 | path: "/machines/",
25 | },
26 | {
27 | title: "Scripts",
28 | path: "/scripts/",
29 | },
30 | {
31 | title: "Utils",
32 | path: "/utils/",
33 | },
34 | ],
35 | },
36 | markdown: {
37 | lineNumbers: true,
38 | },
39 | }
40 |
--------------------------------------------------------------------------------
/docs/directory-structure/index.md:
--------------------------------------------------------------------------------
1 | # Directory Structure
2 |
3 | ### /components
4 |
5 | Contains all of the React components used in the app.
6 |
7 | ### /content
8 |
9 | Contains all of the markdown files for the course lesson pages and real world examples
10 |
11 | ### /cypress
12 |
13 | Contains everything necessary for our Cypress E2E tests
14 |
15 | ### /docs
16 |
17 | You are here. This is where all of the documentation for the app lives.
18 |
19 | ### /machines
20 |
21 | Contains all of the [XState](https://xstate.js.org/docs/) machines used in the app.
22 |
23 | ### /pages
24 |
25 | Contains all of the pages for the app.
26 |
27 | ### /public
28 |
29 | This folder contains all of the images used in the app.
30 |
31 | ### /scripts
32 |
33 | Custom JS scripts
34 |
35 | ### /styles
36 |
37 | CSS files
38 |
39 | ### /tests
40 |
41 | Unit Tests written with Mocha
42 |
43 | ### /types
44 |
45 | TypeScript Type defintion files
46 |
47 | ### /utils
48 |
49 | Helpful utility methods and functions
50 |
51 | ### ./learn.json
52 |
53 | Contains all of the data for the courses
54 |
55 | ### ./real-world-examples.json
56 |
57 | Contains all of the data for the real world examples
58 |
--------------------------------------------------------------------------------
/docs/images/progress.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cypress-io/cypress-realworld-testing-course-app/9232930cdb32138b6e693b82a806110a82fa88d4/docs/images/progress.png
--------------------------------------------------------------------------------
/docs/index.md:
--------------------------------------------------------------------------------
1 | # Real World Testing with Cypress
2 |
3 | Real World Testing with Cypress is built with [Next.js](https://nextjs.org). Check out the various links in the left-hand sidebar for more documentation.
4 |
--------------------------------------------------------------------------------
/docs/machines/index.md:
--------------------------------------------------------------------------------
1 | # Machines
2 |
3 | This application is using [Xstate](https://xstate.js.org/docs/) for state management.
4 |
5 | ## progressMachine.ts
6 |
7 | This state machine is used to keep track of a user's progress as they complete lessons within the app. Whenever a user completes a challenge/quiz on a lesson page the progress indicators throughout the site are updated.
8 |
9 | 
10 |
11 | ## progressService.ts
12 |
13 | Since this app is a static site, we need to use `localStorage` to keep track of state across page loads and refreshes. This service is responsible for keeping the state in sync with `localStorage`.
14 |
--------------------------------------------------------------------------------
/docs/pages/index.md:
--------------------------------------------------------------------------------
1 | # Pages
2 |
3 | All of the pages in the application and how Next.js uses some of them to generate [dynamic routes](https://nextjs.org/docs/routing/dynamic-routes).
4 |
5 | ## index.tsx
6 |
7 | This is the index/home page of the app, i.e., `/`
8 |
9 | ## \_app.tsx
10 |
11 | This page acts like a "layout" page and wraps all of the other pages within the app
12 |
13 | ```js{11}
14 | function MyApp({ Component, pageProps }) {
15 | return (
16 | <>
17 |
18 |
19 |
23 |
24 |
25 | >
26 | )
27 | }
28 | ```
29 |
30 | The `` on line `11` is where the other pages are passed into this special component and rendered.
31 |
32 | ## [section]/
33 |
34 | This is a special directory that Next.js uses for [dynamic routes](https://nextjs.org/docs/routing/dynamic-routes). There are two pages within this directory.
35 |
36 | 1. `index.tsx` - is reponsible for rendering all of the course landing pages, i.e.,
37 |
38 | - `/testing-your-first-application`.
39 | - `/testing-foundations`.
40 | - `/cypress-fundamentals`.
41 | - `/advanced-cypress-concepts`.
42 |
43 | 2. `[slug].tsx` - is a [dynamic page](https://nextjs.org/docs/basic-features/pages) and is responsible for rendering all of the lesson pages for the courses, i.e., `/testing-your-first-application/todomvc-app-install-and-overview`.
44 |
45 | ## real-world-examples/
46 |
47 | This is a special directory that Next.js uses for [dynamic routes](https://nextjs.org/docs/routing/dynamic-routes). There are two pages within this directory.
48 |
49 | 1. `index.tsx` - is reponsible for rendering the real world examples landing page, i.e.,
50 |
51 | - `/real-world-examples`.
52 |
53 | 2. `[slug].tsx` - is a [dynamic page](https://nextjs.org/docs/basic-features/pages) and is responsible for rendering all of the real world example pages, i.e., `/real-world-examples/authentication-overview-and-setup`.
54 |
55 | ## getStaticProps()
56 |
57 | [getStaticProps()](https://nextjs.org/docs/basic-features/data-fetching#getstaticprops-static-generation) is a special method from Next.js that is used to fetch the data necessary to render the page and its components.
58 |
59 | ## getStaticPaths()
60 |
61 | [getStaticPaths()](https://nextjs.org/docs/basic-features/data-fetching#getstaticpaths-static-generation) is a special method from Next.js that is used to get the lists of paths necessary to render dynamic routes.
62 |
--------------------------------------------------------------------------------
/docs/scripts/index.md:
--------------------------------------------------------------------------------
1 | # Scripts
2 |
3 | ## generateContentFiles.ts
4 |
5 | This script was used primarily during the early stages of development of the app to render some markdown files with dummy data for the courses content.
6 |
7 | ## generateRealWorldExamples.ts
8 |
9 | This script was used primarily during the early stages of development of the app to render some markdown files with dummy data for the real world examples content.
10 |
11 | ## updateSlugs.ts
12 |
13 | This script takes all of the titles from the lesson in `learn.json` and 'slugify's' them to populate the `slug:` property.
14 |
15 | ## updateRWESlugs.ts
16 |
17 | This script takes all of the titles from the lesson in `real-world-examples.json` and 'slugify's' them to populate the `slug:` property.
18 |
--------------------------------------------------------------------------------
/docs/utils/index.md:
--------------------------------------------------------------------------------
1 | # Utils
2 |
3 | ## machineUtils.ts
4 |
5 | Contains several utility methods used for fetching lesson data and state from `machines/progressMachine.ts`
6 |
7 | ## mdxUtils.ts
8 |
9 | Contains several utility methods used for fetching _courses_ and _real world examples_ content files and data from the `/content` directory.
10 |
--------------------------------------------------------------------------------
/lib/fetch-courses.ts:
--------------------------------------------------------------------------------
1 | import coursesJson from "../data/courses.json"
2 |
3 | export async function fetchCourses() {
4 | return coursesJson
5 | }
6 |
--------------------------------------------------------------------------------
/machines/progressMachine.ts:
--------------------------------------------------------------------------------
1 | import { createMachine, assign } from "xstate"
2 | import { ProgressContext } from "common"
3 | import { concat } from "lodash/fp"
4 | import {
5 | getCourse,
6 | getChallenge,
7 | isSectionCompleted,
8 | } from "../utils/machineUtils"
9 | import coursesJson from "../data/courses.json"
10 |
11 | const defaultContext: ProgressContext = {
12 | sectionsCompleted: [],
13 | lessons: [],
14 | disableChallenges: false,
15 | }
16 |
17 | export const progressMachine = createMachine(
18 | {
19 | id: "progress",
20 | initial: "started",
21 | context: defaultContext,
22 | states: {
23 | started: {
24 | on: {
25 | SKIP_ANSWER: {
26 | actions: ["saveProgress"],
27 | },
28 | SUBMIT_ANSWER: {
29 | actions: ["validateAndLogAnswer", "isSectionCompleted"],
30 | },
31 | DISABLE_CHALLENGES: { actions: ["disableChallenges"] },
32 | COMPLETE_LESSON: { actions: ["saveProgress"] },
33 | },
34 | },
35 | completed: {
36 | on: {
37 | CLEAR_All_PROGRESS: "",
38 | },
39 | },
40 | },
41 | },
42 | {
43 | actions: {
44 | saveProgress: assign((context: any, event: any) => ({
45 | lessons: concat(context.lessons, {
46 | id: event.id,
47 | status: "completed",
48 | }),
49 | })),
50 | validateAndLogAnswer: assign((context: any, event: any) => {
51 | const challenge = getChallenge(
52 | coursesJson,
53 | event.id,
54 | event.challengeIndex
55 | )
56 |
57 | const isCorrectMultipleChoiceAnswer =
58 | challenge.challengeType === "multiple-choice" &&
59 | challenge.correctAnswerIndex === event.userAnswerIndex
60 |
61 | if (isCorrectMultipleChoiceAnswer) {
62 | return {
63 | lessons: concat(context.lessons, {
64 | id: event.id,
65 | status: "completed",
66 | }),
67 | }
68 | }
69 | }),
70 | disableChallenges: assign((context: any, event: any) => ({
71 | disableChallenges: event.value,
72 | })),
73 |
74 | isSectionCompleted: assign((context: any, event: any) => {
75 | const [sectionSlug] = event.id.split("/")
76 | const course = getCourse(coursesJson, event.id)
77 | const completedLessons = context.lessons.filter(
78 | (lesson) => lesson.status === "completed"
79 | )
80 |
81 | if (
82 | completedLessons.length === course.lessons.length &&
83 | !isSectionCompleted(context.sectionsCompleted, sectionSlug)
84 | ) {
85 | return {
86 | sectionsCompleted: concat(context.sectionsCompleted, sectionSlug),
87 | }
88 | }
89 | }),
90 | },
91 | }
92 | )
93 |
--------------------------------------------------------------------------------
/machines/progressService.ts:
--------------------------------------------------------------------------------
1 | import { interpret, State } from "xstate"
2 | import { progressMachine } from "./progressMachine"
3 | const LOCAL_STORAGE_ITEM = "progressState"
4 |
5 | // @ts-ignore
6 | const stateDefinition =
7 | typeof window !== "undefined"
8 | ? JSON.parse(window.localStorage.getItem(LOCAL_STORAGE_ITEM))
9 | : undefined
10 |
11 | let resolvedState
12 | if (stateDefinition) {
13 | const previousState = State.create(stateDefinition)
14 |
15 | // @ts-ignore
16 | resolvedState = progressMachine.resolveState(previousState)
17 | }
18 |
19 | export const progressService = interpret(progressMachine, {
20 | devTools: true,
21 | })
22 | .onTransition((state) => {
23 | if (state.changed) {
24 | typeof window !== "undefined" &&
25 | window.localStorage.setItem(LOCAL_STORAGE_ITEM, JSON.stringify(state))
26 | }
27 | })
28 | .start(resolvedState)
29 |
--------------------------------------------------------------------------------
/next-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 |
4 | // NOTE: This file should not be edited
5 | // see https://nextjs.org/docs/basic-features/typescript for more information.
6 |
--------------------------------------------------------------------------------
/next-sitemap.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | siteUrl: 'https://cypress-realworld-testing-course-app.vercel.app/',
3 | generateRobotsTxt: true,
4 | }
--------------------------------------------------------------------------------
/next.config.js:
--------------------------------------------------------------------------------
1 |
2 | module.exports = {
3 | swcMinify: true,
4 | images: {
5 | domains: ["images.unsplash.com", "source.unsplash.com", "tailwindui.com"],
6 | },
7 | webpack: function (config) {
8 | config.module.rules.push({
9 | test: /\.(eot|woff|woff2|ttf|svg|png|jpg|gif)$/,
10 | use: {
11 | loader: "url-loader",
12 | options: {
13 | limit: 100000,
14 | name: "[name].[ext]",
15 | },
16 | },
17 | })
18 | return config
19 | },
20 | }
21 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "private": true,
3 | "scripts": {
4 | "dev": "next dev",
5 | "build": "next build",
6 | "start": "next start",
7 | "lint": "next lint"
8 | },
9 | "dependencies": {
10 | "@headlessui/react": "^1.5.0",
11 | "@heroicons/react": "^1.0.6",
12 | "@mapbox/rehype-prism": "^0.8.0",
13 | "@tailwindcss/aspect-ratio": "^0.4.0",
14 | "@tailwindcss/forms": "^0.5.0",
15 | "@tailwindcss/typography": "^0.5.2",
16 | "@types/glob": "^7.2.0",
17 | "@xstate/react": "^2.0.1",
18 | "dotenv": "^16.0.0",
19 | "glob": "^7.2.0",
20 | "globby": "^13.1.1",
21 | "linkinator": "^3.0.3",
22 | "next": "^12.3.4",
23 | "react": "^17.0.2",
24 | "react-dom": "^17.0.2",
25 | "react-hook-form": "^7.27.1",
26 | "react-slick": "^0.28.1",
27 | "rehype-slug": "^5.0.1",
28 | "xstate": "^4.30.5"
29 | },
30 | "devDependencies": {
31 | "@types/chai": "^4.3.0",
32 | "@types/lodash": "^4.14.179",
33 | "@types/mocha": "^9.1.0",
34 | "@types/react": "^17.0.39",
35 | "autoprefixer": "^10.4.2",
36 | "chai": "^4.3.6",
37 | "eslint": "^8.10.0",
38 | "eslint-config-next": "^12.1.0",
39 | "eslint-config-prettier": "^8.5.0",
40 | "eslint-plugin-prettier": "^4.0.0",
41 | "gray-matter": "^4.0.3",
42 | "markdown-toc-unlazy": "^1.0.1",
43 | "mocha": "^9.2.1",
44 | "next-mdx-remote": "^4.0.0",
45 | "next-remote-watch": "^1.0.0",
46 | "next-sitemap": "^2.5.7",
47 | "postcss": "^8.4.8",
48 | "prettier": "^2.5.1",
49 | "pretty-quick": "^3.1.3",
50 | "slugify": "^1.6.5",
51 | "tailwindcss": "^3.0.23",
52 | "tsconfig-paths": "^3.13.0",
53 | "typescript": "^4.6.2",
54 | "unslugify": "^1.0.2",
55 | "url-loader": "^4.1.1"
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/pages/[course]/[slug].tsx:
--------------------------------------------------------------------------------
1 | import fs from "fs"
2 | import matter from "gray-matter"
3 | import { MDXRemoteSerializeResult } from "next-mdx-remote"
4 | import { serialize } from "next-mdx-remote/serialize"
5 | import Head from "next/head"
6 | import dynamic from "next/dynamic"
7 | import path from "path"
8 | import { find, findIndex } from "lodash/fp"
9 | import { useActor } from "@xstate/react"
10 | import rehypeSlug from "rehype-slug"
11 | import rehypePrism from "@mapbox/rehype-prism"
12 | import { progressService } from "../../machines/progressService"
13 | import Layout from "../../components/Layout"
14 | import LessonLayout from "../../components/Lesson/LessonLayout"
15 | import MCChallenge from "../../components/Lesson/MultipleChoiceChallenge"
16 | import { fetchCourses } from "../../lib/fetch-courses"
17 | import {
18 | LessonTableOfContents,
19 | MultipleChoiceChallenge,
20 | } from "../../types/common"
21 | import {
22 | CONTENT_PATH,
23 | allContentFilePaths,
24 | getToCForMarkdown,
25 | } from "../../utils/mdxUtils"
26 | import { isLessonCompleted } from "../../utils/machineUtils"
27 |
28 | const NextLessonBtn = dynamic(
29 | () => import("../../components/Lesson/NextLessonBtn"),
30 | {
31 | ssr: false,
32 | }
33 | )
34 |
35 | const CompleteLessonBtn = dynamic(
36 | () => import("../../components/Lesson/CompleteLessonBtn"),
37 | {
38 | ssr: false,
39 | }
40 | )
41 |
42 | const SkipChallenge = dynamic(
43 | () => import("../../components/Lesson/SkipChallenge"),
44 | {
45 | ssr: false,
46 | }
47 | )
48 |
49 | // Custom components/renderers to pass to MDX.
50 | // Since the MDX files aren't loaded by webpack, they have no knowledge of how
51 | // to handle import statements. Instead, you must include components in scope
52 | // here.
53 | const components = {
54 | //a: CustomLink,
55 | // It also works with dynamically-imported components, which is especially
56 | // useful for conditionally loading components for certain routes.
57 | // See the notes in README.md for more details.
58 | //TestComponent: dynamic(() => import('../../components/TestComponent')),
59 | Head,
60 | }
61 |
62 | type Props = {
63 | source: MDXRemoteSerializeResult>
64 | frontMatter: {
65 | [key: string]: any
66 | }
67 | toc: LessonTableOfContents[]
68 | lessonData: {
69 | title: string
70 | slug: string
71 | description: string
72 | status: string
73 | videoURL: string
74 | challenges: MultipleChoiceChallenge[]
75 | }
76 | sectionLessons: []
77 | nextLesson: string
78 | sectionTitle: string
79 | lessonPath: string
80 | coursesJson: object
81 | courses: []
82 | course: string
83 | }
84 |
85 | export default function LessonPage({
86 | source,
87 | toc,
88 | lessonData,
89 | sectionLessons,
90 | nextLesson,
91 | sectionTitle,
92 | lessonPath,
93 | coursesJson,
94 | courses,
95 | course,
96 | }: Props) {
97 | useActor(progressService)
98 |
99 | return (
100 |
105 |
106 |
107 | {lessonData.title} | Testing Next.js Applications with Cypress
108 |
109 |
110 |
111 |
112 |
123 |
124 | {(!lessonData.challenges ||
125 | progressService.state.context.disableChallenges) && (
126 |
131 | )}
132 |
133 | {lessonData.challenges &&
134 | progressService.state.context.disableChallenges == false && (
135 | <>
136 |
141 |
142 |
146 | >
147 | )}
148 |
149 | {lessonData.challenges && (
150 |
151 | )}
152 |
153 | )
154 | }
155 |
156 | export const getStaticProps = async ({ params }) => {
157 | const coursesJson = await fetchCourses()
158 | const courses = Object.keys(coursesJson)
159 | const contentFilePath = path.join(
160 | CONTENT_PATH,
161 | `${params.course}/${params.slug}.mdx`
162 | )
163 | const source = fs.readFileSync(contentFilePath)
164 | const { content, data } = matter(source)
165 | const toc: LessonTableOfContents[] = getToCForMarkdown(content)
166 | const mdxSource = await serialize(content, {
167 | // Optionally pass remark/rehype plugins
168 | mdxOptions: {
169 | remarkPlugins: [],
170 | // @ts-ignore
171 | rehypePlugins: [rehypeSlug, rehypePrism],
172 | },
173 | scope: data,
174 | })
175 | const lessonData = find(
176 | { slug: params.slug },
177 | coursesJson[params.course].lessons
178 | )
179 | const { title, lessons } = coursesJson[params.course]
180 | const nextLessonIndex = findIndex({ slug: params.slug }, lessons) + 1
181 | let nextLesson
182 |
183 | if (nextLessonIndex < lessons.length) {
184 | nextLesson = lessons[nextLessonIndex].slug
185 | } else {
186 | nextLesson = null
187 | }
188 |
189 | return {
190 | props: {
191 | source: mdxSource,
192 | frontMatter: data,
193 | toc,
194 | lessonData,
195 | sectionLessons: lessons,
196 | nextLesson,
197 | sectionTitle: title,
198 | lessonPath: `${params.course}/${params.slug}`,
199 | coursesJson,
200 | courses,
201 | course: params.course,
202 | },
203 | }
204 | }
205 |
206 | export const getStaticPaths = async () => {
207 | const paths = allContentFilePaths
208 | // Remove file extensions for page paths
209 | .map((path) => path.replace(/\.mdx?$/, ""))
210 | // Map the path into the static paths object required by Next.js
211 | .map((filePath) => {
212 | const [course, slug] = filePath.split("/")
213 | return { params: { slug, course } }
214 | })
215 |
216 | return {
217 | paths,
218 | fallback: false,
219 | }
220 | }
221 |
--------------------------------------------------------------------------------
/pages/[course]/index.tsx:
--------------------------------------------------------------------------------
1 | import Head from "next/head"
2 | import Layout from "../../components/Layout"
3 | import CourseHero from "../../components/Course/CourseHero"
4 | import CourseContent from "../../components/Course/CourseContent"
5 | import { progressService } from "../../machines/progressService"
6 | import { fetchCourses } from "../../lib/fetch-courses"
7 |
8 | export default function SectionPage({
9 | title,
10 | lessons,
11 | description,
12 | learnFeatures,
13 | content,
14 | courses,
15 | course,
16 | }) {
17 | return (
18 |
23 |
24 | {title} | Testing Next.js Applications with Cypress
25 |
26 |
27 |
28 |
33 |
40 |
41 | )
42 | }
43 |
44 | export async function getStaticProps({ params }) {
45 | const coursesJson = await fetchCourses()
46 | const { title, lessons, description, learnFeatures } =
47 | coursesJson[params.course]
48 | const courses = Object.keys(coursesJson)
49 |
50 | return {
51 | props: {
52 | lessons,
53 | title,
54 | description,
55 | learnFeatures,
56 | content: coursesJson,
57 | courses,
58 | course: params.course,
59 | },
60 | }
61 | }
62 |
63 | export async function getStaticPaths() {
64 | const coursesJson = await fetchCourses()
65 | const courses = Object.keys(coursesJson)
66 | const paths = courses.map((course) => {
67 | const { title, lessons } = coursesJson[course]
68 | return { params: { course, lessons, title } }
69 | })
70 |
71 | return {
72 | paths,
73 | fallback: false,
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/pages/_app.tsx:
--------------------------------------------------------------------------------
1 | import "tailwindcss/tailwind.css"
2 | import "../styles/global.css"
3 |
4 | function MyApp({ Component, pageProps }) {
5 | return (
6 | <>
7 |
8 | >
9 | )
10 | }
11 |
12 | export default MyApp
13 |
--------------------------------------------------------------------------------
/pages/_document.js:
--------------------------------------------------------------------------------
1 | import { Html, Head, Main, NextScript } from "next/document"
2 |
3 | export default function Document() {
4 | return (
5 |
6 |
7 |
8 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | )
19 | }
20 |
--------------------------------------------------------------------------------
/pages/api/courses.js:
--------------------------------------------------------------------------------
1 | import coursesJson from "../../data/courses.json"
2 |
3 | export default function handler(req, res) {
4 | res.status(200).json(coursesJson)
5 | }
6 |
--------------------------------------------------------------------------------
/pages/api/subscribe.js:
--------------------------------------------------------------------------------
1 | const subscribed = ["john@example.com"]
2 |
3 | export default function handler(req, res) {
4 | if (!req.rawHeaders.includes("application/json")) {
5 | res.status(400).json({
6 | message: `Error: request must be sent as JSON`,
7 | })
8 |
9 | return
10 | }
11 |
12 | if (
13 | req.method === "POST" &&
14 | req.body.email &&
15 | !subscribed.includes(req.body.email)
16 | ) {
17 | res.status(200).json({
18 | message: `Success: ${req.body.email} has been successfully subscribed`,
19 | })
20 |
21 | return
22 | }
23 |
24 | if (
25 | req.method === "POST" &&
26 | req.body.email &&
27 | subscribed.includes(req.body.email)
28 | ) {
29 | res.status(403).json({
30 | message: `Error: ${req.body.email} already exists. Please use a different email address.`,
31 | })
32 |
33 | return
34 | }
35 |
36 | res.status(400).json({
37 | message: "Error: There was an error with your request. Please try again.",
38 | })
39 | }
40 |
--------------------------------------------------------------------------------
/pages/index.tsx:
--------------------------------------------------------------------------------
1 | import Head from "next/head"
2 | import Layout from "../components/Layout"
3 | import HomeHero from "../components/Home/HomeHero"
4 | import HomeFeatures from "../components/Home/HomeFeatures"
5 | import HomeCourses from "../components/Home/HomeCourses"
6 | import { progressService } from "../machines/progressService"
7 | import { fetchCourses } from "../lib/fetch-courses"
8 |
9 | export default function Home({ content, courses }) {
10 | return (
11 |
16 |
17 | Testing Next.js Applications with Cypress
18 |
22 |
23 |
24 |
25 |
26 |
31 |
32 | )
33 | }
34 |
35 | export async function getStaticProps({ params }) {
36 | const coursesJson = await fetchCourses()
37 | const courses = Object.keys(coursesJson)
38 | return {
39 | props: {
40 | content: coursesJson,
41 | courses,
42 | },
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | // If you want to use other PostCSS plugins, see the following:
2 | // https://tailwindcss.com/docs/using-with-preprocessors
3 | module.exports = {
4 | plugins: {
5 | tailwindcss: {},
6 | autoprefixer: {},
7 | },
8 | }
9 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cypress-io/cypress-realworld-testing-course-app/9232930cdb32138b6e693b82a806110a82fa88d4/public/favicon.ico
--------------------------------------------------------------------------------
/public/images/courses/cypress-rwa.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cypress-io/cypress-realworld-testing-course-app/9232930cdb32138b6e693b82a806110a82fa88d4/public/images/courses/cypress-rwa.png
--------------------------------------------------------------------------------
/public/images/courses/store.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cypress-io/cypress-realworld-testing-course-app/9232930cdb32138b6e693b82a806110a82fa88d4/public/images/courses/store.png
--------------------------------------------------------------------------------
/public/images/logo/cypress-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cypress-io/cypress-realworld-testing-course-app/9232930cdb32138b6e693b82a806110a82fa88d4/public/images/logo/cypress-logo.png
--------------------------------------------------------------------------------
/public/images/logo/cypress-logo.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cypress-io/cypress-realworld-testing-course-app/9232930cdb32138b6e693b82a806110a82fa88d4/public/images/logo/cypress-logo.webp
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | # *
2 | User-agent: *
3 | Allow: /
4 |
5 | # Host
6 | Host: https://cypress-realworld-testing-course-app.vercel.app/
7 |
8 | # Sitemaps
9 | Sitemap: https://cypress-realworld-testing-course-app.vercel.app/sitemap.xml
10 |
--------------------------------------------------------------------------------
/public/sitemap.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | https://cypress-realworld-testing-course-app.vercel.app/sitemap-0.xml
4 |
--------------------------------------------------------------------------------
/renovate.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["config:base"],
3 | "baseBranch": "main",
4 | "automerge": false,
5 | "commitMessage": "{{semanticPrefix}}Update {{depName}} to {{newVersion}} 🌟",
6 | "prTitle": "{{semanticPrefix}}{{#if isPin}}Pin{{else}}Update{{/if}} dependency {{depName}} to version {{#if isRange}}{{newVersion}}{{else}}{{#if isMajor}}{{newVersionMajor}}.x{{else}}{{newVersion}}{{/if}}{{/if}} 🌟",
7 | "major": {
8 | "automerge": false
9 | },
10 | "minor": {
11 | "automerge": false
12 | },
13 | "prHourlyLimit": 1,
14 | "updateNotScheduled": false,
15 | "timezone": "America/New_York",
16 | "masterIssue": true,
17 | "schedule": ["every weekend"],
18 | "packageRules": [
19 | {
20 | "packageNames": ["cypress"],
21 | "schedule": ["every weekday"]
22 | },
23 | {
24 | "groupName": "automergeTypesMinor",
25 | "automerge": true,
26 | "major": { "automerge": false },
27 | "matchPackagePatterns": ["^@types/"]
28 | },
29 | {
30 | "groupName": "devDeps",
31 | "description": "automerge minor updates of widely used libraries in devDeps",
32 | "automerge": true,
33 | "matchUpdateTypes": ["minor"],
34 | "matchDepTypes": ["devDependencies"]
35 | },
36 | {
37 | "groupName": "Typescript",
38 | "matchPackageNames": ["typescript", "ts-node", "tsconfig-paths"]
39 | },
40 | {
41 | "groupName": "React",
42 | "matchPackageNames": ["react", "react-dom"]
43 | },
44 | {
45 | "groupName": "Tailwind",
46 | "matchPackagePatterns": [
47 | "@headlessui/react",
48 | "@heroicons/react",
49 | "tailwindcss",
50 | "postcss",
51 | "autoprefixer"
52 | ]
53 | },
54 | {
55 | "groupName": "Next",
56 | "matchPackageNames": ["next"],
57 | "matchPackagePatterns": ["^next-", "^@zeit/next-"]
58 | },
59 | {
60 | "groupName": "Xstate",
61 | "matchPackageNames": ["xstate", "@xstate/react"]
62 | },
63 | {
64 | "groupName": "ESLint and Prettier",
65 | "matchPackageNames": ["eslint", "prettier"],
66 | "matchPackagePatterns": ["^eslint-config-", "^eslint-plugin-"]
67 | },
68 | {
69 | "groupName": "Misc FE libs",
70 | "matchPackageNames": ["gray-matter", "slugify", "markdown-toc-unlazy"]
71 | },
72 | {
73 | "groupName": "Misc dev tooling",
74 | "matchPackageNames": ["husky", "mocha", "chai", "pretty-quick"]
75 | }
76 | ]
77 | }
78 |
--------------------------------------------------------------------------------
/styles/global.css:
--------------------------------------------------------------------------------
1 | /*
2 | Lesson Content
3 | */
4 |
5 | .lesson-content h1 {
6 | @apply mt-2 block text-3xl text-center leading-8 font-extrabold tracking-tight text-gray-900;
7 | }
8 |
9 | /*
10 | Modal for screenshots
11 | */
12 |
13 | #modal {
14 | display: none;
15 | position: fixed;
16 | z-index: 99;
17 | left: 0;
18 | top: 0;
19 | width: 100%;
20 | height: 100%;
21 | overflow: auto;
22 | background-color: rgba(0, 0, 0, 0.5);
23 | }
24 |
25 | /* Modal Content/Box */
26 | .modal-content {
27 | background-color: white;
28 | margin: 20px auto;
29 | padding: 20px;
30 | border: 1px solid #888;
31 | width: 80%;
32 | }
33 |
34 | /* The Close Button */
35 | .close {
36 | color: #aaa;
37 | float: right;
38 | font-size: 35px;
39 | font-weight: bold;
40 | }
41 |
42 | .close:hover,
43 | .close:focus {
44 | color: black;
45 | text-decoration: none;
46 | cursor: pointer;
47 | }
48 |
49 | /*
50 | Removes backticks `` from being redered
51 | for inline code in markdown files
52 | */
53 | .prose code::before,
54 | .prose code::after {
55 | content: "";
56 | }
57 |
58 | .lesson-content img {
59 | cursor: pointer;
60 | }
61 |
62 | /*
63 | Doc Search
64 | */
65 |
66 | input[type='search']:focus {
67 | --tw-ring-color: none;
68 | }
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | const defaultTheme = require("tailwindcss/defaultTheme")
2 |
3 | module.exports = {
4 | content: [
5 | "./pages/**/*.{js,ts,jsx,tsx}",
6 | "./components/**/*.{js,ts,jsx,tsx}",
7 | ],
8 | theme: {
9 | extend: {
10 | fontFamily: {
11 | sans: ["Inter var", ...defaultTheme.fontFamily.sans],
12 | },
13 | colors: {
14 | gray: {
15 | 50: "#f3f4fa",
16 | 100: "#e1e3ed",
17 | 200: "#d0d2e0",
18 | 300: "#bfc2d4",
19 | 400: "#afb3c7",
20 | 500: "#9095ad",
21 | 600: "#747994",
22 | 700: "#5a5f7a",
23 | 800: "#434861",
24 | 900: "#2e3247",
25 | 1000: "#1b1e2e",
26 | },
27 | red: {
28 | 50: "#fbefef",
29 | 100: "#fad9d9",
30 | 200: "#f8c4c4",
31 | 300: "#f59a9a",
32 | 400: "#e4575a",
33 | 500: "#c62b34",
34 | 600: "#9f1321",
35 | 700: "#7a0718",
36 | 800: "#5e0216",
37 | 900: "#4f0016",
38 | 1000: "#490018",
39 | },
40 | orange: {
41 | 50: "#fcf1e0",
42 | 100: "#fbe3c3",
43 | 200: "#f9d4a7",
44 | 300: "#f6b66f",
45 | 400: "#e5771a",
46 | 500: "#c74f00",
47 | 600: "#9f3500",
48 | 700: "#792100",
49 | 800: "#5d1300",
50 | 900: "#4e0b00",
51 | 1000: "#470600",
52 | },
53 | jade: {
54 | 50: "#def8ed",
55 | 100: "#b9eed8",
56 | 200: "#96e4c4",
57 | 300: "#58d09e",
58 | 400: "#0aa767",
59 | 500: "#00804c",
60 | 600: "#005e39",
61 | 700: "#00432a",
62 | 800: "#003220",
63 | 900: "#00291b",
64 | 1000: "#00261a",
65 | },
66 | indigo: {
67 | 50: "#eff1fd",
68 | 100: "#dce2fb",
69 | 200: "#cad2fa",
70 | 300: "#a6b3f7",
71 | 400: "#6b7de8",
72 | 500: "#4356cf",
73 | 600: "#2a3aae",
74 | 700: "#1b288e",
75 | 800: "#131d76",
76 | 900: "#0f1767",
77 | 1000: "#0e1461",
78 | },
79 | purple: {
80 | 50: "#f5f0fb",
81 | 100: "#e9ddfa",
82 | 200: "#decbf8",
83 | 300: "#c8a7f5",
84 | 400: "#a06ce4",
85 | 500: "#7f43c9",
86 | 600: "#632aa6",
87 | 700: "#4b1a83",
88 | 800: "#3b1268",
89 | 900: "#320e58",
90 | 1000: "#2f0c52",
91 | },
92 | },
93 | },
94 | },
95 | plugins: [
96 | // https://github.com/tailwindlabs/tailwindcss-aspect-ratio
97 | require("@tailwindcss/aspect-ratio"),
98 | // https://github.com/tailwindlabs/tailwindcss-forms
99 | require("@tailwindcss/forms"),
100 | // https://github.com/tailwindlabs/tailwindcss-typography
101 | require("@tailwindcss/typography"),
102 | ],
103 | }
104 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": ".",
4 | "paths": {
5 | "*": [
6 | "types/*.d.ts"
7 | ]
8 | },
9 | "target": "es5",
10 | "lib": [
11 | "dom",
12 | "dom.iterable",
13 | "esnext"
14 | ],
15 | "allowJs": true,
16 | "skipLibCheck": true,
17 | "strict": false,
18 | "forceConsistentCasingInFileNames": true,
19 | "noEmit": true,
20 | "esModuleInterop": true,
21 | "module": "esnext",
22 | "moduleResolution": "node",
23 | "resolveJsonModule": true,
24 | "isolatedModules": true,
25 | "jsx": "preserve",
26 | "incremental": true
27 | },
28 | "ts-node": {
29 | // these options are overrides used only by ts-node
30 | // same as our --compilerOptions flag and our TS_NODE_COMPILER_OPTIONS environment variable
31 | "compilerOptions": {
32 | "module": "commonjs"
33 | }
34 | },
35 | "include": [
36 | "next-env.d.ts",
37 | "**/*.ts",
38 | "**/*.tsx"
39 | ],
40 | "exclude": [
41 | "**/node_modules",
42 | "**/.*/"
43 | ]
44 | }
45 |
--------------------------------------------------------------------------------
/types/common.d.ts:
--------------------------------------------------------------------------------
1 | export type LessonTableOfContents = {
2 | content: string
3 | slug: string
4 | lvl: number
5 | current?: boolean
6 | }
7 |
8 | export interface ChallengeAnswer {
9 | id: string
10 | answeredCorrectly?: boolean
11 | skipped?: boolean
12 | }
13 | export interface EventPayload {
14 | type: string
15 | id: string
16 | challengeIndex: number
17 | }
18 |
19 | export interface MultipleChoicePayload extends EventPayload {
20 | userAnswerIndex: number
21 | }
22 |
23 | export interface ProgressContext {
24 | sectionsCompleted: string[]
25 | lessons: object[]
26 | disableChallenges: boolean
27 | }
28 |
29 | export interface Challenge {
30 | challengeType: string
31 | question: string
32 | }
33 |
34 | export interface MultipleChoiceChallenge extends Challenge {
35 | answers: []
36 | correctAnswerIndex: string
37 | }
38 |
--------------------------------------------------------------------------------
/utils/machineUtils.ts:
--------------------------------------------------------------------------------
1 | import { find, findIndex, get, gte } from "lodash/fp"
2 |
3 | export const getAllLessons = (coursesJson: object, lessonPath) => {
4 | const [sectionSlug] = lessonPath.split("/")
5 | const course = getCourse(coursesJson, sectionSlug)
6 | return get("lessons", course)
7 | }
8 |
9 | export const findLesson = (coursesJson: object, lessonPath: string) => {
10 | const [, lessonSlug] = lessonPath.split("/")
11 | const lessons = getAllLessons(coursesJson, lessonPath)
12 | return find({ slug: lessonSlug }, lessons)
13 | }
14 |
15 | export const getChallenge = (
16 | coursesJson: object,
17 | lessonPath: string,
18 | challengeIndex: number
19 | ) => {
20 | const lesson = findLesson(coursesJson, lessonPath)
21 | return lesson.challenges[challengeIndex]
22 | }
23 |
24 | export const getLessonIndex = (coursesJson: object, lessonPath: string) => {
25 | const [, lessonSlug] = lessonPath.split("/")
26 | const lessons = getAllLessons(coursesJson, lessonPath)
27 | return findIndex({ slug: lessonSlug }, lessons)
28 | }
29 |
30 | export const getCourse = (coursesJson: object, lessonPath: string) => {
31 | const [sectionSlug] = lessonPath.split("/")
32 | return coursesJson[sectionSlug]
33 | }
34 |
35 | export const isLessonCompleted = (progressService, lessonPath) => {
36 | return gte(
37 | findIndex(
38 | { id: lessonPath, status: "completed" },
39 | progressService.state.context.lessons
40 | ),
41 | 0
42 | )
43 | }
44 |
45 | export const isSectionCompleted = (sectionsCompleted, sectionSlug) => {
46 | return sectionsCompleted.includes(sectionSlug)
47 | }
48 |
--------------------------------------------------------------------------------
/utils/mdxUtils.ts:
--------------------------------------------------------------------------------
1 | import fs from "fs"
2 | import path from "path"
3 | import glob from "glob"
4 | import toc from "markdown-toc-unlazy"
5 | import { LessonTableOfContents } from "../types/common"
6 |
7 | export const CONTENT_PATH = path.join(process.cwd(), "content/courses")
8 |
9 | export const contentFilePaths = fs
10 | .readdirSync(CONTENT_PATH)
11 | // Only include md(x) files
12 | .filter((path) => /\.mdx?$/.test(path))
13 |
14 | export const allContentFilePaths = glob
15 | .sync("content/courses/**/*")
16 | .filter((path) => /\.mdx?$/.test(path))
17 | .map((path) => path.replace(/^content\/courses\//, ""))
18 |
19 | export const getToCForMarkdown = (markdown): LessonTableOfContents[] =>
20 | toc(markdown).json
21 |
--------------------------------------------------------------------------------