Commit 7734d09a authored by tom goriunov's avatar tom goriunov Committed by GitHub

Rootstock block fields updates (#1102)

* small fixes in blocks fields + config for rootstock

* add env for hidding fields in block views

* [skip ci] update docs
parent 1b98bfc1
......@@ -319,6 +319,7 @@
"poa_core",
"eth_goerli",
"eth",
"rootstock",
"localhost",
],
"default": "main"
......
......@@ -2,6 +2,7 @@ import type { NavItemExternal } from 'types/client/navigation-items';
import type { NetworkExplorer } from 'types/networks';
import type { ChainIndicatorId } from 'ui/home/indicators/types';
import * as views from './ui/views';
import { getEnvValue, parseEnvJson } from './utils';
// eslint-disable-next-line max-len
......@@ -34,6 +35,7 @@ const UI = Object.freeze({
showGasTracker: getEnvValue(process.env.NEXT_PUBLIC_HOMEPAGE_SHOW_GAS_TRACKER) === 'false' ? false : true,
showAvgBlockTime: getEnvValue(process.env.NEXT_PUBLIC_HOMEPAGE_SHOW_AVG_BLOCK_TIME) === 'false' ? false : true,
},
views,
indexingAlert: {
isHidden: getEnvValue(process.env.NEXT_PUBLIC_HIDE_INDEXING_ALERT),
},
......
import type { ArrayElement } from 'types/utils';
import { getEnvValue, parseEnvJson } from 'configs/app/utils';
export const BLOCK_FIELDS_IDS = [
'burnt_fees',
'total_reward',
] as const;
export type BlockFieldId = ArrayElement<typeof BLOCK_FIELDS_IDS>;
const blockHiddenFields = (() => {
const parsedValue = parseEnvJson<Array<BlockFieldId>>(getEnvValue(process.env.NEXT_PUBLIC_VIEWS_BLOCK_HIDDEN_FIELDS)) || [];
if (!Array.isArray(parsedValue)) {
return undefined;
}
const result = BLOCK_FIELDS_IDS.reduce((result, item) => {
result[item] = parsedValue.includes(item);
return result;
}, {} as Record<BlockFieldId, boolean>);
return result;
})();
const config = Object.freeze({
hiddenFields: blockHiddenFields,
});
export default config;
export { default as block } from './block';
......@@ -39,6 +39,8 @@ NEXT_PUBLIC_NETWORK_ICON_DARK=
## footer
NEXT_PUBLIC_GIT_TAG=v1.0.11
NEXT_PUBLIC_FOOTER_LINKS=
## views
NEXT_PUBLIC_VIEWS_BLOCK_HIDDEN_FIELDS=
## misc
NEXT_PUBLIC_NETWORK_EXPLORERS=[{'title':'Bitquery','baseUrl':'https://explorer.bitquery.io/','paths':{'tx':'/goerli/tx','address':'/goerli/address','token':'/goerli/token','block':'/goerli/block'}},{'title':'Etherscan','baseUrl':'https://goerli.etherscan.io/','paths':{'tx':'/tx','address':'/address','token':'/token','block':'/block'}}]
......
# Set of ENVs for Ethereum network explorer
# https://eth.blockscout.com/
# app configuration
NEXT_PUBLIC_APP_PROTOCOL=http
NEXT_PUBLIC_APP_HOST=localhost
NEXT_PUBLIC_APP_PORT=3000
# blockchain parameters
NEXT_PUBLIC_NETWORK_NAME=Rootstock Testnet
NEXT_PUBLIC_NETWORK_SHORT_NAME=Rootstock
NEXT_PUBLIC_NETWORK_ID=31
NEXT_PUBLIC_NETWORK_CURRENCY_NAME=tRBTC
NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL=tRBTC
NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS=18
NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE=validation
NEXT_PUBLIC_NETWORK_RPC_URL=https://public-node.testnet.rsk.co
# api configuration
NEXT_PUBLIC_API_HOST=rootstock-testnet.blockscout.com
NEXT_PUBLIC_API_BASE_PATH=/
# ui config
## homepage
NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs']
NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND="rgb(255, 145, 0)"
## sidebar
NEXT_PUBLIC_FEATURED_NETWORKS=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/featured-networks/rsk-testnet.json
NEXT_PUBLIC_NETWORK_LOGO=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-logos/rootstock.svg
NEXT_PUBLIC_NETWORK_ICON=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-icons/rootstock-short.svg
## footer
NEXT_PUBLIC_FOOTER_LINKS=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/footer-links/rootstock.json
## views
NEXT_PUBLIC_VIEWS_BLOCK_HIDDEN_FIELDS=['burnt_fees','total_reward']
## misc
# app features
NEXT_PUBLIC_APP_INSTANCE=local
NEXT_PUBLIC_APP_ENV=development
NEXT_PUBLIC_GRAPHIQL_TRANSACTION=0x97fa753626b8d44011d0b9f9a947c735f20b6e895efdee49d7cda76a50001017
NEXT_PUBLIC_API_SPEC_URL=https://raw.githubusercontent.com/blockscout/blockscout-api-v2-swagger/main/swagger.yaml
NEXT_PUBLIC_HAS_BEACON_CHAIN=false
NEXT_PUBLIC_STATS_API_HOST=https://stats-rsk-testnet.k8s.blockscout.com
NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.services.blockscout.com
......@@ -12,6 +12,8 @@ The app instance could be customized by passing following variables to NodeJS en
- [Homepage](ENVS.md#homepage)
- [Sidebar](ENVS.md#sidebar)
- [Footer](ENVS.md#footer)
- [Views](ENVS.md#views)
- [Block](ENVS.md#block-views)
- [Misc](ENVS.md#misc)
- [App features](ENVS.md#app-features)
- [My account](ENVS.md#my-account)
......@@ -130,6 +132,23 @@ The app version shown in the footer is derived from build-time ENV variables `NE
&nbsp;
### Views
#### Block views
| Variable | Type | Description | Compulsoriness | Default value | Example value |
| --- | --- | --- | --- | --- | --- |
| NEXT_PUBLIC_VIEWS_BLOCK_HIDDEN_FIELDS | `Array<BlockFieldId>` | Array of the block fields ids that should be hidden. See below the list of the possible id values. | - | - | `'["burnt_fees","total_reward"]'` |
##### Block fields list
| Id | Description |
| --- | --- |
| `burnt_fees` | Burnt fees |
| `total_reward` | Total block reward |
&nbsp;
### Misc
| Variable | Type| Description | Compulsoriness | Default value | Example value |
......
......@@ -19,3 +19,11 @@ export const featureEnvs = {
{ name: 'NEXT_PUBLIC_L2_WITHDRAWAL_URL', value: 'https://localhost:3102' },
],
};
export const viewsEnvs = {
block: {
hiddenFields: [
{ name: 'NEXT_PUBLIC_VIEWS_BLOCK_HIDDEN_FIELDS', value: '["burnt_fees", "total_reward"]' },
],
},
};
export type NextPublicEnvs = {
// network config
// app envs
NEXT_PUBLIC_APP_PROTOCOL?: 'http' | 'https';
NEXT_PUBLIC_APP_HOST: string;
NEXT_PUBLIC_APP_PORT?: string;
// blockchain parameters
NEXT_PUBLIC_NETWORK_NAME: string;
NEXT_PUBLIC_NETWORK_SHORT_NAME?: string;
NEXT_PUBLIC_NETWORK_ID: string;
......@@ -7,51 +12,49 @@ export type NextPublicEnvs = {
NEXT_PUBLIC_NETWORK_CURRENCY_NAME?: string;
NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL?: string;
NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS?: string;
NEXT_PUBLIC_NETWORK_ASSETS_PATHNAME?: string;
NEXT_PUBLIC_NETWORK_LOGO?: string;
NEXT_PUBLIC_NETWORK_LOGO_DARK?: string;
NEXT_PUBLIC_NETWORK_ICON?: string;
NEXT_PUBLIC_NETWORK_ICON_DARK?: string;
NEXT_PUBLIC_NETWORK_EXPLORERS?: string;
NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE?: 'validation' | 'mining';
NEXT_PUBLIC_IS_TESTNET?: 'true' | '';
// UI config
NEXT_PUBLIC_FEATURED_NETWORKS?: string;
NEXT_PUBLIC_OTHER_LINKS?: string;
NEXT_PUBLIC_FOOTER_LINKS?: string;
NEXT_PUBLIC_API_SPEC_URL?: string;
NEXT_PUBLIC_GRAPHIQL_TRANSACTION?: string;
NEXT_PUBLIC_WEB3_WALLETS?: string;
NEXT_PUBLIC_WEB3_DISABLE_ADD_TOKEN_TO_WALLET?: 'true' | 'false';
NEXT_PUBLIC_HIDE_INDEXING_ALERT?: 'true' | 'false';
// api envs
NEXT_PUBLIC_API_PROTOCOL?: 'http' | 'https';
NEXT_PUBLIC_API_HOST: string;
NEXT_PUBLIC_API_PORT?: string;
NEXT_PUBLIC_API_BASE_PATH?: string;
NEXT_PUBLIC_API_WEBSOCKET_PROTOCOL?: 'ws' | 'wss';
// Homepage config
// UI configuration envs
// homepage
NEXT_PUBLIC_HOMEPAGE_CHARTS?: string;
NEXT_PUBLIC_HOMEPAGE_PLATE_TEXT_COLOR?: string;
NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND?: string;
NEXT_PUBLIC_HOMEPAGE_SHOW_GAS_TRACKER?: 'true' | 'false';
NEXT_PUBLIC_HOMEPAGE_SHOW_AVG_BLOCK_TIME?: 'true' | 'false';
// sidebar
NEXT_PUBLIC_FEATURED_NETWORKS?: string;
NEXT_PUBLIC_OTHER_LINKS?: string;
NEXT_PUBLIC_NETWORK_LOGO?: string;
NEXT_PUBLIC_NETWORK_LOGO_DARK?: string;
NEXT_PUBLIC_NETWORK_ICON?: string;
NEXT_PUBLIC_NETWORK_ICON_DARK?: string;
// footer
NEXT_PUBLIC_FOOTER_LINKS?: string;
// views
NEXT_PUBLIC_VIEWS_BLOCK_HIDDEN_FIELDS?: string;
// misc
NEXT_PUBLIC_NETWORK_EXPLORERS?: string;
NEXT_PUBLIC_HIDE_INDEXING_ALERT?: 'true' | 'false';
// Text ads config
// features envs
NEXT_PUBLIC_API_SPEC_URL?: string;
NEXT_PUBLIC_GRAPHIQL_TRANSACTION?: string;
NEXT_PUBLIC_WEB3_WALLETS?: string;
NEXT_PUBLIC_WEB3_DISABLE_ADD_TOKEN_TO_WALLET?: 'true' | 'false';
NEXT_PUBLIC_AD_TEXT_PROVIDER?: 'coinzilla' | 'none';
// App config
NEXT_PUBLIC_APP_PROTOCOL?: 'http' | 'https';
NEXT_PUBLIC_APP_HOST: string;
NEXT_PUBLIC_APP_PORT?: string;
// API config
NEXT_PUBLIC_API_PROTOCOL?: 'http' | 'https';
NEXT_PUBLIC_API_HOST: string;
NEXT_PUBLIC_API_PORT?: string;
NEXT_PUBLIC_API_BASE_PATH?: string;
NEXT_PUBLIC_API_WEBSOCKET_PROTOCOL?: 'ws' | 'wss';
NEXT_PUBLIC_STATS_API_HOST?: string;
NEXT_PUBLIC_VISUALIZE_API_HOST?: string;
NEXT_PUBLIC_CONTRACT_INFO_API_HOST?: string;
// external services config
// external services envs
NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID?: string;
NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY?: string;
NEXT_PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID?: string;
......
......@@ -6,7 +6,9 @@ import type { Block } from 'types/api/block';
import type { ResourceError } from 'lib/api/resources';
import * as blockMock from 'mocks/blocks/block';
import contextWithEnvs from 'playwright/fixtures/contextWithEnvs';
import TestApp from 'playwright/TestApp';
import * as configs from 'playwright/utils/configs';
import BlockDetails from './BlockDetails';
......@@ -52,7 +54,12 @@ test('genesis block', async({ mount, page }) => {
await expect(component).toHaveScreenshot();
});
test('rootstock custom fields', async({ mount, page }) => {
const customFieldsTest = test.extend({
// eslint-disable-next-line @typescript-eslint/no-explicit-any
context: contextWithEnvs(configs.viewsEnvs.block.hiddenFields) as any,
});
customFieldsTest('rootstock custom fields', async({ mount, page }) => {
const query = {
data: blockMock.rootstock,
isLoading: false,
......
......@@ -15,7 +15,7 @@ import clockIcon from 'icons/clock.svg';
import flameIcon from 'icons/flame.svg';
import type { ResourceError } from 'lib/api/resources';
import getBlockReward from 'lib/block/getBlockReward';
import { WEI, WEI_IN_GWEI, ZERO } from 'lib/consts';
import { GWEI, WEI, WEI_IN_GWEI, ZERO } from 'lib/consts';
import dayjs from 'lib/date/dayjs';
import { space } from 'lib/html-entities';
import getNetworkValidatorTitle from 'lib/networks/getNetworkValidatorTitle';
......@@ -211,7 +211,7 @@ const BlockDetails = ({ query }: Props) => {
{ /* api doesn't return the block processing time yet */ }
{ /* <Text>{ dayjs.duration(block.minedIn, 'second').humanize(true) }</Text> */ }
</DetailsInfoItem>
{ !config.features.rollup.isEnabled && !totalReward.isEqualTo(ZERO) && (
{ !config.features.rollup.isEnabled && !totalReward.isEqualTo(ZERO) && !config.UI.views.block.hiddenFields?.total_reward && (
<DetailsInfoItem
title="Block reward"
hint={
......@@ -280,7 +280,7 @@ const BlockDetails = ({ query }: Props) => {
isLoading={ isPlaceholderData }
>
<Skeleton isLoaded={ !isPlaceholderData }>
{ BigNumber(data.minimum_gas_price).toFormat() }
{ BigNumber(data.minimum_gas_price).dividedBy(GWEI).toFormat() } Gwei
</Skeleton>
</DetailsInfoItem>
) }
......@@ -302,31 +302,33 @@ const BlockDetails = ({ query }: Props) => {
) }
</DetailsInfoItem>
) }
<DetailsInfoItem
title="Burnt fees"
hint={
`Amount of ${ config.chain.currency.symbol || 'native token' } burned from transactions included in the block.
{ !config.UI.views.block.hiddenFields?.burnt_fees && (
<DetailsInfoItem
title="Burnt fees"
hint={
`Amount of ${ config.chain.currency.symbol || 'native token' } burned from transactions included in the block.
Equals Block Base Fee per Gas * Gas Used`
}
isLoading={ isPlaceholderData }
>
<Icon as={ flameIcon } boxSize={ 5 } color="gray.500" isLoading={ isPlaceholderData }/>
<Skeleton isLoaded={ !isPlaceholderData } ml={ 1 }>
{ burntFees.dividedBy(WEI).toFixed() } { config.chain.currency.symbol }
</Skeleton>
{ !txFees.isEqualTo(ZERO) && (
<Tooltip label="Burnt fees / Txn fees * 100%">
<Box>
<Utilization
ml={ 4 }
value={ burntFees.dividedBy(txFees).toNumber() }
isLoading={ isPlaceholderData }
/>
</Box>
</Tooltip>
) }
</DetailsInfoItem>
}
isLoading={ isPlaceholderData }
>
<Icon as={ flameIcon } boxSize={ 5 } color="gray.500" isLoading={ isPlaceholderData }/>
<Skeleton isLoaded={ !isPlaceholderData } ml={ 1 }>
{ burntFees.dividedBy(WEI).toFixed() } { config.chain.currency.symbol }
</Skeleton>
{ !txFees.isEqualTo(ZERO) && (
<Tooltip label="Burnt fees / Txn fees * 100%">
<Box>
<Utilization
ml={ 4 }
value={ burntFees.dividedBy(txFees).toNumber() }
isLoading={ isPlaceholderData }
/>
</Box>
</Tooltip>
) }
</DetailsInfoItem>
) }
{ data.priority_fee !== null && BigNumber(data.priority_fee).gt(ZERO) && (
<DetailsInfoItem
title="Priority fee / Tip"
......@@ -467,12 +469,14 @@ const BlockDetails = ({ query }: Props) => {
>
<Text wordBreak="break-all" whiteSpace="break-spaces">{ data.state_root }</Text>
</DetailsInfoItem> */ }
<DetailsInfoItem
title="Nonce"
hint="Block nonce is a value used during mining to demonstrate proof of work for a block"
>
{ data.nonce }
</DetailsInfoItem>
{ config.chain.verificationType !== 'validation' && (
<DetailsInfoItem
title="Nonce"
hint="Block nonce is a value used during mining to demonstrate proof of work for a block"
>
{ data.nonce }
</DetailsInfoItem>
) }
</>
) }
</Grid>
......
......@@ -89,7 +89,7 @@ const BlocksListItem = ({ data, isLoading, enableTimeIncrement }: Props) => {
) }
</Flex>
</Box>
{ !config.features.rollup.isEnabled && (
{ !config.features.rollup.isEnabled && !config.UI.views.block.hiddenFields?.total_reward && (
<Flex columnGap={ 2 }>
<Text fontWeight={ 500 }>Reward { config.chain.currency.symbol }</Text>
<Skeleton isLoaded={ !isLoading } display="inline-block" color="text_secondary">
......@@ -97,7 +97,7 @@ const BlocksListItem = ({ data, isLoading, enableTimeIncrement }: Props) => {
</Skeleton>
</Flex>
) }
{ !config.features.rollup.isEnabled && (
{ !config.features.rollup.isEnabled && !config.UI.views.block.hiddenFields?.burnt_fees && (
<Box>
<Text fontWeight={ 500 }>Burnt fees</Text>
<Flex columnGap={ 4 } mt={ 2 }>
......
......@@ -28,8 +28,10 @@ const BlocksTable = ({ data, isLoading, top, page }: Props) => {
<Th width={ config.features.rollup.isEnabled ? '37%' : '21%' } minW="144px">{ capitalize(getNetworkValidatorTitle()) }</Th>
<Th width="64px" isNumeric>Txn</Th>
<Th width={ config.features.rollup.isEnabled ? '63%' : '35%' }>Gas used</Th>
{ !config.features.rollup.isEnabled && <Th width="22%">Reward { config.chain.currency.symbol }</Th> }
{ !config.features.rollup.isEnabled && <Th width="22%">Burnt fees { config.chain.currency.symbol }</Th> }
{ !config.features.rollup.isEnabled && !config.UI.views.block.hiddenFields?.total_reward &&
<Th width="22%">Reward { config.chain.currency.symbol }</Th> }
{ !config.features.rollup.isEnabled && !config.UI.views.block.hiddenFields?.burnt_fees &&
<Th width="22%">Burnt fees { config.chain.currency.symbol }</Th> }
</Tr>
</Thead>
<Tbody>
......
......@@ -88,7 +88,7 @@ const BlocksTableItem = ({ data, isLoading, enableTimeIncrement }: Props) => {
</Skeleton>
) : data.tx_count }
</Td>
{ !config.features.rollup.isEnabled && (
{ !config.features.rollup.isEnabled && !config.UI.views.block.hiddenFields?.total_reward && (
<Td fontSize="sm">
<Skeleton isLoaded={ !isLoading } display="inline-block">{ BigNumber(data.gas_used || 0).toFormat() }</Skeleton>
<Flex mt={ 2 }>
......@@ -115,7 +115,7 @@ const BlocksTableItem = ({ data, isLoading, enableTimeIncrement }: Props) => {
{ totalReward.toFixed(8) }
</Skeleton>
</Td>
{ !config.features.rollup.isEnabled && (
{ !config.features.rollup.isEnabled && !config.UI.views.block.hiddenFields?.burnt_fees && (
<Td fontSize="sm">
<Flex alignItems="center" columnGap={ 1 }>
<Icon as={ flameIcon } boxSize={ 5 } color={ burntFeesIconColor } isLoading={ isLoading }/>
......
......@@ -18,10 +18,6 @@ import StatsItem from './StatsItem';
const hasGasTracker = config.UI.homepage.showGasTracker;
const hasAvgBlockTime = config.UI.homepage.showAvgBlockTime;
let itemsCount = 5;
!hasGasTracker && itemsCount--;
!hasAvgBlockTime && itemsCount--;
const Stats = () => {
const { data, isPlaceholderData, isError } = useApiQuery('homepage_stats', {
queryOptions: {
......@@ -37,8 +33,13 @@ const Stats = () => {
const lastItemTouchStyle = { gridColumn: { base: 'span 2', lg: 'unset' } };
let itemsCount = 5;
!hasGasTracker && itemsCount--;
!hasAvgBlockTime && itemsCount--;
if (data) {
const isOdd = Boolean(hasGasTracker && !data.gas_prices ? (itemsCount - 1) % 2 : itemsCount % 2);
!data.gas_prices && itemsCount--;
const isOdd = Boolean(itemsCount % 2);
const gasLabel = hasGasTracker && data.gas_prices ? <StatsGasPrices gasPrices={ data.gas_prices }/> : null;
content = (
......
......@@ -4,9 +4,11 @@ import React from 'react';
import * as textAdMock from 'mocks/ad/textAd';
import * as blockMock from 'mocks/blocks/block';
import * as statsMock from 'mocks/stats/index';
import contextWithEnvs from 'playwright/fixtures/contextWithEnvs';
import * as socketServer from 'playwright/fixtures/socketServer';
import TestApp from 'playwright/TestApp';
import buildApiUrl from 'playwright/utils/buildApiUrl';
import * as configs from 'playwright/utils/configs';
import Blocks from './Blocks';
......@@ -61,8 +63,35 @@ test('base view +@dark-mode', async({ mount, page }) => {
await expect(component).toHaveScreenshot();
});
const hiddenFieldsTest = test.extend({
// eslint-disable-next-line @typescript-eslint/no-explicit-any
context: contextWithEnvs(configs.viewsEnvs.block.hiddenFields) as any,
});
hiddenFieldsTest('hidden fields', async({ mount, page }) => {
await page.route(BLOCKS_API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify(blockMock.baseListResponse),
}));
await page.route(STATS_API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify(statsMock.base),
}));
const component = await mount(
<TestApp>
<Blocks/>
</TestApp>,
{ hooksConfig },
);
await page.waitForResponse(BLOCKS_API_URL);
await expect(component).toHaveScreenshot();
});
test.describe('mobile', () => {
test.use({ viewport: devices['iPhone 13 Pro'].viewport });
test(' base view', async({ mount, page }) => {
await page.route(BLOCKS_API_URL, (route) => route.fulfill({
status: 200,
......@@ -83,6 +112,32 @@ test.describe('mobile', () => {
await expect(component).toHaveScreenshot();
});
const hiddenFieldsTest = test.extend({
// eslint-disable-next-line @typescript-eslint/no-explicit-any
context: contextWithEnvs(configs.viewsEnvs.block.hiddenFields) as any,
});
hiddenFieldsTest('hidden fields', async({ mount, page }) => {
await page.route(BLOCKS_API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify(blockMock.baseListResponse),
}));
await page.route(STATS_API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify(statsMock.base),
}));
const component = await mount(
<TestApp>
<Blocks/>
</TestApp>,
{ hooksConfig },
);
await page.waitForResponse(BLOCKS_API_URL);
await expect(component).toHaveScreenshot();
});
});
test('new item from socket', async({ mount, page, createSocket }) => {
......
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