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
54 changes: 50 additions & 4 deletions static/app/components/onboarding/onboardingContext.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,26 @@
import {createContext, useContext, useMemo} from 'react';

import type {ProductSolution} from 'sentry/components/onboarding/gettingStartedDoc/types';
import type {Integration, Repository} from 'sentry/types/integrations';
import type {OnboardingSelectedSDK} from 'sentry/types/onboarding';
import {useSessionStorage} from 'sentry/utils/useSessionStorage';

type OnboardingContextProps = {
setSelectedFeatures: (features?: ProductSolution[]) => void;
setSelectedIntegration: (integration?: Integration) => void;
setSelectedPlatform: (selectedSDK?: OnboardingSelectedSDK) => void;
setSelectedRepositories: (repos?: Repository[]) => void;
selectedFeatures?: ProductSolution[];
selectedIntegration?: Integration;
selectedPlatform?: OnboardingSelectedSDK;
selectedRepositories?: Repository[];
};

type OnboardingSessionState = {
selectedFeatures?: ProductSolution[];
selectedIntegration?: Integration;
selectedPlatform?: OnboardingSelectedSDK;
selectedRepositories?: Repository[];
};

/**
Expand All @@ -14,6 +29,12 @@ type OnboardingContextProps = {
const OnboardingContext = createContext<OnboardingContextProps>({
selectedPlatform: undefined,
setSelectedPlatform: () => {},
selectedIntegration: undefined,
setSelectedIntegration: () => {},
selectedRepositories: undefined,
setSelectedRepositories: () => {},
selectedFeatures: undefined,
setSelectedFeatures: () => {},
});

type ProviderProps = {
Expand All @@ -26,7 +47,7 @@ type ProviderProps = {

export function OnboardingContextProvider({children, value}: ProviderProps) {
const [onboarding, setOnboarding, removeOnboarding] = useSessionStorage<
NonNullable<Pick<OnboardingContextProps, 'selectedPlatform'>> | undefined
OnboardingSessionState | undefined
>(
'onboarding',
value?.selectedPlatform ? {selectedPlatform: value.selectedPlatform} : undefined
Expand All @@ -36,13 +57,38 @@ export function OnboardingContextProvider({children, value}: ProviderProps) {
() => ({
selectedPlatform: onboarding?.selectedPlatform,
setSelectedPlatform: (selectedPlatform?: OnboardingSelectedSDK) => {
// If platform is undefined, remove the item from session storage
if (selectedPlatform === undefined) {
removeOnboarding();
// Clear platform but preserve other SCM state (integration, repos, features).
// Full reset only happens if no other state remains.
const nextState = {
...onboarding,
selectedPlatform: undefined,
};
const hasOtherState =
nextState.selectedIntegration ||
nextState.selectedRepositories ||
nextState.selectedFeatures;
if (hasOtherState) {
setOnboarding(nextState);
} else {
removeOnboarding();
}
} else {
setOnboarding({selectedPlatform});
setOnboarding({...onboarding, selectedPlatform});
}
},
selectedIntegration: onboarding?.selectedIntegration,
setSelectedIntegration: (selectedIntegration?: Integration) => {
setOnboarding({...onboarding, selectedIntegration});
},
selectedRepositories: onboarding?.selectedRepositories,
setSelectedRepositories: (selectedRepositories?: Repository[]) => {
setOnboarding({...onboarding, selectedRepositories});
},
selectedFeatures: onboarding?.selectedFeatures,
setSelectedFeatures: (selectedFeatures?: ProductSolution[]) => {
setOnboarding({...onboarding, selectedFeatures});
},
}),
[onboarding, setOnboarding, removeOnboarding]
);
Expand Down
173 changes: 173 additions & 0 deletions static/app/views/onboarding/onboarding.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -549,6 +549,179 @@ describe('Onboarding', () => {
).not.toBeInTheDocument();
});

describe('SCM onboarding flow', () => {
const scmOrganization = OrganizationFixture({
features: ['onboarding-scm'],
});

function renderOnboarding(step: string) {
return render(
<OnboardingContextProvider>
<OnboardingWithoutContext />
</OnboardingContextProvider>,
{
organization: scmOrganization,
initialRouterConfig: {
location: {
pathname: `/onboarding/${scmOrganization.slug}/${step}/`,
},
route: '/onboarding/:orgId/:step/',
},
}
);
}

it('navigates from welcome to scm-connect', async () => {
const {router} = renderOnboarding('welcome');

await userEvent.click(screen.getByTestId('onboarding-welcome-start'));

await waitFor(() => {
expect(router.location.pathname).toBe(
`/onboarding/${scmOrganization.slug}/scm-connect/`
);
});
});

it('renders scm-connect step and advances to scm-platform-features', async () => {
const {router} = renderOnboarding('scm-connect');

expect(screen.getByText('Connect your repository')).toBeInTheDocument();

await userEvent.click(screen.getByRole('button', {name: 'Continue'}));

await waitFor(() => {
expect(router.location.pathname).toBe(
`/onboarding/${scmOrganization.slug}/scm-platform-features/`
);
});
});

it('renders scm-platform-features step and advances to scm-project-details', async () => {
const {router} = renderOnboarding('scm-platform-features');

expect(screen.getByText('Platform & features')).toBeInTheDocument();

await userEvent.click(screen.getByRole('button', {name: 'Continue'}));

await waitFor(() => {
expect(router.location.pathname).toBe(
`/onboarding/${scmOrganization.slug}/scm-project-details/`
);
});
});

it('renders scm-project-details step and advances to setup-docs when platform is set', async () => {
const nextJsProject = ProjectFixture({
platform: 'javascript-nextjs',
id: '2',
slug: 'javascript-nextjs',
});

MockApiClient.addMockResponse({
url: `/organizations/${scmOrganization.slug}/sdks/`,
body: {},
});
MockApiClient.addMockResponse({
url: `/projects/${scmOrganization.slug}/${nextJsProject.slug}/keys/`,
method: 'GET',
body: [ProjectKeysFixture()[0]],
});
MockApiClient.addMockResponse({
url: `/projects/${scmOrganization.slug}/${nextJsProject.slug}/issues/`,
body: [],
});

jest
.spyOn(useRecentCreatedProjectHook, 'useRecentCreatedProject')
.mockImplementation(() => ({
project: nextJsProject,
isProjectActive: false,
}));

const {router} = render(
<OnboardingContextProvider
value={{
selectedPlatform: {
key: nextJsProject.slug as PlatformKey,
type: 'framework',
language: 'javascript',
category: 'browser',
name: 'Next.js',
link: 'https://docs.sentry.io/platforms/javascript/guides/nextjs/',
},
}}
>
<OnboardingWithoutContext />
</OnboardingContextProvider>,
{
organization: scmOrganization,
initialRouterConfig: {
location: {
pathname: `/onboarding/${scmOrganization.slug}/scm-project-details/`,
},
route: '/onboarding/:orgId/:step/',
},
}
);

expect(screen.getByText('Project details')).toBeInTheDocument();

await userEvent.click(screen.getByRole('button', {name: 'Continue'}));

await waitFor(() => {
expect(router.location.pathname).toBe(
`/onboarding/${scmOrganization.slug}/setup-docs/`
);
});
});

it('does not advance to setup-docs without a platform selected', async () => {
const {router} = renderOnboarding('scm-project-details');

await userEvent.click(screen.getByRole('button', {name: 'Continue'}));

// Should stay on the same step
expect(router.location.pathname).toBe(
`/onboarding/${scmOrganization.slug}/scm-project-details/`
);
});

it('navigates back from scm-connect to welcome', async () => {
const {router} = renderOnboarding('scm-connect');

await userEvent.click(screen.getByRole('button', {name: 'Back'}));

await waitFor(() => {
expect(router.location.pathname).toBe(
`/onboarding/${scmOrganization.slug}/welcome/`
);
});
});

it('redirects invalid step to welcome', () => {
const {router} = render(
<OnboardingContextProvider>
<OnboardingWithoutContext />
</OnboardingContextProvider>,
{
organization: scmOrganization,
initialRouterConfig: {
location: {
pathname: `/onboarding/${scmOrganization.slug}/select-platform/`,
},
route: '/onboarding/:orgId/:step/',
},
}
);

// select-platform doesn't exist in SCM flow, should redirect to welcome
expect(router.location.pathname).toBe(
`/onboarding/${scmOrganization.slug}/welcome/`
);
});
});

it('loads doc on platform click', async () => {
const organization = OrganizationFixture();
const nextJsProject = ProjectFixture({
Expand Down
Loading
Loading