import { expect, test } from '../playwright/fixtures'; import { PLAYWRIGHT_BASE_URL } from '../playwright/config'; async function goToOrganizationSettings(page) { await page.goto(PLAYWRIGHT_BASE_URL + '/dashboard'); await page.locator('[data-testid="organization_switcher"]:visible').click(); await page.getByText('Organization Settings').click(); } async function createTimeEntry(page, duration: string) { await page.goto(PLAYWRIGHT_BASE_URL + '/time'); // Open the dropdown menu and click "Manual time entry" await page.getByRole('button', { name: 'Time entry actions' }).click(); await page.getByRole('menuitem', { name: 'Manual time entry' }).click(); // Fill in the time entry details await page.getByTestId('time_entry_description').fill('Test time entry'); // Set duration await page.locator('[role="dialog"] input[name="Duration"]').fill(duration); await page.locator('[role="dialog"] input[name="Duration"]').press('Tab'); // Submit the time entry await Promise.all([ page.getByRole('button', { name: 'Create Time Entry' }).click(), page.waitForResponse( async (response) => response.url().includes('/time-entries') && response.request().method() === 'POST' && response.status() === 201 ), ]); } test('test that organization name can be updated', async ({ page }) => { await goToOrganizationSettings(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' ); }); test('test that organization billable rate can be updated with all existing time entries', async ({ page, }) => { 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 .locator('form') .filter({ hasText: 'Organization Billable' }) .getByRole('button', { name: 'Save' }) .click(); await Promise.all([ page.getByRole('button', { name: 'Yes, update existing time entries' }).click(), page.waitForRequest( async (request) => request.url().includes('/organizations/') && request.method() === 'PUT' && request.postDataJSON().billable_rate === newBillableRate * 100 ), page.waitForResponse( async (response) => response.url().includes('/organizations/') && response.request().method() === 'PUT' && response.status() === 200 && (await response.json()).data.billable_rate === newBillableRate * 100 ), ]); }); test('test that organization format settings can be updated', async ({ page }) => { await goToOrganizationSettings(page); // Test number format await page.getByLabel('Number Format').click(); await page.getByRole('option', { name: '1,111.11' }).click(); await Promise.all([ page .locator('form') .filter({ hasText: 'Number Format' }) .getByRole('button', { name: 'Save' }) .click(), page.waitForResponse( async (response) => response.url().includes('/organizations/') && response.request().method() === 'PUT' && response.status() === 200 && (await response.json()).data.number_format === 'comma-point' ), ]); // Test currency format await page.getByLabel('Currency Format').click(); await page.getByRole('option', { name: '111 EUR' }).click(); await Promise.all([ page .locator('form') .filter({ hasText: 'Currency Format' }) .getByRole('button', { name: 'Save' }) .click(), page.waitForResponse( async (response) => response.url().includes('/organizations/') && response.request().method() === 'PUT' && response.status() === 200 && (await response.json()).data.currency_format === 'iso-code-after-with-space' ), ]); // Test date format await page.getByLabel('Date Format').click(); await page.getByRole('option', { name: 'DD/MM/YYYY' }).click(); await Promise.all([ page .locator('form') .filter({ hasText: 'Date Format' }) .getByRole('button', { name: 'Save' }) .click(), page.waitForResponse( async (response) => response.url().includes('/organizations/') && response.request().method() === 'PUT' && response.status() === 200 && (await response.json()).data.date_format === 'slash-separated-dd-mm-yyyy' ), ]); // Test time format await page.getByLabel('Time Format').click(); await page.getByRole('option', { name: '24-hour clock' }).click(); await Promise.all([ page .locator('form') .filter({ hasText: 'Time Format' }) .getByRole('button', { name: 'Save' }) .click(), page.waitForResponse( async (response) => response.url().includes('/organizations/') && response.request().method() === 'PUT' && response.status() === 200 && (await response.json()).data.time_format === '24-hours' ), ]); // Test interval format await page.getByLabel('Time Duration Format').click(); await page.getByRole('option', { name: '12:03', exact: true }).click(); await Promise.all([ page .locator('form') .filter({ hasText: 'Time Duration Format' }) .getByRole('button', { name: 'Save' }) .click(), page.waitForResponse( async (response) => response.url().includes('/organizations/') && response.request().method() === 'PUT' && response.status() === 200 && (await response.json()).data.interval_format === 'hours-minutes-colon-separated' ), ]); }); 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(); // First set the format settings await goToOrganizationSettings(page); // Set number format to comma-point await page.getByLabel('Number Format').click(); await page.getByRole('option', { name: '1,111.11' }).click(); // Set currency format to symbol-after await page.getByLabel('Currency Format').click(); await page.getByRole('option', { name: '111€' }).click(); // Set interval format to hours-minutes-colon-separated await page.getByLabel('Time Duration Format').click(); await page.getByRole('option', { name: '12:03', exact: true }).click(); // Set date format to DD/MM/YYYY await page.getByLabel('Date Format').click(); await page.getByRole('option', { name: 'DD/MM/YYYY' }).click(); await Promise.all([ page .locator('form') .filter({ hasText: 'Time Duration Format' }) .getByRole('button', { name: 'Save' }) .click(), page.waitForResponse( async (response) => 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.number_format === 'comma-point' ), ]); await createTimeEntry(page, '00:00'); // Go to dashboard and check the formats await page.goto(PLAYWRIGHT_BASE_URL + '/dashboard'); // Check billable amount format (number and currency) await expect(page.getByText('0.00€')).toBeVisible(); // 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(); // check that the current date is displayed in the dd/mm/yyyy format on the time page await page.goto(PLAYWRIGHT_BASE_URL + '/time'); // Wait for time entries to load so organization data is available for date formatting await page.waitForResponse( (response) => response.url().includes('/time-entries') && response.status() === 200 ); await expect( page.getByText(new Date().toLocaleDateString('en-GB'), { exact: true }).nth(0) ).toBeVisible({ timeout: 10000 }); }); test('test that organization time entry settings can be toggled', async ({ page }) => { await goToOrganizationSettings(page); const preventOverlappingCheckbox = page.getByLabel( 'Prevent overlapping time entries (new entries only)' ); const manageTasksCheckbox = page.getByLabel('Allow Employees to manage tasks'); // Get current states and toggle both const wasOverlappingChecked = await preventOverlappingCheckbox.isChecked(); const wasManageTasksChecked = await manageTasksCheckbox.isChecked(); if (wasOverlappingChecked) { await preventOverlappingCheckbox.uncheck(); } else { await preventOverlappingCheckbox.check(); } if (wasManageTasksChecked) { await manageTasksCheckbox.uncheck(); } else { await manageTasksCheckbox.check(); } // Save const settingsForm = page.locator('form').filter({ hasText: 'Prevent overlapping' }); await Promise.all([ settingsForm.getByRole('button', { name: 'Save' }).click(), page.waitForResponse( async (response) => response.url().includes('/organizations/') && response.request().method() === 'PUT' && response.status() === 200 && (await response.json()).data.prevent_overlapping_time_entries === !wasOverlappingChecked ), ]); // Reload and verify both settings persisted await page.reload(); await expect(preventOverlappingCheckbox).toBeChecked({ checked: !wasOverlappingChecked }); await expect(manageTasksCheckbox).toBeChecked({ checked: !wasManageTasksChecked }); // Toggle both back to restore original state if (!wasOverlappingChecked) { await preventOverlappingCheckbox.uncheck(); } else { await preventOverlappingCheckbox.check(); } if (!wasManageTasksChecked) { await manageTasksCheckbox.uncheck(); } else { await manageTasksCheckbox.check(); } await Promise.all([ settingsForm.getByRole('button', { name: 'Save' }).click(), page.waitForResponse( async (response) => response.url().includes('/organizations/') && response.request().method() === 'PUT' && response.status() === 200 && (await response.json()).data.prevent_overlapping_time_entries === wasOverlappingChecked ), ]); }); test('test that 12-hour clock format can be set', async ({ page }) => { await goToOrganizationSettings(page); await page.getByLabel('Time Format').click(); await page.getByRole('option', { name: '12-hour clock' }).click(); await Promise.all([ page .locator('form') .filter({ hasText: 'Time Format' }) .getByRole('button', { name: 'Save' }) .click(), page.waitForResponse( async (response) => response.url().includes('/organizations/') && response.request().method() === 'PUT' && response.status() === 200 && (await response.json()).data.time_format === '12-hours' ), ]); // Reload and verify it persisted await page.reload(); await expect(page.getByLabel('Time Format')).toContainText('12-hour clock'); // Reset back to 24-hour await page.getByLabel('Time Format').click(); await page.getByRole('option', { name: '24-hour clock' }).click(); await Promise.all([ page .locator('form') .filter({ hasText: 'Time Format' }) .getByRole('button', { name: 'Save' }) .click(), page.waitForResponse( async (response) => response.url().includes('/organizations/') && response.request().method() === 'PUT' && response.status() === 200 && (await response.json()).data.time_format === '24-hours' ), ]); }); test('test that format settings persist after page reload', async ({ page }) => { await goToOrganizationSettings(page); // Set a specific date format await page.getByLabel('Date Format').click(); await page.getByRole('option', { name: 'DD/MM/YYYY' }).click(); await Promise.all([ page .locator('form') .filter({ hasText: 'Date Format' }) .getByRole('button', { name: 'Save' }) .click(), page.waitForResponse( async (response) => response.url().includes('/organizations/') && response.request().method() === 'PUT' && response.status() === 200 ), ]); // Reload and verify it persisted await page.reload(); await expect(page.getByLabel('Date Format')).toContainText('DD/MM/YYYY'); }); // ============================================= // Admin Permission Tests // ============================================= test.describe('Admin Organization Settings Access', () => { test('admin can see and edit organization settings', async ({ ctx, admin }) => { await admin.page.goto(PLAYWRIGHT_BASE_URL + '/teams/' + ctx.orgId); // Organization Name section is visible await expect( admin.page.getByRole('heading', { name: 'Organization Name', level: 3 }) ).toBeVisible({ timeout: 10000 }); // Editable settings sections should be visible await expect( admin.page.getByRole('heading', { name: 'Billable Rate', level: 3 }) ).toBeVisible(); await expect( admin.page.getByRole('heading', { name: 'Format Settings', level: 3 }) ).toBeVisible(); await expect( admin.page.getByRole('heading', { name: 'Organization Settings', level: 3 }) ).toBeVisible(); // Save buttons should be visible (admin can update) await expect(admin.page.getByRole('button', { name: 'Save' }).first()).toBeVisible(); // Delete organization should NOT be visible (owner only) await expect( admin.page.getByRole('heading', { name: 'Delete Organization' }) ).not.toBeVisible(); }); }); // ============================================= // Employee Permission Tests // ============================================= test.describe('Employee Organization Settings Restrictions', () => { test('employee can see org name but not editable settings', async ({ ctx, employee }) => { await employee.page.goto(PLAYWRIGHT_BASE_URL + '/teams/' + ctx.orgId); // Organization Name section is visible (but inputs are disabled) await expect( employee.page.getByRole('heading', { name: 'Organization Name', level: 3 }) ).toBeVisible({ timeout: 10000 }); // Editable settings sections should NOT be visible await expect( employee.page.getByRole('heading', { name: 'Billable Rate', level: 3 }) ).not.toBeVisible(); await expect( employee.page.getByRole('heading', { name: 'Format Settings', level: 3 }) ).not.toBeVisible(); await expect( employee.page.getByRole('heading', { name: 'Organization Settings', level: 3 }) ).not.toBeVisible(); // Save button should not be visible (employee cannot update) await expect(employee.page.getByRole('button', { name: 'Save' })).not.toBeVisible(); }); });