The demo account — one shared user that whitelisted emails entered from the login page, reset on a schedule — goes end to end (owner decision, E34): demo.py and the reset job, users.is_demo and demo_whitelist (migration 0098 deletes the demo user, its audit rows, the reset job's override and notices, then drops the column and table; the downgrade restores the empty schema), the /auth/demo entry, the admin whitelist and reset, the Demo tab, the banner, the login page's "Try the demo", and every is_demo branch in routes, userscope, scheduler and the UI. require_human only ever refused the demo account, so its 87 uses become current_user (owner-approved); require_write_scope keeps its own check. The login page's "or" divider now shows only beside the Google button: without the demo entry under it, an instance without Google would have said "or" to nothing. A contract test (removedFeatures) fails on the word anywhere in the backend, frontend, e2e or scripts — seen red on a planted hit. Size ceilings tightened for the modules that shrank; admin.py is back under the limit.
56 lines
1.8 KiB
TypeScript
56 lines
1.8 KiB
TypeScript
// @vitest-environment jsdom
|
|
//
|
|
// Settings → Account names the account's latest sign-in (prod UAT 2026-09-19: only the admin's user
|
|
// list showed it), and its role in the reader's language — the raw `user` read in English under HU.
|
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
import { cleanup, render, screen } from "@testing-library/react";
|
|
|
|
import type { Me } from "../lib/api";
|
|
|
|
vi.mock("react-i18next", async (importOriginal) => {
|
|
const actual = await importOriginal<typeof import("react-i18next")>();
|
|
return {
|
|
...actual,
|
|
useTranslation: () => ({
|
|
t: (key: string, vars?: Record<string, unknown>) =>
|
|
vars?.time != null ? `${key}:${String(vars.time)}` : key,
|
|
i18n: { language: "en" },
|
|
}),
|
|
};
|
|
});
|
|
vi.mock("../lib/format", async (importOriginal) => {
|
|
const actual = await importOriginal<typeof import("../lib/format")>();
|
|
return { ...actual, formatDateTime: (iso: string) => `at ${iso}` };
|
|
});
|
|
|
|
import { AccountHeader } from "./SettingsPanel";
|
|
|
|
afterEach(cleanup);
|
|
|
|
const me = (extra: Partial<Me>): Me =>
|
|
({
|
|
email: "someone@example.com",
|
|
display_name: "Someone",
|
|
avatar_url: null,
|
|
role: "user",
|
|
last_login_at: null,
|
|
...extra,
|
|
}) as Me;
|
|
|
|
describe("AccountHeader", () => {
|
|
it("names the latest sign-in and the role in the reader's language", () => {
|
|
render(<AccountHeader me={me({ last_login_at: "2026-09-19T02:00:00Z" })} />);
|
|
expect(screen.getByTestId("account-last-login").textContent).toBe(
|
|
"settings.account.lastLogin:at 2026-09-19T02:00:00Z",
|
|
);
|
|
expect(screen.getByText("nav.role.user")).toBeTruthy();
|
|
});
|
|
|
|
it("says when none is recorded", () => {
|
|
render(<AccountHeader me={me({})} />);
|
|
expect(screen.getByTestId("account-last-login").textContent).toBe(
|
|
"settings.account.lastLoginNone",
|
|
);
|
|
});
|
|
});
|