diff --git a/src/components/tutorials-sidebar/components/tutorial-view-sidebar-content/__tests__/tutorial-view-sidebar-content.test.tsx b/src/components/tutorials-sidebar/components/tutorial-view-sidebar-content/__tests__/tutorial-view-sidebar-content.test.tsx
new file mode 100644
index 0000000000..80350f14fa
--- /dev/null
+++ b/src/components/tutorials-sidebar/components/tutorial-view-sidebar-content/__tests__/tutorial-view-sidebar-content.test.tsx
@@ -0,0 +1,160 @@
+import { vi } from 'vitest'
+
+// Mock hooks to avoid provider/context requirements and network
+vi.mock('hooks/progress', () => ({
+ useCollectionProgress: () => ({ data: {}, isLoading: false }),
+ useTutorialProgress: () => ({ tutorialProgressStatus: 'not-started' }),
+}))
+
+// Mock child components to simple renderers
+vi.mock('components/collection-progress-group', () => ({
+ CollectionProgressStatusSection: ({ completedTutorialCount, tutorialCount, isInProgress }) => (
+
+ Progress: {completedTutorialCount}/{tutorialCount} {isInProgress ? '(in progress)' : ''}
+
+ ),
+ parseCollectionProgress: () => ({ completedTutorialCount: 0, tutorialCount: 0, isInProgress: false }),
+}))
+
+vi.mock('components/tutorial-progress-icon', () => ({
+ default: () => ,
+}))
+
+vi.mock('components/tutorials-sidebar', () => ({
+ SectionList: ({ children }) => {children}
,
+}))
+
+vi.mock('components/sidebar/components', () => ({
+ SidebarNavMenuItem: ({ item }) => (
+
+ {item.title}
+
+ ),
+}))
+
+import { render, screen } from '@testing-library/react'
+import { describe, it, expect } from 'vitest'
+import { ErrorBoundary } from 'components/error-boundary'
+import TutorialViewSidebarContent from '../index'
+import type { Collection } from 'lib/learn-client/types'
+import type { TutorialListItemProps } from 'components/tutorials-sidebar/types'
+import userEvent from '@testing-library/user-event'
+
+describe('TutorialViewSidebarContent', () => {
+ it('renders with data and correct href', () => {
+ const mockCollection = {
+ id: 'collection-id',
+ slug: 'collection-slug',
+ tutorials: [{ id: 'tutorial-id', title: 'Tutorial Title' }],
+ } as unknown as Collection
+
+ const mockItems: TutorialListItemProps[] = [
+ {
+ text: 'Tutorial Title',
+ href: '/tutorial',
+ isActive: false,
+ tutorialId: 'tutorial-id',
+ collectionId: 'collection-id',
+ },
+ ]
+
+ render()
+
+ expect(screen.getByText('Tutorial Title')).toBeInTheDocument()
+ const link = screen.getByText('Tutorial Title').closest('a')
+ expect(link).toHaveAttribute('href', '/tutorial')
+ })
+
+ it('handles loading state gracefully (no crash, container renders)', () => {
+ const mockCollection = {} as unknown as Collection // Simulate loading/missing data
+ const mockItems: TutorialListItemProps[] = []
+
+ render(
+
+
+
+ )
+
+ expect(screen.queryByText('Tutorial Title')).not.toBeInTheDocument()
+ expect(screen.getByTestId('section-list')).toBeInTheDocument()
+ })
+
+ it('handles rapid navigation (re-render) without errors', async () => {
+ const mockCollection1 = {
+ id: 'collection-1',
+ slug: 'collection-1-slug',
+ tutorials: [{ id: 'tutorial-1', title: 'Tutorial 1' }],
+ } as unknown as Collection
+
+ const mockCollection2 = {
+ id: 'collection-2',
+ slug: 'collection-2-slug',
+ tutorials: [{ id: 'tutorial-2', title: 'Tutorial 2' }],
+ } as unknown as Collection
+
+ const mockItems1: TutorialListItemProps[] = [
+ {
+ text: 'Tutorial 1',
+ href: '/tutorial-1',
+ isActive: false,
+ tutorialId: 'tutorial-1',
+ collectionId: 'collection-1',
+ },
+ ]
+
+ const mockItems2: TutorialListItemProps[] = [
+ {
+ text: 'Tutorial 2',
+ href: '/tutorial-2',
+ isActive: false,
+ tutorialId: 'tutorial-2',
+ collectionId: 'collection-2',
+ },
+ ]
+
+ const { rerender } = render(
+
+
+
+ )
+
+ expect(screen.getByText('Tutorial 1')).toBeInTheDocument()
+
+ rerender(
+
+
+
+ )
+
+ expect(screen.getByText('Tutorial 2')).toBeInTheDocument()
+ expect(screen.queryByText('Tutorial 1')).not.toBeInTheDocument()
+ })
+
+ it('navigates correctly when clicking a sidebar item (no error)', async () => {
+ const mockCollection = {
+ id: 'collection-id',
+ slug: 'collection-slug',
+ tutorials: [{ id: 'tutorial-id', title: 'Tutorial Title' }],
+ } as unknown as Collection
+
+ const mockItems: TutorialListItemProps[] = [
+ {
+ text: 'Tutorial Title',
+ href: '/tutorial',
+ isActive: false,
+ tutorialId: 'tutorial-id',
+ collectionId: 'collection-id',
+ },
+ ]
+
+ render()
+
+ const linkEl = screen.getByText('Tutorial Title')
+ await userEvent.click(linkEl)
+
+ expect(screen.getByText('Tutorial Title')).toBeInTheDocument()
+ expect(screen.queryByText(/Something went wrong/)).not.toBeInTheDocument()
+ const link = linkEl.closest('a')
+ expect(link).toHaveAttribute('href', '/tutorial')
+ })
+})
diff --git a/src/components/tutorials-sidebar/components/tutorial-view-sidebar-content/index.tsx b/src/components/tutorials-sidebar/components/tutorial-view-sidebar-content/index.tsx
index d70376f4d6..74f5d38cab 100644
--- a/src/components/tutorials-sidebar/components/tutorial-view-sidebar-content/index.tsx
+++ b/src/components/tutorials-sidebar/components/tutorial-view-sidebar-content/index.tsx
@@ -15,11 +15,13 @@ import {
import TutorialProgressIcon from 'components/tutorial-progress-icon'
import { SidebarNavMenuItem } from 'components/sidebar/components'
import { SectionList } from 'components/tutorials-sidebar'
+import { ErrorBoundary } from 'components/error-boundary'
// Types
import { TutorialListItemProps } from 'components/tutorials-sidebar/types'
// Styles
import s from './tutorial-view-sidebar-content.module.css'
import { Collection } from 'lib/learn-client/types'
+import React from 'react'
/**
* Renders sidebar content for tutorial views.
@@ -56,30 +58,44 @@ function TutorialViewSidebarContent({
* Displays collection progress status.
*/
function CollectionProgress({ collection }: { collection: Collection }) {
- const { id, slug, tutorials } = collection
+ const { id, slug, tutorials } = collection ?? {}
/**
* Get collection progress, which affects the
* CTA bar we display for the collection.
*/
- const { data: progressData } = useCollectionProgress({ collectionId: id })
+ const { data: progressData, isLoading } = useCollectionProgress({
+ collectionId: id,
+ })
/**
* Parse the progress-related information we need from the progress records,
* current collection slug, and list of tutorials in this collection.
*/
- const { completedTutorialCount, tutorialCount, isInProgress } = useMemo(
- () => parseCollectionProgress(progressData, tutorials.length, { id, slug }),
- [progressData, tutorials, id, slug]
- )
+ const { completedTutorialCount, tutorialCount, isInProgress } =
+ useMemo(() => {
+ if (!collection || !id || !tutorials) {
+ return {
+ completedTutorialCount: 0,
+ tutorialCount: 0,
+ isInProgress: false,
+ }
+ }
+ return parseCollectionProgress(progressData, tutorials.length, {
+ id,
+ slug,
+ })
+ }, [progressData, tutorials, id, slug, collection])
return (
-
+ {!isLoading && collection && id && tutorials && (
+
+ )}
)
}
@@ -96,19 +112,35 @@ function TutorialListItem({
collectionId,
}: TutorialListItemProps) {
/**
- * Query for progress, and display the appropriate status icon
+ * Query for progress if we have valid IDs
*/
const { tutorialProgressStatus } = useTutorialProgress({
tutorialId,
collectionId,
})
- const trailingIcon = (
-
- )
+
+ /**
+ * Only show the progress icon if we have valid IDs
+ */
+ const trailingIcon =
+ tutorialId && collectionId ? (
+
+ ) : null
return (
)
}
-export default TutorialViewSidebarContent
+function TutorialViewSidebarContentWithBoundary(props) {
+ return (
+
+
+
+ )
+}
+
+export default TutorialViewSidebarContentWithBoundary