From 9f59a98fa47a4a3fcf1526a3de765171173b1335 Mon Sep 17 00:00:00 2001 From: Matthew Wilcoxson Date: Tue, 18 Nov 2025 16:20:06 +0000 Subject: [PATCH 1/3] Add auth to User control. --- src/components/controls/User.stories.tsx | 92 ++++++++++++++++++++++- src/components/controls/User.test.tsx | 96 ++++++++++++++++++++++-- src/components/controls/User.tsx | 30 ++++++-- 3 files changed, 199 insertions(+), 19 deletions(-) diff --git a/src/components/controls/User.stories.tsx b/src/components/controls/User.stories.tsx index 7e64d04..6f83d8f 100644 --- a/src/components/controls/User.stories.tsx +++ b/src/components/controls/User.stories.tsx @@ -2,11 +2,19 @@ import { Meta, StoryObj } from "@storybook/react"; import { User } from "./User"; import { Avatar, Link, MenuItem } from "@mui/material"; +import {Auth} from "../systems/auth"; const meta: Meta = { title: "Components/Controls/User", component: User, tags: ["autodocs"], + parameters: { + docs: { + description: { + component: "A control to login/logout with, and to show user info.", + }, + }, + }, }; export default meta; @@ -14,14 +22,35 @@ type Story = StoryObj; export const LoggedOut: Story = { args: { user: null }, + parameters: { + docs: { + description: { + story: "Default display when not yet logged in.", + }, + }, + } }; export const LoggedIn: Story = { args: { user: { name: "Name Surname", fedid: "FedID" }, onLogout: () => {} }, + parameters: { + docs: { + description: { + story: "Default display when logged in.", + }, + }, + }, }; -export const LoggedInNoName: Story = { - args: { user: { fedid: "FedID" }, onLogout: () => {} }, +export const LoggedInNoFedId: Story = { + args: { user: { name: "User's Name" }, onLogout: () => {} }, + parameters: { + docs: { + description: { + story: "Logged in, but no Fed ID.", + }, + }, + }, }; export const LoggedInLongName: Story = { @@ -29,6 +58,13 @@ export const LoggedInLongName: Story = { user: { name: "Jonathan Edwards Longname", fedid: "abc12345" }, onLogout: () => {}, }, + parameters: { + docs: { + description: { + story: "Logged in with a long name.", + }, + }, + }, }; export const LoggedInChangeColour: Story = { @@ -37,14 +73,28 @@ export const LoggedInChangeColour: Story = { user: { name: "Name Surname", fedid: "abc12345" }, onLogout: () => {}, }, + parameters: { + docs: { + description: { + story: "You can change the colour used to display it.", + }, + }, + }, }; export const LoggedInReplaceAvatar: Story = { args: { user: { name: "Name Surname", fedid: "abc12345" }, - avatar: JL, + avatar: SRU, onLogout: () => {}, }, + parameters: { + docs: { + description: { + story: "You can change the avatar image. Perhaps use a photo.", + }, + }, + }, }; export const AdditionalMenuItems: Story = { @@ -63,4 +113,40 @@ export const AdditionalMenuItems: Story = { ], onLogout: () => {}, }, + parameters: { + docs: { + description: { + story: "You can add additional menu items.", + }, + }, + }, }; + + +export const UsingAuth: Story = { + args: { + auth: { + authenticated: false, + initialised: false, + getProfileUrl:() => "", + getToken: () => "", + login() {}, + logout() {}, + _keycloak: null, + user: { + name: "User Name ", + givenName: "", + familyName: "", + fedId: "", + email: "" + } + } + }, + parameters: { + docs: { + description: { + story: "If you are using SciReactUI's auth mechanism, you can simply pass the useAuth counterpart in.", + }, + }, + }, +}; \ No newline at end of file diff --git a/src/components/controls/User.test.tsx b/src/components/controls/User.test.tsx index 67cd46d..9915771 100644 --- a/src/components/controls/User.test.tsx +++ b/src/components/controls/User.test.tsx @@ -1,16 +1,21 @@ import { fireEvent, screen } from "@testing-library/react"; import { Avatar, MenuItem } from "@mui/material"; -import { User } from "./User"; import { renderWithProviders } from "../../__test-utils__/helpers"; +import {Auth} from "../systems/auth"; + +import { User } from "./User"; +import {KeycloakLoginOptions, KeycloakLogoutOptions} from "keycloak-js"; describe("User", () => { + it("should render", () => { renderWithProviders( - {}} onLogout={() => {}} user={null} />, + 0} onLogout={()=>0} user={null} />, ); - renderWithProviders( {}} user={null} />); - renderWithProviders( {}} user={null} />); + renderWithProviders(0} user={null} />); + renderWithProviders(0} user={null} />); renderWithProviders(); + renderWithProviders(); }); it("should display login button when not authenticated", () => { @@ -103,11 +108,13 @@ describe("User", () => { }); it("should display additional menu items when provided", () => { - const { getByRole } = renderWithProviders( + const {getByRole} = renderWithProviders( {}} - onLogout={() => {}} - user={{ name: "Name", fedid: "FedID" }} + onLogin={() => { + }} + onLogout={() => { + }} + user={{name: "Name", fedid: "FedID"}} menuItems={[ Profile @@ -126,4 +133,77 @@ describe("User", () => { expect(screen.getByText("Settings")).toBeInTheDocument(); expect(screen.getByText("Logout")).toBeInTheDocument(); }); + }); + +describe("User with Auth", () => { + + const authDummy: Auth = { + authenticated: false, + initialised: false, + getProfileUrl:() => "", + getToken: () => "", + login() {}, + logout() {}, + _keycloak: null, + } + const authDummyUser = { + name: "", + givenName: "", + familyName: "", + fedId: "", + email: "" + } + + it("should render", () => { + renderWithProviders(); + }); + + it("should use auth name when passed in", () => { + const auth: Auth = { + ...authDummy, + user: { + ...authDummyUser, + name: "test name", + } + } + const { queryByText } = renderWithProviders(); + // @ts-ignore + expect( queryByText(auth.user.name)).toBeInTheDocument() + }); + + it("should fire auth login callback when button is clicked", () => { + const loginCallback = vi.fn(); + const auth = { + ...authDummy, + login: loginCallback + } + const { getByText } = renderWithProviders( + , + ); + + const loginButton = getByText("Login"); + fireEvent.click(loginButton); + + expect(loginCallback).toHaveBeenCalledTimes(1); + }); + + it("should display additional menu item when auth", () => { + const auth: Auth = { + ...authDummy, + user: { + ...authDummyUser, + name: "test name", + } + } + const { getByRole } = renderWithProviders( + , + ); + + const userMenu = getByRole("button"); + fireEvent.click(userMenu); + + expect(screen.getByText("Profile")).toBeInTheDocument(); + }); + +}) \ No newline at end of file diff --git a/src/components/controls/User.tsx b/src/components/controls/User.tsx index 77cd4cd..b167212 100644 --- a/src/components/controls/User.tsx +++ b/src/components/controls/User.tsx @@ -10,23 +10,24 @@ import { Typography, useTheme, } from "@mui/material"; +import {ReactElement, ReactNode, useState} from "react"; +import {MdLogin} from "react-icons/md"; -import { ReactElement, ReactNode, useState } from "react"; - -import { MdLogin } from "react-icons/md"; +import {Auth} from "../systems/auth" interface AuthState { - fedid: string; + fedid?: string; name?: string; } interface UserProps { - user: AuthState | null; + user?: AuthState | null; onLogin?: () => void; onLogout?: () => void; avatar?: ReactNode; colour?: string; menuItems?: ReactElement | ReactElement[]; + auth?: Auth; } const User = ({ @@ -36,6 +37,7 @@ const User = ({ avatar, colour, menuItems, + auth }: UserProps) => { const [anchorEl, setAnchorEl] = useState(null); const open = Boolean(anchorEl); @@ -48,15 +50,21 @@ const User = ({ }; const handleLogin = () => { - if (onLogin) onLogin(); + if(auth) auth.login() + if(onLogin) onLogin(); }; const handleLogout = () => { handleClose(); - if (onLogout) onLogout(); + if(auth) auth.logout() + if(onLogout) onLogout(); }; const theme = useTheme(); + if( !user && auth && auth.user ) { + user = { name: auth.user.name } + } + return ( <> @@ -117,7 +125,8 @@ const User = ({ - {(onLogout || menuItems) && ( + + {(onLogout || menuItems || auth) && ( {menuItems} + {auth && ( + + Profile + + )} Logout From 2e3f309a0bda882e5cfa5b34e5e7f2e2ac2e39eb Mon Sep 17 00:00:00 2001 From: Matthew Wilcoxson Date: Tue, 25 Nov 2025 10:55:36 +0000 Subject: [PATCH 2/3] Documentation tweaks --- src/components/controls/User.stories.tsx | 7 +++++-- src/storybook/helpers/Auth.mdx | 8 ++------ 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/src/components/controls/User.stories.tsx b/src/components/controls/User.stories.tsx index 6f83d8f..98d3476 100644 --- a/src/components/controls/User.stories.tsx +++ b/src/components/controls/User.stories.tsx @@ -122,7 +122,6 @@ export const AdditionalMenuItems: Story = { }, }; - export const UsingAuth: Story = { args: { auth: { @@ -145,7 +144,11 @@ export const UsingAuth: Story = { parameters: { docs: { description: { - story: "If you are using SciReactUI's auth mechanism, you can simply pass the useAuth counterpart in.", + story: "If you are using SciReactUI's auth mechanism, you can simply pass the useAuth counterpart in." + + "

" + + "
const auth = useAuth();
" + + "
" + + "
<User auth={auth}/>
", }, }, }, diff --git a/src/storybook/helpers/Auth.mdx b/src/storybook/helpers/Auth.mdx index 1302705..7870e11 100644 --- a/src/storybook/helpers/Auth.mdx +++ b/src/storybook/helpers/Auth.mdx @@ -52,13 +52,9 @@ import {useAuth} from "../../components/systems/auth"; return } + // Return page content for authenticated users return (<> - + I'm authenticating ) } From 6ba661649add63c7774a83015fd778246f5a1d00 Mon Sep 17 00:00:00 2001 From: Matthew Wilcoxson Date: Tue, 25 Nov 2025 11:22:12 +0000 Subject: [PATCH 3/3] Headache inducing linting. --- src/components/controls/User.stories.tsx | 16 +++--- src/components/controls/User.test.tsx | 67 ++++++++++-------------- src/components/controls/User.tsx | 35 +++++++------ 3 files changed, 56 insertions(+), 62 deletions(-) diff --git a/src/components/controls/User.stories.tsx b/src/components/controls/User.stories.tsx index 98d3476..2bff14d 100644 --- a/src/components/controls/User.stories.tsx +++ b/src/components/controls/User.stories.tsx @@ -2,7 +2,6 @@ import { Meta, StoryObj } from "@storybook/react"; import { User } from "./User"; import { Avatar, Link, MenuItem } from "@mui/material"; -import {Auth} from "../systems/auth"; const meta: Meta = { title: "Components/Controls/User", @@ -28,7 +27,7 @@ export const LoggedOut: Story = { story: "Default display when not yet logged in.", }, }, - } + }, }; export const LoggedIn: Story = { @@ -127,7 +126,7 @@ export const UsingAuth: Story = { auth: { authenticated: false, initialised: false, - getProfileUrl:() => "", + getProfileUrl: () => "", getToken: () => "", login() {}, logout() {}, @@ -137,14 +136,15 @@ export const UsingAuth: Story = { givenName: "", familyName: "", fedId: "", - email: "" - } - } + email: "", + }, + }, }, parameters: { docs: { description: { - story: "If you are using SciReactUI's auth mechanism, you can simply pass the useAuth counterpart in." + + story: + "If you are using SciReactUI's auth mechanism, you can simply pass the useAuth counterpart in." + "

" + "
const auth = useAuth();
" + "
" + @@ -152,4 +152,4 @@ export const UsingAuth: Story = { }, }, }, -}; \ No newline at end of file +}; diff --git a/src/components/controls/User.test.tsx b/src/components/controls/User.test.tsx index 9915771..4188bec 100644 --- a/src/components/controls/User.test.tsx +++ b/src/components/controls/User.test.tsx @@ -1,19 +1,17 @@ import { fireEvent, screen } from "@testing-library/react"; import { Avatar, MenuItem } from "@mui/material"; import { renderWithProviders } from "../../__test-utils__/helpers"; -import {Auth} from "../systems/auth"; +import { Auth } from "../systems/auth"; import { User } from "./User"; -import {KeycloakLoginOptions, KeycloakLogoutOptions} from "keycloak-js"; describe("User", () => { - it("should render", () => { renderWithProviders( - 0} onLogout={()=>0} user={null} />, + 0} onLogout={() => 0} user={null} />, ); - renderWithProviders(0} user={null} />); - renderWithProviders(0} user={null} />); + renderWithProviders( 0} user={null} />); + renderWithProviders( 0} user={null} />); renderWithProviders(); renderWithProviders(); }); @@ -108,13 +106,11 @@ describe("User", () => { }); it("should display additional menu items when provided", () => { - const {getByRole} = renderWithProviders( + const { getByRole } = renderWithProviders( { - }} - onLogout={() => { - }} - user={{name: "Name", fedid: "FedID"}} + onLogin={() => {}} + onLogout={() => {}} + user={{ name: "Name", fedid: "FedID" }} menuItems={[ Profile @@ -133,77 +129,70 @@ describe("User", () => { expect(screen.getByText("Settings")).toBeInTheDocument(); expect(screen.getByText("Logout")).toBeInTheDocument(); }); - }); describe("User with Auth", () => { - const authDummy: Auth = { authenticated: false, initialised: false, - getProfileUrl:() => "", + getProfileUrl: () => "", getToken: () => "", login() {}, logout() {}, _keycloak: null, - } + }; const authDummyUser = { name: "", givenName: "", familyName: "", fedId: "", - email: "" - } + email: "", + }; it("should render", () => { - renderWithProviders(); + renderWithProviders(); }); - + it("should use auth name when passed in", () => { const auth: Auth = { ...authDummy, user: { ...authDummyUser, name: "test name", - } - } - const { queryByText } = renderWithProviders(); - // @ts-ignore - expect( queryByText(auth.user.name)).toBeInTheDocument() + }, + }; + const { queryByText } = renderWithProviders(); + // @ts-expect-error It is not null, it will never be null. + expect(queryByText(auth.user.name)).toBeInTheDocument(); }); - + it("should fire auth login callback when button is clicked", () => { const loginCallback = vi.fn(); const auth = { ...authDummy, - login: loginCallback - } - const { getByText } = renderWithProviders( - , - ); + login: loginCallback, + }; + const { getByText } = renderWithProviders(); const loginButton = getByText("Login"); fireEvent.click(loginButton); expect(loginCallback).toHaveBeenCalledTimes(1); }); - + it("should display additional menu item when auth", () => { const auth: Auth = { ...authDummy, user: { ...authDummyUser, name: "test name", - } - } - const { getByRole } = renderWithProviders( - , - ); + }, + }; + const { getByRole } = renderWithProviders(); const userMenu = getByRole("button"); fireEvent.click(userMenu); expect(screen.getByText("Profile")).toBeInTheDocument(); }); - -}) \ No newline at end of file +}); diff --git a/src/components/controls/User.tsx b/src/components/controls/User.tsx index b167212..75b27e4 100644 --- a/src/components/controls/User.tsx +++ b/src/components/controls/User.tsx @@ -10,10 +10,10 @@ import { Typography, useTheme, } from "@mui/material"; -import {ReactElement, ReactNode, useState} from "react"; -import {MdLogin} from "react-icons/md"; +import { ReactElement, ReactNode, useState } from "react"; +import { MdLogin } from "react-icons/md"; -import {Auth} from "../systems/auth" +import { Auth } from "../systems/auth"; interface AuthState { fedid?: string; @@ -37,7 +37,7 @@ const User = ({ avatar, colour, menuItems, - auth + auth, }: UserProps) => { const [anchorEl, setAnchorEl] = useState(null); const open = Boolean(anchorEl); @@ -50,21 +50,21 @@ const User = ({ }; const handleLogin = () => { - if(auth) auth.login() - if(onLogin) onLogin(); + if (auth) auth.login(); + if (onLogin) onLogin(); }; const handleLogout = () => { handleClose(); - if(auth) auth.logout() - if(onLogout) onLogout(); + if (auth) auth.logout(); + if (onLogout) onLogout(); }; const theme = useTheme(); - if( !user && auth && auth.user ) { - user = { name: auth.user.name } + if (!user && auth && auth.user) { + user = { name: auth.user.name }; } - + return ( <> @@ -125,7 +125,7 @@ const User = ({ - + {(onLogout || menuItems || auth) && ( {menuItems} {auth && ( - - Profile - + + + Profile + + )} Logout