From 886a3003298a55df5c17ef713765e8462e07ba49 Mon Sep 17 00:00:00 2001 From: chengpeiquan Date: Sun, 8 Mar 2026 01:45:21 +0800 Subject: [PATCH] fix(truncate): cover middle truncation edge cases --- ...026-03-08-utils-coverage-implementation.md | 50 ++++++ src/Truncate/utils.tsx | 26 ++- test/Truncate.spec.tsx | 157 ++++++++++++++++++ 3 files changed, 224 insertions(+), 9 deletions(-) create mode 100644 docs/plans/2026-03-08-utils-coverage-implementation.md diff --git a/docs/plans/2026-03-08-utils-coverage-implementation.md b/docs/plans/2026-03-08-utils-coverage-implementation.md new file mode 100644 index 0000000..fea2cc4 --- /dev/null +++ b/docs/plans/2026-03-08-utils-coverage-implementation.md @@ -0,0 +1,50 @@ +# Utils Coverage Implementation Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** Add missing unit tests so the current `vitest` coverage scope for `src/**` reaches 100% without changing production code. + +**Architecture:** Keep all changes in the existing `test/Truncate.spec.tsx` suite. Target uncovered branches in `src/Truncate/utils.tsx`, especially the expansion, proportional truncation, and final fine-tuning paths inside `getMiddleTruncateFragments`. + +**Tech Stack:** React, Vitest, Testing Library, Sinon + +--- + +### Task 1: Add failing coverage tests for `getMiddleTruncateFragments` + +**Files:** +- Modify: `test/Truncate.spec.tsx` +- Test: `test/Truncate.spec.tsx` + +**Step 1: Write the failing test** + +Add focused test cases for: +- expanding `startFragment` before hitting `targetWidth` +- expanding `endFragment` when more width remains +- truncating `endFragment` when it is proportionally wider +- truncating only one side after the opposite side becomes empty +- fine-tuning with one extra character added to `startFragment` +- fine-tuning with one extra character added to `endFragment` + +**Step 2: Run test to verify expected behavior** + +Run: `pnpm test test/Truncate.spec.tsx` + +Expected: the new cases either pass immediately because behavior already exists, or fail with assertion output that identifies the remaining uncovered path. + +### Task 2: Verify full coverage + +**Files:** +- Test: `test/Truncate.spec.tsx` + +**Step 1: Run the targeted suite** + +Run: `pnpm test test/Truncate.spec.tsx` + +Expected: all tests pass. + +**Step 2: Run coverage** + +Run: `pnpm coverage` + +Expected: `src/**` coverage reaches 100% for statements, branches, functions, and lines. If any path remains uncovered, add one more focused test and re-run coverage. diff --git a/src/Truncate/utils.tsx b/src/Truncate/utils.tsx index 91b2313..cd49afe 100644 --- a/src/Truncate/utils.tsx +++ b/src/Truncate/utils.tsx @@ -85,13 +85,13 @@ export const getMiddleTruncateFragments = ({ // If current width is less than target width, attempt to expand fragments if (fullWidth < targetWidth) { // Try to expand startFragment to utilize available space - // Only expand if startSliceIndex > 0 (i.e., when end is not greater than text length) + // Only expand while there is still uncovered text between the two fragments. while ( - startSliceIndex > 0 && - startSliceIndex + startFragment.length < length && + startFragment.length < length && + startFragment.length + endFragment.length < fullText.length && fullWidth < targetWidth ) { - const nextChar = lastLineText[startSliceIndex + startFragment.length] + const nextChar = lastLineText[startFragment.length] const testStartFragment = startFragment + nextChar const testWidth = getFragmentsTotalWidth(testStartFragment, endFragment) @@ -104,7 +104,11 @@ export const getMiddleTruncateFragments = ({ } // If there's still space available, try to expand endFragment - while (endFragment.length < fullText.length && fullWidth < targetWidth) { + while ( + endFragment.length < fullText.length && + startFragment.length + endFragment.length < fullText.length && + fullWidth < targetWidth + ) { const nextChar = fullText[fullText.length - endFragment.length - 1] const testEndFragment = nextChar + endFragment const testWidth = getFragmentsTotalWidth(startFragment, testEndFragment) @@ -145,8 +149,6 @@ export const getMiddleTruncateFragments = ({ startFragment = startFragment.slice(0, startFragment.length - 1) } else if (endFragment.length > 0) { endFragment = endFragment.slice(1) - } else { - break } fullWidth = getFragmentsTotalWidth(startFragment, endFragment) @@ -156,12 +158,18 @@ export const getMiddleTruncateFragments = ({ // This step ensures to use every available pixel efficiently if (fullWidth < targetWidth) { const remainingWidth = targetWidth - fullWidth + const hasHiddenText = + startFragment.length + endFragment.length < fullText.length // Try to add characters to both ends while maintaining visual balance // Only add to startFragment if startSliceIndex > 0 const startChar = - startSliceIndex > 0 ? lastLineText[startFragment.length] : null - const endChar = fullText[fullText.length - endFragment.length - 1] + hasHiddenText && startSliceIndex > 0 + ? lastLineText[startFragment.length] + : null + const endChar = hasHiddenText + ? fullText[fullText.length - endFragment.length - 1] + : null if (startChar && measureWidth(startChar) <= remainingWidth) { startFragment += startChar diff --git a/test/Truncate.spec.tsx b/test/Truncate.spec.tsx index 0972b59..d3ca716 100644 --- a/test/Truncate.spec.tsx +++ b/test/Truncate.spec.tsx @@ -566,6 +566,15 @@ describe('', () => { describe('getMiddleTruncateFragments', () => { const targetWidth = width const ellipsisWidth = measureWidth(ellipsis) + const createVariableMeasureWidth = ( + widths: Record, + fallback = 1, + ) => { + return (text: string) => + text.split('').reduce((total, char) => { + return total + (widths[char] ?? fallback) + }, 0) + } it('should return correct fragments when text fits within target width', () => { const options = { @@ -639,6 +648,154 @@ describe('', () => { endFragment: 'This is a long text', }) }) + + it('should expand startFragment with the next uncovered character only', () => { + const measureCompactWidth = (text: string) => text.length + const result = getMiddleTruncateFragments({ + end: -2, + lastLineText: 'abcdef', + fullText: 'abcdefghij', + targetWidth: 8, + ellipsisWidth: 1, + measureWidth: measureCompactWidth, + }) + + expect(result).toEqual({ + startFragment: 'abcde', + endFragment: 'ij', + }) + }) + + it('should stop expanding when fragments already cover the full text', () => { + const measureCompactWidth = (text: string) => text.length + const result = getMiddleTruncateFragments({ + end: -4, + lastLineText: 'abcdef', + fullText: 'abcdef', + targetWidth: 8, + ellipsisWidth: 1, + measureWidth: measureCompactWidth, + }) + + expect(result).toEqual({ + startFragment: 'ab', + endFragment: 'cdef', + }) + }) + + it('should break start expansion when the next character exceeds the target width', () => { + const result = getMiddleTruncateFragments({ + end: -1, + lastLineText: 'abc', + fullText: 'abcdef', + targetWidth: 5, + ellipsisWidth: 1, + measureWidth: createVariableMeasureWidth({ c: 2 }), + }) + + expect(result).toEqual({ + startFragment: 'ab', + endFragment: 'ef', + }) + }) + + it('should expand endFragment when start expansion cannot use the remaining width', () => { + const measureCompactWidth = (text: string) => text.length + const result = getMiddleTruncateFragments({ + end: -1, + lastLineText: 'abc', + fullText: 'abcdef', + targetWidth: 6, + ellipsisWidth: 1, + measureWidth: measureCompactWidth, + }) + + expect(result).toEqual({ + startFragment: 'abc', + endFragment: 'ef', + }) + }) + + it('should break end expansion when prepending one more character exceeds the target width', () => { + const result = getMiddleTruncateFragments({ + end: -1, + lastLineText: 'abc', + fullText: 'abcdef', + targetWidth: 6, + ellipsisWidth: 1, + measureWidth: createVariableMeasureWidth({ e: 2 }), + }) + + expect(result).toEqual({ + startFragment: 'abc', + endFragment: 'f', + }) + }) + + it('should truncate only the end fragment when the start fragment is empty', () => { + const measureCompactWidth = (text: string) => text.length + const result = getMiddleTruncateFragments({ + end: -3, + lastLineText: 'abc', + fullText: 'abcdef', + targetWidth: 2, + ellipsisWidth: 1, + measureWidth: measureCompactWidth, + }) + + expect(result).toEqual({ + startFragment: '', + endFragment: 'f', + }) + }) + + it('should truncate the wider end fragment first and then trim the start fragment if needed', () => { + const result = getMiddleTruncateFragments({ + end: -1, + lastLineText: 'abc', + fullText: 'abcdef', + targetWidth: 2, + ellipsisWidth: 1, + measureWidth: createVariableMeasureWidth({ f: 3 }), + }) + + expect(result).toEqual({ + startFragment: 'a', + endFragment: '', + }) + }) + + it('should fine-tune by adding one start character when there is remaining width', () => { + const result = getMiddleTruncateFragments({ + end: -1, + lastLineText: 'abc', + fullText: 'abcdef', + targetWidth: 3, + ellipsisWidth: 1, + measureWidth: createVariableMeasureWidth({ f: 2 }), + }) + + expect(result).toEqual({ + startFragment: 'ab', + endFragment: '', + }) + }) + + it('should fine-tune by adding one end character when start fine-tuning does not fit', () => { + const result = getMiddleTruncateFragments({ + end: -1, + lastLineText: 'abc', + fullText: 'abcdef', + targetWidth: 4, + ellipsisWidth: 1, + measureWidth: createVariableMeasureWidth({ b: 2 }), + }) + + expect(result).toEqual({ + startFragment: 'a', + endFragment: 'ef', + }) + }) }) describe('Testing side effects of other props values', () => {