Commit 671443d5 authored by tom's avatar tom

Merge branch 'main' of github.com:blockscout/frontend into client-api-calls

parents 0bcef249 59b7b3bb
...@@ -11,7 +11,7 @@ ...@@ -11,7 +11,7 @@
"detail": "start local dev server", "detail": "start local dev server",
"presentation": { "presentation": {
"reveal": "silent", "reveal": "silent",
"panel": "dedicated", "panel": "shared",
"close": true, "close": true,
"revealProblems": "onProblem", "revealProblems": "onProblem",
}, },
...@@ -31,7 +31,7 @@ ...@@ -31,7 +31,7 @@
"detail": "start local dev server for POA network", "detail": "start local dev server for POA network",
"presentation": { "presentation": {
"reveal": "silent", "reveal": "silent",
"panel": "dedicated", "panel": "shared",
"close": true, "close": true,
"revealProblems": "onProblem", "revealProblems": "onProblem",
}, },
...@@ -51,7 +51,7 @@ ...@@ -51,7 +51,7 @@
"detail": "start local dev server for Goerli network", "detail": "start local dev server for Goerli network",
"presentation": { "presentation": {
"reveal": "silent", "reveal": "silent",
"panel": "dedicated", "panel": "shared",
"close": true, "close": true,
"revealProblems": "onProblem", "revealProblems": "onProblem",
}, },
...@@ -77,7 +77,7 @@ ...@@ -77,7 +77,7 @@
}, },
"presentation": { "presentation": {
"reveal": "never", "reveal": "never",
"panel": "dedicated", "panel": "shared",
"close": true, "close": true,
"revealProblems": "onProblem", "revealProblems": "onProblem",
}, },
...@@ -91,7 +91,7 @@ ...@@ -91,7 +91,7 @@
"detail": "run eslint", "detail": "run eslint",
"presentation": { "presentation": {
"reveal": "silent", "reveal": "silent",
"panel": "dedicated", "panel": "shared",
"revealProblems": "onProblem", "revealProblems": "onProblem",
}, },
"icon": { "icon": {
...@@ -112,7 +112,7 @@ ...@@ -112,7 +112,7 @@
"detail": "run visual components tests for current file", "detail": "run visual components tests for current file",
"presentation": { "presentation": {
"reveal": "always", "reveal": "always",
"panel": "dedicated", "panel": "shared",
"focus": true, "focus": true,
}, },
"icon": { "icon": {
...@@ -131,7 +131,7 @@ ...@@ -131,7 +131,7 @@
"detail": "run visual components tests for current file", "detail": "run visual components tests for current file",
"presentation": { "presentation": {
"reveal": "always", "reveal": "always",
"panel": "dedicated", "panel": "shared",
"focus": true, "focus": true,
}, },
"icon": { "icon": {
...@@ -150,7 +150,7 @@ ...@@ -150,7 +150,7 @@
"detail": "run visual components tests", "detail": "run visual components tests",
"presentation": { "presentation": {
"reveal": "always", "reveal": "always",
"panel": "dedicated", "panel": "shared",
"focus": true, "focus": true,
}, },
"icon": { "icon": {
...@@ -171,7 +171,7 @@ ...@@ -171,7 +171,7 @@
"detail": "run jest tests", "detail": "run jest tests",
"presentation": { "presentation": {
"reveal": "always", "reveal": "always",
"panel": "dedicated", "panel": "shared",
"focus": true, "focus": true,
}, },
"icon": { "icon": {
...@@ -190,7 +190,7 @@ ...@@ -190,7 +190,7 @@
"detail": "run jest tests in watch mode", "detail": "run jest tests in watch mode",
"presentation": { "presentation": {
"reveal": "always", "reveal": "always",
"panel": "dedicated", "panel": "shared",
"close": true, "close": true,
"focus": true, "focus": true,
}, },
...@@ -210,7 +210,7 @@ ...@@ -210,7 +210,7 @@
"detail": "run jest tests in watch mode for current file", "detail": "run jest tests in watch mode for current file",
"presentation": { "presentation": {
"reveal": "always", "reveal": "always",
"panel": "dedicated", "panel": "shared",
"focus": true, "focus": true,
}, },
"icon": { "icon": {
...@@ -230,7 +230,7 @@ ...@@ -230,7 +230,7 @@
"detail": "build docker image", "detail": "build docker image",
"presentation": { "presentation": {
"reveal": "always", "reveal": "always",
"panel": "dedicated", "panel": "shared",
"close": true, "close": true,
"revealProblems": "onProblem", "revealProblems": "onProblem",
"focus": true, "focus": true,
...@@ -251,7 +251,7 @@ ...@@ -251,7 +251,7 @@
"detail": "run docker container for POA network", "detail": "run docker container for POA network",
"presentation": { "presentation": {
"reveal": "silent", "reveal": "silent",
"panel": "dedicated", "panel": "shared",
"close": true, "close": true,
"revealProblems": "onProblem", "revealProblems": "onProblem",
}, },
...@@ -271,7 +271,7 @@ ...@@ -271,7 +271,7 @@
"detail": "format svg files with svgo", "detail": "format svg files with svgo",
"presentation": { "presentation": {
"reveal": "silent", "reveal": "silent",
"panel": "dedicated", "panel": "shared",
"close": true, "close": true,
"revealProblems": "onProblem", "revealProblems": "onProblem",
}, },
......
...@@ -11,6 +11,8 @@ NEXT_PUBLIC_FOOTER_GITHUB_LINK=https://github.com/blockscout/blockscout ...@@ -11,6 +11,8 @@ NEXT_PUBLIC_FOOTER_GITHUB_LINK=https://github.com/blockscout/blockscout
NEXT_PUBLIC_FOOTER_TWITTER_LINK=https://www.twitter.com/blockscoutcom NEXT_PUBLIC_FOOTER_TWITTER_LINK=https://www.twitter.com/blockscoutcom
NEXT_PUBLIC_HOMEPAGE_SHOW_AVG_BLOCK_TIME=true NEXT_PUBLIC_HOMEPAGE_SHOW_AVG_BLOCK_TIME=true
NEXT_PUBLIC_HOMEPAGE_SHOW_GAS_TRACKER=true NEXT_PUBLIC_HOMEPAGE_SHOW_GAS_TRACKER=true
NEXT_PUBLIC_NETWORK_LOGO=
NEXT_PUBLIC_NETWORK_SMALL_LOGO=
# api config # api config
NEXT_PUBLIC_API_HOST=blockscout.com NEXT_PUBLIC_API_HOST=blockscout.com
......
...@@ -35,7 +35,31 @@ const oldUrls = [ ...@@ -35,7 +35,31 @@ const oldUrls = [
}, },
{ {
oldPath: '/block/:id/transactions', oldPath: '/block/:id/transactions',
newPath: `${ PATHS.block }?tab=txs`, newPath: `${ PATHS.block }`,
},
{
oldPath: '/address/:id/transactions',
newPath: `${ PATHS.address_index }`,
},
{
oldPath: '/address/:id/token-transfers',
newPath: `${ PATHS.address_index }?tab=token_transfers`,
},
{
oldPath: '/address/:id/tokens',
newPath: `${ PATHS.address_index }?tab=tokens`,
},
{
oldPath: '/address/:id/internal-transactions',
newPath: `${ PATHS.address_index }?tab=internal_txns`,
},
{
oldPath: '/address/:id/coin-balances',
newPath: `${ PATHS.address_index }?tab=coin_balance_history`,
},
{
oldPath: '/address/:id/validations',
newPath: `${ PATHS.address_index }?tab=blocks_validated`,
}, },
]; ];
......
...@@ -28,6 +28,7 @@ blockscout: ...@@ -28,6 +28,7 @@ blockscout:
- 'nginx.ingress.kubernetes.io/cors-allow-credentials: "true"' - 'nginx.ingress.kubernetes.io/cors-allow-credentials: "true"'
- 'nginx.ingress.kubernetes.io/cors-allow-methods: PUT, GET, POST, OPTIONS, DELETE, PATCH' - 'nginx.ingress.kubernetes.io/cors-allow-methods: PUT, GET, POST, OPTIONS, DELETE, PATCH'
- 'nginx.ingress.kubernetes.io/enable-cors: "true"' - 'nginx.ingress.kubernetes.io/enable-cors: "true"'
- 'nginx.ingress.kubernetes.io/cors-allow-headers: "DNT,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization,x-csrf-token"'
host: host:
_default: blockscout.test.blockscout.aws-k8s.blockscout.com _default: blockscout.test.blockscout.aws-k8s.blockscout.com
# enable https # enable https
......
...@@ -395,4 +395,5 @@ frontend: ...@@ -395,4 +395,5 @@ frontend:
NEXT_PUBLIC_APP_HOST: NEXT_PUBLIC_APP_HOST:
_default: blockscout.com _default: blockscout.com
NEXT_PUBLIC_NETWORK_EXPLORERS: NEXT_PUBLIC_NETWORK_EXPLORERS:
_default: "[{'title':'Anyblock','baseUrl':'https://explorer.anyblock.tools','paths':{'tx':'/ethereum/poa/core/transaction','address':'/ethereum/poa/core/address'}}]" _default: "[{'title':'Anyblock','baseUrl':'https://explorer.anyblock.tools','paths':{'tx':'/ethereum/ethereum/goerli/transaction','address':'/ethereum/ethereum/goerli/address'}},{'title':'Etherscan','baseUrl':'https://goerli.etherscan.io/','paths':{'tx':'/tx','address':'/address'}}]"
<svg viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="m12.18 3.765-1.944-1.944a1.674 1.674 0 0 0-1.179-.488H4.333c-.92 0-1.666.746-1.666 1.667v10c0 .92.746 1.667 1.667 1.667H11c.917 0 1.667-.75 1.667-1.667V4.943c0-.44-.175-.865-.487-1.178ZM11.417 13c0 .23-.187.417-.417.417H4.334A.417.417 0 0 1 3.917 13V3.003c0-.23.186-.416.416-.416H8.5v2.08c0 .46.373.833.833.833h2.06V13h.024ZM8.273 8.938a.365.365 0 0 0-.303.162l-1.032 1.55-.305-.456a.363.363 0 0 0-.606-.001l-1.215 1.823a.364.364 0 0 0-.018.374c.063.12.186.192.297.192h5.104a.365.365 0 0 0 .304-.566L8.554 9.1c-.044-.103-.158-.162-.28-.162ZM6 8.833a.833.833 0 1 0 0-1.666.833.833 0 0 0 0 1.666Z" fill="currentColor"/>
</svg>
<svg viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2.667 2A.667.667 0 0 0 2 2.667V6a.667.667 0 0 0 1.333 0V3.333H6A.667.667 0 1 0 6 2H2.667Zm0 12A.667.667 0 0 1 2 13.333V10a.667.667 0 0 1 1.333 0v2.667H6A.667.667 0 1 1 6 14H2.667ZM14 2.667A.667.667 0 0 0 13.333 2H10a.667.667 0 0 0 0 1.333h2.667V6A.667.667 0 1 0 14 6V2.667ZM13.333 14a.667.667 0 0 0 .667-.667V10a.667.667 0 0 0-1.333 0v2.667H10A.667.667 0 1 0 10 14h3.333Z" fill="currentColor"/>
</svg>
...@@ -9,5 +9,5 @@ export default function getBlockTotalReward(block: Block) { ...@@ -9,5 +9,5 @@ export default function getBlockTotalReward(block: Block) {
?.map(({ reward }) => BigNumber(reward)) ?.map(({ reward }) => BigNumber(reward))
.reduce((result, item) => result.plus(item), ZERO) || ZERO; .reduce((result, item) => result.plus(item), ZERO) || ZERO;
return totalReward.div(WEI).toFixed(); return totalReward.div(WEI);
} }
import type { Types } from 'typescript-cookie'; import Cookies from 'js-cookie';
import { Cookies } from 'typescript-cookie';
import isBrowser from './isBrowser'; import isBrowser from './isBrowser';
...@@ -14,10 +13,13 @@ export function get(name?: NAMES | undefined | null, serverCookie?: string) { ...@@ -14,10 +13,13 @@ export function get(name?: NAMES | undefined | null, serverCookie?: string) {
if (!isBrowser()) { if (!isBrowser()) {
return serverCookie ? getFromCookieString(serverCookie, name) : undefined; return serverCookie ? getFromCookieString(serverCookie, name) : undefined;
} }
if (name) {
return Cookies.get(name); return Cookies.get(name);
}
} }
export function set(name: string, value: string, attributes: Types.CookieAttributes = {}) { export function set(name: string, value: string, attributes: Cookies.CookieAttributes = {}) {
attributes.path = '/'; attributes.path = '/';
return Cookies.set(name, value, attributes); return Cookies.set(name, value, attributes);
......
...@@ -36,6 +36,7 @@ export default function useQueryWithPages<QueryName extends PaginatedQueryKeys>( ...@@ -36,6 +36,7 @@ export default function useQueryWithPages<QueryName extends PaginatedQueryKeys>(
const fetch = useFetch(); const fetch = useFetch();
const isMounted = React.useRef(false); const isMounted = React.useRef(false);
const canGoBackwards = React.useRef(!router.query.page); const canGoBackwards = React.useRef(!router.query.page);
const [ hasPagination, setHasPagination ] = React.useState(page > 1);
const queryKey = [ queryName, ...(queryIds || []), { page, filters } ]; const queryKey = [ queryName, ...(queryIds || []), { page, filters } ];
...@@ -74,6 +75,7 @@ export default function useQueryWithPages<QueryName extends PaginatedQueryKeys>( ...@@ -74,6 +75,7 @@ export default function useQueryWithPages<QueryName extends PaginatedQueryKeys>(
const nextPageQuery = { ...router.query }; const nextPageQuery = { ...router.query };
Object.entries(nextPageParams).forEach(([ key, val ]) => nextPageQuery[key] = val.toString()); Object.entries(nextPageParams).forEach(([ key, val ]) => nextPageQuery[key] = val.toString());
nextPageQuery.page = String(page + 1); nextPageQuery.page = String(page + 1);
setHasPagination(true);
router.push({ pathname: router.pathname, query: nextPageQuery }, undefined, { shallow: true }) router.push({ pathname: router.pathname, query: nextPageQuery }, undefined, { shallow: true })
.then(() => { .then(() => {
...@@ -101,6 +103,7 @@ export default function useQueryWithPages<QueryName extends PaginatedQueryKeys>( ...@@ -101,6 +103,7 @@ export default function useQueryWithPages<QueryName extends PaginatedQueryKeys>(
setPage(prev => prev - 1); setPage(prev => prev - 1);
page === 2 && queryClient.removeQueries({ queryKey: [ queryName ] }); page === 2 && queryClient.removeQueries({ queryKey: [ queryName ] });
}); });
setHasPagination(true);
}, [ router, page, paginationFields, pageParams, queryClient, scrollToTop, queryName ]); }, [ router, page, paginationFields, pageParams, queryClient, scrollToTop, queryName ]);
const resetPage = useCallback(() => { const resetPage = useCallback(() => {
...@@ -118,6 +121,8 @@ export default function useQueryWithPages<QueryName extends PaginatedQueryKeys>( ...@@ -118,6 +121,8 @@ export default function useQueryWithPages<QueryName extends PaginatedQueryKeys>(
queryClient.removeQueries({ queryKey: [ queryName ], type: 'inactive' }); queryClient.removeQueries({ queryKey: [ queryName ], type: 'inactive' });
}, 100); }, 100);
}); });
setHasPagination(true);
}, [ queryClient, queryName, router, paginationFields, scrollToTop ]); }, [ queryClient, queryName, router, paginationFields, scrollToTop ]);
const onFilterChange = useCallback((newFilters: PaginationFilters<QueryName> | undefined) => { const onFilterChange = useCallback((newFilters: PaginationFilters<QueryName> | undefined) => {
...@@ -156,6 +161,8 @@ export default function useQueryWithPages<QueryName extends PaginatedQueryKeys>( ...@@ -156,6 +161,8 @@ export default function useQueryWithPages<QueryName extends PaginatedQueryKeys>(
canGoBackwards: canGoBackwards.current, canGoBackwards: canGoBackwards.current,
}; };
const isPaginationVisible = hasPagination || (!queryResult.isLoading && !queryResult.isError && pagination.hasNextPage);
React.useEffect(() => { React.useEffect(() => {
if (page !== 1 && isMounted.current) { if (page !== 1 && isMounted.current) {
queryClient.cancelQueries({ queryKey }); queryClient.cancelQueries({ queryKey });
...@@ -171,5 +178,5 @@ export default function useQueryWithPages<QueryName extends PaginatedQueryKeys>( ...@@ -171,5 +178,5 @@ export default function useQueryWithPages<QueryName extends PaginatedQueryKeys>(
}, 0); }, 0);
}, []); }, []);
return { ...queryResult, pagination, onFilterChange }; return { ...queryResult, pagination, onFilterChange, isPaginationVisible };
} }
...@@ -41,10 +41,11 @@ function getUpdateParams(ts: string) { ...@@ -41,10 +41,11 @@ function getUpdateParams(ts: string) {
}; };
} }
export default function useTimeAgoIncrement(ts: string, isEnabled?: boolean) { export default function useTimeAgoIncrement(ts: string | null, isEnabled?: boolean) {
const [ value, setValue ] = React.useState(dayjs(ts).fromNow()); const [ value, setValue ] = React.useState(ts ? dayjs(ts).fromNow() : null);
React.useEffect(() => { React.useEffect(() => {
if (ts !== null) {
const timeouts: Array<number> = []; const timeouts: Array<number> = [];
const intervals: Array<number> = []; const intervals: Array<number> = [];
...@@ -81,6 +82,7 @@ export default function useTimeAgoIncrement(ts: string, isEnabled?: boolean) { ...@@ -81,6 +82,7 @@ export default function useTimeAgoIncrement(ts: string, isEnabled?: boolean) {
timeouts.forEach(window.clearTimeout); timeouts.forEach(window.clearTimeout);
intervals.forEach(window.clearInterval); intervals.forEach(window.clearInterval);
}; };
}
}, [ isEnabled, ts ]); }, [ isEnabled, ts ]);
return value; return value;
......
...@@ -5,6 +5,7 @@ import type { NewBlockSocketResponse } from 'types/api/block'; ...@@ -5,6 +5,7 @@ import type { NewBlockSocketResponse } from 'types/api/block';
export type SocketMessageParams = SocketMessage.NewBlock | export type SocketMessageParams = SocketMessage.NewBlock |
SocketMessage.BlocksIndexStatus | SocketMessage.BlocksIndexStatus |
SocketMessage.InternalTxsIndexStatus |
SocketMessage.TxStatusUpdate | SocketMessage.TxStatusUpdate |
SocketMessage.NewTx | SocketMessage.NewTx |
SocketMessage.NewPendingTx | SocketMessage.NewPendingTx |
...@@ -23,7 +24,8 @@ interface SocketMessageParamsGeneric<Event extends string | undefined, Payload e ...@@ -23,7 +24,8 @@ interface SocketMessageParamsGeneric<Event extends string | undefined, Payload e
// eslint-disable-next-line @typescript-eslint/no-namespace // eslint-disable-next-line @typescript-eslint/no-namespace
export namespace SocketMessage { export namespace SocketMessage {
export type NewBlock = SocketMessageParamsGeneric<'new_block', NewBlockSocketResponse>; export type NewBlock = SocketMessageParamsGeneric<'new_block', NewBlockSocketResponse>;
export type BlocksIndexStatus = SocketMessageParamsGeneric<'index_status', {finished: boolean; ratio: string}>; export type BlocksIndexStatus = SocketMessageParamsGeneric<'block_index_status', {finished: boolean; ratio: string}>;
export type InternalTxsIndexStatus = SocketMessageParamsGeneric<'internal_txs_index_status', {finished: boolean; ratio: string}>;
export type TxStatusUpdate = SocketMessageParamsGeneric<'collated', NewBlockSocketResponse>; export type TxStatusUpdate = SocketMessageParamsGeneric<'collated', NewBlockSocketResponse>;
export type NewTx = SocketMessageParamsGeneric<'transaction', { transaction: number }>; export type NewTx = SocketMessageParamsGeneric<'transaction', { transaction: number }>;
export type NewPendingTx = SocketMessageParamsGeneric<'pending_transaction', { pending_transaction: number }>; export type NewPendingTx = SocketMessageParamsGeneric<'pending_transaction', { pending_transaction: number }>;
......
import type { FeaturedNetwork } from 'types/networks';
const FEATURED_NETWORKS: Array<FeaturedNetwork> = [
{ title: 'Gnosis Chain', url: 'https://blockscout.com/xdai/mainnet', group: 'mainnets', type: 'xdai_mainnet' },
{ title: 'Optimism on Gnosis Chain', url: 'https://blockscout.com/xdai/optimism', group: 'mainnets', type: 'xdai_optimism' },
{ title: 'Arbitrum on xDai', url: 'https://blockscout.com/xdai/aox', group: 'mainnets' },
{ title: 'Ethereum', url: 'https://blockscout.com/eth/mainnet', group: 'mainnets', type: 'eth_mainnet' },
{ title: 'Ethereum Classic', url: 'https://blockscout.com/etx/mainnet', group: 'mainnets', type: 'etc_mainnet', icon: 'https://example.com/my-logo.png' },
{ title: 'POA', url: 'https://blockscout.com/poa/core', group: 'mainnets', type: 'poa_core' },
{ title: 'RSK', url: 'https://blockscout.com/rsk/mainnet', group: 'mainnets', type: 'rsk_mainnet' },
{ title: 'Gnosis Chain Testnet', url: 'https://blockscout.com/xdai/testnet', group: 'testnets', type: 'xdai_testnet' },
{ title: 'POA Sokol', url: 'https://blockscout.com/poa/sokol', group: 'testnets', type: 'poa_sokol' },
{ title: 'ARTIS Σ1', url: 'https://blockscout.com/artis/sigma1', group: 'other', type: 'artis_sigma1' },
{ title: 'LUKSO L14', url: 'https://blockscout.com/lukso/l14', group: 'other', type: 'lukso_l14' },
{ title: 'Astar', url: 'https://blockscout.com/astar', group: 'other', type: 'astar' },
];
export const FEATURED_NETWORKS_MOCK = JSON.stringify(FEATURED_NETWORKS).replaceAll('"', '\'');
...@@ -37,6 +37,7 @@ export const erc20: TokenTransfer = { ...@@ -37,6 +37,7 @@ export const erc20: TokenTransfer = {
}, },
tx_hash: '0x62d597ebcf3e8d60096dd0363bc2f0f5e2df27ba1dacd696c51aa7c9409f3193', tx_hash: '0x62d597ebcf3e8d60096dd0363bc2f0f5e2df27ba1dacd696c51aa7c9409f3193',
type: 'token_transfer', type: 'token_transfer',
timestamp: '2022-10-10T14:34:30.000000Z',
}; };
export const erc721: TokenTransfer = { export const erc721: TokenTransfer = {
...@@ -75,6 +76,7 @@ export const erc721: TokenTransfer = { ...@@ -75,6 +76,7 @@ export const erc721: TokenTransfer = {
}, },
tx_hash: '0xf13bc7afe5e02b494dd2f22078381d36a4800ef94a0ccc147431db56c301e6cc', tx_hash: '0xf13bc7afe5e02b494dd2f22078381d36a4800ef94a0ccc147431db56c301e6cc',
type: 'token_transfer', type: 'token_transfer',
timestamp: '2022-10-10T14:34:30.000000Z',
}; };
export const erc1155: TokenTransfer = { export const erc1155: TokenTransfer = {
...@@ -115,6 +117,7 @@ export const erc1155: TokenTransfer = { ...@@ -115,6 +117,7 @@ export const erc1155: TokenTransfer = {
}, },
tx_hash: '0x05d6589367633c032d757a69c5fb16c0e33e3994b0d9d1483f82aeee1f05d746', tx_hash: '0x05d6589367633c032d757a69c5fb16c0e33e3994b0d9d1483f82aeee1f05d746',
type: 'token_minting', type: 'token_minting',
timestamp: '2022-10-10T14:34:30.000000Z',
}; };
export const erc1155multiple: TokenTransfer = { export const erc1155multiple: TokenTransfer = {
......
export const base = {
avatar: 'https://avatars.githubusercontent.com/u/22130104',
email: 'tom@ohhhh.me',
name: 'tom goriunov',
nickname: 'tom2drum',
};
import handler from 'lib/api/handler';
const getUrl = () => '/v2/main-page/indexing-status';
const requestHandler = handler(getUrl, [ 'GET' ]);
export default requestHandler;
...@@ -96,6 +96,15 @@ const config: PlaywrightTestConfig = { ...@@ -96,6 +96,15 @@ const config: PlaywrightTestConfig = {
colorScheme: 'dark', colorScheme: 'dark',
}, },
}, },
{
name: 'dark color mode desktop xl',
grep: /\+@dark-mode-xl/,
use: {
...devices['Desktop Chrome'],
viewport: { width: 1600, height: 1000 },
colorScheme: 'dark',
},
},
], ],
}; };
......
...@@ -2,6 +2,8 @@ import { ChakraProvider } from '@chakra-ui/react'; ...@@ -2,6 +2,8 @@ import { ChakraProvider } from '@chakra-ui/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import React from 'react'; import React from 'react';
import { AppContextProvider } from 'lib/appContext';
import type { Props as PageProps } from 'lib/next/getServerSideProps';
import { SocketProvider } from 'lib/socket/context'; import { SocketProvider } from 'lib/socket/context';
import { PORT } from 'playwright/fixtures/socketServer'; import { PORT } from 'playwright/fixtures/socketServer';
import theme from 'theme'; import theme from 'theme';
...@@ -9,9 +11,19 @@ import theme from 'theme'; ...@@ -9,9 +11,19 @@ import theme from 'theme';
type Props = { type Props = {
children: React.ReactNode; children: React.ReactNode;
withSocket?: boolean; withSocket?: boolean;
appContext?: {
pageProps: PageProps;
};
} }
const TestApp = ({ children, withSocket }: Props) => { const defaultAppContext = {
pageProps: {
cookies: '',
referrer: '',
},
};
const TestApp = ({ children, withSocket, appContext = defaultAppContext }: Props) => {
const [ queryClient ] = React.useState(() => new QueryClient({ const [ queryClient ] = React.useState(() => new QueryClient({
defaultOptions: { defaultOptions: {
queries: { queries: {
...@@ -25,7 +37,9 @@ const TestApp = ({ children, withSocket }: Props) => { ...@@ -25,7 +37,9 @@ const TestApp = ({ children, withSocket }: Props) => {
<ChakraProvider theme={ theme }> <ChakraProvider theme={ theme }>
<QueryClientProvider client={ queryClient }> <QueryClientProvider client={ queryClient }>
<SocketProvider url={ withSocket ? `ws://localhost:${ PORT }` : undefined }> <SocketProvider url={ withSocket ? `ws://localhost:${ PORT }` : undefined }>
<AppContextProvider { ...appContext }>
{ children } { children }
</AppContextProvider>
</SocketProvider> </SocketProvider>
</QueryClientProvider> </QueryClientProvider>
</ChakraProvider> </ChakraProvider>
......
import type { BrowserContext } from '@playwright/test';
import * as cookies from 'lib/cookies';
export default function authFixture(context: BrowserContext) {
context.addCookies([ { name: cookies.NAMES.API_TOKEN, value: 'foo', domain: 'localhost', path: '/' } ]);
}
...@@ -25,10 +25,8 @@ do ...@@ -25,10 +25,8 @@ do
configValue="$(cut -d'=' -f2- <<<"$line")"; configValue="$(cut -d'=' -f2- <<<"$line")";
# if there is a value, escape it and add line to target file # if there is a value, escape it and add line to target file
if [ -n "$configValue" ]; then
escapedConfigValue=$(echo $configValue | sed s/\'/\"/g); escapedConfigValue=$(echo $configValue | sed s/\'/\"/g);
echo "window.process.env.${configName} = localStorage.getItem('${configName}') || '${escapedConfigValue}';" >> $targetFile; echo "window.process.env.${configName} = localStorage.getItem('${configName}') || '${escapedConfigValue}';" >> $targetFile;
fi
done < $envFile done < $envFile
done done
......
...@@ -18,7 +18,7 @@ const baseStyle = definePartsStyle({ ...@@ -18,7 +18,7 @@ const baseStyle = definePartsStyle({
container: { container: {
borderRadius: 'md', borderRadius: 'md',
px: 6, px: 6,
py: 4, py: 3,
}, },
}); });
......
import { Button } from '@chakra-ui/react';
import { test, expect } from '@playwright/experimental-ct-react';
import React from 'react';
import TestApp from 'playwright/TestApp';
[
{ variant: 'solid' },
{ variant: 'solid', colorScheme: 'gray', withDarkMode: true },
{ variant: 'outline', colorScheme: 'gray', withDarkMode: true },
{ variant: 'outline', colorScheme: 'gray-dark', withDarkMode: true },
{ variant: 'outline', colorScheme: 'blue', withDarkMode: true },
{ variant: 'simple', withDarkMode: true },
{ variant: 'ghost', withDarkMode: true },
{ variant: 'subtle' },
{ variant: 'subtle', colorScheme: 'gray', withDarkMode: true },
].forEach(({ variant, colorScheme, withDarkMode }) => {
test.describe(`variant ${ variant }${ colorScheme ? ` with ${ colorScheme } color scheme` : '' }${ withDarkMode ? ' +@dark-mode' : '' }`, () => {
test('base', async({ mount }) => {
const component = await mount(
<TestApp>
<Button variant={ variant } colorScheme={ colorScheme }>Click me</Button>
</TestApp>,
);
await expect(component.locator('button')).toHaveScreenshot();
});
test('disabled', async({ mount }) => {
const component = await mount(
<TestApp>
<Button variant={ variant } colorScheme={ colorScheme } disabled>Click me</Button>
</TestApp>,
);
await expect(component.locator('button')).toHaveScreenshot();
});
test('hovered', async({ mount }) => {
const component = await mount(
<TestApp>
<Button variant={ variant } colorScheme={ colorScheme }>Click me</Button>
</TestApp>,
);
await component.getByText(/click/i).hover();
await expect(component.locator('button')).toHaveScreenshot();
});
test('active', async({ mount }) => {
const component = await mount(
<TestApp>
<Button variant={ variant } colorScheme={ colorScheme } isActive>Click me</Button>
</TestApp>,
);
await expect(component.locator('button')).toHaveScreenshot();
});
});
});
import { FormControl, Input, FormLabel } from '@chakra-ui/react';
import { test, expect } from '@playwright/experimental-ct-react';
import React from 'react';
import TestApp from 'playwright/TestApp';
test.use({ viewport: { width: 500, height: 300 } });
test.describe('floating label size md +@dark-mode', () => {
test('empty', async({ mount }) => {
const component = await mount(
<TestApp>
<FormControl variant="floating" id="name" isRequired size="md">
<Input required value=""/>
<FormLabel>Smart contract / Address (0x...)</FormLabel>
</FormControl>
</TestApp>,
);
await expect(component).toHaveScreenshot();
await component.locator('input').focus();
await expect(component).toHaveScreenshot();
});
test('empty error', async({ mount }) => {
const component = await mount(
<TestApp>
<FormControl variant="floating" id="name" isRequired size="md">
<Input required value="" isInvalid/>
<FormLabel>Smart contract / Address (0x...)</FormLabel>
</FormControl>
</TestApp>,
);
await expect(component).toHaveScreenshot();
await component.locator('input').focus();
await expect(component).toHaveScreenshot();
});
test('filled', async({ mount }) => {
const component = await mount(
<TestApp>
<FormControl variant="floating" id="name" isRequired size="md">
<Input required value="foo"/>
<FormLabel>Smart contract / Address (0x...)</FormLabel>
</FormControl>
</TestApp>,
);
await expect(component).toHaveScreenshot();
});
test('filled disabled', async({ mount }) => {
const component = await mount(
<TestApp>
<FormControl variant="floating" id="name" isRequired size="md">
<Input required value="foo" isDisabled/>
<FormLabel>Smart contract / Address (0x...)</FormLabel>
</FormControl>
</TestApp>,
);
await expect(component).toHaveScreenshot();
});
test('filled error', async({ mount }) => {
const component = await mount(
<TestApp>
<FormControl variant="floating" id="name" isRequired size="md">
<Input required value="foo" isInvalid/>
<FormLabel>Smart contract / Address (0x...)</FormLabel>
</FormControl>
</TestApp>,
);
await expect(component).toHaveScreenshot();
});
});
test.describe('floating label size lg +@dark-mode', () => {
test('empty', async({ mount }) => {
const component = await mount(
<TestApp>
<FormControl variant="floating" id="name" isRequired size="lg">
<Input required value=""/>
<FormLabel>Smart contract / Address (0x...)</FormLabel>
</FormControl>
</TestApp>,
);
await expect(component).toHaveScreenshot();
await component.locator('input').focus();
await expect(component).toHaveScreenshot();
});
test('filled', async({ mount }) => {
const component = await mount(
<TestApp>
<FormControl variant="floating" id="name" isRequired size="lg">
<Input required value="foo"/>
<FormLabel>Smart contract / Address (0x...)</FormLabel>
</FormControl>
</TestApp>,
);
await expect(component).toHaveScreenshot();
await component.locator('input').focus();
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.
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.
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.
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.
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.
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