Commit 734091d5 authored by tom's avatar tom

tests for account part

parent 4a4ecfbc
import type { TokenInfoApplication, TokenInfoApplications, VerifiedAddress, VerifiedAddressResponse } from 'types/api/account';
import type { AddressValidationResponseSuccess } from 'ui/addressVerification/types';
export const SIGNATURE = '0x96491e0cd1b99c14951552361b7f6ff64f41651b5d1c12501914342c8a6847e21e08726c3505e11ba2af9a40ac0b05c8d113e7fd1f74594224b9c7276ebb3a661b';
export const VERIFIED_ADDRESS: Record<string, VerifiedAddress> = {
NEW_ITEM: {
userId: '1',
chainId: '99',
contractAddress: '0xF822070D07067D1519490dBf49448a7E30EE9ea5',
verifiedDate: '2022-09-01',
metadata: {
tokenName: 'Test Token',
tokenSymbol: 'TT',
},
},
ITEM_1: {
userId: '1',
chainId: '99',
contractAddress: '0xd0e3010d1ecdbd17aae178b2bf36eb413d8a7441',
verifiedDate: '2022-08-01',
metadata: {
tokenName: 'My Token',
tokenSymbol: 'MYT',
},
},
ITEM_2: {
userId: '1',
chainId: '99',
contractAddress: '0xa8FCe579a11E551635b9c9CB915BEcd873C51254',
verifiedDate: '2022-09-23',
metadata: {
tokenName: 'Cat Token',
tokenSymbol: 'CATT',
},
},
};
export const ADDRESS_CHECK_RESPONSE = {
SUCCESS: {
status: 'SUCCESS',
result: {
// eslint-disable-next-line max-len
signingMessage: '[eth-goerli.blockscout.com] [2023-04-18 18:47:40] I, hereby verify that I am the owner/creator of the address [0xf822070d07067d1519490dbf49448a7e30ee9ea5]',
contractCreator: '0xd0e3010d1ecdbd17aae178b2bf36eb413d8a7441',
contractOwner: '0xa8FCe579a11E551635b9c9CB915BEcd873C51254',
},
},
SOURCE_CODE_NOT_VERIFIED_ERROR: {
status: 'SOURCE_CODE_NOT_VERIFIED_ERROR',
},
};
export const ADDRESS_VERIFY_RESPONSE: Record<string, AddressValidationResponseSuccess> = {
SUCCESS: {
status: 'SUCCESS',
result: {
verifiedAddress: VERIFIED_ADDRESS.NEW_ITEM,
},
},
INVALID_SIGNER_ERROR: {
status: 'INVALID_SIGNER_ERROR',
invalidSigner: {
signer: '0xF822070D07067D1519490dBf49448a7E30EE9ea5',
},
},
};
export const VERIFIED_ADDRESS_RESPONSE: Record<string, VerifiedAddressResponse> = {
DEFAULT: {
verifiedAddresses: [
VERIFIED_ADDRESS.ITEM_1,
VERIFIED_ADDRESS.ITEM_2,
],
},
};
export const TOKEN_INFO_APPLICATION_BASE = {
id: '1',
tokenAddress: VERIFIED_ADDRESS.ITEM_1.contractAddress,
status: 'APPROVED',
updatedAt: '2022-11-08 12:47:10.149148Z',
requesterName: 'Tom',
requesterEmail: 'tom@example.com',
projectName: 'My project',
projectWebsite: 'http://example.com',
projectEmail: 'token@example.com',
iconUrl: 'https://placekitten.com/100',
projectDescription: 'description',
projectSector: 'DeFi',
comment: '',
docs: 'https://example.com/docs',
github: 'https://github.com',
telegram: 'https://telegram.com',
linkedin: 'https://linkedin.com',
discord: 'https://discord.com',
slack: 'https://slack.com',
twitter: 'https://twitter.com',
openSea: 'https://opensea.com',
facebook: 'https://facebook.com',
medium: 'https://medium.com',
reddit: 'https://reddit.com',
support: 'support@example.com',
coinMarketCapTicker: 'https://coinmarketcap.com',
coinGeckoTicker: 'https://coingecko.com',
defiLlamaTicker: 'https://defillama.com',
};
export const TOKEN_INFO_APPLICATION: Record<string, TokenInfoApplication> = {
APPROVED: {
...TOKEN_INFO_APPLICATION_BASE,
tokenAddress: VERIFIED_ADDRESS.ITEM_1.contractAddress,
id: '1',
status: 'APPROVED',
updatedAt: '2022-11-08 12:47:10.149148Z',
},
IN_PROCESS: {
...TOKEN_INFO_APPLICATION_BASE,
tokenAddress: VERIFIED_ADDRESS.ITEM_2.contractAddress,
id: '2',
status: 'IN_PROCESS',
updatedAt: '2022-11-10 08:11:10.149148Z',
},
UPDATED_ITEM: {
...TOKEN_INFO_APPLICATION_BASE,
tokenAddress: VERIFIED_ADDRESS.ITEM_1.contractAddress,
id: '1',
status: 'IN_PROCESS',
updatedAt: '2022-11-11 05:11:10.149148Z',
},
};
export const TOKEN_INFO_APPLICATIONS_RESPONSE: Record<string, TokenInfoApplications> = {
DEFAULT: {
submissions: [
TOKEN_INFO_APPLICATION.APPROVED,
TOKEN_INFO_APPLICATION.IN_PROCESS,
],
},
FOR_UPDATE: {
submissions: [
{
...TOKEN_INFO_APPLICATION.APPROVED,
status: 'UPDATE_REQUIRED',
},
TOKEN_INFO_APPLICATION.IN_PROCESS,
],
},
};
export const TOKEN_INFO_FORM_CONFIG = {
projectSectors: [
'Infra & Dev tooling',
'DeFi',
'Data',
'Bridge',
'NFT',
'Payments',
'Faucet',
'DAO',
'Games',
'Wallet',
],
};
......@@ -4,13 +4,16 @@ import React from 'react';
import getNetworkTitle from 'lib/networks/getNetworkTitle';
import VerifiedAddresses from 'ui/pages/VerifiedAddresses';
import Page from 'ui/shared/Page/Page';
const VerifiedAddressesPage: NextPage = () => {
const title = getNetworkTitle();
return (
<>
<Head><title>{ title }</title></Head>
<Page>
<VerifiedAddresses/>
</Page>
</>
);
};
......
......@@ -3,7 +3,7 @@ import { compile } from 'path-to-regexp';
import type { ResourceName, ResourcePathParams } from 'lib/api/resources';
import { RESOURCES } from 'lib/api/resources';
export default function buildApiUrl<R extends ResourceName>(resourceName: R, pathParams?: ResourcePathParams<R>) {
export default function buildApiUrl<R extends ResourceName>(resourceName: R, pathParams?: ResourcePathParams<R>, noBasePath?: boolean) {
const resource = RESOURCES[resourceName];
return compile('/node-api/proxy/poa/core' + resource.path)(pathParams);
return compile(`/node-api/proxy${ noBasePath ? '' : '/poa/core' }` + resource.path)(pathParams);
}
......@@ -183,6 +183,7 @@ export interface TokenInfoApplicationConfig {
export interface TokenInfoApplication {
coinGeckoTicker?: string;
coinMarketCapTicker?: string;
comment?: string;
defiLlamaTicker?: string;
discord?: string;
docs?: string;
......
......@@ -60,6 +60,7 @@ const AddressVerificationModal = ({ defaultAddress, isOpen, onClose, onSubmit, o
{
title: 'Copy and sign message',
content: <AddressVerificationStepSignature { ...data } onContinue={ handleGoToThirdStep }/>,
fallback: <AddressVerificationStepSignature { ...data } onContinue={ handleGoToThirdStep } noWeb3Provider/>,
},
{
title: 'Congrats! Address is verified.',
......@@ -89,7 +90,7 @@ const AddressVerificationModal = ({ defaultAddress, isOpen, onClose, onSubmit, o
</ModalHeader>
<ModalCloseButton/>
<ModalBody mb={ 0 }>
<Web3ModalProvider>
<Web3ModalProvider fallback={ step?.fallback || step.content }>
{ step.content }
</Web3ModalProvider>
</ModalBody>
......
import { test, expect } from '@playwright/experimental-ct-react';
import React from 'react';
import * as mocks from 'mocks/account/verifiedAddresses';
import TestApp from 'playwright/TestApp';
import buildApiUrl from 'playwright/utils/buildApiUrl';
import AddressVerificationStepAddress from './AddressVerificationStepAddress';
const CHECK_ADDRESS_URL = buildApiUrl('address_verification', { chainId: '99', type: ':prepare' }, true);
test('base view', async({ mount, page }) => {
await page.route(CHECK_ADDRESS_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify(mocks.ADDRESS_CHECK_RESPONSE.SUCCESS),
}));
const props = {
onContinue: () => {},
defaultAddress: mocks.VERIFIED_ADDRESS.NEW_ITEM.contractAddress,
};
await mount(
<TestApp>
<AddressVerificationStepAddress { ...props }/>
</TestApp>,
);
await expect(page).toHaveScreenshot();
});
test('SOURCE_CODE_NOT_VERIFIED_ERROR view +@mobile', async({ mount, page }) => {
await page.route(CHECK_ADDRESS_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify(mocks.ADDRESS_CHECK_RESPONSE.SOURCE_CODE_NOT_VERIFIED_ERROR),
}));
const props = {
onContinue: () => {},
};
await mount(
<TestApp>
<AddressVerificationStepAddress { ...props }/>
</TestApp>,
);
const addressInput = page.getByLabel(/smart contract address/i);
await addressInput.focus();
await addressInput.type(mocks.VERIFIED_ADDRESS.NEW_ITEM.contractAddress);
await page.getByRole('button', { name: /continue/i }).click();
await expect(page).toHaveScreenshot();
});
......@@ -103,8 +103,8 @@ const AddressVerificationStepAddress = ({ defaultAddress, onContinue }: Props) =
<Box>Enter the contract address you are verifying ownership for.</Box>
{ rootError && <Alert status="warning" mt={ 3 }>{ rootError }</Alert> }
<AddressVerificationFieldAddress formState={ formState } control={ control }/>
<Flex alignItems="center" mt={ 8 } columnGap={ 5 }>
<Button size="lg" type="submit" isDisabled={ formState.isSubmitting }>
<Flex alignItems={{ base: 'flex-start', lg: 'center' }} mt={ 8 } columnGap={ 5 } rowGap={ 2 } flexDir={{ base: 'column', lg: 'row' }}>
<Button size="lg" type="submit" isDisabled={ formState.isSubmitting } flexShrink={ 0 }>
Continue
</Button>
<AdminSupportText/>
......
import { test, expect } from '@playwright/experimental-ct-react';
import React from 'react';
import * as mocks from 'mocks/account/verifiedAddresses';
import TestApp from 'playwright/TestApp';
import buildApiUrl from 'playwright/utils/buildApiUrl';
import AddressVerificationStepSignature from './AddressVerificationStepSignature';
const VERIFY_ADDRESS_URL = buildApiUrl('address_verification', { chainId: '99', type: ':verify' }, true);
test('base view', async({ mount, page }) => {
await page.route(VERIFY_ADDRESS_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify(mocks.ADDRESS_VERIFY_RESPONSE.SUCCESS),
}));
const props = {
onContinue: () => {},
noWeb3Provider: true,
address: mocks.VERIFIED_ADDRESS.NEW_ITEM.contractAddress,
signingMessage: mocks.ADDRESS_CHECK_RESPONSE.SUCCESS.result.signingMessage,
};
await mount(
<TestApp>
<AddressVerificationStepSignature { ...props }/>
</TestApp>,
);
await expect(page).toHaveScreenshot();
});
test('INVALID_SIGNER_ERROR view +@mobile', async({ mount, page }) => {
await page.route(VERIFY_ADDRESS_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify(mocks.ADDRESS_VERIFY_RESPONSE.INVALID_SIGNER_ERROR),
}));
const props = {
onContinue: () => {},
noWeb3Provider: true,
address: mocks.VERIFIED_ADDRESS.NEW_ITEM.contractAddress,
...mocks.ADDRESS_CHECK_RESPONSE.SUCCESS.result,
};
await mount(
<TestApp>
<AddressVerificationStepSignature { ...props }/>
</TestApp>,
);
const signatureInput = page.getByLabel(/signature hash/i);
await signatureInput.fill(mocks.SIGNATURE);
await page.getByRole('button', { name: /verify/i }).click();
await expect(page).toHaveScreenshot();
});
......@@ -28,10 +28,11 @@ type Fields = RootFields & AddressVerificationFormSecondStepFields;
interface Props extends AddressVerificationFormFirstStepFields, AddressCheckStatusSuccess{
onContinue: (newItem: VerifiedAddress) => void;
noWeb3Provider?: boolean;
}
const AddressVerificationStepSignature = ({ address, signingMessage, contractCreator, contractOwner, onContinue }: Props) => {
const [ signMethod, setSignMethod ] = React.useState<'wallet' | 'manually'>('wallet');
const AddressVerificationStepSignature = ({ address, signingMessage, contractCreator, contractOwner, onContinue, noWeb3Provider }: Props) => {
const [ signMethod, setSignMethod ] = React.useState<'wallet' | 'manually'>(noWeb3Provider ? 'manually' : 'wallet');
const { open: openWeb3Modal } = useWeb3Modal();
const { isConnected } = useAccount();
......@@ -208,13 +209,15 @@ const AddressVerificationStepSignature = ({ address, signingMessage, contractCre
<CopyToClipboard text={ signingMessage } ml="auto" display="block"/>
<AddressVerificationFieldMessage formState={ formState } control={ control }/>
</div>
{ !noWeb3Provider && (
<RadioGroup onChange={ handleSignMethodChange } value={ signMethod } display="flex" flexDir="column" rowGap={ 4 }>
<Radio value="wallet">Sign via Web3 wallet</Radio>
<Radio value="manually">Sign manually</Radio>
</RadioGroup>
) }
{ signMethod === 'manually' && <AddressVerificationFieldSignature formState={ formState } control={ control }/> }
</Flex>
<Flex alignItems="center" mt={ 8 } columnGap={ 5 }>
<Flex alignItems={{ base: 'flex-start', lg: 'center' }} mt={ 8 } columnGap={ 5 } rowGap={ 2 } flexDir={{ base: 'column', lg: 'row' }}>
{ button }
<AdminSupportText/>
</Flex>
......
......@@ -43,7 +43,6 @@ export type AddressValidationResponseSuccess = {
status: 'INVALID_SIGNER_ERROR';
invalidSigner: {
signer: string;
validAddresses: Array<string>;
};
} |
{ status: 'VALIDITY_EXPIRED_ERROR' } |
......
import { test, expect } from '@playwright/experimental-ct-react';
import React from 'react';
import * as mocks from 'mocks/account/verifiedAddresses';
import TestApp from 'playwright/TestApp';
import buildApiUrl from 'playwright/utils/buildApiUrl';
import VerifiedAddresses from './VerifiedAddresses';
const VERIFIED_ADDRESS_URL = buildApiUrl('verified_addresses', { chainId: '99' }, true);
const TOKEN_INFO_APPLICATIONS_URL = buildApiUrl('token_info_applications', { chainId: '99', id: undefined }, true);
test.beforeEach(async({ context }) => {
await context.route(mocks.TOKEN_INFO_APPLICATION_BASE.iconUrl, (route) => {
return route.fulfill({
status: 200,
path: './playwright/image_s.jpg',
});
});
});
test('base view +@mobile', async({ mount, page }) => {
await page.route(VERIFIED_ADDRESS_URL, (route) => route.fulfill({
body: JSON.stringify(mocks.VERIFIED_ADDRESS_RESPONSE.DEFAULT),
}));
await page.route(TOKEN_INFO_APPLICATIONS_URL, (route) => route.fulfill({
body: JSON.stringify(mocks.TOKEN_INFO_APPLICATIONS_RESPONSE.DEFAULT),
}));
const component = await mount(
<TestApp>
<VerifiedAddresses/>
</TestApp>,
);
await expect(component).toHaveScreenshot();
});
test('address verification flow', async({ mount, page }) => {
const CHECK_ADDRESS_URL = buildApiUrl('address_verification', { chainId: '99', type: ':prepare' }, true);
const VERIFY_ADDRESS_URL = buildApiUrl('address_verification', { chainId: '99', type: ':verify' }, true);
await page.route(VERIFIED_ADDRESS_URL, (route) => route.fulfill({
body: JSON.stringify(mocks.VERIFIED_ADDRESS_RESPONSE.DEFAULT),
}));
await page.route(TOKEN_INFO_APPLICATIONS_URL, (route) => route.fulfill({
body: JSON.stringify(mocks.TOKEN_INFO_APPLICATIONS_RESPONSE.DEFAULT),
}));
await page.route(CHECK_ADDRESS_URL, (route) => route.fulfill({
body: JSON.stringify(mocks.ADDRESS_CHECK_RESPONSE.SUCCESS),
}));
await page.route(VERIFY_ADDRESS_URL, (route) => {
return route.fulfill({
body: JSON.stringify(mocks.ADDRESS_VERIFY_RESPONSE.SUCCESS),
});
});
await mount(
<TestApp>
<VerifiedAddresses/>
</TestApp>,
);
// open modal
await page.getByRole('button', { name: /add address/i }).click();
// fill first step
const addressInput = page.getByLabel(/smart contract address/i);
await addressInput.fill(mocks.VERIFIED_ADDRESS.NEW_ITEM.contractAddress);
await page.getByRole('button', { name: /continue/i }).click();
// fill second step
const option = page.getByText(/sign manually/i);
option.click();
const signatureInput = page.getByLabel(/signature hash/i);
await signatureInput.fill(mocks.SIGNATURE);
await page.getByRole('button', { name: /verify/i }).click();
// success screen
await page.getByRole('button', { name: /view my verified addresses/i }).click();
await expect(page).toHaveScreenshot();
});
test('application update flow', async({ mount, page }) => {
const TOKEN_INFO_APPLICATION_URL = buildApiUrl('token_info_applications', { chainId: '99', id: mocks.TOKEN_INFO_APPLICATION.UPDATED_ITEM.id }, true);
const FORM_CONFIG_URL = buildApiUrl('token_info_applications_config', { chainId: '99' }, true);
await page.route(VERIFIED_ADDRESS_URL, (route) => route.fulfill({
body: JSON.stringify(mocks.VERIFIED_ADDRESS_RESPONSE.DEFAULT),
}));
await page.route(TOKEN_INFO_APPLICATIONS_URL, (route) => route.fulfill({
body: JSON.stringify(mocks.TOKEN_INFO_APPLICATIONS_RESPONSE.FOR_UPDATE),
}));
await page.route(FORM_CONFIG_URL, (route) => route.fulfill({
body: JSON.stringify(mocks.TOKEN_INFO_FORM_CONFIG),
}));
// PUT request
await page.route(TOKEN_INFO_APPLICATION_URL, (route) => route.fulfill({
body: JSON.stringify(mocks.TOKEN_INFO_APPLICATION.UPDATED_ITEM),
}));
await mount(
<TestApp>
<VerifiedAddresses/>
</TestApp>,
);
// open form
await page.locator('tr').filter({ hasText: 'waiting for update' }).locator('button[aria-label="edit"]').click();
// change project name
const addressInput = page.getByLabel(/project name/i);
await addressInput.fill('New name');
await page.getByRole('button', { name: /send request/i }).click();
const locator = page.locator('tr').filter({ hasText: 'in progress' }).filter({ hasText: 'nov 11, 2022' });
await expect(locator).toBeVisible();
});
......@@ -12,7 +12,6 @@ import getQueryParamString from 'lib/router/getQueryParamString';
import AddressVerificationModal from 'ui/addressVerification/AddressVerificationModal';
import AccountPageDescription from 'ui/shared/AccountPageDescription';
import DataListDisplay from 'ui/shared/DataListDisplay';
import Page from 'ui/shared/Page/Page';
import PageTitle from 'ui/shared/Page/PageTitle';
import SkeletonListAccount from 'ui/shared/skeletons/SkeletonListAccount';
import SkeletonTable from 'ui/shared/skeletons/SkeletonTable';
......@@ -86,9 +85,9 @@ const VerifiedAddresses = () => {
return { submissions: [ newItem ] };
}
const isExisting = prevData.submissions.some((item) => item.id === newItem.id);
const isExisting = prevData.submissions.some((item) => item.id.toLowerCase() === newItem.id.toLowerCase());
const submissions = isExisting ?
prevData.submissions.map((item) => item.id === newItem.id ? newItem : item) :
prevData.submissions.map((item) => item.id.toLowerCase() === newItem.id.toLowerCase() ? newItem : item) :
[ newItem, ...prevData.submissions ];
return { submissions };
});
......@@ -128,14 +127,14 @@ const VerifiedAddresses = () => {
if (selectedAddress) {
return (
<Page>
<>
<PageTitle text="Token info application form" backLink={ backLink }/>
<TokenInfoForm
address={ selectedAddress }
application={ applicationsQuery.data?.submissions.find(({ tokenAddress }) => tokenAddress === selectedAddress) }
application={ applicationsQuery.data?.submissions.find(({ tokenAddress }) => tokenAddress.toLowerCase() === selectedAddress.toLowerCase()) }
onSubmit={ handleApplicationSubmit }
/>
</Page>
</>
);
}
......@@ -146,7 +145,7 @@ const VerifiedAddresses = () => {
<VerifiedAddressesListItem
key={ item.contractAddress }
item={ item }
application={ applicationsQuery.data?.submissions?.find(({ tokenAddress }) => tokenAddress === item.contractAddress) }
application={ applicationsQuery.data?.submissions?.find(({ tokenAddress }) => tokenAddress.toLowerCase() === item.contractAddress.toLowerCase()) }
onAdd={ handleItemAdd }
onEdit={ handleItemEdit }
/>
......@@ -164,7 +163,7 @@ const VerifiedAddresses = () => {
) : null;
return (
<Page>
<>
<PageTitle text="My verified addresses"/>
<AccountPageDescription allowCut={ false }>
<span>
......@@ -205,7 +204,7 @@ const VerifiedAddresses = () => {
onAddTokenInfoClick={ handleItemAdd }
onShowListClick={ modalProps.onClose }
/>
</Page>
</>
);
};
......
......@@ -45,7 +45,7 @@ const TokenInfoMenuItem = ({ className, hash }: Props) => {
}, [ router ]);
const content = (() => {
if (!verifiedAddressesQuery.data?.verifiedAddresses.find(({ contractAddress }) => contractAddress === hash)) {
if (!verifiedAddressesQuery.data?.verifiedAddresses.find(({ contractAddress }) => contractAddress.toLowerCase() === hash.toLowerCase())) {
return (
<MenuItem className={ className } onClick={ handleAddAddressClick }>
Add token info
......@@ -55,7 +55,11 @@ const TokenInfoMenuItem = ({ className, hash }: Props) => {
return (
<MenuItem className={ className } onClick={ handleAddApplicationClick }>
{ applicationsQuery.data?.submissions.some(({ tokenAddress }) => tokenAddress === hash) ? 'Update token info' : 'Add token info' }
{
applicationsQuery.data?.submissions.some(({ tokenAddress }) => tokenAddress.toLowerCase() === hash.toLowerCase()) ?
'Update token info' :
'Add token info'
}
</MenuItem>
);
})();
......
......@@ -55,7 +55,7 @@ const { wagmiClient, ethereumClient } = (() => {
interface Props {
children: React.ReactNode;
fallback?: () => JSX.Element;
fallback?: JSX.Element | (() => JSX.Element);
}
const Web3ModalProvider = ({ children, fallback }: Props) => {
......@@ -63,7 +63,7 @@ const Web3ModalProvider = ({ children, fallback }: Props) => {
const web3ModalTheme = useColorModeValue('light', 'dark');
if (!wagmiClient || !ethereumClient) {
return fallback?.() || null;
return typeof fallback === 'function' ? fallback() : (fallback || null);
}
return (
......
......@@ -29,7 +29,7 @@ const VerifiedAddressesTable = ({ data, applications, onItemEdit, onItemAdd }: P
<VerifiedAddressesTableItem
key={ item.contractAddress }
item={ item }
application={ applications?.find(({ tokenAddress }) => tokenAddress === item.contractAddress) }
application={ applications?.find(({ tokenAddress }) => tokenAddress.toLowerCase() === item.contractAddress.toLowerCase()) }
onAdd={ onItemAdd }
onEdit={ onItemEdit }
/>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment