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
171 changes: 171 additions & 0 deletions src/components/docImage/docImage.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
import {beforeEach, describe, expect, it, vi} from 'vitest';

import {
cleanUrl,
DIMENSION_PATTERN,
extractHash,
parseDimensionsFromHash,
} from 'sentry-docs/components/docImage';
import {serverContext} from 'sentry-docs/serverContext';

// Mock the serverContext
vi.mock('sentry-docs/serverContext', () => ({
serverContext: vi.fn(),
}));

// Mock the ImageLightbox component
vi.mock('../imageLightbox', () => ({
ImageLightbox: vi.fn(({src, alt, width, height, imgPath}) => ({
type: 'ImageLightbox',
props: {src, alt, width, height, imgPath},
})),
}));

const mockServerContext = serverContext as any;

describe('DocImage Helper Functions', () => {
beforeEach(() => {
mockServerContext.mockReturnValue({
path: '/docs/test-page',
});
});

describe('dimension validation bounds', () => {
it('should validate dimensions within acceptable range', () => {
// Test actual boundary conditions used in parseDimensionsFromHash
expect(parseDimensionsFromHash('/image.jpg#1x1')).toEqual([1, 1]); // minimum valid
expect(parseDimensionsFromHash('/image.jpg#10000x10000')).toEqual([10000, 10000]); // maximum valid
expect(parseDimensionsFromHash('/image.jpg#0x600')).toEqual([]); // below minimum
expect(parseDimensionsFromHash('/image.jpg#10001x5000')).toEqual([]); // above maximum
});
});

describe('dimension pattern regex', () => {
it('should match valid dimension patterns', () => {
expect(DIMENSION_PATTERN.test('800x600')).toBe(true);
expect(DIMENSION_PATTERN.test('1920x1080')).toBe(true);
expect(DIMENSION_PATTERN.test('10000x10000')).toBe(true);
});

it('should not match invalid dimension patterns', () => {
expect(DIMENSION_PATTERN.test('800x')).toBe(false);
expect(DIMENSION_PATTERN.test('x600')).toBe(false);
expect(DIMENSION_PATTERN.test('800')).toBe(false);
expect(DIMENSION_PATTERN.test('800x600x400')).toBe(false);
expect(DIMENSION_PATTERN.test('abc800x600')).toBe(false);
expect(DIMENSION_PATTERN.test('section-heading')).toBe(false);
});
});

describe('hash extraction', () => {
it('should extract hash from URLs', () => {
expect(extractHash('/image.jpg#800x600')).toBe('800x600');
expect(extractHash('./img/issue_page.png#1200x800')).toBe('1200x800');
expect(extractHash('https://example.com/image.jpg#section')).toBe('section');
expect(extractHash('/image.jpg')).toBe('');
});
});

describe('dimension parsing from hash', () => {
it('should parse valid dimensions from URL hash', () => {
expect(parseDimensionsFromHash('/image.jpg#800x600')).toEqual([800, 600]);
expect(parseDimensionsFromHash('/image.jpg#1920x1080')).toEqual([1920, 1080]);
expect(parseDimensionsFromHash('/image.jpg#10000x10000')).toEqual([10000, 10000]);
expect(parseDimensionsFromHash('./img/issue_page.png#1200x800')).toEqual([
1200, 800,
]);
});

it('should return empty array for invalid dimensions', () => {
const invalidCases = [
'/image.jpg#0x600',
'/image.jpg#10001x5000',
'/image.jpg#800x',
'/image.jpg#section-heading',
'/image.jpg',
'./img/error_level.png#malformed',
];

invalidCases.forEach(url => {
expect(parseDimensionsFromHash(url)).toEqual([]);
});
});
});

describe('URL cleaning', () => {
it('should remove dimension hashes from URLs', () => {
const testCases = [
{input: '/image.jpg#800x600', expected: '/image.jpg'},
{
input: 'https://example.com/image.jpg#1920x1080',
expected: 'https://example.com/image.jpg',
},
{input: './img/issue_page.png#1200x800', expected: './img/issue_page.png'},
{input: './img/error_level.png#32x32', expected: './img/error_level.png'},
];

testCases.forEach(({input, expected}) => {
expect(cleanUrl(input)).toBe(expected);
});
});

it('should preserve non-dimension hashes', () => {
const testCases = [
'./img/issue_page.png#section-heading',
'/image.jpg#important-section',
'/image.jpg#anchor',
];

testCases.forEach(url => {
expect(cleanUrl(url)).toBe(url);
});
});

it('should handle URLs without hashes', () => {
const testCases = [
'/image.jpg',
'https://example.com/image.jpg',
'./img/issue_page.png',
];

testCases.forEach(url => {
expect(cleanUrl(url)).toBe(url);
});
});
});

describe('Issues page integration scenarios', () => {
const issuesPageImages = [
'./img/issue_page.png#1200x800',
'./img/error_level.png#32x32',
'./img/issue_sort.png#600x400',
];

it('should handle Issues page image paths correctly', () => {
issuesPageImages.forEach(path => {
const hash = extractHash(path);
const cleanedUrl = cleanUrl(path);
const dimensions = parseDimensionsFromHash(path);

expect(hash).toMatch(DIMENSION_PATTERN);
expect(cleanedUrl).not.toContain('#');
expect(dimensions).toHaveLength(2);
expect(dimensions[0]).toBeGreaterThan(0);
expect(dimensions[1]).toBeGreaterThan(0);
});
});

it('should handle malformed relative paths gracefully', () => {
const malformedPaths = [
'./img/issue_page.png#800x',
'./img/error_level.png#invalid',
'./img/issue_sort.png#section-anchor',
];

malformedPaths.forEach(path => {
expect(cleanUrl(path)).toBe(path); // Should not clean non-dimension hashes
expect(parseDimensionsFromHash(path)).toEqual([]); // Should return empty array
});
});
});
});
23 changes: 13 additions & 10 deletions src/components/docImage.tsx → src/components/docImage/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,41 +3,42 @@ import path from 'path';
import {isExternalImage} from 'sentry-docs/config/images';
import {serverContext} from 'sentry-docs/serverContext';

