description |
---|
Make sure to name your files using the testName.gui.ts naming convention |
Actions within tests generally boil down to navigation and interaction. For a more thorough overview, head over to the Playwright documentation.
Tests begin with navigating to a specific URL.
await page.goto("https://guardianui.com");
Performing interactions involves two pieces.
- The locator of the object to interact with (for more information, check out the Playwright Locators API documentation).
- The action to take.
By default Playwright waits for an element to be actionable before attempting to perform an action.
// Click the button with text "Click Me!"
await page.locator("button:has-text('Click Me!')").click();
Playwright has a robust suite of actions built into the Locator API, the most common are the following:
Action | Behavior |
---|---|
locator.check() |
Check an input checkbox |
locator.uncheck() |
Uncheck an input checkbox |
locator.click() |
Click an element |
locator.hover() |
Hover mouse over an element |
locator.fill() |
Fill an input form field in one go |
locator.type() |
Fill an input form field character by character |
locator.focus() |
Focus an element |
locator.press() |
Press a single key |
locator.selectOption() |
Select on option from a drop down |
Playwright natively includes test assertions by way of an expect
function. For more information on the available functionality of expect
, check out the Playwright Assertions documentation.
There are generic matchers like toEqual
, toContain
, toBeTruthy
, or toBeFalsy
that perform simple checks on the value or state of a locator.
expect(textLocator).toEqual("Hello world!");
expect(navbarLocator).toBeTruthy();
There are also many async assertions which wait until the expected condition is met before continuing with the test. The most common are the following:
Assertion | Behavior |
---|---|
expect(locator).toBeChecked() |
Assert that the checkbox is checked |
expect(locator).toBeVisible() |
Assert that an element is visible |
expect(locator).toContainText() |
Assert that an element contains certain text |
expect(locator).toHaveAttribute() |
Assert that an element has a certain attribute |
expect(locator).toHaveCount() |
Assert that a list of elements has a certain length |
expect(locator).toHaveText() |
Assert that an element matches certain text |
expect(locator).toHaveValue() |
Assert that an input element matches a certain value |
expect(page).toHaveTitle() |
Assert that the page has a certain title |
expect(page).toHaveURL() |
Assert that the page has a certain URL |
You can use test hooks to define groups of tests, or hooks like test.beforeAll
, test.beforeEach
, test.afterAll
, and test.afterEach
to perform reused sets of actions before/after each test or as setup/teardown for test suites. For more information visit the Playwright Test Hooks documentation.
// tests/example.spec.ts
import { test } from "@guardianui/test";
import { expect } from "@playwright/test";
test.describe("hooks example", () => {
test.beforeEach(async ({ page, gui }) => {
// Initialize Arbitrum fork before each test
await gui.initializeChain(42161);
});
test.afterEach(async ({ page, gui }) => {
// Do something after each test
});
test("example test", async ({ page, gui }) => {
await page.goto("https://guardianui.com");
// Set ETH balance to 1 for the test wallet
await gui.setEthBalance("1000000000000000000");
// Set ARB balance to 1 for the test wallet
await gui.setBalance("0x912ce59144191c1204e64559fe8253a0e49e6548", "1000000000000000000");
// Give Permit2 an allowance to spend 1 ARB
await gui.setAllowance("0x912ce59144191c1204e64559fe8253a0e49e6548", "0x000000000022d473030f116ddee9f6b43ac78ba3", "1000000000000000000");
await expect("text=Guardian").toBeTruthy();
});
});ypoe
There are eight phases to writing your first GuardianUI end-to-end test:
- Forked network initialization
- Navigation to the dApp
- Wallet state mocking
- Wallet connection
- Performing actions within the dApp
- Transaction submission and verification
In order to set network state and perform network interactions without expending real tokens, every test that plans to interact with a live network needs to begin by initializing a forked network. Within this call you can specify the network you wish to fork using its chain ID, and optionally what block number you want to fork at (if you don't specify a block it runs at the latest). We recommend pinning to a block as it increases deterministicness of the tests.
import { test } from "@guardianui/test";
test.describe("Olympus Example Suite", () => {
test("Should stake OHM to gOHM", async ({ page, gui }) => {
// Initialize a fork of Ethereum mainnet (chain ID 1)
await gui.initializeChain(1);
// Complete the rest of the test
});
});
To navigate to a dApp, provide the framework with the live URL where you wish to perform the test. Use the Playwright page.goto(url)
asynchronous function to do this.
import { test } from "@guardianui/test";
test.describe("Olympus Example Suite", () => {
test("Should stake OHM to gOHM", async ({ page, gui }) => {
// Initialize a fork of Ethereum mainnet (chain ID 1)
await gui.initializeChain(1);
// Navigate to the Olympus dApp
await page.goto("https://app.olympusdao.finance/#/stake");
// Complete the rest of the test
});
});
The test wallet injected into the browser during these tests is empty by default. We can provide it with gas tokens, ERC20 tokens, and set allowances in a few quick lines.
import { test } from "@guardianui/test";
test.describe("Olympus Example Suite", () => {
test("Should stake OHM to gOHM", async ({ page, gui }) => {
// Initialize a fork of Ethereum mainnet (chain ID 1)
await gui.initializeChain(1);
// Navigate to the Olympus dApp
await page.goto("https://app.olympusdao.finance/#/stake");
// Set ETH balance
await gui.setEthBalance("100000000000000000000000");
// Set OHM balance
await gui.setAllowance("0x64aa3364f17a4d01c6f1751fd97c2bd3d7e7f1d5", "0xb63cac384247597756545b500253ff8e607a8020", "1000000000000000000000000");
// Set staking contract's approval to spend OHM
await gui.setBalance("0x64aa3364f17a4d01c6f1751fd97c2bd3d7e7f1d5", "1000000000000000000000000");
// Complete the rest of the test
});
});
The wallet connection flow will vary slightly from dApp to dApp, but usually requires locating a "Connect Wallet" button, and then selecting the "Metamask" option in an ensuing modal. While our wallet is not actually Metamask, it is surfaced to apps in a way that looks like Metamask to avoid issues where sites may not have an option to select an injected wallet.
To get a sense of what to write in the test, manually go to your live application and identify the visual and textual elements you need to click to go from the not-connected state to the connected state. Use the "inspect element" tool in browsers to help with this.
See examples in our test examples section.
import { test } from "@guardianui/test";
test.describe("Olympus Example Suite", () => {
test("Should stake OHM to gOHM", async ({ page, gui }) => {
// Initialize a fork of Ethereum mainnet (chain ID 1)
await gui.initializeChain(1);
// Navigate to the Olympus dApp
await page.goto("https://app.olympusdao.finance/#/stake");
// Set ETH balance
await gui.setEthBalance("100000000000000000000000");
// Set OHM balance
await gui.setAllowance("0x64aa3364f17a4d01c6f1751fd97c2bd3d7e7f1d5", "0xb63cac384247597756545b500253ff8e607a8020", "1000000000000000000000000");
// Set staking contract's approval to spend OHM
await gui.setBalance("0x64aa3364f17a4d01c6f1751fd97c2bd3d7e7f1d5", "1000000000000000000000000");
// Connect wallet
await page.waitForSelector("text=Connect Wallet");
await page.locator("text=Connect Wallet").first().click();
await page.waitForSelector("text=Connect Wallet");
await page.locator("text=Connect Wallet").first().click();
await page.locator("text=Metamask").first().click();
// This is specific to Olympus. A side tab is opened upon wallet connection
// so we click out of it
await page.locator("[id='root']").click({ position: {x: 0, y: 0}, force: true });
// Complete the rest of the test
});
});
This will vary drastically from dApp to dApp, but generally requires identifying elements on the web page based on class or ID selectors and clicking or entering information into them.
To get a sense of what to write, manually go to your live application and identify the visual and textual elements you need to click to achieve the desired user interaction. Use the "inspect element" tool in browsers to help with this.
import { test } from "@guardianui/test";
test.describe("Olympus Example Suite", () => {
test("Should stake OHM to gOHM", async ({ page, gui }) => {
// Initialize a fork of Ethereum mainnet (chain ID 1)
await gui.initializeChain(1);
// Navigate to the Olympus dApp
await page.goto("https://app.olympusdao.finance/#/stake");
// Set ETH balance
await gui.setEthBalance("100000000000000000000000");
// Set OHM balance
const ohmToken = "0x64aa3364f17a4d01c6f1751fd97c2bd3d7e7f1d5";
await gui.setAllowance(ohmToken, "0xb63cac384247597756545b500253ff8e607a8020", "1000000000000000000000000");
// Set staking contract's approval to spend OHM
await gui.setBalance(ohmToken, "1000000000000000000000000");
// Connect wallet
await page.waitForSelector("text=Connect Wallet");
await page.locator("text=Connect Wallet").first().click();
await page.waitForSelector("text=Connect Wallet");
await page.locator("text=Connect Wallet").first().click();
await page.locator("text=Metamask").first().click();
// This is specific to Olympus. A side tab is opened upon wallet connection
// so we click out of it
await page.locator("[id='root']").click({ position: {x: 0, y: 0}, force: true });
// Performing actions within the dApp
// Enter OHM input amount
await page.locator("[data-testid='ohm-input']").type("0.1");
// Click the stake button to open the modals
await page.waitForSelector("[data-testid='submit-button']");
await page.locator("[data-testid='submit-button']").click();
// Click the pre-transaction checkbox
await page.waitForSelector("[class='PrivateSwitchBase-input css-1m9pwf3']");
await page.locator("[class='PrivateSwitchBase-input css-1m9pwf3']").click();
// Complete the rest of the test
});
});
One of the primary novel behaviors of the GuardianTest framework is its ability to validate information around network transactions following a site interaction such as a button click. Doing this requires two pieces of information:
- The Playwright locator for the button to interact with.
- The contract address the button click should trigger a transaction with or an ERC20 approval for.
The GuardianTest framework takes care of the button click itself behind the scenes.
import { test } from "@guardianui/test";
test.describe("Olympus Example Suite", () => {
test("Should stake OHM to gOHM", async ({ page, gui }) => {
// Initialize a fork of Ethereum mainnet (chain ID 1)
await gui.initializeChain(1);
// Navigate to the Olympus dApp
await page.goto("https://app.olympusdao.finance/#/stake");
// Set ETH balance
await gui.setEthBalance("100000000000000000000000");
// Set OHM balance
await gui.setAllowance("0x64aa3364f17a4d01c6f1751fd97c2bd3d7e7f1d5", "0xb63cac384247597756545b500253ff8e607a8020", "1000000000000000000000000");
// Set staking contract's approval to spend OHM
await gui.setBalance("0x64aa3364f17a4d01c6f1751fd97c2bd3d7e7f1d5", "1000000000000000000000000");
// Connect wallet
await page.waitForSelector("text=Connect Wallet");
await page.locator("text=Connect Wallet").first().click();
await page.waitForSelector("text=Connect Wallet");
await page.locator("text=Connect Wallet").first().click();
await page.locator("text=Metamask").first().click();
// This is specific to Olympus. A side tab is opened upon wallet connection
// so we click out of it
await page.locator("[id='root']").click({ position: {x: 0, y: 0}, force: true });
// Performing actions within the dApp
// Enter OHM input amount
await page.locator("[data-testid='ohm-input']").type("0.1");
// Click the stake button to open the modals
await page.waitForSelector("[data-testid='submit-button']");
await page.locator("[data-testid='submit-button']").click();
// Click the pre-transaction checkbox
await page.waitForSelector("[class='PrivateSwitchBase-input css-1m9pwf3']");
await page.locator("[class='PrivateSwitchBase-input css-1m9pwf3']").click();
// Submit stake transaction
await gui.validateContractInteraction("[data-testid='submit-modal-button']", "0xb63cac384247597756545b500253ff8e607a8020");
});
});