Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/App.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -621,7 +621,7 @@ describe("Switchify PC shell", () => {
const entry = await (await sidebar()).findByRole("button", { name: /^Account, signed in/ });
fireEvent.click(entry);
expect(await screen.findByRole("heading", { name: "Account", level: 1 })).toBeInTheDocument();
expect(await screen.findByText(/Signed in as/)).toBeInTheDocument();
expect(await screen.findByRole("heading", { name: "me@example.com" })).toBeInTheDocument();
expect(entry).toHaveAttribute("aria-current", "page");
expect(screen.queryByRole("tablist", { name: "Settings sections" })).not.toBeInTheDocument();
});
Expand Down
7 changes: 1 addition & 6 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Button, Input, Select } from "./ui/controls";
import { Button, Input, Select, StatusIcon } from "./ui/controls";
import { Demonstration, DemonstrationProvider } from "./help/Demonstration";
import { useEffect, useId, useMemo, useRef, useState, type ReactNode } from "react";
import {
Expand Down Expand Up @@ -67,11 +67,6 @@ function NavButton({ active, icon, children, onClick }: { active: boolean; icon:
return <Button className="nav-button" data-active={active} aria-current={active ? "page" : undefined} onClick={onClick}>{icon}<span>{children}</span></Button>;
}

// `ok` omitted marks a row that has no state to report, only a subject.
function StatusIcon({ ok, children }: { ok?: boolean; children: ReactNode }) {
return <span className="status-icon" data-ok={ok} aria-hidden="true">{children}</span>;
}

function AccessibilityCopy({ state, detailed = false }: { state: AppState; detailed?: boolean }) {
if (state.accessibility === "granted") return <p>Ready</p>;
if (state.accessibility === "unavailable") return <p>Unavailable on this system</p>;
Expand Down
48 changes: 42 additions & 6 deletions src/settings/AccountSection.test.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
import { act, fireEvent, render, screen, waitFor, within } from "@testing-library/react";
import { afterEach, describe, expect, it, vi } from "vitest";
import { api } from "../api";
import type { AccountView, SyncView } from "../types";
Expand Down Expand Up @@ -59,6 +59,19 @@ describe("AccountSection", () => {
expect(await screen.findByText(/saved by a newer Switchify PC/)).toBeInTheDocument();
});

it("announces the sync status only once it is known", async () => {
vi.spyOn(api, "account").mockResolvedValue(signedIn);
vi.spyOn(api, "onAccount").mockResolvedValue(() => undefined);
let resolve: (view: SyncView) => void = () => undefined;
vi.spyOn(api, "settingsSync").mockImplementation(() => new Promise((done) => { resolve = done; }));
vi.spyOn(api, "onSettingsSync").mockResolvedValue(() => undefined);
render(<AccountSection />);
const placeholder = await screen.findByText("Checking settings sync…");
expect(placeholder).not.toHaveAttribute("role", "status");
act(() => resolve({ status: "upToDate", lastSyncedAt: null, message: null }));
expect(await screen.findByText("Settings are up to date.")).toHaveAttribute("role", "status");
});

it("follows sync changes from the backend", async () => {
let receive: (view: SyncView) => void = () => undefined;
vi.spyOn(api, "account").mockResolvedValue(signedIn);
Expand All @@ -71,17 +84,20 @@ describe("AccountSection", () => {
expect(await screen.findByText("Could not reach Switchify to sync settings.")).toBeInTheDocument();
});

it("is hidden while confirming account deletion", async () => {
it("stays in its own row while deletion is confirmed in another", async () => {
start(signedIn);
await screen.findByRole("button", { name: "Sync now" });
fireEvent.click(screen.getByRole("button", { name: "Delete account…" }));
expect(screen.queryByRole("button", { name: "Sync now" })).not.toBeInTheDocument();
const confirm = screen.getByRole("group", { name: "Confirm account deletion" });
expect(within(confirm).getByRole("button", { name: "Delete permanently" })).toBeInTheDocument();
expect(within(confirm).queryByRole("button", { name: "Sync now" })).not.toBeInTheDocument();
expect(screen.getByRole("button", { name: "Sync now" })).toBeInTheDocument();
});
});

it("says when accounts are unavailable in this build", async () => {
start({ ...signedOut, available: false });
expect(await screen.findByText("Accounts are unavailable in this build.")).toBeInTheDocument();
expect(await screen.findByRole("heading", { name: "Accounts are unavailable in this build" })).toBeInTheDocument();
expect(screen.queryByRole("textbox")).not.toBeInTheDocument();
});

Expand All @@ -103,7 +119,8 @@ describe("AccountSection", () => {
fireEvent.click(screen.getByRole("button", { name: "Sign in" }));
expect(verify).toHaveBeenCalledWith("123456");
await waitFor(() => expect(screen.getByRole("button", { name: "Sign out" })).toHaveFocus());
expect(screen.getByText(/Signed in as/)).toHaveTextContent("Signed in as me@example.com.");
expect(screen.getByRole("heading", { name: "me@example.com" })).toBeInTheDocument();
expect(screen.getByText(/^Signed in./)).toHaveTextContent("Signed in. It's the same account as Switchify on Android.");
});

it("shows a wrong-code error and keeps the code form", async () => {
Expand Down Expand Up @@ -136,7 +153,7 @@ describe("AccountSection", () => {
const remove = vi.spyOn(api, "deleteAccount").mockResolvedValue(signedOut);
fireEvent.click(await screen.findByRole("button", { name: "Delete account…" }));
expect(remove).not.toHaveBeenCalled();
expect(screen.getByText(/removes it and its saved settings from every device/)).toBeInTheDocument();
expect(screen.getByText(/removes your account and its saved settings from every device/)).toBeInTheDocument();
await waitFor(() => expect(screen.getByRole("button", { name: "Keep account" })).toHaveFocus());
fireEvent.click(screen.getByRole("button", { name: "Keep account" }));
await waitFor(() => expect(screen.getByRole("button", { name: "Delete account…" })).toHaveFocus());
Expand Down Expand Up @@ -237,10 +254,29 @@ describe("AccountSection", () => {

it("explains a locked keychain instead of offering sign-in", async () => {
start({ ...signedOut, keychainUnavailable: true });
expect(await screen.findByRole("heading", { name: "Can't read your saved sign-in" })).toBeInTheDocument();
expect(await screen.findByText(/keychain is locked or unavailable/)).toBeInTheDocument();
expect(screen.queryByLabelText("Email")).not.toBeInTheDocument();
});

it("shows a sign-out failure beside Sign out", async () => {
start(signedIn);
vi.spyOn(api, "signOut").mockRejectedValue("Could not reach Switchify.");
fireEvent.click(await screen.findByRole("button", { name: "Sign out" }));
const alert = await screen.findByRole("alert");
expect(alert).toHaveTextContent("Could not reach Switchify.");
expect(alert.closest("section")).toContainElement(screen.getByRole("button", { name: "Sign out" }));
});

it("shows a delete failure inside the confirmation", async () => {
start(signedIn);
vi.spyOn(api, "deleteAccount").mockRejectedValue("Could not delete the account. Try again.");
fireEvent.click(await screen.findByRole("button", { name: "Delete account…" }));
fireEvent.click(screen.getByRole("button", { name: "Delete permanently" }));
const confirm = screen.getByRole("group", { name: "Confirm account deletion" });
expect(await within(confirm).findByRole("alert")).toHaveTextContent("Could not delete the account.");
});

it("follows account changes from the backend", async () => {
let receive: (view: AccountView) => void = () => undefined;
vi.spyOn(api, "account").mockResolvedValue(signedOut);
Expand Down
Loading
Loading