Skip to main content

Cypress Guidebook

What is Cypress?

Cypress is an end-to-end (E2E) testing tool, designed for automated testing of modern web applications. Built with JavaScript, Cypress provides a developer-friendly testing environment with powerful features to help QA teams test web applications easily and efficiently. Cypress provides various functions and methods for carrying out end-to-end tests on an application.

Cypress allows you to set up, write, run and debug tests. The types of tests that you can write are end-to-end tests, component tests, integration tests and unit tests.

To guide the way, the Cypress team has created the Real World App (RWA), a full-stack example application that demonstrates testing with Cypress in practical and realistic scenarios.

Why We Use Cypress?

Tentunya ada alasan dibalik pemilihan cypress sebagai tool testing untuk project web base Arkademi, berikut detail dari alasan-alasan tersebut.

The Initial Goal

The main purpose of using Cypress is to ensure the entire application system works correctly from the user's perspective. From the several types of tests that can be performed using Cypress, for now, testing is only conducted on the E2E test and unit test type.

Why E2E Testing?

  • E2E validates user interaction with the application from start until finish, covering all system components and workflows that the user might go through
  • The initiation of E2E: Aim to explore and improve the quality of Arkademi's products such as avoiding bugs to serve the best performance.
  • User's perspective: it requires a good understanding of how users operate the application.
  • E2E will be used as a reference and standard in maintaining the Arkademi Website which will change in the future both in terms of features and functionality of the Arkademi website.

In detail, here are the purposes of using Cypress:

  1. Core Functionality Validation: E2E testing ensures that the core features of the application function as expected. For example, login or registration works properly, navigation buttons go to the correct page, or the search feature returns appropriate results.
  2. Workflow Testing: E2E testing tests the workflow that a user should go through from start to finish. This includes testing navigation between pages, logging in and out of accounts, and performing other actions that represent everyday user experience.
  3. Compatibility and Responsiveness: E2E testing also includes ensuring that the application functions properly across devices and browsers. This helps ensure that users from different platforms have a uniform and satisfying experience.
  4. System Resilience: E2E testing can also test the system's resilience to extreme situations or unexpected conditions, such as a slow internet connection or server failure.
  5. Data Safety: E2E testing can ensure that sensitive user data is properly enclosed and can only be accessed by authorized users. This includes validation of access permission settings and authentication security.
  6. Reduces the Risk of Bugs: By identifying and fixing bugs before an application is launched, E2E testing helps reduce the risk of bugs and problems that may occur in a production environment.
  7. Improving Product Quality: Overall, E2E testing aims to improve product quality by ensuring that the application functions as users expect and meets established quality standards.

The Final Goal

Setting feature development standards, in this case providing test cases at the beginning of feature development by the prototype provided by the relevant team. This test case will ensure that the features built have specifications that match the initial design.

How to

Install Cypress

Install Cypress via npm:

  1. Make sure node.js is installed.
     node -v
  2. Go to your project.
    cd your/project/path
  3. Install Cypress on the project.
    npm install cypress --save-dev
  4. Run the Cypress.
    npx cypress open

Cypress GUI

The following are the steps for running cypress:

  1. Make sure it is on the project that will be tested.
  2. Make sure Cypress is installed and the testing code is available.
  3. Run command npx cypress open, if the project is run locally, run it simultaneously.
  4. A cypress popup window appears.
  5. Select E2E Testing.
  6. Select the browser for testing.
    tip

    Suggestion: Using Chrome

  7. Select Specs on the left menu.
  8. Select the E2E spec to run.

Developed Cypress Test

Engineers already developed some tests using Cypress, which are detailed below:

Folder Structure

Here is Arkademi's Cypress e2e folder structure:

#NameDescription
1DownloadsSave all files downloaded via the Cypress browser.
2E2EContains the main code used for testing.
3FixtureStores data for use in testing, the data used is generally in .json format.
4ScreenshotsSave screenshots when testing is run (currently test cases run on Cypress Cloud).
5Supportcontains helper codes that are useful for separating unit test code, which can be combined into one larger test case.

Developed E2E Testing

