Commit e94d00e5 authored by isstuev's avatar isstuev
parents 3d043760 44ef777d
...@@ -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 },
......
...@@ -47,3 +47,5 @@ yarn-error.log* ...@@ -47,3 +47,5 @@ yarn-error.log*
/test-results/ /test-results/
/playwright-report/ /playwright-report/
/playwright/.cache/ /playwright/.cache/
/playwright/.browser/
/playwright/envs.js
...@@ -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": [],
"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": [], "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", "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
# app config
NEXT_PUBLIC_APP_HOST=blockscout.com
NEXT_PUBLIC_APP_INSTANCE=pw
NEXT_PUBLIC_APP_ENV=testing
# ui config
NEXT_PUBLIC_BLOCKSCOUT_VERSION=v4.1.7-beta
NEXT_PUBLIC_FOOTER_GITHUB_LINK=https://github.com/blockscout/blockscout
NEXT_PUBLIC_FOOTER_TWITTER_LINK=https://www.twitter.com/blockscoutcom
# api config
NEXT_PUBLIC_API_HOST=blockscout.com
const PATHS = require('../../lib/link/paths'); const PATHS = require('../../lib/link/paths.json');
const oldUrls = [ const oldUrls = [
{ {
......
...@@ -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'>;
} }
......
const paths = {
network_index: `/`,
watchlist: `/account/watchlist`,
private_tags: `/account/tag_address`,
public_tags: `/account/public_tags_request`,
api_keys: `/account/api_key`,
custom_abi: `/account/custom_abi`,
profile: `/auth/profile`,
txs: `/txs`,
tx: `/tx/:id`,
blocks: `/blocks`,
block: `/block/:id`,
tokens: `/tokens`,
token_index: `/token/:hash`,
token_instance_item: `/token/:hash/instance/:id`,
address_index: `/address/:id`,
address_contract_verification: `/address/:id/contract_verifications/new`,
apps: `/apps`,
app_index: `/apps/:id`,
search_results: `/search-results`,
other: `/search-results`,
auth: `/auth/auth0`,
stats: '/stats',
};
module.exports = paths;
{
"network_index": "/",
"watchlist": "/account/watchlist",
"private_tags": "/account/tag_address",
"public_tags": "/account/public_tags_request",
"api_keys": "/account/api_key",
"custom_abi": "/account/custom_abi",
"profile": "/auth/profile",
"txs": "/txs",
"tx": "/tx/:id",
"blocks": "/blocks",
"block": "/block/:id",
"tokens": "/tokens",
"token_index": "/token/:hash",
"token_instance_item": "/token/:hash/instance/:id",
"address_index": "/address/:id",
"address_contract_verification": "/address/:id/contract_verifications/new",
"apps": "/apps",
"app_index": "/apps/:id",
"search_results": "/search-results",
"other": "/search-results",
"auth": "/auth/auth0",
"stats": "/stats"
}
import appConfig from 'configs/app/config'; import appConfig from 'configs/app/config';
import PATHS from './paths.js'; import PATHS from './paths.json';
export interface Route { export interface Route {
pattern: string; pattern: string;
......
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',
};
...@@ -21,7 +21,8 @@ ...@@ -21,7 +21,8 @@
"lint:tsc": "./node_modules/.bin/tsc -p ./tsconfig.json", "lint:tsc": "./node_modules/.bin/tsc -p ./tsconfig.json",
"prepare": "husky install", "prepare": "husky install",
"format-svg": "./node_modules/.bin/svgo -r ./icons", "format-svg": "./node_modules/.bin/svgo -r ./icons",
"test:pw": "playwright test -c playwright-ct.config.ts", "test:pw": "./playwright/make-envs-script.sh && playwright test -c playwright-ct.config.ts",
"test:pw:local": "export NODE_PATH=$(pwd)/node_modules && yarn test:pw",
"test:pw:docker": "docker run --rm --network host -v $(pwd):/work/ -w /work/ -it mcr.microsoft.com/playwright:v1.28.0-focal ./playwright/run-tests.sh", "test:pw:docker": "docker run --rm --network host -v $(pwd):/work/ -w /work/ -it mcr.microsoft.com/playwright:v1.28.0-focal ./playwright/run-tests.sh",
"test:jest": "jest", "test:jest": "jest",
"test:jest:watch": "jest --watch" "test:jest:watch": "jest --watch"
...@@ -84,6 +85,7 @@ ...@@ -84,6 +85,7 @@
"ts-jest": "^29.0.3", "ts-jest": "^29.0.3",
"ts-node": "^10.9.1", "ts-node": "^10.9.1",
"typescript": "4.7.2", "typescript": "4.7.2",
"vite-plugin-svgr": "^2.2.2",
"vite-tsconfig-paths": "^3.5.2" "vite-tsconfig-paths": "^3.5.2"
}, },
"lint-staged": { "lint-staged": {
......
...@@ -9,7 +9,7 @@ import { Chakra } from 'lib/Chakra'; ...@@ -9,7 +9,7 @@ import { Chakra } from 'lib/Chakra';
import useConfigSentry from 'lib/hooks/useConfigSentry'; import useConfigSentry from 'lib/hooks/useConfigSentry';
import type { ErrorType } from 'lib/hooks/useFetch'; import type { ErrorType } from 'lib/hooks/useFetch';
import theme from 'theme'; import theme from 'theme';
import AppError from 'ui/shared/AppError'; import AppError from 'ui/shared/AppError/AppError';
import ErrorBoundary from 'ui/shared/ErrorBoundary'; import ErrorBoundary from 'ui/shared/ErrorBoundary';
function MyApp({ Component, pageProps }: AppProps) { function MyApp({ Component, pageProps }: AppProps) {
......
...@@ -27,7 +27,7 @@ import * as cookies from 'lib/cookies'; ...@@ -27,7 +27,7 @@ import * as cookies from 'lib/cookies';
import getNetworkTitle from 'lib/networks/getNetworkTitle'; import getNetworkTitle from 'lib/networks/getNetworkTitle';
import type { Props as ServerSidePropsCommon } from 'lib/next/getServerSideProps'; import type { Props as ServerSidePropsCommon } from 'lib/next/getServerSideProps';
import { getServerSideProps as getServerSidePropsCommon } from 'lib/next/getServerSideProps'; import { getServerSideProps as getServerSidePropsCommon } from 'lib/next/getServerSideProps';
import AppError from 'ui/shared/AppError'; import AppError from 'ui/shared/AppError/AppError';
import Page from 'ui/shared/Page/Page'; import Page from 'ui/shared/Page/Page';
type Props = ServerSidePropsCommon & { type Props = ServerSidePropsCommon & {
......
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';
......
import type { PlaywrightTestConfig } from '@playwright/experimental-ct-react'; import type { PlaywrightTestConfig } from '@playwright/experimental-ct-react';
import { devices } from '@playwright/experimental-ct-react'; import { devices } from '@playwright/experimental-ct-react';
import react from '@vitejs/plugin-react';
import svgr from 'vite-plugin-svgr';
import tsconfigPaths from 'vite-tsconfig-paths'; import tsconfigPaths from 'vite-tsconfig-paths';
/** /**
...@@ -10,7 +12,7 @@ const config: PlaywrightTestConfig = { ...@@ -10,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,
...@@ -42,15 +44,49 @@ const config: PlaywrightTestConfig = { ...@@ -42,15 +44,49 @@ const config: PlaywrightTestConfig = {
headless: true, headless: true,
ctViteConfig: { ctViteConfig: {
plugins: [ tsconfigPaths() ], plugins: [
tsconfigPaths(),
react(),
svgr({
exportAsDefault: true,
}),
],
}, },
}, },
/* 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.
...@@ -7,6 +7,7 @@ ...@@ -7,6 +7,7 @@
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>
<script type="module" src="/playwright/envs.js"></script>
<script type="module" src="/playwright/index.ts"></script> <script type="module" src="/playwright/index.ts"></script>
</body> </body>
</html> </html>
// 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 {};
#!/bin/bash
targetFile='./playwright/envs.js'
declare -a envFiles=('./configs/envs/.env.pw' './configs/envs/.env.poa_core')
touch $targetFile;
truncate -s 0 $targetFile;
echo "Creating script file with envs"
echo "window.process = { env: { } };" >> $targetFile;
for envFile in "${envFiles[@]}"
do
# read each env file
while read line; do
# if it is a comment or an empty line, continue to next one
if [ "${line:0:1}" == "#" ] || [ "${line}" == "" ]; then
continue
fi
# split by "=" sign to get variable name and value
configName="$(cut -d'=' -f1 <<<"$line")";
configValue="$(cut -d'=' -f2- <<<"$line")";
# if there is a value, escape it and add line to target file
if [ -n "$configValue" ]; then
escapedConfigValue=$(echo $configValue | sed s/\'/\"/g);
echo "window.process.env.${configName} = '${escapedConfigValue}';" >> $targetFile;
fi
done < $envFile
done
echo "Done"
\ No newline at end of file
#!/bin/sh #!/bin/sh
yarn install yarn install --modules-folder node_modules_linux
yarn test:pw export NODE_PATH=$(pwd)/node_modules_linux
\ No newline at end of file rm -rf ./playwright/.cache
yarn test:pw "$@"
\ 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,19 @@ const LatestBlocks = () => { ...@@ -78,15 +83,19 @@ const LatestBlocks = () => {
content = ( content = (
<> <>
{ statsQueryResult.isLoading && (
<Skeleton h="24px" w="170px" mb={{ base: 6, lg: 9 }}/>
) }
{ statsQueryResult.data?.network_utilization_percentage !== undefined && (
<Box mb={{ base: 6, lg: 9 }}> <Box mb={{ base: 6, lg: 9 }}>
<Text as="span" fontSize="sm"> <Text as="span" fontSize="sm">
Network utilization:{ nbsp } Network utilization:{ nbsp }
</Text> </Text>
{ /* Not implemented in API yet */ }
<Text as="span" fontSize="sm" color="blue.400" fontWeight={ 700 }> <Text as="span" fontSize="sm" color="blue.400" fontWeight={ 700 }>
43.8% { statsQueryResult.data?.network_utilization_percentage.toFixed(2) }%
</Text> </Text>
</Box> </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 React from 'react';
import TestApp from 'playwright/TestApp';
import AppError from './AppError';
test.use({ viewport: { width: 900, height: 400 } });
test('status code 404', async({ mount }) => {
const component = await mount(
<TestApp>
<AppError statusCode={ 404 }/>
</TestApp>,
);
await expect(component).toHaveScreenshot();
});
test('status code 500', async({ mount }) => {
const component = await mount(
<TestApp>
<AppError statusCode={ 500 }/>
</TestApp>,
);
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;
} }
......
...@@ -10,7 +10,7 @@ import useFetch from 'lib/hooks/useFetch'; ...@@ -10,7 +10,7 @@ import useFetch from 'lib/hooks/useFetch';
import useScrollDirection from 'lib/hooks/useScrollDirection'; import useScrollDirection from 'lib/hooks/useScrollDirection';
import { SocketProvider } from 'lib/socket/context'; import { SocketProvider } from 'lib/socket/context';
import ScrollDirectionContext from 'ui/ScrollDirectionContext'; import ScrollDirectionContext from 'ui/ScrollDirectionContext';
import AppError from 'ui/shared/AppError'; import AppError from 'ui/shared/AppError/AppError';
import ErrorBoundary from 'ui/shared/ErrorBoundary'; import ErrorBoundary from 'ui/shared/ErrorBoundary';
import PageContent from 'ui/shared/Page/PageContent'; import PageContent from 'ui/shared/Page/PageContent';
import Header from 'ui/snippets/header/Header'; import Header from 'ui/snippets/header/Header';
......
...@@ -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 (
<>
{ txHash !== undefined && <Skeleton mb={ 6 } h={ 6 } w="100%"/> }
<Box> <Box>
{ Array.from(Array(2)).map((item, index) => ( { Array.from(Array(2)).map((item, index) => (
<Flex <Flex
...@@ -51,7 +49,6 @@ const TokenTransferSkeletonMobile = ({ showTxInfo, txHash }: { showTxInfo?: bool ...@@ -51,7 +49,6 @@ const TokenTransferSkeletonMobile = ({ showTxInfo, txHash }: { showTxInfo?: bool
</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.
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