ch07-ModalYesNo
In the Angular version of the app, we can see that the component will be a modal with lots of css, header, section and finally a footer with 2 buttons. For a walking skeleton, we can start with a
div
wrapping header
, section
, footer
and the two buttons under the footer
.
ModalYesNo-initial
Create a branch
feat/modalYesNo
. Create 2 files under src/components/
folder; ModalYesNo.cy.tsx
, ModalYesNo.tsx
. As usual, start minimal with a component rendering; copy the below to the files and execute the test after opening the runner with yarn cy:open-ct
.// src/components/ModalYesNo.cy.tsx
import ModalYesNo from "./ModalYesNo";
describe("ModalYesNo", () => {
it("should", () => {
cy.mount(<ModalYesNo />);
});
});
// src/components/ModalYesNo.tsx
export default function ModalYesNo() {
return <div>hello</div>;
}
To keep things simple we will use the original recipe from React TypeScript Cheatsheet, modal portal example. Create
src/components/Modal.tsx
and paste-in the following code./* eslint-disable @typescript-eslint/no-non-null-assertion */
import { useEffect, useRef, ReactNode } from "react";
import { createPortal } from "react-dom";
type ModalProps = {
children?: ReactNode;
};
const Modal = ({ children }: ModalProps) => {
const el = useRef(document.createElement("div"));
let modalRoot = document.getElementById("modal-root");
if (!modalRoot) {
modalRoot = document.createElement("div");
modalRoot.setAttribute("id", "modal-root");
document.body.appendChild(modalRoot);
}
useEffect(() => {
const currentEl = el.current;
modalRoot!.appendChild(currentEl);
return () => {
modalRoot!.removeChild(currentEl);
};
}, [modalRoot]);
return createPortal(children, el.current);
};
export default Modal;
For the moment we will assume that the modal is always open, and write a failing tests the ensures that an element with an id
modal-root
exists in the DOM (Red 1).// src/components/ModalYesNo.cy.tsx
import ModalYesNo from "./ModalYesNo";
describe("ModalYesNo", () => {
it("should", () => {
cy.mount(<ModalYesNo />);
cy.get("#modal-root").should("exist");
});
});
We must import and render
Modal
to pass the test (Green 1).// src/components/ModalYesNo.tsx
import Modal from "./Modal";
export default function ModalYesNo() {
return <Modal></Modal>;
}
We decided on a skeleton of the app with
header
, section
and footer
with two buttons. Let's write a failing test for it (Red 2).// src/components/ModalYesNo.cy.tsx
import ModalYesNo from "./ModalYesNo";
describe("ModalYesNo", () => {
it("should", () => {
cy.mount(<ModalYesNo />);
cy.get("#modal-root").should("exist");
cy.get("div")
.last()
.within(() => {
cy.get("header");
cy.get("section");
cy.get("footer");
cy.get("button").should("have.length", 2);
});
});
});
We create the walking skeleton of the modal to make the test pass (Green 2).
import Modal from "./Modal";
export default function ModalYesNo() {
return (
<Modal>
<div>
<header></header>
<section></section>
<footer>
<button>No</button>
<button>Yes</button>
</footer>
</div>
</Modal>
);
}

ModalYesNo-green2
That is looking a bit bare. Let us copy the styles from the Angular version of the app, and add a few more tags (Refactor 2). Similar to the previous chapter, we are able to do a RedGreenRefactor cycle with visual aids for refactor increments.
// src/components/ModalYesNo.tsx
import Modal from "./Modal";
export default function ModalYesNo() {
return (
<Modal>
<div className="modal is-active">
<div className="modal-background" />
<div className="modal-card">
<header className="modal-card-head">
<p className="modal-card-title"></p>
</header>
<section className="modal-card-body"></section>
<footer className="modal-card-foot card-footer">
<button className="button modal-no"></button>
<button className="button is-primary modal-yes"></button>
</footer>
</div>
</div>
</Modal>
);
}
The test mostly stays the same, the only difference being the import of styles.
// src/components/ModalYesNo.cy.tsx
import ModalYesNo from "./ModalYesNo";
import "../styles.scss";
describe("ModalYesNo", () => {
it("should", () => {
cy.mount(<ModalYesNo />);
cy.get("#modal-root").should("exist");
cy.get("div")
.last()
.within(() => {
cy.get("header");
cy.get("section");
cy.get("footer");
cy.get("button").should("have.length", 2);
});
});
});
The visuals are looking like the real thing. Now what is remaining are some text, and onClick handlers for the buttons.