The following are the cypress E2E tests that have currently been implemented:

#NameProject TypeTesting Type
1HomepageRegularunit
2Coursepage JRCRegularunit
3Checkout PageRegularunit
4Course StatusRegularE2E

Write Cypress

Start to Write

The code below describes the test spec, beforeEach function will be run before each part of the test function it is executed.

export default describe("example spec", () => {
beforeEach(() => {
// do something before test
});

it("passes", () => {
// do something
});
});

Visiting a Page

To visit a page, add the URL to cy.visit()

describe("example spec", () => {
it("Visits a Page", () => {
cy.visit("https://example-page");
});
});

Ensure that the destination URL matches the URL code command.

describe('example spec', () => {
it('Visits a Page', () => {
cy.visit('https://example-page')
cy.url().should(‘contain’, 'example-page')
})
})

Queries

Queries itself refer to the methods and commands used to select and interact with elements on a web page. These queries are fundamental for writing tests that verify the behavior and functionality of web applications. Cypress provides a robust set of query commands that allow you to locate elements in various ways, enabling you to simulate user interactions and assert expected outcomes. Below are commonly used queries in Arkademi cypress:

as

Provide another name/alias. Used to store elements, values, and so on.

.as(aliasName)
.as(aliasName, options) // options: query / static

In general, aliases are used directly without requiring additional options. Such as storing data on intercepts (spy & stub network requests and responses).

cy.intercept(`**/course/${course_id}`).as("courseDetail");

The the code above stores the data obtained from the API response as courseDetail and can be used in testing using the .wrap() command.

contain

Search for elements containing the same text in the DOM. can easily find elements but the bad side.

In the code example below it will call a DOM element that contains the word About, however, if there is more than one element in the DOM that contains the same text then both elements will be selected. Here are tips for using it.

cy.get("button").contains("Kirim").should("exist").click();

With the code below, only elements with About text that are inside elements with the .nav class will be called.

cy.get("button").contains("Kirim").should("exist").click();

// or

cy.get(.nav).within(() => {
cy.get("button").contains("Kirim").should("exist").click();
})

get

Get one or more DOM elements by selector or alias. A frequently used command can be used by calling a class or attribute element.

cy.get(selector);
cy.get(alias);
cy.get(selector, options);
cy.get(alias, options);

Can retrieve DOM elements in 4 ways. Examples of use below.

// with tag html
cy.get("ul[class=isi-kelas-course-tab]");

// with tag html & class like
cy.get("ul[class^=isi-kelas-course]");

// with class
cy.get(".isi-kelas-course-tab");

// with custom data-test / data-cy / data-testid
cy.get("ul[data-cy=”cy-isi-kelas-course-tab”]");

In the code example above, it is recommended to use the .get() command to use the data-cy custom attribute so that the test code does not fail when there are changes to the HTML class or tag. Using similar classes is also recommended only if there are many of the same classes in one parent element, for example:

// code
<ul class=”tab-wrapper”>
<li class=”course-tab-active”>tab 1</li>
<li class=”course-tab”>tab 2</li>
</ul>

// cypress
cy.get("ul[class=tab-wrapper]").within(() => {
cy.wrap(@data).each((data, index) => {
cy.get(li[class^=”course-tab”]).eq(index).should(“exist”)
})
})

The the code above checks whether the course-tab elements in UL match the amount of data. By using the code above, all course-tab* elements can be selected.

find

Has almost the same function as .get() but has a different context, find is used more specifically after the .get() command is executed.

cy.get(".form").find("input");

eq

It is indexing if more than one element is selected in the parent element. Generally used to check elements by looping data from response, fixture, or state. The following is an example code for using the .eq() command:

cy.wrap(data?.globalWatchCourse?.watchCourseLampiran).each(
(lampiran: any, index: number) => {
cy.get("li[class='isi-lampiran']")
.eq(index)
.within(() => {
cy.get("div[class='judul']")
.get("h6[class='nama-file']")
.should("have.text", he.decode(lampiran?.namaFile));
cy.get("a[class='tbl-download']").click();
});
}
);