import {ImageLightbox} from './imageLightbox';
import {ImageLightbox} from '../imageLightbox';

// Helper function to safely parse dimension values
const parseDimension = (value: string | number | undefined): number | undefined => {
if (typeof value === 'number' && value > 0 && value <= 10000) return value;
if (typeof value === 'number' && value > 0 && value <= MAX_DIMENSION) return value;
if (typeof value === 'string') {
const parsed = parseInt(value, 10);
return parsed > 0 && parsed <= 10000 ? parsed : undefined;
return parsed > 0 && parsed <= MAX_DIMENSION ? parsed : undefined;
}
return undefined;
};

// Dimension pattern regex - used to identify dimension hashes like "800x600"
const DIMENSION_PATTERN = /^(\d+)x(\d+)$/;
export const DIMENSION_PATTERN = /^(\d+)x(\d+)$/;
export const MAX_DIMENSION = 10000;

// Helper function to extract hash from URL string (works with both relative and absolute URLs)
const extractHash = (url: string): string => {
export const extractHash = (url: string): string => {
const hashIndex = url.indexOf('#');
return hashIndex !== -1 ? url.slice(hashIndex + 1) : '';
};

// Helper function to check if a hash contains dimension information
const isDimensionHash = (hash: string): boolean => {
export const isDimensionHash = (hash: string): boolean => {
return DIMENSION_PATTERN.test(hash);
};

// Helper function to parse dimensions from URL hash
const parseDimensionsFromHash = (url: string): number[] => {
export const parseDimensionsFromHash = (url: string): number[] => {
const hash = extractHash(url);
const match = hash.match(DIMENSION_PATTERN);

if (match) {
const width = parseInt(match[1], 10);
const height = parseInt(match[2], 10);
return width > 0 && width <= 10000 && height > 0 && height <= 10000
return width > 0 && width <= MAX_DIMENSION && height > 0 && height <= MAX_DIMENSION
? [width, height]
: [];
}
Expand All @@ -46,7 +47,7 @@ const parseDimensionsFromHash = (url: string): number[] => {
};

// Helper function to remove dimension hash from URL while preserving fragment identifiers
const cleanUrl = (url: string): string => {
export const cleanUrl = (url: string): string => {
const hash = extractHash(url);

// If no hash or hash is not a dimension pattern, return original URL
Expand Down Expand Up @@ -78,7 +79,9 @@ export default function DocImage({
// For internal images, process the path
if (!isExternal) {
if (src.startsWith('./')) {
finalSrc = path.join('/mdx-images', src);
// Remove ./ prefix and properly join with mdx-images path
const cleanSrc = src.slice(2);
finalSrc = path.join('/mdx-images', cleanSrc);
} else if (!src?.startsWith('/') && !src?.includes('://')) {
finalSrc = `/${pagePath.join('/')}/${src}`;
}
Expand Down
Loading
Loading