By default Playwright waits for an element to be actionable before attempting to perform an action.
// Click the button with text "Click Me!"awaitpage.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:
Making General Assertions
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.
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:
Using Hooks
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.tsimport { test } from"@guardianui/test";import { expect } from"@playwright/test";test.describe("hooks example", () => {test.beforeEach(async ({ page, gui }) => {// Initialize Arbitrum fork before each testawaitgui.initializeChain(42161); });test.afterEach(async ({ page, gui }) => {// Do something after each test });test("example test",async ({ page, gui }) => {awaitpage.goto("https://guardianui.com");// Set ETH balance to 1 for the test walletawaitgui.setEthBalance("1000000000000000000");// Set ARB balance to 1 for the test walletawaitgui.setBalance("0x912ce59144191c1204e64559fe8253a0e49e6548","1000000000000000000");// Give Permit2 an allowance to spend 1 ARB await gui.setAllowance("0x912ce59144191c1204e64559fe8253a0e49e6548", "0x000000000022d473030f116ddee9f6b43ac78ba3", "1000000000000000000");
awaitexpect("text=Guardian").toBeTruthy(); });});ypoe
Writing a GuardianTest Test
There are eight phases to writing your first GuardianUI end-to-end test:
NOTE: A common error is encountering ReferenceError: window is not defined. This occurs if you are trying to set wallet state prior to navigating to the page. The order of the first three steps is very important due to the way the wallet gets initialized within the browser.
Forked network initialization
Navigation to the dApp
Wallet state mocking
Wallet connection
Performing actions within the dApp
Transaction submission and verification
Phase 1: Forked Network Initialization
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)awaitgui.initializeChain(1);// Complete the rest of the test });});
Phase 2: Navigation To The dApp
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)awaitgui.initializeChain(1);// Navigate to the Olympus dAppawaitpage.goto("https://app.olympusdao.finance/#/stake");// Complete the rest of the test });});
Phase 3: Wallet State Mocking
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)awaitgui.initializeChain(1);// Navigate to the Olympus dAppawaitpage.goto("https://app.olympusdao.finance/#/stake");// Set ETH balanceawaitgui.setEthBalance("100000000000000000000000");// Set OHM balance await gui.setAllowance("0x64aa3364f17a4d01c6f1751fd97c2bd3d7e7f1d5", "0xb63cac384247597756545b500253ff8e607a8020", "1000000000000000000000000");
// Set staking contract's approval to spend OHMawaitgui.setBalance("0x64aa3364f17a4d01c6f1751fd97c2bd3d7e7f1d5","1000000000000000000000000");// Complete the rest of the test });});
Phase 4: Wallet Connection
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.
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)awaitgui.initializeChain(1);// Navigate to the Olympus dAppawaitpage.goto("https://app.olympusdao.finance/#/stake");// Set ETH balanceawaitgui.setEthBalance("100000000000000000000000");// Set OHM balance await gui.setAllowance("0x64aa3364f17a4d01c6f1751fd97c2bd3d7e7f1d5", "0xb63cac384247597756545b500253ff8e607a8020", "1000000000000000000000000");
// Set staking contract's approval to spend OHMawaitgui.setBalance("0x64aa3364f17a4d01c6f1751fd97c2bd3d7e7f1d5","1000000000000000000000000");// Connect walletawaitpage.waitForSelector("text=Connect Wallet");awaitpage.locator("text=Connect Wallet").first().click();awaitpage.waitForSelector("text=Connect Wallet");awaitpage.locator("text=Connect Wallet").first().click();awaitpage.locator("text=Metamask").first().click();// This is specific to Olympus. A side tab is opened upon wallet connection// so we click out of itawaitpage.locator("[id='root']").click({ position: {x:0, y:0}, force:true });// Complete the rest of the test });});
Phase 5: Performing Actions Within the dApp
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)awaitgui.initializeChain(1);// Navigate to the Olympus dAppawaitpage.goto("https://app.olympusdao.finance/#/stake");// Set ETH balanceawaitgui.setEthBalance("100000000000000000000000");// Set OHM balanceconstohmToken="0x64aa3364f17a4d01c6f1751fd97c2bd3d7e7f1d5";awaitgui.setAllowance(ohmToken,"0xb63cac384247597756545b500253ff8e607a8020","1000000000000000000000000");// Set staking contract's approval to spend OHMawaitgui.setBalance(ohmToken,"1000000000000000000000000");// Connect walletawaitpage.waitForSelector("text=Connect Wallet");awaitpage.locator("text=Connect Wallet").first().click();awaitpage.waitForSelector("text=Connect Wallet");awaitpage.locator("text=Connect Wallet").first().click();awaitpage.locator("text=Metamask").first().click();// This is specific to Olympus. A side tab is opened upon wallet connection// so we click out of itawaitpage.locator("[id='root']").click({ position: {x:0, y:0}, force:true });// Performing actions within the dApp// Enter OHM input amountawaitpage.locator("[data-testid='ohm-input']").type("0.1");// Click the stake button to open the modalsawaitpage.waitForSelector("[data-testid='submit-button']");awaitpage.locator("[data-testid='submit-button']").click();// Click the pre-transaction checkboxawaitpage.waitForSelector("[class='PrivateSwitchBase-input css-1m9pwf3']");awaitpage.locator("[class='PrivateSwitchBase-input css-1m9pwf3']").click();// Complete the rest of the test });});
Phase 6: Transaction Submission and Verification
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)awaitgui.initializeChain(1);// Navigate to the Olympus dAppawaitpage.goto("https://app.olympusdao.finance/#/stake");// Set ETH balanceawaitgui.setEthBalance("100000000000000000000000");// Set OHM balance await gui.setAllowance("0x64aa3364f17a4d01c6f1751fd97c2bd3d7e7f1d5", "0xb63cac384247597756545b500253ff8e607a8020", "1000000000000000000000000");
// Set staking contract's approval to spend OHMawaitgui.setBalance("0x64aa3364f17a4d01c6f1751fd97c2bd3d7e7f1d5","1000000000000000000000000");// Connect walletawaitpage.waitForSelector("text=Connect Wallet");awaitpage.locator("text=Connect Wallet").first().click();awaitpage.waitForSelector("text=Connect Wallet");awaitpage.locator("text=Connect Wallet").first().click();awaitpage.locator("text=Metamask").first().click();// This is specific to Olympus. A side tab is opened upon wallet connection// so we click out of itawaitpage.locator("[id='root']").click({ position: {x:0, y:0}, force:true });// Performing actions within the dApp// Enter OHM input amountawaitpage.locator("[data-testid='ohm-input']").type("0.1");// Click the stake button to open the modalsawaitpage.waitForSelector("[data-testid='submit-button']");awaitpage.locator("[data-testid='submit-button']").click();// Click the pre-transaction checkboxawaitpage.waitForSelector("[class='PrivateSwitchBase-input css-1m9pwf3']");awaitpage.locator("[class='PrivateSwitchBase-input css-1m9pwf3']").click();// Submit stake transaction await gui.validateContractInteraction("[data-testid='submit-modal-button']", "0xb63cac384247597756545b500253ff8e607a8020");
});});