From e0c5c092f93ac6298b900d917fc32266241d0dda Mon Sep 17 00:00:00 2001 From: Gregor Gilchrist Date: Tue, 11 Jul 2023 10:46:01 +0200 Subject: [PATCH] feat: added live app webview page object model --- .../tests/models/DiscoverPage.ts | 121 ------------ .../tests/models/LiveAppWebview.ts | 176 ++++++++++++++++++ .../tests/specs/services/buy.spec.ts | 110 +++++------ .../tests/specs/services/discover.spec.ts | 82 ++++---- .../tests/specs/services/wallet-api.spec.ts | 49 ++--- libs/test-utils/src/index.ts | 7 +- 6 files changed, 272 insertions(+), 273 deletions(-) create mode 100644 apps/ledger-live-desktop/tests/models/LiveAppWebview.ts diff --git a/apps/ledger-live-desktop/tests/models/DiscoverPage.ts b/apps/ledger-live-desktop/tests/models/DiscoverPage.ts index 232880ea4c2e..918d3b12e189 100644 --- a/apps/ledger-live-desktop/tests/models/DiscoverPage.ts +++ b/apps/ledger-live-desktop/tests/models/DiscoverPage.ts @@ -32,131 +32,10 @@ export class DiscoverPage { this.testAppCatalogItem = page.locator("#platform-catalog-app-dummy-live-app"); this.disclaimerTitle = page.locator("data-test-id=live-app-disclaimer-drawer-title"); this.disclaimerText = page.locator("text=External Application"); - this.liveAppTitle = page.locator("data-test-id=live-app-title"); - this.liveAppLoadingSpinner = page.locator("data-test-id=live-app-loading-spinner"); - this.getAllAccountsButton = page.locator("data-test-id=get-all-accounts-button"); // TODO: make this into its own model - this.requestAccountButton = page.locator("data-test-id=request-single-account-button"); - this.selectAssetTitle = page.locator("data-test-id=select-asset-drawer-title"); - this.selectAssetSearchBar = page.locator("data-test-id=select-asset-drawer-search-input"); - this.selectAccountTitle = page.locator("data-test-id=select-account-drawer-title"); - this.selectBtcAsset = page.locator("text=Bitcoin").first(); - this.selectBtcAccount = page.locator("text=Bitcoin 1 (legacy)").first(); - this.disclaimerCheckbox = page.locator("data-test-id=dismiss-disclaimer"); - this.signNetworkWarning = page.locator("text=Network fees are above 10% of the amount").first(); - this.signContinueButton = page.locator("text=Continue"); - this.confirmText = page.locator( - "text=Please confirm the operation on your device to finalize it", - ); } async openTestApp() { await this.testAppCatalogItem.click(); await this.disclaimerTitle.waitFor({ state: "visible" }); } - - async getLiveAppTitle() { - return await this.liveAppTitle.textContent(); - } - - async getLiveAppDappURL() { - try { - const src = await this.webview.getAttribute("src"); - const url = new URL(src ?? ""); - const { dappUrl }: { dappUrl: string | null } = JSON.parse( - url.searchParams.get("params") ?? "", - ); - return dappUrl; - } catch (e) { - return null; - } - } - - async getAccountsList() { - await this.clickWebviewElement("[data-test-id=get-all-accounts-button]"); - } - - async requestAsset() { - await this.clickWebviewElement("[data-test-id=request-single-account-button]"); - await this.selectAssetTitle.isVisible(); - await this.selectAssetSearchBar.isEnabled(); - } - - async selectAsset() { - await this.selectBtcAsset.click(); - } - - async selectAccount() { - await this.selectAccountTitle.isVisible(); - // TODO: make this dynamic with passed in variable - await this.selectBtcAccount.click(); - } - - async verifyAddress() { - await this.clickWebviewElement("[data-test-id=verify-address-button]"); - } - - async listCurrencies() { - await this.clickWebviewElement("[data-test-id=list-currencies-button]"); - } - - async signTransaction() { - await this.clickWebviewElement("[data-test-id=sign-transaction-button]"); - await this.signNetworkWarning.waitFor({ state: "visible" }); - } - - async continueToSignTransaction() { - await this.signContinueButton.click({ force: true }); - } - - async waitForConfirmationScreenToBeDisplayed() { - await this.confirmText.waitFor({ state: "visible" }); - } - - async clickWebviewElement(elementName: string) { - await this.page.evaluate(elementName => { - const webview = document.querySelector("webview"); - (webview as any).executeJavaScript( - `(function() { - const element = document.querySelector('${elementName}'); - element.click(); - })(); - `, - ); - }, elementName); - } - - async waitForCorrectTextInWebview(textToCheck: string) { - return waitFor(() => this.textIsPresent(textToCheck)); - } - - async textIsPresent(textToCheck: string) { - const result: boolean = await this.page.evaluate(textToCheck => { - const webview = document.querySelector("webview"); - return (webview as any) - .executeJavaScript( - `(function() { - return document.querySelector('*').innerHTML; - })(); - `, - ) - .then((text: string) => { - return text.includes(textToCheck); - }); - }, textToCheck); - - return result; - } - - send(request: Record) { - const sendFunction = ` - (function() { - return window.ledger.e2e.walletApi.send('${JSON.stringify(request)}'); - })() - `; - - return this.page.evaluate(functionToExecute => { - const webview = document.querySelector("webview") as WebviewTag; - return webview.executeJavaScript(functionToExecute); - }, sendFunction); - } } diff --git a/apps/ledger-live-desktop/tests/models/LiveAppWebview.ts b/apps/ledger-live-desktop/tests/models/LiveAppWebview.ts new file mode 100644 index 000000000000..46da71042d9a --- /dev/null +++ b/apps/ledger-live-desktop/tests/models/LiveAppWebview.ts @@ -0,0 +1,176 @@ +import { Page, Locator } from "@playwright/test"; +import { WebviewTag } from "../../src/renderer/components/Web3AppWebview/types"; +import { waitFor } from "../utils/waitFor"; +import { getLiveAppManifest, startDummyServer } from "@ledgerhq/test-utils"; +import { AppManifest } from "@ledgerhq/live-common/wallet-api/types"; + +export class LiveAppWebview { + readonly page: Page; + readonly liveAppTitle: Locator; + readonly liveAppLoadingSpinner: Locator; + readonly getAllAccountsButton: Locator; + readonly requestAccountButton: Locator; + readonly selectAssetTitle: Locator; + readonly selectAssetSearchBar: Locator; + readonly selectAccountTitle: Locator; + readonly selectBtcAsset: Locator; + readonly selectBtcAccount: Locator; + readonly disclaimerCheckbox: Locator; + readonly signNetworkWarning: Locator; + readonly signContinueButton: Locator; + readonly confirmText: Locator; + readonly webview: Locator; + + constructor(page: Page) { + this.page = page; + this.webview = page.locator("webview"); + this.liveAppTitle = page.locator("data-test-id=live-app-title"); + this.liveAppLoadingSpinner = page.locator("data-test-id=live-app-loading-spinner"); + this.getAllAccountsButton = page.locator("data-test-id=get-all-accounts-button"); // TODO: make this into its own model + this.requestAccountButton = page.locator("data-test-id=request-single-account-button"); + this.selectAssetTitle = page.locator("data-test-id=select-asset-drawer-title"); + this.selectAssetSearchBar = page.locator("data-test-id=select-asset-drawer-search-input"); + this.selectAccountTitle = page.locator("data-test-id=select-account-drawer-title"); + this.selectBtcAsset = page.locator("text=Bitcoin").first(); + this.selectBtcAccount = page.locator("text=Bitcoin 1 (legacy)").first(); + this.disclaimerCheckbox = page.locator("data-test-id=dismiss-disclaimer"); + this.signNetworkWarning = page.locator("text=Network fees are above 10% of the amount").first(); + this.signContinueButton = page.locator("text=Continue"); + this.confirmText = page.locator( + "text=Please confirm the operation on your device to finalize it", + ); + } + + static async startLiveApp( + liveAppDirectory: string, + liveAppManifest: Partial & Pick, + ) { + try { + const port = await startDummyServer(`${liveAppDirectory}`); + + const url = `http://localhost:${port}`; + const response = await fetch(url); + if (response.ok) { + // eslint-disable-next-line no-console + console.info(`========> Live app successfully running on port ${port}! <=========`); + + const localManifests = JSON.stringify(getLiveAppManifest(liveAppManifest)); + process.env.MOCK_REMOTE_LIVE_MANIFEST = localManifests; + console.log("mock manifest:", process.env.MOCK_REMOTE_LIVE_MANIFEST); + return true; + } else { + throw new Error("Ping response != 200, got: " + response.status); + } + } catch (error) { + console.warn(`========> Live app not running! <=========`); + console.error(error); + return false; + } + } + + async getLiveAppTitle() { + return await this.liveAppTitle.textContent(); + } + + async getLiveAppDappURL() { + try { + const src = await this.webview.getAttribute("src"); + const url = new URL(src ?? ""); + const { dappUrl }: { dappUrl: string | null } = JSON.parse( + url.searchParams.get("params") ?? "", + ); + return dappUrl; + } catch (e) { + return null; + } + } + + async getAccountsList() { + await this.clickWebviewElement("[data-test-id=get-all-accounts-button]"); + } + + async requestAsset() { + await this.clickWebviewElement("[data-test-id=request-single-account-button]"); + await this.selectAssetTitle.isVisible(); + await this.selectAssetSearchBar.isEnabled(); + } + + async selectAsset() { + await this.selectBtcAsset.click(); + } + + async selectAccount() { + await this.selectAccountTitle.isVisible(); + // TODO: make this dynamic with passed in variable + await this.selectBtcAccount.click(); + } + + async verifyAddress() { + await this.clickWebviewElement("[data-test-id=verify-address-button]"); + } + + async listCurrencies() { + await this.clickWebviewElement("[data-test-id=list-currencies-button]"); + } + + async signTransaction() { + await this.clickWebviewElement("[data-test-id=sign-transaction-button]"); + await this.signNetworkWarning.waitFor({ state: "visible" }); + } + + async continueToSignTransaction() { + await this.signContinueButton.click({ force: true }); + } + + async waitForConfirmationScreenToBeDisplayed() { + await this.confirmText.waitFor({ state: "visible" }); + } + + async clickWebviewElement(elementName: string) { + await this.page.evaluate(elementName => { + const webview = document.querySelector("webview"); + (webview as any).executeJavaScript( + `(function() { + const element = document.querySelector('${elementName}'); + element.click(); + })(); + `, + ); + }, elementName); + } + + async waitForCorrectTextInWebview(textToCheck: string) { + return waitFor(() => this.textIsPresent(textToCheck)); + } + + async textIsPresent(textToCheck: string) { + const result: boolean = await this.page.evaluate(textToCheck => { + const webview = document.querySelector("webview"); + return (webview as any) + .executeJavaScript( + `(function() { + return document.querySelector('*').innerHTML; + })(); + `, + ) + .then((text: string) => { + return text.includes(textToCheck); + }); + }, textToCheck); + + return result; + } + + send(request: Record) { + const sendFunction = ` + (function() { + return window.ledger.e2e.walletApi.send('${JSON.stringify(request)}'); + })() + `; + + return this.page.evaluate(functionToExecute => { + const webview = document.querySelector("webview") as WebviewTag; + return webview.executeJavaScript(functionToExecute); + }, sendFunction); + } +} diff --git a/apps/ledger-live-desktop/tests/specs/services/buy.spec.ts b/apps/ledger-live-desktop/tests/specs/services/buy.spec.ts index 4ed8f71ffec5..8576344ec8d7 100644 --- a/apps/ledger-live-desktop/tests/specs/services/buy.spec.ts +++ b/apps/ledger-live-desktop/tests/specs/services/buy.spec.ts @@ -1,14 +1,14 @@ import test from "../../fixtures/common"; import { expect } from "@playwright/test"; import { Layout } from "../../models/Layout"; -import { DiscoverPage } from "../../models/DiscoverPage"; import { PortfolioPage } from "../../models/PortfolioPage"; import { AssetPage } from "../../models/AssetPage"; import { AccountsPage } from "../../models/AccountsPage"; import { AccountPage } from "../../models/AccountPage"; import { SettingsPage } from "../../models/SettingsPage"; import { MarketPage } from "../../models/MarketPage"; -import { getLiveAppManifest, startDummyServer, stopDummyServer } from "@ledgerhq/test-utils"; +import { LiveAppWebview } from "../../models/LiveAppWebview"; +import { stopDummyServer } from "@ledgerhq/test-utils"; test.use({ userdata: "1AccountBTC1AccountETH", @@ -23,50 +23,29 @@ test.use({ let continueTest = false; -test.beforeAll(async ({ request }) => { - // Check that dummy app in tests/utils/dummy-ptx-app has been started successfully - try { - const port = await startDummyServer("dummy-ptx-app/public"); - const url = `http://localhost:${port}`; - const response = await request.get(url); - if (response.ok() && port) { - continueTest = true; - console.info(`========> Dummy test app successfully running on port ${port}! <=========`); - process.env.MOCK_REMOTE_LIVE_MANIFEST = JSON.stringify( - getLiveAppManifest({ - id: "multibuy", - url, - name: "Dummy Buy / Sell App", - content: { - shortDescription: { - en: "Dummy app for testing the Buy app is accessed correctly", - }, - description: { - en: "Dummy app for testing the Buy app is accessed correctly", - }, - }, - permissions: [ - { - method: "account.request", - params: { - currencies: ["ethereum", "bitcoin", "algorand"], - }, - }, - ], - }), - ); - } else { - throw new Error("Ping response != 200, got: " + response.status); - } - } catch (error) { - console.warn(`========> Dummy test app not running! <=========`); - console.error(error); +test.beforeAll(async () => { + // Check that dummy app in tests/utils/dummy-app-build has been started successfully + continueTest = await LiveAppWebview.startLiveApp("dummy-ptx-app/public", { + name: "Buy App", + id: "multibuy", + permissions: [ + { + method: "account.request", + params: { + currencies: ["ethereum", "bitcoin", "algorand"], + }, + }, + ], + }); + + if (!continueTest) { + console.warn("Stopping Buy/Sell test setup"); + return; // need to make this a proper ignore/jest warning } }); test.afterAll(async () => { await stopDummyServer(); - console.info(`========> Dummy test app stopped <=========`); delete process.env.MOCK_REMOTE_LIVE_MANIFEST; }); @@ -75,8 +54,8 @@ test("Buy / Sell", async ({ page }) => { if (!continueTest) return; const layout = new Layout(page); - const liveApp = new DiscoverPage(page); const portfolioPage = new PortfolioPage(page); + const liveAppWebview = new LiveAppWebview(page); const assetPage = new AssetPage(page); const accountPage = new AccountPage(page); const accountsPage = new AccountsPage(page); @@ -84,21 +63,24 @@ test("Buy / Sell", async ({ page }) => { const marketPage = new MarketPage(page); await test.step("Navigate to Buy app from portfolio banner", async () => { + await page.pause(); await portfolioPage.startBuyFlow(); - await expect(await liveApp.waitForCorrectTextInWebview("theme: dark")).toBe(true); - await expect(await liveApp.waitForCorrectTextInWebview("lang: en")).toBe(true); + // await expect(await liveAppWebview.waitForCorrectTextInWebview("theme: dark")).toBe(true); + // await expect(await liveAppWebview.waitForCorrectTextInWebview("lang: en")).toBe(true); await expect.soft(page).toHaveScreenshot("buy-app-opened.png"); }); await test.step("Navigate to Buy app from market", async () => { await layout.goToMarket(); await marketPage.openBuyPage("usdt"); - await expect(await liveApp.waitForCorrectTextInWebview("theme: dark")).toBe(true); + await expect(await liveAppWebview.waitForCorrectTextInWebview("theme: dark")).toBe(true); await expect( - await liveApp.waitForCorrectTextInWebview("currency: ethereum/erc20/usd_tether__erc20_"), + await liveAppWebview.waitForCorrectTextInWebview( + "currency: ethereum/erc20/usd_tether__erc20_", + ), ).toBe(true); - await expect(await liveApp.waitForCorrectTextInWebview("mode: buy")).toBe(true); - await expect(await liveApp.waitForCorrectTextInWebview("lang: en")).toBe(true); + await expect(await liveAppWebview.waitForCorrectTextInWebview("mode: buy")).toBe(true); + await expect(await liveAppWebview.waitForCorrectTextInWebview("lang: en")).toBe(true); }); await test.step("Navigate to Buy app from asset", async () => { @@ -106,10 +88,10 @@ test("Buy / Sell", async ({ page }) => { await portfolioPage.navigateToAsset("ethereum"); await assetPage.startBuyFlow(); - await expect(await liveApp.waitForCorrectTextInWebview("theme: dark")).toBe(true); - await expect(await liveApp.waitForCorrectTextInWebview("lang: en")).toBe(true); - await expect(await liveApp.waitForCorrectTextInWebview("currency: ethereum")).toBe(true); - await expect(await liveApp.waitForCorrectTextInWebview("mode: buy")).toBe(true); + await expect(await liveAppWebview.waitForCorrectTextInWebview("theme: dark")).toBe(true); + await expect(await liveAppWebview.waitForCorrectTextInWebview("lang: en")).toBe(true); + await expect(await liveAppWebview.waitForCorrectTextInWebview("currency: ethereum")).toBe(true); + await expect(await liveAppWebview.waitForCorrectTextInWebview("mode: buy")).toBe(true); }); await test.step("Navigate to Buy app from account", async () => { @@ -117,13 +99,13 @@ test("Buy / Sell", async ({ page }) => { await accountsPage.navigateToAccountByName("Bitcoin 1 (legacy)"); await accountPage.navigateToBuy(); - await expect(await liveApp.waitForCorrectTextInWebview("theme: dark")).toBe(true); - await expect(await liveApp.waitForCorrectTextInWebview("currency: bitcoin")).toBe(true); + await expect(await liveAppWebview.waitForCorrectTextInWebview("theme: dark")).toBe(true); + await expect(await liveAppWebview.waitForCorrectTextInWebview("currency: bitcoin")).toBe(true); await expect( - await liveApp.waitForCorrectTextInWebview("account: mock:1:bitcoin:true_bitcoin_0:"), + await liveAppWebview.waitForCorrectTextInWebview("account: mock:1:bitcoin:true_bitcoin_0:"), ).toBe(true); - await expect(await liveApp.waitForCorrectTextInWebview("lang: en")).toBe(true); - await expect(await liveApp.waitForCorrectTextInWebview("mode: buy")).toBe(true); + await expect(await liveAppWebview.waitForCorrectTextInWebview("lang: en")).toBe(true); + await expect(await liveAppWebview.waitForCorrectTextInWebview("mode: buy")).toBe(true); }); await test.step("Navigate to Buy app from account", async () => { @@ -131,13 +113,13 @@ test("Buy / Sell", async ({ page }) => { await accountsPage.navigateToAccountByName("Bitcoin 1 (legacy)"); await accountPage.navigateToSell(); - await expect(await liveApp.waitForCorrectTextInWebview("theme: dark")).toBe(true); - await expect(await liveApp.waitForCorrectTextInWebview("currency: bitcoin")).toBe(true); + await expect(await liveAppWebview.waitForCorrectTextInWebview("theme: dark")).toBe(true); + await expect(await liveAppWebview.waitForCorrectTextInWebview("currency: bitcoin")).toBe(true); await expect( - await liveApp.waitForCorrectTextInWebview("account: mock:1:bitcoin:true_bitcoin_0:"), + await liveAppWebview.waitForCorrectTextInWebview("account: mock:1:bitcoin:true_bitcoin_0:"), ).toBe(true); - await expect(await liveApp.waitForCorrectTextInWebview("lang: en")).toBe(true); - await expect(await liveApp.waitForCorrectTextInWebview("mode: sell")).toBe(true); + await expect(await liveAppWebview.waitForCorrectTextInWebview("lang: en")).toBe(true); + await expect(await liveAppWebview.waitForCorrectTextInWebview("mode: sell")).toBe(true); }); await test.step("Navigate to Buy app from sidebar with light theme and French Language", async () => { @@ -146,7 +128,7 @@ test("Buy / Sell", async ({ page }) => { await settingsPage.changeTheme(); await await layout.goToBuyCrypto(); - await expect(await liveApp.waitForCorrectTextInWebview("theme: light")).toBe(true); - await expect(await liveApp.waitForCorrectTextInWebview("lang: fr")).toBe(true); + await expect(await liveAppWebview.waitForCorrectTextInWebview("theme: light")).toBe(true); + await expect(await liveAppWebview.waitForCorrectTextInWebview("lang: fr")).toBe(true); }); }); diff --git a/apps/ledger-live-desktop/tests/specs/services/discover.spec.ts b/apps/ledger-live-desktop/tests/specs/services/discover.spec.ts index a398e1f462bf..abbd27bfbea0 100644 --- a/apps/ledger-live-desktop/tests/specs/services/discover.spec.ts +++ b/apps/ledger-live-desktop/tests/specs/services/discover.spec.ts @@ -5,48 +5,29 @@ import { Layout } from "../../models/Layout"; import { Drawer } from "../../models/Drawer"; import { Modal } from "../../models/Modal"; import { DeviceAction } from "../../models/DeviceAction"; -import { startDummyServer, getLiveAppManifest, stopDummyServer } from "@ledgerhq/test-utils"; +import { stopDummyServer } from "@ledgerhq/test-utils"; +import { LiveAppWebview } from "../../models/LiveAppWebview"; test.use({ userdata: "1AccountBTC1AccountETH" }); let continueTest = false; -test.beforeAll(async ({ request }) => { +test.beforeAll(async () => { // Check that dummy app in tests/utils/dummy-app-build has been started successfully - try { - const port = await startDummyServer("dummy-live-app/build"); - const url = `http://localhost:${port}`; - const response = await request.get(url); - if (response.ok()) { - continueTest = true; - console.info(`========> Dummy test app successfully running on port ${port}! <=========`); - process.env.MOCK_REMOTE_LIVE_MANIFEST = JSON.stringify( - getLiveAppManifest({ - id: "dummy-live-app", - url, - permissions: [{ method: "*" }], - content: { - shortDescription: { - en: "App to test the Live App SDK", - }, - description: { - en: "App to test the Live App SDK with Playwright", - }, - }, - }), - ); - } else { - throw new Error("Ping response != 200, got: " + response.status); - } - } catch (error) { - console.warn(`========> Dummy test app not running! <=========`); - console.error(error); + continueTest = await LiveAppWebview.startLiveApp("dummy-live-app/build", { + name: "Dummy Live App", + id: "dummy-live-app", + permissions: [{ method: "*" }], + }); + + if (!continueTest) { + console.warn("Stopping Buy/Sell test setup"); + return; // need to make this a proper ignore/jest warning } }); test.afterAll(async () => { await stopDummyServer(); - console.info(`========> Dummy test app stopped <=========`); delete process.env.MOCK_REMOTE_LIVE_MANIFEST; }); @@ -56,6 +37,7 @@ test("Discover", async ({ page }) => { if (!continueTest) return; const discoverPage = new DiscoverPage(page); + const liveAppWebview = new LiveAppWebview(page); const drawer = new Drawer(page); const modal = new Modal(page); const layout = new Layout(page); @@ -66,59 +48,59 @@ test("Discover", async ({ page }) => { await discoverPage.openTestApp(); await drawer.continue(); await drawer.waitForDrawerToDisappear(); // macos runner was having screenshot issues here because the drawer wasn't disappearing fast enough - await discoverPage.waitForCorrectTextInWebview("Ledger Live Dummy Test App"); + await liveAppWebview.waitForCorrectTextInWebview("Ledger Live Dummy Test App"); }); await test.step("List all accounts", async () => { - await discoverPage.getAccountsList(); - await discoverPage.waitForCorrectTextInWebview("mock:1:bitcoin:true_bitcoin_0:"); - await discoverPage.waitForCorrectTextInWebview("mock:1:bitcoin:true_bitcoin_1:"); + await liveAppWebview.getAccountsList(); + await liveAppWebview.waitForCorrectTextInWebview("mock:1:bitcoin:true_bitcoin_0:"); + await liveAppWebview.waitForCorrectTextInWebview("mock:1:bitcoin:true_bitcoin_1:"); }); await test.step("Request Account drawer - open", async () => { - await discoverPage.requestAsset(); - await expect(discoverPage.selectAssetTitle).toBeVisible(); + await liveAppWebview.requestAsset(); + await expect(liveAppWebview.selectAssetTitle).toBeVisible(); }); await test.step("Request Account - select asset", async () => { - await discoverPage.selectAsset(); - await expect(discoverPage.selectAccountTitle).toBeVisible(); - await expect(discoverPage.selectAssetSearchBar).toBeEnabled(); + await liveAppWebview.selectAsset(); + await expect(liveAppWebview.selectAccountTitle).toBeVisible(); + await expect(liveAppWebview.selectAssetSearchBar).toBeEnabled(); }); await test.step("Request Account - select BTC", async () => { - await discoverPage.selectAccount(); + await liveAppWebview.selectAccount(); await drawer.waitForDrawerToDisappear(); - await discoverPage.waitForCorrectTextInWebview("mock:1:bitcoin:true_bitcoin_0:"); + await liveAppWebview.waitForCorrectTextInWebview("mock:1:bitcoin:true_bitcoin_0:"); }); await test.step("List currencies", async () => { - await discoverPage.listCurrencies(); - await discoverPage.waitForCorrectTextInWebview("CryptoCurrency"); + await liveAppWebview.listCurrencies(); + await liveAppWebview.waitForCorrectTextInWebview("CryptoCurrency"); }); await test.step("Verify Address - modal", async () => { - await discoverPage.verifyAddress(); + await liveAppWebview.verifyAddress(); await deviceAction.openApp(); await expect.soft(page).toHaveScreenshot("live-app-verify-address.png"); }); await test.step("Verify Address - address output", async () => { await modal.waitForModalToDisappear(); - await discoverPage.waitForCorrectTextInWebview("1xey"); + await liveAppWebview.waitForCorrectTextInWebview("1xey"); }); await test.step("Sign Transaction - info modal", async () => { - await discoverPage.signTransaction(); + await liveAppWebview.signTransaction(); await expect.soft(page).toHaveScreenshot("live-app-sign-transaction-info.png", { timeout: 20000, }); }); await test.step("Sign Transaction - confirmation modal", async () => { - await discoverPage.continueToSignTransaction(); + await liveAppWebview.continueToSignTransaction(); await layout.waitForLoadingSpinnerToHaveDisappeared(); - await discoverPage.waitForConfirmationScreenToBeDisplayed(); + await liveAppWebview.waitForConfirmationScreenToBeDisplayed(); await expect(page.locator("text=0.0000123")).toBeVisible(); await expect(page.locator("text=0.0000025")).toBeVisible(); // This screenshot is flaky as the loading spinner appears again after this confirm modal, and on slow CI runners the screenshot can take a picture of this instead of the confirm. @@ -127,6 +109,6 @@ test("Discover", async ({ page }) => { await test.step("Sign Transaction - signature output", async () => { await modal.waitForModalToDisappear(); - await discoverPage.waitForCorrectTextInWebview("mock_op_100_mock:1:bitcoin:true_bitcoin_0:"); + await liveAppWebview.waitForCorrectTextInWebview("mock_op_100_mock:1:bitcoin:true_bitcoin_0:"); }); }); diff --git a/apps/ledger-live-desktop/tests/specs/services/wallet-api.spec.ts b/apps/ledger-live-desktop/tests/specs/services/wallet-api.spec.ts index e9b772b39183..a3c05b5d510d 100644 --- a/apps/ledger-live-desktop/tests/specs/services/wallet-api.spec.ts +++ b/apps/ledger-live-desktop/tests/specs/services/wallet-api.spec.ts @@ -5,52 +5,29 @@ import { Layout } from "../../models/Layout"; import { Drawer } from "../../models/Drawer"; import { Modal } from "../../models/Modal"; import { DeviceAction } from "../../models/DeviceAction"; -import { startDummyServer, getLiveAppManifest, stopDummyServer } from "@ledgerhq/test-utils"; +import { stopDummyServer } from "@ledgerhq/test-utils"; import { randomUUID } from "crypto"; +import { LiveAppWebview } from "../../models/LiveAppWebview"; test.use({ userdata: "1AccountBTC1AccountETH" }); +let liveAppWebview; let continueTest = false; -test.beforeAll(async ({ request }) => { +test.beforeAll(async ({ page }) => { + liveAppWebview = new LiveAppWebview(page); + // Check that dummy app in tests/utils/dummy-app-build has been started successfully - try { - const port = await startDummyServer("dummy-wallet-app/build"); - const url = `http://localhost:${port}`; - const response = await request.get(url); - if (response.ok()) { - continueTest = true; - console.info( - `========> Dummy Wallet API app successfully running on port ${port}! <=========`, - ); - process.env.MOCK_REMOTE_LIVE_MANIFEST = JSON.stringify( - getLiveAppManifest({ - id: "dummy-live-app", - url, - name: "Dummy Wallet API Live App", - apiVersion: "2.0.0", - content: { - shortDescription: { - en: "App to test the Wallet API", - }, - description: { - en: "App to test the Wallet API with Playwright", - }, - }, - }), - ); - } else { - throw new Error("Ping response != 200, got: " + response.status); - } - } catch (error) { - console.warn(`========> Dummy test app not running! <=========`); - console.error(error); + continueTest = await liveAppWebview.startLiveApp("dummy-wallet-app/build"); + + if (!continueTest) { + console.warn("Stopping Wallet API test setup"); + return; // need to make this a proper ignore/jest warning } }); test.afterAll(async () => { await stopDummyServer(); - console.info(`========> Dummy Wallet API app stopped <=========`); delete process.env.MOCK_REMOTE_LIVE_MANIFEST; }); @@ -70,7 +47,7 @@ test("Wallet API methods", async ({ page }) => { await drawer.waitForDrawerToDisappear(); const id = randomUUID(); - const response = discoverPage.send({ + const response = liveAppWebview.send({ jsonrpc: "2.0", id, method: "account.request", @@ -102,7 +79,7 @@ test("Wallet API methods", async ({ page }) => { await test.step("account.receive", async () => { const id = randomUUID(); - const response = discoverPage.send({ + const response = liveAppWebview.send({ jsonrpc: "2.0", id, method: "account.receive", diff --git a/libs/test-utils/src/index.ts b/libs/test-utils/src/index.ts index d0c7fa199d4e..eca272c56f4c 100644 --- a/libs/test-utils/src/index.ts +++ b/libs/test-utils/src/index.ts @@ -34,7 +34,9 @@ export function startDummyServer(appPath: string, port = 0): Promise { }); } -export function getLiveAppManifest(params: Partial & Pick) { +export function getLiveAppManifest( + params: Partial & Pick, +): AppManifest[] { const manifest = [ { name: "Generic Live App", @@ -76,7 +78,7 @@ export function getLiveAppManifest(params: Partial & Pick { dummyAppServer.close(); return new Promise(resolve => { dummyAppServer.on("close", () => { + console.info(`========> Live app stopped <=========`); resolve(); }); });