#708 apply strictier linting rules (#717)

* #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>
This commit is contained in:
lethemanh
2026-04-01 22:15:10 +07:00
committed by GitHub
parent 2bff6aae78
commit cadfa70e60
321 changed files with 27452 additions and 27600 deletions
@@ -1,482 +1,480 @@
import SettingsPage from "@/features/Settings/SettingsPage";
import SettingsPage from '@/features/Settings/SettingsPage'
import settingsReducer, {
setIsBrowserDefaultTimeZone,
setTimeZone,
} from "@/features/Settings/SettingsSlice";
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";
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");
jest.mock('@/utils/apiUtils')
describe("Timezone synchronization after getOpenPaasUserDataAsync", () => {
let apiGetSpy: jest.SpyInstance;
describe('Timezone synchronization after getOpenPaasUserDataAsync', () => {
let apiGetSpy: jest.SpyInstance
beforeEach(() => {
jest.clearAllMocks();
localStorage.clear();
apiGetSpy = jest.spyOn(api, "get");
});
jest.clearAllMocks()
localStorage.clear()
apiGetSpy = jest.spyOn(api, 'get')
})
afterEach(() => {
apiGetSpy.mockRestore();
});
apiGetSpy.mockRestore()
})
it("should sync timezone to both user and settings state after fetching user data", async () => {
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"],
id: '667037022b752d0026472254',
firstname: 'John',
lastname: 'Doe',
preferredEmail: ['test@test.com'],
configurations: {
modules: [
{
name: "core",
name: 'core',
configurations: [
{ name: "language", value: "fr" },
{ name: "datetime", value: { timeZone: "Europe/Paris" } },
],
},
],
},
};
{ name: 'language', value: 'fr' },
{ name: 'datetime', value: { timeZone: 'Europe/Paris' } }
]
}
]
}
}
// Return a fake fetch-like response
apiGetSpy.mockResolvedValue({
json: async () => mockUserData,
});
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",
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" } },
coreConfig: { language: 'en', datetime: { timeZone: 'UTC' } },
loading: false,
error: null,
error: null
},
settings: { language: "en", timeZone: "UTC", view: "calendar" },
},
});
settings: { language: 'en', timeZone: 'UTC', view: 'calendar' }
}
})
const result = await store.dispatch(getOpenPaasUserDataAsync());
const result = await store.dispatch(getOpenPaasUserDataAsync())
expect(result.type).toBe("user/getOpenPaasUserData/fulfilled");
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");
});
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 () => {
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)",
id: '667037022b752d0026472254',
firstname: 'John',
lastname: 'Doe',
preferredEmail: '[test@test.com](mailto:test@test.com)',
configurations: {
modules: [
{ name: "core", configurations: [{ name: "language", value: "en" }] },
],
},
};
{ name: 'core', configurations: [{ name: 'language', value: 'en' }] }
]
}
}
apiGetSpy.mockResolvedValue({
json: async () => mockUserData,
});
json: async () => mockUserData
})
const browserTimezone = browserDefaultTimeZone ?? "UTC";
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",
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 },
language: 'en',
datetime: { timeZone: browserTimezone }
},
loading: false,
error: null,
error: null
},
settings: {
language: "en",
language: 'en',
timeZone: browserTimezone,
view: "calendar",
},
},
});
view: 'calendar'
}
}
})
const result = await store.dispatch(getOpenPaasUserDataAsync());
const result = await store.dispatch(getOpenPaasUserDataAsync())
expect(result.type).toBe("user/getOpenPaasUserData/fulfilled");
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);
});
});
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> = {};
let store: Record<string, string> = {}
return {
getItem: (key: string) => store[key] || null,
setItem: (key: string, value: string) => {
store[key] = value;
store[key] = value
},
removeItem: (key: string) => {
delete store[key];
delete store[key]
},
clear: () => {
store = {};
},
};
})();
store = {}
}
}
})()
Object.defineProperty(window, "localStorage", {
value: localStorageMock,
});
Object.defineProperty(window, 'localStorage', {
value: localStorageMock
})
describe("Timezone Logic - Backend to Frontend Flow", () => {
let store: any;
describe('Timezone Logic - Backend to Frontend Flow', () => {
let store: any
beforeEach(() => {
localStorageMock.clear();
localStorageMock.clear()
store = configureStore({
reducer: {
settings: settingsReducer,
user: userReducer,
user: userReducer
},
preloadedState: {
user: {
userData: {
sub: "test",
email: "test@test.com",
family_name: "Doe",
name: "John",
sid: "mockSid",
openpaasId: "667037022b752d0026472254",
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" } },
coreConfig: { language: 'en', datetime: { timeZone: 'UTC' } },
loading: false,
error: null,
error: null
},
settings: { language: "en", timeZone: "UTC", view: "calendar" },
},
});
});
settings: { language: 'en', timeZone: 'UTC', view: 'calendar' }
}
})
})
describe("Backend Response Handling", () => {
test("Backend returns NULL => Should use browser default timezone", async () => {
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",
firstname: 'John',
lastname: 'Doe',
id: '123',
preferredEmail: 'john@example.com',
configurations: {
modules: [
{
name: "core",
name: 'core',
configurations: [
{
name: "datetime",
name: 'datetime',
value: {
timeZone: null,
},
},
],
},
],
},
};
timeZone: null
}
}
]
}
]
}
}
await store.dispatch(
getOpenPaasUserDataAsync.fulfilled(backendResponse, "", undefined)
);
getOpenPaasUserDataAsync.fulfilled(backendResponse, '', undefined)
)
const settingsState = store.getState().settings;
const userState = store.getState().user;
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);
});
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";
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",
firstname: 'Jane',
lastname: 'Smith',
id: '456',
preferredEmail: 'jane@example.com',
configurations: {
modules: [
{
name: "core",
name: 'core',
configurations: [
{
name: "datetime",
name: 'datetime',
value: {
timeZone: specificTimezone,
},
},
],
},
],
},
};
timeZone: specificTimezone
}
}
]
}
]
}
}
await store.dispatch(
getOpenPaasUserDataAsync.fulfilled(backendResponse, "", undefined)
);
getOpenPaasUserDataAsync.fulfilled(backendResponse, '', undefined)
)
const settingsState = store.getState().settings;
const userState = store.getState().user;
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);
});
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 () => {
test('Backend returns NO datetime config => Should use browser default', async () => {
const backendResponse = {
firstname: "Bob",
lastname: "Johnson",
id: "789",
preferredEmail: "bob@example.com",
firstname: 'Bob',
lastname: 'Johnson',
id: '789',
preferredEmail: 'bob@example.com',
configurations: {
modules: [
{
name: "core",
name: 'core',
configurations: [
// No datetime config
],
},
],
},
};
]
}
]
}
}
await store.dispatch(
getOpenPaasUserDataAsync.fulfilled(backendResponse, "", undefined)
);
getOpenPaasUserDataAsync.fulfilled(backendResponse, '', undefined)
)
const settingsState = store.getState().settings;
const userState = store.getState().user;
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);
});
});
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('Settings state ALWAYS has a concrete value (never null)', () => {
// Test 1: Browser default scenario
store.dispatch(setIsBrowserDefaultTimeZone(true));
let settingsState = store.getState().settings;
store.dispatch(setIsBrowserDefaultTimeZone(true))
let settingsState = store.getState().settings
expect(settingsState.timeZone).toBe(browserDefaultTimeZone);
expect(settingsState.timeZone).not.toBe(null);
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;
store.dispatch(setTimeZone('Europe/Paris'))
settingsState = store.getState().settings
expect(settingsState.timeZone).toBe("Europe/Paris");
expect(settingsState.timeZone).not.toBe(null);
});
expect(settingsState.timeZone).toBe('Europe/Paris')
expect(settingsState.timeZone).not.toBe(null)
})
describe("User Actions - Changing Timezone", () => {
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 () => {
;(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" },
userData: { sub: 'test' },
organiserData: null,
tokens: null,
coreConfig: {
language: "en",
datetime: { timeZone: "America/Los_Angeles" },
language: 'en',
datetime: { timeZone: 'America/Los_Angeles' }
},
loading: false,
error: null,
error: null
},
settings: {
language: "en",
timeZone: "America/Los_Angeles",
language: 'en',
timeZone: 'America/Los_Angeles',
isBrowserDefaultTimeZone: false,
view: "settings",
},
});
view: 'settings'
}
})
const browserDefaultSwitch = screen.getAllByLabelText(
"settings.timeZoneBrowserDefault"
)[0];
expect(browserDefaultSwitch).toBeInTheDocument();
'settings.timeZoneBrowserDefault'
)[0]
expect(browserDefaultSwitch).toBeInTheDocument()
// Enable browser default
fireEvent.click(browserDefaultSwitch);
fireEvent.click(browserDefaultSwitch)
await waitFor(() => {
const state = store.getState();
expect(state.settings.isBrowserDefaultTimeZone).toBe(true);
});
const state = store.getState()
expect(state.settings.isBrowserDefaultTimeZone).toBe(true)
})
await waitFor(() => {
const state = store.getState();
expect(state.settings.timeZone).toBe(browserDefaultTimeZone);
});
const state = store.getState()
expect(state.settings.timeZone).toBe(browserDefaultTimeZone)
})
await waitFor(() => {
const state = store.getState();
expect(state.user.coreConfig.datetime.timeZone).toBe(null);
});
});
const state = store.getState()
expect(state.user.coreConfig.datetime.timeZone).toBe(null)
})
})
test("User selects specific timezone => Both states get the value", async () => {
test('User selects specific timezone => Both states get the value', async () => {
const { store } = renderWithProviders(<SettingsPage />, {
user: {
userData: { sub: "test" },
userData: { sub: 'test' },
organiserData: null,
tokens: null,
coreConfig: { language: "en", datetime: { timeZone: null } },
coreConfig: { language: 'en', datetime: { timeZone: null } },
loading: false,
error: null,
error: null
},
settings: {
language: "en",
language: 'en',
timeZone: browserDefaultTimeZone,
isBrowserDefaultTimeZone: true,
view: "settings",
},
});
view: 'settings'
}
})
const browserDefaultSwitch = screen.getAllByLabelText(
"settings.timeZoneBrowserDefault"
)[0];
'settings.timeZoneBrowserDefault'
)[0]
// Disable browser default so manual selector appears
fireEvent.click(browserDefaultSwitch);
fireEvent.click(browserDefaultSwitch)
// Now timezone combobox is visible
const timezoneInput = screen.getAllByRole("combobox")[1];
const timezoneInput = screen.getAllByRole('combobox')[1]
// Type to filter options
fireEvent.change(timezoneInput, {
target: { value: "Australia/Sydney" },
});
target: { value: 'Australia/Sydney' }
})
// Select from autocomplete dropdown
const option = await screen.findByText(/Australia\/Sydney/i);
fireEvent.click(option);
const option = await screen.findByText(/Australia\/Sydney/i)
fireEvent.click(option)
await waitFor(() => {
const state = store.getState();
expect(state.settings.isBrowserDefaultTimeZone).toBe(false);
});
const state = store.getState()
expect(state.settings.isBrowserDefaultTimeZone).toBe(false)
})
await waitFor(() => {
const state = store.getState();
expect(state.settings.timeZone).toBe("Australia/Sydney");
});
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"
);
});
});
});
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)", () => {
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");
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");
});
});
store.dispatch(setTimeZone('Africa/Cairo'))
expect(localStorage.getItem('timeZone')).toBe('Africa/Cairo')
})
})
test("Full user journey: Backend null -> User changes -> Backend saves", async () => {
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",
firstname: 'Test',
lastname: 'User',
id: '999',
preferredEmail: 'test@example.com',
configurations: {
modules: [
{
name: "core",
name: 'core',
configurations: [
{
name: "datetime",
value: { timeZone: null },
},
],
},
],
},
};
name: 'datetime',
value: { timeZone: null }
}
]
}
]
}
}
await store.dispatch(
getOpenPaasUserDataAsync.fulfilled(backendResponseNull, "", undefined)
);
getOpenPaasUserDataAsync.fulfilled(backendResponseNull, '', undefined)
)
// Verify initial state
expect(store.getState().settings.timeZone).toBe(browserDefaultTimeZone);
expect(store.getState().user.coreConfig.datetime.timeZone).toBe(null);
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));
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().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));
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);
});
});
expect(store.getState().settings.timeZone).toBe(browserDefaultTimeZone)
expect(store.getState().user.coreConfig.datetime.timeZone).toBe(null)
})
})
File diff suppressed because it is too large Load Diff
+268 -272
View File
@@ -1,467 +1,463 @@
import SettingsPage from "@/features/Settings/SettingsPage";
import settingsReducer from "@/features/Settings/SettingsSlice";
import SettingsPage from '@/features/Settings/SettingsPage'
import settingsReducer from '@/features/Settings/SettingsSlice'
import userReducer, {
getOpenPaasUserDataAsync,
} from "@/features/User/userSlice";
import { api } from "@/utils/apiUtils";
import { configureStore } from "@reduxjs/toolkit";
import "@testing-library/jest-dom";
import { fireEvent, screen, waitFor } from "@testing-library/react";
import { renderWithProviders } from "../../utils/Renderwithproviders";
getOpenPaasUserDataAsync
} from '@/features/User/userSlice'
import { api } from '@/utils/apiUtils'
import { configureStore } from '@reduxjs/toolkit'
import '@testing-library/jest-dom'
import { fireEvent, screen, waitFor } from '@testing-library/react'
import { renderWithProviders } from '../../utils/Renderwithproviders'
jest.mock("@/utils/apiUtils");
jest.mock('@/utils/apiUtils')
const basePreloadedState = {
user: {
userData: {
sub: "test",
email: "test@test.com",
family_name: "Doe",
name: "John",
sid: "mockSid",
openpaasId: "667037022b752d0026472254",
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" },
language: 'en',
datetime: { timeZone: 'UTC' }
},
alarmEmailsEnabled: false,
loading: false,
error: null,
error: null
},
settings: {
language: "en",
timeZone: "UTC",
language: 'en',
timeZone: 'UTC',
isBrowserDefaultTimeZone: false,
view: "settings",
view: 'settings',
businessHours: {
start: "8:0",
end: "19:0",
daysOfWeek: [1, 2, 3, 4, 5],
start: '8:0',
end: '19:0',
daysOfWeek: [1, 2, 3, 4, 5]
},
workingDays: false,
},
};
workingDays: false
}
}
describe("Working Days and Business Hours Settings", () => {
describe('Working Days and Business Hours Settings', () => {
beforeEach(() => {
jest.clearAllMocks();
(api.patch as jest.Mock).mockResolvedValue({ status: 204 });
});
jest.clearAllMocks()
;(api.patch as jest.Mock).mockResolvedValue({ status: 204 })
})
describe("Initial rendering", () => {
it("renders the working days section with day selector", () => {
renderWithProviders(<SettingsPage />, basePreloadedState);
describe('Initial rendering', () => {
it('renders the working days section with day selector', () => {
renderWithProviders(<SettingsPage />, basePreloadedState)
expect(screen.getByText('settings.chooseWorkingDays')).toBeInTheDocument()
})
it('renders the show only working days toggle', () => {
renderWithProviders(<SettingsPage />, basePreloadedState)
expect(
screen.getByText("settings.chooseWorkingDays")
).toBeInTheDocument();
});
screen.getByRole('switch', { name: /settings.showOnlyWorkingDays/i })
).toBeInTheDocument()
})
it("renders the show only working days toggle", () => {
renderWithProviders(<SettingsPage />, basePreloadedState);
it('shows working days toggle as unchecked when workingDays is false', () => {
renderWithProviders(<SettingsPage />, basePreloadedState)
expect(
screen.getByRole("switch", { name: /settings.showOnlyWorkingDays/i })
).toBeInTheDocument();
});
const toggle = screen.getByRole('switch', {
name: /settings.showOnlyWorkingDays/i
}) as HTMLInputElement
expect(toggle.checked).toBe(false)
})
it("shows working days toggle as unchecked when workingDays is false", () => {
renderWithProviders(<SettingsPage />, basePreloadedState);
const toggle = screen.getByRole("switch", {
name: /settings.showOnlyWorkingDays/i,
}) as HTMLInputElement;
expect(toggle.checked).toBe(false);
});
it("shows working days toggle as checked when workingDays is true", () => {
it('shows working days toggle as checked when workingDays is true', () => {
renderWithProviders(<SettingsPage />, {
...basePreloadedState,
settings: { ...basePreloadedState.settings, workingDays: true },
});
settings: { ...basePreloadedState.settings, workingDays: true }
})
const toggle = screen.getByRole("switch", {
name: /settings.showOnlyWorkingDays/i,
}) as HTMLInputElement;
expect(toggle.checked).toBe(true);
});
const toggle = screen.getByRole('switch', {
name: /settings.showOnlyWorkingDays/i
}) as HTMLInputElement
expect(toggle.checked).toBe(true)
})
it("shows working days toggle as unchecked when workingDays is null", () => {
it('shows working days toggle as unchecked when workingDays is null', () => {
renderWithProviders(<SettingsPage />, {
...basePreloadedState,
settings: { ...basePreloadedState.settings, workingDays: null },
});
settings: { ...basePreloadedState.settings, workingDays: null }
})
const toggle = screen.getByRole("switch", {
name: /settings.showOnlyWorkingDays/i,
}) as HTMLInputElement;
expect(toggle.checked).toBe(false);
});
});
const toggle = screen.getByRole('switch', {
name: /settings.showOnlyWorkingDays/i
}) as HTMLInputElement
expect(toggle.checked).toBe(false)
})
})
describe("workingDays toggle", () => {
it("updates workingDays immediately (optimistic update) and calls API", async () => {
describe('workingDays toggle', () => {
it('updates workingDays immediately (optimistic update) and calls API', async () => {
const { store } = renderWithProviders(
<SettingsPage />,
basePreloadedState
);
)
const toggle = screen.getByRole("switch", {
name: /settings.showOnlyWorkingDays/i,
});
fireEvent.click(toggle);
const toggle = screen.getByRole('switch', {
name: /settings.showOnlyWorkingDays/i
})
fireEvent.click(toggle)
await waitFor(() => {
expect(store.getState().settings.workingDays).toBe(true);
});
expect(store.getState().settings.workingDays).toBe(true)
})
await waitFor(() => {
expect(api.patch).toHaveBeenCalledWith(
"api/configurations?scope=user",
'api/configurations?scope=user',
expect.objectContaining({
json: expect.arrayContaining([
expect.objectContaining({
name: "linagora.esn.calendar",
name: 'linagora.esn.calendar',
configurations: expect.arrayContaining([
expect.objectContaining({
name: "workingDays",
value: true,
}),
]),
}),
]),
name: 'workingDays',
value: true
})
])
})
])
})
);
});
});
)
})
})
it("rolls back workingDays if API call fails", async () => {
(api.patch as jest.Mock).mockRejectedValue(new Error("API Error"));
it('rolls back workingDays if API call fails', async () => {
;(api.patch as jest.Mock).mockRejectedValue(new Error('API Error'))
const { store } = renderWithProviders(
<SettingsPage />,
basePreloadedState
);
)
const toggle = screen.getByRole("switch", {
name: /settings.showOnlyWorkingDays/i,
});
fireEvent.click(toggle);
const toggle = screen.getByRole('switch', {
name: /settings.showOnlyWorkingDays/i
})
fireEvent.click(toggle)
await waitFor(
() => {
expect(store.getState().settings.workingDays).toBe(false);
expect(store.getState().settings.workingDays).toBe(false)
},
{ timeout: 3000 }
);
)
await waitFor(() => {
expect(
screen.getByText("settings.workingDaysUpdateError")
).toBeInTheDocument();
});
});
screen.getByText('settings.workingDaysUpdateError')
).toBeInTheDocument()
})
})
it("toggles workingDays from true to false", async () => {
it('toggles workingDays from true to false', async () => {
const { store } = renderWithProviders(<SettingsPage />, {
...basePreloadedState,
settings: { ...basePreloadedState.settings, workingDays: true },
});
settings: { ...basePreloadedState.settings, workingDays: true }
})
const toggle = screen.getByRole("switch", {
name: /settings.showOnlyWorkingDays/i,
});
fireEvent.click(toggle);
const toggle = screen.getByRole('switch', {
name: /settings.showOnlyWorkingDays/i
})
fireEvent.click(toggle)
await waitFor(() => {
expect(store.getState().settings.workingDays).toBe(false);
});
expect(store.getState().settings.workingDays).toBe(false)
})
await waitFor(() => {
expect(api.patch).toHaveBeenCalledWith(
"api/configurations?scope=user",
'api/configurations?scope=user',
expect.objectContaining({
json: expect.arrayContaining([
expect.objectContaining({
name: "linagora.esn.calendar",
name: 'linagora.esn.calendar',
configurations: expect.arrayContaining([
expect.objectContaining({
name: "workingDays",
value: false,
}),
]),
}),
]),
name: 'workingDays',
value: false
})
])
})
])
})
);
});
});
});
)
})
})
})
describe("businessHours day selector", () => {
it("updates businessHours in Redux immediately when a day is clicked", async () => {
describe('businessHours day selector', () => {
it('updates businessHours in Redux immediately when a day is clicked', async () => {
const { store } = renderWithProviders(
<SettingsPage />,
basePreloadedState
);
)
// Sunday (0) is not in initial daysOfWeek [1,2,3,4,5], click it to add
const sundayButton = screen.getByRole("button", {
name: /sunday/i,
});
fireEvent.click(sundayButton);
const sundayButton = screen.getByRole('button', {
name: /sunday/i
})
fireEvent.click(sundayButton)
await waitFor(() => {
expect(store.getState().settings.businessHours?.daysOfWeek).toContain(
0
);
});
});
expect(store.getState().settings.businessHours?.daysOfWeek).toContain(0)
})
})
it("removes a day from businessHours when an already selected day is clicked", async () => {
it('removes a day from businessHours when an already selected day is clicked', async () => {
const { store } = renderWithProviders(
<SettingsPage />,
basePreloadedState
);
)
// Monday (1) is in initial daysOfWeek, click to remove
const mondayButton = screen.getByRole("button", { name: /monday/i });
fireEvent.click(mondayButton);
const mondayButton = screen.getByRole('button', { name: /monday/i })
fireEvent.click(mondayButton)
await waitFor(() => {
expect(
store.getState().settings.businessHours?.daysOfWeek
).not.toContain(1);
});
});
).not.toContain(1)
})
})
it("debounces API call when multiple days are clicked rapidly", async () => {
jest.useFakeTimers();
it('debounces API call when multiple days are clicked rapidly', async () => {
jest.useFakeTimers()
renderWithProviders(<SettingsPage />, basePreloadedState);
renderWithProviders(<SettingsPage />, basePreloadedState)
const sundayButton = screen.getByRole("button", { name: /sunday/i });
const saturdayButton = screen.getByRole("button", { name: /saturday/i });
const sundayButton = screen.getByRole('button', { name: /sunday/i })
const saturdayButton = screen.getByRole('button', { name: /saturday/i })
fireEvent.click(sundayButton);
fireEvent.click(saturdayButton);
fireEvent.click(sundayButton);
fireEvent.click(sundayButton)
fireEvent.click(saturdayButton)
fireEvent.click(sundayButton)
// API should not have been called yet
expect(api.patch).not.toHaveBeenCalled();
expect(api.patch).not.toHaveBeenCalled()
// Advance timers past debounce threshold
jest.runAllTimers();
jest.runAllTimers()
await waitFor(() => {
expect(api.patch).toHaveBeenCalledTimes(1);
});
expect(api.patch).toHaveBeenCalledTimes(1)
})
jest.useRealTimers();
});
jest.useRealTimers()
})
it("sends businessHours as array to API", async () => {
jest.useFakeTimers();
it('sends businessHours as array to API', async () => {
jest.useFakeTimers()
renderWithProviders(<SettingsPage />, basePreloadedState);
renderWithProviders(<SettingsPage />, basePreloadedState)
const sundayButton = screen.getByRole("button", { name: /sunday/i });
fireEvent.click(sundayButton);
const sundayButton = screen.getByRole('button', { name: /sunday/i })
fireEvent.click(sundayButton)
jest.runAllTimers();
jest.runAllTimers()
await waitFor(() => {
expect(api.patch).toHaveBeenCalledWith(
"api/configurations?scope=user",
'api/configurations?scope=user',
expect.objectContaining({
json: expect.arrayContaining([
expect.objectContaining({
name: "core",
name: 'core',
configurations: expect.arrayContaining([
expect.objectContaining({
name: "businessHours",
name: 'businessHours',
value: expect.arrayContaining([
expect.objectContaining({
start: "8:0",
end: "19:0",
daysOfWeek: expect.arrayContaining([0]),
}),
]),
}),
]),
}),
]),
start: '8:0',
end: '19:0',
daysOfWeek: expect.arrayContaining([0])
})
])
})
])
})
])
})
);
});
)
})
jest.useRealTimers();
});
jest.useRealTimers()
})
it("rolls back businessHours if API call fails", async () => {
jest.useFakeTimers();
(api.patch as jest.Mock).mockRejectedValue(new Error("API Error"));
it('rolls back businessHours if API call fails', async () => {
jest.useFakeTimers()
;(api.patch as jest.Mock).mockRejectedValue(new Error('API Error'))
const { store } = renderWithProviders(
<SettingsPage />,
basePreloadedState
);
)
const sundayButton = screen.getByRole("button", { name: /sunday/i });
fireEvent.click(sundayButton);
const sundayButton = screen.getByRole('button', { name: /sunday/i })
fireEvent.click(sundayButton)
jest.runAllTimers();
jest.runAllTimers()
await waitFor(
() => {
expect(
store.getState().settings.businessHours?.daysOfWeek
).not.toContain(0);
).not.toContain(0)
},
{ timeout: 3000 }
);
)
await waitFor(() => {
expect(
screen.getByText("settings.workingDaysUpdateError")
).toBeInTheDocument();
});
screen.getByText('settings.workingDaysUpdateError')
).toBeInTheDocument()
})
jest.useRealTimers();
});
});
jest.useRealTimers()
})
})
describe("getOpenPaasUserDataAsync integration", () => {
it("populates businessHours from core module configurations", async () => {
describe('getOpenPaasUserDataAsync integration', () => {
it('populates businessHours from core module configurations', async () => {
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",
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" } },
coreConfig: { language: 'en', datetime: { timeZone: 'UTC' } },
alarmEmailsEnabled: null,
loading: false,
error: null,
error: null
},
settings: {
language: "en",
timeZone: "UTC",
language: 'en',
timeZone: 'UTC',
isBrowserDefaultTimeZone: false,
view: "calendar",
view: 'calendar',
businessHours: null,
workingDays: null,
},
},
});
workingDays: null
}
}
})
const mockResponse = {
id: "667037022b752d0026472254",
firstname: "John",
lastname: "Doe",
preferredEmail: "test@test.com",
id: '667037022b752d0026472254',
firstname: 'John',
lastname: 'Doe',
preferredEmail: 'test@test.com',
configurations: {
modules: [
{
name: "core",
name: 'core',
configurations: [
{
name: "businessHours",
name: 'businessHours',
value: [
{ start: "8:0", end: "19:0", daysOfWeek: [1, 2, 3, 4, 5] },
],
},
],
{ start: '8:0', end: '19:0', daysOfWeek: [1, 2, 3, 4, 5] }
]
}
]
},
{
name: "linagora.esn.calendar",
configurations: [{ name: "workingDays", value: true }],
},
],
},
};
name: 'linagora.esn.calendar',
configurations: [{ name: 'workingDays', value: true }]
}
]
}
}
await store.dispatch(
getOpenPaasUserDataAsync.fulfilled(mockResponse, "", undefined)
);
getOpenPaasUserDataAsync.fulfilled(mockResponse, '', undefined)
)
const state = store.getState();
const state = store.getState()
expect(state.settings.businessHours).toEqual({
start: "8:0",
end: "19:0",
daysOfWeek: [1, 2, 3, 4, 5],
});
expect(state.settings.workingDays).toBe(true);
});
start: '8:0',
end: '19:0',
daysOfWeek: [1, 2, 3, 4, 5]
})
expect(state.settings.workingDays).toBe(true)
})
it("sets businessHours to null when not present in API response", async () => {
it('sets businessHours to null when not present in API response', async () => {
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",
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" } },
coreConfig: { language: 'en', datetime: { timeZone: 'UTC' } },
alarmEmailsEnabled: null,
loading: false,
error: null,
error: null
},
settings: {
language: "en",
timeZone: "UTC",
language: 'en',
timeZone: 'UTC',
isBrowserDefaultTimeZone: false,
view: "calendar",
view: 'calendar',
businessHours: null,
workingDays: null,
},
},
});
workingDays: null
}
}
})
const mockResponse = {
id: "667037022b752d0026472254",
firstname: "John",
lastname: "Doe",
preferredEmail: "test@test.com",
id: '667037022b752d0026472254',
firstname: 'John',
lastname: 'Doe',
preferredEmail: 'test@test.com',
configurations: {
modules: [
{
name: "core",
configurations: [],
name: 'core',
configurations: []
},
{
name: "linagora.esn.calendar",
configurations: [{ name: "workingDays", value: null }],
},
],
},
};
name: 'linagora.esn.calendar',
configurations: [{ name: 'workingDays', value: null }]
}
]
}
}
await store.dispatch(
getOpenPaasUserDataAsync.fulfilled(mockResponse, "", undefined)
);
getOpenPaasUserDataAsync.fulfilled(mockResponse, '', undefined)
)
const state = store.getState();
expect(state.settings.businessHours).toBeNull();
expect(state.settings.workingDays).toBeNull();
});
});
});
const state = store.getState()
expect(state.settings.businessHours).toBeNull()
expect(state.settings.workingDays).toBeNull()
})
})
})