mirror of
https://github.com/solidtime-io/solidtime.git
synced 2026-08-08 00:02:15 +01:00
642 lines
24 KiB
TypeScript
642 lines
24 KiB
TypeScript
import { PLAYWRIGHT_BASE_URL } from '../playwright/config';
|
|
import { test } from '../playwright/fixtures';
|
|
import { expect } from '@playwright/test';
|
|
import type { Page } from '@playwright/test';
|
|
import { createProjectViaApi, createTaskViaApi, createTimeEntryOnDateViaApi } from './utils/api';
|
|
|
|
// ──────────────────────────────────────────────────
|
|
// Helpers
|
|
// ──────────────────────────────────────────────────
|
|
|
|
async function goToTimesheet(page: Page) {
|
|
await page.addInitScript(() => {
|
|
window.localStorage.setItem('showReleaseInfo-desktop', 'false');
|
|
});
|
|
await page.goto(PLAYWRIGHT_BASE_URL + '/timesheet');
|
|
}
|
|
|
|
function getMonday(d: Date): Date {
|
|
const date = new Date(d);
|
|
const day = date.getUTCDay();
|
|
const diff = date.getUTCDate() - day + (day === 0 ? -6 : 1);
|
|
date.setUTCDate(diff);
|
|
date.setUTCHours(0, 0, 0, 0);
|
|
return date;
|
|
}
|
|
|
|
function getCurrentWeekMonday(): Date {
|
|
return getMonday(new Date());
|
|
}
|
|
|
|
function getLastWeekMonday(): Date {
|
|
const monday = getCurrentWeekMonday();
|
|
monday.setUTCDate(monday.getUTCDate() - 7);
|
|
return monday;
|
|
}
|
|
|
|
function getDayOfWeek(weekStart: Date, dayOffset: number): Date {
|
|
const date = new Date(weekStart);
|
|
date.setUTCDate(date.getUTCDate() + dayOffset);
|
|
return date;
|
|
}
|
|
|
|
async function waitForTimesheetLoad(page: Page) {
|
|
await page.waitForURL(/\/timesheet(?:$|\?)/);
|
|
await expect(page.getByTestId('timesheet_view')).toBeVisible();
|
|
await expect(page.getByTestId('timesheet_week_display')).toBeVisible();
|
|
|
|
const timezoneMismatchModal = page
|
|
.getByRole('dialog')
|
|
.filter({ hasText: 'Timezone mismatch detected' });
|
|
if (await timezoneMismatchModal.isVisible().catch(() => false)) {
|
|
await timezoneMismatchModal.getByRole('button', { name: 'Cancel' }).click();
|
|
await expect(timezoneMismatchModal).not.toBeVisible();
|
|
}
|
|
}
|
|
|
|
function addRowButton(page: Page) {
|
|
return page.getByRole('button', { name: /Add row/i }).first();
|
|
}
|
|
|
|
async function chooseRowIdentity(page: Page, optionName: string) {
|
|
await addRowButton(page).click();
|
|
|
|
const dialog = page.getByRole('dialog', { name: /Add row/i });
|
|
const dialogVisible = await dialog
|
|
.waitFor({ state: 'visible', timeout: 1000 })
|
|
.then(() => true)
|
|
.catch(() => false);
|
|
|
|
if (dialogVisible) {
|
|
await dialog.getByRole('option', { name: optionName }).click();
|
|
return;
|
|
}
|
|
|
|
if (optionName === 'No Project') return;
|
|
|
|
const row = page.locator('[data-testid="timesheet_row"]').first();
|
|
await row.getByText('No Project').click();
|
|
await page.getByText(optionName).click();
|
|
}
|
|
|
|
// ──────────────────────────────────────────────────
|
|
// Navigation & Page Load
|
|
// ──────────────────────────────────────────────────
|
|
|
|
test('timesheet renders empty with add row + copy last week actions', async ({ page }) => {
|
|
await Promise.all([goToTimesheet(page), waitForTimesheetLoad(page)]);
|
|
|
|
await expect(page.locator('[data-testid="timesheet_row"]')).toHaveCount(0);
|
|
await expect(addRowButton(page)).toBeVisible();
|
|
await expect(page.getByRole('button', { name: /Copy last week/i })).toBeVisible();
|
|
});
|
|
|
|
// ──────────────────────────────────────────────────
|
|
// Display Existing Time Entries
|
|
// ──────────────────────────────────────────────────
|
|
|
|
test('timesheet displays existing time entries grouped by project', async ({ page, ctx }) => {
|
|
const monday = getCurrentWeekMonday();
|
|
const tuesday = getDayOfWeek(monday, 1);
|
|
const wednesday = getDayOfWeek(monday, 2);
|
|
|
|
const projectA = await createProjectViaApi(ctx, { name: 'Project Alpha' });
|
|
const projectB = await createProjectViaApi(ctx, { name: 'Project Beta' });
|
|
|
|
await createTimeEntryOnDateViaApi(ctx, {
|
|
date: monday,
|
|
duration: '2h',
|
|
projectId: projectA.id,
|
|
});
|
|
await createTimeEntryOnDateViaApi(ctx, {
|
|
date: wednesday,
|
|
duration: '1h',
|
|
projectId: projectA.id,
|
|
});
|
|
await createTimeEntryOnDateViaApi(ctx, {
|
|
date: tuesday,
|
|
duration: '3h',
|
|
projectId: projectB.id,
|
|
});
|
|
|
|
await Promise.all([goToTimesheet(page), waitForTimesheetLoad(page)]);
|
|
|
|
const rows = page.locator('[data-testid="timesheet_row"]');
|
|
await expect(rows).toHaveCount(2);
|
|
|
|
// Check that the grand total is shown
|
|
await expect(page.getByTestId('timesheet_grand_total')).toBeVisible();
|
|
});
|
|
|
|
test('timesheet groups entries by project and task combination', async ({ page, ctx }) => {
|
|
const monday = getCurrentWeekMonday();
|
|
|
|
const project = await createProjectViaApi(ctx, { name: 'Task Project' });
|
|
const taskA = await createTaskViaApi(ctx, { name: 'Task A', project_id: project.id });
|
|
const taskB = await createTaskViaApi(ctx, { name: 'Task B', project_id: project.id });
|
|
|
|
await createTimeEntryOnDateViaApi(ctx, {
|
|
date: monday,
|
|
duration: '1h',
|
|
projectId: project.id,
|
|
taskId: taskA.id,
|
|
});
|
|
await createTimeEntryOnDateViaApi(ctx, {
|
|
date: monday,
|
|
duration: '2h',
|
|
projectId: project.id,
|
|
taskId: taskB.id,
|
|
});
|
|
|
|
await Promise.all([goToTimesheet(page), waitForTimesheetLoad(page)]);
|
|
|
|
const rows = page.locator('[data-testid="timesheet_row"]');
|
|
await expect(rows).toHaveCount(2);
|
|
});
|
|
|
|
// ──────────────────────────────────────────────────
|
|
// Enter Duration in Cell
|
|
// ──────────────────────────────────────────────────
|
|
|
|
test('entering duration in empty cell creates a time entry', async ({ page, ctx }) => {
|
|
await createProjectViaApi(ctx, { name: 'Duration Test' });
|
|
|
|
await Promise.all([goToTimesheet(page), waitForTimesheetLoad(page)]);
|
|
|
|
await chooseRowIdentity(page, 'Duration Test');
|
|
|
|
const row = page.locator('[data-testid="timesheet_row"]').first();
|
|
|
|
// Click the first day cell and enter duration
|
|
const cells = row.locator('[data-testid="timesheet_cell"]');
|
|
const mondayCell = cells.first();
|
|
const mondayInput = mondayCell.locator('input');
|
|
|
|
await mondayInput.click();
|
|
await mondayInput.fill('2');
|
|
|
|
// Submit and wait for create response
|
|
const [response] = await Promise.all([
|
|
page.waitForResponse(
|
|
(resp) =>
|
|
resp.url().includes('/time-entries') &&
|
|
resp.request().method() === 'POST' &&
|
|
resp.status() === 201
|
|
),
|
|
mondayInput.press('Enter'),
|
|
]);
|
|
|
|
expect(response.status()).toBe(201);
|
|
|
|
// Verify the cell shows the duration
|
|
await expect(mondayInput).not.toHaveValue('');
|
|
});
|
|
|
|
// ──────────────────────────────────────────────────
|
|
// Edit Duration (Increase)
|
|
// ──────────────────────────────────────────────────
|
|
|
|
test('increasing duration in cell extends the last time entry', async ({ page, ctx }) => {
|
|
const monday = getCurrentWeekMonday();
|
|
const project = await createProjectViaApi(ctx, { name: 'Increase Test' });
|
|
|
|
await createTimeEntryOnDateViaApi(ctx, {
|
|
date: monday,
|
|
duration: '1h',
|
|
projectId: project.id,
|
|
});
|
|
|
|
await Promise.all([goToTimesheet(page), waitForTimesheetLoad(page)]);
|
|
|
|
const row = page.locator('[data-testid="timesheet_row"]').first();
|
|
const cells = row.locator('[data-testid="timesheet_cell"]');
|
|
const mondayInput = cells.first().locator('input');
|
|
|
|
// Click and change to 3 hours
|
|
await mondayInput.click();
|
|
await mondayInput.fill('3');
|
|
|
|
const [response] = await Promise.all([
|
|
page.waitForResponse(
|
|
(resp) =>
|
|
resp.url().includes('/time-entries') &&
|
|
resp.request().method() === 'PUT' &&
|
|
resp.status() === 200
|
|
),
|
|
mondayInput.press('Enter'),
|
|
]);
|
|
|
|
expect(response.status()).toBe(200);
|
|
});
|
|
|
|
// ──────────────────────────────────────────────────
|
|
// Edit Duration (Decrease)
|
|
// ──────────────────────────────────────────────────
|
|
|
|
test('decreasing duration in cell shortens the last time entry', async ({ page, ctx }) => {
|
|
const monday = getCurrentWeekMonday();
|
|
const project = await createProjectViaApi(ctx, { name: 'Decrease Test' });
|
|
|
|
await createTimeEntryOnDateViaApi(ctx, {
|
|
date: monday,
|
|
duration: '3h',
|
|
projectId: project.id,
|
|
});
|
|
|
|
await Promise.all([goToTimesheet(page), waitForTimesheetLoad(page)]);
|
|
|
|
const row = page.locator('[data-testid="timesheet_row"]').first();
|
|
const cells = row.locator('[data-testid="timesheet_cell"]');
|
|
const mondayInput = cells.first().locator('input');
|
|
|
|
await mondayInput.click();
|
|
await mondayInput.fill('1');
|
|
|
|
const [response] = await Promise.all([
|
|
page.waitForResponse(
|
|
(resp) =>
|
|
resp.url().includes('/time-entries') &&
|
|
resp.request().method() === 'PUT' &&
|
|
resp.status() === 200
|
|
),
|
|
mondayInput.press('Enter'),
|
|
]);
|
|
|
|
expect(response.status()).toBe(200);
|
|
});
|
|
|
|
// ──────────────────────────────────────────────────
|
|
// Clear Cell
|
|
// ──────────────────────────────────────────────────
|
|
|
|
test('clearing a cell deletes all time entries for that project+day', async ({ page, ctx }) => {
|
|
const monday = getCurrentWeekMonday();
|
|
const project = await createProjectViaApi(ctx, { name: 'Clear Test' });
|
|
|
|
await createTimeEntryOnDateViaApi(ctx, {
|
|
date: monday,
|
|
duration: '2h',
|
|
projectId: project.id,
|
|
});
|
|
|
|
await Promise.all([goToTimesheet(page), waitForTimesheetLoad(page)]);
|
|
|
|
const row = page.locator('[data-testid="timesheet_row"]').first();
|
|
const cells = row.locator('[data-testid="timesheet_cell"]');
|
|
const mondayInput = cells.first().locator('input');
|
|
|
|
await mondayInput.click();
|
|
await mondayInput.fill('0');
|
|
|
|
const [response] = await Promise.all([
|
|
page.waitForResponse(
|
|
(resp) =>
|
|
resp.url().includes('/time-entries') &&
|
|
resp.request().method() === 'DELETE' &&
|
|
resp.status() === 200
|
|
),
|
|
mondayInput.press('Enter'),
|
|
]);
|
|
|
|
expect(response.status()).toBe(200);
|
|
});
|
|
|
|
test('Escape during cell edit reverts the displayed value without an API call', async ({
|
|
page,
|
|
ctx,
|
|
}) => {
|
|
const monday = getCurrentWeekMonday();
|
|
const project = await createProjectViaApi(ctx, { name: 'Escape Cancel Test' });
|
|
|
|
await createTimeEntryOnDateViaApi(ctx, {
|
|
date: monday,
|
|
duration: '2h',
|
|
projectId: project.id,
|
|
});
|
|
|
|
await Promise.all([goToTimesheet(page), waitForTimesheetLoad(page)]);
|
|
|
|
const row = page.locator('[data-testid="timesheet_row"]').first();
|
|
const cells = row.locator('[data-testid="timesheet_cell"]');
|
|
const mondayInput = cells.first().locator('input');
|
|
|
|
// Capture the formatted display value before editing.
|
|
const originalValue = await mondayInput.inputValue();
|
|
expect(originalValue).toMatch(/2/);
|
|
|
|
let mutationFired = false;
|
|
page.on('request', (req) => {
|
|
if (req.url().includes('/time-entries') && req.method() !== 'GET') {
|
|
mutationFired = true;
|
|
}
|
|
});
|
|
|
|
await mondayInput.click();
|
|
await mondayInput.fill('5');
|
|
await mondayInput.press('Escape');
|
|
|
|
// The Escape handler reverts the displayed value synchronously, so
|
|
// once this assertion passes we know the handler ran. Any mutation
|
|
// request would have been queued by then.
|
|
await expect(mondayInput).toHaveValue(originalValue);
|
|
expect(mutationFired).toBe(false);
|
|
});
|
|
|
|
// ──────────────────────────────────────────────────
|
|
// Week Navigation
|
|
// ──────────────────────────────────────────────────
|
|
|
|
test('navigating to previous week shows entries from that week', async ({ page, ctx }) => {
|
|
const lastMonday = getLastWeekMonday();
|
|
const project = await createProjectViaApi(ctx, { name: 'Last Week Project' });
|
|
|
|
await createTimeEntryOnDateViaApi(ctx, {
|
|
date: lastMonday,
|
|
duration: '2h',
|
|
projectId: project.id,
|
|
});
|
|
|
|
await Promise.all([goToTimesheet(page), waitForTimesheetLoad(page)]);
|
|
|
|
// Current week should have no entries
|
|
await expect(page.locator('[data-testid="timesheet_row"]')).toHaveCount(0);
|
|
|
|
// Go to previous week — the row-count assertion below auto-retries
|
|
// until the new week's data arrives.
|
|
await page.getByTestId('timesheet_prev_week').click();
|
|
|
|
// Should now see the entry
|
|
const rows = page.locator('[data-testid="timesheet_row"]');
|
|
await expect(rows).toHaveCount(1);
|
|
});
|
|
|
|
test('can navigate forward and return to current week', async ({ page }) => {
|
|
await Promise.all([goToTimesheet(page), waitForTimesheetLoad(page)]);
|
|
|
|
// Should show "This week"
|
|
await expect(page.getByTestId('timesheet_week_display')).toContainText('This week');
|
|
|
|
// Go to next week — the text assertions below auto-retry until the
|
|
// header label flips.
|
|
await page.getByTestId('timesheet_next_week').click();
|
|
|
|
// Should no longer show "This week"
|
|
await expect(page.getByTestId('timesheet_week_display')).not.toContainText('This week');
|
|
|
|
// Go back to this week
|
|
await page.getByTestId('timesheet_week_display').click();
|
|
|
|
await expect(page.getByTestId('timesheet_week_display')).toContainText('This week');
|
|
});
|
|
|
|
// ──────────────────────────────────────────────────
|
|
// Copy Last Week
|
|
// ──────────────────────────────────────────────────
|
|
|
|
test('copy last week adds project rows from previous week without hours', async ({ page, ctx }) => {
|
|
const lastMonday = getLastWeekMonday();
|
|
const lastWednesday = getDayOfWeek(lastMonday, 2);
|
|
|
|
const projectA = await createProjectViaApi(ctx, { name: 'Copy Project A' });
|
|
const projectB = await createProjectViaApi(ctx, { name: 'Copy Project B' });
|
|
|
|
await createTimeEntryOnDateViaApi(ctx, {
|
|
date: lastMonday,
|
|
duration: '2h',
|
|
projectId: projectA.id,
|
|
});
|
|
await createTimeEntryOnDateViaApi(ctx, {
|
|
date: lastWednesday,
|
|
duration: '3h',
|
|
projectId: projectB.id,
|
|
});
|
|
|
|
await Promise.all([goToTimesheet(page), waitForTimesheetLoad(page)]);
|
|
|
|
// Current week should have no populated rows yet.
|
|
await expect(page.locator('[data-testid="timesheet_row"]')).toHaveCount(0);
|
|
|
|
// Open copy last week dropdown and click "Copy rows only"
|
|
await page.getByRole('button', { name: /Copy last week/i }).click();
|
|
await page.getByText('Copy rows only').click();
|
|
|
|
// Should now show 2 rows (one per project)
|
|
const rows = page.locator('[data-testid="timesheet_row"]');
|
|
await expect(rows).toHaveCount(2);
|
|
|
|
// All row totals should be 0
|
|
const rowTotals = page.locator('[data-testid="timesheet_row_total"]');
|
|
const count = await rowTotals.count();
|
|
for (let i = 0; i < count; i++) {
|
|
await expect(rowTotals.nth(i)).toContainText('-');
|
|
}
|
|
});
|
|
|
|
test('copy last week does not duplicate rows that already exist', async ({ page, ctx }) => {
|
|
const lastMonday = getLastWeekMonday();
|
|
const thisMonday = getCurrentWeekMonday();
|
|
const thisTuesday = getDayOfWeek(thisMonday, 1);
|
|
|
|
const project = await createProjectViaApi(ctx, { name: 'No Dup Project' });
|
|
|
|
// Create entry for last week
|
|
await createTimeEntryOnDateViaApi(ctx, {
|
|
date: lastMonday,
|
|
duration: '2h',
|
|
projectId: project.id,
|
|
});
|
|
|
|
// Create entry for current week
|
|
await createTimeEntryOnDateViaApi(ctx, {
|
|
date: thisTuesday,
|
|
duration: '1h',
|
|
projectId: project.id,
|
|
});
|
|
|
|
await Promise.all([goToTimesheet(page), waitForTimesheetLoad(page)]);
|
|
|
|
// Should have 1 row (from current week entry)
|
|
const rows = page.locator('[data-testid="timesheet_row"]');
|
|
await expect(rows).toHaveCount(1);
|
|
|
|
// Open copy last week dropdown and click "Copy rows only"
|
|
await page.getByRole('button', { name: /Copy last week/i }).click();
|
|
await page.getByText('Copy rows only').click();
|
|
|
|
// Should still have only 1 row (not duplicated)
|
|
await expect(rows).toHaveCount(1);
|
|
});
|
|
|
|
test('copy last week with time entries creates rows and entries', async ({ page, ctx }) => {
|
|
const lastMonday = getLastWeekMonday();
|
|
|
|
const project = await createProjectViaApi(ctx, { name: 'Copy Time Project' });
|
|
|
|
await createTimeEntryOnDateViaApi(ctx, {
|
|
date: lastMonday,
|
|
duration: '2h',
|
|
projectId: project.id,
|
|
});
|
|
|
|
await Promise.all([goToTimesheet(page), waitForTimesheetLoad(page)]);
|
|
|
|
// Current week should have no populated rows yet.
|
|
await expect(page.locator('[data-testid="timesheet_row"]')).toHaveCount(0);
|
|
|
|
// Open copy last week dropdown and click "Copy rows and time entries"
|
|
await page.getByRole('button', { name: /Copy last week/i }).click();
|
|
|
|
await Promise.all([
|
|
page.waitForResponse(
|
|
(resp) =>
|
|
resp.url().includes('/time-entries') &&
|
|
resp.request().method() === 'POST' &&
|
|
resp.status() === 201
|
|
),
|
|
page.getByText('Copy rows and time entries').click(),
|
|
]);
|
|
|
|
// Should now show 1 row with time entries
|
|
const rows = page.locator('[data-testid="timesheet_row"]');
|
|
await expect(rows).toHaveCount(1);
|
|
|
|
// Row total should not be 0 (entries were copied)
|
|
const rowTotal = page.locator('[data-testid="timesheet_row_total"]').first();
|
|
await expect(rowTotal).not.toContainText('0 h');
|
|
});
|
|
|
|
// ──────────────────────────────────────────────────
|
|
// Row Removal
|
|
// ──────────────────────────────────────────────────
|
|
|
|
test('can remove an empty project row without confirmation', async ({ page, ctx }) => {
|
|
const project = await createProjectViaApi(ctx, { name: 'Empty Remove Project' });
|
|
|
|
await Promise.all([goToTimesheet(page), waitForTimesheetLoad(page)]);
|
|
|
|
await chooseRowIdentity(page, project.name);
|
|
|
|
const rows = page.locator('[data-testid="timesheet_row"]');
|
|
await expect(rows).toHaveCount(1);
|
|
|
|
// Hover the row to reveal the X button, then click it
|
|
await rows.first().hover();
|
|
await rows.first().getByRole('button', { name: 'Remove row' }).click();
|
|
|
|
// Row should be removed immediately (no dialog)
|
|
await expect(rows).toHaveCount(0);
|
|
});
|
|
|
|
test('removing a row with entries shows confirmation dialog and deletes entries', async ({
|
|
page,
|
|
ctx,
|
|
}) => {
|
|
const monday = getCurrentWeekMonday();
|
|
const project = await createProjectViaApi(ctx, { name: 'Delete Row Project' });
|
|
|
|
await createTimeEntryOnDateViaApi(ctx, {
|
|
date: monday,
|
|
duration: '2h',
|
|
projectId: project.id,
|
|
});
|
|
|
|
await Promise.all([goToTimesheet(page), waitForTimesheetLoad(page)]);
|
|
|
|
const rows = page.locator('[data-testid="timesheet_row"]');
|
|
await expect(rows).toHaveCount(1);
|
|
|
|
// Hover and click X
|
|
await rows.first().hover();
|
|
await rows.first().getByRole('button', { name: 'Remove row' }).click();
|
|
|
|
// Confirmation dialog should appear
|
|
await expect(page.getByRole('alertdialog')).toBeVisible();
|
|
await expect(page.getByText('Remove timesheet row?')).toBeVisible();
|
|
|
|
// Click Delete
|
|
await Promise.all([
|
|
page.waitForResponse(
|
|
(resp) =>
|
|
resp.url().includes('/time-entries') &&
|
|
resp.request().method() === 'DELETE' &&
|
|
resp.status() === 200
|
|
),
|
|
page
|
|
.getByRole('alertdialog')
|
|
.getByRole('button', { name: /Delete/i })
|
|
.click(),
|
|
]);
|
|
|
|
// Row should be gone
|
|
await expect(rows).toHaveCount(0);
|
|
});
|
|
|
|
// ──────────────────────────────────────────────────
|
|
// Multiple Entries Same Cell
|
|
// ──────────────────────────────────────────────────
|
|
|
|
test('cell correctly sums multiple entries for same project+day', async ({ page, ctx }) => {
|
|
const monday = getCurrentWeekMonday();
|
|
const project = await createProjectViaApi(ctx, { name: 'Sum Test' });
|
|
|
|
// Create 2 entries for the same project on Monday
|
|
await createTimeEntryOnDateViaApi(ctx, {
|
|
date: monday,
|
|
duration: '1h',
|
|
projectId: project.id,
|
|
description: 'Entry 1',
|
|
});
|
|
await createTimeEntryOnDateViaApi(ctx, {
|
|
date: monday,
|
|
duration: '2h',
|
|
projectId: project.id,
|
|
description: 'Entry 2',
|
|
});
|
|
|
|
await Promise.all([goToTimesheet(page), waitForTimesheetLoad(page)]);
|
|
|
|
// Should be 1 row (both entries grouped)
|
|
const rows = page.locator('[data-testid="timesheet_row"]');
|
|
await expect(rows).toHaveCount(1);
|
|
|
|
// The Monday cell should show 3h total
|
|
const cells = rows.first().locator('[data-testid="timesheet_cell"]');
|
|
const mondayInput = cells.first().locator('input');
|
|
// The value should contain "3" (for 3h in some format)
|
|
await expect(mondayInput).toHaveValue(/3/);
|
|
});
|
|
|
|
// ──────────────────────────────────────────────────
|
|
// Duration Input Formats
|
|
// ──────────────────────────────────────────────────
|
|
|
|
test('cell accepts various duration input formats', async ({ page, ctx }) => {
|
|
await createProjectViaApi(ctx, { name: 'Format Test' });
|
|
|
|
await Promise.all([goToTimesheet(page), waitForTimesheetLoad(page)]);
|
|
|
|
await chooseRowIdentity(page, 'Format Test');
|
|
|
|
const row = page.locator('[data-testid="timesheet_row"]').first();
|
|
|
|
// Test entering "1.5" (should be 1h 30min)
|
|
const cells = row.locator('[data-testid="timesheet_cell"]');
|
|
const mondayInput = cells.first().locator('input');
|
|
|
|
await mondayInput.click();
|
|
await mondayInput.fill('1.5');
|
|
|
|
await Promise.all([
|
|
page.waitForResponse(
|
|
(resp) =>
|
|
resp.url().includes('/time-entries') &&
|
|
resp.request().method() === 'POST' &&
|
|
resp.status() === 201
|
|
),
|
|
mondayInput.press('Enter'),
|
|
]);
|
|
|
|
// 1.5 hours = 1h 30min
|
|
await expect(mondayInput).toHaveValue('1h 30min');
|
|
});
|