ModalYesNo-Refactor2
There are 4 pieces of text in the modal; the title, the message, and the buttons. Let's write a failing test checking for these strings. We will use hard-coded values, and decide later what can be parameterized (Red 3).
// src/components/ModalYesNo.cy.tsx
import ModalYesNo from "./ModalYesNo";
import "../styles.scss";
describe("ModalYesNo", () => {
it("should", () => {
cy.mount(<ModalYesNo />);
cy.get("#modal-root").should("exist");
cy.get("div")
.last()
.within(() => {
cy.get("header").contains("Confirm");
cy.get("section").contains("Are you sure?");
cy.get("footer");
cy.getByCy("button-yes").contains("Yes");
cy.getByCy("button-no").contains("No");
});
});
});
We add the hard-coded strings into respective tags to pass the test (Green 3).
// src/components/ModalYesNo.tsx
import Modal from "./Modal";
export default function ModalYesNo() {
return (
<Modal>
<div className="modal is-active">
<div className="modal-background" />
<div className="modal-card">
<header className="modal-card-head">
<p className="modal-card-title">Confirm</p>
</header>
<section className="modal-card-body">Are you sure?</section>
<footer className="modal-card-foot card-footer">
<button data-cy="button-no" className="button modal-no">
No
</button>
<button
data-cy="button-yes"
className="button is-primary modal-yes"
>
Yes
</button>
</footer>
</div>
</div>
</Modal>
);
}
With the visuals, we can make a better judgement on what needs to be parameterized. Confirm, No and Yes are most likely to stay constants. The message, if anything, should be parameterized as a prop. It is significant here that the tool is aiding us in the refactoring of the component as well as the design.

ModalYesNo-Green4
We will tweak the test to accept a prop for the message. The test passes, but the TS compiler is warning us against the newly added prop (Red 4). It is significant here that TS also aids us in the RedGreenRefactor cycles.
// src/components/ModalYesNo.cy.tsx
import ModalYesNo from "./ModalYesNo";
import "../styles.scss";
describe("ModalYesNo", () => {
it("should", () => {
const message = "Are you sure?";
cy.mount(<ModalYesNo message={message} />);
cy.get("#modal-root").should("exist");
cy.get("div")
.last()
.within(() => {
cy.get("header").contains("Confirm");
cy.get("section").contains(message);
cy.get("footer");
cy.getByCy("button-yes").contains("Yes");
cy.getByCy("button-no").contains("No");
});
});
});
We follow the well established pattern of adding a prop type, an argument to the component and using the value in the component (Green 4).
// src/components/ModalYesNo.tsx
import Modal from "./Modal";
type ModalYesNoProps = {
message: string;
};
export default function ModalYesNo({ message }: ModalYesNoProps) {
return (
<Modal>
<div className="modal is-active">
<div className="modal-background" />
<div className="modal-card">
<header className="modal-card-head">
<p className="modal-card-title">Confirm</p>
</header>
<section className="modal-card-body">{message}</section>
<footer className="modal-card-foot card-footer">
<button data-cy="button-no" className="button modal-no">
No
</button>
<button
data-cy="button-yes"
className="button is-primary modal-yes"
>
Yes
</button>
</footer>
</div>
</div>
</Modal>
);
}
Let's add click handlers for the Yes and No buttons. We are going to need to pass in props and ensure that they are called. Write a failing test (Red 5).
// src/components/ModalYesNo.cy.tsx
import ModalYesNo from "./ModalYesNo";
import "../styles.scss";
describe("ModalYesNo", () => {
it("should", () => {
const message = "Are you sure?";
cy.mount(<ModalYesNo message={message} onYes={cy.stub().as("onYes")} />);
cy.get("#modal-root").should("exist");