1
0
mirror of https://github.com/matrix-org/matrix-authentication-service.git synced 2025-11-20 12:02:22 +03:00

frontend: write stories and tests for the user home

This commit is contained in:
Quentin Gliech
2023-08-08 17:30:42 +02:00
parent 3feb9113bb
commit d98e75f7b2
7 changed files with 733 additions and 4 deletions

View File

@@ -135,7 +135,9 @@ const routeToPath = (route: Route): string =>
.map((part) => encodeURIComponent(part)) .map((part) => encodeURIComponent(part))
.join("/"); .join("/");
export const appConfigAtom = atom(window.APP_CONFIG); export const appConfigAtom = atom<AppConfig>(
typeof window !== "undefined" ? window.APP_CONFIG : { root: "/" },
);
const pathToRoute = (path: string): Route => { const pathToRoute = (path: string): Route => {
const segments = path.split("/").map(decodeURIComponent); const segments = path.split("/").map(decodeURIComponent);

View File

@@ -27,7 +27,7 @@ import styles from "./UserEmail.module.css";
// This component shows a single user email address, with controls to verify it, // This component shows a single user email address, with controls to verify it,
// resend the verification email, remove it, and set it as the primary email address. // resend the verification email, remove it, and set it as the primary email address.
const FRAGMENT = graphql(/* GraphQL */ ` export const FRAGMENT = graphql(/* GraphQL */ `
fragment UserEmail_email on UserEmail { fragment UserEmail_email on UserEmail {
id id
email email

View File

@@ -12,4 +12,4 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
export { default } from "./UserEmail"; export { default, FRAGMENT } from "./UserEmail";

View File

@@ -0,0 +1,146 @@
// Copyright 2022 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import type { Meta, StoryObj } from "@storybook/react";
import { Provider } from "jotai";
import { useHydrateAtoms } from "jotai/utils";
import { appConfigAtom, locationAtom } from "../../Router";
import { makeFragmentData } from "../../gql";
import { FRAGMENT as EMAIL_FRAGMENT } from "../UserEmail";
import UserHome, { FRAGMENT } from "./UserHome";
type Props = {
primaryEmail: string | null;
unverifiedEmails: number;
confirmedEmails: number;
oauth2Sessions: number;
browserSessions: number;
compatSessions: number;
};
const WithHomePage: React.FC<React.PropsWithChildren<{}>> = ({ children }) => {
useHydrateAtoms([
[appConfigAtom, { root: "/" }],
[locationAtom, { pathname: "/" }],
]);
return <>{children}</>;
};
const Template: React.FC<Props> = ({
primaryEmail: email,
unverifiedEmails,
confirmedEmails,
oauth2Sessions,
browserSessions,
compatSessions,
}) => {
let primaryEmail = null;
if (email) {
primaryEmail = {
id: "email:123",
...makeFragmentData(
{
id: "email:123",
email,
confirmedAt: new Date(),
},
EMAIL_FRAGMENT,
),
};
}
const data = makeFragmentData(
{
id: "user:123",
primaryEmail,
unverifiedEmails: {
totalCount: unverifiedEmails,
},
confirmedEmails: {
totalCount: confirmedEmails,
},
oauth2Sessions: {
totalCount: oauth2Sessions,
},
browserSessions: {
totalCount: browserSessions,
},
compatSessions: {
totalCount: compatSessions,
},
},
FRAGMENT,
);
return (
<Provider>
<WithHomePage>
<UserHome user={data} />
</WithHomePage>
</Provider>
);
};
const meta = {
title: "Pages/User Home",
component: Template,
tags: ["autodocs"],
} satisfies Meta<typeof Template>;
export default meta;
type Story = StoryObj<typeof Template>;
export const Basic: Story = {
args: {
primaryEmail: "hello@example.com",
unverifiedEmails: 0,
confirmedEmails: 5,
oauth2Sessions: 3,
compatSessions: 1,
browserSessions: 2,
},
};
export const Empty: Story = {
args: {
primaryEmail: "hello@example.com",
unverifiedEmails: 0,
confirmedEmails: 1,
oauth2Sessions: 0,
compatSessions: 0,
browserSessions: 0,
},
};
export const NoPrimaryEmail: Story = {
args: {
primaryEmail: null,
unverifiedEmails: 0,
confirmedEmails: 0,
oauth2Sessions: 0,
compatSessions: 0,
browserSessions: 0,
},
};
export const UnverifiedEmails: Story = {
args: {
primaryEmail: "hello@example.com",
unverifiedEmails: 1,
confirmedEmails: 1,
oauth2Sessions: 0,
compatSessions: 0,
browserSessions: 0,
},
};

View File

@@ -0,0 +1,131 @@
// Copyright 2023 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { create } from "react-test-renderer";
import { describe, expect, it } from "vitest";
import { makeFragmentData } from "../../gql";
import { FRAGMENT as EMAIL_FRAGMENT } from "../UserEmail";
import UserHome, { FRAGMENT } from "./UserHome";
describe("UserHome", () => {
it("render an simple <UserHome />", () => {
const primaryEmail = makeFragmentData(
{
id: "email:123",
email: "hello@example.com",
confirmedAt: new Date(),
},
EMAIL_FRAGMENT,
);
const user = makeFragmentData(
{
id: "user:123",
primaryEmail: {
id: "email:123",
...primaryEmail,
},
compatSessions: {
totalCount: 0,
},
browserSessions: {
totalCount: 0,
},
oauth2Sessions: {
totalCount: 0,
},
unverifiedEmails: {
totalCount: 0,
},
confirmedEmails: {
totalCount: 1,
},
},
FRAGMENT,
);
const component = create(<UserHome user={user} />);
const tree = component.toJSON();
expect(tree).toMatchSnapshot();
});
it("render a <UserHome /> without primary email", () => {
const user = makeFragmentData(
{
id: "user:123",
primaryEmail: null,
compatSessions: {
totalCount: 0,
},
browserSessions: {
totalCount: 0,
},
oauth2Sessions: {
totalCount: 0,
},
unverifiedEmails: {
totalCount: 0,
},
confirmedEmails: {
totalCount: 0,
},
},
FRAGMENT,
);
const component = create(<UserHome user={user} />);
const tree = component.toJSON();
expect(tree).toMatchSnapshot();
});
it("render a <UserHome /> with an unverified email", () => {
const primaryEmail = makeFragmentData(
{
id: "email:123",
email: "hello@example.com",
confirmedAt: new Date(),
},
EMAIL_FRAGMENT,
);
const user = makeFragmentData(
{
id: "user:123",
primaryEmail: {
id: "email:123",
...primaryEmail,
},
compatSessions: {
totalCount: 0,
},
browserSessions: {
totalCount: 0,
},
oauth2Sessions: {
totalCount: 0,
},
unverifiedEmails: {
totalCount: 1,
},
confirmedEmails: {
totalCount: 1,
},
},
FRAGMENT,
);
const component = create(<UserHome user={user} />);
const tree = component.toJSON();
expect(tree).toMatchSnapshot();
});
});

View File

@@ -19,7 +19,7 @@ import { Link } from "../../Router";
import { FragmentType, graphql, useFragment } from "../../gql"; import { FragmentType, graphql, useFragment } from "../../gql";
import UserEmail from "../UserEmail"; import UserEmail from "../UserEmail";
const FRAGMENT = graphql(/* GraphQL */ ` export const FRAGMENT = graphql(/* GraphQL */ `
fragment UserHome_user on User { fragment UserHome_user on User {
id id

View File

@@ -0,0 +1,450 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`UserHome > render a <UserHome /> with an unverified email 1`] = `
[
<div
className="_alert_fxw8y_19"
data-type="critical"
>
<svg
aria-hidden={true}
className="_icon_fxw8y_52"
fill="currentColor"
height={24}
viewBox="0 0 24 24"
width={24}
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 17a.97.97 0 0 0 .713-.288A.968.968 0 0 0 13 16a.968.968 0 0 0-.287-.713A.968.968 0 0 0 12 15a.968.968 0 0 0-.713.287A.968.968 0 0 0 11 16c0 .283.096.52.287.712.192.192.43.288.713.288Zm0-4c.283 0 .52-.096.713-.287A.968.968 0 0 0 13 12V8a.967.967 0 0 0-.287-.713A.968.968 0 0 0 12 7a.968.968 0 0 0-.713.287A.967.967 0 0 0 11 8v4c0 .283.096.52.287.713.192.191.43.287.713.287Zm0 9a9.738 9.738 0 0 1-3.9-.788 10.099 10.099 0 0 1-3.175-2.137c-.9-.9-1.612-1.958-2.137-3.175A9.738 9.738 0 0 1 2 12a9.74 9.74 0 0 1 .788-3.9 10.099 10.099 0 0 1 2.137-3.175c.9-.9 1.958-1.612 3.175-2.137A9.738 9.738 0 0 1 12 2a9.74 9.74 0 0 1 3.9.788 10.098 10.098 0 0 1 3.175 2.137c.9.9 1.613 1.958 2.137 3.175A9.738 9.738 0 0 1 22 12a9.738 9.738 0 0 1-.788 3.9 10.098 10.098 0 0 1-2.137 3.175c-.9.9-1.958 1.613-3.175 2.137A9.738 9.738 0 0 1 12 22Z"
fill="currentColor"
/>
</svg>
<div
className="_content_fxw8y_44"
>
<p
className="_title_fxw8y_48"
>
Unverified email
</p>
<p>
You have
1
unverified email address(es).
<a
href="/emails"
onClick={[Function]}
>
Check
</a>
</p>
</div>
<svg
aria-label="Close"
className="_close_fxw8y_68"
fill="currentColor"
height={16}
onClick={[Function]}
role="button"
viewBox="0 0 24 24"
width={16}
xmlns="http://www.w3.org/2000/svg"
>
<path
d="m12 13.4-4.9 4.9a.948.948 0 0 1-.7.275.948.948 0 0 1-.7-.275.948.948 0 0 1-.275-.7.95.95 0 0 1 .275-.7l4.9-4.9-4.9-4.9a.948.948 0 0 1-.275-.7.95.95 0 0 1 .275-.7.948.948 0 0 1 .7-.275.95.95 0 0 1 .7.275l4.9 4.9 4.9-4.9a.948.948 0 0 1 .7-.275.95.95 0 0 1 .7.275.948.948 0 0 1 .275.7.948.948 0 0 1-.275.7L13.4 12l4.9 4.9a.948.948 0 0 1 .275.7.948.948 0 0 1-.275.7.948.948 0 0 1-.7.275.948.948 0 0 1-.7-.275L12 13.4Z"
fill="currentColor"
/>
</svg>
</div>,
<div
className="_userEmail_e2a518"
>
<p
style={
{
"font": "var(--cpd-font-body-md-regular)",
"letterSpacing": "var(--cpd-font-letter-spacing-body-md)",
}
}
>
Primary email
</p>
<div
className="_userEmailLine_e2a518"
>
<div
className="_userEmailField_e2a518"
>
hello@example.com
</div>
<button
className="_userEmailDelete_e2a518"
disabled={true}
onClick={[Function]}
title="Remove email address"
>
<svg
className="_userEmailDeleteIcon_e2a518"
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M7 21c-.55 0-1.02-.196-1.412-.587A1.926 1.926 0 0 1 5 19V6a.968.968 0 0 1-.713-.287A.968.968 0 0 1 4 5c0-.283.096-.52.287-.713A.968.968 0 0 1 5 4h4a.97.97 0 0 1 .287-.712A.968.968 0 0 1 10 3h4a.97.97 0 0 1 .713.288A.968.968 0 0 1 15 4h4a.97.97 0 0 1 .712.287c.192.192.288.43.288.713s-.096.52-.288.713A.968.968 0 0 1 19 6v13c0 .55-.196 1.02-.587 1.413A1.926 1.926 0 0 1 17 21H7Zm2-5c0 .283.096.52.287.712.192.192.43.288.713.288s.52-.096.713-.288A.968.968 0 0 0 11 16V9a.967.967 0 0 0-.287-.713A.968.968 0 0 0 10 8a.968.968 0 0 0-.713.287A.968.968 0 0 0 9 9v7Zm4 0c0 .283.096.52.287.712.192.192.43.288.713.288s.52-.096.713-.288A.968.968 0 0 0 15 16V9a.967.967 0 0 0-.287-.713A.968.968 0 0 0 14 8a.968.968 0 0 0-.713.287A.967.967 0 0 0 13 9v7Z"
fill="currentColor"
/>
</svg>
</button>
</div>
</div>,
<p
style={
{
"font": "var(--cpd-font-body-md-regular)",
"letterSpacing": "var(--cpd-font-letter-spacing-body-md)",
}
}
>
0
active browser session(s).
<a
href="/sessions"
onClick={[Function]}
>
View all
</a>
</p>,
<p
style={
{
"font": "var(--cpd-font-body-md-regular)",
"letterSpacing": "var(--cpd-font-letter-spacing-body-md)",
}
}
>
0
active OAuth2 session(s).
<a
href="/oauth2-sessions"
onClick={[Function]}
>
View all
</a>
</p>,
<p
style={
{
"font": "var(--cpd-font-body-md-regular)",
"letterSpacing": "var(--cpd-font-letter-spacing-body-md)",
}
}
>
0
active compatibility layer session(s).
<a
href="/compat-sessions"
onClick={[Function]}
>
View all
</a>
</p>,
]
`;
exports[`UserHome > render a <UserHome /> without primary email 1`] = `
[
<div
className="_alert_fxw8y_19"
data-type="critical"
>
<svg
aria-hidden={true}
className="_icon_fxw8y_52"
fill="currentColor"
height={24}
viewBox="0 0 24 24"
width={24}
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 17a.97.97 0 0 0 .713-.288A.968.968 0 0 0 13 16a.968.968 0 0 0-.287-.713A.968.968 0 0 0 12 15a.968.968 0 0 0-.713.287A.968.968 0 0 0 11 16c0 .283.096.52.287.712.192.192.43.288.713.288Zm0-4c.283 0 .52-.096.713-.287A.968.968 0 0 0 13 12V8a.967.967 0 0 0-.287-.713A.968.968 0 0 0 12 7a.968.968 0 0 0-.713.287A.967.967 0 0 0 11 8v4c0 .283.096.52.287.713.192.191.43.287.713.287Zm0 9a9.738 9.738 0 0 1-3.9-.788 10.099 10.099 0 0 1-3.175-2.137c-.9-.9-1.612-1.958-2.137-3.175A9.738 9.738 0 0 1 2 12a9.74 9.74 0 0 1 .788-3.9 10.099 10.099 0 0 1 2.137-3.175c.9-.9 1.958-1.612 3.175-2.137A9.738 9.738 0 0 1 12 2a9.74 9.74 0 0 1 3.9.788 10.098 10.098 0 0 1 3.175 2.137c.9.9 1.613 1.958 2.137 3.175A9.738 9.738 0 0 1 22 12a9.738 9.738 0 0 1-.788 3.9 10.098 10.098 0 0 1-2.137 3.175c-.9.9-1.958 1.613-3.175 2.137A9.738 9.738 0 0 1 12 22Z"
fill="currentColor"
/>
</svg>
<div
className="_content_fxw8y_44"
>
<p
className="_title_fxw8y_48"
>
No primary email adress
</p>
<p />
</div>
</div>,
<p
style={
{
"font": "var(--cpd-font-body-md-regular)",
"letterSpacing": "var(--cpd-font-letter-spacing-body-md)",
}
}
>
0
active browser session(s).
<a
href="/sessions"
onClick={[Function]}
>
View all
</a>
</p>,
<p
style={
{
"font": "var(--cpd-font-body-md-regular)",
"letterSpacing": "var(--cpd-font-letter-spacing-body-md)",
}
}
>
0
active OAuth2 session(s).
<a
href="/oauth2-sessions"
onClick={[Function]}
>
View all
</a>
</p>,
<p
style={
{
"font": "var(--cpd-font-body-md-regular)",
"letterSpacing": "var(--cpd-font-letter-spacing-body-md)",
}
}
>
0
active compatibility layer session(s).
<a
href="/compat-sessions"
onClick={[Function]}
>
View all
</a>
</p>,
]
`;
exports[`UserHome > render an active <NavItem /> 1`] = `
[
<div
className="_userEmail_e2a518"
>
<p
style={
{
"font": "var(--cpd-font-body-md-regular)",
"letterSpacing": "var(--cpd-font-letter-spacing-body-md)",
}
}
>
Primary email
</p>
<div
className="_userEmailLine_e2a518"
>
<div
className="_userEmailField_e2a518"
>
hello@example.com
</div>
<button
className="_userEmailDelete_e2a518"
disabled={true}
onClick={[Function]}
title="Remove email address"
>
<svg
className="_userEmailDeleteIcon_e2a518"
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M7 21c-.55 0-1.02-.196-1.412-.587A1.926 1.926 0 0 1 5 19V6a.968.968 0 0 1-.713-.287A.968.968 0 0 1 4 5c0-.283.096-.52.287-.713A.968.968 0 0 1 5 4h4a.97.97 0 0 1 .287-.712A.968.968 0 0 1 10 3h4a.97.97 0 0 1 .713.288A.968.968 0 0 1 15 4h4a.97.97 0 0 1 .712.287c.192.192.288.43.288.713s-.096.52-.288.713A.968.968 0 0 1 19 6v13c0 .55-.196 1.02-.587 1.413A1.926 1.926 0 0 1 17 21H7Zm2-5c0 .283.096.52.287.712.192.192.43.288.713.288s.52-.096.713-.288A.968.968 0 0 0 11 16V9a.967.967 0 0 0-.287-.713A.968.968 0 0 0 10 8a.968.968 0 0 0-.713.287A.968.968 0 0 0 9 9v7Zm4 0c0 .283.096.52.287.712.192.192.43.288.713.288s.52-.096.713-.288A.968.968 0 0 0 15 16V9a.967.967 0 0 0-.287-.713A.968.968 0 0 0 14 8a.968.968 0 0 0-.713.287A.967.967 0 0 0 13 9v7Z"
fill="currentColor"
/>
</svg>
</button>
</div>
</div>,
<p
style={
{
"font": "var(--cpd-font-body-md-regular)",
"letterSpacing": "var(--cpd-font-letter-spacing-body-md)",
}
}
>
0
active browser session(s).
<a
href="/sessions"
onClick={[Function]}
>
View all
</a>
</p>,
<p
style={
{
"font": "var(--cpd-font-body-md-regular)",
"letterSpacing": "var(--cpd-font-letter-spacing-body-md)",
}
}
>
0
active OAuth2 session(s).
<a
href="/oauth2-sessions"
onClick={[Function]}
>
View all
</a>
</p>,
<p
style={
{
"font": "var(--cpd-font-body-md-regular)",
"letterSpacing": "var(--cpd-font-letter-spacing-body-md)",
}
}
>
0
active compatibility layer session(s).
<a
href="/compat-sessions"
onClick={[Function]}
>
View all
</a>
</p>,
]
`;
exports[`UserHome > render an simple <UserHome /> 1`] = `
[
<div
className="_userEmail_e2a518"
>
<p
style={
{
"font": "var(--cpd-font-body-md-regular)",
"letterSpacing": "var(--cpd-font-letter-spacing-body-md)",
}
}
>
Primary email
</p>
<div
className="_userEmailLine_e2a518"
>
<div
className="_userEmailField_e2a518"
>
hello@example.com
</div>
<button
className="_userEmailDelete_e2a518"
disabled={true}
onClick={[Function]}
title="Remove email address"
>
<svg
className="_userEmailDeleteIcon_e2a518"
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M7 21c-.55 0-1.02-.196-1.412-.587A1.926 1.926 0 0 1 5 19V6a.968.968 0 0 1-.713-.287A.968.968 0 0 1 4 5c0-.283.096-.52.287-.713A.968.968 0 0 1 5 4h4a.97.97 0 0 1 .287-.712A.968.968 0 0 1 10 3h4a.97.97 0 0 1 .713.288A.968.968 0 0 1 15 4h4a.97.97 0 0 1 .712.287c.192.192.288.43.288.713s-.096.52-.288.713A.968.968 0 0 1 19 6v13c0 .55-.196 1.02-.587 1.413A1.926 1.926 0 0 1 17 21H7Zm2-5c0 .283.096.52.287.712.192.192.43.288.713.288s.52-.096.713-.288A.968.968 0 0 0 11 16V9a.967.967 0 0 0-.287-.713A.968.968 0 0 0 10 8a.968.968 0 0 0-.713.287A.968.968 0 0 0 9 9v7Zm4 0c0 .283.096.52.287.712.192.192.43.288.713.288s.52-.096.713-.288A.968.968 0 0 0 15 16V9a.967.967 0 0 0-.287-.713A.968.968 0 0 0 14 8a.968.968 0 0 0-.713.287A.967.967 0 0 0 13 9v7Z"
fill="currentColor"
/>
</svg>
</button>
</div>
</div>,
<p
style={
{
"font": "var(--cpd-font-body-md-regular)",
"letterSpacing": "var(--cpd-font-letter-spacing-body-md)",
}
}
>
0
active browser session(s).
<a
href="/sessions"
onClick={[Function]}
>
View all
</a>
</p>,
<p
style={
{
"font": "var(--cpd-font-body-md-regular)",
"letterSpacing": "var(--cpd-font-letter-spacing-body-md)",
}
}
>
0
active OAuth2 session(s).
<a
href="/oauth2-sessions"
onClick={[Function]}
>
View all
</a>
</p>,
<p
style={
{
"font": "var(--cpd-font-body-md-regular)",
"letterSpacing": "var(--cpd-font-letter-spacing-body-md)",
}
}
>
0
active compatibility layer session(s).
<a
href="/compat-sessions"
onClick={[Function]}
>
View all
</a>
</p>,
]
`;