From decf25171f24d5246d1fb37e4456e89a8db959c1 Mon Sep 17 00:00:00 2001 From: Rohan Agarwal Date: Sat, 17 May 2025 11:30:10 -0700 Subject: [PATCH 1/3] New onboarding/notice flow --- .../streamline/sidebar/seerNotices.tsx | 506 +++++++++++------- .../projectSeer/autofixRepositories.tsx | 41 +- 2 files changed, 351 insertions(+), 196 deletions(-) diff --git a/static/app/views/issueDetails/streamline/sidebar/seerNotices.tsx b/static/app/views/issueDetails/streamline/sidebar/seerNotices.tsx index ac8c087b35abbc..2e9a8c6ef839a7 100644 --- a/static/app/views/issueDetails/streamline/sidebar/seerNotices.tsx +++ b/static/app/views/issueDetails/streamline/sidebar/seerNotices.tsx @@ -1,17 +1,32 @@ import {Fragment} from 'react'; import styled from '@emotion/styled'; -import onboardingInstall from 'sentry-images/spot/onboarding-install.svg'; +import addIntegrationProvider from 'sentry-images/spot/add-integration-provider.svg'; +import feedbackOnboardingImg from 'sentry-images/spot/feedback-onboarding.svg'; +import onboardingCompass from 'sentry-images/spot/onboarding-compass.svg'; +import waitingForEventImg from 'sentry-images/spot/waiting-for-event.svg'; +import { + addErrorMessage, + addLoadingMessage, + addSuccessMessage, +} from 'sentry/actionCreators/indicator'; +import {SeerWaitingIcon} from 'sentry/components/ai/SeerIcon'; import {Alert} from 'sentry/components/core/alert'; +import {Button} from 'sentry/components/core/button'; import {LinkButton} from 'sentry/components/core/button/linkButton'; import {useProjectSeerPreferences} from 'sentry/components/events/autofix/preferences/hooks/useProjectSeerPreferences'; import {useAutofixRepos} from 'sentry/components/events/autofix/useAutofix'; +import {GuidedSteps} from 'sentry/components/guidedSteps/guidedSteps'; import ExternalLink from 'sentry/components/links/externalLink'; import {t, tct} from 'sentry/locale'; import {space} from 'sentry/styles/space'; import type {Project} from 'sentry/types/project'; +import {FieldKey} from 'sentry/utils/fields'; import useOrganization from 'sentry/utils/useOrganization'; +import {useCreateGroupSearchView} from 'sentry/views/issueList/mutations/useCreateGroupSearchView'; +import {IssueSortOptions} from 'sentry/views/issueList/utils'; +import {useStarredIssueViews} from 'sentry/views/nav/secondary/sections/issues/issueViews/useStarredIssueViews'; interface SeerNoticesProps { groupId: string; @@ -19,90 +34,6 @@ interface SeerNoticesProps { hasGithubIntegration?: boolean; } -function GithubIntegrationSetupCard() { - const organization = useOrganization(); - - return ( - - - {t('Set Up the GitHub Integration')} - - - {tct('Seer is [bold:a lot better] when it has your codebase as context.', { - bold: , - })} - - - {tct( - 'Set up the [integrationLink:GitHub Integration] to allow Seer to go deeper when troubleshooting and fixing your issues–including writing the code and opening PRs.', - { - integrationLink: ( - - ), - } - )} - - - - {t('Set Up Now')} - - - - - ); -} - -function SelectReposCard() { - const organization = useOrganization(); - - return ( - - - {t('Pick Repositories to Work In')} - - - {tct('Seer is [bold:a lot better] when it has your codebase as context.', { - bold: , - })} - - - {tct( - 'Select the repos Seer can explore in this project to allow it to go deeper when troubleshooting and fixing your issues–including writing the code and opening PRs.', - { - integrationLink: ( - - ), - } - )} - - - {t( - 'You can also configure working branches and custom instructions so Seer acts just how you like.' - )} - - - {tct( - '[bold:Open the Project Settings menu in the top right] to get started.', - { - bold: , - } - )} - - - - - - ); -} - export function SeerNotices({groupId, hasGithubIntegration, project}: SeerNoticesProps) { const organization = useOrganization(); const {repos} = useAutofixRepos(groupId); @@ -111,132 +42,341 @@ export function SeerNotices({groupId, hasGithubIntegration, project}: SeerNotice codeMappingRepos, isLoading: isLoadingPreferences, } = useProjectSeerPreferences(project); + const {starredViews: views} = useStarredIssueViews(); + const {mutate: createIssueView} = useCreateGroupSearchView({ + onMutate: () => { + addLoadingMessage(t('Creating view...')); + }, + onSuccess: () => { + addSuccessMessage(t('View starred successfully')); + }, + onError: () => { + addErrorMessage(t('Failed to create view')); + }, + }); + + const isAutomationAllowed = organization.features.includes( + 'trigger-autofix-on-issue-summary' + ); const unreadableRepos = repos.filter(repo => repo.is_readable === false); - const notices: React.JSX.Element[] = []; + const githubRepos = unreadableRepos.filter(repo => repo.provider.includes('github')); + const nonGithubRepos = unreadableRepos.filter( + repo => !repo.provider.includes('github') + ); - if (!hasGithubIntegration) { - notices.push(); - } else if ( + // Onboarding conditions + const needsGithubIntegration = !hasGithubIntegration; + const needsRepoSelection = + hasGithubIntegration && repos.length === 0 && !preference?.repositories?.length && !codeMappingRepos?.length && - !isLoadingPreferences - ) { - notices.push(); - } - - if (unreadableRepos.length > 1) { - const githubRepos = unreadableRepos.filter(repo => repo.provider.includes('github')); - const nonGithubRepos = unreadableRepos.filter( - repo => !repo.provider.includes('github') - ); - - notices.push( - - {tct("Seer can't access these repositories: [repoList].", { - repoList: {unreadableRepos.map(repo => repo.name).join(', ')}, - })} - {githubRepos.length > 0 && ( - - {' '} - {tct( - 'For best performance, enable the [integrationLink:GitHub integration].', - { - integrationLink: ( - - ), - } - )} - - )} - {nonGithubRepos.length > 0 && ( - {t('Seer currently only supports GitHub repositories.')} - )} - - ); - } else if (unreadableRepos.length === 1) { - const unreadableRepo = unreadableRepos[0]!; - notices.push( - - {unreadableRepo.provider.includes('github') - ? tct( - "Seer can't access the [repo] repository, make sure the [integrationLink:GitHub integration] is correctly set up.", - { - repo: {unreadableRepo.name}, - integrationLink: ( - - ), - } - ) - : tct( - "Seer can't access the [repo] repository. It currently only supports GitHub repositories.", - {repo: {unreadableRepo.name}} - )} - - ); - } + !isLoadingPreferences; + const needsAutomation = + project.autofixAutomationTuning === 'off' && isAutomationAllowed; + const needsFixabilityView = + !views.some(view => view.query.includes(FieldKey.ISSUE_SEER_ACTIONABILITY)) && + isAutomationAllowed; + + // Warning conditions + const hasMultipleUnreadableRepos = unreadableRepos.length > 1; + const hasSingleUnreadableRepo = unreadableRepos.length === 1; - if (notices.length === 0) { - return null; - } + const anyStepIncomplete = + needsGithubIntegration || + needsRepoSelection || + needsAutomation || + needsFixabilityView; - return {notices}; + const handleStarFixabilityView = () => { + createIssueView({ + name: 'Easy Fixes 🤖', + query: 'is:unresolved issue.seer_actionability:high', + querySort: IssueSortOptions.DATE, + projects: [], + environments: [], + timeFilters: { + start: null, + end: null, + period: '7d', + utc: null, + }, + starred: true, + }); + }; + + return ( + + {anyStepIncomplete && ( + + + + Debug Faster with Seer + + + {/* Step 1: GitHub Integration */} + + + + + + {tct( + 'Seer is [bold:a lot better] when it has your codebase as context.', + { + bold: , + } + )} + + + {tct( + 'Set up the [integrationLink:GitHub Integration] to allow Seer to find the most accurate root causes, solutions, and code changes for your issues.', + { + integrationLink: ( + + ), + } + )} + + + + + + + + + + {t('Set Up Integration')} + + + + + {/* Step 2: Repo Selection */} + + + + + {t('Select the repos Seer can explore in this project.')} + + {t( + 'You can also configure working branches and custom instructions so Seer fits your unique workflow.' + )} + + + + + + + + + + {t('Configure Repos')} + + + + + {/* Step 3: Unleash Automation */} + {isAutomationAllowed && ( + + + + + + {t( + 'Let Seer automatically deep dive into incoming issues, so you wake up to solutions, not headaches.' + )} + + + + + + + + + + {t('Enable Automation')} + + + + )} + + {/* Step 4: Fixability View */} + {isAutomationAllowed && ( + + + + + + {t( + 'Seer scans all your issues and highlights the ones that are likely quick to fix.' + )} + + + {t( + 'Star the recommended issue view to keep an eye on quick debugging opportunities. You can customize the view later.' + )} + + + + + + + + + + + + )} + + + + )} + {/* Banners for unreadable repos */} + {hasMultipleUnreadableRepos && ( + + {tct("Seer can't access these repositories: [repoList].", { + repoList: {unreadableRepos.map(repo => repo.name).join(', ')}, + })} + {githubRepos.length > 0 && ( + + {' '} + {tct( + 'For best performance, enable the [integrationLink:GitHub integration].', + { + integrationLink: ( + + ), + } + )} + + )} + {nonGithubRepos.length > 0 && ( + {t('Seer currently only supports GitHub repositories.')} + )} + + )} + {hasSingleUnreadableRepo && ( + + {unreadableRepos[0]?.provider.includes('github') + ? tct( + "Seer can't access the [repo] repository, make sure the [integrationLink:GitHub integration] is correctly set up.", + { + repo: {unreadableRepos[0]?.name}, + integrationLink: ( + + ), + } + ) + : tct( + "Seer can't access the [repo] repository. It currently only supports GitHub repositories.", + {repo: {unreadableRepos[0]?.name}} + )} + + )} + + ); } +const StyledAlert = styled(Alert)` + margin-bottom: ${space(1)}; +`; + const NoticesContainer = styled('div')` display: flex; flex-direction: column; - gap: ${space(2)}; align-items: stretch; - margin-bottom: ${space(2)}; `; -const IntegrationCard = styled('div')` - position: relative; - overflow: hidden; - border: 1px solid ${p => p.theme.border}; - border-radius: ${p => p.theme.borderRadius}; +const CardDescription = styled('div')` display: flex; - flex-direction: row; - align-items: flex-end; + flex-direction: column; gap: ${space(1)}; - background: linear-gradient( - 90deg, - ${p => p.theme.backgroundSecondary}00 0%, - ${p => p.theme.backgroundSecondary}FF 70%, - ${p => p.theme.backgroundSecondary}FF 100% - ); `; -const CardContent = styled('div')` - padding: ${space(2)}; +const CardIllustration = styled('img')` + width: 100%; + max-width: 200px; + min-width: 100px; + height: auto; + object-fit: contain; + margin-bottom: -6px; + margin-right: 10px; +`; + +const StepContentRow = styled('div')` + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + width: 100%; + gap: ${space(3)}; +`; + +const StepTextCol = styled('div')` display: flex; flex-direction: column; gap: ${space(2)}; - align-items: flex-start; + flex: 0 0 75%; + min-width: 0; `; -const CardDescription = styled('div')` +const StepImageCol = styled('div')` display: flex; - flex-direction: column; - gap: ${space(1)}; + align-items: flex-end; + justify-content: flex-end; + flex-grow: 1; `; -const CardTitle = styled('h3')` - font-size: ${p => p.theme.fontSizeLarge}; - font-weight: 600; - margin-bottom: 0; +const StepsHeader = styled('h3')` + display: flex; + align-items: center; + gap: ${space(1)}; + font-size: ${p => p.theme.fontSizeExtraLarge}; + margin-bottom: ${space(0.5)}; `; -const CardIllustration = styled('img')` - height: 100%; - object-fit: contain; - max-width: 30%; - margin-bottom: -6px; - margin-right: 10px; +const StepsDivider = styled('hr')` + border: none; + border-top: 1px solid ${p => p.theme.border}; + margin: ${space(3)} 0; `; diff --git a/static/app/views/settings/projectSeer/autofixRepositories.tsx b/static/app/views/settings/projectSeer/autofixRepositories.tsx index 575b1f065118d6..19d6fee424086f 100644 --- a/static/app/views/settings/projectSeer/autofixRepositories.tsx +++ b/static/app/views/settings/projectSeer/autofixRepositories.tsx @@ -5,18 +5,21 @@ import {openModal} from 'sentry/actionCreators/modal'; import {Flex} from 'sentry/components/container/flex'; import {Alert} from 'sentry/components/core/alert'; import {Button} from 'sentry/components/core/button'; +import {Tooltip} from 'sentry/components/core/tooltip'; import {useOrganizationRepositories} from 'sentry/components/events/autofix/preferences/hooks/useOrganizationRepositories'; import {useProjectSeerPreferences} from 'sentry/components/events/autofix/preferences/hooks/useProjectSeerPreferences'; import {useUpdateProjectSeerPreferences} from 'sentry/components/events/autofix/preferences/hooks/useUpdateProjectSeerPreferences'; import type {RepoSettings} from 'sentry/components/events/autofix/types'; +import Link from 'sentry/components/links/link'; import LoadingIndicator from 'sentry/components/loadingIndicator'; import Panel from 'sentry/components/panels/panel'; import PanelHeader from 'sentry/components/panels/panelHeader'; import QuestionTooltip from 'sentry/components/questionTooltip'; import {IconAdd} from 'sentry/icons'; -import {t} from 'sentry/locale'; +import {t, tct} from 'sentry/locale'; import {space} from 'sentry/styles/space'; import type {Project} from 'sentry/types/project'; +import useOrganization from 'sentry/utils/useOrganization'; import {AddAutofixRepoModalContent} from './addAutofixRepoModal'; import {AutofixRepoItem} from './autofixRepoItem'; @@ -27,6 +30,7 @@ interface ProjectSeerProps { } export function AutofixRepositories({project}: ProjectSeerProps) { + const organization = useOrganization(); const {data: repositories, isFetching: isFetchingRepositories} = useOrganizationRepositories(); const { @@ -195,7 +199,7 @@ export function AutofixRepositories({project}: ProjectSeerProps) { - {t('Autofix Repositories')} + {t('Seer Repositories')}
- + +
{showSaveNotice && ( {t( - 'Changes will apply on the next Seer run or hit "Start Over" in the Seer panel to start a new run and use your changes.' + 'Changes will apply on future Seer runs. Hit "Start Over" in the Seer panel to start a new run and use your new selected repositories.' )} )} From ae888af5457887a249e1885eba2f5f4b8490b54c Mon Sep 17 00:00:00 2001 From: Rohan Agarwal Date: Sat, 17 May 2025 12:14:23 -0700 Subject: [PATCH 2/3] Fix tests --- static/app/types/project.tsx | 2 +- .../streamline/sidebar/seerDrawer.spec.tsx | 8 +- .../streamline/sidebar/seerNotices.spec.tsx | 294 ++++-------------- 3 files changed, 74 insertions(+), 230 deletions(-) diff --git a/static/app/types/project.tsx b/static/app/types/project.tsx index 6b606f32f76a13..16f277951e7f31 100644 --- a/static/app/types/project.tsx +++ b/static/app/types/project.tsx @@ -64,7 +64,7 @@ export type Project = { team: Team; teams: Team[]; verifySSL: boolean; - autofixAutomationTuning?: 'off' | 'low' | 'medium' | 'high'; + autofixAutomationTuning?: 'off' | 'low' | 'medium' | 'high' | 'always'; builtinSymbolSources?: string[]; defaultEnvironment?: string; eventProcessing?: { diff --git a/static/app/views/issueDetails/streamline/sidebar/seerDrawer.spec.tsx b/static/app/views/issueDetails/streamline/sidebar/seerDrawer.spec.tsx index 344614df55982e..1c59e509fc36b3 100644 --- a/static/app/views/issueDetails/streamline/sidebar/seerDrawer.spec.tsx +++ b/static/app/views/issueDetails/streamline/sidebar/seerDrawer.spec.tsx @@ -132,6 +132,10 @@ describe('SeerDrawer', () => { preference: null, }, }); + MockApiClient.addMockResponse({ + url: `/organizations/${mockProject.organization.slug}/group-search-views/starred/`, + body: [], + }); }); it('renders consent state if not consented', async () => { @@ -217,7 +221,7 @@ describe('SeerDrawer', () => { ); expect(screen.getByText('Set Up the GitHub Integration')).toBeInTheDocument(); - expect(screen.getByText('Set Up Now')).toBeInTheDocument(); + expect(screen.getByText('Set Up Integration')).toBeInTheDocument(); const startButton = screen.getByRole('button', {name: 'Start Seer'}); expect(startButton).toBeInTheDocument(); @@ -486,7 +490,7 @@ describe('SeerDrawer', () => { // Since "Install the GitHub Integration" text isn't found, let's check for // the "Set Up the GitHub Integration" text which is what the component is actually showing expect(screen.getByText('Set Up the GitHub Integration')).toBeInTheDocument(); - expect(screen.getByText('Set Up Now')).toBeInTheDocument(); + expect(screen.getByText('Set Up Integration')).toBeInTheDocument(); }); it('does not render SeerNotices when all repositories are readable', async () => { diff --git a/static/app/views/issueDetails/streamline/sidebar/seerNotices.spec.tsx b/static/app/views/issueDetails/streamline/sidebar/seerNotices.spec.tsx index 9f7b91463cfa86..36830e6b864c0f 100644 --- a/static/app/views/issueDetails/streamline/sidebar/seerNotices.spec.tsx +++ b/static/app/views/issueDetails/streamline/sidebar/seerNotices.spec.tsx @@ -1,14 +1,12 @@ +import {GroupSearchViewFixture} from 'sentry-fixture/groupSearchView'; +import {OrganizationFixture} from 'sentry-fixture/organization'; import {ProjectFixture} from 'sentry-fixture/project'; -import {render, screen, waitFor} from 'sentry-test/reactTestingLibrary'; +import {render, screen} from 'sentry-test/reactTestingLibrary'; -import {useAutofixRepos} from 'sentry/components/events/autofix/useAutofix'; import {SeerNotices} from 'sentry/views/issueDetails/streamline/sidebar/seerNotices'; -jest.mock('sentry/components/events/autofix/useAutofix'); - describe('SeerNotices', function () { - // Helper function to create repository objects const createRepository = (overrides = {}) => ({ external_id: 'repo-123', name: 'org/repo', @@ -20,251 +18,93 @@ describe('SeerNotices', function () { ...overrides, }); - const project = ProjectFixture(); + function getProjectWithAutomation( + automationTuning = 'off' as 'off' | 'low' | 'medium' | 'high' | 'always' + ) { + return { + ...ProjectFixture(), + autofixAutomationTuning: automationTuning, + organization: { + ...ProjectFixture().organization, + features: ['trigger-autofix-on-issue-summary'], + }, + }; + } + + const organization = OrganizationFixture({ + features: ['trigger-autofix-on-issue-summary'], + }); beforeEach(() => { + MockApiClient.clearMockResponses(); MockApiClient.addMockResponse({ - url: `/projects/${project.organization.slug}/${project.slug}/seer/preferences/`, + url: `/projects/${organization.slug}/${ProjectFixture().slug}/seer/preferences/`, body: { code_mapping_repos: [], preference: null, }, }); - - // Reset mock before each test - jest.mocked(useAutofixRepos).mockReset(); - }); - - it('renders nothing when all repositories are readable', function () { - const repositories = [createRepository(), createRepository({name: 'org/repo2'})]; - jest.mocked(useAutofixRepos).mockReturnValue({ - repos: repositories, - codebases: {}, - }); - - const {container} = render( - - ); - - expect(container).toBeEmptyDOMElement(); - }); - - it('renders GitHub integration setup card when hasGithubIntegration is false', function () { - jest.mocked(useAutofixRepos).mockReturnValue({ - repos: [createRepository()], - codebases: {}, - }); - - render(); - - expect(screen.getByText('Set Up the GitHub Integration')).toBeInTheDocument(); - - // Test for text fragments with formatting - expect(screen.getByText(/Seer is/, {exact: false})).toBeInTheDocument(); - expect(screen.getByText('a lot better')).toBeInTheDocument(); - expect( - screen.getByText(/when it has your codebase as context/, {exact: false}) - ).toBeInTheDocument(); - - // Test for text with links - expect(screen.getByText(/Set up the/, {exact: false})).toBeInTheDocument(); - expect(screen.getByText('GitHub Integration', {selector: 'a'})).toBeInTheDocument(); - expect( - screen.getByText(/to allow Seer to go deeper/, {exact: false}) - ).toBeInTheDocument(); - - expect(screen.getByText('Set Up Now')).toBeInTheDocument(); - expect(screen.getByRole('img', {name: 'Install'})).toBeInTheDocument(); - }); - - it('renders warning for a single unreadable GitHub repository', function () { - const repositories = [createRepository({is_readable: false})]; - jest.mocked(useAutofixRepos).mockReturnValue({ - repos: repositories, - codebases: {}, - }); - - render(); - - expect(screen.getByText(/Seer can't access the/)).toBeInTheDocument(); - expect(screen.getByText('org/repo')).toBeInTheDocument(); - expect(screen.getByText(/GitHub integration/)).toBeInTheDocument(); - }); - - it('renders warning for a single unreadable non-GitHub repository', function () { - const repositories = [ - createRepository({is_readable: false, provider: 'gitlab', name: 'org/gitlab-repo'}), - ]; - jest.mocked(useAutofixRepos).mockReturnValue({ - repos: repositories, - codebases: {}, - }); - - render(); - - expect(screen.getByText(/Seer can't access the/)).toBeInTheDocument(); - expect(screen.getByText('org/gitlab-repo')).toBeInTheDocument(); - expect( - screen.getByText(/It currently only supports GitHub repositories/) - ).toBeInTheDocument(); - }); - - it('renders warning for multiple unreadable repositories (all GitHub)', function () { - const repositories = [ - createRepository({is_readable: false, name: 'org/repo1'}), - createRepository({is_readable: false, name: 'org/repo2'}), - ]; - jest.mocked(useAutofixRepos).mockReturnValue({ - repos: repositories, - codebases: {}, - }); - - render(); - - expect(screen.getByText(/Seer can't access these repositories:/)).toBeInTheDocument(); - expect(screen.getByText('org/repo1, org/repo2')).toBeInTheDocument(); - expect(screen.getByText(/For best performance, enable the/)).toBeInTheDocument(); - expect(screen.getByText(/GitHub integration/)).toBeInTheDocument(); - }); - - it('renders warning for multiple unreadable repositories (all non-GitHub)', function () { - const repositories = [ - createRepository({ - is_readable: false, - provider: 'gitlab', - name: 'org/gitlab-repo1', - }), - createRepository({ - is_readable: false, - provider: 'bitbucket', - name: 'org/bitbucket-repo2', - }), - ]; - jest.mocked(useAutofixRepos).mockReturnValue({ - repos: repositories, - codebases: {}, + MockApiClient.addMockResponse({ + url: `/organizations/${organization.slug}/group-search-views/starred/`, + body: [], }); - - render(); - - expect(screen.getByText(/Seer can't access these repositories:/)).toBeInTheDocument(); - expect(screen.getByText('org/gitlab-repo1, org/bitbucket-repo2')).toBeInTheDocument(); - expect( - screen.getByText(/Seer currently only supports GitHub repositories/) - ).toBeInTheDocument(); - }); - - it('renders warning for multiple unreadable repositories (mixed GitHub and non-GitHub)', function () { - const repositories = [ - createRepository({is_readable: false, name: 'org/github-repo'}), - createRepository({is_readable: false, provider: 'gitlab', name: 'org/gitlab-repo'}), - ]; - jest.mocked(useAutofixRepos).mockReturnValue({ - repos: repositories, - codebases: {}, + MockApiClient.addMockResponse({ + url: `/projects/${organization.slug}/${ProjectFixture().slug}/autofix-repos/`, + body: [createRepository()], }); - - render(); - - expect(screen.getByText(/Seer can't access these repositories:/)).toBeInTheDocument(); - expect(screen.getByText('org/github-repo, org/gitlab-repo')).toBeInTheDocument(); - expect(screen.getByText(/For best performance, enable the/)).toBeInTheDocument(); - expect(screen.getByText(/GitHub integration/)).toBeInTheDocument(); - expect( - screen.getByText(/Seer currently only supports GitHub repositories/) - ).toBeInTheDocument(); }); - it('renders warning for unreadable repositories along with GitHub setup card when no GitHub integration', function () { - const repositories = [ - createRepository({is_readable: false, name: 'org/repo1'}), - createRepository({is_readable: false, name: 'org/repo2'}), - ]; - jest.mocked(useAutofixRepos).mockReturnValue({ - repos: repositories, - codebases: {}, + it('shows automation step if automation is allowed and tuning is off', () => { + const project = getProjectWithAutomation('off'); + render(, { + organization, }); - - render(); - - // GitHub setup card - expect(screen.getByText('Set Up the GitHub Integration')).toBeInTheDocument(); - expect(screen.getByText('Set Up Now')).toBeInTheDocument(); - - // Unreadable repos warning - expect(screen.getByText(/Seer can't access these repositories:/)).toBeInTheDocument(); - expect(screen.getByText('org/repo1, org/repo2')).toBeInTheDocument(); + expect(screen.getByText('Unleash Automation')).toBeInTheDocument(); + expect(screen.getByText('Enable Automation')).toBeInTheDocument(); }); - it('renders GitHub integration link correctly', function () { - const repositories = [createRepository({is_readable: false, name: 'org/repo1'})]; - jest.mocked(useAutofixRepos).mockReturnValue({ - repos: repositories, - codebases: {}, + it('does not show automation step if automation is not allowed', () => { + const project = { + ...ProjectFixture(), + autofixAutomationTuning: 'off' as const, + organization: { + ...ProjectFixture().organization, + features: [], + }, + }; + render(, { + organization: {...organization, features: []}, }); - - render(); - - const integrationLink = screen.getByText('GitHub integration'); - expect(integrationLink).toHaveAttribute( - 'href', - '/settings/org-slug/integrations/github/' - ); + expect(screen.queryByText('Unleash Automation')).not.toBeInTheDocument(); }); - it('combines multiple notices when necessary', function () { - const repositories = [ - createRepository({is_readable: false, name: 'org/repo1'}), - createRepository({is_readable: false, name: 'org/repo2'}), - ]; - jest.mocked(useAutofixRepos).mockReturnValue({ - repos: repositories, - codebases: {}, + it('shows fixability view step if automation is allowed and view not starred', () => { + const project = getProjectWithAutomation('on' as any); + render(, { + organization, }); - - render(); - - // Should have both the GitHub setup card and the unreadable repos warning - const setupCard = screen.getByText('Set Up the GitHub Integration').closest('div'); - const warningAlert = screen - .getByText(/Seer can't access these repositories:/) - .closest('div'); - - expect(setupCard).toBeInTheDocument(); - expect(warningAlert).toBeInTheDocument(); - expect(setupCard).not.toBe(warningAlert); + expect(screen.getByText('Get Some Quick Wins')).toBeInTheDocument(); + expect(screen.getByText('Star Recommended View')).toBeInTheDocument(); }); - it('renders repository selection card when no repos are selected but GitHub integration is enabled', async function () { - jest.mocked(useAutofixRepos).mockReturnValue({ - repos: [], - codebases: {}, - }); - + it('does not render guided steps if all onboarding steps are complete', () => { MockApiClient.addMockResponse({ - url: `/projects/${project.organization.slug}/${project.slug}/seer/preferences/`, - body: { - code_mapping_repos: null, - preference: null, - }, + url: `/organizations/${organization.slug}/group-search-views/starred/`, + body: [ + GroupSearchViewFixture({ + query: 'is:unresolved issue.seer_actionability:high', + starred: true, + }), + ], }); - - render(); - - await waitFor(() => { - expect(screen.getByText('Pick Repositories to Work In')).toBeInTheDocument(); + const project = getProjectWithAutomation('medium'); + render(, { + ...{organization: {...organization, features: []}}, }); - - const titleElement = screen.getByText('Pick Repositories to Work In'); - const cardDescriptionElement = titleElement.nextElementSibling; - const firstSpanInDescription = - cardDescriptionElement?.querySelector('span:first-child'); - expect(firstSpanInDescription?.textContent?.replace(/\s+/g, ' ').trim()).toBe( - 'Seer is a lot better when it has your codebase as context.' - ); - - expect( - screen.getByText(/Open the Project Settings menu in the top right/) - ).toBeInTheDocument(); + // Should not find any step titles + expect(screen.queryByText('Set Up the GitHub Integration')).not.toBeInTheDocument(); + expect(screen.queryByText('Pick Repositories to Work In')).not.toBeInTheDocument(); + expect(screen.queryByText('Unleash Automation')).not.toBeInTheDocument(); + expect(screen.queryByText('Get Some Quick Wins')).not.toBeInTheDocument(); }); }); From 759faf31d1b0a446a6d6dbbea42b50d051d8517d Mon Sep 17 00:00:00 2001 From: Rohan Agarwal Date: Sat, 17 May 2025 12:14:59 -0700 Subject: [PATCH 3/3] Fix test --- .../views/issueDetails/streamline/sidebar/seerNotices.spec.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/app/views/issueDetails/streamline/sidebar/seerNotices.spec.tsx b/static/app/views/issueDetails/streamline/sidebar/seerNotices.spec.tsx index 36830e6b864c0f..e5e3fe6b742e7c 100644 --- a/static/app/views/issueDetails/streamline/sidebar/seerNotices.spec.tsx +++ b/static/app/views/issueDetails/streamline/sidebar/seerNotices.spec.tsx @@ -79,7 +79,7 @@ describe('SeerNotices', function () { }); it('shows fixability view step if automation is allowed and view not starred', () => { - const project = getProjectWithAutomation('on' as any); + const project = getProjectWithAutomation('high'); render(, { organization, });