From 7a125cea67619712d0ce5605b2f90f222edd99fa Mon Sep 17 00:00:00 2001 From: Maxime Beauchamp <15185355+baktun14@users.noreply.github.com> Date: Wed, 13 May 2026 22:26:28 -0500 Subject: [PATCH 1/4] chore(wallet): remove self-custody Playwright e2e suite Self-custody is being turned off in Console as part of AEP-84. The Playwright specs in apps/deploy-web/tests/ui/ that drove the self-custody wallet via an injected Leap extension will break the moment the feature flag flips, since the UI they exercise stops rendering. Delete those specs and their helpers so the remaining managed-wallet e2e suite keeps CI green. Kept playwright.config.ts, the test:e2e script, and @playwright/test because the managed-wallet-*.spec.ts files (added since the issue was filed) still rely on them. Pruned WebWallet/walletType plumbing from the shared DeployPage and dropped TEST_WALLET_MNEMONIC from the env schema and CI inputs. Closes CON-265 --- .../actions/console-web-ui-testing/action.yml | 11 - .github/workflows/console-web-release.yml | 1 - apps/deploy-web/script/closeDeployments.ts | 66 ----- .../tests/ui/actions/selectChainNetwork.ts | 30 --- .../tests/ui/authorize-spending.spec.ts | 93 ------- .../tests/ui/build-template.spec.ts | 12 - .../tests/ui/change-wallets.spec.ts | 28 -- .../tests/ui/custom-container-form.spec.ts | 19 -- .../tests/ui/deploy-from-a-template.spec.ts | 33 --- apps/deploy-web/tests/ui/deploy-linux.spec.ts | 26 -- .../deploy-self-custody-hello-world.spec.ts | 30 --- .../ui/fixture/context-with-extension.ts | 51 ---- .../tests/ui/fixture/test-env.config.ts | 3 +- .../tests/ui/fixture/testing-helpers.ts | 19 -- .../tests/ui/fixture/wallet-setup.ts | 255 ------------------ .../ui/fixture/web-wallet/CosmjsWebWallet.ts | 199 -------------- .../web-wallet/initLeapWebWalletMock.ts | 65 ----- .../ui/fixture/web-wallet/injectWebWallet.ts | 42 --- .../tests/ui/managed-wallet-alerts.spec.ts | 2 +- .../ui/managed-wallet-deployment.spec.ts | 2 +- .../tests/ui/pages/AuthorizationsPage.tsx | 89 ------ .../tests/ui/pages/BuildTemplatePage.tsx | 53 ---- apps/deploy-web/tests/ui/pages/DeployPage.tsx | 86 ++---- .../tests/ui/pages/PlainLinuxPage.tsx | 9 - apps/deploy-web/tests/ui/pages/WebWallet.ts | 67 ----- .../tests/ui/sdl-builder-deployment.spec.ts | 107 -------- .../tests/ui/uiState/isWalletConnected.ts | 22 -- 27 files changed, 20 insertions(+), 1400 deletions(-) delete mode 100644 apps/deploy-web/script/closeDeployments.ts delete mode 100644 apps/deploy-web/tests/ui/actions/selectChainNetwork.ts delete mode 100644 apps/deploy-web/tests/ui/authorize-spending.spec.ts delete mode 100644 apps/deploy-web/tests/ui/build-template.spec.ts delete mode 100644 apps/deploy-web/tests/ui/change-wallets.spec.ts delete mode 100644 apps/deploy-web/tests/ui/custom-container-form.spec.ts delete mode 100644 apps/deploy-web/tests/ui/deploy-from-a-template.spec.ts delete mode 100644 apps/deploy-web/tests/ui/deploy-linux.spec.ts delete mode 100644 apps/deploy-web/tests/ui/deploy-self-custody-hello-world.spec.ts delete mode 100644 apps/deploy-web/tests/ui/fixture/context-with-extension.ts delete mode 100644 apps/deploy-web/tests/ui/fixture/testing-helpers.ts delete mode 100644 apps/deploy-web/tests/ui/fixture/wallet-setup.ts delete mode 100644 apps/deploy-web/tests/ui/fixture/web-wallet/CosmjsWebWallet.ts delete mode 100644 apps/deploy-web/tests/ui/fixture/web-wallet/initLeapWebWalletMock.ts delete mode 100644 apps/deploy-web/tests/ui/fixture/web-wallet/injectWebWallet.ts delete mode 100644 apps/deploy-web/tests/ui/pages/AuthorizationsPage.tsx delete mode 100644 apps/deploy-web/tests/ui/pages/BuildTemplatePage.tsx delete mode 100644 apps/deploy-web/tests/ui/pages/PlainLinuxPage.tsx delete mode 100644 apps/deploy-web/tests/ui/pages/WebWallet.ts delete mode 100644 apps/deploy-web/tests/ui/sdl-builder-deployment.spec.ts delete mode 100644 apps/deploy-web/tests/ui/uiState/isWalletConnected.ts diff --git a/.github/actions/console-web-ui-testing/action.yml b/.github/actions/console-web-ui-testing/action.yml index 51cb923dc8..5f1ea21d1b 100644 --- a/.github/actions/console-web-ui-testing/action.yml +++ b/.github/actions/console-web-ui-testing/action.yml @@ -8,9 +8,6 @@ inputs: url: description: Base URL to Console Web required: true - test-wallet-mnemonic: - description: Test wallet mnemonic - required: true slack-webhook-url: description: Slack webhook URL required: false @@ -61,7 +58,6 @@ runs: - name: Run e2e tests id: e2e-tests env: - TEST_WALLET_MNEMONIC: ${{ inputs.test-wallet-mnemonic }} BASE_URL: ${{ inputs.url }} E2E_TESTING_CLIENT_TOKEN: ${{ inputs.testing-client-token }} AUTH0_M2M_DOMAIN: ${{ inputs.auth0-m2m-domain }} @@ -75,13 +71,6 @@ runs: CI: "true" shell: bash run: npm run test:e2e --workspace=apps/deploy-web - - name: Tests cleanup - if: ${{ !cancelled() }} - shell: bash - env: - TEST_WALLET_MNEMONIC: ${{ inputs.test-wallet-mnemonic }} - run: | - npx --yes tsx@4.20.6 apps/deploy-web/script/closeDeployments.ts - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 id: playwright-report if: ${{ !cancelled() }} diff --git a/.github/workflows/console-web-release.yml b/.github/workflows/console-web-release.yml index c12ce6adcf..532492cba3 100644 --- a/.github/workflows/console-web-release.yml +++ b/.github/workflows/console-web-release.yml @@ -58,7 +58,6 @@ jobs: ref: console-web/v${{ needs.setup.outputs.image_tag }} url: ${{ vars.CONSOLE_WEB_BETA_URL }} slack-webhook-url: ${{ secrets.FAILED_E2E_TESTS_SLACK_WEBHOOK_URL }} - test-wallet-mnemonic: ${{ secrets.CONSOLE_WEB_E2E_TEST_WALLET_MNEMONIC }} gh-user-to-slack-user: ${{ vars.GH_USER_TO_SLACK_USER }} testing-client-token: ${{ secrets.CONSOLE_WEB_E2E_TESTING_CLIENT_TOKEN_BETA }} auth0-m2m-domain: ${{ secrets.AUTH0_M2M_DOMAIN }} diff --git a/apps/deploy-web/script/closeDeployments.ts b/apps/deploy-web/script/closeDeployments.ts deleted file mode 100644 index a4b9841aa4..0000000000 --- a/apps/deploy-web/script/closeDeployments.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { MsgCloseDeployment } from "@akashnetwork/chain-sdk/private-types/akash.v1beta4"; -import { netConfig } from "@akashnetwork/net"; -import type { GeneratedType } from "@cosmjs/proto-signing"; -import { DirectSecp256k1HdWallet, Registry } from "@cosmjs/proto-signing"; -import { SigningStargateClient } from "@cosmjs/stargate"; - -const mnemonic = process.env.TEST_WALLET_MNEMONIC; - -const newAkashTypes: ReadonlyArray<[string, GeneratedType]> = [MsgCloseDeployment] - .filter(x => "$type" in x) - .map(x => ["/" + x.$type, x as unknown as GeneratedType]); -const registry = new Registry([...newAkashTypes]); - -async function main() { - if (!mnemonic) { - throw new Error("TEST_WALLET_MNEMONIC is not provided"); - } - - const signer = await DirectSecp256k1HdWallet.fromMnemonic(mnemonic, { - prefix: "akash" - }); - - const account = (await signer.getAccounts())[0]; - console.log("Fetching deployments..."); - const deploymentsResponse = await fetch( - `${netConfig.getBaseAPIUrl("sandbox")}/akash/deployment/v1beta4/deployments/list?filters.owner=${account.address}&filters.state=active&pagination.limit=100` - ); - const { deployments } = await deploymentsResponse.json(); - - if (deployments.length === 0) { - console.log("No active deployments found. Exiting..."); - return; - } - - console.log(`Found ${deployments.length} active deployments. Going to close them...`); - - const closeDeploymentsMessages = deployments.map((deployment: any) => { - return { - typeUrl: `/${MsgCloseDeployment.$type}`, - value: MsgCloseDeployment.fromPartial({ - id: deployment.deployment.id - }) - }; - }); - - const txClient = await SigningStargateClient.connectWithSigner(netConfig.getBaseRpcUrl("sandbox"), signer, { - registry - }); - - console.log("Closing deployments..."); - const gas = await txClient.simulate(account.address, closeDeploymentsMessages, "close deployments via script"); - const tx = await txClient.signAndBroadcast(account.address, closeDeploymentsMessages, { - amount: [{ amount: Math.ceil(2500 * closeDeploymentsMessages.length).toString(), denom: "uakt" }], - gas: Math.floor(1.3 * gas).toString() - }); - - if (tx.code !== 0) { - console.error(`Transaction failed with code ${tx.code}: ${tx.rawLog}`); - } else { - console.log(`Transaction hash: ${tx.transactionHash}`); - } - - txClient.disconnect(); -} - -main().catch(console.error); diff --git a/apps/deploy-web/tests/ui/actions/selectChainNetwork.ts b/apps/deploy-web/tests/ui/actions/selectChainNetwork.ts deleted file mode 100644 index 7789e17ee6..0000000000 --- a/apps/deploy-web/tests/ui/actions/selectChainNetwork.ts +++ /dev/null @@ -1,30 +0,0 @@ -import type { NetworkId } from "@akashnetwork/chain-sdk"; -import type { Page } from "@playwright/test"; - -import { isWalletConnected } from "../uiState/isWalletConnected"; - -export async function selectChainNetwork(page: Page, networkId: NetworkId = "sandbox") { - await page.getByRole("link", { name: "App Settings" }).click(); - const selectNetworkButton = page.getByLabel("Select Network"); - const selectedNetwork = await selectNetworkButton.locator("xpath=..").textContent(); - if (selectedNetwork?.toLowerCase().includes(networkId)) return; - - await selectNetworkButton.click({ timeout: 20_000 }); - - const networkRadioLocator = page.getByLabel(new RegExp(networkId, "i")); - await networkRadioLocator.click(); - const popupPromise = page - .context() - .waitForEvent("page", { timeout: 5_000 }) - .catch(() => null); - await page.getByRole("button", { name: "Save" }).click(); - - const popupPage = await Promise.race([ - popupPromise, - isWalletConnected(page).then( - () => null, - () => null - ) - ]); - await popupPage?.getByRole("button", { name: /Approve/i }).click(); -} diff --git a/apps/deploy-web/tests/ui/authorize-spending.spec.ts b/apps/deploy-web/tests/ui/authorize-spending.spec.ts deleted file mode 100644 index 27a1b697f2..0000000000 --- a/apps/deploy-web/tests/ui/authorize-spending.spec.ts +++ /dev/null @@ -1,93 +0,0 @@ -import { DirectSecp256k1HdWallet } from "@cosmjs/proto-signing"; -import type { BrowserContext, Page } from "@playwright/test"; - -import { createPage, expect, test } from "./fixture/context-with-extension"; -import { topUpWallet } from "./fixture/wallet-setup"; -import type { AuthorizationType } from "./pages/AuthorizationsPage"; -import { AuthorizationsPage, shortenAddress } from "./pages/AuthorizationsPage"; -import { WebWallet } from "./pages/WebWallet"; - -test.describe("Deployment Authorizations", () => { - includeAuthorizationTests({ authType: "deployment", denom: "ACT" }); -}); - -test.describe("Tx Fee Authorizations", () => { - includeAuthorizationTests({ authType: "tx_fee", denom: "AKT" }); -}); - -function includeAuthorizationTests(input: { authType: AuthorizationType; denom: "ACT" | "AKT" }) { - test.beforeAll(async ({ browser }) => { - const context = await browser.newContext(); - try { - const page = await createPage(context); - const authorizationsPage = new AuthorizationsPage(context, page); - await authorizationsPage.goto(); - await authorizationsPage.revokeAll(input.authType); - } finally { - await context.close(); - } - }); - - test.afterAll(async ({ browser }) => { - const context = await browser.newContext(); - try { - const page = await createPage(context); - const authorizationsPage = new AuthorizationsPage(context, page); - await authorizationsPage.goto(); - await authorizationsPage.revokeAll(input.authType); - } finally { - await context.close(); - } - }); - - test("can authorize spending", async ({ page, context }) => { - const { authorizationsPage, anotherWalletAddress: address } = await setup({ page, context, ...input }); - - const shortenedAddress = shortenAddress(address); - const grantList = authorizationsPage.getListLocator(input.authType); - - await expect(grantList.locator("td", { hasText: shortenedAddress })).toBeVisible({ timeout: 10_000 }); - }); - - test("can edit spending", async ({ page, context }) => { - const { authorizationsPage, anotherWalletAddress: address, extension } = await setup({ page, context, ...input }); - await Promise.all([extension.acceptTransaction("high"), authorizationsPage.editSpending(input.authType, address)]); - await extension.waitForTransaction("success"); - - const grantList = authorizationsPage.getListLocator(input.authType); - await expect(grantList.locator("tr", { hasText: new RegExp(`10(\\.0+?)\\s*${input.denom}`) })).toBeVisible({ timeout: 10_000 }); - }); - - test("can revoke spending", async ({ page, context }) => { - const { authorizationsPage, anotherWalletAddress: address, extension } = await setup({ page, context, ...input }); - - await Promise.all([extension.acceptTransaction("high"), authorizationsPage.revokeSpending(input.authType, address)]); - await extension.waitForTransaction("success"); - - const shortenedAddress = shortenAddress(address); - const grantList = authorizationsPage.getListLocator(input.authType); - await expect(grantList.locator("tr", { hasText: shortenedAddress })).not.toBeVisible({ timeout: 10_000 }); - }); -} - -async function setup({ page, context, authType }: { page: Page; context: BrowserContext; authType: AuthorizationType }) { - const extension = new WebWallet(context, page); - const anotherWallet = await DirectSecp256k1HdWallet.generate(12, { prefix: "akash" }); - const anotherWalletAccounts = await anotherWallet.getAccounts(); - const anotherWalletAddress = anotherWalletAccounts[0].address; - - await topUpWallet(anotherWalletAddress); - - const authorizationsPage = new AuthorizationsPage(context, page); - await authorizationsPage.goto(); - - await Promise.all([extension.acceptTransaction("high"), authorizationsPage.authorizeSpending(authType, anotherWalletAccounts[0].address)]); - const notification = await extension.getTransaction("success"); - await notification.close(); - - return { - authorizationsPage, - anotherWalletAddress, - extension - }; -} diff --git a/apps/deploy-web/tests/ui/build-template.spec.ts b/apps/deploy-web/tests/ui/build-template.spec.ts deleted file mode 100644 index 15d3c20331..0000000000 --- a/apps/deploy-web/tests/ui/build-template.spec.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { expect, test } from "./fixture/base-test"; -import { BuildTemplatePage } from "./pages/BuildTemplatePage"; - -test("ssh function absence", async ({ page, context }) => { - const sdlBuilderPage = new BuildTemplatePage(context, page); - await sdlBuilderPage.gotoInteractive(); - - await expect(page.getByRole("button", { name: /generate new key/i })).not.toBeVisible(); - await expect(page.getByRole("checkbox", { name: /expose ssh/i })).not.toBeVisible(); - await expect(page.getByRole("combobox", { name: /os image/i })).not.toBeVisible(); - await expect(page.getByLabel(/docker image/i)).toBeVisible(); -}); diff --git a/apps/deploy-web/tests/ui/change-wallets.spec.ts b/apps/deploy-web/tests/ui/change-wallets.spec.ts deleted file mode 100644 index 5826b6c882..0000000000 --- a/apps/deploy-web/tests/ui/change-wallets.spec.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { expect } from "@playwright/test"; - -import { test } from "./fixture/context-with-extension"; -import { WebWallet } from "./pages/WebWallet"; - -test.describe("Custodial wallet", () => { - test("switching to another wallet in the extension switches the wallet in Console", async ({ page, context }) => { - const extension = new WebWallet(context, page); - await extension.goto(); - const newWalletAddress = await extension.switchToNewWallet(); - - const container = page.getByLabel("Connected wallet name and balance"); - await container.waitFor({ state: "visible", timeout: 20_000 }); - await container.click({ timeout: 20_000 }); - await page.getByLabel("wallet address").click(); - const clipboardContent = await page.evaluate(() => navigator.clipboard.readText()); - - expect(clipboardContent).toEqual(newWalletAddress); - }); - - test("wallet stays disconnected after disconnecting and reloading", async ({ page, context }) => { - const extension = new WebWallet(context, page); - await extension.goto(); - await extension.disconnectWallet(); - - await expect(page.getByTestId("connect-wallet-btn")).toBeVisible({ timeout: 20_000 }); - }); -}); diff --git a/apps/deploy-web/tests/ui/custom-container-form.spec.ts b/apps/deploy-web/tests/ui/custom-container-form.spec.ts deleted file mode 100644 index 7daf05b838..0000000000 --- a/apps/deploy-web/tests/ui/custom-container-form.spec.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { SSH_VM_IMAGES } from "@src/utils/sdl/data"; -import { expect, test } from "./fixture/base-test"; -import { DeployPage } from "./pages/DeployPage"; -import { HomePage } from "./pages/HomePage"; -import { Sidebar } from "./pages/Sidebar"; - -test("custom container form shows connect wallet prompt", async ({ page, context }) => { - const homePage = new HomePage(page); - const sidebar = new Sidebar(page); - const deployPage = new DeployPage(context, page, { walletType: "extension" }); - - await homePage.goto(); - await sidebar.openDeploy(); - await deployPage.selectTemplate("Run Custom Container"); - await deployPage.fillImageName(SSH_VM_IMAGES["Ubuntu 24.04"]); - await page.getByRole("button", { name: /create deployment/i }).click(); - - await expect(page.getByRole("button", { name: /connect wallet/i }).first()).toBeVisible(); -}); diff --git a/apps/deploy-web/tests/ui/deploy-from-a-template.spec.ts b/apps/deploy-web/tests/ui/deploy-from-a-template.spec.ts deleted file mode 100644 index d9eeada342..0000000000 --- a/apps/deploy-web/tests/ui/deploy-from-a-template.spec.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { expect, test } from "./fixture/base-test"; -import { DeployPage } from "./pages/DeployPage"; - -test("user can choose a template on deployment page", async ({ page, context }) => { - test.setTimeout(3 * 60 * 1000); - - const deploymentPage = new DeployPage(context, page); - await deploymentPage.goto(); - - const templateList = page.getByLabel("Template list"); - - await expect(templateList).toBeVisible(); - - const templateLinks = templateList.getByRole("link"); - await expect(templateLinks.nth(0)).toBeVisible({ timeout: 15_000 }); - - const templateCount = await templateLinks.count(); - - for (let i = 0; i < templateCount; i++) { - const link = templateLinks.nth(i); - const linkText = (await link.textContent())?.split("\n")[0] ?? `template ${i}`; - - await test.step(`verify template "${linkText}"`, async () => { - const href = await link.getAttribute("href"); - const newPage = await context.newPage(); - await newPage.goto(new URL(href!, page.url()).href); - - const templateName = await newPage.getByLabel(/Name your deployment/i).inputValue({ timeout: 15_000 }); - await expect(link).toContainText(templateName); - await newPage.close(); - }); - } -}); diff --git a/apps/deploy-web/tests/ui/deploy-linux.spec.ts b/apps/deploy-web/tests/ui/deploy-linux.spec.ts deleted file mode 100644 index 1730239899..0000000000 --- a/apps/deploy-web/tests/ui/deploy-linux.spec.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { expect, test } from "./fixture/base-test"; -import { HomePage } from "./pages/HomePage"; -import { Sidebar } from "./pages/Sidebar"; - -import { PlainLinuxPage } from "@tests/ui/pages/PlainLinuxPage"; - -test("ssh keys generation", async ({ page, context }) => { - const homePage = new HomePage(page); - const sidebar = new Sidebar(page); - const deployPage = new PlainLinuxPage(context, page); - - await homePage.goto(); - await sidebar.openDeploy(); - - await deployPage.selectTemplate("Launch Container-VM"); - await deployPage.selectDistro("Ubuntu 24.04"); - - const { input, download } = await deployPage.generateSSHKeys(); - - expect(download.suggestedFilename()).toBe("keypair.zip"); - await expect(input).toHaveValue(/ssh-/); - - await page.getByRole("button", { name: /create deployment/i }).click(); - - await expect(page.getByRole("button", { name: /connect wallet/i }).first()).toBeVisible(); -}); diff --git a/apps/deploy-web/tests/ui/deploy-self-custody-hello-world.spec.ts b/apps/deploy-web/tests/ui/deploy-self-custody-hello-world.spec.ts deleted file mode 100644 index ef1ee90e54..0000000000 --- a/apps/deploy-web/tests/ui/deploy-self-custody-hello-world.spec.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { test } from "./fixture/context-with-extension"; -import { DeployPage } from "./pages/DeployPage"; -import { HomePage } from "./pages/HomePage"; -import { Sidebar } from "./pages/Sidebar"; - -test("deploy hello world via self custody wallet", async ({ context, page }) => { - test.setTimeout(5 * 60 * 1000); - - const homePage = new HomePage(page); - const sidebar = new Sidebar(page); - const deployPage = new DeployPage(context, page, { walletType: "extension", feeType: "medium" }); - - await test.step("navigate to deploy page and select template", async () => { - await homePage.goto(); - await sidebar.openDeploy(); - await deployPage.selectTemplate("Hello World"); - }); - - await test.step("create deployment", async () => { - await deployPage.createDeployment(); - }); - - await test.step("create lease", async () => { - await deployPage.createLease(); - }); - - await test.step("validate lease and close", async () => { - await deployPage.validateLeaseAndClose(); - }); -}); diff --git a/apps/deploy-web/tests/ui/fixture/context-with-extension.ts b/apps/deploy-web/tests/ui/fixture/context-with-extension.ts deleted file mode 100644 index ac8dae3590..0000000000 --- a/apps/deploy-web/tests/ui/fixture/context-with-extension.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { DirectSecp256k1HdWallet } from "@cosmjs/proto-signing"; -import type { BrowserContext, Page } from "@playwright/test"; - -import { selectChainNetwork } from "../actions/selectChainNetwork"; -import { injectWebWallet } from "./web-wallet/injectWebWallet"; -import { injectUIConfig, test as baseTest } from "./base-test"; -import { testEnvConfig } from "./test-env.config"; -import { connectWalletViaLeap, topUpWallet } from "./wallet-setup"; - -// @see https://playwright.dev/docs/chrome-extensions -export const test = baseTest.extend({ - page: [ - async ({ context }, use) => { - const page = await createPage(context); - - try { - await use(page); - } finally { - await page.close(); - } - }, - { scope: "test", timeout: 5 * 60 * 1000 } - ] -}); - -export const expect = test.expect; - -export interface ExtensionContext { - context: BrowserContext; - page: Page; -} - -export async function createPage(context: BrowserContext): Promise { - const w = await DirectSecp256k1HdWallet.fromMnemonic(testEnvConfig.TEST_WALLET_MNEMONIC, { prefix: "akash" }); - const accounts = await w.getAccounts(); - await topUpWallet(accounts[0].address); - - const page = await context.newPage(); - await injectUIConfig(page); - await injectWebWallet(page, testEnvConfig.TEST_WALLET_MNEMONIC); - - if (testEnvConfig.NETWORK_ID !== "mainnet") { - await page.goto(testEnvConfig.BASE_URL); - await connectWalletViaLeap(context, page); - await selectChainNetwork(page, testEnvConfig.NETWORK_ID); - } - - await page.goto(testEnvConfig.BASE_URL); - await connectWalletViaLeap(context, page); - return page; -} diff --git a/apps/deploy-web/tests/ui/fixture/test-env.config.ts b/apps/deploy-web/tests/ui/fixture/test-env.config.ts index 02f09cfc03..3c937d18dd 100644 --- a/apps/deploy-web/tests/ui/fixture/test-env.config.ts +++ b/apps/deploy-web/tests/ui/fixture/test-env.config.ts @@ -7,7 +7,6 @@ export const testEnvSchema = z.object({ .string() .default("http://localhost:3000") .transform(url => url.replace(/\/+$/, "")), - TEST_WALLET_MNEMONIC: z.string(), NETWORK_ID: z.enum(["mainnet", "sandbox", "testnet"]).default("sandbox"), USER_DATA_DIR: z.string().default(path.join(tmpdir(), "akash-console-web-ui-tests", crypto.randomUUID())), E2E_TESTING_CLIENT_TOKEN: z.string({ @@ -24,7 +23,7 @@ export const testEnvSchema = z.object({ export const testEnvConfig = testEnvSchema.parse({ BASE_URL: process.env.BASE_URL, - TEST_WALLET_MNEMONIC: process.env.TEST_WALLET_MNEMONIC, + NETWORK_ID: process.env.NETWORK_ID, USER_DATA_DIR: process.env.USER_DATA_DIR, E2E_TESTING_CLIENT_TOKEN: process.env.E2E_TESTING_CLIENT_TOKEN, AUTH0_M2M_DOMAIN: process.env.AUTH0_M2M_DOMAIN, diff --git a/apps/deploy-web/tests/ui/fixture/testing-helpers.ts b/apps/deploy-web/tests/ui/fixture/testing-helpers.ts deleted file mode 100644 index b69b048a0b..0000000000 --- a/apps/deploy-web/tests/ui/fixture/testing-helpers.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { type Page } from "@playwright/test"; - -export const clickWalletSelectorDropdown = async (page: Page) => { - return await page.getByLabel("wallet dropdown").click(); -}; - -export const clickConnectWalletButton = async (page: Page) => { - await page.getByRole("button", { name: /connect button/i }).click({ timeout: 20_000 }); -}; - -export const clickCopyAddressButton = async (page: Page) => { - await page.getByRole("button", { name: /akash\.\.\.[a-z0-9]{5}/ }).click(); - - const clipboardContents = await page.evaluate(async () => { - return await navigator.clipboard.readText(); - }); - - return clipboardContents; -}; diff --git a/apps/deploy-web/tests/ui/fixture/wallet-setup.ts b/apps/deploy-web/tests/ui/fixture/wallet-setup.ts deleted file mode 100644 index f91f23891a..0000000000 --- a/apps/deploy-web/tests/ui/fixture/wallet-setup.ts +++ /dev/null @@ -1,255 +0,0 @@ -import type { NetworkId } from "@akashnetwork/chain-sdk"; -import { netConfig } from "@akashnetwork/net"; -import { DirectSecp256k1HdWallet } from "@cosmjs/proto-signing"; -import type { BrowserContext, Page } from "@playwright/test"; -import fs from "fs"; -import path from "path"; -import { setTimeout as wait } from "timers/promises"; - -import { isWalletConnected } from "../uiState/isWalletConnected"; -import { testEnvConfig } from "./test-env.config"; -import { clickWalletSelectorDropdown } from "./testing-helpers"; - -const WALLET_PASSWORD = "12345678"; - -export async function getExtensionPage(context: BrowserContext): Promise { - const extensionId = await getExtensionId(context); - const extUrl = `chrome-extension://${extensionId}`; - const extPage = context.pages().find(page => page.url().startsWith(extUrl)); - - if (!extPage) { - const page = await context.newPage(); - await page.goto(`${extUrl}/index.html`, { waitUntil: "domcontentloaded" }); - return page; - } - - return extPage; -} - -let extensionId: string | undefined; -export async function getExtensionId(context: BrowserContext): Promise { - if (extensionId) return extensionId; - - let [background] = context.serviceWorkers(); - if (!background) { - background = await context.waitForEvent("serviceworker"); - } - - extensionId = background.url().split("/")[2]; - return extensionId; -} - -export async function setupWallet(page: Page) { - const address = await restoreExtensionStorage(page, testEnvConfig.NETWORK_ID); - await topUpWallet(address); - await unlockWallet(page); -} - -export async function createWallet(context: BrowserContext): Promise<{ - extPage: Page; - address: string; -}> { - const extPage = await getExtensionPage(context); - await extPage.waitForLoadState("load"); - - await clickWalletSelectorDropdown(extPage); - await extPage.getByRole("button", { name: /import wallet/i }).click(); - await extPage.getByRole("button", { name: /recovery phrase/i }).click(); - const tmpWallet = await DirectSecp256k1HdWallet.generate(12, { prefix: "akash" }); - await fillInMnemonic(extPage, tmpWallet.mnemonic); - await extPage.getByRole("button", { name: /import wallet/i }).click(); - - const accounts = await tmpWallet.getAccounts(); - - return { - extPage, - address: accounts[0].address - }; -} - -export async function connectWalletViaLeap(context: BrowserContext, page: Page) { - if (!(await isWalletConnected(page))) { - await page.getByRole("button", { name: /connect wallet/i }).click({ timeout: 30_000 }); - const popupPagePromise = context.waitForEvent("page").catch(() => null); - - await page.getByRole("button", { name: "Leap Leap" }).click(); - const popupPage = await Promise.race([popupPagePromise, isWalletConnected(page).then(() => null)]); - - if (popupPage) { - await connectOrUnlockWallet(popupPage); - await isWalletConnected(page); - } - } -} - -async function connectOrUnlockWallet(popupPage: Page) { - const buttonLocator = popupPage - .getByRole("button", { name: /Unlock wallet/i }) - .or(popupPage.getByRole("button", { name: /connect button in approve connection flow/i })) - .or(popupPage.getByRole("button", { name: /Approve/i })); - const buttonText = (await buttonLocator.textContent())?.trim(); - if (buttonText === "Unlock wallet") { - await unlockWallet(popupPage); - } else if (buttonText === "Connect" || buttonText === "Approve") { - await buttonLocator.click(); - } else { - throw new Error(`Unexpected state in wallet popup: ${buttonText}`); - } -} - -export async function awaitWalletAndApprove(context: BrowserContext, page: Page) { - const popupPage = await Promise.race([context.waitForEvent("page", { timeout: 5_000 }), getExtensionPage(context)]); - await approveWalletOperation(popupPage); - await isWalletConnected(page); -} - -export async function approveWalletOperation(popupPage: Page | null) { - if (!popupPage) return; - const buttonLocator = popupPage.locator("button", { hasText: /^\s*(Approve|Unlock wallet|Connect)\s*$/i }); - await buttonLocator.waitFor({ state: "visible", timeout: 5_000 }); - - const buttonText = await buttonLocator.textContent(); - - switch (buttonText?.trim()) { - case "Approve": { - // increase gas limit - await popupPage.getByText(/show additional settings/i).click(); - const gasInput = popupPage - .getByText("Enter gas limit manually") - .locator("xpath=..") // get parent - .locator("input"); - - const value = Number(await gasInput.inputValue()); - await gasInput.fill(Math.ceil(1.5 * value).toString()); - await buttonLocator.click(); - break; - } - case "Unlock wallet": - await unlockWallet(popupPage); - await popupPage - .getByRole("button", { name: /connect button in approve connection flow/i }) - .or(popupPage.getByLabel("wallet dropdown")) - .click(); - break; - case "Connect": - await buttonLocator.click(); - break; - default: - throw new Error("Unexpected state in wallet popup"); - } -} - -export async function unlockWallet(page: Page) { - await page.waitForEvent("load", { timeout: 2_000 }).catch(() => {}); - await page.locator("input").fill(WALLET_PASSWORD); - await page.getByRole("button", { name: /unlock wallet/i }).click(); -} - -export async function importWalletToLeap(page: Page, mnemonic: string) { - await page.getByText(/import an existing wallet/i).click(); - await page.getByText(/recovery phrase/i).click(); - await fillInMnemonic(page, mnemonic); - - await page.getByRole("button", { name: /Continue/i }).click(); - await page.waitForTimeout(2000); - await page.getByRole("checkbox", { name: "Wallet 1" }).setChecked(true); - await page.getByRole("button", { name: /Proceed/i }).click(); - - // Set password - await page.getByPlaceholder("Enter password").fill(WALLET_PASSWORD); - await page.getByPlaceholder("Confirm password").fill(WALLET_PASSWORD); - await page.locator("button", { hasText: /Set Password/i }).click(); - - await page.waitForLoadState("domcontentloaded"); - - return await DirectSecp256k1HdWallet.fromMnemonic(mnemonic, { - prefix: "akash" - }); -} - -async function fillInMnemonic(page: Page, mnemonic: string) { - const mnemonicArray = mnemonic.trim().split(" "); - - await page.locator('input[type="text"]:first-of-type').first().focus(); - - for (const word of mnemonicArray) { - await page.locator("input:focus").fill(word); - await page.keyboard.press("Tab"); - } -} - -export async function topUpWallet(address: string, attempt = 0) { - try { - const balance = await getBalance(address); - - if (balance > 50 * 1_000_000) { - // 50 AKT should be enough - return; - } - - let faucetUrl = netConfig.getFaucetUrl(testEnvConfig.NETWORK_ID); - if (!faucetUrl) { - console.error(`Faucet URL is not set for this network: ${testEnvConfig.NETWORK_ID}. Cannot auto top up wallet`); - return; - } - - if (faucetUrl.endsWith("/")) { - faucetUrl = faucetUrl.slice(0, -1); - } - - const response = await fetch(`${faucetUrl}/faucet`, { - method: "POST", - headers: { - "Content-Type": "application/x-www-form-urlencoded" - }, - body: `address=${encodeURIComponent(address)}` - }); - if (response.status >= 300) { - const error = await response.text(); - console.error(`Unexpected faucet response status: ${response.status}`); - console.error(error); - - if (error.includes("account sequence mismatch") && attempt < 10) { - console.log("retrying top up attempt...", attempt + 1); - await wait(2000); - await topUpWallet(address, attempt + 1); - return; - } - } - } catch (error) { - console.error("Unable to top up wallet"); - console.error(error); - } -} - -async function getBalance(address: string) { - const response = await fetch(`${netConfig.getBaseAPIUrl(testEnvConfig.NETWORK_ID)}/cosmos/bank/v1beta1/balances/${address}`); - const data = await response.json(); - if (!response.ok) return 0; - return data.balances.find((balance: Record) => balance.denom === "uakt")?.amount || 0; -} - -/** - * To get the extension storage, follow these steps: - * 1. Open Chrome with Leap extension installed - * 2. Open DevTools (F12) on Leap extension page - * 3. Run this in the script: - * ```js - * chrome.storage.local.get(null, (data) => { - * const json = JSON.stringify(data, null, 2); - * const blob = new Blob([json], {type: 'application/json'}); - * const url = URL.createObjectURL(blob); - * const a = document.createElement('a'); - * a.href = url; - * a.download = 'leapExtensionLocalStorage.json'; - * a.click(); - * }); - * ``` - * - * @see https://github.com/microsoft/playwright/issues/14949 - */ -export async function restoreExtensionStorage(page: Page, networkId: NetworkId): Promise { - const extensionStorage = JSON.parse(fs.readFileSync(path.join(__dirname, `leapExtensionLocalStorage.${networkId}.json`), "utf8")); - await page.evaluate(data => chrome.storage.local.set(data), extensionStorage); - return extensionStorage["active-wallet"].addresses.akash; -} diff --git a/apps/deploy-web/tests/ui/fixture/web-wallet/CosmjsWebWallet.ts b/apps/deploy-web/tests/ui/fixture/web-wallet/CosmjsWebWallet.ts deleted file mode 100644 index 24f43c78b1..0000000000 --- a/apps/deploy-web/tests/ui/fixture/web-wallet/CosmjsWebWallet.ts +++ /dev/null @@ -1,199 +0,0 @@ -import type { SignDoc } from "@akashnetwork/chain-sdk/private-types/cosmos.v1beta1"; -import { AuthInfo } from "@akashnetwork/chain-sdk/private-types/cosmos.v1beta1"; -import type { AminoSignResponse, StdSignature, StdSignDoc } from "@cosmjs/amino"; -import { Secp256k1HdWallet, serializeSignDoc } from "@cosmjs/amino"; -import { Secp256k1, Secp256k1Signature, sha256 } from "@cosmjs/crypto"; -import { fromBase64, fromBech32 } from "@cosmjs/encoding"; -import type { AccountData, DirectSignResponse } from "@cosmjs/proto-signing"; -import { DirectSecp256k1HdWallet } from "@cosmjs/proto-signing"; -import { SigningStargateClient } from "@cosmjs/stargate"; - -export type FeeType = "low" | "medium" | "high"; - -const GAS_MULTIPLIERS: Record = { - low: 1, - medium: 1.3, - high: 1.6 -}; - -export class CosmjsWebWallet { - #nextFeeType: FeeType = "low"; - #currentMnemonic = ""; - #wallets: Record = {}; - #suggestedChains: Record = {}; - #stargateClients: Record = {}; - - setFeeType(feeType: FeeType): void { - this.#nextFeeType = feeType; - } - - async switchWallet(mnemonic: string): Promise { - this.#currentMnemonic = mnemonic; - const chainIds = Object.keys(this.#suggestedChains); - for (const chainId of chainIds) { - delete this.#wallets[chainId]; - this.#stargateClients[chainId]?.disconnect(); - delete this.#stargateClients[chainId]; - } - for (const chainId of chainIds) { - const chainInfo = this.#suggestedChains[chainId]; - const prefix = chainInfo.bech32Config?.bech32PrefixAccAddr || "cosmos"; - await this.#getOrCreateWallet(mnemonic, chainId, prefix); - } - } - - async suggestChain(chainInfo: ChainInfo): Promise { - this.#suggestedChains[chainInfo.chainId] = chainInfo; - const prefix = chainInfo.bech32Config?.bech32PrefixAccAddr || "cosmos"; - await this.#getOrCreateWallet(this.#currentMnemonic, chainInfo.chainId, prefix); - } - - async getKey(chainId: string): Promise<{ - name: string; - algo: string; - pubKey: Uint8Array; - address: Uint8Array; - bech32Address: string; - ethereumHexAddress: string; - isNanoLedger: boolean; - isKeystone: boolean; - }> { - const w = this.#getWallet(chainId); - return { - name: "e2e-test-wallet", - algo: w.account.algo, - pubKey: w.account.pubkey, - address: fromBech32(w.account.address).data, - bech32Address: w.account.address, - ethereumHexAddress: "", - isNanoLedger: false, - isKeystone: false - }; - } - - async getAccounts(chainId: string): Promise { - const w = this.#getWallet(chainId); - return [{ address: w.account.address, algo: w.account.algo, pubkey: w.account.pubkey }]; - } - - async signDirect(chainId: string, signer: string, serializedDoc: SignDoc): Promise { - const w = this.#getWallet(chainId); - let { authInfoBytes } = serializedDoc; - const multiplier = GAS_MULTIPLIERS[this.#nextFeeType]; - if (multiplier !== 1) { - let authInfo = AuthInfo.decode(authInfoBytes); - if (authInfo.fee) { - authInfo = AuthInfo.fromPartial({ - ...authInfo, - fee: { - ...authInfo.fee, - gasLimit: BigInt(Math.ceil(Number(authInfo.fee.gasLimit) * multiplier)) - } - }); - } - authInfoBytes = AuthInfo.encode(authInfo).finish(); - this.#nextFeeType = "low"; - } - const signDoc: SignDoc = { - bodyBytes: serializedDoc.bodyBytes, - authInfoBytes, - chainId: serializedDoc.chainId, - accountNumber: serializedDoc.accountNumber - }; - const result = await w.direct.signDirect(signer, signDoc as any); - return result; - } - - async signAmino(chainId: string, signer: string, signDoc: StdSignDoc): Promise { - const w = this.#getWallet(chainId); - const result = await w.amino.signAmino(signer, signDoc); - return result; - } - - async signArbitrary(chainId: string, signer: string, data: string): Promise { - const w = this.#getWallet(chainId); - const b64Data = btoa(data); - const signDoc: StdSignDoc = { - chain_id: "", - account_number: "0", - sequence: "0", - fee: { gas: "0", amount: [] }, - msgs: [{ type: "sign/MsgSignData", value: { signer, data: b64Data } }], - memo: "" - }; - const result = await w.amino.signAmino(signer, signDoc); - return { pub_key: result.signature.pub_key, signature: result.signature.signature }; - } - - async verifyArbitrary(_: unknown, signer: string, data: string, signature: StdSignature): Promise { - const b64Data = btoa(data); - const signDoc: StdSignDoc = { - chain_id: "", - account_number: "0", - sequence: "0", - fee: { gas: "0", amount: [] }, - msgs: [{ type: "sign/MsgSignData", value: { signer, data: b64Data } }], - memo: "" - }; - const serialized = serializeSignDoc(signDoc); - const hash = sha256(serialized); - const pubkeyBytes = fromBase64(signature.pub_key.value); - const sigBytes = fromBase64(signature.signature); - const sig = Secp256k1Signature.fromFixedLength(sigBytes); - return await Secp256k1.verifySignature(sig, hash, pubkeyBytes); - } - - async sendTx(chainId: string, tx: Uint8Array): Promise { - const w = this.#getWallet(chainId); - const chainInfo = this.#suggestedChains[chainId]; - if (!chainInfo?.rpc) throw new Error(`No RPC for chain ${chainId}.`); - if (!this.#stargateClients[chainId]) { - this.#stargateClients[chainId] = await SigningStargateClient.connectWithSigner(chainInfo.rpc, w.direct); - } - const txBytes = tx instanceof Uint8Array ? tx : fromBase64(tx as unknown as string); - const result = await this.#stargateClients[chainId].broadcastTx(txBytes); - if (result.code !== 0) throw new Error(`Broadcast failed: code ${result.code}`); - const hashBytes = new Uint8Array(result.transactionHash.length / 2); - for (let i = 0; i < hashBytes.length; i++) { - hashBytes[i] = parseInt(result.transactionHash.substring(i * 2, i * 2 + 2), 16); - } - return hashBytes; - } - - #getWallet(chainId: string): WalletEntry { - const w = this.#wallets[chainId]; - if (!w) throw new Error(`Chain ${chainId} not registered. Call experimentalSuggestChain first.`); - return w; - } - - async #getOrCreateWallet(mnemonic: string, chainId: string, prefix: string): Promise { - if (this.#wallets[chainId]) return this.#wallets[chainId]; - - const direct = await DirectSecp256k1HdWallet.fromMnemonic(mnemonic, { prefix }); - const amino = await Secp256k1HdWallet.fromMnemonic(mnemonic, { prefix }); - const [account] = await direct.getAccounts(); - - this.#wallets[chainId] = { direct, amino, account }; - return this.#wallets[chainId]; - } -} - -interface WalletEntry { - direct: DirectSecp256k1HdWallet; - amino: Secp256k1HdWallet; - account: AccountData; -} - -interface ChainInfo { - chainId: string; - rpc: string; - rest: string; - bech32Config?: { - bech32PrefixAccAddr: string; - bech32PrefixAccPub: string; - bech32PrefixValAddr: string; - bech32PrefixValPub: string; - bech32PrefixConsAddr: string; - bech32PrefixConsPub: string; - }; -} diff --git a/apps/deploy-web/tests/ui/fixture/web-wallet/initLeapWebWalletMock.ts b/apps/deploy-web/tests/ui/fixture/web-wallet/initLeapWebWalletMock.ts deleted file mode 100644 index 9be318a78c..0000000000 --- a/apps/deploy-web/tests/ui/fixture/web-wallet/initLeapWebWalletMock.ts +++ /dev/null @@ -1,65 +0,0 @@ -export function initLeapWebWalletMock(options: { rpcHandlerName: string }) { - async function rpc(method: string, ...args: unknown[]): Promise { - const result: string = await (window as any)[options.rpcHandlerName](method, args); - return result; - } - - function makeSigner(chainId: string) { - return { - async getAccounts() { - return rpc("getAccounts", chainId); - }, - async signDirect(signerAddress: string, signDoc: any) { - return rpc("signDirect", chainId, signerAddress, signDoc); - }, - async signAmino(signerAddress: string, signDoc: any) { - return rpc("signAmino", chainId, signerAddress, signDoc); - } - }; - } - - const leap = { - async enable() {}, - async disconnect() {}, - async experimentalSuggestChain(chainInfo: any) { - await rpc("suggestChain", chainInfo); - }, - async getKey(chainId: string) { - return rpc("getKey", chainId); - }, - getOfflineSigner(chainId: string) { - return makeSigner(chainId); - }, - async getOfflineSignerAuto(chainId: string) { - return makeSigner(chainId); - }, - getOfflineSignerOnlyAmino(chainId: string) { - const s = makeSigner(chainId); - return { getAccounts: s.getAccounts, signAmino: s.signAmino }; - }, - async signDirect(chainId: string, signer: string, signDoc: any) { - return rpc("signDirect", chainId, signer, signDoc); - }, - async signAmino(chainId: string, signer: string, signDoc: any) { - return rpc("signAmino", chainId, signer, signDoc); - }, - async signArbitrary(chainId: string, signer: string, data: string) { - return rpc("signArbitrary", chainId, signer, data); - }, - async verifyArbitrary(chainId: string, signer: string, data: string, signature: any) { - return rpc("verifyArbitrary", chainId, signer, data, signature); - }, - async sendTx(chainId: string, tx: Uint8Array, mode: string) { - return rpc("sendTx", chainId, tx, mode); - }, - defaultOptions: { - sign: { - preferNoSetFee: false, - preferNoSetMemo: true, - disableBalanceCheck: true - } - } - }; - - Object.defineProperty(window, "leap", { value: leap, writable: false, configurable: false }); -} diff --git a/apps/deploy-web/tests/ui/fixture/web-wallet/injectWebWallet.ts b/apps/deploy-web/tests/ui/fixture/web-wallet/injectWebWallet.ts deleted file mode 100644 index bc3eae4876..0000000000 --- a/apps/deploy-web/tests/ui/fixture/web-wallet/injectWebWallet.ts +++ /dev/null @@ -1,42 +0,0 @@ -import type { Page } from "@playwright/test"; - -import type { FeeType } from "./CosmjsWebWallet"; -import { CosmjsWebWallet } from "./CosmjsWebWallet"; -import { initLeapWebWalletMock } from "./initLeapWebWalletMock"; - -export type { FeeType } from "./CosmjsWebWallet"; - -const WALLETS = new Map(); -const getWallet = (page: Page): CosmjsWebWallet => { - let wallet = WALLETS.get(page); - if (!wallet) { - wallet = new CosmjsWebWallet(); - WALLETS.set(page, wallet); - page.once("close", () => WALLETS.delete(page)); - } - return wallet; -}; - -export function setFeeType(page: Page, feeType: FeeType) { - getWallet(page).setFeeType(feeType); -} - -export async function switchWebWallet(page: Page, mnemonic: string) { - await getWallet(page).switchWallet(mnemonic); - await page.evaluate(() => window.dispatchEvent(new Event("leap_keystorechange"))); -} - -const RPC_HANDLER_NAME = "__akashCosmjsWalletRpc"; - -export async function injectWebWallet(page: Page, mnemonic: string) { - const wallet = getWallet(page); - await wallet.switchWallet(mnemonic); - await page.exposeFunction(RPC_HANDLER_NAME, async (method: keyof typeof wallet, args: unknown[]) => { - if (!wallet[method]) throw new Error(`Unknown wallet RPC method: ${method}`); - const result = await (wallet[method] as (...args: unknown[]) => Promise)(...args); - return result; - }); - await page.addInitScript(initLeapWebWalletMock, { - rpcHandlerName: RPC_HANDLER_NAME - }); -} diff --git a/apps/deploy-web/tests/ui/managed-wallet-alerts.spec.ts b/apps/deploy-web/tests/ui/managed-wallet-alerts.spec.ts index 505872b159..03f6b148c6 100644 --- a/apps/deploy-web/tests/ui/managed-wallet-alerts.spec.ts +++ b/apps/deploy-web/tests/ui/managed-wallet-alerts.spec.ts @@ -18,7 +18,7 @@ test.describe("Managed wallet alerts", () => { const alertsPage = new AlertsPage(page); const alertsForm = new DeploymentAlertsForm(page); const billingPage = new BillingPage(page); - const deployPage = new DeployPage(context, page, { walletType: "api" }); + const deployPage = new DeployPage(context, page); await test.step("login", async () => { await homePage.goto(); diff --git a/apps/deploy-web/tests/ui/managed-wallet-deployment.spec.ts b/apps/deploy-web/tests/ui/managed-wallet-deployment.spec.ts index f7a6ac16ea..a375c0bff2 100644 --- a/apps/deploy-web/tests/ui/managed-wallet-deployment.spec.ts +++ b/apps/deploy-web/tests/ui/managed-wallet-deployment.spec.ts @@ -14,7 +14,7 @@ test.describe("Managed wallet deployment", () => { const authPage = new AuthPage(page); const billingPage = new BillingPage(page); const sidebar = new Sidebar(page); - const deployPage = new DeployPage(context, page, { walletType: "api" }); + const deployPage = new DeployPage(context, page); await test.step("login", async () => { await homePage.goto(); diff --git a/apps/deploy-web/tests/ui/pages/AuthorizationsPage.tsx b/apps/deploy-web/tests/ui/pages/AuthorizationsPage.tsx deleted file mode 100644 index bec1a4db0c..0000000000 --- a/apps/deploy-web/tests/ui/pages/AuthorizationsPage.tsx +++ /dev/null @@ -1,89 +0,0 @@ -import type { BrowserContext as Context, Locator, Page } from "@playwright/test"; - -import { testEnvConfig } from "../fixture/test-env.config"; -import { WebWallet } from "./WebWallet"; - -export type AuthorizationType = "deployment" | "tx_fee"; - -const AUTHORIZE_BUTTON_LABELS = { - deployment: "Authorize Spend", - tx_fee: "Authorize Fee Spend" -}; -const AUTHORIZATION_LIST_LABELS = { - deployment: { - title: /Deployment Authorization/i, - emptyTitle: /No authorizations given/i - }, - tx_fee: { - title: /Tx Fee Authorization/i, - emptyTitle: /No allowances issued/i - } -}; - -export class AuthorizationsPage { - constructor( - readonly context: Context, - readonly page: Page - ) {} - - async goto(url = `${testEnvConfig.BASE_URL}/settings/authorizations`) { - await this.page.goto(url); - } - - async clickGrantButton() { - return await this.page.getByRole("button", { name: /^\s*Grant\s*$/ }).click(); - } - - async authorizeSpending(type: AuthorizationType, address: string) { - await this.page.getByRole("button", { name: AUTHORIZE_BUTTON_LABELS[type] }).click(); - await this.page.getByLabel("Spending Limit").fill("5"); - await this.page.getByLabel("Grantee Address").fill(address); - await this.clickGrantButton(); - } - - async editSpending(type: AuthorizationType, address: string) { - const shortenedAddress = shortenAddress(address); - await this.getListLocator(type).locator("tr", { hasText: shortenedAddress }).getByLabel("Edit Authorization").click(); - await this.page.getByLabel("Spending Limit").fill("10"); - await this.clickGrantButton(); - } - - async revokeSpending(type: AuthorizationType, address: string) { - const shortenedAddress = shortenAddress(address); - await this.page.getByLabel(AUTHORIZATION_LIST_LABELS[type].title).locator("tr", { hasText: shortenedAddress }).getByLabel("Revoke Authorization").click(); - await this.page.getByRole("button", { name: "Confirm" }).click(); - } - - async revokeAll(type: AuthorizationType): Promise { - const extension = new WebWallet(this.context, this.page); - const selectors = AUTHORIZATION_LIST_LABELS[type]; - const hasGrants = await Promise.race([ - this.getListLocator(type) - .getByRole("button", { name: /revoke all/i }) - .click() - .then( - () => true, - () => false - ), - this.getListLocator(type) - .getByText(selectors.emptyTitle) - .waitFor({ state: "visible" }) - .then( - () => false, - () => true - ) - ]); - if (hasGrants) { - await Promise.all([this.page.getByRole("button", { name: "Confirm" }).click(), extension.acceptTransaction("high")]); - await extension.waitForTransaction("success"); - } - } - - getListLocator(type: AuthorizationType): Locator { - return this.page.getByLabel(AUTHORIZATION_LIST_LABELS[type].title); - } -} - -export function shortenAddress(address: string) { - return `${address.slice(0, 8)}...${address.slice(-5)}`; -} diff --git a/apps/deploy-web/tests/ui/pages/BuildTemplatePage.tsx b/apps/deploy-web/tests/ui/pages/BuildTemplatePage.tsx deleted file mode 100644 index 1c66ea1f73..0000000000 --- a/apps/deploy-web/tests/ui/pages/BuildTemplatePage.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import { testEnvConfig } from "../fixture/test-env.config"; -import { DeployPage } from "./DeployPage"; - -export class BuildTemplatePage extends DeployPage { - async gotoInteractive() { - await this.page.goto(testEnvConfig.BASE_URL); - await this.page - .getByRole("link", { name: /sdl builder/i }) - .first() - .click(); - } - - async addService() { - await this.page.getByRole("button", { name: /add service/i }).click(); - } - - async clickDeploy() { - await this.page.getByRole("button", { name: /^deploy$/i }).click(); - } - - async clickPreview() { - await this.page.getByRole("button", { name: /preview/i }).click(); - } - - getPreviewTextLocator(text: string) { - return this.page.getByText(text).first(); - } - - async closePreview() { - await this.page.getByRole("button", { name: /close/i }).first().click(); - } - - getDeployButton() { - return this.page.getByRole("button", { name: /^deploy$/i }); - } - - getPreviewButton() { - return this.page.getByRole("button", { name: /preview/i }); - } - - getAddServiceButton() { - return this.page.getByRole("button", { name: /add service/i }); - } - - getServiceLocator(serviceName: string) { - const escapedName = serviceName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); - return this.page.getByText(new RegExp(`${escapedName}:`)).first(); - } - - async waitForServiceAdded(serviceName: string, timeout = 10000) { - await this.page.locator(`input[type="text"][value="${serviceName}"]`).first().waitFor({ state: "visible", timeout }); - } -} diff --git a/apps/deploy-web/tests/ui/pages/DeployPage.tsx b/apps/deploy-web/tests/ui/pages/DeployPage.tsx index 14068fc613..3c41c336ef 100644 --- a/apps/deploy-web/tests/ui/pages/DeployPage.tsx +++ b/apps/deploy-web/tests/ui/pages/DeployPage.tsx @@ -2,24 +2,12 @@ import type { BrowserContext as Context, Page } from "@playwright/test"; import { expect } from "@playwright/test"; import { PROVIDERS_WHITELIST, testEnvConfig } from "../fixture/test-env.config"; -import { WebWallet } from "./WebWallet"; - -export type FeeType = "low" | "medium" | "high"; -export type WalletType = "api" | "extension"; -export type SignOptions = { feeType?: FeeType; walletType: WalletType }; export class DeployPage { - protected feeType: FeeType = "low"; - protected walletType: WalletType; - constructor( readonly context: Context, - readonly page: Page, - private readonly options: SignOptions = { walletType: "api" } - ) { - this.feeType = options.feeType ?? this.feeType; - this.walletType = options.walletType; - } + readonly page: Page + ) {} async goto() { await this.page.goto(`${testEnvConfig.BASE_URL}/new-deployment`); @@ -29,20 +17,6 @@ export class DeployPage { await this.page.getByLabel(name).or(this.page.getByRole("link", { name })).first().click(); } - async fillImageName(name: string) { - await this.page.getByLabel(/docker image/i).fill(name); - } - - async generateSSHKeys() { - const downloadPromise = this.page.waitForEvent("download"); - await this.page.getByRole("button", { name: /generate new key/i }).click(); - - return { - download: await downloadPromise, - input: this.page.getByLabel(/ssh public key/i) - }; - } - async openDepositDialog() { await this.page.getByRole("button", { name: /create deployment/i }).click(); const dialog = this.page.getByRole("dialog"); @@ -50,41 +24,25 @@ export class DeployPage { return dialog; } - async createDeployment() { - await this.withTxAccepted(async () => { - await this.page.getByRole("button", { name: /create deployment/i }).click(); - await this.page.getByRole("button", { name: /^continue$/i }).click(); - }); - } - async createLease(providerName?: string) { - await this.withTxAccepted(async () => { - if (providerName) { - await this.page.getByLabel(providerName).click(); + if (providerName) { + await this.page.getByLabel(providerName).click(); + } else { + const providers = PROVIDERS_WHITELIST[testEnvConfig.NETWORK_ID]; + if (!providers.length) { + await this.page.getByRole("radio", { checked: false }).first().click(); } else { - const providers = PROVIDERS_WHITELIST[testEnvConfig.NETWORK_ID]; - if (!providers.length) { - await this.page.getByRole("radio", { checked: false }).first().click(); - } else { - const locator = providers - .slice(1) - .reduce( - (combined, owner) => combined.or(this.page.locator(`[role="radio"][aria-description="${owner}"]`)), - this.page.locator(`[role="radio"][aria-description="${providers[0]}"]`) - ); - await locator.first().click({ timeout: 60_000 }); - } + const locator = providers + .slice(1) + .reduce( + (combined, owner) => combined.or(this.page.locator(`[role="radio"][aria-description="${owner}"]`)), + this.page.locator(`[role="radio"][aria-description="${providers[0]}"]`) + ); + await locator.first().click({ timeout: 60_000 }); } + } - await this.page.getByRole("button", { name: /accept bid/i }).click(); - }); - } - - async validateLeaseAndClose() { - await this.validateLease(); - await this.withTxAccepted(async () => { - await this.closeDeployment(); - }); + await this.page.getByRole("button", { name: /accept bid/i }).click(); } async validateLease() { @@ -102,14 +60,4 @@ export class DeployPage { await this.page.getByRole("button", { name: /deployment actions/i }).click(); await this.page.getByRole("menuitem", { name: /close deployment/i }).click(); } - - private async withTxAccepted(fn: () => Promise) { - await Promise.all([this.walletType === "extension" ? this.signTransaction() : Promise.resolve(), fn()]); - } - - async signTransaction() { - const extension = new WebWallet(this.context, this.page); - await extension.acceptTransaction(this.feeType); - await extension.waitForTransaction("success"); - } } diff --git a/apps/deploy-web/tests/ui/pages/PlainLinuxPage.tsx b/apps/deploy-web/tests/ui/pages/PlainLinuxPage.tsx deleted file mode 100644 index a9c3b17fee..0000000000 --- a/apps/deploy-web/tests/ui/pages/PlainLinuxPage.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import type { SSH_VM_IMAGES } from "@src/utils/sdl/data"; -import { DeployPage } from "./DeployPage"; - -export class PlainLinuxPage extends DeployPage { - async selectDistro(distro: keyof typeof SSH_VM_IMAGES) { - await this.page.getByRole("combobox", { name: /os image/i }).click(); - await this.page.getByRole("option", { name: distro }).click(); - } -} diff --git a/apps/deploy-web/tests/ui/pages/WebWallet.ts b/apps/deploy-web/tests/ui/pages/WebWallet.ts deleted file mode 100644 index a7fa35f54e..0000000000 --- a/apps/deploy-web/tests/ui/pages/WebWallet.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { DirectSecp256k1HdWallet } from "@cosmjs/proto-signing"; -import type { BrowserContext, Page } from "@playwright/test"; - -import { testEnvConfig } from "../fixture/test-env.config"; -import { topUpWallet } from "../fixture/wallet-setup"; -import type { FeeType } from "../fixture/web-wallet/injectWebWallet"; -import { setFeeType, switchWebWallet } from "../fixture/web-wallet/injectWebWallet"; - -export class WebWallet { - constructor( - readonly context: BrowserContext, - readonly page: Page - ) {} - - async goto() {} - - async switchToNewWallet(): Promise { - const tmpWallet = await DirectSecp256k1HdWallet.generate(12, { prefix: "akash" }); - const [account] = await tmpWallet.getAccounts(); - await topUpWallet(account.address); - await switchWebWallet(this.page, tmpWallet.mnemonic); - return account.address; - } - - async switchToTestWallet(): Promise { - await switchWebWallet(this.page, testEnvConfig.TEST_WALLET_MNEMONIC); - } - - async disconnectWallet() { - await this.page.getByLabel("Connected wallet name and balance").click(); - await this.page.getByRole("button", { name: "Disconnect Wallet" }).click(); - await this.page.reload({ waitUntil: "networkidle" }); - } - - acceptTransaction(feeType: FeeType = "low") { - setFeeType(this.page, feeType); - } - - async waitForTransaction(type: "success" | "error"): Promise { - await this.getTransaction(type); - } - - async getTransaction(type: "success" | "error"): Promise<{ - type: "success" | "error"; - text: string; - close: () => Promise; - }> { - const MESSAGES = { - success: /Transaction success/i, - error: /Transaction has failed/i - }; - const resultLocator = this.page - .locator('[role="alert"]', { hasText: MESSAGES.success }) - .or(this.page.locator('[role="alert"]', { hasText: MESSAGES.error })); - const text = await resultLocator.textContent({ timeout: 25_000 }); - - if (!text || !MESSAGES[type].test(text)) { - throw new Error(`Expected transaction "${type}" but got "${text}"`); - } - - return { - type, - text, - close: () => resultLocator.getByRole("button").click() - }; - } -} diff --git a/apps/deploy-web/tests/ui/sdl-builder-deployment.spec.ts b/apps/deploy-web/tests/ui/sdl-builder-deployment.spec.ts deleted file mode 100644 index 909f821aaf..0000000000 --- a/apps/deploy-web/tests/ui/sdl-builder-deployment.spec.ts +++ /dev/null @@ -1,107 +0,0 @@ -import type { BrowserContext, Page } from "@playwright/test"; - -import { expect, test } from "./fixture/base-test"; -import { BuildTemplatePage } from "./pages/BuildTemplatePage"; - -test.describe("SDL Builder Deployment Flow", () => { - test("navigate to SDL builder page", async ({ page, context }) => { - const { sdlBuilderPage } = await setup({ page, context }); - - await expect(sdlBuilderPage.getDeployButton()).toBeVisible(); - await expect(sdlBuilderPage.getPreviewButton()).toBeVisible(); - await expect(sdlBuilderPage.getAddServiceButton()).toBeVisible(); - }); - - test("fill image name and preview SDL", async ({ page, context }) => { - const { sdlBuilderPage } = await setup({ page, context, imageName: "nginx:latest" }); - - await sdlBuilderPage.clickPreview(); - - await expect(sdlBuilderPage.getPreviewTextLocator("nginx:latest")).toBeVisible(); - await expect(sdlBuilderPage.getPreviewTextLocator("version:")).toBeVisible(); - await expect(sdlBuilderPage.getPreviewTextLocator("services:")).toBeVisible(); - - await sdlBuilderPage.closePreview(); - }); - - test("create deployment from SDL builder", async ({ page, context }) => { - const { sdlBuilderPage } = await setup({ page, context, imageName: "nginx:alpine" }); - - await sdlBuilderPage.clickDeploy(); - - await expect(page.getByRole("button", { name: /connect wallet/i }).first()).toBeVisible({ timeout: 10000 }); - }); - - test("add multiple services", async ({ page, context }) => { - const { sdlBuilderPage } = await setup({ page, context, imageName: "nginx:latest" }); - - await sdlBuilderPage.addService(); - - await sdlBuilderPage.waitForServiceAdded("service-2"); - - await sdlBuilderPage.clickPreview(); - await expect(sdlBuilderPage.getPreviewTextLocator("service-1")).toBeVisible(); - await expect(sdlBuilderPage.getPreviewTextLocator("service-2")).toBeVisible(); - await sdlBuilderPage.closePreview(); - }); - - test("preview SDL with different images", async ({ page, context }) => { - const { sdlBuilderPage } = await setup({ page, context }); - - const images = ["postgres:15", "redis:7", "node:18-alpine"]; - - for (const image of images) { - await sdlBuilderPage.fillImageName(image); - await sdlBuilderPage.clickPreview(); - await expect(sdlBuilderPage.getPreviewTextLocator(image)).toBeVisible(); - await sdlBuilderPage.closePreview(); - } - }); - - test("verify SDL YAML structure", async ({ page, context }) => { - const { sdlBuilderPage } = await setup({ page, context, imageName: "ubuntu:22.04" }); - - await sdlBuilderPage.clickPreview(); - - await expect(sdlBuilderPage.getPreviewTextLocator("version:")).toBeVisible(); - await expect(sdlBuilderPage.getPreviewTextLocator("services:")).toBeVisible(); - await expect(sdlBuilderPage.getPreviewTextLocator("profiles:")).toBeVisible(); - await expect(sdlBuilderPage.getPreviewTextLocator("deployment:")).toBeVisible(); - - await sdlBuilderPage.closePreview(); - }); - - test("add service then preview shows both services", async ({ page, context }) => { - const { sdlBuilderPage } = await setup({ page, context, imageName: "nginx:latest" }); - - await sdlBuilderPage.addService(); - await sdlBuilderPage.waitForServiceAdded("service-2"); - - await sdlBuilderPage.clickPreview(); - - await expect(sdlBuilderPage.getServiceLocator("service-1")).toBeVisible(); - await expect(sdlBuilderPage.getServiceLocator("service-2")).toBeVisible(); - - await sdlBuilderPage.closePreview(); - }); - - test("preview button always available with valid image", async ({ page, context }) => { - const sdlBuilderPage = new BuildTemplatePage(context, page); - await sdlBuilderPage.gotoInteractive(); - - await sdlBuilderPage.fillImageName("alpine:latest"); - - await expect(sdlBuilderPage.getPreviewButton()).toBeEnabled(); - }); - - async function setup({ page, context, imageName }: { page: Page; context: BrowserContext; imageName?: string }) { - const sdlBuilderPage = new BuildTemplatePage(context, page); - await sdlBuilderPage.gotoInteractive(); - - if (imageName) { - await sdlBuilderPage.fillImageName(imageName); - } - - return { sdlBuilderPage }; - } -}); diff --git a/apps/deploy-web/tests/ui/uiState/isWalletConnected.ts b/apps/deploy-web/tests/ui/uiState/isWalletConnected.ts deleted file mode 100644 index d148c38414..0000000000 --- a/apps/deploy-web/tests/ui/uiState/isWalletConnected.ts +++ /dev/null @@ -1,22 +0,0 @@ -import type { Page } from "@playwright/test"; - -export async function isWalletConnected(page: Page) { - const result = await Promise.race([ - page - .getByLabel("Connected wallet name and balance") - .waitFor({ state: "visible" }) - .then(() => true) - .catch(() => null), - page - .getByRole("button", { name: /connect wallet/i }) - .waitFor({ state: "visible" }) - .then(() => false) - .catch(() => null) - ]); - - if (result === null) { - throw new Error("Wallet is not connected and there is no button to connect it"); - } - - return result; -} From a6e74c4bfa0de8d6fd06b5ba8d00164a17ccf138 Mon Sep 17 00:00:00 2001 From: Maxime Beauchamp <15185355+baktun14@users.noreply.github.com> Date: Wed, 13 May 2026 22:37:00 -0500 Subject: [PATCH 2/4] chore(wallet): drop unused @cosmjs/crypto from deploy-web Knip flagged @cosmjs/crypto as an unused devDependency after the self-custody Playwright suite was removed. It was only consumed by tests/ui/fixture/web-wallet/CosmjsWebWallet.ts (deleted in the previous commit). --- apps/deploy-web/package.json | 1 - package-lock.json | 1 - 2 files changed, 2 deletions(-) diff --git a/apps/deploy-web/package.json b/apps/deploy-web/package.json index 54cb548efb..0b98b3f95c 100644 --- a/apps/deploy-web/package.json +++ b/apps/deploy-web/package.json @@ -126,7 +126,6 @@ "@akashnetwork/releaser": "*", "@chain-registry/types": "^0.50.12", "@cosmjs/amino": "~0.38.0", - "@cosmjs/crypto": "~0.38.0", "@faker-js/faker": "^9.4.0", "@keplr-wallet/types": "^0.12.111", "@next/bundle-analyzer": "^14.0.1", diff --git a/package-lock.json b/package-lock.json index 20492bf7c8..23547942b8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -626,7 +626,6 @@ "@akashnetwork/releaser": "*", "@chain-registry/types": "^0.50.12", "@cosmjs/amino": "~0.38.0", - "@cosmjs/crypto": "~0.38.0", "@faker-js/faker": "^9.4.0", "@keplr-wallet/types": "^0.12.111", "@next/bundle-analyzer": "^14.0.1", From 6fb4475c55db17015ec86f3cb4fdf2edfe0b7a76 Mon Sep 17 00:00:00 2001 From: Maxime Beauchamp <15185355+baktun14@users.noreply.github.com> Date: Thu, 14 May 2026 13:41:56 -0500 Subject: [PATCH 3/4] chore(wallet): restore wallet-agnostic deploy-web e2e specs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous commit deleted UI-only Playwright specs (build-template, deploy-from-a-template, deploy-linux, custom-container-form) together with their page objects and the closeDeployments cleanup script. None of those specs actually drove a self-custody wallet flow — they only verify the new-deployment UI up to the "connect wallet" prompt, so they still pass after self-custody is turned off. Bring them back along with the closeDeployments.ts cleanup script and its CI wiring. Drop the now-removed walletType option from the custom container spec and re-add the fillImageName / generateSSHKeys helpers on DeployPage that those specs depend on. Truly self-custody specs (authorize-spending, change-wallets, deploy-self-custody-hello-world) remain removed. --- .../actions/console-web-ui-testing/action.yml | 11 ++++ .github/workflows/console-web-release.yml | 1 + apps/deploy-web/script/closeDeployments.ts | 66 +++++++++++++++++++ .../tests/ui/build-template.spec.ts | 12 ++++ .../tests/ui/custom-container-form.spec.ts | 19 ++++++ .../tests/ui/deploy-from-a-template.spec.ts | 33 ++++++++++ apps/deploy-web/tests/ui/deploy-linux.spec.ts | 26 ++++++++ .../tests/ui/pages/BuildTemplatePage.tsx | 53 +++++++++++++++ apps/deploy-web/tests/ui/pages/DeployPage.tsx | 14 ++++ .../tests/ui/pages/PlainLinuxPage.tsx | 9 +++ 10 files changed, 244 insertions(+) create mode 100644 apps/deploy-web/script/closeDeployments.ts create mode 100644 apps/deploy-web/tests/ui/build-template.spec.ts create mode 100644 apps/deploy-web/tests/ui/custom-container-form.spec.ts create mode 100644 apps/deploy-web/tests/ui/deploy-from-a-template.spec.ts create mode 100644 apps/deploy-web/tests/ui/deploy-linux.spec.ts create mode 100644 apps/deploy-web/tests/ui/pages/BuildTemplatePage.tsx create mode 100644 apps/deploy-web/tests/ui/pages/PlainLinuxPage.tsx diff --git a/.github/actions/console-web-ui-testing/action.yml b/.github/actions/console-web-ui-testing/action.yml index 5f1ea21d1b..51cb923dc8 100644 --- a/.github/actions/console-web-ui-testing/action.yml +++ b/.github/actions/console-web-ui-testing/action.yml @@ -8,6 +8,9 @@ inputs: url: description: Base URL to Console Web required: true + test-wallet-mnemonic: + description: Test wallet mnemonic + required: true slack-webhook-url: description: Slack webhook URL required: false @@ -58,6 +61,7 @@ runs: - name: Run e2e tests id: e2e-tests env: + TEST_WALLET_MNEMONIC: ${{ inputs.test-wallet-mnemonic }} BASE_URL: ${{ inputs.url }} E2E_TESTING_CLIENT_TOKEN: ${{ inputs.testing-client-token }} AUTH0_M2M_DOMAIN: ${{ inputs.auth0-m2m-domain }} @@ -71,6 +75,13 @@ runs: CI: "true" shell: bash run: npm run test:e2e --workspace=apps/deploy-web + - name: Tests cleanup + if: ${{ !cancelled() }} + shell: bash + env: + TEST_WALLET_MNEMONIC: ${{ inputs.test-wallet-mnemonic }} + run: | + npx --yes tsx@4.20.6 apps/deploy-web/script/closeDeployments.ts - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 id: playwright-report if: ${{ !cancelled() }} diff --git a/.github/workflows/console-web-release.yml b/.github/workflows/console-web-release.yml index 532492cba3..c12ce6adcf 100644 --- a/.github/workflows/console-web-release.yml +++ b/.github/workflows/console-web-release.yml @@ -58,6 +58,7 @@ jobs: ref: console-web/v${{ needs.setup.outputs.image_tag }} url: ${{ vars.CONSOLE_WEB_BETA_URL }} slack-webhook-url: ${{ secrets.FAILED_E2E_TESTS_SLACK_WEBHOOK_URL }} + test-wallet-mnemonic: ${{ secrets.CONSOLE_WEB_E2E_TEST_WALLET_MNEMONIC }} gh-user-to-slack-user: ${{ vars.GH_USER_TO_SLACK_USER }} testing-client-token: ${{ secrets.CONSOLE_WEB_E2E_TESTING_CLIENT_TOKEN_BETA }} auth0-m2m-domain: ${{ secrets.AUTH0_M2M_DOMAIN }} diff --git a/apps/deploy-web/script/closeDeployments.ts b/apps/deploy-web/script/closeDeployments.ts new file mode 100644 index 0000000000..a4b9841aa4 --- /dev/null +++ b/apps/deploy-web/script/closeDeployments.ts @@ -0,0 +1,66 @@ +import { MsgCloseDeployment } from "@akashnetwork/chain-sdk/private-types/akash.v1beta4"; +import { netConfig } from "@akashnetwork/net"; +import type { GeneratedType } from "@cosmjs/proto-signing"; +import { DirectSecp256k1HdWallet, Registry } from "@cosmjs/proto-signing"; +import { SigningStargateClient } from "@cosmjs/stargate"; + +const mnemonic = process.env.TEST_WALLET_MNEMONIC; + +const newAkashTypes: ReadonlyArray<[string, GeneratedType]> = [MsgCloseDeployment] + .filter(x => "$type" in x) + .map(x => ["/" + x.$type, x as unknown as GeneratedType]); +const registry = new Registry([...newAkashTypes]); + +async function main() { + if (!mnemonic) { + throw new Error("TEST_WALLET_MNEMONIC is not provided"); + } + + const signer = await DirectSecp256k1HdWallet.fromMnemonic(mnemonic, { + prefix: "akash" + }); + + const account = (await signer.getAccounts())[0]; + console.log("Fetching deployments..."); + const deploymentsResponse = await fetch( + `${netConfig.getBaseAPIUrl("sandbox")}/akash/deployment/v1beta4/deployments/list?filters.owner=${account.address}&filters.state=active&pagination.limit=100` + ); + const { deployments } = await deploymentsResponse.json(); + + if (deployments.length === 0) { + console.log("No active deployments found. Exiting..."); + return; + } + + console.log(`Found ${deployments.length} active deployments. Going to close them...`); + + const closeDeploymentsMessages = deployments.map((deployment: any) => { + return { + typeUrl: `/${MsgCloseDeployment.$type}`, + value: MsgCloseDeployment.fromPartial({ + id: deployment.deployment.id + }) + }; + }); + + const txClient = await SigningStargateClient.connectWithSigner(netConfig.getBaseRpcUrl("sandbox"), signer, { + registry + }); + + console.log("Closing deployments..."); + const gas = await txClient.simulate(account.address, closeDeploymentsMessages, "close deployments via script"); + const tx = await txClient.signAndBroadcast(account.address, closeDeploymentsMessages, { + amount: [{ amount: Math.ceil(2500 * closeDeploymentsMessages.length).toString(), denom: "uakt" }], + gas: Math.floor(1.3 * gas).toString() + }); + + if (tx.code !== 0) { + console.error(`Transaction failed with code ${tx.code}: ${tx.rawLog}`); + } else { + console.log(`Transaction hash: ${tx.transactionHash}`); + } + + txClient.disconnect(); +} + +main().catch(console.error); diff --git a/apps/deploy-web/tests/ui/build-template.spec.ts b/apps/deploy-web/tests/ui/build-template.spec.ts new file mode 100644 index 0000000000..15d3c20331 --- /dev/null +++ b/apps/deploy-web/tests/ui/build-template.spec.ts @@ -0,0 +1,12 @@ +import { expect, test } from "./fixture/base-test"; +import { BuildTemplatePage } from "./pages/BuildTemplatePage"; + +test("ssh function absence", async ({ page, context }) => { + const sdlBuilderPage = new BuildTemplatePage(context, page); + await sdlBuilderPage.gotoInteractive(); + + await expect(page.getByRole("button", { name: /generate new key/i })).not.toBeVisible(); + await expect(page.getByRole("checkbox", { name: /expose ssh/i })).not.toBeVisible(); + await expect(page.getByRole("combobox", { name: /os image/i })).not.toBeVisible(); + await expect(page.getByLabel(/docker image/i)).toBeVisible(); +}); diff --git a/apps/deploy-web/tests/ui/custom-container-form.spec.ts b/apps/deploy-web/tests/ui/custom-container-form.spec.ts new file mode 100644 index 0000000000..764336476a --- /dev/null +++ b/apps/deploy-web/tests/ui/custom-container-form.spec.ts @@ -0,0 +1,19 @@ +import { SSH_VM_IMAGES } from "@src/utils/sdl/data"; +import { expect, test } from "./fixture/base-test"; +import { DeployPage } from "./pages/DeployPage"; +import { HomePage } from "./pages/HomePage"; +import { Sidebar } from "./pages/Sidebar"; + +test("custom container form shows connect wallet prompt", async ({ page, context }) => { + const homePage = new HomePage(page); + const sidebar = new Sidebar(page); + const deployPage = new DeployPage(context, page); + + await homePage.goto(); + await sidebar.openDeploy(); + await deployPage.selectTemplate("Run Custom Container"); + await deployPage.fillImageName(SSH_VM_IMAGES["Ubuntu 24.04"]); + await page.getByRole("button", { name: /create deployment/i }).click(); + + await expect(page.getByRole("button", { name: /connect wallet/i }).first()).toBeVisible(); +}); diff --git a/apps/deploy-web/tests/ui/deploy-from-a-template.spec.ts b/apps/deploy-web/tests/ui/deploy-from-a-template.spec.ts new file mode 100644 index 0000000000..d9eeada342 --- /dev/null +++ b/apps/deploy-web/tests/ui/deploy-from-a-template.spec.ts @@ -0,0 +1,33 @@ +import { expect, test } from "./fixture/base-test"; +import { DeployPage } from "./pages/DeployPage"; + +test("user can choose a template on deployment page", async ({ page, context }) => { + test.setTimeout(3 * 60 * 1000); + + const deploymentPage = new DeployPage(context, page); + await deploymentPage.goto(); + + const templateList = page.getByLabel("Template list"); + + await expect(templateList).toBeVisible(); + + const templateLinks = templateList.getByRole("link"); + await expect(templateLinks.nth(0)).toBeVisible({ timeout: 15_000 }); + + const templateCount = await templateLinks.count(); + + for (let i = 0; i < templateCount; i++) { + const link = templateLinks.nth(i); + const linkText = (await link.textContent())?.split("\n")[0] ?? `template ${i}`; + + await test.step(`verify template "${linkText}"`, async () => { + const href = await link.getAttribute("href"); + const newPage = await context.newPage(); + await newPage.goto(new URL(href!, page.url()).href); + + const templateName = await newPage.getByLabel(/Name your deployment/i).inputValue({ timeout: 15_000 }); + await expect(link).toContainText(templateName); + await newPage.close(); + }); + } +}); diff --git a/apps/deploy-web/tests/ui/deploy-linux.spec.ts b/apps/deploy-web/tests/ui/deploy-linux.spec.ts new file mode 100644 index 0000000000..1730239899 --- /dev/null +++ b/apps/deploy-web/tests/ui/deploy-linux.spec.ts @@ -0,0 +1,26 @@ +import { expect, test } from "./fixture/base-test"; +import { HomePage } from "./pages/HomePage"; +import { Sidebar } from "./pages/Sidebar"; + +import { PlainLinuxPage } from "@tests/ui/pages/PlainLinuxPage"; + +test("ssh keys generation", async ({ page, context }) => { + const homePage = new HomePage(page); + const sidebar = new Sidebar(page); + const deployPage = new PlainLinuxPage(context, page); + + await homePage.goto(); + await sidebar.openDeploy(); + + await deployPage.selectTemplate("Launch Container-VM"); + await deployPage.selectDistro("Ubuntu 24.04"); + + const { input, download } = await deployPage.generateSSHKeys(); + + expect(download.suggestedFilename()).toBe("keypair.zip"); + await expect(input).toHaveValue(/ssh-/); + + await page.getByRole("button", { name: /create deployment/i }).click(); + + await expect(page.getByRole("button", { name: /connect wallet/i }).first()).toBeVisible(); +}); diff --git a/apps/deploy-web/tests/ui/pages/BuildTemplatePage.tsx b/apps/deploy-web/tests/ui/pages/BuildTemplatePage.tsx new file mode 100644 index 0000000000..1c66ea1f73 --- /dev/null +++ b/apps/deploy-web/tests/ui/pages/BuildTemplatePage.tsx @@ -0,0 +1,53 @@ +import { testEnvConfig } from "../fixture/test-env.config"; +import { DeployPage } from "./DeployPage"; + +export class BuildTemplatePage extends DeployPage { + async gotoInteractive() { + await this.page.goto(testEnvConfig.BASE_URL); + await this.page + .getByRole("link", { name: /sdl builder/i }) + .first() + .click(); + } + + async addService() { + await this.page.getByRole("button", { name: /add service/i }).click(); + } + + async clickDeploy() { + await this.page.getByRole("button", { name: /^deploy$/i }).click(); + } + + async clickPreview() { + await this.page.getByRole("button", { name: /preview/i }).click(); + } + + getPreviewTextLocator(text: string) { + return this.page.getByText(text).first(); + } + + async closePreview() { + await this.page.getByRole("button", { name: /close/i }).first().click(); + } + + getDeployButton() { + return this.page.getByRole("button", { name: /^deploy$/i }); + } + + getPreviewButton() { + return this.page.getByRole("button", { name: /preview/i }); + } + + getAddServiceButton() { + return this.page.getByRole("button", { name: /add service/i }); + } + + getServiceLocator(serviceName: string) { + const escapedName = serviceName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + return this.page.getByText(new RegExp(`${escapedName}:`)).first(); + } + + async waitForServiceAdded(serviceName: string, timeout = 10000) { + await this.page.locator(`input[type="text"][value="${serviceName}"]`).first().waitFor({ state: "visible", timeout }); + } +} diff --git a/apps/deploy-web/tests/ui/pages/DeployPage.tsx b/apps/deploy-web/tests/ui/pages/DeployPage.tsx index 3c41c336ef..70a74839c1 100644 --- a/apps/deploy-web/tests/ui/pages/DeployPage.tsx +++ b/apps/deploy-web/tests/ui/pages/DeployPage.tsx @@ -17,6 +17,20 @@ export class DeployPage { await this.page.getByLabel(name).or(this.page.getByRole("link", { name })).first().click(); } + async fillImageName(name: string) { + await this.page.getByLabel(/docker image/i).fill(name); + } + + async generateSSHKeys() { + const downloadPromise = this.page.waitForEvent("download"); + await this.page.getByRole("button", { name: /generate new key/i }).click(); + + return { + download: await downloadPromise, + input: this.page.getByLabel(/ssh public key/i) + }; + } + async openDepositDialog() { await this.page.getByRole("button", { name: /create deployment/i }).click(); const dialog = this.page.getByRole("dialog"); diff --git a/apps/deploy-web/tests/ui/pages/PlainLinuxPage.tsx b/apps/deploy-web/tests/ui/pages/PlainLinuxPage.tsx new file mode 100644 index 0000000000..a9c3b17fee --- /dev/null +++ b/apps/deploy-web/tests/ui/pages/PlainLinuxPage.tsx @@ -0,0 +1,9 @@ +import type { SSH_VM_IMAGES } from "@src/utils/sdl/data"; +import { DeployPage } from "./DeployPage"; + +export class PlainLinuxPage extends DeployPage { + async selectDistro(distro: keyof typeof SSH_VM_IMAGES) { + await this.page.getByRole("combobox", { name: /os image/i }).click(); + await this.page.getByRole("option", { name: distro }).click(); + } +} From 54ec35e6738b89380b809157e1a1446f3df5c8cb Mon Sep 17 00:00:00 2001 From: Maxime Beauchamp <15185355+baktun14@users.noreply.github.com> Date: Thu, 14 May 2026 13:53:15 -0500 Subject: [PATCH 4/4] chore(wallet): restore SDL builder e2e spec MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit sdl-builder-deployment.spec.ts only exercises the SDL builder UI (preview, add service, YAML structure) and stops at the "connect wallet" prompt — no self-custody flow. Got swept up in the self-custody removal alongside the other UI-only specs. --- .../tests/ui/sdl-builder-deployment.spec.ts | 107 ++++++++++++++++++ 1 file changed, 107 insertions(+) create mode 100644 apps/deploy-web/tests/ui/sdl-builder-deployment.spec.ts diff --git a/apps/deploy-web/tests/ui/sdl-builder-deployment.spec.ts b/apps/deploy-web/tests/ui/sdl-builder-deployment.spec.ts new file mode 100644 index 0000000000..909f821aaf --- /dev/null +++ b/apps/deploy-web/tests/ui/sdl-builder-deployment.spec.ts @@ -0,0 +1,107 @@ +import type { BrowserContext, Page } from "@playwright/test"; + +import { expect, test } from "./fixture/base-test"; +import { BuildTemplatePage } from "./pages/BuildTemplatePage"; + +test.describe("SDL Builder Deployment Flow", () => { + test("navigate to SDL builder page", async ({ page, context }) => { + const { sdlBuilderPage } = await setup({ page, context }); + + await expect(sdlBuilderPage.getDeployButton()).toBeVisible(); + await expect(sdlBuilderPage.getPreviewButton()).toBeVisible(); + await expect(sdlBuilderPage.getAddServiceButton()).toBeVisible(); + }); + + test("fill image name and preview SDL", async ({ page, context }) => { + const { sdlBuilderPage } = await setup({ page, context, imageName: "nginx:latest" }); + + await sdlBuilderPage.clickPreview(); + + await expect(sdlBuilderPage.getPreviewTextLocator("nginx:latest")).toBeVisible(); + await expect(sdlBuilderPage.getPreviewTextLocator("version:")).toBeVisible(); + await expect(sdlBuilderPage.getPreviewTextLocator("services:")).toBeVisible(); + + await sdlBuilderPage.closePreview(); + }); + + test("create deployment from SDL builder", async ({ page, context }) => { + const { sdlBuilderPage } = await setup({ page, context, imageName: "nginx:alpine" }); + + await sdlBuilderPage.clickDeploy(); + + await expect(page.getByRole("button", { name: /connect wallet/i }).first()).toBeVisible({ timeout: 10000 }); + }); + + test("add multiple services", async ({ page, context }) => { + const { sdlBuilderPage } = await setup({ page, context, imageName: "nginx:latest" }); + + await sdlBuilderPage.addService(); + + await sdlBuilderPage.waitForServiceAdded("service-2"); + + await sdlBuilderPage.clickPreview(); + await expect(sdlBuilderPage.getPreviewTextLocator("service-1")).toBeVisible(); + await expect(sdlBuilderPage.getPreviewTextLocator("service-2")).toBeVisible(); + await sdlBuilderPage.closePreview(); + }); + + test("preview SDL with different images", async ({ page, context }) => { + const { sdlBuilderPage } = await setup({ page, context }); + + const images = ["postgres:15", "redis:7", "node:18-alpine"]; + + for (const image of images) { + await sdlBuilderPage.fillImageName(image); + await sdlBuilderPage.clickPreview(); + await expect(sdlBuilderPage.getPreviewTextLocator(image)).toBeVisible(); + await sdlBuilderPage.closePreview(); + } + }); + + test("verify SDL YAML structure", async ({ page, context }) => { + const { sdlBuilderPage } = await setup({ page, context, imageName: "ubuntu:22.04" }); + + await sdlBuilderPage.clickPreview(); + + await expect(sdlBuilderPage.getPreviewTextLocator("version:")).toBeVisible(); + await expect(sdlBuilderPage.getPreviewTextLocator("services:")).toBeVisible(); + await expect(sdlBuilderPage.getPreviewTextLocator("profiles:")).toBeVisible(); + await expect(sdlBuilderPage.getPreviewTextLocator("deployment:")).toBeVisible(); + + await sdlBuilderPage.closePreview(); + }); + + test("add service then preview shows both services", async ({ page, context }) => { + const { sdlBuilderPage } = await setup({ page, context, imageName: "nginx:latest" }); + + await sdlBuilderPage.addService(); + await sdlBuilderPage.waitForServiceAdded("service-2"); + + await sdlBuilderPage.clickPreview(); + + await expect(sdlBuilderPage.getServiceLocator("service-1")).toBeVisible(); + await expect(sdlBuilderPage.getServiceLocator("service-2")).toBeVisible(); + + await sdlBuilderPage.closePreview(); + }); + + test("preview button always available with valid image", async ({ page, context }) => { + const sdlBuilderPage = new BuildTemplatePage(context, page); + await sdlBuilderPage.gotoInteractive(); + + await sdlBuilderPage.fillImageName("alpine:latest"); + + await expect(sdlBuilderPage.getPreviewButton()).toBeEnabled(); + }); + + async function setup({ page, context, imageName }: { page: Page; context: BrowserContext; imageName?: string }) { + const sdlBuilderPage = new BuildTemplatePage(context, page); + await sdlBuilderPage.gotoInteractive(); + + if (imageName) { + await sdlBuilderPage.fillImageName(imageName); + } + + return { sdlBuilderPage }; + } +});