cadfa70e60
* #708 apply strictier linting rules and fix simple eslint bugs * #708 fix eslint errors relate to promise * #708 fix eslint import/no-extraneous-dependencies * #708 fix eslint errors of react-hook * #708 enable eslint check for typescript --------- Co-authored-by: lethemanh <lethemanh@lethemanhs-MacBook-Pro.local>
481 lines
14 KiB
TypeScript
481 lines
14 KiB
TypeScript
import SettingsPage from '@/features/Settings/SettingsPage'
|
|
import settingsReducer, {
|
|
setIsBrowserDefaultTimeZone,
|
|
setTimeZone
|
|
} from '@/features/Settings/SettingsSlice'
|
|
import userReducer, {
|
|
getOpenPaasUserDataAsync,
|
|
setTimezone as setUserTimeZone
|
|
} from '@/features/User/userSlice'
|
|
import { api } from '@/utils/apiUtils'
|
|
import { browserDefaultTimeZone } from '@/utils/timezone'
|
|
import { configureStore } from '@reduxjs/toolkit'
|
|
import { fireEvent, screen, waitFor } from '@testing-library/react'
|
|
import { renderWithProviders } from '../../utils/Renderwithproviders'
|
|
|
|
jest.mock('@/utils/apiUtils')
|
|
|
|
describe('Timezone synchronization after getOpenPaasUserDataAsync', () => {
|
|
let apiGetSpy: jest.SpyInstance
|
|
|
|
beforeEach(() => {
|
|
jest.clearAllMocks()
|
|
localStorage.clear()
|
|
apiGetSpy = jest.spyOn(api, 'get')
|
|
})
|
|
|
|
afterEach(() => {
|
|
apiGetSpy.mockRestore()
|
|
})
|
|
|
|
it('should sync timezone to both user and settings state after fetching user data', async () => {
|
|
const mockUserData = {
|
|
id: '667037022b752d0026472254',
|
|
firstname: 'John',
|
|
lastname: 'Doe',
|
|
preferredEmail: ['test@test.com'],
|
|
configurations: {
|
|
modules: [
|
|
{
|
|
name: 'core',
|
|
configurations: [
|
|
{ name: 'language', value: 'fr' },
|
|
{ name: 'datetime', value: { timeZone: 'Europe/Paris' } }
|
|
]
|
|
}
|
|
]
|
|
}
|
|
}
|
|
|
|
// Return a fake fetch-like response
|
|
apiGetSpy.mockResolvedValue({
|
|
json: async () => mockUserData
|
|
})
|
|
|
|
const store = configureStore({
|
|
reducer: { user: userReducer, settings: settingsReducer },
|
|
preloadedState: {
|
|
user: {
|
|
userData: {
|
|
sub: 'test',
|
|
email: 'test@test.com',
|
|
family_name: 'Doe',
|
|
name: 'John',
|
|
sid: 'mockSid',
|
|
openpaasId: '667037022b752d0026472254'
|
|
},
|
|
organiserData: null,
|
|
tokens: null,
|
|
coreConfig: { language: 'en', datetime: { timeZone: 'UTC' } },
|
|
loading: false,
|
|
error: null
|
|
},
|
|
settings: { language: 'en', timeZone: 'UTC', view: 'calendar' }
|
|
}
|
|
})
|
|
|
|
const result = await store.dispatch(getOpenPaasUserDataAsync())
|
|
|
|
expect(result.type).toBe('user/getOpenPaasUserData/fulfilled')
|
|
|
|
const state = store.getState()
|
|
expect(state.user.coreConfig.datetime.timeZone).toBe('Europe/Paris')
|
|
expect(state.settings.timeZone).toBe('Europe/Paris')
|
|
expect(localStorage.getItem('timeZone')).toBe('Europe/Paris')
|
|
})
|
|
|
|
it('should keep browser timezone if API response has no timezone configuration', async () => {
|
|
const mockUserData = {
|
|
id: '667037022b752d0026472254',
|
|
firstname: 'John',
|
|
lastname: 'Doe',
|
|
preferredEmail: '[test@test.com](mailto:test@test.com)',
|
|
configurations: {
|
|
modules: [
|
|
{ name: 'core', configurations: [{ name: 'language', value: 'en' }] }
|
|
]
|
|
}
|
|
}
|
|
|
|
apiGetSpy.mockResolvedValue({
|
|
json: async () => mockUserData
|
|
})
|
|
|
|
const browserTimezone = browserDefaultTimeZone ?? 'UTC'
|
|
|
|
const store = configureStore({
|
|
reducer: { user: userReducer, settings: settingsReducer },
|
|
preloadedState: {
|
|
user: {
|
|
userData: {
|
|
sub: 'test',
|
|
email: 'test@test.com',
|
|
family_name: 'Doe',
|
|
name: 'John',
|
|
sid: 'mockSid',
|
|
openpaasId: '667037022b752d0026472254'
|
|
},
|
|
organiserData: null,
|
|
tokens: null,
|
|
coreConfig: {
|
|
language: 'en',
|
|
datetime: { timeZone: browserTimezone }
|
|
},
|
|
loading: false,
|
|
error: null
|
|
},
|
|
settings: {
|
|
language: 'en',
|
|
timeZone: browserTimezone,
|
|
view: 'calendar'
|
|
}
|
|
}
|
|
})
|
|
|
|
const result = await store.dispatch(getOpenPaasUserDataAsync())
|
|
|
|
expect(result.type).toBe('user/getOpenPaasUserData/fulfilled')
|
|
|
|
const state = store.getState()
|
|
expect(state.user.coreConfig.datetime.timeZone).toBe(null)
|
|
expect(state.settings.timeZone).toBe(browserTimezone)
|
|
})
|
|
})
|
|
|
|
// Mock localStorage
|
|
const localStorageMock = (() => {
|
|
let store: Record<string, string> = {}
|
|
return {
|
|
getItem: (key: string) => store[key] || null,
|
|
setItem: (key: string, value: string) => {
|
|
store[key] = value
|
|
},
|
|
removeItem: (key: string) => {
|
|
delete store[key]
|
|
},
|
|
clear: () => {
|
|
store = {}
|
|
}
|
|
}
|
|
})()
|
|
|
|
Object.defineProperty(window, 'localStorage', {
|
|
value: localStorageMock
|
|
})
|
|
|
|
describe('Timezone Logic - Backend to Frontend Flow', () => {
|
|
let store: any
|
|
|
|
beforeEach(() => {
|
|
localStorageMock.clear()
|
|
store = configureStore({
|
|
reducer: {
|
|
settings: settingsReducer,
|
|
user: userReducer
|
|
},
|
|
preloadedState: {
|
|
user: {
|
|
userData: {
|
|
sub: 'test',
|
|
email: 'test@test.com',
|
|
family_name: 'Doe',
|
|
name: 'John',
|
|
sid: 'mockSid',
|
|
openpaasId: '667037022b752d0026472254'
|
|
},
|
|
organiserData: null,
|
|
tokens: null,
|
|
coreConfig: { language: 'en', datetime: { timeZone: 'UTC' } },
|
|
loading: false,
|
|
error: null
|
|
},
|
|
settings: { language: 'en', timeZone: 'UTC', view: 'calendar' }
|
|
}
|
|
})
|
|
})
|
|
|
|
describe('Backend Response Handling', () => {
|
|
test('Backend returns NULL => Should use browser default timezone', async () => {
|
|
const backendResponse = {
|
|
firstname: 'John',
|
|
lastname: 'Doe',
|
|
id: '123',
|
|
preferredEmail: 'john@example.com',
|
|
configurations: {
|
|
modules: [
|
|
{
|
|
name: 'core',
|
|
configurations: [
|
|
{
|
|
name: 'datetime',
|
|
value: {
|
|
timeZone: null
|
|
}
|
|
}
|
|
]
|
|
}
|
|
]
|
|
}
|
|
}
|
|
|
|
await store.dispatch(
|
|
getOpenPaasUserDataAsync.fulfilled(backendResponse, '', undefined)
|
|
)
|
|
|
|
const settingsState = store.getState().settings
|
|
const userState = store.getState().user
|
|
|
|
expect(settingsState.timeZone).toBe(browserDefaultTimeZone)
|
|
expect(settingsState.isBrowserDefaultTimeZone).toBe(true)
|
|
expect(userState.coreConfig.datetime.timeZone).toBe(null)
|
|
expect(localStorage.getItem('timeZone')).toBe(browserDefaultTimeZone)
|
|
})
|
|
|
|
test('Backend returns SPECIFIC VALUE => Should use that value', async () => {
|
|
const specificTimezone = 'America/New_York'
|
|
const backendResponse = {
|
|
firstname: 'Jane',
|
|
lastname: 'Smith',
|
|
id: '456',
|
|
preferredEmail: 'jane@example.com',
|
|
configurations: {
|
|
modules: [
|
|
{
|
|
name: 'core',
|
|
configurations: [
|
|
{
|
|
name: 'datetime',
|
|
value: {
|
|
timeZone: specificTimezone
|
|
}
|
|
}
|
|
]
|
|
}
|
|
]
|
|
}
|
|
}
|
|
|
|
await store.dispatch(
|
|
getOpenPaasUserDataAsync.fulfilled(backendResponse, '', undefined)
|
|
)
|
|
|
|
const settingsState = store.getState().settings
|
|
const userState = store.getState().user
|
|
|
|
expect(settingsState.timeZone).toBe(specificTimezone)
|
|
expect(settingsState.isBrowserDefaultTimeZone).toBe(false)
|
|
expect(userState.coreConfig.datetime.timeZone).toBe(specificTimezone)
|
|
expect(localStorage.getItem('timeZone')).toBe(specificTimezone)
|
|
})
|
|
|
|
test('Backend returns NO datetime config => Should use browser default', async () => {
|
|
const backendResponse = {
|
|
firstname: 'Bob',
|
|
lastname: 'Johnson',
|
|
id: '789',
|
|
preferredEmail: 'bob@example.com',
|
|
configurations: {
|
|
modules: [
|
|
{
|
|
name: 'core',
|
|
configurations: [
|
|
// No datetime config
|
|
]
|
|
}
|
|
]
|
|
}
|
|
}
|
|
|
|
await store.dispatch(
|
|
getOpenPaasUserDataAsync.fulfilled(backendResponse, '', undefined)
|
|
)
|
|
|
|
const settingsState = store.getState().settings
|
|
const userState = store.getState().user
|
|
|
|
expect(settingsState.timeZone).toBe(browserDefaultTimeZone)
|
|
expect(settingsState.isBrowserDefaultTimeZone).toBe(true)
|
|
expect(userState.coreConfig.datetime.timeZone).toBe(null)
|
|
expect(localStorage.getItem('timeZone')).toBe(browserDefaultTimeZone)
|
|
})
|
|
})
|
|
|
|
test('Settings state ALWAYS has a concrete value (never null)', () => {
|
|
// Test 1: Browser default scenario
|
|
store.dispatch(setIsBrowserDefaultTimeZone(true))
|
|
let settingsState = store.getState().settings
|
|
|
|
expect(settingsState.timeZone).toBe(browserDefaultTimeZone)
|
|
expect(settingsState.timeZone).not.toBe(null)
|
|
|
|
// Test 2: Specific timezone scenario
|
|
store.dispatch(setTimeZone('Europe/Paris'))
|
|
settingsState = store.getState().settings
|
|
|
|
expect(settingsState.timeZone).toBe('Europe/Paris')
|
|
expect(settingsState.timeZone).not.toBe(null)
|
|
})
|
|
|
|
describe('User Actions - Changing Timezone', () => {
|
|
beforeEach(() => {
|
|
;(api.patch as jest.Mock).mockResolvedValue({ status: 204 })
|
|
})
|
|
test('User enables browser default => Settings gets browser TZ, User gets null', async () => {
|
|
const { store } = renderWithProviders(<SettingsPage />, {
|
|
user: {
|
|
userData: { sub: 'test' },
|
|
organiserData: null,
|
|
tokens: null,
|
|
coreConfig: {
|
|
language: 'en',
|
|
datetime: { timeZone: 'America/Los_Angeles' }
|
|
},
|
|
loading: false,
|
|
error: null
|
|
},
|
|
settings: {
|
|
language: 'en',
|
|
timeZone: 'America/Los_Angeles',
|
|
isBrowserDefaultTimeZone: false,
|
|
view: 'settings'
|
|
}
|
|
})
|
|
|
|
const browserDefaultSwitch = screen.getAllByLabelText(
|
|
'settings.timeZoneBrowserDefault'
|
|
)[0]
|
|
expect(browserDefaultSwitch).toBeInTheDocument()
|
|
|
|
// Enable browser default
|
|
fireEvent.click(browserDefaultSwitch)
|
|
|
|
await waitFor(() => {
|
|
const state = store.getState()
|
|
expect(state.settings.isBrowserDefaultTimeZone).toBe(true)
|
|
})
|
|
await waitFor(() => {
|
|
const state = store.getState()
|
|
expect(state.settings.timeZone).toBe(browserDefaultTimeZone)
|
|
})
|
|
await waitFor(() => {
|
|
const state = store.getState()
|
|
expect(state.user.coreConfig.datetime.timeZone).toBe(null)
|
|
})
|
|
})
|
|
|
|
test('User selects specific timezone => Both states get the value', async () => {
|
|
const { store } = renderWithProviders(<SettingsPage />, {
|
|
user: {
|
|
userData: { sub: 'test' },
|
|
organiserData: null,
|
|
tokens: null,
|
|
coreConfig: { language: 'en', datetime: { timeZone: null } },
|
|
loading: false,
|
|
error: null
|
|
},
|
|
settings: {
|
|
language: 'en',
|
|
timeZone: browserDefaultTimeZone,
|
|
isBrowserDefaultTimeZone: true,
|
|
view: 'settings'
|
|
}
|
|
})
|
|
|
|
const browserDefaultSwitch = screen.getAllByLabelText(
|
|
'settings.timeZoneBrowserDefault'
|
|
)[0]
|
|
|
|
// Disable browser default so manual selector appears
|
|
fireEvent.click(browserDefaultSwitch)
|
|
|
|
// Now timezone combobox is visible
|
|
const timezoneInput = screen.getAllByRole('combobox')[1]
|
|
|
|
// Type to filter options
|
|
fireEvent.change(timezoneInput, {
|
|
target: { value: 'Australia/Sydney' }
|
|
})
|
|
|
|
// Select from autocomplete dropdown
|
|
const option = await screen.findByText(/Australia\/Sydney/i)
|
|
fireEvent.click(option)
|
|
|
|
await waitFor(() => {
|
|
const state = store.getState()
|
|
expect(state.settings.isBrowserDefaultTimeZone).toBe(false)
|
|
})
|
|
await waitFor(() => {
|
|
const state = store.getState()
|
|
expect(state.settings.timeZone).toBe('Australia/Sydney')
|
|
})
|
|
await waitFor(() => {
|
|
const state = store.getState()
|
|
expect(state.user.coreConfig.datetime.timeZone).toBe('Australia/Sydney')
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('LocalStorage Persistence', () => {
|
|
test('LocalStorage always stores concrete values (browser TZ or specific TZ)', () => {
|
|
// Scenario 1: Browser default
|
|
store.dispatch(setTimeZone(browserDefaultTimeZone))
|
|
expect(localStorage.getItem('timeZone')).toBe(browserDefaultTimeZone)
|
|
expect(localStorage.getItem('timeZone')).not.toBe('null')
|
|
|
|
// Scenario 2: Specific timezone
|
|
store.dispatch(setTimeZone('Africa/Cairo'))
|
|
expect(localStorage.getItem('timeZone')).toBe('Africa/Cairo')
|
|
})
|
|
})
|
|
|
|
test('Full user journey: Backend null -> User changes -> Backend saves', async () => {
|
|
// Step 1: Backend returns null
|
|
const backendResponseNull = {
|
|
firstname: 'Test',
|
|
lastname: 'User',
|
|
id: '999',
|
|
preferredEmail: 'test@example.com',
|
|
configurations: {
|
|
modules: [
|
|
{
|
|
name: 'core',
|
|
configurations: [
|
|
{
|
|
name: 'datetime',
|
|
value: { timeZone: null }
|
|
}
|
|
]
|
|
}
|
|
]
|
|
}
|
|
}
|
|
|
|
await store.dispatch(
|
|
getOpenPaasUserDataAsync.fulfilled(backendResponseNull, '', undefined)
|
|
)
|
|
|
|
// Verify initial state
|
|
expect(store.getState().settings.timeZone).toBe(browserDefaultTimeZone)
|
|
expect(store.getState().user.coreConfig.datetime.timeZone).toBe(null)
|
|
|
|
// Step 2: User changes to specific timezone
|
|
const userSelectedTZ = 'Europe/London'
|
|
store.dispatch(setIsBrowserDefaultTimeZone(false))
|
|
store.dispatch(setTimeZone(userSelectedTZ))
|
|
store.dispatch(setUserTimeZone(userSelectedTZ))
|
|
|
|
expect(store.getState().settings.timeZone).toBe(userSelectedTZ)
|
|
expect(store.getState().user.coreConfig.datetime.timeZone).toBe(
|
|
userSelectedTZ
|
|
)
|
|
|
|
// Step 3: User switches back to browser default
|
|
store.dispatch(setIsBrowserDefaultTimeZone(true))
|
|
store.dispatch(setUserTimeZone(null))
|
|
store.dispatch(setTimeZone(browserDefaultTimeZone))
|
|
|
|
expect(store.getState().settings.timeZone).toBe(browserDefaultTimeZone)
|
|
expect(store.getState().user.coreConfig.datetime.timeZone).toBe(null)
|
|
})
|
|
})
|