Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions e2e/tests/timezone.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { test, expect } from '../fixtures/electron-test'

test.describe('Account timezone application', () => {
test('renders a UTC time entry in the account timezone, not the fallback', async ({
page,
mockState,
}) => {
test.setTimeout(60_000)

// UTC+5:30, no DST — unambiguously different from CI/device zones and from
// the pre-fix Europe/Vienna fallback, and it keeps every mock entry on its
// original calendar day (so day-grouping is unaffected).
const ACCOUNT_TZ = 'Asia/Kolkata'
const ACCOUNT_TIME = /14:30\s*-\s*17:00/ // 09:00Z–11:30Z in Asia/Kolkata
const FALLBACK_TIME = /10:00\s*-\s*12:30/ // the same entry in Europe/Vienna (pre-fix)

mockState.user.timezone = ACCOUNT_TZ

// Hold GET /users/me open. Registered after the catch-all, so it wins for
// this exact path (Playwright runs handlers last-registered-first). The
// /users/me/memberships and /time-entries/active paths still hit the
// catch-all, so the entry list can load while the timezone is pending.
let releaseMe: () => void = () => {}
const meGate = new Promise<void>((resolve) => {
releaseMe = resolve
})
await page.route(/\/users\/me(\?.*)?$/, async (route) => {
await meGate
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ data: mockState.user }),
})
})

// Re-bootstrap cold: the vue-query cache is cleared by the reload while the
// seeded auth token persists in localStorage (so we are "logged in").
await page.reload({ waitUntil: 'domcontentloaded' })

// Give the pre-fix build time to mount the time page and render the entry
// with its fallback timezone (and freeze it under <keep-alive>). The fixed
// build renders nothing here — it withholds the UI until the tz is known.
await page.waitForTimeout(3_000)

// Deliver the account timezone.
releaseMe()

// The entry is shown — and it must be in the account timezone, not the fallback.
await expect(page.getByText('Implement navigation component').first()).toBeVisible({
timeout: 10_000,
})
await expect(page.getByText(ACCOUNT_TIME).first()).toBeVisible({ timeout: 10_000 })
await expect(page.getByText(FALLBACK_TIME)).toHaveCount(0)

// ...and the timezone actually in effect is the account one.
const tzInEffect = await page.evaluate(() =>
(window as Window & { getTimezoneSetting: () => string }).getTimezoneSetting()
)
expect(tzInEffect).toBe(ACCOUNT_TZ)
})
})
22 changes: 12 additions & 10 deletions src/renderer/src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -66,14 +66,16 @@ watchEffect(() => {
const { data: meResponse } = useQuery({
queryKey: ['me'],
queryFn: () => getMe(),
enabled: isLoggedIn,
})

watch(meResponse, () => {
if (meResponse.value?.data) {
window.getTimezoneSetting = () => meResponse.value.data.timezone
window.getWeekStartSetting = () => meResponse.value.data.week_start
}
})
const deviceTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC'
window.getTimezoneSetting = () => meResponse.value?.data?.timezone || deviceTimezone
window.getWeekStartSetting = () => meResponse.value?.data?.week_start || 'monday'

// Time-formatting UI must not mount before the real timezone is loaded, or
// <keep-alive> will cache it with the fallback for the whole session.
const isMeLoaded = computed(() => !!meResponse.value?.data)

// Watch timer state and notify main process for idle detection
watch(isActive, (active) => {
Expand All @@ -83,9 +85,6 @@ watch(isActive, (active) => {
})

onMounted(async () => {
window.getTimezoneSetting = () => 'Europe/Vienna'
window.getWeekStartSetting = () => 'monday'

initializeAuth(queryClient)
useTheme()

Expand Down Expand Up @@ -167,7 +166,7 @@ whenever(cmdComma, () => {
</div>
<div v-if="!isMac" class="flex-1 h-full" style="-webkit-app-region: drag"></div>
</div>
<div v-if="isLoggedIn" class="flex-1 flex flex-col overflow-hidden">
<div v-if="isLoggedIn && isMeLoaded" class="flex-1 flex flex-col overflow-hidden">
<div class="flex-1 flex overflow-hidden">
<SidebarNavigation />
<router-view v-slot="{ Component }">
Expand All @@ -192,6 +191,9 @@ whenever(cmdComma, () => {
</div>
</div>
</div>
<div v-else-if="isLoggedIn" class="flex-1 flex items-center justify-center">
<div class="text-text-tertiary font-medium text-sm">Loading…</div>
</div>
<div v-else class="flex-1">
<div class="flex flex-col space-y-6 py-12 items-center justify-center">
<svg
Expand Down
Loading