diff --git a/package-lock.json b/package-lock.json index 3f503e4c09..3bc7c072e9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2412,28 +2412,6 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, - "node_modules/ansi-to-html": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/ansi-to-html/-/ansi-to-html-0.7.2.tgz", - "integrity": "sha512-v6MqmEpNlxF+POuyhKkidusCHWWkaLcGRURzivcU3I9tv7k4JVhFcnukrM5Rlk2rUywdZuzYAZ+kbZqWCnfN3g==", - "dependencies": { - "entities": "^2.2.0" - }, - "bin": { - "ansi-to-html": "bin/ansi-to-html" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/ansi-to-html/node_modules/entities": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", - "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, "node_modules/anymatch": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", @@ -7895,10 +7873,7 @@ } }, "packages/html-reporter": { - "version": "0.0.0", - "dependencies": { - "ansi-to-html": "^0.7.2" - } + "version": "0.0.0" }, "packages/playwright": { "version": "1.49.0-next", diff --git a/packages/html-reporter/package.json b/packages/html-reporter/package.json index bdd0485a07..c1beb75b89 100644 --- a/packages/html-reporter/package.json +++ b/packages/html-reporter/package.json @@ -7,8 +7,5 @@ "dev": "vite", "build": "vite build && tsc", "preview": "vite preview" - }, - "dependencies": { - "ansi-to-html": "^0.7.2" } } diff --git a/packages/html-reporter/src/testErrorView.css b/packages/html-reporter/src/testErrorView.css index d5a4534e7e..e29ea2a18b 100644 --- a/packages/html-reporter/src/testErrorView.css +++ b/packages/html-reporter/src/testErrorView.css @@ -14,6 +14,8 @@ limitations under the License. */ +@import '@web/third_party/vscode/colors.css'; + .test-error-view { white-space: pre; overflow: auto; diff --git a/packages/html-reporter/src/testErrorView.tsx b/packages/html-reporter/src/testErrorView.tsx index 8d2bb13bd3..d63f5d7945 100644 --- a/packages/html-reporter/src/testErrorView.tsx +++ b/packages/html-reporter/src/testErrorView.tsx @@ -14,7 +14,7 @@ limitations under the License. */ -import ansi2html from 'ansi-to-html'; +import { ansi2html } from '@web/ansi2html'; import * as React from 'react'; import './testErrorView.css'; import type { ImageDiff } from '@web/shared/imageDiffView'; @@ -43,33 +43,9 @@ export const TestScreenshotErrorView: React.FC<{ }; function ansiErrorToHtml(text?: string): string { - const config: any = { + const defaultColors = { bg: 'var(--color-canvas-subtle)', fg: 'var(--color-fg-default)', }; - config.colors = ansiColors; - return new ansi2html(config).toHtml(escapeHTML(text || '')); -} - -const ansiColors = { - 0: '#000', - 1: '#C00', - 2: '#0C0', - 3: '#C50', - 4: '#00C', - 5: '#C0C', - 6: '#0CC', - 7: '#CCC', - 8: '#555', - 9: '#F55', - 10: '#5F5', - 11: '#FF5', - 12: '#55F', - 13: '#F5F', - 14: '#5FF', - 15: '#FFF' -}; - -function escapeHTML(text: string): string { - return text.replace(/[&"<>]/g, c => ({ '&': '&', '"': '"', '<': '<', '>': '>' }[c]!)); + return ansi2html(text || '', defaultColors); } diff --git a/packages/web/src/ansi2html.ts b/packages/web/src/ansi2html.ts index e1d80ed073..827e081ae3 100644 --- a/packages/web/src/ansi2html.ts +++ b/packages/web/src/ansi2html.ts @@ -14,11 +14,16 @@ limitations under the License. */ -export function ansi2html(text: string): string { +export function ansi2html(text: string, defaultColors?: { bg: string, fg: string }): string { const regex = /(\x1b\[(\d+(;\d+)*)m)|([^\x1b]+)/g; const tokens: string[] = []; let match; let style: any = {}; + + let reverse = false; + let fg: string | undefined = defaultColors?.fg; + let bg: string | undefined = defaultColors?.bg; + while ((match = regex.exec(text)) !== null) { const [, , codeStr, , text] = match; if (codeStr) { @@ -29,11 +34,28 @@ export function ansi2html(text: string): string { case 2: style['opacity'] = '0.8'; break; case 3: style['font-style'] = 'italic'; break; case 4: style['text-decoration'] = 'underline'; break; + case 7: + reverse = true; + break; case 8: style.display = 'none'; break; case 9: style['text-decoration'] = 'line-through'; break; - case 22: style = { ...style, 'font-weight': undefined, 'font-style': undefined, 'opacity': undefined, 'text-decoration': undefined }; break; - case 23: style = { ...style, 'font-weight': undefined, 'font-style': undefined, 'opacity': undefined }; break; - case 24: style = { ...style, 'text-decoration': undefined }; break; + case 22: + delete style['font-weight']; + delete style['font-style']; + delete style['opacity']; + delete style['text-decoration']; + break; + case 23: + delete style['font-weight']; + delete style['font-style']; + delete style['opacity']; + break; + case 24: + delete style['text-decoration']; + break; + case 27: + reverse = false; + break; case 30: case 31: case 32: @@ -41,8 +63,12 @@ export function ansi2html(text: string): string { case 34: case 35: case 36: - case 37: style.color = ansiColors[code - 30]; break; - case 39: style = { ...style, color: undefined }; break; + case 37: + fg = ansiColors[code - 30]; + break; + case 39: + fg = defaultColors?.fg; + break; case 40: case 41: case 42: @@ -50,8 +76,12 @@ export function ansi2html(text: string): string { case 44: case 45: case 46: - case 47: style['background-color'] = ansiColors[code - 40]; break; - case 49: style = { ...style, 'background-color': undefined }; break; + case 47: + bg = ansiColors[code - 40]; + break; + case 49: + bg = defaultColors?.bg; + break; case 53: style['text-decoration'] = 'overline'; break; case 90: case 91: @@ -60,7 +90,9 @@ export function ansi2html(text: string): string { case 94: case 95: case 96: - case 97: style.color = brightAnsiColors[code - 90]; break; + case 97: + fg = brightAnsiColors[code - 90]; + break; case 100: case 101: case 102: @@ -68,10 +100,19 @@ export function ansi2html(text: string): string { case 104: case 105: case 106: - case 107: style['background-color'] = brightAnsiColors[code - 100]; break; + case 107: + bg = brightAnsiColors[code - 100]; + break; } } else if (text) { - tokens.push(`${escapeHTML(text)}`); + const styleCopy = { ...style }; + const color = reverse ? bg : fg; + if (color !== undefined) + styleCopy['color'] = color; + const backgroundColor = reverse ? fg : bg; + if (backgroundColor !== undefined) + styleCopy['background-color'] = backgroundColor; + tokens.push(`${escapeHTML(text)}`); } } return tokens.join(''); diff --git a/tests/playwright-test/reporter-html.spec.ts b/tests/playwright-test/reporter-html.spec.ts index 51734be9ff..59d986857f 100644 --- a/tests/playwright-test/reporter-html.spec.ts +++ b/tests/playwright-test/reporter-html.spec.ts @@ -472,7 +472,7 @@ for (const useIntermediateMergeReport of [true, false] as const) { await showReport(); await page.click('text=fails'); - await expect(page.locator('.test-error-view span:has-text("received")').nth(1)).toHaveCSS('color', 'rgb(204, 0, 0)'); + await expect(page.locator('.test-error-view span:has-text("true")').first()).toHaveCSS('color', 'rgb(205, 49, 49)'); }); test('should show trace source', async ({ runInlineTest, page, showReport }) => { @@ -939,8 +939,9 @@ for (const useIntermediateMergeReport of [true, false] as const) { expect(result.exitCode).toBe(1); await showReport(); await page.click('text="is a test"'); - const stricken = await page.locator('css=strike').innerText(); - expect(stricken).toBe('old'); + + await expect(page.locator('.test-error-view').getByText('old')).toHaveCSS('text-decoration', 'line-through solid rgb(205, 49, 49)'); + await expect(page.locator('.test-error-view').getByText('new', { exact: true })).toHaveCSS('text-decoration', 'none solid rgb(0, 188, 0)'); }); test('should strikethrough textual diff with commonalities', async ({ runInlineTest, showReport, page }) => { @@ -966,8 +967,32 @@ for (const useIntermediateMergeReport of [true, false] as const) { expect(result.exitCode).toBe(1); await showReport(); await page.click('text="is a test"'); - const stricken = await page.locator('css=strike').innerText(); - expect(stricken).toBe('old'); + await expect(page.locator('.test-error-view').getByText('old')).toHaveCSS('text-decoration', 'line-through solid rgb(205, 49, 49)'); + await expect(page.locator('.test-error-view').getByText('new', { exact: true })).toHaveCSS('text-decoration', 'none solid rgb(0, 188, 0)'); + await expect(page.locator('.test-error-view').getByText('common Expected:')).toHaveCSS('text-decoration', 'none solid rgb(36, 41, 47)'); + }); + + test('should highlight inline textual diff in toHaveText', async ({ runInlineTest, showReport, page }) => { + const result = await runInlineTest({ + 'a.spec.ts': ` + import { test, expect } from '@playwright/test'; + test('is a test', async ({ page }) => { + await page.setContent('
begin inner end
'); + await expect(page.locator('div')).toHaveText('inner', { timeout: 500 }); + }); + ` + }, { reporter: 'dot,html' }, { PLAYWRIGHT_HTML_OPEN: 'never' }); + expect(result.exitCode).toBe(1); + await showReport(); + await page.click('text="is a test"'); + await expect(page.locator('.test-error-view').getByText('begin ', { exact: true })).toHaveCSS('color', 'rgb(246, 248, 250)'); + await expect(page.locator('.test-error-view').getByText('begin ', { exact: true })).toHaveCSS('background-color', 'rgb(205, 49, 49)'); + + await expect(page.locator('.test-error-view').getByText('inner', { exact: true })).toHaveCSS('color', 'rgb(205, 49, 49)'); + await expect(page.locator('.test-error-view').getByText('inner', { exact: true })).toHaveCSS('background-color', 'rgb(246, 248, 250)'); + + await expect(page.locator('.test-error-view').getByText('end ', { exact: true })).toHaveCSS('color', 'rgb(246, 248, 250)'); + await expect(page.locator('.test-error-view').getByText('end ', { exact: true })).toHaveCSS('background-color', 'rgb(205, 49, 49)'); }); test('should differentiate repeat-each test cases', async ({ runInlineTest, showReport, page }) => { @@ -984,13 +1009,13 @@ for (const useIntermediateMergeReport of [true, false] as const) { expect(result.exitCode).toBe(1); await showReport(); - await page.locator('text=sample').first().click(); - await expect(page.locator('text=ouch')).toHaveCount(1); - await page.locator('text=All').first().click(); + await page.getByText('sample').first().click(); + await expect(page.getByText('ouch')).toHaveCount(2); + await page.getByText('All').first().click(); - await page.locator('text=sample').nth(1).click(); - await expect(page.locator('text=Before Hooks')).toBeVisible(); - await expect(page.locator('text=ouch')).toBeHidden(); + await page.getByText('sample').nth(1).click(); + await expect(page.getByText('Before Hooks')).toBeVisible(); + await expect(page.getByText('ouch')).toBeHidden(); }); test('should group similar / loop steps', async ({ runInlineTest, showReport, page }) => {