Skip to content

Commit

Permalink
Feat/Add e2e test with Playwright (#161)
Browse files Browse the repository at this point in the history
# Description
Part of #157 

Add Playwrigh and add the first few tests. The main goals are:

- [x] Add Playwright and make it work locally
- [x] Add a few tests and confirm they are working correctly (with
localhost)
- [x] Support connection to Metamask
- [x] Add a test with API mocks, specifically for subgraph
- [x] Add a test with Odoo and Wallet connection
- [x] Make the API mocks typesafe
- [x] Add documentation

⚠️ There is an issue with how the wallet is handled after being cached
for the e2e test: Synthetixio/synpress#1103

### A few notes:

I started mocking with MSW but it was a painful experience. First,
Playwright doesn't work well with libraries that [override their service
worker
](https://playwright.dev/docs/network#missing-network-events-and-service-workers).
Then, the most promising plugin to support graphql-codegen with MSW and
Playwright does not support multiple endpoints, which is an issue for
testing the legacy flow (like the resolutions).

In the end, I'm fairly happy with the final solution. It's not a lot of
code and it doesn't use new libraries but reuses what we have without
sacrificing functionalities.

---------

Co-authored-by: Andrea Tosatto <[email protected]>
  • Loading branch information
andtos90 and andtos90 authored Feb 22, 2024
1 parent 405c6c0 commit e212288
Show file tree
Hide file tree
Showing 21 changed files with 2,945 additions and 164 deletions.
5 changes: 5 additions & 0 deletions .env.test
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,8 @@ NEXT_PUBLIC_IPFS_ENDPOINT=https://api.neokingdom.org/ipfs/api/v0
# Cookies stuff
COOKIE_NAME=neokingdom
COOKIE_PASSWORD=ohQdJPDC9vN8e7cPMGsWWRqWjvwzTjjz

# E2E stuff
E2E_WALLET_ENDPOINT=test test test test test test test test test test test junk
E2E_ODOO_USERNAME=invalid
E2E_ODOO_PASSWORD=invalid
8 changes: 8 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,11 @@ next-env.d.ts
/public/sw*.js*

.vscode/

# playwright
/test-results/
/playwright-report/
/blob-report/
/playwright/.cache//Users/atosatto/dev/test-playwright/tests

/.cache-synpress/
File renamed without changes.
File renamed without changes.
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,12 @@ Note: if you want to change from i.e. teledisko to neokingdom, just re-run `verc
- When the query is ready or after each update remember to run `pnpm codegen:subgraph`

If the schema on https://api.neokingdom.org/subgraphs/name/NeokingdomDAO/vigodarzere is not working - as the server is down - try with https://api2.neokingdom.org/subgraphs/name/NeokingdomDAO/vigodarzere. You can find in `./codegen-subgraph.ts`

## How to run e2e tests

You can run e2e tests with Playwright in two different ways. In both case remember to start the local server with `pnpm dev`.

The first time you should run `pnpm e2e:init` before running e2e tests. After that you can use one of the following options:

- With the CLI (faster): `pnpm e2e`
- With the UI (easier to debug): `pnpm e2e:ui`
2 changes: 1 addition & 1 deletion codegen-subgraph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { CodegenConfig } from "@graphql-codegen/cli";

const config: CodegenConfig = {
overwrite: true,
schema: "https://api.neokingdom.org/subgraphs/name/NeokingdomDAO/vigodarzere",
schema: process.env.NEXT_PUBLIC_GRAPHQL_ENDPOINT,
documents: "lib/graphql/subgraph/queries/**/*.tsx",
generates: {
"lib/graphql/subgraph/generated/": {
Expand Down
62 changes: 62 additions & 0 deletions e2e/fixtures/get-resolutions-legacy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { GetLegacyResolutionsQuery } from "@graphql/subgraph/generated/graphql";

export const legacyResolutionsMock: GetLegacyResolutionsQuery = {
resolutions: [
{
id: "100",
title: "Title Legacy Resolution 1",
content: "Content Legacy Resolution 1",
isNegative: false,
resolutionType: {
id: "8",
name: "30sNotice3mVoting",
quorum: "66",
noticePeriod: "30",
votingPeriod: "180",
canBeNegative: false,
},
yesVotesTotal: "0",
createTimestamp: "1707852970",
updateTimestamp: null,
approveTimestamp: null,
rejectTimestamp: "1707854760",
executionTimestamp: null,
createBy: "0x0000000000000000000000000000000000000001",
updateBy: null,
approveBy: null,
rejectBy: "0x0000000000000000000000000000000000000002",
hasQuorum: null,
executionTo: [],
executionData: [],
voters: [],
},
{
id: "101",
title: "Title Legacy Resolution 2",
content: "Content Legacy Resolution 2",
isNegative: false,
resolutionType: {
id: "6",
name: "routine",
quorum: "51",
noticePeriod: "259200",
votingPeriod: "172800",
canBeNegative: true,
},
yesVotesTotal: "0",
createTimestamp: "1707505249",
updateTimestamp: null,
approveTimestamp: null,
rejectTimestamp: null,
executionTimestamp: null,
createBy: "0x0000000000000000000000000000000000000003",
updateBy: null,
approveBy: null,
rejectBy: null,
hasQuorum: null,
executionTo: [],
executionData: [],
voters: [],
},
],
};
64 changes: 64 additions & 0 deletions e2e/fixtures/get-resolutions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { GetResolutionsQuery } from "@graphql/subgraph/generated/graphql";

export const resolutionsMock: GetResolutionsQuery = {
resolutions: [
{
id: "1",
title: "Title Resolution 1",
content: "Content Resolution 1",
isNegative: false,
resolutionType: {
id: "8",
name: "30sNotice3mVoting",
quorum: "66",
noticePeriod: "30",
votingPeriod: "180",
canBeNegative: false,
},
yesVotesTotal: "0",
createTimestamp: "1707852970",
updateTimestamp: null,
approveTimestamp: null,
rejectTimestamp: "1707854760",
executionTimestamp: null,
createBy: "0x0000000000000000000000000000000000000001",
updateBy: null,
approveBy: null,
rejectBy: "0x0000000000000000000000000000000000000002",
hasQuorum: null,
executionTo: [],
executionData: [],
addressedContributor: "0x0000000000000000000000000000000000000000",
voters: [],
},
{
id: "2",
title: "Title Resolution 2",
content: "Content Resolution 2",
isNegative: false,
resolutionType: {
id: "6",
name: "routine",
quorum: "51",
noticePeriod: "259200",
votingPeriod: "172800",
canBeNegative: true,
},
yesVotesTotal: "0",
createTimestamp: "1707505249",
updateTimestamp: null,
approveTimestamp: null,
rejectTimestamp: null,
executionTimestamp: null,
createBy: "0x0000000000000000000000000000000000000003",
updateBy: null,
approveBy: null,
rejectBy: null,
hasQuorum: null,
executionTo: [],
executionData: [],
addressedContributor: "0x0000000000000000000000000000000000000000",
voters: [],
},
],
};
64 changes: 64 additions & 0 deletions e2e/testWithMock.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { type TypedDocumentNode } from "@graphql-typed-document-node/core";
import { Page, Route, test as baseTest, expect } from "@playwright/test";

// original from https://www.jayfreestone.com/writing/stubbing-graphql-playwright/
export async function interceptGQL<TResult, TVariables>(
page: Page,
document: TypedDocumentNode<TResult, TVariables>,
resp: TResult,
isLegacy?: boolean,
): Promise<TVariables[]> {
// A list of GQL variables which the handler has been called with.
const reqs: TVariables[] = [];

// Register a new handler which intercepts all GQL requests.
await page.route(
isLegacy ? process.env.NEXT_PUBLIC_LEGACY_GRAPHQL_ENDPOINT || "" : process.env.NEXT_PUBLIC_GRAPHQL_ENDPOINT,
function (route: Route) {
const req = route.request().postDataJSON();
// @ts-ignore wrong types from library
const documentOperationName = document.definitions[0]["name"]["value"] as string;

// Pass along to the previous handler in the chain if the request
// is for a different operation.
if (req.operationName !== documentOperationName) {
return route.fallback();
}

// Store what variables we called the API with.
reqs.push(req.variables);
return route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({ data: resp }),
});
},
);

return reqs;
}

const test = baseTest.extend<{ interceptGQL: typeof interceptGQL }>({
interceptGQL: async ({}, use) => {
await use(interceptGQL);
},
page: async ({ page }, use) => {
await page.route(process.env.NEXT_PUBLIC_GRAPHQL_ENDPOINT, function (route: Route) {
const req = route.request().postDataJSON();
console.warn(`No mock provided for public graphql request: ${req.operationName}`);
route.continue();
});

if (process.env.NEXT_PUBLIC_LEGACY_GRAPHQL_ENDPOINT) {
await page.route(process.env.NEXT_PUBLIC_LEGACY_GRAPHQL_ENDPOINT, function (route: Route) {
const req = route.request().postDataJSON();
console.warn(`No mock provided for public legacy graphql request: ${req.operationName}`);
route.continue();
});
}

await use(page);
},
});

export { test, expect };
52 changes: 52 additions & 0 deletions e2e/tests/login.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { expect, test } from "@playwright/test";
import { MetaMask, testWithSynpress, unlockForFixture } from "@synthetixio/synpress";

import BasicSetup from "../wallet-setup/basic.setup";

const walletTest = testWithSynpress(BasicSetup, unlockForFixture);
const { expect: walletExpect } = test;

test("login with just Odoo", async ({ page }) => {
await page.goto("/");
await page.getByRole("button", { name: "Log in with username and password" }).click();

await page.getByLabel("Odoo Username *").fill(process.env.E2E_ODOO_USERNAME);
await page.getByLabel("Odoo Password *").fill(process.env.E2E_ODOO_PASSWORD);

await page.getByRole("button", { name: "Log in" }).click();
await expect(
page.getByText(
"You are currently just connected through Odoo. Please connect your wallet for seamless interaction within the dapp.",
),
).toBeVisible();
});

// see https://github.com/Synthetixio/synpress/issues/1103
walletTest.fixme("login just with the wallet (account in DAO)", async ({ context, page, extensionId }) => {
const metamask = new MetaMask(context, page, BasicSetup.walletPassword, extensionId);

await page.goto("http://localhost:3000/");
await page.getByRole("button", { name: "Connect wallet" }).click();
await page.getByRole("button", { name: "MetaMask MetaMask" }).first().click();
await metamask.connectToDapp();

await walletExpect(page.getByRole("button", { name: "Log in to odoo with wallet" })).toBeVisible();
await walletExpect(page.getByRole("button", { name: "Log in with username and" })).toBeVisible();
});

// see https://github.com/Synthetixio/synpress/issues/1103
walletTest.fixme("login with both the wallet and Odoo", async ({ context, page, extensionId }) => {
const metamask = new MetaMask(context, page, BasicSetup.walletPassword, extensionId);

await page.goto("http://localhost:3000/");
await page.getByRole("button", { name: "Connect wallet" }).click();
await page.getByRole("button", { name: "MetaMask MetaMask" }).first().click();
await metamask.connectToDapp();

page.getByRole("button", { name: "Log in with username and password" }).click();
await page.getByLabel("Odoo Username *").fill(process.env.E2E_ODOO_USERNAME);
await page.getByLabel("Odoo Password *").fill(process.env.E2E_ODOO_PASSWORD);

await page.getByRole("button", { name: "Log in" }).click();
await walletExpect(page.getByRole("heading", { name: "Resolutions stats" })).toBeVisible();
});
16 changes: 16 additions & 0 deletions e2e/tests/main-menu.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { expect, test } from "@playwright/test";

test("get shareholders link", async ({ page }) => {
await page.goto("/");
await expect(page.getByRole("link", { name: "Shareholders" })).toBeVisible();
});

test("get resolutions link", async ({ page }) => {
await page.goto("/");
await expect(page.getByRole("link", { name: "Resolutions" })).toBeVisible();
});

test("get IBC tool link", async ({ page }) => {
await page.goto("/");
await expect(page.getByRole("link", { name: "IBC tool" })).toBeVisible();
});
20 changes: 20 additions & 0 deletions e2e/tests/resolutions-page.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { GetLegacyResolutionsDocument, GetResolutionsDocument } from "@graphql/subgraph/generated/graphql";

import { resolutionsMock } from "../fixtures/get-resolutions";
import { legacyResolutionsMock } from "../fixtures/get-resolutions-legacy";
import { expect, test } from "../testWithMock";

test("should show at least one resolution", async ({ page, interceptGQL }) => {
await interceptGQL(page, GetResolutionsDocument, resolutionsMock);
await interceptGQL(page, GetLegacyResolutionsDocument, legacyResolutionsMock, true);

await page.goto("/");
await page.getByRole("link", { name: "Resolutions" }).click();

await expect(page.getByRole("link", { name: "Title resolution 2" })).toBeVisible();
await expect(page.getByRole("link", { name: "Title resolution 1" })).not.toBeVisible();

// legacy resolutions
await expect(page.getByRole("link", { name: "Title Legacy Resolution 2" })).toBeVisible();
await expect(page.getByRole("link", { name: "Title Legacy Resolution 1" })).not.toBeVisible();
});
18 changes: 18 additions & 0 deletions e2e/wallet-setup/basic.setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { MetaMask, defineWalletSetup } from "@synthetixio/synpress";
import "dotenv/config";

const DEFAULT_SEED_PHRASE = "test test test test test test test test test test test junk";
const PASSWORD = "SynpressIsAwesomeNow!!!";

export default defineWalletSetup(PASSWORD, async (context, walletPage) => {
const metamask = new MetaMask(context, walletPage, PASSWORD);

await metamask.importWallet(process.env.E2E_WALLET_ENDPOINT || DEFAULT_SEED_PHRASE);

await metamask.addNetwork({
name: "Mumbai",
rpcUrl: "https://rpc-mumbai.maticvigil.com",
chainId: 80001,
symbol: "MATIC",
});
});
3 changes: 3 additions & 0 deletions env.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,8 @@ namespace NodeJS {
NEXT_PUBLIC_ENV: "staging" | "production";
COOKIE_NAME: string;
COOKIE_PASSWORD: string;
E2E_WALLET_ENDPOINT: string;
E2E_ODOO_USERNAME: string;
E2E_ODOO_PASSWORD: string;
}
}
File renamed without changes.
Loading

0 comments on commit e212288

Please sign in to comment.