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
+2
View File
@@ -9,6 +9,8 @@ package-lock.json
# testing
/coverage
/playwright-report
/test-results
# next.js
next-env.d.ts
-13
View File
@@ -1,13 +0,0 @@
import { defineConfig } from 'cypress';
export default defineConfig({
e2e: {
baseUrl: 'http://localhost:3000',
},
// default username / password on init
env: {
umami_user: 'admin',
umami_password: 'umami',
umami_user_id: '41e2b680-648e-4b09-bcd7-3e2b10c06264',
},
});
-52
View File
@@ -1,52 +0,0 @@
---
version: '3'
services:
umami:
build: ../
#image: ghcr.io/umami-software/umami:postgresql-latest
ports:
- '3000:3000'
environment:
DATABASE_URL: postgresql://umami:umami@db:5432/umami
DATABASE_TYPE: postgresql
APP_SECRET: replace-me-with-a-random-string
depends_on:
db:
condition: service_healthy
restart: always
healthcheck:
test: ['CMD-SHELL', 'curl http://localhost:3000/api/heartbeat']
interval: 5s
timeout: 5s
retries: 5
db:
image: postgres:15-alpine
environment:
POSTGRES_DB: umami
POSTGRES_USER: umami
POSTGRES_PASSWORD: umami
volumes:
- umami-db-data:/var/lib/postgresql/data
restart: always
healthcheck:
test: ['CMD-SHELL', 'pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}']
interval: 5s
timeout: 5s
retries: 5
cypress:
image: 'cypress/included:13.6.0'
depends_on:
- umami
- db
environment:
- CYPRESS_baseUrl=http://umami:3000
- CYPRESS_umami_user=admin
- CYPRESS_umami_password=umami
volumes:
- ./tsconfig.json:/tsconfig.json
- ../cypress.config.ts:/cypress.config.ts
- ./:/cypress
- ../node_modules/:/node_modules
- ../src/lib/crypto.ts:/src/lib/crypto.ts
volumes:
umami-db-data:
-209
View File
@@ -1,209 +0,0 @@
describe('Team API tests', () => {
Cypress.session.clearAllSavedSessions();
let teamId;
let userId;
before(() => {
cy.login(Cypress.env('umami_user'), Cypress.env('umami_password'));
cy.fixture('users').then(data => {
const userCreate = data.userCreate;
cy.request({
method: 'POST',
url: '/api/users',
headers: {
'Content-Type': 'application/json',
Authorization: Cypress.env('authorization'),
},
body: userCreate,
}).then(response => {
userId = response.body.id;
expect(response.status).to.eq(200);
expect(response.body).to.have.property('username', 'cypress1');
expect(response.body).to.have.property('role', 'user');
});
});
});
it('Creates a team.', () => {
cy.fixture('teams').then(data => {
const teamCreate = data.teamCreate;
cy.request({
method: 'POST',
url: '/api/teams',
headers: {
'Content-Type': 'application/json',
Authorization: Cypress.env('authorization'),
},
body: teamCreate,
}).then(response => {
teamId = response.body[0].id;
expect(response.status).to.eq(200);
expect(response.body[0]).to.have.property('name', 'cypress');
expect(response.body[1]).to.have.property('role', 'team-owner');
});
});
});
it('Gets a teams by ID.', () => {
cy.request({
method: 'GET',
url: `/api/teams/${teamId}`,
headers: {
'Content-Type': 'application/json',
Authorization: Cypress.env('authorization'),
},
}).then(response => {
expect(response.status).to.eq(200);
expect(response.body).to.have.property('id', teamId);
});
});
it('Updates a team.', () => {
cy.fixture('teams').then(data => {
const teamUpdate = data.teamUpdate;
cy.request({
method: 'POST',
url: `/api/teams/${teamId}`,
headers: {
'Content-Type': 'application/json',
Authorization: Cypress.env('authorization'),
},
body: teamUpdate,
}).then(response => {
expect(response.status).to.eq(200);
expect(response.body).to.have.property('id', teamId);
expect(response.body).to.have.property('name', 'cypressUpdate');
});
});
});
it('Get all users that belong to a team.', () => {
cy.request({
method: 'GET',
url: `/api/teams/${teamId}/users`,
headers: {
'Content-Type': 'application/json',
Authorization: Cypress.env('authorization'),
},
}).then(response => {
expect(response.status).to.eq(200);
expect(response.body.data[0]).to.have.property('id');
expect(response.body.data[0]).to.have.property('teamId');
expect(response.body.data[0]).to.have.property('userId');
expect(response.body.data[0]).to.have.property('user');
});
});
it('Get a user belonging to a team.', () => {
cy.request({
method: 'GET',
url: `/api/teams/${teamId}/users/${Cypress.env('umami_user_id')}`,
headers: {
'Content-Type': 'application/json',
Authorization: Cypress.env('authorization'),
},
}).then(response => {
expect(response.status).to.eq(200);
expect(response.body).to.have.property('teamId');
expect(response.body).to.have.property('userId');
expect(response.body).to.have.property('role');
});
});
it('Get all websites belonging to a team.', () => {
cy.request({
method: 'GET',
url: `/api/teams/${teamId}/websites`,
headers: {
'Content-Type': 'application/json',
Authorization: Cypress.env('authorization'),
},
}).then(response => {
expect(response.status).to.eq(200);
expect(response.body).to.have.property('data');
});
});
it('Add a user to a team.', () => {
cy.request({
method: 'POST',
url: `/api/teams/${teamId}/users`,
headers: {
'Content-Type': 'application/json',
Authorization: Cypress.env('authorization'),
},
body: {
userId,
role: 'team-member',
},
}).then(response => {
expect(response.status).to.eq(200);
expect(response.body).to.have.property('userId', userId);
expect(response.body).to.have.property('role', 'team-member');
});
});
it(`Update a user's role on a team.`, () => {
cy.request({
method: 'POST',
url: `/api/teams/${teamId}/users/${userId}`,
headers: {
'Content-Type': 'application/json',
Authorization: Cypress.env('authorization'),
},
body: {
role: 'team-view-only',
},
}).then(response => {
expect(response.status).to.eq(200);
expect(response.body).to.have.property('userId', userId);
expect(response.body).to.have.property('role', 'team-view-only');
});
});
it(`Remove a user from a team.`, () => {
cy.request({
method: 'DELETE',
url: `/api/teams/${teamId}/users/${userId}`,
headers: {
'Content-Type': 'application/json',
Authorization: Cypress.env('authorization'),
},
}).then(response => {
expect(response.status).to.eq(200);
});
});
it('Deletes a team.', () => {
cy.request({
method: 'DELETE',
url: `/api/teams/${teamId}`,
headers: {
'Content-Type': 'application/json',
Authorization: Cypress.env('authorization'),
},
}).then(response => {
expect(response.status).to.eq(200);
expect(response.body).to.have.property('ok', true);
});
});
// it('Gets all teams that belong to a user.', () => {
// cy.request({
// method: 'GET',
// url: `/api/users/${userId}/teams`,
// headers: {
// 'Content-Type': 'application/json',
// Authorization: Cypress.env('authorization'),
// },
// }).then(response => {
// expect(response.status).to.eq(200);
// expect(response.body).to.have.property('data');
// });
// });
after(() => {
cy.deleteUser(userId);
});
});
-125
View File
@@ -1,125 +0,0 @@
describe('User API tests', () => {
Cypress.session.clearAllSavedSessions();
before(() => {
cy.login(Cypress.env('umami_user'), Cypress.env('umami_password'));
});
let userId;
it('Creates a user.', () => {
cy.fixture('users').then(data => {
const userCreate = data.userCreate;
cy.request({
method: 'POST',
url: '/api/users',
headers: {
'Content-Type': 'application/json',
Authorization: Cypress.env('authorization'),
},
body: userCreate,
}).then(response => {
userId = response.body.id;
expect(response.status).to.eq(200);
expect(response.body).to.have.property('username', 'cypress1');
expect(response.body).to.have.property('role', 'user');
});
});
});
it('Returns all users. Admin access is required.', () => {
cy.request({
method: 'GET',
url: '/api/admin/users',
headers: {
'Content-Type': 'application/json',
Authorization: Cypress.env('authorization'),
},
}).then(response => {
expect(response.status).to.eq(200);
expect(response.body.data[0]).to.have.property('id');
expect(response.body.data[0]).to.have.property('username');
expect(response.body.data[0]).to.have.property('password');
expect(response.body.data[0]).to.have.property('role');
});
});
it('Updates a user.', () => {
cy.fixture('users').then(data => {
const userUpdate = data.userUpdate;
cy.request({
method: 'POST',
url: `/api/users/${userId}`,
headers: {
'Content-Type': 'application/json',
Authorization: Cypress.env('authorization'),
},
body: userUpdate,
}).then(response => {
userId = response.body.id;
expect(response.status).to.eq(200);
expect(response.body).to.have.property('id', userId);
expect(response.body).to.have.property('username', 'cypress1');
expect(response.body).to.have.property('role', 'view-only');
});
});
});
it('Gets a user by ID.', () => {
cy.request({
method: 'GET',
url: `/api/users/${userId}`,
headers: {
'Content-Type': 'application/json',
Authorization: Cypress.env('authorization'),
},
}).then(response => {
expect(response.status).to.eq(200);
expect(response.body).to.have.property('id', userId);
expect(response.body).to.have.property('username', 'cypress1');
expect(response.body).to.have.property('role', 'view-only');
});
});
it('Deletes a user.', () => {
cy.request({
method: 'DELETE',
url: `/api/users/${userId}`,
headers: {
'Content-Type': 'application/json',
Authorization: Cypress.env('authorization'),
},
}).then(response => {
expect(response.status).to.eq(200);
expect(response.body).to.have.property('ok', true);
});
});
it('Gets all websites that belong to a user.', () => {
cy.request({
method: 'GET',
url: `/api/users/${userId}/websites`,
headers: {
'Content-Type': 'application/json',
Authorization: Cypress.env('authorization'),
},
}).then(response => {
expect(response.status).to.eq(200);
expect(response.body).to.have.property('data');
});
});
it('Gets all teams that belong to a user.', () => {
cy.request({
method: 'GET',
url: `/api/users/${userId}/teams`,
headers: {
'Content-Type': 'application/json',
Authorization: Cypress.env('authorization'),
},
}).then(response => {
expect(response.status).to.eq(200);
expect(response.body).to.have.property('data');
});
});
});
-198
View File
@@ -1,198 +0,0 @@
import { uuid } from '../../src/lib/crypto';
describe('Website API tests', () => {
Cypress.session.clearAllSavedSessions();
let websiteId;
let teamId;
before(() => {
cy.login(Cypress.env('umami_user'), Cypress.env('umami_password'));
cy.fixture('teams').then(data => {
const teamCreate = data.teamCreate;
cy.request({
method: 'POST',
url: '/api/teams',
headers: {
'Content-Type': 'application/json',
Authorization: Cypress.env('authorization'),
},
body: teamCreate,
}).then(response => {
teamId = response.body[0].id;
expect(response.status).to.eq(200);
expect(response.body[0]).to.have.property('name', 'cypress');
expect(response.body[1]).to.have.property('role', 'team-owner');
});
});
});
it('Creates a website for user.', () => {
cy.fixture('websites').then(data => {
const websiteCreate = data.websiteCreate;
cy.request({
method: 'POST',
url: '/api/websites',
headers: {
'Content-Type': 'application/json',
Authorization: Cypress.env('authorization'),
},
body: websiteCreate,
}).then(response => {
websiteId = response.body.id;
expect(response.status).to.eq(200);
expect(response.body).to.have.property('name', 'Cypress Website');
expect(response.body).to.have.property('domain', 'cypress.com');
});
});
});
it('Creates a website for team.', () => {
cy.request({
method: 'POST',
url: '/api/websites',
headers: {
'Content-Type': 'application/json',
Authorization: Cypress.env('authorization'),
},
body: {
name: 'Team Website',
domain: 'teamwebsite.com',
teamId: teamId,
},
}).then(response => {
expect(response.status).to.eq(200);
expect(response.body).to.have.property('name', 'Team Website');
expect(response.body).to.have.property('domain', 'teamwebsite.com');
});
});
it('Creates a website with a fixed ID.', () => {
cy.fixture('websites').then(data => {
const websiteCreate = data.websiteCreate;
const fixedId = uuid();
cy.request({
method: 'POST',
url: '/api/websites',
headers: {
'Content-Type': 'application/json',
Authorization: Cypress.env('authorization'),
},
body: { ...websiteCreate, id: fixedId },
}).then(response => {
expect(response.status).to.eq(200);
expect(response.body).to.have.property('id', fixedId);
expect(response.body).to.have.property('name', 'Cypress Website');
expect(response.body).to.have.property('domain', 'cypress.com');
// cleanup
cy.request({
method: 'DELETE',
url: `/api/websites/${fixedId}`,
headers: {
'Content-Type': 'application/json',
Authorization: Cypress.env('authorization'),
},
});
});
});
});
it('Returns all tracked websites.', () => {
cy.request({
method: 'GET',
url: '/api/websites',
headers: {
'Content-Type': 'application/json',
Authorization: Cypress.env('authorization'),
},
}).then(response => {
expect(response.status).to.eq(200);
expect(response.body.data[0]).to.have.property('id');
expect(response.body.data[0]).to.have.property('name');
expect(response.body.data[0]).to.have.property('domain');
});
});
it('Gets a website by ID.', () => {
cy.request({
method: 'GET',
url: `/api/websites/${websiteId}`,
headers: {
'Content-Type': 'application/json',
Authorization: Cypress.env('authorization'),
},
}).then(response => {
expect(response.status).to.eq(200);
expect(response.body).to.have.property('name', 'Cypress Website');
expect(response.body).to.have.property('domain', 'cypress.com');
});
});
it('Updates a website.', () => {
cy.fixture('websites').then(data => {
const websiteUpdate = data.websiteUpdate;
cy.request({
method: 'POST',
url: `/api/websites/${websiteId}`,
headers: {
'Content-Type': 'application/json',
Authorization: Cypress.env('authorization'),
},
body: websiteUpdate,
}).then(response => {
websiteId = response.body.id;
expect(response.status).to.eq(200);
expect(response.body).to.have.property('name', 'Cypress Website Updated');
expect(response.body).to.have.property('domain', 'cypressupdated.com');
});
});
});
it('Updates a website with only shareId.', () => {
cy.request({
method: 'POST',
url: `/api/websites/${websiteId}`,
headers: {
'Content-Type': 'application/json',
Authorization: Cypress.env('authorization'),
},
body: { shareId: 'ABCDEF' },
}).then(response => {
expect(response.status).to.eq(200);
expect(response.body).to.have.property('shareId', 'ABCDEF');
});
});
it('Resets a website by removing all data related to the website.', () => {
cy.request({
method: 'POST',
url: `/api/websites/${websiteId}/reset`,
headers: {
'Content-Type': 'application/json',
Authorization: Cypress.env('authorization'),
},
}).then(response => {
expect(response.status).to.eq(200);
expect(response.body).to.have.property('ok', true);
});
});
it('Deletes a website.', () => {
cy.request({
method: 'DELETE',
url: `/api/websites/${websiteId}`,
headers: {
'Content-Type': 'application/json',
Authorization: Cypress.env('authorization'),
},
}).then(response => {
expect(response.status).to.eq(200);
expect(response.body).to.have.property('ok', true);
});
});
after(() => {
cy.deleteTeam(teamId);
});
});
-36
View File
@@ -1,36 +0,0 @@
describe('Login tests', () => {
beforeEach(() => {
cy.visit('/login');
});
it(
'logs user in with correct credentials and logs user out',
{
defaultCommandTimeout: 10000,
},
() => {
cy.getDataTest('input-username').find('input').as('inputUsername').click();
cy.get('@inputUsername').type(Cypress.env('umami_user'), { delay: 0 });
cy.get('@inputUsername').click();
cy.getDataTest('input-password')
.find('input')
.type(Cypress.env('umami_password'), { delay: 0 });
cy.getDataTest('button-submit').click();
cy.url().should('eq', Cypress.config().baseUrl + '/dashboard');
cy.logout();
},
);
it('login with blank inputs or incorrect credentials', () => {
cy.getDataTest('button-submit').click();
cy.contains(/Required/i).should('be.visible');
cy.getDataTest('input-username').find('input').as('inputUsername');
cy.get('@inputUsername').click();
cy.get('@inputUsername').type(Cypress.env('umami_user'), { delay: 0 });
cy.get('@inputUsername').click();
cy.getDataTest('input-password').find('input').type('wrongpassword', { delay: 0 });
cy.getDataTest('button-submit').click();
cy.contains(/Incorrect username and\/or password./i).should('be.visible');
});
});
-65
View File
@@ -1,65 +0,0 @@
describe('User tests', () => {
Cypress.session.clearAllSavedSessions();
beforeEach(() => {
cy.login(Cypress.env('umami_user'), Cypress.env('umami_password'));
cy.visit('/settings/users');
});
it('Add a User', () => {
// add user
cy.contains(/Create user/i).should('be.visible');
cy.getDataTest('button-create-user').click();
cy.getDataTest('input-username').find('input').as('inputName').click();
cy.get('@inputName').type('Test-user', { delay: 0 });
cy.getDataTest('input-password').find('input').as('inputPassword').click();
cy.get('@inputPassword').type('testPasswordCypress', { delay: 0 });
cy.getDataTest('dropdown-role').click();
cy.getDataTest('dropdown-item-user').click();
cy.getDataTest('button-submit').click();
cy.get('td[label="Username"]').should('contain.text', 'Test-user');
cy.get('td[label="Role"]').should('contain.text', 'User');
});
it('Edit a User role and password', () => {
// edit user
cy.get('table tbody tr')
.contains('td', /Test-user/i)
.parent()
.within(() => {
cy.getDataTest('link-button-edit').click(); // Clicks the button inside the row
});
cy.getDataTest('input-password').find('input').as('inputPassword').click();
cy.get('@inputPassword').type('newPassword', { delay: 0 });
cy.getDataTest('dropdown-role').click();
cy.getDataTest('dropdown-item-viewOnly').click();
cy.getDataTest('button-submit').click();
cy.visit('/settings/users');
cy.get('table tbody tr')
.contains('td', /Test-user/i)
.parent()
.should('contain.text', 'View only');
cy.logout();
cy.url().should('eq', Cypress.config().baseUrl + '/login');
cy.getDataTest('input-username').find('input').as('inputUsername').click();
cy.get('@inputUsername').type('Test-user', { delay: 0 });
cy.get('@inputUsername').click();
cy.getDataTest('input-password').find('input').type('newPassword', { delay: 0 });
cy.getDataTest('button-submit').click();
cy.url().should('eq', Cypress.config().baseUrl + '/dashboard');
});
it('Delete a user', () => {
// delete user
cy.get('table tbody tr')
.contains('td', /Test-user/i)
.parent()
.within(() => {
cy.getDataTest('button-delete').click(); // Clicks the button inside the row
});
cy.contains(/Are you sure you want to delete Test-user?/i).should('be.visible');
cy.getDataTest('button-confirm').click();
});
});
-89
View File
@@ -1,89 +0,0 @@
describe('Website tests', () => {
Cypress.session.clearAllSavedSessions();
beforeEach(() => {
cy.login(Cypress.env('umami_user'), Cypress.env('umami_password'));
});
it('Add a website', () => {
// add website
cy.visit('/settings/websites');
cy.getDataTest('button-website-add').click();
cy.contains(/Add website/i).should('be.visible');
cy.getDataTest('input-name').find('input').as('inputUsername').click();
cy.getDataTest('input-name').find('input').type('Add test', { delay: 0 });
cy.getDataTest('input-domain').find('input').click();
cy.getDataTest('input-domain').find('input').type('addtest.com', { delay: 0 });
cy.getDataTest('button-submit').click();
cy.get('td[label="Name"]').should('contain.text', 'Add test');
cy.get('td[label="Domain"]').should('contain.text', 'addtest.com');
// clean-up data
cy.getDataTest('link-button-edit').first().click();
cy.contains(/Details/i).should('be.visible');
cy.getDataTest('text-field-websiteId')
.find('input')
.then($input => {
const websiteId = $input[0].value;
cy.deleteWebsite(websiteId);
});
cy.visit('/settings/websites');
cy.contains(/Add test/i).should('not.exist');
});
it('Edit a website', () => {
// prep data
cy.addWebsite('Update test', 'updatetest.com');
cy.visit('/settings/websites');
// edit website
cy.getDataTest('link-button-edit').first().click();
cy.contains(/Details/i).should('be.visible');
cy.getDataTest('input-name').find('input').click();
cy.getDataTest('input-name').find('input').clear();
cy.getDataTest('input-name').find('input').type('Updated website', { delay: 0 });
cy.getDataTest('input-domain').find('input').click();
cy.getDataTest('input-domain').find('input').clear();
cy.getDataTest('input-domain').find('input').type('updatedwebsite.com', { delay: 0 });
cy.getDataTest('button-submit').click({ force: true });
cy.getDataTest('input-name').find('input').should('have.value', 'Updated website');
cy.getDataTest('input-domain').find('input').should('have.value', 'updatedwebsite.com');
// verify tracking script
cy.get('div')
.contains(/Tracking code/i)
.click();
cy.get('textarea').should('contain.text', Cypress.config().baseUrl + '/script.js');
// clean-up data
cy.get('div')
.contains(/Details/i)
.click();
cy.contains(/Details/i).should('be.visible');
cy.getDataTest('text-field-websiteId')
.find('input')
.then($input => {
const websiteId = $input[0].value;
cy.deleteWebsite(websiteId);
});
cy.visit('/settings/websites');
cy.contains(/Add test/i).should('not.exist');
});
it('Delete a website', () => {
// prep data
cy.addWebsite('Delete test', 'deletetest.com');
cy.visit('/settings/websites');
// delete website
cy.getDataTest('link-button-edit').first().click();
cy.contains(/Data/i).should('be.visible');
cy.get('div').contains(/Data/i).click();
cy.contains(/All website data will be deleted./i).should('be.visible');
cy.getDataTest('button-delete').click();
cy.contains(/Type DELETE in the box below to confirm./i).should('be.visible');
cy.get('input[name="confirm"').type('DELETE');
cy.get('button[type="submit"]').click();
cy.contains(/Delete test/i).should('not.exist');
});
});
-8
View File
@@ -1,8 +0,0 @@
{
"teamCreate": {
"name": "cypress"
},
"teamUpdate": {
"name": "cypressUpdate"
}
}
-11
View File
@@ -1,11 +0,0 @@
{
"userCreate": {
"username": "cypress1",
"password": "password",
"role": "user"
},
"userUpdate": {
"username": "cypress1",
"role": "view-only"
}
}
-10
View File
@@ -1,10 +0,0 @@
{
"websiteCreate": {
"name": "Cypress Website",
"domain": "cypress.com"
},
"websiteUpdate": {
"name": "Cypress Website Updated",
"domain": "cypressupdated.com"
}
}
-123
View File
@@ -1,123 +0,0 @@
/// <reference types="cypress" />
import { uuid } from '../../src/lib/crypto';
Cypress.Commands.add('getDataTest', (value: string) => {
return cy.get(`[data-test=${value}]`);
});
Cypress.Commands.add('logout', () => {
cy.getDataTest('button-profile').click();
cy.getDataTest('item-logout').click();
cy.url().should('eq', Cypress.config().baseUrl + '/login');
});
Cypress.Commands.add('login', (username: string, password: string) => {
cy.session([username, password], () => {
cy.request({
method: 'POST',
url: '/api/auth/login',
body: {
username,
password,
},
})
.then(response => {
Cypress.env('authorization', `bearer ${response.body.token}`);
window.localStorage.setItem('umami.auth', JSON.stringify(response.body.token));
})
.its('status')
.should('eq', 200);
});
});
Cypress.Commands.add('addWebsite', (name: string, domain: string) => {
cy.request({
method: 'POST',
url: '/api/websites',
headers: {
'Content-Type': 'application/json',
Authorization: Cypress.env('authorization'),
},
body: {
id: uuid(),
createdBy: '41e2b680-648e-4b09-bcd7-3e2b10c06264',
name: name,
domain: domain,
},
}).then(response => {
expect(response.status).to.eq(200);
});
});
Cypress.Commands.add('deleteWebsite', (websiteId: string) => {
cy.request({
method: 'DELETE',
url: `/api/websites/${websiteId}`,
headers: {
'Content-Type': 'application/json',
Authorization: Cypress.env('authorization'),
},
}).then(response => {
expect(response.status).to.eq(200);
});
});
Cypress.Commands.add('addUser', (username: string, password: string, role: string) => {
cy.request({
method: 'POST',
url: '/api/users',
headers: {
'Content-Type': 'application/json',
Authorization: Cypress.env('authorization'),
},
body: {
username: username,
password: password,
role: role,
},
}).then(response => {
expect(response.status).to.eq(200);
});
});
Cypress.Commands.add('deleteUser', (userId: string) => {
cy.request({
method: 'DELETE',
url: `/api/users/${userId}`,
headers: {
'Content-Type': 'application/json',
Authorization: Cypress.env('authorization'),
},
}).then(response => {
expect(response.status).to.eq(200);
});
});
Cypress.Commands.add('addTeam', (name: string) => {
cy.request({
method: 'POST',
url: '/api/teams',
headers: {
'Content-Type': 'application/json',
Authorization: Cypress.env('authorization'),
},
body: {
name: name,
},
}).then(response => {
expect(response.status).to.eq(200);
});
});
Cypress.Commands.add('deleteTeam', (teamId: string) => {
cy.request({
method: 'DELETE',
url: `/api/teams/${teamId}`,
headers: {
'Content-Type': 'application/json',
Authorization: Cypress.env('authorization'),
},
}).then(response => {
expect(response.status).to.eq(200);
});
});
-56
View File
@@ -1,56 +0,0 @@
/// <reference types="cypress" />
/* global JQuery */
declare namespace Cypress {
interface Chainable {
/**
* Custom command to select DOM element by data-test attribute.
* @example cy.getDataTest('greeting')
*/
getDataTest(value: string): Chainable<JQuery<HTMLElement>>;
/**
* Custom command to logout through UI.
* @example cy.logout()
*/
logout(): Chainable<JQuery<HTMLElement>>;
/**
* Custom command to login user into the app.
* @example cy.login('admin', 'password)
*/
login(username: string, password: string): Chainable<JQuery<HTMLElement>>;
/**
* Custom command to create a website
* @example cy.addWebsite('test', 'test.com')
*/
addWebsite(name: string, domain: string): Chainable<JQuery<HTMLElement>>;
/**
* Custom command to delete a website
* @example cy.deleteWebsite('02d89813-7a72-41e1-87f0-8d668f85008b')
*/
deleteWebsite(websiteId: string): Chainable<JQuery<HTMLElement>>;
/**
* Custom command to create a website
* @example cy.deleteWebsite('02d89813-7a72-41e1-87f0-8d668f85008b')
*/
/**
* Custom command to create a user
* @example cy.addUser('cypress', 'password', 'User')
*/
addUser(username: string, password: string, role: string): Chainable<JQuery<HTMLElement>>;
/**
* Custom command to delete a user
* @example cy.deleteUser('02d89813-7a72-41e1-87f0-8d668f85008b')
*/
deleteUser(userId: string): Chainable<JQuery<HTMLElement>>;
/**
* Custom command to create a team
* @example cy.addTeam('cypressTeam')
*/
addTeam(name: string): Chainable<JQuery<HTMLElement>>;
/**
* Custom command to create a website
* @example cy.deleteTeam('02d89813-7a72-41e1-87f0-8d668f85008b')
*/
deleteTeam(teamId: string): Chainable<JQuery<HTMLElement>>;
}
}
-8
View File
@@ -1,8 +0,0 @@
{
"compilerOptions": {
"target": "es5",
"lib": ["es5", "dom"],
"types": ["cypress", "node"]
},
"include": ["**/*.ts", "../cypress.config.ts"]
}
-10
View File
@@ -1,10 +0,0 @@
export default {
roots: ['./src'],
testMatch: ['**/__tests__/**/*.+(ts|tsx|js)', '**/?(*.)+(spec|test).+(ts|tsx|js)'],
transform: {
'^.+\\.(ts|tsx)$': 'ts-jest',
},
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/src/$1',
},
};
+12 -8
View File
@@ -39,9 +39,10 @@
"download-language-names": "node scripts/download-language-names.js",
"change-password": "node scripts/change-password.js",
"postbuild": "node scripts/postbuild.js",
"test": "jest",
"cypress-open": "cypress open cypress run",
"cypress-run": "cypress run cypress run",
"test": "vitest run",
"test:watch": "vitest",
"test:e2e": "playwright test",
"test:e2e:ui": "playwright test --ui",
"seed-data": "tsx scripts/seed-data.ts",
"lint": "biome lint .",
"format": "biome format --write .",
@@ -121,6 +122,7 @@
"devDependencies": {
"@biomejs/biome": "^2.4.12",
"@netlify/plugin-nextjs": "^5.15.9",
"@playwright/test": "^1.59.1",
"@rollup/plugin-alias": "^6.0.0",
"@rollup/plugin-commonjs": "^29.0.2",
"@rollup/plugin-json": "^6.0.0",
@@ -128,15 +130,17 @@
"@rollup/plugin-replace": "^6.0.3",
"@rollup/plugin-terser": "^1.0.0",
"@rollup/plugin-typescript": "^12.3.0",
"@types/jest": "^30.0.0",
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.2",
"@testing-library/user-event": "^14.6.1",
"@types/node": "^25.6.0",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.2",
"babel-plugin-react-compiler": "19.1.0-rc.2",
"cross-env": "^10.1.0",
"cypress": "^15.14.0",
"dotenv-cli": "^11.0.0",
"jest": "^30.3.0",
"jsdom": "^29.1.1",
"msw": "^2.14.5",
"postcss": "^8.5.10",
"postcss-flexbugs-fixes": "^5.0.2",
"postcss-import": "^16.1.1",
@@ -150,11 +154,11 @@
"rollup-plugin-peer-deps-external": "^2.2.4",
"rollup-plugin-postcss": "^4.0.2",
"tar": "^7.5.13",
"ts-jest": "^29.4.9",
"ts-morph": "^28.0.0",
"ts-node": "^10.9.1",
"tsup": "^8.5.0",
"tsx": "^4.19.0",
"typescript": "^6.0.3"
"typescript": "^6.0.3",
"vitest": "^4.1.5"
}
}
+32
View File
@@ -0,0 +1,32 @@
import { defineConfig, devices } from '@playwright/test';
const port = process.env.PORT ?? '3000';
const baseURL = process.env.PLAYWRIGHT_BASE_URL ?? `http://localhost:${port}`;
export default defineConfig({
testDir: './tests/e2e',
fullyParallel: false,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: process.env.CI ? [['list'], ['html', { open: 'never' }]] : 'list',
use: {
baseURL,
testIdAttribute: 'data-test',
trace: 'on-first-retry',
},
webServer: process.env.PLAYWRIGHT_SKIP_WEB_SERVER
? undefined
: {
command: process.env.PLAYWRIGHT_WEB_SERVER_COMMAND ?? 'pnpm dev',
url: baseURL,
reuseExistingServer: !process.env.CI,
timeout: 120_000,
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
],
});
+1397 -2966
View File
File diff suppressed because it is too large Load Diff
-1
View File
@@ -1,7 +1,6 @@
packages:
- '**'
ignoredBuiltDependencies:
- cypress
- esbuild
- sharp
onlyBuiltDependencies:
+1 -1
View File
@@ -1,4 +1,4 @@
import { expect, test } from '@jest/globals';
import { expect, test } from 'vitest';
import { getApiUrl } from '../api-url';
test('uses the default api path', () => {
+1 -1
View File
@@ -257,7 +257,7 @@ export const DOMAIN_REGEX =
/^(localhost(:[1-9]\d{0,4})?|((?=[a-z0-9-_]{1,63}\.)(xn--)?[a-z0-9-_]+(-[a-z0-9-_]+)*\.)+(xn--)?[a-z0-9-_]{2,63})$/;
export const SHARE_ID_REGEX = /^[a-zA-Z0-9]{8,50}$/;
export const DATETIME_REGEX =
/^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\.[0-9]{3}(Z|\+[0-9]{2}:[0-9]{2})?)?$/;
/^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\.[0-9]{3})?(Z|\+[0-9]{2}:[0-9]{2})?$/;
export const URL_LENGTH = 500;
export const PAGE_TITLE_LENGTH = 500;
+3
View File
@@ -0,0 +1,3 @@
import type { RequestHandler } from 'msw';
export const handlers: RequestHandler[] = [];
+5
View File
@@ -0,0 +1,5 @@
import { setupServer } from 'msw/node';
import { handlers } from './handlers';
export const server = setupServer(...handlers);
export { HttpResponse, http } from 'msw';
+15
View File
@@ -0,0 +1,15 @@
import '@testing-library/jest-dom/vitest';
import { afterAll, afterEach, beforeAll } from 'vitest';
import { server } from './msw/server';
beforeAll(() => {
server.listen({ onUnhandledRequest: 'error' });
});
afterEach(() => {
server.resetHandlers();
});
afterAll(() => {
server.close();
});
+10
View File
@@ -0,0 +1,10 @@
import { type RenderOptions, render } from '@testing-library/react';
import type { ReactElement } from 'react';
const customRender = (ui: ReactElement, options?: Omit<RenderOptions, 'wrapper'>) => {
return render(ui, options);
};
export * from '@testing-library/react';
export { default as userEvent } from '@testing-library/user-event';
export { customRender as render };
+2
View File
@@ -0,0 +1,2 @@
/// <reference types="vitest/globals" />
/// <reference types="@testing-library/jest-dom" />
+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);
});
});
+1 -1
View File
@@ -39,5 +39,5 @@
".next/types/**/*.ts",
".next/dev/types/**/*.ts"
],
"exclude": ["node_modules", "./cypress.config.ts", "cypress"]
"exclude": ["node_modules", "./playwright.config.ts", "tests/e2e"]
}
+22
View File
@@ -0,0 +1,22 @@
import { fileURLToPath } from 'node:url';
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
environment: 'jsdom',
exclude: [
'**/node_modules/**',
'**/tests/e2e/**',
'**/playwright-report/**',
'**/test-results/**',
],
globals: true,
include: ['src/**/*.{test,spec}.{ts,tsx,js,jsx}'],
setupFiles: ['./src/test/setup.ts'],
},
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)),
},
},
});