Migrate test suite from Cypress and Jest to Playwright and Vitest

This commit is contained in:
Mike Cao
2026-05-09 01:23:20 -07:00
parent 2fd319f910
commit 216cf6c448
37 changed files with 2218 additions and 3991 deletions
+153
View File
@@ -0,0 +1,153 @@
import { expect, test } from '@playwright/test';
import { teams, users } from './fixtures';
import { type Auth, authHeaders, deleteUser, loginViaApi, umamiUser } from './helpers';
test.describe('Team API tests', () => {
test.describe.configure({ mode: 'serial' });
let auth: Auth;
let teamId = '';
let userId = '';
test.beforeAll(async ({ request }) => {
auth = await loginViaApi(request);
const response = await request.post('/api/users', {
headers: authHeaders(auth),
data: users.userCreate,
});
const body = await response.json();
userId = body.id;
expect(response.status()).toBe(200);
expect(body).toHaveProperty('username', 'playwright1');
expect(body).toHaveProperty('role', 'user');
});
test.afterAll(async ({ request }) => {
if (userId) {
await deleteUser(request, auth, userId);
}
});
test('creates a team', async ({ request }) => {
const response = await request.post('/api/teams', {
headers: authHeaders(auth),
data: teams.teamCreate,
});
const body = await response.json();
teamId = body[0].id;
expect(response.status()).toBe(200);
expect(body[0]).toHaveProperty('name', 'playwright');
expect(body[1]).toHaveProperty('role', 'team-owner');
});
test('gets a team by ID', async ({ request }) => {
const response = await request.get(`/api/teams/${teamId}`, {
headers: authHeaders(auth),
});
const body = await response.json();
expect(response.status()).toBe(200);
expect(body).toHaveProperty('id', teamId);
});
test('updates a team', async ({ request }) => {
const response = await request.post(`/api/teams/${teamId}`, {
headers: authHeaders(auth),
data: teams.teamUpdate,
});
const body = await response.json();
expect(response.status()).toBe(200);
expect(body).toHaveProperty('id', teamId);
expect(body).toHaveProperty('name', 'playwrightUpdate');
});
test('gets all users that belong to a team', async ({ request }) => {
const response = await request.get(`/api/teams/${teamId}/users`, {
headers: authHeaders(auth),
});
const body = await response.json();
expect(response.status()).toBe(200);
expect(body.data[0]).toHaveProperty('id');
expect(body.data[0]).toHaveProperty('teamId');
expect(body.data[0]).toHaveProperty('userId');
expect(body.data[0]).toHaveProperty('user');
});
test('gets a user belonging to a team', async ({ request }) => {
const response = await request.get(`/api/teams/${teamId}/users/${umamiUser.id}`, {
headers: authHeaders(auth),
});
const body = await response.json();
expect(response.status()).toBe(200);
expect(body).toHaveProperty('teamId');
expect(body).toHaveProperty('userId');
expect(body).toHaveProperty('role');
});
test('gets all websites belonging to a team', async ({ request }) => {
const response = await request.get(`/api/teams/${teamId}/websites`, {
headers: authHeaders(auth),
});
const body = await response.json();
expect(response.status()).toBe(200);
expect(body).toHaveProperty('data');
});
test('adds a user to a team', async ({ request }) => {
const response = await request.post(`/api/teams/${teamId}/users`, {
headers: authHeaders(auth),
data: {
userId,
role: 'team-member',
},
});
const body = await response.json();
expect(response.status()).toBe(200);
expect(body).toHaveProperty('userId', userId);
expect(body).toHaveProperty('role', 'team-member');
});
test('updates a user role on a team', async ({ request }) => {
const response = await request.post(`/api/teams/${teamId}/users/${userId}`, {
headers: authHeaders(auth),
data: {
role: 'team-view-only',
},
});
const body = await response.json();
expect(response.status()).toBe(200);
expect(body).toHaveProperty('userId', userId);
expect(body).toHaveProperty('role', 'team-view-only');
});
test('removes a user from a team', async ({ request }) => {
const response = await request.delete(`/api/teams/${teamId}/users/${userId}`, {
headers: authHeaders(auth),
});
expect(response.status()).toBe(200);
});
test('deletes a team', async ({ request }) => {
const response = await request.delete(`/api/teams/${teamId}`, {
headers: authHeaders(auth),
});
const body = await response.json();
teamId = '';
expect(response.status()).toBe(200);
expect(body).toHaveProperty('ok', true);
});
});
+98
View File
@@ -0,0 +1,98 @@
import { expect, test } from '@playwright/test';
import { users } from './fixtures';
import { type Auth, authHeaders, loginViaApi } from './helpers';
test.describe('User API tests', () => {
test.describe.configure({ mode: 'serial' });
let auth: Auth;
let userId = '';
test.beforeAll(async ({ request }) => {
auth = await loginViaApi(request);
});
test('creates a user', async ({ request }) => {
const response = await request.post('/api/users', {
headers: authHeaders(auth),
data: users.userCreate,
});
const body = await response.json();
userId = body.id;
expect(response.status()).toBe(200);
expect(body).toHaveProperty('username', 'playwright1');
expect(body).toHaveProperty('role', 'user');
});
test('returns all users when admin access is used', async ({ request }) => {
const response = await request.get('/api/admin/users', {
headers: authHeaders(auth),
});
const body = await response.json();
expect(response.status()).toBe(200);
expect(body.data[0]).toHaveProperty('id');
expect(body.data[0]).toHaveProperty('username');
expect(body.data[0]).toHaveProperty('password');
expect(body.data[0]).toHaveProperty('role');
});
test('updates a user', async ({ request }) => {
const response = await request.post(`/api/users/${userId}`, {
headers: authHeaders(auth),
data: users.userUpdate,
});
const body = await response.json();
userId = body.id;
expect(response.status()).toBe(200);
expect(body).toHaveProperty('id', userId);
expect(body).toHaveProperty('username', 'playwright1');
expect(body).toHaveProperty('role', 'view-only');
});
test('gets a user by ID', async ({ request }) => {
const response = await request.get(`/api/users/${userId}`, {
headers: authHeaders(auth),
});
const body = await response.json();
expect(response.status()).toBe(200);
expect(body).toHaveProperty('id', userId);
expect(body).toHaveProperty('username', 'playwright1');
expect(body).toHaveProperty('role', 'view-only');
});
test('deletes a user', async ({ request }) => {
const response = await request.delete(`/api/users/${userId}`, {
headers: authHeaders(auth),
});
const body = await response.json();
expect(response.status()).toBe(200);
expect(body).toHaveProperty('ok', true);
});
test('gets all websites that belong to a user', async ({ request }) => {
const response = await request.get(`/api/users/${userId}/websites`, {
headers: authHeaders(auth),
});
const body = await response.json();
expect(response.status()).toBe(200);
expect(body).toHaveProperty('data');
});
test('gets all teams that belong to a user', async ({ request }) => {
const response = await request.get(`/api/users/${userId}/teams`, {
headers: authHeaders(auth),
});
const body = await response.json();
expect(response.status()).toBe(200);
expect(body).toHaveProperty('data');
});
});
+152
View File
@@ -0,0 +1,152 @@
import { expect, test } from '@playwright/test';
import { uuid } from '../../src/lib/crypto';
import { teams, websites } from './fixtures';
import { type Auth, authHeaders, deleteTeam, loginViaApi } from './helpers';
test.describe('Website API tests', () => {
test.describe.configure({ mode: 'serial' });
let auth: Auth;
let websiteId = '';
let teamId = '';
test.beforeAll(async ({ request }) => {
auth = await loginViaApi(request);
const response = await request.post('/api/teams', {
headers: authHeaders(auth),
data: teams.teamCreate,
});
const body = await response.json();
teamId = body[0].id;
expect(response.status()).toBe(200);
expect(body[0]).toHaveProperty('name', 'playwright');
expect(body[1]).toHaveProperty('role', 'team-owner');
});
test.afterAll(async ({ request }) => {
if (teamId) {
await deleteTeam(request, auth, teamId);
}
});
test('creates a website for user', async ({ request }) => {
const response = await request.post('/api/websites', {
headers: authHeaders(auth),
data: websites.websiteCreate,
});
const body = await response.json();
websiteId = body.id;
expect(response.status()).toBe(200);
expect(body).toHaveProperty('name', 'Playwright Website');
expect(body).toHaveProperty('domain', 'playwright.com');
});
test('creates a website for team', async ({ request }) => {
const response = await request.post('/api/websites', {
headers: authHeaders(auth),
data: {
name: 'Team Website',
domain: 'teamwebsite.com',
teamId,
},
});
const body = await response.json();
expect(response.status()).toBe(200);
expect(body).toHaveProperty('name', 'Team Website');
expect(body).toHaveProperty('domain', 'teamwebsite.com');
});
test('creates a website with a fixed ID', async ({ request }) => {
const fixedId = uuid();
const response = await request.post('/api/websites', {
headers: authHeaders(auth),
data: { ...websites.websiteCreate, id: fixedId },
});
const body = await response.json();
expect(response.status()).toBe(200);
expect(body).toHaveProperty('id', fixedId);
expect(body).toHaveProperty('name', 'Playwright Website');
expect(body).toHaveProperty('domain', 'playwright.com');
await request.delete(`/api/websites/${fixedId}`, {
headers: authHeaders(auth),
});
});
test('returns all tracked websites', async ({ request }) => {
const response = await request.get('/api/websites', {
headers: authHeaders(auth),
});
const body = await response.json();
expect(response.status()).toBe(200);
expect(body.data[0]).toHaveProperty('id');
expect(body.data[0]).toHaveProperty('name');
expect(body.data[0]).toHaveProperty('domain');
});
test('gets a website by ID', async ({ request }) => {
const response = await request.get(`/api/websites/${websiteId}`, {
headers: authHeaders(auth),
});
const body = await response.json();
expect(response.status()).toBe(200);
expect(body).toHaveProperty('name', 'Playwright Website');
expect(body).toHaveProperty('domain', 'playwright.com');
});
test('updates a website', async ({ request }) => {
const response = await request.post(`/api/websites/${websiteId}`, {
headers: authHeaders(auth),
data: websites.websiteUpdate,
});
const body = await response.json();
websiteId = body.id;
expect(response.status()).toBe(200);
expect(body).toHaveProperty('name', 'Playwright Website Updated');
expect(body).toHaveProperty('domain', 'playwrightupdated.com');
});
test('updates a website with only shareId', async ({ request }) => {
const response = await request.post(`/api/websites/${websiteId}`, {
headers: authHeaders(auth),
data: { shareId: 'ABCDEF' },
});
const body = await response.json();
expect(response.status()).toBe(200);
expect(body).toHaveProperty('shareId', 'ABCDEF');
});
test('resets a website by removing all data related to the website', async ({ request }) => {
const response = await request.post(`/api/websites/${websiteId}/reset`, {
headers: authHeaders(auth),
});
const body = await response.json();
expect(response.status()).toBe(200);
expect(body).toHaveProperty('ok', true);
});
test('deletes a website', async ({ request }) => {
const response = await request.delete(`/api/websites/${websiteId}`, {
headers: authHeaders(auth),
});
const body = await response.json();
websiteId = '';
expect(response.status()).toBe(200);
expect(body).toHaveProperty('ok', true);
});
});
+31
View File
@@ -0,0 +1,31 @@
export const users = {
userCreate: {
username: 'playwright1',
password: 'password',
role: 'user',
},
userUpdate: {
username: 'playwright1',
role: 'view-only',
},
};
export const teams = {
teamCreate: {
name: 'playwright',
},
teamUpdate: {
name: 'playwrightUpdate',
},
};
export const websites = {
websiteCreate: {
name: 'Playwright Website',
domain: 'playwright.com',
},
websiteUpdate: {
name: 'Playwright Website Updated',
domain: 'playwrightupdated.com',
},
};
+120
View File
@@ -0,0 +1,120 @@
import { type APIRequestContext, expect, type Page } from '@playwright/test';
import { uuid } from '../../src/lib/crypto';
export type Auth = {
token: string;
authorization: string;
};
export const umamiUser = {
username: process.env.UMAMI_USER ?? 'admin',
password: process.env.UMAMI_PASSWORD ?? 'umami',
id: process.env.UMAMI_USER_ID ?? '41e2b680-648e-4b09-bcd7-3e2b10c06264',
};
export const authHeaders = (auth: Auth) => ({
'Content-Type': 'application/json',
Authorization: auth.authorization,
});
export async function loginViaApi(
request: APIRequestContext,
username = umamiUser.username,
password = umamiUser.password,
): Promise<Auth> {
const response = await request.post('/api/auth/login', {
data: { username, password },
});
expect(response.status()).toBe(200);
const body = await response.json();
return {
token: body.token,
authorization: `bearer ${body.token}`,
};
}
export async function loginPage(page: Page, request: APIRequestContext): Promise<Auth> {
const auth = await loginViaApi(request);
await page.addInitScript(token => {
window.localStorage.setItem('umami.auth', JSON.stringify(token));
}, auth.token);
return auth;
}
export async function logout(page: Page) {
await page.getByTestId('button-profile').click();
await page.getByTestId('item-logout').click();
await expect(page).toHaveURL(/\/login$/);
}
export async function addWebsite(
request: APIRequestContext,
auth: Auth,
name: string,
domain: string,
) {
const response = await request.post('/api/websites', {
headers: authHeaders(auth),
data: {
id: uuid(),
createdBy: umamiUser.id,
name,
domain,
},
});
expect(response.status()).toBe(200);
}
export async function deleteWebsite(request: APIRequestContext, auth: Auth, websiteId: string) {
const response = await request.delete(`/api/websites/${websiteId}`, {
headers: authHeaders(auth),
});
expect(response.status()).toBe(200);
}
export async function addUser(
request: APIRequestContext,
auth: Auth,
username: string,
password: string,
role: string,
) {
const response = await request.post('/api/users', {
headers: authHeaders(auth),
data: { username, password, role },
});
expect(response.status()).toBe(200);
}
export async function deleteUser(request: APIRequestContext, auth: Auth, userId: string) {
const response = await request.delete(`/api/users/${userId}`, {
headers: authHeaders(auth),
});
expect(response.status()).toBe(200);
}
export async function addTeam(request: APIRequestContext, auth: Auth, name: string) {
const response = await request.post('/api/teams', {
headers: authHeaders(auth),
data: { name },
});
expect(response.status()).toBe(200);
}
export async function deleteTeam(request: APIRequestContext, auth: Auth, teamId: string) {
const response = await request.delete(`/api/teams/${teamId}`, {
headers: authHeaders(auth),
});
expect(response.status()).toBe(200);
}
+29
View File
@@ -0,0 +1,29 @@
import { expect, test } from '@playwright/test';
import { logout, umamiUser } from './helpers';
test.describe('Login tests', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/login');
});
test('logs user in with correct credentials and logs user out', async ({ page }) => {
await page.getByTestId('input-username').locator('input').fill(umamiUser.username);
await page.getByTestId('input-password').locator('input').fill(umamiUser.password);
await page.getByTestId('button-submit').click();
await expect(page).toHaveURL(/\/dashboard$/);
await logout(page);
});
test('shows validation for blank inputs or incorrect credentials', async ({ page }) => {
await page.getByTestId('button-submit').click();
await expect(page.getByText(/Required/i)).toBeVisible();
await page.getByTestId('input-username').locator('input').fill(umamiUser.username);
await page.getByTestId('input-password').locator('input').fill('wrongpassword');
await page.getByTestId('button-submit').click();
await expect(page.getByText(/Incorrect username and\/or password./i)).toBeVisible();
});
});
+59
View File
@@ -0,0 +1,59 @@
import { expect, test } from '@playwright/test';
import { loginPage, logout } from './helpers';
test.describe('User tests', () => {
test.describe.configure({ mode: 'serial' });
test.beforeEach(async ({ page, request }) => {
await loginPage(page, request);
await page.goto('/settings/users');
});
test('adds a user', async ({ page }) => {
await expect(page.getByText(/Create user/i)).toBeVisible();
await page.getByTestId('button-create-user').click();
await page.getByTestId('input-username').locator('input').fill('Test-user');
await page.getByTestId('input-password').locator('input').fill('testPasswordPlaywright');
await page.getByTestId('dropdown-role').click();
await page.getByTestId('dropdown-item-user').click();
await page.getByTestId('button-submit').click();
await expect(page.locator('td[label="Username"]')).toContainText('Test-user');
await expect(page.locator('td[label="Role"]')).toContainText('User');
});
test('edits a user role and password', async ({ page }) => {
const userRow = page.locator('table tbody tr').filter({
has: page.locator('td', { hasText: /Test-user/i }),
});
await userRow.getByTestId('link-button-edit').click();
await page.getByTestId('input-password').locator('input').fill('newPassword');
await page.getByTestId('dropdown-role').click();
await page.getByTestId('dropdown-item-viewOnly').click();
await page.getByTestId('button-submit').click();
await page.goto('/settings/users');
await expect(
page.locator('table tbody tr').filter({ has: page.locator('td', { hasText: /Test-user/i }) }),
).toContainText('View only');
await logout(page);
await page.getByTestId('input-username').locator('input').fill('Test-user');
await page.getByTestId('input-password').locator('input').fill('newPassword');
await page.getByTestId('button-submit').click();
await expect(page).toHaveURL(/\/dashboard$/);
});
test('deletes a user', async ({ page }) => {
const userRow = page.locator('table tbody tr').filter({
has: page.locator('td', { hasText: /Test-user/i }),
});
await userRow.getByTestId('button-delete').click();
await expect(page.getByText(/Are you sure you want to delete Test-user?/i)).toBeVisible();
await page.getByTestId('button-confirm').click();
});
});
+73
View File
@@ -0,0 +1,73 @@
import { expect, test } from '@playwright/test';
import { addWebsite, deleteWebsite, loginPage } from './helpers';
test.describe('Website tests', () => {
test('adds a website', async ({ page, request }) => {
const auth = await loginPage(page, request);
await page.goto('/settings/websites');
await page.getByTestId('button-website-add').click();
await expect(page.getByText(/Add website/i)).toBeVisible();
await page.getByTestId('input-name').locator('input').fill('Add test');
await page.getByTestId('input-domain').locator('input').fill('addtest.com');
await page.getByTestId('button-submit').click();
await expect(page.locator('td[label="Name"]')).toContainText('Add test');
await expect(page.locator('td[label="Domain"]')).toContainText('addtest.com');
await page.getByTestId('link-button-edit').first().click();
await expect(page.getByText(/Details/i)).toBeVisible();
const websiteId = await page.getByTestId('text-field-websiteId').locator('input').inputValue();
await deleteWebsite(request, auth, websiteId);
await page.goto('/settings/websites');
await expect(page.getByText(/Add test/i)).toHaveCount(0);
});
test('edits a website', async ({ page, request }) => {
const auth = await loginPage(page, request);
await addWebsite(request, auth, 'Update test', 'updatetest.com');
await page.goto('/settings/websites');
await page.getByTestId('link-button-edit').first().click();
await expect(page.getByText(/Details/i)).toBeVisible();
await page.getByTestId('input-name').locator('input').fill('Updated website');
await page.getByTestId('input-domain').locator('input').fill('updatedwebsite.com');
await page.getByTestId('button-submit').click();
await expect(page.getByTestId('input-name').locator('input')).toHaveValue('Updated website');
await expect(page.getByTestId('input-domain').locator('input')).toHaveValue(
'updatedwebsite.com',
);
await page.getByText(/Tracking code/i).click();
await expect(page.locator('textarea')).toContainText('/script.js');
await page.getByText(/Details/i).click();
const websiteId = await page.getByTestId('text-field-websiteId').locator('input').inputValue();
await deleteWebsite(request, auth, websiteId);
await page.goto('/settings/websites');
await expect(page.getByText(/Update test/i)).toHaveCount(0);
});
test('deletes a website', async ({ page, request }) => {
const auth = await loginPage(page, request);
await addWebsite(request, auth, 'Delete test', 'deletetest.com');
await page.goto('/settings/websites');
await page.getByTestId('link-button-edit').first().click();
await expect(page.getByText(/Data/i)).toBeVisible();
await page.getByText(/Data/i).click();
await expect(page.getByText(/All website data will be deleted./i)).toBeVisible();
await page.getByTestId('button-delete').click();
await expect(page.getByText(/Type DELETE in the box below to confirm./i)).toBeVisible();
await page.locator('input[name="confirm"]').fill('DELETE');
await page.locator('button[type="submit"]').click();
await expect(page.getByText(/Delete test/i)).toHaveCount(0);
});
});