Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,10 @@ amplifytools.xcconfig
!.yarn/releases
!.yarn/sdks
!.yarn/versions

# Playwright
/test-results/
/playwright-report/
/blob-report/
/playwright/.cache/
/playwright/.auth/
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,11 @@ yarn cypress:open
>
> Avoid committing the modified `cypress.config.ts` into Git since the CI environments still expect the application to be run on default ports.

### Start Playwright
```shell
yarn test:playwright
```

## Tests

| Type | Location |
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
"@faker-js/faker": "6.1.2",
"@percy/cli": "^1.27.4",
"@percy/cypress": "3.1.6",
"@playwright/test": "^1.58.2",
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^14.0.0",
"@types/bcryptjs": "2.4.2",
Expand Down Expand Up @@ -166,6 +167,7 @@
"cypress:run:component": "cypress run --component",
"cypress:run:mobile": "cypress run --config '{\"e2e\":{\"viewportWidth\":375,\"viewportHeight\":667}}'",
"test": "yarn cypress:open",
"test:playwright": "playwright test",
"test:headless": "yarn cypress:run",
"test:api": "yarn cypress:run --spec 'cypress/tests/api/*'",
"test:unit": "vitest",
Expand Down
30 changes: 30 additions & 0 deletions playwright.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { defineConfig, devices } from "@playwright/test";
import dotenv from "dotenv";
import path from "path";

dotenv.config({ path: path.resolve(__dirname, ".env") });

export default defineConfig({
testDir: "./playwright/tests",
fullyParallel: true,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: "html",

use: {
baseURL: `http://localhost:${process.env.PORT || 3000}`,
testIdAttribute: "data-test",
headless: false,
},

projects: [
{
name: "chromium",
use: { ...devices["Desktop Chrome"] },
},
// {
// name: "mobile",
// use: { ...devices["iPhone 12"] },
// },
],
});
19 changes: 19 additions & 0 deletions playwright/api/test-data.api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { APIRequestContext } from "@playwright/test";
import { User } from "../dto/user.dto";

export class TestDataApi {
testDataPath = "/testData";

constructor(private readonly request: APIRequestContext) {}

async seedDatabase(): Promise<void> {
await this.request.post(`${this.testDataPath}/seed`);
}

async getUsers(): Promise<User[]> {
const response = await this.request.get(`${this.testDataPath}/users`);
const data = await response.json();

return data.results;
}
}
13 changes: 13 additions & 0 deletions playwright/components/input.component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Locator } from "@playwright/test";

export class FieldComponent {
constructor(private readonly root: Locator) {}

get input(): Locator {
return this.root.locator("input");
}

get errorMessage(): Locator {
return this.root.locator("p.Mui-error");
}
}
11 changes: 11 additions & 0 deletions playwright/const/bank-account-data.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export type BankAccountData = {
bankName: string;
routingNumber: string;
accountNumber: string;
};

export const defaultBankAccountData: BankAccountData = {
bankName: "The Best Bank",
routingNumber: "987654321",
accountNumber: "123456789",
};
19 changes: 19 additions & 0 deletions playwright/const/user-data.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
if (!process.env.SEED_DEFAULT_USER_PASSWORD) {
throw new Error("SEED_DEFAULT_USER_PASSWORD is not defined");
}

export const PASSWORD = process.env.SEED_DEFAULT_USER_PASSWORD;

export type UserData = {
firstName: string;
lastName: string;
username: string;
password: string;
};

export const defaultUserData: UserData = {
firstName: "Bob",
lastName: "Ross",
username: "PainterJoy90",
password: PASSWORD,
};
15 changes: 15 additions & 0 deletions playwright/dto/user.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
export type User = {
id: string;
uuid: string;
firstName: string;
lastName: string;
username: string;
password: string;
email: string;
phoneNumber: string;
avatar: string;
defaultPrivacyLevel: string;
balance: number;
createdAt: string;
modifiedAt: string;
};
18 changes: 18 additions & 0 deletions playwright/fixtures/api.fixture.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { test as base, request } from "@playwright/test";
import { APIRequestContext } from "@playwright/test";

type ApiFixtures = {
apiRequest: APIRequestContext;
};

const apiBaseURL = `http://localhost:${process.env.VITE_BACKEND_PORT || 3001}`;

export const apiFixtures = base.extend<ApiFixtures>({
// eslint-disable-next-line no-empty-pattern
apiRequest: async ({}, use) => {
const apiContext = await request.newContext({ baseURL: apiBaseURL });
await use(apiContext);

await apiContext.dispose();
},
});
4 changes: 4 additions & 0 deletions playwright/fixtures/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { userFixtures } from "./user.fixture";

export const test = userFixtures;
export { expect } from "@playwright/test";
15 changes: 15 additions & 0 deletions playwright/fixtures/user.fixture.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { TestDataApi } from "../api/test-data.api";
import { User } from "../dto/user.dto";
import { apiFixtures } from "./api.fixture";

type UserFixtures = {
user: User;
};

export const userFixtures = apiFixtures.extend<UserFixtures>({
user: async ({ apiRequest }, use) => {
const api = new TestDataApi(apiRequest);
const users = await api.getUsers();
await use(users[0]);
},
});
9 changes: 9 additions & 0 deletions playwright/pages/base.page.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { Page } from "@playwright/test";

