diff --git a/frontend/src/components/UnverifiedEmailAlert/UnverifiedEmailAlert.module.css b/frontend/src/components/UnverifiedEmailAlert/UnverifiedEmailAlert.module.css new file mode 100644 index 000000000..82c9ef32c --- /dev/null +++ b/frontend/src/components/UnverifiedEmailAlert/UnverifiedEmailAlert.module.css @@ -0,0 +1,18 @@ +/* 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. + */ + +.alert { + margin-top: var(--cpd-space-4x); +} \ No newline at end of file diff --git a/frontend/src/components/UnverifiedEmailAlert/UnverifiedEmailAlert.test.tsx b/frontend/src/components/UnverifiedEmailAlert/UnverifiedEmailAlert.test.tsx new file mode 100644 index 000000000..bb254274d --- /dev/null +++ b/frontend/src/components/UnverifiedEmailAlert/UnverifiedEmailAlert.test.tsx @@ -0,0 +1,95 @@ +// 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. + +// @vitest-environment happy-dom + +import { render, cleanup, fireEvent } from "@testing-library/react"; +import { describe, it, expect, afterEach } from "vitest"; + +import { makeFragmentData } from "../../gql/fragment-masking"; +import { WithLocation } from "../../test-utils/WithLocation"; + +import UnverifiedEmailAlert, { + UNVERIFIED_EMAILS_FRAGMENT, +} from "./UnverifiedEmailAlert"; + +describe("", () => { + afterEach(cleanup); + + it("does not render a warning when there are no unverified emails", () => { + const data = makeFragmentData( + { + id: "abc123", + unverifiedEmails: { + totalCount: 0, + }, + }, + UNVERIFIED_EMAILS_FRAGMENT, + ); + + const { container } = render( + + + , + ); + + expect(container).toMatchInlineSnapshot("
"); + }); + + it("renders a warning when there are unverified emails", () => { + const data = makeFragmentData( + { + id: "abc123", + unverifiedEmails: { + totalCount: 2, + }, + }, + UNVERIFIED_EMAILS_FRAGMENT, + ); + + const { container } = render( + + + , + ); + + expect(container).toMatchSnapshot(); + }); + + it("hides warning after it has been dismissed", () => { + const data = makeFragmentData( + { + id: "abc123", + unverifiedEmails: { + totalCount: 2, + }, + }, + UNVERIFIED_EMAILS_FRAGMENT, + ); + + const { container, getByText, getByLabelText } = render( + + + , + ); + + // warning is rendered + expect(getByText("Unverified email")).toBeTruthy(); + + fireEvent.click(getByLabelText("Close")); + + // no more warning + expect(container).toMatchInlineSnapshot("
"); + }); +}); diff --git a/frontend/src/components/UnverifiedEmailAlert/UnverifiedEmailAlert.tsx b/frontend/src/components/UnverifiedEmailAlert/UnverifiedEmailAlert.tsx new file mode 100644 index 000000000..fe3581e0b --- /dev/null +++ b/frontend/src/components/UnverifiedEmailAlert/UnverifiedEmailAlert.tsx @@ -0,0 +1,60 @@ +// 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 { Alert } from "@vector-im/compound-web"; +import { useState } from "react"; + +import { Link } from "../../Router"; +import { FragmentType, useFragment } from "../../gql/fragment-masking"; +import { graphql } from "../../gql/gql"; + +import styles from "./UnverifiedEmailAlert.module.css"; + +export const UNVERIFIED_EMAILS_FRAGMENT = graphql(/* GraphQL */ ` + fragment UnverifiedEmailAlert on User { + id + unverifiedEmails: emails(first: 0, state: PENDING) { + totalCount + } + } +`); + +const UnverifiedEmailAlert: React.FC<{ + unverifiedEmails?: FragmentType; +}> = ({ unverifiedEmails }) => { + const data = useFragment(UNVERIFIED_EMAILS_FRAGMENT, unverifiedEmails); + const [dismiss, setDismiss] = useState(false); + + const doDismiss = (): void => setDismiss(true); + + if (!data?.unverifiedEmails?.totalCount || dismiss) { + return null; + } + + return ( + + You have {data.unverifiedEmails.totalCount} unverified email address(es).{" "} + + Review and verify + + + ); +}; + +export default UnverifiedEmailAlert; diff --git a/frontend/src/components/UnverifiedEmailAlert/__snapshots__/UnverifiedEmailAlert.test.tsx.snap b/frontend/src/components/UnverifiedEmailAlert/__snapshots__/UnverifiedEmailAlert.test.tsx.snap new file mode 100644 index 000000000..f6fdd7623 --- /dev/null +++ b/frontend/src/components/UnverifiedEmailAlert/__snapshots__/UnverifiedEmailAlert.test.tsx.snap @@ -0,0 +1,61 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[` > renders a warning when there are unverified emails 1`] = ` +
+
+ +
+

