diff --git a/features/ics/apply-form/apply-form.tsx b/features/ics/apply-form/apply-form.tsx index a42354446..9fab1acd0 100644 --- a/features/ics/apply-form/apply-form.tsx +++ b/features/ics/apply-form/apply-form.tsx @@ -12,7 +12,7 @@ import { export const ApplyForm: FC = memo(() => ( - +
diff --git a/features/ics/apply-form/controls/additional-addresses.tsx b/features/ics/apply-form/controls/additional-addresses.tsx index f604d3293..773bd5e42 100644 --- a/features/ics/apply-form/controls/additional-addresses.tsx +++ b/features/ics/apply-form/controls/additional-addresses.tsx @@ -27,7 +27,7 @@ export const AdditionalAddresses: FC = () => { ); return ( - + Optional}>Additional Addresses @@ -59,6 +59,7 @@ export const AdditionalAddresses: FC = () => { size="sm" onClick={handleAddAddress} fullwidth + data-testid="addNewAddressBtn" > Add new address diff --git a/features/ics/apply-form/controls/address-item.tsx b/features/ics/apply-form/controls/address-item.tsx index 8bdc63cbb..21d36316b 100644 --- a/features/ics/apply-form/controls/address-item.tsx +++ b/features/ics/apply-form/controls/address-item.tsx @@ -108,7 +108,12 @@ export const AddressItem: FC = ({ ); return ( - + Additional address #{index + 1} @@ -118,6 +123,7 @@ export const AddressItem: FC = ({ variant="text" color="error" onClick={() => onRemove(index)} + data-testid="removeBtn" > Remove @@ -137,7 +143,11 @@ export const AddressItem: FC = ({ ) : ( - + Step 1. Insert your Ethereum address = ({ placeholder="0x..." /> - + Step 2. Copy the message and sign it on Etherscan (or other tool) = ({ } > } size="xs" variant="translucent" @@ -175,7 +191,11 @@ export const AddressItem: FC = ({ } /> - + Step 3. Paste the signature in the field below @@ -189,6 +209,7 @@ export const AddressItem: FC = ({ variant="translucent" onClick={() => void onVerify(index)} disabled={isVerifying} + data-testid="verifySignatureBtn" > {isVerifying ? 'Verifying...' : 'Verify'} diff --git a/features/ics/apply-form/controls/main-address.tsx b/features/ics/apply-form/controls/main-address.tsx index 567080665..be9ad0252 100644 --- a/features/ics/apply-form/controls/main-address.tsx +++ b/features/ics/apply-form/controls/main-address.tsx @@ -8,7 +8,7 @@ export const MainAddress: FC = () => { const { mainAddress } = useApplyFormData(true); return ( - + Main address @@ -18,6 +18,7 @@ export const MainAddress: FC = () => { Main address Verified diff --git a/features/ics/apply-form/controls/social-proof.tsx b/features/ics/apply-form/controls/social-proof.tsx index 07a53486e..f15348ba1 100644 --- a/features/ics/apply-form/controls/social-proof.tsx +++ b/features/ics/apply-form/controls/social-proof.tsx @@ -17,7 +17,7 @@ export const SocialProof: FC = () => { const { twitterMessage, discordMessage } = useSocialMessages(); return ( - + Optional}>Socials @@ -33,13 +33,13 @@ export const SocialProof: FC = () => { {/* Twitter Section */} - + X (formerly Twitter) - + Step 1. Prove the ownership of the X account by posting a tweet with the following text @@ -56,7 +56,7 @@ export const SocialProof: FC = () => { /> - + Step 2. Paste the link to this post { {/* Discord Section */} - + Discord - + Step 1. Prove the ownership of the Discord account by posting the following message to{' '} @@ -102,7 +102,7 @@ export const SocialProof: FC = () => { /> - + Step 2. Paste the link to this message ( - Submit application + + Submit application + ); diff --git a/features/ics/form-status/components/application.tsx b/features/ics/form-status/components/application.tsx index 16e606b48..afd6b04ea 100644 --- a/features/ics/form-status/components/application.tsx +++ b/features/ics/form-status/components/application.tsx @@ -17,12 +17,13 @@ export const Application: FC = ({ createdAt, }) => ( + Your application - + Submitted {formatDate(parseISO(createdAt), 'dd.MM.yyyy')} @@ -40,6 +41,7 @@ export const Application: FC = ({ label="Main address" value={form.mainAddress} error={!!comments.mainAddress} + name="mainAddress" /> {comments.mainAddress} diff --git a/features/ics/form-status/components/score-chip/score-chip.tsx b/features/ics/form-status/components/score-chip/score-chip.tsx index 05877fa7c..b2eae64dc 100644 --- a/features/ics/form-status/components/score-chip/score-chip.tsx +++ b/features/ics/form-status/components/score-chip/score-chip.tsx @@ -18,7 +18,7 @@ export const ScoreChip: FC> = ({ children, type = 'default', }) => ( - + {ICONS[type]} {children} diff --git a/features/ics/form-status/form-status.tsx b/features/ics/form-status/form-status.tsx index 5c22437c1..24b22188c 100644 --- a/features/ics/form-status/form-status.tsx +++ b/features/ics/form-status/form-status.tsx @@ -31,7 +31,7 @@ export const FormStatus: FC = ({ const haveScores = Object.values(scores).some((score) => score !== null); return ( - + { const { signIn } = useSiweAuth(); return ( - + @@ -29,6 +29,7 @@ export const SiweSignIn: FC = () => { label="Main address" value={address} fullwidth + name="mainAddress" /> diff --git a/package.json b/package.json index 574852f24..126223e84 100644 --- a/package.json +++ b/package.json @@ -76,8 +76,9 @@ "@nestjs/common": "^10.4.16", "@next/bundle-analyzer": "^13.2.4", "@next/eslint-plugin-next": "^13.4.13", - "@playwright/test": "1.56.1", + "@playwright/test": "1.57.0", "@svgr/webpack": "8.1.0", + "@scure/bip39": "2.0.1", "@types/jest": "28.1.6", "@types/lodash": "^4.14.186", "@types/memory-cache": "0.2.2", @@ -104,6 +105,7 @@ "eslint-plugin-react-hooks": "^4.6.2", "eslint-plugin-sonarjs": "^0.20.0", "eslint-plugin-unicorn": "^48.0.1", + "ethers": "5.8.0", "husky": "^7.0.1", "jest": "^29.5.0", "jsonschema": "^1.4.1", diff --git a/shared/components/copy-button/copy-button.tsx b/shared/components/copy-button/copy-button.tsx index 8508604b1..6ba56cc91 100644 --- a/shared/components/copy-button/copy-button.tsx +++ b/shared/components/copy-button/copy-button.tsx @@ -12,6 +12,7 @@ export const CopyButton: FC = ({ return ( > = ({ children, chip, extra }) => ( - + {children} diff --git a/shared/components/input-address/verified-chip.tsx b/shared/components/input-address/verified-chip.tsx index a49852529..34fca6871 100644 --- a/shared/components/input-address/verified-chip.tsx +++ b/shared/components/input-address/verified-chip.tsx @@ -5,7 +5,7 @@ export const VerifiedChip: FC> = ({ children, color, }) => ( - + {children} diff --git a/tests/config/configs/base.config.ts b/tests/config/configs/base.config.ts index a36abe720..8908e6d1a 100644 --- a/tests/config/configs/base.config.ts +++ b/tests/config/configs/base.config.ts @@ -15,6 +15,11 @@ export type StandConfig = { nodeConfig: EthereumNodeServiceOptions & { host: string; }; + mockConfig?: { + urls: { + csmSurveysApi: string; + }; + }; }; export type IConfig = { diff --git a/tests/config/configs/testnet.config.ts b/tests/config/configs/testnet.config.ts index 062e77a4f..70ed7c596 100644 --- a/tests/config/configs/testnet.config.ts +++ b/tests/config/configs/testnet.config.ts @@ -20,6 +20,11 @@ export class TestnetConfig extends BaseConfig { host: '127.0.0.1', port: 8545, }, + mockConfig: { + urls: { + csmSurveysApi: 'https://csm-surveys-api-testnet.up.railway.app', + }, + }, }; } } diff --git a/tests/pages/base.page.ts b/tests/pages/base.page.ts index 51b433455..9d40604c7 100644 --- a/tests/pages/base.page.ts +++ b/tests/pages/base.page.ts @@ -55,12 +55,32 @@ export class BasePage { } async getStorageData(name: string | string[]) { - return await this.page.evaluate((names) => { - if (Array.isArray(names)) { - return names.map((name) => localStorage.getItem(name)); - } else { - return localStorage.getItem(names); - } + return test.step(`Get data from Local Storage by key '${name}'`, async () => { + return this.page.evaluate((names) => { + if (Array.isArray(names)) { + return names.map((name) => localStorage.getItem(name)); + } else { + return localStorage.getItem(names); + } + }, name); + }); + } + + async getSessionStorageData(name: string | string[]) { + return test.step(`Get data from Session Storage by key '${name}'`, async () => { + return this.page.evaluate((names) => { + if (Array.isArray(names)) { + return names.map((name) => sessionStorage.getItem(name)); + } else { + return sessionStorage.getItem(names); + } + }, name); + }); + } + + async removeKeyFromSessionStorage(name: string) { + await this.page.evaluate((key) => { + sessionStorage.removeItem(key); }, name); } diff --git a/tests/pages/operatorType.page.ts b/tests/pages/operatorType.page.ts new file mode 100644 index 000000000..f63330423 --- /dev/null +++ b/tests/pages/operatorType.page.ts @@ -0,0 +1,22 @@ +import { Page } from '@playwright/test'; +import { BasePage } from './base.page'; +import { + WalletPage, + WalletConnectType, +} from '@lidofinance/wallets-testing-wallets'; +import { TxModal } from './elements/common/element.txProgressModal'; +import { ApplicationForm } from './tabs/operatorType/applicationForm.page'; + +export class OperatorTypePage extends BasePage { + applicationForm: ApplicationForm; + txModal: TxModal; + + constructor( + page: Page, + public walletPage: WalletPage, + ) { + super(page); + this.applicationForm = new ApplicationForm(page, walletPage); + this.txModal = new TxModal(page); + } +} diff --git a/tests/pages/tabs/operatorType/applicationForm.page.ts b/tests/pages/tabs/operatorType/applicationForm.page.ts new file mode 100644 index 000000000..a5d4ce0cf --- /dev/null +++ b/tests/pages/tabs/operatorType/applicationForm.page.ts @@ -0,0 +1,31 @@ +import { Page, test } from '@playwright/test'; +import { + WalletConnectType, + WalletPage, +} from '@lidofinance/wallets-testing-wallets'; +import { SignInForm } from './applicationFormStates/signInForm.page'; +import { SubmitApplicationForm } from './applicationFormStates/submitApplicationForm.page'; +import { ApplicationFormStatus } from './applicationFormStates/applicationFormStatus.page'; + +export class ApplicationForm { + page: Page; + signInForm: SignInForm; + submitApplicationForm: SubmitApplicationForm; + applicationFormStatus: ApplicationFormStatus; + + constructor( + page: Page, + public walletPage: WalletPage, + ) { + this.page = page; + this.signInForm = new SignInForm(page, walletPage); + this.submitApplicationForm = new SubmitApplicationForm(page, walletPage); + this.applicationFormStatus = new ApplicationFormStatus(page); + } + + async open() { + await test.step('Open Application Form tab for Operator Type page', async () => { + await this.page.goto('/type/ics-apply'); + }); + } +} diff --git a/tests/pages/tabs/operatorType/applicationFormStates/additionalAddress.page.ts b/tests/pages/tabs/operatorType/applicationFormStates/additionalAddress.page.ts new file mode 100644 index 000000000..8f84f6e5c --- /dev/null +++ b/tests/pages/tabs/operatorType/applicationFormStates/additionalAddress.page.ts @@ -0,0 +1,59 @@ +import { Locator, Page } from '@playwright/test'; + +export class AdditionalAddressPage { + // Common + addressInfo: Locator; + + // Step 1 + addressField: Locator; + + // Step 2 + signMessageField: Locator; + signMessageInput: Locator; + copySignMessageBtn: Locator; + signMessageBtn: Locator; + + // Step 3 + signatureField: Locator; + signatureInput: Locator; + verifySignatureBtn: Locator; + + // Verified + verifyChip: Locator; + verifiedAddressInput: Locator; + + constructor( + public page: Page, + public addressIndex: number, + public section: Locator, + ) { + // Common + this.addressInfo = section.getByTestId( + `additionalAddressInfo-${addressIndex}`, + ); + + // Step 1 + this.addressField = this.section.getByTestId('additionalAddressStep1'); + + // Step 2 + this.signMessageField = this.section.getByTestId('additionalAddressStep2'); + this.signMessageInput = this.signMessageField.locator( + `xpath=//input[@name="additionalAddresses.${this.addressIndex}.messageToSign"]`, + ); + this.copySignMessageBtn = this.signMessageField.getByTestId('copyBtn'); + this.signMessageBtn = this.signMessageField.getByTestId('signBtn'); + + // Step 3 + this.signatureField = this.section.getByTestId('additionalAddressStep3'); + this.signatureInput = this.section.locator( + `xpath=(//input[@name="additionalAddresses.${this.addressIndex}.signature"])`, + ); + this.verifySignatureBtn = this.section.getByTestId('verifySignatureBtn'); + + // Verified + this.verifyChip = this.addressInfo.getByTestId('verifiedChip'); + this.verifiedAddressInput = this.addressInfo.locator( + `xpath=(//input[@name="additionalAddresses.${addressIndex}.address"])`, + ); + } +} diff --git a/tests/pages/tabs/operatorType/applicationFormStates/applicationFormStatus.page.ts b/tests/pages/tabs/operatorType/applicationFormStates/applicationFormStatus.page.ts new file mode 100644 index 000000000..c32169c5b --- /dev/null +++ b/tests/pages/tabs/operatorType/applicationFormStates/applicationFormStatus.page.ts @@ -0,0 +1,13 @@ +import { Locator, Page } from '@playwright/test'; + +export class ApplicationFormStatus { + page: Page; + form: Locator; + scoreChip: Locator; + + constructor(page: Page) { + this.page = page; + this.form = page.getByTestId('applicationFormStatus'); + this.scoreChip = this.form.getByTestId('scoreChip'); + } +} diff --git a/tests/pages/tabs/operatorType/applicationFormStates/signInForm.page.ts b/tests/pages/tabs/operatorType/applicationFormStates/signInForm.page.ts new file mode 100644 index 000000000..e53340bb1 --- /dev/null +++ b/tests/pages/tabs/operatorType/applicationFormStates/signInForm.page.ts @@ -0,0 +1,49 @@ +import { Locator, Page, test } from '@playwright/test'; +import { + WalletConnectType, + WalletPage, +} from '@lidofinance/wallets-testing-wallets'; +import { + STAGE_WAIT_TIMEOUT, + WALLET_PAGE_TIMEOUT_WAITER, +} from 'tests/consts/timeouts'; +import { BasePage } from 'tests/pages/base.page'; + +export class SignInForm extends BasePage { + form: Locator; + mainAddressInput: Locator; + mainAddressLabel: Locator; + + constructor( + page: Page, + public walletPage: WalletPage, + ) { + super(page); + this.form = page.getByTestId('signInForm'); + this.mainAddressInput = this.form.locator('input[name="mainAddress"]'); + this.mainAddressLabel = this.form.locator( + 'xpath=//input[@name="mainAddress"]/ancestor::label', + ); + } + + async signIn() { + let txPage; + await test.step('Sign in a message to prove ownership', async () => { + const signInButton = this.form.getByRole('button', { name: 'Sign in' }); + + [txPage] = await Promise.all([ + this.waitForPage(WALLET_PAGE_TIMEOUT_WAITER), + signInButton.click(), + ]); + await this.page.waitForSelector(`text=Please sign the message`, { + timeout: STAGE_WAIT_TIMEOUT, + }); + + await this.walletPage.confirmTx(txPage); + + await this.page.waitForSelector(`text=Submit application`, { + timeout: STAGE_WAIT_TIMEOUT, + }); + }); + } +} diff --git a/tests/pages/tabs/operatorType/applicationFormStates/submitApplicationForm.page.ts b/tests/pages/tabs/operatorType/applicationFormStates/submitApplicationForm.page.ts new file mode 100644 index 000000000..3f28c8081 --- /dev/null +++ b/tests/pages/tabs/operatorType/applicationFormStates/submitApplicationForm.page.ts @@ -0,0 +1,156 @@ +import { Locator, Page, test, expect } from '@playwright/test'; +import { + WalletConnectType, + WalletPage, +} from '@lidofinance/wallets-testing-wallets'; +import { BasePage } from 'tests/pages/base.page'; +import { AdditionalAddressPage } from './additionalAddress.page'; +import { Wallet, utils } from 'ethers'; +import { HDAccount } from 'viem/accounts'; + +export class SubmitApplicationForm extends BasePage { + form: Locator; + + mainAddressSection: Locator; + mainAddressTitle: Locator; + mainAddressInput: Locator; + mainAddressLabel: Locator; + mainAddressVerifiedChip: Locator; + + additionalAddressesSection: Locator; + addNewAddressBtn: Locator; + + // Socials section + socialProofSection: Locator; + socialProofTitile: Locator; + + // Twitter section + twitterSection: Locator; + twitterProofStep1: Locator; + twitterProofStep1Input: Locator; + twitterProofStep1CopyBtn: Locator; + twitterProofStep2: Locator; + defaultTwitterMessageUrl = 'https://x.com/someuser/status/1234567890'; + + // Discord section + discordSection: Locator; + discordProofStep1: Locator; + discordProofStep1Input: Locator; + discordProofStep1CopyBtn: Locator; + discordProofStep2: Locator; + defaultDiscordMessageUrl = 'https://discord.com/channels/123/456/789'; + + // btn + submitBtn: Locator; + + constructor( + page: Page, + public walletPage: WalletPage, + ) { + super(page); + this.form = page.getByTestId('applyForm'); + + this.mainAddressSection = this.form.getByTestId('mainAddressSection'); + this.mainAddressTitle = this.mainAddressSection.getByTestId('formTitle'); + this.mainAddressInput = this.mainAddressSection.locator( + 'input[name="mainAddress"]', + ); + this.mainAddressLabel = this.mainAddressSection.locator( + 'xpath=//input[@name="mainAddress"]/ancestor::label', + ); + this.mainAddressVerifiedChip = + this.mainAddressSection.getByTestId('verifiedChip'); + + this.additionalAddressesSection = this.form.getByTestId( + 'additionalAddressesSection', + ); + this.addNewAddressBtn = + this.additionalAddressesSection.getByTestId('addNewAddressBtn'); + + // socialsSection + this.socialProofSection = this.form.getByTestId('socialProofSection'); + this.socialProofTitile = this.socialProofSection.getByTestId('formTitle'); + + // twitterSection + this.twitterSection = this.socialProofSection.getByTestId('twitterSection'); + this.twitterProofStep1 = + this.twitterSection.getByTestId('twitterProofStep1'); + this.twitterProofStep1Input = + this.twitterProofStep1.locator('#twitter-message'); + this.twitterProofStep1CopyBtn = + this.twitterProofStep1.getByTestId('copyBtn'); + this.twitterProofStep2 = + this.twitterSection.getByTestId('twitterProofStep2'); + + // discordSection + this.discordSection = this.socialProofSection.getByTestId('discordSection'); + this.discordProofStep1 = + this.discordSection.getByTestId('discordProofStep1'); + this.discordProofStep1Input = + this.discordProofStep1.locator('#discord-message'); + this.discordProofStep1CopyBtn = + this.discordProofStep1.getByTestId('copyBtn'); + this.discordProofStep2 = + this.discordSection.getByTestId('discordProofStep2'); + + // btn + this.submitBtn = this.form.getByRole('button', { + name: 'Submit application', + }); + } + + getAdditionalAddressFieldByIndex(index: number) { + return new AdditionalAddressPage( + this.page, + index, + this.additionalAddressesSection, + ); + } + + async fillAdditionalAddress(index: number, address: string) { + const addressInput = this.additionalAddressesSection.locator( + `xpath=(//input[@name="additionalAddresses.${index}.address"])`, + ); + + await addressInput.fill(address); + } + + async addAdditionalAddress(account: HDAccount, mainAddress: `0x${string}`) { + await this.addNewAddressBtn.click(); + const lastAdditionalAddressIndex = this.additionalAddressesSection.locator( + '[data-testid*="additionalAddressInfo"]', + ); + const count = await lastAdditionalAddressIndex.count(); + const lastIndex = count - 1; + await this.fillAdditionalAddress(lastIndex, account.address); + + await test.step('Check verified state for address', async () => { + const signMessage = `Verify ownership of address ${account.address.toLowerCase()} for ICS with main address ${mainAddress.toLowerCase()}`; + const signature = await new Wallet( + // @ts-expect-error may be null + utils.hexlify(account.getHdKey().privateKey), + ).signMessage(signMessage); + + const addressField = this.getAdditionalAddressFieldByIndex(lastIndex); + + await addressField.signatureInput.fill(signature); + await expect(addressField.signMessageInput).toHaveValue(signMessage); + await addressField.verifySignatureBtn.click(); + await expect(addressField.verifyChip).toBeVisible(); + }); + } + + async addSocialsProof() { + await test.step('Fill Twitter proof', async () => { + await this.twitterProofStep2 + .locator('input') + .fill(this.defaultTwitterMessageUrl); + }); + + await test.step('Fill Discord proof', async () => { + await this.discordProofStep2 + .locator('input') + .fill(this.defaultDiscordMessageUrl); + }); + } +} diff --git a/tests/services/httpMocker.service.ts b/tests/services/httpMocker.service.ts new file mode 100644 index 000000000..3742c5aeb --- /dev/null +++ b/tests/services/httpMocker.service.ts @@ -0,0 +1,41 @@ +import { Page, test } from '@playwright/test'; + +type IConfig = { + urls: { + csmSurveysApi: string; + }; +}; + +export class HttpMockerService { + constructor( + private page: Page, + private config: IConfig, + ) {} + + async mockIcsApply(response: Record) { + const mockUrl = `${this.config.urls.csmSurveysApi}/ics/apply`; + await test.step('Mock ICS Apply request', async () => { + await this.page.route(mockUrl, async (route) => { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify(response), + }); + }); + }); + } + + async mockIcsStatus(response: Record) { + const mockUrl = `${this.config.urls.csmSurveysApi}/ics/status`; + + await test.step('Mock ICS Status request', async () => { + await this.page.route(mockUrl, async (route) => { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify(response), + }); + }); + }); + } +} diff --git a/tests/services/mockResponses/applyApplication.mock.ts b/tests/services/mockResponses/applyApplication.mock.ts new file mode 100644 index 000000000..9950aa174 --- /dev/null +++ b/tests/services/mockResponses/applyApplication.mock.ts @@ -0,0 +1,44 @@ +export const applicationStatus = { + REVIEW: 'REVIEW', + APPROVED: 'APPROVED', + REJECTED: 'REJECTED', +}; + +export const applyApplicationMockResponse = { + form: { + mainAddress: null, + twitterLink: null, + discordLink: null, + additionalAddresses: [], + }, + status: 'REVIEW', + issued: false, + comments: { + reason: null, + mainAddress: null, + twitterLink: null, + discordLink: null, + additionalAddresses: [], + }, + scores: { + ethStaker: null, + stakeCat: null, + obolTechne: null, + ssvVerified: null, + csmTestnet: null, + csmMainnet: null, + sdvtTestnet: null, + sdvtMainnet: null, + humanPassport: null, + circles: null, + discord: null, + twitter: null, + aragonVotes: null, + snapshotVotes: null, + lidoGalxe: null, + highSignal: null, + gitPoaps: null, + }, + createdAt: '2025-12-12T19:10:41.145Z', + updatedAt: '2025-12-12T19:10:41.145Z', +}; diff --git a/tests/services/widget.service.ts b/tests/services/widget.service.ts index e7f921b38..ff1067c69 100644 --- a/tests/services/widget.service.ts +++ b/tests/services/widget.service.ts @@ -15,6 +15,7 @@ import { } from 'tests/consts/timeouts'; import { BondRewardsPage } from 'tests/pages/bondRewards.page'; import { TOKENS } from '@lidofinance/lido-csm-sdk'; +import { OperatorTypePage } from 'tests/pages/operatorType.page'; export class WidgetService { public mainPage: MainPage; @@ -22,6 +23,7 @@ export class WidgetService { public dashboardPage: DashboardPage; public rolesPage: RolesPage; public bondRewardsPage: BondRewardsPage; + public operatorType: OperatorTypePage; constructor( public page: Page, @@ -32,11 +34,12 @@ export class WidgetService { this.dashboardPage = new DashboardPage(this.page); this.rolesPage = new RolesPage(this.page, this.walletPage); this.bondRewardsPage = new BondRewardsPage(this.page); + this.operatorType = new OperatorTypePage(this.page, this.walletPage); } async connectWallet(expectConnectionState = true) { await test.step('Open default page for connect.', async () => { - await this.page.goto('/?survey-setup=1&ics-appy=1&wallet-rpc=1'); + await this.page.goto('/?survey-setup=1&ics-apply=1&wallet-rpc=1'); }); await test.step('Connect wallet to widget', async () => { const element = new ElementController(this.page); diff --git a/tests/widget/operatorWithValidator/operatorType/applyApplication/addresses.spec.ts b/tests/widget/operatorWithValidator/operatorType/applyApplication/addresses.spec.ts new file mode 100644 index 000000000..b96cc0efa --- /dev/null +++ b/tests/widget/operatorWithValidator/operatorType/applyApplication/addresses.spec.ts @@ -0,0 +1,248 @@ +import { test } from '../../../test.fixture'; +import { expect } from '@playwright/test'; +import { mnemonicToAccount, generateMnemonic } from 'viem/accounts'; +import { wordlist as english } from '@scure/bip39/wordlists/english.js'; +import { Wallet, utils } from 'ethers'; +import { PAGE_WAIT_TIMEOUT } from 'tests/consts/timeouts'; + +const secretPhrase = generateMnemonic(english, 128); + +test.use({ secretPhrase: secretPhrase }); + +test.describe('Operator with keys. ICS. Apply application. Addresses', async () => { + test.beforeAll(async ({ widgetService }) => { + const applicationForm = widgetService.operatorType.applicationForm; + await applicationForm.open(); + + await applicationForm.signInForm.signIn(); + }); + + test.beforeEach(async ({ widgetService }) => { + const applicationForm = widgetService.operatorType.applicationForm; + + await applicationForm.open(); + }); + + test.afterAll(async ({ widgetService }) => { + await test.step('Clear session storage', async () => { + await widgetService.page.evaluate(() => { + sessionStorage.clear(); + }); + }); + }); + + test('Check main address appearance', async ({ + widgetService, + secretPhrase, + }) => { + const applicationForm = widgetService.operatorType.applicationForm; + + await test.step('Verify texts', async () => { + await expect( + applicationForm.submitApplicationForm.mainAddressTitle, + ).toContainText('Main address'); + await expect( + applicationForm.submitApplicationForm.mainAddressSection, + ).toContainText( + 'You are requesting ICS operator type to the following address:', + ); + }); + + await test.step('Verify main address input', async () => { + const address = mnemonicToAccount(secretPhrase).address; + await expect( + applicationForm.submitApplicationForm.mainAddressInput, + ).toHaveValue(address); + await expect( + applicationForm.submitApplicationForm.mainAddressLabel, + ).toBeDisabled(); + }); + + await test.step('Verify verified chip', async () => { + await expect( + applicationForm.submitApplicationForm.mainAddressVerifiedChip, + ).toBeVisible(); + await expect( + applicationForm.submitApplicationForm.mainAddressVerifiedChip, + ).toContainText('Verified'); + }); + }); + + test('Should add maximum 5 additional addresses', async ({ + widgetService, + }) => { + const applicationForm = widgetService.operatorType.applicationForm; + + await test.step('Verify section description', async () => { + await expect( + applicationForm.submitApplicationForm.additionalAddressesSection, + ).toContainText( + 'You can add up to 5 addresses where your achievements are stored. To prove you own each address, sign a message on Etherscan. For more info see the guide', + ); + }); + + await test.step('Add 5 additional addresses', async () => { + for (let i = 0; i < 5; i++) { + await applicationForm.submitApplicationForm.addNewAddressBtn.click(); + } + }); + + await expect( + applicationForm.submitApplicationForm.addNewAddressBtn, + ).toBeHidden(); + }); + + test('Check additional addresses appearance', async ({ widgetService }) => { + const applicationForm = widgetService.operatorType.applicationForm; + const additionalAddress = mnemonicToAccount(generateMnemonic(english, 128)); + + const additionalAddressField0 = + applicationForm.submitApplicationForm.getAdditionalAddressFieldByIndex(0); + + await test.step('Add new additional address', async () => { + await applicationForm.submitApplicationForm.addNewAddressBtn.click(); + await expect(additionalAddressField0.addressField).toContainText( + 'Step 1. Insert your Ethereum address', + ); + + await applicationForm.submitApplicationForm.fillAdditionalAddress( + 0, + additionalAddress.address, + ); + }); + const address = mnemonicToAccount(secretPhrase).address; + + const expectedSignMessage = `Verify ownership of address ${additionalAddress.address.toLowerCase()} for ICS with main address ${address.toLowerCase()}`; + await test.step('Verify autofill message to sign input', async () => { + await expect(additionalAddressField0.signMessageField).toContainText( + 'Step 2. Copy the message and sign it on Etherscan (or other tool)', + ); + + await expect(additionalAddressField0.signMessageInput).toHaveValue( + expectedSignMessage, + ); + }); + + await test.step('Verify signature input', async () => { + const signature = await new Wallet( + // @ts-expect-error may be null + utils.hexlify(additionalAddress.getHdKey().privateKey), + ).signMessage(expectedSignMessage); + + await additionalAddressField0.signatureInput.fill(signature); + await expect(additionalAddressField0.signatureField).toContainText( + 'Step 3. Paste the signature in the field below', + ); + + await expect(additionalAddressField0.signMessageInput).toHaveValue( + expectedSignMessage, + ); + }); + }); + + test('Should successfully verify signature', async ({ + widgetService, + secretPhrase, + }) => { + const applicationForm = widgetService.operatorType.applicationForm; + const additionalAddress = mnemonicToAccount(generateMnemonic(english, 128)); + + const additionalAddressField0 = + applicationForm.submitApplicationForm.getAdditionalAddressFieldByIndex(0); + + await test.step('Add new additional address', async () => { + await applicationForm.submitApplicationForm.addNewAddressBtn.click(); + await applicationForm.submitApplicationForm.fillAdditionalAddress( + 0, + additionalAddress.address, + ); + }); + + const address = mnemonicToAccount(secretPhrase).address; + + const signMessage = `Verify ownership of address ${additionalAddress.address.toLowerCase()} for ICS with main address ${address.toLowerCase()}`; + + await expect(additionalAddressField0.signMessageInput).toHaveValue( + signMessage, + ); + + await test.step('Check verified state for address', async () => { + const signature = await new Wallet( + // @ts-expect-error may be null + utils.hexlify(additionalAddress.getHdKey().privateKey), + ).signMessage(signMessage); + + await additionalAddressField0.signatureInput.fill(signature); + await additionalAddressField0.verifySignatureBtn.click(); + await expect(additionalAddressField0.verifyChip).toBeVisible(); + await expect(additionalAddressField0.verifiedAddressInput).toBeDisabled(); + await expect(additionalAddressField0.verifiedAddressInput).toHaveValue( + additionalAddress.address, + ); + }); + }); + + test('Should copy message to sign after click to copy button', async ({ + widgetService, + }) => { + const applicationForm = widgetService.operatorType.applicationForm; + const additionalAddress = mnemonicToAccount( + generateMnemonic(english, 128), + ).address; + const additionalAddressField0 = + applicationForm.submitApplicationForm.getAdditionalAddressFieldByIndex(0); + + await test.step('Add new additional address', async () => { + await applicationForm.submitApplicationForm.addNewAddressBtn.click(); + + await applicationForm.submitApplicationForm.fillAdditionalAddress( + 0, + additionalAddress, + ); + }); + + await test.step('Verify copy button for sign message', async () => { + const address = mnemonicToAccount(secretPhrase).address; + const signMessage = `Verify ownership of address ${additionalAddress.toLowerCase()} for ICS with main address ${address.toLowerCase()}`; + + await expect(additionalAddressField0.signMessageInput).toHaveValue( + signMessage, + ); + await additionalAddressField0.copySignMessageBtn.click(); + + const clipboardText = await widgetService.page.evaluate(() => + navigator.clipboard.readText(), + ); + expect(clipboardText).toBe(signMessage); + }); + }); + + test('Should open sign link in a new tab', async ({ widgetService }) => { + const applicationForm = widgetService.operatorType.applicationForm; + const additionalAddress = mnemonicToAccount( + generateMnemonic(english, 128), + ).address; + const additionalAddressField0 = + applicationForm.submitApplicationForm.getAdditionalAddressFieldByIndex(0); + + await test.step('Add new additional address', async () => { + await applicationForm.submitApplicationForm.addNewAddressBtn.click(); + + await applicationForm.submitApplicationForm.fillAdditionalAddress( + 0, + additionalAddress, + ); + }); + + await test.step('Verify sign link opening', async () => { + const [etherscanPageForSign] = await Promise.all([ + widgetService.operatorType.waitForPage(PAGE_WAIT_TIMEOUT), + additionalAddressField0.signMessageBtn.click(), + ]); + await expect(etherscanPageForSign).toHaveURL( + 'https://etherscan.io/verifiedSignatures#', + ); + await etherscanPageForSign.close(); + }); + }); +}); diff --git a/tests/widget/operatorWithValidator/operatorType/applyApplication/socials.spec.ts b/tests/widget/operatorWithValidator/operatorType/applyApplication/socials.spec.ts new file mode 100644 index 000000000..8d8e9dadc --- /dev/null +++ b/tests/widget/operatorWithValidator/operatorType/applyApplication/socials.spec.ts @@ -0,0 +1,276 @@ +import { test } from '../../../test.fixture'; +import { expect } from '@playwright/test'; +import { mnemonicToAccount, generateMnemonic } from 'viem/accounts'; +import { wordlist as english } from '@scure/bip39/wordlists/english.js'; + +const secretPhrase = generateMnemonic(english, 128); + +test.use({ secretPhrase: secretPhrase }); + +test.describe('Operator with keys. ICS. Apply application. Socials', async () => { + test.beforeAll(async ({ widgetService }) => { + const applicationForm = widgetService.operatorType.applicationForm; + await applicationForm.open(); + + await applicationForm.signInForm.signIn(); + }); + + test.beforeEach(async ({ widgetService }) => { + const applicationForm = widgetService.operatorType.applicationForm; + + await applicationForm.open(); + }); + + test.afterAll(async ({ widgetService }) => { + await test.step('Clear session storage', async () => { + await widgetService.page.evaluate(() => { + sessionStorage.clear(); + }); + }); + }); + + test('Check socials text appearance', async ({ widgetService }) => { + const applicationForm = widgetService.operatorType.applicationForm; + + await test.step('Verify main texts', async () => { + await expect( + applicationForm.submitApplicationForm.socialProofSection, + ).toContainText( + 'You can add your social accounts. To prove you own an account, post a message. For more info see the guide', + ); + + await expect( + applicationForm.submitApplicationForm.socialProofTitile, + ).toContainText('Optional'); + }); + + await test.step('Verify link in socials section', async () => { + const guideLink = + applicationForm.submitApplicationForm.socialProofSection.getByRole( + 'link', + { name: 'the guide' }, + ); + await expect(guideLink).toHaveAttribute( + 'href', + 'https://www.youtube.com/watch?v=yUX34iCbCWE', + ); + }); + }); + + test('Check Twitter section appearance', async ({ + widgetService, + secretPhrase, + }) => { + const applicationForm = widgetService.operatorType.applicationForm; + + await test.step('Verify Twitter section texts', async () => { + await expect( + applicationForm.submitApplicationForm.twitterSection.locator('h4'), + ).toContainText('X (formerly Twitter)'); + await expect( + applicationForm.submitApplicationForm.twitterProofStep1, + ).toContainText( + 'Step 1. Prove the ownership of the X account by posting a tweet with the following text', + ); + + await expect( + applicationForm.submitApplicationForm.twitterProofStep2, + ).toContainText('Step 2. Paste the link to this post'); + }); + const address = mnemonicToAccount(secretPhrase).address; + + await test.step('Verify Twitter proof message input', async () => { + await expect( + applicationForm.submitApplicationForm.twitterProofStep1Input, + ).toHaveValue( + `This post is proof that I am the owner of this X account. My address to get verified for ICS: ${address.toLowerCase()}`, + ); + + await expect( + applicationForm.submitApplicationForm.twitterProofStep1CopyBtn, + ).toBeVisible(); + }); + }); + + test('Check Discord section appearance', async ({ widgetService }) => { + const applicationForm = widgetService.operatorType.applicationForm; + + await test.step('Verify Discord section texts', async () => { + await expect( + applicationForm.submitApplicationForm.discordSection.locator('h4'), + ).toContainText('Discord'); + await expect( + applicationForm.submitApplicationForm.discordProofStep1, + ).toContainText( + 'Step 1. Prove the ownership of the Discord account by posting the following message to the CSM channel', + ); + await test.step('Verify link in description of step 1', async () => { + const csmChannelLink = + applicationForm.submitApplicationForm.discordSection.getByRole( + 'link', + { name: 'the CSM channel' }, + ); + await expect(csmChannelLink).toHaveAttribute( + 'href', + 'https://discord.com/channels/761182643269795850/1404810479292907662', + ); + }); + await expect( + applicationForm.submitApplicationForm.discordProofStep2, + ).toContainText('Step 2. Paste the link to this message'); + }); + const address = mnemonicToAccount(secretPhrase).address; + + await test.step('Verify Discord proof message input', async () => { + await expect( + applicationForm.submitApplicationForm.discordProofStep1Input, + ).toHaveValue( + `This post is proof that I am the owner of this Discord account. My address to get verified for ICS: ${address.toLowerCase()}`, + ); + + await expect( + applicationForm.submitApplicationForm.discordProofStep1CopyBtn, + ).toBeVisible(); + }); + }); + + test('Should copy Twitter message to post after click to copy button', async ({ + widgetService, + secretPhrase, + }) => { + const applicationForm = widgetService.operatorType.applicationForm; + + await test.step('Copy Twitter proof message', async () => { + await applicationForm.submitApplicationForm.twitterProofStep1CopyBtn.click(); + const twitterMessage = await widgetService.page.evaluate(() => + navigator.clipboard.readText(), + ); + const address = mnemonicToAccount(secretPhrase).address; + const expectedTwitterMessage = `This post is proof that I am the owner of this X account. My address to get verified for ICS: ${address.toLowerCase()}`; + + expect(twitterMessage).toBe(expectedTwitterMessage); + }); + }); + + test('Should copy Discord message to post after click to copy button', async ({ + widgetService, + secretPhrase, + }) => { + const applicationForm = widgetService.operatorType.applicationForm; + + await test.step('Copy Discord proof message', async () => { + await applicationForm.submitApplicationForm.discordProofStep1CopyBtn.click(); + const discordMessage = await widgetService.page.evaluate(() => + navigator.clipboard.readText(), + ); + const address = mnemonicToAccount(secretPhrase).address; + const expectedDiscordMessage = `This post is proof that I am the owner of this Discord account. My address to get verified for ICS: ${address.toLowerCase()}`; + expect(discordMessage).toBe(expectedDiscordMessage); + }); + }); + + test('Should correctly paste Twitter proof link', async ({ + widgetService, + }) => { + const applicationForm = widgetService.operatorType.applicationForm; + const twitterProofLink = 'https://x.com/someuser/status/1234567890'; + + await test.step('Paste Twitter proof link', async () => { + await applicationForm.submitApplicationForm.twitterProofStep2 + .locator('input') + .fill(twitterProofLink); + + await expect( + applicationForm.submitApplicationForm.twitterProofStep2.locator( + 'input', + ), + ).toHaveValue(twitterProofLink); + + await expect( + applicationForm.submitApplicationForm.twitterProofStep2.locator( + 'input', + ), + ).toHaveAttribute('placeholder', 'https://x.com/username/status/...'); + await expect( + applicationForm.submitApplicationForm.twitterProofStep2.getByText( + 'Must be a valid Twitter/X status URL', + ), + ).toBeHidden(); + }); + }); + + test('Should correctly paste Discord proof link', async ({ + widgetService, + }) => { + const applicationForm = widgetService.operatorType.applicationForm; + const discordProofLink = 'https://discord.com/channels/123/456/789'; + + await test.step('Paste Discord proof link', async () => { + await applicationForm.submitApplicationForm.discordProofStep2 + .locator('input') + .fill(discordProofLink); + + await expect( + applicationForm.submitApplicationForm.discordProofStep2.locator( + 'input', + ), + ).toHaveValue(discordProofLink); + + await expect( + applicationForm.submitApplicationForm.discordProofStep2.locator( + 'input', + ), + ).toHaveAttribute('placeholder', 'https://discord.com/channels/...'); + await expect( + applicationForm.submitApplicationForm.discordProofStep2.getByText( + 'Must be a valid Discord message URL', + ), + ).toBeHidden(); + }); + }); + + test('Should show error if paste incorrect Twitter proof link', async ({ + widgetService, + }) => { + const applicationForm = widgetService.operatorType.applicationForm; + const twitterProofLink = 'https://x.com/someuser/1234567890'; + + await test.step('Paste Twitter incorrect proof link', async () => { + await applicationForm.submitApplicationForm.twitterProofStep2 + .locator('input') + .fill(twitterProofLink); + + await expect( + applicationForm.submitApplicationForm.twitterProofStep2.getByText( + 'Must be a valid Twitter/X status URL', + ), + ).toBeVisible(); + + await expect( + applicationForm.submitApplicationForm.submitBtn, + ).toBeDisabled(); + }); + }); + + test('Should show error if paste incorrect Discord proof link', async ({ + widgetService, + }) => { + const applicationForm = widgetService.operatorType.applicationForm; + const discordProofLink = 'https://discord.com/chan/123/456/789'; + await test.step('Paste Discord incorrect proof link', async () => { + await applicationForm.submitApplicationForm.discordProofStep2 + .locator('input') + .fill(discordProofLink); + + await expect( + applicationForm.submitApplicationForm.discordProofStep2.getByText( + 'Must be a valid Discord message URL', + ), + ).toBeVisible(); + + await expect( + applicationForm.submitApplicationForm.submitBtn, + ).toBeDisabled(); + }); + }); +}); diff --git a/tests/widget/operatorWithValidator/operatorType/applyApplication/submitHappyPath.spec.ts b/tests/widget/operatorWithValidator/operatorType/applyApplication/submitHappyPath.spec.ts new file mode 100644 index 000000000..39e790370 --- /dev/null +++ b/tests/widget/operatorWithValidator/operatorType/applyApplication/submitHappyPath.spec.ts @@ -0,0 +1,60 @@ +import { test } from '../../../test.fixture'; +import { expect } from '@playwright/test'; +import { mnemonicToAccount, generateMnemonic } from 'viem/accounts'; +import { wordlist as english } from '@scure/bip39/wordlists/english.js'; +import { Tags } from 'tests/consts/common.const'; + +const secretPhrase = generateMnemonic(english, 128); +test.use({ secretPhrase: secretPhrase }); + +test.describe('Operator with keys. ICS. Apply application. Submit Happy Path', async () => { + test.beforeAll(async ({ widgetService }) => { + const applicationForm = widgetService.operatorType.applicationForm; + await applicationForm.open(); + + await applicationForm.signInForm.signIn(); + }); + + test( + 'Should successfully verify signature with 1 additional addresses and socials', + { + tag: [Tags.smoke], + }, + async ({ widgetService, secretPhrase }) => { + const applicationForm = widgetService.operatorType.applicationForm; + const additionalAddress = mnemonicToAccount( + generateMnemonic(english, 128), + ); + await applicationForm.submitApplicationForm.addAdditionalAddress( + additionalAddress, + mnemonicToAccount(secretPhrase).address, + ); + + await applicationForm.submitApplicationForm.addSocialsProof(); + + await applicationForm.submitApplicationForm.submitBtn.click(); + await widgetService.operatorType.txModal.modal.waitFor({ + state: 'visible', + }); + await expect(widgetService.operatorType.txModal.title).toContainText( + 'Your application has been submitted', + ); + await expect( + widgetService.operatorType.txModal.description, + ).toContainText( + 'You can track your application’s status on the Operator Type tab.', + ); + + await widgetService.operatorType.txModal.closeModal(); + await applicationForm.applicationFormStatus.form.waitFor({ + state: 'visible', + }); + await expect( + applicationForm.applicationFormStatus.scoreChip, + ).toBeVisible(); + await expect( + applicationForm.applicationFormStatus.scoreChip, + ).toContainText('Pending'); + }, + ); +}); diff --git a/tests/widget/operatorWithValidator/operatorType/sign.spec.ts b/tests/widget/operatorWithValidator/operatorType/sign.spec.ts new file mode 100644 index 000000000..ac9d7d4c6 --- /dev/null +++ b/tests/widget/operatorWithValidator/operatorType/sign.spec.ts @@ -0,0 +1,73 @@ +import { test } from '../../test.fixture'; +import { expect } from '@playwright/test'; +import { mnemonicToAccount } from 'viem/accounts'; + +test.use({ secretPhrase: process.env.EMPTY_SECRET_PHRASE }); + +test.describe('Operator with keys. ICS. Sign in', async () => { + test.afterEach(async ({ widgetService }) => { + await widgetService.page.evaluate(() => { + sessionStorage.clear(); + }); + }); + + test('Should make sign transaction and save data to Session Storage', async ({ + widgetService, + secretPhrase, + }) => { + const applicationForm = widgetService.operatorType.applicationForm; + await applicationForm.open(); + + await applicationForm.signInForm.signIn(); + + await expect( + widgetService.page.locator('text=Submit application'), + ).toBeVisible(); + + const address = mnemonicToAccount(secretPhrase).address; + const icsToken = await applicationForm.signInForm.getSessionStorageData( + `ics-token-${address}`, + ); + + expect(icsToken).not.toBeNull(); + }); + + test('Should sign out after remove ICS token from Session Storage', async ({ + widgetService, + secretPhrase, + }) => { + const applicationForm = widgetService.operatorType.applicationForm; + await applicationForm.open(); + + await applicationForm.signInForm.signIn(); + + const address = mnemonicToAccount(secretPhrase).address; + await applicationForm.signInForm.removeKeyFromSessionStorage( + `ics-token-${address}`, + ); + + await widgetService.page.reload(); + await expect(applicationForm.signInForm.form).toBeVisible(); + }); + + test('Check sign in appearence', async ({ widgetService, secretPhrase }) => { + const applicationForm = widgetService.operatorType.applicationForm; + await applicationForm.open(); + + await test.step('Verify sign in form text', async () => { + await expect(applicationForm.signInForm.form).toContainText('Sign in'); + await expect(applicationForm.signInForm.form).toContainText( + 'To continue, please sign a message with your connected address to prove ownership.', + ); + await expect(applicationForm.signInForm.form).toContainText( + 'You are requesting ICS operator type for the following address:', + ); + }); + + await test.step('Verify sign in input', async () => { + const mainAddressInput = applicationForm.signInForm.mainAddressInput; + const address = mnemonicToAccount(secretPhrase).address; + await expect(mainAddressInput).toHaveValue(address); + }); + }); +}); diff --git a/tests/widget/test.fixture.ts b/tests/widget/test.fixture.ts index 40ad0dafc..791eba56b 100644 --- a/tests/widget/test.fixture.ts +++ b/tests/widget/test.fixture.ts @@ -11,6 +11,7 @@ import { mnemonicToAccount } from 'viem/accounts'; import { FORK_WARM_UP_TIMEOUT } from 'tests/consts/timeouts'; import ForkActionsService from 'tests/services/forkActions.service'; import { warmUpForkedNode } from 'tests/helpers/warmUpFork'; +import { HttpMockerService } from 'tests/services/httpMocker.service'; type WorkerFixtures = { // fixture-options @@ -23,6 +24,7 @@ type WorkerFixtures = { csmSDK: LidoSDKClient; ethereumSDK: SdkService; forkActionService: ForkActionsService; + httpMockerService: HttpMockerService; }; export const test = base.extend<{ widgetConfig: IConfig }, WorkerFixtures>({ @@ -138,6 +140,18 @@ export const test = base.extend<{ widgetConfig: IConfig }, WorkerFixtures>({ }, { scope: 'worker' }, ], + httpMockerService: [ + async ({ widgetService }, use) => { + await use( + new HttpMockerService( + widgetService.page, + // @ts-expect-error may be null + widgetFullConfig.standConfig.mockConfig, + ), + ); + }, + { scope: 'worker' }, + ], }); /** diff --git a/yarn.lock b/yarn.lock index e1196d25c..0b41f7189 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3617,6 +3617,11 @@ resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.8.0.tgz#cee43d801fcef9644b11b8194857695acd5f815a" integrity sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A== +"@noble/hashes@2.0.1": + version "2.0.1" + resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-2.0.1.tgz#fc1a928061d1232b0a52bb754393c37a5216c89e" + integrity sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw== + "@nodelib/fs.scandir@2.1.5": version "2.1.5" resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" @@ -3676,12 +3681,12 @@ resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33" integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg== -"@playwright/test@1.56.1": - version "1.56.1" - resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.56.1.tgz#6e3bf3d0c90c5cf94bf64bdb56fd15a805c8bd3f" - integrity sha512-vSMYtL/zOcFpvJCW71Q/OEGQb7KYBPAdKh35WNSkaZA75JlAO8ED8UN6GUNTm3drWomcbcqRPFqQbLae8yBTdg== +"@playwright/test@1.57.0": + version "1.57.0" + resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.57.0.tgz#a14720ffa9ed7ef7edbc1f60784fc6134acbb003" + integrity sha512-6TyEnHgd6SArQO8UO2OMTxshln3QMWBtPGrOCgs3wVEmQmwyuNtB10IZMfmYDE0riwNR1cu4q+pPcxMVtaG3TA== dependencies: - playwright "1.56.1" + playwright "1.57.0" "@polka/url@^1.0.0-next.20": version "1.0.0-next.24" @@ -3992,6 +3997,11 @@ resolved "https://registry.yarnpkg.com/@safe-global/safe-gateway-typescript-sdk/-/safe-gateway-typescript-sdk-3.22.9.tgz#7f6571aaf1aecbe1217f6dd294ad2f3d90c2c8c2" integrity sha512-7ojVK/crhOaGowEO8uYWaopZzcr5rR76emgllGIfjCLR70aY4PbASpi9Pbs+7jIRzPDBBkM0RBo+zYx5UduX8Q== +"@scure/base@2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@scure/base/-/base-2.0.0.tgz#ba6371fddf92c2727e88ad6ab485db6e624f9a98" + integrity sha512-3E1kpuZginKkek01ovG8krQ0Z44E3DHPjc5S2rjJw9lZn3KSQOs8S7wqikF/AH7iRanHypj85uGyxk0XAyC37w== + "@scure/base@^1.1.3": version "1.2.4" resolved "https://registry.yarnpkg.com/@scure/base/-/base-1.2.4.tgz#002eb571a35d69bdb4c214d0995dff76a8dcd2a9" @@ -4063,6 +4073,14 @@ "@noble/hashes" "~1.8.0" "@scure/base" "~1.2.5" +"@scure/bip39@2.0.1": + version "2.0.1" + resolved "https://registry.yarnpkg.com/@scure/bip39/-/bip39-2.0.1.tgz#47a6dc15e04faf200041239d46ae3bb7c3c96add" + integrity sha512-PsxdFj/d2AcJcZDX1FXN3dDgitDDTmwf78rKZq1a6c1P1Nan1X/Sxc7667zU3U+AN60g7SxxP0YCVw2H/hBycg== + dependencies: + "@noble/hashes" "2.0.1" + "@scure/base" "2.0.0" + "@sinclair/typebox@^0.24.1": version "0.24.51" resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.24.51.tgz#645f33fe4e02defe26f2f5c0410e1c094eac7f5f" @@ -7428,7 +7446,7 @@ ethereum-cryptography@^3.0.0: "@scure/bip32" "1.7.0" "@scure/bip39" "1.6.0" -ethers@^5.8.0: +ethers@5.8.0, ethers@^5.8.0: version "5.8.0" resolved "https://registry.yarnpkg.com/ethers/-/ethers-5.8.0.tgz#97858dc4d4c74afce83ea7562fe9493cedb4d377" integrity sha512-DUq+7fHrCg1aPDFCHx6UIPb3nmt2XMpM7Y/g2gLhsl3lIBqeAfOJIl1qEvRf2uq3BiKxmh6Fh5pfp2ieyek7Kg== @@ -10360,10 +10378,10 @@ pkg-dir@^4.2.0: dependencies: find-up "^4.0.0" -playwright-core@1.56.1: - version "1.56.1" - resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.56.1.tgz#24a66481e5cd33a045632230aa2c4f0cb6b1db3d" - integrity sha512-hutraynyn31F+Bifme+Ps9Vq59hKuUCz7H1kDOcBs+2oGguKkWTU50bBWrtz34OUWmIwpBTWDxaRPXrIXkgvmQ== +playwright-core@1.57.0: + version "1.57.0" + resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.57.0.tgz#3dcc9a865af256fa9f0af0d67fc8dd54eecaebf5" + integrity sha512-agTcKlMw/mjBWOnD6kFZttAAGHgi/Nw0CZ2o6JqWSbMlI219lAFLZZCyqByTsvVAJq5XA5H8cA6PrvBRpBWEuQ== playwright-qase-reporter@^2.1.6: version "2.1.6" @@ -10374,12 +10392,12 @@ playwright-qase-reporter@^2.1.6: qase-javascript-commons "~2.4.2" uuid "^9.0.0" -playwright@1.56.1: - version "1.56.1" - resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.56.1.tgz#62e3b99ddebed0d475e5936a152c88e68be55fbf" - integrity sha512-aFi5B0WovBHTEvpM3DzXTUaeN6eN0qWnTkKx4NQaH4Wvcmc153PdaY2UBdSYKaGYw+UyWXSVyxDUg5DoPEttjw== +playwright@1.57.0: + version "1.57.0" + resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.57.0.tgz#74d1dacff5048dc40bf4676940b1901e18ad0f46" + integrity sha512-ilYQj1s8sr2ppEJ2YVadYBN0Mb3mdo9J0wQ+UuDhzYqURwSoW4n1Xs5vs7ORwgDGmyEh33tRMeS8KhdkMoLXQw== dependencies: - playwright-core "1.56.1" + playwright-core "1.57.0" optionalDependencies: fsevents "2.3.2"