Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

test(web): add e2e test example to visualizer #1140

Merged
merged 12 commits into from
Sep 13, 2024
7 changes: 6 additions & 1 deletion web/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,9 @@ REEARTH_WEB_AUTH0_AUDIENCE=
REEARTH_WEB_AUTH0_CLIENT_ID=

# Optional
REEARTH_WEB_CESIUM_ION_TOKEN_URL=
REEARTH_WEB_CESIUM_ION_TOKEN_URL=

# E2E for playwright
REEARTH_WEB_E2E_BASEURL=
REEARTH_WEB_E2E_ACCOUNT=
REEARTH_WEB_E2E_ACCOUNT_PASSWORD=
2 changes: 2 additions & 0 deletions web/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,5 @@ __screenshots__
/cesium_ion_token.txt
.idea/*
.yarn/*
e2e/utils/.auth
e2e/testSnapshot
90 changes: 90 additions & 0 deletions web/e2e/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
# Playwright E2E Testing Project

## Project Overview

This project is an end-to-end (E2E) testing suite built using [Playwright](https://playwright.dev/). It follows the Page Object Model (POM) design pattern to organize tests for various components of the `visualizer` module, including `dashboard`, `projectSetting`, and `editor`.

## Project Structure

web
├── e2e
│ ├── pom
│ │ ├── visualizer
│ │ │ ├── dashboard
│ │ │ │ ├── index.ts
│ │ │ │ └── ProjectsPage.ts
│ │ │ ├── projectSetting
│ │ │ │ ├── generalPage.ts
│ │ │ │ └── index.ts
│ │ │ ├── editor
│ │ │ │ ├── index.ts
│ │ │ │ └── MapPage.ts
│ │ └── index.ts
│ ├── utils
│ │ ├── .auth
│ │ ├── config.ts //old setting
│ │ ├── index.ts //old setting
│ │ ├── login.ts //old setting
│ │ ├── setup.ts //old setting
│ │ └── auth.setup.ts
│ ├── dashboard.spec.ts //old testing
│ └── test.spec.ts //testing file
└── README.md

## Prerequisites

- [Node.js](https://nodejs.org/) >= 14.x
- [npm](https://www.npmjs.com/) or [Yarn](https://yarnpkg.com/)

## Setup

### Install Dependencies

To install the required packages, run the following commands:

```bash
cd web
yarn install
npx playwright install //if needed
```

To set required .env for frontend. Recording following variable.

```bash
# Local backend with Auth0 OSS tenant
REEARTH_WEB_E2E_BASEURL=http://localhost:3000
REEARTH_WEB_E2E_ACCOUNT // your visualizer accounnt
REEARTH_WEB_E2E_ACCOUNT_PASSWORD // your visualizer password
```
mulengawilfred marked this conversation as resolved.
Show resolved Hide resolved

## Running Tests

First run api and frontend, And to run all tests:

```bash
yarn e2e
```

## Configuration

- utils/auth.setup.ts

Contains the authentication setup process for the tests. You can define login flows here to ensure tests are executed under authenticated conditions.

## Snapshots

Visual regression testing snapshots will be storied in `e2e/__screenshots__` directory. Test will fail on first run if you don't have the snapshots locally.

## Page Object Model (POM)

The project follows the Page Object Model (POM) pattern, where each page is represented as an object. This improves code maintainability and reusability.

Key Directories:

- dashboard: Manages interactions with the dashboard page via ProjectsPage.ts.
- projectSetting: Handles interactions with the project settings page (generalPage.ts).
- editor: Contains methods for the editor page, including MapPage.ts.

## Memo

We need to improve the implementation later.
40 changes: 40 additions & 0 deletions web/e2e/auth.setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// globalSetup.js
import path, { dirname } from "path";
import { fileURLToPath } from "url";

import { chromium, expect } from "@reearth/e2e/utils";

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const authFile = path.join(__dirname, "./utils/.auth/user.json");

export default async () => {
if (
!process.env.REEARTH_WEB_E2E_ACCOUNT ||
!process.env.REEARTH_WEB_E2E_ACCOUNT_PASSWORD
) {
throw new Error(
"please setup .env for REEARTH_WEB_E2E_ACCOUNT and REEARTH_WEB_E2E_ACCOUNT_PASSWORD"
);
}

const browser = await chromium.launch();
const page = await browser.newPage();
await page.goto(
process.env.REEARTH_WEB_E2E_BASEURL || "http://localhost:3000/"
);
await page
.getByPlaceholder("username/email")
.fill(process.env.REEARTH_WEB_E2E_ACCOUNT);
await page
.getByPlaceholder("your password")
.fill(process.env.REEARTH_WEB_E2E_ACCOUNT_PASSWORD);
await page.getByText("LOG IN").click();
await page.waitForTimeout(10 * 1000);
await page.goto(
process.env.REEARTH_WEB_E2E_BASEURL || "http://localhost:3000/"
);
await expect(page.getByRole("button", { name: "New Project" })).toBeVisible();
await page.context().storageState({ path: authFile });
await browser.close();
};
13 changes: 7 additions & 6 deletions web/e2e/dashboard.spec.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { expect, test } from "@reearth/e2e/utils";
// not in use
// import { expect, test } from "@reearth/e2e/utils";

test("dasboard can be logged in", async ({ page, reearth }) => {
await reearth.initUser();
await reearth.goto(`/dashboard/${reearth.teamId}`);
// test("dasboard can be logged in", async ({ page, reearth }) => {
// await reearth.initUser();
// await reearth.goto(`/dashboard/${reearth.teamId}`);

await expect(page.getByText(`${reearth.userName}'s workspace`)).toBeVisible();
});
// await expect(page.getByText(`${reearth.userName}'s workspace`)).toBeVisible();
// });
64 changes: 64 additions & 0 deletions web/e2e/editorMapAddLayer.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import path, { dirname } from "path";
import { fileURLToPath } from "url";

import { expect, test } from "@reearth/e2e/utils";
import { v4 as uuidv4 } from "uuid";

import pom from "./pom";

//use session, also could set in playwright config
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const authFile = path.join(__dirname, "./utils/.auth/user.json");
test.use({ storageState: authFile });

//test
test("create project and add layer", async ({ page }) => {
const url = process.env.REEARTH_WEB_E2E_BASEURL || "http://localhost:3000/";

await page.goto(url);
await page.waitForSelector("button:text('New Project')");

// create new project and get in
const dashboardPage = new pom.visualizer.dashboard.ProjectsPage(page);
const projectName = "playwright_test_" + uuidv4();
await dashboardPage.createProject(projectName);
await page.waitForSelector(`div:text('${projectName}')`);
await dashboardPage.dbClickInProjectByName(projectName);

// clocs sky box, add new layer
const mapPage = new pom.visualizer.editor.MapPage(page);
const layerTitle = "layer_" + uuidv4();
await mapPage.closeSkyBox();
await page.waitForTimeout(8 * 1000);

// take screenshot
const canvas = page.locator("canvas");
expect.soft(await canvas.screenshot()).toMatchSnapshot("before-action.png", {
maxDiffPixels: 100
});

await mapPage.createNewLayerBySketch(layerTitle);

//add circle
await page.getByText(layerTitle).click();
await mapPage.circleButton.click();
const { x, y } = await mapPage.getLocatorOfCanvs();
await mapPage.createCircleToEarthByLocator(x, y);

//wait result for stateble
await page.waitForTimeout(10 * 1000);

expect
.soft(await canvas.screenshot())
.not.toMatchSnapshot("before-action.png");
expect.soft(await canvas.screenshot()).toMatchSnapshot("after-action.png");

//delete created project
await dashboardPage.goto();
await dashboardPage.intoProjectSettingByName(projectName);
const generalSettingPage = new pom.visualizer.projectSetting.generalPage(
page
);
await generalSettingPage.deleteProject();
});
5 changes: 5 additions & 0 deletions web/e2e/pom/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import * as visualizer from "./visualizer";

export default {
visualizer
};
39 changes: 39 additions & 0 deletions web/e2e/pom/visualizer/dashboard/ProjectsPage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import type { Page, Locator } from "@reearth/e2e/utils";

export default class ProjectsPage {
readonly page: Page;
readonly newProjectButton: Locator;
readonly projectNameInput: Locator;
readonly applyButton: Locator;

constructor(page: Page) {
this.page = page;
this.newProjectButton = this.page.getByText("New Project");
this.projectNameInput = this.page.getByPlaceholder("Text");
this.applyButton = this.page.getByText("Apply");
}

async goto() {
await this.page.goto(`/`);
}

async createProject(name: string) {
await this.newProjectButton.click();
await this.projectNameInput.fill(name);
await this.applyButton.click();
}

async dbClickInProjectByName(name: string) {
await this.page
.locator(`div:has(> div > div > div:text('${name}') ) > :first-child`)
.dblclick();
}

async intoProjectSettingByName(name: string) {
await this.page
.locator(`div:has(> div > div:text('${name}')) button`)
.click();
await this.page.getByText("Project Setting").click();
await this.page.waitForSelector("p:text('Project Name')");
}
}
3 changes: 3 additions & 0 deletions web/e2e/pom/visualizer/dashboard/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import ProjectsPage from "./ProjectsPage";

export { ProjectsPage };
58 changes: 58 additions & 0 deletions web/e2e/pom/visualizer/editor/MapPage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import type { Page, Locator } from "@reearth/e2e/utils";

export default class MapPage {
readonly page: Page;
readonly newLayerButton: Locator;
readonly addSketchLayerButton: Locator;
readonly layerNameInput: Locator;
readonly createButton: Locator;
readonly circleButton: Locator;
readonly skyBoxSelectButton: Locator;

constructor(page: Page) {
this.page = page;
this.newLayerButton = this.page.getByText("New Layer");
this.addSketchLayerButton = this.page.getByText("Add Sketch Layer");
this.layerNameInput = this.page.getByPlaceholder("Text");
this.createButton = this.page.getByRole("button").getByText("Create");
this.circleButton = this.page
.locator("div[direction='column'] > div[direction='row'] button")
.nth(2);
this.skyBoxSelectButton = this.page.locator(
"div:has(> div > p:text('Sky Box')) div:has(> p:text('Description needed.')) > div"
);
}

async goto() {
await this.page.goto(`/`);
}

async createNewLayerBySketch(name: string) {
await this.page.waitForSelector("button:text('New Layer')");
await this.newLayerButton.click();
await this.addSketchLayerButton.click();
await this.layerNameInput.fill(name);
await this.createButton.click();
}

async getLocatorOfCanvs() {
const box = await this.page.locator("canvas").boundingBox();
if (!box) throw new Error("no canvas found");
const x = box.x + box.width / 2;
const y = box.y + box.height / 2;
return { x, y };
}

async createCircleToEarthByLocator(x: number, y: number) {
await this.page.mouse.move(x, y);
await this.page.mouse.click(x, y, { button: "left" });
await this.page.waitForTimeout(5 * 1000);
await this.page.mouse.move(x + 20, y + 20);
await this.page.mouse.click(x + 20, y + 20, { button: "left", delay: 500 });
}

async closeSkyBox() {
await this.page.getByText("Sky").click();
await this.skyBoxSelectButton.click();
}
}
3 changes: 3 additions & 0 deletions web/e2e/pom/visualizer/editor/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import MapPage from "./MapPage";

export { MapPage };
5 changes: 5 additions & 0 deletions web/e2e/pom/visualizer/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import * as dashboard from "./dashboard";
import * as editor from "./editor";
import * as projectSetting from "./projectSetting";

export { dashboard, editor, projectSetting };
28 changes: 28 additions & 0 deletions web/e2e/pom/visualizer/projectSetting/generalPage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import type { Page, Locator } from "@reearth/e2e/utils";

export default class generalPage {
readonly page: Page;
readonly projectNameElement: Locator;

constructor(page: Page) {
this.page = page;

this.projectNameElement = this.page
.locator(
"div:has( > p:text('Please type your project name to continue.')) > p"
)
.first();
}

async deleteProject() {
await this.page.getByText("Delete project").click();
const projectname = await this.projectNameElement.textContent();
const projectNameInput = this.page.locator(
"div:has( > p:text('Please type your project name to continue.')) > div > input"
);
await projectNameInput.fill(projectname as string);
await this.page
.getByText("I am sure I want to delete this project.")
.click();
}
ZTongci marked this conversation as resolved.
Show resolved Hide resolved
}
3 changes: 3 additions & 0 deletions web/e2e/pom/visualizer/projectSetting/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import generalPage from "./generalPage";

export { generalPage };
1 change: 1 addition & 0 deletions web/e2e/utils/config.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
//not in use
export const config = {
api: process.env["REEARTH_WEB_API"],
userId: process.env["REEARTH_WEB_E2E_USER_ID"],
Expand Down
2 changes: 1 addition & 1 deletion web/e2e/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
import { config, getAccessToken, type Config } from "./config";

// eslint-disable-next-line no-restricted-imports
export { expect } from "@playwright/test";
export { expect, chromium, type Page, type Locator } from "@playwright/test";

export type Reearth = {
initUser: () => Promise<{
Expand All @@ -20,9 +20,9 @@
}>;
goto: Page["goto"];
token: string | undefined;
gql: <T = any>(

Check warning on line 23 in web/e2e/utils/index.ts

View workflow job for this annotation

GitHub Actions / ci-web / ci

Unexpected any. Specify a different type
query: string,
variables?: Record<string, any>,

Check warning on line 25 in web/e2e/utils/index.ts

View workflow job for this annotation

GitHub Actions / ci-web / ci

Unexpected any. Specify a different type
options?: { ignoreError?: boolean }
) => Promise<T>;
} & Config;
Expand Down
Loading
Loading