- Run install script
Install browsers and OS dependencies for Playwright.
yarn test:e2e:install
- Install the VS Code extension (optional)
Playwright Test extension was created specifically to accommodate the needs of e2e testing. Install Playwright Test for VSCode by reading this page. It will help you to debug a problem in tests if needed.
- Run all tests
You can run your tests with the playwright test command. Tests run in headless mode by default meaning no browser window will be opened while running the tests and results will be seen in the terminal.
yarn test:e2e
- Run tests in ui mode
It's highly recommended to run your tests with UI Mode for a better developer experience where you can easily walk through each step of the test and visually see what was happening before, during, and after each step. UI mode also comes with many other features such as the locator picker, watch mode and more.
yarn test:e2e --ui
- Run a single test
To run a single test file pass in the name of the test file that you want to run.
yarn test:e2e playwright/tests/slices/slices.spec.ts
- Run a single test in headed mode
To run your tests in headed mode use the --headed
flag. It's slower but this will give you the ability to visually see, how Playwright interacts with the website.
yarn test:e2e playwright/tests/slices/slices.spec.ts --headed
See other examples on the official documentation.
The HTML Reporter shows you a full report of your tests allowing you to filter the report by browsers, passed tests, failed tests, skipped tests and flaky tests.
To open the local test report:
yarn test:e2e:report
To open a downloaded CI test report from anywhere in your computer:
- Copy-paste the content of the download report in a
playwright-report
folder withinplaywright
folder - Execute the same command as above
Use test()
to create a test. Optionally call test.use()
to specify options to use in a single test file or a test.describe()
group.
With test.use()
, you can configure if you want an onboarded test. Default is onboarded.
Example for user not onboarded:
test.use({ loggedIn: true, onboarded: false });
test("I can ...", async ({ sliceBuilderPage, slicesListPage }) => {
// Test content
});
You can also override default storage values:
Example (redux):
test.use({
onboarded: false,
reduxStorage: {
lastSyncChange: new Date().getTime(),
},
});
test("I can ...", async ({ sliceBuilderPage, slicesListPage }) => {
// Test content
});
Example (new way):
test.use({
onboarded: false,
storage: {
isInAppGuideOpen: true,
},
});
test("I can ...", async ({ sliceBuilderPage, slicesListPage }) => {
// Test content
});
Use the procedures
fixture to mock manager procedure responses:
test("I can ...", async ({ procedures }) => {
await procedures.mock("getState", ({ data }) => {
return {
...data,
customTypes: [],
};
});
});
data
contains the unmocked procedure's response. You can use it in your mocked response.
If you don't need the unmocked procedure's data or don't want the manager to execute the procedure at all, you can disable the unmocked procedure with the execute
option:
test(
"I can ...",
async ({ procedures }) => {
await procedures.mock("project.checkIsTypeScript", () => false);
},
{ execute: false },
);
If you only want the procedure to be mocked a set number of times, set the times
option to the number of times you want it to be mocked:
test("I can ...", async ({ procedures }) => {
await procedures.mock(
"getState",
({ data }) => {
return {
...data,
customTypes: [],
};
},
{ times: 1 },
);
});
You may stack procedure.mock
calls as many times and anywhere you want. The most recent mock for a procedure will be used first.
test("I can ...", async ({ procedures }) => {
await procedures.mock("project.checkIsTypeScript", () => false);
// `project.checkIsTypeScript` will return `false`
// Perform actions...
await procedures.mock("project.checkIsTypeScript", () => true);
// `project.checkIsTypeScript` will now return `true`
});
Caution
Only mock when it's necessary because the state of Slice Machine or the remote repository can change.
We want to ensure tests can be launched on any state of Slice Machine and any state of repository. Mocking will help you do that.
In theory, we want to avoid mocking while doing e2e tests. Smoke tests don't have any mocking but standalone tests can when it's necessary. It improves the DX and reduce the necessary setup that we can have for Smoke tests.
In the context of web development and testing, the Page Object Model (POM) is a design pattern that encourages abstraction of web pages and components. This means that each web page or component should be represented as a class, and the various elements on the page or component should be defined as methods within this class. These classes should be written in the "pages" folder.
This approach has several benefits:
- Maintainability: By encapsulating the page structure and possible interactions with a page or component in one place, any changes to the page or component only need to be made once.
- Readability: Tests become more readable and easier to understand.
- Reusability: You can reuse code across different test cases.
Warning
Never use a locator directly in a test file (getBy...). Always use the Page Object Model design pattern for that.
In order to be sure of what you are targeting, always use the exact
option when possible.
Use true
when you want an exact matching.
Examples:
this.customTypesLink = this.menu.getByRole("link", {
name: "Custom types",
exact: true,
});
this.customTypesLink = this.menu.getByText("Custom types", {
exact: true,
});
Use false
when you deliberately don't want an exact matching.
Examples:
this.customTypesLink = this.menu.getByRole("link", {
name: "Custom types",
exact: false,
});
this.customTypesLink = this.menu.getByText("Custom types", {
exact: false,
});
.toBeVisible()
ensures the element is present and visible to users. Explicitly checking for visibility prevents a false-positive test where the element is in the DOM, but cannot be seen or interacted by the user.
If the element is purposely hidden, use .not.toBeVisible()
.
Out goal is to prevent testing implementation details such as:
this.appVersion = this.menu.locator('a[href="/changelog"] > div:nth-child(2)');
Instead, for example you can use getByTestId
:
this.appVersion = this.menu.getByTestId("slicemachine-version");
Our e2e tests should not break whatever the current state of Slice Machine is. Having existing data or not, staging or production, etc.
When creating tests it's important to test what users can see and do. Start the test name with "I" so you prevent yourself testing implementation details.
Example:
test("I can create a slice", async () => {
// Test content
});
Directly checking that a locator is not visible is not correct if the page is currently loading. The loading blank page will not contain your locator and it will always pass.
Example (bad):
test("I cannot see the updates available warning", async ({
pageTypesTablePage,
}) => {
await pageTypesTablePage.goto();
await expect(pageTypesTablePage.menu.updatesAvailableTitle).not.toBeVisible();
});
Example (good):
test("I cannot see the updates available warning", async ({
pageTypesTablePage,
}) => {
await pageTypesTablePage.goto();
await expect(pageTypesTablePage.menu.appVersion).toBeVisible();
await expect(pageTypesTablePage.menu.updatesAvailableTitle).not.toBeVisible();
});
Note
Include the check within the goto function so you don't need to do it manually every time.
All Amplitude experiments are disabled by default to prevent unexpected test results. Tests should be pass regardless of the configured Prismic repository.
To run tests with an experiment, enable the experiment in the test directly:
test("does something when the experiment is on", async ({ procedures }) => {
procedures.mock("getExperimentVariant", ({ data }) => {
if (data === "example-experiment-name") {
return { value: "on" };
}
});
});