In the code above there is a loop for course data that calls the .get()command to call the isi-lampiran element but according to the index the element is located.

first

Taking the first element in several elements with the same attribute or class is an example of use.

// html
<ul>
<li class=”example-class>example 1</li>
<li class=”example-class>example 2</li>
<li class=”example-class>example 3</li>
<li class=”example-class>example 4</li>
</ul>

// cypress
cy.get(“li[class=’example-class]).fist()

From the code above, the only element selected is the first element, namely <li class="example-class">example 1</li>.

last

Taking the last element in several elements with the same attribute or class is an example of use.

// html
<ul>
<li class=”example-class>example 1</li>
<li class=”example-class>example 2</li>
<li class=”example-class>example 3</li>
<li class=”example-class>example 4</li>
</ul>

// cypress
cy.get(“li[class=’example-class]).fist()

From the code above, the only element selected is the first element, namely <li class="example-class">example 4</li>.

invoke

In Cypress, the invoke() command is used to call a function on a previously yielded subject. This command is particularly useful when need to interact with or assert properties, methods, or values that are not directly accessible through Cypress commands. Berikut ini adalah beberapa contoh penggunaan invoke:

a. Access properties of a DOM element or a JavaScript object.

// Get the value of the 'textContent' property of an element
cy.get("h1").invoke("text");

b. Interact with and manipulate DOM elements

// example change width to greater than 100
cy.get(".box")
.invoke("width")
.then((width) => {
expect(width).to.be.greaterThan(100);
});

// example for change tag <a> attribute
cy.get("a[data-cy='button-review']")
.scrollIntoView()
.invoke("removeAttr", "target")
.should("be.visible")
.click();

In the code above, example 1 manipulates the DOM to change the width to more than 100 and example 2 removes the target attribute in the <a> tag. Invoke can also be used for complex assertions for example:

cy.get("input").invoke("val").should("equal", "expected value");

url()

Get the current URL of the page that is currently active.

// syntax
cy.url()
cy.url(options)

// usage
cy.url() // Yields the current URL as a string

// usage example visiting a page and make sure url is valid
cy.visit('https://example-page')
cy.url().should(‘contain’, 'example-page')

window()

Get the window object of the page that is currently active. Can be used in code testing to retrieve window objects such as store, localStorage and so on.

cy.window();
cy.window(options); // options: log | timeout

// use for calling window store
cy.window()
.its("store")
.invoke("getState")
.then((state) => {
return state;
});

Assertions

Assertions in Cypress are used to verify that the application is behaving as expected. They are statements that check whether a condition is true or false at a specific point. If an assertion fails, Cypress will stop the test and provide feedback on what went wrong, making it easier to identify and fix issues. Cypress offers a range of built-in assertions, which can be used to validate various aspects of your web application, such as element visibility, content, state, and more. Below are some assertions examples:

Content

contain

Asserts that an element contains specific text.

cy.get("h1").should("contain", "Welcome");
have.text

Asserts that an element has exact text.

cy.get("h1").should("have.text", "Welcome to Cypress");

Visibility

be.visible

Asserts that an element is visible.

cy.get("button").should("be.visible");
not.be.visible

Asserts that an element is not visible.

cy.get("button").should("not.be.visible");

Existence

exist

Asserts that an element exists in the DOM

cy.get(".nav-item").should("exist");
not.exist

Asserts that an element does not exist in the DOM.

cy.get(".nav-item").should("not.exist");

Attribute

have.attr

Asserts that an element has a specific attribute with a specific value.

cy.get("input").should("have.attr", "type", "text");
have.class

Asserts that an element has a specific class.

cy.get(".btn").should("have.class", "btn-primary");

State

be.checked

Asserts that a checkbox or radio button is checked.

cy.get('input[type="checkbox"]').should("be.checked");
be.disabled

Asserts that an element is disabled.

cy.get("button").should("be.disabled");

Value

have.value

Asserts that an input element has a specific value.

cy.get("input").should("have.value", "testuser");

Combining assertions (chain) multiple assertions using .and() to combine conditions in a single statement.

cy.get("input")
.should("have.value", "testuser")
.and("be.visible")
.and("have.attr", "placeholder", "Username");

