From 49408033bfa6ff73c0c24aa67020be42b214b1f0 Mon Sep 17 00:00:00 2001 From: Yuriy Zubkov Date: Mon, 6 Apr 2026 03:08:44 +0500 Subject: [PATCH 1/6] Added the ability to disable group similar time entries --- e2e/profile.spec.ts | 29 +++++++++++++++ e2e/time.spec.ts | 36 +++++++++++++++++++ .../js/Pages/Profile/Partials/ThemeForm.vue | 10 +++++- resources/js/Pages/Time.vue | 2 ++ .../ui/src/GroupedItemsCountButton.vue | 6 ++++ .../src/TimeEntry/TimeEntryGroupedTable.vue | 5 +-- resources/js/utils/theme.ts | 2 ++ 7 files changed, 87 insertions(+), 3 deletions(-) diff --git a/e2e/profile.spec.ts b/e2e/profile.spec.ts index 93d9248cf..ef1ec3c71 100644 --- a/e2e/profile.spec.ts +++ b/e2e/profile.spec.ts @@ -230,6 +230,35 @@ test('test that theme can be changed to dark and light', async ({ page }) => { await expect(page.getByText('System default:')).toBeVisible(); }); +// ============================================= +// Group similar time entries +// ============================================= + +test('test that group similar time entries setting can be toggled', async ({ page }) => { + await goToProfilePage(page); + + // Get the checkbox + const checkbox = page.getByLabel('Group similar time entries'); + + // Get initial value and verify it is checked (default is true) + const initialValue = await checkbox.isChecked(); + await expect(checkbox).toBeChecked(); + + // Toggle the checkbox + await checkbox.click(); + + // Reload + await page.reload(); + + // Verify the value is toggled + const afterValue = await page.getByLabel('Group similar time entries').isChecked(); + expect(afterValue).toBe(!initialValue); + + // Verify localStorage persists the setting + const storedValue = await page.evaluate(() => localStorage.getItem('theme-group-similar-time-entries')); + expect(storedValue).toBe(String(!initialValue)); +}); + // ============================================= // Two Factor Authentication Tests // ============================================= diff --git a/e2e/time.spec.ts b/e2e/time.spec.ts index 9c311b033..6982cd5f0 100644 --- a/e2e/time.spec.ts +++ b/e2e/time.spec.ts @@ -39,6 +39,10 @@ function getMonthFromTimestamp(timestamp: string): number { return new Date(timestamp).getUTCMonth() + 1; } +async function goToProfilePage(page: Page) { + await page.goto(PLAYWRIGHT_BASE_URL + '/user/profile'); +} + async function goToTimeOverview(page: Page) { await page.goto(PLAYWRIGHT_BASE_URL + '/time'); } @@ -67,6 +71,14 @@ async function createEmptyTimeEntry(page: Page) { ]); } +async function goToProfileAndSetGrouping(page: Page, enabled: boolean) { + await goToProfilePage(page); + const checkbox = page.getByLabel('Group similar time entries'); + const isChecked = await checkbox.isChecked(); + if (isChecked !== enabled) + await checkbox.click(); +} + test('test that starting and stopping an empty time entry shows a new time entry in the overview', async ({ page, }) => { @@ -333,6 +345,30 @@ test.skip('test that load more works when the end of page is reached', async ({ await expect(page.locator('body')).toHaveText(/All time entries are loaded!/); }); +test('test that Group similar time entries option is affected', async ({ page }) => { + // Enable grouping and go to Time page + await goToProfileAndSetGrouping(page, true); + await goToTimeOverview(page); + + // Create 2 similar time entries + await createEmptyTimeEntry(page); + await page.waitForTimeout(500); + await createEmptyTimeEntry(page); + await page.waitForTimeout(500); + + // Verify similar time entries are grouped + const groupedCount = await page.getByTestId('grouped_items_count_button').count(); + expect(groupedCount).toBeGreaterThan(0); + + // Disable grouping and go to Time page + await goToProfileAndSetGrouping(page, false); + await goToTimeOverview(page); + + // Verify similar time entries are not grouped + const groupedCountDisabled = await page.getByTestId('grouped_items_count_button').count(); + expect(groupedCountDisabled).toBe(0); +}); + // TODO: Test that updating the time entry start / end times works while it is running // TODO: Test for project update diff --git a/resources/js/Pages/Profile/Partials/ThemeForm.vue b/resources/js/Pages/Profile/Partials/ThemeForm.vue index 69de17862..4b691ebe0 100644 --- a/resources/js/Pages/Profile/Partials/ThemeForm.vue +++ b/resources/js/Pages/Profile/Partials/ThemeForm.vue @@ -2,8 +2,9 @@ import FormSection from '@/Components/FormSection.vue'; import { Field, FieldLabel, FieldDescription } from '@/packages/ui/src/field'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/packages/ui/src'; +import { Checkbox } from '@/packages/ui/src'; import { usePreferredColorScheme } from '@vueuse/core'; -import { themeSetting } from '@/utils/theme'; +import { themeSetting, groupSimilarTimeEntriesSettings } from '@/utils/theme'; const preferredColor = usePreferredColorScheme(); @@ -15,6 +16,7 @@ const preferredColor = usePreferredColorScheme(); diff --git a/resources/js/Pages/Time.vue b/resources/js/Pages/Time.vue index 7d7726823..f20c041e1 100644 --- a/resources/js/Pages/Time.vue +++ b/resources/js/Pages/Time.vue @@ -16,6 +16,7 @@ import { useElementVisibility } from '@vueuse/core'; import { ClockIcon } from '@heroicons/vue/20/solid'; import LoadingSpinner from '@/packages/ui/src/LoadingSpinner.vue'; import { useCurrentTimeEntryStore } from '@/utils/useCurrentTimeEntry'; +import { groupSimilarTimeEntriesSettings } from '@/utils/theme'; import { useTasksQuery } from '@/utils/useTasksQuery'; import { useProjectsQuery } from '@/utils/useProjectsQuery'; import TimeEntryGroupedTable from '@/packages/ui/src/TimeEntry/TimeEntryGroupedTable.vue'; @@ -151,6 +152,7 @@ function deleteSelected() { :tasks="tasks" :currency="getOrganizationCurrencyString()" :time-entries="timeEntries" + :group-similar-time-entries="groupSimilarTimeEntriesSettings" :tags="tags">
diff --git a/resources/js/packages/ui/src/GroupedItemsCountButton.vue b/resources/js/packages/ui/src/GroupedItemsCountButton.vue index 23a1d9656..c9a8a1f1e 100644 --- a/resources/js/packages/ui/src/GroupedItemsCountButton.vue +++ b/resources/js/packages/ui/src/GroupedItemsCountButton.vue @@ -6,10 +6,15 @@ const props = withDefaults( defineProps<{ expanded?: boolean; size?: string; + /** + * Test ID used for Playwright/E2E tests. + */ + testId?: string; }>(), { expanded: false, size: 'w-7 h-7', + testId: 'grouped_items_count_button', } ); @@ -23,6 +28,7 @@ const expandedStatusClasses = computed(() => { diff --git a/resources/js/Pages/Time.vue b/resources/js/Pages/Time.vue index f20c041e1..ebc210e93 100644 --- a/resources/js/Pages/Time.vue +++ b/resources/js/Pages/Time.vue @@ -16,7 +16,7 @@ import { useElementVisibility } from '@vueuse/core'; import { ClockIcon } from '@heroicons/vue/20/solid'; import LoadingSpinner from '@/packages/ui/src/LoadingSpinner.vue'; import { useCurrentTimeEntryStore } from '@/utils/useCurrentTimeEntry'; -import { groupSimilarTimeEntriesSettings } from '@/utils/theme'; +import { groupSimilarTimeEntriesSetting } from '@/utils/timeEntryGrouping'; import { useTasksQuery } from '@/utils/useTasksQuery'; import { useProjectsQuery } from '@/utils/useProjectsQuery'; import TimeEntryGroupedTable from '@/packages/ui/src/TimeEntry/TimeEntryGroupedTable.vue'; @@ -152,7 +152,7 @@ function deleteSelected() { :tasks="tasks" :currency="getOrganizationCurrencyString()" :time-entries="timeEntries" - :group-similar-time-entries="groupSimilarTimeEntriesSettings" + :group-similar-time-entries="groupSimilarTimeEntriesSetting" :tags="tags">
diff --git a/resources/js/utils/theme.ts b/resources/js/utils/theme.ts index 19b777c4c..c42fec5e1 100644 --- a/resources/js/utils/theme.ts +++ b/resources/js/utils/theme.ts @@ -24,5 +24,3 @@ function useTheme() { } export { type themeOption, themeSetting, theme, useTheme }; - -export const groupSimilarTimeEntriesSettings = useStorage('theme-group-similar-time-entries', true); diff --git a/resources/js/utils/timeEntryGrouping.ts b/resources/js/utils/timeEntryGrouping.ts new file mode 100644 index 000000000..8197942f9 --- /dev/null +++ b/resources/js/utils/timeEntryGrouping.ts @@ -0,0 +1,3 @@ +import { useStorage } from '@vueuse/core'; + +export const groupSimilarTimeEntriesSetting = useStorage('group-similar-time-entries', true); From 2ac386704be7d08b78a4dc46991497d4118d94a3 Mon Sep 17 00:00:00 2001 From: Yuriy Zubkov Date: Thu, 16 Apr 2026 00:37:19 +0500 Subject: [PATCH 5/6] Replace fixed `waitForTimeout` calls in E2E tests with element-based waits and assertions --- e2e/profile.spec.ts | 2 +- e2e/time.spec.ts | 28 ++++++++++------------------ 2 files changed, 11 insertions(+), 19 deletions(-) diff --git a/e2e/profile.spec.ts b/e2e/profile.spec.ts index ef1ec3c71..3e2a61d1b 100644 --- a/e2e/profile.spec.ts +++ b/e2e/profile.spec.ts @@ -255,7 +255,7 @@ test('test that group similar time entries setting can be toggled', async ({ pag expect(afterValue).toBe(!initialValue); // Verify localStorage persists the setting - const storedValue = await page.evaluate(() => localStorage.getItem('theme-group-similar-time-entries')); + const storedValue = await page.evaluate(() => localStorage.getItem('group-similar-time-entries')); expect(storedValue).toBe(String(!initialValue)); }); diff --git a/e2e/time.spec.ts b/e2e/time.spec.ts index 2ca5303cc..763a542d0 100644 --- a/e2e/time.spec.ts +++ b/e2e/time.spec.ts @@ -71,12 +71,13 @@ async function createEmptyTimeEntry(page: Page) { ]); } -async function goToProfileAndSetGrouping(page: Page, enabled: boolean) { +async function setTimeEntriesGrouping(page: Page, enabled: boolean) { await goToProfilePage(page); const checkbox = page.getByLabel('Group similar time entries'); const isChecked = await checkbox.isChecked(); if (isChecked !== enabled) await checkbox.click(); + await goToTimeOverview(page); } test('test that starting and stopping an empty time entry shows a new time entry in the overview', async ({ @@ -346,32 +347,23 @@ test.skip('test that load more works when the end of page is reached', async ({ }); test('test that Group similar time entries option is affected', async ({ page }) => { - // Enable grouping and go to Time page - await goToProfileAndSetGrouping(page, true); - await goToTimeOverview(page); - await page.waitForTimeout(500); + // Enable grouping + await setTimeEntriesGrouping(page, true); // Create 2 similar time entries await createEmptyTimeEntry(page); - await page.waitForTimeout(500); + await page.waitForSelector('[data-testid="time_entry_row"]', { timeout: 1000 }); await createEmptyTimeEntry(page); - await page.waitForTimeout(500); // Verify similar time entries are grouped - const groupedCount = await page.getByTestId('grouped_items_count_button').count(); - expect(groupedCount).toBeGreaterThan(0); + await expect(page.getByTestId('grouped_items_count_button').first()).toBeVisible({ timeout: 1000 }); - // Disable grouping and go to Time page - await goToProfileAndSetGrouping(page, false); - await goToTimeOverview(page); - await page.waitForTimeout(500); + // Disable grouping + await setTimeEntriesGrouping(page, false); // Verify similar time entries are not grouped - const timeEntryRows = await page.locator('[data-testid="time_entry_row"]').count(); - expect(timeEntryRows).toBe(2); - - const groupedCountDisabled = await page.getByTestId('grouped_items_count_button').count(); - expect(groupedCountDisabled).toBe(0); + await expect(page.locator('[data-testid="time_entry_row"]')).toHaveCount(2, { timeout: 1000 }); + await expect(page.locator('[data-testid="grouped_items_count_button"]')).toHaveCount(0, { timeout: 1000 }); }); // TODO: Test that updating the time entry start / end times works while it is running From ee7f9973d324c9d368a0abb903027d21779aa6df Mon Sep 17 00:00:00 2001 From: Yuriy Zubkov Date: Thu, 16 Apr 2026 00:44:43 +0500 Subject: [PATCH 6/6] Run frontend linting and formatting for changes --- e2e/profile.spec.ts | 4 +++- e2e/time.spec.ts | 11 +++++++---- resources/js/Pages/Profile/Partials/ThemeForm.vue | 4 +++- resources/js/utils/timeEntryGrouping.ts | 5 ++++- 4 files changed, 17 insertions(+), 7 deletions(-) diff --git a/e2e/profile.spec.ts b/e2e/profile.spec.ts index 3e2a61d1b..302312621 100644 --- a/e2e/profile.spec.ts +++ b/e2e/profile.spec.ts @@ -255,7 +255,9 @@ test('test that group similar time entries setting can be toggled', async ({ pag expect(afterValue).toBe(!initialValue); // Verify localStorage persists the setting - const storedValue = await page.evaluate(() => localStorage.getItem('group-similar-time-entries')); + const storedValue = await page.evaluate(() => + localStorage.getItem('group-similar-time-entries') + ); expect(storedValue).toBe(String(!initialValue)); }); diff --git a/e2e/time.spec.ts b/e2e/time.spec.ts index 763a542d0..dfd4ba4ec 100644 --- a/e2e/time.spec.ts +++ b/e2e/time.spec.ts @@ -75,8 +75,7 @@ async function setTimeEntriesGrouping(page: Page, enabled: boolean) { await goToProfilePage(page); const checkbox = page.getByLabel('Group similar time entries'); const isChecked = await checkbox.isChecked(); - if (isChecked !== enabled) - await checkbox.click(); + if (isChecked !== enabled) await checkbox.click(); await goToTimeOverview(page); } @@ -356,14 +355,18 @@ test('test that Group similar time entries option is affected', async ({ page }) await createEmptyTimeEntry(page); // Verify similar time entries are grouped - await expect(page.getByTestId('grouped_items_count_button').first()).toBeVisible({ timeout: 1000 }); + await expect(page.getByTestId('grouped_items_count_button').first()).toBeVisible({ + timeout: 1000, + }); // Disable grouping await setTimeEntriesGrouping(page, false); // Verify similar time entries are not grouped await expect(page.locator('[data-testid="time_entry_row"]')).toHaveCount(2, { timeout: 1000 }); - await expect(page.locator('[data-testid="grouped_items_count_button"]')).toHaveCount(0, { timeout: 1000 }); + await expect(page.locator('[data-testid="grouped_items_count_button"]')).toHaveCount(0, { + timeout: 1000, + }); }); // TODO: Test that updating the time entry start / end times works while it is running diff --git a/resources/js/Pages/Profile/Partials/ThemeForm.vue b/resources/js/Pages/Profile/Partials/ThemeForm.vue index 8009447a3..022a1d707 100644 --- a/resources/js/Pages/Profile/Partials/ThemeForm.vue +++ b/resources/js/Pages/Profile/Partials/ThemeForm.vue @@ -37,7 +37,9 @@ const preferredColor = usePreferredColorScheme(); - + Group similar time entries diff --git a/resources/js/utils/timeEntryGrouping.ts b/resources/js/utils/timeEntryGrouping.ts index 8197942f9..34957d383 100644 --- a/resources/js/utils/timeEntryGrouping.ts +++ b/resources/js/utils/timeEntryGrouping.ts @@ -1,3 +1,6 @@ import { useStorage } from '@vueuse/core'; -export const groupSimilarTimeEntriesSetting = useStorage('group-similar-time-entries', true); +export const groupSimilarTimeEntriesSetting = useStorage( + 'group-similar-time-entries', + true +);