Skip to content

Commit 79e6d00

Browse files
authored
test: set up cypress component testing and test footer (#140)
* add cypress asset folders to .gitignore * update cypress to latest * set up Cypress component testing * remove red squiglies * add custom cy.validateLink command for this project * add data-cy attributes for testing * add validateFooter util and footer component test * test homepage footer from e2e homepage test * minor tweaks * use cy.within() to simplify selector code * add check-ts for easy typescript validation * add declarations.d.ts
1 parent a9e771c commit 79e6d00

13 files changed

+182
-45
lines changed

.gitignore

+5-1
Original file line numberDiff line numberDiff line change
@@ -42,4 +42,8 @@ next-env.d.ts
4242

4343
# VSCODE
4444
*.code-workspace
45-
.vscode
45+
.vscode
46+
47+
# cypress
48+
cypress/videos
49+
cypress/screenshots

cypress.config.ts

+9-2
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,13 @@ export default defineConfig({
44
e2e: {
55
setupNodeEvents(on, config) {
66
// implement node event listeners here
7-
}
8-
}
7+
},
8+
},
9+
10+
component: {
11+
devServer: {
12+
framework: "next",
13+
bundler: "webpack",
14+
},
15+
},
916
});

cypress/e2e/homepage.cy.ts

+4
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import validateFooter from "../util/validateFooter";
2+
13
describe("Homepage", () => {
24
beforeEach(() => {
35
cy.visit("http://localhost:3000", { timeout: 30000 });
@@ -8,5 +10,7 @@ describe("Homepage", () => {
810
cy.get("header").should("be.visible");
911
cy.get('.sticky > :nth-child(1) > a > .chakra-text').contains('TechIsHiring');
1012
cy.get('header > div > nav > ul').should('be.visible');
13+
14+
validateFooter('desktop')
1115
});
1216
});

cypress/support/commands.ts

+29
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,32 @@
3535
// }
3636
// }
3737
// }
38+
export {};
39+
40+
declare global {
41+
namespace Cypress {
42+
interface Chainable {
43+
validateLink(name: string, destination : string, target?: 'new tab'): Chainable<void>
44+
}
45+
}
46+
}
47+
48+
49+
/**
50+
*
51+
* @param name - the name of the link
52+
* @param destination - the URL the link navigates to
53+
* @param target - target attribute - e.g. "new tab" or not
54+
*/
55+
const validateLink = (name: string, destination : string, target?: 'new tab') => {
56+
const shouldOpenInNewTab = target === 'new tab'
57+
const targetAssertion = shouldOpenInNewTab ? 'have.attr' : 'not.have.attr'
58+
59+
// find a link with the expected name
60+
cy.contains(`a`, name)
61+
.should('be.visible') // make sure it is visible
62+
.and('have.attr', 'href', destination) // and has the right href
63+
.and(targetAssertion, 'target', '_blank') // and opens in a new tab, or not
64+
}
65+
66+
Cypress.Commands.add('validateLink', validateLink)

cypress/support/component-index.html

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
<meta charset="utf-8">
5+
<meta http-equiv="X-UA-Compatible" content="IE=edge">
6+
<meta name="viewport" content="width=device-width,initial-scale=1.0">
7+
<title>Components App</title>
8+
<!-- Used by Next.js to inject CSS. -->
9+
<div id="__next_css__DO_NOT_USE__"></div>
10+
</head>
11+
<body>
12+
<div data-cy-root></div>
13+
</body>
14+
</html>

cypress/support/component.ts

+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
// ***********************************************************
2+
// This example support/component.ts 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 { ChakraProvider } from "@chakra-ui/react";
19+
import { createElement } from "react";
20+
import theme from "../../src/styles/globals.css";
21+
22+
// Alternatively you can use CommonJS syntax:
23+
// require('./commands')
24+
25+
import { mount } from 'cypress/react18'
26+
27+
// Augment the Cypress namespace to include type definitions for
28+
// your custom command.
29+
// Alternatively, can be defined in cypress/support/component.d.ts
30+
// with a <reference path="./component" /> at the top of your spec.
31+
declare global {
32+
namespace Cypress {
33+
interface Chainable {
34+
mount: typeof mount
35+
}
36+
}
37+
}
38+
39+
40+
Cypress.Commands.add("mount", (component, options) => {
41+
const wrappedComponent = createElement(ChakraProvider, {theme}, component);
42+
43+
return mount(wrappedComponent, options);
44+
});

cypress/util/validateFooter.ts

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
type Size = 'mobile' | 'desktop'
2+
3+
const validateFooter = (size: Size) => {
4+
5+
cy.log('-----START FOOTER-----')
6+
// since both mobile and desktop versions are in the DOM as the same time, we scope with [data-cy]
7+
cy.get(`[data-cy=${size}-footer]`).within(() => {
8+
cy.validateLink('Home', '/')
9+
cy.validateLink('TechIsHiring', '/')
10+
cy.validateLink('Newsletter', 'https://newsletter.techishiring.com/', 'new tab')
11+
cy.validateLink('About', '/about')
12+
cy.validateLink('Contact Us', '/contact')
13+
14+
// now test the SVG icon links at the bottom
15+
cy.validateLink('Twitter for Tech Is Hiring', 'https://www.twitter.com/techishiring', 'new tab')
16+
cy.validateLink('GitHub for Tech Is Hiring', 'https://www.github.com/techishiring', 'new tab')
17+
cy.validateLink('LinkedIn for Tech Is Hiring', 'https://www.linkedin.com/company/techishiring', 'new tab')
18+
cy.validateLink('Patreon for Tech Is Hiring', 'https://www.patreon.com/techishiring', 'new tab')
19+
cy.validateLink('Tech Is Hiring Newsletter on Substack', 'https://newsletter.techishiring.com/', 'new tab')
20+
21+
cy.contains(`p`, `© Copyright ${new Date().getFullYear()}, All rights reserved.`).should('be.visible')
22+
cy.contains(`p`, `Website designed by Inetimi Ade ([email protected])`).should('be.visible')
23+
})
24+
// test the main navigation links for the site
25+
cy.log('-----END FOOTER-----')
26+
}
27+
28+
export default validateFooter

declarations.d.ts

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
declare module '*.css'; // needed for Cypress component tests to import CSS files

package-lock.json

+19-34
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+7-3
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,13 @@
99
"lint": "next lint",
1010
"format": "next lint -- --fix --dir .",
1111
"test": "vitest",
12-
"test:cypress": "npx cypress open",
12+
"cypress:open:e2e": "npx cypress open --e2e",
13+
"cypress:open:component": "npx cypress open --component",
14+
"cypress:run:e2e": "npx cypress run",
15+
"cypress:run:component": "npx cypress run --component",
1316
"reset-deps": "rm -rf node_modules && npm ci",
14-
"postbuild": "next-sitemap"
17+
"postbuild": "next-sitemap",
18+
"check-ts": "tsc --noEmit"
1519
},
1620
"dependencies": {
1721
"@chakra-ui/react": "^2.3.6",
@@ -48,7 +52,7 @@
4852
"@vitejs/plugin-react": "^3.1.0",
4953
"autoprefixer": "^10.4.7",
5054
"babel-loader": "^8.2.5",
51-
"cypress": "^12.5.1",
55+
"cypress": "^12.12.0",
5256
"eslint": "8.26.0",
5357
"eslint-config-next": "^13.2.4",
5458
"eslint-plugin-storybook": "^0.6.11",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import React from 'react'
2+
import Footer from './footer'
3+
import validateFooter from '../../../../cypress/util/validateFooter'
4+
5+
describe('<Footer />', () => {
6+
it('renders mobile footer', () => {
7+
cy.mount(<Footer />)
8+
validateFooter('mobile')
9+
})
10+
it('renders desktop footer', {
11+
viewportWidth: 1200
12+
}, () => {
13+
cy.mount(<Footer />)
14+
validateFooter('desktop')
15+
})
16+
})

src/components/organisms/footer/footer.tsx

+3-3
Original file line numberDiff line numberDiff line change
@@ -217,11 +217,11 @@ const Footer = () => {
217217
});
218218

219219
return (
220-
<footer className="flex justify-center w-full bg-[#134D82]">y
221-
<div className="hidden w-full md:block 3xl:w-max-screen-size px-4 pt-6 pb-8 md:px-5 md:pb-16 lg:px-14">
220+
<footer className="flex justify-center w-full bg-[#134D82]">
221+
<div data-cy="desktop-footer" className="hidden w-full md:block 3xl:w-max-screen-size px-4 pt-6 pb-8 md:px-5 md:pb-16 lg:px-14">
222222
<DesktopFooter mobileNav={navItems} />
223223
</div>
224-
<div className="md:hidden">
224+
<div data-cy="mobile-footer" className="md:hidden">
225225
<MobileFooter mobileNav={navItems}/>
226226
</div>
227227
</footer>

tsconfig.json

+3-2
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,9 @@
1515
"resolveJsonModule": true,
1616
"isolatedModules": true,
1717
"jsx": "preserve",
18-
"incremental": true
18+
"incremental": true,
19+
"types": ["cypress"]
1920
},
2021
"include": ["next-env.d.ts", "next-types.d.ts", "**/*.ts", "**/*.tsx"],
21-
"exclude": ["node_modules", "cypress"]
22+
"exclude": ["node_modules"]
2223
}

0 commit comments

Comments
 (0)