Skip to content

Commit

Permalink
test(web): add e2e test example to visualizer (#1140)
Browse files Browse the repository at this point in the history
Co-authored-by: tcsola <[email protected]>
Co-authored-by: airslice <[email protected]>
  • Loading branch information
3 people authored Sep 13, 2024
1 parent 416f048 commit ff37246
Show file tree
Hide file tree
Showing 19 changed files with 380 additions and 12 deletions.
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
```

## 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();
}
}
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 {
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 Down
Loading

0 comments on commit ff37246

Please sign in to comment.