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

CPF 155 Playwright e2e test framework #156

Open
wants to merge 4 commits into
base: develop
Choose a base branch
from
Open
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
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
10 changes: 8 additions & 2 deletions frontend/src/components/modules/Topbar/Topbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,10 @@ export const Topbar: FC<TopbarProps> = ({ userData }) => {
{/* Profile dropdown */}
<Menu as="div" className="relative ml-3">
<div>
<MenuButton className="bg-gray-800 focus:ring-offset-gray-800 relative flex rounded-full text-sm focus:outline-none focus:ring-2 focus:ring-white focus:ring-offset-2">
<MenuButton
className="bg-gray-800 focus:ring-offset-gray-800 relative flex rounded-full text-sm focus:outline-none focus:ring-2 focus:ring-white focus:ring-offset-2"
data-testid="button-user-menu"
>
<span className="absolute -inset-1.5" />
<span className="sr-only">Open user menu</span>
<Avatar initials={`${firstName[0]}${lastName[0]}`} variant="40" />
Expand All @@ -51,7 +54,10 @@ export const Topbar: FC<TopbarProps> = ({ userData }) => {
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
>
<MenuItems className="ring-black absolute right-0 z-10 mt-2 w-48 origin-top-right rounded-md bg-white py-1 shadow-lg ring-1 ring-opacity-5 focus:outline-none">
<MenuItems
className="ring-black absolute right-0 z-10 mt-2 w-48 origin-top-right rounded-md bg-white py-1 shadow-lg ring-1 ring-opacity-5 focus:outline-none"
data-testid="user-menu-items"
>
{menuItems.map(({ href, label, icon }) => (
<MenuItem key={label}>
<Link
Expand Down
3 changes: 3 additions & 0 deletions playwright/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
test-results/
playwright-report/
.auth/
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/select_element.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { BaseElement } from './base_element';

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

async select(option: string) {
await this.element().getByText(option).click();
}
}
14 changes: 14 additions & 0 deletions playwright/elements/text_element.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
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', '**/*.setup.ts'],
rules: {
'@typescript-eslint/no-unused-vars': 'off',
'playwright/expect-expect': 'off',
},
},
{
ignores: ['node_modules/', 'playwright-report/', 'test-results/'],
},
];
40 changes: 40 additions & 0 deletions playwright/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
{
"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 --trace on",
"test:debug": "playwright test --ui",
"show:report": "playwright show-report"
},
"dependencies": {
"@faker-js/faker": "^9.0.3",
"@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"
}
}
Loading