Custom Assertions, Cypress using chai syntax or plugins. Cypress is built on Mocha and Chai, allowing you to extend its capabilities.

cy.get("input").should(($input) => {
expect($input).to.have.length(1);
expect($input).to.have.attr("placeholder", "Username");
});

Actions

Actions are used to interact through input to HTML elements. For example, writing, clicking, hovering, scrolling to selected elements, and so on. This can be seen at Cypress Documentation.

cy.get("div[data-cy='cs-video-container']")
.should("be.visible", { timeout: 10000 })
.trigger("mouseover")
.get("h6[data-cy='player-time']", { timeout: 50000 })
.should("be.visible")
.click();

The code above is an example of a hover element in Cypress, but using actions in Cypress sometimes results in problems such as hover not working properly, therefore there is a recommended library for running this command cypress-real-events. The following is an example of the cypress real events install command, import package and use in the test file.

// run in terminal
> npm i cypress-real-events

// import to support/e2e.js or e2e.ts file
import "cypress-real-events";

// implementation example for playing video course status
cy.get("div[data-cy='cs-video-container']")
.should("be.visible", { timeout: 10000 })
.realHover()
.get("h6[data-cy='player-time']", { timeout: 50000 })
.should("be.visible")
.click();

In using the code above, realHover is used as an alternative to hovering over elements and its use can be seen more easily in the hover documentation by Cypress.

Other Command

Some other commands used for testing are as follows:

log

As the name suggests, it is useful for displaying string logs, data, and so on. Reducing the use of console.log() in test files can be done with the command cy.log(message) or cy.log(message, args...).

each

Perform looping on array data which can be used to test many elements at once, for example in quiz course status where many questions and answers must be chosen. Below is an example of use in Cypress course status.

cy.wrap(quizzes).each((question: any, index) => {
cy.get("ul[class='radio-group-komponen']").within(() => {
cy.wrap(question?.jawaban).each((jawaban: string, indexJawaban) => {
if (question?.idJawaban === indexJawaban) {
cy.get("li[class='radio-button-komponen']")
.eq(indexJawaban)
.should("exist")
.click()
.wait(500);
}
});
});
});

The code above loops through the quizzes and tests the click action on the quiz answer according to the answer data.

fixture

The fixture reads data in several types of files, at Arkademi's the current fixture files are .json and .pdf which are used to store course status data and dummy PDFs for final assignments. The following is an example of using the fixture command.

cy.fixture('courseStatus.json')
cy.fixture('courseStatus.json').as(“dataCourse”)

It will automatically retrieve data from files in the fixture folder on Cypress, complete documentation regarding file types, encoding, options is provided in Cypress Documentation.

reload

Used to reload the active page on Cypress, can be used when there is a process that requires a page refresh if there is an error, for example when user login data does not appear in the browser, command .reload().

cy.reload();
cy.reload(forceReload);
cy.reload(options);
cy.reload(forceReload, options);

then

Allows working with subjects generated from previous commands, for example when using the command .fixture(), .get(), or other custom commands. The following is an example of using them in Arkademi's testing.

// untuk identifikasi element exist
cy.get(".modal-login-pengguna", { timeout: 30000 }).then((modal) => {
if (modal) {
// do something
} else {
// do something else
}
});

// mengolah data state
cy.get_state().then((state) => {
// do something
});

// mengolah data fixture
cy.fixture(“courseStatus.json”).then((dataCourse) => {
// do something
});

// mengolah data .wrap()
cy.wrap(“courseStatus”).then((dataCourse) => {
// do something
})

viewport

Select the desired display in the Cypress browser, a more complete viewport can be seen at viewport | Cypress Documentation. The following is an implementation example.

cy.viewport("macbook-15");

visit

Visiting the desired URL in the test file is usually used at the start of the test run in the .beforeEach() command or when taking action after a process. For example checkout and moving on to the next page.

