diff --git a/packages/playwright/src/reporters/base.ts b/packages/playwright/src/reporters/base.ts index fb0930c545..9c47f68016 100644 --- a/packages/playwright/src/reporters/base.ts +++ b/packages/playwright/src/reporters/base.ts @@ -490,30 +490,75 @@ export function stripAnsiEscapes(str: string): string { } // Leaves enough space for the "prefix" to also fit. -function fitToWidth(line: string, width: number, prefix?: string): string { +export function fitToWidth(line: string, width: number, prefix?: string): string { const prefixLength = prefix ? stripAnsiEscapes(prefix).length : 0; - width -= prefixLength; - if (line.length <= width) - return line; + return wrapText(line, width - prefixLength); +} - // Even items are plain text, odd items are control sequences. - const parts = line.split(ansiRegex); - const taken: string[] = []; - for (let i = parts.length - 1; i >= 0; i--) { - if (i % 2) { - // Include all control sequences to preserve formatting. - taken.push(parts[i]); - } else { - let part = parts[i].substring(parts[i].length - width); - if (part.length < parts[i].length && part.length > 0) { - // Add ellipsis if we are truncating. - part = '\u2026' + part.substring(1); - } - taken.push(part); - width -= part.length; +export function wrapText(text: string, width: number) { + // Handle edge cases + if (!text) + return ''; + if (width <= 0) + return text; + + const lines = []; + let currentLine = ''; + let currentWidth = 0; + + const segments = text.split(' '); + + for (const segment of segments) { + // Calculate segment width (Chinese characters count as 2, others as 1) + const segmentWidth = segment + .split('') + .reduce((width, char) => { + return width + (/[\u4e00-\u9fff]/.test(char) ? 2 : 1); + }, 0); + + // Check if adding this segment would exceed the line width + if (currentWidth + segmentWidth > width && currentLine) { + // If current line isn't empty, start a new line + lines.push(currentLine.trim()); + currentLine = ''; + currentWidth = 0; } + + // Special handling for long segments that exceed width on their own + if (segmentWidth > width) { + if (currentLine) { + lines.push(currentLine.trim()); + currentLine = ''; + } + // Split long segment into chunks + let remaining = segment; + while (remaining) { + let chunk = ''; + let chunkWidth = 0; + for (const char of remaining) { + const charWidth = /[\u4e00-\u9fff]/.test(char) ? 2 : 1; + if (chunkWidth + charWidth > width) + break; + chunk += char; + chunkWidth += charWidth; + } + lines.push(chunk); + remaining = remaining.slice(chunk.length); + } + continue; + } + + // Add segment to current line + currentLine += segment; + currentWidth += segmentWidth; } - return taken.reverse().join(''); + + // Add the last line if it's not empty + if (currentLine.trim()) + lines.push(currentLine.trim()); + + + return lines.join('\n'); } function belongsToNodeModules(file: string) { diff --git a/tests/playwright-test/fit-to-width.spec.ts b/tests/playwright-test/fit-to-width.spec.ts new file mode 100644 index 0000000000..0b349b7501 --- /dev/null +++ b/tests/playwright-test/fit-to-width.spec.ts @@ -0,0 +1,60 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { fitToWidth, wrapText } from 'packages/playwright/lib/reporters/base'; +import { test, expect } from './playwright-test-fixtures'; + +test.describe('wrapText', () => { + test('chinese characters', () => { + expect(wrapText('你好', 2)).toBe('你\n好'); + expect(wrapText('你好你好', 4)).toBe('你好\n你好'); + }); + + test('mixed characters', () => { + expect(wrapText('hello你好', 5)).toBe('hello\n你好'); + expect(wrapText('你好hello', 5)).toBe('你好h\nello'); + }); + + test('special characters', () => { + expect(wrapText('hello@world', 5)).toBe('hello\n@worl\nd'); + expect(wrapText('你好@世界', 3)).toBe('你\n好@\n世\n界'); + }); + + test('long words', () => { + expect(wrapText('supercalifragilisticexpialidocious', 10)).toBe('supercalif\nragilistic\nexpialidoc\nious'); + expect(wrapText('你好超级长的词', 5)).toBe('你好\n超级\n长的\n词'); + }); + + test('empty string', () => { + expect(wrapText('', 5)).toBe(''); + expect(wrapText('', 5)).toBe(''); + }); + + test('single character', () => { + expect(wrapText('a', 1)).toBe('a'); + expect(wrapText('a', 1)).toBe('a'); + }); + + test('spaces', () => { + expect(wrapText('hello world', 5)).toBe('hello\nworld'); + expect(wrapText('hello world', 5)).toBe('hello\nworld'); + }); +}); + +test('fitToWidth', () => { + expect(fitToWidth('hello world', 5, '~>')).toBe('hel\nlo\nwor\nld'); +}); +