├── public ├── favicon.ico └── images │ ├── developer_activity.png │ └── cypress-logo.svg ├── postcss.config.js ├── pages ├── _app.js ├── api │ └── posts.js ├── posts │ └── [id].js └── index.js ├── components ├── layout.module.css ├── date.js ├── layout.js └── Footer.js ├── tailwind.config.js ├── cypress.config.js ├── .gitignore ├── styles ├── global.css └── utils.module.css ├── README.md ├── posts ├── pre-rendering.md └── ssg-ssr.md ├── cypress ├── support │ ├── e2e.js │ ├── commands.js │ └── Answers │ │ └── commands.js ├── e2e │ ├── Practice │ │ ├── cypress-commands.cy.js │ │ ├── debug-failing-tests.cy.js │ │ ├── cypress-methods.cy.js │ │ ├── network-requests.cy.js │ │ └── just-javascript.cy.js │ └── Answers │ │ ├── debug-failing-tests.cy.js │ │ ├── cypress-commands.cy.js │ │ ├── cypress-methods.cy.js │ │ ├── network-requests.cy.js │ │ └── just-javascript.cy.js ├── plugins │ └── index.js └── fixtures │ └── posts.json ├── package.json ├── .circleci └── config.yml └── lib └── posts.js /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cypress-io/cypress-realworld-testing-blog/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /public/images/developer_activity.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cypress-io/cypress-realworld-testing-blog/HEAD/public/images/developer_activity.png -------------------------------------------------------------------------------- /pages/_app.js: -------------------------------------------------------------------------------- 1 | import '../styles/global.css' 2 | import 'tailwindcss/tailwind.css' 3 | 4 | export default function App({ Component, pageProps }) { 5 | return 6 | } 7 | -------------------------------------------------------------------------------- /components/layout.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | max-width: 36rem; 3 | padding: 0 1rem; 4 | margin: 3rem auto 6rem; 5 | } 6 | 7 | .header { 8 | display: flex; 9 | flex-direction: column; 10 | align-items: center; 11 | } 12 | 13 | .backToHome { 14 | margin: 3rem 0 0; 15 | } 16 | -------------------------------------------------------------------------------- /components/date.js: -------------------------------------------------------------------------------- 1 | import { parseISO, format } from "date-fns"; 2 | 3 | export default function Date({ dateString, index }) { 4 | const date = parseISO(dateString); 5 | return ( 6 | 9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | purge: ['./pages/**/*.{js,ts,jsx,tsx}', './components/**/*.{js,ts,jsx,tsx}'], 3 | darkMode: false, // or 'media' or 'class' 4 | theme: { 5 | extend: {}, 6 | }, 7 | variants: { 8 | extend: {}, 9 | }, 10 | plugins: [ 11 | require('@tailwindcss/typography'), 12 | ], 13 | } 14 | -------------------------------------------------------------------------------- /pages/api/posts.js: -------------------------------------------------------------------------------- 1 | import { getAllPostIds, getPostData } from '../../lib/posts' 2 | 3 | export default async function handler(req, res) { 4 | const posts = [] 5 | const postIds = getAllPostIds() 6 | postIds.map((postID) => { 7 | posts.push(getPostData(postID.params.id)) 8 | }) 9 | 10 | res.status(200).json(await Promise.all(posts)) 11 | } 12 | -------------------------------------------------------------------------------- /cypress.config.js: -------------------------------------------------------------------------------- 1 | const { defineConfig } = require('cypress') 2 | 3 | module.exports = defineConfig({ 4 | e2e: { 5 | // We've imported your old cypress plugins here. 6 | // You may want to clean this up later by importing these. 7 | setupNodeEvents(on, config) { 8 | return require('./cypress/plugins/index.js')(on, config) 9 | }, 10 | }, 11 | }) 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | .env* 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # cypress 28 | cypress/videos 29 | cypress/screenshots -------------------------------------------------------------------------------- /styles/global.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | padding: 0; 4 | margin: 0; 5 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, 6 | Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; 7 | line-height: 1.6; 8 | font-size: 18px; 9 | } 10 | 11 | * { 12 | box-sizing: border-box; 13 | } 14 | 15 | a { 16 | color: #0070f3; 17 | text-decoration: none; 18 | } 19 | 20 | a:hover { 21 | text-decoration: underline; 22 | } 23 | 24 | img { 25 | max-width: 100%; 26 | display: block; 27 | } 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Real World Testing with Cypress Blog 2 | 3 | A Next.js Blog for the Real World Testing with Cypress Curriculum. 4 | 5 | ## Installation 6 | 7 | ```bash 8 | yarn install 9 | ``` 10 | 11 | Start the local dev server. 12 | 13 | ```bash 14 | yarn dev 15 | ``` 16 | 17 | While the dev server is running, in a separate terminal window, run the following command to launch Cypress. 18 | 19 | ```bash 20 | yarn cypress:open 21 | ``` 22 | 23 | ## Pratice Tests & Answers 24 | 25 | The practice tests are located in `cypress/integration/Practice` 26 | 27 | The answers are located in `cypress/integration/Answers` 28 | 29 | --- 30 | 31 | This repo is a fork of the [Next.js Learn Starter](https://github.com/vercel/next-learn-starter/) 32 | -------------------------------------------------------------------------------- /posts/pre-rendering.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Two Forms of Pre-rendering" 3 | date: "2020-01-01" 4 | --- 5 | 6 | Next.js has two forms of pre-rendering: **Static Generation** and **Server-side Rendering**. The difference is in **when** it generates the HTML for a page. 7 | 8 | - **Static Generation** is the pre-rendering method that generates the HTML at **build time**. The pre-rendered HTML is then _reused_ on each request. 9 | - **Server-side Rendering** is the pre-rendering method that generates the HTML on **each request**. 10 | 11 | Importantly, Next.js lets you **choose** which pre-rendering form to use for each page. You can create a "hybrid" Next.js app by using Static Generation for most pages and using Server-side Rendering for others. -------------------------------------------------------------------------------- /cypress/support/e2e.js: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/index.js is processed and 3 | // loaded automatically before your test files. 4 | // 5 | // This is a great place to put global configuration and 6 | // behavior that modifies Cypress. 7 | // 8 | // You can change the location of this file or turn off 9 | // automatically serving support files with the 10 | // 'supportFile' configuration option. 11 | // 12 | // You can read more here: 13 | // https://on.cypress.io/configuration 14 | // *********************************************************** 15 | 16 | // Import commands.js using ES2015 syntax: 17 | import "./commands"; 18 | import "./Answers/commands"; 19 | 20 | // Alternatively you can use CommonJS syntax: 21 | // require('./commands') 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "my-app", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "cypress:open": "cypress open", 10 | "cypress:run": "cypress run" 11 | }, 12 | "dependencies": { 13 | "@headlessui/react": "^1.4.1", 14 | "@heroicons/react": "^1.0.4", 15 | "date-fns": "^2.11.1", 16 | "gray-matter": "^4.0.2", 17 | "next": "^11.0.0", 18 | "react": "17.0.2", 19 | "react-dom": "17.0.2", 20 | "remark": "^12.0.0", 21 | "remark-html": "^12.0.0" 22 | }, 23 | "devDependencies": { 24 | "@tailwindcss/typography": "^0.4.1", 25 | "autoprefixer": "^10.3.3", 26 | "cypress": "^10.4.0", 27 | "postcss": "^8.3.6", 28 | "tailwindcss": "^2.2.9" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /cypress/e2e/Practice/cypress-commands.cy.js: -------------------------------------------------------------------------------- 1 | describe("Custom Cypress Commands", () => { 2 | it("uses cy.getAllPosts()to retrieve all posts from the /api/posts endpoint", () => { 3 | // Create a custom Cypress Command called 'getAllPosts' which uses 4 | // cy.request() to get all of the posts from the /api/posts endpoint 5 | // Then use this custom command in this test to assert that the length of the posts 6 | // returned is equal to 2 7 | }); 8 | 9 | it("uses cy.getFirstPost() to retrieve the first post from the /api/posts endpoint", () => { 10 | // Create a custom Cypress Command called 'getFirstPost' which uses 11 | // cy.request() to get the first post from the /api/posts endpoint 12 | // Then use this custom command in this test to assert that the id of the first post 13 | // is equal to 'pre-rendering' 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /cypress/plugins/index.js: -------------------------------------------------------------------------------- 1 | /// 2 | // *********************************************************** 3 | // This example plugins/index.js can be used to load plugins 4 | // 5 | // You can change the location of this file or turn off loading 6 | // the plugins file with the 'pluginsFile' configuration option. 7 | // 8 | // You can read more here: 9 | // https://on.cypress.io/plugins-guide 10 | // *********************************************************** 11 | 12 | // This function is called when a project is opened or re-opened (e.g. due to 13 | // the project's config changing) 14 | 15 | /** 16 | * @type {Cypress.PluginConfig} 17 | */ 18 | // eslint-disable-next-line no-unused-vars 19 | module.exports = (on, config) => { 20 | // `on` is used to hook into various events Cypress emits 21 | // `config` is the resolved Cypress config 22 | } 23 | -------------------------------------------------------------------------------- /styles/utils.module.css: -------------------------------------------------------------------------------- 1 | .heading2Xl { 2 | font-size: 2.5rem; 3 | line-height: 1.2; 4 | font-weight: 800; 5 | letter-spacing: -0.05rem; 6 | margin: 1rem 0; 7 | } 8 | 9 | .headingXl { 10 | font-size: 2rem; 11 | line-height: 1.3; 12 | font-weight: 800; 13 | letter-spacing: -0.05rem; 14 | margin: 1rem 0; 15 | } 16 | 17 | .headingLg { 18 | font-size: 1.5rem; 19 | line-height: 1.4; 20 | margin: 1rem 0; 21 | } 22 | 23 | .headingMd { 24 | font-size: 1.2rem; 25 | line-height: 1.5; 26 | } 27 | 28 | .borderCircle { 29 | border-radius: 9999px; 30 | } 31 | 32 | .colorInherit { 33 | color: inherit; 34 | } 35 | 36 | .padding1px { 37 | padding-top: 1px; 38 | } 39 | 40 | .list { 41 | list-style: none; 42 | padding: 0; 43 | margin: 0; 44 | } 45 | 46 | .listItem { 47 | margin: 0 0 1.25rem; 48 | } 49 | 50 | .lightText { 51 | color: #666; 52 | } 53 | -------------------------------------------------------------------------------- /cypress/support/commands.js: -------------------------------------------------------------------------------- 1 | // *********************************************** 2 | // This example commands.js shows you how to 3 | // create various custom commands and overwrite 4 | // existing commands. 5 | // 6 | // For more comprehensive examples of custom 7 | // commands please read more here: 8 | // https://on.cypress.io/custom-commands 9 | // *********************************************** 10 | // 11 | // 12 | // -- This is a parent command -- 13 | // Cypress.Commands.add('login', (email, password) => { ... }) 14 | // 15 | // 16 | // -- This is a child command -- 17 | // Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... }) 18 | // 19 | // 20 | // -- This is a dual command -- 21 | // Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... }) 22 | // 23 | // 24 | // -- This will overwrite an existing command -- 25 | // Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... }) 26 | -------------------------------------------------------------------------------- /cypress/e2e/Answers/debug-failing-tests.cy.js: -------------------------------------------------------------------------------- 1 | /* 2 | All of these tests are failing and it is your job to debug them 3 | to find out why. 4 | */ 5 | describe("Debug Failing Tests", () => { 6 | beforeEach(() => { 7 | cy.visit("http://localhost:3000"); 8 | }); 9 | 10 | it("displays the correct header text", () => { 11 | cy.get('[data-test="home-header"]').contains("Real World Testing Blog"); 12 | }); 13 | 14 | it("the post links on the homepage link to the correct posts", () => { 15 | cy.get('[data-test="post-link-0"]').click(); 16 | cy.location("pathname").should("eq", "/posts/ssg-ssr"); 17 | }); 18 | 19 | it("displays all of the posts on the homepage", () => { 20 | cy.get('[data-test="posts-list"]').within(($post) => { 21 | cy.get("a").should("have.length", 2); 22 | }); 23 | }); 24 | 25 | it("/api/posts returns a status code of 200", () => { 26 | cy.request("GET", "http://localhost:3000/api/posts").then((response) => { 27 | expect(response.status).to.eq(200); 28 | }); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /cypress/e2e/Practice/debug-failing-tests.cy.js: -------------------------------------------------------------------------------- 1 | /* 2 | All of these tests are failing and it is your job to debug them 3 | to find out why. 4 | */ 5 | describe("Debug Failing Tests", () => { 6 | beforeEach(() => { 7 | cy.visit("http://localhost:3000"); 8 | }); 9 | 10 | it("displays the correct header text", () => { 11 | cy.get('[data-test="home-header"]').contains("real world testing blog"); 12 | }); 13 | 14 | it("the post links on the homepage link to the correct posts", () => { 15 | cy.get('[data-test="post-link-0"]').click(); 16 | cy.location("pathname").should("eq", "/posts/pre-rendering"); 17 | }); 18 | 19 | it("displays all of the posts on the homepage", () => { 20 | cy.get('[data-test="posts-list"]').within(($post) => { 21 | cy.get("a").should("have.length", 1); 22 | }); 23 | }); 24 | 25 | it("/api/posts returns a status code of 201", () => { 26 | cy.request("GET", "http://localhost:3000/api/posts").then((response) => { 27 | expect(response.status).to.eq(201); 28 | }); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | 3 | orbs: 4 | cypress: cypress-io/cypress@1.28.0 5 | 6 | executors: 7 | with-chrome-and-firefox: 8 | docker: 9 | - image: "cypress/browsers:node14.17.0-chrome88-ff89" 10 | resource_class: medium+ 11 | 12 | workflows: 13 | build-and-test: 14 | jobs: 15 | # Setup 16 | # 1. Install Cypress 17 | - cypress/install: 18 | name: "Setup Linux" 19 | yarn: true 20 | executor: with-chrome-and-firefox 21 | build: "yarn build" 22 | post-steps: 23 | - run: 24 | name: Print machine info ℹ️ 25 | command: yarn cypress info 26 | 27 | # Run E2E tests in Chrome 28 | - cypress/run: 29 | name: "UI Tests - Chrome" 30 | browser: chrome 31 | spec: cypress/integration/Answers/* 32 | executor: with-chrome-and-firefox 33 | wait-on: "http://localhost:3000" 34 | yarn: true 35 | start: yarn start 36 | requires: 37 | - Setup Linux 38 | -------------------------------------------------------------------------------- /cypress/e2e/Answers/cypress-commands.cy.js: -------------------------------------------------------------------------------- 1 | describe("Custom Cypress Commands", () => { 2 | it("uses cy.getAllPosts()to retrieve all posts from the /api/posts endpoint", () => { 3 | // Create a custom Cypress Command called 'getAllPosts' which uses 4 | // cy.request() to get all of the posts from the /api/posts endpoint 5 | // Then use this custom command in this test to assert that the length of the posts 6 | // returned is equal to 2 7 | 8 | cy.getAllPosts().then((post) => { 9 | cy.wrap(post).its("length").should("eq", 2); 10 | }); 11 | }); 12 | 13 | it("uses cy.getFirstPost() to retrieve the first post from the /api/posts endpoint", () => { 14 | // Create a custom Cypress Command called 'getFirstPost' which uses 15 | // cy.request() to get the first post from the /api/posts endpoint 16 | // Then use this custom command in this test to assert that the id of the first post 17 | // is equal to 'pre-rendering' 18 | 19 | cy.getFirstPost().then((post) => { 20 | cy.wrap(post).its("id").should("eq", "pre-rendering"); 21 | }); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /posts/ssg-ssr.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "When to Use Static Generation v.s. Server-side Rendering" 3 | date: "2020-01-02" 4 | --- 5 | 6 | We recommend using **Static Generation** (with and without data) whenever possible because your page can be built once and served by CDN, which makes it much faster than having a server render the page on every request. 7 | 8 | You can use Static Generation for many types of pages, including: 9 | 10 | - Marketing pages 11 | - Blog posts 12 | - E-commerce product listings 13 | - Help and documentation 14 | 15 | You should ask yourself: "Can I pre-render this page **ahead** of a user's request?" If the answer is yes, then you should choose Static Generation. 16 | 17 | On the other hand, Static Generation is **not** a good idea if you cannot pre-render a page ahead of a user's request. Maybe your page shows frequently updated data, and the page content changes on every request. 18 | 19 | In that case, you can use **Server-Side Rendering**. It will be slower, but the pre-rendered page will always be up-to-date. Or you can skip pre-rendering and use client-side JavaScript to populate data. -------------------------------------------------------------------------------- /pages/posts/[id].js: -------------------------------------------------------------------------------- 1 | import Layout from '../../components/layout' 2 | import { getAllPostIds, getPostData } from '../../lib/posts' 3 | import Head from 'next/head' 4 | import Date from '../../components/date' 5 | import utilStyles from '../../styles/utils.module.css' 6 | 7 | export default function Post({ postData }) { 8 | return ( 9 | 10 | 11 | {postData.title} 12 | 13 |
14 |

{postData.title}

15 |
16 | 17 |
18 |
19 |
20 |
21 | ) 22 | } 23 | 24 | export async function getStaticPaths() { 25 | const paths = getAllPostIds() 26 | return { 27 | paths, 28 | fallback: false 29 | } 30 | } 31 | 32 | export async function getStaticProps({ params }) { 33 | const postData = await getPostData(params.id) 34 | return { 35 | props: { 36 | postData 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /cypress/e2e/Practice/cypress-methods.cy.js: -------------------------------------------------------------------------------- 1 | describe("Important Cypress Methods", () => { 2 | it("uses cy.request() and cy.invoke() to slice the posts array", () => { 3 | // Use cy.request() to get all of the posts from the /api/posts endpoint 4 | // Then use cy.invoke() to 'slice' the response body by 1. 5 | // Hint: you will need to use cy.wrap() around the response.body before calling .invoke() 6 | // https://docs.cypress.io/api/commands/wrap 7 | }); 8 | 9 | it("uses cy.request() and cy.its() to get the first posts ID", () => { 10 | // Use cy.request() to get all of the posts from the /api/posts endpoint 11 | // Then use cy.its() to get the first post and also its id 12 | // Write and assertion that the first posts id === 'pre-rendering' 13 | // Hint: you will need to use cy.wrap() around the response.body before calling .invoke() 14 | // You will also need to use .its() twice. 15 | // https://docs.cypress.io/api/commands/wrap 16 | }); 17 | 18 | it("uses cy.within() to get the

inside of the
", () => { 19 | // Use cy.within() to get the

nested inside of the
element 20 | // Then assert that the

contains the correct text. 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /cypress/support/Answers/commands.js: -------------------------------------------------------------------------------- 1 | // *********************************************** 2 | // This example commands.js shows you how to 3 | // create various custom commands and overwrite 4 | // existing commands. 5 | // 6 | // For more comprehensive examples of custom 7 | // commands please read more here: 8 | // https://on.cypress.io/custom-commands 9 | // *********************************************** 10 | // 11 | // 12 | // -- This is a parent command -- 13 | // Cypress.Commands.add('login', (email, password) => { ... }) 14 | // 15 | // 16 | // -- This is a child command -- 17 | // Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... }) 18 | // 19 | // 20 | // -- This is a dual command -- 21 | // Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... }) 22 | // 23 | // 24 | // -- This will overwrite an existing command -- 25 | // Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... }) 26 | 27 | Cypress.Commands.add("getAllPosts", () => { 28 | cy.request("GET", "http://localhost:3000/api/posts").then((response) => { 29 | return cy.wrap(response.body); 30 | }); 31 | }); 32 | 33 | Cypress.Commands.add("getFirstPost", () => { 34 | cy.request("GET", "http://localhost:3000/api/posts").then((response) => { 35 | return cy.wrap(response.body).its(0); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /pages/index.js: -------------------------------------------------------------------------------- 1 | import Head from "next/head"; 2 | import Layout, { siteTitle } from "../components/layout"; 3 | import utilStyles from "../styles/utils.module.css"; 4 | import { getSortedPostsData } from "../lib/posts"; 5 | import Link from "next/link"; 6 | import Date from "../components/date"; 7 | 8 | export default function Home({ allPostsData }) { 9 | fetch("http://localhost:3000/api/posts") 10 | .then((response) => response.json()) 11 | .then((data) => data); 12 | 13 | return ( 14 | 15 | 16 | {siteTitle} 17 | 18 |
19 |
    20 | {allPostsData.map(({ id, date, title }, index) => ( 21 |
  • 22 | 23 | {title} 24 | 25 |
    26 | 27 | 28 | 29 |
  • 30 | ))} 31 |
32 |
33 |
34 | ); 35 | } 36 | 37 | export async function getStaticProps() { 38 | const allPostsData = getSortedPostsData(); 39 | return { 40 | props: { 41 | allPostsData, 42 | }, 43 | }; 44 | } 45 | -------------------------------------------------------------------------------- /cypress/e2e/Answers/cypress-methods.cy.js: -------------------------------------------------------------------------------- 1 | describe("Important Cypress Methods", () => { 2 | it("uses cy.request() and cy.invoke() to slice the posts array", () => { 3 | // Use cy.request() to get all of the posts from the /api/posts endpoint 4 | // Then use cy.invoke() to 'slice' the response body by 1. 5 | // Hint: you will need to use cy.wrap() around the response.body before calling .invoke() 6 | // https://docs.cypress.io/api/commands/wrap 7 | 8 | cy.request("GET", "http://localhost:3000/api/posts").then((response) => { 9 | cy.wrap(response.body).invoke("slice", 0, 1); 10 | }); 11 | }); 12 | 13 | it("uses cy.request() and cy.its() to get the first posts ID", () => { 14 | // Use cy.request() to get all of the posts from the /api/posts endpoint 15 | // Then use cy.its() to get the first post and also its id 16 | // Write and assertion that the first posts id === 'pre-rendering' 17 | // Hint: you will need to use cy.wrap() around the reponse.body before calling .invoke() 18 | // You will also need to use .its() twice. 19 | // https://docs.cypress.io/api/commands/wrap 20 | 21 | cy.request("GET", "http://localhost:3000/api/posts").then((response) => { 22 | cy.wrap(response.body).its(0).its("id").should("eq", "pre-rendering"); 23 | }); 24 | }); 25 | 26 | it("uses cy.within() to get the

inside of the
", () => { 27 | // Use cy.within() to get the

nested inside of the
element 28 | // Then assert that the

contains the correct text. 29 | 30 | cy.visit("http://localhost:3000"); 31 | 32 | cy.get("header").within(($header) => { 33 | cy.get("h1").contains("Real World Testing Blog"); 34 | }); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /cypress/e2e/Practice/network-requests.cy.js: -------------------------------------------------------------------------------- 1 | /* 2 | It is very helpful to use console.log() to log out the response data 3 | in order to see the data you are working with. 4 | 5 | You can also click on the request in the Cypress Command Log for an even 6 | better experience. 7 | */ 8 | describe("Network Requests", () => { 9 | beforeEach(() => { 10 | cy.intercept("GET", "http://localhost:3000/api/posts", (req) => { 11 | // this is to disable browser caching 12 | // https://glebbahmutov.com/blog/cypress-intercept-problems/ 13 | delete req.headers["if-none-match"]; 14 | }).as("posts"); 15 | 16 | cy.visit("http://localhost:3000"); 17 | }); 18 | 19 | it("/api/posts returns a status code of 200", () => { 20 | // Write an assertion that the route '/api/posts' 21 | // returns a status code of 200 22 | // Hint: You will need to use cy.request() 23 | // https://docs.cypress.io/api/commands/request 24 | }); 25 | 26 | it("/api/posts returns the correct number of posts", () => { 27 | // Write an assertion that the route '/api/posts' 28 | // returns the correct number of posts. 29 | }); 30 | 31 | it("the posts.json fixture returns the correct number of posts", () => { 32 | // Write an assertion that the route '/api/posts' 33 | // returns the correct number of posts. 34 | // There are 25 total posts in the fixture 35 | // Hint: You will need to use cy.fixture() 36 | // https://docs.cypress.io/api/commands/fixture 37 | }); 38 | 39 | it("intercepts /api/posts and returns the correct number of posts", () => { 40 | // Wait upon the @posts intercept that happens in the beforeEach() 41 | // and assert that the response contains the correct number of posts 42 | // Hint: you will need to cy.wait() to wait upon the @posts alias. 43 | // https://docs.cypress.io/api/commands/wait 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /lib/posts.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import path from 'path' 3 | import matter from 'gray-matter' 4 | import remark from 'remark' 5 | import html from 'remark-html' 6 | 7 | const postsDirectory = path.join(process.cwd(), 'posts') 8 | 9 | export function getSortedPostsData() { 10 | // Get file names under /posts 11 | const fileNames = fs.readdirSync(postsDirectory) 12 | const allPostsData = fileNames.map(fileName => { 13 | // Remove ".md" from file name to get id 14 | const id = fileName.replace(/\.md$/, '') 15 | 16 | // Read markdown file as string 17 | const fullPath = path.join(postsDirectory, fileName) 18 | const fileContents = fs.readFileSync(fullPath, 'utf8') 19 | 20 | // Use gray-matter to parse the post metadata section 21 | const matterResult = matter(fileContents) 22 | 23 | // Combine the data with the id 24 | return { 25 | id, 26 | ...matterResult.data 27 | } 28 | }) 29 | // Sort posts by date 30 | return allPostsData.sort((a, b) => { 31 | if (a.date < b.date) { 32 | return 1 33 | } else { 34 | return -1 35 | } 36 | }) 37 | } 38 | 39 | export function getAllPostIds() { 40 | const fileNames = fs.readdirSync(postsDirectory) 41 | return fileNames.map(fileName => { 42 | return { 43 | params: { 44 | id: fileName.replace(/\.md$/, '') 45 | } 46 | } 47 | }) 48 | } 49 | 50 | export async function getPostData(id) { 51 | const fullPath = path.join(postsDirectory, `${id}.md`) 52 | const fileContents = fs.readFileSync(fullPath, 'utf8') 53 | 54 | // Use gray-matter to parse the post metadata section 55 | const matterResult = matter(fileContents) 56 | 57 | // Use remark to convert markdown into HTML string 58 | const processedContent = await remark() 59 | .use(html) 60 | .process(matterResult.content) 61 | const contentHtml = processedContent.toString() 62 | 63 | // Combine the data with the id and contentHtml 64 | return { 65 | id, 66 | contentHtml, 67 | ...matterResult.data 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /cypress/e2e/Practice/just-javascript.cy.js: -------------------------------------------------------------------------------- 1 | const { _ } = Cypress; 2 | import { format, parseISO } from "date-fns"; 3 | 4 | describe("Cypress is just JavaScript", () => { 5 | it("uses _.each() from lodash to make sure the titles from the posts api are displayed correctly on the home page", () => { 6 | // Use _.each() from lodash to iterate over the posts inside of response.body 7 | // while also getting the post titles on the homepage to make sure that the 8 | // titles displayed on the homepage match the titles in the response.body 9 | 10 | // Hint: Since the posts are sorted by date on the homepage, we have included 11 | // a sortedPosts function which will sort the posts inside of response.body for you. 12 | // https://lodash.com/docs/4.17.15#forEach 13 | 14 | cy.visit("http://localhost:3000"); 15 | 16 | cy.request("GET", "http://localhost:3000/api/posts").then((response) => { 17 | const sortedPosts = (posts) => { 18 | return posts.sort( 19 | (a, b) => new Date(b.date).valueOf() - new Date(a.date).valueOf() 20 | ); 21 | }; 22 | }); 23 | }); 24 | 25 | it("formats the post date correctly on the homepage", () => { 26 | // Use _.each() from lodash to iterate over the posts inside of response.body 27 | // while also getting the post dates on the homepage to make sure that the 28 | // dates displayed on the homepage are formatted correctly. 29 | // i.e 2020-01-02 from the API is formatted and displayed as January 2, 2020 on the homepage 30 | 31 | // Hint: We are included format and parseISO from the 'date-fns' library. 32 | // You will need to use both of these methods to format the dates coming 33 | // from the API into the correct format. If you get stuck, the formatting string, 34 | // can be found inside of /components/date.js 35 | 36 | cy.visit("http://localhost:3000"); 37 | 38 | cy.request("GET", "http://localhost:3000/api/posts").then((response) => { 39 | const sortedPosts = (posts) => { 40 | return posts.sort( 41 | (a, b) => new Date(b.date).valueOf() - new Date(a.date).valueOf() 42 | ); 43 | }; 44 | }); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /cypress/e2e/Answers/network-requests.cy.js: -------------------------------------------------------------------------------- 1 | /* 2 | It is very helpful to use console.log() to log out the response data 3 | in order to see the data you are working with. 4 | */ 5 | describe("Network Requests", () => { 6 | beforeEach(() => { 7 | cy.intercept("GET", "http://localhost:3000/api/posts", (req) => { 8 | // this is to disable browser caching 9 | // https://glebbahmutov.com/blog/cypress-intercept-problems/ 10 | delete req.headers["if-none-match"]; 11 | }).as("posts"); 12 | 13 | cy.visit("http://localhost:3000"); 14 | }); 15 | 16 | it("/api/posts returns a status code of 200", () => { 17 | // Write an assertion that the route '/api/posts' 18 | // returns a status code of 200 19 | // Hint: You will need to use cy.request() 20 | // https://docs.cypress.io/api/commands/request 21 | 22 | cy.request("GET", "http://localhost:3000/api/posts").then((response) => { 23 | expect(response.status).to.eq(200); 24 | }); 25 | }); 26 | 27 | it("/api/posts returns the correct number of posts", () => { 28 | // Write an assertion that the route '/api/posts' 29 | // returns the correct number of posts. 30 | 31 | cy.request("GET", "http://localhost:3000/api/posts").then((response) => { 32 | expect(response.body.length).to.eq(2); 33 | }); 34 | }); 35 | 36 | it("the posts.json fixture returns the correct number of posts", () => { 37 | // Write an assertion that the route '/api/posts' 38 | // returns the correct number of posts. 39 | // There are 25 total posts in the fixture 40 | // Hint: You will need to use cy.fixture() 41 | // https://docs.cypress.io/api/commands/fixture 42 | 43 | cy.fixture("posts").then((response) => { 44 | expect(response.length).to.eq(25); 45 | }); 46 | }); 47 | 48 | it("intercepts /api/posts and returns the correct number of posts", () => { 49 | // Wait upon the @posts intercept that happens in the beforeEach() 50 | // and assert that the response contains the correct number of posts 51 | // Hint: you will need to cy.wait() to wait upon the @posts alias. 52 | // https://docs.cypress.io/api/commands/wait 53 | 54 | cy.wait("@posts").its("response.body").should("have.length", 2); 55 | }); 56 | }); 57 | -------------------------------------------------------------------------------- /cypress/e2e/Answers/just-javascript.cy.js: -------------------------------------------------------------------------------- 1 | const { _ } = Cypress; 2 | import { format, parseISO } from "date-fns"; 3 | 4 | describe("Cypress is just JavaScript", () => { 5 | it("uses _.each() from lodash to make sure the titles from the posts api are displayed correctly on the home page", () => { 6 | // Use _.each() from lodash to iterate over the posts inside of response.body 7 | // while also getting the post titles on the homepage to make sure that the 8 | // titles displayed on the homepage match the titles in the response.body 9 | 10 | // Hint: Since the posts are sorted by date on the homepage, we have included 11 | // a sortedPosts function which will sort the posts inside of response.body for you. 12 | // https://lodash.com/docs/4.17.15#forEach 13 | 14 | cy.visit("http://localhost:3000"); 15 | 16 | cy.request("GET", "http://localhost:3000/api/posts").then((response) => { 17 | const sortedPosts = (posts) => { 18 | return posts.sort( 19 | (a, b) => new Date(b.date).valueOf() - new Date(a.date).valueOf() 20 | ); 21 | }; 22 | 23 | _.each(sortedPosts(response.body), (post, index) => { 24 | cy.get(`[data-test="post-link-${index}"]`).contains(post.title); 25 | }); 26 | }); 27 | }); 28 | 29 | it("formats the post date correctly on the homepage", () => { 30 | // Use _.each() from lodash to iterate over the posts inside of response.body 31 | // while also getting the post dates on the homepage to make sure that the 32 | // dates displayed on the homepage are formatted correctly. 33 | // i.e 2020-01-02 from the API is formatted and displayed as January 2, 2020 on the homepage 34 | 35 | // Hint: We are included format and parseISO from the 'date-fns' library. 36 | // You will need to use both of these methods to format the dates coming 37 | // from the API into the correct format. If you get stuck, the formatting string, 38 | // can be found inside of /componets/date.js 39 | 40 | cy.visit("http://localhost:3000"); 41 | 42 | cy.request("GET", "http://localhost:3000/api/posts").then((response) => { 43 | const sortedPosts = (posts) => { 44 | return posts.sort( 45 | (a, b) => new Date(b.date).valueOf() - new Date(a.date).valueOf() 46 | ); 47 | }; 48 | 49 | _.each(sortedPosts(response.body), (post, index) => { 50 | const formattedDate = format(parseISO(post.date), "LLLL d, yyyy"); 51 | cy.get(`[data-test="post-date-${index}"]`).contains(formattedDate); 52 | }); 53 | }); 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /components/layout.js: -------------------------------------------------------------------------------- 1 | import Head from "next/head"; 2 | import Image from "next/image"; 3 | import styles from "./layout.module.css"; 4 | import utilStyles from "../styles/utils.module.css"; 5 | import Link from "next/link"; 6 | import Footer from "./Footer"; 7 | 8 | const name = "Real World Testing Blog"; 9 | export const siteTitle = "Real World Testing Blog"; 10 | 11 | export default function Layout({ children, home }) { 12 | return ( 13 | <> 14 |
15 | 16 | 17 | 21 | 27 | 28 | 29 | 30 |
31 | {home ? ( 32 | <> 33 | {name} 41 |

42 | {name} 43 |

44 | 45 | ) : ( 46 | <> 47 | 48 | 49 | {name} 57 | 58 | 59 |

60 | 61 | {name} 62 | 63 |

64 | 65 | )} 66 |
67 |
{children}
68 | {!home && ( 69 |
70 | 71 | ← Back to home 72 | 73 |
74 | )} 75 |
76 | 77 |