beforeEach(() => {
cy.viewport("macbook-15");
cy.intercept(`**/course/${course_id}`).as("getCourseDetail");
cy.intercept("**/user/coursestatus/*").as("getUserCourseStatus");
cy.intercept("**/course/viewPertanyaan/*").as("getPertanyaan");
cy.visit(
Cypress.env("baseUrl") +
"/lihat-course/" +
course_id +
"?access_token=" +
user_token
);
cy.wait(5000);
});

wait

The .wait() command is to provide a delay in milliseconds or wait for the data, aka to be resolved. Used when a process needs a certain time before executing the next command to avoid errors in the test file. The .wait() command is to provide a delay in milliseconds or wait for the data, aka to be resolved. Used when a process needs a certain time before executing the next command to avoid errors in the test file.

cy.intercept(
`https://api-course.arkademi.com/api/v1/course/${dataDetail?.i}
/curriculums`
).as("curriculum");

cy.wait("@curriculum").then(({ response }) => {
const dataCurriculum = response?.body?.data;
cy.writeFile(
"cypress/fixtures/dataCurriculum.json",
JSON.stringify(dataCurriculum)
);
});

within

Provides scope to selected child elements, used to prevent elements with the same attribute or class from being accidentally selected. Usually used after .get() followed by the command .within() as an arrow function.

cy.get(“div[class=”form-input-personal-info”]).within(() => {
cy.get(“input[class=’dropdown-input’])
})

In the code above, if there is an input element with the same class on one page, it will not be selected as a monitored element.

wrap

Used to wrap objects, arrays, or aliases. Can be combined with the commands .each(), .invoke(), promises, and assertions. In general use, it is used to wrap data from state or API. The following is an example of .wrap() for state data.

cy.wrap(data?.globalWatchCourse?.watchCourseLampiran).each(
(lampiran: any, index: number) => {
cy.get("li[class='isi-lampiran']")
.eq(index)
.within(() => {
cy.get("div[class='judul']")
.get("h6[class='nama-file']")
.should("have.text", he.decode(lampiran?.namaFile));
cy.get("a[class='tbl-download']").click();
});
}
);

writeFile

