diff --git a/CHANGELOG.md b/CHANGELOG.md index 19358afd3..bdaf89492 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Send anonymous server-side PostHog events as personless so unauthenticated requests don't inflate person counts. [#1367](https://github.com/sourcebot-dev/sourcebot/pull/1367) - [EE] Fixed Ask Sourcebot mermaid diagrams overflowing their container by contain-fitting them to both width and height, and made revealing a diagram from the answer jump it into view instantly to avoid over/undershooting. [#1373](https://github.com/sourcebot-dev/sourcebot/pull/1373) - Passed Zoekt index parameters via argv to preserve revision names with punctuation. [#1376](https://github.com/sourcebot-dev/sourcebot/pull/1376) +- Show a clearer missing-file state in the code browser when a blob path no longer exists. [#1381](https://github.com/sourcebot-dev/sourcebot/pull/1381) ## [5.0.4] - 2026-06-18 diff --git a/packages/web/src/app/(app)/browse/[...path]/components/codePreviewPanel/codePreviewPanel.test.tsx b/packages/web/src/app/(app)/browse/[...path]/components/codePreviewPanel/codePreviewPanel.test.tsx new file mode 100644 index 000000000..28f185c6a --- /dev/null +++ b/packages/web/src/app/(app)/browse/[...path]/components/codePreviewPanel/codePreviewPanel.test.tsx @@ -0,0 +1,134 @@ +import { cleanup, render, screen } from '@testing-library/react'; +import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'; +import { TooltipProvider } from '@/components/ui/tooltip'; +import { ErrorCode } from '@/lib/errorCodes'; + +const mocks = vi.hoisted(() => ({ + getRepoInfoByName: vi.fn(), + getFileSource: vi.fn(), + getFileBlame: vi.fn(), +})); + +vi.mock('@/actions', () => ({ + getRepoInfoByName: mocks.getRepoInfoByName, +})); + +vi.mock('@/features/git', () => ({ + getFileSource: mocks.getFileSource, + getFileBlame: mocks.getFileBlame, +})); + +vi.mock('@/app/(app)/components/pathHeader', () => ({ + PathHeader: ({ path, revisionName }: { path: string; revisionName?: string }) => ( +
Path: {path}; Revision: {revisionName ?? 'default'}
+ ), +})); + +vi.mock('./pureCodePreviewPanel', () => ({ + PureCodePreviewPanel: () =>
Code preview
, +})); + +vi.mock('@/ee/features/codeNav/components/symbolHoverPopup', () => ({ + SymbolHoverPopup: () => null, +})); + +import { CodePreviewPanel } from './codePreviewPanel'; + +const renderCodePreviewPanel = async (props: Parameters[0]) => { + return render( + + {await CodePreviewPanel(props)} + + ); +}; + +afterEach(() => { + cleanup(); +}); + +describe('CodePreviewPanel', () => { + beforeEach(() => { + vi.clearAllMocks(); + + mocks.getRepoInfoByName.mockResolvedValue({ + name: 'github.com/sourcebot-dev/sourcebot', + displayName: 'sourcebot-dev/sourcebot', + codeHostType: 'github', + externalWebUrl: 'https://github.com/sourcebot-dev/sourcebot', + }); + }); + + test('renders a browse 404 when the requested file does not exist', async () => { + mocks.getFileSource.mockResolvedValue({ + statusCode: 404, + errorCode: ErrorCode.FILE_NOT_FOUND, + message: 'File "src/missing.ts" not found in repository "github.com/sourcebot-dev/sourcebot"', + }); + + await renderCodePreviewPanel({ + path: 'src/missing.ts', + repoName: 'github.com/sourcebot-dev/sourcebot', + revisionName: 'feature-branch', + }); + + expect(screen.queryByText('File not found')).toBeTruthy(); + expect(screen.queryAllByText(/src\/missing\.ts/).length).toBeGreaterThan(0); + expect(screen.queryByText(/Error loading file source/)).toBeNull(); + expect(screen.getByRole('link', { name: 'Return to repository root' }).getAttribute('href')).toBe( + '/browse/github.com/sourcebot-dev/sourcebot@feature-branch/-/tree' + ); + }); + + test('keeps preview 404 navigation anchored to the browse revision', async () => { + mocks.getFileSource.mockResolvedValue({ + statusCode: 404, + errorCode: ErrorCode.FILE_NOT_FOUND, + message: 'File "src/missing.ts" not found in repository "github.com/sourcebot-dev/sourcebot"', + }); + + await renderCodePreviewPanel({ + path: 'src/missing.ts', + repoName: 'github.com/sourcebot-dev/sourcebot', + revisionName: 'main', + previewRef: 'abc123def456', + }); + + expect(screen.queryByText('File not found')).toBeTruthy(); + expect(screen.getAllByText('abc123def456').length).toBeGreaterThan(0); + expect(screen.getByText('src/missing.ts').className).toContain('break-all'); + expect(screen.getAllByText('abc123def456').some((element) => element.className.includes('break-all'))).toBe(true); + expect(screen.getByRole('link', { name: 'Return to repository root' }).getAttribute('href')).toBe( + '/browse/github.com/sourcebot-dev/sourcebot@main/-/tree' + ); + expect(screen.getByRole('link', { name: 'Close preview' }).getAttribute('href')).toBe( + '/browse/github.com/sourcebot-dev/sourcebot@main/-/blob/src%2Fmissing.ts' + ); + }); + + test('keeps successful preview header anchored to the browse revision', async () => { + mocks.getFileSource.mockResolvedValue({ + source: 'const value = 1;\n', + language: 'typescript', + }); + + await renderCodePreviewPanel({ + path: 'src/index.ts', + repoName: 'github.com/sourcebot-dev/sourcebot', + revisionName: 'main', + previewRef: 'abc123def456', + }); + + expect(mocks.getFileSource).toHaveBeenCalledWith({ + path: 'src/index.ts', + repo: 'github.com/sourcebot-dev/sourcebot', + ref: 'abc123def456', + }, { source: 'sourcebot-web-client' }); + expect(screen.getByTestId('path-header').textContent).toBe('Path: src/index.ts; Revision: main'); + + const closePreviewLink = screen.getByRole('link', { name: 'Close preview' }); + expect(closePreviewLink.getAttribute('href')).toBe( + '/browse/github.com/sourcebot-dev/sourcebot@main/-/blob/src%2Findex.ts' + ); + expect(closePreviewLink.closest('button')).toBeNull(); + }); +}); diff --git a/packages/web/src/app/(app)/browse/[...path]/components/codePreviewPanel/codePreviewPanel.tsx b/packages/web/src/app/(app)/browse/[...path]/components/codePreviewPanel/codePreviewPanel.tsx index 7209abdab..200c80864 100644 --- a/packages/web/src/app/(app)/browse/[...path]/components/codePreviewPanel/codePreviewPanel.tsx +++ b/packages/web/src/app/(app)/browse/[...path]/components/codePreviewPanel/codePreviewPanel.tsx @@ -3,6 +3,7 @@ import { PathHeader } from "@/app/(app)/components/pathHeader"; import { Button } from "@/components/ui/button"; import { Separator } from "@/components/ui/separator"; import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; +import { ErrorCode } from "@/lib/errorCodes"; import { cn, getCodeHostInfoForRepo, isServiceError, truncateSha } from "@/lib/utils"; import { X } from "lucide-react"; import Image from "next/image"; @@ -10,6 +11,7 @@ import Link from "next/link"; import { getBrowsePath } from "../../../hooks/utils"; import { BlameAgeLegend } from "./blameAgeLegend"; import { BlameViewToggle } from "./blameViewToggle"; +import { FileNotFoundPanel } from "./fileNotFoundPanel"; import { PureCodePreviewPanel } from "./pureCodePreviewPanel"; import { getFileBlame, getFileSource } from '@/features/git'; @@ -54,14 +56,31 @@ export const CodePreviewPanel = async ({ path, repoName, revisionName, previewRe : Promise.resolve(undefined), ]); - if (isServiceError(fileSourceResponse)) { - return
Error loading file source: {fileSourceResponse.message}
- } - if (isServiceError(repoInfoResponse)) { return
Error loading repo info: {repoInfoResponse.message}
} + if (isServiceError(fileSourceResponse)) { + if (fileSourceResponse.errorCode === ErrorCode.FILE_NOT_FOUND) { + return ( + + ); + } + + return
Error loading file source: {fileSourceResponse.message}
+ } + if (blameResponse !== undefined && isServiceError(blameResponse)) { return
Error loading blame: {blameResponse.message}
} @@ -96,7 +115,7 @@ export const CodePreviewPanel = async ({ path, repoName, revisionName, previewRe displayName: repoInfoResponse.displayName, externalWebUrl: repoInfoResponse.externalWebUrl, }} - revisionName={contentRef} + revisionName={revisionName} /> {fileWebUrl && ( @@ -154,7 +173,7 @@ export const CodePreviewPanel = async ({ path, repoName, revisionName, previewRe - + + + Close preview + + + )} +
+
+ +
+
+

File not found

+

+ The path {path} does not exist + {missingRevisionName ? <> at {missingRevisionName} : null}. +

+
+ +
+ + ); +}