Created
November 21, 2025 22:32
-
-
Save JohnPhamous/2c23ad308d76fde49586e9b678b18559 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import React from 'react'; | |
| import { describe, it, expect } from 'vitest'; | |
| import { render, screen } from '@testing-library/react'; | |
| import '@testing-library/jest-dom'; | |
| import { | |
| calculateSegments, | |
| TruncateMiddleOfString, | |
| } from '@/lib/string-components'; | |
| describe('calculateSegments', () => { | |
| it('handles user/feature pattern', () => { | |
| const input = 'john/feature-branch-name'; | |
| const { prefix, middle, suffix } = calculateSegments(input); | |
| expect(prefix).toBe('john/'); | |
| expect(middle).toBe('feature-branch'); | |
| expect(suffix).toBe('-name'); | |
| }); | |
| it('handles ticket id pattern', () => { | |
| const input = | |
| 'vade-183-fix-image-aspect-ratios-and-sizing-in-markdown-renderer'; | |
| const { prefix, middle, suffix } = calculateSegments(input); | |
| // "vade-183" is 8 chars. Suffix should be last 8 chars "renderer". | |
| expect(prefix).toBe('vade-183'); | |
| expect(suffix).toBe('renderer'); | |
| expect(middle).toBe('-fix-image-aspect-ratios-and-sizing-in-markdown-'); | |
| }); | |
| it('handles ticket id pattern with different structure', () => { | |
| const input = 'PROJ-1234-some-description'; | |
| const { prefix, middle, suffix } = calculateSegments(input); | |
| // "PROJ-1234" is 9 chars. Suffix should be last 9 chars: "scription". | |
| expect(prefix).toBe('PROJ-1234'); | |
| expect(suffix).toBe('scription'); | |
| expect(middle).toBe('-some-de'); | |
| }); | |
| it('handles fallback for plain long strings', () => { | |
| // Default length is 4 | |
| const input = 'abcdefghijklmnopqrstuvwxyz'; | |
| const { prefix, middle, suffix } = calculateSegments(input); | |
| expect(prefix).toBe('abcd'); | |
| expect(suffix).toBe('wxyz'); | |
| expect(middle).toBe('efghijklmnopqrstuv'); | |
| }); | |
| it('handles short strings without splitting', () => { | |
| const input = 'short'; | |
| const { prefix, middle, suffix } = calculateSegments(input); | |
| expect(prefix).toBe('short'); | |
| expect(middle).toBe(''); | |
| expect(suffix).toBe(''); | |
| }); | |
| it('handles empty string', () => { | |
| const { prefix, middle, suffix } = calculateSegments(''); | |
| expect(prefix).toBe(''); | |
| expect(middle).toBe(''); | |
| expect(suffix).toBe(''); | |
| }); | |
| it('handles edge case where string length is close to 2x prefix', () => { | |
| // prefix "user/" is 5 chars. 2x = 10. | |
| // Input length 10. Should not split. | |
| const input = 'user/12345'; | |
| const { prefix, middle, suffix } = calculateSegments(input); | |
| expect(prefix).toBe('user/12345'); | |
| expect(middle).toBe(''); | |
| expect(suffix).toBe(''); | |
| }); | |
| it('handles edge case where string length is > 2x prefix', () => { | |
| // prefix "user/" is 5 chars. 2x = 10. | |
| // Input length 11. Should split. | |
| const input = 'user/123456'; | |
| const { prefix, middle, suffix } = calculateSegments(input); | |
| expect(prefix).toBe('user/'); | |
| expect(middle).toBe('1'); | |
| expect(suffix).toBe('23456'); | |
| }); | |
| }); | |
| describe('TruncateMiddleOfString', () => { | |
| it('renders full string when short', () => { | |
| render(<TruncateMiddleOfString>short</TruncateMiddleOfString>); | |
| expect(screen.getByText('short')).toBeInTheDocument(); | |
| }); | |
| it('renders split parts for long string', () => { | |
| const input = 'john/feature-branch-name'; | |
| const { container } = render( | |
| <TruncateMiddleOfString>{input}</TruncateMiddleOfString>, | |
| ); | |
| // We expect 3 spans usually, but if the logic splits it. | |
| // prefix: john/ | |
| // middle: feature-branch | |
| // suffix: -name | |
| expect(screen.getByText('john/')).toBeInTheDocument(); | |
| expect(screen.getByText('feature-branch')).toBeInTheDocument(); | |
| expect(screen.getByText('-name')).toBeInTheDocument(); | |
| const spans = container.querySelectorAll('span'); | |
| // Root span + 3 children spans = 4 spans | |
| expect(spans.length).toBeGreaterThanOrEqual(3); | |
| }); | |
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import React from 'react'; | |
| import { clsx } from 'clsx'; | |
| /** | |
| * Calculates the prefix, middle, and suffix parts of a string for smart truncation. | |
| */ | |
| export const calculateSegments = ( | |
| text: string, | |
| ): { prefix: string; middle: string; suffix: string } => { | |
| if (!text) { | |
| return { prefix: '', middle: '', suffix: '' }; | |
| } | |
| let prefixLength = 0; | |
| // 1. Check for path/branch pattern: user/feature -> prefix is "user/" | |
| const slashMatch = text.match(/^([^\/]+\/)/); | |
| if (slashMatch) { | |
| prefixLength = slashMatch[1].length; | |
| } else { | |
| // 2. Check for issue ID pattern: TICKET-123 -> prefix is "TICKET-123" | |
| // Only matching if it starts with letters, followed by hyphen, then numbers | |
| // And typically followed by another hyphen or end of string | |
| const issueMatch = text.match(/^([a-zA-Z]+-\d+)/); | |
| if (issueMatch) { | |
| prefixLength = issueMatch[1].length; | |
| } else { | |
| // 3. Fallback: Use a default length (e.g., 4 chars) | |
| // If the string is very short, we'll handle it by checking total length | |
| prefixLength = 4; | |
| } | |
| } | |
| // If the string is short enough that splitting doesn't make sense, return it as prefix | |
| // "Short enough" could be defined as <= prefixLength * 2 + some buffer | |
| // But for simplicity, let's just follow the "prefix and suffix lengths should be equal" rule strictly | |
| // unless the string is shorter than 2 * prefixLength. | |
| if (text.length <= prefixLength * 2) { | |
| // If it's too short to split nicely, we can just return the whole thing as prefix | |
| // or handle it gracefully. | |
| // Let's return it as prefix so it shows up fully. | |
| return { prefix: text, middle: '', suffix: '' }; | |
| } | |
| const prefix = text.slice(0, prefixLength); | |
| const suffix = text.slice(-prefixLength); | |
| const middle = text.slice(prefixLength, -prefixLength); | |
| return { prefix, middle, suffix }; | |
| }; | |
| /** | |
| * This component should return a <span> with content. We'll split the string into 3 parts: prefix, middle, suffix. | |
| * Prefix and suffix should never be truncated. The middle can be truncated based on the available width. | |
| * Prefix and suffix lengths should be equal. | |
| * The prefix should be smart and try to parse for semantics. | |
| * | |
| * The truncation should happen with CSS so it's responsive. | |
| */ | |
| export const TruncateMiddleOfString = ({ | |
| children, | |
| className, | |
| }: { | |
| children: string; | |
| className?: string; | |
| }) => { | |
| const { prefix, middle, suffix } = calculateSegments(children); | |
| if (!middle && !suffix) { | |
| return <span className={className}>{prefix}</span>; | |
| } | |
| return ( | |
| <span className={clsx('inline-flex min-w-0 max-w-full', className)}> | |
| <span>{prefix}</span> | |
| <span className="truncate min-w-0 shrink-100">{middle}</span> | |
| <span className="whitespace-nowrap">{suffix}</span> | |
| </span> | |
| ); | |
| }; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment