@amirrustam
A payment application to demonstrate real-world usage of
Cypress testing methods, patterns, and workflows.
Open Sourced at github.com/cypress-io/cypress-realworld-app
Open Sourced at github.com/cypress-io/cypress-realworld-app
State Management
UI Framework
Component Library
API Server
Database
TypeScript
Easy & Quick Setup
Open Sourced at github.com/cypress-io/cypress-realworld-app
Zero additional dependencies. Clone & Run.
Responsive Testing
Practices for testing across desktop & mobile viewports.
100% Code Coverage
Full coverage of backend & frontend code purely w/ E2E tests.
Cross-Browser Testing
All test suits run in Chrome & Firefox in CI.
Visual Regression Testing
Demonstrates image snapshotting in practice w/ Cypress Percy plugin.
CI Best Practices
Clear CI config w/ parallelization, code coverage reporting, etc.
Real World App test runs are
publicly available at
Open Sourced at github.com/cypress-io/cypress-realworld-app
To scale your test suites as your app grows in functionality and complexity, it can be tremendously beneficial to:
Add helpful contextual details to the command log for easier debugging.
Minimize extraneous log "noise" to improve readability.
Customize control of command log visual display and DOM snapshotting.
cy.pickDateRange()
cy.setAmountRange()
cy.login()
Cypress.Commands.add("login", (username, password, rememberUser = false) => {
const signinPath = "/signin";
const log = Cypress.log({
name: "login",
displayName: "LOGIN",
message: [`🔐 Authenticating | ${username}`],
autoEnd: false,
});
cy.server();
cy.route("POST", "/login").as("loginUser");
cy.route("GET", "checkAuth").as("getUserProfile");
cy.location("pathname", { log: false }).then((currentPath) => {
if (currentPath !== signinPath) {
cy.visit(signinPath);
}
});
log.snapshot("before");
cy.getBySel("signin-username").type(username);
cy.getBySel("signin-password").type(password);
if (rememberUser) {
cy.getBySel("signin-remember-me").find("input").check();
}
cy.getBySel("signin-submit").click();
cy.wait("@loginUser").then((loginUser: any) => {
log.set({
consoleProps() {
return {
username,
password,
rememberUser,
userId: loginUser.response.body.user?.id,
};
},
});
log.snapshot("after");
log.end();
});
});
cy.login()
const log = Cypress.log({
name: "login",
displayName: "LOGIN",
message: [`🔐 Authenticating | ${username}`],
autoEnd: false,
});
const log = Cypress.log({
name: "login",
displayName: "LOGIN",
message: [`🔐 Authenticating | ${username}`],
autoEnd: false,
});
You can control when a command is marked as complete within the log.
Cypress.Commands.add("login", (username, password, rememberUser = false) => {
const signinPath = "/signin";
const log = Cypress.log({
name: "login",
displayName: "LOGIN",
message: [`🔐 Authenticating | ${username}`],
autoEnd: false,
});
cy.server();
cy.route("POST", "/login").as("loginUser");
cy.route("GET", "checkAuth").as("getUserProfile");
cy.location("pathname", { log: false }).then((currentPath) => {
if (currentPath !== signinPath) {
cy.visit(signinPath);
}
});
log.snapshot("before");
cy.getBySel("signin-username").type(username);
cy.getBySel("signin-password").type(password);
if (rememberUser) {
cy.getBySel("signin-remember-me").find("input").check();
}
cy.getBySel("signin-submit").click();
cy.wait("@loginUser").then((loginUser: any) => {
log.set({
consoleProps() {
return {
username,
password,
rememberUser,
userId: loginUser.response.body.user?.id,
};
},
});
log.snapshot("after");
log.end();
});
});
Cypress.Commands.add("login", (username, password, rememberUser = false) => {
const signinPath = "/signin";
const log = Cypress.log({
name: "login",
displayName: "LOGIN",
message: [`🔐 Authenticating | ${username}`],
autoEnd: false,
});
cy.server();
cy.route("POST", "/login").as("loginUser");
cy.route("GET", "checkAuth").as("getUserProfile");
cy.location("pathname", { log: false }).then((currentPath) => {
if (currentPath !== signinPath) {
cy.visit(signinPath);
}
});
log.snapshot("before");
cy.getBySel("signin-username").type(username);
cy.getBySel("signin-password").type(password);
if (rememberUser) {
cy.getBySel("signin-remember-me").find("input").check();
}
cy.getBySel("signin-submit").click();
cy.wait("@loginUser").then((loginUser: any) => {
log.set({
consoleProps() {
return {
username,
password,
rememberUser,
userId: loginUser.response.body.user?.id,
};
},
});
log.snapshot("after");
log.end();
});
});
Cypress.Commands.add("login", (username, password, rememberUser = false) => {
const signinPath = "/signin";
const log = Cypress.log({
name: "login",
displayName: "LOGIN",
message: [`🔐 Authenticating | ${username}`],
autoEnd: false,
});
cy.server();
cy.route("POST", "/login").as("loginUser");
cy.route("GET", "checkAuth").as("getUserProfile");
cy.location("pathname", { log: false }).then((currentPath) => {
if (currentPath !== signinPath) {
cy.visit(signinPath);
}
});
log.snapshot("before");
cy.getBySel("signin-username").type(username);
cy.getBySel("signin-password").type(password);
if (rememberUser) {
cy.getBySel("signin-remember-me").find("input").check();
}
cy.getBySel("signin-submit").click();
cy.wait("@loginUser").then((loginUser: any) => {
log.set({
consoleProps() {
return {
username,
password,
rememberUser,
userId: loginUser.response.body.user?.id,
};
},
});
log.snapshot("after");
log.end();
});
});
Cypress.Commands.add("login", (username, password, rememberUser = false) => {
const signinPath = "/signin";
const log = Cypress.log({
name: "login",
displayName: "LOGIN",
message: [`🔐 Authenticating | ${username}`],
autoEnd: false,
});
cy.server();
cy.route("POST", "/login").as("loginUser");
cy.route("GET", "checkAuth").as("getUserProfile");
cy.location("pathname", { log: false }).then((currentPath) => {
if (currentPath !== signinPath) {
cy.visit(signinPath);
}
});
log.snapshot("before");
cy.getBySel("signin-username").type(username);
cy.getBySel("signin-password").type(password);
if (rememberUser) {
cy.getBySel("signin-remember-me").find("input").check();
}
cy.getBySel("signin-submit").click();
cy.wait("@loginUser").then((loginUser: any) => {
log.set({
consoleProps() {
return {
username,
password,
rememberUser,
userId: loginUser.response.body.user?.id,
};
},
});
log.snapshot("after");
log.end();
});
});
cy.login()
Cypress.Commands.add("login", (username, password, rememberUser = false) => {
const signinPath = "/signin";
const log = Cypress.log({
name: "login",
displayName: "LOGIN",
message: [`🔐 Authenticating | ${username}`],
autoEnd: false,
});
cy.server();
cy.route("POST", "/login").as("loginUser");
cy.route("GET", "checkAuth").as("getUserProfile");
cy.location("pathname", { log: false }).then((currentPath) => {
if (currentPath !== signinPath) {
cy.visit(signinPath);
}
});
log.snapshot("before");
cy.getBySel("signin-username").type(username);
cy.getBySel("signin-password").type(password);
if (rememberUser) {
cy.getBySel("signin-remember-me").find("input").check();
}
cy.getBySel("signin-submit").click();
cy.wait("@loginUser").then((loginUser: any) => {
log.set({
consoleProps() {
return {
username,
password,
rememberUser,
userId: loginUser.response.body.user?.id,
};
},
});
log.snapshot("after");
log.end();
});
});
Identify Provider API Token Endpoint
Store credentials locally (cookie, localStorage, etc)
Bypass Auth Redirect Flow (varies based on provider)
Cypress.Commands.add("loginByApi",
(username, password = Cypress.env("defaultPassword")) => {
return cy.request("POST", `${Cypress.env("apiUrl")}/login`, {
username,
password,
});
});
it("login as user", function () {
cy.loginByApi(ctx.authenticatedUser!.username).then((response) => {
expect(response.status).to.eq(200);
});
});
cy.loginByApi()
Cypress.Commands.add("loginByAuth0Api", (username: string, password: string) => {
cy.log(`Logging in as ${username}`);
const client_id = Cypress.env("auth0_client_id");
const client_secret = Cypress.env("auth0_client_secret");
const audience = Cypress.env("auth0_audience");
const scope = Cypress.env("auth0_scope");
cy.request({
method: "POST",
url: `https://${Cypress.env("auth0_domain")}/oauth/token`,
body: {
grant_type: "password",
username,
password,
// ...
},
}).then(({ body }) => {
const claims: any = jwt.decode(body.id_token);
// ...
window.localStorage.setItem("auth0Cypress", JSON.stringify(item));
cy.visit("/");
});
});
cy.loginByAuth0Api()
import React, { useEffect } from "react";
import { useService, useMachine } from "@xstate/react";
import { makeStyles } from "@material-ui/core/styles";
import { CssBaseline } from "@material-ui/core";
import { snackbarMachine } from "../machines/snackbarMachine";
import { notificationsMachine } from "../machines/notificationsMachine";
import { authService } from "../machines/authMachine";
import AlertBar from "../components/AlertBar";
import { bankAccountsMachine } from "../machines/bankAccountsMachine";
import PrivateRoutesContainer from "./PrivateRoutesContainer";
import { useAuth0, withAuthenticationRequired } from "@auth0/auth0-react";
// @ts-ignore
if (window.Cypress) {
// Expose authService on window for Cypress
// @ts-ignore
window.authService = authService;
}
const useStyles = makeStyles((theme) => ({
root: {
display: "flex",
},
}));
const AppAuth0: React.FC = () => {
const { isAuthenticated, user, getAccessTokenSilently } = useAuth0();
const classes = useStyles();
const [authState] = useService(authService);
const [, , notificationsService] = useMachine(notificationsMachine);
const [, , snackbarService] = useMachine(snackbarMachine);
const [, , bankAccountsService] = useMachine(bankAccountsMachine);
// @ts-ignore
if (window.Cypress) {
// eslint-disable-next-line react-hooks/rules-of-hooks
useEffect(() => {
const auth0 = JSON.parse(localStorage.getItem("auth0Cypress")!);
authService.send("AUTH0", {
user: auth0.body.decodedToken.user,
token: auth0.body.access_token,
});
}, []);
} else {
// eslint-disable-next-line react-hooks/rules-of-hooks
useEffect(() => {
(async function waitForToken() {
const token = await getAccessTokenSilently();
authService.send("AUTH0", { user, token });
})();
}, [isAuthenticated, user, getAccessTokenSilently]);
}
const isLoggedIn =
authState.matches("authorized") ||
authState.matches("refreshing") ||
authState.matches("updating");
return (
<div className={classes.root}>
<CssBaseline />
{isLoggedIn && (
<PrivateRoutesContainer
isLoggedIn={isLoggedIn}
notificationsService={notificationsService}
authService={authService}
snackbarService={snackbarService}
bankAccountsService={bankAccountsService}
/>
)}
<AlertBar snackbarService={snackbarService} />
</div>
);
};
//@ts-ignore
let appAuth0 = window.Cypress ? AppAuth0 : withAuthenticationRequired(AppAuth0);
export default appAuth0;
cy.loginByAuth0Api() - Application State
cy.loginByXstate()
Cypress.Commands.add("loginByXstate", (username, password = Cypress.env("defaultPassword")) => {
const log = Cypress.log({
name: "loginbyxstate",
displayName: "LOGIN BY XSTATE",
message: [`🔐 Authenticating | ${username}`],
// @ts-ignore
autoEnd: false,
});
cy.server();
cy.route("POST", "/login").as("loginUser");
cy.route("GET", "/checkAuth").as("getUserProfile");
cy.visit("/signin", { log: false }).then(() => {
log.snapshot("before");
});
// Cypress.Commands.add("loginByXstate" ... );
cy.window({ log: false }).then((win) => win.authService.send("LOGIN", { username, password }));
return cy.wait("@loginUser").then((loginUser) => {
log.set({
consoleProps() {
return {
username,
password,
// @ts-ignore
userId: loginUser.response.body.user.id,
};
},
});
log.snapshot("after");
log.end();
});
});
// Application Code
import React, { useEffect } from "react";
import { useService, useMachine } from "@xstate/react";
import { authService } from "../machines/authMachine";
if (window.Cypress) {
// Expose authService on window for Cypress
window.authService = authService;
}
cy.switchUser()
cy.loginByXstate()
cy.logoutByXstate()
+
it("User C comments on a transaction between User A and User B; User A and B get notifications that User C commented", function () {
// User C
cy.loginByXstate(ctx.userC.username);
// ...
// User A
cy.switchUser(ctx.userA.username);
// ...
// User B
cy.switchUser(ctx.userB.username);
// ...
});
version: 2.1
linux-workflow: &linux-workflow
jobs:
- cypress/install:
name: "Setup Linux"
# Run API tests against backend server
- cypress/run:
name: "API Tests"
# Run E2E tests in Chrome
- cypress/run:
name: "UI Tests - Chrome"
# Run E2E tests in Chrome with mobile device viewport
- cypress/run:
name: "UI Tests - Chrome - Mobile"
# Run E2E tests in Firefox
- cypress/run:
name: "UI Tests - Firefox"
# Run E2E tests in Firefox with mobile device viewport
- cypress/run:
name: "UI Tests - Firefox - Mobile"
windows-workflow: &windows-workflow
jobs:
- cypress/install:
name: "Setup Windows"
# Run E2E tests in Windows in Electron
- cypress/run:
name: "UI Tests - Electron - Windows"
# Run E2E tests in Chrome
- cypress/run:
name: "UI Tests - Chrome"
browser: chrome
spec: cypress/tests/ui/*
executor: with-chrome-and-firefox
wait-on: "http://localhost:3000"
yarn: true
start: yarn start:ci
record: true
parallel: true
parallelism: 5
ci-build-id: ${CIRCLE_SHA1:0:8}
group: "UI - Chrome - Mobile"
requires:
- Setup Linux
post-steps:
- report-coverage
# Run E2E tests in Windows in Electron
- cypress/run:
name: "UI Tests - Electron - Windows"
spec: cypress/tests/ui/*
executor:
# executor comes from the "windows" orb
name: win/default
shell: bash.exe
wait-on: "http://localhost:3000"
yarn: true
start: yarn start:ci
record: true
parallel: true
parallelism: 5
ci-build-id: ${CIRCLE_SHA1:0:8}
group: "UI - Electron - Windows"
requires:
- Setup Windows
post-steps:
- report-coverage
no-workspace: true
# Run E2E tests in Chrome with mobile device viewport
- cypress/run:
name: "UI Tests - Chrome - Mobile"
browser: chrome
spec: cypress/tests/ui/*
config: "viewportWidth=375,viewportHeight=667"
executor: with-chrome-and-firefox
wait-on: "http://localhost:3000"
yarn: true
start: yarn start:ci
record: true
parallel: true
parallelism: 5
ci-build-id: ${CIRCLE_SHA1:0:8}
group: "UI - Chrome - Mobile"
requires:
- Setup Linux
post-steps:
- report-coverage
# Run E2E tests in Chrome with mobile device viewport
- cypress/run:
name: "UI Tests - Chrome - Mobile"
browser: chrome
spec: cypress/tests/ui/*
config: "viewportWidth=375,viewportHeight=667"
executor: with-chrome-and-firefox
wait-on: "http://localhost:3000"
yarn: true
start: yarn start:ci
record: true
parallel: true
parallelism: 5
ci-build-id: ${CIRCLE_SHA1:0:8}
group: "UI - Chrome - Mobile"
requires:
- Setup Linux
post-steps:
- report-coverage
# Run E2E tests in Chrome with mobile device viewport
- cypress/run:
name: "UI Tests - Chrome - Mobile"
browser: chrome
spec: cypress/tests/ui/*
config: "viewportWidth=375,viewportHeight=667"
executor: with-chrome-and-firefox
wait-on: "http://localhost:3000"
yarn: true
start: yarn start:ci
record: true
parallel: true
parallelism: 5
ci-build-id: ${CIRCLE_SHA1:0:8}
group: "UI - Chrome - Mobile"
requires:
- Setup Linux
post-steps:
- report-coverage
# Run E2E tests in Chrome with mobile device viewport
- cypress/run:
name: "UI Tests - Chrome - Mobile"
browser: chrome
spec: cypress/tests/ui/*
config: "viewportWidth=375,viewportHeight=667"
executor: with-chrome-and-firefox
wait-on: "http://localhost:3000"
yarn: true
start: yarn start:ci
record: true
parallel: true
parallelism: 5
ci-build-id: ${CIRCLE_SHA1:0:8}
group: "UI - Chrome - Mobile"
requires:
- Setup Linux
post-steps:
- report-coverage
commands:
report-coverage:
description: |
Store coverage report as an artifact and send it to Codecov service.
steps:
- store_artifacts:
path: coverage
- run: npx nyc report --reporter=text || true
# Use Codecov Circle CI Orb for easy report uploads
- codecov/upload:
file: coverage/coverage-final.json
# Setup
# 1. Install Cypress
# 2. Validate types
# 3. Run server unit-tests
- cypress/install:
name: "Setup Linux"
yarn: true
executor: with-chrome-and-firefox
post-steps:
- run:
name: Check Types
command: yarn types
- run:
name: Run Unit Tests
command: yarn test:unit:ci
executors:
with-chrome-and-firefox:
docker:
- image: "cypress/browsers:node12.18.0-chrome83-ff77"
Open Sourced at github.com/cypress-io/cypress-realworld-app
@amirrustam