add format check, update prettier rules, apply rules consistently

This commit is contained in:
Gregor Vostrak
2025-07-29 18:58:31 +02:00
parent b11672732b
commit cb30487a21
323 changed files with 3728 additions and 5579 deletions

View File

@@ -7,11 +7,8 @@ async function goToProjectsOverview(page: Page) {
}
// Create new project via modal
test('test that creating and deleting a new client via the modal works', async ({
page,
}) => {
const newClientName =
'New Project ' + Math.floor(1 + Math.random() * 10000);
test('test that creating and deleting a new client via the modal works', async ({ page }) => {
const newClientName = 'New Project ' + Math.floor(1 + Math.random() * 10000);
await goToProjectsOverview(page);
await page.getByRole('button', { name: 'Create Client' }).click();
await page.getByPlaceholder('Client Name').fill(newClientName);
@@ -28,13 +25,9 @@ test('test that creating and deleting a new client via the modal works', async (
]);
await expect(page.getByTestId('client_table')).toContainText(newClientName);
const moreButton = page.locator(
"[aria-label='Actions for Client " + newClientName + "']"
);
const moreButton = page.locator("[aria-label='Actions for Client " + newClientName + "']");
moreButton.click();
const deleteButton = page.locator(
"[aria-label='Delete Client " + newClientName + "']"
);
const deleteButton = page.locator("[aria-label='Delete Client " + newClientName + "']");
await Promise.all([
deleteButton.click(),
@@ -45,9 +38,7 @@ test('test that creating and deleting a new client via the modal works', async (
response.status() === 204
),
]);
await expect(page.getByTestId('client_table')).not.toContainText(
newClientName
);
await expect(page.getByTestId('client_table')).not.toContainText(newClientName);
});
test('test that archiving and unarchiving clients works', async ({ page }) => {

View File

@@ -22,12 +22,8 @@ test('test that new manager can be invited', async ({ page }) => {
await page.getByLabel('Email').fill(`new+${editorId}@editor.test`);
await page.getByRole('button', { name: 'Manager' }).click();
await Promise.all([
page
.getByRole('button', { name: 'Invite Member', exact: true })
.click(),
expect(page.getByRole('main')).toContainText(
`new+${editorId}@editor.test`
),
page.getByRole('button', { name: 'Invite Member', exact: true }).click(),
expect(page.getByRole('main')).toContainText(`new+${editorId}@editor.test`),
]);
});
@@ -38,12 +34,8 @@ test('test that new employee can be invited', async ({ page }) => {
await page.getByLabel('Email').fill(`new+${editorId}@editor.test`);
await page.getByRole('button', { name: 'Employee' }).click();
await Promise.all([
page
.getByRole('button', { name: 'Invite Member', exact: true })
.click(),
await expect(page.getByRole('main')).toContainText(
`new+${editorId}@editor.test`
),
page.getByRole('button', { name: 'Invite Member', exact: true }).click(),
await expect(page.getByRole('main')).toContainText(`new+${editorId}@editor.test`),
]);
});
@@ -54,12 +46,8 @@ test('test that new admin can be invited', async ({ page }) => {
await page.getByLabel('Email').fill(`new+${adminId}@admin.test`);
await page.getByRole('button', { name: 'Administrator' }).click();
await Promise.all([
page
.getByRole('button', { name: 'Invite Member', exact: true })
.click(),
expect(page.getByRole('main')).toContainText(
`new+${adminId}@admin.test`
),
page.getByRole('button', { name: 'Invite Member', exact: true }).click(),
expect(page.getByRole('main')).toContainText(`new+${adminId}@admin.test`),
]);
});
test('test that error shows if no role is selected', async ({ page }) => {
@@ -69,9 +57,7 @@ test('test that error shows if no role is selected', async ({ page }) => {
await page.getByLabel('Email').fill(`new+${noRoleId}@norole.test`);
await Promise.all([
page
.getByRole('button', { name: 'Invite Member', exact: true })
.click(),
page.getByRole('button', { name: 'Invite Member', exact: true }).click(),
expect(page.getByText('Please select a role')).toBeVisible(),
]);
});
@@ -85,9 +71,7 @@ test('test that organization billable rate can be updated with all existing time
await page.getByRole('menuitem').getByText('Edit').click();
await page.getByText('Organization Default Rate').click();
await page.getByText('Custom Rate').click();
await page
.getByPlaceholder('Billable Rate')
.fill(newBillableRate.toString());
await page.getByPlaceholder('Billable Rate').fill(newBillableRate.toString());
await page.getByRole('button', { name: 'Update Member' }).click();
await Promise.all([
@@ -103,8 +87,7 @@ test('test that organization billable rate can be updated with all existing time
response.url().includes('/organizations/') &&
response.request().method() === 'PUT' &&
response.status() === 200 &&
(await response.json()).data.billable_rate ===
newBillableRate * 100
(await response.json()).data.billable_rate === newBillableRate * 100
),
]);
});

View File

@@ -35,9 +35,9 @@ test('test that organization name can be updated', async ({ page }) => {
await page.getByLabel('Organization Name').fill('NEW ORG NAME');
await page.getByLabel('Organization Name').press('Enter');
await page.getByLabel('Organization Name').press('Meta+r');
await expect(
page.locator('[data-testid="organization_switcher"]:visible')
).toContainText('NEW ORG NAME');
await expect(page.locator('[data-testid="organization_switcher"]:visible')).toContainText(
'NEW ORG NAME'
);
});
test('test that organization billable rate can be updated with all existing time entries', async ({
@@ -46,9 +46,7 @@ test('test that organization billable rate can be updated with all existing time
await goToOrganizationSettings(page);
const newBillableRate = Math.round(Math.random() * 10000);
await page.getByLabel('Organization Billable Rate').click();
await page
.getByLabel('Organization Billable Rate')
.fill(newBillableRate.toString());
await page.getByLabel('Organization Billable Rate').fill(newBillableRate.toString());
await page
.locator('form')
.filter({ hasText: 'Organization Billable' })
@@ -56,9 +54,7 @@ test('test that organization billable rate can be updated with all existing time
.click();
await Promise.all([
page
.getByRole('button', { name: 'Yes, update existing time entries' })
.click(),
page.getByRole('button', { name: 'Yes, update existing time entries' }).click(),
page.waitForRequest(
async (request) =>
request.url().includes('/organizations/') &&
@@ -70,15 +66,12 @@ test('test that organization billable rate can be updated with all existing time
response.url().includes('/organizations/') &&
response.request().method() === 'PUT' &&
response.status() === 200 &&
(await response.json()).data.billable_rate ===
newBillableRate * 100
(await response.json()).data.billable_rate === newBillableRate * 100
),
]);
});
test('test that organization format settings can be updated', async ({
page,
}) => {
test('test that organization format settings can be updated', async ({ page }) => {
await goToOrganizationSettings(page);
// Test number format
@@ -113,8 +106,7 @@ test('test that organization format settings can be updated', async ({
response.url().includes('/organizations/') &&
response.request().method() === 'PUT' &&
response.status() === 200 &&
(await response.json()).data.currency_format ===
'iso-code-after-with-space'
(await response.json()).data.currency_format === 'iso-code-after-with-space'
),
]);
@@ -132,8 +124,7 @@ test('test that organization format settings can be updated', async ({
response.url().includes('/organizations/') &&
response.request().method() === 'PUT' &&
response.status() === 200 &&
(await response.json()).data.date_format ===
'slash-separated-dd-mm-yyyy'
(await response.json()).data.date_format === 'slash-separated-dd-mm-yyyy'
),
]);
@@ -169,19 +160,14 @@ test('test that organization format settings can be updated', async ({
response.url().includes('/organizations/') &&
response.request().method() === 'PUT' &&
response.status() === 200 &&
(await response.json()).data.interval_format ===
'hours-minutes-colon-separated'
(await response.json()).data.interval_format === 'hours-minutes-colon-separated'
),
]);
});
test('test that format settings are reflected in the dashboard', async ({
page,
}) => {
test('test that format settings are reflected in the dashboard', async ({ page }) => {
// check that 0h 00min is displayed
await expect(
page.getByText('0h 00min', { exact: true }).nth(0)
).toBeVisible();
await expect(page.getByText('0h 00min', { exact: true }).nth(0)).toBeVisible();
// First set the format settings
await goToOrganizationSettings(page);
@@ -213,10 +199,8 @@ test('test that format settings are reflected in the dashboard', async ({
response.url().includes('/organizations/') &&
response.request().method() === 'PUT' &&
response.status() === 200 &&
(await response.json()).data.interval_format ===
'hours-minutes-colon-separated' &&
(await response.json()).data.currency_format ===
'symbol-after' &&
(await response.json()).data.interval_format === 'hours-minutes-colon-separated' &&
(await response.json()).data.currency_format === 'symbol-after' &&
(await response.json()).data.number_format === 'comma-point'
),
]);
@@ -232,16 +216,12 @@ test('test that format settings are reflected in the dashboard', async ({
// check that 00:00 is displayed
await expect(page.getByText('0:00', { exact: true }).nth(0)).toBeVisible();
// check that 0h 00min is not displayed
await expect(
page.getByText('0h 00min', { exact: true }).nth(0)
).not.toBeVisible();
await expect(page.getByText('0h 00min', { exact: true }).nth(0)).not.toBeVisible();
// check that the current date is displayed in the dd/mm/yyyy format on the time page
await page.goto(PLAYWRIGHT_BASE_URL + '/time');
await expect(
page
.getByText(new Date().toLocaleDateString('en-GB'), { exact: true })
.nth(0)
page.getByText(new Date().toLocaleDateString('en-GB'), { exact: true }).nth(0)
).toBeVisible();
});

View File

@@ -1,34 +1,32 @@
import {test, expect} from '../playwright/fixtures';
import {PLAYWRIGHT_BASE_URL} from '../playwright/config';
import { test, expect } from '../playwright/fixtures';
import { PLAYWRIGHT_BASE_URL } from '../playwright/config';
test('test that user name can be updated', async ({page}) => {
test('test that user name can be updated', async ({ page }) => {
await page.goto(PLAYWRIGHT_BASE_URL + '/user/profile');
await page.getByLabel('Name', {exact: true} ).fill('NEW NAME');
await page.getByLabel('Name', { exact: true }).fill('NEW NAME');
await Promise.all([
page.getByRole('button', {name: 'Save'}).first().click(),
page.getByRole('button', { name: 'Save' }).first().click(),
page.waitForResponse('**/user/profile-information'),
]);
await page.reload();
await expect(page.getByLabel('Name', {exact: true})).toHaveValue('NEW NAME');
await expect(page.getByLabel('Name', { exact: true })).toHaveValue('NEW NAME');
});
test.skip('test that user email can be updated', async ({page}) => {
test.skip('test that user email can be updated', async ({ page }) => {
// this does not work because of email verification currently
await page.goto(PLAYWRIGHT_BASE_URL + '/user/profile');
const emailId = Math.round(Math.random() * 10000);
await page.getByLabel('Email').fill(`newemail+${emailId}@test.com`);
await page.getByRole('button', {name: 'Save'}).first().click();
await page.getByRole('button', { name: 'Save' }).first().click();
await page.reload();
await expect(page.getByLabel('Email')).toHaveValue(
`newemail+${emailId}@test.com`
);
await expect(page.getByLabel('Email')).toHaveValue(`newemail+${emailId}@test.com`);
});
async function createNewApiToken(page) {
await page.getByLabel('API Key Name').fill('NEW API KEY');
await Promise.all([
page.getByRole('button', {name: 'Create API Key'}).click(),
page.waitForResponse('**/users/me/api-tokens')
page.getByRole('button', { name: 'Create API Key' }).click(),
page.waitForResponse('**/users/me/api-tokens'),
]);
await expect(page.locator('body')).toContainText('API Token created successfully');
@@ -36,34 +34,37 @@ async function createNewApiToken(page) {
await expect(page.locator('body')).toContainText('NEW API KEY');
}
test('test that user can create an API key', async ({page}) => {
test('test that user can create an API key', async ({ page }) => {
await page.goto(PLAYWRIGHT_BASE_URL + '/user/profile');
await createNewApiToken(page);
});
test('test that user can delete an API key', async ({page}) => {
test('test that user can delete an API key', async ({ page }) => {
await page.goto(PLAYWRIGHT_BASE_URL + '/user/profile');
await createNewApiToken(page);
page.getByLabel('Delete API Token NEW API KEY').click();
await expect(page.getByRole('dialog')).toContainText('Are you sure you would like to delete this API token?');
await expect(page.getByRole('dialog')).toContainText(
'Are you sure you would like to delete this API token?'
);
await Promise.all([
page.getByRole('dialog').getByRole('button', {name: 'Delete'}).click(),
page.waitForResponse('**/users/me/api-tokens')
page.getByRole('dialog').getByRole('button', { name: 'Delete' }).click(),
page.waitForResponse('**/users/me/api-tokens'),
]);
await expect(page.locator('body')).not.toContainText('NEW API KEY');
});
test('test that user can revoke an API key', async ({page}) => {
test('test that user can revoke an API key', async ({ page }) => {
await page.goto(PLAYWRIGHT_BASE_URL + '/user/profile');
await createNewApiToken(page);
page.getByLabel('Revoke API Token NEW API KEY').click();
await expect(page.getByRole('dialog')).toContainText('Are you sure you would like to revoke this API token?');
await expect(page.getByRole('dialog')).toContainText(
'Are you sure you would like to revoke this API token?'
);
await Promise.all([
page.getByRole('dialog').getByRole('button', {name: 'Revoke'}).click(),
page.waitForResponse('**/users/me/api-tokens')
page.getByRole('dialog').getByRole('button', { name: 'Revoke' }).click(),
page.waitForResponse('**/users/me/api-tokens'),
]);
await expect(page.getByRole('button', {name: 'Revoke'})).toBeHidden();
await expect(page.getByRole('button', { name: 'Revoke' })).toBeHidden();
await expect(page.locator('body')).toContainText('NEW API KEY');
await expect(page.locator('body')).toContainText('Revoked');
});

View File

@@ -12,8 +12,7 @@ async function goToProjectsOverview(page: Page) {
test('test that updating project member billable rate works for existing time entries', async ({
page,
}) => {
const newProjectName =
'New Project ' + Math.floor(1 + Math.random() * 10000);
const newProjectName = 'New Project ' + Math.floor(1 + Math.random() * 10000);
const newBillableRate = Math.round(Math.random() * 10000);
await goToProjectsOverview(page);
await page.getByRole('button', { name: 'Create Project' }).click();
@@ -36,9 +35,7 @@ test('test that updating project member billable rate works for existing time en
.first()
.getByRole('button')
.click();
await page
.getByRole('menuitem', { name: 'Edit Project Member' })
.click();
await page.getByRole('menuitem', { name: 'Edit Project Member' }).click();
await page.getByLabel('Billable Rate').fill(newBillableRate.toString());
await page.getByRole('button', { name: 'Update Project Member' }).click();
@@ -55,8 +52,7 @@ test('test that updating project member billable rate works for existing time en
response.url().includes('/project-members/') &&
response.request().method() === 'PUT' &&
response.status() === 200 &&
(await response.json()).data.billable_rate ===
newBillableRate * 100
(await response.json()).data.billable_rate === newBillableRate * 100
),
]);
await expect(

View File

@@ -9,11 +9,8 @@ async function goToProjectsOverview(page: Page) {
}
// Create new project via modal
test('test that creating and deleting a new project via the modal works', async ({
page,
}) => {
const newProjectName =
'New Project ' + Math.floor(1 + Math.random() * 10000);
test('test that creating and deleting a new project via the modal works', async ({ page }) => {
const newProjectName = 'New Project ' + Math.floor(1 + Math.random() * 10000);
await goToProjectsOverview(page);
await page.getByRole('button', { name: 'Create Project' }).click();
await page.getByLabel('Project Name').fill(newProjectName);
@@ -31,16 +28,10 @@ test('test that creating and deleting a new project via the modal works', async
),
]);
await expect(page.getByTestId('project_table')).toContainText(
newProjectName
);
const moreButton = page.locator(
"[aria-label='Actions for Project " + newProjectName + "']"
);
await expect(page.getByTestId('project_table')).toContainText(newProjectName);
const moreButton = page.locator("[aria-label='Actions for Project " + newProjectName + "']");
moreButton.click();
const deleteButton = page.locator(
"[aria-label='Delete Project " + newProjectName + "']"
);
const deleteButton = page.locator("[aria-label='Delete Project " + newProjectName + "']");
await Promise.all([
deleteButton.click(),
@@ -51,14 +42,11 @@ test('test that creating and deleting a new project via the modal works', async
response.status() === 204
),
]);
await expect(page.getByTestId('project_table')).not.toContainText(
newProjectName
);
await expect(page.getByTestId('project_table')).not.toContainText(newProjectName);
});
test('test that archiving and unarchiving projects works', async ({ page }) => {
const newProjectName =
'New Project ' + Math.floor(1 + Math.random() * 10000);
const newProjectName = 'New Project ' + Math.floor(1 + Math.random() * 10000);
await goToProjectsOverview(page);
await page.getByRole('button', { name: 'Create Project' }).click();
await page.getByLabel('Project Name').fill(newProjectName);
@@ -87,11 +75,8 @@ test('test that archiving and unarchiving projects works', async ({ page }) => {
]);
});
test('test that updating billable rate works with existing time entries', async ({
page,
}) => {
const newProjectName =
'New Project ' + Math.floor(1 + Math.random() * 10000);
test('test that updating billable rate works with existing time entries', async ({ page }) => {
const newProjectName = 'New Project ' + Math.floor(1 + Math.random() * 10000);
const newBillableRate = Math.round(Math.random() * 10000);
await goToProjectsOverview(page);
await page.getByRole('button', { name: 'Create Project' }).click();
@@ -104,15 +89,11 @@ test('test that updating billable rate works with existing time entries', async
await page.getByRole('menuitem').getByText('Edit').first().click();
await page.getByText('Non-Billable').click();
await page.getByText('Custom Rate').click();
await page
.getByPlaceholder('Billable Rate')
.fill(newBillableRate.toString());
await page.getByPlaceholder('Billable Rate').fill(newBillableRate.toString());
await page.getByRole('button', { name: 'Update Project' }).click();
await Promise.all([
page
.locator('button').filter({ hasText: 'Yes, update existing time' })
.click(),
page.locator('button').filter({ hasText: 'Yes, update existing time' }).click(),
page.waitForRequest(
async (request) =>
request.url().includes('/projects/') &&
@@ -124,8 +105,7 @@ test('test that updating billable rate works with existing time entries', async
response.url().includes('/projects/') &&
response.request().method() === 'PUT' &&
response.status() === 200 &&
(await response.json()).data.billable_rate ===
newBillableRate * 100
(await response.json()).data.billable_rate === newBillableRate * 100
),
]);
await expect(

View File

@@ -2,8 +2,6 @@ import { expect, Page } from '@playwright/test';
import { PLAYWRIGHT_BASE_URL } from '../playwright/config';
import { test } from '../playwright/fixtures';
async function goToTimeOverview(page: Page) {
await page.goto(PLAYWRIGHT_BASE_URL + '/time');
}
@@ -31,7 +29,10 @@ async function createTimeEntryWithProject(page: Page, projectName: string, durat
await page.getByRole('button', { name: 'Manual time entry' }).click();
// Fill in the time entry details
await page.getByRole('dialog').getByRole('textbox', { name: 'Description' }).fill(`Time entry for ${projectName}`);
await page
.getByRole('dialog')
.getByRole('textbox', { name: 'Description' })
.fill(`Time entry for ${projectName}`);
await page.getByRole('button', { name: 'No Project' }).click();
await page.getByText(projectName).click();
@@ -43,7 +44,9 @@ async function createTimeEntryWithProject(page: Page, projectName: string, durat
// Submit the time entry
await Promise.all([
page.getByRole('button', { name: 'Create Time Entry' }).click(),
page.waitForResponse(response => response.url().includes('/time-entries') && response.status() === 201)
page.waitForResponse(
(response) => response.url().includes('/time-entries') && response.status() === 201
),
]);
}
@@ -52,7 +55,10 @@ async function createTimeEntryWithTag(page: Page, tagName: string, duration: str
await page.getByRole('button', { name: 'Manual time entry' }).click();
// Fill in the time entry details
await page.getByRole('dialog').getByRole('textbox', { name: 'Description' }).fill(`Time entry with tag ${tagName}`);
await page
.getByRole('dialog')
.getByRole('textbox', { name: 'Description' })
.fill(`Time entry with tag ${tagName}`);
// Add tag
await page.getByRole('button', { name: 'Tags' }).click();
@@ -69,12 +75,19 @@ async function createTimeEntryWithTag(page: Page, tagName: string, duration: str
await page.getByRole('button', { name: 'Create Time Entry' }).click();
}
async function createTimeEntryWithBillableStatus(page: Page, isBillable: boolean, duration: string) {
async function createTimeEntryWithBillableStatus(
page: Page,
isBillable: boolean,
duration: string
) {
await goToTimeOverview(page);
await page.getByRole('button', { name: 'Manual time entry' }).click();
// Fill in the time entry details
await page.getByRole('dialog').getByRole('textbox', { name: 'Description' }).fill(`Time entry ${isBillable ? 'billable' : 'non-billable'}`);
await page
.getByRole('dialog')
.getByRole('textbox', { name: 'Description' })
.fill(`Time entry ${isBillable ? 'billable' : 'non-billable'}`);
// Set billable status
await page.getByRole('button', { name: 'Non-Billable' }).click();
@@ -109,7 +122,10 @@ test('test that project filtering works in reporting', async ({ page }) => {
// escape
page.keyboard.press('Escape'),
// wait for API request to finish
page.waitForResponse(response => response.url().includes('/time-entries/aggregate') && response.status() === 200)
page.waitForResponse(
(response) =>
response.url().includes('/time-entries/aggregate') && response.status() === 200
),
]);
await page.waitForLoadState('networkidle');
@@ -138,7 +154,10 @@ test('test that tag filtering works in reporting', async ({ page }) => {
// escape
page.keyboard.press('Escape'),
// wait for API request to finish
page.waitForResponse(response => response.url().includes('/time-entries/aggregate') && response.status() === 200)
page.waitForResponse(
(response) =>
response.url().includes('/time-entries/aggregate') && response.status() === 200
),
]);
// Verify only time entries with tag1 are shown
@@ -160,14 +179,16 @@ test('test that billable status filtering works in reporting', async ({ page })
// escape
page.keyboard.press('Escape'),
// wait for API request to finish
page.waitForResponse(response => response.url().includes('/time-entries/aggregate') && response.status() === 200)
page.waitForResponse(
(response) =>
response.url().includes('/time-entries/aggregate') && response.status() === 200
),
]);
await page.waitForLoadState('networkidle');
await expect(page.getByTestId('reporting_view').getByText('1h 00min').first()).toBeVisible();
});
test('test that detailed view shows time entries correctly', async ({ page }) => {
const projectName = 'Detailed View Project ' + Math.floor(Math.random() * 10000);

View File

@@ -7,9 +7,7 @@ async function goToTagsOverview(page: Page) {
}
// Create new project via modal
test('test that creating and deleting a new client via the modal works', async ({
page,
}) => {
test('test that creating and deleting a new client via the modal works', async ({ page }) => {
const newTagName = 'New Tag ' + Math.floor(1 + Math.random() * 10000);
await goToTagsOverview(page);
await page.getByRole('button', { name: 'Create Tag' }).click();
@@ -27,13 +25,9 @@ test('test that creating and deleting a new client via the modal works', async (
]);
await expect(page.getByTestId('tag_table')).toContainText(newTagName);
const moreButton = page.locator(
"[aria-label='Actions for Tag " + newTagName + "']"
);
const moreButton = page.locator("[aria-label='Actions for Tag " + newTagName + "']");
moreButton.click();
const deleteButton = page.locator(
"[aria-label='Delete Tag " + newTagName + "']"
);
const deleteButton = page.locator("[aria-label='Delete Tag " + newTagName + "']");
await Promise.all([
deleteButton.click(),

View File

@@ -7,11 +7,8 @@ async function goToProjectsOverview(page: Page) {
}
// Create new project via modal
test('test that creating and deleting a new tag in a new project works', async ({
page,
}) => {
const newProjectName =
'New Project ' + Math.floor(1 + Math.random() * 10000);
test('test that creating and deleting a new tag in a new project works', async ({ page }) => {
const newProjectName = 'New Project ' + Math.floor(1 + Math.random() * 10000);
await goToProjectsOverview(page);
await page.getByRole('button', { name: 'Create Project' }).click();
await page.getByLabel('Project Name').fill(newProjectName);
@@ -29,9 +26,7 @@ test('test that creating and deleting a new tag in a new project works', async (
),
]);
await expect(page.getByTestId('project_table')).toContainText(
newProjectName
);
await expect(page.getByTestId('project_table')).toContainText(newProjectName);
await page.getByText(newProjectName).click();
@@ -55,13 +50,9 @@ test('test that creating and deleting a new tag in a new project works', async (
await expect(page.getByTestId('task_table')).toContainText(newTaskName);
const taskMoreButton = page.locator(
"[aria-label='Actions for Task " + newTaskName + "']"
);
const taskMoreButton = page.locator("[aria-label='Actions for Task " + newTaskName + "']");
taskMoreButton.click();
const taskDeleteButton = page.locator(
"[aria-label='Delete Task " + newTaskName + "']"
);
const taskDeleteButton = page.locator("[aria-label='Delete Task " + newTaskName + "']");
await Promise.all([
taskDeleteButton.click(),
@@ -76,13 +67,9 @@ test('test that creating and deleting a new tag in a new project works', async (
await goToProjectsOverview(page);
const moreButton = page.locator(
"[aria-label='Actions for Project " + newProjectName + "']"
);
const moreButton = page.locator("[aria-label='Actions for Project " + newProjectName + "']");
moreButton.click();
const deleteButton = page.locator(
"[aria-label='Delete Project " + newProjectName + "']"
);
const deleteButton = page.locator("[aria-label='Delete Project " + newProjectName + "']");
await Promise.all([
deleteButton.click(),
@@ -93,14 +80,11 @@ test('test that creating and deleting a new tag in a new project works', async (
response.status() === 204
),
]);
await expect(page.getByTestId('project_table')).not.toContainText(
newProjectName
);
await expect(page.getByTestId('project_table')).not.toContainText(newProjectName);
});
test('test that archiving and unarchiving tasks works', async ({ page }) => {
const newProjectName =
'New Project ' + Math.floor(1 + Math.random() * 10000);
const newProjectName = 'New Project ' + Math.floor(1 + Math.random() * 10000);
const newTaskName = 'New Project ' + Math.floor(1 + Math.random() * 10000);
await goToProjectsOverview(page);

View File

@@ -25,9 +25,7 @@ async function createEmptyTimeEntry(page: Page) {
startOrStopTimerWithButton(page),
assertThatTimerIsStopped(page),
page.waitForResponse(
(response) =>
response.url().includes('/time-entries') &&
response.status() === 200
(response) => response.url().includes('/time-entries') && response.status() === 200
),
]);
}
@@ -38,9 +36,7 @@ test('test that starting and stopping an empty time entry shows a new time entry
await Promise.all([
goToTimeOverview(page),
page.waitForResponse(
(response) =>
response.url().includes('/time-entries') &&
response.status() === 200
(response) => response.url().includes('/time-entries') && response.status() === 200
),
]);
await page.waitForTimeout(100);
@@ -56,9 +52,7 @@ test('test that starting and stopping an empty time entry shows a new time entry
// Test that description update works
async function assertThatTimeEntryRowIsStopped(newTimeEntry: Locator) {
await expect(newTimeEntry.getByTestId('timer_button')).toHaveClass(
/bg-accent-300\/70/
);
await expect(newTimeEntry.getByTestId('timer_button')).toHaveClass(/bg-accent-300\/70/);
}
test('test that updating a description of a time entry in the overview works on blur', async ({
@@ -71,17 +65,14 @@ test('test that updating a description of a time entry in the overview works on
await assertThatTimeEntryRowIsStopped(newTimeEntry);
const newDescription = Math.floor(Math.random() * 1000000).toString();
const descriptionElement = newTimeEntry.getByTestId(
'time_entry_description'
);
const descriptionElement = newTimeEntry.getByTestId('time_entry_description');
await descriptionElement.fill(newDescription);
await Promise.all([
descriptionElement.press('Tab'),
page.waitForResponse(async (response) => {
return (
response.status() === 200 &&
(await response.headerValue('Content-Type')) ===
'application/json' &&
(await response.headerValue('Content-Type')) === 'application/json' &&
(await response.json()).data.id !== null &&
(await response.json()).data.start !== null &&
(await response.json()).data.end !== null &&
@@ -90,8 +81,7 @@ test('test that updating a description of a time entry in the overview works on
(await response.json()).data.task_id === null &&
(await response.json()).data.duration !== null &&
(await response.json()).data.user_id !== null &&
JSON.stringify((await response.json()).data.tags) ===
JSON.stringify([])
JSON.stringify((await response.json()).data.tags) === JSON.stringify([])
);
}),
]);
@@ -107,17 +97,14 @@ test('test that updating a description of a time entry in the overview works on
const newTimeEntry = timeEntryRows.first();
await assertThatTimeEntryRowIsStopped(newTimeEntry);
const newDescription = Math.floor(Math.random() * 1000000).toString();
const descriptionElement = newTimeEntry.getByTestId(
'time_entry_description'
);
const descriptionElement = newTimeEntry.getByTestId('time_entry_description');
await descriptionElement.fill(newDescription);
await Promise.all([
descriptionElement.press('Enter'),
page.waitForResponse(async (response) => {
return (
response.status() === 200 &&
(await response.headerValue('Content-Type')) ===
'application/json' &&
(await response.headerValue('Content-Type')) === 'application/json' &&
(await response.json()).data.id !== null &&
(await response.json()).data.start !== null &&
(await response.json()).data.end !== null &&
@@ -126,16 +113,13 @@ test('test that updating a description of a time entry in the overview works on
(await response.json()).data.task_id === null &&
(await response.json()).data.duration !== null &&
(await response.json()).data.user_id !== null &&
JSON.stringify((await response.json()).data.tags) ===
JSON.stringify([])
JSON.stringify((await response.json()).data.tags) === JSON.stringify([])
);
}),
]);
});
test('test that adding a new tag to an existing time entry works', async ({
page,
}) => {
test('test that adding a new tag to an existing time entry works', async ({ page }) => {
await goToTimeOverview(page);
const timeEntryRows = page.locator('[data-testid="time_entry_row"]');
await createEmptyTimeEntry(page);
@@ -152,8 +136,7 @@ test('test that adding a new tag to an existing time entry works', async ({
page.waitForResponse(async (response) => {
return (
response.status() === 201 &&
(await response.headerValue('Content-Type')) ===
'application/json' &&
(await response.headerValue('Content-Type')) === 'application/json' &&
(await response.json()).data.name === newTagName
);
}),
@@ -163,8 +146,7 @@ test('test that adding a new tag to an existing time entry works', async ({
await page.waitForResponse(async (response) => {
return (
response.status() === 200 &&
(await response.headerValue('Content-Type')) ===
'application/json' &&
(await response.headerValue('Content-Type')) === 'application/json' &&
(await response.json()).data.id !== null &&
(await response.json()).data.start !== null &&
(await response.json()).data.end !== null &&
@@ -187,17 +169,14 @@ test('test that updating a the start of an existing time entry in the overview w
const newTimeEntry = timeEntryRows.first();
await assertThatTimeEntryRowIsStopped(newTimeEntry);
await page.waitForTimeout(1500);
const timeEntryRangeElement = newTimeEntry.getByTestId(
'time_entry_range_selector'
);
const timeEntryRangeElement = newTimeEntry.getByTestId('time_entry_range_selector');
await timeEntryRangeElement.click();
await page.getByTestId('time_entry_range_start').first().fill('1');
await Promise.all([
page.waitForResponse(async (response) => {
return (
response.status() === 200 &&
(await response.headerValue('Content-Type')) ===
'application/json' &&
(await response.headerValue('Content-Type')) === 'application/json' &&
(await response.json()).data.id !== null &&
// TODO! Actually check the value
(await response.json()).data.start !== null &&
@@ -208,9 +187,7 @@ test('test that updating a the start of an existing time entry in the overview w
]);
});
test('test that updating a the duration in the overview works on blur', async ({
page,
}) => {
test('test that updating a the duration in the overview works on blur', async ({ page }) => {
await goToTimeOverview(page);
const timeEntryRows = page.locator('[data-testid="time_entry_row"]');
await createEmptyTimeEntry(page);
@@ -225,8 +202,7 @@ test('test that updating a the duration in the overview works on blur', async ({
page.waitForResponse(async (response) => {
return (
response.status() === 200 &&
(await response.headerValue('Content-Type')) ===
'application/json' &&
(await response.headerValue('Content-Type')) === 'application/json' &&
(await response.json()).data.id !== null &&
// TODO! Actually check the value
(await response.json()).data.start !== null &&
@@ -240,9 +216,7 @@ test('test that updating a the duration in the overview works on blur', async ({
});
// Test that start stop button stops running timer
test('test that starting a time entry from the overview works', async ({
page,
}) => {
test('test that starting a time entry from the overview works', async ({ page }) => {
await goToTimeOverview(page);
const timeEntryRows = page.locator('[data-testid="time_entry_row"]');
await createEmptyTimeEntry(page);
@@ -255,8 +229,7 @@ test('test that starting a time entry from the overview works', async ({
page.waitForResponse(async (response) => {
return (
response.status() === 200 &&
(await response.headerValue('Content-Type')) ===
'application/json' &&
(await response.headerValue('Content-Type')) === 'application/json' &&
(await response.json()).data.id !== null &&
(await response.json()).data.start !== null &&
(await response.json()).data.end !== null
@@ -272,8 +245,7 @@ test('test that starting a time entry from the overview works', async ({
page.waitForResponse(async (response) => {
return (
response.status() === 200 &&
(await response.headerValue('Content-Type')) ===
'application/json' &&
(await response.headerValue('Content-Type')) === 'application/json' &&
(await response.json()).data.id !== null &&
(await response.json()).data.start !== null &&
(await response.json()).data.end !== null
@@ -284,9 +256,7 @@ test('test that starting a time entry from the overview works', async ({
]);
});
test('test that deleting a time entry from the overview works', async ({
page,
}) => {
test('test that deleting a time entry from the overview works', async ({ page }) => {
await goToTimeOverview(page);
const timeEntryRows = page.locator('[data-testid="time_entry_row"]');
await createEmptyTimeEntry(page);
@@ -302,16 +272,12 @@ test('test that deleting a time entry from the overview works', async ({
await expect(timeEntryRows).toHaveCount(0);
});
test.skip('test that load more works when the end of page is reached', async ({
page,
}) => {
test.skip('test that load more works when the end of page is reached', async ({ page }) => {
// this test is flaky when you do not need to scroll
await Promise.all([
goToTimeOverview(page),
page.waitForResponse(
(response) =>
response.url().includes('/time-entries') &&
response.status() === 200
(response) => response.url().includes('/time-entries') && response.status() === 200
),
]);
@@ -322,18 +288,14 @@ test.skip('test that load more works when the end of page is reached', async ({
return (
response.status() === 200 &&
response.url().includes('before') &&
(await response.headerValue('Content-Type')) ===
'application/json' &&
JSON.stringify((await response.json()).data) ===
JSON.stringify([])
(await response.headerValue('Content-Type')) === 'application/json' &&
JSON.stringify((await response.json()).data) === JSON.stringify([])
);
}),
]);
// assert that "All time entries are loaded!" is visible on page
await expect(page.locator('body')).toHaveText(
/All time entries are loaded!/
);
await expect(page.locator('body')).toHaveText(/All time entries are loaded!/);
});
// TODO: Test that updating the time entry start / end times works while it is running

View File

@@ -24,22 +24,15 @@ test('test that starting and stopping a timer without description and project wo
assertThatTimerHasStarted(page),
]);
await page.waitForTimeout(1500);
await Promise.all([
stoppedTimeEntryResponse(page),
startOrStopTimerWithButton(page),
]);
await Promise.all([stoppedTimeEntryResponse(page), startOrStopTimerWithButton(page)]);
await assertThatTimerIsStopped(page);
});
test('test that starting and stopping a timer with a description works', async ({
page,
}) => {
test('test that starting and stopping a timer with a description works', async ({ page }) => {
await goToDashboard(page);
// TODO: Fix flakyness by disabling description input field until timer is loaded
await page.waitForTimeout(500);
await page
.getByTestId('time_entry_description')
.fill('New Time Entry Description');
await page.getByTestId('time_entry_description').fill('New Time Entry Description');
await Promise.all([
newTimeEntryResponse(page, {
description: 'New Time Entry Description',
@@ -62,47 +55,29 @@ test('test that starting the time entry starts the live timer and that it keeps
}) => {
await goToDashboard(page);
await Promise.all([
newTimeEntryResponse(page),
startOrStopTimerWithButton(page),
]);
await Promise.all([newTimeEntryResponse(page), startOrStopTimerWithButton(page)]);
await assertThatTimerHasStarted(page);
await page.waitForTimeout(500);
const beforeTimerValue = await page
.getByTestId('time_entry_time')
.inputValue();
const beforeTimerValue = await page.getByTestId('time_entry_time').inputValue();
await page.waitForTimeout(2000);
const afterWaitTimeValue = await page
.getByTestId('time_entry_time')
.inputValue();
const afterWaitTimeValue = await page.getByTestId('time_entry_time').inputValue();
expect(afterWaitTimeValue).not.toEqual(beforeTimerValue);
await page.reload();
await page.waitForTimeout(500);
const afterReloadTimerValue = await page
.getByTestId('time_entry_time')
.inputValue();
const afterReloadTimerValue = await page.getByTestId('time_entry_time').inputValue();
await page.waitForTimeout(2000);
const afterReloadAfterWaitTimerValue = await page
.getByTestId('time_entry_time')
.inputValue();
const afterReloadAfterWaitTimerValue = await page.getByTestId('time_entry_time').inputValue();
expect(afterReloadTimerValue).not.toEqual(afterReloadAfterWaitTimerValue);
});
test('test that starting and updating the description while running works', async ({
page,
}) => {
test('test that starting and updating the description while running works', async ({ page }) => {
await goToDashboard(page);
await Promise.all([
newTimeEntryResponse(page),
startOrStopTimerWithButton(page),
]);
await Promise.all([newTimeEntryResponse(page), startOrStopTimerWithButton(page)]);
await assertThatTimerHasStarted(page);
await page.waitForTimeout(500);
await page
.getByTestId('time_entry_description')
.fill('New Time Entry Description');
await page.getByTestId('time_entry_description').fill('New Time Entry Description');
await Promise.all([
newTimeEntryResponse(page, {
@@ -121,9 +96,7 @@ test('test that starting and updating the description while running works', asyn
await assertThatTimerIsStopped(page);
});
test('test that starting and updating the time while running works', async ({
page,
}) => {
test('test that starting and updating the time while running works', async ({ page }) => {
await goToDashboard(page);
const [createResponse] = await Promise.all([
newTimeEntryResponse(page),
@@ -138,19 +111,16 @@ test('test that starting and updating the time while running works', async ({
return (
response.url().includes('/time-entries') &&
response.status() === 200 &&
(await response.headerValue('Content-Type')) ===
'application/json' &&
(await response.headerValue('Content-Type')) === 'application/json' &&
(await response.json()).data.id !== null &&
(await response.json()).data.start !== null &&
(await response.json()).data.start !==
(await createResponse.json()).data.start &&
(await response.json()).data.start !== (await createResponse.json()).data.start &&
(await response.json()).data.end === null &&
(await response.json()).data.project_id === null &&
(await response.json()).data.description === '' &&
(await response.json()).data.task_id === null &&
(await response.json()).data.user_id !== null &&
JSON.stringify((await response.json()).data.tags) ===
JSON.stringify([])
JSON.stringify((await response.json()).data.tags) === JSON.stringify([])
);
}),
page.getByTestId('time_entry_time').press('Enter'),
@@ -158,16 +128,11 @@ test('test that starting and updating the time while running works', async ({
await expect(page.getByTestId('time_entry_time')).toHaveValue(/00:20/);
await page.waitForTimeout(500);
await Promise.all([
stoppedTimeEntryResponse(page),
startOrStopTimerWithButton(page),
]);
await Promise.all([stoppedTimeEntryResponse(page), startOrStopTimerWithButton(page)]);
await assertThatTimerIsStopped(page);
});
test('test that entering a human readable time starts the timer on blur', async ({
page,
}) => {
test('test that entering a human readable time starts the timer on blur', async ({ page }) => {
await goToDashboard(page);
await page.getByTestId('time_entry_time').fill('20min');
await Promise.all([
@@ -177,18 +142,13 @@ test('test that entering a human readable time starts the timer on blur', async
await expect(page.getByTestId('time_entry_time')).toHaveValue(/00:20:/);
await assertThatTimerHasStarted(page);
await Promise.all([
stoppedTimeEntryResponse(page),
startOrStopTimerWithButton(page),
]);
await Promise.all([stoppedTimeEntryResponse(page), startOrStopTimerWithButton(page)]);
await page.locator(
'[data-testid="dashboard_timer"] [data-testid="timer_button"].bg-accent-300/70'
);
});
test('test that entering a number in the time range starts the timer on blur', async ({
page,
}) => {
test('test that entering a number in the time range starts the timer on blur', async ({ page }) => {
await goToDashboard(page);
await page.getByTestId('time_entry_time').fill('5');
await Promise.all([
@@ -198,10 +158,7 @@ test('test that entering a number in the time range starts the timer on blur', a
await expect(page.getByTestId('time_entry_time')).toHaveValue(/00:05:/);
await assertThatTimerHasStarted(page);
await Promise.all([
stoppedTimeEntryResponse(page),
startOrStopTimerWithButton(page),
]);
await Promise.all([stoppedTimeEntryResponse(page), startOrStopTimerWithButton(page)]);
await page.locator(
'[data-testid="dashboard_timer"] [data-testid="timer_button"].bg-accent-300/70'
);
@@ -219,10 +176,7 @@ test('test that entering a value with the format hh:mm in the time range starts
await expect(page.getByTestId('time_entry_time')).toHaveValue(/12:30:/);
await assertThatTimerHasStarted(page);
await Promise.all([
stoppedTimeEntryResponse(page),
startOrStopTimerWithButton(page),
]);
await Promise.all([stoppedTimeEntryResponse(page), startOrStopTimerWithButton(page)]);
await page.locator(
'[data-testid="dashboard_timer"] [data-testid="timer_button"].bg-accent-300/70'
);
@@ -239,9 +193,7 @@ test('test that entering a random value in the time range does not start the tim
);
});
test('test that entering a time starts the timer on enter', async ({
page,
}) => {
test('test that entering a time starts the timer on enter', async ({ page }) => {
await goToDashboard(page);
await page.getByTestId('time_entry_time').fill('20min');
await Promise.all([
@@ -249,10 +201,7 @@ test('test that entering a time starts the timer on enter', async ({
page.getByTestId('time_entry_time').press('Enter'),
]);
await assertThatTimerHasStarted(page);
await Promise.all([
stoppedTimeEntryResponse(page),
startOrStopTimerWithButton(page),
]);
await Promise.all([stoppedTimeEntryResponse(page), startOrStopTimerWithButton(page)]);
await assertThatTimerIsStopped(page);
});
@@ -273,15 +222,10 @@ test('test that adding a new tag works', async ({ page }) => {
await expect(page.getByRole('option', { name: newTagName })).toBeVisible();
});
test('test that adding a new tag when the timer is running', async ({
page,
}) => {
test('test that adding a new tag when the timer is running', async ({ page }) => {
const newTagName = 'New Tag' + Math.floor(Math.random() * 10000);
await goToDashboard(page);
await Promise.all([
newTimeEntryResponse(page),
startOrStopTimerWithButton(page),
]);
await Promise.all([newTimeEntryResponse(page), startOrStopTimerWithButton(page)]);
await assertThatTimerHasStarted(page);
await page.getByTestId('tag_dropdown').click();
await page.getByText('Create new tag').click();

View File

@@ -1,9 +1,7 @@
import { expect, Page } from '@playwright/test';
export async function startOrStopTimerWithButton(page: Page) {
await page
.locator('[data-testid="dashboard_timer"] [data-testid="timer_button"]')
.click();
await page.locator('[data-testid="dashboard_timer"] [data-testid="timer_button"]').click();
}
export async function assertThatTimerHasStarted(page: Page) {
@@ -20,8 +18,7 @@ export function newTimeEntryResponse(
return (
response.url().includes('/time-entries') &&
response.status() === status &&
(await response.headerValue('Content-Type')) ===
'application/json' &&
(await response.headerValue('Content-Type')) === 'application/json' &&
(await response.json()).data.id !== null &&
(await response.json()).data.start !== null &&
(await response.json()).data.end === null &&
@@ -29,30 +26,23 @@ export function newTimeEntryResponse(
(await response.json()).data.description === description &&
(await response.json()).data.task_id === null &&
(await response.json()).data.user_id !== null &&
JSON.stringify((await response.json()).data.tags) ===
JSON.stringify(tags)
JSON.stringify((await response.json()).data.tags) === JSON.stringify(tags)
);
});
}
export async function assertThatTimerIsStopped(page: Page) {
await expect(
page.locator(
'[data-testid="dashboard_timer"] [data-testid="timer_button"]'
)
page.locator('[data-testid="dashboard_timer"] [data-testid="timer_button"]')
).toHaveClass(/bg-accent-300\/70/);
}
export async function stoppedTimeEntryResponse(
page: Page,
{ description = '', tags = [] } = {}
) {
export async function stoppedTimeEntryResponse(page: Page, { description = '', tags = [] } = {}) {
return page.waitForResponse(async (response) => {
return (
response.status() === 200 &&
response.url().includes('/time-entries/') &&
(await response.headerValue('Content-Type')) ===
'application/json' &&
(await response.headerValue('Content-Type')) === 'application/json' &&
(await response.json()).data.id !== null &&
(await response.json()).data.start !== null &&
(await response.json()).data.end !== null &&
@@ -61,8 +51,7 @@ export async function stoppedTimeEntryResponse(
(await response.json()).data.task_id === null &&
(await response.json()).data.duration !== null &&
(await response.json()).data.user_id !== null &&
JSON.stringify((await response.json()).data.tags) ===
JSON.stringify(tags)
JSON.stringify((await response.json()).data.tags) === JSON.stringify(tags)
);
});
}

View File

@@ -14,4 +14,4 @@ export function formatCentsWithOrganizationDefaults(
currencySymbol,
'point-comma' as NumberFormat
);
}
}

View File

@@ -4,8 +4,7 @@ export function newTagResponse(page: Page, { name = '' } = {}) {
return page.waitForResponse(async (response) => {
return (
response.status() === 201 &&
(await response.headerValue('Content-Type')) ===
'application/json' &&
(await response.headerValue('Content-Type')) === 'application/json' &&
(await response.json()).data.name === name
);
});