Migrate test suite from Cypress and Jest to Playwright and Vitest
This commit is contained in:
@@ -9,6 +9,8 @@ package-lock.json
|
|||||||
|
|
||||||
# testing
|
# testing
|
||||||
/coverage
|
/coverage
|
||||||
|
/playwright-report
|
||||||
|
/test-results
|
||||||
|
|
||||||
# next.js
|
# next.js
|
||||||
next-env.d.ts
|
next-env.d.ts
|
||||||
|
|||||||
@@ -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',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
@@ -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:
|
|
||||||
@@ -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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -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');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -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');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -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();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -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');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
{
|
|
||||||
"teamCreate": {
|
|
||||||
"name": "cypress"
|
|
||||||
},
|
|
||||||
"teamUpdate": {
|
|
||||||
"name": "cypressUpdate"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
{
|
|
||||||
"userCreate": {
|
|
||||||
"username": "cypress1",
|
|
||||||
"password": "password",
|
|
||||||
"role": "user"
|
|
||||||
},
|
|
||||||
"userUpdate": {
|
|
||||||
"username": "cypress1",
|
|
||||||
"role": "view-only"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
{
|
|
||||||
"websiteCreate": {
|
|
||||||
"name": "Cypress Website",
|
|
||||||
"domain": "cypress.com"
|
|
||||||
},
|
|
||||||
"websiteUpdate": {
|
|
||||||
"name": "Cypress Website Updated",
|
|
||||||
"domain": "cypressupdated.com"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
Vendored
-56
@@ -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>>;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"target": "es5",
|
|
||||||
"lib": ["es5", "dom"],
|
|
||||||
"types": ["cypress", "node"]
|
|
||||||
},
|
|
||||||
"include": ["**/*.ts", "../cypress.config.ts"]
|
|
||||||
}
|
|
||||||
@@ -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
@@ -39,9 +39,10 @@
|
|||||||
"download-language-names": "node scripts/download-language-names.js",
|
"download-language-names": "node scripts/download-language-names.js",
|
||||||
"change-password": "node scripts/change-password.js",
|
"change-password": "node scripts/change-password.js",
|
||||||
"postbuild": "node scripts/postbuild.js",
|
"postbuild": "node scripts/postbuild.js",
|
||||||
"test": "jest",
|
"test": "vitest run",
|
||||||
"cypress-open": "cypress open cypress run",
|
"test:watch": "vitest",
|
||||||
"cypress-run": "cypress run cypress run",
|
"test:e2e": "playwright test",
|
||||||
|
"test:e2e:ui": "playwright test --ui",
|
||||||
"seed-data": "tsx scripts/seed-data.ts",
|
"seed-data": "tsx scripts/seed-data.ts",
|
||||||
"lint": "biome lint .",
|
"lint": "biome lint .",
|
||||||
"format": "biome format --write .",
|
"format": "biome format --write .",
|
||||||
@@ -121,6 +122,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@biomejs/biome": "^2.4.12",
|
"@biomejs/biome": "^2.4.12",
|
||||||
"@netlify/plugin-nextjs": "^5.15.9",
|
"@netlify/plugin-nextjs": "^5.15.9",
|
||||||
|
"@playwright/test": "^1.59.1",
|
||||||
"@rollup/plugin-alias": "^6.0.0",
|
"@rollup/plugin-alias": "^6.0.0",
|
||||||
"@rollup/plugin-commonjs": "^29.0.2",
|
"@rollup/plugin-commonjs": "^29.0.2",
|
||||||
"@rollup/plugin-json": "^6.0.0",
|
"@rollup/plugin-json": "^6.0.0",
|
||||||
@@ -128,15 +130,17 @@
|
|||||||
"@rollup/plugin-replace": "^6.0.3",
|
"@rollup/plugin-replace": "^6.0.3",
|
||||||
"@rollup/plugin-terser": "^1.0.0",
|
"@rollup/plugin-terser": "^1.0.0",
|
||||||
"@rollup/plugin-typescript": "^12.3.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/node": "^25.6.0",
|
||||||
"@types/react": "^19.2.14",
|
"@types/react": "^19.2.14",
|
||||||
"@types/react-dom": "^19.2.2",
|
"@types/react-dom": "^19.2.2",
|
||||||
"babel-plugin-react-compiler": "19.1.0-rc.2",
|
"babel-plugin-react-compiler": "19.1.0-rc.2",
|
||||||
"cross-env": "^10.1.0",
|
"cross-env": "^10.1.0",
|
||||||
"cypress": "^15.14.0",
|
|
||||||
"dotenv-cli": "^11.0.0",
|
"dotenv-cli": "^11.0.0",
|
||||||
"jest": "^30.3.0",
|
"jsdom": "^29.1.1",
|
||||||
|
"msw": "^2.14.5",
|
||||||
"postcss": "^8.5.10",
|
"postcss": "^8.5.10",
|
||||||
"postcss-flexbugs-fixes": "^5.0.2",
|
"postcss-flexbugs-fixes": "^5.0.2",
|
||||||
"postcss-import": "^16.1.1",
|
"postcss-import": "^16.1.1",
|
||||||
@@ -150,11 +154,11 @@
|
|||||||
"rollup-plugin-peer-deps-external": "^2.2.4",
|
"rollup-plugin-peer-deps-external": "^2.2.4",
|
||||||
"rollup-plugin-postcss": "^4.0.2",
|
"rollup-plugin-postcss": "^4.0.2",
|
||||||
"tar": "^7.5.13",
|
"tar": "^7.5.13",
|
||||||
"ts-jest": "^29.4.9",
|
|
||||||
"ts-morph": "^28.0.0",
|
"ts-morph": "^28.0.0",
|
||||||
"ts-node": "^10.9.1",
|
"ts-node": "^10.9.1",
|
||||||
"tsup": "^8.5.0",
|
"tsup": "^8.5.0",
|
||||||
"tsx": "^4.19.0",
|
"tsx": "^4.19.0",
|
||||||
"typescript": "^6.0.3"
|
"typescript": "^6.0.3",
|
||||||
|
"vitest": "^4.1.5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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'] },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
Generated
+1397
-2966
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,6 @@
|
|||||||
packages:
|
packages:
|
||||||
- '**'
|
- '**'
|
||||||
ignoredBuiltDependencies:
|
ignoredBuiltDependencies:
|
||||||
- cypress
|
|
||||||
- esbuild
|
- esbuild
|
||||||
- sharp
|
- sharp
|
||||||
onlyBuiltDependencies:
|
onlyBuiltDependencies:
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { expect, test } from '@jest/globals';
|
import { expect, test } from 'vitest';
|
||||||
import { getApiUrl } from '../api-url';
|
import { getApiUrl } from '../api-url';
|
||||||
|
|
||||||
test('uses the default api path', () => {
|
test('uses the default api path', () => {
|
||||||
|
|||||||
@@ -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})$/;
|
/^(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 SHARE_ID_REGEX = /^[a-zA-Z0-9]{8,50}$/;
|
||||||
export const DATETIME_REGEX =
|
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 URL_LENGTH = 500;
|
||||||
export const PAGE_TITLE_LENGTH = 500;
|
export const PAGE_TITLE_LENGTH = 500;
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
import type { RequestHandler } from 'msw';
|
||||||
|
|
||||||
|
export const handlers: RequestHandler[] = [];
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import { setupServer } from 'msw/node';
|
||||||
|
import { handlers } from './handlers';
|
||||||
|
|
||||||
|
export const server = setupServer(...handlers);
|
||||||
|
export { HttpResponse, http } from 'msw';
|
||||||
@@ -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();
|
||||||
|
});
|
||||||
@@ -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 };
|
||||||
Vendored
+2
@@ -0,0 +1,2 @@
|
|||||||
|
/// <reference types="vitest/globals" />
|
||||||
|
/// <reference types="@testing-library/jest-dom" />
|
||||||
@@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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',
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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
@@ -39,5 +39,5 @@
|
|||||||
".next/types/**/*.ts",
|
".next/types/**/*.ts",
|
||||||
".next/dev/types/**/*.ts"
|
".next/dev/types/**/*.ts"
|
||||||
],
|
],
|
||||||
"exclude": ["node_modules", "./cypress.config.ts", "cypress"]
|
"exclude": ["node_modules", "./playwright.config.ts", "tests/e2e"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user