From b3dbf5594b870dcb3a9b0549e8b9d469ea721bf5 Mon Sep 17 00:00:00 2001 From: Rohit Date: Wed, 25 Sep 2024 14:14:55 -0400 Subject: [PATCH 01/15] initial commit --- src/pages/CodecovAIPage/CodecovAIPage.tsx | 10 +- .../ConfiguredRepositories.tsx | 165 ++++++++++++++++++ .../codecovAI/useCodecovAIInstallation.tsx | 57 ++++++ .../codecovAI/useCodecovAIInstalledRepos.tsx | 58 ++++++ src/shared/ListRepo/ReposTable/ReposTable.tsx | 1 + 5 files changed, 288 insertions(+), 3 deletions(-) create mode 100644 src/pages/CodecovAIPage/ConfiguredRepositories/ConfiguredRepositories.tsx create mode 100644 src/services/codecovAI/useCodecovAIInstallation.tsx create mode 100644 src/services/codecovAI/useCodecovAIInstalledRepos.tsx diff --git a/src/pages/CodecovAIPage/CodecovAIPage.tsx b/src/pages/CodecovAIPage/CodecovAIPage.tsx index 25b163c531..5c9d7f8579 100644 --- a/src/pages/CodecovAIPage/CodecovAIPage.tsx +++ b/src/pages/CodecovAIPage/CodecovAIPage.tsx @@ -1,8 +1,10 @@ import { Redirect, useParams } from 'react-router-dom' +import { useCodecovAIInstallation } from 'services/codecovAI/useCodecovAIInstallation' import { useFlags } from 'shared/featureFlags' import CodecovAICommands from './CodecovAICommands/CodecovAICommands' +import ConfiguredRepositories from './ConfiguredRepositories/ConfiguredRepositories' import InstallCodecovAI from './InstallCodecovAI/InstallCodecovAI' import LearnMoreBlurb from './LearnMoreBlurb/LearnMoreBlurb' import Tabs from './Tabs/Tabs' @@ -14,11 +16,13 @@ interface URLParams { const CodecovAIPage: React.FC = () => { const { provider, owner } = useParams() - const { codecovAiFeaturesTab } = useFlags({ codecovAiFeaturesTab: false, }) - + const { data: aiFeaturesEnabled } = useCodecovAIInstallation({ + owner, + provider, + }) if (!codecovAiFeaturesTab) { return } @@ -36,7 +40,7 @@ const CodecovAIPage: React.FC = () => {

- + {!aiFeaturesEnabled ? : }
diff --git a/src/pages/CodecovAIPage/ConfiguredRepositories/ConfiguredRepositories.tsx b/src/pages/CodecovAIPage/ConfiguredRepositories/ConfiguredRepositories.tsx new file mode 100644 index 0000000000..6d04b3b160 --- /dev/null +++ b/src/pages/CodecovAIPage/ConfiguredRepositories/ConfiguredRepositories.tsx @@ -0,0 +1,165 @@ +import { + createColumnHelper, + flexRender, + getCoreRowModel, + useReactTable, +} from '@tanstack/react-table' +import React, { useMemo, useState } from 'react' +import { useParams } from 'react-router-dom' + +import { useCodecovAIInstalledRepos } from 'services/codecovAI/useCodecovAIInstalledRepos' +import A from 'ui/A' +import { Card } from 'ui/Card' +import Icon from 'ui/Icon' +import Spinner from 'ui/Spinner' + +import InstallCodecovAI from '../InstallCodecovAI/InstallCodecovAI' + +interface URLParams { + owner: string + provider: string +} + +const Loader = () => ( +
+ +
+) + +const columnHelper = createColumnHelper<{ name: string }>() + +function ConfiguredRepositories() { + const { owner, provider } = useParams() + + const { data: installedRepos, isLoading } = useCodecovAIInstalledRepos({ + owner, + provider, + }) + + const [tempInstalledRepos, setTempInstalledRepos] = useState([ + 'gazebo', + 'review-prompt-os', + ]) + + const [isSortedAscending, setIsSortedAscending] = useState(true) + + const sortRepos = () => { + const nextIsSortedAscending = !isSortedAscending + const sortedRepos = [...tempInstalledRepos].sort((a, b) => + nextIsSortedAscending ? a.localeCompare(b) : b.localeCompare(a) + ) + setTempInstalledRepos(sortedRepos) + setIsSortedAscending(nextIsSortedAscending) + } + + const tableData = useMemo( + () => tempInstalledRepos.map((name) => ({ name })), + [tempInstalledRepos] + ) + + const columns = useMemo( + () => [ + columnHelper.accessor('name', { + header: 'Repo Name', + cell: (info) => { + const repoName = info.getValue() + const link = `/${provider}/${owner}/${repoName}` + return ( + + {repoName} + + ) + }, + }), + ], + [provider, owner] + ) + + const table = useReactTable({ + data: tableData, + columns, + getCoreRowModel: getCoreRowModel(), + }) + + //This should technically never happen, but render a fallback just in case + if (!installedRepos || installedRepos.length === 0) { + return + } + + return ( +
+ + + + {tempInstalledRepos?.length} configured repositories + +

+ To install more repos, please manage your Codecov AI app on Github. +
+ To uninstall the app, please go to your GitHub Apps settings. +

+
+
+
+ + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => ( + + ))} + + ))} + + + {isLoading ? ( + + + + ) : ( + table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + ))} + + )) + )} + +
+
+ {flexRender( + header.column.columnDef.header, + header.getContext() + )} + + + +
+
+ +
+ {flexRender( + cell.column.columnDef.cell, + cell.getContext() + )} +
+
+
+ ) +} + +export default ConfiguredRepositories diff --git a/src/services/codecovAI/useCodecovAIInstallation.tsx b/src/services/codecovAI/useCodecovAIInstallation.tsx new file mode 100644 index 0000000000..30bc6f93d0 --- /dev/null +++ b/src/services/codecovAI/useCodecovAIInstallation.tsx @@ -0,0 +1,57 @@ +import { useQuery } from '@tanstack/react-query' +import z from 'zod' + +import Api from 'shared/api' +import { NetworkErrorObject } from 'shared/api/helpers' + +const ResponseSchema = z.object({ + owner: z + .object({ + aiFeaturesEnabled: z.boolean(), + }) + .nullable(), +}) + +const query = ` + query GetCodecovAIAppInstallInfo($username: String!) { + owner(username: $username) { + aiFeaturesEnabled + } + } +` + +interface CodecovAIInstallationProps { + owner: string + provider: string +} + +export function useCodecovAIInstallation({ + owner, + provider, +}: CodecovAIInstallationProps) { + return useQuery({ + queryKey: ['GetCodecovAIAppInstallInfo', provider, owner], + queryFn: ({ signal }) => { + return Api.graphql({ + provider, + query, + signal, + variables: { + username: owner, + }, + }).then((res) => { + const parsedRes = ResponseSchema.safeParse(res?.data) + + if (!parsedRes.success) { + return Promise.reject({ + status: 404, + data: {}, + dev: 'useCodecovAIInstallation - 404 failed to parse', + } satisfies NetworkErrorObject) + } + + return parsedRes.data.owner?.aiFeaturesEnabled + }) + }, + }) +} diff --git a/src/services/codecovAI/useCodecovAIInstalledRepos.tsx b/src/services/codecovAI/useCodecovAIInstalledRepos.tsx new file mode 100644 index 0000000000..2e9ca691c0 --- /dev/null +++ b/src/services/codecovAI/useCodecovAIInstalledRepos.tsx @@ -0,0 +1,58 @@ +import { useQuery } from '@tanstack/react-query' +import z from 'zod' + +import Api from 'shared/api' +import { NetworkErrorObject } from 'shared/api/helpers' + +const ResponseSchema = z.object({ + owner: z + .object({ + aiEnabledRepos: z.array(z.string().nullable()).nullable(), + }) + .nullable(), +}) + +const query = ` + query GetCodecovAIInstalledRepos($username: String!) { + owner(username: $username) { + aiEnabledRepos + } + } +` + +interface CodecovAIInstalledReposProps { + owner: string + provider: string +} + +export function useCodecovAIInstalledRepos({ + owner, + provider, +}: CodecovAIInstalledReposProps) { + return useQuery({ + queryKey: ['GetCodecovAIInstalledRepos', provider, owner], + queryFn: ({ signal }) => { + return Api.graphql({ + provider, + query, + signal, + variables: { + username: owner, + }, + }).then((res) => { + const parsedRes = ResponseSchema.safeParse(res?.data) + + if (!parsedRes.success) { + console.log(res?.data) + return Promise.reject({ + status: 404, + data: {}, + dev: 'useCodecovAIInstalledRepos - 404 failed to parse', + } satisfies NetworkErrorObject) + } + + return parsedRes.data.owner?.aiEnabledRepos + }) + }, + }) +} diff --git a/src/shared/ListRepo/ReposTable/ReposTable.tsx b/src/shared/ListRepo/ReposTable/ReposTable.tsx index 0dc269c6da..efa9a597bb 100644 --- a/src/shared/ListRepo/ReposTable/ReposTable.tsx +++ b/src/shared/ListRepo/ReposTable/ReposTable.tsx @@ -224,6 +224,7 @@ const ReposTable = ({ className="text-ds-blue-darker group-hover/columnheader:opacity-100" data-sort-direction={header.column.getIsSorted()} > + {''} From d8a8da0aa3aac1a8ceeb9f61a86d9a6e5f3c7051 Mon Sep 17 00:00:00 2001 From: Rohit Vinnakota <148245014+rohitvinnakota-codecov@users.noreply.github.com> Date: Wed, 25 Sep 2024 14:16:28 -0400 Subject: [PATCH 02/15] Updates from editor --- src/shared/ListRepo/ReposTable/ReposTable.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/shared/ListRepo/ReposTable/ReposTable.tsx b/src/shared/ListRepo/ReposTable/ReposTable.tsx index efa9a597bb..0dc269c6da 100644 --- a/src/shared/ListRepo/ReposTable/ReposTable.tsx +++ b/src/shared/ListRepo/ReposTable/ReposTable.tsx @@ -224,7 +224,6 @@ const ReposTable = ({ className="text-ds-blue-darker group-hover/columnheader:opacity-100" data-sort-direction={header.column.getIsSorted()} > - {''} From 5138d9ef9f64c88fbe45fc9500975488ba79cec0 Mon Sep 17 00:00:00 2001 From: Rohit Date: Wed, 25 Sep 2024 16:53:16 -0400 Subject: [PATCH 03/15] tests for hooks --- .../useCodecovAIInstallation.spec.tsx | 110 ++++++++++++++++++ .../codecovAI/useCodecovAIInstallation.tsx | 5 +- .../useCodecovAIInstalledRepos.spec.tsx | 110 ++++++++++++++++++ .../codecovAI/useCodecovAIInstalledRepos.tsx | 5 +- 4 files changed, 227 insertions(+), 3 deletions(-) create mode 100644 src/services/codecovAI/useCodecovAIInstallation.spec.tsx create mode 100644 src/services/codecovAI/useCodecovAIInstalledRepos.spec.tsx diff --git a/src/services/codecovAI/useCodecovAIInstallation.spec.tsx b/src/services/codecovAI/useCodecovAIInstallation.spec.tsx new file mode 100644 index 0000000000..df074d6159 --- /dev/null +++ b/src/services/codecovAI/useCodecovAIInstallation.spec.tsx @@ -0,0 +1,110 @@ +import { QueryClient, QueryClientProvider } from '@tanstack/react-query' +import { renderHook, waitFor } from '@testing-library/react' +import { graphql } from 'msw' +import { setupServer } from 'msw/node' + +import { useCodecovAIInstallation } from './useCodecovAIInstallation' + +const mockAiFeaturesEnabled = { + owner: { + aiFeaturesEnabled: true, + }, +} + +const mockUnsuccessfulParseError = { + owner: { + wrong: 'schema', + }, +} + +const server = setupServer() +const queryClient = new QueryClient({ + defaultOptions: { queries: { retry: false } }, +}) + +const wrapper: React.FC = ({ children }) => ( + {children} +) + +beforeAll(() => { + server.listen() +}) + +afterEach(() => { + queryClient.clear() + server.resetHandlers() +}) + +afterAll(() => { + server.close() +}) + +interface SetupArgs { + isUnsuccessfulParseError?: boolean +} + +describe('useCodecovAIInstallation', () => { + function setup({ isUnsuccessfulParseError = false }: SetupArgs) { + server.use( + graphql.query('GetCodecovAIAppInstallInfo', (req, res, ctx) => { + if (isUnsuccessfulParseError) { + return res(ctx.status(200), ctx.data(mockUnsuccessfulParseError)) + } + return res(ctx.status(200), ctx.data(mockAiFeaturesEnabled)) + }) + ) + } + + describe('there is valid data', () => { + it('fetches the owner app installation info', async () => { + setup({}) + const { result } = renderHook( + () => + useCodecovAIInstallation({ + owner: 'codecov', + provider: 'gh', + }), + { wrapper } + ) + + await waitFor(() => + expect(result.current.data).toStrictEqual({ + aiFeaturesEnabled: true, + }) + ) + }) + }) + + describe('unsuccessful parse of zod schema', () => { + let oldConsoleError = console.error + + beforeEach(() => { + console.error = () => null + }) + + afterEach(() => { + console.error = oldConsoleError + }) + + it('throws a 404', async () => { + setup({ isUnsuccessfulParseError: true }) + const { result } = renderHook( + () => + useCodecovAIInstallation({ + owner: 'codecov', + provider: 'gh', + }), + { wrapper } + ) + + await waitFor(() => expect(result.current.isError).toBeTruthy()) + await waitFor(() => + expect(result.current.error).toEqual( + expect.objectContaining({ + status: 404, + }) + ) + ) + }) + }) +}) diff --git a/src/services/codecovAI/useCodecovAIInstallation.tsx b/src/services/codecovAI/useCodecovAIInstallation.tsx index 30bc6f93d0..1f5efeb62a 100644 --- a/src/services/codecovAI/useCodecovAIInstallation.tsx +++ b/src/services/codecovAI/useCodecovAIInstallation.tsx @@ -40,6 +40,7 @@ export function useCodecovAIInstallation({ username: owner, }, }).then((res) => { + console.log(res?.data) const parsedRes = ResponseSchema.safeParse(res?.data) if (!parsedRes.success) { @@ -50,7 +51,9 @@ export function useCodecovAIInstallation({ } satisfies NetworkErrorObject) } - return parsedRes.data.owner?.aiFeaturesEnabled + return { + aiFeaturesEnabled: parsedRes.data.owner?.aiFeaturesEnabled, + } }) }, }) diff --git a/src/services/codecovAI/useCodecovAIInstalledRepos.spec.tsx b/src/services/codecovAI/useCodecovAIInstalledRepos.spec.tsx new file mode 100644 index 0000000000..a065fb3d47 --- /dev/null +++ b/src/services/codecovAI/useCodecovAIInstalledRepos.spec.tsx @@ -0,0 +1,110 @@ +import { QueryClient, QueryClientProvider } from '@tanstack/react-query' +import { renderHook, waitFor } from '@testing-library/react' +import { graphql } from 'msw' +import { setupServer } from 'msw/node' + +import { useCodecovAIInstalledRepos } from './useCodecovAIInstalledRepos' + +const mockAiInstalledRepos = { + owner: { + aiEnabledRepos: ['repo-1', 'repo-2'], + }, +} + +const mockUnsuccessfulParseError = { + owner: { + wrong: 'schema', + }, +} + +const server = setupServer() +const queryClient = new QueryClient({ + defaultOptions: { queries: { retry: false } }, +}) + +const wrapper: React.FC = ({ children }) => ( + {children} +) + +beforeAll(() => { + server.listen() +}) + +afterEach(() => { + queryClient.clear() + server.resetHandlers() +}) + +afterAll(() => { + server.close() +}) + +interface SetupArgs { + isUnsuccessfulParseError?: boolean +} + +describe('useCodecovAIInstalledRepos', () => { + function setup({ isUnsuccessfulParseError = false }: SetupArgs) { + server.use( + graphql.query('GetCodecovAIInstalledRepos', (req, res, ctx) => { + if (isUnsuccessfulParseError) { + return res(ctx.status(200), ctx.data(mockUnsuccessfulParseError)) + } + return res(ctx.status(200), ctx.data(mockAiInstalledRepos)) + }) + ) + } + + describe('there is valid data', () => { + it('fetches the correct list of repos', async () => { + setup({}) + const { result } = renderHook( + () => + useCodecovAIInstalledRepos({ + owner: 'codecov', + provider: 'gh', + }), + { wrapper } + ) + + await waitFor(() => + expect(result.current.data).toStrictEqual({ + aiEnabledRepos: ['repo-1', 'repo-2'], + }) + ) + }) + }) + + describe('unsuccessful parse of zod schema', () => { + let oldConsoleError = console.error + + beforeEach(() => { + console.error = () => null + }) + + afterEach(() => { + console.error = oldConsoleError + }) + + it('throws a 404', async () => { + setup({ isUnsuccessfulParseError: true }) + const { result } = renderHook( + () => + useCodecovAIInstalledRepos({ + owner: 'codecov', + provider: 'gh', + }), + { wrapper } + ) + + await waitFor(() => expect(result.current.isError).toBeTruthy()) + await waitFor(() => + expect(result.current.error).toEqual( + expect.objectContaining({ + status: 404, + }) + ) + ) + }) + }) +}) diff --git a/src/services/codecovAI/useCodecovAIInstalledRepos.tsx b/src/services/codecovAI/useCodecovAIInstalledRepos.tsx index 2e9ca691c0..ce978a8e6a 100644 --- a/src/services/codecovAI/useCodecovAIInstalledRepos.tsx +++ b/src/services/codecovAI/useCodecovAIInstalledRepos.tsx @@ -43,7 +43,6 @@ export function useCodecovAIInstalledRepos({ const parsedRes = ResponseSchema.safeParse(res?.data) if (!parsedRes.success) { - console.log(res?.data) return Promise.reject({ status: 404, data: {}, @@ -51,7 +50,9 @@ export function useCodecovAIInstalledRepos({ } satisfies NetworkErrorObject) } - return parsedRes.data.owner?.aiEnabledRepos + return { + aiEnabledRepos: parsedRes.data.owner?.aiEnabledRepos, + } }) }, }) From 169f4a34e152c11defdbf4d7f4de802c553e9aef Mon Sep 17 00:00:00 2001 From: Rohit Date: Wed, 25 Sep 2024 16:57:49 -0400 Subject: [PATCH 04/15] use proper response --- src/pages/CodecovAIPage/CodecovAIPage.tsx | 10 ++++++++-- .../ConfiguredRepositories/ConfiguredRepositories.tsx | 4 ++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/pages/CodecovAIPage/CodecovAIPage.tsx b/src/pages/CodecovAIPage/CodecovAIPage.tsx index 5c9d7f8579..5d55c28d5e 100644 --- a/src/pages/CodecovAIPage/CodecovAIPage.tsx +++ b/src/pages/CodecovAIPage/CodecovAIPage.tsx @@ -19,10 +19,12 @@ const CodecovAIPage: React.FC = () => { const { codecovAiFeaturesTab } = useFlags({ codecovAiFeaturesTab: false, }) - const { data: aiFeaturesEnabled } = useCodecovAIInstallation({ + + const { data: installationData } = useCodecovAIInstallation({ owner, provider, }) + if (!codecovAiFeaturesTab) { return } @@ -40,7 +42,11 @@ const CodecovAIPage: React.FC = () => {

- {!aiFeaturesEnabled ? : } + {!installationData?.aiFeaturesEnabled ? ( + + ) : ( + + )}
diff --git a/src/pages/CodecovAIPage/ConfiguredRepositories/ConfiguredRepositories.tsx b/src/pages/CodecovAIPage/ConfiguredRepositories/ConfiguredRepositories.tsx index 6d04b3b160..7912957c2f 100644 --- a/src/pages/CodecovAIPage/ConfiguredRepositories/ConfiguredRepositories.tsx +++ b/src/pages/CodecovAIPage/ConfiguredRepositories/ConfiguredRepositories.tsx @@ -31,7 +31,7 @@ const columnHelper = createColumnHelper<{ name: string }>() function ConfiguredRepositories() { const { owner, provider } = useParams() - const { data: installedRepos, isLoading } = useCodecovAIInstalledRepos({ + const { data, isLoading } = useCodecovAIInstalledRepos({ owner, provider, }) @@ -87,7 +87,7 @@ function ConfiguredRepositories() { }) //This should technically never happen, but render a fallback just in case - if (!installedRepos || installedRepos.length === 0) { + if (!data?.aiEnabledRepos || data.aiEnabledRepos.length === 0) { return } From a454ca60f544759fc22089708c860a6ddb5e9ea6 Mon Sep 17 00:00:00 2001 From: Rohit Date: Wed, 25 Sep 2024 18:01:30 -0400 Subject: [PATCH 05/15] More tests --- .../CodecovAIPage/CodecovAIPage.spec.tsx | 53 ++++++++++++++++++- src/pages/CodecovAIPage/CodecovAIPage.tsx | 2 +- .../ConfiguredRepositories.tsx | 39 +++++--------- .../InstallCodecovAI/InstallCodecovAI.tsx | 1 - .../codecovAI/useCodecovAIInstallation.tsx | 2 - .../codecovAI/useCodecovAIInstalledRepos.tsx | 2 +- 6 files changed, 66 insertions(+), 33 deletions(-) diff --git a/src/pages/CodecovAIPage/CodecovAIPage.spec.tsx b/src/pages/CodecovAIPage/CodecovAIPage.spec.tsx index 7ef597cf11..66401af421 100644 --- a/src/pages/CodecovAIPage/CodecovAIPage.spec.tsx +++ b/src/pages/CodecovAIPage/CodecovAIPage.spec.tsx @@ -1,5 +1,6 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { render, screen } from '@testing-library/react' +import { graphql } from 'msw' import { setupServer } from 'msw/node' import { MemoryRouter, Route } from 'react-router-dom' @@ -15,7 +16,6 @@ const queryClient = new QueryClient({ defaultOptions: { queries: { suspense: true, - retry: false, }, }, }) @@ -45,7 +45,33 @@ afterAll(() => { }) describe('CodecovAIPage', () => { + function setup(aiFeaturesEnabled = false) { + server.use( + graphql.query('GetCodecovAIAppInstallInfo', (req, res, ctx) => { + return res( + ctx.status(200), + ctx.data({ + owner: { + aiFeaturesEnabled, + }, + }) + ) + }), + graphql.query('GetCodecovAIInstalledRepos', (req, res, ctx) => { + return res( + ctx.status(200), + ctx.data({ + owner: { + aiEnabledRepos: ['repo-1', 'repo-2'], + }, + }) + ) + }) + ) + } + beforeEach(() => { + setup() mockedUseFlags.mockReturnValue({ codecovAiFeaturesTab: true }) }) @@ -74,7 +100,7 @@ describe('CodecovAIPage', () => { it('renders the install button', async () => { render(, { wrapper }) - const buttonEl = screen.getByRole('link', { name: /Install Codecov AI/i }) + const buttonEl = await screen.findByText(/Install Codecov AI/) expect(buttonEl).toBeInTheDocument() }) @@ -126,6 +152,29 @@ describe('CodecovAIPage', () => { const docLink = await screen.findByText(/Visit our guide/) expect(docLink).toBeInTheDocument() }) + + describe('AI features are enabled and configured', () => { + beforeEach(() => { + setup(true) + mockedUseFlags.mockReturnValue({ codecovAiFeaturesTab: true }) + }) + + it('does not render install link', () => { + setup(true) + render(, { wrapper }) + const topSection = screen.queryByText(/Install Codecov AI/) + expect(topSection).not.toBeInTheDocument() + }) + + it('renders list of repos', async () => { + render(, { wrapper }) + + const repo1Link = await screen.findByText(/repo-1/) + expect(repo1Link).toBeInTheDocument() + const repo2Link = await screen.findByText(/repo-1/) + expect(repo2Link).toBeInTheDocument() + }) + }) }) describe('flag is off', () => { diff --git a/src/pages/CodecovAIPage/CodecovAIPage.tsx b/src/pages/CodecovAIPage/CodecovAIPage.tsx index 5d55c28d5e..0f9cb42a0c 100644 --- a/src/pages/CodecovAIPage/CodecovAIPage.tsx +++ b/src/pages/CodecovAIPage/CodecovAIPage.tsx @@ -42,7 +42,7 @@ const CodecovAIPage: React.FC = () => {

- {!installationData?.aiFeaturesEnabled ? ( + {installationData?.aiFeaturesEnabled ? ( ) : ( diff --git a/src/pages/CodecovAIPage/ConfiguredRepositories/ConfiguredRepositories.tsx b/src/pages/CodecovAIPage/ConfiguredRepositories/ConfiguredRepositories.tsx index 7912957c2f..c39ff7b882 100644 --- a/src/pages/CodecovAIPage/ConfiguredRepositories/ConfiguredRepositories.tsx +++ b/src/pages/CodecovAIPage/ConfiguredRepositories/ConfiguredRepositories.tsx @@ -30,32 +30,24 @@ const columnHelper = createColumnHelper<{ name: string }>() function ConfiguredRepositories() { const { owner, provider } = useParams() - const { data, isLoading } = useCodecovAIInstalledRepos({ owner, provider, }) - const [tempInstalledRepos, setTempInstalledRepos] = useState([ - 'gazebo', - 'review-prompt-os', - ]) - const [isSortedAscending, setIsSortedAscending] = useState(true) const sortRepos = () => { - const nextIsSortedAscending = !isSortedAscending - const sortedRepos = [...tempInstalledRepos].sort((a, b) => - nextIsSortedAscending ? a.localeCompare(b) : b.localeCompare(a) - ) - setTempInstalledRepos(sortedRepos) - setIsSortedAscending(nextIsSortedAscending) + setIsSortedAscending(!isSortedAscending) } - const tableData = useMemo( - () => tempInstalledRepos.map((name) => ({ name })), - [tempInstalledRepos] - ) + const tableData = useMemo(() => { + if (!data?.aiEnabledRepos) return [] + const sortedRepos = [...data.aiEnabledRepos].sort((a, b) => + isSortedAscending ? a.localeCompare(b) : b.localeCompare(a) + ) + return sortedRepos.map((name) => ({ name })) + }, [data?.aiEnabledRepos, isSortedAscending]) const columns = useMemo( () => [ @@ -65,12 +57,7 @@ function ConfiguredRepositories() { const repoName = info.getValue() const link = `/${provider}/${owner}/${repoName}` return ( - + {repoName} ) @@ -86,8 +73,8 @@ function ConfiguredRepositories() { getCoreRowModel: getCoreRowModel(), }) - //This should technically never happen, but render a fallback just in case - if (!data?.aiEnabledRepos || data.aiEnabledRepos.length === 0) { + // This should technically never happen, but render a fallback just in case + if (tableData.length === 0) { return } @@ -96,10 +83,10 @@ function ConfiguredRepositories() { - {tempInstalledRepos?.length} configured repositories + {tableData.length} configured repositories

- To install more repos, please manage your Codecov AI app on Github. + To install more repos, please manage your Codecov AI app on GitHub.
To uninstall the app, please go to your GitHub Apps settings.

diff --git a/src/pages/CodecovAIPage/InstallCodecovAI/InstallCodecovAI.tsx b/src/pages/CodecovAIPage/InstallCodecovAI/InstallCodecovAI.tsx index 055015b772..b7510c3795 100644 --- a/src/pages/CodecovAIPage/InstallCodecovAI/InstallCodecovAI.tsx +++ b/src/pages/CodecovAIPage/InstallCodecovAI/InstallCodecovAI.tsx @@ -9,7 +9,6 @@ const COPY_APP_INSTALL_STRING = const InstallCodecovAI: React.FC = () => { const { theme } = useThemeContext() - const isDarkMode = theme === Theme.DARK const githubImage = loginProviderImage('Github', !isDarkMode) diff --git a/src/services/codecovAI/useCodecovAIInstallation.tsx b/src/services/codecovAI/useCodecovAIInstallation.tsx index 1f5efeb62a..80e4404697 100644 --- a/src/services/codecovAI/useCodecovAIInstallation.tsx +++ b/src/services/codecovAI/useCodecovAIInstallation.tsx @@ -40,9 +40,7 @@ export function useCodecovAIInstallation({ username: owner, }, }).then((res) => { - console.log(res?.data) const parsedRes = ResponseSchema.safeParse(res?.data) - if (!parsedRes.success) { return Promise.reject({ status: 404, diff --git a/src/services/codecovAI/useCodecovAIInstalledRepos.tsx b/src/services/codecovAI/useCodecovAIInstalledRepos.tsx index ce978a8e6a..23953d323d 100644 --- a/src/services/codecovAI/useCodecovAIInstalledRepos.tsx +++ b/src/services/codecovAI/useCodecovAIInstalledRepos.tsx @@ -7,7 +7,7 @@ import { NetworkErrorObject } from 'shared/api/helpers' const ResponseSchema = z.object({ owner: z .object({ - aiEnabledRepos: z.array(z.string().nullable()).nullable(), + aiEnabledRepos: z.array(z.string()).nullable(), }) .nullable(), }) From 2fe4f86e7ea58d7d7a22da9535546d7049e2e280 Mon Sep 17 00:00:00 2001 From: Rohit Date: Thu, 26 Sep 2024 11:10:35 -0400 Subject: [PATCH 06/15] Migrate --- ...vAIInstallation.spec.tsx => useCodecovAIInstallation.test.tsx} | 0 ...nstalledRepos.spec.tsx => useCodecovAIInstalledRepos.test.tsx} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename src/services/codecovAI/{useCodecovAIInstallation.spec.tsx => useCodecovAIInstallation.test.tsx} (100%) rename src/services/codecovAI/{useCodecovAIInstalledRepos.spec.tsx => useCodecovAIInstalledRepos.test.tsx} (100%) diff --git a/src/services/codecovAI/useCodecovAIInstallation.spec.tsx b/src/services/codecovAI/useCodecovAIInstallation.test.tsx similarity index 100% rename from src/services/codecovAI/useCodecovAIInstallation.spec.tsx rename to src/services/codecovAI/useCodecovAIInstallation.test.tsx diff --git a/src/services/codecovAI/useCodecovAIInstalledRepos.spec.tsx b/src/services/codecovAI/useCodecovAIInstalledRepos.test.tsx similarity index 100% rename from src/services/codecovAI/useCodecovAIInstalledRepos.spec.tsx rename to src/services/codecovAI/useCodecovAIInstalledRepos.test.tsx From 0b9012317d008ef8cf13468de7d5bd97f655d467 Mon Sep 17 00:00:00 2001 From: Rohit Date: Thu, 26 Sep 2024 11:42:27 -0400 Subject: [PATCH 07/15] Update tests --- .../CodecovAIPage/CodecovAIPage.test.tsx | 72 +++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/src/pages/CodecovAIPage/CodecovAIPage.test.tsx b/src/pages/CodecovAIPage/CodecovAIPage.test.tsx index 9d1d3309bd..415c5ca7f7 100644 --- a/src/pages/CodecovAIPage/CodecovAIPage.test.tsx +++ b/src/pages/CodecovAIPage/CodecovAIPage.test.tsx @@ -1,10 +1,14 @@ +import { QueryClient } from '@tanstack/react-query' import { render, screen } from '@testing-library/react' +import { graphql, HttpResponse } from 'msw2' +import { setupServer } from 'msw2/node' import { MemoryRouter, Route } from 'react-router-dom' import { ThemeContextProvider } from 'shared/ThemeContext' import CodecovAIPage from './CodecovAIPage' + const mocks = vi.hoisted(() => ({ useFlags: vi.fn(), })) @@ -17,6 +21,16 @@ vi.mock('shared/featureFlags', async () => { } }) +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + suspense: true, + }, + }, +}) + +const server = setupServer() + const wrapper: React.FC = ({ children }) => ( @@ -25,7 +39,42 @@ const wrapper: React.FC = ({ children }) => ( ) +beforeAll(() => { + server.listen() +}) + +afterEach(() => { + queryClient.clear() + server.resetHandlers() +}) + +afterAll(() => { + server.close() +}) + describe('CodecovAIPage', () => { + function setup(aiFeaturesEnabled = false) { + server.use( + graphql.query('GetCodecovAIAppInstallInfo', (info) => { + return HttpResponse.json({ + data: { + owner: { + aiFeaturesEnabled, + }, + }, + }) + }), + graphql.query('GetCodecovAIInstalledRepos', (info) => { + return HttpResponse.json({ + data: { + owner: { + aiEnabledRepos: ['repo-1', 'repo-2'], + }, + }, + }) + }) + ) + } beforeEach(() => { mocks.useFlags.mockReturnValue({ codecovAiFeaturesTab: true }) }) @@ -107,6 +156,29 @@ describe('CodecovAIPage', () => { const docLink = await screen.findByText(/Visit our guide/) expect(docLink).toBeInTheDocument() }) + + describe('AI features are enabled and configured', () => { + beforeEach(() => { + setup(true) + mocks.useFlags.mockReturnValue({ codecovAiFeaturesTab: true }) + }) + + it('does not render install link', () => { + setup(true) + render(, { wrapper }) + const topSection = screen.queryByText(/Install Codecov AI/) + expect(topSection).not.toBeInTheDocument() + }) + + it('renders list of repos', async () => { + render(, { wrapper }) + + const repo1Link = await screen.findByText(/repo-1/) + expect(repo1Link).toBeInTheDocument() + const repo2Link = await screen.findByText(/repo-1/) + expect(repo2Link).toBeInTheDocument() + }) + }) }) describe('flag is off', () => { From c759d865e883dbcab2a88906a7090ab690385b04 Mon Sep 17 00:00:00 2001 From: Rohit Date: Thu, 26 Sep 2024 12:04:15 -0400 Subject: [PATCH 08/15] Add query client --- src/pages/CodecovAIPage/CodecovAIPage.test.tsx | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/pages/CodecovAIPage/CodecovAIPage.test.tsx b/src/pages/CodecovAIPage/CodecovAIPage.test.tsx index 415c5ca7f7..4752dde516 100644 --- a/src/pages/CodecovAIPage/CodecovAIPage.test.tsx +++ b/src/pages/CodecovAIPage/CodecovAIPage.test.tsx @@ -1,4 +1,4 @@ -import { QueryClient } from '@tanstack/react-query' +import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { render, screen } from '@testing-library/react' import { graphql, HttpResponse } from 'msw2' import { setupServer } from 'msw2/node' @@ -8,7 +8,6 @@ import { ThemeContextProvider } from 'shared/ThemeContext' import CodecovAIPage from './CodecovAIPage' - const mocks = vi.hoisted(() => ({ useFlags: vi.fn(), })) @@ -32,11 +31,13 @@ const queryClient = new QueryClient({ const server = setupServer() const wrapper: React.FC = ({ children }) => ( - - - {children} - - + + + + {children} + + + ) beforeAll(() => { From 780bce6e4f54c96871045721d2e11d186fbf203b Mon Sep 17 00:00:00 2001 From: Rohit Date: Thu, 26 Sep 2024 14:41:19 -0400 Subject: [PATCH 09/15] add query client --- src/pages/CodecovAIPage/CodecovAIPage.test.tsx | 14 ++++---------- src/pages/CodecovAIPage/CodecovAIPage.tsx | 4 ++-- .../codecovAI/useCodecovAIInstallation.tsx | 1 + 3 files changed, 7 insertions(+), 12 deletions(-) diff --git a/src/pages/CodecovAIPage/CodecovAIPage.test.tsx b/src/pages/CodecovAIPage/CodecovAIPage.test.tsx index 4752dde516..3e92c86f2f 100644 --- a/src/pages/CodecovAIPage/CodecovAIPage.test.tsx +++ b/src/pages/CodecovAIPage/CodecovAIPage.test.tsx @@ -20,21 +20,15 @@ vi.mock('shared/featureFlags', async () => { } }) -const queryClient = new QueryClient({ - defaultOptions: { - queries: { - suspense: true, - }, - }, -}) +const queryClient = new QueryClient() const server = setupServer() const wrapper: React.FC = ({ children }) => ( - - {children} + + {children} @@ -105,7 +99,7 @@ describe('CodecovAIPage', () => { it('renders the install button', async () => { render(, { wrapper }) - const buttonEl = screen.getByRole('link', { name: /Install Codecov AI/i }) + const buttonEl = screen.queryByText(/Install Codecov AI/i) expect(buttonEl).toBeInTheDocument() }) diff --git a/src/pages/CodecovAIPage/CodecovAIPage.tsx b/src/pages/CodecovAIPage/CodecovAIPage.tsx index 0f9cb42a0c..9f42a37039 100644 --- a/src/pages/CodecovAIPage/CodecovAIPage.tsx +++ b/src/pages/CodecovAIPage/CodecovAIPage.tsx @@ -24,11 +24,11 @@ const CodecovAIPage: React.FC = () => { owner, provider, }) - + console.log('???') if (!codecovAiFeaturesTab) { return } - + console.log(installationData) return ( <> diff --git a/src/services/codecovAI/useCodecovAIInstallation.tsx b/src/services/codecovAI/useCodecovAIInstallation.tsx index 80e4404697..6be4bf4776 100644 --- a/src/services/codecovAI/useCodecovAIInstallation.tsx +++ b/src/services/codecovAI/useCodecovAIInstallation.tsx @@ -41,6 +41,7 @@ export function useCodecovAIInstallation({ }, }).then((res) => { const parsedRes = ResponseSchema.safeParse(res?.data) + console.log(res?.data) if (!parsedRes.success) { return Promise.reject({ status: 404, From e2db32bac3198c03c99db7b18e040d2b2aea8aab Mon Sep 17 00:00:00 2001 From: Rohit Date: Thu, 26 Sep 2024 16:31:10 -0400 Subject: [PATCH 10/15] Convert to test --- ...vAIInstallation.test.tsx => useCodecovAIInstallation.spec.tsx} | 0 ...nstalledRepos.test.tsx => useCodecovAIInstalledRepos.spec.tsx} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename src/services/codecovAI/{useCodecovAIInstallation.test.tsx => useCodecovAIInstallation.spec.tsx} (100%) rename src/services/codecovAI/{useCodecovAIInstalledRepos.test.tsx => useCodecovAIInstalledRepos.spec.tsx} (100%) diff --git a/src/services/codecovAI/useCodecovAIInstallation.test.tsx b/src/services/codecovAI/useCodecovAIInstallation.spec.tsx similarity index 100% rename from src/services/codecovAI/useCodecovAIInstallation.test.tsx rename to src/services/codecovAI/useCodecovAIInstallation.spec.tsx diff --git a/src/services/codecovAI/useCodecovAIInstalledRepos.test.tsx b/src/services/codecovAI/useCodecovAIInstalledRepos.spec.tsx similarity index 100% rename from src/services/codecovAI/useCodecovAIInstalledRepos.test.tsx rename to src/services/codecovAI/useCodecovAIInstalledRepos.spec.tsx From 82d6b80dcbd0f2c17d45c9f59dad788ae48d3b7b Mon Sep 17 00:00:00 2001 From: Rohit Date: Thu, 26 Sep 2024 16:42:03 -0400 Subject: [PATCH 11/15] Update --- src/pages/CodecovAIPage/CodecovAIPage.test.tsx | 11 +++++++++-- src/pages/CodecovAIPage/CodecovAIPage.tsx | 4 ++-- src/services/codecovAI/useCodecovAIInstallation.tsx | 1 - 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/src/pages/CodecovAIPage/CodecovAIPage.test.tsx b/src/pages/CodecovAIPage/CodecovAIPage.test.tsx index 3e92c86f2f..596eef119e 100644 --- a/src/pages/CodecovAIPage/CodecovAIPage.test.tsx +++ b/src/pages/CodecovAIPage/CodecovAIPage.test.tsx @@ -20,7 +20,14 @@ vi.mock('shared/featureFlags', async () => { } }) -const queryClient = new QueryClient() +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + retry: false, + suspense: true, + }, + }, +}) const server = setupServer() @@ -99,7 +106,7 @@ describe('CodecovAIPage', () => { it('renders the install button', async () => { render(, { wrapper }) - const buttonEl = screen.queryByText(/Install Codecov AI/i) + const buttonEl = await screen.findByText(/Install Codecov AI/i) expect(buttonEl).toBeInTheDocument() }) diff --git a/src/pages/CodecovAIPage/CodecovAIPage.tsx b/src/pages/CodecovAIPage/CodecovAIPage.tsx index 9f42a37039..0f9cb42a0c 100644 --- a/src/pages/CodecovAIPage/CodecovAIPage.tsx +++ b/src/pages/CodecovAIPage/CodecovAIPage.tsx @@ -24,11 +24,11 @@ const CodecovAIPage: React.FC = () => { owner, provider, }) - console.log('???') + if (!codecovAiFeaturesTab) { return } - console.log(installationData) + return ( <> diff --git a/src/services/codecovAI/useCodecovAIInstallation.tsx b/src/services/codecovAI/useCodecovAIInstallation.tsx index 6be4bf4776..80e4404697 100644 --- a/src/services/codecovAI/useCodecovAIInstallation.tsx +++ b/src/services/codecovAI/useCodecovAIInstallation.tsx @@ -41,7 +41,6 @@ export function useCodecovAIInstallation({ }, }).then((res) => { const parsedRes = ResponseSchema.safeParse(res?.data) - console.log(res?.data) if (!parsedRes.success) { return Promise.reject({ status: 404, From 4403632f3d1de5962ed56cc0112970dad442bfd4 Mon Sep 17 00:00:00 2001 From: Rohit Date: Thu, 26 Sep 2024 19:43:26 -0400 Subject: [PATCH 12/15] cleanup --- src/pages/CodecovAIPage/CodecovAIPage.test.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/pages/CodecovAIPage/CodecovAIPage.test.tsx b/src/pages/CodecovAIPage/CodecovAIPage.test.tsx index 596eef119e..dccd8aaef4 100644 --- a/src/pages/CodecovAIPage/CodecovAIPage.test.tsx +++ b/src/pages/CodecovAIPage/CodecovAIPage.test.tsx @@ -1,5 +1,5 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query' -import { render, screen } from '@testing-library/react' +import { cleanup, render, screen } from '@testing-library/react' import { graphql, HttpResponse } from 'msw2' import { setupServer } from 'msw2/node' import { MemoryRouter, Route } from 'react-router-dom' @@ -50,6 +50,12 @@ afterEach(() => { server.resetHandlers() }) +afterEach(() => { + cleanup() + vi.clearAllMocks() + queryClient.clear() +}) + afterAll(() => { server.close() }) From 3b3f72473be5334d709e27d547576a3ad6b0c9c7 Mon Sep 17 00:00:00 2001 From: Rohit Date: Thu, 26 Sep 2024 20:02:29 -0400 Subject: [PATCH 13/15] update tests --- src/pages/CodecovAIPage/CodecovAIPage.test.tsx | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/pages/CodecovAIPage/CodecovAIPage.test.tsx b/src/pages/CodecovAIPage/CodecovAIPage.test.tsx index dccd8aaef4..8c95a4733b 100644 --- a/src/pages/CodecovAIPage/CodecovAIPage.test.tsx +++ b/src/pages/CodecovAIPage/CodecovAIPage.test.tsx @@ -85,6 +85,7 @@ describe('CodecovAIPage', () => { } beforeEach(() => { mocks.useFlags.mockReturnValue({ codecovAiFeaturesTab: true }) + setup() }) it('renders top section', async () => { @@ -187,15 +188,16 @@ describe('CodecovAIPage', () => { expect(repo2Link).toBeInTheDocument() }) }) -}) -describe('flag is off', () => { - it('does not render page', async () => { - mocks.useFlags.mockReturnValue({ codecovAiFeaturesTab: false }) + describe('flag is off', () => { + it('does not render page', async () => { + setup(true) + mocks.useFlags.mockReturnValue({ codecovAiFeaturesTab: false }) - render(, { wrapper }) + render(, { wrapper }) - const topSection = screen.queryByText(/Codecov AI is a/) - expect(topSection).not.toBeInTheDocument() + const topSection = screen.queryByText(/Codecov AI is a/) + expect(topSection).not.toBeInTheDocument() + }) }) }) From 9ff292154cc1a469bc5dbc4a3c7fddc9468346b1 Mon Sep 17 00:00:00 2001 From: Rohit Date: Fri, 27 Sep 2024 09:25:49 -0400 Subject: [PATCH 14/15] Update tests --- src/pages/CodecovAIPage/CodecovAIPage.test.tsx | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/src/pages/CodecovAIPage/CodecovAIPage.test.tsx b/src/pages/CodecovAIPage/CodecovAIPage.test.tsx index 8c95a4733b..5138f2d556 100644 --- a/src/pages/CodecovAIPage/CodecovAIPage.test.tsx +++ b/src/pages/CodecovAIPage/CodecovAIPage.test.tsx @@ -23,7 +23,7 @@ vi.mock('shared/featureFlags', async () => { const queryClient = new QueryClient({ defaultOptions: { queries: { - retry: false, + retry: true, suspense: true, }, }, @@ -61,7 +61,10 @@ afterAll(() => { }) describe('CodecovAIPage', () => { - function setup(aiFeaturesEnabled = false) { + function setup( + aiFeaturesEnabled = false, + aiEnabledRepos = ['repo-1', 'repo-2'] + ) { server.use( graphql.query('GetCodecovAIAppInstallInfo', (info) => { return HttpResponse.json({ @@ -76,7 +79,7 @@ describe('CodecovAIPage', () => { return HttpResponse.json({ data: { owner: { - aiEnabledRepos: ['repo-1', 'repo-2'], + aiEnabledRepos, }, }, }) @@ -187,6 +190,15 @@ describe('CodecovAIPage', () => { const repo2Link = await screen.findByText(/repo-1/) expect(repo2Link).toBeInTheDocument() }) + + describe('No repos returned', () => { + it('renders install link', async () => { + setup(true, []) + render(, { wrapper }) + const buttonEl = await screen.findByText(/Install Codecov AI/i) + expect(buttonEl).toBeInTheDocument() + }) + }) }) describe('flag is off', () => { From 3c06152b65ff8c981a607f4b8370f679a9a32938 Mon Sep 17 00:00:00 2001 From: Rohit Vinnakota <148245014+rohitvinnakota-codecov@users.noreply.github.com> Date: Fri, 27 Sep 2024 18:41:20 -0400 Subject: [PATCH 15/15] Updates from editor --- src/pages/CodecovAIPage/CodecovAIPage.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/CodecovAIPage/CodecovAIPage.test.tsx b/src/pages/CodecovAIPage/CodecovAIPage.test.tsx index 5138f2d556..ee2eb0c4f5 100644 --- a/src/pages/CodecovAIPage/CodecovAIPage.test.tsx +++ b/src/pages/CodecovAIPage/CodecovAIPage.test.tsx @@ -187,7 +187,7 @@ describe('CodecovAIPage', () => { const repo1Link = await screen.findByText(/repo-1/) expect(repo1Link).toBeInTheDocument() - const repo2Link = await screen.findByText(/repo-1/) + const repo2Link = await screen.findByText(/repo-2/) expect(repo2Link).toBeInTheDocument() })