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 = { ...@@ -195,7 +195,7 @@ module.exports = {
groups: [ groups: [
'module', 'module',
'/types/', '/types/',
[ '/^configs/', '/^data/', '/^deploy/', '/^icons/', '/^lib/', '/^pages/', '/^playwright/', '/^theme/', '/^ui/' ], [ '/^configs/', '/^data/', '/^deploy/', '/^icons/', '/^lib/', '/^mocks/', '/^pages/', '/^playwright/', '/^theme/', '/^ui/' ],
[ 'parent', 'sibling', 'index' ], [ 'parent', 'sibling', 'index' ],
], ],
alphabetize: { order: 'asc', ignoreCase: true }, alphabetize: { order: 'asc', ignoreCase: true },
......
...@@ -102,15 +102,56 @@ ...@@ -102,15 +102,56 @@
"instanceLimit": 1 "instanceLimit": 1
} }
}, },
// PW TESTS
{ {
"type": "npm", "type": "shell",
"script": "test:pw:docker", "command": "${input:pwDebugFlag} yarn test:pw:local ${relativeFileDirname}/${fileBasename} ${input:pwArgs}",
"problemMatcher": [], "problemMatcher": [],
"label": "test: playwright", "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: 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", "detail": "run visual components tests",
"presentation": { "presentation": {
"reveal": "always", "reveal": "always",
"panel": "new", "panel": "new",
"focus": true,
}, },
"icon": { "icon": {
"color": "terminal.ansiBlue", "color": "terminal.ansiBlue",
...@@ -120,6 +161,8 @@ ...@@ -120,6 +161,8 @@
"instanceLimit": 1 "instanceLimit": 1
} }
}, },
// JEST TESTS
{ {
"type": "npm", "type": "npm",
"script": "test:jest", "script": "test:jest",
...@@ -129,6 +172,7 @@ ...@@ -129,6 +172,7 @@
"presentation": { "presentation": {
"reveal": "always", "reveal": "always",
"panel": "new", "panel": "new",
"focus": true,
}, },
"icon": { "icon": {
"color": "terminal.ansiBlue", "color": "terminal.ansiBlue",
...@@ -148,6 +192,7 @@ ...@@ -148,6 +192,7 @@
"reveal": "always", "reveal": "always",
"panel": "new", "panel": "new",
"close": true, "close": true,
"focus": true,
}, },
"icon": { "icon": {
"color": "terminal.ansiBlue", "color": "terminal.ansiBlue",
...@@ -157,6 +202,26 @@ ...@@ -157,6 +202,26 @@
"instanceLimit": 1 "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", "type": "npm",
"script": "build:docker", "script": "build:docker",
...@@ -168,6 +233,7 @@ ...@@ -168,6 +233,7 @@
"panel": "new", "panel": "new",
"close": true, "close": true,
"revealProblems": "onProblem", "revealProblems": "onProblem",
"focus": true,
}, },
"icon": { "icon": {
"color": "terminal.ansiRed", "color": "terminal.ansiRed",
...@@ -217,5 +283,29 @@ ...@@ -217,5 +283,29 @@
"instanceLimit": 1 "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 = { ...@@ -95,7 +95,9 @@ const config: JestConfigWithTsJest = {
// moduleNameMapper: {}, // moduleNameMapper: {},
// An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader // 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 // Activates notifications for test results
// notify: false, // notify: false,
......
...@@ -5,10 +5,8 @@ import { useRouter } from 'next/router'; ...@@ -5,10 +5,8 @@ import { useRouter } from 'next/router';
import React, { useCallback } from 'react'; import React, { useCallback } from 'react';
import { animateScroll } from 'react-scroll'; import { animateScroll } from 'react-scroll';
import type { BlockFilters } from 'types/api/block';
import { PAGINATION_FIELDS } from 'types/api/pagination'; import { PAGINATION_FIELDS } from 'types/api/pagination';
import type { PaginationParams, PaginatedResponse, PaginatedQueryKeys } from 'types/api/pagination'; import type { PaginationParams, PaginatedResponse, PaginatedQueryKeys, PaginationFilters } from 'types/api/pagination';
import type { TTxsFilters } from 'types/api/txsFilters';
import useFetch from 'lib/hooks/useFetch'; import useFetch from 'lib/hooks/useFetch';
...@@ -16,7 +14,7 @@ interface Params<QueryName extends PaginatedQueryKeys> { ...@@ -16,7 +14,7 @@ interface Params<QueryName extends PaginatedQueryKeys> {
apiPath: string; apiPath: string;
queryName: QueryName; queryName: QueryName;
queryIds?: Array<string>; queryIds?: Array<string>;
filters?: TTxsFilters | BlockFilters; filters?: PaginationFilters<QueryName>;
options?: Omit<UseQueryOptions<unknown, unknown, PaginatedResponse<QueryName>>, 'queryKey' | 'queryFn'>; 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'; import handler from 'lib/api/handler';
const getUrl = () => '/v2/stats'; const getUrl = () => '/v2/stats';
......
...@@ -12,7 +12,7 @@ const config: PlaywrightTestConfig = { ...@@ -12,7 +12,7 @@ const config: PlaywrightTestConfig = {
testMatch: /.*\.pw\.tsx/, 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. */ /* Maximum time one test can run for. */
timeout: 10 * 1000, timeout: 10 * 1000,
...@@ -57,8 +57,36 @@ const config: PlaywrightTestConfig = { ...@@ -57,8 +57,36 @@ const config: PlaywrightTestConfig = {
/* Configure projects for major browsers */ /* Configure projects for major browsers */
projects: [ projects: [
{ {
name: 'chromium', name: 'default',
use: devices['Desktop Chrome'], 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 diff is collapsed.
...@@ -4,38 +4,6 @@ ...@@ -4,38 +4,6 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Testing Page</title> <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> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>
......
// Import styles, initialize component theme here. import './fonts.css';
// import '../src/common.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 {}; export {};
...@@ -2,4 +2,4 @@ ...@@ -2,4 +2,4 @@
yarn install --modules-folder node_modules_linux yarn install --modules-folder node_modules_linux
export NODE_PATH=$(pwd)/node_modules_linux export NODE_PATH=$(pwd)/node_modules_linux
rm -rf ./playwright/.cache rm -rf ./playwright/.cache
yarn test:pw yarn test:pw "$@"
\ No newline at end of file \ No newline at end of file
...@@ -11,9 +11,10 @@ export interface WatchlistName { ...@@ -11,9 +11,10 @@ export interface WatchlistName {
export interface AddressParam { export interface AddressParam {
hash: string; hash: string;
implementation_name: string; implementation_name: string | null;
name: string | null; name: string | null;
is_contract: boolean; is_contract: boolean;
is_verified: boolean | null;
private_tags: Array<AddressTag> | null; private_tags: Array<AddressTag> | null;
watchlist_names: Array<WatchlistName> | null; watchlist_names: Array<WatchlistName> | null;
public_tags: Array<AddressTag> | null; public_tags: Array<AddressTag> | null;
......
...@@ -16,10 +16,10 @@ export interface Block { ...@@ -16,10 +16,10 @@ export interface Block {
total_difficulty: string; total_difficulty: string;
gas_used: string | null; gas_used: string | null;
gas_limit: string; gas_limit: string;
nonce: number; nonce: string;
base_fee_per_gas: number | null; base_fee_per_gas: string | null;
burnt_fees: number | null; burnt_fees: string | null;
priority_fee: number | null; priority_fee: string | null;
extra_data: string | null; extra_data: string | null;
state_root: string | null; state_root: string | null;
rewards?: Array<Reward>; rewards?: Array<Reward>;
......
...@@ -2,14 +2,21 @@ import type { AddressParam } from './addressParams'; ...@@ -2,14 +2,21 @@ import type { AddressParam } from './addressParams';
export type TxInternalsType = 'call' | 'delegatecall' | 'staticcall' | 'create' | 'create2' | 'selfdestruct' | 'reward' 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; error: string | null;
success: boolean; success: boolean;
type: TxInternalsType; type: TxInternalsType;
transaction_hash: string; transaction_hash: string;
from: AddressParam; from: AddressParam;
to: AddressParam;
created_contract: AddressParam;
value: string; value: string;
index: number; index: number;
block: number; block: number;
...@@ -25,5 +32,5 @@ export interface InternalTransactionsResponse { ...@@ -25,5 +32,5 @@ export interface InternalTransactionsResponse {
items_count: number; items_count: number;
transaction_hash: string; transaction_hash: string;
transaction_index: number; transaction_index: number;
}; } | null;
} }
...@@ -3,7 +3,7 @@ import type { DecodedInput } from './decodedInput'; ...@@ -3,7 +3,7 @@ import type { DecodedInput } from './decodedInput';
export interface Log { export interface Log {
address: AddressParam; address: AddressParam;
topics: Array<string>; topics: Array<string | null>;
data: string; data: string;
index: number; index: number;
decoded: DecodedInput | null; 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 { InternalTransactionsResponse } from 'types/api/internalTransaction';
import type { LogsResponse } from 'types/api/log'; 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 { TransactionsResponseValidated, TransactionsResponsePending } from 'types/api/transaction';
import type { TTxsFilters } from 'types/api/txsFilters';
import { QueryKeys } from 'types/client/queries'; import { QueryKeys } from 'types/client/queries';
import type { KeysOfObjectOrNull } from 'types/utils/KeysOfObjectOrNull'; import type { KeysOfObjectOrNull } from 'types/utils/KeysOfObjectOrNull';
...@@ -25,6 +26,13 @@ export type PaginatedResponse<Q extends PaginatedQueryKeys> = ...@@ -25,6 +26,13 @@ export type PaginatedResponse<Q extends PaginatedQueryKeys> =
Q extends QueryKeys.txTokenTransfers ? TokenTransferResponse : Q extends QueryKeys.txTokenTransfers ? TokenTransferResponse :
never 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']; export type PaginationParams<Q extends PaginatedQueryKeys> = PaginatedResponse<Q>['next_page_params'];
type PaginationFields = { type PaginationFields = {
......
export interface Reward { export interface Reward {
reward: string; 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 = { ...@@ -10,4 +10,5 @@ export type Stats = {
gas_prices: {average: number; fast: number; slow: number}; gas_prices: {average: number; fast: number; slow: number};
static_gas_price: string; static_gas_price: string;
market_cap: string; market_cap: string;
network_utilization_percentage: number;
} }
import type { AddressParam } from './addressParams'; import type { AddressParam } from './addressParams';
import type { TokenInfoGeneric } from './tokenInfo'; import type { TokenInfoGeneric, TokenType } from './tokenInfo';
export type Erc20TotalPayload = { export type Erc20TotalPayload = {
decimals: string | null; decimals: string | null;
...@@ -47,3 +47,7 @@ export interface TokenTransferResponse { ...@@ -47,3 +47,7 @@ export interface TokenTransferResponse {
transaction_hash: string; transaction_hash: string;
} | null; } | null;
} }
export interface TokenTransferFilters {
type: Array<TokenType>;
}
...@@ -5,10 +5,18 @@ import type { TokenTransfer } from './tokenTransfer'; ...@@ -5,10 +5,18 @@ import type { TokenTransfer } from './tokenTransfer';
export type TransactionRevertReason = { export type TransactionRevertReason = {
raw: string; raw: string;
decoded: string;
} | DecodedInput; } | DecodedInput;
export interface Transaction { export type Transaction = (
{
to: AddressParam;
created_contract: null;
} |
{
to: null;
created_contract: AddressParam;
}
) & {
hash: string; hash: string;
result: string; result: string;
confirmations: number; confirmations: number;
...@@ -17,21 +25,19 @@ export interface Transaction { ...@@ -17,21 +25,19 @@ export interface Transaction {
timestamp: string | null; timestamp: string | null;
confirmation_duration: Array<number>; confirmation_duration: Array<number>;
from: AddressParam; from: AddressParam;
to: AddressParam | null;
created_contract: AddressParam;
value: string; value: string;
fee: Fee; fee: Fee;
gas_price: number; gas_price: string;
type: number; type: number | null;
gas_used: string | null; gas_used: string | null;
gas_limit: string; gas_limit: string;
max_fee_per_gas: number | null; max_fee_per_gas: string | null;
max_priority_fee_per_gas: number | null; max_priority_fee_per_gas: string | null;
priority_fee: number | null; priority_fee: string | null;
base_fee_per_gas: number | null; base_fee_per_gas: string | null;
tx_burnt_fee: number | null; tx_burnt_fee: string | null;
nonce: number; nonce: number;
position: number; position: number | null;
revert_reason: TransactionRevertReason | null; revert_reason: TransactionRevertReason | null;
raw_input: string; raw_input: string;
decoded_input: DecodedInput | null; 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 = () => { ...@@ -228,7 +228,7 @@ const BlockDetails = () => {
</Tooltip> </Tooltip>
) } ) }
</DetailsInfoItem> </DetailsInfoItem>
{ data.priority_fee !== null && data.priority_fee > 0 && ( { data.priority_fee !== null && BigNumber(data.priority_fee).gt(ZERO) && (
<DetailsInfoItem <DetailsInfoItem
title="Priority fee / Tip" title="Priority fee / Tip"
hint="User-defined tips sent to validator for transaction priority/inclusion." 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'; ...@@ -5,6 +5,7 @@ import React from 'react';
import type { SocketMessage } from 'lib/socket/types'; import type { SocketMessage } from 'lib/socket/types';
import type { Block } from 'types/api/block'; import type { Block } from 'types/api/block';
import type { Stats } from 'types/api/stats';
import { QueryKeys } from 'types/client/queries'; import { QueryKeys } from 'types/client/queries';
import useFetch from 'lib/hooks/useFetch'; import useFetch from 'lib/hooks/useFetch';
...@@ -26,10 +27,14 @@ const LatestBlocks = () => { ...@@ -26,10 +27,14 @@ const LatestBlocks = () => {
const fetch = useFetch(); const fetch = useFetch();
const { data, isLoading, isError } = useQuery<unknown, unknown, Array<Block>>( const { data, isLoading, isError } = useQuery<unknown, unknown, Array<Block>>(
[ QueryKeys.indexBlocks ], [ QueryKeys.indexBlocks ],
async() => await fetch(`/api/index/blocks`), async() => await fetch(`/node-api/index/blocks`),
); );
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const statsQueryResult = useQuery<unknown, unknown, Stats>(
[ QueryKeys.stats ],
() => fetch('/node-api/stats'),
);
const handleNewBlockMessage: SocketMessage.NewBlock['handler'] = React.useCallback((payload) => { const handleNewBlockMessage: SocketMessage.NewBlock['handler'] = React.useCallback((payload) => {
queryClient.setQueryData([ QueryKeys.indexBlocks ], (prevData: Array<Block> | undefined) => { queryClient.setQueryData([ QueryKeys.indexBlocks ], (prevData: Array<Block> | undefined) => {
...@@ -78,15 +83,20 @@ const LatestBlocks = () => { ...@@ -78,15 +83,20 @@ const LatestBlocks = () => {
content = ( content = (
<> <>
<Box mb={{ base: 6, lg: 9 }}> { statsQueryResult.isLoading && (
<Text as="span" fontSize="sm"> <Skeleton h="24px" w="170px" mb={{ base: 6, lg: 9 }}/>
Network utilization:{ nbsp } ) }
</Text> { statsQueryResult.data?.network_utilization_percentage && (
{ /* Not implemented in API yet */ } <Box mb={{ base: 6, lg: 9 }}>
<Text as="span" fontSize="sm" color="blue.400" fontWeight={ 700 }> <Text as="span" fontSize="sm">
43.8% Network utilization:{ nbsp }
</Text> </Text>
</Box> { /* Not implemented in API yet */ }
<Text as="span" fontSize="sm" color="blue.400" fontWeight={ 700 }>
{ 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"> <VStack spacing={ `${ BLOCK_MARGIN }px` } mb={ 6 } height={ `${ BLOCK_HEIGHT * blocksCount + BLOCK_MARGIN * (blocksCount - 1) }px` } overflow="hidden">
<AnimatePresence initial={ false } > <AnimatePresence initial={ false } >
{ dataToShow.map((block => <LatestBlocksItem key={ block.height } block={ block } h={ BLOCK_HEIGHT }/>)) } { dataToShow.map((block => <LatestBlocksItem key={ block.height } block={ block } h={ BLOCK_HEIGHT }/>)) }
......
...@@ -19,7 +19,7 @@ const LatestTransactions = () => { ...@@ -19,7 +19,7 @@ const LatestTransactions = () => {
const fetch = useFetch(); const fetch = useFetch();
const { data, isLoading, isError } = useQuery<unknown, unknown, Array<Transaction>>( const { data, isLoading, isError } = useQuery<unknown, unknown, Array<Transaction>>(
[ QueryKeys.indexTxs ], [ QueryKeys.indexTxs ],
async() => await fetch(`/api/index/txs`), async() => await fetch(`/node-api/index/txs`),
); );
let content; let content;
......
...@@ -40,7 +40,7 @@ const LatestBlocksItem = ({ tx }: Props) => { ...@@ -40,7 +40,7 @@ const LatestBlocksItem = ({ tx }: Props) => {
const borderColor = useColorModeValue('gray.200', 'whiteAlpha.200'); const borderColor = useColorModeValue('gray.200', 'whiteAlpha.200');
const iconColor = useColorModeValue('blue.600', 'blue.300'); 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 timeAgo = useTimeAgoIncrement(tx.timestamp || '0', true);
const isMobile = useIsMobile(); const isMobile = useIsMobile();
......
...@@ -28,7 +28,7 @@ const Stats = () => { ...@@ -28,7 +28,7 @@ const Stats = () => {
const { data, isLoading, isError } = useQuery<unknown, unknown, Stats>( const { data, isLoading, isError } = useQuery<unknown, unknown, Stats>(
[ QueryKeys.stats ], [ QueryKeys.stats ],
async() => await fetch(`/api/index/stats`), async() => await fetch(`/node-api/stats`),
); );
if (isError) { if (isError) {
......
import { test, expect } from '@playwright/experimental-ct-react'; import { test, expect } from '@playwright/experimental-ct-react';
import React from 'react'; import React from 'react';
import RenderWithChakra from 'playwright/RenderWithChakra'; import TestApp from 'playwright/TestApp';
import AppError from './AppError'; import AppError from './AppError';
...@@ -9,18 +9,18 @@ test.use({ viewport: { width: 900, height: 400 } }); ...@@ -9,18 +9,18 @@ test.use({ viewport: { width: 900, height: 400 } });
test('status code 404', async({ mount }) => { test('status code 404', async({ mount }) => {
const component = await mount( const component = await mount(
<RenderWithChakra> <TestApp>
<AppError statusCode={ 404 }/> <AppError statusCode={ 404 }/>
</RenderWithChakra>, </TestApp>,
); );
await expect(component).toHaveScreenshot(); await expect(component).toHaveScreenshot();
}); });
test('status code 500', async({ mount }) => { test('status code 500', async({ mount }) => {
const component = await mount( const component = await mount(
<RenderWithChakra> <TestApp>
<AppError statusCode={ 500 }/> <AppError statusCode={ 500 }/>
</RenderWithChakra>, </TestApp>,
); );
await expect(component).toHaveScreenshot(); await expect(component).toHaveScreenshot();
}); });
...@@ -6,7 +6,7 @@ import filterIcon from 'icons/filter.svg'; ...@@ -6,7 +6,7 @@ import filterIcon from 'icons/filter.svg';
const FilterIcon = <Icon as={ filterIcon } boxSize={ 5 } mr={{ base: 0, lg: 2 }}/>; const FilterIcon = <Icon as={ filterIcon } boxSize={ 5 } mr={{ base: 0, lg: 2 }}/>;
interface Props { interface Props {
isActive: boolean; isActive?: boolean;
appliedFiltersNum?: number; appliedFiltersNum?: number;
onClick: () => void; onClick: () => void;
} }
......
...@@ -3,7 +3,7 @@ import React from 'react'; ...@@ -3,7 +3,7 @@ import React from 'react';
interface Props { interface Props {
children: React.ReactNode; children: React.ReactNode;
hasSearch: boolean; hasSearch?: boolean;
} }
const PageContent = ({ children, hasSearch }: Props) => { 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 React from 'react';
import type { TokenType } from 'types/api/tokenInfo';
import type { QueryKeys } from 'types/client/queries'; import type { QueryKeys } from 'types/client/queries';
import useQueryWithPages from 'lib/hooks/useQueryWithPages'; 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 ActionBar from 'ui/shared/ActionBar';
import DataFetchAlert from 'ui/shared/DataFetchAlert'; import DataFetchAlert from 'ui/shared/DataFetchAlert';
import HashStringShorten from 'ui/shared/HashStringShorten';
import Pagination from 'ui/shared/Pagination'; import Pagination from 'ui/shared/Pagination';
import SkeletonTable from 'ui/shared/SkeletonTable'; import SkeletonTable from 'ui/shared/SkeletonTable';
import { flattenTotal } from 'ui/shared/TokenTransfer/helpers'; import { flattenTotal } from 'ui/shared/TokenTransfer/helpers';
import TokenTransferFilter from 'ui/shared/TokenTransfer/TokenTransferFilter';
import TokenTransferList from 'ui/shared/TokenTransfer/TokenTransferList'; import TokenTransferList from 'ui/shared/TokenTransfer/TokenTransferList';
import TokenTransferSkeletonMobile from 'ui/shared/TokenTransfer/TokenTransferSkeletonMobile'; import TokenTransferSkeletonMobile from 'ui/shared/TokenTransfer/TokenTransferSkeletonMobile';
import TokenTransferTable from 'ui/shared/TokenTransfer/TokenTransferTable'; import TokenTransferTable from 'ui/shared/TokenTransfer/TokenTransferTable';
...@@ -25,29 +28,34 @@ interface Props { ...@@ -25,29 +28,34 @@ interface Props {
txHash?: string; 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({ const { isError, isLoading, data, pagination } = useQueryWithPages({
apiPath: path, apiPath: path,
queryName, queryName,
queryIds, queryIds,
options: { enabled: !isDisabled }, 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 = (() => { const content = (() => {
if (isLoading || isLoadingProp) { if (isLoading || isLoadingProp) {
return ( return (
<> <>
<Hide below="lg"> <Hide below="lg">
{ txHash !== undefined && <Skeleton mb={ 6 } h={ 6 } w="340px"/> }
<SkeletonTable columns={ showTxInfo ? <SkeletonTable columns={ showTxInfo ?
[ '44px', '185px', '160px', '25%', '25%', '25%', '25%' ] : [ '44px', '185px', '160px', '25%', '25%', '25%', '25%' ] :
[ '185px', '25%', '25%', '25%', '25%' ] } [ '185px', '25%', '25%', '25%', '25%' ] }
/> />
</Hide> </Hide>
<Show below="lg"> <Show below="lg">
<TokenTransferSkeletonMobile showTxInfo={ showTxInfo } txHash={ txHash }/> <TokenTransferSkeletonMobile showTxInfo={ showTxInfo }/>
</Show> </Show>
</> </>
); );
...@@ -57,15 +65,19 @@ const TokenTransfer = ({ isLoading: isLoadingProp, isDisabled, queryName, queryI ...@@ -57,15 +65,19 @@ const TokenTransfer = ({ isLoading: isLoadingProp, isDisabled, queryName, queryI
return <DataFetchAlert/>; return <DataFetchAlert/>;
} }
if (!data.items?.length) { if (!data.items?.length && filters.length === 0) {
return <Text as="span">There are no token transfers</Text>; 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, []); const items = data.items.reduce(flattenTotal, []);
return ( return (
<> <>
<Hide below="lg"> <Hide below="lg">
<TokenTransferTable data={ items } baseAddress={ baseAddress } showTxInfo={ showTxInfo } top={ isPaginatorHidden ? 0 : 80 }/> <TokenTransferTable data={ items } baseAddress={ baseAddress } showTxInfo={ showTxInfo } top={ 80 }/>
</Hide> </Hide>
<Show below="lg"> <Show below="lg">
<TokenTransferList data={ items } baseAddress={ baseAddress } showTxInfo={ showTxInfo }/> <TokenTransferList data={ items } baseAddress={ baseAddress } showTxInfo={ showTxInfo }/>
...@@ -76,14 +88,9 @@ const TokenTransfer = ({ isLoading: isLoadingProp, isDisabled, queryName, queryI ...@@ -76,14 +88,9 @@ const TokenTransfer = ({ isLoading: isLoadingProp, isDisabled, queryName, queryI
return ( return (
<> <>
{ txHash && (data?.items.length || 0 > 0) && ( { !isActionBarHidden && (
<Flex mb={ isPaginatorHidden ? 6 : 0 } w="100%"> <ActionBar mt={ -6 }>
<Text as="span" fontWeight={ 600 } whiteSpace="pre">Token transfers for by txn hash: </Text> <TokenTransferFilter defaultFilters={ filters } onFilterChange={ handleFilterChange } appliedFiltersNum={ filters.length }/>
<HashStringShorten hash={ txHash }/>
</Flex>
) }
{ isPaginatorHidden ? null : (
<ActionBar>
<Pagination ml="auto" { ...pagination }/> <Pagination ml="auto" { ...pagination }/>
</ActionBar> </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'; ...@@ -11,7 +11,7 @@ import Address from 'ui/shared/address/Address';
import AddressIcon from 'ui/shared/address/AddressIcon'; import AddressIcon from 'ui/shared/address/AddressIcon';
import AddressLink from 'ui/shared/address/AddressLink'; import AddressLink from 'ui/shared/address/AddressLink';
import InOutTag from 'ui/shared/InOutTag'; 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 { getTokenTransferTypeText } from 'ui/shared/TokenTransfer/helpers';
import TokenTransferNft from 'ui/shared/TokenTransfer/TokenTransferNft'; import TokenTransferNft from 'ui/shared/TokenTransfer/TokenTransferNft';
......
import { Skeleton, SkeletonCircle, Flex, Box, useColorModeValue } from '@chakra-ui/react'; import { Skeleton, SkeletonCircle, Flex, Box, useColorModeValue } from '@chakra-ui/react';
import React from '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'); const borderColor = useColorModeValue('blackAlpha.200', 'whiteAlpha.200');
return ( return (
<> <Box>
{ txHash !== undefined && <Skeleton mb={ 6 } h={ 6 } w="100%"/> } { Array.from(Array(2)).map((item, index) => (
<Box> <Flex
{ Array.from(Array(2)).map((item, index) => ( key={ index }
<Flex rowGap={ 3 }
key={ index } flexDirection="column"
rowGap={ 3 } paddingY={ 6 }
flexDirection="column" borderTopWidth="1px"
paddingY={ 6 } borderColor={ borderColor }
borderTopWidth="1px" _last={{
borderColor={ borderColor } borderBottomWidth: '1px',
_last={{ }}
borderBottomWidth: '1px', >
}} <Flex h={ 6 }>
> <SkeletonCircle size="6" mr={ 2 } flexShrink={ 0 }/>
<Flex h={ 6 }> <Skeleton w="100px" mr={ 2 }/>
<SkeletonCircle size="6" mr={ 2 } flexShrink={ 0 }/> <Skeleton w="50px"/>
<Skeleton w="100px" mr={ 2 }/> { showTxInfo && <Skeleton w="24px" ml="auto"/> }
<Skeleton w="50px"/> </Flex>
{ showTxInfo && <Skeleton w="24px" ml="auto"/> } <Flex h={ 6 } columnGap={ 2 }>
</Flex> <Skeleton w="70px"/>
<Flex h={ 6 } columnGap={ 2 }> <Skeleton w="24px"/>
<Skeleton w="70px"/> <Skeleton w="90px"/>
<Skeleton w="24px"/> </Flex>
<Skeleton w="90px"/> { showTxInfo && (
</Flex>
{ showTxInfo && (
<Flex h={ 6 } columnGap={ 2 }>
<Skeleton w="70px" flexShrink={ 0 }/>
<Skeleton w="100%"/>
</Flex>
) }
<Flex h={ 6 }>
<SkeletonCircle size="6" mr={ 2 } flexShrink={ 0 }/>
<Skeleton flexGrow={ 1 } mr={ 3 }/>
<Skeleton w="50px" mr={ 3 }/>
<SkeletonCircle size="6" mr={ 2 } flexShrink={ 0 }/>
<Skeleton flexGrow={ 1 } mr={ 3 }/>
</Flex>
<Flex h={ 6 } columnGap={ 2 }> <Flex h={ 6 } columnGap={ 2 }>
<Skeleton w="45px"/> <Skeleton w="70px" flexShrink={ 0 }/>
<Skeleton w="90px"/> <Skeleton w="100%"/>
</Flex> </Flex>
) }
<Flex h={ 6 }>
<SkeletonCircle size="6" mr={ 2 } flexShrink={ 0 }/>
<Skeleton flexGrow={ 1 } mr={ 3 }/>
<Skeleton w="50px" mr={ 3 }/>
<SkeletonCircle size="6" mr={ 2 } flexShrink={ 0 }/>
<Skeleton flexGrow={ 1 } mr={ 3 }/>
</Flex>
<Flex h={ 6 } columnGap={ 2 }>
<Skeleton w="45px"/>
<Skeleton w="90px"/>
</Flex> </Flex>
)) } </Flex>
</Box> )) }
</> </Box>
); );
}; };
......
...@@ -9,7 +9,7 @@ import Address from 'ui/shared/address/Address'; ...@@ -9,7 +9,7 @@ import Address from 'ui/shared/address/Address';
import AddressIcon from 'ui/shared/address/AddressIcon'; import AddressIcon from 'ui/shared/address/AddressIcon';
import AddressLink from 'ui/shared/address/AddressLink'; import AddressLink from 'ui/shared/address/AddressLink';
import InOutTag from 'ui/shared/InOutTag'; 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 { getTokenTransferTypeText } from 'ui/shared/TokenTransfer/helpers';
import TokenTransferNft from 'ui/shared/TokenTransfer/TokenTransferNft'; import TokenTransferNft from 'ui/shared/TokenTransfer/TokenTransferNft';
...@@ -18,7 +18,7 @@ type Props = TokenTransfer & { ...@@ -18,7 +18,7 @@ type Props = TokenTransfer & {
showTxInfo?: boolean; 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 = (() => { const value = (() => {
if (!('value' in total)) { if (!('value' in total)) {
return '-'; return '-';
...@@ -75,4 +75,4 @@ const TxInternalTableItem = ({ token, total, tx_hash: txHash, from, to, baseAddr ...@@ -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'; import type { TokenTransfer } from 'types/api/tokenTransfer';
export const flattenTotal = (result: Array<TokenTransfer>, item: TokenTransfer): Array<TokenTransfer> => { export const flattenTotal = (result: Array<TokenTransfer>, item: TokenTransfer): Array<TokenTransfer> => {
...@@ -24,3 +25,9 @@ export const getTokenTransferTypeText = (type: TokenTransfer['type']) => { ...@@ -24,3 +25,9 @@ export const getTokenTransferTypeText = (type: TokenTransfer['type']) => {
return 'Token transfer'; 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 { test, expect } from '@playwright/experimental-ct-react';
import React from 'react'; import React from 'react';
import RenderWithChakra from 'playwright/RenderWithChakra'; import TestApp from 'playwright/TestApp';
import Utilization from './Utilization'; import Utilization from './Utilization';
test.use({ viewport: { width: 100, height: 50 } }); 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( const component = await mount(
<RenderWithChakra> <TestApp>
<Utilization value={ 0.423 }/> <Utilization value={ 0.423 }/>
</RenderWithChakra>, </TestApp>,
); );
await expect(component).toHaveScreenshot(); 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( const component = await mount(
<RenderWithChakra colorMode="dark"> <TestApp>
<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">
<Utilization value={ 0.423 } colorScheme="gray"/> <Utilization value={ 0.423 } colorScheme="gray"/>
</RenderWithChakra>, </TestApp>,
); );
await expect(component).toHaveScreenshot(); await expect(component).toHaveScreenshot();
}); });
...@@ -4,7 +4,7 @@ import React from 'react'; ...@@ -4,7 +4,7 @@ import React from 'react';
import nftIcon from 'icons/nft_shield.svg'; import nftIcon from 'icons/nft_shield.svg';
import link from 'lib/link/link'; import link from 'lib/link/link';
import AddressLink from 'ui/shared/address/AddressLink'; import AddressLink from 'ui/shared/address/AddressLink';
import TokenSnippet from 'ui/shared/TokenSnippet'; import TokenSnippet from 'ui/shared/TokenSnippet/TokenSnippet';
interface Props { interface Props {
value: string; value: string;
...@@ -26,7 +26,7 @@ const NftTokenTransferSnippet = ({ value, name, hash, symbol, tokenId }: Props) ...@@ -26,7 +26,7 @@ const NftTokenTransferSnippet = ({ value, name, hash, symbol, tokenId }: Props)
<Link href={ url } fontWeight={ 600 }>{ tokenId }</Link> <Link href={ url } fontWeight={ 600 }>{ tokenId }</Link>
</Box> </Box>
{ name ? ( { name ? (
<TokenSnippet symbol={ symbol } hash={ hash } name={ name }/> <TokenSnippet symbol={ symbol } hash={ hash } name={ name } w="auto"/>
) : ( ) : (
<AddressLink hash={ hash } truncation="constant" type="token"/> <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'; ...@@ -27,7 +27,7 @@ import Utilization from 'ui/shared/Utilization/Utilization';
import TxDetailsSkeleton from 'ui/tx/details/TxDetailsSkeleton'; import TxDetailsSkeleton from 'ui/tx/details/TxDetailsSkeleton';
import TxDetailsTokenTransfers from 'ui/tx/details/TxDetailsTokenTransfers'; import TxDetailsTokenTransfers from 'ui/tx/details/TxDetailsTokenTransfers';
import TxRevertReason from 'ui/tx/details/TxRevertReason'; 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 TxSocketAlert from 'ui/tx/TxSocketAlert';
import useFetchTxInfo from 'ui/tx/useFetchTxInfo'; import useFetchTxInfo from 'ui/tx/useFetchTxInfo';
...@@ -58,7 +58,7 @@ const TxDetails = () => { ...@@ -58,7 +58,7 @@ const TxDetails = () => {
...data.from.watchlist_names || [], ...data.from.watchlist_names || [],
].map((tag) => <Tag key={ tag.label }>{ tag.display_name }</Tag>); ].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 = [ const addressToTags = [
...toAddress.private_tags || [], ...toAddress.private_tags || [],
...toAddress.public_tags || [], ...toAddress.public_tags || [],
...@@ -156,7 +156,7 @@ const TxDetails = () => { ...@@ -156,7 +156,7 @@ const TxDetails = () => {
<CopyToClipboard text={ toAddress.hash }/> <CopyToClipboard text={ toAddress.hash }/>
</Address> </Address>
) : ( ) : (
<Flex width="100%" whiteSpace="pre"> <Flex width={{ base: '100%', lg: 'auto' }} whiteSpace="pre">
<span>[Contract </span> <span>[Contract </span>
<AddressLink hash={ toAddress.hash }/> <AddressLink hash={ toAddress.hash }/>
<span> created]</span> <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();
});
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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