├── 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 |
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 |