Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
165 changes: 165 additions & 0 deletions app/client/src/git/components/DisconnectModal/index.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
import React from "react";
import { render, screen, fireEvent } from "@testing-library/react";
import "@testing-library/jest-dom";
import AnalyticsUtil from "ee/utils/AnalyticsUtil";
import DisconnectModal from ".";

jest.mock("ee/utils/AnalyticsUtil", () => ({
logEvent: jest.fn(),
}));

describe("DisconnectModal", () => {
const defaultProps = {
isModalOpen: true,
disconnectingApp: {
id: "app123",
name: "TestApp",
},
closeModal: jest.fn(),
onBackClick: jest.fn(),
onDisconnect: jest.fn(),
};

afterEach(() => {
jest.clearAllMocks();
});

it("should render the modal when isModalOpen is true", () => {
render(<DisconnectModal {...defaultProps} />);
expect(screen.getByTestId("t--disconnect-git-modal")).toBeInTheDocument();
});

it("should not render the modal when isModalOpen is false", () => {
render(<DisconnectModal {...defaultProps} isModalOpen={false} />);
expect(
screen.queryByTestId("t--disconnect-git-modal"),
).not.toBeInTheDocument();
});

it("should display the correct modal header", () => {
render(<DisconnectModal {...defaultProps} />);
expect(screen.getByText("Revoke access to TestApp")).toBeInTheDocument();
});

it("should display the correct instruction text", () => {
render(<DisconnectModal {...defaultProps} />);
expect(
screen.getByText("Type “TestApp” in the input box to revoke access."),
).toBeInTheDocument();
});

it("should update appName state when input changes", () => {
render(<DisconnectModal {...defaultProps} />);
const input = screen.getByLabelText("Application name");

fireEvent.change(input, { target: { value: "TestApp" } });
expect(input).toHaveValue("TestApp");
});

it("should enable Revoke button when appName matches disconnectingApp.name", () => {
render(<DisconnectModal {...defaultProps} />);
const input = screen.getByLabelText("Application name");
const revokeButton = document.getElementsByClassName(
"t--git-revoke-button",
)[0];

expect(revokeButton).toBeDisabled();

fireEvent.change(input, { target: { value: "TestApp" } });
expect(revokeButton).toBeEnabled();
});

it("should disable Revoke button when appName does not match disconnectingApp.name", () => {
render(<DisconnectModal {...defaultProps} />);
const input = screen.getByLabelText("Application name");
const revokeButton = document.getElementsByClassName(
"t--git-revoke-button",
)[0];

fireEvent.change(input, { target: { value: "WrongAppName" } });
expect(revokeButton).toBeDisabled();
});

it("should call onBackClick when Go Back button is clicked", () => {
render(<DisconnectModal {...defaultProps} />);
const goBackButton = document.getElementsByClassName(
"t--git-revoke-back-button",
)[0];

fireEvent.click(goBackButton);
expect(defaultProps.onBackClick).toHaveBeenCalledTimes(1);
});

it("should call onDisconnect when Revoke button is clicked", () => {
render(<DisconnectModal {...defaultProps} />);
const input = screen.getByLabelText("Application name");
const revokeButton = document.getElementsByClassName(
"t--git-revoke-button",
)[0];

fireEvent.change(input, { target: { value: "TestApp" } });
fireEvent.click(revokeButton);

expect(defaultProps.onDisconnect).toHaveBeenCalledTimes(1);
});

it("should disable Revoke button when isRevoking is true", () => {
const { rerender } = render(<DisconnectModal {...defaultProps} />);
const input = screen.getByLabelText("Application name");
const revokeButton = document.getElementsByClassName(
"t--git-revoke-button",
)[0];

fireEvent.change(input, { target: { value: "TestApp" } });
expect(revokeButton).toBeEnabled();

fireEvent.click(revokeButton);
// Rerender to reflect state change
rerender(<DisconnectModal {...defaultProps} />);

expect(defaultProps.onDisconnect).toHaveBeenCalledTimes(1);
expect(revokeButton).toBeDisabled();
});

it("should log analytics event on input blur", () => {
render(<DisconnectModal {...defaultProps} />);
const input = screen.getByLabelText("Application name");

fireEvent.change(input, { target: { value: "SomeValue" } });
fireEvent.blur(input);

expect(AnalyticsUtil.logEvent).toHaveBeenCalledWith(
"GS_MATCHING_REPO_NAME_ON_GIT_DISCONNECT_MODAL",
{
value: "SomeValue",
expecting: "TestApp",
},
);
});

it("should display callout with non-reversible message and learn more link", () => {
render(<DisconnectModal {...defaultProps} />);
expect(
screen.getByText(
"This action is non-reversible. Please proceed with caution.",
),
).toBeInTheDocument();
const learnMoreLink = screen.getByText("Learn more").parentElement;

expect(learnMoreLink).toBeInTheDocument();
expect(learnMoreLink).toHaveAttribute(
"href",
"https://docs.appsmith.com/advanced-concepts/version-control-with-git/disconnect-the-git-repository",
);
});

it("should not call onDisconnect when Revoke button is clicked and appName does not match", () => {
render(<DisconnectModal {...defaultProps} />);
const revokeButton = document.getElementsByClassName(
"t--git-revoke-button",
)[0];

fireEvent.click(revokeButton);
expect(defaultProps.onDisconnect).not.toHaveBeenCalled();
});
});
146 changes: 146 additions & 0 deletions app/client/src/git/components/DisconnectModal/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
import React, { useCallback, useState } from "react";
import {
Button,
Callout,
Flex,
Input,
Modal,
ModalBody,
ModalContent,
ModalFooter,
ModalHeader,
Text,
} from "@appsmith/ads";
import {
APPLICATION_NAME,
createMessage,
GIT_REVOKE_ACCESS,
GIT_TYPE_REPO_NAME_FOR_REVOKING_ACCESS,
GO_BACK,
NONE_REVERSIBLE_MESSAGE,
REVOKE,
} from "ee/constants/messages";
import AnalyticsUtil from "ee/utils/AnalyticsUtil";
import styled from "styled-components";

