Skip to content
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

Agrim/dapi 536/copyproduction #38

Merged
3 changes: 2 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"editor.codeActionsOnSave": { "source.fixAll.eslint": true },
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true
"editor.formatOnSave": true,
"typescript.tsdk": "node_modules/typescript/lib"
}
10 changes: 10 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
},
"dependencies": {
"@deriv/deriv-api": "^1.0.11",
"@deriv/quill-icons": "^1.22.10",
"@deriv/ui": "^0.8.0",
"@docusaurus/core": "^3.3.2",
"@docusaurus/plugin-client-redirects": "^3.3.2",
Expand Down
2 changes: 2 additions & 0 deletions src/contexts/api-token/api-token.context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ export interface IApiTokenContext {
isLoadingTokens: boolean;
currentToken: TTokenType;
updateCurrentToken: (token: TTokenType) => void;
lastTokenDisplayName: string;
setLastTokenDisplayName: (name: string) => void;
}

export const ApiTokenContext = React.createContext<IApiTokenContext | null>(null);
13 changes: 12 additions & 1 deletion src/contexts/api-token/api-token.provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ type TTokenProviderProps = {
const ApiTokenProvider = ({ children }: TTokenProviderProps) => {
const [tokens, setTokens] = useState<TTokensArrayType>([]);
const [currentToken, setCurrentToken] = useState<TTokenType>();
const [lastTokenDisplayName, setLastTokenDisplayName] = useState<string>('');

const { send: getAllTokens, data, is_loading } = useWS('api_token');
const { is_authorized } = useAuthContext();
Expand Down Expand Up @@ -46,8 +47,18 @@ const ApiTokenProvider = ({ children }: TTokenProviderProps) => {
currentToken,
updateCurrentToken,
updateTokens,
lastTokenDisplayName,
setLastTokenDisplayName,
};
}, [currentToken, is_loading, tokens, updateCurrentToken, updateTokens]);
}, [
currentToken,
is_loading,
tokens,
updateCurrentToken,
updateTokens,
lastTokenDisplayName,
setLastTokenDisplayName,
]);

return <ApiTokenContext.Provider value={contextValue}>{children}</ApiTokenContext.Provider>;
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ const CreateTokenField = ({
setFormIsCleared(false);
}
}, [form_is_cleared]);

const getTokenNames = useMemo(() => {
const token_names = [];
for (const token_object of tokens) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ describe('Home Page', () => {
valid_for_ip: '',
},
],
lastTokenDisplayName: '',
}));

render(<ApiTokenForm />);
Expand Down Expand Up @@ -214,13 +215,9 @@ describe('Home Page', () => {
});

const submitButton = screen.getByRole('button', { name: /Create/i });

await act(async () => {
await userEvent.click(submitButton);
});

const modal = await screen.getByText('Your API token is ready to be used.');
expect(modal).toBeVisible();
shafin-deriv marked this conversation as resolved.
Show resolved Hide resolved
});

