- Sign in to view and manage your identity, contract, and device data.
+
+
+ Sign in to view and manage your identity, contract, and device data.
+
+
);
}
diff --git a/example-apps/dashnote/test/DeleteNoteModal.test.tsx b/example-apps/dashnote/test/DeleteNoteModal.test.tsx
new file mode 100644
index 0000000..f891399
--- /dev/null
+++ b/example-apps/dashnote/test/DeleteNoteModal.test.tsx
@@ -0,0 +1,150 @@
+// @vitest-environment jsdom
+
+import { cleanup, fireEvent, render, screen } from "@testing-library/react";
+import { afterEach, describe, expect, it, vi } from "vitest";
+
+import { DeleteNoteModal } from "../src/components/DeleteNoteModal";
+
+afterEach(() => {
+ cleanup();
+});
+
+describe("DeleteNoteModal", () => {
+ it("renders nothing when open is false", () => {
+ const { container } = render(
+
,
+ );
+ expect(container.firstChild).toBeNull();
+ });
+
+ it("shows the note title in the body when present", () => {
+ render(
+
,
+ );
+ expect(screen.getByText(/groceries/i)).toBeTruthy();
+ });
+
+ it("falls back to 'this note' when the title is blank", () => {
+ render(
+
,
+ );
+ expect(screen.getByText(/this note/i)).toBeTruthy();
+ });
+
+ it("fires onConfirm when Delete is clicked", () => {
+ const onConfirm = vi.fn();
+ render(
+
,
+ );
+ fireEvent.click(screen.getByRole("button", { name: /^delete$/i }));
+ expect(onConfirm).toHaveBeenCalledTimes(1);
+ });
+
+ it("fires onCancel when Cancel is clicked", () => {
+ const onCancel = vi.fn();
+ render(
+
,
+ );
+ fireEvent.click(screen.getByRole("button", { name: /^cancel$/i }));
+ expect(onCancel).toHaveBeenCalledTimes(1);
+ });
+
+ it("disables both buttons while a delete is in flight", () => {
+ render(
+
,
+ );
+ expect(
+ screen.getByRole("button", { name: /deleting…/i }).hasAttribute("disabled"),
+ ).toBe(true);
+ expect(
+ screen.getByRole("button", { name: /^cancel$/i }).hasAttribute("disabled"),
+ ).toBe(true);
+ });
+
+ it("does not fire onCancel when the disabled Cancel button is clicked mid-delete", () => {
+ const onCancel = vi.fn();
+ render(
+
,
+ );
+ fireEvent.click(screen.getByRole("button", { name: /^cancel$/i }));
+ expect(onCancel).not.toHaveBeenCalled();
+ });
+
+ it("ignores Escape dismiss while deleting", () => {
+ const onCancel = vi.fn();
+ render(
+
,
+ );
+ // The shared Modal listens for Escape on window and calls onClose;
+ // DeleteNoteModal's onClose wrapper short-circuits while deleting.
+ fireEvent.keyDown(window, { key: "Escape" });
+ expect(onCancel).not.toHaveBeenCalled();
+ });
+
+ it("ignores backdrop dismiss while deleting", () => {
+ const onCancel = vi.fn();
+ render(
+
,
+ );
+ // The shared Modal forwards backdrop clicks through onClose. The
+ // wrapper guards onCancel when deleting=true so an accidental
+ // dismiss mid-delete doesn't desync the parent's state.
+ const dialog = screen.getByRole("dialog");
+ fireEvent.click(dialog.parentElement!);
+ expect(onCancel).not.toHaveBeenCalled();
+ });
+});
diff --git a/example-apps/dashnote/test/IdentityCard.test.tsx b/example-apps/dashnote/test/IdentityCard.test.tsx
index e9d66eb..d658464 100644
--- a/example-apps/dashnote/test/IdentityCard.test.tsx
+++ b/example-apps/dashnote/test/IdentityCard.test.tsx
@@ -91,13 +91,13 @@ describe("IdentityCard", () => {
expect(screen.getByText(TRUNCATED_ID)).toBeTruthy();
});
- it("hides the identity layout and shows a Login button when not connected", () => {
+ it("hides the identity layout and shows a Sign in button when not connected", () => {
renderCard({
status: "idle",
identityId: IDENTITY_ID,
dpnsName: "alice",
});
- expect(screen.getByRole("button", { name: /login/i })).toBeTruthy();
+ expect(screen.getByRole("button", { name: /^sign in$/i })).toBeTruthy();
// The identity layout (name + truncated id) must not render in the
// disconnected state, even when an id and name are passed in.
expect(screen.queryByText("@alice")).toBeNull();
@@ -106,8 +106,8 @@ describe("IdentityCard", () => {
// Regression: when the card was unified into a single menu trigger, readonly
// (connected-but-not-signed-in) silently lost its one-click path to the
- // login modal — the menu offered Settings and Switch identity but no direct
- // Login. The card must call onLoginClick on click and render no menu.
+ // login modal — the menu offered Settings and Sign in but no direct Sign in
+ // affordance. The card must call onLoginClick on click and render no menu.
it("calls onLoginClick on click when readonly, without opening a menu", () => {
const onLoginClick = vi.fn();
renderCard({
@@ -159,6 +159,25 @@ describe("IdentityCard", () => {
expect(onLoginClick).toHaveBeenCalled();
});
+ // In browsing mode the user has no signed-in key to switch *from*, so the
+ // menu entry is labeled Sign in (not Switch identity). It still calls
+ // onLoginClick — only the affordance changes.
+ it("labels the login entry 'Sign in' in browsing mode and calls onLoginClick", () => {
+ const onLoginClick = vi.fn();
+ renderCard({
+ status: "browsing",
+ identityId: IDENTITY_ID,
+ dpnsName: null,
+ onLoginClick,
+ });
+ fireEvent.click(screen.getByRole("button", { expanded: false }));
+ expect(
+ screen.queryByRole("menuitem", { name: /switch identity/i }),
+ ).toBeNull();
+ fireEvent.click(screen.getByRole("menuitem", { name: /^sign in$/i }));
+ expect(onLoginClick).toHaveBeenCalled();
+ });
+
it("calls session.logout when Log out is chosen from the menu", () => {
const logout = vi.fn();
mockUseSession.mockReturnValue({ logout });
diff --git a/example-apps/dashnote/test/LoginModal.test.tsx b/example-apps/dashnote/test/LoginModal.test.tsx
index 87d3a3a..6faf75c 100644
--- a/example-apps/dashnote/test/LoginModal.test.tsx
+++ b/example-apps/dashnote/test/LoginModal.test.tsx
@@ -99,7 +99,7 @@ describe("LoginModal", () => {
expect(onClose).not.toHaveBeenCalled();
expect(screen.getByPlaceholderText(/mnemonic phrase/i)).toBeTruthy();
- expect(screen.getByRole("button", { name: /^login$/i })).toBeTruthy();
+ expect(screen.getByRole("button", { name: /^sign in$/i })).toBeTruthy();
});
it("submits the mnemonic via session.login and closes on success", async () => {
@@ -112,7 +112,7 @@ describe("LoginModal", () => {
fireEvent.change(screen.getByPlaceholderText(/mnemonic phrase/i), {
target: { value: "test mnemonic phrase" },
});
- fireEvent.click(screen.getByRole("button", { name: /^login$/i }));
+ fireEvent.click(screen.getByRole("button", { name: /^sign in$/i }));
await waitFor(() => {
expect(login).toHaveBeenCalledWith("test mnemonic phrase", {
@@ -134,7 +134,7 @@ describe("LoginModal", () => {
fireEvent.change(screen.getByPlaceholderText(/mnemonic phrase/i), {
target: { value: "garbage" },
});
- fireEvent.click(screen.getByRole("button", { name: /^login$/i }));
+ fireEvent.click(screen.getByRole("button", { name: /^sign in$/i }));
expect(await screen.findByText("Bad mnemonic")).toBeTruthy();
});
@@ -145,11 +145,103 @@ describe("LoginModal", () => {
render(
);
const button = screen.getByRole("button", {
- name: /^login$/i,
+ name: /^sign in$/i,
}) as HTMLButtonElement;
expect(button.disabled).toBe(true);
});
+ it("keeps the login button disabled for a whitespace-only secret", () => {
+ mockUseSession.mockReturnValue(makeSession());
+
+ render(
);
+
+ fireEvent.change(screen.getByPlaceholderText(/mnemonic phrase/i), {
+ target: { value: " \t " },
+ });
+
+ const button = screen.getByRole("button", {
+ name: /^sign in$/i,
+ }) as HTMLButtonElement;
+ expect(button.disabled).toBe(true);
+ });
+
+ it("disables the login button while a login is in flight", async () => {
+ // Resolve manually so we can observe the in-flight state. The label
+ // also flips to "Connecting…" while submitting is true.
+ let resolveLogin: (() => void) | undefined;
+ const login = vi.fn(
+ () =>
+ new Promise
((resolve) => {
+ resolveLogin = () => resolve();
+ }),
+ );
+ mockUseSession.mockReturnValue(makeSession({ login }));
+
+ render();
+
+ fireEvent.change(screen.getByPlaceholderText(/mnemonic phrase/i), {
+ target: { value: "phrase" },
+ });
+ fireEvent.click(screen.getByRole("button", { name: /^sign in$/i }));
+
+ const connectingButton = (await screen.findByRole("button", {
+ name: /connecting/i,
+ })) as HTMLButtonElement;
+ expect(connectingButton.disabled).toBe(true);
+
+ resolveLogin?.();
+ await waitFor(() => {
+ expect(login).toHaveBeenCalled();
+ });
+ });
+
+ it("falls back to identityIndex=0 when the index field is non-numeric", async () => {
+ // Number.parseInt("abc", 10) is NaN; the handler must coerce that to 0
+ // rather than passing NaN through to session.login.
+ const login = vi.fn().mockResolvedValue(undefined);
+ mockUseSession.mockReturnValue(makeSession({ login }));
+
+ render();
+
+ fireEvent.click(screen.getByText(/advanced settings/i));
+ fireEvent.change(screen.getByRole("spinbutton"), {
+ target: { value: "abc" },
+ });
+ fireEvent.change(screen.getByPlaceholderText(/mnemonic phrase/i), {
+ target: { value: "phrase" },
+ });
+ fireEvent.click(screen.getByRole("button", { name: /^sign in$/i }));
+
+ await waitFor(() => {
+ expect(login).toHaveBeenCalledWith("phrase", {
+ identityIndex: 0,
+ rememberMe: true,
+ });
+ });
+ });
+
+ it("preserves identityIndex and showAdvanced across modal reopens", () => {
+ // The open-effect resets secret/rememberMe/useDifferentIdentity/error,
+ // but NOT identityIndex or showAdvanced — verify that contract so a
+ // future refactor doesn't quietly change it.
+ mockUseSession.mockReturnValue(makeSession());
+
+ const { rerender } = render();
+
+ fireEvent.click(screen.getByText(/advanced settings/i));
+ fireEvent.change(screen.getByRole("spinbutton"), {
+ target: { value: "7" },
+ });
+
+ rerender();
+ rerender();
+
+ // Disclosure is still open, value still 7.
+ expect((screen.getByRole("spinbutton") as HTMLInputElement).value).toBe(
+ "7",
+ );
+ });
+
it("uses identity index from advanced settings", async () => {
const login = vi.fn().mockResolvedValue(undefined);
mockUseSession.mockReturnValue(makeSession({ login }));
@@ -163,7 +255,7 @@ describe("LoginModal", () => {
fireEvent.change(screen.getByPlaceholderText(/mnemonic phrase/i), {
target: { value: "phrase" },
});
- fireEvent.click(screen.getByRole("button", { name: /^login$/i }));
+ fireEvent.click(screen.getByRole("button", { name: /^sign in$/i }));
await waitFor(() => {
expect(login).toHaveBeenCalledWith("phrase", {
@@ -221,7 +313,7 @@ describe("LoginModal", () => {
fireEvent.change(screen.getByPlaceholderText(/mnemonic phrase/i), {
target: { value: "phrase" },
});
- fireEvent.click(screen.getByRole("button", { name: /^login$/i }));
+ fireEvent.click(screen.getByRole("button", { name: /^sign in$/i }));
await waitFor(() => {
expect(login).toHaveBeenCalledWith("phrase", {
@@ -245,7 +337,7 @@ describe("LoginModal", () => {
fireEvent.change(screen.getByPlaceholderText(/mnemonic phrase/i), {
target: { value: "phrase" },
});
- fireEvent.click(screen.getByRole("button", { name: /^login$/i }));
+ fireEvent.click(screen.getByRole("button", { name: /^sign in$/i }));
await waitFor(() => {
expect(login).toHaveBeenCalledWith("phrase", {
@@ -341,23 +433,6 @@ describe("LoginModal", () => {
expect(checkbox.checked).toBe(true);
});
- it("calls forgetIdentity when Forget this device is clicked from the login form", () => {
- const forgetIdentity = vi.fn();
- mockUseSession.mockReturnValue(
- makeSession({
- rememberedIdentityId: "remembered-identity-id",
- forgetIdentity,
- }),
- );
-
- render();
-
- fireEvent.click(
- screen.getByRole("button", { name: /forget this device/i }),
- );
- expect(forgetIdentity).toHaveBeenCalled();
- });
-
it("hides the remembered identity panel when no identity is remembered", () => {
mockUseSession.mockReturnValue(makeSession());
@@ -395,7 +470,7 @@ describe("LoginModal", () => {
screen.getByPlaceholderText(/mnemonic phrase/i) as HTMLInputElement,
{ target: { value: "fresh mnemonic" } },
);
- fireEvent.click(screen.getByRole("button", { name: /^login$/i }));
+ fireEvent.click(screen.getByRole("button", { name: /^sign in$/i }));
await waitFor(() => {
expect(login).toHaveBeenCalledWith("fresh mnemonic", {
@@ -483,15 +558,17 @@ describe("LoginModal", () => {
expect(screen.queryByRole("spinbutton")).not.toBeNull();
});
- it("hides the identity-index field when the input parses as a WIF", () => {
+ it("hides the Advanced settings disclosure when the input parses as a WIF", () => {
+ // WIF input has no DIP-13 derivation, so the only knob inside Advanced
+ // (Identity index) is irrelevant — the disclosure itself disappears.
mockUseSession.mockReturnValue(makeSession());
render();
fireEvent.change(screen.getByPlaceholderText(/mnemonic phrase/i), {
target: { value: "cVHcfvcWNc7DvqaPCwM6Z3DqZ" },
});
- fireEvent.click(screen.getByText(/advanced settings/i));
+ expect(screen.queryByText(/advanced settings/i)).toBeNull();
expect(screen.queryByRole("spinbutton")).toBeNull();
});
@@ -504,7 +581,7 @@ describe("LoginModal", () => {
fireEvent.change(screen.getByPlaceholderText(/mnemonic phrase/i), {
target: { value: "cVHcfvcWNc7DvqaPCwM6Z3DqZ" },
});
- fireEvent.click(screen.getByRole("button", { name: /^login$/i }));
+ fireEvent.click(screen.getByRole("button", { name: /^sign in$/i }));
await waitFor(() => {
expect(login).toHaveBeenCalledWith("cVHcfvcWNc7DvqaPCwM6Z3DqZ", {
@@ -613,7 +690,7 @@ describe("LoginModal", () => {
securityLevelName: "CRITICAL",
});
const button = screen.getByRole("button", {
- name: /^login$/i,
+ name: /^sign in$/i,
}) as HTMLButtonElement;
expect(button.disabled).toBe(true);
});
@@ -625,7 +702,7 @@ describe("LoginModal", () => {
dpnsName: null,
});
const button = screen.getByRole("button", {
- name: /^login$/i,
+ name: /^sign in$/i,
}) as HTMLButtonElement;
expect(button.disabled).toBe(true);
});
@@ -635,7 +712,7 @@ describe("LoginModal", () => {
// be able to hit Enter immediately after pasting.
renderWithSecret({ status: "checking" });
const button = screen.getByRole("button", {
- name: /^login$/i,
+ name: /^sign in$/i,
}) as HTMLButtonElement;
expect(button.disabled).toBe(false);
});
diff --git a/example-apps/dashnote/test/NotesWorkspace.test.tsx b/example-apps/dashnote/test/NotesWorkspace.test.tsx
index 9e7da1b..ec7f151 100644
--- a/example-apps/dashnote/test/NotesWorkspace.test.tsx
+++ b/example-apps/dashnote/test/NotesWorkspace.test.tsx
@@ -6,6 +6,7 @@ import {
render,
screen,
waitFor,
+ within,
} from "@testing-library/react";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
@@ -113,7 +114,7 @@ describe("NotesWorkspace", () => {
);
expect(screen.getByText(/sign in to see your notes/i)).toBeTruthy();
- const loginButton = screen.getByRole("button", { name: /^log in$/i });
+ const loginButton = screen.getByRole("button", { name: /^sign in$/i });
fireEvent.click(loginButton);
expect(onOpenLogin).toHaveBeenCalled();
expect(screen.queryByRole("button", { name: /new note/i })).toBeNull();
@@ -239,6 +240,10 @@ describe("NotesWorkspace", () => {
});
fireEvent.click(screen.getByRole("button", { name: /^delete$/i }));
+ const confirmDialog = screen.getByRole("dialog");
+ fireEvent.click(
+ within(confirmDialog).getByRole("button", { name: /^delete$/i }),
+ );
await waitFor(() => {
expect(mockDeleteNote).toHaveBeenCalledWith(
expect.objectContaining({ noteId: "note-2" }),
@@ -664,7 +669,7 @@ describe("NotesWorkspace", () => {
).toBe("Edited offline");
});
- it("deletes a note via the bottom 'Delete note' button", async () => {
+ it("deletes a note via the bottom 'Delete note' button after confirming the modal", async () => {
mockUseSession.mockReturnValue(makeSession());
mockListMyNotes.mockResolvedValue([noteFixture]);
mockGetNote.mockResolvedValue(noteFixture);
@@ -682,6 +687,14 @@ describe("NotesWorkspace", () => {
fireEvent.click(screen.getByRole("button", { name: /delete note/i }));
+ // Trigger doesn't fire the delete directly anymore — the
+ // confirmation modal must be acknowledged first.
+ expect(mockDeleteNote).not.toHaveBeenCalled();
+ const dialog = screen.getByRole("dialog");
+ fireEvent.click(
+ within(dialog).getByRole("button", { name: /^delete$/i }),
+ );
+
await waitFor(() => {
expect(mockDeleteNote).toHaveBeenCalledWith(
expect.objectContaining({ noteId: "note-mobile" }),
diff --git a/example-apps/dashnote/test/SettingsPanel.test.tsx b/example-apps/dashnote/test/SettingsPanel.test.tsx
index a5a7414..a76c25f 100644
--- a/example-apps/dashnote/test/SettingsPanel.test.tsx
+++ b/example-apps/dashnote/test/SettingsPanel.test.tsx
@@ -81,7 +81,7 @@ describe("SettingsPanel", () => {
mockUseSession.mockReturnValue(
makeSession({ identityId: "id-abc", dpnsName: "alice" }),
);
- render();
+ render();
const block = screen.getByTestId("settings-identity-block");
expect(within(block).getByText("id-abc")).toBeTruthy();
expect(within(block).getByText("✓ alice.dash")).toBeTruthy();
@@ -91,29 +91,32 @@ describe("SettingsPanel", () => {
mockUseSession.mockReturnValue(
makeSession({ identityId: "id-abc", dpnsName: null }),
);
- render();
+ render();
const block = screen.getByTestId("settings-identity-block");
expect(within(block).queryByText(/\.dash$/)).toBeNull();
});
it("renders the network indicator as testnet", () => {
mockUseSession.mockReturnValue(makeSession());
- render();
+ render();
expect(screen.getByText("testnet")).toBeTruthy();
});
- it("shows an empty state when not signed in or browsing", () => {
+ it("shows an empty state with a Sign in button when not signed in or browsing", () => {
mockUseSession.mockReturnValue(
makeSession({ status: "readonly", identityId: null, keyManager: null }),
);
- render();
+ const onOpenLogin = vi.fn();
+ render();
expect(screen.getByText(/sign in to view/i)).toBeTruthy();
expect(screen.queryByTestId("settings-identity-block")).toBeNull();
+ fireEvent.click(screen.getByRole("button", { name: /^sign in$/i }));
+ expect(onOpenLogin).toHaveBeenCalled();
});
it("hides the danger zone when nothing is remembered", () => {
mockUseSession.mockReturnValue(makeSession({ rememberedIdentityId: null }));
- render();
+ render();
expect(
screen.queryByRole("button", { name: /forget this device/i }),
).toBeNull();
@@ -127,7 +130,7 @@ describe("SettingsPanel", () => {
forgetIdentity,
}),
);
- render();
+ render();
fireEvent.click(
screen.getByRole("button", { name: /forget this device/i }),
);
@@ -141,7 +144,7 @@ describe("SettingsPanel", () => {
value: { writeText },
});
mockUseSession.mockReturnValue(makeSession({ identityId: "id-xyz" }));
- render();
+ render();
fireEvent.click(screen.getByRole("button", { name: /copy identity id/i }));
await waitFor(() => {
expect(writeText).toHaveBeenCalledWith("id-xyz");
@@ -153,7 +156,7 @@ describe("SettingsPanel", () => {
mockUseSession.mockReturnValue(
makeSession({ contractId: "old", setContractId }),
);
- render();
+ render();
const input = screen.getByPlaceholderText(/paste a note contract id/i);
fireEvent.change(input, { target: { value: " new-contract " } });
fireEvent.click(screen.getByRole("button", { name: /use this id/i }));
@@ -169,7 +172,7 @@ describe("SettingsPanel", () => {
mockUseSession.mockReturnValue(
makeSession({ setContractId, sdk, keyManager, log }),
);
- render();
+ render();
fireEvent.click(
screen.getByRole("button", { name: /register a fresh contract/i }),
);
@@ -197,7 +200,7 @@ describe("SettingsPanel", () => {
}),
);
mockUseSession.mockReturnValue(makeSession({ setContractId: vi.fn() }));
- render();
+ render();
const button = screen.getByRole("button", {
name: /register a fresh contract/i,
});
@@ -214,7 +217,7 @@ describe("SettingsPanel", () => {
const setContractId = vi.fn();
mockRegisterContract.mockRejectedValue(new Error("Network down"));
mockUseSession.mockReturnValue(makeSession({ setContractId }));
- render();
+ render();
fireEvent.click(
screen.getByRole("button", { name: /register a fresh contract/i }),
);
@@ -224,7 +227,7 @@ describe("SettingsPanel", () => {
it("invokes clearCachedNotes with the current identity ID", () => {
mockUseSession.mockReturnValue(makeSession({ identityId: "id-cache" }));
- render();
+ render();
fireEvent.click(
screen.getByRole("button", {
name: /clear local cache for this device/i,
diff --git a/example-apps/dashnote/test/e2e/auth.spec.ts b/example-apps/dashnote/test/e2e/auth.spec.ts
index a510f14..649d54a 100644
--- a/example-apps/dashnote/test/e2e/auth.spec.ts
+++ b/example-apps/dashnote/test/e2e/auth.spec.ts
@@ -80,43 +80,6 @@ test("forget-this-device via the Settings panel drops back to readonly", async (
).toBeHidden();
});
-test("forget-this-device via the LoginModal also clears the remembered identity", async ({
- page,
-}) => {
- await loginViaModal(page, { rememberMe: true });
-
- // Log out so the modal can be opened with the rememberedIdentity panel
- // visible. Logout-without-forget keeps the hint, dropping to browsing.
- await openIdentityMenu(page);
- await page.getByRole("menuitem", { name: /^log out$/i }).click();
- await expect(
- page.getByText("Browsing (read-only)", { exact: true }),
- ).toBeVisible({ timeout: 30_000 });
-
- // Open the login modal — the "Forget this device" button is rendered
- // beside "Use a different identity" when rememberedIdentityId is set.
- // The IdentityCard in browsing state is a button with aria-haspopup="menu"
- // (the "Switch identity" menuitem opens the login modal).
- await openIdentityMenu(page);
- await page.getByRole("menuitem", { name: /switch identity/i }).click();
-
- const dialog = page.getByRole("dialog");
- await expect(dialog).toBeVisible();
- await dialog.getByRole("button", { name: /forget this device/i }).click();
-
- // The form re-renders without the remembered panel.
- await expect(
- dialog.locator('[data-testid="remembered-identity-panel"]'),
- ).toBeHidden();
-
- // Close and reload — readonly state confirms the hint is gone.
- await dialog.getByRole("button", { name: /^cancel$/i }).click();
- await page.reload();
- await expect(
- page.getByText("Browsing (read-only)", { exact: true }),
- ).toBeHidden();
-});
-
test("Switch identity from the IdentityCard menu re-opens the login form", async ({
page,
}) => {
@@ -130,7 +93,9 @@ test("Switch identity from the IdentityCard menu re-opens the login form", async
const dialog = page.getByRole("dialog");
await expect(dialog).toBeVisible();
await expect(dialog.getByPlaceholder(/mnemonic phrase|wif/i)).toBeVisible();
- await expect(dialog.getByRole("button", { name: /^Login$/ })).toBeDisabled();
+ await expect(
+ dialog.getByRole("button", { name: /^Sign in$/ }),
+ ).toBeDisabled();
});
test("Settings tab is reachable from both the IdentityCard menu and the sidebar NavButton", async ({
diff --git a/example-apps/dashnote/test/e2e/fixtures.ts b/example-apps/dashnote/test/e2e/fixtures.ts
index 74c725f..324dc92 100644
--- a/example-apps/dashnote/test/e2e/fixtures.ts
+++ b/example-apps/dashnote/test/e2e/fixtures.ts
@@ -52,7 +52,7 @@ function isMobile(page: Page): boolean {
}
/**
- * Resolve a sidebar nav button (Notes / How it works / Login) by label.
+ * Resolve a sidebar nav button (Notes / How it works / Sign in) by label.
*
* On mobile the sidebar is off-canvas; this helper opens the hamburger
* drawer first so the button is in the visible viewport.
@@ -74,9 +74,11 @@ export async function navButton(page: Page, label: RegExp | string) {
}
/**
- * Open the IdentityCard menu (the popover with Settings / Switch identity
- * / Log out items). Only available when the session is `authenticated` or
- * `browsing` — caller is responsible for being in that state.
+ * Open the IdentityCard menu (the popover with Settings, Switch identity
+ * or Sign in, and Log out items). Only available when the session is
+ * `authenticated` or `browsing` — caller is responsible for being in that
+ * state. The middle entry reads "Switch identity" when authenticated and
+ * "Sign in" when browsing read-only.
*
* On mobile the sidebar is off-canvas; opens the drawer transparently
* via the hamburger first.
@@ -102,9 +104,14 @@ export async function openIdentityMenu(page: Page) {
}
/**
- * Open the LoginModal via the sidebar "Login" nav button, fill the
- * mnemonic from PLATFORM_MNEMONIC, submit, and wait for the
- * IdentityCard to report `Authenticated`.
+ * Open the LoginModal, fill the mnemonic from PLATFORM_MNEMONIC, submit,
+ * and wait for the IdentityCard to report `Authenticated`.
+ *
+ * The entry point depends on session state: in `idle/connecting/readonly`
+ * the sidebar exposes a "Sign in" NavButton; in `browsing` (remembered
+ * identity after reload) the sidebar entry is hidden and the modal is
+ * reached via the IdentityCard menu instead. We detect which surface
+ * exists and use whichever one is visible.
*
* Defaults to `rememberMe: false` (the modal's default is true) so tests
* start from a clean localStorage; opt in explicitly when exercising the
@@ -121,7 +128,17 @@ export async function loginViaModal(
throw new Error("PLATFORM_MNEMONIC is required for loginViaModal");
}
- await (await navButton(page, /login$/i)).click();
+ const browsing = await page
+ .getByText("Browsing (read-only)", { exact: true })
+ .isVisible()
+ .catch(() => false);
+
+ if (browsing) {
+ await openIdentityMenu(page);
+ await page.getByRole("menuitem", { name: /^sign in$/i }).click();
+ } else {
+ await (await navButton(page, /sign in$/i)).click();
+ }
const dialog = page.getByRole("dialog");
await expect(dialog).toBeVisible();
@@ -132,7 +149,7 @@ export async function loginViaModal(
if (!rememberMe) {
await rememberCheckbox.uncheck();
}
- await dialog.getByRole("button", { name: /^Login$/ }).click();
+ await dialog.getByRole("button", { name: /^Sign in$/ }).click();
await expect(dialog).toBeHidden({ timeout: 60_000 });
await expect(page.getByText("Authenticated", { exact: true })).toBeVisible({
@@ -251,12 +268,6 @@ export async function deleteNoteByTitle(page: Page, title: string) {
timeout: 30_000,
});
- // window.confirm("Delete this note permanently?") — auto-accept.
- const dialogHandler = (dialog: import("@playwright/test").Dialog) => {
- void dialog.accept();
- };
- page.once("dialog", dialogHandler);
-
// Desktop renders the "Delete" button in the editor header; mobile
// renders "Delete note" near the bottom. Match either label.
await page
@@ -264,6 +275,12 @@ export async function deleteNoteByTitle(page: Page, title: string) {
.first()
.click();
+ // DeleteNoteModal opens for confirmation; scope to its dialog so we
+ // don't accidentally re-match the editor's Delete trigger.
+ const confirmDialog = page.getByRole("dialog");
+ await expect(confirmDialog).toBeVisible();
+ await confirmDialog.getByRole("button", { name: /^delete$/i }).click();
+
// Item leaves the list after reloadNotes resolves.
await expect(page.locator("button", { hasText: title })).toHaveCount(0, {
timeout: 60_000,
diff --git a/example-apps/dashnote/test/e2e/smoke.spec.ts b/example-apps/dashnote/test/e2e/smoke.spec.ts
index 3add610..cee777a 100644
--- a/example-apps/dashnote/test/e2e/smoke.spec.ts
+++ b/example-apps/dashnote/test/e2e/smoke.spec.ts
@@ -61,15 +61,15 @@ test.describe("theme toggle", () => {
});
test.describe("login modal", () => {
- test("sidebar Login button opens the modal", async ({ page }) => {
- await (await navButton(page, /login$/i)).click();
+ test("sidebar Sign in button opens the modal", async ({ page }) => {
+ await (await navButton(page, /sign in$/i)).click();
const dialog = page.getByRole("dialog");
await expect(dialog).toBeVisible();
await expect(dialog.getByPlaceholder(/mnemonic phrase|wif/i)).toBeVisible();
});
test("Cancel button closes the modal", async ({ page }) => {
- await (await navButton(page, /login$/i)).click();
+ await (await navButton(page, /sign in$/i)).click();
const dialog = page.getByRole("dialog");
await expect(dialog).toBeVisible();
await dialog.getByRole("button", { name: /^cancel$/i }).click();
@@ -77,19 +77,19 @@ test.describe("login modal", () => {
});
test("Escape closes the modal", async ({ page }) => {
- await (await navButton(page, /login$/i)).click();
+ await (await navButton(page, /sign in$/i)).click();
const dialog = page.getByRole("dialog");
await expect(dialog).toBeVisible();
await page.keyboard.press("Escape");
await expect(dialog).toBeHidden();
});
- test("Login button is disabled until a secret is entered", async ({
+ test("Sign in button is disabled until a secret is entered", async ({
page,
}) => {
- await (await navButton(page, /login$/i)).click();
+ await (await navButton(page, /sign in$/i)).click();
const dialog = page.getByRole("dialog");
- const submit = dialog.getByRole("button", { name: /^Login$/ });
+ const submit = dialog.getByRole("button", { name: /^Sign in$/ });
await expect(submit).toBeDisabled();
await dialog.getByPlaceholder(/mnemonic phrase|wif/i).fill("abandon");
await expect(submit).toBeEnabled();
@@ -98,7 +98,7 @@ test.describe("login modal", () => {
test("Advanced settings reveals the identity-index field for mnemonic input", async ({
page,
}) => {
- await (await navButton(page, /login$/i)).click();
+ await (await navButton(page, /sign in$/i)).click();
const dialog = page.getByRole("dialog");
// Type something with whitespace so detectSecretShape() picks "mnemonic"
// and the identity-index field is rendered inside Advanced.
@@ -108,14 +108,27 @@ test.describe("login modal", () => {
await expect(dialog.locator('input[type="number"]')).toBeVisible();
});
- test("Advanced settings exposes a contract-ID field", async ({ page }) => {
- await (await navButton(page, /login$/i)).click();
+ test("Advanced settings disclosure is hidden when input parses as a WIF", async ({
+ page,
+ }) => {
+ // WIF input has no DIP-13 derivation, so identity-index is irrelevant
+ // and the whole Advanced disclosure should disappear.
+ await (await navButton(page, /sign in$/i)).click();
const dialog = page.getByRole("dialog");
- await dialog.getByRole("button", { name: /advanced settings/i }).click();
- await expect(dialog.getByText(/contract id/i).first()).toBeVisible();
+
+ // Mnemonic-shaped input first → disclosure renders.
+ await dialog.getByPlaceholder(/mnemonic phrase|wif/i).fill("word word");
await expect(
- dialog.getByPlaceholder(/dashnote note contract id/i),
+ dialog.getByRole("button", { name: /advanced settings/i }),
).toBeVisible();
+
+ // Switch to WIF-shaped input → disclosure disappears.
+ await dialog
+ .getByPlaceholder(/mnemonic phrase|wif/i)
+ .fill("cVHcfvcWNc7DvqaPCwM6Z3DqZ");
+ await expect(
+ dialog.getByRole("button", { name: /advanced settings/i }),
+ ).toBeHidden();
});
});