const DOCS_URL =
"https://docs.appsmith.com/advanced-concepts/version-control-with-git/disconnect-the-git-repository";
const DOCS_LINK_PROPS = [
{
children: "Learn more",
to: DOCS_URL,
className: "t--disconnect-learn-more",
},
];
const MODAL_WIDTH = 640;

interface DisconnectModalProps {
isModalOpen: boolean;
disconnectingApp: {
id: string;
name: string;
};
closeModal: () => void;
onBackClick: () => void;
onDisconnect: () => void;
}

const StyledModalContent = styled(ModalContent)`
width: ${MODAL_WIDTH}px;
`;

function DisconnectModal({
closeModal,
disconnectingApp,
isModalOpen,
onBackClick,
onDisconnect,
}: DisconnectModalProps) {
const [appName, setAppName] = useState("");
const [isRevoking, setIsRevoking] = useState(false);

const onDisconnectGit = useCallback(() => {
setIsRevoking(true);
onDisconnect();
}, [onDisconnect]);

const shouldDisableRevokeButton =
disconnectingApp.id === "" ||
appName !== disconnectingApp.name ||
isRevoking;

const onModalOpenValueChange = useCallback(
(open: boolean) => {
if (!open) {
closeModal();
}
},
[closeModal],
);

const inputOnBlur = useCallback(
(event: React.FocusEvent<Element, Element>) => {
AnalyticsUtil.logEvent("GS_MATCHING_REPO_NAME_ON_GIT_DISCONNECT_MODAL", {
value: "value" in event.target ? event.target.value : "",
expecting: disconnectingApp.name,
});
},
[disconnectingApp.name],
);

const inputOnChange = useCallback((value: string) => {
setAppName(value);
}, []);

return (
<Modal onOpenChange={onModalOpenValueChange} open={isModalOpen}>
<StyledModalContent data-testid="t--disconnect-git-modal">
<ModalHeader>
{createMessage(GIT_REVOKE_ACCESS, disconnectingApp.name)}
</ModalHeader>
<ModalBody>
<Flex flexDirection="column" gap="spaces-1">
<Text color={"var(--ads-v2-color-fg-emphasis)"} kind="heading-s">
{createMessage(
GIT_TYPE_REPO_NAME_FOR_REVOKING_ACCESS,
disconnectingApp.name,
)}
</Text>
<Input
className="t--git-app-name-input"
label={createMessage(APPLICATION_NAME)}
onBlur={inputOnBlur}
onChange={inputOnChange}
size="md"
value={appName}
/>
<Callout kind="error" links={DOCS_LINK_PROPS}>
{createMessage(NONE_REVERSIBLE_MESSAGE)}
</Callout>
</Flex>
</ModalBody>
<ModalFooter>
<Button
className="t--git-revoke-back-button"
kind="secondary"
onClick={onBackClick}
size="md"
>
{createMessage(GO_BACK)}
</Button>
<Button
className="t--git-revoke-button"
isDisabled={shouldDisableRevokeButton}
kind="primary"
onClick={onDisconnectGit}
size="md"
>
{createMessage(REVOKE)}
</Button>
</ModalFooter>
</StyledModalContent>
</Modal>
);
}

export default DisconnectModal;