export class BasePage {
public page: Page;

constructor(page: Page) {
this.page = page;
}
}
7 changes: 7 additions & 0 deletions playwright/pages/home.page.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { BasePage } from "./base.page";

export class HomePage extends BasePage {
get transactions() {
return this.page.getByTestId("transaction-list");
}
}
42 changes: 42 additions & 0 deletions playwright/pages/onboarding.page.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { FieldComponent } from "../components/input.component";
import { BankAccountData } from "../const/bank-account-data";
import { BasePage } from "./base.page";

export class OnboardingPage extends BasePage {
private readonly userOnboardingPrefix = "user-onboarding";
get context() {
return this.page.getByTestId(`${this.userOnboardingPrefix}-dialog`);
}
get nextButton() {
return this.page.getByTestId(`${this.userOnboardingPrefix}-next`);
}
get title() {
return this.page.getByTestId(`${this.userOnboardingPrefix}-dialog-title`);
}

private readonly bankAccountPrefix = "bankaccount";
get bankName() {
return new FieldComponent(this.page.getByTestId(`${this.bankAccountPrefix}-bankName-input`));
}
get accountNumber() {
return new FieldComponent(
this.page.getByTestId(`${this.bankAccountPrefix}-accountNumber-input`)
);
}
get routingNumber() {
return new FieldComponent(
this.page.getByTestId(`${this.bankAccountPrefix}-routingNumber-input`)
);
}
get saveButton() {
return this.page.getByTestId(`${this.bankAccountPrefix}-submit`);
}

async fillBankAccount(data: BankAccountData) {
await this.bankName.input.fill(data.bankName);
await this.routingNumber.input.fill(data.routingNumber);
await this.accountNumber.input.fill(data.accountNumber);

await this.saveButton.click();
}
}
23 changes: 23 additions & 0 deletions playwright/pages/side-nav.page.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { BasePage } from "./base.page";

export class SideNavPage extends BasePage {
private readonly sidenavPrefix = "sidenav";

// Note: only mobile
get toggle() {
return this.page.getByTestId(`${this.sidenavPrefix}-toggle`);
}
get signOutButton() {
return this.page.getByTestId(`${this.sidenavPrefix}-signout`);
}
get notificationsCount() {
return this.page.getByTestId(`${this.sidenavPrefix}-top-notifications-count`);
}

async signOut(isMobile: boolean) {
if (isMobile) {
await this.toggle.click();
}
await this.signOutButton.click();
}
}
43 changes: 43 additions & 0 deletions playwright/pages/signin.page.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { FieldComponent } from "../components/input.component";
import { UrlPath } from "../providers/url-path";
import { BasePage } from "./base.page";

type LoginOptions = {
rememberUser: boolean;
};

export class SignInPage extends BasePage {
private readonly prefix = "signin";

get username() {
return new FieldComponent(this.page.getByTestId(`${this.prefix}-username`));
}
get password() {
return new FieldComponent(this.page.getByTestId(`${this.prefix}-password`));
}
get signInButton() {
return this.page.getByTestId(`${this.prefix}-submit`);
}
get rememberMe() {
return this.page.getByTestId(`${this.prefix}-remember-me`);
}
get signUpLink() {
return this.page.getByTestId("signup");
}
get errorMessage() {
return this.page.getByTestId(`${this.prefix}-error`);
}

async login(username: string, password: string, options?: LoginOptions) {
await this.page.goto(UrlPath.signin);

await this.username.input.fill(username);
await this.password.input.fill(password);

if (options?.rememberUser) {
await this.rememberMe.click();
}

await this.signInButton.click();
}
}
39 changes: 39 additions & 0 deletions playwright/pages/signup.page.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { FieldComponent } from "../components/input.component";
import { UserData } from "../const/user-data";
import { BasePage } from "./base.page";

export class SignUpPage extends BasePage {
private readonly prefix = "signup";

get title() {
return this.page.getByTestId(`${this.prefix}-title`);
}
get firstName() {
return new FieldComponent(this.page.getByTestId(`${this.prefix}-first-name`));
}
get lastName() {
return new FieldComponent(this.page.getByTestId(`${this.prefix}-last-name`));
}
get username() {
return new FieldComponent(this.page.getByTestId(`${this.prefix}-username`));
}
get password() {
return new FieldComponent(this.page.getByTestId(`${this.prefix}-password`));
}
get confirmPassword() {
return new FieldComponent(this.page.getByTestId(`${this.prefix}-confirmPassword`));
}
get signUpButton() {
return this.page.getByTestId(`${this.prefix}-submit`);
}

async signUp(data: UserData) {
await this.firstName.input.fill(data.firstName);
await this.lastName.input.fill(data.lastName);
await this.username.input.fill(data.username);
await this.password.input.fill(data.password);
await this.confirmPassword.input.fill(data.password);

await this.signUpButton.click();
}
}
5 changes: 5 additions & 0 deletions playwright/providers/url-path.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export enum UrlPath {
personal = "/personal",
signin = "/signin",
signup = "/signup",
}
Loading