Skip to content

Commit

Permalink
[playwright] feat: Page object examples (#41)
Browse files Browse the repository at this point in the history
* [playwright] feat: Page object examples
  • Loading branch information
helloitsdave authored May 11, 2024
1 parent cb37717 commit 9ca3046
Show file tree
Hide file tree
Showing 15 changed files with 319 additions and 103 deletions.
17 changes: 17 additions & 0 deletions backend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"license": "ISC",
"devDependencies": {
"@eslint/js": "^9.2.0",
"@faker-js/faker": "^8.4.1",
"@types/cors": "2.8.17",
"@types/express": "4.17.21",
"@types/jsonwebtoken": "^9.0.6",
Expand Down Expand Up @@ -60,7 +61,7 @@
},
"husky": {
"hooks": {
"pre-commit": "pretty-quick --staged"
"pre-commit": "pretty-quick --staged"
}
}
}
}
11 changes: 11 additions & 0 deletions backend/src/routes/userRoutes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,4 +52,15 @@ router.post('/api/users', async (req, res) => {
}
});

router.delete('/api/users/:id', authenticateToken, async (req, res) => {
const { id } = req.params;

try {
await prisma.user.delete({ where: { id } });
res.status(204).send({ deleted: true });
} catch (error) {
res.status(500).send({ error: 'Oops, something went wrong' });
}
});

export default router;
3 changes: 1 addition & 2 deletions backend/tests/service/notes.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { config } from 'dotenv';
import { test, expect, beforeAll } from 'vitest';
import { test, expect, beforeAll, describe } from 'vitest';
import request from 'supertest';
import { describe } from 'node:test';

config();

Expand Down
24 changes: 18 additions & 6 deletions backend/tests/service/users.spec.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
import { config } from 'dotenv';
import { test, expect, beforeAll } from 'vitest';
import { test, expect, beforeAll, describe } from 'vitest';
import request from 'supertest';
import { describe } from 'node:test';
import { faker } from '@faker-js/faker';

config();

const BASE_URL = `${process.env.API_URL}`;
const USERS_URL = `${BASE_URL}/api/users`;

const username = faker.internet.userName().toLowerCase();
const email = faker.internet.email();
const password = faker.internet.password();

let token: string;
let createdID: string;

describe('Unauthenticated Flows', () => {
test('Should not be able to get the list of Users', async () => {
Expand Down Expand Up @@ -51,16 +56,23 @@ describe('Authenticated Flows', () => {
});

test('Create a new User', async () => {
const randomUsername = Math.random().toString(36).substring(7);
const response = await request(USERS_URL)
.post('/')
.set('Authorization', `Bearer ${token}`)
.send({
username: randomUsername,
password: 'n0te$App!23',
email: `${randomUsername}@testing.com`,
username,
password,
email,
});
expect(response.status).toBe(200);
expect(response.body.id).toBeDefined();
createdID = response.body.id;
});

test('Delete a User', async () => {
const response = await request(USERS_URL)
.delete(`/${createdID}`)
.set('Authorization', `Bearer ${token}`);
expect(response.status).toBe(204);
});
});
30 changes: 30 additions & 0 deletions backend/tests/unit/users.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,3 +133,33 @@ describe('Create User', () => {
});
});
});

describe('Delete User', () => {
test('DELETE with id', async ({}) => {
prisma.user.delete.mockResolvedValue({
id: 'gcf89a7e-b941-4f17-bbe0-4e0c8b2cd272',
username: 'Dave',
password: 'check',
email: null,
createdAt: new Date('2024-02-05T23:33:42.252Z'),
updatedAt: new Date('2024-02-05T23:33:42.252Z'),
});
const response = await request(app).delete(
'/api/users/gcf89a7e-b941-4f17-bbe0-4e0c8b2cd272'
);
expect(response.status).toBe(204);
});

test('DELETE with error', async ({}) => {
prisma.user.delete.mockImplementation(() => {
throw new Error('Test error');
});
const response = await request(app).delete(
'/api/users/gcf89a7e-b941-4f17-bbe0-4e0c8b2cd272'
);
expect(response.status).toBe(500);
expect(response.body).toStrictEqual({
error: 'Oops, something went wrong',
});
});
});
5 changes: 5 additions & 0 deletions frontend/src/api/apiService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,3 +83,8 @@ export const createUser = async ( user:
);
return response;
}

