Tests for @shared components (#1737)

* asset teaser test, story, useUserPreferences Jest mock

* fixtures & mocks

* more tests

* cleanup

* more tests

* more tests

* more tests

* more tests

* typing fix

* package updates

* reorg

* more tests

* more tests

* more tests

* more tests

* reorg, more tests

* reorg fixes

* more tests

* rebase fix

* subgraph query typing fix

* restore some storybook stories

* more tests

* more tests

* package lock fix

* graphql override
This commit is contained in:
Matthias Kretschmann 2022-11-04 13:52:40 +00:00 committed by GitHub
parent a042387804
commit 56f5e77c49
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
131 changed files with 4013 additions and 7390 deletions

View File

@ -0,0 +1,71 @@
import { Asset } from '@oceanprotocol/lib'
export const assetAquarius: Asset = {
'@context': ['https://w3id.org/did/v1'],
id: 'did:op:6654b0793765b269696cec8d2f0d077d9bbcdd3c4f033d941ab9684e8ad06630',
nftAddress: '0xbA5BA7B09e2FA1eb0258f647503F81D2Af5cb07d',
version: '4.1.0',
chainId: 5,
metadata: {
created: '2022-09-29T11:30:26Z',
updated: '2022-09-29T11:30:26Z',
type: 'dataset',
name: 'Testitest',
description: 'This is a test.',
tags: [],
author: 'Test User',
license: 'https://market.oceanprotocol.com/terms',
additionalInformation: {
termsAndConditions: true
}
},
services: [
{
id: 'dbc42f4c62d2452f8731fd023eacfae74e9c7a42fbd12ce84310f13342e4aab1',
type: 'access',
files:
'0x04022ef1afafe340f41b261ef721b8dd55dee094975cc70330803d760beef38871948ce572ff1c533d56cda2665749ed2eb8283e243ec5ee19011f510b6b263b2da0af537e3f1fdff7ddd90fa26c7a4761a6d26928bc1348a302634012aac7998e92c84456ab73e9a847120c44ebda15781787e8c382391b2eaefc8b8d36998f3998d1c4647f4f7bb28f4278093c1d231f66e78f81452049443b9e540aeb42ebbdc1b748c024eb10218532814736e241efa1c2a687685b4e2ea7a877685aa0ea325d1a8cf765d1b423b32d81ec3c3e22fc9c15c6b9b71f2862edaec4e4cf7c3a638ffc0ecb88ede3cabb511d4780543a53c001a95f42de1877796e13c997b57bc671507e92198934b4ea7c2e6554993388421253e8c2f10458dec872a7ebfa71b6e77ed359222c93261ba252028c5da06ccf8defcd529885b2125816325a47e23728b513',
datatokenAddress: '0x067e1E6ec580F3F0f6781679A4A5AB07A6464b08',
serviceEndpoint: 'https://v4.provider.goerli.oceanprotocol.com',
timeout: 604800
}
],
event: {
tx: '0x3e07a75c1cc5d4146222a93ab4319144e60ecca3ebfb8b15f1ff339d6f479dc9',
block: 7680195,
from: '0x903322C7E45A60d7c8C3EA236c5beA9Af86310c7',
contract: '0xbA5BA7B09e2FA1eb0258f647503F81D2Af5cb07d',
datetime: '2022-09-29T11:31:12'
},
nft: {
address: '0xbA5BA7B09e2FA1eb0258f647503F81D2Af5cb07d',
name: 'Ocean Data NFT',
symbol: 'OCEAN-NFT',
state: 0,
tokenURI:
'data:application/json;base64,eyJuYW1lIjoiT2NlYW4gRGF0YSBORlQiLCJzeW1ib2wiOiJPQ0VBTi1ORlQiLCJkZXNjcmlwdGlvbiI6IlRoaXMgTkZUIHJlcHJlc2VudHMgYW4gYXNzZXQgaW4gdGhlIE9jZWFuIFByb3RvY29sIHY0IGVjb3N5c3RlbS5cblxuVmlldyBvbiBPY2VhbiBNYXJrZXQ6IGh0dHBzOi8vbWFya2V0Lm9jZWFucHJvdG9jb2wuY29tL2Fzc2V0L2RpZDpvcDo2NjU0YjA3OTM3NjViMjY5Njk2Y2VjOGQyZjBkMDc3ZDliYmNkZDNjNGYwMzNkOTQxYWI5Njg0ZThhZDA2NjMwIiwiZXh0ZXJuYWxfdXJsIjoiaHR0cHM6Ly9tYXJrZXQub2NlYW5wcm90b2NvbC5jb20vYXNzZXQvZGlkOm9wOjY2NTRiMDc5Mzc2NWIyNjk2OTZjZWM4ZDJmMGQwNzdkOWJiY2RkM2M0ZjAzM2Q5NDFhYjk2ODRlOGFkMDY2MzAiLCJiYWNrZ3JvdW5kX2NvbG9yIjoiMTQxNDE0IiwiaW1hZ2VfZGF0YSI6ImRhdGE6aW1hZ2Uvc3ZnK3htbCwlM0Nzdmcgdmlld0JveD0nMCAwIDk5IDk5JyBmaWxsPSd1bmRlZmluZWQnIHhtbG5zPSdodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyclM0UlM0NwYXRoIGZpbGw9JyUyM2ZmNDA5Mjc3JyBkPSdNMCw5OUwwLDI5QzksMjUgMTksMjIgMjksMjFDMzgsMTkgNDksMTkgNjEsMjFDNzIsMjIgODUsMjUgOTksMjlMOTksOTlaJy8lM0UlM0NwYXRoIGZpbGw9JyUyM2ZmNDA5MmJiJyBkPSdNMCw5OUwwLDU1QzgsNDkgMTcsNDQgMjgsNDNDMzgsNDEgNTAsNDIgNjMsNDNDNzUsNDMgODcsNDIgOTksNDJMOTksOTlaJyUzRSUzQy9wYXRoJTNFJTNDcGF0aCBmaWxsPSclMjNmZjQwOTJmZicgZD0nTTAsOTlMMCw2OEMxMSw2NiAyMiw2NSAzMiw2N0M0MSw2OCA1MCw3MyA2MSw3NkM3MSw3OCA4NSw3OCA5OSw3OUw5OSw5OVonJTNFJTNDL3BhdGglM0UlM0Mvc3ZnJTNFIn0=',
owner: '0x99840Df5Cb42faBE0Feb8811Aaa4BC99cA6C84e0',
created: '2022-09-29T11:31:12'
},
datatokens: [
{
address: '0x067e1E6ec580F3F0f6781679A4A5AB07A6464b08',
name: 'Stupendous Orca Token',
symbol: 'STUORC-59',
serviceId:
'dbc42f4c62d2452f8731fd023eacfae74e9c7a42fbd12ce84310f13342e4aab1'
}
],
stats: {
orders: 22
// price: {
// value: 3231343254,
// tokenAddress: '0xCfDdA22C9837aE76E0faA845354f33C62E03653a',
// tokenSymbol: 'OCEAN'
// }
},
purgatory: {
state: false,
reason: ''
}
}

View File

@ -0,0 +1,26 @@
import { assetAquarius } from './assetAquarius'
export const asset: AssetExtended = {
...assetAquarius,
accessDetails: {
publisherMarketOrderFee: '0',
type: 'fixed',
addressOrId:
'0x00e3b740e4d8bf6e97010ecb5b14d1b7efc0913bfa291fcf5adb8eb9e6c29e93',
price: '3231343254',
isPurchasable: true,
isOwned: false,
validOrderTx: null,
baseToken: {
address: '0xcfdda22c9837ae76e0faa845354f33c62e03653a',
name: 'Ocean Token',
symbol: 'OCEAN',
decimals: 18
},
datatoken: {
address: '0x067e1e6ec580f3f0f6781679a4a5ab07a6464b08',
name: 'Stupendous Orca Token',
symbol: 'STUORC-59'
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,64 @@
export default {
prices: {
h2o: {
btc: 0.00009013,
cad: 2.38,
cny: 12.36,
eth: 0.00132713,
eur: 1.78,
gbp: 1.55,
hkd: 13.53,
inr: 141.78,
jpy: 253.21,
link: 0.24050954,
rub: 109.81,
sgd: 2.47,
usd: 1.72
},
'matic-network': {
btc: 0.0000414,
cad: 1.093,
cny: 5.67,
eth: 0.00061102,
eur: 0.816959,
gbp: 0.715112,
hkd: 6.21,
inr: 65.06,
jpy: 116.23,
link: 0.11072143,
rub: 50.4,
sgd: 1.14,
usd: 0.790922
},
ethereum: {
btc: 0.06775775,
cad: 1789.03,
cny: 9288.09,
eth: 1,
eur: 1337.1,
gbp: 1170.41,
hkd: 10161.71,
inr: 106490,
jpy: 190239,
link: 181.216,
rub: 82491,
sgd: 1859.58,
usd: 1294.49
},
'ocean-protocol': {
btc: 0.00000809,
cad: 0.213554,
cny: 1.11,
eth: 0.00011937,
eur: 0.159608,
gbp: 0.13971,
hkd: 1.21,
inr: 12.71,
jpy: 22.71,
link: 0.02163146,
rub: 9.85,
sgd: 0.221976,
usd: 0.154521
}
}
}

View File

@ -0,0 +1,41 @@
export const columns = [
{
name: 'Name',
selector: (row: any) => row.name,
maxWidth: '45rem',
grow: 1
},
{
name: 'Symbol',
selector: (row: any) => row.symbol,
maxWidth: '10rem'
},
{
name: 'Price',
selector: (row: any) => row.price,
right: true
}
]
export const data = [
{
name: 'Title asset',
symbol: 'DATA-70',
price: '1.011'
},
{
name: 'Title asset Title asset Title asset Title asset Title asset',
symbol: 'DATA-71',
price: '1.011'
},
{
name: 'Title asset',
symbol: 'DATA-72',
price: '1.011'
},
{
name: 'Title asset Title asset Title asset Title asset Title asset Title asset Title asset Title asset Title asset Title asset',
symbol: 'DATA-71',
price: '1.011'
}
]

View File

@ -0,0 +1,10 @@
export default {
debug: true,
currency: 'EUR',
locale: 'en-US',
chainIds: [5, 1, 137, 56, 1285, 246],
bookmarks: [],
privacyPolicySlug: '/privacy/en',
showPPC: true,
infiniteApproval: false
}

View File

@ -0,0 +1,33 @@
export default {
accountEns: 'jellymcjellyfish.eth',
accountEnsAvatar:
'https://metadata.ens.domains/mainnet/avatar/jellymcjellyfish.eth',
accountId: '0x99840Df5Cb42faBE0Feb8811Aaa4BC99cA6C84e0',
approvedBaseTokens: [
{
address: '0xcfdda22c9837ae76e0faa845354f33c62e03653a',
symbol: 'OCEAN',
name: 'Ocean Token',
decimals: 18
}
],
balance: { eth: '0', ocean: '1000' },
block: 7751969,
chainId: 5,
connect: jest.fn(),
isSupportedOceanNetwork: true,
isTestnet: true,
logout: jest.fn(),
networkData: { name: 'Görli', title: 'Ethereum Testnet Görli', chain: 'ETH' },
networkDisplayName: 'ETH Görli',
networkId: 5,
web3: { currentProvider: {} },
web3Loading: false,
web3Modal: { show: false, eventController: {}, connect: jest.fn() },
web3Provider: {},
web3ProviderInfo: {
id: 'injected',
name: 'MetaMask',
logo: ''
}
}

View File

@ -0,0 +1,3 @@
import { assets } from '../../__fixtures__/assetsWithAccessDetails'
export const getAccessDetailsForAssets = jest.fn().mockResolvedValue(assets)

View File

@ -0,0 +1 @@
export default true

View File

@ -1,7 +1,23 @@
import '@testing-library/jest-dom/extend-expect'
import './__mocks__/matchMedia'
import marketMetadataMock from './__mocks__/MarketMetadata'
import marketMetadata from './__fixtures__/marketMetadata'
import userPreferences from './__fixtures__/userPreferences'
import web3 from './__fixtures__/web3'
import { asset } from './__fixtures__/assetWithAccessDetails'
jest.mock('../../src/@context/MarketMetadata', () => ({
useMarketMetadata: () => marketMetadataMock
useMarketMetadata: () => marketMetadata
}))
jest.mock('../../src/@context/UserPreferences', () => ({
useUserPreferences: () => userPreferences
}))
jest.mock('../../src/@context/Web3', () => ({
useWeb3: () => web3
}))
jest.mock('../../../@context/Asset', () => ({
useAsset: () => ({ asset })
}))

7587
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -17,7 +17,7 @@
"format": "prettier --ignore-path .gitignore './**/*.{css,yml,js,ts,tsx,json}' --write",
"type-check": "tsc --noEmit",
"deploy:s3": "bash scripts/deploy-s3.sh",
"postinstall": "husky install && rm -r node_modules/apollo-language-server/node_modules/graphql",
"postinstall": "husky install",
"codegen:apollo": "apollo client:codegen --endpoint=https://v4.subgraph.goerli.oceanprotocol.com/subgraphs/name/oceanprotocol/ocean-subgraph --target typescript --tsFileExtension=d.ts --outputFlat src/@types/subgraph/",
"storybook": "cross-env NODE_ENV=test start-storybook -p 6006 --quiet",
"storybook:build": "cross-env NODE_ENV=test build-storybook"
@ -43,8 +43,6 @@
"gray-matter": "^4.0.3",
"is-url-superb": "^6.1.0",
"js-cookie": "^3.0.1",
"lodash.debounce": "^4.0.8",
"lodash.omit": "^4.5.0",
"match-sorter": "^6.3.1",
"myetherwallet-blockies": "^0.1.1",
"next": "12.3.1",
@ -72,8 +70,8 @@
"yup": "^0.32.11"
},
"devDependencies": {
"@storybook/addon-essentials": "^6.5.12",
"@storybook/builder-webpack5": "^6.5.12",
"@storybook/addon-essentials": "^6.5.13",
"@storybook/builder-webpack5": "^6.5.13",
"@storybook/manager-webpack5": "^6.5.13",
"@storybook/react": "^6.5.13",
"@svgr/webpack": "^6.5.1",
@ -81,26 +79,24 @@
"@testing-library/react": "^13.4.0",
"@types/js-cookie": "^3.0.2",
"@types/loadable__component": "^5.13.4",
"@types/lodash.debounce": "^4.0.7",
"@types/lodash.omit": "^4.5.7",
"@types/node": "^18.7.18",
"@types/node": "^18.8.5",
"@types/react": "^18.0.21",
"@types/react-dom": "^18.0.5",
"@types/react-modal": "^3.13.1",
"@types/react-paginate": "^7.1.1",
"@types/remove-markdown": "^0.3.1",
"@typescript-eslint/eslint-plugin": "^5.38.1",
"@typescript-eslint/parser": "^5.38.1",
"@typescript-eslint/eslint-plugin": "^5.42.0",
"@typescript-eslint/parser": "^5.42.0",
"apollo": "^2.34.0",
"cross-env": "^7.0.3",
"eslint": "^8.23.1",
"eslint": "^8.25.0",
"eslint-config-oceanprotocol": "^2.0.4",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-jest-dom": "^4.0.2",
"eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-react": "^7.31.8",
"eslint-plugin-react": "^7.31.10",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-testing-library": "^5.7.0",
"eslint-plugin-testing-library": "^5.7.2",
"https-browserify": "^1.0.0",
"husky": "^8.0.1",
"jest": "^29.1.2",
@ -111,14 +107,17 @@
"serve": "^14.0.1",
"stream-http": "^3.2.0",
"tsconfig-paths-webpack-plugin": "^4.0.0",
"typescript": "^4.8.3"
"typescript": "^4.8.4"
},
"overrides": {
"graphql": "15.8.0"
},
"repository": {
"type": "git",
"url": "https://github.com/oceanprotocol/market"
},
"engines": {
"node": ">=14"
"node": "16"
},
"browserslist": [
">0.2%",

View File

@ -323,7 +323,7 @@ export async function getAccessDetailsForAssets(
},
queryContext
)
tokenQueryResult.data?.tokens.forEach((token) => {
tokenQueryResult?.data?.tokens?.forEach((token) => {
const currentAsset = assetsExtended.find(
(asset) =>
asset.services[0].datatokenAddress.toLowerCase() === token.id

View File

@ -1,5 +1,5 @@
import { Asset, LoggerInstance } from '@oceanprotocol/lib'
import { AssetSelectionAsset } from '@shared/FormFields/AssetSelection'
import { AssetSelectionAsset } from '@shared/FormInput/InputElement/AssetSelection'
import axios, { CancelToken, AxiosResponse } from 'axios'
import { OrdersData_orders as OrdersData } from '../@types/subgraph/OrdersData'
import { metadataCacheUri } from '../../app.config'

View File

@ -1,6 +1,6 @@
import { getAccessDetailsForAssets } from './accessDetailsAndPricing'
import { PublisherTrustedAlgorithm, Asset } from '@oceanprotocol/lib'
import { AssetSelectionAsset } from '@shared/FormFields/AssetSelection'
import { AssetSelectionAsset } from '@shared/FormInput/InputElement/AssetSelection'
import { getServiceByName } from './ddo'
export async function transformAssetToAssetSelection(

View File

@ -21,10 +21,10 @@ import {
} from './aquarius'
import { fetchDataForMultipleChains } from './subgraph'
import { getServiceById, getServiceByName } from './ddo'
import { SortTermOptions } from 'src/@types/aquarius/SearchQuery'
import { AssetSelectionAsset } from '@shared/FormFields/AssetSelection'
import { SortTermOptions } from '../@types/aquarius/SearchQuery'
import { AssetSelectionAsset } from '@shared/FormInput/InputElement/AssetSelection'
import { transformAssetToAssetSelection } from './assetConvertor'
import { ComputeEditForm } from 'src/components/Asset/Edit/_types'
import { ComputeEditForm } from '../components/Asset/Edit/_types'
import { getFileDidInfo } from './provider'
const getComputeOrders = gql`

View File

@ -13,7 +13,7 @@ import {
} from '@hooks/useNetworkMetadata'
import { getAssetsFromNftList } from './aquarius'
import { chainIdsSupported } from 'app.config'
import { Asset, LoggerInstance } from '@oceanprotocol/lib'
import { Asset } from '@oceanprotocol/lib'
const AllLocked = gql`
query AllLocked {
@ -42,7 +42,7 @@ const NftOwnAllocation = gql`
}
`
const OceanLocked = gql`
query OceanLocked($address: String) {
query OceanLocked($address: ID!) {
veOCEAN(id: $address) {
id
lockedAmount

View File

@ -1,24 +0,0 @@
.accountList {
display: grid;
grid-template-columns: 1fr;
gap: calc(var(--spacer) / 2);
}
@media screen and (min-width: 25rem) {
.accountList {
grid-template-columns: repeat(auto-fit, minmax(20rem, 1fr));
gap: var(--spacer);
}
}
.empty {
color: var(--color-secondary);
font-size: var(--font-size-small);
font-style: italic;
}
.loaderWrap {
display: flex;
justify-content: center;
align-items: center;
}

View File

@ -0,0 +1,29 @@
import React from 'react'
import { ComponentStory, ComponentMeta } from '@storybook/react'
import AddToken, { AddTokenProps } from '@shared/AddToken'
export default {
title: 'Component/@shared/AddToken',
component: AddToken
} as ComponentMeta<typeof AddToken>
const Template: ComponentStory<typeof AddToken> = (args: AddTokenProps) => {
return <AddToken {...args} />
}
interface Props {
args: AddTokenProps
}
export const Default: Props = Template.bind({})
Default.args = {
address: '0xd8992Ed72C445c35Cb4A2be468568Ed1079357c8',
symbol: 'OCEAN'
}
export const Minimal: Props = Template.bind({})
Minimal.args = {
address: '0xd8992Ed72C445c35Cb4A2be468568Ed1079357c8',
symbol: 'OCEAN',
minimal: true
}

View File

@ -0,0 +1,24 @@
import { fireEvent, render, screen } from '@testing-library/react'
import React from 'react'
import testRender from '../../../../.jest/testRender'
import AddToken from './index'
jest.mock('../../../@utils/web3', () => ({ addTokenToWallet: jest.fn() }))
describe('@shared/AddToken', () => {
const propsBase = {
address: '0xd8992Ed72C445c35Cb4A2be468568Ed1079357c8',
symbol: 'OCEAN'
}
testRender(<AddToken {...propsBase} />)
it('renders with custom text', () => {
render(<AddToken {...propsBase} text="Hello Text" />)
expect(screen.getByText('Hello Text')).toBeInTheDocument()
fireEvent.click(screen.getByRole('button'))
})
it('renders minimal', () => {
render(<AddToken {...propsBase} minimal />)
})
})

View File

@ -8,19 +8,21 @@ import styles from './index.module.css'
const cx = classNames.bind(styles)
export interface AddTokenProps {
address: string
symbol: string
text?: string
className?: string
minimal?: boolean
}
export default function AddToken({
address,
symbol,
text,
className,
minimal
}: {
address: string
symbol: string
text?: string
className?: string
minimal?: boolean
}): ReactElement {
}: AddTokenProps): ReactElement {
const { web3Provider } = useWeb3()
const styleClasses = cx({

View File

@ -0,0 +1,65 @@
import React from 'react'
import { ComponentStory, ComponentMeta } from '@storybook/react'
import AddAnnouncementBanner, {
AnnouncementBannerProps
} from '@shared/AnnouncementBanner'
export default {
title: 'Component/@shared/AnnouncementBanner',
component: AddAnnouncementBanner
} as ComponentMeta<typeof AddAnnouncementBanner>
const Template: ComponentStory<typeof AddAnnouncementBanner> = (
args: AnnouncementBannerProps
) => <AddAnnouncementBanner {...args} />
interface Props {
args: AnnouncementBannerProps
}
export const Default: Props = Template.bind({})
Default.args = {
text: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce malesuada ipsum ac enim auctor placerat.',
action: {
name: 'see more',
handleAction: () => {
alert('Link clicked!')
}
}
}
export const Success: Props = Template.bind({})
Success.args = {
text: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce malesuada ipsum ac enim auctor placerat.',
state: 'success',
action: {
name: 'see more',
handleAction: () => {
alert('Link clicked!')
}
}
}
export const Warning: Props = Template.bind({})
Warning.args = {
text: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce malesuada ipsum ac enim auctor placerat.',
state: 'warning',
action: {
name: 'see more',
handleAction: () => {
alert('Link clicked!')
}
}
}
export const Error: Props = Template.bind({})
Error.args = {
text: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce malesuada ipsum ac enim auctor placerat.',
state: 'error',
action: {
name: 'see more',
handleAction: () => {
alert('Link clicked!')
}
}
}

View File

@ -0,0 +1,13 @@
import React from 'react'
import testRender from '../../../../.jest/testRender'
import AnnouncementBanner from './index'
describe('@shared/AnnouncementBanner', () => {
testRender(
<AnnouncementBanner
text="# Hello World!"
action={{ name: 'hello', handleAction: jest.fn() }}
state="success"
/>
)
})

View File

@ -12,17 +12,19 @@ export interface AnnouncementAction {
handleAction: () => void
}
export interface AnnouncementBannerProps {
text: string
action?: AnnouncementAction
state?: 'success' | 'warning' | 'error'
className?: string
}
export default function AnnouncementBanner({
text,
action,
state,
className
}: {
text: string
action?: AnnouncementAction
state?: 'success' | 'warning' | 'error'
className?: string
}): ReactElement {
}: AnnouncementBannerProps): ReactElement {
const styleClasses = cx({
banner: true,
error: state === 'error',

View File

@ -1,59 +0,0 @@
.display {
composes: selection from '@shared/FormFields/AssetSelection/index.module.css';
}
.display [class*='loaderWrap'] {
margin: calc(var(--spacer) / 3);
}
.scroll {
composes: scroll from '@shared/FormFields/AssetSelection/index.module.css';
margin-top: 0;
border-top: none;
width: 100%;
}
.row {
composes: row from '@shared/FormFields/AssetSelection/index.module.css';
}
.row:last-child {
border-bottom: none;
}
.row:first-child {
border-top: none;
}
.row:hover {
background-color: var(--background-content);
}
.info {
display: block;
width: 100%;
}
.title {
composes: title from '@shared/FormFields/AssetSelection/index.module.css';
}
.hover:hover {
color: var(--color-primary);
}
.price {
composes: price from '@shared/FormFields/AssetSelection/index.module.css';
}
.price [class*='symbol'] {
font-size: calc(var(--font-size-small) / 1.2) !important;
}
.did {
composes: did from '@shared/FormFields/AssetSelection/index.module.css';
}
.empty {
composes: empty from '@shared/FormFields/AssetSelection/index.module.css';
}

View File

@ -0,0 +1,35 @@
import React from 'react'
import { ComponentStory, ComponentMeta } from '@storybook/react'
import MarketMetadataProvider from '@context/MarketMetadata'
import { UserPreferencesProvider } from '@context/UserPreferences'
import AssetList, { AssetListProps } from '.'
import { assets } from '../../../../.jest/__fixtures__/assetsWithAccessDetails'
export default {
title: 'Component/@shared/AssetList',
component: AssetList
} as ComponentMeta<typeof AssetList>
const Template: ComponentStory<typeof AssetList> = (args: AssetListProps) => {
return (
<MarketMetadataProvider>
<UserPreferencesProvider>
<AssetList {...args} />
</UserPreferencesProvider>
</MarketMetadataProvider>
)
}
export const Default: { args: AssetListProps } = Template.bind({})
Default.args = {
assets,
showPagination: true,
page: 1,
totalPages: 10
}
export const Empty: { args: AssetListProps } = Template.bind({})
Empty.args = {
assets: [],
showPagination: false
}

View File

@ -0,0 +1,32 @@
import { render, screen, fireEvent } from '@testing-library/react'
import React from 'react'
import AssetList from './index'
import { assetAquarius } from '../../../../.jest/__fixtures__/assetAquarius'
describe('@shared/AssetList', () => {
it('renders without crashing', async () => {
const onPageChange = jest.fn()
render(
<AssetList
assets={[assetAquarius]}
showPagination
page={1}
totalPages={10}
onPageChange={onPageChange}
/>
)
await screen.findAllByText('OCEAN')
fireEvent.click(screen.getByLabelText('Page 2'))
expect(onPageChange).toBeCalled()
})
it('renders empty', async () => {
render(<AssetList assets={[]} showPagination={false} isLoading={false} />)
await screen.findByText('No results found')
})
it('renders loading', async () => {
render(<AssetList assets={[]} showPagination={false} isLoading />)
})
})

View File

@ -2,15 +2,11 @@ import AssetTeaser from '@shared/AssetTeaser'
import React, { ReactElement, useEffect, useState } from 'react'
import Pagination from '@shared/Pagination'
import styles from './index.module.css'
import classNames from 'classnames/bind'
import Loader from '@shared/atoms/Loader'
import { useUserPreferences } from '@context/UserPreferences'
import { useIsMounted } from '@hooks/useIsMounted'
import { getAccessDetailsForAssets } from '@utils/accessDetailsAndPricing'
import { useWeb3 } from '@context/Web3'
const cx = classNames.bind(styles)
function LoaderArea() {
return (
<div className={styles.loaderWrap}>
@ -19,7 +15,7 @@ function LoaderArea() {
)
}
declare type AssetListProps = {
export declare type AssetListProps = {
assets: AssetExtended[]
showPagination: boolean
page?: number
@ -40,14 +36,14 @@ export default function AssetList({
className,
noPublisher
}: AssetListProps): ReactElement {
const { chainIds } = useUserPreferences()
const { accountId } = useWeb3()
const [assetsWithPrices, setAssetsWithPrices] = useState<AssetExtended[]>()
const [assetsWithPrices, setAssetsWithPrices] =
useState<AssetExtended[]>(assets)
const [loading, setLoading] = useState<boolean>(isLoading)
const isMounted = useIsMounted()
useEffect(() => {
if (!assets) return
if (!assets || !assets.length) return
setAssetsWithPrices(assets as AssetExtended[])
setLoading(false)
@ -67,16 +63,9 @@ export default function AssetList({
onPageChange(selected + 1)
}
const styleClasses = cx({
assetList: true,
[className]: className
})
const styleClasses = `${styles.assetList} ${className || ''}`
return chainIds.length === 0 ? (
<div className={styleClasses}>
<div className={styles.empty}>No network selected</div>
</div>
) : assetsWithPrices && !loading ? (
return assetsWithPrices && !loading ? (
<>
<div className={styleClasses}>
{assetsWithPrices.length > 0 ? (

View File

@ -0,0 +1,24 @@
import React from 'react'
import testRender from '../../../../.jest/testRender'
import AssetListTitle from '.'
import { render } from '@testing-library/react'
jest.mock('../../../@utils/aquarius', () => ({
getAssetsNames: () => Promise.resolve('Test')
}))
describe('AssetListTitle', () => {
testRender(
<AssetListTitle asset={{ metadata: { name: 'Hello world' } } as any} />
)
it('renders with passed title', () => {
render(<AssetListTitle title="Hello Title" />)
})
it('renders with passed DID', () => {
render(
<AssetListTitle did="did:op:764b81877039fa2651b919fc91c399799acb837f270e6d17bfb7973fbe6e9408" />
)
})
})

View File

@ -1,7 +1,7 @@
import Link from 'next/link'
import React, { ReactElement, useEffect, useState } from 'react'
import { getAssetsNames } from '@utils/aquarius'
import styles from './AssetListTitle.module.css'
import styles from './index.module.css'
import axios from 'axios'
import { Asset } from '@oceanprotocol/lib'
import { useMarketMetadata } from '@context/MarketMetadata'

View File

@ -0,0 +1,8 @@
import React from 'react'
import testRender from '../../../../.jest/testRender'
import AssetTeaser from './index'
import { asset } from '../../../../.jest/__fixtures__/assetWithAccessDetails'
describe('@shared/AssetTeaser', () => {
testRender(<AssetTeaser asset={asset} />)
})

View File

@ -11,7 +11,7 @@ import { getServiceByName } from '@utils/ddo'
import { formatPrice } from '@shared/Price/PriceUnit'
import { useUserPreferences } from '@context/UserPreferences'
declare type AssetTeaserProps = {
export declare type AssetTeaserProps = {
asset: AssetExtended
noPublisher?: boolean
}

View File

@ -0,0 +1,7 @@
import React from 'react'
import testRender from '../../../../.jest/testRender'
import DebugOutput from './index'
describe('@shared/DebugOutput', () => {
testRender(<DebugOutput title="Debug" output="Hello Output" />)
})

View File

@ -0,0 +1,21 @@
import testRender from '../../../../.jest/testRender'
import { render, screen } from '@testing-library/react'
import React from 'react'
import ExplorerLink from './index'
describe('@shared/ExplorerLink', () => {
testRender(
<ExplorerLink networkId={1} path="/tx">
Hello Link
</ExplorerLink>
)
it('renders without networkId', () => {
render(
<ExplorerLink networkId={null} path="/tx">
Hello Link
</ExplorerLink>
)
expect(screen.getByRole('link')).toHaveTextContent('Hello Link')
})
})

View File

@ -1,12 +1,9 @@
import React, { ReactElement, ReactNode, useEffect, useState } from 'react'
import External from '@images/external.svg'
import classNames from 'classnames/bind'
import { Config } from '@oceanprotocol/lib'
import styles from './index.module.css'
import { getOceanConfig } from '@utils/ocean'
const cx = classNames.bind(styles)
export default function ExplorerLink({
networkId,
path,
@ -20,10 +17,6 @@ export default function ExplorerLink({
}): ReactElement {
const [url, setUrl] = useState<string>()
const [oceanConfig, setOceanConfig] = useState<Config>()
const styleClasses = cx({
link: true,
[className]: className
})
useEffect(() => {
if (!networkId) return
@ -39,7 +32,7 @@ export default function ExplorerLink({
title={`View on ${oceanConfig?.explorerUri}`}
target="_blank"
rel="noreferrer"
className={styleClasses}
className={`${styles.link} ${className || ''}`}
>
{children} <External />
</a>

View File

@ -0,0 +1,37 @@
import testRender from '../../../../.jest/testRender'
import { FileInfo } from '@oceanprotocol/lib'
import { render } from '@testing-library/react'
import React from 'react'
import FileIcon from './index'
describe('@shared/FileIcon', () => {
const file: FileInfo = {
type: 'url',
contentType: 'text/plain',
contentLength: '123'
}
testRender(<FileIcon file={file} />)
it('renders small', () => {
render(<FileIcon file={file} small />)
})
it('renders loading', () => {
render(<FileIcon file={file} isLoading />)
})
it('renders empty', () => {
const file: FileInfo = { type: 'url' }
render(<FileIcon file={file} />)
})
it('renders with 0 contentLength', () => {
const file: FileInfo = {
type: 'url',
contentType: 'text/plain',
contentLength: '0'
}
render(<FileIcon file={file} />)
})
})

View File

@ -1,17 +0,0 @@
export function prettySize(
bytes: number,
separator = ' ',
postFix = ''
): string {
if (bytes) {
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']
const i = Math.min(
Math.floor(Math.log(bytes) / Math.log(1024)),
sizes.length - 1
)
return `${(bytes / 1024 ** i).toFixed(i ? 1 : 0)}${separator}${
sizes[i]
}${postFix}`
}
return 'n/a'
}

View File

@ -0,0 +1,21 @@
import { render, screen } from '@testing-library/react'
import React from 'react'
import Error from './Error'
describe('@shared/FormInput/Error', () => {
const propsBase = {
value: '',
touched: false,
initialTouched: false
}
it('renders without crashing', () => {
render(<Error meta={{ ...propsBase, error: 'Hello Error' }} />)
expect(screen.getByText('Hello Error')).toBeInTheDocument()
})
it('renders nothing without error passed', () => {
render(<Error meta={{ ...propsBase }} />)
expect(screen.queryByText('Hello Error')).not.toBeInTheDocument()
})
})

View File

@ -1,9 +1,6 @@
import React, { ReactElement } from 'react'
import styles from './Help.module.css'
import Markdown from '@shared/Markdown'
import classNames from 'classnames/bind'
const cx = classNames.bind(styles)
const FormHelp = ({
children,
@ -12,12 +9,9 @@ const FormHelp = ({
children: string
className?: string
}): ReactElement => {
const styleClasses = cx({
help: true,
[className]: className
})
return <Markdown className={styleClasses} text={children} />
return (
<Markdown className={`${styles.help} ${className || ''}`} text={children} />
)
}
export default FormHelp

View File

@ -58,11 +58,11 @@
}
.radio {
composes: radio from '@shared/FormInput/InputRadio.module.css';
composes: radio from '@shared/FormInput/InputElement/Radio/index.module.css';
}
.checkbox {
composes: checkbox from '@shared/FormInput/InputRadio.module.css';
composes: checkbox from '@shared/FormInput/InputElement/Radio/index.module.css';
}
.title {

View File

@ -0,0 +1,52 @@
import AssetSelection, { AssetSelectionAsset } from './'
import { fireEvent, render, screen } from '@testing-library/react'
import React from 'react'
describe('@shared/FormInput/InputElement/AssetSelection', () => {
const assets: AssetSelectionAsset[] = [
{
did: 'did:op:xxx',
name: 'Asset',
price: '10',
checked: false,
symbol: 'OCEAN'
},
{
did: 'did:op:yyy',
name: 'Asset',
price: '10',
checked: true,
symbol: 'OCEAN'
},
{
did: 'did:op:zzz',
name: 'Asset',
price: '0',
checked: false,
symbol: 'OCEAN'
}
]
it('renders without crashing', () => {
render(<AssetSelection assets={assets} />)
const searchInput = screen.getByPlaceholderText(
'Search by title, datatoken, or DID...'
)
fireEvent.change(searchInput, { target: { value: 'Assets' } })
fireEvent.change(searchInput, { target: { value: '' } })
})
it('renders empty assetSelection', () => {
render(<AssetSelection assets={[]} />)
expect(screen.getByText('No assets found.')).toBeInTheDocument()
})
it('renders disabled assetSelection', () => {
render(<AssetSelection assets={[]} disabled />)
expect(screen.getByText('No assets found.')).toBeInTheDocument()
})
it('renders assetSelectionMultiple', () => {
render(<AssetSelection assets={assets} multiple />)
})
})

View File

@ -1,15 +1,12 @@
import React, { ChangeEvent, useState } from 'react'
import Dotdotdot from 'react-dotdotdot'
import slugify from 'slugify'
import classNames from 'classnames/bind'
import PriceUnit from '@shared/Price/PriceUnit'
import External from '@images/external.svg'
import InputElement from '@shared/FormInput/InputElement'
import Loader from '@shared/atoms/Loader'
import styles from './index.module.css'
const cx = classNames.bind(styles)
export interface AssetSelectionAsset {
did: string
name: string
@ -34,18 +31,19 @@ export default function AssetSelection({
}): JSX.Element {
const [searchValue, setSearchValue] = useState('')
const styleClassesInput = cx({
input: true,
[styles.checkbox]: multiple,
[styles.radio]: !multiple
})
const styleClassesWrapper = `${styles.selection} ${
disabled ? styles.disabled : ''
}`
const styleClassesInput = `${styles.input} ${
multiple ? styles.checkbox : styles.radio
}`
function handleSearchInput(e: ChangeEvent<HTMLInputElement>) {
setSearchValue(e.target.value)
}
return (
<div className={`${styles.selection} ${disabled ? styles.disabled : ''}`}>
<div className={styleClassesWrapper}>
<InputElement
type="search"
name="search"

View File

@ -0,0 +1,38 @@
import BoxSelection, { BoxSelectionOption } from './'
import { fireEvent, render, screen } from '@testing-library/react'
import React from 'react'
describe('@shared/FormInput/InputElement/BoxSelection', () => {
const handleChange = jest.fn()
const options: BoxSelectionOption[] = [
{
name: 'option1',
value: 'option1',
title: 'Option 1',
checked: true,
text: 'Option 1 Text',
icon: <div>Icon</div>
},
{
name: 'option2',
title: 'Option 2 Text',
checked: false
}
]
it('renders without crashing', () => {
render(
<BoxSelection name="box" options={options} handleChange={handleChange} />
)
fireEvent.click(screen.getByText('Option 2 Text'))
expect(handleChange).toHaveBeenCalled()
})
it('renders disabled', () => {
render(<BoxSelection name="box" options={options} disabled />)
})
it('renders loader without options', () => {
render(<BoxSelection name="box" options={null} />)
})
})

View File

@ -1,10 +1,7 @@
import React, { ChangeEvent } from 'react'
import classNames from 'classnames/bind'
import Loader from '@shared/atoms/Loader'
import styles from './index.module.css'
const cx = classNames.bind(styles)
export interface BoxSelectionOption {
name: string
value?: string
@ -26,15 +23,10 @@ export default function BoxSelection({
disabled?: boolean
handleChange?: (event: ChangeEvent<HTMLInputElement>) => void
}): JSX.Element {
const styleClassesWrapper = cx({
boxSelectionsWrapper: true,
[styles.disabled]: disabled
})
const styleClassesInput = cx({
input: true,
radio: true
})
const styleClassesWrapper = `${styles.boxSelectionsWrapper} ${
disabled ? styles.disabled : ''
}`
const styleClassesInput = `${styles.input} ${styles.radio}`
return (
<div className={styleClassesWrapper}>
@ -51,7 +43,7 @@ export default function BoxSelection({
type="radio"
className={styleClassesInput}
disabled={disabled}
value={option.value ? option.value : option.name}
value={option.value || option.name}
name={name}
/>
<label

View File

@ -0,0 +1,59 @@
import { fireEvent, render, screen } from '@testing-library/react'
import React from 'react'
import Datatoken from './index'
import { useField } from 'formik'
jest.mock('formik')
const props = {
name: 'Datatoken'
}
const mockMeta = {
touched: false,
error: '',
initialError: '',
initialTouched: false,
initialValue: '',
value: ''
}
const mockField = {
value: {
name: '',
symbol: ''
},
checked: false,
onChange: jest.fn(),
onBlur: jest.fn(),
name: 'NFT'
}
const mockHelpers = {
setValue: jest.fn()
}
describe('@shared/FormInput/InputElement/Datatoken', () => {
it('renders without crashing', () => {
;(useField as jest.Mock).mockReturnValue([mockField, mockMeta, mockHelpers])
render(<Datatoken {...props} />)
fireEvent.click(screen.getByRole('button'))
})
it('does nothing when data already present', () => {
;(useField as jest.Mock).mockReturnValue([
{
value: {
name: 'Hello Name'
},
checked: false,
onChange: jest.fn(),
onBlur: jest.fn(),
name: 'NFT'
},
mockMeta,
mockHelpers
])
render(<Datatoken {...props} />)
})
})

View File

@ -0,0 +1,125 @@
import { fireEvent, render, screen } from '@testing-library/react'
import React from 'react'
import FilesInput from './index'
import { useField } from 'formik'
import { getFileUrlInfo } from '@utils/provider'
jest.mock('formik')
jest.mock('@utils/provider')
const props = {
name: 'File'
}
const mockMeta = {
touched: false,
error: '',
initialError: '',
initialTouched: false,
initialValue: '',
value: ''
}
const mockField = {
value: 'https://hello.com',
checked: false,
onChange: jest.fn(),
onBlur: jest.fn(),
name: 'url'
}
const mockHelpers = {
setValue: jest.fn(),
setTouched: jest.fn()
}
const mockForm = {
values: {
services: [{ providerUrl: 'https://provider.url' }]
},
errors: {},
touched: {},
isSubmitting: false,
isValidating: false,
submitCount: 0,
setFieldError: jest.fn()
}
describe('@shared/FormInput/InputElement/FilesInput', () => {
it('renders without crashing', async () => {
;(useField as jest.Mock).mockReturnValue([mockField, mockMeta, mockHelpers])
;(getFileUrlInfo as jest.Mock).mockReturnValue([
{
valid: true,
url: 'https://hello.com',
contentType: 'text/html',
contentLength: 100
}
])
render(<FilesInput form={mockForm} {...props} />)
expect(screen.getByText('Validate')).toBeInTheDocument()
fireEvent.click(screen.getByText('Validate'))
// can't really re-mock our helpers.setValue() behavior switching
// to Info component, so we just wait for Validate button to be back again.
await screen.findByText('Validate')
expect(mockHelpers.setValue).toHaveBeenCalled()
})
it('renders fileinfo when file is valid', () => {
;(useField as jest.Mock).mockReturnValue([
{
value: [
{
valid: true,
url: 'https://hello.com',
contentType: 'text/html',
contentLength: 100
}
]
},
mockMeta,
mockHelpers
])
render(<FilesInput {...props} />)
expect(screen.getByText('https://hello.com')).toBeInTheDocument()
})
it('renders fileinfo without contentType', () => {
;(useField as jest.Mock).mockReturnValue([
{
value: [
{
valid: true,
url: 'https://hello.com',
contentLength: 100
}
]
},
mockMeta,
mockHelpers
])
render(<FilesInput {...props} />)
})
it('renders fileinfo placeholder when hideUrl is passed', () => {
;(useField as jest.Mock).mockReturnValue([
{
value: [
{
valid: true,
url: 'https://hello.com',
type: 'hidden'
}
]
},
mockMeta,
mockHelpers
])
render(<FilesInput {...props} />)
expect(
screen.getByText('https://oceanprotocol/placeholder')
).toBeInTheDocument()
})
})

View File

@ -1,17 +1,15 @@
import React, { ReactElement, useState } from 'react'
import { useField, useFormikContext } from 'formik'
import { useField } from 'formik'
import FileInfo from './Info'
import UrlInput from '../URLInput'
import { InputProps } from '@shared/FormInput'
import { getFileUrlInfo } from '@utils/provider'
import { FormPublishData } from 'src/components/Publish/_types'
import { LoggerInstance } from '@oceanprotocol/lib'
import { useAsset } from '@context/Asset'
export default function FilesInput(props: InputProps): ReactElement {
const [field, meta, helpers] = useField(props.name)
const [isLoading, setIsLoading] = useState(false)
const { values, setFieldError } = useFormikContext<FormPublishData>()
const { asset } = useAsset()
async function handleValidation(e: React.SyntheticEvent, url: string) {
@ -19,8 +17,8 @@ export default function FilesInput(props: InputProps): ReactElement {
e?.preventDefault()
try {
const providerUrl = values?.services
? values?.services[0].providerUrl.url
const providerUrl = props.form?.values?.services
? props.form?.values?.services[0].providerUrl.url
: asset.services[0].serviceEndpoint
setIsLoading(true)
const checkedFile = await getFileUrlInfo(url, providerUrl)
@ -35,7 +33,7 @@ export default function FilesInput(props: InputProps): ReactElement {
// if all good, add file to formik state
helpers.setValue([{ url, ...checkedFile[0] }])
} catch (error) {
setFieldError(`${field.name}[0].url`, error.message)
props.form.setFieldError(`${field.name}[0].url`, error.message)
LoggerInstance.error(error.message)
} finally {
setIsLoading(false)

View File

@ -0,0 +1,16 @@
export function prettySize(
bytes: number,
separator = ' ',
postFix = ''
): string {
if (!bytes) return 'n/a'
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']
const i = Math.min(
Math.floor(Math.log(bytes) / Math.log(1024)),
sizes.length - 1
)
return `${(bytes / 1024 ** i).toFixed(i ? 1 : 0)}${separator}${
sizes[i]
}${postFix}`
}

View File

@ -0,0 +1,63 @@
import { fireEvent, render, screen } from '@testing-library/react'
import React from 'react'
import Nft from './index'
import { useField } from 'formik'
jest.mock('formik')
const props = {
name: 'NFT'
}
const mockMeta = {
touched: false,
error: '',
initialError: '',
initialTouched: false,
initialValue: '',
value: ''
}
const mockField = {
value: {
name: '',
symbol: '',
description: '',
external_url: '',
background_color: '',
image_data: ''
},
checked: false,
onChange: jest.fn(),
onBlur: jest.fn(),
name: 'NFT'
}
const mockHelpers = {
setValue: jest.fn()
}
describe('@shared/FormInput/InputElement/Nft', () => {
it('renders without crashing', () => {
;(useField as jest.Mock).mockReturnValue([mockField, mockMeta, mockHelpers])
render(<Nft {...props} />)
fireEvent.click(screen.getByRole('button'))
})
it('does nothing when data already present', () => {
;(useField as jest.Mock).mockReturnValue([
{
value: {
name: 'Hello Name'
},
checked: false,
onChange: jest.fn(),
onBlur: jest.fn(),
name: 'NFT'
},
mockMeta,
mockHelpers
])
render(<Nft {...props} />)
})
})

View File

@ -16,7 +16,7 @@
.radio,
.checkbox {
composes: input from './InputElement.module.css';
composes: input from '../index.module.css';
position: relative;
padding: 0;
width: 18px;

View File

@ -1,7 +1,7 @@
import React, { InputHTMLAttributes, ReactElement } from 'react'
import slugify from 'slugify'
import classNames from 'classnames/bind'
import styles from './InputRadio.module.css'
import styles from './index.module.css'
const cx = classNames.bind(styles)

View File

@ -2,11 +2,11 @@ import React, { ReactElement, useEffect, useState } from 'react'
import CreatableSelect from 'react-select/creatable'
import { OnChangeValue } from 'react-select'
import { useField } from 'formik'
import { InputProps } from '.'
import { InputProps } from '../..'
import { getTagsList } from '@utils/aquarius'
import { chainIds } from 'app.config'
import { chainIds } from '../../../../../../app.config'
import { useCancelToken } from '@hooks/useCancelToken'
import styles from './TagsAutoComplete.module.css'
import styles from './index.module.css'
import { matchSorter } from 'match-sorter'
interface AutoCompleteOption {

View File

@ -1,5 +1,5 @@
.input {
composes: input from '@shared/FormInput/InputElement.module.css';
composes: input from '@shared/FormInput/InputElement/index.module.css';
}
.hasError {

View File

@ -0,0 +1,57 @@
import { fireEvent, render, screen } from '@testing-library/react'
import React from 'react'
import URLInput, { URLInputProps } from './index'
import { useField } from 'formik'
jest.mock('formik')
const props: URLInputProps = {
submitText: 'Submit',
handleButtonClick: jest.fn(),
isLoading: false,
name: 'Hello Name'
}
const mockMeta = {
touched: false,
error: '',
initialError: '',
initialTouched: false,
initialValue: '',
value: ''
}
describe('@shared/FormInput/InputElement/URLInput', () => {
it('renders without crashing', () => {
const mockField = {
value: '',
checked: false,
onChange: jest.fn(),
onBlur: jest.fn(),
name: 'url'
}
;(useField as jest.Mock).mockReturnValue([mockField, mockMeta])
render(<URLInput {...props} />)
expect(screen.getByRole('button')).toBeDisabled()
fireEvent.change(screen.getByRole('textbox'), {
target: { value: 'https://google.com' }
})
})
it('renders button enabled with value', () => {
const mockField = {
value: 'https://google.com',
checked: false,
onChange: jest.fn(),
onBlur: jest.fn(),
name: 'url'
}
;(useField as jest.Mock).mockReturnValue([mockField, mockMeta])
render(<URLInput {...props} />)
expect(screen.getByRole('button')).toBeEnabled()
fireEvent.click(screen.getByRole('button'))
})
})

View File

@ -7,6 +7,14 @@ import InputGroup from '@shared/FormInput/InputGroup'
import InputElement from '@shared/FormInput/InputElement'
import isUrl from 'is-url-superb'
export interface URLInputProps {
submitText: string
handleButtonClick(e: React.SyntheticEvent, data: string): void
isLoading: boolean
name: string
checkUrl?: boolean
}
export default function URLInput({
submitText,
handleButtonClick,
@ -14,13 +22,7 @@ export default function URLInput({
name,
checkUrl,
...props
}: {
submitText: string
handleButtonClick(e: React.SyntheticEvent, data: string): void
isLoading: boolean
name: string
checkUrl?: boolean
}): ReactElement {
}: URLInputProps): ReactElement {
const [field, meta] = useField(name)
const [isButtonDisabled, setIsButtonDisabled] = useState(true)

View File

@ -1,17 +1,15 @@
import React, { ReactElement } from 'react'
import styles from './InputElement.module.css'
import { InputProps } from '.'
import FilesInput from '../FormFields/FilesInput'
import CustomProvider from '../FormFields/Provider'
import BoxSelection, { BoxSelectionOption } from '../FormFields/BoxSelection'
import Datatoken from '../FormFields/Datatoken'
import styles from './index.module.css'
import { InputProps } from '..'
import FilesInput from './FilesInput'
import CustomProvider from './Provider'
import BoxSelection, { BoxSelectionOption } from './BoxSelection'
import Datatoken from './Datatoken'
import classNames from 'classnames/bind'
import AssetSelection, {
AssetSelectionAsset
} from '../FormFields/AssetSelection'
import Nft from '../FormFields/Nft'
import InputRadio from './InputRadio'
import ContainerInput from '@shared/FormFields/ContainerInput'
import AssetSelection, { AssetSelectionAsset } from './AssetSelection'
import Nft from './Nft'
import InputRadio from './Radio'
import ContainerInput from '@shared/FormInput/InputElement/ContainerInput'
import TagsAutoComplete from './TagsAutoComplete'
const cx = classNames.bind(styles)
@ -87,12 +85,14 @@ export default function InputElement({
case 'radio':
case 'checkbox':
return <InputRadio options={options} inputSize={size} {...props} />
return (
<InputRadio options={options as string[]} inputSize={size} {...props} />
)
case 'assetSelection':
return (
<AssetSelection
assets={options as unknown as AssetSelectionAsset[]}
assets={options as AssetSelectionAsset[]}
{...field}
{...props}
/>
@ -101,14 +101,14 @@ export default function InputElement({
case 'assetSelectionMultiple':
return (
<AssetSelection
assets={options as unknown as AssetSelectionAsset[]}
assets={options as AssetSelectionAsset[]}
multiple
{...field}
{...props}
/>
)
case 'files':
return <FilesInput {...field} {...props} />
return <FilesInput {...field} form={form} {...props} />
case 'container':
return <ContainerInput {...field} {...props} />
case 'providerUrl':
@ -120,7 +120,7 @@ export default function InputElement({
case 'boxSelection':
return (
<BoxSelection
options={options as unknown as BoxSelectionOption[]}
options={options as BoxSelectionOption[]}
{...field}
{...props}
/>

View File

@ -1,3 +0,0 @@
.row {
margin-bottom: var(--spacer);
}

View File

@ -1,8 +0,0 @@
import React, { ReactElement, ReactNode } from 'react'
import styles from './Row.module.css'
const Row = ({ children }: { children: ReactNode }): ReactElement => (
<div className={styles.row}>{children}</div>
)
export default Row

View File

@ -0,0 +1,105 @@
import { BoxSelectionOption } from '@shared/FormInput/InputElement/BoxSelection'
import { AssetSelectionAsset } from '@shared/FormInput/InputElement/AssetSelection'
import { render, screen } from '@testing-library/react'
import React from 'react'
import FormInput from './index'
describe('@shared/FormInput', () => {
it('renders without crashing', () => {
render(
<FormInput
type="text"
name="Hello Name"
label="Hello Label"
placeholder="Hello Placeholder"
required
help="Hello Help"
/>
)
expect(screen.getByText('Hello Label')).toBeInTheDocument()
expect(screen.getByPlaceholderText('Hello Placeholder')).toBeInTheDocument()
})
it('renders prominent help', () => {
render(<FormInput type="text" help="Hello Help" prominentHelp />)
expect(screen.getByText('Hello Help')).toBeInTheDocument()
})
it('renders disclaimer', () => {
render(<FormInput type="text" disclaimer="Hello Disclaimer" />)
expect(screen.getByText('Hello Disclaimer')).toBeInTheDocument()
})
it('renders with prefix & postfix', () => {
render(
<FormInput type="text" prefix="Hello Prefix" postfix="Hello Postfix" />
)
expect(screen.getByText('Hello Prefix')).toBeInTheDocument()
expect(screen.getByText('Hello Postfix')).toBeInTheDocument()
})
it('renders textarea', () => {
render(<FormInput type="textarea" />)
})
it('renders radio', () => {
render(<FormInput type="radio" options={['option1', 'option2']} />)
})
it('renders checkbox', () => {
render(<FormInput type="checkbox" options={['option1', 'option2']} />)
})
it('renders select', () => {
render(<FormInput type="select" options={['option1', 'option2']} />)
})
it('renders assetSelection', () => {
const assets: AssetSelectionAsset[] = [
{
did: 'did:op:xxx',
name: 'Asset',
price: '10',
checked: false,
symbol: 'OCEAN'
},
{
did: 'did:op:yyy',
name: 'Asset',
price: '10',
checked: true,
symbol: 'OCEAN'
}
]
render(<FormInput type="assetSelection" options={assets} />)
})
it('renders assetSelectionMultiple', () => {
render(<FormInput type="assetSelectionMultiple" />)
})
it('renders boxSelection', () => {
const options: BoxSelectionOption[] = [
{
name: 'option1',
title: 'Option 1',
checked: true,
text: 'Option 1 Text',
icon: <div>Icon</div>
},
{
name: 'option2',
title: 'Option 2',
checked: true
}
]
render(
<FormInput
type="boxSelection"
options={options}
onChange={() => jest.fn()}
/>
)
})
})

View File

@ -16,6 +16,8 @@ import Disclaimer from './Disclaimer'
import Tooltip from '@shared/atoms/Tooltip'
import Markdown from '@shared/Markdown'
import FormHelp from './Help'
import { AssetSelectionAsset } from '@shared/FormInput/InputElement/AssetSelection'
import { BoxSelectionOption } from '@shared/FormInput/InputElement/BoxSelection'
const cx = classNames.bind(styles)
@ -28,7 +30,7 @@ export interface InputProps {
prominentHelp?: boolean
tag?: string
type?: string
options?: string[]
options?: string[] | AssetSelectionAsset[] | BoxSelectionOption[]
sortOptions?: boolean
additionalComponent?: ReactElement
value?: string | number

View File

@ -1,73 +0,0 @@
import * as React from 'react'
import { FormikProps, connect } from 'formik'
import debounce from 'lodash.debounce'
import omit from 'lodash.omit'
import isEqual from 'react-fast-compare'
import { LoggerInstance } from '@oceanprotocol/lib'
export interface PersistProps {
name: string
ignoreFields?: string[]
debounce?: number
isSessionStorage?: boolean
}
// TODO: refactor into functional component
class PersistImpl extends React.Component<
PersistProps & { formik: FormikProps<any> },
any
> {
static defaultProps = {
debounce: 300
}
saveForm = debounce((data: FormikProps<any>) => {
const dataToSave = this.omitIgnoredFields(data)
LoggerInstance.log('data to save', dataToSave)
if (this.props.isSessionStorage) {
window.sessionStorage.setItem(this.props.name, JSON.stringify(dataToSave))
} else {
window.localStorage.setItem(this.props.name, JSON.stringify(dataToSave))
}
}, this.props.debounce)
omitIgnoredFields = (data: FormikProps<any>) => {
const { ignoreFields } = this.props
LoggerInstance.log('omitted fields', ignoreFields)
const { values, touched, errors } = data
LoggerInstance.log('values', values, omit(values, ignoreFields))
return ignoreFields
? omit(
{
...data,
values: omit(values, ignoreFields),
touched: omit(touched, ignoreFields),
errors: omit(errors, ignoreFields)
},
ignoreFields
)
: data
}
componentDidUpdate(prevProps: PersistProps & { formik: FormikProps<any> }) {
if (!isEqual(prevProps.formik, this.props.formik)) {
this.saveForm(this.props.formik)
}
}
componentDidMount() {
const maybeState = this.props.isSessionStorage
? window.sessionStorage.getItem(this.props.name)
: window.localStorage.getItem(this.props.name)
if (maybeState && maybeState !== null) {
this.props.formik.setFormikState(JSON.parse(maybeState))
}
}
render(): null {
return null
}
}
export const Persist = connect<PersistProps, any>(PersistImpl)

View File

@ -19,5 +19,5 @@ export function NetworkIcon({ name }: { name: string }): ReactElement {
? EnergywebIcon
: EthIcon // ETH icon as fallback
return IconMapped ? <IconMapped className={styles.icon} /> : null
return <IconMapped className={styles.icon} />
}

View File

@ -0,0 +1,33 @@
import { render } from '@testing-library/react'
import React from 'react'
import NetworkName from './index'
describe('@shared/NetworkName', () => {
it('renders without crashing', () => {
render(<NetworkName networkId={1} />)
})
it('renders minimal', () => {
render(<NetworkName networkId={1} minimal />)
})
it('renders Polygon', () => {
render(<NetworkName networkId={137} />)
})
it('renders BSC', () => {
render(<NetworkName networkId={56} />)
})
it('renders Energy Web', () => {
render(<NetworkName networkId={246} />)
})
it('renders Moonriver', () => {
render(<NetworkName networkId={1285} />)
})
it('renders icon fallback', () => {
render(<NetworkName networkId={99999} />)
})
})

View File

@ -0,0 +1,20 @@
import { render, screen } from '@testing-library/react'
import React from 'react'
import Page from './index'
describe('@shared/Page', () => {
it('renders without crashing', () => {
render(
<Page uri="/hello" title="Hello Title" description="Hello Description">
Hello Children
</Page>
)
expect(screen.getByText('Hello Children')).toBeInTheDocument()
expect(screen.getByText('Hello Title')).toBeInTheDocument()
expect(screen.getByText('Hello Description')).toBeInTheDocument()
})
it('renders without title', () => {
render(<Page uri="/hello">Hello Children</Page>)
})
})

View File

@ -0,0 +1,28 @@
import testRender from '../../../../.jest/testRender'
import { render } from '@testing-library/react'
import React from 'react'
import Pagination from './index'
import { MAXIMUM_NUMBER_OF_PAGES_WITH_RESULTS } from '@utils/aquarius'
describe('@shared/Pagination', () => {
testRender(
<Pagination
totalPages={MAXIMUM_NUMBER_OF_PAGES_WITH_RESULTS + 1}
currentPage={2}
rowsPerPage={10}
rowCount={30}
onChangePage={() => jest.fn()}
/>
)
it('renders without currentPage prop', () => {
render(
<Pagination
totalPages={10}
rowsPerPage={10}
rowCount={30}
onChangePage={() => jest.fn()}
/>
)
})
})

View File

@ -0,0 +1,51 @@
import { render, screen } from '@testing-library/react'
import React from 'react'
import Price from './index'
import { asset } from '../../../../.jest/__fixtures__/assetWithAccessDetails'
import prices from '../../../../.jest/__fixtures__/prices'
jest.mock('../../../@context/Prices', () => ({
usePrices: () => prices,
getCoingeckoTokenId: () => 'ocean-protocol'
}))
describe('@shared/Price', () => {
it('renders fixed price', () => {
render(
<Price
accessDetails={{ ...asset.accessDetails, type: 'fixed', price: '10' }}
/>
)
expect(screen.getByText('10')).toBeInTheDocument()
})
it('renders free price', () => {
render(<Price accessDetails={{ ...asset.accessDetails, type: 'free' }} />)
expect(screen.getByText('Free')).toBeInTheDocument()
})
it('renders null price', () => {
render(<Price accessDetails={{ ...asset.accessDetails, price: null }} />)
expect(screen.getByText('-')).toBeInTheDocument()
})
it('renders conversion', async () => {
render(
<Price
accessDetails={{ ...asset.accessDetails, price: '10' }}
conversion
/>
)
expect(await screen.findByText('≈')).toBeInTheDocument()
})
it('renders no conversion when no price defined', async () => {
render(
<Price
accessDetails={{ ...asset.accessDetails, price: null }}
conversion
/>
)
expect(screen.queryByText('≈')).not.toBeInTheDocument()
})
})

View File

@ -7,7 +7,7 @@ const account = '0x0000000000000000000000000000000000000000'
jest.mock('axios')
describe('Publisher', () => {
describe('@shared/Publisher', () => {
test('should return correct markup by default', async () => {
;(axios as any).get.mockImplementationOnce(() =>
Promise.resolve({ data: { name: 'jellymcjellyfish.eth' } })

View File

@ -0,0 +1,12 @@
import { render } from '@testing-library/react'
import React from 'react'
import testRender from '../../../../.jest/testRender'
import SuccessConfetti from './index'
describe('@shared/SuccessConfetti', () => {
testRender(<SuccessConfetti success="Nice Success!" />)
it('renders without success', () => {
render(<SuccessConfetti success={null} />)
})
})

View File

@ -0,0 +1,18 @@
import { render, fireEvent, screen } from '@testing-library/react'
import React from 'react'
import WalletNetworkSwitcher from './'
jest.mock('../../../@utils/web3', () => ({
addCustomNetwork: () => jest.fn()
}))
describe('@shared/WalletNetworkSwitcher', () => {
it('renders without crashing', () => {
render(<WalletNetworkSwitcher />)
})
it('switching networks can be invoked', () => {
render(<WalletNetworkSwitcher />)
fireEvent.click(screen.getByRole('button'))
})
})

View File

@ -13,6 +13,7 @@ export default function WalletNetworkSwitcher(): ReactElement {
const { networkId, web3Provider } = useWeb3()
const { asset } = useAsset()
const { networksList } = useNetworkMetadata()
const ddoNetworkData = getNetworkDataById(networksList, asset.chainId)
const walletNetworkData = getNetworkDataById(networksList, networkId)

View File

@ -0,0 +1,52 @@
import { render, screen } from '@testing-library/react'
import React from 'react'
import Web3Feedback from './'
import { useGraphSyncStatus } from '../../../@hooks/useGraphSyncStatus'
jest.mock('../../../@hooks/useGraphSyncStatus')
describe('@shared/Web3Feedback', () => {
;(useGraphSyncStatus as jest.Mock).mockImplementation(() => ({
isGraphSynced: true,
blockGraph: '333333',
blockHead: '333333'
}))
it('renders without crashing', () => {
render(<Web3Feedback networkId={1} accountId="0xxxx" />)
})
it('renders isAssetNetwork === false', async () => {
render(
<Web3Feedback networkId={1} accountId="0xxxx" isAssetNetwork={false} />
)
expect(
await screen.findByText('Not connected to asset network')
).toBeInTheDocument()
})
it('renders isGraphSynced === false', async () => {
;(useGraphSyncStatus as jest.Mock).mockImplementation(() => ({
isGraphSynced: false
}))
render(
<Web3Feedback networkId={1} accountId="0xxxx" isAssetNetwork={true} />
)
expect(await screen.findByText('Data out of sync')).toBeInTheDocument()
})
it('renders no account', async () => {
render(<Web3Feedback networkId={1} accountId={undefined} />)
expect(await screen.findByText('No account connected')).toBeInTheDocument()
})
it('do nothing if nothing to show', async () => {
;(useGraphSyncStatus as jest.Mock).mockImplementation(() => ({
isGraphSynced: true
}))
render(
<Web3Feedback networkId={1} accountId="0xxxx" isAssetNetwork={true} />
)
expect(screen.queryByRole('heading')).not.toBeInTheDocument()
})
})

View File

@ -13,12 +13,13 @@ export declare type Web3Error = {
export default function Web3Feedback({
networkId,
accountId,
isAssetNetwork
}: {
networkId: number
accountId: string
isAssetNetwork?: boolean
}): ReactElement {
const { accountId } = useWeb3()
const { isGraphSynced, blockGraph, blockHead } = useGraphSyncStatus(networkId)
const [state, setState] = useState<string>()
const [title, setTitle] = useState<string>()

View File

@ -28,7 +28,7 @@ Full.args = {
state: 'info',
action: {
name: 'Action',
handleAction: () => null as any
handleAction: () => null
},
badge: 'Hello',
onDismiss: () => {

View File

@ -1,7 +1,34 @@
import React from 'react'
import testRender from '../../../../../.jest/testRender'
import Alert from '@shared/atoms/Alert'
import { render } from '@testing-library/react'
describe('Alert', () => {
testRender(<Alert text="Alert text" state="info" />)
testRender(
<Alert
title="Alert Title"
text="Alert text"
state="info"
badge="Hello"
action={{
name: 'Hello action',
style: 'text',
handleAction: () => null
}}
onDismiss={() => null}
/>
)
it('renders without action style', () => {
render(
<Alert
text="Alert text"
state="info"
action={{
name: 'Hello action',
handleAction: () => null
}}
/>
)
})
})

View File

@ -2,6 +2,15 @@ import React from 'react'
import { ComponentStory, ComponentMeta } from '@storybook/react'
import { ListItem } from '@shared/atoms/Lists'
export const items = [
'List item short',
'List item long ipsum dolor sit amet, consectetur adipiscing elit. Mauris aliquam facilisis molestie',
'List item long ipsum dolor sit amet, consectetur adipiscing elit',
'List item short',
'List item long ipsum dolor sit amet, consectetur adipiscing elit. Mauris aliquam facilisis molestie',
'List item long ipsum dolor sit amet, consectetur adipiscing elit'
]
export default {
title: 'Component/@shared/atoms/Lists',
component: ListItem
@ -11,15 +20,6 @@ const Template: ComponentStory<typeof ListItem> = (args) => (
<ListItem {...args} />
)
const items = [
'List item short',
'List item long ipsum dolor sit amet, consectetur adipiscing elit. Mauris aliquam facilisis molestie',
'List item long ipsum dolor sit amet, consectetur adipiscing elit',
'List item short',
'List item long ipsum dolor sit amet, consectetur adipiscing elit. Mauris aliquam facilisis molestie',
'List item long ipsum dolor sit amet, consectetur adipiscing elit'
]
export const Unordered = Template.bind({})
Unordered.decorators = [
() => (

View File

@ -0,0 +1,28 @@
import { render } from '@testing-library/react'
import React from 'react'
import { ListItem } from './index'
import { items } from './index.stories'
describe('Lists', () => {
it('renders unordered', () => {
render(
<ul>
{items.map((item, key) => (
<ListItem key={key}>{item}</ListItem>
))}
</ul>
)
})
it('renders ordered', () => {
render(
<ol>
{items.map((item, key) => (
<ListItem ol key={key}>
{item}
</ListItem>
))}
</ol>
)
})
})

View File

@ -1,8 +1,18 @@
import React from 'react'
import testRender from '../../../../../.jest/testRender'
import Loader from '@shared/atoms/Loader'
import { Default } from './index.stories'
import Loader, { LoaderProps } from '@shared/atoms/Loader'
import { Default, WithMessage } from './index.stories'
import { render } from '@testing-library/react'
describe('Loader', () => {
testRender(<Loader {...Default.args} />)
it('renders without wordmark', () => {
render(<Loader {...WithMessage.args} />)
})
it('renders white', () => {
const props: LoaderProps = { white: true }
render(<Loader {...props} />)
})
})

View File

@ -1,8 +1,13 @@
import React from 'react'
import testRender from '../../../../../.jest/testRender'
import Logo from '@shared/atoms/Logo'
import { Default } from './index.stories'
import { Default, WithoutWordmark } from './index.stories'
import { render } from '@testing-library/react'
describe('Logo', () => {
testRender(<Logo {...Default.args} />)
it('renders without wordmark', () => {
render(<Logo {...WithoutWordmark.args} />)
})
})

View File

@ -0,0 +1,23 @@
import React from 'react'
import { render } from '@testing-library/react'
import Modal from './'
import ReactModal from 'react-modal'
describe('Modal', () => {
it('renders without crashing', () => {
ReactModal.setAppElement(document.createElement('div'))
const { rerender } = render(
<Modal title="Hello" isOpen onToggleModal={() => null}>
Hello
</Modal>
)
expect(document.querySelector('.ReactModalPortal')).toBeInTheDocument()
rerender(
<Modal title="Hello" isOpen={false} onToggleModal={() => null}>
Hello
</Modal>
)
})
})

View File

@ -1,6 +1,26 @@
import React from 'react'
import { ComponentStory, ComponentMeta } from '@storybook/react'
import Table, { TableOceanProps } from '@shared/atoms/Table'
import { columns, data } from '../../../../../.jest/__fixtures__/table'
export const args: TableOceanProps<any> = { columns, data }
export const argsWithPagination: TableOceanProps<any> = {
columns,
data: data.flatMap((i) => [i, i, i])
}
export const argsLoading: TableOceanProps<any> = {
isLoading: true,
columns: [],
data: []
}
export const argsEmpty: TableOceanProps<any> = {
emptyMessage: 'I am empty',
columns: [],
data: []
}
export default {
title: 'Component/@shared/atoms/Table',
@ -13,70 +33,14 @@ interface Props {
args: TableOceanProps<any>
}
const columns = [
{
name: 'Name',
selector: (row: any) => row.name,
maxWidth: '45rem',
grow: 1
},
{
name: 'Symbol',
selector: (row: any) => row.symbol,
maxWidth: '10rem'
},
{
name: 'Price',
selector: (row: any) => row.price,
right: true
}
]
const data = [
{
name: 'Title asset',
symbol: 'DATA-70',
price: '1.011'
},
{
name: 'Title asset Title asset Title asset Title asset Title asset',
symbol: 'DATA-71',
price: '1.011'
},
{
name: 'Title asset',
symbol: 'DATA-72',
price: '1.011'
},
{
name: 'Title asset Title asset Title asset Title asset Title asset Title asset Title asset Title asset Title asset Title asset',
symbol: 'DATA-71',
price: '1.011'
}
]
export const WithData: Props = Template.bind({})
WithData.args = {
columns,
data
}
WithData.args = args
export const WithPagination: Props = Template.bind({})
WithPagination.args = {
columns,
data: data.flatMap((i) => [i, i, i])
}
WithPagination.args = argsWithPagination
export const Loading: Props = Template.bind({})
Loading.args = {
isLoading: true,
columns: [],
data: []
}
Loading.args = argsLoading
export const Empty: Props = Template.bind({})
Empty.args = {
emptyMessage: 'I am empty',
columns: [],
data: []
}
Empty.args = argsEmpty

View File

@ -0,0 +1,34 @@
import { render, screen } from '@testing-library/react'
import React from 'react'
import Table, { TableOceanProps } from '.'
import {
args,
argsWithPagination,
argsLoading,
argsEmpty
} from './index.stories'
describe('Table', () => {
it('renders without crashing', () => {
render(<Table {...args} />)
})
it('renders WithPagination', () => {
render(<Table {...argsWithPagination} />)
})
it('renders Loading', () => {
render(<Table {...argsLoading} />)
})
it('renders Empty', () => {
render(<Table {...argsEmpty} />)
expect(screen.getByText('I am empty')).toBeInTheDocument()
})
it('renders Empty without message', () => {
const args: TableOceanProps<any> = { ...argsEmpty, emptyMessage: undefined }
render(<Table {...args} />)
expect(screen.getByText('No results found')).toBeInTheDocument()
})
})

Some files were not shown because too many files have changed in this diff Show More