Testing with Cypress
Testing with Cypress
About Cypress
Advantages include snapshots at the time of test execution, implicit wait commands, multiple browsers in which to test and real-time command execution with visual feedback.
Cypress allows for end-to-end testing, replicating how "users interact with your app by using a real browser", and component testing, also known as unit testing, which tests a component's functionality, styling and appearance in isolation.
Cypress and the Shadow DOM
Cypress gives two ways in which to test@ukic
components in the
Configuring at test level
AddincludeShadowDom: true
to thecy.get
query.
// cypress/e2e/test.cy.js
it("has an app bar with title link", () => {
cy.get("ic-top-navigation", { includeShadowDom: true })
.shadow()
.find(".title-link")
.should("have.attr", "href", "/")
.should("have.text", "My App Title");
});
This allows traversing the shadow DOM by chaining the.shadow()
method.
Configuring at global level
AddincludeShadowDom: true
to thecypress.config.js
.
// cypress.config.js
const { defineConfig } = require("cypress");
module.exports = defineConfig({
includeShadowDom: true,
component: {
devServer: {
framework: "create-react-app",
bundler: "webpack",
},
},
});
This allows traversing the shadow DOM using assertions without having to explicitly chain the.shadow()
method.
Example: Component testing
Below are examples of component tests on the@ukic/react
componentsIcTextfield
andIcButton
. Component tests are normally located adjacent to the component they are testing and are rendered on a development server.
// Textfield.tsx
import { IcTextField, IcButton } from "@ukic/react";
import React, { useState } from "react";
function Textfield() {
const [favouriteCoffee, setFavouriteCoffee] = useState<String>("");
const handleChange = (event) => {
event.preventDefault();
setFavouriteCoffee(event.detail.value);
};
const handleClick = () => {
console.log("This is the best coffee going:", favouriteCoffee);
};
return (
<form><IcTextField
label="What is your favourite coffee?"
placeholder="Placeholder"
helperText="Such as Arabica, Robusta or Liberica"
onIcChange={(ev) => handleChange(ev)}
/><IcButton variant="primary" onClick={() => handleClick()}>
Send
</IcButton></form>
);
}
export default Textfield;
// Textfield.cy.tsx
import React from "react";
import Textfield from "./Textfield.tsx";
describe("<Textfield />", () => {
it("renders", () => {
cy.mount(<Textfield />).should("have.property", "component");
});
it("takes a value in the textfield and prints to console when the button is clicked", () => {
// Render the component
cy.mount(<Textfield />);
// Capture the log to check later
cy.stub(window.console, "log").as("consoleLog");
// Check the IcComponents have finished rendering
cy.get("ic-text-field").should("have.class", "hydrated");
cy.get("ic-button").should("have.class", "hydrated");
// Interact with the components and provide assertions
cy.get("#ic-text-field-input-1").type("Black");
cy.get("ic-button").click();
cy.get("@consoleLog").should(
"be.calledWith",
"This is the best coffee going:",
"Black"
);
});
});
Example: End-to-end testing
Below is an example of end-to-end tests written against a ‘test app’ that has been created using ICDS components.
// StackBlitz cannot currently run Cypress testsit("should fill the form with no errors/validation",()=>{mount(<Subscription/>);// checkHydrated will wait until the component is hydrated and ensures it is ready to testcheckHydrated(IC_PAGE_HEADER);// Check the first step is visible cy.get(IC_STEPPER).should(BE_VISIBLE);checkCurrentStep(0);// Select a radio optionfindShadowEl(IC_RADIO_OPTION,RADIO+'[value="house"]').check({force:true}); cy.get(IC_RADIO_OPTION+'[label="House Blend"]').should(HAVE_ATTR,SELECTED);// Select a select option using mouseclickOnShadowEl(IC_SELECT,IC_INPUT_CONTAINER);clickOnShadowEl(IC_SELECT,IC_MENU_OPTION+'[data-value="aeropress"]');// Select a select option using keyboardclickOnShadowEl(IC_SELECT,IC_INPUT_CONTAINER,1); cy.realPress([ARROW_DOWN_KEY,ESCAPE_KEY]);// Go to next step, check the stepper and the logged formValues so far cy.get(IC_BUTTON).contains("Add to order").click();checkCurrentStep(1); cy.get(CONSOLE_LOG).should(HAVE_BEEN_CALLED_WITH,filledForm());// Fill out the text fieldsclickOnShadowEl(IC_TEXT_FIELD,IC_INPUT_CONTAINER); cy.realType("Java the Hutt").realPress(TAB_KEY).realType("javadahutt@tattooine.com").realPress(TAB_KEY).realType("1234567890");// Check both options in the checkbox groupfindShadowEl(IC_CHECKBOX,CHECKBOX).eq(0).check();findShadowEl(IC_CHECKBOX,CHECKBOX).eq(1).check(); cy.get(IC_CHECKBOX).eq(0).should(HAVE_ATTR,CHECKED); cy.get(IC_CHECKBOX).eq(1).should(HAVE_ATTR,CHECKED);// Go to next step, check the stepper and the logged formValues so far cy.get(IC_BUTTON).contains("Add to order").click(); cy.get(CONSOLE_LOG).should(HAVE_BEEN_CALLED_WITH,filledForm("details"));checkCurrentStep(2);// Select a date using the date pickerfindShadowEl(IC_DATE_PICKER,IC_DATE_INPUT).shadow().find(CALENDAR_BUTTON_ID).click();findShadowEl(IC_DATE_PICKER,FOCUSSED_DAY_BTN_CLASS).click();checkDateInputValue(newDate());// Agree to termsfindShadowEl(IC_RADIO_OPTION,RADIO+'[name="agree"]').check({force:true}); cy.get(IC_RADIO_OPTION+'[name="agree"]').should(HAVE_ATTR,SELECTED);// Submit and check the logged formValues cy.get(IC_BUTTON).contains("Submit order").click();let date =newDate(); cy.get(CONSOLE_LOG).should(HAVE_BEEN_CALLED_WITH,filledForm("checkout"));});
<IcPageHeaderheading="Customise your coffee subscription"subheading="Choose your coffee, enter your details and checkout, easy as 1... 2... 3!"size="small"id="top"stickyaligned="center"><IcChipslot="heading-adornment"label="V0.0.01"size="large"/><IcStepperslot="stepper"><IcStepstepTitle="Choose coffee"stepType={handleSteps(formSteps.chooseCoffee)}/><IcStepstepTitle="Enter Details"stepType={handleSteps(formSteps.enterDetails)}/><IcStepstepTitle="Checkout"stepType={handleSteps(formSteps.checkout)}/></IcStepper></IcPageHeader><IcBackToToptarget="top"/><formonSubmit={handleSubmit}>{formSteps.chooseCoffee.current&&(<IcSectionContaineraligned="left">{formValidation &&(<IcAlertvariant="error"heading="Error"message="Please fill in all required fields"announced/>)}<IcTypographyvariant="subtitle-large"> Please choose your coffee</IcTypography><IcTypographyvariant="body"maxLines={2}> Sip back and relax as we embark on a journey through the aromatic fields of coffee-inspired lorem ipsum. In the heart of a lush, verdant valley kissed by the golden hues of dawn, there lies a quaint little plantation where the beans of legend are nurtured. Each bean, a tiny vessel of dreams and whispers of faraway lands, cradled in the earth's embrace until it bursts forth with a promise of warmth and vigor.</IcTypography><divclassName="input-container"><IcRadioGroupname="radio-group-1"label="What variety of coffee would you like?"helperText="House blend is the default option"size="small"requiredonIcChange={(ev)=>handleChange("coffeeForm","variety", ev.detail.value)}{...(formValidation&&formValues.coffeeForm.variety===""&&{validationText:"Pleasechooseanoption",validationStatus:"error",})}><IcRadioOptionvalue="house"label="House Blend"selected={formValues.coffeeForm.variety==="house"}/><IcRadioOptionvalue="liberica"label="Liberica"selected={formValues.coffeeForm.variety==="liberica"}/><IcRadioOptionvalue="arabica"label="Arabica"selected={formValues.coffeeForm.variety==="arabica"}/><IcRadioOptionvalue="mundo"label="Mundo Nova"selected={formValues.coffeeForm.variety==="mundo"}/></IcRadioGroup></div><divclassName="input-container"><IcSelectlabel="Grind"helperText="Please select a grind type"name="grind-select"options={grindOptions}size="small"className="input"value={formValues.coffeeForm.grind}onIcChange={(ev)=>handleChange("coffeeForm","grind", ev.detail.value)}{...(formValidation&&formValues.coffeeForm.grind===""&&{validationText:"Pleasechooseagrindsize",validationStatus:"error",})}/><IcSelectlabel="Size"helperText="Please select a bag size"name="size-select"requiredoptions={sizeOptions}size="small"className="input"value={formValues.coffeeForm.size}onIcChange={(ev)=>handleChange("coffeeForm","size", ev.detail.value)}{...(formValidation&&formValues.coffeeForm.size===""&&{validationText:"Pleasechooseasize",validationStatus:"error",})}/></div><divclassName="input-container"><IcButtonvariant="primary"className="button"onClick={(ev)=>handleClick(ev, next)}data-testid="coffee-submit-btn"> Add to order</IcButton></div></IcSectionContainer>)}{formSteps.enterDetails.current&&(<IcSectionContaineraligned="left">{formValidation &&(<IcAlertvariant="error"heading="Error"message="Please fill in all required fields"announced/>)}<IcTypographyvariant="subtitle-large"> Please enter your details</IcTypography><IcTypographyvariant="body"> Nearly there, we just need a few more details. Purchases must be made by an adult over the age of 18. We will never share your details with fourth parties.</IcTypography><divclassName="input-container"><IcTextFieldlabel="Name"name="name"requiredclassName="input"size="small"value={formValues.detailForm.name}onIcChange={(ev)=>handleChange("detailForm","name", ev.detail.value)}{...(formValidation&&formValues.detailForm.name===""&&{validationText:"Pleaseenteryourname",validationStatus:"error",})}autoFocus/><IcTextFieldlabel="Email"name="email"type="email"requiredsize="small"className="input"value={formValues.detailForm.email}onIcInput={(ev)=>handleChange("detailForm","email", ev.detail.value)}{...(formValidation&&(formValues.detailForm.email===""||!formValues.detailForm.email.includes("@"))&&{validationText:"Pleaseenteranemail",validationStatus:"error",})}data-test-id="email-text-field"/><IcTextFieldlabel="Phone"name="phone"type="number"requiredsize="small"className="input"value={formValues.detailForm.phone}onIcInput={(ev)=>handleChange("detailForm","phone", ev.detail.value)}{...(formValidation&&formValues.detailForm.phone===""&&{validationText:"Pleaseenteranumberonwhichwecancontactyou",validationStatus:"error",})}/></div><divclassName="input-container"><IcCheckboxGroupname="signup"onIcChange={(ev)=>handleChange("detailForm","contact", ev.detail.value)}label="Sign up for notifications about future products?"className="input"><IcCheckboxlabel="SMS"name="sms"value="sms"checked={formValues.detailForm.contact.includes("sms")}/><IcCheckboxlabel="Email"name="email"value="email"checked={formValues.detailForm.contact.includes("email")}/></IcCheckboxGroup></div><divclassName="input-container"></div><divclassName="input-container"><IcButtonvariant="secondary"onClick={(ev)=>handleClick(ev, back)}className="button"> Go Back</IcButton><IcButtonvariant="primary"onClick={(ev)=>handleClick(ev, next)}className="button"data-testid="details-submit-btn"> Add to order</IcButton></div></IcSectionContainer>)}{formSteps.checkout.current&&(<IcSectionContaineraligned="left">{formValidation &&(<IcAlertvariant="error"heading="Error"message="Please fill in all required fields"announced/>)}<IcTypographyvariant="subtitle-large">Last step!</IcTypography><IcTypographyvariant="body"> Please choose a start date for your subscription and agree to the terms and conditions. Feel free to cancel your subscriptions at any time.</IcTypography><divclassName="input-container"><IcDatePickerlabel="When would you like your subscription to start?"className="input"disablePastrequiredsize="small"data-testid="date-picker"value={formValues.checkoutForm.dateToStart}onIcChange={(ev)=>handleChange("checkoutForm","dateToStart", ev.detail.value)}{...(formValidation&&formValues.checkoutForm.dateToStart===""&&{validationText:"Pleasechooseadate",validationStatus:"error",})}/></div><divclassName="input-container"><IcRadioGrouplabel="Please agree to the terms and conditions"name="terms"requiredclassName="input"size="small"onIcChange={(ev)=>handleChange("checkoutForm","terms", ev.detail.value)}{...(((formValidation&&formValues.checkoutForm.terms==="")||(formValidation&&formValues.checkoutForm.terms==="decline"))&&{validationText:"Pleaseagreetothetermsandconditions",validationStatus:"error",})}><IcRadioOptionlabel="Agree"name="agree"value="agree"selected={formValues.checkoutForm.terms==="agree"}/><IcRadioOptionlabel="Decline"name="decline"value="decline"selected={formValues.checkoutForm.terms==="decline"}/></IcRadioGroup></div><divclassName="input-container"><IcButtonvariant="secondary"onClick={(ev)=>handleClick(ev, back)}className="button"> Go Back</IcButton><IcButtonvariant="primary"onClick={handleSubmit}className="button"> Submit order</IcButton><IcToastRegionref={toastRegionEl}><IcToastheading="Thanks for your order!"ref={toastEl}dismissMode="automatic"autoDismissTimeout={3000}variant="success"onIcDismiss={()=>resetForm()}/></IcToastRegion></div></IcSectionContainer>)}</form>
interfaceCoffeeForm{variety: string;grind: string;size: string;}interfaceDetailForm{name: string;email: string;phone: string;contact: string[];}interfaceCheckoutForm{dateToStart: string;terms: string;}exportinterfaceFormValues{coffeeForm:CoffeeForm;detailForm:DetailForm;checkoutForm:CheckoutForm;}exportinterfacechooseCoffee{active: boolean;completed: boolean;current: boolean;disabled: boolean;}exportinterfaceenterDetails{active: boolean;completed: boolean;current: boolean;disabled: boolean;}exportinterfacecheckout{active: boolean;completed: boolean;current: boolean;disabled: boolean;}exportinterfacestepTypes{active: boolean;completed: boolean;current: boolean;disabled: boolean;}exportinterfaceFormSteps{chooseCoffee: chooseCoffee;enterDetails: enterDetails;checkout: checkout;}
import{FormValues,FormSteps}from"./types";exportconst grindOptions =[{value:"whole",label:"Whole Bean (default)"},{value:"filter",label:"Filter",description:"A medium grind ideal for drip and pour-over methods",},{value:"espresso",label:"Espresso",description:"The fine grind size ensures a slow, even extraction",},{value:"aeropress",label:"Aeropress"},{value:"cafetiere",label:"Cafetiere",description:"Coarse grinds work best for brewing methods like the French press and cold brew",},];exportconst sizeOptions =[{value:"250",label:"250g"},{value:"500",label:"500g"},{value:"1000",label:"1000g"},];exportconst next ="next";exportconst back ="back";exportconstinitialFormValues:FormValues={coffeeForm:{variety:"",grind:"whole",size:"",},detailForm:{name:"",email:"",phone:"",contact:["",""],},checkoutForm:{dateToStart:"",terms:"",},};exportconstinitialFormSteps:FormSteps={chooseCoffee:{active:true,completed:false,current:true,disabled:false,},enterDetails:{active:true,completed:false,current:false,disabled:false,},checkout:{active:true,completed:false,current:false,disabled:false,},};
import{IcStepTypes}from"@ukic/web-components";import{ stepTypes }from"./types";exportconst handleSteps =(step: stepTypes):IcStepTypes|undefined=>{return step.current?("current"asIcStepTypes): step.completed?("completed"asIcStepTypes): step.active?("active"asIcStepTypes): step.disabled?("disabled"asIcStepTypes):undefined;};