chore: support reverse in ansi2html, drop ansi-to-html (#33389)

This commit is contained in:
Yury Semikhatsky 2024-10-31 21:42:06 -07:00 committed by GitHub
parent 26c2049d5a
commit c95feccce4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 94 additions and 78 deletions

27
package-lock.json generated
View File

@ -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",

View File

@ -7,8 +7,5 @@
"dev": "vite",
"build": "vite build && tsc",
"preview": "vite preview"
},
"dependencies": {
"ansi-to-html": "^0.7.2"
}
}

View File

@ -14,6 +14,8 @@
limitations under the License.
*/
@import '@web/third_party/vscode/colors.css';
.test-error-view {
white-space: pre;
overflow: auto;

View File

@ -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 => ({ '&': '&amp;', '"': '&quot;', '<': '&lt;', '>': '&gt;' }[c]!));
return ansi2html(text || '', defaultColors);
}

View File

@ -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(`<span style="${styleBody(style)}">${escapeHTML(text)}</span>`);
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(`<span style="${styleBody(styleCopy)}">${escapeHTML(text)}</span>`);
}
}
return tokens.join('');

View File

@ -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('<div>begin inner end</div>');
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 }) => {