Skip to content

Commit

Permalink
Merge branch 'master' of https://github.com/gmx-io/gmx-interface
Browse files Browse the repository at this point in the history
  • Loading branch information
actions-user committed Aug 14, 2024
2 parents 3f61999 + ec8e005 commit d6ceaf4
Show file tree
Hide file tree
Showing 160 changed files with 8,787 additions and 2,150 deletions.
1 change: 1 addition & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
autotests/
10 changes: 10 additions & 0 deletions autotests/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
node_modules
test-results
.env

.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/sdks
!.yarn/versions
42 changes: 42 additions & 0 deletions autotests/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Autotests

This repository contains the autotests for the GMX interface.

## Prerequisites

- Git [Installation guide](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git)
- Node.js >= 20 [Installation guide](https://nodejs.org/en/download/)
- Yarn >= 1.22.5 [Installation guide](https://classic.yarnpkg.com/en/docs/install)

## Install

1. Clone the repository to your local machine.
2. Install the dependencies by running `yarn`.
3. Install playwright dependencies by running `npx playwright install`

## How to run autotests locally

1. Setup `.env` file in same directory as tests with the following content:

```bash
GMX_BASE_URL=<URL to GMX interface for testing>
SEED=<SEED PHRASE FOR WALLET>
PWDEBUG=<true if want to see tests UI>
USE_METAMASK=<true if using real metamask>
PW_WORKERS=<number of workers for parallel tests, preferable 1>
NETWORK=arbitrum or fuji
```
2. Run tests: `yarn test`
## Writing tests
Tests consist of test cases and page objects. The tests are designed to be simple, any complex interactions should be incapsulated in page objects. Each page object is either a [extended Locator](./src/elements/base-page.ts#L5) or inheritor of [BasePage](./src/elements/base-page.ts#12). Locators are extended interface to use only data-qa attributes for locating elements and implementing following API:
- `.selector` - returns locator's selector string including data-qa attribute
- `.waitForSelector()` - waits for element to appear on page
- `.waitForVisible()` - waits for element to be visible
BasePage is a class that provides basic methods for interacting with the page and manipulating locators. These classes are used to keep tests cases clean, declarative and simple, also to reuse same UI interactions in different pages. Each tests accepts [GmxApp object](./src/elements/page-objects.ts#L382) as a parameter, all interactions with APP should be accessible through this object.
Ideally tests should be isolated from each other, so that they can be run in any order and not depend on each other. In other case use `test.describe.serial` to group tests that depend on each other.
21 changes: 21 additions & 0 deletions autotests/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"name": "gmx-ui-autotests",
"version": "1.0.0",
"main": "index.js",
"scripts": {
"test": " npx playwright test",
"postinstall": "npx playwright install"
},
"author": "",
"license": "ISC",
"description": "",
"dependencies": {
"@depay/web3-mock": "^14.18.0",
"@playwright/test": "^1.45.1",
"@tenkeylabs/dappwright": "^2.8.5",
"dotenv": "16.4.5"
},
"devDependencies": {
"@types/node": "^20.14.11"
}
}
13 changes: 13 additions & 0 deletions autotests/playwright.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { PlaywrightTestConfig } from "@playwright/test";

const config: PlaywrightTestConfig = {
timeout: process.env.PW_TIMEOUT ? Number(process.env.PW_TIMEOUT) : 60_000,
testDir: "./src/tests",
testMatch: "*.spec.ts",
workers: process.env.PW_WORKERS ? Number(process.env.PW_WORKERS) : 1,
use: {
viewport: { width: 1600, height: 1200 },
},
};

export default config;
104 changes: 104 additions & 0 deletions autotests/src/base.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { BrowserContext, test as baseTest } from "@playwright/test";
import dappwright, { Dappwright, MetaMaskWallet } from "@tenkeylabs/dappwright";
import { GmxApp } from "./elements/page-objects";

import { config } from "dotenv";
import { mockWeb3 } from "./mocks/web3";

config();

let savedContext: BrowserContext | undefined;
let savedWallet: Dappwright | undefined;

export const test = baseTest.extend<{
context: BrowserContext;
wallet: Dappwright;
appUrl: string;
gmx: GmxApp;
}>({
page: async ({ context }, use) => {
const page = await context.newPage();

if (!process.env.USE_METAMASK) {
/**
* @todo Fix this mock to work with rainbowkit
*/
await mockWeb3(page, () => {
Web3Mock.mock({
blockchain: "ethereum",
accounts: { return: [process.env.ACCOUNT] },
});
});
}

await use(page);
},

context: async ({ appUrl }, use) => {
if (savedContext && savedWallet) {
await use(savedContext);
return;
}

const [wallet, metamaskPage, ctx] = await dappwright.bootstrap("", {
wallet: "metamask",
version: MetaMaskWallet.recommendedVersion,
seed: process.env.SEED,
headless: !Boolean(process.env.PWDEBUG),
});

if (process.env.USE_METAMASK) {
console.log("[Preparing Metamask]");
if (process.env.CHAIN?.toLocaleLowerCase() === "arbitrum") {
await metamaskPage.locator('[data-testid="network-display"]').click();
await metamaskPage.locator('button:has-text("Add Network")').click();
await metamaskPage.locator('//h6[contains(text(), "Arbitrum One")]/../../../*[2]/button').click();
await metamaskPage.locator('[data-testid="confirmation-submit-button"]').click();
await metamaskPage.locator('//button/h6[contains(text(), "Switch to Arbitrum One")]').click();
await metamaskPage.waitForTimeout(1000);
await metamaskPage.locator('[data-testid="detected-token-banner"] button').click();
await metamaskPage.waitForTimeout(1000);
await metamaskPage
.locator('//*[contains(@class, "popover-footer")]/button[contains(text(), "Import")]')
.click();
} else {
await wallet.addNetwork({
chainId: 43113,
networkName: "Avalanche Fuji",
rpc: "https://avalanche-fuji-c-chain.publicnode.com",
symbol: "AVAX",
});
}
}

console.log("[Preparing GMX App]");
const page = await ctx.newPage();
await page.goto(appUrl);
const gmx = new GmxApp(page, wallet, appUrl);
await gmx.header.connectWallet();
await gmx.closeAllToasts();

savedContext = ctx;
savedWallet = wallet;

await use(ctx);
},

wallet: async ({ context }, use) => {
if (savedWallet) {
await use(savedWallet);
return;
}

const metamask = await dappwright.getWallet("metamask", context);

await use(metamask);
},

appUrl: async ({}, use) => use(process.env.GMX_BASE_URL || "https://app.gmx.io"),

gmx: async ({ wallet, page, appUrl }, use) => {
const gmx = new GmxApp(page, wallet, appUrl);
await use(gmx);
},
});
91 changes: 91 additions & 0 deletions autotests/src/elements/base-page.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { Locator, Page } from "@playwright/test";
import { Dappwright } from "@tenkeylabs/dappwright";

declare module "@playwright/test" {
interface Locator {
selector: string;
waitForVisible(): void;
waitForSelector(): void;
waitForDetached(): void;
}
}

export class BasePage {
public root: Locator;
public wallet: Dappwright;

private wrapLocatorWithRoot(locator: Locator, selector: string) {
locator.selector = selector;
locator.waitForVisible = async () => {
if (process.env.PWDEBUG) {
console.log(`Waiting for ${selector} to be visible`);
}

await this.page.waitForSelector(selector, {
state: "visible",
});
};

locator.waitForDetached = async () => {
if (process.env.PWDEBUG) {
console.log(`Waiting for ${selector} to be detached`);
}

await this.page.waitForSelector(selector, {
state: "detached",
});
};

locator.waitForSelector = async () => {
if (process.env.PWDEBUG) {
console.log(`Waiting for selector = ${selector}`);
}

await this.page.waitForSelector(selector);
};

return locator;
}

constructor(
public page: Page,
wallet: Dappwright,
root?: Locator
) {
this.page = page;
this.wallet = wallet;
this.root = root ? root : page.locator("body");
if (root) {
this.root = root;
} else {
this.root = page.locator("body");
this.wrapLocatorWithRoot(this.root, "body");
}
}

locator(selector: string, root?: Locator) {
const locatorSelector =
selector.startsWith(".") || selector.startsWith("//") || selector.startsWith("[")
? selector
: `[data-qa="${selector}"]`;
const locator = (root ?? this.root).locator(locatorSelector);
return this.wrapLocatorWithRoot(locator, locatorSelector);
}

async has(locator: string | Locator, timeout = 5000) {
const selector = typeof locator === "string" ? this.locator(locator).selector : locator.selector;

try {
await this.page.waitForSelector(selector, { state: "attached", timeout });
return true;
} catch (e) {
return false;
}
}

async waitForTransactionToBeSent() {
/**
* @todo Implement me!
*/
}
}
30 changes: 30 additions & 0 deletions autotests/src/elements/common.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { Locator, Page } from "@playwright/test";
import { Dappwright } from "@tenkeylabs/dappwright";
import { BasePage } from "./base-page";

export class Tabs<T extends string> extends BasePage {
tabs: Record<string, Locator>;

setTabs(tabs: Record<T, Locator>) {
this.tabs = tabs;
}

async select(value: T) {
const tab = this.tabs[value];

const style = await tab.evaluate((el) => {
return window.getComputedStyle(el).pointerEvents;
});

if (style !== "none") {
await tab.click();
}
}
}

export class Modal extends BasePage {
constructor(page: Page, wallet: Dappwright, modalAttribute: string) {
super(page, wallet);
this.root = this.locator(modalAttribute);
}
}
Loading

0 comments on commit d6ceaf4

Please sign in to comment.