Used to write or save files with specific content. For example, a .json`` file into a fixture, or a .txt` file.

cy.writeFile(`cypress/fixtures/${filename}.json`, JSON.stringify(data));

Write E2E Testing for Course Status

The following are Arkademi's Cypress E2E main code files:

#File NameDescription
1_index.tsxDon't do the test here, but save the initial state data needed for the course status e2e test in the course_status.tsx file
2course_status.tsxThe main code file in course status e2e testing, contains a collection of support files for handling testing per course status unit type.
3unit_course_status.tsxThe same as course_status.tsx but is useful for conducting tests only for the desired units using the section index and curriculum sub-section index.

The following is the code that is the essence of cypress course status:

Lifecycle Hook

beforeEach(() => {
cy.viewport("macbook-15");
cy.intercept(`**/course/${course_id}`).as("getCourseDetail");
cy.intercept("**/user/coursestatus/*").as("getUserCourseStatus");
cy.intercept("**/course/viewPertanyaan/*").as("getPertanyaan");
cy.visit(
Cypress.env("baseUrl") +
"/lihat-course/" +
course_id +
"?access_token=" +
user_token
);
cy.wait(5000);
});

beforeEach() function is a lifecycle hook used to execute a specific set of code before each test within a described block. This function helps in setting up the initial state or context required for the tests, ensuring consistency and reducing redundancy by avoiding repetitive code in individual test cases.

Save Redux State Data Into The Fixture

The following is a custom command to save data into the fixture, in the form of a function that saves data from the Redux state with .writeFile().

// command inside test file
it("save", () => {
cy.save_redux_to_fixture();
});

// custom command declaration
declare namespace Cypress {
interface Chainable<Subject> {
get_state(): Chainable<any>;
save_redux_to_fixture(): Chainable<any>;
}
}

// custom command declaration
Cypress.Commands.add("save_redux_to_fixture", () => {
cy.get_state().then((data: any) => {
cy.save_fixture("courseStatus", data);
});
});

// get redux state
Cypress.Commands.add("get_state", () => {
cy.window()
.its("store")
.invoke("getState")
.then((state) => {
return state;
});

The code above saves redux state data into the fixture, is called from a custom command save_redux_to_fixture then runs the custom command get_state which calls the command window to receive the store and then returns the value of that state.

Save Fixture as Json Custom Command

// save json fixture
Cypress.Commands.add("save_fixture", (filename, data) => {
cy.writeFile(`cypress/fixtures/${filename}.json`, JSON.stringify(data));
});

// example
cy.intercept(**/course/{course_id}).as(“course”)
cy.wrap(@course).then((dataCourse) => {
cy.save_fixture(“courseData”, dataCourse)
})

The custom command above functions to make it easier to save data without specifying paths or long code in saving .json data.

Promise on Tests with Dynamic Data

courseStatus &&
courseStatus?.globalWatchCourse?.watchCourseKurikulum.reduce(
async (promise, mainData, mainIndex) => {
await promise;
return await mainData?.isiKelas.reduce(
async (subPromise, subData, subIndex) => {
await subPromise;
return await new Promise((resolve) => {
it(`Test ${mainIndex + 1}.${subIndex + 1}: ${mainData.bagian}
- ${subData?.kelas}`,{ retries: 4 },() => {
// support code
resolve();
});
},
Promise.resolve()
);
},
Promise.resolve()
);

In tests on dynamic data according to API or state and so on, this can be done by creating a promise, the promise will be executed at the beginning before the next command is executed. In this case, the course status has a lot of data in the form of curriculum data. Because .wrap() is followed by .each(), it cannot be used to declare each curriculum unit as a different part of the test, so tracking errors is difficult. After all the test parts in the promise are complete, the other test parts are executed.

Conditional Call of Custom Commands for Each Unit Type

Initiation();
cy.sidebar_selector(mainIndex, subIndex);
cy.get_state().then((state) => {
const courseState = state.globalWatchCourse
.watchCourseKurikulum[mainIndex]
.isiKelas[subIndex];
if (courseState?.jenis === "KUIS" && !courseState?.selesai) {
cy.quiz_spec();
}
if (courseState?.jenis === "VIDEO" &&N!courseState?.selesai){
cy.video_spec();
}

. .. .

});

// check button next
cy.wait(1000).get_state().then((state) => {
const courseTab = state.globalWatchCourse
.watchCourseTab;
const isSelesai = courseTab.selesai;
const isTerakhir = courseTab.kelasTerakhir;
const isVideo = courseTab.jenis === "VIDEO";
const isQuiz = courseTab.jenis === "KUIS";
if (isSelesai && !isTerakhir && !isVideo && !isQuiz) {
cy.get("div[data-cy='cs-next-button']", {timeout: 10000,})
.should("not.have.attr", "disabled");
cy.get("div[data-cy='cs-next-button']").click();
}
});

The code above starts from Initiation(), which is a function that works like Cypress's beforeEach(), placed in the test section because beforeEach() is not called when the test is inside a promise. Next, call the .get_state() command to use the redux state data and retrieve the value from the unit data. The unit data is then used to determine which custom command to use according to the unit type. Finally, the latest state will be called to determine whether the unit is complete or not, not the last unit and the type is not a quiz, if the conditions are met then it will perform the action .click().

Test Submit Complete Class & Submit Class

The final test is run when the test in the promise is finished, this test takes the latest state data and then uses it to determine the course is complete, when it is finished then generates a certificate, waits for loading and ensures the certificate appears in the <img/> element, then click the review button and provide a rating and review of the course then submit the review.

Custom Command Explanation

Each custom command used to test each unit according to the type carries out tasks according to its needs in completing the test. The separation of commands aims to ensure that changes that occur in either features or code can be resolved more quickly according to the unit experiencing changes. And code separation makes the main test file neater. Writing long tests must use custom commands for scalability.

Cypress State Setup

// add this following code to store index file
...
if (window.Cypress) {
window["store"] = store;
}

// also add this code to app.js for react
if (window.Cypress) {
window.Cypress.store = store;
}

Add the two codes above to access the store in the browser. The disadvantage of state cypress is that it can only be run in a local environment.


Writer: Chaton & Media
31 May 2024