diff --git a/android/app/capacitor.build.gradle b/android/app/capacitor.build.gradle index c909025a..5b5832d4 100644 --- a/android/app/capacitor.build.gradle +++ b/android/app/capacitor.build.gradle @@ -12,12 +12,14 @@ dependencies { implementation project(':capacitor-mlkit-barcode-scanning') implementation project(':capacitor-app') implementation project(':capacitor-app-launcher') + implementation project(':capacitor-camera') implementation project(':capacitor-clipboard') implementation project(':capacitor-filesystem') implementation project(':capacitor-haptics') implementation project(':capacitor-share') implementation project(':capacitor-status-bar') implementation project(':capacitor-toast') + implementation project(':capacitor-secure-storage-plugin') } diff --git a/android/capacitor.settings.gradle b/android/capacitor.settings.gradle index 8a2fdb13..ba78cf2e 100644 --- a/android/capacitor.settings.gradle +++ b/android/capacitor.settings.gradle @@ -11,6 +11,9 @@ project(':capacitor-app').projectDir = new File('../node_modules/.pnpm/@capacito include ':capacitor-app-launcher' project(':capacitor-app-launcher').projectDir = new File('../node_modules/.pnpm/@capacitor+app-launcher@5.0.6_@capacitor+core@5.5.1/node_modules/@capacitor/app-launcher/android') +include ':capacitor-camera' +project(':capacitor-camera').projectDir = new File('../node_modules/.pnpm/@capacitor+camera@5.0.9_@capacitor+core@5.5.1/node_modules/@capacitor/camera/android') + include ':capacitor-clipboard' project(':capacitor-clipboard').projectDir = new File('../node_modules/.pnpm/@capacitor+clipboard@5.0.6_@capacitor+core@5.5.1/node_modules/@capacitor/clipboard/android') @@ -28,3 +31,6 @@ project(':capacitor-status-bar').projectDir = new File('../node_modules/.pnpm/@c include ':capacitor-toast' project(':capacitor-toast').projectDir = new File('../node_modules/.pnpm/@capacitor+toast@5.0.6_@capacitor+core@5.5.1/node_modules/@capacitor/toast/android') + +include ':capacitor-secure-storage-plugin' +project(':capacitor-secure-storage-plugin').projectDir = new File('../node_modules/.pnpm/capacitor-secure-storage-plugin@0.9.0_@capacitor+core@5.5.1/node_modules/capacitor-secure-storage-plugin/android') diff --git a/e2e/encrypt.spec.ts b/e2e/encrypt.spec.ts index f0e897aa..81f6f61c 100644 --- a/e2e/encrypt.spec.ts +++ b/e2e/encrypt.spec.ts @@ -1,28 +1,14 @@ import { expect, test } from "@playwright/test"; +import { loadHome, visitSettings } from "./utils"; + test.beforeEach(async ({ page }) => { await page.goto("http://localhost:3420/"); }); test("test local encrypt", async ({ page }) => { - // Expect a title "to contain" a substring. - await expect(page).toHaveTitle(/Mutiny Wallet/); - - // Wait for an element matching the selector to appear in DOM. - await page.waitForSelector("text=0 SATS"); - - console.log("Page loaded."); - - // Wait for a while just to make sure we can load everything - await page.waitForTimeout(1000); - - // Navigate to settings - const settingsLink = await page.getByRole("link", { name: "Settings" }); - - settingsLink.click(); - - // Wait for settings to load - await page.waitForSelector("text=Settings"); + await loadHome(page); + await visitSettings(page); // Click the "Backup" link await page.click("text=Backup"); @@ -48,6 +34,13 @@ test("test local encrypt", async ({ page }) => { // Click the "I wrote down the words" button await wroteDownButton.click(); + // Make sure the balance box ready light is on + await page.locator("title=READY"); + + // Go back to settings / change password + await visitSettings(page); + await page.click("text=Change Password"); + // The header should now say "Encrypt your seed words" await expect(page.locator("h1")).toContainText(["Encrypt your seed words"]); @@ -56,7 +49,7 @@ test("test local encrypt", async ({ page }) => { const passwordInput = await page.locator(`input[name='password']`); // 2. Type the password into the input field - await passwordInput.type("test"); + await passwordInput.fill("test"); // 3. Find the input field with the name "confirmPassword" const confirmPasswordInput = await page.locator( @@ -64,15 +57,21 @@ test("test local encrypt", async ({ page }) => { ); // 4. Type the password into the input field - await confirmPasswordInput.type("test"); + await confirmPasswordInput.fill("test"); // The "Encrypt" button should not be disabled const encryptButton = await page.locator("button", { hasText: "Encrypt" }); await expect(encryptButton).not.toBeDisabled(); + // wait 5 seconds for no reason (SADLY THIS IS IMPORTANT FOR THE TEST TO PASS) + await page.waitForTimeout(5000); + // Click the "Encrypt" button await encryptButton.click(); + // wait for a while just to see what happens + // await page.waitForTimeout(10000); + // Wait for a modal with the text "Enter your password" await page.waitForSelector("text=Enter your password"); @@ -80,11 +79,11 @@ test("test local encrypt", async ({ page }) => { const passwordInput2 = await page.locator(`input[name='password']`); // Type the password into the input field - await passwordInput2.type("test"); + await passwordInput2.fill("test"); // Click the "Decrypt Wallet" button await page.click("text=Decrypt Wallet"); // Wait for an element matching the selector to appear in DOM. - await page.waitForSelector("text=0 SATS"); + await page.locator(`text=0 sats`).first(); }); diff --git a/e2e/fedimint.spec.ts b/e2e/fedimint.spec.ts index 6ba9d244..933326ff 100644 --- a/e2e/fedimint.spec.ts +++ b/e2e/fedimint.spec.ts @@ -1,5 +1,7 @@ import { expect, test } from "@playwright/test"; +import { loadHome, visitSettings } from "./utils"; + const SIGNET_INVITE_CODE = "fed11qgqzc2nhwden5te0vejkg6tdd9h8gepwvejkg6tdd9h8garhduhx6at5d9h8jmn9wshxxmmd9uqqzgxg6s3evnr6m9zdxr6hxkdkukexpcs3mn7mj3g5pc5dfh63l4tj6g9zk4er"; @@ -8,24 +10,8 @@ test.beforeEach(async ({ page }) => { }); test("fedmint join, receive, send", async ({ page }) => { - // Expect a title "to contain" a substring. - await expect(page).toHaveTitle(/Mutiny Wallet/); - - // Wait for an element matching the selector to appear in DOM. - await page.waitForSelector("text=0 SATS"); - - console.log("Page loaded."); - - // Wait for a while just to make sure we can load everything - await page.waitForTimeout(1000); - - // Navigate to settings - const settingsLink = await page.getByRole("link", { name: "Settings" }); - - settingsLink.click(); - - // Wait for settings to load - await page.waitForSelector("text=Settings"); + await loadHome(page); + await visitSettings(page); // Click "Manage Federations" link await page.click("text=Manage Federations"); @@ -45,11 +31,20 @@ test("fedmint join, receive, send", async ({ page }) => { await page.goBack(); await page.goBack(); - // Make sure there's a fedimint icon - await expect(page.getByRole("img", { name: "community" })).toBeVisible(); + // Click the top left button (it's the profile button), a child of header + // TODO: better ARIA stuff + await page.locator(`header button`).first().click(); - // Click the receive button - await page.click("text=Receive"); + // Make sure there's text that says "fedimint" + await page.locator("text=fedimint").first(); + + // Navigate back home + await page.goBack(); + + // Click the fab button + await page.locator("#fab").click(); + // Click the receive button in the fab + await page.locator("text=Receive").last().click(); // Expect the url to conain receive await expect(page).toHaveURL(/.*receive/); @@ -57,9 +52,6 @@ test("fedmint join, receive, send", async ({ page }) => { // At least one h1 should show "0 sats" await expect(page.locator("h1")).toContainText(["0 SATS"]); - // At least one h2 should show "0 USD" - await expect(page.locator("h2")).toContainText(["$0 USD"]); - // Type 100 into the input await page.locator("#sats-input").pressSequentially("100"); @@ -72,11 +64,7 @@ test("fedmint join, receive, send", async ({ page }) => { }); await expect(continueButton).not.toBeDisabled(); - // Wait one second - // TODO: figure out how to not get an error without waiting - await page.waitForTimeout(1000); - - continueButton.click(); + await continueButton.click(); await expect( page.getByText("Keep Mutiny open to complete the payment.") @@ -109,21 +97,17 @@ test("fedmint join, receive, send", async ({ page }) => { ); // Wait for an h1 to appear in the dom that says "Payment Received" - await page.waitForSelector("text=Payment Received", { timeout: 30000 }); + await page.waitForSelector("text=Payment Received"); // Click the "Nice" button await page.click("text=Nice"); - // Make sure we have 100 sats in the fedimint balance - await expect( - page - .locator("div") - .filter({ hasText: /^100 eSATS$/ }) - .nth(1) - ).toBeVisible(); + // Make sure we have 100 sats in the top balance + await page.waitForSelector("text=100 SATS"); // Now we send - await page.click("text=Send"); + await page.locator("#fab").click(); + await page.locator("text=Send").last().click(); // type refund@lnurl-staging.mutinywallet.com const sendInput = await page.locator("input"); @@ -131,9 +115,8 @@ test("fedmint join, receive, send", async ({ page }) => { await page.click("text=Continue"); - // Wait two seconds (the destination doesn't show up immediately) - // TODO: figure out how to not get an error without waiting - await page.waitForTimeout(2000); + // Wait for the destination to show up + await page.waitForSelector("text=LIGHTNING"); // Type 90 into the input await page.locator("#sats-input").fill("90"); @@ -147,8 +130,8 @@ test("fedmint join, receive, send", async ({ page }) => { }); await expect(confirmButton).not.toBeDisabled(); - confirmButton.click(); + await confirmButton.click(); // Wait for an h1 to appear in the dom that says "Payment Sent" - await page.waitForSelector("text=Payment Sent", { timeout: 30000 }); + await page.waitForSelector("text=Payment Sent"); }); diff --git a/e2e/load.spec.ts b/e2e/load.spec.ts index d41af63c..4774a688 100644 --- a/e2e/load.spec.ts +++ b/e2e/load.spec.ts @@ -1,22 +1,11 @@ -import { expect, test } from "@playwright/test"; +import { test } from "@playwright/test"; + +import { loadHome } from "./utils"; test.beforeEach(async ({ page }) => { await page.goto("http://localhost:3420/"); }); test("initial load", async ({ page }) => { - // Expect a title "to contain" a substring. - await expect(page).toHaveTitle(/Mutiny Wallet/); - - await expect(page.locator("header")).toContainText(["Activity"], { - timeout: 30000 - }); - - // Wait up to 30 seconds for an image element matching the selector to be visible - await page.waitForSelector("img[alt='lightning']", { timeout: 30000 }); - - // Wait for an element matching the selector to appear in DOM. - await page.waitForSelector("text=0 SATS"); - - console.log("Page loaded."); + await loadHome(page); }); diff --git a/e2e/restore.spec.ts b/e2e/restore.spec.ts index 651e6f2e..d054d046 100644 --- a/e2e/restore.spec.ts +++ b/e2e/restore.spec.ts @@ -1,49 +1,37 @@ import { expect, test } from "@playwright/test"; +import { visitSettings } from "./utils"; + test.beforeEach(async ({ page }) => { await page.goto("http://localhost:3420/"); }); test("restore from seed @slow", async ({ page }) => { - // should have 100k sats on-chain - const TEST_SEED_WORDS = - "rival hood review write spoon tide orange ill opera enrich clip acoustic"; - - // Expect a title "to contain" a substring. + // Start on the home page await expect(page).toHaveTitle(/Mutiny Wallet/); + await page.waitForSelector("text=Welcome to the Mutiny!"); - // Wait for an element matching the selector to appear in DOM. - await page.waitForSelector("text=0 SATS"); - - console.log("Page loaded."); - - // Wait for a while just to make sure we can load everything - await page.waitForTimeout(1000); - - // Navigate to settings - const settingsLink = await page.getByRole("link", { name: "Settings" }); - - settingsLink.click(); + console.log("Waiting for new wallet to be created..."); - // Wait for settings to load - await page.waitForSelector("text=Settings"); + await page.locator(`button:has-text('Import Existing')`).click(); - // Click the "Restore" link - page.click("text=Restore"); + // should have 100k sats on-chain + const TEST_SEED_WORDS = + "rival hood review write spoon tide orange ill opera enrich clip acoustic"; // There should be some warning text: "This will replace your existing wallet" await expect(page.locator("p")).toContainText([ "This will replace your existing wallet" ]); - let seedWords = TEST_SEED_WORDS.split(" "); + const seedWords = TEST_SEED_WORDS.split(" "); // Find the input field with the name "words.0" for (let i = 0; i < 12; i++) { const wordInput = await page.locator(`input[name='words.${i}']`); // Type the seed words into the input field - await wordInput.type(seedWords[i]); + await wordInput.fill(seedWords[i]); } // There should be a button with the text "Restore" and it should not be disabled @@ -54,33 +42,29 @@ test("restore from seed @slow", async ({ page }) => { // A modal should pop up, click the "Confirm" button const confirmButton = await page.locator("button", { hasText: "Confirm" }); - confirmButton.click(); - - // Wait for the wallet to load - await page.waitForSelector("img[alt='lightning']"); + await confirmButton.click(); // Eventually we should have a balance of 100k sats - await page.waitForSelector("text=100,000 SATS"); + await page.locator("text=100,000 SATS"); // Now we should clean up after ourselves and delete the wallet - settingsLink.click(); - - // Wait for settings to load - await page.waitForSelector("text=Settings"); + await visitSettings(page); // Click the "Restore" link - page.click("text=Admin Page"); + await page.click("text=Admin Page"); // Clicke the Delete Everything button - page.click("text=Delete Everything"); + await page.click("text=Delete Everything"); // A modal should pop up, click the "Confirm" button const confirmDeleteButton = await page.locator("button", { hasText: "Confirm" }); - confirmDeleteButton.click(); - // Wait for the wallet to load - // Wait for the wallet to load - await page.waitForSelector("img[alt='lightning']"); + // wait 5 seconds for no reason + await page.waitForTimeout(5000); + + await confirmDeleteButton.click(); + + await page.locator("text=Welcome to the Mutiny!"); }); diff --git a/e2e/roundtrip.spec.ts b/e2e/roundtrip.spec.ts index 0acefd5b..2450d334 100644 --- a/e2e/roundtrip.spec.ts +++ b/e2e/roundtrip.spec.ts @@ -1,21 +1,26 @@ import { expect, test } from "@playwright/test"; +import { loadHome } from "./utils"; + test.beforeEach(async ({ page }) => { await page.goto("http://localhost:3420/"); }); test("rountrip receive and send", async ({ page }) => { - // Click the receive button - await page.click("text=Receive"); + await loadHome(page); + + await page.locator("#fab").click(); + await page.locator("text=Receive").last().click(); - // Expect the url to conain receive + // Expect the url to contain receive await expect(page).toHaveURL(/.*receive/); // At least one h1 should show "0 sats" await expect(page.locator("h1")).toContainText(["0 SATS"]); // At least one h2 should show "0 USD" - await expect(page.locator("h2")).toContainText(["$0 USD"]); + // await expect(page.locator("h2")).toContainText(["$0 USD"]); + await page.waitForSelector("text=$0 USD"); // Type 100000 into the input await page.locator("#sats-input").pressSequentially("100000"); @@ -72,7 +77,8 @@ test("rountrip receive and send", async ({ page }) => { await page.click("text=Nice"); // Now we send - await page.click("text=Send"); + await page.locator("#fab").click(); + await page.locator("text=Send").click(); // In the textarea with the placeholder "bitcoin:..." type refund@lnurl-staging.mutinywallet.com const sendInput = await page.locator("input"); diff --git a/e2e/routes.spec.ts b/e2e/routes.spec.ts index 97234753..a99293b3 100644 --- a/e2e/routes.spec.ts +++ b/e2e/routes.spec.ts @@ -1,12 +1,14 @@ import { expect, Page, test } from "@playwright/test"; +import { loadHome, visitSettings } from "./utils"; + const routes = [ "/", - "/activity", "/feedback", "/gift", "/receive", "/scanner", + "/search", "/send", "/swap", "/settings" @@ -57,25 +59,13 @@ test.beforeEach(async ({ page }) => { }); test("visit each route", async ({ page }) => { - // Start on the home page - // Expect a title "to contain" a substring. - await expect(page).toHaveTitle(/Mutiny Wallet/); - - // Wait for an element matching the selector to appear in DOM. - await page.waitForSelector("text=0 SATS"); - - console.log("Page loaded."); - - // Wait for a while just to make sure we can load everything - await page.waitForTimeout(1000); + await loadHome(page); checklist.set("/", true); - await checkRoute(page, "/activity", "Activity", checklist); - await page.goBack(); + await visitSettings(page); - // Navigate to settings - await checkRoute(page, "/settings", "Settings", checklist); + checklist.set("/settings", true); // Mutiny+ await checkRoute(page, "/settings/plus", "Mutiny+", checklist); @@ -146,22 +136,43 @@ test("visit each route", async ({ page }) => { await checkRoute(page, "/settings/admin", "Secret Debug Tools", checklist); await page.goBack(); - // Go back home - await page.goBack(); - // Feedback await checkRoute(page, "/feedback", "Give us feedback!", checklist); await page.goBack(); - // Receive is covered in another test - checklist.set("/receive", true); + // Go back home + await page.goBack(); + + // Try the fab button + await page.locator("#fab").click(); + await page.locator("text=Send").click(); + await expect(page.locator("input").first()).toBeFocused(); // Send is covered in another test checklist.set("/send", true); + await page.goBack(); + + // Try the fab button again + await page.locator("#fab").click(); + // (There are actually two buttons with the "Receive text on first run) + await page.locator("text=Receive").last().click(); + + await expect(page.locator("h1").first()).toHaveText("Receive Bitcoin"); + + // Actual receive is covered in another test + checklist.set("/receive", true); + + await page.goBack(); + + // Try the fab button again + await page.locator("#fab").click(); + await page.locator("text=Scan").click(); + // Scanner - await page.locator(`a[href='/scanner']`).first().click(); - await expect(page.locator("button").first()).toHaveText("Paste Something"); + await expect( + page.locator("button:has-text('Paste Something')") + ).toBeVisible(); checklist.set("/scanner", true); // Now we have to check routes that aren't linked to directly for whatever reason diff --git a/e2e/utils.ts b/e2e/utils.ts new file mode 100644 index 00000000..d4cafd33 --- /dev/null +++ b/e2e/utils.ts @@ -0,0 +1,27 @@ +import { expect, Page } from "@playwright/test"; + +export async function loadHome(page: Page) { + // Start on the home page + await expect(page).toHaveTitle(/Mutiny Wallet/); + await page.waitForSelector("text=Welcome to the Mutiny!"); + + console.log("Waiting for new wallet to be created..."); + + await page.locator(`button:has-text('New Wallet')`).click(); + + await page.locator("text=Create your profile").first(); + + await page.locator("button:has-text('Skip for now')").click(); + + // Should have a balance up top now + await page.locator(`text=0 sats`).first(); + // Status light should be ready + await page.locator(`title="READY"`).first(); +} + +export async function visitSettings(page: Page) { + // Find an image with an alt text of "mutiny" and click it + // TODO: probably should have better ARIA stuff for this + await page.locator("img[alt='mutiny']").first().click(); + await expect(page.locator("h1").first()).toHaveText("Settings"); +} diff --git a/index.html b/index.html index 3c0d91be..5be48c9f 100644 --- a/index.html +++ b/index.html @@ -47,6 +47,9 @@ #no-script { margin: 1rem; } + body { + background-color: hsla(0, 0%, 5%, 1); + } @@ -63,8 +66,10 @@ Please update or enable WebAssembly to run this app.

- If you're running iOS in lockdown mode you'll need to add an - exception for Mutiny Wallet. + If you're running iOS in lockdown mode you'll need to + add an exception for Mutiny Wallet.

-
+