diff --git a/src/frontend/src/lib/locales/de.po b/src/frontend/src/lib/locales/de.po index 2c752ee5f0..4cbe0e7e86 100644 --- a/src/frontend/src/lib/locales/de.po +++ b/src/frontend/src/lib/locales/de.po @@ -16,6 +16,9 @@ msgstr "" msgid "{0} logo" msgstr "" +msgid "{0} user" +msgstr "" + msgid "{application} has moved to the new Internet Identity" msgstr "" @@ -359,9 +362,6 @@ msgstr "Wie funktionieren Passkeys?" msgid "How does Internet Identity compare to other Web3 authentication tools?" msgstr "Wie vergleicht sich Internet Identity mit anderen Web3-Authentifizierungstools?" -msgid "How it works" -msgstr "So funktioniert's" - msgid "How to stay secure" msgstr "So bleiben Sie sicher" @@ -662,8 +662,8 @@ msgstr "" msgid "Please go back to your <0>existing device and choose <1>Start over to try again." msgstr "Gehen Sie zu Ihrem <0>bestehenden Gerät zurück und wählen Sie <1>Neu starten, um es erneut zu versuchen." -msgid "Powered by Internet Identity" -msgstr "Unterstützt von Internet Identity" +msgid "Powered by" +msgstr "" msgid "Real Privacy" msgstr "Echte Privatsphäre" @@ -896,9 +896,6 @@ msgstr "" msgid "This passkey is no longer associated with any identity." msgstr "" -msgid "This takes a few seconds." -msgstr "Dies dauert einige Sekunden." - msgid "This YubiKey has older firmware (5.1) that's incompatible." msgstr "" diff --git a/src/frontend/src/lib/locales/en.po b/src/frontend/src/lib/locales/en.po index 8c271c01cb..84970933f5 100644 --- a/src/frontend/src/lib/locales/en.po +++ b/src/frontend/src/lib/locales/en.po @@ -16,6 +16,9 @@ msgstr "" msgid "{0} logo" msgstr "{0} logo" +msgid "{0} user" +msgstr "{0} user" + msgid "{application} has moved to the new Internet Identity" msgstr "{application} has moved to the new Internet Identity" @@ -359,9 +362,6 @@ msgstr "How do passkeys work?" msgid "How does Internet Identity compare to other Web3 authentication tools?" msgstr "How does Internet Identity compare to other Web3 authentication tools?" -msgid "How it works" -msgstr "How it works" - msgid "How to stay secure" msgstr "How to stay secure" @@ -662,8 +662,8 @@ msgstr "Pick something recognizable, like your name" msgid "Please go back to your <0>existing device and choose <1>Start over to try again." msgstr "Please go back to your <0>existing device and choose <1>Start over to try again." -msgid "Powered by Internet Identity" -msgstr "Powered by Internet Identity" +msgid "Powered by" +msgstr "Powered by" msgid "Real Privacy" msgstr "Real Privacy" @@ -896,9 +896,6 @@ msgstr "This may take a few seconds" msgid "This passkey is no longer associated with any identity." msgstr "This passkey is no longer associated with any identity." -msgid "This takes a few seconds." -msgstr "This takes a few seconds." - msgid "This YubiKey has older firmware (5.1) that's incompatible." msgstr "This YubiKey has older firmware (5.1) that's incompatible." diff --git a/src/frontend/src/lib/locales/es.po b/src/frontend/src/lib/locales/es.po index 3d5f5de73d..1c965e1df6 100644 --- a/src/frontend/src/lib/locales/es.po +++ b/src/frontend/src/lib/locales/es.po @@ -16,6 +16,9 @@ msgstr "" msgid "{0} logo" msgstr "" +msgid "{0} user" +msgstr "" + msgid "{application} has moved to the new Internet Identity" msgstr "" @@ -359,9 +362,6 @@ msgstr "¿Cómo funcionan las passkeys?" msgid "How does Internet Identity compare to other Web3 authentication tools?" msgstr "¿Cómo se compara Internet Identity con otras herramientas de autenticación Web3?" -msgid "How it works" -msgstr "Cómo funciona" - msgid "How to stay secure" msgstr "Cómo mantenerse seguro" @@ -662,8 +662,8 @@ msgstr "Elija algo que puedas reconocer fácilmente, como tu nombre" msgid "Please go back to your <0>existing device and choose <1>Start over to try again." msgstr "Vuelve a tu <0>dispositivo existente y elige <1>Empezar de nuevo para volver a intentarlo." -msgid "Powered by Internet Identity" -msgstr "Con la tecnología de Internet Identity" +msgid "Powered by" +msgstr "" msgid "Real Privacy" msgstr "Privacidad Verdadera" @@ -896,9 +896,6 @@ msgstr "" msgid "This passkey is no longer associated with any identity." msgstr "" -msgid "This takes a few seconds." -msgstr "Esto tarda unos segundos." - msgid "This YubiKey has older firmware (5.1) that's incompatible." msgstr "" diff --git a/src/frontend/src/lib/locales/id.po b/src/frontend/src/lib/locales/id.po index 579a12ffaf..d9d8992e37 100644 --- a/src/frontend/src/lib/locales/id.po +++ b/src/frontend/src/lib/locales/id.po @@ -16,6 +16,9 @@ msgstr "" msgid "{0} logo" msgstr "" +msgid "{0} user" +msgstr "" + msgid "{application} has moved to the new Internet Identity" msgstr "" @@ -359,9 +362,6 @@ msgstr "Bagaimana cara kerja passkey?" msgid "How does Internet Identity compare to other Web3 authentication tools?" msgstr "Bagaimana Internet Identity dibandingkan dengan alat autentikasi Web3 lainnya?" -msgid "How it works" -msgstr "Cara kerja" - msgid "How to stay secure" msgstr "Cara tetap aman" @@ -662,8 +662,8 @@ msgstr "" msgid "Please go back to your <0>existing device and choose <1>Start over to try again." msgstr "Silakan kembali ke <0>perangkat Anda yang ada dan pilih <1>Mulai ulang untuk mencoba lagi." -msgid "Powered by Internet Identity" -msgstr "Didukung oleh Internet Identity" +msgid "Powered by" +msgstr "" msgid "Real Privacy" msgstr "Privasi nyata" @@ -896,9 +896,6 @@ msgstr "" msgid "This passkey is no longer associated with any identity." msgstr "" -msgid "This takes a few seconds." -msgstr "Proses ini butuh beberapa detik." - msgid "This YubiKey has older firmware (5.1) that's incompatible." msgstr "" diff --git a/src/frontend/tests/e2e-playwright/dashboard/addPasskeys.spec.ts b/src/frontend/tests/e2e-playwright/dashboard/addPasskeys.spec.ts deleted file mode 100644 index effae40c35..0000000000 --- a/src/frontend/tests/e2e-playwright/dashboard/addPasskeys.spec.ts +++ /dev/null @@ -1,315 +0,0 @@ -import { expect, test } from "@playwright/test"; -import { - addPasskeyCurrentDevice, - clearStorage, - createNewIdentityInII, - dummyAuth, - II_URL, - renamePasskey, - signOut, -} from "../utils"; - -const TEST_USER_NAME = "Test User"; - -test("User can log into the dashboard and add a new passkey from the same device and log in with it after clearing storage", async ({ - page, - context, -}) => { - const auth = dummyAuth(); - await page.goto(II_URL); - await page.getByRole("button", { name: "Sign in" }).click(); - await createNewIdentityInII(page, TEST_USER_NAME, auth); - await page.waitForURL(II_URL + "/manage"); - await clearStorage(page); - await page.goto(II_URL); - await page.getByRole("button", { name: "Sign in" }).click(); - await page.getByRole("button", { name: "Continue with passkey" }).click(); - auth(page); - await page.getByRole("button", { name: "Use existing identity" }).click(); - - // Verify we're at the dashboard - await page.waitForURL(II_URL + "/manage"); - - // Navigate to access methods - const menuButton = page.getByRole("button", { name: "Open menu" }); - if (await menuButton.isVisible()) { - await menuButton.click(); - } - await page.getByRole("link", { name: "Access and recovery" }).click(); - - // Verify we have one passkey and rename it - await expect( - page.getByRole("listitem").filter({ hasText: "Passkey" }), - ).toHaveCount(1); - await renamePasskey(page, "Unknown", "Old passkey"); - - // Start the "add passkey" flow - const auth2 = dummyAuth(); - await addPasskeyCurrentDevice(page, auth2); - await renamePasskey(page, "Unknown", "New passkey"); - - // Verify we have two passkeys - await expect( - page.getByRole("listitem").filter({ hasText: "Passkey" }), - ).toHaveCount(2); - - // Verify that the new passkey is not the one currently in use - await expect( - page - .getByRole("listitem") - .filter({ hasText: "Old passkey" }) - .getByText("Right now"), - ).toBeVisible(); - await expect( - page - .getByRole("listitem") - .filter({ hasText: "New passkey" }) - .getByText("Right now"), - ).toBeHidden(); - - await signOut(page); - - // Clear storage and log in again with new passkey - await clearStorage(page); - const newPage = await context.newPage(); - await newPage.goto(II_URL); - await newPage.getByRole("button", { name: "Sign in" }).click(); - await newPage.getByRole("button", { name: "Continue with passkey" }).click(); - auth2(newPage); - await newPage.getByRole("button", { name: "Use existing identity" }).click(); - await newPage.waitForURL(II_URL + "/manage"); - - // Navigate to access methods - const newMenuButton = newPage.getByRole("button", { name: "Open menu" }); - if (await newMenuButton.isVisible()) { - await newMenuButton.click(); - } - await newPage.getByRole("link", { name: "Access and recovery" }).click(); - - // Verify that new passkey is the one currently in use - await expect( - newPage - .getByRole("listitem") - .filter({ hasText: "Old passkey" }) - .getByText("Right now"), - ).toBeHidden(); - await expect( - newPage - .getByRole("listitem") - .filter({ hasText: "New passkey" }) - .getByText("Right now"), - ).toBeVisible(); - await newPage.close(); -}); - -test("User can log in the dashboard and add a new passkey from another device", async ({ - page, - browser, -}) => { - const auth = dummyAuth(); - await page.goto(II_URL); - await page.getByRole("button", { name: "Sign in" }).click(); - await createNewIdentityInII(page, TEST_USER_NAME, auth); - await page.waitForURL(II_URL + "/manage"); - await clearStorage(page); - await page.goto(II_URL); - await page.getByRole("button", { name: "Sign in" }).click(); - await page.getByRole("button", { name: "Continue with passkey" }).click(); - auth(page); - await page.getByRole("button", { name: "Use existing identity" }).click(); - - // Verify we're at the dashboard - await page.waitForURL(II_URL + "/manage"); - - // Navigate to access methods - const menuButton = page.getByRole("button", { name: "Open menu" }); - if (await menuButton.isVisible()) { - await menuButton.click(); - } - await page.getByRole("link", { name: "Access and recovery" }).click(); - - // Verify we have one passkey - await expect(page.getByText("Unknown")).toHaveCount(1); - - // Start the "add passkey" flow - await page.getByRole("button", { name: "Add new" }).click(); - await page.getByRole("button", { name: "Continue with passkey" }).click(); - await page - .getByRole("button", { name: "Continue on another device" }) - .click(); - - const linkToPair = await page.getByLabel("Pairing link").innerText(); - - const newContext = await browser.newContext(); - const linkPage = await newContext.newPage(); - await linkPage.goto(`https://${linkToPair}`); - - const authorizeNewDevicePromise = page - .getByRole("heading", { level: 1, name: "Authorize new device" }) - .waitFor(); - - await linkPage.getByLabel("Confirmation Code").waitFor(); - await linkPage - .getByRole("button", { name: "Generating code..." }) - .waitFor({ state: "hidden" }); - await authorizeNewDevicePromise; - - const confirmationCode = await linkPage - .getByLabel("Confirmation Code") - .innerText(); - const confirmationCodeArray = confirmationCode.split(""); - - for (let i = 0; i < confirmationCodeArray.length; i++) { - const code = confirmationCodeArray[i]; - await page.getByLabel(`Code input ${i}`).fill(code); - } - - await page.getByRole("button", { name: "Confirm sign-in" }).click(); - - await page - .getByRole("heading", { level: 1, name: "Continue on your new device" }) - .waitFor(); - - await linkPage - .getByRole("heading", { level: 1, name: "Confirm your sign-in" }) - .waitFor(); - - // Create and register new passkey - const authLinkPage = dummyAuth(); - authLinkPage(linkPage); - await linkPage.getByRole("button", { name: "Create passkey" }).click(); - - await linkPage - .getByRole("heading", { level: 1, name: "Confirm your sign-in" }) - .waitFor({ state: "hidden" }); - - await page - .getByRole("heading", { level: 1, name: "Continue on your new device" }) - .waitFor({ state: "hidden" }); - - // Verify that we now have two passkeys - await expect( - page.getByRole("listitem").filter({ hasText: "Passkey" }), - ).toHaveCount(2); -}); - -test("User can add a new passkey and use it with cached identity without clearing storage", async ({ - page, - context, -}) => { - const auth = dummyAuth(); - await page.goto(II_URL); - await page.getByRole("button", { name: "Sign in" }).click(); - await createNewIdentityInII(page, TEST_USER_NAME, auth); - await page.waitForURL(II_URL + "/manage"); - - // Verify we're at the dashboard - await page.waitForURL(II_URL + "/manage"); - - // Navigate to access methods - const menuButton = page.getByRole("button", { name: "Open menu" }); - if (await menuButton.isVisible()) { - await menuButton.click(); - } - await page.getByRole("link", { name: "Access and recovery" }).click(); - - // Verify we have one passkey and rename it - await expect( - page.getByRole("listitem").filter({ hasText: "Passkey" }), - ).toHaveCount(1); - await renamePasskey(page, "Unknown", "Old passkey"); - - // Start the "add passkey" flow - const auth2 = dummyAuth(); - await addPasskeyCurrentDevice(page, auth2); - await renamePasskey(page, "Unknown", "New Passkey"); - - // Verify we have two passkeys - await expect( - page.getByRole("listitem").filter({ hasText: "Passkey" }), - ).toHaveCount(2); - - // Verify that the new passkey is not the one currently in use - await expect( - page - .getByRole("listitem") - .filter({ hasText: "Old passkey" }) - .getByText("Right now"), - ).toBeVisible(); - await expect( - page - .getByRole("listitem") - .filter({ hasText: "New passkey" }) - .getByText("Right now"), - ).toBeHidden(); - - await signOut(page); - - // Log in again with new passkey WITHOUT clearing storage (key difference) - // This should use the cached identity - const newPage = await context.newPage(); - await newPage.goto(II_URL); - await newPage.getByRole("button", { name: "Switch identity" }).click(); - - // Click on the cached identity button directly - // But use the new passkey to authenticate - auth2(newPage); - await newPage.getByRole("button", { name: "Manage identity" }).click(); - - // Verify we're logged in with the new passkey - await newPage.waitForURL(II_URL + "/manage"); - await expect( - newPage.getByRole("heading", { - name: new RegExp(`Welcome, ${TEST_USER_NAME}!`), - }), - ).toBeVisible(); - - await newPage.close(); -}); - -test("User can log into the dashboard and add up to 15 additional passkeys", async ({ - page, -}) => { - const auth = dummyAuth(); - await page.goto(II_URL); - await page.getByRole("button", { name: "Sign in" }).click(); - await createNewIdentityInII(page, TEST_USER_NAME, auth); - await page.waitForURL(II_URL + "/manage"); - await clearStorage(page); - await page.goto(II_URL); - await page.getByRole("button", { name: "Sign in" }).click(); - await page.getByRole("button", { name: "Continue with passkey" }).click(); - auth(page); - await page.getByRole("button", { name: "Use existing identity" }).click(); - - // Verify we're at the dashboard - await page.waitForURL(II_URL + "/manage"); - - // Navigate to access methods - const menuButton = page.getByRole("button", { name: "Open menu" }); - if (await menuButton.isVisible()) { - await menuButton.click(); - } - await page.getByRole("link", { name: "Access and recovery" }).click(); - - // Verify we have one passkey - await expect( - page.getByRole("listitem").filter({ hasText: "Passkey" }), - ).toHaveCount(1); - - // Add 15 more passkeys - for (let i = 0; i < 15; i++) { - await addPasskeyCurrentDevice(page, dummyAuth()); - } - - // Verify we have 16 passkeys - await expect( - page.getByRole("listitem").filter({ hasText: "Passkey" }), - ).toHaveCount(16); - - // Verify we cannot add more passkeys - await page.getByRole("button", { name: "Add new" }).click(); - await expect( - page.getByRole("button", { name: "Continue with passkey" }), - ).toBeDisabled(); -}); diff --git a/src/frontend/tests/e2e-playwright/dashboard/removePasskey.spec.ts b/src/frontend/tests/e2e-playwright/dashboard/removePasskey.spec.ts deleted file mode 100644 index 42a8fb1ad5..0000000000 --- a/src/frontend/tests/e2e-playwright/dashboard/removePasskey.spec.ts +++ /dev/null @@ -1,231 +0,0 @@ -import { expect, test } from "@playwright/test"; -import { - clearStorage, - createNewIdentityInII, - dummyAuth, - II_URL, - addPasskeyCurrentDevice, - renamePasskey, - removePasskey, -} from "../utils"; - -const TEST_USER_NAME = "Test User"; - -test("User can remove a passkey when they have multiple access methods", async ({ - page, -}) => { - const auth = dummyAuth(); - await page.goto(II_URL); - await page.getByRole("button", { name: "Sign in" }).click(); - await createNewIdentityInII(page, TEST_USER_NAME, auth); - await page.waitForURL(II_URL + "/manage"); - await clearStorage(page); - await page.goto(II_URL); - await page.getByRole("button", { name: "Sign in" }).click(); - await page.getByRole("button", { name: "Continue with passkey" }).click(); - auth(page); - await page.getByRole("button", { name: "Use existing identity" }).click(); - - // Verify we're at the dashboard - await page.waitForURL(II_URL + "/manage"); - - // Navigate to access methods - const menuButton = page.getByRole("button", { name: "Open menu" }); - if (await menuButton.isVisible()) { - await menuButton.click(); - } - await page.getByRole("link", { name: "Access and recovery" }).click(); - - // Assign name to current passkey - await renamePasskey(page, "Unknown", "Current passkey"); - - // Verify we have one passkey - await expect( - page.getByRole("listitem").filter({ hasText: "Passkey" }), - ).toHaveCount(1); - - // Start the "add passkey" flow to create a second passkey - await addPasskeyCurrentDevice(page, dummyAuth()); - await renamePasskey(page, "Unknown", "New passkey"); - - // Verify that we now have two passkeys - await expect( - page.getByRole("listitem").filter({ hasText: "Passkey" }), - ).toHaveCount(2); - - // Remove new passkey - await removePasskey(page, "New passkey", false); - - // Verify that we now have one passkey - await expect( - page.getByRole("listitem").filter({ hasText: "Passkey" }), - ).toHaveCount(1); - - // Verify we're still logged in and at the dashboard - await expect(page).toHaveURL(II_URL + "/manage/access"); -}); - -test("User cannot remove passkey if they only have one access method", async ({ - page, -}) => { - const auth = dummyAuth(); - await page.goto(II_URL); - await page.getByRole("button", { name: "Sign in" }).click(); - await createNewIdentityInII(page, TEST_USER_NAME, auth); - await page.waitForURL(II_URL + "/manage"); - await clearStorage(page); - await page.goto(II_URL); - await page.getByRole("button", { name: "Sign in" }).click(); - await page.getByRole("button", { name: "Continue with passkey" }).click(); - auth(page); - await page.getByRole("button", { name: "Use existing identity" }).click(); - - // Verify we're at the dashboard - await page.waitForURL(II_URL + "/manage"); - - // Navigate to access methods - const menuButton = page.getByRole("button", { name: "Open menu" }); - if (await menuButton.isVisible()) { - await menuButton.click(); - } - await page.getByRole("link", { name: "Access and recovery" }).click(); - - // Verify we have one passkey - await expect( - page.getByRole("listitem").filter({ hasText: "Passkey" }), - ).toHaveCount(1); - - // Verify that the remove button is not visible when there's only one access method - await page - .getByRole("listitem") - .filter({ hasText: "Unknown" }) - .getByRole("button", { name: "More options" }) - .click(); - await expect(page.getByRole("menuitem", { name: "Remove" })).toBeHidden(); - - // Verify that the rename button is still visible (to ensure we're looking at the right area) - await expect(page.getByRole("menuitem", { name: "Rename" })).toBeVisible(); -}); - -test("User is logged out after removing the passkey they used to authenticate", async ({ - page, -}) => { - const auth = dummyAuth(); - await page.goto(II_URL); - await page.getByRole("button", { name: "Sign in" }).click(); - await createNewIdentityInII(page, TEST_USER_NAME, auth); - await page.waitForURL(II_URL + "/manage"); - await clearStorage(page); - await page.goto(II_URL); - await page.getByRole("button", { name: "Sign in" }).click(); - await page.getByRole("button", { name: "Continue with passkey" }).click(); - auth(page); - await page.getByRole("button", { name: "Use existing identity" }).click(); - - // Verify we're at the dashboard - await page.waitForURL(II_URL + "/manage"); - - // Navigate to access methods - const menuButton = page.getByRole("button", { name: "Open menu" }); - if (await menuButton.isVisible()) { - await menuButton.click(); - } - await page.getByRole("link", { name: "Access and recovery" }).click(); - - // Assign name to current passkey - await renamePasskey(page, "Unknown", "Current passkey"); - - // Verify we have one passkey - await expect( - page.getByRole("listitem").filter({ hasText: "Passkey" }), - ).toHaveCount(1); - - // Start the "add passkey" flow to create a second passkey - await addPasskeyCurrentDevice(page, dummyAuth()); - await renamePasskey(page, "Unknown", "New passkey"); - - // Verify that we now have two passkeys - await expect( - page.getByRole("listitem").filter({ hasText: "Passkey" }), - ).toHaveCount(2); - - // Remove current passkey - await removePasskey(page, "Current passkey", true); - - // Verify the user is logged out and redirected to the landing page - // The URL should change from /manage to landing page - await page.waitForURL(II_URL); -}); - -test("User can cancel passkey removal", async ({ page }) => { - const auth = dummyAuth(); - await page.goto(II_URL); - await page.getByRole("button", { name: "Sign in" }).click(); - await createNewIdentityInII(page, TEST_USER_NAME, auth); - await page.waitForURL(II_URL + "/manage"); - await clearStorage(page); - await page.goto(II_URL); - await page.getByRole("button", { name: "Sign in" }).click(); - await page.getByRole("button", { name: "Continue with passkey" }).click(); - auth(page); - await page.getByRole("button", { name: "Use existing identity" }).click(); - - // Verify we're at the dashboard - await page.waitForURL(II_URL + "/manage"); - - // Navigate to access methods - const menuButton = page.getByRole("button", { name: "Open menu" }); - if (await menuButton.isVisible()) { - await menuButton.click(); - } - await page.getByRole("link", { name: "Access and recovery" }).click(); - - // Assign name to current passkey - await renamePasskey(page, "Unknown", "Current passkey"); - - // Verify we have one passkey - await expect( - page.getByRole("listitem").filter({ hasText: "Passkey" }), - ).toHaveCount(1); - - await addPasskeyCurrentDevice(page, dummyAuth()); - - // Verify that we now have two passkeys - await expect( - page.getByRole("listitem").filter({ hasText: "Passkey" }), - ).toHaveCount(2); - - // Open the remove passkey dialog for the current passkey - await page - .getByRole("listitem") - .filter({ hasText: "Current passkey" }) - .getByRole("button", { name: "More options" }) - .click(); - await page.getByRole("menuitem", { name: "Remove" }).click(); - - // Wait for the remove dialog to open - await expect( - page.getByRole("heading", { name: "Are you sure?" }), - ).toBeVisible(); - await expect( - page.getByText( - "Removing this passkey means you won't be able to use it to sign in anymore. You can always add a new one later.", - ), - ).toBeVisible(); - - // Cancel the removal - await page.getByRole("button", { name: "Cancel" }).click(); - - // Wait for the remove dialog to close - await expect( - page.getByRole("heading", { name: "Are you sure?" }), - ).toBeHidden(); - - // Verify that we still have two passkeys - await expect( - page.getByRole("listitem").filter({ hasText: "Passkey" }), - ).toHaveCount(2); - - // Verify we're still logged in and at the dashboard - await expect(page).toHaveURL(II_URL + "/manage/access"); -}); diff --git a/src/frontend/tests/e2e-playwright/dashboard/renamePasskeys.spec.ts b/src/frontend/tests/e2e-playwright/dashboard/renamePasskeys.spec.ts deleted file mode 100644 index 1977ef9fa6..0000000000 --- a/src/frontend/tests/e2e-playwright/dashboard/renamePasskeys.spec.ts +++ /dev/null @@ -1,213 +0,0 @@ -import { expect, test } from "@playwright/test"; -import { - addPasskeyCurrentDevice, - clearStorage, - createNewIdentityInII, - dummyAuth, - II_URL, - renamePasskey, -} from "../utils"; - -const TEST_USER_NAME = "Test User"; - -test("User can rename the current passkey used for authentication", async ({ - page, -}) => { - const auth = dummyAuth(); - await page.goto(II_URL); - await page.getByRole("button", { name: "Sign in" }).click(); - await createNewIdentityInII(page, TEST_USER_NAME, auth); - await page.waitForURL(II_URL + "/manage"); - await clearStorage(page); - await page.goto(II_URL); - await page.getByRole("button", { name: "Sign in" }).click(); - await page.getByRole("button", { name: "Continue with passkey" }).click(); - auth(page); - await page.getByRole("button", { name: "Use existing identity" }).click(); - - // Verify we're at the dashboard - await page.waitForURL(II_URL + "/manage"); - - // Navigate to access methods - const menuButton = page.getByRole("button", { name: "Open menu" }); - if (await menuButton.isVisible()) { - await menuButton.click(); - } - await page.getByRole("link", { name: "Access and recovery" }).click(); - - // Verify we have one passkey - await expect( - page.getByRole("listitem").filter({ hasText: "Passkey" }), - ).toHaveCount(1); - - // Rename passkey - await renamePasskey(page, "Unknown", "My Main Passkey"); - - // Verify passkey has been renamed - await expect( - page - .getByRole("listitem") - .filter({ hasText: "Passkey" }) - .filter({ hasText: "Unknown" }), - ).toBeHidden(); - await expect( - page - .getByRole("listitem") - .filter({ hasText: "Passkey" }) - .filter({ hasText: "My Main Passkey" }), - ).toBeVisible(); -}); - -test("User can rename a newly added passkey from the same device", async ({ - page, -}) => { - const auth = dummyAuth(); - await page.goto(II_URL); - await page.getByRole("button", { name: "Sign in" }).click(); - await createNewIdentityInII(page, TEST_USER_NAME, auth); - await page.waitForURL(II_URL + "/manage"); - await clearStorage(page); - await page.goto(II_URL); - await page.getByRole("button", { name: "Sign in" }).click(); - await page.getByRole("button", { name: "Continue with passkey" }).click(); - auth(page); - await page.getByRole("button", { name: "Use existing identity" }).click(); - - // Verify we're at the dashboard - await page.waitForURL(II_URL + "/manage"); - - // Navigate to access methods - const menuButton = page.getByRole("button", { name: "Open menu" }); - if (await menuButton.isVisible()) { - await menuButton.click(); - } - await page.getByRole("link", { name: "Access and recovery" }).click(); - - // Verify we have one passkey - await expect( - page.getByRole("listitem").filter({ hasText: "Passkey" }), - ).toHaveCount(1); - - // Rename passkey to current passkey - await renamePasskey(page, "Unknown", "Current passkey"); - - // Add another passkey - await addPasskeyCurrentDevice(page, dummyAuth()); - await renamePasskey(page, "Unknown", "New passkey"); - - // Verify that we now have two passkeys - await expect( - page.getByRole("listitem").filter({ hasText: "Passkey" }), - ).toHaveCount(2); - - // Verify we now have both passkeys with different names - await expect( - page - .getByRole("listitem") - .filter({ hasText: "Passkey" }) - .filter({ hasText: "Current passkey" }), - ).toBeVisible(); - await expect( - page - .getByRole("listitem") - .filter({ hasText: "Passkey" }) - .filter({ hasText: "New passkey" }), - ).toBeVisible(); -}); - -test("User cannot rename passkey to an empty name nor is it renamed on cancel", async ({ - page, -}) => { - const auth = dummyAuth(); - await page.goto(II_URL); - await page.getByRole("button", { name: "Sign in" }).click(); - await createNewIdentityInII(page, TEST_USER_NAME, auth); - await page.waitForURL(II_URL + "/manage"); - await clearStorage(page); - await page.goto(II_URL); - await page.getByRole("button", { name: "Sign in" }).click(); - await page.getByRole("button", { name: "Continue with passkey" }).click(); - auth(page); - await page.getByRole("button", { name: "Use existing identity" }).click(); - - // Verify we're at the dashboard - await page.waitForURL(II_URL + "/manage"); - - // Navigate to access methods - const menuButton = page.getByRole("button", { name: "Open menu" }); - if (await menuButton.isVisible()) { - await menuButton.click(); - } - await page.getByRole("link", { name: "Access and recovery" }).click(); - - // Verify we have one passkey - await expect( - page.getByRole("listitem").filter({ hasText: "Passkey" }), - ).toHaveCount(1); - - // Assign it an initial name before moving forward - await renamePasskey(page, "Unknown", "My passkey"); - - // Open the rename passkey dialog - await page - .getByRole("listitem") - .filter({ hasText: "Passkey" }) - .filter({ hasText: "My passkey" }) - .getByRole("button", { name: "More options" }) - .click(); - await page.getByRole("menuitem", { name: "Rename" }).click(); - - // Wait for the rename dialog to open - await expect( - page.getByRole("heading", { name: "Rename passkey" }), - ).toBeVisible(); - - // Expect input to be prefilled with current name - const input = page.getByRole("textbox"); - await expect(input).toHaveValue("My passkey"); - - // Expect save button to be disabled since it's unchanged - await expect( - page.getByRole("button", { name: "Save changes" }), - ).toBeDisabled(); - - // Clear input - await input.clear(); - - // Expect save button to be disabled since the input is empty - await expect( - page.getByRole("button", { name: "Save changes" }), - ).toBeDisabled(); - - // Try entering only whitespace - await input.fill(" "); - - // Expect save button to be disabled since the input is still empty - await expect( - page.getByRole("button", { name: "Save changes" }), - ).toBeDisabled(); - - // Enter valid name - await input.fill("Valid name"); - - // Expect save button to be enabled now - await expect( - page.getByRole("button", { name: "Save changes" }), - ).toBeEnabled(); - - // Cancel the dialog - await page.getByRole("button", { name: "Cancel" }).click(); - - // Wait for the rename dialog to close - await expect( - page.getByRole("heading", { name: "Rename passkey" }), - ).toBeHidden(); - - // Verify the original name is still there - await expect( - page - .getByRole("listitem") - .filter({ hasText: "Passkey" }) - .filter({ hasText: "My passkey" }), - ).toBeVisible(); -}); diff --git a/src/frontend/tests/e2e-playwright/fixtures/identity.ts b/src/frontend/tests/e2e-playwright/fixtures/identity.ts index d9bd9979c2..7aec3f5598 100644 --- a/src/frontend/tests/e2e-playwright/fixtures/identity.ts +++ b/src/frontend/tests/e2e-playwright/fixtures/identity.ts @@ -1,4 +1,4 @@ -import { Page, test as base } from "@playwright/test"; +import { Page, test as base, expect } from "@playwright/test"; import { dummyAuth, DummyAuthFn, getRandomIndex, II_URL } from "../utils"; import { Actor, type ActorSubclass, HttpAgent } from "@icp-sdk/core/agent"; import type { _SERVICE } from "$lib/generated/internet_identity_types"; @@ -47,21 +47,21 @@ class IdentityWizard { async signIn(auth: DummyAuthFn): Promise { await this.#goto(); auth(this.#page); - await this.#page - .getByRole("button", { name: "Use existing identity" }) - .click(); - await this.#page.waitForURL((url) => url.pathname.startsWith("/manage")); + const dialog = this.#page.getByRole("dialog"); + await expect(dialog).toBeVisible(); + await dialog.getByRole("button", { name: "Use existing identity" }).click(); + await expect(dialog).toBeHidden(); } async signUp(auth: DummyAuthFn, name: string): Promise { await this.#goto(); - await this.#page - .getByRole("button", { name: "Create new identity" }) - .click(); + const dialog = this.#page.getByRole("dialog"); + await expect(dialog).toBeVisible(); + await dialog.getByRole("button", { name: "Create new identity" }).click(); await this.#page.getByLabel("Identity name").fill(name); auth(this.#page); await this.#page.getByRole("button", { name: "Create identity" }).click(); - await this.#page.waitForURL((url) => url.pathname.startsWith("/manage")); + await expect(dialog).toBeHidden(); } /** @@ -130,6 +130,10 @@ export const test = base.extend<{ actorForIdentity: ( identityNumber: bigint, ) => Promise>; + replaceAuthForIdentity: ( + identityNumber: bigint, + newDummyAuthIndex: bigint, + ) => void; }>({ identityConfig: { createIdentities: [ @@ -200,4 +204,14 @@ export const test = base.extend<{ identity.dummyAuthIndex, ); }), + replaceAuthForIdentity: ({ identities }, use) => + use((identityNumber: bigint, newDummyAuthIndex: bigint) => { + const identity = identities.find( + (identity) => identity.identityNumber === identityNumber, + ); + if (identity === undefined) { + throw new Error("Identity not found"); + } + identity.dummyAuthIndex = newDummyAuthIndex; + }), }); diff --git a/src/frontend/tests/e2e-playwright/fixtures/index.ts b/src/frontend/tests/e2e-playwright/fixtures/index.ts index 87370a06c0..e8941ca90d 100644 --- a/src/frontend/tests/e2e-playwright/fixtures/index.ts +++ b/src/frontend/tests/e2e-playwright/fixtures/index.ts @@ -4,6 +4,7 @@ import { test as openIdTest } from "./openid"; import { test as authorizeTest } from "./authorize"; import { test as recoveryPageTest } from "./recoveryPage"; import { test as managePageTest } from "./managePage"; +import { test as manageAccessPageTest } from "./manageAccessPage"; import { test as manageRecoveryPageTest } from "./manageRecoveryPage"; export const test = mergeTests( @@ -12,5 +13,6 @@ export const test = mergeTests( authorizeTest, recoveryPageTest, managePageTest, + manageAccessPageTest, manageRecoveryPageTest, ); diff --git a/src/frontend/tests/e2e-playwright/fixtures/manageAccessPage.ts b/src/frontend/tests/e2e-playwright/fixtures/manageAccessPage.ts new file mode 100644 index 0000000000..0a5eca1cc7 --- /dev/null +++ b/src/frontend/tests/e2e-playwright/fixtures/manageAccessPage.ts @@ -0,0 +1,227 @@ +import { expect, Locator, Page, test as base } from "@playwright/test"; +import { DummyAuthFn, II_URL } from "../utils"; + +export const DEFAULT_PASSKEY_NAME = "Unknown"; + +class RenamePasskeyDialog { + readonly #dialog: Locator; + readonly #onChange: (value: string) => void; + + constructor(dialog: Locator, onChange: (value: string) => void) { + this.#dialog = dialog; + this.#onChange = onChange; + } + + get locator(): Locator { + return this.#dialog.filter({ + has: this.#dialog.page().getByRole("heading", { name: "Rename passkey" }), + }); + } + + async fill(value: string): Promise { + await this.locator + .getByRole("textbox", { name: "Passkey name" }) + .fill(value); + } + + async submit(): Promise { + const value = await this.locator + .getByRole("textbox", { name: "Passkey name" }) + .inputValue(); + await this.locator.getByRole("button", { name: "Save changes" }).click(); + this.#onChange(value); + } + + async assertSubmitDisabled(): Promise { + await expect( + this.locator.getByRole("button", { name: "Save changes" }), + ).toBeDisabled(); + } + + async close(): Promise { + await this.locator.getByRole("button", { name: "Close" }).click(); + } +} + +class RemovePasskeyDialog { + readonly #dialog: Locator; + + constructor(dialog: Locator) { + this.#dialog = dialog; + } + + get locator(): Locator { + return this.#dialog.filter({ + has: this.#dialog.page().getByRole("heading", { name: "Are you sure?" }), + }); + } + + async confirm(): Promise { + await this.locator.getByRole("button", { name: "Remove passkey" }).click(); + } + + async cancel(): Promise { + await this.locator.getByRole("button", { name: "Cancel" }).click(); + } + + async assertSignOutWarningShown(): Promise { + await expect(this.locator).toHaveText(/you will be signed out/); + } +} + +class PasskeyItem { + readonly #item: Locator; + #name: string; + + constructor(item: Locator, name: string) { + this.#item = item; + this.#name = name; + } + + get locator(): Locator { + return this.#item.filter({ hasText: this.#name }).first(); + } + + async rename(fn: (dialog: RenamePasskeyDialog) => Promise): Promise { + await this.locator.getByRole("button", { name: "More options" }).click(); + await this.locator + .getByRole("menu") + .getByRole("menuitem", { name: "Rename" }) + .click(); + const dialog = new RenamePasskeyDialog( + this.locator.page().getByRole("dialog"), + (value) => (this.#name = value), + ); + await expect(dialog.locator).toBeVisible(); + const value = await fn(dialog); + await expect(dialog.locator).toBeHidden(); + return value; + } + + async remove(fn: (dialog: RemovePasskeyDialog) => Promise): Promise { + await this.locator.getByRole("button", { name: "More options" }).click(); + await this.locator + .getByRole("menu") + .getByRole("menuitem", { name: "Remove" }) + .click(); + const dialog = new RemovePasskeyDialog( + this.locator.page().getByRole("dialog"), + ); + await expect(dialog.locator).toBeVisible(); + const value = await fn(dialog); + await expect(dialog.locator).toBeHidden(); + return value; + } + + async assertUnremovable(): Promise { + await this.locator.getByRole("button", { name: "More options" }).click(); + await expect( + this.locator.getByRole("menu").getByRole("menuitem", { name: "Remove" }), + ).toBeHidden(); + } +} + +class AddDialog { + readonly #dialog: Locator; + + constructor(dialog: Locator) { + this.#dialog = dialog; + } + + async passkey(auth: DummyAuthFn): Promise { + await this.#dialog + .getByRole("button", { name: "Continue with passkey" }) + .click(); + await expect( + this.#dialog.getByRole("heading", { + name: /Add a passkey|Add another passkey/, + }), + ).toBeVisible(); + auth(this.#dialog.page()); + await this.#dialog.getByRole("button", { name: "Create passkey" }).click(); + } + + async assertPasskeyUnavailable(): Promise { + await expect( + this.#dialog.getByRole("button", { name: "Continue with passkey" }), + ).toBeDisabled(); + } + + async close(): Promise { + await this.#dialog.getByRole("button", { name: "Close" }).click(); + } +} + +class ManageAccessPage { + readonly #page: Page; + + constructor(page: Page) { + this.#page = page; + } + + async goto() { + await this.#page.goto(II_URL + "/manage/access"); + } + + async add(fn: (dialog: AddDialog) => Promise): Promise { + await this.#page + .getByRole("main") + .getByRole("button", { name: "Add new" }) + .click(); + const dialog = this.#page.getByRole("dialog"); + await expect(dialog).toBeVisible(); + const addDialog = new AddDialog(dialog); + const value = await fn(addDialog); + await expect(dialog).toBeHidden(); + return value; + } + + findPasskey(name: string): PasskeyItem { + const item = this.#page + .getByRole("main") + .getByRole("listitem") + .filter({ hasText: "Passkey" }); + return new PasskeyItem(item, name); + } + + async assertPasskeyExists(name: string): Promise { + await expect( + this.#page + .getByRole("main") + .getByRole("listitem") + .filter({ hasText: "Passkey" }) + .filter({ hasText: name }), + ).toBeVisible(); + } + + async assertPasskeyCount(count: number): Promise { + await expect( + this.#page + .getByRole("main") + .getByRole("listitem") + .filter({ hasText: "Passkey" }), + ).toHaveCount(count); + } + + async assertVisible() { + await this.#page.waitForURL( + (url) => url.origin === II_URL && url.pathname === "/manage/access", + ); + await expect( + this.#page.getByRole("heading", { name: "Access methods" }), + ).toBeVisible(); + await expect( + this.#page.getByText( + "Add or remove the ways you can sign in with your identity.", + ), + ).toBeVisible(); + } +} + +export const test = base.extend<{ + manageAccessPage: ManageAccessPage; +}>({ + manageAccessPage: async ({ page }, use) => { + await use(new ManageAccessPage(page)); + }, +}); diff --git a/src/frontend/tests/e2e-playwright/fixtures/managePage.ts b/src/frontend/tests/e2e-playwright/fixtures/managePage.ts index 022e52bacd..4b373e50c8 100644 --- a/src/frontend/tests/e2e-playwright/fixtures/managePage.ts +++ b/src/frontend/tests/e2e-playwright/fixtures/managePage.ts @@ -1,4 +1,4 @@ -import { Page, test as base } from "@playwright/test"; +import { Page, test as base, expect } from "@playwright/test"; import { II_URL } from "../utils"; class ManagePage { @@ -18,6 +18,20 @@ class ManagePage { await this.#page.waitForURL(II_URL); // Wait for redirect to landing page after sign out } + async assertVisible() { + await this.#page.waitForURL( + (url) => + url.origin === II_URL && + (url.pathname === "/manage" || url.pathname.startsWith("/manage/")), + ); + await expect( + this.#page.getByRole("heading", { name: "Welcome" }), + ).toBeVisible(); + await expect( + this.#page.getByText("Your identity and sign-in methods at a glance."), + ).toBeVisible(); + } + async #openMenuOnMobile() { const menuButton = this.#page .getByRole("banner") diff --git a/src/frontend/tests/e2e-playwright/authorize/account.spec.ts b/src/frontend/tests/e2e-playwright/routes/authorize/account.spec.ts similarity index 98% rename from src/frontend/tests/e2e-playwright/authorize/account.spec.ts rename to src/frontend/tests/e2e-playwright/routes/authorize/account.spec.ts index 73b8bfcc9d..d91c5cf0c2 100644 --- a/src/frontend/tests/e2e-playwright/authorize/account.spec.ts +++ b/src/frontend/tests/e2e-playwright/routes/authorize/account.spec.ts @@ -1,5 +1,5 @@ import { expect, test } from "@playwright/test"; -import { authorize, createIdentity, dummyAuth } from "../utils"; +import { authorize, createIdentity, dummyAuth } from "../../utils"; test("Create another account and authorize with primary", async ({ page }) => { const auth = dummyAuth(); diff --git a/src/frontend/tests/e2e-playwright/authorize/alternativeOrigins.spec.ts b/src/frontend/tests/e2e-playwright/routes/authorize/alternativeOrigins.spec.ts similarity index 99% rename from src/frontend/tests/e2e-playwright/authorize/alternativeOrigins.spec.ts rename to src/frontend/tests/e2e-playwright/routes/authorize/alternativeOrigins.spec.ts index 5924864e12..e31b94cffb 100644 --- a/src/frontend/tests/e2e-playwright/authorize/alternativeOrigins.spec.ts +++ b/src/frontend/tests/e2e-playwright/routes/authorize/alternativeOrigins.spec.ts @@ -5,7 +5,7 @@ import { NOT_TEST_APP_URL, TEST_APP_CANONICAL_URL, TEST_APP_URL, -} from "../utils"; +} from "../../utils"; test("Should not issue delegation when alternative origins are empty", async ({ page, diff --git a/src/frontend/tests/e2e-playwright/authorize/continue.spec.ts b/src/frontend/tests/e2e-playwright/routes/authorize/continue.spec.ts similarity index 99% rename from src/frontend/tests/e2e-playwright/authorize/continue.spec.ts rename to src/frontend/tests/e2e-playwright/routes/authorize/continue.spec.ts index 0920fdd772..f5471d8408 100644 --- a/src/frontend/tests/e2e-playwright/authorize/continue.spec.ts +++ b/src/frontend/tests/e2e-playwright/routes/authorize/continue.spec.ts @@ -8,7 +8,7 @@ import { TEST_APP_CANONICAL_URL, II_URL, createNewIdentityInII, -} from "../utils"; +} from "../../utils"; test("Authorize with last used identity", async ({ page }) => { const auth = dummyAuth(); diff --git a/src/frontend/tests/e2e-playwright/authorize/delegationTtl.spec.ts b/src/frontend/tests/e2e-playwright/routes/authorize/delegationTtl.spec.ts similarity index 98% rename from src/frontend/tests/e2e-playwright/authorize/delegationTtl.spec.ts rename to src/frontend/tests/e2e-playwright/routes/authorize/delegationTtl.spec.ts index b2ae7c7245..c2c402e5a5 100644 --- a/src/frontend/tests/e2e-playwright/authorize/delegationTtl.spec.ts +++ b/src/frontend/tests/e2e-playwright/routes/authorize/delegationTtl.spec.ts @@ -1,5 +1,5 @@ import { expect, test } from "@playwright/test"; -import { dummyAuth, II_URL, TEST_APP_URL } from "../utils"; +import { dummyAuth, II_URL, TEST_APP_URL } from "../../utils"; test("Delegation maxTimeToLive: 1 min", async ({ page }) => { const auth = dummyAuth(); diff --git a/src/frontend/tests/e2e-playwright/authorize/index.spec.ts b/src/frontend/tests/e2e-playwright/routes/authorize/index.spec.ts similarity index 99% rename from src/frontend/tests/e2e-playwright/authorize/index.spec.ts rename to src/frontend/tests/e2e-playwright/routes/authorize/index.spec.ts index c91bcd345a..af0b0e1c5d 100644 --- a/src/frontend/tests/e2e-playwright/authorize/index.spec.ts +++ b/src/frontend/tests/e2e-playwright/routes/authorize/index.spec.ts @@ -9,8 +9,8 @@ import { TEST_APP_CANONICAL_URL, II_URL, cancelDummyAuth, -} from "../utils"; -import { test } from "../fixtures"; +} from "../../utils"; +import { test } from "../../fixtures"; const DEFAULT_USER_NAME = "John Doe"; diff --git a/src/frontend/tests/e2e-playwright/authorize/legacy.spec.ts b/src/frontend/tests/e2e-playwright/routes/authorize/legacy.spec.ts similarity index 98% rename from src/frontend/tests/e2e-playwright/authorize/legacy.spec.ts rename to src/frontend/tests/e2e-playwright/routes/authorize/legacy.spec.ts index 7b3c14e06a..871c41e807 100644 --- a/src/frontend/tests/e2e-playwright/authorize/legacy.spec.ts +++ b/src/frontend/tests/e2e-playwright/routes/authorize/legacy.spec.ts @@ -6,7 +6,7 @@ import { TEST_APP_URL, dummyAuth, authorizeWithUrl, -} from "../utils"; +} from "../../utils"; [LEGACY_II_URL, ALT_LEGACY_II_URL].forEach((legacyURL) => { test.describe(`Legacy domain ${legacyURL}`, () => { test(`sees upgrade banner during authentication`, async ({ page }) => { diff --git a/src/frontend/tests/e2e-playwright/authorize/migration.spec.ts b/src/frontend/tests/e2e-playwright/routes/authorize/migration.spec.ts similarity index 99% rename from src/frontend/tests/e2e-playwright/authorize/migration.spec.ts rename to src/frontend/tests/e2e-playwright/routes/authorize/migration.spec.ts index 71f6c9f39b..287ef2cfb7 100644 --- a/src/frontend/tests/e2e-playwright/authorize/migration.spec.ts +++ b/src/frontend/tests/e2e-playwright/routes/authorize/migration.spec.ts @@ -9,7 +9,7 @@ import { II_URL, LEGACY_II_URL, TEST_APP_URL, -} from "../utils"; +} from "../../utils"; import { isNullish } from "@dfinity/utils"; const TEST_USER_NAME = "Test User"; diff --git a/src/frontend/tests/e2e-playwright/authorize/openid.spec.ts b/src/frontend/tests/e2e-playwright/routes/authorize/openid.spec.ts similarity index 97% rename from src/frontend/tests/e2e-playwright/authorize/openid.spec.ts rename to src/frontend/tests/e2e-playwright/routes/authorize/openid.spec.ts index 6aa7c9bf33..c947a8f9e3 100644 --- a/src/frontend/tests/e2e-playwright/authorize/openid.spec.ts +++ b/src/frontend/tests/e2e-playwright/routes/authorize/openid.spec.ts @@ -1,7 +1,10 @@ import { expect } from "@playwright/test"; -import { test } from "../fixtures"; -import { ALTERNATE_OPENID_PORT, DEFAULT_OPENID_PORT } from "../fixtures/openid"; -import { II_URL } from "../utils"; +import { test } from "../../fixtures"; +import { + ALTERNATE_OPENID_PORT, + DEFAULT_OPENID_PORT, +} from "../../fixtures/openid"; +import { II_URL } from "../../utils"; test.describe("Authorize with direct OpenID", () => { test.describe("without any attributes", () => { diff --git a/src/frontend/tests/e2e-playwright/authorize/postMessages.spec.ts b/src/frontend/tests/e2e-playwright/routes/authorize/postMessages.spec.ts similarity index 99% rename from src/frontend/tests/e2e-playwright/authorize/postMessages.spec.ts rename to src/frontend/tests/e2e-playwright/routes/authorize/postMessages.spec.ts index aa95deacdd..11c1f62e01 100644 --- a/src/frontend/tests/e2e-playwright/authorize/postMessages.spec.ts +++ b/src/frontend/tests/e2e-playwright/routes/authorize/postMessages.spec.ts @@ -7,7 +7,7 @@ import { openIiTab, openTestAppWithII, waitForNthMessage, -} from "../utils"; +} from "../../utils"; test("Authorize ready message should be sent immediately", async ({ page }) => { await openTestAppWithII(page); diff --git a/src/frontend/tests/e2e-playwright/index.spec.ts b/src/frontend/tests/e2e-playwright/routes/index.spec.ts similarity index 99% rename from src/frontend/tests/e2e-playwright/index.spec.ts rename to src/frontend/tests/e2e-playwright/routes/index.spec.ts index 5eb6cd57c3..c4b09013f7 100644 --- a/src/frontend/tests/e2e-playwright/index.spec.ts +++ b/src/frontend/tests/e2e-playwright/routes/index.spec.ts @@ -5,8 +5,8 @@ import { createIdentity, dummyAuth, II_URL, -} from "./utils"; -import { test } from "./fixtures"; +} from "../utils"; +import { test } from "../fixtures"; const DEFAULT_USER_NAME = "John Doe"; const SECONDARY_USER_NAME = "Jane Doe"; diff --git a/src/frontend/tests/e2e-playwright/routes/manage/access.spec.ts b/src/frontend/tests/e2e-playwright/routes/manage/access.spec.ts new file mode 100644 index 0000000000..53b2360c28 --- /dev/null +++ b/src/frontend/tests/e2e-playwright/routes/manage/access.spec.ts @@ -0,0 +1,213 @@ +import { test } from "../../fixtures"; +import { dummyAuth, getRandomIndex, II_URL } from "../../utils"; +import { DEFAULT_PASSKEY_NAME } from "../../fixtures/manageAccessPage"; + +test.describe("Access methods", () => { + test.beforeEach( + async ({ page, manageAccessPage, identities, signInWithIdentity }) => { + await manageAccessPage.goto(); + await signInWithIdentity(page, identities[0].identityNumber); + }, + ); + + test("can add a passkey", async ({ + page, + managePage, + manageAccessPage, + identities, + signInWithIdentity, + replaceAuthForIdentity, + }) => { + const authIndex = getRandomIndex(); + const auth = dummyAuth(authIndex); + await manageAccessPage.assertPasskeyCount(1); + await manageAccessPage.add((dialog) => dialog.passkey(auth)); + await manageAccessPage.assertPasskeyCount(2); + + // Verify we can still sign in with the existing passkey + await managePage.signOut(); + await manageAccessPage.goto(); + await signInWithIdentity(page, identities[0].identityNumber); + await manageAccessPage.assertVisible(); + + // Verify we can now also sign in with the new passkey + await managePage.signOut(); + replaceAuthForIdentity(identities[0].identityNumber, authIndex); + await manageAccessPage.goto(); + await signInWithIdentity(page, identities[0].identityNumber); + await manageAccessPage.assertVisible(); + }); + + test.describe("can rename a passkey", () => { + test("to device name", async ({ manageAccessPage }) => { + // Passkeys are tied to a particular device in e.g. Windows Hello, + // so this is an example where it's renamed to know which device. + await manageAccessPage.assertPasskeyCount(1); + await manageAccessPage + .findPasskey(DEFAULT_PASSKEY_NAME) + .rename(async (dialog) => { + await dialog.fill("Dell XPS"); + await dialog.submit(); + }); + await manageAccessPage.assertPasskeyCount(1); + await manageAccessPage.assertPasskeyExists("Dell XPS"); + }); + + test("unless it's an empty string", async ({ manageAccessPage }) => { + await manageAccessPage.assertPasskeyCount(1); + await manageAccessPage + .findPasskey(DEFAULT_PASSKEY_NAME) + .rename(async (dialog) => { + await dialog.fill(""); + await dialog.assertSubmitDisabled(); + await dialog.close(); + }); + await manageAccessPage.assertPasskeyCount(1); + await manageAccessPage.assertPasskeyExists(DEFAULT_PASSKEY_NAME); + }); + }); + + test.describe("can remove a passkey", () => { + test.beforeEach(async ({ manageAccessPage, identities }) => { + // Rename passkey that's currently in use + await manageAccessPage + .findPasskey(DEFAULT_PASSKEY_NAME) + .rename(async (dialog) => { + await dialog.fill("in-use-passkey"); + await dialog.submit(); + }); + + // Add additional passkey and rename it + const auth = dummyAuth(identities[0].dummyAuthIndex + BigInt(1)); + await manageAccessPage.add((dialog) => dialog.passkey(auth)); + await manageAccessPage + .findPasskey(DEFAULT_PASSKEY_NAME) + .rename(async (dialog) => { + await dialog.fill("additional-passkey"); + await dialog.submit(); + }); + }); + + test("which is currently not in use", async ({ manageAccessPage }) => { + // Remove additional passkey + await manageAccessPage.assertPasskeyCount(2); + await manageAccessPage + .findPasskey("additional-passkey") + .remove((dialog) => dialog.confirm()); + + // Assert it has been removed + await manageAccessPage.assertPasskeyCount(1); + await manageAccessPage.assertPasskeyExists("in-use-passkey"); + }); + + test("which is currently in use", async ({ + page, + manageAccessPage, + identities, + signInWithIdentity, + replaceAuthForIdentity, + }) => { + // Remove currently in use passkey + await manageAccessPage.assertPasskeyCount(2); + await manageAccessPage + .findPasskey("in-use-passkey") + .remove(async (dialog) => { + await dialog.assertSignOutWarningShown(); + await dialog.confirm(); + }); + + await page.waitForURL(II_URL); // Expect to be signed out + await manageAccessPage.goto(); // Go back to the manage page + + // Sign in with the new passkey and assert it's the only passkey + replaceAuthForIdentity( + identities[0].identityNumber, + identities[0].dummyAuthIndex + BigInt(1), + ); + await signInWithIdentity(page, identities[0].identityNumber); + await manageAccessPage.assertPasskeyCount(1); + await manageAccessPage.assertPasskeyExists("additional-passkey"); + }); + }); + + test("cannot remove a single passkey", async ({ manageAccessPage }) => { + await manageAccessPage.assertPasskeyCount(1); + await manageAccessPage + .findPasskey(DEFAULT_PASSKEY_NAME) + .assertUnremovable(); + await manageAccessPage.assertPasskeyCount(1); + }); + + test("cannot have more than 16 passkeys", async ({ manageAccessPage }) => { + await manageAccessPage.assertPasskeyCount(1); + for (let i = 0; i < 15; i++) { + await manageAccessPage.add((dialog) => dialog.passkey(dummyAuth())); + } + await manageAccessPage.assertPasskeyCount(16); + await manageAccessPage.add(async (dialog) => { + await dialog.assertPasskeyUnavailable(); + await dialog.close(); + }); + await manageAccessPage.assertPasskeyCount(16); + }); + + test.describe("can be cancelled", () => { + test.afterEach( + async ({ + page, + managePage, + manageAccessPage, + identities, + signInWithIdentity, + }) => { + // Verify the number of passkeys hasn't changed + await manageAccessPage.assertPasskeyCount(1); + + // Verify we can still sign in with the existing passkey + await managePage.signOut(); + await manageAccessPage.goto(); + await signInWithIdentity(page, identities[0].identityNumber); + await manageAccessPage.assertVisible(); + }, + ); + + test("when adding an access method", async ({ manageAccessPage }) => { + await manageAccessPage.add((dialog) => dialog.close()); + }); + + test("when renaming a passkey", async ({ manageAccessPage }) => { + await manageAccessPage + .findPasskey(DEFAULT_PASSKEY_NAME) + .rename((dialog) => dialog.close()); + }); + + test("when removing a passkey", async ({ manageAccessPage }) => { + // Rename current + await manageAccessPage + .findPasskey(DEFAULT_PASSKEY_NAME) + .rename(async (dialog) => { + await dialog.fill("in-use-passkey"); + await dialog.submit(); + }); + + // Add another, since we cannot remove a single passkey + await manageAccessPage.add((dialog) => dialog.passkey(dummyAuth())); + + // Then remove the current passkey, but cancel instead of confirm + await manageAccessPage + .findPasskey("in-use-passkey") + .remove((dialog) => dialog.cancel()); + + // Cleanup additional passkey and undo renaming of current + await manageAccessPage + .findPasskey(DEFAULT_PASSKEY_NAME) + .remove((dialog) => dialog.confirm()); + await manageAccessPage + .findPasskey("in-use-passkey") + .rename(async (dialog) => { + await dialog.fill(DEFAULT_PASSKEY_NAME); + await dialog.submit(); + }); + }); + }); +}); diff --git a/src/frontend/tests/e2e-playwright/dashboard/index.spec.ts b/src/frontend/tests/e2e-playwright/routes/manage/index.spec.ts similarity index 99% rename from src/frontend/tests/e2e-playwright/dashboard/index.spec.ts rename to src/frontend/tests/e2e-playwright/routes/manage/index.spec.ts index 7aea737786..026763e72d 100644 --- a/src/frontend/tests/e2e-playwright/dashboard/index.spec.ts +++ b/src/frontend/tests/e2e-playwright/routes/manage/index.spec.ts @@ -1,5 +1,5 @@ import { expect, test } from "@playwright/test"; -import { clearStorage, createIdentity, dummyAuth, II_URL } from "../utils"; +import { clearStorage, createIdentity, dummyAuth, II_URL } from "../../utils"; const TEST_USER_NAME = "Test User"; diff --git a/src/frontend/tests/e2e-playwright/dashboard/migration.spec.ts b/src/frontend/tests/e2e-playwright/routes/manage/migration.spec.ts similarity index 99% rename from src/frontend/tests/e2e-playwright/dashboard/migration.spec.ts rename to src/frontend/tests/e2e-playwright/routes/manage/migration.spec.ts index f824eed758..2719ad38e5 100644 --- a/src/frontend/tests/e2e-playwright/dashboard/migration.spec.ts +++ b/src/frontend/tests/e2e-playwright/routes/manage/migration.spec.ts @@ -7,7 +7,7 @@ import { II_URL, LEGACY_II_URL, signOut, -} from "../utils"; +} from "../../utils"; const TEST_USER_NAME = "Test User"; const TEST_USER_NAME_2 = "Test User 2"; diff --git a/src/frontend/tests/e2e-playwright/dashboard/recovery.spec.ts b/src/frontend/tests/e2e-playwright/routes/manage/recovery.spec.ts similarity index 95% rename from src/frontend/tests/e2e-playwright/dashboard/recovery.spec.ts rename to src/frontend/tests/e2e-playwright/routes/manage/recovery.spec.ts index 69aeb9271e..bdf5ec4792 100644 --- a/src/frontend/tests/e2e-playwright/dashboard/recovery.spec.ts +++ b/src/frontend/tests/e2e-playwright/routes/manage/recovery.spec.ts @@ -1,11 +1,10 @@ -import { test as base } from "../fixtures"; +import { test as base } from "../../fixtures"; import { expect } from "@playwright/test"; import { fromMnemonicWithoutValidation, generateMnemonic, IC_DERIVATION_PATH, } from "$lib/utils/recoveryPhrase"; -import { II_URL } from "../utils"; /** * Swap the first word around with the next different word found, @@ -51,6 +50,7 @@ test.describe("Recovery phrase", () => { page, managePage, manageRecoveryPage, + manageAccessPage, identities, signInWithIdentity, words, @@ -68,10 +68,7 @@ test.describe("Recovery phrase", () => { await wizard.enterRecoveryPhrase(words.current!); await wizard.confirmFoundIdentity(identities[0].name); }); - await page.waitForURL(II_URL + "/manage/access"); - await expect( - page.getByRole("heading", { name: "Access methods" }), - ).toBeVisible(); + await manageAccessPage.assertVisible(); }, ); @@ -112,6 +109,7 @@ test.describe("Recovery phrase", () => { page, managePage, manageRecoveryPage, + manageAccessPage, identities, signInWithIdentity, recoveryPage, @@ -129,10 +127,7 @@ test.describe("Recovery phrase", () => { await wizard.enterRecoveryPhrase(words.current!); await wizard.confirmFoundIdentity(identities[0].name); }); - await page.waitForURL(II_URL + "/manage/access"); - await expect( - page.getByRole("heading", { name: "Access methods" }), - ).toBeVisible(); + await manageAccessPage.assertVisible(); }, ); @@ -211,6 +206,7 @@ test.describe("Recovery phrase", () => { page, managePage, manageRecoveryPage, + manageAccessPage, identities, signInWithIdentity, recoveryPage, @@ -228,10 +224,7 @@ test.describe("Recovery phrase", () => { await wizard.enterRecoveryPhrase(words.current!); await wizard.confirmFoundIdentity(identities[0].name); }); - await page.waitForURL(II_URL + "/manage/access"); - await expect( - page.getByRole("heading", { name: "Access methods" }), - ).toBeVisible(); + await manageAccessPage.assertVisible(); }, ); @@ -380,9 +373,9 @@ test.describe("Recovery phrase", () => { }); test("when resetting", async ({ - page, identities, manageRecoveryPage, + manageAccessPage, recoveryPage, }) => { const oldWords = await manageRecoveryPage.activate(async (wizard) => { @@ -402,10 +395,7 @@ test.describe("Recovery phrase", () => { await wizard.enterRecoveryPhrase(oldWords); await wizard.confirmFoundIdentity(identities[0].name); }); - await page.waitForURL(II_URL + "/manage/access"); - await expect( - page.getByRole("heading", { name: "Access methods" }), - ).toBeVisible(); + await manageAccessPage.assertVisible(); }); }); diff --git a/src/frontend/tests/e2e-playwright/recovery.spec.ts b/src/frontend/tests/e2e-playwright/routes/recovery.spec.ts similarity index 98% rename from src/frontend/tests/e2e-playwright/recovery.spec.ts rename to src/frontend/tests/e2e-playwright/routes/recovery.spec.ts index c805b7e67b..475953531d 100644 --- a/src/frontend/tests/e2e-playwright/recovery.spec.ts +++ b/src/frontend/tests/e2e-playwright/routes/recovery.spec.ts @@ -1,7 +1,7 @@ -import { test as base } from "./fixtures"; +import { test as base } from "../fixtures"; import { expect } from "@playwright/test"; import { generateMnemonic } from "$lib/utils/recoveryPhrase"; -import { II_URL } from "./utils"; +import { II_URL } from "../utils"; /** * Replace the last word with the first different word found,