+ Unverified email +

+

+ You have + 2 + unverified email address(es). + + + Review and verify + +

+
+ + + +
+
+`; diff --git a/frontend/src/components/UnverifiedEmailAlert/index.ts b/frontend/src/components/UnverifiedEmailAlert/index.ts new file mode 100644 index 000000000..09eb11394 --- /dev/null +++ b/frontend/src/components/UnverifiedEmailAlert/index.ts @@ -0,0 +1,15 @@ +// 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. + +export { default } from "./UnverifiedEmailAlert"; diff --git a/frontend/src/components/UserGreeting.tsx b/frontend/src/components/UserGreeting.tsx index 0d81d89fd..d91d53f1b 100644 --- a/frontend/src/components/UserGreeting.tsx +++ b/frontend/src/components/UserGreeting.tsx @@ -18,6 +18,11 @@ import { atomFamily } from "jotai/utils"; import { atomWithQuery } from "jotai-urql"; import { graphql } from "../gql"; +import { FragmentType } from "../gql/fragment-masking"; + +import UnverifiedEmailAlert, { + UNVERIFIED_EMAILS_FRAGMENT, +} from "./UnverifiedEmailAlert/UnverifiedEmailAlert"; const QUERY = graphql(/* GraphQL */ ` query UserGreeting($userId: ID!) { @@ -29,6 +34,14 @@ const QUERY = graphql(/* GraphQL */ ` displayName } } + viewer { + __typename + + ... on User { + id + ...UnverifiedEmailAlert + } + } } `); @@ -47,17 +60,26 @@ const UserGreeting: React.FC<{ userId: string }> = ({ userId }) => { if (result.data?.user) { const user = result.data.user; return ( -
- +
+ + + {user.matrix.displayName || user.username} + + {user.matrix.mxid} +
+ + } /> - - {user.matrix.displayName || user.username} - - {user.matrix.mxid} -
+ ); } diff --git a/frontend/src/components/UserHome/UserHome.tsx b/frontend/src/components/UserHome/UserHome.tsx index fbfcc1b2c..c4c6119d7 100644 --- a/frontend/src/components/UserHome/UserHome.tsx +++ b/frontend/src/components/UserHome/UserHome.tsx @@ -12,8 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Alert, Body, H3, H6 } from "@vector-im/compound-web"; -import { useState } from "react"; +import { Body, H3, H6 } from "@vector-im/compound-web"; import { Link } from "../../Router"; import { FragmentType, graphql, useFragment } from "../../gql"; @@ -35,10 +34,6 @@ export const FRAGMENT = graphql(/* GraphQL */ ` totalCount } - unverifiedEmails: emails(first: 0, state: PENDING) { - totalCount - } - browserSessions(first: 0, state: ACTIVE) { totalCount } @@ -57,11 +52,6 @@ const UserHome: React.FC<{ user: FragmentType; }> = ({ user }) => { const data = useFragment(FRAGMENT, user); - const [dismiss, setDismiss] = useState(false); - - const doDismiss = (): void => { - setDismiss(true); - }; // allow this until we get i18n const pluraliseSession = (count: number): string => @@ -74,14 +64,7 @@ const UserHome: React.FC<{ return ( - {data.unverifiedEmails.totalCount > 0 && !dismiss && ( - - You have {data.unverifiedEmails.totalCount} unverified email - address(es). Check - - )} {/* This is a short term solution, so I won't bother extracting these blocks into components */} -

Where you're signed in

@@ -91,7 +74,9 @@ const UserHome: React.FC<{ {pluraliseSession(data.browserSessions.totalCount)}
- View all + + View all +
@@ -101,7 +86,9 @@ const UserHome: React.FC<{ {pluraliseSession(data.oauth2Sessions.totalCount)}
- View all + + View all +
@@ -111,7 +98,9 @@ const UserHome: React.FC<{ {pluraliseSession(data.compatSessions.totalCount)}
- View all + + View all +
); diff --git a/frontend/src/components/UserHome/__snapshots__/UserHome.test.tsx.snap b/frontend/src/components/UserHome/__snapshots__/UserHome.test.tsx.snap index b57554800..4f85ba28f 100644 --- a/frontend/src/components/UserHome/__snapshots__/UserHome.test.tsx.snap +++ b/frontend/src/components/UserHome/__snapshots__/UserHome.test.tsx.snap @@ -30,6 +30,7 @@ exports[`UserHome > render a with sessions 1`] = `

@@ -57,6 +58,7 @@ exports[`UserHome > render a with sessions 1`] = `

@@ -84,6 +86,7 @@ exports[`UserHome > render a with sessions 1`] = `

@@ -123,6 +126,7 @@ exports[`UserHome > render an simple 1`] = `

@@ -150,6 +154,7 @@ exports[`UserHome > render an simple 1`] = `

@@ -177,6 +182,7 @@ exports[`UserHome > render an simple 1`] = `

diff --git a/frontend/src/gql/gql.ts b/frontend/src/gql/gql.ts index 199583a95..8591d7444 100644 --- a/frontend/src/gql/gql.ts +++ b/frontend/src/gql/gql.ts @@ -37,15 +37,17 @@ const documents = { types.EndOAuth2SessionDocument, "\n query OAuth2SessionListQuery(\n $userId: ID!\n $state: Oauth2SessionState\n $first: Int\n $after: String\n $last: Int\n $before: String\n ) {\n user(id: $userId) {\n id\n oauth2Sessions(\n state: $state\n first: $first\n after: $after\n last: $last\n before: $before\n ) {\n edges {\n cursor\n node {\n id\n ...OAuth2Session_session\n }\n }\n\n totalCount\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n }\n }\n": types.OAuth2SessionListQueryDocument, + "\n fragment UnverifiedEmailAlert on User {\n id\n unverifiedEmails: emails(first: 0, state: PENDING) {\n totalCount\n }\n }\n": + types.UnverifiedEmailAlertFragmentDoc, "\n fragment UserEmail_email on UserEmail {\n id\n email\n confirmedAt\n }\n": types.UserEmail_EmailFragmentDoc, "\n mutation RemoveEmail($id: ID!) {\n removeEmail(input: { userEmailId: $id }) {\n status\n\n user {\n id\n }\n }\n }\n": types.RemoveEmailDocument, "\n mutation SetPrimaryEmail($id: ID!) {\n setPrimaryEmail(input: { userEmailId: $id }) {\n status\n user {\n id\n primaryEmail {\n id\n }\n }\n }\n }\n": types.SetPrimaryEmailDocument, - "\n query UserGreeting($userId: ID!) {\n user(id: $userId) {\n id\n username\n matrix {\n mxid\n displayName\n }\n }\n }\n": + "\n query UserGreeting($userId: ID!) {\n user(id: $userId) {\n id\n username\n matrix {\n mxid\n displayName\n }\n }\n viewer {\n __typename\n\n ... on User {\n id\n ...UnverifiedEmailAlert\n }\n }\n }\n": types.UserGreetingDocument, - "\n fragment UserHome_user on User {\n id\n\n primaryEmail {\n id\n ...UserEmail_email\n }\n\n confirmedEmails: emails(first: 0, state: CONFIRMED) {\n totalCount\n }\n\n unverifiedEmails: emails(first: 0, state: PENDING) {\n totalCount\n }\n\n browserSessions(first: 0, state: ACTIVE) {\n totalCount\n }\n\n oauth2Sessions(first: 0, state: ACTIVE) {\n totalCount\n }\n\n compatSessions(first: 0, state: ACTIVE) {\n totalCount\n }\n }\n": + "\n fragment UserHome_user on User {\n id\n\n primaryEmail {\n id\n ...UserEmail_email\n }\n\n confirmedEmails: emails(first: 0, state: CONFIRMED) {\n totalCount\n }\n\n browserSessions(first: 0, state: ACTIVE) {\n totalCount\n }\n\n oauth2Sessions(first: 0, state: ACTIVE) {\n totalCount\n }\n\n compatSessions(first: 0, state: ACTIVE) {\n totalCount\n }\n }\n": types.UserHome_UserFragmentDoc, "\n mutation AddEmail($userId: ID!, $email: String!) {\n addEmail(input: { userId: $userId, email: $email }) {\n status\n violations\n email {\n id\n ...UserEmail_email\n }\n }\n }\n": types.AddEmailDocument, @@ -157,6 +159,12 @@ export function graphql( export function graphql( source: "\n query OAuth2SessionListQuery(\n $userId: ID!\n $state: Oauth2SessionState\n $first: Int\n $after: String\n $last: Int\n $before: String\n ) {\n user(id: $userId) {\n id\n oauth2Sessions(\n state: $state\n first: $first\n after: $after\n last: $last\n before: $before\n ) {\n edges {\n cursor\n node {\n id\n ...OAuth2Session_session\n }\n }\n\n totalCount\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n }\n }\n", ): (typeof documents)["\n query OAuth2SessionListQuery(\n $userId: ID!\n $state: Oauth2SessionState\n $first: Int\n $after: String\n $last: Int\n $before: String\n ) {\n user(id: $userId) {\n id\n oauth2Sessions(\n state: $state\n first: $first\n after: $after\n last: $last\n before: $before\n ) {\n edges {\n cursor\n node {\n id\n ...OAuth2Session_session\n }\n }\n\n totalCount\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n }\n }\n"]; +/** + * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. + */ +export function graphql( + source: "\n fragment UnverifiedEmailAlert on User {\n id\n unverifiedEmails: emails(first: 0, state: PENDING) {\n totalCount\n }\n }\n", +): (typeof documents)["\n fragment UnverifiedEmailAlert on User {\n id\n unverifiedEmails: emails(first: 0, state: PENDING) {\n totalCount\n }\n }\n"]; /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ @@ -179,14 +187,14 @@ export function graphql( * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ export function graphql( - source: "\n query UserGreeting($userId: ID!) {\n user(id: $userId) {\n id\n username\n matrix {\n mxid\n displayName\n }\n }\n }\n", -): (typeof documents)["\n query UserGreeting($userId: ID!) {\n user(id: $userId) {\n id\n username\n matrix {\n mxid\n displayName\n }\n }\n }\n"]; + source: "\n query UserGreeting($userId: ID!) {\n user(id: $userId) {\n id\n username\n matrix {\n mxid\n displayName\n }\n }\n viewer {\n __typename\n\n ... on User {\n id\n ...UnverifiedEmailAlert\n }\n }\n }\n", +): (typeof documents)["\n query UserGreeting($userId: ID!) {\n user(id: $userId) {\n id\n username\n matrix {\n mxid\n displayName\n }\n }\n viewer {\n __typename\n\n ... on User {\n id\n ...UnverifiedEmailAlert\n }\n }\n }\n"]; /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ export function graphql( - source: "\n fragment UserHome_user on User {\n id\n\n primaryEmail {\n id\n ...UserEmail_email\n }\n\n confirmedEmails: emails(first: 0, state: CONFIRMED) {\n totalCount\n }\n\n unverifiedEmails: emails(first: 0, state: PENDING) {\n totalCount\n }\n\n browserSessions(first: 0, state: ACTIVE) {\n totalCount\n }\n\n oauth2Sessions(first: 0, state: ACTIVE) {\n totalCount\n }\n\n compatSessions(first: 0, state: ACTIVE) {\n totalCount\n }\n }\n", -): (typeof documents)["\n fragment UserHome_user on User {\n id\n\n primaryEmail {\n id\n ...UserEmail_email\n }\n\n confirmedEmails: emails(first: 0, state: CONFIRMED) {\n totalCount\n }\n\n unverifiedEmails: emails(first: 0, state: PENDING) {\n totalCount\n }\n\n browserSessions(first: 0, state: ACTIVE) {\n totalCount\n }\n\n oauth2Sessions(first: 0, state: ACTIVE) {\n totalCount\n }\n\n compatSessions(first: 0, state: ACTIVE) {\n totalCount\n }\n }\n"]; + source: "\n fragment UserHome_user on User {\n id\n\n primaryEmail {\n id\n ...UserEmail_email\n }\n\n confirmedEmails: emails(first: 0, state: CONFIRMED) {\n totalCount\n }\n\n browserSessions(first: 0, state: ACTIVE) {\n totalCount\n }\n\n oauth2Sessions(first: 0, state: ACTIVE) {\n totalCount\n }\n\n compatSessions(first: 0, state: ACTIVE) {\n totalCount\n }\n }\n", +): (typeof documents)["\n fragment UserHome_user on User {\n id\n\n primaryEmail {\n id\n ...UserEmail_email\n }\n\n confirmedEmails: emails(first: 0, state: CONFIRMED) {\n totalCount\n }\n\n browserSessions(first: 0, state: ACTIVE) {\n totalCount\n }\n\n oauth2Sessions(first: 0, state: ACTIVE) {\n totalCount\n }\n\n compatSessions(first: 0, state: ACTIVE) {\n totalCount\n }\n }\n"]; /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ diff --git a/frontend/src/gql/graphql.ts b/frontend/src/gql/graphql.ts index 96748bedf..1fa926957 100644 --- a/frontend/src/gql/graphql.ts +++ b/frontend/src/gql/graphql.ts @@ -1146,6 +1146,12 @@ export type OAuth2SessionListQueryQuery = { } | null; }; +export type UnverifiedEmailAlertFragment = { + __typename?: "User"; + id: string; + unverifiedEmails: { __typename?: "UserEmailConnection"; totalCount: number }; +} & { " $fragmentName"?: "UnverifiedEmailAlertFragment" }; + export type UserEmail_EmailFragment = { __typename?: "UserEmail"; id: string; @@ -1199,6 +1205,13 @@ export type UserGreetingQuery = { displayName?: string | null; }; } | null; + viewer: + | { __typename: "Anonymous" } + | ({ __typename: "User"; id: string } & { + " $fragmentRefs"?: { + UnverifiedEmailAlertFragment: UnverifiedEmailAlertFragment; + }; + }); }; export type UserHome_UserFragment = { @@ -1210,7 +1223,6 @@ export type UserHome_UserFragment = { }) | null; confirmedEmails: { __typename?: "UserEmailConnection"; totalCount: number }; - unverifiedEmails: { __typename?: "UserEmailConnection"; totalCount: number }; browserSessions: { __typename?: "BrowserSessionConnection"; totalCount: number; @@ -1565,6 +1577,48 @@ export const OAuth2Session_SessionFragmentDoc = { }, ], } as unknown as DocumentNode; +export const UnverifiedEmailAlertFragmentDoc = { + kind: "Document", + definitions: [ + { + kind: "FragmentDefinition", + name: { kind: "Name", value: "UnverifiedEmailAlert" }, + typeCondition: { + kind: "NamedType", + name: { kind: "Name", value: "User" }, + }, + selectionSet: { + kind: "SelectionSet", + selections: [ + { kind: "Field", name: { kind: "Name", value: "id" } }, + { + kind: "Field", + alias: { kind: "Name", value: "unverifiedEmails" }, + name: { kind: "Name", value: "emails" }, + arguments: [ + { + kind: "Argument", + name: { kind: "Name", value: "first" }, + value: { kind: "IntValue", value: "0" }, + }, + { + kind: "Argument", + name: { kind: "Name", value: "state" }, + value: { kind: "EnumValue", value: "PENDING" }, + }, + ], + selectionSet: { + kind: "SelectionSet", + selections: [ + { kind: "Field", name: { kind: "Name", value: "totalCount" } }, + ], + }, + }, + ], + }, + }, + ], +} as unknown as DocumentNode; export const UserEmail_EmailFragmentDoc = { kind: "Document", definitions: [ @@ -1637,29 +1691,6 @@ export const UserHome_UserFragmentDoc = { ], }, }, - { - kind: "Field", - alias: { kind: "Name", value: "unverifiedEmails" }, - name: { kind: "Name", value: "emails" }, - arguments: [ - { - kind: "Argument", - name: { kind: "Name", value: "first" }, - value: { kind: "IntValue", value: "0" }, - }, - { - kind: "Argument", - name: { kind: "Name", value: "state" }, - value: { kind: "EnumValue", value: "PENDING" }, - }, - ], - selectionSet: { - kind: "SelectionSet", - selections: [ - { kind: "Field", name: { kind: "Name", value: "totalCount" } }, - ], - }, - }, { kind: "Field", name: { kind: "Name", value: "browserSessions" }, @@ -3058,6 +3089,70 @@ export const UserGreetingDocument = { ], }, }, + { + kind: "Field", + name: { kind: "Name", value: "viewer" }, + selectionSet: { + kind: "SelectionSet", + selections: [ + { kind: "Field", name: { kind: "Name", value: "__typename" } }, + { + kind: "InlineFragment", + typeCondition: { + kind: "NamedType", + name: { kind: "Name", value: "User" }, + }, + selectionSet: { + kind: "SelectionSet", + selections: [ + { kind: "Field", name: { kind: "Name", value: "id" } }, + { + kind: "FragmentSpread", + name: { kind: "Name", value: "UnverifiedEmailAlert" }, + }, + ], + }, + }, + ], + }, + }, + ], + }, + }, + { + kind: "FragmentDefinition", + name: { kind: "Name", value: "UnverifiedEmailAlert" }, + typeCondition: { + kind: "NamedType", + name: { kind: "Name", value: "User" }, + }, + selectionSet: { + kind: "SelectionSet", + selections: [ + { kind: "Field", name: { kind: "Name", value: "id" } }, + { + kind: "Field", + alias: { kind: "Name", value: "unverifiedEmails" }, + name: { kind: "Name", value: "emails" }, + arguments: [ + { + kind: "Argument", + name: { kind: "Name", value: "first" }, + value: { kind: "IntValue", value: "0" }, + }, + { + kind: "Argument", + name: { kind: "Name", value: "state" }, + value: { kind: "EnumValue", value: "PENDING" }, + }, + ], + selectionSet: { + kind: "SelectionSet", + selections: [ + { kind: "Field", name: { kind: "Name", value: "totalCount" } }, + ], + }, + }, ], }, }, @@ -3951,29 +4046,6 @@ export const HomeQueryDocument = { ], }, }, - { - kind: "Field", - alias: { kind: "Name", value: "unverifiedEmails" }, - name: { kind: "Name", value: "emails" }, - arguments: [ - { - kind: "Argument", - name: { kind: "Name", value: "first" }, - value: { kind: "IntValue", value: "0" }, - }, - { - kind: "Argument", - name: { kind: "Name", value: "state" }, - value: { kind: "EnumValue", value: "PENDING" }, - }, - ], - selectionSet: { - kind: "SelectionSet", - selections: [ - { kind: "Field", name: { kind: "Name", value: "totalCount" } }, - ], - }, - }, { kind: "Field", name: { kind: "Name", value: "browserSessions" }, diff --git a/frontend/src/test-utils/WithLocation.tsx b/frontend/src/test-utils/WithLocation.tsx new file mode 100644 index 000000000..5f9aa4cae --- /dev/null +++ b/frontend/src/test-utils/WithLocation.tsx @@ -0,0 +1,53 @@ +// 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. + +// @vitest-environment happy-dom + +import { Provider } from "jotai"; +import { useHydrateAtoms } from "jotai/utils"; + +import { appConfigAtom, locationAtom } from "../Router"; + +const HydrateLocation: React.FC> = ({ + children, + path, +}) => { + useHydrateAtoms([ + [appConfigAtom, { root: "/" }], + [locationAtom, { pathname: path }], + ]); + return <>{children}; +}; + +/** + * Utility for testing components that rely on routing or location + * For example any component that includes a + * Eg: + * ``` + * const component = create( + + Active + , + ); + * ``` + */ +export const WithLocation: React.FC< + React.PropsWithChildren<{ path?: string }> +> = ({ children, path }) => { + return ( + + {children} + + ); +};