forked from appsmithorg/appsmith
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
chore: Git mod - Connect/Import modal (appsmithorg#38098)
## Description Git mod components, add connect/import from git modal components Fixes appsmithorg#37812 Fixes appsmithorg#37802 ## Automation /ok-to-test tags="@tag.Git" ### 🔍 Cypress test results <!-- This is an auto-generated comment: Cypress test results --> > [!TIP] > 🟢 🟢 🟢 All cypress tests have passed! 🎉 🎉 🎉 > Workflow run: <https://github.com/appsmithorg/appsmith/actions/runs/12291098002> > Commit: e94ebe0 > <a href="https://internal.appsmith.com/app/cypress-dashboard/rundetails-65890b3c81d7400d08fa9ee5?branch=master&workflowId=12291098002&attempt=2" target="_blank">Cypress dashboard</a>. > Tags: `@tag.Git` > Spec: > <hr>Thu, 12 Dec 2024 07:43:05 UTC <!-- end of auto-generated comment: Cypress test results --> ## Communication Should the DevRel and Marketing teams inform users about this change? - [ ] Yes - [ ] No <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit ## Release Notes - **New Features** - Introduced a multi-step `ConnectModal` for Git provider connections. - Added components for generating SSH keys and managing Git remote URLs. - New constants for Git integration steps and demo GIFs for user guidance. - Added optional `errorType` property to enhance error handling in API responses. - New `Steps` component for step navigation in the modal. - New `CopyButton` component for clipboard functionality with visual feedback. - **Improvements** - Enhanced error handling and user prompts related to Git operations. - Improved user interface with styled components for better layout and presentation. - **Bug Fixes** - Improved validation and error messaging for SSH URL inputs. - **Refactor** - Renamed `AutocommitStatusbar` to `Statusbar` for consistency across components and tests. - **Tests** - Comprehensive test coverage for new components and functionalities related to Git integration. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
- Loading branch information
1 parent
a040815
commit ebb341a
Showing
19 changed files
with
2,288 additions
and
31 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,270 @@ | ||
import React from "react"; | ||
import { render, screen, fireEvent, waitFor } from "@testing-library/react"; | ||
import type { AddDeployKeyProps } from "./AddDeployKey"; | ||
import AddDeployKey from "./AddDeployKey"; | ||
import AnalyticsUtil from "ee/utils/AnalyticsUtil"; | ||
import "@testing-library/jest-dom"; | ||
|
||
jest.mock("ee/utils/AnalyticsUtil", () => ({ | ||
logEvent: jest.fn(), | ||
})); | ||
|
||
jest.mock("copy-to-clipboard", () => ({ | ||
__esModule: true, | ||
default: () => true, | ||
})); | ||
|
||
const DEFAULT_DOCS_URL = | ||
"https://docs.appsmith.com/advanced-concepts/version-control-with-git/connecting-to-git-repository"; | ||
|
||
const defaultProps: AddDeployKeyProps = { | ||
isModalOpen: true, | ||
onChange: jest.fn(), | ||
value: { | ||
gitProvider: "github", | ||
isAddedDeployKey: false, | ||
remoteUrl: "[email protected]:owner/repo.git", | ||
}, | ||
fetchSSHKeyPair: jest.fn(), | ||
generateSSHKey: jest.fn(), | ||
isFetchingSSHKeyPair: false, | ||
isGeneratingSSHKey: false, | ||
sshKeyPair: "ecdsa-sha2-nistp256 AAAAE2VjZHNhAAAIBaj...", | ||
}; | ||
|
||
describe("AddDeployKey Component", () => { | ||
beforeEach(() => { | ||
jest.clearAllMocks(); | ||
}); | ||
|
||
it("renders without crashing and shows default UI", () => { | ||
render(<AddDeployKey {...defaultProps} />); | ||
expect( | ||
screen.getByText("Add deploy key & give write access"), | ||
).toBeInTheDocument(); | ||
expect(screen.getByRole("combobox")).toBeInTheDocument(); | ||
// Should show ECDSA by default since sshKeyPair includes "ecdsa" | ||
expect(screen.getByText(defaultProps.sshKeyPair)).toBeInTheDocument(); | ||
expect( | ||
screen.getByText("I've added the deploy key and gave it write access"), | ||
).toBeInTheDocument(); | ||
}); | ||
|
||
it("calls fetchSSHKeyPair if modal is open and not importing", () => { | ||
render(<AddDeployKey {...defaultProps} isImport={false} />); | ||
expect(defaultProps.fetchSSHKeyPair).toHaveBeenCalledTimes(1); | ||
}); | ||
|
||
it("does not call fetchSSHKeyPair if importing", () => { | ||
render(<AddDeployKey {...defaultProps} isImport />); | ||
expect(defaultProps.fetchSSHKeyPair).not.toHaveBeenCalled(); | ||
}); | ||
|
||
it("shows dummy key loader if loading keys", () => { | ||
render( | ||
<AddDeployKey {...defaultProps} isFetchingSSHKeyPair sshKeyPair="" />, | ||
); | ||
// The actual key text should not be displayed | ||
expect(screen.queryByText("ecdsa-sha2-nistp256")).not.toBeInTheDocument(); | ||
}); | ||
|
||
it("changes SSH key type when user selects a different type and triggers generateSSHKey if needed", async () => { | ||
const generateSSHKey = jest.fn(); | ||
|
||
render( | ||
<AddDeployKey | ||
{...defaultProps} | ||
generateSSHKey={generateSSHKey} | ||
sshKeyPair="" // No key to force generation | ||
/>, | ||
); | ||
|
||
fireEvent.mouseDown(screen.getByRole("combobox")); | ||
const rsaOption = screen.getByText("RSA 4096"); | ||
|
||
fireEvent.click(rsaOption); | ||
|
||
await waitFor(() => { | ||
expect(generateSSHKey).toHaveBeenCalledWith("RSA", expect.any(Object)); | ||
}); | ||
}); | ||
|
||
it("displays a generic error when errorData is provided and error code is not AE-GIT-4032 or AE-GIT-4033", () => { | ||
// eslint-disable-next-line react-perf/jsx-no-new-object-as-prop | ||
const errorData = { | ||
data: {}, | ||
responseMeta: { | ||
success: false, | ||
status: 503, | ||
error: { | ||
code: "GENERIC-ERROR", | ||
errorType: "Some Error", | ||
message: "Something went wrong", | ||
}, | ||
}, | ||
}; | ||
|
||
render(<AddDeployKey {...defaultProps} errorData={errorData} />); | ||
expect(screen.getByText("Some Error")).toBeInTheDocument(); | ||
expect(screen.getByText("Something went wrong")).toBeInTheDocument(); | ||
}); | ||
|
||
it("displays a misconfiguration error if error code is AE-GIT-4032", () => { | ||
// eslint-disable-next-line react-perf/jsx-no-new-object-as-prop | ||
const errorData = { | ||
data: {}, | ||
responseMeta: { | ||
success: false, | ||
status: 503, | ||
error: { | ||
code: "AE-GIT-4032", | ||
errorType: "SSH Key Error", | ||
message: "SSH Key misconfiguration", | ||
}, | ||
}, | ||
}; | ||
|
||
render(<AddDeployKey {...defaultProps} errorData={errorData} />); | ||
expect(screen.getByText("SSH key misconfiguration")).toBeInTheDocument(); | ||
expect( | ||
screen.getByText( | ||
"It seems that your SSH key hasn't been added to your repository. To proceed, please revisit the steps below and configure your SSH key correctly.", | ||
), | ||
).toBeInTheDocument(); | ||
}); | ||
|
||
it("invokes onChange callback when checkbox is toggled", () => { | ||
const onChange = jest.fn(); | ||
|
||
render(<AddDeployKey {...defaultProps} onChange={onChange} />); | ||
const checkbox = screen.getByTestId("t--added-deploy-key-checkbox"); | ||
|
||
fireEvent.click(checkbox); | ||
expect(onChange).toHaveBeenCalledWith({ isAddedDeployKey: true }); | ||
}); | ||
|
||
it("calls AnalyticsUtil on copy button click", () => { | ||
render(<AddDeployKey {...defaultProps} />); | ||
const copyButton = screen.getByTestId("t--copy-generic"); | ||
|
||
fireEvent.click(copyButton); | ||
expect(AnalyticsUtil.logEvent).toHaveBeenCalledWith( | ||
"GS_COPY_SSH_KEY_BUTTON_CLICK", | ||
); | ||
}); | ||
|
||
it("hides copy button when connectLoading is true", () => { | ||
render(<AddDeployKey {...defaultProps} connectLoading />); | ||
expect(screen.queryByTestId("t--copy-generic")).not.toBeInTheDocument(); | ||
}); | ||
|
||
it("shows repository settings link if gitProvider is known and not 'others'", () => { | ||
render(<AddDeployKey {...defaultProps} />); | ||
const link = screen.getByRole("link", { name: "repository settings." }); | ||
|
||
expect(link).toHaveAttribute( | ||
"href", | ||
"https://github.com/owner/repo/settings/keys", | ||
); | ||
}); | ||
|
||
it("does not show repository link if gitProvider = 'others'", () => { | ||
render( | ||
<AddDeployKey | ||
{...defaultProps} | ||
value={{ gitProvider: "others", remoteUrl: "[email protected]:repo.git" }} | ||
/>, | ||
); | ||
expect( | ||
screen.queryByRole("link", { name: "repository settings." }), | ||
).not.toBeInTheDocument(); | ||
}); | ||
|
||
it("shows collapsible section if gitProvider is not 'others'", () => { | ||
render( | ||
<AddDeployKey | ||
{...defaultProps} | ||
// eslint-disable-next-line react-perf/jsx-no-new-object-as-prop | ||
value={{ | ||
gitProvider: "gitlab", | ||
remoteUrl: "[email protected]:owner/repo.git", | ||
}} | ||
/>, | ||
); | ||
expect( | ||
screen.getByText("How to paste SSH Key in repo and give write access?"), | ||
).toBeInTheDocument(); | ||
expect(screen.getByAltText("Add deploy key in gitlab")).toBeInTheDocument(); | ||
}); | ||
|
||
it("does not display collapsible if gitProvider = 'others'", () => { | ||
render( | ||
<AddDeployKey | ||
{...defaultProps} | ||
// eslint-disable-next-line react-perf/jsx-no-new-object-as-prop | ||
value={{ gitProvider: "others", remoteUrl: "[email protected]:repo.git" }} | ||
/>, | ||
); | ||
expect( | ||
screen.queryByText("How to paste SSH Key in repo and give write access?"), | ||
).not.toBeInTheDocument(); | ||
}); | ||
|
||
it("uses default documentation link if none provided", () => { | ||
render(<AddDeployKey {...defaultProps} />); | ||
const docsLink = screen.getByRole("link", { name: "Read Docs" }); | ||
|
||
expect(docsLink).toHaveAttribute("href", DEFAULT_DOCS_URL); | ||
}); | ||
|
||
it("uses custom documentation link if provided", () => { | ||
render( | ||
<AddDeployKey | ||
{...defaultProps} | ||
deployKeyDocUrl="https://custom-docs.com" | ||
/>, | ||
); | ||
const docsLink = screen.getByRole("link", { name: "Read Docs" }); | ||
|
||
expect(docsLink).toHaveAttribute("href", "https://custom-docs.com"); | ||
}); | ||
|
||
it("does not generate SSH key if modal is closed", () => { | ||
const generateSSHKey = jest.fn(); | ||
|
||
render( | ||
<AddDeployKey | ||
{...defaultProps} | ||
generateSSHKey={generateSSHKey} | ||
isModalOpen={false} | ||
sshKeyPair="" | ||
/>, | ||
); | ||
// Should not call generateSSHKey since modal is not open | ||
expect(generateSSHKey).not.toHaveBeenCalled(); | ||
}); | ||
|
||
it("generates SSH key if none is present and conditions are met", async () => { | ||
const fetchSSHKeyPair = jest.fn((props) => { | ||
props.onSuccessCallback && props.onSuccessCallback(); | ||
}); | ||
const generateSSHKey = jest.fn(); | ||
|
||
render( | ||
<AddDeployKey | ||
{...defaultProps} | ||
fetchSSHKeyPair={fetchSSHKeyPair} | ||
generateSSHKey={generateSSHKey} | ||
isFetchingSSHKeyPair={false} | ||
isGeneratingSSHKey={false} | ||
sshKeyPair="" | ||
/>, | ||
); | ||
|
||
expect(fetchSSHKeyPair).toHaveBeenCalledTimes(1); | ||
|
||
await waitFor(() => { | ||
expect(generateSSHKey).toHaveBeenCalledWith("ECDSA", expect.any(Object)); | ||
}); | ||
}); | ||
}); |
Oops, something went wrong.