-
Notifications
You must be signed in to change notification settings - Fork 3
feat: add app setting page #33
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
windforce852
wants to merge
1
commit into
dev
Choose a base branch
from
feat/CP-36-add-setting-page-v2
base: dev
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,201 @@ | ||
| import { BrowserRouter as Router } from 'react-router-dom'; | ||
| import { useUserContext } from '@/hooks/userHooks'; | ||
| import { render, screen } from '@/shared/utils/mockThemeProvider'; | ||
| import { hexToRgb } from '@/shared/utils/hexToRGB'; | ||
| import 'jest-styled-components'; | ||
| import userEvent from '@testing-library/user-event'; | ||
| import AppSetting from './page'; | ||
|
|
||
| // Mock 404.tsx to get rid of | ||
| // 'Expression produces a union type that is too complex to represent' error from its <Button/> | ||
| jest.mock('../../not-found', () => { | ||
| // Return a simple mock component that does nothing or renders a placeholder | ||
| const MockNotFound404 = () => <div data-testid="NotFound404">Not Found Placeholder</div>; | ||
|
|
||
| return { __esModule: true, default: MockNotFound404 }; | ||
| }); | ||
|
|
||
| // Mocking localStorage | ||
| const mockLocalStorage = (() => { | ||
| let store: Record<string, string> = {}; | ||
| return { | ||
| getItem(key: string): string | null { | ||
| return store[key] || null; | ||
| }, | ||
| setItem(key: string, value: string): void { | ||
| store[key] = value.toString(); | ||
| }, | ||
| clear(): void { | ||
| store = {}; | ||
| }, | ||
| }; | ||
| })(); | ||
|
|
||
| Object.defineProperty(window, 'localStorage', { | ||
| value: mockLocalStorage, | ||
| }); | ||
|
|
||
| // Mocking useUserContext | ||
| jest.mock('@/hooks/userHooks', () => ({ | ||
| useUserContext: jest.fn(), | ||
| })); | ||
|
|
||
| // Test case start | ||
| describe('<AppSetting />', () => { | ||
| beforeAll(() => { | ||
| // simulates the current state managed by the context which represents the "live" state | ||
| (useUserContext as jest.Mock).mockImplementation(() => ({ | ||
| store: { themeColor: 'dark' }, | ||
| setStore: jest.fn(), | ||
| })); | ||
|
|
||
| // mock the theme in localStorage that simulates | ||
| // a user has previously chosen 'dark' andhas been saved to localStorage. | ||
| localStorage.setItem('THEME', 'dark'); | ||
| }); | ||
|
|
||
| it('should successfully render the page and show titles and labels', async () => { | ||
| render( | ||
| <Router> | ||
| <AppSetting /> | ||
| </Router> | ||
| ); | ||
|
|
||
| // check background color | ||
| const cardBody = screen.getByTestId('card-body'); | ||
| const styles = getComputedStyle(cardBody); | ||
| expect(styles.backgroundColor).toBe(hexToRgb('#232329')); | ||
|
|
||
| // Title | ||
| expect(screen.getByText('App Settings')).toBeInTheDocument(); | ||
| expect(screen.getByText('Change your app settings')).toBeInTheDocument(); | ||
|
|
||
| // Language drop down label | ||
| expect(screen.getByText('Language')).toBeInTheDocument(); | ||
|
|
||
| // Theme drop down lable | ||
| expect(screen.getByText('Theme')).toBeInTheDocument(); | ||
| }); | ||
|
|
||
| it('should render the language dropdown, and trigger changeLanguage function and setStore when li is clicked', async () => { | ||
| const setStoreMock = jest.fn(); | ||
|
|
||
| (useUserContext as jest.Mock).mockImplementation(() => ({ | ||
| store: { language: '中文' }, | ||
| setStore: setStoreMock, | ||
| })); | ||
|
|
||
| render( | ||
| <Router> | ||
| <AppSetting /> | ||
| </Router> | ||
| ); | ||
|
|
||
| // default state of the language ul | ||
| const englishDiv = screen.getByText('English'); | ||
| expect(englishDiv).toBeInTheDocument(); | ||
|
|
||
| // user click the language ul, pop 2 options | ||
| await userEvent.click(englishDiv); | ||
| const englishLi = screen.getByTestId('languagesList-English'); | ||
| const chineseLi = screen.getByTestId('languagesList-中文'); | ||
| expect(englishLi).toBeInTheDocument; | ||
| expect(chineseLi).toBeInTheDocument; | ||
|
|
||
| const logSpy = jest.spyOn(console, 'log'); | ||
|
|
||
| // user click chinese option | ||
| await userEvent.click(chineseLi); | ||
| expect(setStoreMock).toHaveBeenCalled(); | ||
|
|
||
| // changeLanguage fired | ||
| expect(logSpy).toHaveBeenCalledWith('中文'); | ||
|
|
||
| // ul change to '中文' | ||
| const chineseDiv = screen.getByText('中文'); | ||
| expect(chineseDiv).toBeInTheDocument; | ||
| }); | ||
|
|
||
| it('should render the theme dropdown, and trigger changeTheme function and setStore when li is clicked', async () => { | ||
| const setStoreMock = jest.fn(); | ||
|
|
||
| (useUserContext as jest.Mock).mockImplementation(() => ({ | ||
| store: { themeColor: 'light' }, | ||
| setStore: setStoreMock, | ||
| })); | ||
|
|
||
| render( | ||
| <Router> | ||
| <AppSetting /> | ||
| </Router> | ||
| ); | ||
|
|
||
| // default state of the theme ul | ||
| const darkDiv = screen.getByText('Dark'); | ||
| expect(darkDiv).toBeInTheDocument(); | ||
|
|
||
| // user click the theme ul, pop 2 options | ||
| await userEvent.click(darkDiv); | ||
| const darkLi = screen.getByTestId('themeList-Dark'); | ||
| const lightLi = screen.getByTestId('themeList-Light'); | ||
| expect(darkLi).toBeInTheDocument; | ||
| expect(lightLi).toBeInTheDocument; | ||
|
|
||
| // user click chinese option | ||
| await userEvent.click(lightLi); | ||
| expect(setStoreMock).toHaveBeenCalled(); | ||
|
|
||
| // ul change to 'Light' | ||
| const lightDiv = screen.getByText('Light'); | ||
| expect(lightDiv).toBeInTheDocument; | ||
| }); | ||
|
|
||
| describe('<AppSetting /> with edge case local storage value', () => { | ||
| const originalGetItem = localStorage.getItem; | ||
|
|
||
| beforeEach(() => { | ||
| localStorage.getItem = jest.fn(() => null); | ||
| }); | ||
|
|
||
| afterEach(() => { | ||
| localStorage.getItem = originalGetItem; | ||
| }); | ||
|
|
||
| it('should successfully render the page when local storage returns null', async () => { | ||
| localStorage.getItem = jest.fn(() => null); | ||
|
|
||
| render( | ||
| <Router> | ||
| <AppSetting /> | ||
| </Router> | ||
| ); | ||
|
|
||
| expect(screen.getByText('App Settings')).toBeInTheDocument(); | ||
| expect(screen.getByText('Change your app settings')).toBeInTheDocument(); | ||
| expect(screen.getByText('Language')).toBeInTheDocument(); | ||
| expect(screen.getByText('Theme')).toBeInTheDocument(); | ||
| const englishDiv = screen.getByText('English'); | ||
| expect(englishDiv).toBeInTheDocument(); | ||
| const darkDiv = screen.getByText('Dark'); | ||
| expect(darkDiv).toBeInTheDocument(); | ||
| }); | ||
| }); | ||
|
|
||
| it('the <CardBody/> background-color value should be "" when local storage returns unexpected value', () => { | ||
| localStorage.getItem = jest.fn(() => 'blue'); | ||
|
|
||
| render( | ||
| <Router> | ||
| <AppSetting /> | ||
| </Router> | ||
| ); | ||
|
|
||
| const cardBody = screen.getByTestId('card-body'); | ||
| const styles = getComputedStyle(cardBody); | ||
| // expect(styles.backgroundColor).toBe(hexToRgb('#232329')); //fail | ||
| expect(styles.backgroundColor).toBe(''); | ||
|
|
||
| // TODO: value should be #232329 (dark theme) when local storage returns null' | ||
| // but this bug will be solved in the future. | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,121 @@ | ||
| 'use client'; | ||
|
|
||
| import { useEffect, useState } from 'react'; | ||
| import { Col, Container, Row } from 'react-bootstrap'; | ||
| import { Card, CardBody, CardTitleWrap, CardTitle, CardSubhead } from '@/shared/components/Card'; | ||
| import { | ||
| FormContainer, | ||
| FormGroup, | ||
| FormGroupField, | ||
| FormGroupLabel, | ||
| } from '@/shared/components/form/FormElements'; | ||
| import { Controller, useForm } from 'react-hook-form'; | ||
| import { useUserContext } from '@/hooks/userHooks'; | ||
| import { THEME, LANGUAGE } from '@/shared/constants/storage'; | ||
| import CustomDropdownList from '@/shared/components/form/CustomDropdown'; | ||
|
|
||
| const AppSetting = () => { | ||
| const { control } = useForm(); | ||
| const { store, setStore } = useUserContext(); | ||
| const [themeColor, setThemeColor] = useState(store.themeColor || 'dark'); | ||
| const [language, setLanguage] = useState(store.language || 'eng'); | ||
| const languagesList: string[] = ['English', '中文']; | ||
| const themeList: string[] = ['Light', 'Dark']; | ||
| const languagesListId: string = 'languagesList'; | ||
| const themeListId: string = 'themeList'; | ||
|
|
||
| useEffect(() => { | ||
| const storedTheme = localStorage.getItem(THEME) || themeColor; | ||
| setThemeColor(storedTheme); | ||
| if (store.themeColor !== storedTheme) { | ||
| setStore({ ...store, themeColor: storedTheme }); | ||
| } | ||
| }, [store, setStore]); | ||
|
|
||
| const changeTheme = () => { | ||
| // TO DO: CP-33 | ||
| setStore({}); | ||
| }; | ||
|
|
||
| useEffect(() => { | ||
| const storedLanguage = localStorage.getItem(LANGUAGE) || language; | ||
| setLanguage(storedLanguage); | ||
| if (store.language !== storedLanguage) { | ||
| setStore({ ...store, language: storedLanguage }); | ||
| } | ||
| }, []); | ||
|
|
||
| const changeLanguage = (newLanguage: string) => { | ||
| // eslint-disable-next-line no-console | ||
| console.log(`${newLanguage}`); | ||
| setStore({}); | ||
| }; | ||
|
|
||
| return ( | ||
| <Container> | ||
| <Row> | ||
| <Col md={12} lg={12}> | ||
| <Card> | ||
| <CardBody data-testid="card-body"> | ||
| <CardTitleWrap> | ||
| <CardTitle>App Settings</CardTitle> | ||
| <CardSubhead>Change your app settings</CardSubhead> | ||
| </CardTitleWrap> | ||
| <FormContainer $horizontal> | ||
| <FormGroup> | ||
| <FormGroupLabel>Language</FormGroupLabel> | ||
| <FormGroupField> | ||
| <Controller | ||
| name="Language" | ||
| control={control} | ||
| rules={{ required: 'Language selection is required' }} | ||
| defaultValue="English" | ||
| render={({ field: { onChange, value } }) => ( | ||
| <CustomDropdownList | ||
| list={languagesList} | ||
| listName={languagesListId} | ||
| onChange={(selectedLanguage) => { | ||
| onChange(selectedLanguage); | ||
| changeLanguage(selectedLanguage); | ||
| }} | ||
| value={value} | ||
| defaultValue="English" | ||
| /> | ||
| )} | ||
| /> | ||
| </FormGroupField> | ||
| </FormGroup> | ||
|
|
||
| <FormGroup> | ||
| <FormGroupLabel>Theme</FormGroupLabel> | ||
| <FormGroupField> | ||
| <Controller | ||
| name="Theme" | ||
| control={control} | ||
| rules={{ required: 'Theme selection is required' }} | ||
| defaultValue="Dark" | ||
| render={({ field: { onChange, value } }) => ( | ||
| <CustomDropdownList | ||
| list={themeList} | ||
| listName={themeListId} | ||
| onChange={(theme) => { | ||
| onChange(theme); | ||
| changeTheme(); | ||
| }} | ||
| value={value} | ||
| defaultValue="Dark" | ||
| /> | ||
| )} | ||
| /> | ||
| </FormGroupField> | ||
| </FormGroup> | ||
| </FormContainer> | ||
| </CardBody> | ||
| </Card> | ||
| </Col> | ||
| </Row> | ||
| </Container> | ||
| ); | ||
| }; | ||
|
|
||
| export default AppSetting; | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| import { ReactNode } from 'react'; | ||
|
|
||
| export type MainWrapperProps = { | ||
| children: ReactNode; | ||
| }; | ||
|
|
||
| const MainWrapper = ({ children }: MainWrapperProps) => <div>{children}</div>; | ||
|
|
||
| export default MainWrapper; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No need to leave between lines in tsx return part
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
do you mean
/>in line 76 should merge to the tail of 75? done