Skip to content
Open
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
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@
"husky": "^8.0.0",
"jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0",
"jest-styled-components": "^7.2.0",
"jest-transform-stub": "^2.0.0",
"jest-transformer-svg": "^2.0.2",
"less": "^4.2.0",
Expand Down
4 changes: 4 additions & 0 deletions public/img/dropdownArrow.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
201 changes: 201 additions & 0 deletions src/app/(protected)/appsetting/AppSetting.test.tsx
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.
});
});
121 changes: 121 additions & 0 deletions src/app/(protected)/appsetting/page.tsx
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>

Copy link
Contributor

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

Copy link
Author

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

<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;
9 changes: 9 additions & 0 deletions src/containers/App/MainWrapper.tsx
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;
5 changes: 5 additions & 0 deletions src/containers/Layout/sidebar/SidebarContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,11 @@ const SidebarContent = ({ onClick, $collapse }: SidebarContentProps) => {
route={getRouteByKey(ROUTE_KEY.EXCHANGE_MANAGEMENT).path}
onClick={onClick}
/>
<SidebarLink
title={getRouteByKey(ROUTE_KEY.APP_SETTING).name}
route={getRouteByKey(ROUTE_KEY.APP_SETTING).path}
onClick={onClick}
/>
</SidebarCategory>
</SidebarBlock>
<SidebarBlock $collapse={$collapse}>
Expand Down
13 changes: 0 additions & 13 deletions src/graphql/codegen/graphql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,6 @@ export type ExchangeKey = {

export type Mutation = {
__typename?: 'Mutation';
/** Change password */
changePassword: Result;
/** Create exchange key */
createExchangeKey: Scalars['Boolean']['output'];
/** Create new user */
Expand All @@ -89,10 +87,6 @@ export type Mutation = {
updateUser: Scalars['Boolean']['output'];
};

export type MutationChangePasswordArgs = {
input: UpdatePasswordInput;
};

export type MutationCreateExchangeKeyArgs = {
input: CreateExchangeKeyInput;
};
Expand Down Expand Up @@ -167,13 +161,6 @@ export type UpdateExchangeKeyInput = {
secretKey: Scalars['String']['input'];
};

export type UpdatePasswordInput = {
/** New Password */
newPassword: Scalars['String']['input'];
/** Old Password */
oldPassword: Scalars['String']['input'];
};

export type UpdateUserInput = {
/** User display name */
displayName?: InputMaybe<Scalars['String']['input']>;
Expand Down
Loading