diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index cf2aa1bb6..f22f3b561 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -32,16 +32,24 @@ jobs: uses: golangci/golangci-lint-action@v3 with: skip-cache: true - - name: Compile + - name: NX pre-build compile targets shell: bash run: npx nx affected --target=compile --parallel=5 - - name: Test + - name: Test TypeScript shell: bash run: npx nx affected --target=test --parallel=5 - name: Test Go uses: n8maninger/action-golang-test@v1 with: args: '-race' + - name: Install playwright deps for e2e + shell: bash + run: npx playwright install-deps + - name: Test e2e + shell: bash + run: npx nx affected --target=e2e --parallel=5 + env: + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} - name: Build shell: bash run: npx nx affected --target=build --configuration=production --parallel=5 diff --git a/.github/workflows/release-main.yml b/.github/workflows/release-main.yml index 087cd0c26..515f12a71 100644 --- a/.github/workflows/release-main.yml +++ b/.github/workflows/release-main.yml @@ -29,15 +29,31 @@ jobs: registry: ghcr.io username: ${{ github.repository_owner }} password: ${{ secrets.GITHUB_TOKEN }} - - name: Lint + - name: Lint TypeScript shell: bash run: npx nx run-many --target=lint --all --parallel=5 - - name: Compile + - name: Lint Go + uses: golangci/golangci-lint-action@v3 + with: + skip-cache: true + - name: NX pre-build compile targets shell: bash run: npx nx run-many --target=compile --all --parallel=5 - - name: Test + - name: Test TypeScript shell: bash run: npx nx run-many --target=test --all --parallel=5 + - name: Test Go + uses: n8maninger/action-golang-test@v1 + with: + args: '-race' + - name: Install playwright deps for e2e + shell: bash + run: npx playwright install-deps + - name: Test e2e + shell: bash + run: npx nx run-many --target=e2e --all --parallel=5 + env: + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} - name: Build for publishing shell: bash run: npx nx run-many --target=build --configuration=production --all --parallel=5 diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 64553b175..d3f39ca94 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -3,6 +3,7 @@ "nrwl.angular-console", "esbenp.prettier-vscode", "firsttris.vscode-jest-runner", - "dbaeumer.vscode-eslint" + "dbaeumer.vscode-eslint", + "ms-playwright.playwright" ] } diff --git a/apps/walletd-e2e/.eslintrc.json b/apps/walletd-e2e/.eslintrc.json new file mode 100644 index 000000000..fbf2c975e --- /dev/null +++ b/apps/walletd-e2e/.eslintrc.json @@ -0,0 +1,22 @@ +{ + "extends": ["plugin:playwright/recommended", "../../.eslintrc.json"], + "ignorePatterns": ["!**/*"], + "overrides": [ + { + "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], + "rules": {} + }, + { + "files": ["*.ts", "*.tsx"], + "rules": {} + }, + { + "files": ["*.js", "*.jsx"], + "rules": {} + }, + { + "files": ["src/**/*.{ts,js,tsx,jsx}"], + "rules": {} + } + ] +} diff --git a/apps/walletd-e2e/.gitignore b/apps/walletd-e2e/.gitignore new file mode 100644 index 000000000..53752db25 --- /dev/null +++ b/apps/walletd-e2e/.gitignore @@ -0,0 +1 @@ +output diff --git a/apps/walletd-e2e/playwright.config.ts b/apps/walletd-e2e/playwright.config.ts new file mode 100644 index 000000000..d7f247b0f --- /dev/null +++ b/apps/walletd-e2e/playwright.config.ts @@ -0,0 +1,76 @@ +import { defineConfig, devices } from '@playwright/test' +import { nxE2EPreset } from '@nx/playwright/preset' + +import { workspaceRoot } from '@nx/devkit' + +// For CI, you may want to set BASE_URL to the deployed application. +const baseURL = process.env['BASE_URL'] || 'http://localhost:3008' + +/** + * Read environment variables from file. + * https://github.com/motdotla/dotenv + */ +// require('dotenv').config(); + +/** + * See https://playwright.dev/docs/test-configuration. + */ +export default defineConfig({ + ...nxE2EPreset(__filename, { testDir: './src' }), + /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + use: { + baseURL, + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: 'on-first-retry', + video: 'on-first-retry', + }, + expect: { + // Raise the timeout because it is running against next dev mode + // which requires compilation the first to a page is visited + timeout: 10_000, + }, + outputDir: 'output', + /* Run your local dev server before starting the tests */ + webServer: { + command: 'npx nx serve walletd', + url: 'http://localhost:3008', + reuseExistingServer: !process.env.CI, + cwd: workspaceRoot, + }, + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + + { + name: 'firefox', + use: { ...devices['Desktop Firefox'] }, + }, + + { + name: 'webkit', + use: { ...devices['Desktop Safari'] }, + }, + + // Uncomment for mobile browsers support + /* { + name: 'Mobile Chrome', + use: { ...devices['Pixel 5'] }, + }, + { + name: 'Mobile Safari', + use: { ...devices['iPhone 12'] }, + }, */ + + // Uncomment for branded browsers + /* { + name: 'Microsoft Edge', + use: { ...devices['Desktop Edge'], channel: 'msedge' }, + }, + { + name: 'Google Chrome', + use: { ...devices['Desktop Chrome'], channel: 'chrome' }, + } */ + ], +}) diff --git a/apps/walletd-e2e/project.json b/apps/walletd-e2e/project.json new file mode 100644 index 000000000..a7a3ac2f1 --- /dev/null +++ b/apps/walletd-e2e/project.json @@ -0,0 +1,19 @@ +{ + "name": "walletd-e2e", + "$schema": "../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "apps/walletd-e2e/src", + "projectType": "application", + "targets": { + "e2e": { + "executor": "@nx/playwright:playwright", + "outputs": ["{workspaceRoot}/dist/.playwright/apps/walletd-e2e"], + "options": { + "config": "apps/walletd-e2e/playwright.config.ts" + } + }, + "lint": { + "executor": "@nx/eslint:lint" + } + }, + "implicitDependencies": ["walletd"] +} diff --git a/apps/walletd-e2e/src/fixtures/createWallet.ts b/apps/walletd-e2e/src/fixtures/createWallet.ts new file mode 100644 index 000000000..35d83c34c --- /dev/null +++ b/apps/walletd-e2e/src/fixtures/createWallet.ts @@ -0,0 +1,50 @@ +import { expect, Page } from '@playwright/test' +import { mockApiWallet } from '../mocks/mock' +import { + Wallet, + WalletAddressesResponse, + WalletBalanceResponse, + WalletFundResponse, + WalletOutputsSiacoinResponse, +} from '@siafoundation/react-walletd' +import { mockApiWallets } from '../mocks/wallets' + +export async function createWallet({ + page, + seed, + newWallet, + responses = {}, +}: { + page: Page + seed: string + newWallet: Wallet + responses?: { + balance?: WalletBalanceResponse + outputsSiacoin?: WalletOutputsSiacoinResponse + fund?: WalletFundResponse + addresses?: WalletAddressesResponse + } +}) { + const wallets = await mockApiWallets({ page, createWallet: newWallet }) + await mockApiWallet({ + page, + wallet: newWallet, + responses, + }) + + await expect(page.getByRole('button', { name: 'Add wallet' })).toBeVisible() + await page.getByRole('button', { name: 'Add wallet' }).click() + await page.getByRole('button', { name: 'Create a wallet Generate a' }).click() + await page.locator('input[name=name]').fill(newWallet.name) + await page.locator('input[name=name]').press('Tab') + await page.locator('textarea[name=description]').fill(newWallet.name) + await page.locator('textarea[name=description]').press('Tab') + await page.locator('textarea[name=mnemonic]').click() + wallets.push(newWallet) + await page.getByRole('button', { name: 'Add wallet' }).click() + await expect( + page.getByText(`Wallet ${newWallet.name.slice(0, 5)}`) + ).toBeVisible() + await page.locator('input[name=mnemonic]').fill(seed) + await page.getByRole('button', { name: 'Continue' }).click() +} diff --git a/apps/walletd-e2e/src/fixtures/login.ts b/apps/walletd-e2e/src/fixtures/login.ts new file mode 100644 index 000000000..7194cef2a --- /dev/null +++ b/apps/walletd-e2e/src/fixtures/login.ts @@ -0,0 +1,12 @@ +import { Page, expect } from '@playwright/test' + +export async function login({ page }: { page: Page }) { + await page.goto('/') + await expect(page).toHaveTitle('walletd') + await page.getByLabel('login settings').click() + await page.getByRole('menuitem', { name: 'Show custom API' }).click() + await page.locator('input[name=api]').fill('https://walletd.local') + await page.locator('input[name=api]').press('Tab') + await page.locator('input[name=password]').fill('password') + await page.locator('input[name=password]').press('Enter') +} diff --git a/apps/walletd-e2e/src/fixtures/navigateToWallet.ts b/apps/walletd-e2e/src/fixtures/navigateToWallet.ts new file mode 100644 index 000000000..4fc6bef93 --- /dev/null +++ b/apps/walletd-e2e/src/fixtures/navigateToWallet.ts @@ -0,0 +1,13 @@ +import { Page } from '@playwright/test' +import { Wallet } from '@siafoundation/react-walletd' + +export async function navigateToWallet({ + page, + wallet, +}: { + page: Page + wallet: Wallet +}) { + await page.getByLabel('Dashboard').click() + await page.getByText(wallet.name).click() +} diff --git a/apps/walletd-e2e/src/fixtures/sendSiacoinDialog.ts b/apps/walletd-e2e/src/fixtures/sendSiacoinDialog.ts new file mode 100644 index 000000000..2c334e07c --- /dev/null +++ b/apps/walletd-e2e/src/fixtures/sendSiacoinDialog.ts @@ -0,0 +1,19 @@ +import { Page } from 'playwright' + +export async function fillComposeTransactionSiacoin({ + page, + receiveAddress, + changeAddress, + amount, +}: { + page: Page + receiveAddress: string + changeAddress: string + amount: string +}) { + await page.locator('input[name=receiveAddress]').fill(receiveAddress) + await page.getByLabel('customChangeAddress').click() + await page.locator('input[name=changeAddress]').fill(changeAddress) + await page.locator('input[name=siacoin]').fill(amount) + await page.getByRole('button', { name: 'Generate transaction' }).click() +} diff --git a/apps/walletd-e2e/src/mocks/consensusNetwork.ts b/apps/walletd-e2e/src/mocks/consensusNetwork.ts new file mode 100644 index 000000000..660e7c77a --- /dev/null +++ b/apps/walletd-e2e/src/mocks/consensusNetwork.ts @@ -0,0 +1,54 @@ +import { ConsensusNetwork } from '@siafoundation/types' +import { cloneDeep } from '@technically/lodash' +import { Page } from 'playwright' + +const data: ConsensusNetwork = { + name: 'zen', + initialCoinbase: '300000000000000000000000000000', + minimumCoinbase: '300000000000000000000000000000', + initialTarget: + 'bid:0000000100000000000000000000000000000000000000000000000000000000', + hardforkDevAddr: { + height: 1, + oldAddress: + 'addr:000000000000000000000000000000000000000000000000000000000000000089eb0d6a8a69', + newAddress: + 'addr:000000000000000000000000000000000000000000000000000000000000000089eb0d6a8a69', + }, + hardforkTax: { + height: 2, + }, + hardforkStorageProof: { + height: 5, + }, + hardforkOak: { + height: 10, + fixHeight: 12, + genesisTimestamp: '2023-01-13T03:53:20-05:00', + }, + hardforkASIC: { + height: 20, + oakTime: 10000000000000, + oakTarget: + 'bid:0000000100000000000000000000000000000000000000000000000000000000', + }, + hardforkFoundation: { + height: 30, + primaryAddress: + 'addr:053b2def3cbdd078c19d62ce2b4f0b1a3c5e0ffbeeff01280efb1f8969b2f5bb4fdc680f0807', + failsafeAddress: + 'addr:000000000000000000000000000000000000000000000000000000000000000089eb0d6a8a69', + }, + hardforkV2: { + allowHeight: 100000, + requireHeight: 102000, + }, +} + +export async function mockApiConsensusNetwork({ page }: { page: Page }) { + const json = cloneDeep(data) + await page.route('**/api/consensus/network*', async (route) => { + await route.fulfill({ json }) + }) + return json +} diff --git a/apps/walletd-e2e/src/mocks/consensusTip.ts b/apps/walletd-e2e/src/mocks/consensusTip.ts new file mode 100644 index 000000000..dc1309043 --- /dev/null +++ b/apps/walletd-e2e/src/mocks/consensusTip.ts @@ -0,0 +1,16 @@ +import { ConsensusTipResponse } from '@siafoundation/react-walletd' +import { cloneDeep } from '@technically/lodash' +import { Page } from 'playwright' + +const data: ConsensusTipResponse = { + height: 61676, + id: 'bid:00000010d5da9002b9640d920d9eb9f7502c5c3b2a796ecf800a103920bea96f', +} + +export async function mockApiConsensusTipState({ page }: { page: Page }) { + const json = cloneDeep(data) + await page.route('**/api/consensus/tipstate*', async (route) => { + await route.fulfill({ json }) + }) + return json +} diff --git a/apps/walletd-e2e/src/mocks/consensusTipState.ts b/apps/walletd-e2e/src/mocks/consensusTipState.ts new file mode 100644 index 000000000..02d72c34b --- /dev/null +++ b/apps/walletd-e2e/src/mocks/consensusTipState.ts @@ -0,0 +1,63 @@ +import { ConsensusState } from '@siafoundation/types' +import { cloneDeep } from '@technically/lodash' +import { Page } from 'playwright' + +const data: ConsensusState = { + index: { + height: 61676, + id: 'bid:00000010d5da9002b9640d920d9eb9f7502c5c3b2a796ecf800a103920bea96f', + }, + prevTimestamps: [ + '2024-03-25T17:33:24-04:00', + '2024-03-25T17:18:32-04:00', + '2024-03-25T17:07:59-04:00', + '2024-03-25T17:04:15-04:00', + '2024-03-25T16:50:56-04:00', + '2024-03-25T16:38:22-04:00', + '2024-03-25T16:38:12-04:00', + '2024-03-25T16:36:56-04:00', + '2024-03-25T16:35:17-04:00', + '2024-03-25T16:33:59-04:00', + '2024-03-25T16:31:16-04:00', + ], + depth: 'bid:000000000001aa096ddebfbf467c56faba46eb4b3e53e72c775bf9ab202a4890', + childTarget: + 'bid:0000005a6c49bca6186415827e15b13371c3de541dd64b7cf36a519b5128b2f8', + siafundPool: '88386728360671853873752300000', + oakTime: 127103000000000, + oakTarget: + 'bid:0000000024d11bd550a6acf3718192825fa2cef5582314db8e92c4f4e5efbf4c', + foundationPrimaryAddress: + 'addr:053b2def3cbdd078c19d62ce2b4f0b1a3c5e0ffbeeff01280efb1f8969b2f5bb4fdc680f0807', + foundationFailsafeAddress: + 'addr:000000000000000000000000000000000000000000000000000000000000000089eb0d6a8a69', + totalWork: '169134658088983', + difficulty: '47498615', + oakWork: '29864374508', + elements: { + numLeaves: 304383, + trees: [ + 'h:6d5fedd2f6d71cad5fc7bb35ef8bad6631c63e1323142e2a4825314b77f87d1e', + 'h:16e0c70194d2e41a21fe070ff936551b2379df8054890b95bada3c6b6a0ed3f7', + 'h:73defbf6af352d9401698492c407662b8feb62ba82e92028bf9bf0103f261844', + 'h:e22d4da6204096eef1eb0fc217c8900dce1040312cc721b9b6b3fc2be7a77da8', + 'h:4a9860d3e3ccf6cf434ed94d485b4385c62c443fd18e539998bd817a3ebed772', + 'h:6cd1e1d71a0796980193bcbb1adff069ef3f70fd12749d9e877b2bd0c8d561d1', + 'h:7aa0bdb94da13c43abd9446f77638d3e6e2ae4c36c5224a76433044c56db906c', + 'h:66c119cf5cb9ca3a174637c751988f03300e0375b43d22c82df7145841515d38', + 'h:ce8a94296e60887232915d6801a9246827661d329de47a08f25a6a3884809037', + 'h:8aa7872a63ef37e1ca8e4211700daf96068ce2cc215d4e2ab45434b5ffcab818', + 'h:3f0175c94ed31190e6da23e80d6167e2da619b460e27798750491163dbe5677b', + 'h:9f120f6489c3e3a324cd51a6f8f9dd033930688095f4440dc5f8511692b4efc1', + ], + }, + attestations: 0, +} + +export async function mockApiConsensusTip({ page }: { page: Page }) { + const json = cloneDeep(data) + await page.route('**/api/consensus/tip*', async (route) => { + await route.fulfill({ json }) + }) + return json +} diff --git a/apps/walletd-e2e/src/mocks/mock.ts b/apps/walletd-e2e/src/mocks/mock.ts new file mode 100644 index 000000000..8416e599b --- /dev/null +++ b/apps/walletd-e2e/src/mocks/mock.ts @@ -0,0 +1,79 @@ +import { Page } from 'playwright' +import { + Wallet, + WalletAddressesResponse, + WalletBalanceResponse, + WalletFundResponse, + WalletOutputsSiacoinResponse, +} from '@siafoundation/react-walletd' +import { mockSiaCentralExchangeRates } from './siaCentralExchangeRates' +import { mockApiSyncerPeers } from './peers' +import { mockApiConsensusTip } from './consensusTipState' +import { mockApiConsensusTipState } from './consensusTip' +import { mockApiConsensusNetwork } from './consensusNetwork' +import { mockApiWallets } from './wallets' +import { mockApiWalletBalance } from './walletBalance' +import { mockApiWalletAddresses } from './walletAddresses' +import { mockApiWalletEvents } from './walletEvents' +import { mockApiWalletTxpool } from './walletTxpool' +import { mockApiWalletOutputsSiacoin } from './walletOutputsSiacoin' +import { mockApiWalletOutputsSiafund } from './walletOutputsSiafund' +import { mockApiWalletFund } from './walletFund' +import { mockApiTxpoolBroadcast } from './txpoolBroadcast' +import { mockApiWalletRelease } from './walletRelease' + +export async function mockApiDefaults({ page }: { page: Page }) { + await mockSiaCentralExchangeRates({ page }) + await mockApiSyncerPeers({ page }) + await mockApiConsensusTip({ page }) + await mockApiConsensusTipState({ page }) + await mockApiConsensusNetwork({ page }) + await mockApiTxpoolBroadcast({ page }) + const wallets = await mockApiWallets({ page }) + for (const wallet of wallets) { + await mockApiWallet({ page, wallet }) + } +} + +export async function mockApiWallet({ + page, + wallet, + responses = {}, +}: { + page: Page + wallet: Wallet + responses?: { + balance?: WalletBalanceResponse + outputsSiacoin?: WalletOutputsSiacoinResponse + fund?: WalletFundResponse + addresses?: WalletAddressesResponse + } +}) { + await mockApiWalletBalance({ + page, + walletId: wallet.id, + response: responses.balance, + }) + await mockApiWalletAddresses({ + page, + walletId: wallet.id, + response: responses.addresses, + }) + await mockApiWalletEvents({ page, walletId: wallet.id }) + await mockApiWalletTxpool({ page, walletId: wallet.id }) + await mockApiWalletOutputsSiacoin({ + page, + walletId: wallet.id, + response: responses.outputsSiacoin, + }) + await mockApiWalletOutputsSiafund({ page, walletId: wallet.id }) + await mockApiWalletFund({ + page, + walletId: wallet.id, + response: responses.fund, + }) + await mockApiWalletRelease({ + page, + walletId: wallet.id, + }) +} diff --git a/apps/walletd-e2e/src/mocks/peers.ts b/apps/walletd-e2e/src/mocks/peers.ts new file mode 100644 index 000000000..0a5c29593 --- /dev/null +++ b/apps/walletd-e2e/src/mocks/peers.ts @@ -0,0 +1,81 @@ +import { cloneDeep } from '@technically/lodash' +import { Page } from 'playwright' + +const data = [ + { + addr: '51.81.242.140:9881', + inbound: false, + version: '1.5.4', + firstSeen: '2024-03-20T17:14:43Z', + connectedSince: '2024-03-25T21:08:49Z', + syncDuration: 1321006459, + }, + { + addr: '3.36.68.121:9881', + inbound: false, + version: '1.5.4', + firstSeen: '2024-03-20T13:39:10Z', + connectedSince: '2024-03-25T21:08:50Z', + syncDuration: 4230014834, + }, + { + addr: '185.200.116.131:9807', + inbound: false, + version: '1.5.4', + firstSeen: '2024-03-20T17:14:43Z', + connectedSince: '2024-03-25T21:08:51Z', + syncedBlocks: 2, + syncDuration: 6018991169, + }, + { + addr: '62.30.63.90:9881', + inbound: false, + version: '1.5.4', + firstSeen: '2024-03-20T13:39:10Z', + connectedSince: '2024-03-25T21:09:07Z', + syncDuration: 6341708209, + }, + { + addr: '43.203.121.70:9881', + inbound: false, + version: '1.5.4', + firstSeen: '2024-03-20T17:14:44Z', + connectedSince: '2024-03-25T21:09:08Z', + syncedBlocks: 3, + syncDuration: 6860545337, + }, + { + addr: '23.239.8.40:9881', + inbound: false, + version: '1.5.4', + firstSeen: '2024-03-20T13:39:10Z', + connectedSince: '2024-03-25T21:09:08Z', + syncDuration: 580416082, + }, + { + addr: '141.94.161.198:9881', + inbound: false, + version: '1.5.4', + firstSeen: '2024-03-20T13:39:10Z', + connectedSince: '2024-03-25T21:09:13Z', + syncedBlocks: 93, + syncDuration: 4894896166, + }, + { + addr: '64.227.180.244:9881', + inbound: false, + version: '1.5.4', + firstSeen: '2024-03-20T13:39:25Z', + connectedSince: '2024-03-25T21:08:49Z', + syncedBlocks: 2, + syncDuration: 4687705000, + }, +] + +export async function mockApiSyncerPeers({ page }: { page: Page }) { + const json = cloneDeep(data) + await page.route('**/api/syncer/peers*', async (route) => { + await route.fulfill({ json }) + }) + return json +} diff --git a/apps/walletd-e2e/src/mocks/siaCentralExchangeRates.ts b/apps/walletd-e2e/src/mocks/siaCentralExchangeRates.ts new file mode 100644 index 000000000..a24cabd47 --- /dev/null +++ b/apps/walletd-e2e/src/mocks/siaCentralExchangeRates.ts @@ -0,0 +1,38 @@ +import { SiaCentralExchangeRatesResponse } from '@siafoundation/sia-central' +import { cloneDeep } from '@technically/lodash' +import { Page } from 'playwright' + +const data: SiaCentralExchangeRatesResponse = { + message: 'successfully retrieved exchange rate', + type: 'success', + rates: { + sc: { + aud: '0.016136871549', + bch: '0.000021499880703', + btc: '0.000000149047', + cad: '0.014328484298', + cny: '0.076310722577', + eth: '0.0000029068532077', + eur: '0.009737538604', + gbp: '0.008359151948', + jpy: '1.600530478116', + ltc: '0.000116295710314', + rub: '0.978819669836', + scp: '0.0623627615062762', + sf: '0.000000745235', + usd: '0.010571307522', + }, + }, + timestamp: '2024-03-26T13:12:22.7348119Z', +} + +export async function mockSiaCentralExchangeRates({ page }: { page: Page }) { + const json = cloneDeep(data) + await page.route( + 'https://api.siacentral.com/v2/market/exchange-rate?currencies=sc', + async (route) => { + await route.fulfill({ json }) + } + ) + return json +} diff --git a/apps/walletd-e2e/src/mocks/txpoolBroadcast.ts b/apps/walletd-e2e/src/mocks/txpoolBroadcast.ts new file mode 100644 index 000000000..c452c5438 --- /dev/null +++ b/apps/walletd-e2e/src/mocks/txpoolBroadcast.ts @@ -0,0 +1,7 @@ +import { Page } from 'playwright' + +export async function mockApiTxpoolBroadcast({ page }: { page: Page }) { + await page.route(`**/api/txpool/broadcast*`, async (route) => { + await route.fulfill() + }) +} diff --git a/apps/walletd-e2e/src/mocks/walletAddresses.ts b/apps/walletd-e2e/src/mocks/walletAddresses.ts new file mode 100644 index 000000000..04768fbea --- /dev/null +++ b/apps/walletd-e2e/src/mocks/walletAddresses.ts @@ -0,0 +1,57 @@ +import { WalletAddress } from '@siafoundation/react-walletd' +import { cloneDeep } from '@technically/lodash' +import { Page } from 'playwright' + +const data: WalletAddress[] = [ + { + address: + 'addr:f2dbf56b5b0c698d7fbf43f646c76169d84e597e8b37fada97348beeecaa812d400ac4ce7981', + description: '', + metadata: { + index: 0, + publicKey: + 'ed25519:ee122b2169bdae5776b55609e384e0c58372cd5c529d4edc9b9918b26f8e5535', + }, + }, + { + address: + 'addr:90c6057cdd2463eca61f83796e83152dbba28b6cb9a74831a043833051ec9f422726bfff2ee8', + description: '', + metadata: { + index: 1, + publicKey: + 'ed25519:624d6d477a8f4ceac873e6dd9138740f9322cb34a24246f96f9d64c021172f43', + }, + }, + { + address: + 'addr:170173c40ca0f39f9618da30af14c390c7ce70248a3662a7a5d3d5a8a31c9fbfa2071e9f6518', + description: '', + metadata: { + index: 2, + publicKey: + 'ed25519:65cac661a4acf36847c0aa67cbc6956e3449fd82a7430cfd673ea7fedbfcf5fa', + }, + }, +] + +export async function mockApiWalletAddresses({ + page, + walletId, + response, +}: { + page: Page + walletId: string + response?: WalletAddress[] +}) { + const json = response || cloneDeep(data) + await page.route(`**/api/wallets/${walletId}/addresses*`, async (route) => { + if (route.request().method() === 'PUT') { + json.push(JSON.parse(route.request().postData())) + await route.fulfill({ json }) + } else { + await route.fulfill({ json }) + } + }) + return json +} diff --git a/apps/walletd-e2e/src/mocks/walletBalance.ts b/apps/walletd-e2e/src/mocks/walletBalance.ts new file mode 100644 index 000000000..cc2b87dd2 --- /dev/null +++ b/apps/walletd-e2e/src/mocks/walletBalance.ts @@ -0,0 +1,26 @@ +import { WalletBalanceResponse } from '@siafoundation/react-walletd' +import { toHastings } from '@siafoundation/units' +import { cloneDeep } from '@technically/lodash' +import { Page } from 'playwright' + +const data: WalletBalanceResponse = { + siacoins: toHastings('100').toString(), + immatureSiacoins: toHastings('10').toString(), + siafunds: 10, +} + +export async function mockApiWalletBalance({ + page, + walletId, + response, +}: { + page: Page + walletId: string + response?: WalletBalanceResponse +}) { + const json = response || cloneDeep(data) + await page.route(`**/api/wallets/${walletId}/balance*`, async (route) => { + await route.fulfill({ json }) + }) + return json +} diff --git a/apps/walletd-e2e/src/mocks/walletEvents.ts b/apps/walletd-e2e/src/mocks/walletEvents.ts new file mode 100644 index 000000000..7c6f53e8b --- /dev/null +++ b/apps/walletd-e2e/src/mocks/walletEvents.ts @@ -0,0 +1,19 @@ +import { WalletEvent } from '@siafoundation/react-walletd' +import { cloneDeep } from '@technically/lodash' +import { Page } from 'playwright' + +const data: WalletEvent[] = [] + +export async function mockApiWalletEvents({ + page, + walletId, +}: { + page: Page + walletId: string +}) { + const json = cloneDeep(data) + await page.route(`**/api/wallets/${walletId}/events*`, async (route) => { + await route.fulfill({ json }) + }) + return json +} diff --git a/apps/walletd-e2e/src/mocks/walletFund.ts b/apps/walletd-e2e/src/mocks/walletFund.ts new file mode 100644 index 000000000..3a5f1480f --- /dev/null +++ b/apps/walletd-e2e/src/mocks/walletFund.ts @@ -0,0 +1,62 @@ +import { WalletFundResponse } from '@siafoundation/react-walletd' +import { cloneDeep } from '@technically/lodash' +import { Page } from 'playwright' + +const data: WalletFundResponse = { + transaction: { + siacoinInputs: [ + { + parentID: + 'scoid:aa3e781330c9b3991e0141807df1327fadf114ca6c37acb9e58004f942d91dfb', + unlockConditions: { + timelock: 0, + publicKeys: null, + signaturesRequired: 0, + }, + }, + { + parentID: + 'scoid:32e430158591b4073a6834e9f4c4b67162e348844f569f4e472896bb72efb724', + unlockConditions: { + timelock: 0, + publicKeys: null, + signaturesRequired: 0, + }, + }, + ], + siacoinOutputs: [ + { + value: '1000000000000000000000000', + address: + 'addr:90c6057cdd2463eca61f83796e83152dbba28b6cb9a74831a043833051ec9f422726bfff2ee8', + }, + { + value: '97984280000000000000000000', + address: + 'addr:f2dbf56b5b0c698d7fbf43f646c76169d84e597e8b37fada97348beeecaa812d400ac4ce7981', + }, + ], + minerFees: ['3930000000000000000000'], + }, + toSign: [ + 'h:aa3e781330c9b3991e0141807df1327fadf114ca6c37acb9e58004f942d91dfb', + 'h:32e430158591b4073a6834e9f4c4b67162e348844f569f4e472896bb72efb724', + ], + dependsOn: null, +} + +export async function mockApiWalletFund({ + page, + walletId, + response, +}: { + page: Page + walletId: string + response?: WalletFundResponse +}) { + const json = cloneDeep(response || data) + await page.route(`**/api/wallets/${walletId}/fund*`, async (route) => { + await route.fulfill({ json }) + }) + return json +} diff --git a/apps/walletd-e2e/src/mocks/walletOutputsSiacoin.ts b/apps/walletd-e2e/src/mocks/walletOutputsSiacoin.ts new file mode 100644 index 000000000..d49c95803 --- /dev/null +++ b/apps/walletd-e2e/src/mocks/walletOutputsSiacoin.ts @@ -0,0 +1,66 @@ +import { SiacoinElement } from '@siafoundation/types' +import { cloneDeep } from '@technically/lodash' +import { Page } from 'playwright' + +const data: SiacoinElement[] = [ + { + id: 'h:aa3e781330c9b3991e0141807df1327fadf114ca6c37acb9e58004f942d91dfb', + leafIndex: 304248, + merkleProof: [ + 'h:0a7a4c392f78899e3c38c5cd9e6a673b2c7afec97930af539af9c8e20209aa78', + 'h:a1e074dc48634a234b7366a0d7ab19cd05e3e698e1d44bf07e24d75ae0c65b3c', + 'h:44d107342962e2068d289ce090c87b7bf0c847f734bdfad10db5546e402c3ad7', + 'h:64ea3e65ecd5ebc1c0ce014673148060855ce550571208b6303b8e2a83e33451', + 'h:19e37bf7747c6d8c7b87d2474dbbd3a8f5b26d89642f8e1af4b9b02abdfb2ea6', + 'h:1fc2ac9f70211a3be6f334db14feb8b327458aee49d3539770640cbdec9b4a5f', + 'h:defbbc18c64349f11e75537955861ececce0fadf10baec456f6c74b024820af1', + 'h:87d27ff868ca3b1dce59ae754eaec48239718e81e2e6f3b7b418f5a00362bcf7', + 'h:93d823c55fbd09de462a8e355921433b3693d63de58ea8e2780a2c2ffabd0fee', + 'h:a68820d6b79b2735b15c69d0fc26b11252bb27f22b9088559ed13f9420f5dda1', + 'h:1bbcead690290291ea9628214a121ef783411693975171803bf5716a3a6ff19b', + ], + siacoinOutput: { + value: '1000000000000000000000000', + address: + 'addr:f2dbf56b5b0c698d7fbf43f646c76169d84e597e8b37fada97348beeecaa812d400ac4ce7981', + }, + maturityHeight: 0, + }, + { + id: 'h:32e430158591b4073a6834e9f4c4b67162e348844f569f4e472896bb72efb724', + leafIndex: 305723, + merkleProof: [ + 'h:8c02aeec48de589ce497ebe72fb8b527cfe022ef513fcfdc56745c84832f00ec', + 'h:1bf63b9959e60272fd7a48a8cecd4120a852c0e14557ea27ccad6ea2071e70b3', + 'h:21b7e1606e9fd677059c58a1a687682182f71ce09e071431dcaff823a3a5d49e', + 'h:e81ac37d3b4db6166dc1bb10ebfa49f57cbf99aababd36ee4e3e5e12082dc6dc', + 'h:ecc307c6c3e505d97ccf821938e5e5702ef0130d33c991ca95735f7d9706a4b8', + 'h:9560060ee399793f102e092afdfdbdd33692706256955e8390af552de0addfc0', + ], + siacoinOutput: { + value: '97988210000000000000000000', + address: + 'addr:f2dbf56b5b0c698d7fbf43f646c76169d84e597e8b37fada97348beeecaa812d400ac4ce7981', + }, + maturityHeight: 0, + }, +] + +export async function mockApiWalletOutputsSiacoin({ + page, + walletId, + response, +}: { + page: Page + walletId: string + response?: SiacoinElement[] +}) { + const json = cloneDeep(response || data) + await page.route( + `**/api/wallets/${walletId}/outputs/siacoin*`, + async (route) => { + await route.fulfill({ json }) + } + ) + return json +} diff --git a/apps/walletd-e2e/src/mocks/walletOutputsSiafund.ts b/apps/walletd-e2e/src/mocks/walletOutputsSiafund.ts new file mode 100644 index 000000000..4573beddf --- /dev/null +++ b/apps/walletd-e2e/src/mocks/walletOutputsSiafund.ts @@ -0,0 +1,22 @@ +import { SiafundElement } from '@siafoundation/types' +import { cloneDeep } from '@technically/lodash' +import { Page } from 'playwright' + +const data: SiafundElement[] = [] + +export async function mockApiWalletOutputsSiafund({ + page, + walletId, +}: { + page: Page + walletId: string +}) { + const json = cloneDeep(data) + await page.route( + `**/api/wallets/${walletId}/outputs/siafund*`, + async (route) => { + await route.fulfill({ json }) + } + ) + return json +} diff --git a/apps/walletd-e2e/src/mocks/walletRelease.ts b/apps/walletd-e2e/src/mocks/walletRelease.ts new file mode 100644 index 000000000..a47a12c1a --- /dev/null +++ b/apps/walletd-e2e/src/mocks/walletRelease.ts @@ -0,0 +1,15 @@ +import { WalletFundResponse } from '@siafoundation/react-walletd' +import { Page } from 'playwright' + +export async function mockApiWalletRelease({ + page, + walletId, +}: { + page: Page + walletId: string + response?: WalletFundResponse +}) { + await page.route(`**/api/wallets/${walletId}/release*`, async (route) => { + await route.fulfill() + }) +} diff --git a/apps/walletd-e2e/src/mocks/walletTxpool.ts b/apps/walletd-e2e/src/mocks/walletTxpool.ts new file mode 100644 index 000000000..e7c7af18f --- /dev/null +++ b/apps/walletd-e2e/src/mocks/walletTxpool.ts @@ -0,0 +1,19 @@ +import { PoolTransaction } from '@siafoundation/react-walletd' +import { cloneDeep } from '@technically/lodash' +import { Page } from 'playwright' + +const data: PoolTransaction[] = [] + +export async function mockApiWalletTxpool({ + page, + walletId, +}: { + page: Page + walletId: string +}) { + const json = cloneDeep(data) + await page.route(`**/api/wallets/${walletId}/txpool*`, async (route) => { + await route.fulfill({ json }) + }) + return json +} diff --git a/apps/walletd-e2e/src/mocks/wallets.ts b/apps/walletd-e2e/src/mocks/wallets.ts new file mode 100644 index 000000000..862de8567 --- /dev/null +++ b/apps/walletd-e2e/src/mocks/wallets.ts @@ -0,0 +1,58 @@ +import { Wallet } from '@siafoundation/react-walletd' +import { cloneDeep } from '@technically/lodash' +import { Page } from 'playwright' + +const data: Wallet[] = [ + { + id: '1', + name: '1', + description: '1', + dateCreated: '2024-03-20T18:49:01Z', + lastUpdated: '2024-03-20T18:49:01Z', + metadata: { + type: 'seed', + seedHash: + '302626ee851d3712c19007c6141fc7f3a229ae64e6df71403a31b3ad131ac53657cf3dcae9405d9203e57204eb0084a7899224880e8d8beb4e103949255d861e', + }, + }, + { + id: '2', + name: '2', + description: '2', + dateCreated: '2024-03-20T18:49:50Z', + lastUpdated: '2024-03-20T19:30:04Z', + metadata: { + type: 'seed', + seedHash: + '302626ee851d3712c19007c6141fc7f3a229ae64e6df71403a31b3ad131ac53657cf3dcae9405d9203e57204eb0084a7899224880e8d8beb4e103949255d861e', + }, + }, + { + id: '3', + name: '3', + description: '3', + dateCreated: '2024-03-20T18:53:27Z', + lastUpdated: '2024-03-20T18:53:27Z', + metadata: { + type: 'watch', + }, + }, +] + +export async function mockApiWallets({ + page, + createWallet, +}: { + page: Page + createWallet?: Wallet +}) { + const json = cloneDeep(data) + await page.route('**/api/wallets*', async (route) => { + if (route.request().method() === 'POST') { + await route.fulfill({ json: createWallet }) + } else { + await route.fulfill({ json }) + } + }) + return json +} diff --git a/apps/walletd-e2e/src/specs/login.spec.ts b/apps/walletd-e2e/src/specs/login.spec.ts new file mode 100644 index 000000000..58a2676ba --- /dev/null +++ b/apps/walletd-e2e/src/specs/login.spec.ts @@ -0,0 +1,9 @@ +import { test, expect } from '@playwright/test' +import { mockApiDefaults } from '../mocks/mock' +import { login } from '../fixtures/login' + +test('login', async ({ page }) => { + await mockApiDefaults({ page }) + await login({ page }) + await expect(page.getByRole('button', { name: 'Add wallet' })).toBeVisible() +}) diff --git a/apps/walletd-e2e/src/specs/seedGenerateAddresses/seedGenerateAddresses.mock.ts b/apps/walletd-e2e/src/specs/seedGenerateAddresses/seedGenerateAddresses.mock.ts new file mode 100644 index 000000000..f4a327f00 --- /dev/null +++ b/apps/walletd-e2e/src/specs/seedGenerateAddresses/seedGenerateAddresses.mock.ts @@ -0,0 +1,50 @@ +import { WalletAddress } from '@siafoundation/react-walletd' + +export const seed = + 'ridge business wish transfer home glove office salt wealth baby journey diary' + +export const newWallet = { + id: '100', + name: 'test send wallet', + description: 'wallet description', + dateCreated: new Date().toISOString(), + lastUpdated: new Date().toISOString(), + metadata: { + type: 'seed', + seedHash: + 'd4ad57da37936a053b0587fcc4964e9de89da8fbd610f7de190c32e22e6acd2a1c8f9f88bf219ffac04801b709712f3a2e64cc4f0271c402a6c4659573024440', + }, +} + +export const mockWalletAddressesResponse: WalletAddress[] = [ + { + address: + 'addr:f2dbf56b5b0c698d7fbf43f646c76169d84e597e8b37fada97348beeecaa812d400ac4ce7981', + description: '', + metadata: { + index: 0, + publicKey: + 'ed25519:ee122b2169bdae5776b55609e384e0c58372cd5c529d4edc9b9918b26f8e5535', + }, + }, + { + address: + 'addr:90c6057cdd2463eca61f83796e83152dbba28b6cb9a74831a043833051ec9f422726bfff2ee8', + description: '', + metadata: { + index: 1, + publicKey: + 'ed25519:624d6d477a8f4ceac873e6dd9138740f9322cb34a24246f96f9d64c021172f43', + }, + }, + { + address: + 'addr:170173c40ca0f39f9618da30af14c390c7ce70248a3662a7a5d3d5a8a31c9fbfa2071e9f6518', + description: '', + metadata: { + index: 2, + publicKey: + 'ed25519:65cac661a4acf36847c0aa67cbc6956e3449fd82a7430cfd673ea7fedbfcf5fa', + }, + }, +] diff --git a/apps/walletd-e2e/src/specs/seedGenerateAddresses/seedGenerateAddresses.spec.ts b/apps/walletd-e2e/src/specs/seedGenerateAddresses/seedGenerateAddresses.spec.ts new file mode 100644 index 000000000..882350bb0 --- /dev/null +++ b/apps/walletd-e2e/src/specs/seedGenerateAddresses/seedGenerateAddresses.spec.ts @@ -0,0 +1,51 @@ +import { test, expect } from '@playwright/test' +import { mockApiDefaults } from '../../mocks/mock' +import { login } from '../../fixtures/login' +import { createWallet } from '../../fixtures/createWallet' +import { navigateToWallet } from '../../fixtures/navigateToWallet' +import { + seed, + newWallet, + mockWalletAddressesResponse, +} from './seedGenerateAddresses.mock' +import { cloneDeep } from '@technically/lodash' + +const defaultMockWalletResponses = { + addresses: mockWalletAddressesResponse, +} + +test('generate new addresses', async ({ page }) => { + await mockApiDefaults({ page }) + await login({ page }) + + const mockAddressesInvalid = cloneDeep(mockWalletAddressesResponse) + mockAddressesInvalid.forEach((address) => { + address.metadata.publicKey = undefined + }) + await createWallet({ + page, + newWallet, + seed, + responses: defaultMockWalletResponses, + }) + await navigateToWallet({ page, wallet: newWallet }) + await page.getByLabel('view addresses').click() + await page.getByRole('button', { name: 'Add addresses' }).click() + await page.locator('input[name=count]').fill('5') + await page.getByRole('button', { name: 'Continue' }).click() + await expect( + page.getByText('60838523a4bdeeec5b4f70a6678da48a77ad58fe...') + ).toBeVisible() + await expect( + page.getByText('65b40f6a720352ad5b9546b9f5077209672914cc...') + ).toBeVisible() + await expect( + page.getByText('e94e8113563a549f95ff3904dccf77f1b8fbaad4...') + ).toBeVisible() + await expect( + page.getByText('cc7241334772c6d10d47882b06b21a60242a19c3...') + ).toBeVisible() + await expect( + page.getByText('170173c40ca0f39f9618da30af14c390c7ce7024...') + ).toBeVisible() +}) diff --git a/apps/walletd-e2e/src/specs/seedSendSiacoin/seedSendSiacoin.mock.ts b/apps/walletd-e2e/src/specs/seedSendSiacoin/seedSendSiacoin.mock.ts new file mode 100644 index 000000000..8fa2a7212 --- /dev/null +++ b/apps/walletd-e2e/src/specs/seedSendSiacoin/seedSendSiacoin.mock.ts @@ -0,0 +1,154 @@ +import { SiacoinElement } from '@siafoundation/types' +import { + WalletAddress, + WalletBalanceResponse, + WalletFundResponse, +} from '@siafoundation/react-walletd' +import { toHastings } from '@siafoundation/units' + +export const seed = + 'ridge business wish transfer home glove office salt wealth baby journey diary' + +export const receiveAddress = + '5739945c21e60afd70eaf97ccd33ea27836e0219212449f39e4b38acaa8b3119aa4150a9ef0f' +export const changeAddress = + '170173c40ca0f39f9618da30af14c390c7ce70248a3662a7a5d3d5a8a31c9fbfa2071e9f6518' + +export const newWallet = { + id: '100', + name: 'test send wallet', + description: 'wallet description', + dateCreated: new Date().toISOString(), + lastUpdated: new Date().toISOString(), + metadata: { + type: 'seed', + seedHash: + 'd4ad57da37936a053b0587fcc4964e9de89da8fbd610f7de190c32e22e6acd2a1c8f9f88bf219ffac04801b709712f3a2e64cc4f0271c402a6c4659573024440', + }, +} + +export const mockWalletBalanceResponse: WalletBalanceResponse = { + siacoins: toHastings('100').toString(), + immatureSiacoins: toHastings('10').toString(), + siafunds: 10, +} + +export const mockWalletOutputsSiacoinResponse: SiacoinElement[] = [ + { + id: 'h:aa3e781330c9b3991e0141807df1327fadf114ca6c37acb9e58004f942d91dfb', + leafIndex: 304248, + merkleProof: [ + 'h:0a7a4c392f78899e3c38c5cd9e6a673b2c7afec97930af539af9c8e20209aa78', + 'h:a1e074dc48634a234b7366a0d7ab19cd05e3e698e1d44bf07e24d75ae0c65b3c', + 'h:44d107342962e2068d289ce090c87b7bf0c847f734bdfad10db5546e402c3ad7', + 'h:64ea3e65ecd5ebc1c0ce014673148060855ce550571208b6303b8e2a83e33451', + 'h:19e37bf7747c6d8c7b87d2474dbbd3a8f5b26d89642f8e1af4b9b02abdfb2ea6', + 'h:1fc2ac9f70211a3be6f334db14feb8b327458aee49d3539770640cbdec9b4a5f', + 'h:defbbc18c64349f11e75537955861ececce0fadf10baec456f6c74b024820af1', + 'h:87d27ff868ca3b1dce59ae754eaec48239718e81e2e6f3b7b418f5a00362bcf7', + 'h:93d823c55fbd09de462a8e355921433b3693d63de58ea8e2780a2c2ffabd0fee', + 'h:a68820d6b79b2735b15c69d0fc26b11252bb27f22b9088559ed13f9420f5dda1', + 'h:1bbcead690290291ea9628214a121ef783411693975171803bf5716a3a6ff19b', + ], + siacoinOutput: { + value: '1000000000000000000000000', + address: + 'addr:f2dbf56b5b0c698d7fbf43f646c76169d84e597e8b37fada97348beeecaa812d400ac4ce7981', + }, + maturityHeight: 0, + }, + { + id: 'h:32e430158591b4073a6834e9f4c4b67162e348844f569f4e472896bb72efb724', + leafIndex: 305723, + merkleProof: [ + 'h:8c02aeec48de589ce497ebe72fb8b527cfe022ef513fcfdc56745c84832f00ec', + 'h:1bf63b9959e60272fd7a48a8cecd4120a852c0e14557ea27ccad6ea2071e70b3', + 'h:21b7e1606e9fd677059c58a1a687682182f71ce09e071431dcaff823a3a5d49e', + 'h:e81ac37d3b4db6166dc1bb10ebfa49f57cbf99aababd36ee4e3e5e12082dc6dc', + 'h:ecc307c6c3e505d97ccf821938e5e5702ef0130d33c991ca95735f7d9706a4b8', + 'h:9560060ee399793f102e092afdfdbdd33692706256955e8390af552de0addfc0', + ], + siacoinOutput: { + value: '97988210000000000000000000', + address: + 'addr:f2dbf56b5b0c698d7fbf43f646c76169d84e597e8b37fada97348beeecaa812d400ac4ce7981', + }, + maturityHeight: 0, + }, +] + +export const mockWalletFundResponse: WalletFundResponse = { + transaction: { + siacoinInputs: [ + { + parentID: + 'scoid:aa3e781330c9b3991e0141807df1327fadf114ca6c37acb9e58004f942d91dfb', + unlockConditions: { + timelock: 0, + publicKeys: null, + signaturesRequired: 0, + }, + }, + { + parentID: + 'scoid:32e430158591b4073a6834e9f4c4b67162e348844f569f4e472896bb72efb724', + unlockConditions: { + timelock: 0, + publicKeys: null, + signaturesRequired: 0, + }, + }, + ], + siacoinOutputs: [ + { + value: '1000000000000000000000000', + address: + 'addr:90c6057cdd2463eca61f83796e83152dbba28b6cb9a74831a043833051ec9f422726bfff2ee8', + }, + { + value: '97984280000000000000000000', + address: + 'addr:f2dbf56b5b0c698d7fbf43f646c76169d84e597e8b37fada97348beeecaa812d400ac4ce7981', + }, + ], + minerFees: ['3930000000000000000000'], + }, + toSign: [ + 'h:aa3e781330c9b3991e0141807df1327fadf114ca6c37acb9e58004f942d91dfb', + 'h:32e430158591b4073a6834e9f4c4b67162e348844f569f4e472896bb72efb724', + ], + dependsOn: null, +} + +export const mockWalletAddressesResponse: WalletAddress[] = [ + { + address: + 'addr:f2dbf56b5b0c698d7fbf43f646c76169d84e597e8b37fada97348beeecaa812d400ac4ce7981', + description: '', + metadata: { + index: 0, + publicKey: + 'ed25519:ee122b2169bdae5776b55609e384e0c58372cd5c529d4edc9b9918b26f8e5535', + }, + }, + { + address: + 'addr:90c6057cdd2463eca61f83796e83152dbba28b6cb9a74831a043833051ec9f422726bfff2ee8', + description: '', + metadata: { + index: 1, + publicKey: + 'ed25519:624d6d477a8f4ceac873e6dd9138740f9322cb34a24246f96f9d64c021172f43', + }, + }, + { + address: + 'addr:170173c40ca0f39f9618da30af14c390c7ce70248a3662a7a5d3d5a8a31c9fbfa2071e9f6518', + description: '', + metadata: { + index: 2, + publicKey: + 'ed25519:65cac661a4acf36847c0aa67cbc6956e3449fd82a7430cfd673ea7fedbfcf5fa', + }, + }, +] diff --git a/apps/walletd-e2e/src/specs/seedSendSiacoin/seedSendSiacoin.spec.ts b/apps/walletd-e2e/src/specs/seedSendSiacoin/seedSendSiacoin.spec.ts new file mode 100644 index 000000000..8f978625b --- /dev/null +++ b/apps/walletd-e2e/src/specs/seedSendSiacoin/seedSendSiacoin.spec.ts @@ -0,0 +1,194 @@ +import { test, expect } from '@playwright/test' +import { mockApiDefaults } from '../../mocks/mock' +import { login } from '../../fixtures/login' +import { createWallet } from '../../fixtures/createWallet' +import { navigateToWallet } from '../../fixtures/navigateToWallet' +import { + seed, + newWallet, + receiveAddress, + changeAddress, + mockWalletFundResponse, + mockWalletOutputsSiacoinResponse, + mockWalletAddressesResponse, +} from './seedSendSiacoin.mock' +import { fillComposeTransactionSiacoin } from '../../fixtures/sendSiacoinDialog' +import { cloneDeep } from '@technically/lodash' + +const defaultMockWalletResponses = { + fund: mockWalletFundResponse, + outputsSiacoin: mockWalletOutputsSiacoinResponse, + addresses: mockWalletAddressesResponse, +} + +test('send siacoin with a seed wallet', async ({ page }) => { + await mockApiDefaults({ page }) + await login({ page }) + await createWallet({ + page, + newWallet, + seed, + responses: defaultMockWalletResponses, + }) + await navigateToWallet({ page, wallet: newWallet }) + await page.getByLabel('send').click() + await fillComposeTransactionSiacoin({ + page, + receiveAddress, + changeAddress, + amount: '1', + }) + await expect(page.getByText('The wallet is currently unlocked')).toBeVisible() + await expect(page.getByText('Total')).toBeVisible() + await expect(page.getByText('1.004 SC')).toBeVisible() + + await page + .getByRole('button', { name: 'Sign and broadcast transaction' }) + .click() + await expect( + page.getByText('Transaction successfully broadcasted') + ).toBeVisible() + await expect(page.getByText(receiveAddress.slice(0, 5))).toBeVisible() + await expect(page.getByText(changeAddress.slice(0, 5))).toBeVisible() + await expect(page.getByText('Total')).toBeVisible() + await expect(page.getByText('1.004 SC')).toBeVisible() +}) + +test('errors if the input to sign is not found on the transaction', async ({ + page, +}) => { + await mockApiDefaults({ page }) + const mockFundInvalid = cloneDeep(mockWalletFundResponse) + mockFundInvalid.transaction.siacoinInputs[0].parentID = + 'scoid:bb3e781330c9b3991e0141807df1327fadf114ca6c37acb9e58004f942d91dfb' + await login({ page }) + await createWallet({ + page, + newWallet, + seed, + responses: { + ...defaultMockWalletResponses, + fund: mockFundInvalid, + }, + }) + await navigateToWallet({ page, wallet: newWallet }) + await page.getByLabel('send').click() + await fillComposeTransactionSiacoin({ + page, + receiveAddress, + changeAddress, + amount: '1', + }) + await expect(page.getByText('The wallet is currently unlocked')).toBeVisible() + await expect(page.getByText('Total')).toBeVisible() + await expect(page.getByText('1.004 SC')).toBeVisible() + + await page + .getByRole('button', { name: 'Sign and broadcast transaction' }) + .click() + await expect(page.getByText('Missing input')).toBeVisible() +}) + +test('errors if the inputs matching utxo is not found', async ({ page }) => { + await mockApiDefaults({ page }) + const mockOutputsInvalid = cloneDeep(mockWalletOutputsSiacoinResponse) + mockOutputsInvalid.forEach((output) => { + output.id = 'not the matching id' + }) + await login({ page }) + await createWallet({ + page, + newWallet, + seed, + responses: { + ...defaultMockWalletResponses, + outputsSiacoin: mockOutputsInvalid, + }, + }) + await navigateToWallet({ page, wallet: newWallet }) + await page.getByLabel('send').click() + await fillComposeTransactionSiacoin({ + page, + receiveAddress, + changeAddress, + amount: '1', + }) + await expect(page.getByText('The wallet is currently unlocked')).toBeVisible() + await expect(page.getByText('Total')).toBeVisible() + await expect(page.getByText('1.004 SC')).toBeVisible() + + await page + .getByRole('button', { name: 'Sign and broadcast transaction' }) + .click() + await expect(page.getByText('Missing utxo')).toBeVisible() +}) + +test('errors if the address is missing its index', async ({ page }) => { + await mockApiDefaults({ page }) + await login({ page }) + + const mockAddressesInvalid = cloneDeep(mockWalletAddressesResponse) + mockAddressesInvalid.forEach((address) => { + address.metadata.index = undefined + }) + await createWallet({ + page, + newWallet, + seed, + responses: { + ...defaultMockWalletResponses, + addresses: mockAddressesInvalid, + }, + }) + await navigateToWallet({ page, wallet: newWallet }) + await page.getByLabel('send').click() + await fillComposeTransactionSiacoin({ + page, + receiveAddress, + changeAddress, + amount: '1', + }) + await expect(page.getByText('The wallet is currently unlocked')).toBeVisible() + await expect(page.getByText('Total')).toBeVisible() + await expect(page.getByText('1.004 SC')).toBeVisible() + + await page + .getByRole('button', { name: 'Sign and broadcast transaction' }) + .click() + await expect(page.getByText('Missing address index')).toBeVisible() +}) + +test('errors if the address is missing its public key', async ({ page }) => { + await mockApiDefaults({ page }) + await login({ page }) + + const mockAddressesInvalid = cloneDeep(mockWalletAddressesResponse) + mockAddressesInvalid.forEach((address) => { + address.metadata.publicKey = undefined + }) + await createWallet({ + page, + newWallet, + seed, + responses: { + ...defaultMockWalletResponses, + addresses: mockAddressesInvalid, + }, + }) + await navigateToWallet({ page, wallet: newWallet }) + await page.getByLabel('send').click() + await fillComposeTransactionSiacoin({ + page, + receiveAddress, + changeAddress, + amount: '1', + }) + await expect(page.getByText('The wallet is currently unlocked')).toBeVisible() + await expect(page.getByText('Total')).toBeVisible() + await expect(page.getByText('1.004 SC')).toBeVisible() + + await page + .getByRole('button', { name: 'Sign and broadcast transaction' }) + .click() + await expect(page.getByText('Missing address public key')).toBeVisible() +}) diff --git a/apps/walletd-e2e/src/specs/walletCreate.spec.ts b/apps/walletd-e2e/src/specs/walletCreate.spec.ts new file mode 100644 index 000000000..929309078 --- /dev/null +++ b/apps/walletd-e2e/src/specs/walletCreate.spec.ts @@ -0,0 +1,30 @@ +import { test, expect } from '@playwright/test' +import { mockApiDefaults } from '../mocks/mock' +import { login } from '../fixtures/login' +import { createWallet } from '../fixtures/createWallet' + +const seed = + 'credit tonight gauge army noodle reopen pepper property try mom taste solid' + +const newWallet = { + id: '100', + name: 'new wallet 1', + description: 'wallet description', + dateCreated: new Date().toISOString(), + lastUpdated: new Date().toISOString(), + metadata: { + type: 'seed', + seedHash: + '9fe676742eb80d61fea4476e2d33a088d708a24c2762fa028f9bfbe693612d15b83223f7ba5bc2e0fe2390d243a47a18144f19acd34b105757b1ac7751da821f', + }, +} + +test('wallet create', async ({ page }) => { + await mockApiDefaults({ page }) + await login({ page }) + await createWallet({ page, newWallet, seed }) + await expect(page.locator(`span:text('${newWallet.name}')`)).toBeVisible() + await page.locator(`span:text('${newWallet.name}')`).click() + await expect(page.getByText('The wallet has no')).toBeVisible() + await expect(page.getByRole('link', { name: 'Addresses' })).toBeVisible() +}) diff --git a/apps/walletd-e2e/tsconfig.json b/apps/walletd-e2e/tsconfig.json new file mode 100644 index 000000000..fd1b9df91 --- /dev/null +++ b/apps/walletd-e2e/tsconfig.json @@ -0,0 +1,17 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "jsx": "preserve", + "allowJs": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "strict": false, + "forceConsistentCasingInFileNames": true, + "noEmit": true, + "resolveJsonModule": true, + "isolatedModules": true, + "incremental": true, + "types": ["jest", "node"] + }, + "include": ["**/*.ts", "**/*.js"] +} diff --git a/apps/walletd/components/Wallet/AddressesButton.tsx b/apps/walletd/components/Wallet/AddressesButton.tsx index 1e6ccdb70..6e25c4977 100644 --- a/apps/walletd/components/Wallet/AddressesButton.tsx +++ b/apps/walletd/components/Wallet/AddressesButton.tsx @@ -8,6 +8,7 @@ export function AddressesButton() { const id = router.query.id as string return ( {wallet?.metadata.type !== 'watch' && ( diff --git a/libs/design-system/src/core/DropdownMenu.tsx b/libs/design-system/src/core/DropdownMenu.tsx index 3cdaccf39..98f9e8a47 100644 --- a/libs/design-system/src/core/DropdownMenu.tsx +++ b/libs/design-system/src/core/DropdownMenu.tsx @@ -48,6 +48,7 @@ const animationVariants = { export const DropdownMenu = React.forwardRef< React.ElementRef, { + label?: string rootProps?: React.ComponentProps contentProps?: React.ComponentProps trigger: React.ReactNode @@ -60,6 +61,7 @@ export const DropdownMenu = React.forwardRef< ( { trigger, + label, children, rootProps, contentProps, @@ -80,7 +82,7 @@ export const DropdownMenu = React.forwardRef< onOpenChange={onOpenChange} {...rootProps} > - + {trigger} diff --git a/libs/design-system/src/core/SiacoinField.tsx b/libs/design-system/src/core/SiacoinField.tsx index 86efba48e..2dcfe1be2 100644 --- a/libs/design-system/src/core/SiacoinField.tsx +++ b/libs/design-system/src/core/SiacoinField.tsx @@ -39,6 +39,7 @@ export function SiacoinField({ prefix, onBlur, onFocus, + name, ...props }: Props) { const externalSc = useMemo( @@ -171,6 +172,7 @@ export function SiacoinField({ > ) { +export function useConsensusTip( + args?: HookArgsSwr +) { return useGetSwr({ ...args, route: '/consensus/tip', }) } -export function useConsensusTipState(args?: HookArgsSwr) { +export type ConsensusStateResponse = ConsensusState + +export function useConsensusTipState( + args?: HookArgsSwr +) { return useGetSwr({ ...args, route: '/consensus/tipstate', }) } +export type ConsensusNetworkResponse = ConsensusNetwork + export function useConsensusNetwork( - args?: HookArgsSwr + args?: HookArgsSwr ) { return useGetSwr({ ...args, @@ -87,7 +95,9 @@ export function useEstimatedNetworkBlockHeight(): number { export const syncerPeersKey = '/syncer/peers' -export function useSyncerPeers(args?: HookArgsSwr) { +export type SyncerPeersResponse = GatewayPeer[] + +export function useSyncerPeers(args?: HookArgsSwr) { return useGetSwr({ ...args, route: syncerPeersKey, @@ -120,7 +130,9 @@ export function useTxPoolTransactions( return useGetSwr({ ...args, route: txPoolTransactionsRoute }) } -export function useTxPoolFee(args?: HookArgsSwr) { +export type TxPoolFeeResponse = Currency + +export function useTxPoolFee(args?: HookArgsSwr) { return useGetSwr({ ...args, route: '/txpool/fee' }) } @@ -165,7 +177,9 @@ export function useResubscribe( const walletsRoute = '/wallets' -export function useWallets(args?: HookArgsSwr) { +export type WalletsResponse = Wallet[] + +export function useWallets(args?: HookArgsSwr) { return useGetSwr({ ...args, route: walletsRoute, @@ -178,8 +192,10 @@ type WalletUpdatePayload = { metadata: Metadata } +export type WalletAddResponse = Wallet + export function useWalletAdd( - args?: HookArgsCallback + args?: HookArgsCallback ) { return usePostFunc( { @@ -192,8 +208,14 @@ export function useWalletAdd( ) } +export type WalletUpdateResponse = Wallet + export function useWalletUpdate( - args?: HookArgsCallback<{ id: string }, WalletUpdatePayload, Wallet> + args?: HookArgsCallback< + { id: string }, + WalletUpdatePayload, + WalletUpdateResponse + > ) { return usePostFunc( { @@ -220,10 +242,11 @@ export function useWalletDelete( } // addresses +export type WalletAddressesResponse = WalletAddress[] export const walletAddressesRoute = '/wallets/:id/addresses' export function useWalletAddresses( - args: HookArgsSwr<{ id: string }, WalletAddress[]> + args: HookArgsSwr<{ id: string }, WalletAddressesResponse> ) { return useGetSwr({ ...args, @@ -231,8 +254,10 @@ export function useWalletAddresses( }) } +export type WalletAddressAddResponse = WalletAddress + export function useWalletAddressAdd( - args?: HookArgsCallback<{ id: string }, WalletAddress, void> + args?: HookArgsCallback<{ id: string }, WalletAddressAddResponse, void> ) { return usePutFunc( { @@ -260,12 +285,15 @@ export function useWalletAddressDelete( ) } +export type WalletBalanceResponse = { + siacoins: Currency + immatureSiacoins: Currency + siafunds: number +} + const walletBalanceRoute = '/wallets/:id/balance' export function useWalletBalance( - args: HookArgsSwr< - { id: string }, - { siacoins: Currency; immatureSiacoins: Currency; siafunds: number } - > + args: HookArgsSwr<{ id: string }, WalletBalanceResponse> ) { return useGetSwr({ ...args, @@ -288,8 +316,10 @@ export function useWalletEvents( const walletTxPoolRoute = '/wallets/:id/txpool' +export type WalletTxPoolResponse = PoolTransaction[] + export function useWalletTxPool( - args: HookArgsSwr<{ id: string }, PoolTransaction[]> + args: HookArgsSwr<{ id: string }, WalletTxPoolResponse> ) { return useGetSwr({ ...args, @@ -297,8 +327,10 @@ export function useWalletTxPool( }) } +export type WalletOutputsSiacoinResponse = SiacoinElement[] + export function useWalletOutputsSiacoin( - args: HookArgsSwr<{ id: string }, SiacoinElement[]> + args: HookArgsSwr<{ id: string }, WalletOutputsSiacoinResponse> ) { return useGetSwr({ ...args, @@ -306,8 +338,10 @@ export function useWalletOutputsSiacoin( }) } +export type WalletOutputsSiafundResponse = SiafundElement[] + export function useWalletOutputsSiafund( - args: HookArgsSwr<{ id: string }, SiafundElement[]> + args: HookArgsSwr<{ id: string }, WalletOutputsSiafundResponse> ) { return useGetSwr({ ...args, @@ -315,7 +349,7 @@ export function useWalletOutputsSiafund( }) } -type WalletFundSiacoinPayload = { +export type WalletFundSiacoinPayload = { transaction: Transaction amount: Currency changeAddress: string @@ -337,7 +371,7 @@ export function useWalletFundSiacoin( return usePostFunc({ ...args, route: '/wallets/:id/fund' }) } -type WalletFundSiafundPayload = { +export type WalletFundSiafundPayload = { transaction: Transaction amount: number changeAddress: string @@ -354,7 +388,7 @@ export function useWalletFundSiafund( return usePostFunc({ ...args, route: '/wallets/:id/fundsf' }) } -type WalletReservePayload = { +export type WalletReservePayload = { siacoinOutputs?: SiacoinOutputID[] siafundOutputs?: SiafundOutputID[] duration: number @@ -366,7 +400,7 @@ export function useWalletReserve( return usePostFunc({ ...args, route: '/wallets/:id/reserve' }) } -type WalletReleasePayload = { +export type WalletReleasePayload = { siacoinOutputs?: SiacoinOutputID[] siafundOutputs?: SiafundOutputID[] } diff --git a/nx.json b/nx.json index b0758c6dc..1f4b4e3c4 100644 --- a/nx.json +++ b/nx.json @@ -44,6 +44,15 @@ "cache": true, "dependsOn": ["^build"], "inputs": ["production", "^production"] + }, + "@nx/webpack:webpack": { + "cache": true, + "dependsOn": ["^build"], + "inputs": ["production", "^production"] + }, + "e2e": { + "cache": true, + "inputs": ["default", "^production"] } }, "namedInputs": { diff --git a/package-lock.json b/package-lock.json index dfca58b52..b26886cea 100644 --- a/package-lock.json +++ b/package-lock.json @@ -84,6 +84,7 @@ "next-themes": "^0.2.1", "node-cron": "^3.0.2", "node-fetch": "^3.3.2", + "playwright": "^1.42.1", "react": "18.2.0", "react-dom": "18.2.0", "react-dropzone": "^14.2.3", @@ -117,19 +118,25 @@ "@commitlint/cli": "^16.2.4", "@commitlint/config-angular": "^16.2.4", "@commitlint/config-conventional": "^16.2.4", + "@nx/devkit": "18.0.3", "@nx/eslint": "18.0.3", "@nx/eslint-plugin": "18.0.3", "@nx/jest": "18.0.3", "@nx/js": "18.0.3", "@nx/next": "18.0.3", + "@nx/playwright": "18.0.3", "@nx/react": "18.0.3", "@nx/rollup": "18.0.3", "@nx/webpack": "18.0.3", "@nx/workspace": "18.0.3", + "@playwright/test": "^1.36.0", "@pmmmwh/react-refresh-webpack-plugin": "^0.5.7", "@rollup/plugin-wasm": "^6.2.2", "@svgr/webpack": "8.1.0", + "@swc-node/register": "~1.6.7", + "@swc/cli": "~0.1.62", "@swc/core": "1.3.102", + "@swc/helpers": "~0.5.2", "@swc/jest": "0.2.20", "@testing-library/react": "^14.0.0", "@testing-library/user-event": "^14.5.2", @@ -155,6 +162,7 @@ "eslint-plugin-cypress": "2.14.0", "eslint-plugin-import": "2.27.5", "eslint-plugin-jsx-a11y": "6.7.1", + "eslint-plugin-playwright": "^0.15.3", "eslint-plugin-react": "7.32.2", "eslint-plugin-react-hooks": "4.6.0", "husky": "^7.0.4", @@ -446,6 +454,9 @@ "name": "@siafoundation/sdk", "version": "0.0.2", "license": "MIT", + "dependencies": { + "@siafoundation/types": "0.1.3" + }, "devDependencies": { "undici": "5.28.3" } @@ -4860,6 +4871,25 @@ "react": ">=16" } }, + "node_modules/@mole-inc/bin-wrapper": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/@mole-inc/bin-wrapper/-/bin-wrapper-8.0.1.tgz", + "integrity": "sha512-sTGoeZnjI8N4KS+sW2AN95gDBErhAguvkw/tWdCjeM8bvxpz5lqrnd0vOJABA1A+Ic3zED7PYoLP/RANLgVotA==", + "dev": true, + "dependencies": { + "bin-check": "^4.1.0", + "bin-version-check": "^5.0.0", + "content-disposition": "^0.5.4", + "ext-name": "^5.0.0", + "file-type": "^17.1.6", + "filenamify": "^5.0.2", + "got": "^11.8.5", + "os-filter-obj": "^2.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, "node_modules/@motionone/animation": { "version": "10.14.0", "resolved": "https://registry.npmjs.org/@motionone/animation/-/animation-10.14.0.tgz", @@ -5801,6 +5831,52 @@ "node": ">= 10" } }, + "node_modules/@nx/playwright": { + "version": "18.0.3", + "resolved": "https://registry.npmjs.org/@nx/playwright/-/playwright-18.0.3.tgz", + "integrity": "sha512-fdfYndOIKH4FVHY1ZR5TayAD1oaV3iP2PIindi1+mtlTU5MZCWa8x23MQlbcDbhjIuDmpreqy8l7r+A/cN2gNw==", + "dev": true, + "dependencies": { + "@nx/devkit": "18.0.3", + "@nx/eslint": "18.0.3", + "@nx/js": "18.0.3", + "@phenomnomnominal/tsquery": "~5.0.1", + "minimatch": "9.0.3", + "tslib": "^2.3.0" + }, + "peerDependencies": { + "@playwright/test": "^1.36.0" + }, + "peerDependenciesMeta": { + "@playwright/test": { + "optional": true + } + } + }, + "node_modules/@nx/playwright/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@nx/playwright/node_modules/minimatch": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/@nx/react": { "version": "18.0.3", "resolved": "https://registry.npmjs.org/@nx/react/-/react-18.0.3.tgz", @@ -6048,6 +6124,21 @@ "url": "https://opencollective.com/unts" } }, + "node_modules/@playwright/test": { + "version": "1.42.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.42.1.tgz", + "integrity": "sha512-Gq9rmS54mjBL/7/MvBaNOBwbfnh7beHvS6oS4srqXFcQHpQCV1+c8JXWE8VLPyRDhgS3H8x8A7hztqI9VnwrAQ==", + "dev": true, + "dependencies": { + "playwright": "1.42.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/@pmmmwh/react-refresh-webpack-plugin": { "version": "0.5.10", "resolved": "https://registry.npmjs.org/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.5.10.tgz", @@ -7596,6 +7687,18 @@ "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==" }, + "node_modules/@sindresorhus/is": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", + "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, "node_modules/@sinonjs/commons": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.0.tgz", @@ -7935,8 +8038,7 @@ "version": "1.12.0", "resolved": "https://registry.npmjs.org/@swc-node/core/-/core-1.12.0.tgz", "integrity": "sha512-AYrEmPL2BT46wbikHwSMR5IK98SelBEYH+ycjalUxJ5xYjEupjF8Fd+NkadKoZAzf5zDtysFKd5R1PY4QBHIiw==", - "optional": true, - "peer": true, + "devOptional": true, "engines": { "node": ">= 10" }, @@ -7953,8 +8055,7 @@ "version": "1.6.8", "resolved": "https://registry.npmjs.org/@swc-node/register/-/register-1.6.8.tgz", "integrity": "sha512-74ijy7J9CWr1Z88yO+ykXphV29giCrSpANQPQRooE0bObpkTO1g4RzQovIfbIaniBiGDDVsYwDoQ3FIrCE8HcQ==", - "optional": true, - "peer": true, + "devOptional": true, "dependencies": { "@swc-node/core": "^1.10.6", "@swc-node/sourcemap-support": "^0.3.0", @@ -7976,8 +8077,7 @@ "version": "0.3.0", "resolved": "https://registry.npmjs.org/@swc-node/sourcemap-support/-/sourcemap-support-0.3.0.tgz", "integrity": "sha512-gqBJSmJMWomZFxlppaKea7NeAqFrDrrS0RMt24No92M3nJWcyI9YKGEQKl+EyJqZ5gh6w1s0cTklMHMzRwA1NA==", - "optional": true, - "peer": true, + "devOptional": true, "dependencies": { "source-map-support": "^0.5.21", "tslib": "^2.5.0" @@ -7987,13 +8087,86 @@ "version": "0.5.21", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", - "optional": true, - "peer": true, + "devOptional": true, "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, + "node_modules/@swc/cli": { + "version": "0.1.65", + "resolved": "https://registry.npmjs.org/@swc/cli/-/cli-0.1.65.tgz", + "integrity": "sha512-4NcgsvJVHhA7trDnMmkGLLvWMHu2kSy+qHx6QwRhhJhdiYdNUrhdp+ERxen73sYtaeEOYeLJcWrQ60nzKi6rpg==", + "dev": true, + "dependencies": { + "@mole-inc/bin-wrapper": "^8.0.1", + "commander": "^7.1.0", + "fast-glob": "^3.2.5", + "minimatch": "^9.0.3", + "semver": "^7.3.8", + "slash": "3.0.0", + "source-map": "^0.7.3" + }, + "bin": { + "spack": "bin/spack.js", + "swc": "bin/swc.js", + "swcx": "bin/swcx.js" + }, + "engines": { + "node": ">= 12.13" + }, + "peerDependencies": { + "@swc/core": "^1.2.66", + "chokidar": "^3.5.1" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, + "node_modules/@swc/cli/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@swc/cli/node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "dev": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@swc/cli/node_modules/minimatch": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@swc/cli/node_modules/source-map": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", + "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, "node_modules/@swc/core": { "version": "1.3.102", "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.3.102.tgz", @@ -8227,6 +8400,18 @@ "integrity": "sha512-myfUej5naTBWnqOCc/MdVOLVjXUXtIA+NpDrDBKJtLLg2shUjBu3cZmB/85RyitKc55+lUUyl7oRfLOvkr2hsw==", "devOptional": true }, + "node_modules/@szmarczak/http-timer": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", + "integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==", + "dev": true, + "dependencies": { + "defer-to-connect": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/@tailwindcss/container-queries": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/@tailwindcss/container-queries/-/container-queries-0.1.1.tgz", @@ -8295,6 +8480,12 @@ "@testing-library/dom": ">=7.21.4" } }, + "node_modules/@tokenizer/token": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", + "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==", + "dev": true + }, "node_modules/@tootallnate/once": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", @@ -8446,6 +8637,18 @@ "@types/node": "*" } }, + "node_modules/@types/cacheable-request": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz", + "integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==", + "dev": true, + "dependencies": { + "@types/http-cache-semantics": "*", + "@types/keyv": "^3.1.4", + "@types/node": "*", + "@types/responselike": "^1.0.0" + } + }, "node_modules/@types/carbon__icons-react": { "version": "10.31.2", "resolved": "https://registry.npmjs.org/@types/carbon__icons-react/-/carbon__icons-react-10.31.2.tgz", @@ -8638,6 +8841,12 @@ "@types/unist": "*" } }, + "node_modules/@types/http-cache-semantics": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz", + "integrity": "sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==", + "dev": true + }, "node_modules/@types/http-errors": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.1.tgz", @@ -8755,6 +8964,15 @@ "integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4=", "dev": true }, + "node_modules/@types/keyv": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz", + "integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/lodash": { "version": "4.14.202", "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.202.tgz", @@ -8928,6 +9146,15 @@ "@types/node": "*" } }, + "node_modules/@types/responselike": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz", + "integrity": "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/retry": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", @@ -10740,6 +10967,169 @@ "node": "*" } }, + "node_modules/bin-check": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bin-check/-/bin-check-4.1.0.tgz", + "integrity": "sha512-b6weQyEUKsDGFlACWSIOfveEnImkJyK/FGW6FAG42loyoquvjdtOIqO6yBFzHyqyVVhNgNkQxxx09SFLK28YnA==", + "dev": true, + "dependencies": { + "execa": "^0.7.0", + "executable": "^4.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/bin-check/node_modules/cross-spawn": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz", + "integrity": "sha512-pTgQJ5KC0d2hcY8eyL1IzlBPYjTkyH72XRZPnLyKus2mBfNjQs3klqbJU2VILqZryAZUt9JOb3h/mWMy23/f5A==", + "dev": true, + "dependencies": { + "lru-cache": "^4.0.1", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + } + }, + "node_modules/bin-check/node_modules/execa": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-0.7.0.tgz", + "integrity": "sha512-RztN09XglpYI7aBBrJCPW95jEH7YF1UEPOoX9yDhUTPdp7mK+CQvnLTuD10BNXZ3byLTu2uehZ8EcKT/4CGiFw==", + "dev": true, + "dependencies": { + "cross-spawn": "^5.0.1", + "get-stream": "^3.0.0", + "is-stream": "^1.1.0", + "npm-run-path": "^2.0.0", + "p-finally": "^1.0.0", + "signal-exit": "^3.0.0", + "strip-eof": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/bin-check/node_modules/get-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", + "integrity": "sha512-GlhdIUuVakc8SJ6kK0zAFbiGzRFzNnY4jUuEbV9UROo4Y+0Ny4fjvcZFVTeDA4odpFyOQzaw6hXukJSq/f28sQ==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/bin-check/node_modules/is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/bin-check/node_modules/lru-cache": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", + "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==", + "dev": true, + "dependencies": { + "pseudomap": "^1.0.2", + "yallist": "^2.1.2" + } + }, + "node_modules/bin-check/node_modules/npm-run-path": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", + "integrity": "sha512-lJxZYlT4DW/bRUtFh1MQIWqmLwQfAxnqWG4HhEdjMlkrJYnJn0Jrr2u3mgxqaWsdiBc76TYkTG/mhrnYTuzfHw==", + "dev": true, + "dependencies": { + "path-key": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/bin-check/node_modules/path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/bin-check/node_modules/shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==", + "dev": true, + "dependencies": { + "shebang-regex": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/bin-check/node_modules/shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/bin-check/node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, + "node_modules/bin-check/node_modules/yallist": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", + "integrity": "sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A==", + "dev": true + }, + "node_modules/bin-version": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/bin-version/-/bin-version-6.0.0.tgz", + "integrity": "sha512-nk5wEsP4RiKjG+vF+uG8lFsEn4d7Y6FVDamzzftSunXOoOcOOkzcWdKVlGgFFwlUQCj63SgnUkLLGF8v7lufhw==", + "dev": true, + "dependencies": { + "execa": "^5.0.0", + "find-versions": "^5.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/bin-version-check": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/bin-version-check/-/bin-version-check-5.1.0.tgz", + "integrity": "sha512-bYsvMqJ8yNGILLz1KP9zKLzQ6YpljV3ln1gqhuLkUtyfGi3qXKGuK2p+U4NAvjVFzDFiBBtOpCOSFNuYYEGZ5g==", + "dev": true, + "dependencies": { + "bin-version": "^6.0.0", + "semver": "^7.5.3", + "semver-truncate": "^3.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/binary-extensions": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", @@ -10986,6 +11376,48 @@ "node": ">= 0.8" } }, + "node_modules/cacheable-lookup": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", + "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==", + "dev": true, + "engines": { + "node": ">=10.6.0" + } + }, + "node_modules/cacheable-request": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.4.tgz", + "integrity": "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==", + "dev": true, + "dependencies": { + "clone-response": "^1.0.2", + "get-stream": "^5.1.0", + "http-cache-semantics": "^4.0.0", + "keyv": "^4.0.0", + "lowercase-keys": "^2.0.0", + "normalize-url": "^6.0.1", + "responselike": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cacheable-request/node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dev": true, + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/cachedir": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/cachedir/-/cachedir-2.3.0.tgz", @@ -11369,6 +11801,27 @@ "node": ">=0.10.0" } }, + "node_modules/clone-response": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz", + "integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==", + "dev": true, + "dependencies": { + "mimic-response": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/clone-response/node_modules/mimic-response": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", + "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/clsx": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.0.0.tgz", @@ -13314,6 +13767,15 @@ "clone": "^1.0.2" } }, + "node_modules/defer-to-connect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", + "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", + "dev": true, + "engines": { + "node": ">=10" + } + }, "node_modules/define-lazy-prop": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", @@ -14455,6 +14917,21 @@ "semver": "bin/semver.js" } }, + "node_modules/eslint-plugin-playwright": { + "version": "0.15.3", + "resolved": "https://registry.npmjs.org/eslint-plugin-playwright/-/eslint-plugin-playwright-0.15.3.tgz", + "integrity": "sha512-LQMW5y0DLK5Fnpya7JR1oAYL2/7Y9wDiYw6VZqlKqcRGSgjbVKNqxraphk7ra1U3Bb5EK444xMgUlQPbMg2M1g==", + "dev": true, + "peerDependencies": { + "eslint": ">=7", + "eslint-plugin-jest": ">=25" + }, + "peerDependenciesMeta": { + "eslint-plugin-jest": { + "optional": true + } + } + }, "node_modules/eslint-plugin-react": { "version": "7.32.2", "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.32.2.tgz", @@ -15013,6 +15490,31 @@ } ] }, + "node_modules/ext-list": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/ext-list/-/ext-list-2.2.2.tgz", + "integrity": "sha512-u+SQgsubraE6zItfVA0tBuCBhfU9ogSRnsvygI7wht9TS510oLkBRXBsqopeUG/GBOIQyKZO9wjTqIu/sf5zFA==", + "dev": true, + "dependencies": { + "mime-db": "^1.28.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ext-name": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ext-name/-/ext-name-5.0.0.tgz", + "integrity": "sha512-yblEwXAbGv1VQDmow7s38W77hzAgJAO50ztBLMcUyUBfxv1HC+LGwtiEN+Co6LtlqT/5uwVOxsD4TNIilWhwdQ==", + "dev": true, + "dependencies": { + "ext-list": "^2.0.0", + "sort-keys-length": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", @@ -15241,6 +15743,23 @@ "node": ">= 12" } }, + "node_modules/file-type": { + "version": "17.1.6", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-17.1.6.tgz", + "integrity": "sha512-hlDw5Ev+9e883s0pwUsuuYNu4tD7GgpUnOvykjv1Gya0ZIjuKumthDRua90VUn6/nlRKAjcxLUnHNTIUWwWIiw==", + "dev": true, + "dependencies": { + "readable-web-to-node-stream": "^3.0.2", + "strtok3": "^7.0.0-alpha.9", + "token-types": "^5.0.0-alpha.2" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/file-type?sponsor=1" + } + }, "node_modules/filelist": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", @@ -15268,6 +15787,35 @@ "node": ">=10" } }, + "node_modules/filename-reserved-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/filename-reserved-regex/-/filename-reserved-regex-3.0.0.tgz", + "integrity": "sha512-hn4cQfU6GOT/7cFHXBqeBg2TbrMBgdD0kcjLhvSQYYwm3s4B6cjvBfb7nBALJLAXqmU5xajSa7X2NnUud/VCdw==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/filenamify": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/filenamify/-/filenamify-5.1.1.tgz", + "integrity": "sha512-M45CbrJLGACfrPOkrTp3j2EcO9OBkKUYME0eiqOCa7i2poaklU0jhlIaMlr8ijLorT0uLAzrn3qXOp5684CkfA==", + "dev": true, + "dependencies": { + "filename-reserved-regex": "^3.0.0", + "strip-outer": "^2.0.0", + "trim-repeated": "^2.0.0" + }, + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/fill-range": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", @@ -15437,6 +15985,21 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/find-versions": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/find-versions/-/find-versions-5.1.0.tgz", + "integrity": "sha512-+iwzCJ7C5v5KgcBuueqVoNiHVoQpwiUK5XFLjf0affFTep+Wcw93tPvmb8tqujDNmzhBDPddnWV/qgWSXgq+Hg==", + "dev": true, + "dependencies": { + "semver-regex": "^4.0.5" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/find-yarn-workspace-root2": { "version": "1.2.16", "resolved": "https://registry.npmjs.org/find-yarn-workspace-root2/-/find-yarn-workspace-root2-1.2.16.tgz", @@ -16078,6 +16641,31 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/got": { + "version": "11.8.6", + "resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz", + "integrity": "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==", + "dev": true, + "dependencies": { + "@sindresorhus/is": "^4.0.0", + "@szmarczak/http-timer": "^4.0.5", + "@types/cacheable-request": "^6.0.1", + "@types/responselike": "^1.0.0", + "cacheable-lookup": "^5.0.3", + "cacheable-request": "^7.0.2", + "decompress-response": "^6.0.0", + "http2-wrapper": "^1.0.0-beta.5.2", + "lowercase-keys": "^2.0.0", + "p-cancelable": "^2.0.0", + "responselike": "^2.0.0" + }, + "engines": { + "node": ">=10.19.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/got?sponsor=1" + } + }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", @@ -16426,6 +17014,12 @@ "resolved": "https://registry.npmjs.org/html-to-image/-/html-to-image-1.11.11.tgz", "integrity": "sha512-9gux8QhvjRO/erSnDPv28noDZcPZmYE7e1vFsBLKLlRlKDSqNJYebj6Qz1TGd5lsRV+X+xYyjCKjuZdABinWjA==" }, + "node_modules/http-cache-semantics": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", + "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==", + "dev": true + }, "node_modules/http-deceiver": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz", @@ -16545,6 +17139,31 @@ "node": ">=0.10" } }, + "node_modules/http2-wrapper": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz", + "integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==", + "dev": true, + "dependencies": { + "quick-lru": "^5.1.1", + "resolve-alpn": "^1.0.0" + }, + "engines": { + "node": ">=10.19.0" + } + }, + "node_modules/http2-wrapper/node_modules/quick-lru": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/https-proxy-agent": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", @@ -18572,6 +19191,12 @@ "node": ">=4" } }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true + }, "node_modules/json-parse-even-better-errors": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", @@ -18707,6 +19332,15 @@ "node": ">=12" } }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "dependencies": { + "json-buffer": "3.0.1" + } + }, "node_modules/kind-of": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", @@ -19169,6 +19803,15 @@ "tslib": "^2.0.3" } }, + "node_modules/lowercase-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", + "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", @@ -21482,6 +22125,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/os-filter-obj": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/os-filter-obj/-/os-filter-obj-2.0.0.tgz", + "integrity": "sha512-uksVLsqG3pVdzzPvmAHpBK0wKxYItuzZr7SziusRPoz67tGV8rL1szZ6IdeUrbqLjGDwApBtN29eEE3IqGHOjg==", + "dev": true, + "dependencies": { + "arch": "^2.1.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/os-tmpdir": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", @@ -21507,6 +22162,15 @@ "integrity": "sha512-Ou3dJ6bA/UJ5GVHxah4LnqDwZRwAmWxrG3wtrHrbGnP4RnLCtA64A4F+ae7Y8ww660JaddSoArUR5HjipWSHAQ==", "dev": true }, + "node_modules/p-cancelable": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz", + "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/p-filter": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/p-filter/-/p-filter-2.1.0.tgz", @@ -21740,6 +22404,19 @@ "node": ">=8" } }, + "node_modules/peek-readable": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/peek-readable/-/peek-readable-5.0.0.tgz", + "integrity": "sha512-YtCKvLUOvwtMGmrniQPdO7MwPjgkFBtFIrmfSbYmYuq3tKDV/mcfAhBth1+C3ru7uXIZasc/pHnb+YDYNkkj4A==", + "dev": true, + "engines": { + "node": ">=14.16" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, "node_modules/pend": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", @@ -21865,6 +22542,47 @@ "node": ">=8" } }, + "node_modules/playwright": { + "version": "1.42.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.42.1.tgz", + "integrity": "sha512-PgwB03s2DZBcNRoW+1w9E+VkLBxweib6KTXM0M3tkiT4jVxKSi6PmVJ591J+0u10LUrgxB7dLRbiJqO5s2QPMg==", + "dependencies": { + "playwright-core": "1.42.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=16" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.42.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.42.1.tgz", + "integrity": "sha512-mxz6zclokgrke9p1vtdy/COWBH+eOZgYUVVU34C73M+4j4HLlQJHtfcqiqqxpP0o8HhMkflvfbquLX5dg6wlfA==", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/playwright/node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/polished": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/polished/-/polished-4.2.2.tgz", @@ -22966,6 +23684,22 @@ "node": ">= 6" } }, + "node_modules/readable-web-to-node-stream": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/readable-web-to-node-stream/-/readable-web-to-node-stream-3.0.2.tgz", + "integrity": "sha512-ePeK6cc1EcKLEhJFt/AebMCLL+GgSKhuygrZ/GLaKZYEecIgIECf4UaUuaByiGtzckwR4ain9VzUh95T1exYGw==", + "dev": true, + "dependencies": { + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, "node_modules/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", @@ -23355,6 +24089,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/resolve-alpn": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", + "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==", + "dev": true + }, "node_modules/resolve-cwd": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", @@ -23414,6 +24154,18 @@ "node": ">=10" } }, + "node_modules/responselike": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz", + "integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==", + "dev": true, + "dependencies": { + "lowercase-keys": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/restore-cursor": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", @@ -24622,6 +25374,33 @@ "node": ">=10" } }, + "node_modules/semver-regex": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/semver-regex/-/semver-regex-4.0.5.tgz", + "integrity": "sha512-hunMQrEy1T6Jr2uEVjrAIqjwWcQTgOAcIM52C8MY1EZSD3DDNft04XzvYKPqjED65bNVVko0YI38nYeEHCX3yw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/semver-truncate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/semver-truncate/-/semver-truncate-3.0.0.tgz", + "integrity": "sha512-LJWA9kSvMolR51oDE6PN3kALBNaUdkxzAGcexw8gjMA8xr5zUqK0JiR3CgARSqanYF3Z1YHvsErb1KDgh+v7Rg==", + "dev": true, + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/send": { "version": "0.18.0", "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", @@ -25102,6 +25881,39 @@ "uuid": "dist/bin/uuid" } }, + "node_modules/sort-keys": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-1.1.2.tgz", + "integrity": "sha512-vzn8aSqKgytVik0iwdBEi+zevbTYZogewTUM6dtpmGwEcdzbub/TX4bCzRhebDCRC3QzXgJsLRKB2V/Oof7HXg==", + "dev": true, + "dependencies": { + "is-plain-obj": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sort-keys-length": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/sort-keys-length/-/sort-keys-length-1.0.1.tgz", + "integrity": "sha512-GRbEOUqCxemTAk/b32F2xa8wDTs+Z1QHOkbhJDQTvv/6G3ZkbJ+frYWsTcc7cBB3Fu4wy4XlLCuNtJuMn7Gsvw==", + "dev": true, + "dependencies": { + "sort-keys": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sort-keys/node_modules/is-plain-obj": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", + "integrity": "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -25576,6 +26388,15 @@ "node": ">=0.10.0" } }, + "node_modules/strip-eof": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", + "integrity": "sha512-7FCwGGmx8mD5xQd3RPUvnSpUXHM3BWuzjtpD4TXsfcZ9EL4azvVVUscFYwD9nx8Kh+uCBC00XBtAykoMHwTh8Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/strip-final-newline": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", @@ -25608,6 +26429,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/strip-outer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-outer/-/strip-outer-2.0.0.tgz", + "integrity": "sha512-A21Xsm1XzUkK0qK1ZrytDUvqsQWict2Cykhvi0fBQntGG5JSprESasEyV1EZ/4CiR5WB5KjzLTrP/bO37B0wPg==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/strong-log-transformer": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/strong-log-transformer/-/strong-log-transformer-2.1.0.tgz", @@ -25624,6 +26457,23 @@ "node": ">=4" } }, + "node_modules/strtok3": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-7.0.0.tgz", + "integrity": "sha512-pQ+V+nYQdC5H3Q7qBZAz/MO6lwGhoC2gOAjuouGf/VO0m7vQRh8QNMl2Uf6SwAtzZ9bOw3UIeBukEGNJl5dtXQ==", + "dev": true, + "dependencies": { + "@tokenizer/token": "^0.3.0", + "peek-readable": "^5.0.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, "node_modules/style-inject": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/style-inject/-/style-inject-0.3.0.tgz", @@ -26370,6 +27220,23 @@ "node": ">=0.6" } }, + "node_modules/token-types": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/token-types/-/token-types-5.0.1.tgz", + "integrity": "sha512-Y2fmSnZjQdDb9W4w4r1tswlMHylzWIeOKpx0aZH9BgGtACHhrk3OkT52AzwcuqTRBZtvvnTjDBh8eynMulu8Vg==", + "dev": true, + "dependencies": { + "@tokenizer/token": "^0.3.0", + "ieee754": "^1.2.1" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, "node_modules/toposort": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/toposort/-/toposort-2.0.2.tgz", @@ -26410,6 +27277,30 @@ "node": ">=8" } }, + "node_modules/trim-repeated": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/trim-repeated/-/trim-repeated-2.0.0.tgz", + "integrity": "sha512-QUHBFTJGdOwmp0tbOG505xAgOp/YliZP/6UgafFXYZ26WT1bvQmSMJUvkeVSASuJJHbqsFbynTvkd5W8RBTipg==", + "dev": true, + "dependencies": { + "escape-string-regexp": "^5.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/trim-repeated/node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/trough": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/trough/-/trough-2.1.0.tgz", @@ -31536,6 +32427,22 @@ "@types/react": ">=16" } }, + "@mole-inc/bin-wrapper": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/@mole-inc/bin-wrapper/-/bin-wrapper-8.0.1.tgz", + "integrity": "sha512-sTGoeZnjI8N4KS+sW2AN95gDBErhAguvkw/tWdCjeM8bvxpz5lqrnd0vOJABA1A+Ic3zED7PYoLP/RANLgVotA==", + "dev": true, + "requires": { + "bin-check": "^4.1.0", + "bin-version-check": "^5.0.0", + "content-disposition": "^0.5.4", + "ext-name": "^5.0.0", + "file-type": "^17.1.6", + "filenamify": "^5.0.2", + "got": "^11.8.5", + "os-filter-obj": "^2.0.0" + } + }, "@motionone/animation": { "version": "10.14.0", "resolved": "https://registry.npmjs.org/@motionone/animation/-/animation-10.14.0.tgz", @@ -32203,6 +33110,40 @@ "integrity": "sha512-JkBx64Lg7Ezyr6RML/ajOdy9/NIbVO5dI5bG9ZmryeS5mnUPFBY0ExspL0t4X/ud+F4Sh7gN7uGanNd32wRD1Q==", "optional": true }, + "@nx/playwright": { + "version": "18.0.3", + "resolved": "https://registry.npmjs.org/@nx/playwright/-/playwright-18.0.3.tgz", + "integrity": "sha512-fdfYndOIKH4FVHY1ZR5TayAD1oaV3iP2PIindi1+mtlTU5MZCWa8x23MQlbcDbhjIuDmpreqy8l7r+A/cN2gNw==", + "dev": true, + "requires": { + "@nx/devkit": "18.0.3", + "@nx/eslint": "18.0.3", + "@nx/js": "18.0.3", + "@phenomnomnominal/tsquery": "~5.0.1", + "minimatch": "9.0.3", + "tslib": "^2.3.0" + }, + "dependencies": { + "brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0" + } + }, + "minimatch": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "dev": true, + "requires": { + "brace-expansion": "^2.0.1" + } + } + } + }, "@nx/react": { "version": "18.0.3", "resolved": "https://registry.npmjs.org/@nx/react/-/react-18.0.3.tgz", @@ -32436,6 +33377,15 @@ "tslib": "^2.4.0" } }, + "@playwright/test": { + "version": "1.42.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.42.1.tgz", + "integrity": "sha512-Gq9rmS54mjBL/7/MvBaNOBwbfnh7beHvS6oS4srqXFcQHpQCV1+c8JXWE8VLPyRDhgS3H8x8A7hztqI9VnwrAQ==", + "dev": true, + "requires": { + "playwright": "1.42.1" + } + }, "@pmmmwh/react-refresh-webpack-plugin": { "version": "0.5.10", "resolved": "https://registry.npmjs.org/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.5.10.tgz", @@ -33559,6 +34509,7 @@ "@siafoundation/sdk": { "version": "file:libs/sdk", "requires": { + "@siafoundation/types": "0.1.3", "undici": "5.28.3" } }, @@ -33583,6 +34534,12 @@ "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==" }, + "@sindresorhus/is": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", + "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", + "dev": true + }, "@sinonjs/commons": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.0.tgz", @@ -33775,16 +34732,14 @@ "version": "1.12.0", "resolved": "https://registry.npmjs.org/@swc-node/core/-/core-1.12.0.tgz", "integrity": "sha512-AYrEmPL2BT46wbikHwSMR5IK98SelBEYH+ycjalUxJ5xYjEupjF8Fd+NkadKoZAzf5zDtysFKd5R1PY4QBHIiw==", - "optional": true, - "peer": true, + "devOptional": true, "requires": {} }, "@swc-node/register": { "version": "1.6.8", "resolved": "https://registry.npmjs.org/@swc-node/register/-/register-1.6.8.tgz", "integrity": "sha512-74ijy7J9CWr1Z88yO+ykXphV29giCrSpANQPQRooE0bObpkTO1g4RzQovIfbIaniBiGDDVsYwDoQ3FIrCE8HcQ==", - "optional": true, - "peer": true, + "devOptional": true, "requires": { "@swc-node/core": "^1.10.6", "@swc-node/sourcemap-support": "^0.3.0", @@ -33798,8 +34753,7 @@ "version": "0.3.0", "resolved": "https://registry.npmjs.org/@swc-node/sourcemap-support/-/sourcemap-support-0.3.0.tgz", "integrity": "sha512-gqBJSmJMWomZFxlppaKea7NeAqFrDrrS0RMt24No92M3nJWcyI9YKGEQKl+EyJqZ5gh6w1s0cTklMHMzRwA1NA==", - "optional": true, - "peer": true, + "devOptional": true, "requires": { "source-map-support": "^0.5.21", "tslib": "^2.5.0" @@ -33809,8 +34763,7 @@ "version": "0.5.21", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", - "optional": true, - "peer": true, + "devOptional": true, "requires": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" @@ -33818,6 +34771,53 @@ } } }, + "@swc/cli": { + "version": "0.1.65", + "resolved": "https://registry.npmjs.org/@swc/cli/-/cli-0.1.65.tgz", + "integrity": "sha512-4NcgsvJVHhA7trDnMmkGLLvWMHu2kSy+qHx6QwRhhJhdiYdNUrhdp+ERxen73sYtaeEOYeLJcWrQ60nzKi6rpg==", + "dev": true, + "requires": { + "@mole-inc/bin-wrapper": "^8.0.1", + "commander": "^7.1.0", + "fast-glob": "^3.2.5", + "minimatch": "^9.0.3", + "semver": "^7.3.8", + "slash": "3.0.0", + "source-map": "^0.7.3" + }, + "dependencies": { + "brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0" + } + }, + "commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "dev": true + }, + "minimatch": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "dev": true, + "requires": { + "brace-expansion": "^2.0.1" + } + }, + "source-map": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", + "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", + "dev": true + } + } + }, "@swc/core": { "version": "1.3.102", "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.3.102.tgz", @@ -33937,6 +34937,15 @@ "integrity": "sha512-myfUej5naTBWnqOCc/MdVOLVjXUXtIA+NpDrDBKJtLLg2shUjBu3cZmB/85RyitKc55+lUUyl7oRfLOvkr2hsw==", "devOptional": true }, + "@szmarczak/http-timer": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", + "integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==", + "dev": true, + "requires": { + "defer-to-connect": "^2.0.0" + } + }, "@tailwindcss/container-queries": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/@tailwindcss/container-queries/-/container-queries-0.1.1.tgz", @@ -33987,6 +34996,12 @@ "dev": true, "requires": {} }, + "@tokenizer/token": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", + "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==", + "dev": true + }, "@tootallnate/once": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", @@ -34123,6 +35138,18 @@ "@types/node": "*" } }, + "@types/cacheable-request": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz", + "integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==", + "dev": true, + "requires": { + "@types/http-cache-semantics": "*", + "@types/keyv": "^3.1.4", + "@types/node": "*", + "@types/responselike": "^1.0.0" + } + }, "@types/carbon__icons-react": { "version": "10.31.2", "resolved": "https://registry.npmjs.org/@types/carbon__icons-react/-/carbon__icons-react-10.31.2.tgz", @@ -34315,6 +35342,12 @@ "@types/unist": "*" } }, + "@types/http-cache-semantics": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz", + "integrity": "sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==", + "dev": true + }, "@types/http-errors": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.1.tgz", @@ -34424,6 +35457,15 @@ "integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4=", "dev": true }, + "@types/keyv": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz", + "integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, "@types/lodash": { "version": "4.14.202", "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.202.tgz", @@ -34595,6 +35637,15 @@ "@types/node": "*" } }, + "@types/responselike": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz", + "integrity": "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, "@types/retry": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", @@ -35997,6 +37048,132 @@ "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.0.2.tgz", "integrity": "sha512-GAcQvbpsM0pUb0zw1EI0KhQEZ+lRwR5fYaAp3vPOYuP7aDvGy6cVN6XHLauvF8SOga2y0dcLcjt3iQDTSEliyw==" }, + "bin-check": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bin-check/-/bin-check-4.1.0.tgz", + "integrity": "sha512-b6weQyEUKsDGFlACWSIOfveEnImkJyK/FGW6FAG42loyoquvjdtOIqO6yBFzHyqyVVhNgNkQxxx09SFLK28YnA==", + "dev": true, + "requires": { + "execa": "^0.7.0", + "executable": "^4.1.0" + }, + "dependencies": { + "cross-spawn": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz", + "integrity": "sha512-pTgQJ5KC0d2hcY8eyL1IzlBPYjTkyH72XRZPnLyKus2mBfNjQs3klqbJU2VILqZryAZUt9JOb3h/mWMy23/f5A==", + "dev": true, + "requires": { + "lru-cache": "^4.0.1", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + } + }, + "execa": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-0.7.0.tgz", + "integrity": "sha512-RztN09XglpYI7aBBrJCPW95jEH7YF1UEPOoX9yDhUTPdp7mK+CQvnLTuD10BNXZ3byLTu2uehZ8EcKT/4CGiFw==", + "dev": true, + "requires": { + "cross-spawn": "^5.0.1", + "get-stream": "^3.0.0", + "is-stream": "^1.1.0", + "npm-run-path": "^2.0.0", + "p-finally": "^1.0.0", + "signal-exit": "^3.0.0", + "strip-eof": "^1.0.0" + } + }, + "get-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", + "integrity": "sha512-GlhdIUuVakc8SJ6kK0zAFbiGzRFzNnY4jUuEbV9UROo4Y+0Ny4fjvcZFVTeDA4odpFyOQzaw6hXukJSq/f28sQ==", + "dev": true + }, + "is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==", + "dev": true + }, + "lru-cache": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", + "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==", + "dev": true, + "requires": { + "pseudomap": "^1.0.2", + "yallist": "^2.1.2" + } + }, + "npm-run-path": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", + "integrity": "sha512-lJxZYlT4DW/bRUtFh1MQIWqmLwQfAxnqWG4HhEdjMlkrJYnJn0Jrr2u3mgxqaWsdiBc76TYkTG/mhrnYTuzfHw==", + "dev": true, + "requires": { + "path-key": "^2.0.0" + } + }, + "path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", + "dev": true + }, + "shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==", + "dev": true, + "requires": { + "shebang-regex": "^1.0.0" + } + }, + "shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==", + "dev": true + }, + "which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + }, + "yallist": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", + "integrity": "sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A==", + "dev": true + } + } + }, + "bin-version": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/bin-version/-/bin-version-6.0.0.tgz", + "integrity": "sha512-nk5wEsP4RiKjG+vF+uG8lFsEn4d7Y6FVDamzzftSunXOoOcOOkzcWdKVlGgFFwlUQCj63SgnUkLLGF8v7lufhw==", + "dev": true, + "requires": { + "execa": "^5.0.0", + "find-versions": "^5.0.0" + } + }, + "bin-version-check": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/bin-version-check/-/bin-version-check-5.1.0.tgz", + "integrity": "sha512-bYsvMqJ8yNGILLz1KP9zKLzQ6YpljV3ln1gqhuLkUtyfGi3qXKGuK2p+U4NAvjVFzDFiBBtOpCOSFNuYYEGZ5g==", + "dev": true, + "requires": { + "bin-version": "^6.0.0", + "semver": "^7.5.3", + "semver-truncate": "^3.0.0" + } + }, "binary-extensions": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", @@ -36185,6 +37362,38 @@ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==" }, + "cacheable-lookup": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", + "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==", + "dev": true + }, + "cacheable-request": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.4.tgz", + "integrity": "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==", + "dev": true, + "requires": { + "clone-response": "^1.0.2", + "get-stream": "^5.1.0", + "http-cache-semantics": "^4.0.0", + "keyv": "^4.0.0", + "lowercase-keys": "^2.0.0", + "normalize-url": "^6.0.1", + "responselike": "^2.0.0" + }, + "dependencies": { + "get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dev": true, + "requires": { + "pump": "^3.0.0" + } + } + } + }, "cachedir": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/cachedir/-/cachedir-2.3.0.tgz", @@ -36438,6 +37647,23 @@ } } }, + "clone-response": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz", + "integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==", + "dev": true, + "requires": { + "mimic-response": "^1.0.0" + }, + "dependencies": { + "mimic-response": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", + "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", + "dev": true + } + } + }, "clsx": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.0.0.tgz", @@ -37834,6 +39060,12 @@ "clone": "^1.0.2" } }, + "defer-to-connect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", + "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", + "dev": true + }, "define-lazy-prop": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", @@ -38779,6 +40011,13 @@ } } }, + "eslint-plugin-playwright": { + "version": "0.15.3", + "resolved": "https://registry.npmjs.org/eslint-plugin-playwright/-/eslint-plugin-playwright-0.15.3.tgz", + "integrity": "sha512-LQMW5y0DLK5Fnpya7JR1oAYL2/7Y9wDiYw6VZqlKqcRGSgjbVKNqxraphk7ra1U3Bb5EK444xMgUlQPbMg2M1g==", + "dev": true, + "requires": {} + }, "eslint-plugin-react": { "version": "7.32.2", "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.32.2.tgz", @@ -39087,6 +40326,25 @@ } } }, + "ext-list": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/ext-list/-/ext-list-2.2.2.tgz", + "integrity": "sha512-u+SQgsubraE6zItfVA0tBuCBhfU9ogSRnsvygI7wht9TS510oLkBRXBsqopeUG/GBOIQyKZO9wjTqIu/sf5zFA==", + "dev": true, + "requires": { + "mime-db": "^1.28.0" + } + }, + "ext-name": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ext-name/-/ext-name-5.0.0.tgz", + "integrity": "sha512-yblEwXAbGv1VQDmow7s38W77hzAgJAO50ztBLMcUyUBfxv1HC+LGwtiEN+Co6LtlqT/5uwVOxsD4TNIilWhwdQ==", + "dev": true, + "requires": { + "ext-list": "^2.0.0", + "sort-keys-length": "^1.0.0" + } + }, "extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", @@ -39259,6 +40517,17 @@ "tslib": "^2.4.0" } }, + "file-type": { + "version": "17.1.6", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-17.1.6.tgz", + "integrity": "sha512-hlDw5Ev+9e883s0pwUsuuYNu4tD7GgpUnOvykjv1Gya0ZIjuKumthDRua90VUn6/nlRKAjcxLUnHNTIUWwWIiw==", + "dev": true, + "requires": { + "readable-web-to-node-stream": "^3.0.2", + "strtok3": "^7.0.0-alpha.9", + "token-types": "^5.0.0-alpha.2" + } + }, "filelist": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", @@ -39285,6 +40554,23 @@ } } }, + "filename-reserved-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/filename-reserved-regex/-/filename-reserved-regex-3.0.0.tgz", + "integrity": "sha512-hn4cQfU6GOT/7cFHXBqeBg2TbrMBgdD0kcjLhvSQYYwm3s4B6cjvBfb7nBALJLAXqmU5xajSa7X2NnUud/VCdw==", + "dev": true + }, + "filenamify": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/filenamify/-/filenamify-5.1.1.tgz", + "integrity": "sha512-M45CbrJLGACfrPOkrTp3j2EcO9OBkKUYME0eiqOCa7i2poaklU0jhlIaMlr8ijLorT0uLAzrn3qXOp5684CkfA==", + "dev": true, + "requires": { + "filename-reserved-regex": "^3.0.0", + "strip-outer": "^2.0.0", + "trim-repeated": "^2.0.0" + } + }, "fill-range": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", @@ -39401,6 +40687,15 @@ "path-exists": "^4.0.0" } }, + "find-versions": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/find-versions/-/find-versions-5.1.0.tgz", + "integrity": "sha512-+iwzCJ7C5v5KgcBuueqVoNiHVoQpwiUK5XFLjf0affFTep+Wcw93tPvmb8tqujDNmzhBDPddnWV/qgWSXgq+Hg==", + "dev": true, + "requires": { + "semver-regex": "^4.0.5" + } + }, "find-yarn-workspace-root2": { "version": "1.2.16", "resolved": "https://registry.npmjs.org/find-yarn-workspace-root2/-/find-yarn-workspace-root2-1.2.16.tgz", @@ -39866,6 +41161,25 @@ "get-intrinsic": "^1.1.3" } }, + "got": { + "version": "11.8.6", + "resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz", + "integrity": "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==", + "dev": true, + "requires": { + "@sindresorhus/is": "^4.0.0", + "@szmarczak/http-timer": "^4.0.5", + "@types/cacheable-request": "^6.0.1", + "@types/responselike": "^1.0.0", + "cacheable-lookup": "^5.0.3", + "cacheable-request": "^7.0.2", + "decompress-response": "^6.0.0", + "http2-wrapper": "^1.0.0-beta.5.2", + "lowercase-keys": "^2.0.0", + "p-cancelable": "^2.0.0", + "responselike": "^2.0.0" + } + }, "graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", @@ -40140,6 +41454,12 @@ "resolved": "https://registry.npmjs.org/html-to-image/-/html-to-image-1.11.11.tgz", "integrity": "sha512-9gux8QhvjRO/erSnDPv28noDZcPZmYE7e1vFsBLKLlRlKDSqNJYebj6Qz1TGd5lsRV+X+xYyjCKjuZdABinWjA==" }, + "http-cache-semantics": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", + "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==", + "dev": true + }, "http-deceiver": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz", @@ -40230,6 +41550,24 @@ "sshpk": "^1.14.1" } }, + "http2-wrapper": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz", + "integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==", + "dev": true, + "requires": { + "quick-lru": "^5.1.1", + "resolve-alpn": "^1.0.0" + }, + "dependencies": { + "quick-lru": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", + "dev": true + } + } + }, "https-proxy-agent": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", @@ -41687,6 +43025,12 @@ "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", "dev": true }, + "json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true + }, "json-parse-even-better-errors": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", @@ -41790,6 +43134,15 @@ "lodash-es": "4" } }, + "keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "requires": { + "json-buffer": "3.0.1" + } + }, "kind-of": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", @@ -42134,6 +43487,12 @@ "tslib": "^2.0.3" } }, + "lowercase-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", + "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", + "dev": true + }, "lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", @@ -43693,6 +45052,15 @@ "wcwidth": "^1.0.1" } }, + "os-filter-obj": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/os-filter-obj/-/os-filter-obj-2.0.0.tgz", + "integrity": "sha512-uksVLsqG3pVdzzPvmAHpBK0wKxYItuzZr7SziusRPoz67tGV8rL1szZ6IdeUrbqLjGDwApBtN29eEE3IqGHOjg==", + "dev": true, + "requires": { + "arch": "^2.1.0" + } + }, "os-tmpdir": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", @@ -43715,6 +45083,12 @@ "integrity": "sha512-Ou3dJ6bA/UJ5GVHxah4LnqDwZRwAmWxrG3wtrHrbGnP4RnLCtA64A4F+ae7Y8ww660JaddSoArUR5HjipWSHAQ==", "dev": true }, + "p-cancelable": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz", + "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==", + "dev": true + }, "p-filter": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/p-filter/-/p-filter-2.1.0.tgz", @@ -43877,6 +45251,12 @@ "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==" }, + "peek-readable": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/peek-readable/-/peek-readable-5.0.0.tgz", + "integrity": "sha512-YtCKvLUOvwtMGmrniQPdO7MwPjgkFBtFIrmfSbYmYuq3tKDV/mcfAhBth1+C3ru7uXIZasc/pHnb+YDYNkkj4A==", + "dev": true + }, "pend": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", @@ -43976,6 +45356,28 @@ } } }, + "playwright": { + "version": "1.42.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.42.1.tgz", + "integrity": "sha512-PgwB03s2DZBcNRoW+1w9E+VkLBxweib6KTXM0M3tkiT4jVxKSi6PmVJ591J+0u10LUrgxB7dLRbiJqO5s2QPMg==", + "requires": { + "fsevents": "2.3.2", + "playwright-core": "1.42.1" + }, + "dependencies": { + "fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "optional": true + } + } + }, + "playwright-core": { + "version": "1.42.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.42.1.tgz", + "integrity": "sha512-mxz6zclokgrke9p1vtdy/COWBH+eOZgYUVVU34C73M+4j4HLlQJHtfcqiqqxpP0o8HhMkflvfbquLX5dg6wlfA==" + }, "polished": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/polished/-/polished-4.2.2.tgz", @@ -44730,6 +46132,15 @@ "util-deprecate": "^1.0.1" } }, + "readable-web-to-node-stream": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/readable-web-to-node-stream/-/readable-web-to-node-stream-3.0.2.tgz", + "integrity": "sha512-ePeK6cc1EcKLEhJFt/AebMCLL+GgSKhuygrZ/GLaKZYEecIgIECf4UaUuaByiGtzckwR4ain9VzUh95T1exYGw==", + "dev": true, + "requires": { + "readable-stream": "^3.6.0" + } + }, "readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", @@ -45034,6 +46445,12 @@ "supports-preserve-symlinks-flag": "^1.0.0" } }, + "resolve-alpn": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", + "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==", + "dev": true + }, "resolve-cwd": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", @@ -45080,6 +46497,15 @@ "integrity": "sha512-J1l+Zxxp4XK3LUDZ9m60LRJF/mAe4z6a4xyabPHk7pvK5t35dACV32iIjJDFeWZFfZlO29w6SZ67knR0tHzJtQ==", "dev": true }, + "responselike": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz", + "integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==", + "dev": true, + "requires": { + "lowercase-keys": "^2.0.0" + } + }, "restore-cursor": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", @@ -45920,6 +47346,21 @@ "lru-cache": "^6.0.0" } }, + "semver-regex": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/semver-regex/-/semver-regex-4.0.5.tgz", + "integrity": "sha512-hunMQrEy1T6Jr2uEVjrAIqjwWcQTgOAcIM52C8MY1EZSD3DDNft04XzvYKPqjED65bNVVko0YI38nYeEHCX3yw==", + "dev": true + }, + "semver-truncate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/semver-truncate/-/semver-truncate-3.0.0.tgz", + "integrity": "sha512-LJWA9kSvMolR51oDE6PN3kALBNaUdkxzAGcexw8gjMA8xr5zUqK0JiR3CgARSqanYF3Z1YHvsErb1KDgh+v7Rg==", + "dev": true, + "requires": { + "semver": "^7.3.5" + } + }, "send": { "version": "0.18.0", "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", @@ -46307,6 +47748,32 @@ } } }, + "sort-keys": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-1.1.2.tgz", + "integrity": "sha512-vzn8aSqKgytVik0iwdBEi+zevbTYZogewTUM6dtpmGwEcdzbub/TX4bCzRhebDCRC3QzXgJsLRKB2V/Oof7HXg==", + "dev": true, + "requires": { + "is-plain-obj": "^1.0.0" + }, + "dependencies": { + "is-plain-obj": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", + "integrity": "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==", + "dev": true + } + } + }, + "sort-keys-length": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/sort-keys-length/-/sort-keys-length-1.0.1.tgz", + "integrity": "sha512-GRbEOUqCxemTAk/b32F2xa8wDTs+Z1QHOkbhJDQTvv/6G3ZkbJ+frYWsTcc7cBB3Fu4wy4XlLCuNtJuMn7Gsvw==", + "dev": true, + "requires": { + "sort-keys": "^1.0.0" + } + }, "source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -46686,6 +48153,12 @@ "resolved": "https://registry.npmjs.org/strip-bom-string/-/strip-bom-string-1.0.0.tgz", "integrity": "sha1-5SEekiQ2n7uB1jOi8ABE3IztrZI=" }, + "strip-eof": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", + "integrity": "sha512-7FCwGGmx8mD5xQd3RPUvnSpUXHM3BWuzjtpD4TXsfcZ9EL4azvVVUscFYwD9nx8Kh+uCBC00XBtAykoMHwTh8Q==", + "dev": true + }, "strip-final-newline": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", @@ -46706,6 +48179,12 @@ "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", "dev": true }, + "strip-outer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-outer/-/strip-outer-2.0.0.tgz", + "integrity": "sha512-A21Xsm1XzUkK0qK1ZrytDUvqsQWict2Cykhvi0fBQntGG5JSprESasEyV1EZ/4CiR5WB5KjzLTrP/bO37B0wPg==", + "dev": true + }, "strong-log-transformer": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/strong-log-transformer/-/strong-log-transformer-2.1.0.tgz", @@ -46716,6 +48195,16 @@ "through": "^2.3.4" } }, + "strtok3": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-7.0.0.tgz", + "integrity": "sha512-pQ+V+nYQdC5H3Q7qBZAz/MO6lwGhoC2gOAjuouGf/VO0m7vQRh8QNMl2Uf6SwAtzZ9bOw3UIeBukEGNJl5dtXQ==", + "dev": true, + "requires": { + "@tokenizer/token": "^0.3.0", + "peek-readable": "^5.0.0" + } + }, "style-inject": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/style-inject/-/style-inject-0.3.0.tgz", @@ -47257,6 +48746,16 @@ "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==" }, + "token-types": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/token-types/-/token-types-5.0.1.tgz", + "integrity": "sha512-Y2fmSnZjQdDb9W4w4r1tswlMHylzWIeOKpx0aZH9BgGtACHhrk3OkT52AzwcuqTRBZtvvnTjDBh8eynMulu8Vg==", + "dev": true, + "requires": { + "@tokenizer/token": "^0.3.0", + "ieee754": "^1.2.1" + } + }, "toposort": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/toposort/-/toposort-2.0.2.tgz", @@ -47290,6 +48789,23 @@ "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-3.0.1.tgz", "integrity": "sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==" }, + "trim-repeated": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/trim-repeated/-/trim-repeated-2.0.0.tgz", + "integrity": "sha512-QUHBFTJGdOwmp0tbOG505xAgOp/YliZP/6UgafFXYZ26WT1bvQmSMJUvkeVSASuJJHbqsFbynTvkd5W8RBTipg==", + "dev": true, + "requires": { + "escape-string-regexp": "^5.0.0" + }, + "dependencies": { + "escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "dev": true + } + } + }, "trough": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/trough/-/trough-2.1.0.tgz", diff --git a/package.json b/package.json index 2bf8a54b8..6ed1b990c 100644 --- a/package.json +++ b/package.json @@ -96,6 +96,7 @@ "next-themes": "^0.2.1", "node-cron": "^3.0.2", "node-fetch": "^3.3.2", + "playwright": "^1.42.1", "react": "18.2.0", "react-dom": "18.2.0", "react-dropzone": "^14.2.3", @@ -129,19 +130,25 @@ "@commitlint/cli": "^16.2.4", "@commitlint/config-angular": "^16.2.4", "@commitlint/config-conventional": "^16.2.4", + "@nx/devkit": "18.0.3", "@nx/eslint": "18.0.3", "@nx/eslint-plugin": "18.0.3", "@nx/jest": "18.0.3", "@nx/js": "18.0.3", "@nx/next": "18.0.3", + "@nx/playwright": "18.0.3", "@nx/react": "18.0.3", "@nx/rollup": "18.0.3", "@nx/webpack": "18.0.3", "@nx/workspace": "18.0.3", + "@playwright/test": "^1.36.0", "@pmmmwh/react-refresh-webpack-plugin": "^0.5.7", "@rollup/plugin-wasm": "^6.2.2", "@svgr/webpack": "8.1.0", + "@swc-node/register": "~1.6.7", + "@swc/cli": "~0.1.62", "@swc/core": "1.3.102", + "@swc/helpers": "~0.5.2", "@swc/jest": "0.2.20", "@testing-library/react": "^14.0.0", "@testing-library/user-event": "^14.5.2", @@ -167,6 +174,7 @@ "eslint-plugin-cypress": "2.14.0", "eslint-plugin-import": "2.27.5", "eslint-plugin-jsx-a11y": "6.7.1", + "eslint-plugin-playwright": "^0.15.3", "eslint-plugin-react": "7.32.2", "eslint-plugin-react-hooks": "4.6.0", "husky": "^7.0.4",