it('Should have create button disabled in case of empty input or error message', async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ describe('CopyButton', () => {
it('should render the CopyButton', () => {
render(<CopyButton value='testvalue' has_admin />);

const copy_button = screen.getByRole('button');
const copy_button = screen.getByRole('button', { name: 'copy_button' });
expect(copy_button).toBeInTheDocument();
});

Expand Down Expand Up @@ -79,24 +79,4 @@ describe('CopyButton', () => {

expect(modal).not.toBeInTheDocument();
});

it('should show a green check for 2 seconds when copied', async () => {
const user = userEvent.setup({ delay: null });
jest.useFakeTimers();

render(<CopyButton value='testvalue' />);

const copy_button = screen.getByRole('button');
await act(async () => {
await user.click(copy_button);
});

expect(copy_button.classList.contains('is_copying')).toBe(true);

act(() => {
jest.runAllTimers();
});

expect(copy_button.classList.contains('is_copying')).toBe(false);
});
});
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React, { useState } from 'react';
import CopyTokenDialog from '../CopyTokenDialog';
import styles from '../token-cell.module.scss';
import { StandaloneCopyRegularIcon } from '@deriv/quill-icons';

type TCopyButton = {
value: string;
Expand Down Expand Up @@ -28,12 +29,17 @@ const CopyButton = ({ value, has_admin = false }: TCopyButton) => {

return (
<React.Fragment>
<button
<StandaloneCopyRegularIcon
fill='var(--component-textIcon-normal-prominent)'
iconSize='sm'
onClick={() => {
has_admin ? setToggleModal(!toggle_modal) : copyToken();
}}
className={`${styles.copy_button} ${is_copying}`}
className={styles.copy_button}
role='button'
aria-label='copy_button'
/>

{toggle_modal && <CopyTokenDialog setToggleModal={setToggleModal} copyToken={copyToken} />}
</React.Fragment>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import React, { useEffect, useState } from 'react';
import { TTokenType } from '@site/src/types';
import { CellProps } from 'react-table';
import styles from './token-cell.module.scss';
import CopyButton from './CopyButton';

const ApiTokenCell = ({ cell }: React.PropsWithChildren<CellProps<TTokenType, string>>) => {
const [is_hiding_token, setIsHidingToken] = useState(true);
Expand All @@ -24,16 +23,7 @@ const ApiTokenCell = ({ cell }: React.PropsWithChildren<CellProps<TTokenType, st

return (
<div data-testid={'token-cell'} className={styles.token_cell}>
<div>{is_hiding_token ? <HiddenToken /> : cell.value}</div>
<CopyButton has_admin={has_admin_scope} value={token} />
<button
onClick={() => setIsHidingToken(!is_hiding_token)}
className={styles.eye_button}
data-testid='eye-button'
style={{
backgroundImage: is_hiding_token ? 'url(/img/eye_closed.svg)' : 'url(/img/eye_open.svg)',
}}
/>
<div>{cell.value}</div>
</div>
);
};
Expand Down
Original file line number Diff line number Diff line change
@@ -1,82 +1,23 @@
@use 'src/styles/utility' as *;

.hidden_container {
display: flex;
gap: rem(0.45);
.hidden_character {
width: rem(0.5);
height: rem(0.5);
border-radius: 100%;
background-color: var(--ifm-color-emphasis-900);
}
display: flex;
gap: rem(0.45);
.hidden_character {
width: rem(0.5);
height: rem(0.5);
border-radius: 100%;
background-color: var(--ifm-color-emphasis-900);
}
}

.copy_button {
cursor: pointer;
}

.token_cell {
display: flex;
justify-content: center;
align-items: center;
gap: rem(1);
button {
position: relative;
min-width: rem(1.5);
min-height: rem(1.5);
background-repeat: no-repeat;
background-position: center;
background-color: var(--colors-greyLight200);
border: 1px solid var(--colors-greyLight400);
border-radius: 100%;
padding: rem(0.3);
&.copy_button {
cursor: copy;
background-image: url(/img/copy.svg);
}
&:hover {
&::after {
content: '';
text-align: center;
position: absolute;
display: inline-block;
border-radius: 4px;
padding: rem(1);
color: var(--ifm-color-emphasis-100);
background-color: var(--ifm-color-emphasis-700);
font-size: var(--fontSizes-3xs);
top: calc(-50% - 20px);
left: 50%;
min-width: 100px;
transform: translate(-50%, -50%);
}
&::before {
content: '';
position: absolute;
width: 0;
height: 0;
border-left: rem(0.7) solid transparent;
border-right: rem(0.7) solid transparent;
border-top: rem(0.7) solid var(--ifm-color-emphasis-700);
top: calc(-50% + 2px);
transform: translate(-50%, -50%);
left: 50%;
}
}
&.eye_button {
cursor: pointer;
&:hover::after {
content: 'Hide this token';
}
}
&.copy_button {
&:hover::after {
content: 'Copy this token';
}
&.is_copying {
background-image: url(/img/check.svg);
background-color: var(--ifm-color-primary-lightest);
border: 1px solid var(--ifm-color-primary);
&:hover::after {
content: 'Token copied!';
}
}
}
}
display: flex;
justify-content: left;
align-items: center;
gap: 0.625rem;
}
Original file line number Diff line number Diff line change
@@ -1,21 +1,59 @@
import React, { act } from 'react';
import TokenCreationDialogSuccess from '..';
import { screen, render } from '@testing-library/react';
import { screen, render, cleanup } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import useApiToken from '@site/src/hooks/useApiToken';

jest.mock('@site/src/hooks/useApiToken');

const mockUseApiToken = useApiToken as jest.MockedFunction<
() => Partial<ReturnType<typeof useApiToken>>
>;

describe('Token Creation Dialog', () => {
it('Should display correct title on the modal', () => {
beforeEach(() => {
mockUseApiToken.mockImplementation(() => ({
tokens: [
{
display_name: 'testtoken1',
last_used: '',
scopes: ['read', 'trade', 'payments', 'admin'],
token: 'asdf1234',
valid_for_ip: '',
},
{
display_name: 'testtoken2',
last_used: '',
scopes: ['read', 'trade', 'payments', 'admin'],
token: 'asdf1235',
valid_for_ip: '',
},
],
lastTokenDisplayName: 'agrim',
}));

render(<TokenCreationDialogSuccess setToggleModal={jest.fn()} />);
});

const title = screen.getByText(/Token created successfully/i);
expect(title).toHaveTextContent('Token created successfully');
afterEach(() => {
cleanup();
jest.clearAllMocks();
});

it('Should display correct content on the modal', () => {
render(<TokenCreationDialogSuccess setToggleModal={jest.fn()} />);
it('Should display correct title on the modal', () => {
const title = screen.getAllByText(/Token created successfully!/i);
expect(title).toHaveLength(1);
expect(title[0]).toHaveTextContent('Token created successfully!');
});

const textContent = screen.getByText(/Your API token is ready to be used./i);
expect(textContent).toHaveTextContent('Your API token is ready to be used.');
it('Should display correct content on the modal', () => {
const textContent = screen.getAllByText(
/Please save this token key. For security reasons, it can't be viewed or copied again. If you lose this key, you'll need to generate a new token./i,
);
expect(textContent).toHaveLength(1);
expect(textContent[0]).toHaveTextContent(
/Please save this token key. For security reasons, it can't be viewed or copied again. If you lose this key, you'll need to generate a new token./i,
);
});

it('Should close the modal on OK button click', async () => {
Expand All @@ -31,19 +69,4 @@ describe('Token Creation Dialog', () => {

expect(mockOnClose).toHaveBeenCalledTimes(1);
});

it('Should close the modal on cross button click', async () => {
const mockOnClose = jest.fn();

render(<TokenCreationDialogSuccess setToggleModal={mockOnClose} />);
const modal = screen.getByText('Your API token is ready to be used.');

const crossButton = screen.getByTestId('close-button');
await act(async () => {
await userEvent.click(crossButton);
});

expect(modal).not.toBeInTheDocument();
expect(mockOnClose).toHaveBeenCalled();
});
});
Loading
Loading