Skip to content

Commit

Permalink
test: playwright e2e tests
Browse files Browse the repository at this point in the history
  • Loading branch information
Krzysztof Gruszczynski committed Sep 27, 2024
1 parent 83fc2de commit 4e41c25
Show file tree
Hide file tree
Showing 22 changed files with 1,603 additions and 2 deletions.
6 changes: 4 additions & 2 deletions frontend/src/app/(auth)/auth/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export default function LoginPage() {
<div className="flex flex-col gap-x-2">
<label htmlFor="email">Email:</label>
<input
data-testid="input-email"
className="w-full overflow-hidden rounded-lg border border-navy-200 px-2 py-1"
id="email"
name="email"
Expand All @@ -19,6 +20,7 @@ export default function LoginPage() {
<div className="flex flex-col gap-x-2">
<label htmlFor="password">Password:</label>
<input
data-testid="input-password"
className="w-full overflow-hidden rounded-lg border border-navy-200 px-2 py-1"
id="password"
name="password"
Expand All @@ -28,10 +30,10 @@ export default function LoginPage() {
</div>
</div>
<div className="flex flex-col justify-between gap-y-2">
<Button type="submit" formAction={login}>
<Button type="submit" formAction={login} data-testid="button-log-in">
Log in
</Button>
<Button type="submit" formAction={signup}>
<Button type="submit" formAction={signup} data-testid="button-sign-up">
Sign up
</Button>
</div>
Expand Down
2 changes: 2 additions & 0 deletions playwright/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
test-results/
playwright-report/
2 changes: 2 additions & 0 deletions playwright/.husky/pre-commit
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
cd playwright
npm run lint && npm run prettier
4 changes: 4 additions & 0 deletions playwright/.prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Playwright Specific
node_modules/
test-results/
playwright-report
6 changes: 6 additions & 0 deletions playwright/.prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"trailingComma": "es5",
"singleQuote": true,
"printWidth": 100,
"experimentalTernaries": true
}
13 changes: 13 additions & 0 deletions playwright/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Overview

This service helps in E2E testing of the web application.

## Before you start

- Run the app locally from the root directory.
- To run all tests in headless mode use `yarn test`.
- To debug tests use `yarn test:debug` UI mode.

## Coding practices

We use a mix of [Husky](https://github.com/typicode/husky), [ESLint](https://eslint.org/) and [Prettier](https://prettier.io/) within our project to help enforce consistent coding practices.
33 changes: 33 additions & 0 deletions playwright/elements/base_element.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { Locator, expect } from '@playwright/test';
import playwrightObject from '../engine/playwright_object';

export interface Selector {
cssSelector?: string;
label?: string;
testId?: string;
}

export abstract class BaseElement {
constructor(public selector: Selector) {}

element(): Locator {
switch (true) {
case !!this.selector.cssSelector:
return playwrightObject.page().locator(this.selector.cssSelector);
case !!this.selector.label:
return playwrightObject.page().getByLabel(this.selector.label);
case !!this.selector.testId:
return playwrightObject.page().getByTestId(this.selector.testId);
default:
throw new Error('You need to specify some selector');
}
}

public async toBeVisible() {
await this.element().waitFor({ state: 'visible' });
}

public async toHaveText(text: string) {
await expect(this.element()).toHaveText(text);
}
}
15 changes: 15 additions & 0 deletions playwright/elements/button_element.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { BaseElement } from './base_element';

export class ButtonElement extends BaseElement {
constructor(testId: string) {
super({ testId });
}

async click(options?: { force?: boolean; noWaitAfter?: boolean; timeout?: number }) {
await this.element().click(options);
}
}

export function getButtonElement(testId: string): ButtonElement {
return new ButtonElement(testId);
}
27 changes: 27 additions & 0 deletions playwright/elements/input_element.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { expect } from '@playwright/test';
import { BaseElement } from './base_element';

export class InputElement extends BaseElement {
constructor(label: string) {
super({ label });
}

async fill(
value: string,
options?: { force?: boolean; noWaitAfter?: boolean; timeout?: number }
) {
await this.element().fill(value, options);
}

async checkValue(value: string) {
await expect(this.element()).toHaveValue(value);
}

async shouldBeValid() {
await expect(this.element()).toHaveJSProperty('validationMessage', '');
}
}

export function getInputElement(label: string): InputElement {
return new InputElement(label);
}
11 changes: 11 additions & 0 deletions playwright/elements/text_element.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { BaseElement } from './base_element';

export class TextElement extends BaseElement {
constructor(testId: string, public text: string) {
super({ testId });
}

async validateText() {
await this.toHaveText(this.text);
}
}
64 changes: 64 additions & 0 deletions playwright/engine/playwright_object.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { Browser, BrowserContext, Page } from 'playwright-core';

export interface Initialization {
playwrightBrowser?: Browser;
browserContext?: BrowserContext;
page?: Page;
browserName?: string;
}

export class PlaywrightObject {
browser?: Browser;
context?: BrowserContext;
browserName?: string;
private playwrightPage?: Page;

async init(init: Initialization) {
if (this.browser) return;
this.browser = init.playwrightBrowser;
this.context = init.browserContext;
this.playwrightPage = init.page;
}

async initNew(init: Initialization) {
if (!init.playwrightBrowser) {
throw new Error('Cannot start without browser');
}
this.browser = init.playwrightBrowser;
this.browserName = init.browserName;
this.context = await this.browser.newContext();
this.playwrightPage = await this.browser.newPage();
}

async close() {
await this.browser?.close();
this.browser = undefined;
}

async initAll(init: Initialization) {
if (!init.playwrightBrowser) {
throw new Error('Cannot start without browser');
}
if (this.browser) return;
this.browser = init.playwrightBrowser;
this.browserName = init.browserName;
this.context = await this.browser.newContext();
this.playwrightPage = await this.browser.newPage();
}

async open(url: string) {
if (!this.page) {
throw new Error('Cannot open page without context');
}
await this.page().goto(url);
}

page() {
if (!this.playwrightPage) {
throw new Error('Please initialize page first using init() method in Playwright');
}
return this.playwrightPage;
}
}

export default new PlaywrightObject();
12 changes: 12 additions & 0 deletions playwright/engine/test_runner.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { test as base } from '@playwright/test';
import playwrightObject from '@engine/playwright_object';

export const test = base.extend<{ initNew: void }>({
initNew: async ({ browser, browserName }, testFunction) => {
await playwrightObject.initNew({
playwrightBrowser: browser,
browserName: browserName,
});
await testFunction();
},
});
4 changes: 4 additions & 0 deletions playwright/entities/to_to_entity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export interface ToDoEntity {
taskName: string;
isCompleted: boolean;
}
44 changes: 44 additions & 0 deletions playwright/eslint.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import typescriptEslint from '@typescript-eslint/eslint-plugin';
import tsParser from '@typescript-eslint/parser';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import js from '@eslint/js';
import { FlatCompat } from '@eslint/eslintrc';

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const compat = new FlatCompat({
baseDirectory: __dirname,
recommendedConfig: js.configs.recommended,
allConfig: js.configs.all,
});

export default [
js.configs.recommended,

...compat.extends(
'plugin:@typescript-eslint/recommended',
'plugin:@typescript-eslint/stylistic',
'plugin:eslint-plugin-playwright/recommended',
'prettier'
),
{
plugins: {
'@typescript-eslint': typescriptEslint,
},

languageOptions: {
parser: tsParser,
},
},
{
files: ['**/*.spec.ts'],
rules: {
'@typescript-eslint/no-unused-vars': 'off',
'playwright/expect-expect': 'off',
},
},
{
ignores: ['node_modules/', 'playwright-report/', 'test-results/'],
},
];
38 changes: 38 additions & 0 deletions playwright/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
{
"name": "playwright",
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
"type": "module",
"scripts": {
"check-types": "tsc --noemit",
"eslint": "eslint '**/*.{ts, json}' --max-warnings=0",
"lint": "yarn eslint && yarn check-types",
"prepare": "cd .. && husky playwright/.husky",
"prettier": "prettier --check \"**/*.{js,cjs,ts,json,md,yml}\"",
"prettify": "prettier --write \"**/*.{js,cjs,ts,json,md,yml}\"",
"test": "playwright test",
"test:debug": "playwright test --ui"
},
"dependencies": {
"@playwright/test": "^1.47.1",
"lodash": "^4.17.21",
"playwright": "^1.47.1",
"typescript": "^5.6.2"
},
"devDependencies": {
"@types/lodash": "^4.17.9",
"@types/node": "^22.5.5",
"@typescript-eslint/eslint-plugin": "^8.7.0",
"@typescript-eslint/parser": "^8.7.0",
"eslint": "^9.11.1",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-playwright": "^1.6.2",
"husky": "^9.1.6",
"lint-staged": "^15.2.10",
"prettier": "^3.3.3"
},
"lint-staged": {
"**/*": "prettier --write --ignore-unknown"
}
}
60 changes: 60 additions & 0 deletions playwright/page_objects/base_page.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { expect } from '@playwright/test';
import playwrightObject from '../engine/playwright_object';

export abstract class BasePage {
protected constructor(
protected partialUrl?: string,
protected tabName?: string,
public pageSelector?: string
) {}

async open() {
await playwrightObject.open(this.partialUrl as string);
}

async shouldBeOpened() {
await this.waitForLoadState('domcontentloaded');
if (this.pageSelector) {
await this.waitForPageSelector();
}
if (this.partialUrl) {
await this.validateUrl();
}
if (this.tabName) {
await this.validatePageTitle();
}
}

async waitForUrl(url: string) {
await playwrightObject.page().waitForURL(url);
}

async waitForLoadState(
state?: 'load' | 'domcontentloaded' | 'networkidle',
options?: { timeout: number }
) {
await playwrightObject.page().waitForLoadState(state, options);
}

async waitForPageSelector() {
if (!this.pageSelector) {
throw new Error('You need to specify page selector to be a');
}
await playwrightObject.page().locator(this.pageSelector).waitFor();
}

async validateUrl() {
if (!this.partialUrl) {
throw new Error("Can't checkUrl because uri is not specified");
}
await playwrightObject.page().waitForURL(this.partialUrl);
}

async validatePageTitle() {
if (!this.tabName) {
throw new Error("Can't checkTitle because title is not specified");
}
const actualTitle = await playwrightObject.page().title();
expect(actualTitle).toEqual(this.tabName);
}
}
7 changes: 7 additions & 0 deletions playwright/page_objects/dashboard_page.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { BasePage } from './base_page';

export class DashboardPage extends BasePage {
constructor(private url = '/cpf/my-space') {
super(url);
}
}
Loading

0 comments on commit 4e41c25

Please sign in to comment.