export const deleteUser = async (id: string) => {
const response = await api.delete(`users/${id}`);
return response;
}
17 changes: 17 additions & 0 deletions playwright/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions playwright/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"author": "",
"license": "ISC",
"devDependencies": {
"@faker-js/faker": "^8.4.1",
"@playwright/test": "1.42.1",
"@types/node": "20.10.6",
"@typescript-eslint/eslint-plugin": "6.16.0",
Expand Down
29 changes: 29 additions & 0 deletions playwright/pages/LoginPage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { Page } from '@playwright/test';

class LoginPage {
private page: Page;
private defaultTimeout: number = 60 * 1000;

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

usernameInput = () => this.page.getByTestId('username');
passwordInput = () => this.page.getByTestId('password');
loginButton = () => this.page.getByRole('button', { name: 'Login' });
signupLink = () => this.page.getByRole('link', { name: 'Sign up' });

async goto() {
await this.page.goto('/');
await this.page.waitForLoadState('domcontentloaded');
await this.loginButton().waitFor({ timeout: this.defaultTimeout });
}

async login(username: string, password: string) {
await this.usernameInput().fill(username);
await this.passwordInput().fill(password);
await this.page.click('button[type=submit]');
}
}

export default LoginPage;
52 changes: 52 additions & 0 deletions playwright/pages/NotesPage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { Page } from '@playwright/test';

class NotesPage {
private page: Page;

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

spinnerContainer = () => this.page.getByTestId('spinner-container');
addNewNoteButton = () =>
this.page.getByRole('button', { name: 'Add a Note' });
addNoteTitleInput = () => this.page.getByPlaceholder('Title');
addNoteContentInput = () => this.page.getByPlaceholder('Content');
addNoteButton = () => this.page.getByRole('button', { name: 'Add Note' });
noteTitle = () => this.page.getByTestId('note-title');
noteContent = () => this.page.getByTestId('note-content');
deleteNoteButton = () => this.page.getByTestId('note-delete-button');
note = () => this.page.getByTestId('note');
saveNoteButton = () => this.page.getByRole('button', { name: 'Save' });
closeNoteModalButton = () =>
this.page.locator('[class*="modal-close"]').first();

async goto() {
await this.page.goto('/');
await this.page.waitForLoadState('domcontentloaded');
await this.spinnerContainer().waitFor({
state: 'hidden',
timeout: 60 * 1000,
});
}

async addNote(options: { title: string; content: string }) {
await this.addNewNoteButton().click();
await this.addNoteTitleInput().fill(options.title);
await this.addNoteContentInput().fill(options.content);
await this.addNoteButton().click();
}

async deleteNote() {
await this.deleteNoteButton().first().click();
}

async editNote(options: { title: string; content: string }) {
await this.note().first().click();
await this.addNoteTitleInput().fill(options.title);
await this.addNoteContentInput().fill(options.content);
await this.saveNoteButton().click();
}
}

export default NotesPage;
61 changes: 61 additions & 0 deletions playwright/pages/RegistrationPage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { Page } from '@playwright/test';

class RegistrationPage {
/**
* @param {Page} page - Playwright page
*/

private page: Page;
private defaultTimeout: number = 60 * 1000;

constructor(page: Page) {
this.page = page;
}
usernameInput = () => this.page.locator('input[name="username"]');
emailInput = () => this.page.locator('input[name="email"]');
passwordInput = () => this.page.locator('input[name="password"]');
confirmPasswordInput = () =>
this.page.locator('input[name="confirmPassword"]');
registerButton = () => this.page.getByRole('button', { name: 'Register' });
successMessage = () =>
this.page.locator('text="Account created successfully!"');
accountHeader = () =>
this.page.getByRole('heading', { name: 'Register new account' });
errorMessage = () => this.page.locator('.registration-form-error');

goto = async () => {
await this.page.goto('/register');
await this.page.waitForLoadState('domcontentloaded');
await this.registerButton().waitFor({ timeout: this.defaultTimeout });
};

/**
* Register a new user
* @param options - Registration options
* @param options.username - Username
* @param options.email - Email
* @param options.password - Password
* @param options.expectFailure - Expect registration to fail
*/
register = async (options: {
username: string;
email: string;
password: string;
expectFailure?: boolean;
timeout?: number;
}) => {
await this.usernameInput().fill(options.username);
await this.emailInput().fill(options.email);
await this.passwordInput().fill(options.password);
await this.confirmPasswordInput().fill(options.password);
await this.registerButton().click();

if (!options.expectFailure) {
await this.successMessage().waitFor({
timeout: options.timeout || this.defaultTimeout,
});
}
};
}

export default RegistrationPage;
Loading

2 comments on commit 9ca3046

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Coverage for this commit

98.11%

Coverage Report
FileBranchesFuncsLinesUncovered Lines
src
   authenticateToken.ts100%100%100%
   hash.ts100%100%100%
   index.ts50%50%83.33%31–33
   prisma.ts100%100%100%
src/__mocks__
   prisma.ts100%100%100%
src/routes
   loginRoutes.ts100%100%100%
   noteRoutes.ts100%100%100%
   userRoutes.ts100%100%100%

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Coverage for this commit

97.92%

Coverage Report
FileBranchesFuncsLinesUncovered Lines
src
   App.tsx87.50%75%95.95%29–30, 39–40
   NoteApp.tsx94.44%100%96.99%70–74
src/api
   apiService.ts100%85.71%96.67%88–90
src/components
   Header.tsx100%100%100%
   Login.tsx81.82%100%98.59%32, 32–33
   Note.tsx100%100%100%
   NoteForm.tsx100%100%100%
   NoteFormModal.tsx100%100%100%
   NoteGrid.tsx100%100%100%
   RegistrationForm.tsx84.62%100%96.77%51, 51–55
   RegistrationLink.tsx100%100%100%
   Spinner.tsx100%100%100%

Please sign in to comment.