From fcde275a7e162e32e6d2a21507bffae373d641ce Mon Sep 17 00:00:00 2001 From: kAy Date: Tue, 8 Aug 2023 10:16:06 +1000 Subject: [PATCH] ARC 2391 - Refactor UI to meet new design (#2318) ARC-2391 - UI changes and auto org install --- spa/src/components/CollapsibleStep/index.tsx | 99 ----- spa/src/components/CollapsibleStep/test.tsx | 71 --- spa/src/components/Step/index.tsx | 43 ++ spa/src/components/Step/test.tsx | 18 + spa/src/pages/ConfigSteps/index.tsx | 429 ++++++++----------- spa/src/pages/ConfigSteps/test.tsx | 92 +++- 6 files changed, 316 insertions(+), 436 deletions(-) delete mode 100644 spa/src/components/CollapsibleStep/index.tsx delete mode 100644 spa/src/components/CollapsibleStep/test.tsx create mode 100644 spa/src/components/Step/index.tsx create mode 100644 spa/src/components/Step/test.tsx diff --git a/spa/src/components/CollapsibleStep/index.tsx b/spa/src/components/CollapsibleStep/index.tsx deleted file mode 100644 index ae0d0d21fb..0000000000 --- a/spa/src/components/CollapsibleStep/index.tsx +++ /dev/null @@ -1,99 +0,0 @@ -import { useEffect, useState } from "react"; -import { token } from "@atlaskit/tokens"; -import styled from "@emotion/styled"; -import CheckCircleIcon from "@atlaskit/icon/glyph/check-circle"; - -type ContainerType = { - isExpanded: boolean -} - -const Container = styled.div` - width: 100%; - border: ${props => props.isExpanded ? `${token("space.025")} solid ${token("color.border")}` : "none"}; - border-radius: ${token("space.050")}; - padding: ${token("space.400")}; - margin: 0 0 ${token("space.100")}; - box-sizing: border-box; - background: ${props => props.isExpanded ? "transparent" : token("elevation.surface.sunken")}; -`; -const Header = styled.div` - display: flex; - line-height: ${token("space.400")}; - align-items: center; -`; -const StepNumber = styled.span` - background: ${token("color.background.accent.gray.subtlest")}; - height: ${token("space.400")}; - width: ${token("space.400")}; - font-weight: 600; - text-align: center; - border-radius: 50%; -`; -const CompletedIcon = styled.span` - height: ${token("space.400")}; - width: ${token("space.400")}; - display: flex; - justify-content: end; - align-items: center; -`; -const StepTitle = styled.span` - cursor: pointer; - font-weight: 600; - margin: 0 0 0 ${token("space.200")}; - color: ${props => props.isExpanded ? token("color.text") : token("color.text.subtlest") } -`; -const Content = styled.div` - margin: ${token("space.200")} 0 0 ${token("space.600")}; -`; - -const CollapsibleStep = ({ - title, - step, - expanded = false, - completed = false, - canViewContent, - children, -}: { - title: string, - step: string, - canViewContent: boolean, - expanded?: boolean, - completed?: boolean, - children: JSX.Element, -}) => { - const [ isExpanded, setIsExpanded ] = useState(expanded); - - useEffect(() => { - setIsExpanded(expanded); - }, [expanded]); - - const clickedTitle = () => { - if (canViewContent) { - setIsExpanded(!isExpanded); - } - }; - - return ( - -
- { - completed ? ( - - - - ) : {step} - } - {title} -
- { - isExpanded && ( - - {children} - - ) - } -
- ); -}; - -export default CollapsibleStep; diff --git a/spa/src/components/CollapsibleStep/test.tsx b/spa/src/components/CollapsibleStep/test.tsx deleted file mode 100644 index 1c0d53e1a5..0000000000 --- a/spa/src/components/CollapsibleStep/test.tsx +++ /dev/null @@ -1,71 +0,0 @@ -import "@testing-library/jest-dom"; -import { render, screen } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; -import CollapsibleStep from "./index"; - -const DUMMY_STEP = "1"; -const DUMMY_TITLE = "Collapsible Title"; -const DUMMY_CONTENT = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras eros velit, efficitur eget molestie sed, laoreet vitae lectus. Nullam varius, ipsum pharetra commodo viverra, ante leo finibus velit, nec pellentesque metus nulla sit amet nisl. Maecenas vel blandit lectus, ac venenatis tortor. Donec velit erat, hendrerit vestibulum auctor vitae, convallis vel neque. Cras malesuada enim imperdiet leo suscipit rhoncus. Vivamus commodo tincidunt leo, vel sodales nulla efficitur eget. Cras vulputate laoreet odio in consectetur. Proin consectetur fermentum magna, vehicula convallis lacus sollicitudin id. Nunc scelerisque risus eu volutpat mattis. In non tellus ac nibh semper fermentum. Praesent sed nisi tristique, iaculis enim a, iaculis urna. Aliquam ut felis sit amet sapien congue aliquet non a ipsum. Vivamus id turpis ornare, porttitor leo eu, tincidunt ligula. Curabitur nisl eros, congue in tellus ut, porta finibus turpis. Proin tempor diam eu nibh viverra, non tristique erat aliquet. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas."; -const CONTENT_TEST_ID = "collapsible-content"; - -test("When it can not be expanded", async () => { - render( - -
{DUMMY_CONTENT}
-
- ); - - expect(screen.queryByText(DUMMY_STEP)).toBeInTheDocument(); - expect(screen.queryByText(DUMMY_TITLE)).toBeInTheDocument(); - expect(screen.queryByTestId(CONTENT_TEST_ID)).not.toBeInTheDocument(); - - await userEvent.click(screen.getByText("Collapsible Title")); - expect(screen.queryByTestId(CONTENT_TEST_ID)).not.toBeInTheDocument(); -}); - -test("When it can be expanded, but is not expanded", async () => { - render( - -
{DUMMY_CONTENT}
-
- ); - - expect(screen.queryByText(DUMMY_STEP)).toBeInTheDocument(); - expect(screen.queryByText(DUMMY_TITLE)).toBeInTheDocument(); - expect(screen.queryByTestId(CONTENT_TEST_ID)).not.toBeInTheDocument(); - - await userEvent.click(screen.getByText(DUMMY_TITLE)); - expect(screen.queryByTestId(CONTENT_TEST_ID)).toBeInTheDocument(); - expect(screen.getByTestId(CONTENT_TEST_ID)).toHaveTextContent(DUMMY_CONTENT); -}); - -test("When it can be expanded and is expanded", async () => { - render( - -
{DUMMY_CONTENT}
-
- ); - - expect(screen.queryByText(DUMMY_STEP)).toBeInTheDocument(); - expect(screen.queryByText(DUMMY_TITLE)).toBeInTheDocument(); - expect(screen.queryByTestId(CONTENT_TEST_ID)).toBeInTheDocument(); - expect(screen.getByTestId(CONTENT_TEST_ID)).toHaveTextContent(DUMMY_CONTENT); - - await userEvent.click(screen.getByText(DUMMY_TITLE)); - expect(screen.queryByTestId(CONTENT_TEST_ID)).not.toBeInTheDocument(); -}); diff --git a/spa/src/components/Step/index.tsx b/spa/src/components/Step/index.tsx new file mode 100644 index 0000000000..554b132f3c --- /dev/null +++ b/spa/src/components/Step/index.tsx @@ -0,0 +1,43 @@ +import { token } from "@atlaskit/tokens"; +import styled from "@emotion/styled"; + +const Container = styled.div` + width: 100%; + border: ${token("space.025")} solid ${token("color.border")}; + border-radius: ${token("space.050")}; + padding: ${token("space.400")} 80px; + box-sizing: border-box; + background: transparent; + margin: 0 0 ${token("space.100")}; +`; +const Header = styled.div` + line-height: ${token("space.400")}; + align-items: center; +`; +const StepTitle = styled.div` + cursor: pointer; + font-weight: 600; + color: token("color.text"); + margin: 0 0 ${token("space.100")}; +`; + +const Step = ({ + title, + children, +}: { + title: string, + children: JSX.Element, +}) => { + return ( + +
+ {title} +
+
+ {children} +
+
+ ); +}; + +export default Step; diff --git a/spa/src/components/Step/test.tsx b/spa/src/components/Step/test.tsx new file mode 100644 index 0000000000..45b0c9fa8f --- /dev/null +++ b/spa/src/components/Step/test.tsx @@ -0,0 +1,18 @@ +import "@testing-library/jest-dom"; +import { render, screen } from "@testing-library/react"; +import Step from "./index"; + +const DUMMY_TITLE = "Collapsible Title"; +const DUMMY_CONTENT = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras eros velit, efficitur eget molestie sed, laoreet vitae lectus. Nullam varius, ipsum pharetra commodo viverra, ante leo finibus velit, nec pellentesque metus nulla sit amet nisl. Maecenas vel blandit lectus, ac venenatis tortor. Donec velit erat, hendrerit vestibulum auctor vitae, convallis vel neque. Cras malesuada enim imperdiet leo suscipit rhoncus. Vivamus commodo tincidunt leo, vel sodales nulla efficitur eget. Cras vulputate laoreet odio in consectetur. Proin consectetur fermentum magna, vehicula convallis lacus sollicitudin id. Nunc scelerisque risus eu volutpat mattis. In non tellus ac nibh semper fermentum. Praesent sed nisi tristique, iaculis enim a, iaculis urna. Aliquam ut felis sit amet sapien congue aliquet non a ipsum. Vivamus id turpis ornare, porttitor leo eu, tincidunt ligula. Curabitur nisl eros, congue in tellus ut, porta finibus turpis. Proin tempor diam eu nibh viverra, non tristique erat aliquet. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas."; +const CONTENT_TEST_ID = "content"; + +test("When it can not be expanded", async () => { + render( + +
{DUMMY_CONTENT}
+
+ ); + + expect(screen.queryByText(DUMMY_TITLE)).toBeInTheDocument(); + expect(screen.queryByTestId(CONTENT_TEST_ID)).toBeInTheDocument(); +}); diff --git a/spa/src/pages/ConfigSteps/index.tsx b/spa/src/pages/ConfigSteps/index.tsx index 86dad2a835..07cbdf8d20 100644 --- a/spa/src/pages/ConfigSteps/index.tsx +++ b/spa/src/pages/ConfigSteps/index.tsx @@ -1,15 +1,13 @@ -import { useCallback, useEffect, useState } from "react"; +import { useEffect, useState } from "react"; import Button, { LoadingButton } from "@atlaskit/button"; import styled from "@emotion/styled"; import SyncHeader from "../../components/SyncHeader"; import { Wrapper } from "../../common/Wrapper"; -import CollapsibleStep from "../../components/CollapsibleStep"; +import Step from "../../components/Step"; import Tooltip, { TooltipPrimitive } from "@atlaskit/tooltip"; import Skeleton from "@atlaskit/skeleton"; import { token } from "@atlaskit/tokens"; import OpenIcon from "@atlaskit/icon/glyph/open"; -import SelectDropdown, { LabelType, OrgOptionsType } from "../../components/SelectDropdown"; -import OfficeBuildingIcon from "@atlaskit/icon/glyph/office-building"; import { useNavigate } from "react-router-dom"; import Error from "../../components/Error"; import AppManager from "../../services/app-manager"; @@ -18,6 +16,7 @@ import analyticsClient from "../../analytics"; import { AxiosError } from "axios"; import { ErrorObjType, modifyError } from "../../utils/modifyError"; import { popup, reportError } from "../../utils"; +import { GitHubInstallationType } from "../../../../src/rest-interfaces"; type GitHubOptionType = { selectedOption: number; @@ -26,10 +25,6 @@ type GitHubOptionType = { type HostUrlType = { jiraHost: string; }; -type OrgDropdownType = { - label: string; - value: number; -}; const ConfigContainer = styled.div` margin: 0 auto; @@ -77,20 +72,30 @@ const InlineDialog = styled(TooltipPrimitive)` `; const LoggedInContent = styled.div` display: flex; - justify-content: start; align-items: center; + justify-content: center; + margin: 0 auto; `; -const ButtonContainer = styled.div` +const OrgsContainer = styled.div` display: flex; justify-content: space-between; align-items: center; + padding: ${token("space.150")} 0; + margin-bottom: ${token("space.100")}; +`; +const HorizontalDividerSkippingPaddings = styled.div` + border-top: ${token("space.025")} solid ${token("color.border")}; + height: 1px; + margin: 0 -80px; // Using negative margin here to avoid the paddings `; const Paragraph = styled.div` color: ${token("color.text.subtle")}; + margin-bottom: ${token("space.100")}; `; const NoOrgsParagraph = styled.div` color: ${token("color.text.subtle")}; - margin-bottom: ${token("space.400")}; + margin: ${token("space.200")} 0; + text-align: center; `; const ConfigSteps = () => { @@ -105,27 +110,17 @@ const ConfigSteps = () => { const originalUrl = window.location.origin; const [hostUrl, setHostUrl] = useState(undefined); - const [organizations, setOrganizations] = useState>([]); - const [noOrgsFound, setNoOrgsFound] = useState(false); - const [selectedOrg, setSelectedOrg] = useState(undefined); + const [organizations, setOrganizations] = useState>([]); const [loaderForOrgFetching, setLoaderForOrgFetching] = useState(true); - const [loaderForOrgConnection, setLoaderForOrgConnection] = useState(false); - const [orgConnectionDisabled, setOrgConnectionDisabled] = useState(true); const [selectedOption, setSelectedOption] = useState(1); - const [completedStep1, setCompletedStep1] = useState(isAuthenticated); - const [completedStep2] = useState(false); - - const [showStep2, setShowStep2] = useState(true); - const [canViewContentForStep2, setCanViewContentForStep2] = useState(isAuthenticated); - - const [expandStep1, setExpandStep1] = useState(!isAuthenticated); - const [expandStep2, setExpandStep2] = useState(isAuthenticated); - const [isLoggedIn, setIsLoggedIn] = useState(isAuthenticated); const [loggedInUser, setLoggedInUser] = useState(username); const [loaderForLogin, setLoaderForLogin] = useState(false); + const [clickedOrg, setClickedOrg] = useState(0); + const [loaderForOrgClicked, setLoaderForOrgClicked] = useState(false); + const [error, setError] = useState(undefined); const getJiraHostUrls = () => { @@ -137,76 +132,21 @@ const ConfigSteps = () => { }); }; - const getOrganizations = useCallback(async () => { + const getOrganizations = async (autoRedirectToInstall = true) => { setLoaderForOrgFetching(true); const response = await AppManager.fetchOrgs(); setLoaderForOrgFetching(false); if (response instanceof AxiosError) { setError(modifyError(response, {}, { onClearGitHubToken: clearGitHubToken })); } else { - setNoOrgsFound(response?.orgs.length === 0); - const totalOrgs = response?.orgs.map(org => ({ - label: org.account.login, - value: String(org.id), - requiresSsoLogin: org.requiresSsoLogin, - isIPBlocked: org.isIPBlocked, - isAdmin: org.isAdmin - })); - - const orgsWithSSOLogin = totalOrgs?.filter(org => org.requiresSsoLogin); - const orgsWithBlockedIp = totalOrgs?.filter(org => org.isIPBlocked); - const orgsLackAdmin = totalOrgs?.filter(org => !org.isAdmin); - const enabledOrgs = totalOrgs?.filter(org => !org.requiresSsoLogin && !org.isIPBlocked && org.isAdmin); - setOrganizations([ - { options: enabledOrgs }, - { label: "Lack Admin Permission", options: orgsLackAdmin }, - { label: "Requires SSO Login", options: orgsWithSSOLogin }, - { label: "GitHub IP Blocked", options: orgsWithBlockedIp }, - ]); + setOrganizations(response.orgs); + /** + * If there are no orgs and auto-redirect is not disabled, + * then redirect them to the Install New Org screen + */ + autoRedirectToInstall && response.orgs.length === 0 && installNewOrg(); } - }, []); - - useEffect(() => { - getJiraHostUrls(); - const handler = async (event: MessageEvent) => { - if (event.origin !== originalUrl) return; - if (event.data?.type === "oauth-callback" && event.data?.code) { - const response = await OAuthManager.finishOAuthFlow(event.data?.code, event.data?.state); - setLoaderForLogin(false); - if (response instanceof AxiosError) { - setError(modifyError(response, {}, { onClearGitHubToken: clearGitHubToken })); - analyticsClient.sendTrackEvent({ actionSubject: "finishOAuthFlow", action: "fail" }); - return; - } else { - analyticsClient.sendTrackEvent({ actionSubject: "finishOAuthFlow", action: "success" }); - } - setIsLoggedIn(true); - setCompletedStep1(true); - setExpandStep1(false); - setExpandStep2(true); - setCanViewContentForStep2(true); - await getOrganizations(); - } - }; - window.addEventListener("message", handler); - return () => { - window.removeEventListener("message", handler); - }; - }, [ originalUrl, getOrganizations ]); - - useEffect(() => { - const recheckValidity = async () => { - const status: boolean | AxiosError = await OAuthManager.checkValidity(); - if (status instanceof AxiosError) { - setError(modifyError(status, {}, { onClearGitHubToken: clearGitHubToken })); - return; - } - setLoggedInUser(OAuthManager.getUserDetails().username); - setLoaderForLogin(false); - await getOrganizations(); - }; - recheckValidity(); - }, [ isLoggedIn, getOrganizations ]); + }; const authorize = async () => { switch (selectedOption) { @@ -233,41 +173,15 @@ const ConfigSteps = () => { } }; - const onChangingOrg = (value: LabelType | null) => { - if(value) { - if (value?.isIPBlocked) { - setError(modifyError({ errorCode: "IP_BLOCKED" }, { orgLogin: value.label }, { onClearGitHubToken: clearGitHubToken })); - setOrgConnectionDisabled(true); - } else if(value?.requiresSsoLogin) { - setError(modifyError({ errorCode: "SSO_LOGIN" }, { orgLogin: value.label}, { onClearGitHubToken: clearGitHubToken })); - setOrgConnectionDisabled(true); - } else if (!value?.isAdmin) { - setOrgConnectionDisabled(true); - }else { - setSelectedOrg({ - label: value.label, - value: parseInt(value.value) - }); - setOrgConnectionDisabled(false); - setError(undefined); - } - } - }; - const clearGitHubToken = () => { OAuthManager.clear(); setIsLoggedIn(false); - setCompletedStep1(false); setLoaderForLogin(false); - setCanViewContentForStep2(false); - setExpandStep1(true); - setExpandStep2(false); setLoggedInUser(""); setError(undefined); }; const logout = () => { - popup("https://github.com/logout", { width: 400, height: 600 }); clearGitHubToken(); analyticsClient.sendUIEvent({ actionSubject: "switchGitHubAccount", action: "clicked" }); @@ -275,7 +189,6 @@ const ConfigSteps = () => { const doCreateConnection = async (gitHubInstallationId: number, mode: "auto" | "manual") => { try { - setLoaderForOrgConnection(true); analyticsClient.sendUIEvent({ actionSubject: "connectOrganisation", action: "clicked" }); const connected = await AppManager.connectOrg(gitHubInstallationId); analyticsClient.sendTrackEvent({ actionSubject: "organisationConnectResponse", action: connected ? "success" : "fail", attributes: { mode } }); @@ -287,14 +200,6 @@ const ConfigSteps = () => { } catch (e) { analyticsClient.sendTrackEvent({ actionSubject: "organisationConnectResponse", action: "fail", attributes: { mode } }); reportError(e); - } finally { - setLoaderForOrgConnection(false); - } - }; - - const connectGitHubOrg = async () => { - if (selectedOrg?.value) { - await doCreateConnection(selectedOrg.value, "manual"); } }; @@ -304,7 +209,7 @@ const ConfigSteps = () => { await AppManager.installNewApp({ onFinish: async (gitHubInstallationId: number | undefined) => { analyticsClient.sendTrackEvent({ actionSubject: "installNewOrgInGithubResponse", action: "success" }); - getOrganizations(); + getOrganizations(false); if(gitHubInstallationId) { await doCreateConnection(gitHubInstallationId, "auto"); } @@ -320,6 +225,46 @@ const ConfigSteps = () => { } }; + useEffect(() => { + getJiraHostUrls(); + const handler = async (event: MessageEvent) => { + if (event.origin !== originalUrl) return; + if (event.data?.type === "oauth-callback" && event.data?.code) { + const response = await OAuthManager.finishOAuthFlow(event.data?.code, event.data?.state); + setLoaderForLogin(false); + if (response instanceof AxiosError) { + setError(modifyError(response, {}, { onClearGitHubToken: clearGitHubToken })); + analyticsClient.sendTrackEvent({ actionSubject: "finishOAuthFlow", action: "fail" }); + return; + } else { + analyticsClient.sendTrackEvent({ actionSubject: "finishOAuthFlow", action: "success" }); + } + setIsLoggedIn(true); + await getOrganizations(); + } + }; + window.addEventListener("message", handler); + return () => { + window.removeEventListener("message", handler); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [ originalUrl ]); + + useEffect(() => { + const recheckValidity = async () => { + const status: boolean | AxiosError = await OAuthManager.checkValidity(); + if (status instanceof AxiosError) { + setError(modifyError(status, {}, { onClearGitHubToken: clearGitHubToken })); + return; + } + setLoggedInUser(OAuthManager.getUserDetails().username); + setLoaderForLogin(false); + await getOrganizations(); + }; + isLoggedIn && recheckValidity(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [ isLoggedIn ]); + return ( @@ -327,134 +272,120 @@ const ConfigSteps = () => { error && } - - { - isLoggedIn ? <> - { - loaderForLogin ? <> - - : -
Logged in as {loggedInUser}
- -
- } - : <> - - { - setShowStep2(true); - setSelectedOption(1); - analyticsClient.sendUIEvent({ actionSubject: "authorizeTypeGitHubCloud", action: "clicked" }); - }} - > - - GitHub Cloud - - { - setShowStep2(false); - setSelectedOption(2); - analyticsClient.sendUIEvent({ actionSubject: "authorizeTypeGitHubEnt", action: "clicked" }); - }} - > - - GitHub Enterprise Server - - - - - {(props) => How do I check my GitHub product?} - - - { - loaderForLogin ? Loading : - - } - - } -
- { - showStep2 && - { - loaderForOrgFetching ? <> - - : ( - noOrgsFound ? - <> - We couldn’t find any GitHub organizations that you’re an owner of. - - : - <> - - Repositories from this organization will be available to all
- projects in {hostUrl?.jiraHost}. -
+ isLoggedIn ? + <> + + <> + { + loaderForOrgFetching ? + : + <> + + Repositories from this organization will be available to all
+ projects in {hostUrl?.jiraHost}. +
+ { + organizations.length === 0 && + No organizations found! + } - } - /> - - + + {org.account.login} + { + loaderForOrgClicked && clickedOrg === org.id ? + Loading button : + + } + + ) + } + + Can't find an organization you're looking for? + + + + + } + +
+ +
Logged in as {loggedInUser}
+ +
+ + : + + <> + + { + setSelectedOption(1); + analyticsClient.sendUIEvent({ actionSubject: "authorizeTypeGitHubCloud", action: "clicked" }); + }} + > + + GitHub Cloud + + { + setSelectedOption(2); + analyticsClient.sendUIEvent({ actionSubject: "authorizeTypeGitHubEnt", action: "clicked" }); + }} + > + + GitHub Enterprise Server + + + + + {(props) => How do I check my GitHub product?} + + + { + loaderForLogin ? Loading : + - - - } - - ) - } -
+ Get started + + } + + }
diff --git a/spa/src/pages/ConfigSteps/test.tsx b/spa/src/pages/ConfigSteps/test.tsx index db062cec01..dd26159931 100644 --- a/spa/src/pages/ConfigSteps/test.tsx +++ b/spa/src/pages/ConfigSteps/test.tsx @@ -8,12 +8,27 @@ import AppManager from "../../services/app-manager"; jest.mock("../../services/oauth-manager"); jest.mock("../../services/app-manager"); +jest.mock("react-router-dom", () => ({ + ...jest.requireActual("react-router-dom"), + useNavigate: () => jest.fn(), +})); /* eslint-disable react-refresh/only-export-components */ const Authenticated = { checkValidity: jest.fn().mockReturnValue(Promise.resolve(true)), authenticateInGitHub: jest.fn().mockReturnValue(Promise), - fetchOrgs: jest.fn().mockReturnValue({ orgs: []}), + fetchOrgs: jest.fn().mockReturnValue({ orgs: [ { account: { login: "org-1" }, id: 1 }, { account: { login: "org-2" }, id: 2 }, { account: { login: "org-3" }, id: 3 } ]}), + installNewApp: jest.fn(), + connectOrg: jest.fn(), + setTokens: jest.fn(), + getUserDetails: jest.fn().mockReturnValue({ username: "kay", email: "kay"}), + clear: jest.fn(), +}; +const AuthenticatedWithNoOrgs = { + checkValidity: jest.fn().mockReturnValue(Promise.resolve(true)), + authenticateInGitHub: jest.fn().mockReturnValue(Promise), + fetchOrgs: jest.fn().mockReturnValue({ orgs: [] }), + installNewApp: jest.fn(), setTokens: jest.fn(), getUserDetails: jest.fn().mockReturnValue({ username: "kay", email: "kay"}), clear: jest.fn(), @@ -45,7 +60,6 @@ const UnAuthenticated = { window.open = jest.fn(); test("Connect GitHub Screen - Initial Loading of the page when not authenticated", async () => { - jest.mocked(OAuthManager).getUserDetails = UnAuthenticated.getUserDetails; jest.mocked(OAuthManager).checkValidity = UnAuthenticated.checkValidity; @@ -60,12 +74,11 @@ test("Connect GitHub Screen - Initial Loading of the page when not authenticated expect(screen.queryByText("Connect Github to Jira")).toBeInTheDocument(); expect(screen.queryByText("GitHub Cloud")).toBeInTheDocument(); expect(screen.queryByText("GitHub Enterprise Server")).toBeInTheDocument(); - expect(screen.queryByText("Connect your GitHub organization to Jira")).toBeInTheDocument(); - expect(screen.queryByRole("button", { name: "Authorize in GitHub" })).toHaveTextContent("Authorize in GitHub"); + expect(screen.queryByText("Select your GitHub product")).toBeInTheDocument(); + expect(screen.queryByRole("button", { name: "Get started" })).toHaveTextContent("Get started"); }); test("Connect GitHub Screen - Checking the GitHub Enterprise flow when not authenticated", async () => { - jest.mocked(OAuthManager).getUserDetails = UnAuthenticated.getUserDetails; jest.mocked(OAuthManager).checkValidity = UnAuthenticated.checkValidity; @@ -78,13 +91,12 @@ test("Connect GitHub Screen - Checking the GitHub Enterprise flow when not authe }); await act(() => userEvent.click(screen.getByText("GitHub Enterprise Server"))); - await act(() => userEvent.click(screen.getByText("Authorize in GitHub"))); + await act(() => userEvent.click(screen.getByText("Get started"))); expect(AP.getLocation).toHaveBeenCalled(); }); test("Connect GitHub Screen - Checking the GitHub Cloud flow when not authenticated", async () => { - jest.mocked(OAuthManager).getUserDetails = UnAuthenticated.getUserDetails; jest.mocked(OAuthManager).checkValidity = UnAuthenticated.checkValidity; jest.mocked(OAuthManager).authenticateInGitHub = UnAuthenticated.authenticateInGitHub; @@ -98,16 +110,17 @@ test("Connect GitHub Screen - Checking the GitHub Cloud flow when not authentica }); await act(() => userEvent.click(screen.getByText("GitHub Cloud"))); - await act(() => userEvent.click(screen.getByText("Authorize in GitHub"))); + await act(() => userEvent.click(screen.getByText("Get started"))); expect(OAuthManager.authenticateInGitHub).toHaveBeenCalled(); }); -test("Connect GitHub Screen - Checking the GitHub Cloud flow when authenticated", async () => { - +test("Connect GitHub Screen - Checking the GitHub Cloud flow when authenticated with orgs", async () => { jest.mocked(OAuthManager).getUserDetails = Authenticated.getUserDetails; jest.mocked(OAuthManager).checkValidity = Authenticated.checkValidity; jest.mocked(AppManager).fetchOrgs = Authenticated.fetchOrgs; + jest.mocked(AppManager).installNewApp = Authenticated.installNewApp; + jest.mocked(AppManager).connectOrg = Authenticated.connectOrg; await act(async () => { render( @@ -119,12 +132,59 @@ test("Connect GitHub Screen - Checking the GitHub Cloud flow when authenticated" expect(screen.queryByText("GitHub Cloud")).not.toBeInTheDocument(); expect(screen.queryByText("GitHub Enterprise Server")).not.toBeInTheDocument(); - expect(screen.queryByText("Authorize in GitHub")).not.toBeInTheDocument(); - expect(screen.queryByText("Log in and authorize")).toBeInTheDocument(); + expect(screen.queryByText("Get started")).not.toBeInTheDocument(); + expect(screen.queryByText("Select your GitHub product")).not.toBeInTheDocument(); + + // Checking if all the orgs are being displayed + expect(screen.queryByText("Connect your GitHub organization to Jira")).toBeInTheDocument(); + expect(screen.queryByText("org-1")).toBeInTheDocument(); + expect(screen.queryByText("org-2")).toBeInTheDocument(); + expect(screen.queryByText("org-3")).toBeInTheDocument(); + + // Checking the 3 connect buttons + expect(await screen.findAllByRole("button", { name: "Connect" })).toHaveLength(3); + + // Clicking first connect button + await act(async () => { await userEvent.click(screen.getAllByRole("button", { name: "Connect" })[0]); }); + expect(await screen.findAllByRole("button", { name: "Loading button" })).toHaveLength(1); + expect(await screen.findAllByRole("button", { name: "Connect" })).toHaveLength(2); + expect(screen.getAllByRole("button", { name: "Connect" })[0]).toBeDisabled(); + expect(screen.getAllByRole("button", { name: "Connect" })[1]).toBeDisabled(); + expect(screen.getByLabelText("Install new Org")).toBeDisabled(); + expect(AppManager.connectOrg).toBeCalled(); }); -test("Connect GitHub Screen - Changing GitHub login when authenticated", async () => { +test("Connect GitHub Screen - Checking the GitHub Cloud flow when authenticated with no orgs", async () => { + jest.mocked(OAuthManager).getUserDetails = AuthenticatedWithNoOrgs.getUserDetails; + jest.mocked(OAuthManager).checkValidity = AuthenticatedWithNoOrgs.checkValidity; + jest.mocked(AppManager).fetchOrgs = AuthenticatedWithNoOrgs.fetchOrgs; + + await act(async () => { + render( + + + + ); + }); + + expect(screen.queryByText("GitHub Cloud")).not.toBeInTheDocument(); + expect(screen.queryByText("GitHub Enterprise Server")).not.toBeInTheDocument(); + expect(screen.queryByText("Get started")).not.toBeInTheDocument(); + expect(screen.queryByText("Select your GitHub product")).not.toBeInTheDocument(); + + // Checking to see no orgs are being displayed + expect(screen.queryByText("Connect your GitHub organization to Jira")).toBeInTheDocument(); + expect(screen.queryByText("org-1")).not.toBeInTheDocument(); + expect(screen.queryByText("org-2")).not.toBeInTheDocument(); + expect(screen.queryByText("org-3")).not.toBeInTheDocument(); + + // Testing the click on the Install button + expect(screen.queryByText("Install Jira in a new organization")).toBeInTheDocument(); + await act(async() => { await userEvent.click(screen.getByText("Install Jira in a new organization")); }); + expect(AppManager.installNewApp).toBeCalled(); +}); +test("Connect GitHub Screen - Changing GitHub login when authenticated", async () => { jest.mocked(OAuthManager).getUserDetails = Authenticated.getUserDetails; jest.mocked(OAuthManager).checkValidity = Authenticated.checkValidity; jest.mocked(AppManager).fetchOrgs = Authenticated.fetchOrgs; @@ -137,16 +197,14 @@ test("Connect GitHub Screen - Changing GitHub login when authenticated", async ( ); }); - await act(() => userEvent.click(screen.getByText("Log in and authorize"))); - + expect(screen.getByTestId("logged-in-as")).toHaveTextContent(`Logged in as ${OAuthManager.getUserDetails().username}.`); expect(screen.queryByText("Change GitHub login")).toBeInTheDocument(); await act(() => userEvent.click(screen.getByText("Change GitHub login"))); - expect(window.open).toHaveBeenCalled(); expect(screen.queryByText("GitHub Cloud")).toBeInTheDocument(); expect(screen.queryByText("GitHub Enterprise Server")).toBeInTheDocument(); - expect(screen.queryByText("Authorize in GitHub")).toBeInTheDocument(); + expect(screen.queryByText("Get started")).toBeInTheDocument(); });