Commit c4f338b2 authored by tom goriunov's avatar tom goriunov Committed by GitHub

Merge pull request #351 from blockscout/pw-tests

tests for tx logs tab
parents 3e5c96fa 6da786db
......@@ -195,7 +195,7 @@ module.exports = {
groups: [
'module',
'/types/',
[ '/^configs/', '/^data/', '/^deploy/', '/^icons/', '/^lib/', '/^pages/', '/^playwright/', '/^theme/', '/^ui/' ],
[ '/^configs/', '/^data/', '/^deploy/', '/^icons/', '/^lib/', '/^mocks/', '/^pages/', '/^playwright/', '/^theme/', '/^ui/' ],
[ 'parent', 'sibling', 'index' ],
],
alphabetize: { order: 'asc', ignoreCase: true },
......
......@@ -102,15 +102,56 @@
"instanceLimit": 1
}
},
// PW TESTS
{
"type": "npm",
"script": "test:pw:docker",
"type": "shell",
"command": "${input:pwDebugFlag} yarn test:pw:local ${relativeFileDirname}/${fileBasename} ${input:pwArgs}",
"problemMatcher": [],
"label": "test: playwright: local for current file",
"detail": "run visual components tests for current file",
"presentation": {
"reveal": "always",
"panel": "new",
"focus": true,
},
"icon": {
"color": "terminal.ansiBlue",
"id": "beaker"
},
"runOptions": {
"instanceLimit": 1
},
},
{
"type": "shell",
"command": "yarn test:pw:docker ${relativeFileDirname}/${fileBasename} ${input:pwArgs}",
"problemMatcher": [],
"label": "test: playwright",
"label": "test: playwright: docker for current file",
"detail": "run visual components tests for current file",
"presentation": {
"reveal": "always",
"panel": "new",
"focus": true,
},
"icon": {
"color": "terminal.ansiBlue",
"id": "beaker"
},
"runOptions": {
"instanceLimit": 1
},
},
{
"type": "shell",
"command": "yarn test:pw:docker ${input:pwArgs}",
"problemMatcher": [],
"label": "test: playwright: docker for all files",
"detail": "run visual components tests",
"presentation": {
"reveal": "always",
"panel": "new",
"focus": true,
},
"icon": {
"color": "terminal.ansiBlue",
......@@ -120,6 +161,8 @@
"instanceLimit": 1
}
},
// JEST TESTS
{
"type": "npm",
"script": "test:jest",
......@@ -129,6 +172,7 @@
"presentation": {
"reveal": "always",
"panel": "new",
"focus": true,
},
"icon": {
"color": "terminal.ansiBlue",
......@@ -148,6 +192,7 @@
"reveal": "always",
"panel": "new",
"close": true,
"focus": true,
},
"icon": {
"color": "terminal.ansiBlue",
......@@ -157,6 +202,26 @@
"instanceLimit": 1
}
},
{
"type": "shell",
"command": "yarn test:jest ${relativeFileDirname}/${fileBasename} --watch",
"problemMatcher": [],
"label": "test: jest: watch curent file",
"detail": "run jest tests in watch mode for current file",
"presentation": {
"reveal": "always",
"panel": "new",
"focus": true,
},
"icon": {
"color": "terminal.ansiBlue",
"id": "beaker"
},
"runOptions": {
"instanceLimit": 1
},
},
{
"type": "npm",
"script": "build:docker",
......@@ -168,6 +233,7 @@
"panel": "new",
"close": true,
"revealProblems": "onProblem",
"focus": true,
},
"icon": {
"color": "terminal.ansiRed",
......@@ -217,5 +283,29 @@
"instanceLimit": 1
}
},
]
],
"inputs": [
{
"type": "pickString",
"id": "pwDebugFlag",
"description": "What debug flag you want to use?",
"options": [
"",
"PWDEBUG=1",
"DEBUG=pw:browser,pw:api",
"DEBUG=*",
],
"default": ""
},
{
"type": "pickString",
"id": "pwArgs",
"description": "What args you want to pass?",
"options": [
"",
"--update-snapshots",
],
"default": ""
},
],
}
\ No newline at end of file
......@@ -95,7 +95,9 @@ const config: JestConfigWithTsJest = {
// moduleNameMapper: {},
// An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader
// modulePathIgnorePatterns: [],
modulePathIgnorePatterns: [
'node_modules_linux',
],
// Activates notifications for test results
// notify: false,
......
......@@ -5,10 +5,8 @@ import { useRouter } from 'next/router';
import React, { useCallback } from 'react';
import { animateScroll } from 'react-scroll';
import type { BlockFilters } from 'types/api/block';
import { PAGINATION_FIELDS } from 'types/api/pagination';
import type { PaginationParams, PaginatedResponse, PaginatedQueryKeys } from 'types/api/pagination';
import type { TTxsFilters } from 'types/api/txsFilters';
import type { PaginationParams, PaginatedResponse, PaginatedQueryKeys, PaginationFilters } from 'types/api/pagination';
import useFetch from 'lib/hooks/useFetch';
......@@ -16,7 +14,7 @@ interface Params<QueryName extends PaginatedQueryKeys> {
apiPath: string;
queryName: QueryName;
queryIds?: Array<string>;
filters?: TTxsFilters | BlockFilters;
filters?: PaginationFilters<QueryName>;
options?: Omit<UseQueryOptions<unknown, unknown, PaginatedResponse<QueryName>>, 'queryKey' | 'queryFn'>;
}
......
import type { AddressParam } from 'types/api/addressParams';
export const withName: AddressParam = {
hash: '0xd789a607CEac2f0E14867de4EB15b15C9FFB5859',
implementation_name: null,
is_contract: true,
is_verified: null,
name: 'ArianeeStore',
private_tags: [],
watchlist_names: [],
public_tags: [],
};
export const withoutName: AddressParam = {
hash: '0xd789a607CEac2f0E14867de4EB15b15C9FFB5859',
implementation_name: null,
is_contract: true,
is_verified: null,
name: null,
private_tags: [],
watchlist_names: [],
public_tags: [],
};
import type { AddressTag, WatchlistName } from 'types/api/addressParams';
export const privateTag: AddressTag = {
label: 'my-private-tag',
display_name: 'my private tag',
address_hash: '0x',
};
export const publicTag: AddressTag = {
label: 'some-public-tag',
display_name: 'some public tag',
address_hash: '0x',
};
export const watchlistName: WatchlistName = {
label: 'watchlist-name',
display_name: 'watchlist name',
};
import type { Block, BlocksResponse } from 'types/api/block';
export const base: Block = {
base_fee_per_gas: '10000000000',
burnt_fees: '5449200000000000',
burnt_fees_percentage: 20.292245650793845,
difficulty: '340282366920938463463374607431768211454',
extra_data: 'TODO',
gas_limit: '12500000',
gas_target_percentage: -91.28128,
gas_used: '544920',
gas_used_percentage: 4.35936,
hash: '0xccc75136de485434d578b73df66537c06b34c3c9b12d085daf95890c914fc2bc',
height: 30146364,
miner: {
hash: '0xdAd49e6CbDE849353ab27DeC6319E687BFc91A41',
implementation_name: null,
is_contract: false,
is_verified: null,
name: 'Alex Emelyanov',
private_tags: [],
public_tags: [],
watchlist_names: [],
},
nonce: '0x0000000000000000',
parent_hash: '0x44125f0eb36a9d942e0c23bb4e8117f7ba86a9537a69b59c0025986ed2b7500f',
priority_fee: '23211757500000000',
rewards: [
{
reward: '500000000000000000',
type: 'POA Mania Reward',
},
{
reward: '1026853607500000000',
type: 'Validator Reward',
},
{
reward: '500000000000000000',
type: 'Emission Reward',
},
],
size: 2448,
state_root: 'TODO',
timestamp: '2022-11-11T11:59:35Z',
total_difficulty: '10258276095980170141167591583995189665817672619',
tx_count: 5,
tx_fees: '26853607500000000',
type: 'block',
uncles_hashes: [],
};
export const genesis = {
base_fee_per_gas: null,
burnt_fees: null,
burnt_fees_percentage: null,
difficulty: '131072',
extra_data: 'TODO',
gas_limit: '6700000',
gas_target_percentage: -100,
gas_used: '0',
gas_used_percentage: 0,
hash: '0x39f02c003dde5b073b3f6e1700fc0b84b4877f6839bb23edadd3d2d82a488634',
height: 0,
miner: {
hash: '0x0000000000000000000000000000000000000000',
implementation_name: null,
is_contract: false,
is_verified: null,
name: null,
private_tags: [],
public_tags: [],
watchlist_names: [],
},
nonce: '0x0000000000000000',
parent_hash: '0x0000000000000000000000000000000000000000000000000000000000000000',
priority_fee: null,
rewards: [],
size: 533,
state_root: 'TODO',
timestamp: '2017-12-16T00:13:24.000000Z',
total_difficulty: '131072',
tx_count: 0,
tx_fees: '0',
type: 'block',
uncles_hashes: [],
};
export const base2: Block = {
...base,
height: base.height - 1,
size: 592,
miner: {
hash: '0xDfE10D55d9248B2ED66f1647df0b0A46dEb25165',
implementation_name: null,
is_contract: false,
is_verified: null,
name: 'Kiryl Ihnatsyeu',
private_tags: [],
public_tags: [],
watchlist_names: [],
},
timestamp: '2022-11-11T11:46:05Z',
tx_count: 253,
gas_target_percentage: 23.6433,
gas_used: '6333342',
gas_used_percentage: 87.859504,
burnt_fees: '232438000000000000',
burnt_fees_percentage: 65.3333333333334,
rewards: [
{
reward: '500000000000000000',
type: 'Chore Reward',
},
{
reward: '1017432850000000000',
type: 'Miner Reward',
},
{
reward: '500000000000000000',
type: 'Emission Reward',
},
],
};
export const baseListResponse: BlocksResponse = {
items: [
base,
base2,
],
next_page_params: null,
};
import type { TokenTransfer, TokenTransferResponse } from 'types/api/tokenTransfer';
export const erc20: TokenTransfer = {
from: {
hash: '0xd789a607CEac2f0E14867de4EB15b15C9FFB5859',
implementation_name: null,
is_contract: true,
is_verified: true,
name: 'ArianeeStore',
private_tags: [],
public_tags: [],
watchlist_names: [],
},
to: {
hash: '0x7d20a8D54F955b4483A66aB335635ab66e151c51',
implementation_name: null,
is_contract: true,
is_verified: false,
name: null,
private_tags: [],
public_tags: [],
watchlist_names: [],
},
token: {
address: '0x55d536e4d6c1993d8ef2e2a4ef77f02088419420',
decimals: '18',
exchange_rate: null,
holders: '46554',
name: 'ARIANEE',
symbol: 'ARIA',
type: 'ERC-20',
},
total: {
decimals: '18',
value: '31567373703130350',
},
tx_hash: '0x62d597ebcf3e8d60096dd0363bc2f0f5e2df27ba1dacd696c51aa7c9409f3193',
type: 'token_transfer',
};
export const erc721: TokenTransfer = {
from: {
hash: '0x621C2a125ec4A6D8A7C7A655A18a2868d35eb43C',
implementation_name: null,
is_contract: false,
is_verified: false,
name: null,
private_tags: [],
public_tags: [],
watchlist_names: [],
},
to: {
hash: '0x47eE48AEBc4ab9Ed908b805b8c8dAAa71B31Db1A',
implementation_name: null,
is_contract: false,
is_verified: false,
name: null,
private_tags: [],
public_tags: [],
watchlist_names: [],
},
token: {
address: '0x363574E6C5C71c343d7348093D84320c76d5Dd29',
decimals: null,
exchange_rate: null,
holders: '63090',
name: 'Arianee Smart-Asset',
symbol: 'AriaSA',
type: 'ERC-721',
},
total: {
token_id: '875879856',
},
tx_hash: '0xf13bc7afe5e02b494dd2f22078381d36a4800ef94a0ccc147431db56c301e6cc',
type: 'token_transfer',
};
export const erc1155: TokenTransfer = {
from: {
hash: '0x0000000000000000000000000000000000000000',
implementation_name: null,
is_contract: false,
is_verified: false,
name: null,
private_tags: [],
public_tags: [],
watchlist_names: [],
},
to: {
hash: '0xBb36c792B9B45Aaf8b848A1392B0d6559202729E',
implementation_name: null,
is_contract: false,
is_verified: false,
name: null,
private_tags: [],
public_tags: [],
watchlist_names: [],
},
token: {
address: '0xF56b7693E4212C584de4a83117f805B8E89224CB',
decimals: null,
exchange_rate: null,
holders: '1',
name: null,
symbol: null,
type: 'ERC-1155',
},
total: {
token_id: '123',
value: '42',
decimals: null,
},
tx_hash: '0x05d6589367633c032d757a69c5fb16c0e33e3994b0d9d1483f82aeee1f05d746',
type: 'token_minting',
};
export const erc1155multiple: TokenTransfer = {
...erc1155,
token: {
...erc1155.token,
name: 'OLYMPIC',
},
total: [
{ token_id: '456', value: '42', decimals: null },
{ token_id: '12345678', value: '142', decimals: null },
{ token_id: '1000006457499', value: '11', decimals: null },
],
};
export const mixTokens: TokenTransferResponse = {
items: [
erc20,
erc721,
erc1155,
erc1155multiple,
],
next_page_params: null,
};
import type { DecodedInput } from 'types/api/decodedInput';
export const withoutIndexedFields: DecodedInput = {
method_call: 'CreditSpended(uint256 _type, uint256 _quantity)',
method_id: '58cdf94a',
parameters: [
{
name: '_type',
type: 'uint256',
value: '3',
},
{
name: '_quantity',
type: 'uint256',
value: '1',
},
],
};
export const withIndexedFields: DecodedInput = {
method_call: 'Transfer(address indexed from, address indexed to, uint256 value)',
method_id: 'ddf252ad',
parameters: [
{
indexed: true,
name: 'from',
type: 'address',
value: '0xd789a607ceac2f0e14867de4eb15b15c9ffb5859',
},
{
indexed: true,
name: 'to',
type: 'address',
value: '0x7d20a8d54f955b4483a66ab335635ab66e151c51',
},
{
indexed: false,
name: 'value',
type: 'uint256',
value: '31567373703130350',
},
],
};
import type { InternalTransaction, InternalTransactionsResponse } from 'types/api/internalTransaction';
export const base: InternalTransaction = {
block: 29611822,
created_contract: null,
error: null,
from: {
hash: '0xd789a607CEac2f0E14867de4EB15b15C9FFB5859',
implementation_name: null,
is_contract: true,
is_verified: true,
name: 'ArianeeStore',
private_tags: [],
public_tags: [],
watchlist_names: [],
},
gas_limit: '757586',
index: 1,
success: true,
timestamp: '2022-10-10T14:43:05.000000Z',
to: {
hash: '0x502a9C8af2441a1E276909405119FaE21F3dC421',
implementation_name: null,
is_contract: true,
is_verified: true,
name: 'ArianeeCreditHistory',
private_tags: [],
public_tags: [],
watchlist_names: [],
},
transaction_hash: '0xe9e27dfeb183066e26cfe556f74b7219b08df6951e25d14003d4fc7af8bbff61',
type: 'call',
value: '42000000000000000000',
};
export const typeStaticCall: InternalTransaction = {
...base,
type: 'staticcall',
to: {
...base.to,
name: null,
},
gas_limit: '63424243',
};
export const withContractCreated: InternalTransaction = {
...base,
type: 'delegatecall',
to: null,
from: {
...base.from,
name: null,
},
created_contract: {
hash: '0xdda21946FF3FAa027104b15BE6970CA756439F5a',
implementation_name: null,
is_contract: true,
is_verified: null,
name: 'Shavuha token',
private_tags: [],
public_tags: [],
watchlist_names: [],
},
value: '1420000000000000000',
gas_limit: '5433',
};
export const baseResponse: InternalTransactionsResponse = {
items: [
base,
typeStaticCall,
withContractCreated,
],
next_page_params: null,
};
/* eslint-disable max-len */
import type { Transaction } from 'types/api/transaction';
import { publicTag, privateTag, watchlistName } from 'mocks/address/tag';
import * as tokenTransferMock from 'mocks/tokens/tokenTransfer';
import * as decodedInputDataMock from 'mocks/txs/decodedInputData';
export const base: Transaction = {
base_fee_per_gas: '10000000000',
block: 29611750,
confirmation_duration: [
0,
6364,
],
confirmations: 508299,
created_contract: null,
decoded_input: decodedInputDataMock.withoutIndexedFields,
exchange_rate: '0.00254428',
fee: {
type: 'actual',
value: '7143168000000000',
},
from: {
hash: '0x047A81aFB05D9B1f8844bf60fcA05DCCFbC584B9',
implementation_name: null,
is_contract: false,
name: null,
is_verified: null,
private_tags: [ ],
public_tags: [ publicTag ],
watchlist_names: [],
},
gas_limit: '800000',
gas_price: '48000000000',
gas_used: '148816',
hash: '0x62d597ebcf3e8d60096dd0363bc2f0f5e2df27ba1dacd696c51aa7c9409f3193',
max_fee_per_gas: '40190625000',
max_priority_fee_per_gas: '28190625000',
method: 'updateSmartAsset',
nonce: 27831,
position: 7,
priority_fee: '1299672384375000',
raw_input: '0xfa4b78b90000000000000000000000000000000000000000000000000000000005001bcfe835d1028984e9e6e7d016b77164eacbcc6cc061e9333c0b37982b504f7ea791000000000000000000000000a79b29ad7e0196c95b87f4663ded82fbf2e3add8',
result: 'success',
revert_reason: null,
status: 'ok',
timestamp: '2022-10-10T14:34:30.000000Z',
to: {
hash: '0xd789a607CEac2f0E14867de4EB15b15C9FFB5859',
implementation_name: null,
is_contract: false,
is_verified: true,
name: null,
private_tags: [ privateTag ],
public_tags: [],
watchlist_names: [ watchlistName ],
},
token_transfers: [],
token_transfers_overflow: false,
tx_burnt_fee: '461030000000000',
tx_tag: null,
tx_types: [
'contract_call',
'token_transfer',
],
type: 2,
value: '42000000000000000000',
};
export const withContractCreation: Transaction = {
...base,
to: null,
created_contract: {
hash: '0xdda21946FF3FAa027104b15BE6970CA756439F5a',
implementation_name: null,
is_contract: true,
is_verified: null,
name: 'Shavuha token',
private_tags: [],
public_tags: [],
watchlist_names: [],
},
};
export const withTokenTransfer: Transaction = {
...base,
to: {
hash: '0xd789a607CEac2f0E14867de4EB15b15C9FFB5859',
implementation_name: null,
is_contract: true,
is_verified: true,
name: 'ArianeeStore',
private_tags: [ privateTag ],
public_tags: [],
watchlist_names: [ watchlistName ],
},
token_transfers: [
tokenTransferMock.erc20,
tokenTransferMock.erc721,
tokenTransferMock.erc1155,
tokenTransferMock.erc1155multiple,
],
};
export const withDecodedRevertReason: Transaction = {
...base,
status: 'error',
result: 'Reverted',
revert_reason: {
method_call: 'SomeCustomError(address addr, uint256 balance)',
method_id: '50289a9f',
parameters: [
{
name: 'addr',
type: 'address',
value: '0xf26594f585de4eb0ae9de865d9053fee02ac6ef1',
},
{
name: 'balance',
type: 'uint256',
value: '123',
},
],
},
};
export const withRawRevertReason: Transaction = {
...base,
status: 'error',
result: 'Reverted',
revert_reason: {
raw: '4f6e6c79206368616972706572736f6e2063616e206769766520726967687420746f20766f74652e',
},
to: {
hash: '0xd789a607CEac2f0E14867de4EB15b15C9FFB5859',
implementation_name: null,
is_verified: true,
is_contract: true,
name: 'Bad guy',
private_tags: [ ],
public_tags: [],
watchlist_names: [ ],
},
};
export const pending: Transaction = {
...base,
base_fee_per_gas: null,
block: null,
confirmation_duration: [],
confirmations: 0,
decoded_input: null,
gas_used: null,
max_fee_per_gas: null,
max_priority_fee_per_gas: null,
method: null,
position: null,
priority_fee: null,
result: 'pending',
revert_reason: null,
status: null,
timestamp: null,
tx_burnt_fee: null,
tx_tag: null,
type: null,
value: '0',
};
import handler from 'lib/api/handler';
const getUrl = () => '/v2/stats';
const requestHandler = handler(getUrl, [ 'GET' ]);
export default requestHandler;
// todo_tom leave only one api endpoint
import handler from 'lib/api/handler';
const getUrl = () => '/v2/stats';
......
......@@ -12,7 +12,7 @@ const config: PlaywrightTestConfig = {
testMatch: /.*\.pw\.tsx/,
snapshotPathTemplate: '{testDir}/{testFileDir}/__screenshots__/{testFileName}_{arg}{ext}',
snapshotPathTemplate: '{testDir}/{testFileDir}/__screenshots__/{testFileName}_{projectName}_{arg}{ext}',
/* Maximum time one test can run for. */
timeout: 10 * 1000,
......@@ -57,8 +57,36 @@ const config: PlaywrightTestConfig = {
/* Configure projects for major browsers */
projects: [
{
name: 'chromium',
use: devices['Desktop Chrome'],
name: 'default',
grepInvert: /-@default/,
use: {
...devices['Desktop Chrome'],
viewport: { width: 1200, height: 750 },
},
},
{
name: 'mobile',
grep: /\+@mobile/,
use: {
...devices['iPhone 13 Pro'],
},
},
{
name: 'desktop xl',
grep: /\+@desktop-xl/,
use: {
...devices['Desktop Chrome'],
viewport: { width: 1600, height: 1000 },
},
},
{
name: 'dark color mode',
grep: /\+@dark-mode/,
use: {
...devices['Desktop Chrome'],
viewport: { width: 1200, height: 750 },
colorScheme: 'dark',
},
},
],
};
......
import { ChakraProvider } from '@chakra-ui/react';
import type { ColorMode } from '@chakra-ui/react';
import React from 'react';
import theme from 'theme';
type Props = {
children: React.ReactNode;
colorMode?: ColorMode;
}
const RenderWithChakra = ({ children, colorMode = 'light' }: Props) => {
return (
<ChakraProvider theme={{ ...theme, config: { ...theme.config, initialColorMode: colorMode } }}>
{ children }
</ChakraProvider>
);
};
export default RenderWithChakra;
import { ChakraProvider } from '@chakra-ui/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import React from 'react';
import theme from 'theme';
type Props = {
children: React.ReactNode;
}
const TestApp = ({ children }: Props) => {
const [ queryClient ] = React.useState(() => new QueryClient({
defaultOptions: {
queries: {
refetchOnWindowFocus: false,
retry: 0,
},
},
}));
return (
<ChakraProvider theme={ theme }>
<QueryClientProvider client={ queryClient }>
{ children }
</QueryClientProvider>
</ChakraProvider>
);
};
export default TestApp;
This source diff could not be displayed because it is too large. You can view the blob instead.
......@@ -4,38 +4,6 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Testing Page</title>
<style>
@font-face {
font-family: Poppins;
src: url(/playwright/fonts/Poppins/Poppins-SemiBold.ttf);
font-weight: 600;
}
@font-face {
font-family: Poppins;
src: url(/playwright/fonts/Poppins/Poppins-Medium.ttf);
font-weight: 500;
}
@font-face {
font-family: Poppins;
src: url(/playwright/fonts/Poppins/Poppins-Regular.ttf);
font-weight: 400;
}
@font-face {
font-family: Inter;
src: url(/playwright/fonts/Inter/Inter-SemiBold.ttf);
font-weight: 600;
}
@font-face {
font-family: Inter;
src: url(/playwright/fonts/Inter/Inter-Medium.ttf);
font-weight: 500;
}
@font-face {
font-family: Inter;
src: url(/playwright/fonts/Inter/Inter-Regular.ttf);
font-weight: 400;
}
</style>
</head>
<body>
<div id="root"></div>
......
// Import styles, initialize component theme here.
// import '../src/common.css';
import './fonts.css';
import { beforeMount } from '@playwright/experimental-ct-react/hooks';
import MockDate from 'mockdate';
import * as router from 'next/router';
const NEXT_ROUTER_MOCK = {
query: {},
};
beforeMount(async({ hooksConfig }) => {
// Before mount, redefine useRouter to return mock value from test.
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: I really want to redefine this property :)
router.useRouter = () => hooksConfig?.router || NEXT_ROUTER_MOCK;
// set current date
MockDate.set('2022-11-11T12:00:00Z');
});
export {};
......@@ -2,4 +2,4 @@
yarn install --modules-folder node_modules_linux
export NODE_PATH=$(pwd)/node_modules_linux
rm -rf ./playwright/.cache
yarn test:pw
\ No newline at end of file
yarn test:pw "$@"
\ No newline at end of file
......@@ -11,9 +11,10 @@ export interface WatchlistName {
export interface AddressParam {
hash: string;
implementation_name: string;
implementation_name: string | null;
name: string | null;
is_contract: boolean;
is_verified: boolean | null;
private_tags: Array<AddressTag> | null;
watchlist_names: Array<WatchlistName> | null;
public_tags: Array<AddressTag> | null;
......
......@@ -16,10 +16,10 @@ export interface Block {
total_difficulty: string;
gas_used: string | null;
gas_limit: string;
nonce: number;
base_fee_per_gas: number | null;
burnt_fees: number | null;
priority_fee: number | null;
nonce: string;
base_fee_per_gas: string | null;
burnt_fees: string | null;
priority_fee: string | null;
extra_data: string | null;
state_root: string | null;
rewards?: Array<Reward>;
......
......@@ -2,14 +2,21 @@ import type { AddressParam } from './addressParams';
export type TxInternalsType = 'call' | 'delegatecall' | 'staticcall' | 'create' | 'create2' | 'selfdestruct' | 'reward'
export interface InternalTransaction {
export type InternalTransaction = (
{
to: AddressParam;
created_contract: null;
} |
{
to: null;
created_contract: AddressParam;
}
) & {
error: string | null;
success: boolean;
type: TxInternalsType;
transaction_hash: string;
from: AddressParam;
to: AddressParam;
created_contract: AddressParam;
value: string;
index: number;
block: number;
......@@ -25,5 +32,5 @@ export interface InternalTransactionsResponse {
items_count: number;
transaction_hash: string;
transaction_index: number;
};
} | null;
}
......@@ -3,7 +3,7 @@ import type { DecodedInput } from './decodedInput';
export interface Log {
address: AddressParam;
topics: Array<string>;
topics: Array<string | null>;
data: string;
index: number;
decoded: DecodedInput | null;
......
import type { BlocksResponse, BlockTransactionsResponse } from 'types/api/block';
import type { BlocksResponse, BlockTransactionsResponse, BlockFilters } from 'types/api/block';
import type { InternalTransactionsResponse } from 'types/api/internalTransaction';
import type { LogsResponse } from 'types/api/log';
import type { TokenTransferResponse } from 'types/api/tokenTransfer';
import type { TokenTransferResponse, TokenTransferFilters } from 'types/api/tokenTransfer';
import type { TransactionsResponseValidated, TransactionsResponsePending } from 'types/api/transaction';
import type { TTxsFilters } from 'types/api/txsFilters';
import { QueryKeys } from 'types/client/queries';
import type { KeysOfObjectOrNull } from 'types/utils/KeysOfObjectOrNull';
......@@ -25,6 +26,13 @@ export type PaginatedResponse<Q extends PaginatedQueryKeys> =
Q extends QueryKeys.txTokenTransfers ? TokenTransferResponse :
never
export type PaginationFilters<Q extends PaginatedQueryKeys> =
Q extends QueryKeys.blocks ? BlockFilters :
Q extends QueryKeys.txsValidate ? TTxsFilters :
Q extends QueryKeys.txsPending ? TTxsFilters :
Q extends QueryKeys.txTokenTransfers ? TokenTransferFilters :
never
export type PaginationParams<Q extends PaginatedQueryKeys> = PaginatedResponse<Q>['next_page_params'];
type PaginationFields = {
......
export interface Reward {
reward: string;
type: 'Miner Reward' | 'Validator Reward' | 'Emission Reward' | 'Chore Reward' | 'Uncle Reward';
type: 'Miner Reward' | 'Validator Reward' | 'Emission Reward' | 'Chore Reward' | 'Uncle Reward' | 'POA Mania Reward';
}
......@@ -10,4 +10,5 @@ export type Stats = {
gas_prices: {average: number; fast: number; slow: number};
static_gas_price: string;
market_cap: string;
network_utilization_percentage: number;
}
import type { AddressParam } from './addressParams';
import type { TokenInfoGeneric } from './tokenInfo';
import type { TokenInfoGeneric, TokenType } from './tokenInfo';
export type Erc20TotalPayload = {
decimals: string | null;
......@@ -47,3 +47,7 @@ export interface TokenTransferResponse {
transaction_hash: string;
} | null;
}
export interface TokenTransferFilters {
type: Array<TokenType>;
}
......@@ -5,10 +5,18 @@ import type { TokenTransfer } from './tokenTransfer';
export type TransactionRevertReason = {
raw: string;
decoded: string;
} | DecodedInput;
export interface Transaction {
export type Transaction = (
{
to: AddressParam;
created_contract: null;
} |
{
to: null;
created_contract: AddressParam;
}
) & {
hash: string;
result: string;
confirmations: number;
......@@ -17,21 +25,19 @@ export interface Transaction {
timestamp: string | null;
confirmation_duration: Array<number>;
from: AddressParam;
to: AddressParam | null;
created_contract: AddressParam;
value: string;
fee: Fee;
gas_price: number;
type: number;
gas_price: string;
type: number | null;
gas_used: string | null;
gas_limit: string;
max_fee_per_gas: number | null;
max_priority_fee_per_gas: number | null;
priority_fee: number | null;
base_fee_per_gas: number | null;
tx_burnt_fee: number | null;
max_fee_per_gas: string | null;
max_priority_fee_per_gas: string | null;
priority_fee: string | null;
base_fee_per_gas: string | null;
tx_burnt_fee: string | null;
nonce: number;
position: number;
position: number | null;
revert_reason: TransactionRevertReason | null;
raw_input: string;
decoded_input: DecodedInput | null;
......
import { test, expect } from '@playwright/experimental-ct-react';
import React from 'react';
import * as blockMock from 'mocks/blocks/block';
import TestApp from 'playwright/TestApp';
import BlockDetails from './BlockDetails';
const API_URL = '/node-api/blocks/1';
const hooksConfig = {
router: {
query: { id: 1 },
},
};
test('regular block +@mobile +@dark-mode', async({ mount, page }) => {
await page.route(API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify(blockMock.base),
}));
const component = await mount(
<TestApp>
<BlockDetails/>
</TestApp>,
{ hooksConfig },
);
await page.waitForResponse(API_URL),
await page.getByText('View details').click();
await expect(component).toHaveScreenshot();
});
test('genesis block', async({ mount, page }) => {
await page.route(API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify(blockMock.genesis),
}));
const component = await mount(
<TestApp>
<BlockDetails/>
</TestApp>,
{ hooksConfig },
);
await page.waitForResponse(API_URL),
await page.getByText('View details').click();
await expect(component).toHaveScreenshot();
});
......@@ -228,7 +228,7 @@ const BlockDetails = () => {
</Tooltip>
) }
</DetailsInfoItem>
{ data.priority_fee !== null && data.priority_fee > 0 && (
{ data.priority_fee !== null && BigNumber(data.priority_fee).gt(ZERO) && (
<DetailsInfoItem
title="Priority fee / Tip"
hint="User-defined tips sent to validator for transaction priority/inclusion."
......
import { test, expect } from '@playwright/experimental-ct-react';
import React from 'react';
import * as blockMock from 'mocks/blocks/block';
import TestApp from 'playwright/TestApp';
import BlocksContent from './BlocksContent';
const API_URL = '/node-api/blocks';
test('base view +@mobile', async({ mount, page }) => {
await page.route(API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify(blockMock.baseListResponse),
}));
const component = await mount(
<TestApp>
<BlocksContent/>
</TestApp>,
);
await page.waitForResponse(API_URL),
await expect(component).toHaveScreenshot();
});
......@@ -5,6 +5,7 @@ import React from 'react';
import type { SocketMessage } from 'lib/socket/types';
import type { Block } from 'types/api/block';
import type { Stats } from 'types/api/stats';
import { QueryKeys } from 'types/client/queries';
import useFetch from 'lib/hooks/useFetch';
......@@ -26,10 +27,14 @@ const LatestBlocks = () => {
const fetch = useFetch();
const { data, isLoading, isError } = useQuery<unknown, unknown, Array<Block>>(
[ QueryKeys.indexBlocks ],
async() => await fetch(`/api/index/blocks`),
async() => await fetch(`/node-api/index/blocks`),
);
const queryClient = useQueryClient();
const statsQueryResult = useQuery<unknown, unknown, Stats>(
[ QueryKeys.stats ],
() => fetch('/node-api/stats'),
);
const handleNewBlockMessage: SocketMessage.NewBlock['handler'] = React.useCallback((payload) => {
queryClient.setQueryData([ QueryKeys.indexBlocks ], (prevData: Array<Block> | undefined) => {
......@@ -78,15 +83,20 @@ const LatestBlocks = () => {
content = (
<>
{ statsQueryResult.isLoading && (
<Skeleton h="24px" w="170px" mb={{ base: 6, lg: 9 }}/>
) }
{ statsQueryResult.data?.network_utilization_percentage && (
<Box mb={{ base: 6, lg: 9 }}>
<Text as="span" fontSize="sm">
Network utilization:{ nbsp }
</Text>
{ /* Not implemented in API yet */ }
<Text as="span" fontSize="sm" color="blue.400" fontWeight={ 700 }>
43.8%
{ statsQueryResult.data.network_utilization_percentage.toFixed(2) }%
</Text>
</Box>
) }
<VStack spacing={ `${ BLOCK_MARGIN }px` } mb={ 6 } height={ `${ BLOCK_HEIGHT * blocksCount + BLOCK_MARGIN * (blocksCount - 1) }px` } overflow="hidden">
<AnimatePresence initial={ false } >
{ dataToShow.map((block => <LatestBlocksItem key={ block.height } block={ block } h={ BLOCK_HEIGHT }/>)) }
......
......@@ -19,7 +19,7 @@ const LatestTransactions = () => {
const fetch = useFetch();
const { data, isLoading, isError } = useQuery<unknown, unknown, Array<Transaction>>(
[ QueryKeys.indexTxs ],
async() => await fetch(`/api/index/txs`),
async() => await fetch(`/node-api/index/txs`),
);
let content;
......
......@@ -40,7 +40,7 @@ const LatestBlocksItem = ({ tx }: Props) => {
const borderColor = useColorModeValue('gray.200', 'whiteAlpha.200');
const iconColor = useColorModeValue('blue.600', 'blue.300');
const dataTo = tx.to && tx.to.hash ? tx.to : tx.created_contract;
const dataTo = tx.to ? tx.to : tx.created_contract;
const timeAgo = useTimeAgoIncrement(tx.timestamp || '0', true);
const isMobile = useIsMobile();
......
......@@ -28,7 +28,7 @@ const Stats = () => {
const { data, isLoading, isError } = useQuery<unknown, unknown, Stats>(
[ QueryKeys.stats ],
async() => await fetch(`/api/index/stats`),
async() => await fetch(`/node-api/stats`),
);
if (isError) {
......
import { test, expect } from '@playwright/experimental-ct-react';
import React from 'react';
import RenderWithChakra from 'playwright/RenderWithChakra';
import TestApp from 'playwright/TestApp';
import AppError from './AppError';
......@@ -9,18 +9,18 @@ test.use({ viewport: { width: 900, height: 400 } });
test('status code 404', async({ mount }) => {
const component = await mount(
<RenderWithChakra>
<TestApp>
<AppError statusCode={ 404 }/>
</RenderWithChakra>,
</TestApp>,
);
await expect(component).toHaveScreenshot();
});
test('status code 500', async({ mount }) => {
const component = await mount(
<RenderWithChakra>
<TestApp>
<AppError statusCode={ 500 }/>
</RenderWithChakra>,
</TestApp>,
);
await expect(component).toHaveScreenshot();
});
......@@ -6,7 +6,7 @@ import filterIcon from 'icons/filter.svg';
const FilterIcon = <Icon as={ filterIcon } boxSize={ 5 } mr={{ base: 0, lg: 2 }}/>;
interface Props {
isActive: boolean;
isActive?: boolean;
appliedFiltersNum?: number;
onClick: () => void;
}
......
......@@ -3,7 +3,7 @@ import React from 'react';
interface Props {
children: React.ReactNode;
hasSearch: boolean;
hasSearch?: boolean;
}
const PageContent = ({ children, hasSearch }: Props) => {
......
import { test, expect, devices } from '@playwright/experimental-ct-react';
import React from 'react';
import TestApp from 'playwright/TestApp';
import TokenSnippet from './TokenSnippet';
const API_URL = 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/poa/assets/0x363574E6C5C71c343d7348093D84320c76d5Dd29/logo.png';
test.use(devices['iPhone 13 Pro']);
test('unnamed', async({ mount }) => {
const component = await mount(
<TestApp>
<TokenSnippet hash="0x363574E6C5C71c343d7348093D84320c76d5Dd29" symbol="xDAI"/>
</TestApp>,
);
await expect(component).toHaveScreenshot();
});
test('named', async({ mount }) => {
const component = await mount(
<TestApp>
<TokenSnippet hash="0x363574E6C5C71c343d7348093D84320c76d5Dd29" name="Shavuha token" symbol="SHA"/>
</TestApp>,
);
await expect(component).toHaveScreenshot();
});
test('with logo', async({ mount, page }) => {
await page.route(API_URL, (route) => {
return route.fulfill({
status: 200,
path: './playwright/image_s.jpg',
});
});
const component = await mount(
<TestApp>
<TokenSnippet hash="0x363574E6C5C71c343d7348093D84320c76d5Dd29"/>
</TestApp>,
);
await page.waitForResponse(API_URL),
await expect(component).toHaveScreenshot();
});
import { Box } from '@chakra-ui/react';
import { test, expect } from '@playwright/experimental-ct-react';
import React from 'react';
import { QueryKeys } from 'types/client/queries';
import * as tokenTransferMock from 'mocks/tokens/tokenTransfer';
import TestApp from 'playwright/TestApp';
import TokenTransfer from './TokenTransfer';
const API_URL = '/node-api/transactions/1/token-transfers';
test('without tx info +@mobile', async({ mount, page }) => {
await page.route(API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify(tokenTransferMock.mixTokens),
}));
const component = await mount(
<TestApp>
<Box h={{ base: '134px', lg: 6 }}/>
<TokenTransfer
path={ API_URL }
queryName={ QueryKeys.txTokenTransfers }
showTxInfo={ false }
/>
</TestApp>,
);
await page.waitForResponse(API_URL),
await expect(component).toHaveScreenshot();
});
import { Hide, Show, Text, Flex, Skeleton } from '@chakra-ui/react';
import { Hide, Show, Text } from '@chakra-ui/react';
import React from 'react';
import type { TokenType } from 'types/api/tokenInfo';
import type { QueryKeys } from 'types/client/queries';
import useQueryWithPages from 'lib/hooks/useQueryWithPages';
import { apos } from 'lib/html-entities';
import EmptySearchResult from 'ui/apps/EmptySearchResult';
import ActionBar from 'ui/shared/ActionBar';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
import HashStringShorten from 'ui/shared/HashStringShorten';
import Pagination from 'ui/shared/Pagination';
import SkeletonTable from 'ui/shared/SkeletonTable';
import { flattenTotal } from 'ui/shared/TokenTransfer/helpers';
import TokenTransferFilter from 'ui/shared/TokenTransfer/TokenTransferFilter';
import TokenTransferList from 'ui/shared/TokenTransfer/TokenTransferList';
import TokenTransferSkeletonMobile from 'ui/shared/TokenTransfer/TokenTransferSkeletonMobile';
import TokenTransferTable from 'ui/shared/TokenTransfer/TokenTransferTable';
......@@ -25,29 +28,34 @@ interface Props {
txHash?: string;
}
const TokenTransfer = ({ isLoading: isLoadingProp, isDisabled, queryName, queryIds, path, baseAddress, showTxInfo = true, txHash }: Props) => {
const TokenTransfer = ({ isLoading: isLoadingProp, isDisabled, queryName, queryIds, path, baseAddress, showTxInfo = true }: Props) => {
const [ filters, setFilters ] = React.useState<Array<TokenType>>([]);
const { isError, isLoading, data, pagination } = useQueryWithPages({
apiPath: path,
queryName,
queryIds,
options: { enabled: !isDisabled },
filters: filters.length ? { type: filters } : undefined,
});
const isPaginatorHidden = pagination.page === 1 && !pagination.hasNextPage;
const handleFilterChange = React.useCallback((nextValue: Array<TokenType>) => {
setFilters(nextValue);
}, []);
const isActionBarHidden = filters.length === 0 && !data?.items.length;
const content = (() => {
if (isLoading || isLoadingProp) {
return (
<>
<Hide below="lg">
{ txHash !== undefined && <Skeleton mb={ 6 } h={ 6 } w="340px"/> }
<SkeletonTable columns={ showTxInfo ?
[ '44px', '185px', '160px', '25%', '25%', '25%', '25%' ] :
[ '185px', '25%', '25%', '25%', '25%' ] }
/>
</Hide>
<Show below="lg">
<TokenTransferSkeletonMobile showTxInfo={ showTxInfo } txHash={ txHash }/>
<TokenTransferSkeletonMobile showTxInfo={ showTxInfo }/>
</Show>
</>
);
......@@ -57,15 +65,19 @@ const TokenTransfer = ({ isLoading: isLoadingProp, isDisabled, queryName, queryI
return <DataFetchAlert/>;
}
if (!data.items?.length) {
if (!data.items?.length && filters.length === 0) {
return <Text as="span">There are no token transfers</Text>;
}
if (!data.items?.length) {
return <EmptySearchResult text={ `Couldn${ apos }t find any token transfer that matches your query.` }/>;
}
const items = data.items.reduce(flattenTotal, []);
return (
<>
<Hide below="lg">
<TokenTransferTable data={ items } baseAddress={ baseAddress } showTxInfo={ showTxInfo } top={ isPaginatorHidden ? 0 : 80 }/>
<TokenTransferTable data={ items } baseAddress={ baseAddress } showTxInfo={ showTxInfo } top={ 80 }/>
</Hide>
<Show below="lg">
<TokenTransferList data={ items } baseAddress={ baseAddress } showTxInfo={ showTxInfo }/>
......@@ -76,14 +88,9 @@ const TokenTransfer = ({ isLoading: isLoadingProp, isDisabled, queryName, queryI
return (
<>
{ txHash && (data?.items.length || 0 > 0) && (
<Flex mb={ isPaginatorHidden ? 6 : 0 } w="100%">
<Text as="span" fontWeight={ 600 } whiteSpace="pre">Token transfers for by txn hash: </Text>
<HashStringShorten hash={ txHash }/>
</Flex>
) }
{ isPaginatorHidden ? null : (
<ActionBar>
{ !isActionBarHidden && (
<ActionBar mt={ -6 }>
<TokenTransferFilter defaultFilters={ filters } onFilterChange={ handleFilterChange } appliedFiltersNum={ filters.length }/>
<Pagination ml="auto" { ...pagination }/>
</ActionBar>
) }
......
import { Popover, PopoverTrigger, PopoverContent, PopoverBody, CheckboxGroup, Checkbox, Text, useDisclosure } from '@chakra-ui/react';
import React from 'react';
import type { TokenType } from 'types/api/tokenInfo';
import FilterButton from 'ui/shared/FilterButton';
import { TOKEN_TYPE } from './helpers';
interface Props {
appliedFiltersNum?: number;
defaultFilters: Array<TokenType>;
onFilterChange: (nextValue: Array<TokenType>) => void;
}
const TokenTransfer = ({ onFilterChange, defaultFilters, appliedFiltersNum }: Props) => {
const { isOpen, onToggle, onClose } = useDisclosure();
return (
<Popover isOpen={ isOpen } onClose={ onClose } placement="bottom-start" isLazy>
<PopoverTrigger>
<FilterButton
isActive={ isOpen || Number(appliedFiltersNum) > 0 }
onClick={ onToggle }
appliedFiltersNum={ appliedFiltersNum }
/>
</PopoverTrigger>
<PopoverContent w="200px">
<PopoverBody px={ 4 } py={ 6 } display="flex" flexDir="column" rowGap={ 5 }>
<Text variant="secondary" fontWeight={ 600 }>Type</Text>
<CheckboxGroup size="lg" onChange={ onFilterChange } defaultValue={ defaultFilters }>
{ TOKEN_TYPE.map(({ title, id }) => <Checkbox key={ id } value={ id }><Text fontSize="md">{ title }</Text></Checkbox>) }
</CheckboxGroup>
</PopoverBody>
</PopoverContent>
</Popover>
);
};
export default React.memo(TokenTransfer);
......@@ -11,7 +11,7 @@ import Address from 'ui/shared/address/Address';
import AddressIcon from 'ui/shared/address/AddressIcon';
import AddressLink from 'ui/shared/address/AddressLink';
import InOutTag from 'ui/shared/InOutTag';
import TokenSnippet from 'ui/shared/TokenSnippet';
import TokenSnippet from 'ui/shared/TokenSnippet/TokenSnippet';
import { getTokenTransferTypeText } from 'ui/shared/TokenTransfer/helpers';
import TokenTransferNft from 'ui/shared/TokenTransfer/TokenTransferNft';
......
import { Skeleton, SkeletonCircle, Flex, Box, useColorModeValue } from '@chakra-ui/react';
import React from 'react';
const TokenTransferSkeletonMobile = ({ showTxInfo, txHash }: { showTxInfo?: boolean; txHash?: string }) => {
const TokenTransferSkeletonMobile = ({ showTxInfo }: { showTxInfo?: boolean }) => {
const borderColor = useColorModeValue('blackAlpha.200', 'whiteAlpha.200');
return (
<>
{ txHash !== undefined && <Skeleton mb={ 6 } h={ 6 } w="100%"/> }
<Box>
{ Array.from(Array(2)).map((item, index) => (
<Flex
......@@ -51,7 +49,6 @@ const TokenTransferSkeletonMobile = ({ showTxInfo, txHash }: { showTxInfo?: bool
</Flex>
)) }
</Box>
</>
);
};
......
......@@ -9,7 +9,7 @@ import Address from 'ui/shared/address/Address';
import AddressIcon from 'ui/shared/address/AddressIcon';
import AddressLink from 'ui/shared/address/AddressLink';
import InOutTag from 'ui/shared/InOutTag';
import TokenSnippet from 'ui/shared/TokenSnippet';
import TokenSnippet from 'ui/shared/TokenSnippet/TokenSnippet';
import { getTokenTransferTypeText } from 'ui/shared/TokenTransfer/helpers';
import TokenTransferNft from 'ui/shared/TokenTransfer/TokenTransferNft';
......@@ -18,7 +18,7 @@ type Props = TokenTransfer & {
showTxInfo?: boolean;
}
const TxInternalTableItem = ({ token, total, tx_hash: txHash, from, to, baseAddress, showTxInfo, type }: Props) => {
const TokenTransferTableItem = ({ token, total, tx_hash: txHash, from, to, baseAddress, showTxInfo, type }: Props) => {
const value = (() => {
if (!('value' in total)) {
return '-';
......@@ -75,4 +75,4 @@ const TxInternalTableItem = ({ token, total, tx_hash: txHash, from, to, baseAddr
);
};
export default React.memo(TxInternalTableItem);
export default React.memo(TokenTransferTableItem);
import type { TokenType } from 'types/api/tokenInfo';
import type { TokenTransfer } from 'types/api/tokenTransfer';
export const flattenTotal = (result: Array<TokenTransfer>, item: TokenTransfer): Array<TokenTransfer> => {
......@@ -24,3 +25,9 @@ export const getTokenTransferTypeText = (type: TokenTransfer['type']) => {
return 'Token transfer';
}
};
export const TOKEN_TYPE: Array<{ title: string; id: TokenType }> = [
{ title: 'ERC-20', id: 'ERC-20' },
{ title: 'ERC-721', id: 'ERC-721' },
{ title: 'ERC-1155', id: 'ERC-1155' },
];
import { test, expect } from '@playwright/experimental-ct-react';
import React from 'react';
import RenderWithChakra from 'playwright/RenderWithChakra';
import TestApp from 'playwright/TestApp';
import Utilization from './Utilization';
test.use({ viewport: { width: 100, height: 50 } });
test('green color scheme in light mode', async({ mount }) => {
test('green color scheme +@dark-mode', async({ mount }) => {
const component = await mount(
<RenderWithChakra>
<TestApp>
<Utilization value={ 0.423 }/>
</RenderWithChakra>,
</TestApp>,
);
await expect(component).toHaveScreenshot();
});
test('green color scheme in dark mode', async({ mount }) => {
test('gray color scheme +@dark-mode', async({ mount }) => {
const component = await mount(
<RenderWithChakra colorMode="dark">
<Utilization value={ 0.423 }/>
</RenderWithChakra>,
);
await expect(component).toHaveScreenshot();
});
test('gray color scheme in light mode', async({ mount }) => {
const component = await mount(
<RenderWithChakra>
<Utilization value={ 0.423 } colorScheme="gray"/>
</RenderWithChakra>,
);
await expect(component).toHaveScreenshot();
});
test('gray color scheme in dark mode', async({ mount }) => {
const component = await mount(
<RenderWithChakra colorMode="dark">
<TestApp>
<Utilization value={ 0.423 } colorScheme="gray"/>
</RenderWithChakra>,
</TestApp>,
);
await expect(component).toHaveScreenshot();
});
......@@ -4,7 +4,7 @@ import React from 'react';
import nftIcon from 'icons/nft_shield.svg';
import link from 'lib/link/link';
import AddressLink from 'ui/shared/address/AddressLink';
import TokenSnippet from 'ui/shared/TokenSnippet';
import TokenSnippet from 'ui/shared/TokenSnippet/TokenSnippet';
interface Props {
value: string;
......@@ -26,7 +26,7 @@ const NftTokenTransferSnippet = ({ value, name, hash, symbol, tokenId }: Props)
<Link href={ url } fontWeight={ 600 }>{ tokenId }</Link>
</Box>
{ name ? (
<TokenSnippet symbol={ symbol } hash={ hash } name={ name }/>
<TokenSnippet symbol={ symbol } hash={ hash } name={ name } w="auto"/>
) : (
<AddressLink hash={ hash } truncation="constant" type="token"/>
) }
......
import { test, expect } from '@playwright/experimental-ct-react';
import React from 'react';
import * as mocks from 'mocks/txs/decodedInputData';
import TestApp from 'playwright/TestApp';
import TxDecodedInputData from './TxDecodedInputData';
test('with indexed fields +@mobile +@dark-mode', async({ mount }) => {
const component = await mount(
<TestApp>
<TxDecodedInputData data={ mocks.withIndexedFields }/>
</TestApp>,
);
await expect(component).toHaveScreenshot();
});
test('without indexed fields +@mobile', async({ mount }) => {
const component = await mount(
<TestApp>
<TxDecodedInputData data={ mocks.withoutIndexedFields }/>
</TestApp>,
);
await expect(component).toHaveScreenshot();
});
import { test, expect } from '@playwright/experimental-ct-react';
import React from 'react';
import * as txMock from 'mocks/txs/tx';
import TestApp from 'playwright/TestApp';
import TxDetails from './TxDetails';
const API_URL = '/node-api/transactions/1';
const hooksConfig = {
router: {
query: { id: 1 },
},
};
test('between addresses +@mobile +@dark-mode', async({ mount, page }) => {
await page.route(API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify(txMock.base),
}));
const component = await mount(
<TestApp>
<TxDetails/>
</TestApp>,
{ hooksConfig },
);
await page.waitForResponse(API_URL),
await page.getByText('View details').click();
await expect(component).toHaveScreenshot();
});
test('creating contact', async({ mount, page }) => {
await page.route(API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify(txMock.withContractCreation),
}));
const component = await mount(
<TestApp>
<TxDetails/>
</TestApp>,
{ hooksConfig },
);
await page.waitForResponse(API_URL);
await expect(component).toHaveScreenshot();
});
test('with token transfer +@mobile', async({ mount, page }) => {
await page.route(API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify(txMock.withTokenTransfer),
}));
const component = await mount(
<TestApp>
<TxDetails/>
</TestApp>,
{ hooksConfig },
);
await page.waitForResponse(API_URL);
await expect(component).toHaveScreenshot();
});
test('with decoded revert reason', async({ mount, page }) => {
await page.route(API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify(txMock.withDecodedRevertReason),
}));
const component = await mount(
<TestApp>
<TxDetails/>
</TestApp>,
{ hooksConfig },
);
await page.waitForResponse(API_URL);
await expect(component).toHaveScreenshot();
});
test('with decoded raw reason', async({ mount, page }) => {
await page.route(API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify(txMock.withRawRevertReason),
}));
const component = await mount(
<TestApp>
<TxDetails/>
</TestApp>,
{ hooksConfig },
);
await page.waitForResponse(API_URL);
await expect(component).toHaveScreenshot();
});
test('pending', async({ mount, page }) => {
await page.route(API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify(txMock.pending),
}));
const component = await mount(
<TestApp>
<TxDetails/>
</TestApp>,
{ hooksConfig },
);
await page.waitForResponse(API_URL);
await page.getByText('View details').click();
await expect(component).toHaveScreenshot();
});
......@@ -27,7 +27,7 @@ import Utilization from 'ui/shared/Utilization/Utilization';
import TxDetailsSkeleton from 'ui/tx/details/TxDetailsSkeleton';
import TxDetailsTokenTransfers from 'ui/tx/details/TxDetailsTokenTransfers';
import TxRevertReason from 'ui/tx/details/TxRevertReason';
import TxDecodedInputData from 'ui/tx/TxDecodedInputData';
import TxDecodedInputData from 'ui/tx/TxDecodedInputData/TxDecodedInputData';
import TxSocketAlert from 'ui/tx/TxSocketAlert';
import useFetchTxInfo from 'ui/tx/useFetchTxInfo';
......@@ -58,7 +58,7 @@ const TxDetails = () => {
...data.from.watchlist_names || [],
].map((tag) => <Tag key={ tag.label }>{ tag.display_name }</Tag>);
const toAddress = data.to && data.to.hash ? data.to : data.created_contract;
const toAddress = data.to ? data.to : data.created_contract;
const addressToTags = [
...toAddress.private_tags || [],
...toAddress.public_tags || [],
......@@ -156,7 +156,7 @@ const TxDetails = () => {
<CopyToClipboard text={ toAddress.hash }/>
</Address>
) : (
<Flex width="100%" whiteSpace="pre">
<Flex width={{ base: '100%', lg: 'auto' }} whiteSpace="pre">
<span>[Contract </span>
<AddressLink hash={ toAddress.hash }/>
<span> created]</span>
......
import { test, expect } from '@playwright/experimental-ct-react';
import React from 'react';
import * as internalTxsMock from 'mocks/txs/internalTxs';
import * as txMock from 'mocks/txs/tx';
import TestApp from 'playwright/TestApp';
import TxInternals from './TxInternals';
const TX_HASH = txMock.base.hash;
const API_URL_TX = `/node-api/transactions/${ TX_HASH }`;
const API_URL_TX_INTERNALS = `/node-api/transactions/${ TX_HASH }/internal-transactions`;
const hooksConfig = {
router: {
query: { id: TX_HASH },
},
};
test('base view +@mobile', async({ mount, page }) => {
await page.route(API_URL_TX, (route) => route.fulfill({
status: 200,
body: JSON.stringify(txMock.base),
}));
await page.route(API_URL_TX_INTERNALS, (route) => route.fulfill({
status: 200,
body: JSON.stringify(internalTxsMock.baseResponse),
}));
const component = await mount(
<TestApp>
<TxInternals/>
</TestApp>,
{ hooksConfig },
);
await page.waitForResponse(API_URL_TX),
await page.waitForResponse(API_URL_TX_INTERNALS),
await expect(component).toHaveScreenshot();
});
......@@ -7,7 +7,7 @@ import rightArrowIcon from 'icons/arrows/east.svg';
import { space } from 'lib/html-entities';
import AddressLink from 'ui/shared/address/AddressLink';
import CurrencyValue from 'ui/shared/CurrencyValue';
import TokenSnippet from 'ui/shared/TokenSnippet';
import TokenSnippet from 'ui/shared/TokenSnippet/TokenSnippet';
import NftTokenTransferSnippet from 'ui/tx/NftTokenTransferSnippet';
type Props = TTokenTransfer;
......@@ -34,6 +34,7 @@ const TxDetailsTokenTransfer = ({ token, total, to, from }: Props) => {
const payload = total as Erc721TotalPayload;
return (
<NftTokenTransferSnippet
name={ token.name }
tokenId={ payload.token_id }
value="1"
hash={ token.address }
......@@ -47,6 +48,7 @@ const TxDetailsTokenTransfer = ({ token, total, to, from }: Props) => {
const items = Array.isArray(payload) ? payload : [ payload ];
return items.map((item) => (
<NftTokenTransferSnippet
name={ token.name }
key={ item.token_id }
tokenId={ item.token_id }
value={ item.value }
......
......@@ -3,7 +3,8 @@ import React from 'react';
import type { TransactionRevertReason } from 'types/api/transaction';
import TxDecodedInputData from 'ui/tx/TxDecodedInputData';
import hexToUtf8 from 'lib/hexToUtf8';
import TxDecodedInputData from 'ui/tx/TxDecodedInputData/TxDecodedInputData';
type Props = TransactionRevertReason;
......@@ -25,7 +26,7 @@ const TxRevertReason = (props: Props) => {
<GridItem fontWeight={ 500 }>Raw:</GridItem>
<GridItem>{ props.raw }</GridItem>
<GridItem fontWeight={ 500 }>Decoded:</GridItem>
<GridItem>{ props.decoded }</GridItem>
<GridItem>{ hexToUtf8(props.raw) }</GridItem>
</Grid>
);
}
......
......@@ -17,7 +17,7 @@ type Props = InternalTransaction;
const TxInternalsListItem = ({ type, from, to, value, success, error, gas_limit: gasLimit, created_contract: createdContract }: Props) => {
const typeTitle = TX_INTERNALS_ITEMS.find(({ id }) => id === type)?.title;
const toData = to && to.hash ? to : createdContract;
const toData = to ? to : createdContract;
return (
<AccountListItemMobile rowGap={ 3 }>
......@@ -38,7 +38,9 @@ const TxInternalsListItem = ({ type, from, to, value, success, error, gas_limit:
</Box>
<HStack spacing={ 3 }>
<Text fontSize="sm" fontWeight={ 500 }>Value { appConfig.network.currency.symbol }</Text>
<Text fontSize="sm" variant="secondary">{ value }</Text>
<Text fontSize="sm" variant="secondary">
{ BigNumber(value).div(BigNumber(10 ** appConfig.network.currency.decimals)).toFormat() }
</Text>
</HStack>
<HStack spacing={ 3 }>
<Text fontSize="sm" fontWeight={ 500 }>Gas limit</Text>
......
......@@ -4,6 +4,7 @@ import React from 'react';
import type { InternalTransaction } from 'types/api/internalTransaction';
import appConfig from 'configs/app/config';
import rightArrowIcon from 'icons/arrows/east.svg';
import Address from 'ui/shared/address/Address';
import AddressIcon from 'ui/shared/address/AddressIcon';
......@@ -15,7 +16,7 @@ type Props = InternalTransaction
const TxInternalTableItem = ({ type, from, to, value, success, error, gas_limit: gasLimit, created_contract: createdContract }: Props) => {
const typeTitle = TX_INTERNALS_ITEMS.find(({ id }) => id === type)?.title;
const toData = to && to.hash ? to : createdContract;
const toData = to ? to : createdContract;
return (
<Tr alignItems="top">
......@@ -45,7 +46,7 @@ const TxInternalTableItem = ({ type, from, to, value, success, error, gas_limit:
</Address>
</Td>
<Td isNumeric verticalAlign="middle">
{ value }
{ BigNumber(value).div(BigNumber(10 ** appConfig.network.currency.decimals)).toFormat() }
</Td>
<Td isNumeric verticalAlign="middle">
{ BigNumber(gasLimit).toFormat() }
......
import { test, expect } from '@playwright/experimental-ct-react';
import React from 'react';
import * as addressMocks from 'mocks/address/address';
import * as inputDataMocks from 'mocks/txs/decodedInputData';
import TestApp from 'playwright/TestApp';
import TxLogItem from './TxLogItem';
const TOPICS = [
'0x3a4ec416703c36a61a4b1f690847f1963a6829eac0b52debd40a23b66c142a56',
'0x0000000000000000000000000000000000000000000000000000000005001bcf',
'0xe835d1028984e9e6e7d016b77164eacbcc6cc061e9333c0b37982b504f7ea791',
null,
];
const DATA = '0x0000000000000000000000000000000000000000000000000070265bf0112cee';
test('with decoded input data +@mobile +@dark-mode', async({ mount }) => {
const component = await mount(
<TestApp>
<TxLogItem
index={ 42 }
decoded={ inputDataMocks.withIndexedFields }
address={ addressMocks.withName }
topics={ TOPICS }
data={ DATA }
/>
</TestApp>,
);
await expect(component).toHaveScreenshot();
});
test('without decoded input data +@mobile', async({ mount }) => {
const component = await mount(
<TestApp>
<TxLogItem
index={ 42 }
decoded={ null }
address={ addressMocks.withoutName }
topics={ TOPICS }
data={ DATA }
/>
</TestApp>,
);
await expect(component).toHaveScreenshot();
});
......@@ -6,11 +6,12 @@ import type { Log } from 'types/api/log';
// import searchIcon from 'icons/search.svg';
import { space } from 'lib/html-entities';
import link from 'lib/link/link';
import notEmpty from 'lib/notEmpty';
import Address from 'ui/shared/address/Address';
import AddressIcon from 'ui/shared/address/AddressIcon';
import AddressLink from 'ui/shared/address/AddressLink';
import TxLogTopic from 'ui/tx/logs/TxLogTopic';
import DecodedInputData from 'ui/tx/TxDecodedInputData';
import DecodedInputData from 'ui/tx/TxDecodedInputData/TxDecodedInputData';
type Props = Log;
......@@ -74,7 +75,7 @@ const TxLogItem = ({ address, index, topics, data, decoded }: Props) => {
) }
<RowHeader>Topics</RowHeader>
<GridItem>
{ topics.filter(Boolean).map((item, index) => (
{ topics.filter(notEmpty).map((item, index) => (
<TxLogTopic
key={ index }
hex={ item }
......
import { test, expect } from '@playwright/experimental-ct-react';
import React from 'react';
import TestApp from 'playwright/TestApp';
import TxLogTopic from './TxLogTopic';
test('address view +@mobile -@default', async({ mount }) => {
const component = await mount(
<TestApp>
<TxLogTopic hex="0x000000000000000000000000d789a607ceac2f0e14867de4eb15b15c9ffb5859" index={ 42 }/>
</TestApp>,
);
await component.locator('select[aria-label="Data type"]').selectOption('address');
await expect(component).toHaveScreenshot();
});
test('hex view +@mobile -@default', async({ mount }) => {
const component = await mount(
<TestApp>
<TxLogTopic hex="0x000000000000000000000000d789a607ceac2f0e14867de4eb15b15c9ffb5859" index={ 42 }/>
</TestApp>,
);
await component.locator('select[aria-label="Data type"]').selectOption('hex');
await expect(component).toHaveScreenshot();
});
......@@ -74,6 +74,7 @@ const TxLogTopic = ({ hex, index }: Props) => {
mr={ 3 }
flexShrink={ 0 }
w="auto"
aria-label="Data type"
>
{ OPTIONS.map((option) => <option key={ option } value={ option }>{ capitalize(option) }</option>) }
</Select>
......
......@@ -33,7 +33,7 @@ const TxsListItem = ({ tx, showBlockInfo }: {tx: Transaction; showBlockInfo: boo
const iconColor = useColorModeValue('blue.600', 'blue.300');
const borderColor = useColorModeValue('blackAlpha.200', 'whiteAlpha.200');
const dataTo = tx.to && tx.to.hash ? tx.to : tx.created_contract;
const dataTo = tx.to ? tx.to : tx.created_contract;
return (
<>
......
......@@ -44,7 +44,7 @@ const TxsTableItem = ({ tx, showBlockInfo }: {tx: Transaction; showBlockInfo: bo
</Address>
);
const dataTo = tx.to && tx.to.hash ? tx.to : tx.created_contract;
const dataTo = tx.to ? tx.to : tx.created_contract;
const addressTo = (
<Address>
......
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