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==", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"dev": true "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": { "node_modules/anymatch": {
"version": "3.1.3", "version": "3.1.3",
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
@ -7895,10 +7873,7 @@
} }
}, },
"packages/html-reporter": { "packages/html-reporter": {
"version": "0.0.0", "version": "0.0.0"
"dependencies": {
"ansi-to-html": "^0.7.2"
}
}, },
"packages/playwright": { "packages/playwright": {
"version": "1.49.0-next", "version": "1.49.0-next",

View file

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

View file

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

View file

@ -14,7 +14,7 @@
limitations under the License. limitations under the License.
*/ */
import ansi2html from 'ansi-to-html'; import { ansi2html } from '@web/ansi2html';
import * as React from 'react'; import * as React from 'react';
import './testErrorView.css'; import './testErrorView.css';
import type { ImageDiff } from '@web/shared/imageDiffView'; import type { ImageDiff } from '@web/shared/imageDiffView';
@ -43,33 +43,9 @@ export const TestScreenshotErrorView: React.FC<{
}; };
function ansiErrorToHtml(text?: string): string { function ansiErrorToHtml(text?: string): string {
const config: any = { const defaultColors = {
bg: 'var(--color-canvas-subtle)', bg: 'var(--color-canvas-subtle)',
fg: 'var(--color-fg-default)', fg: 'var(--color-fg-default)',
}; };
config.colors = ansiColors; return ansi2html(text || '', defaultColors);
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]!));
} }

View file

@ -14,11 +14,16 @@
limitations under the License. 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 regex = /(\x1b\[(\d+(;\d+)*)m)|([^\x1b]+)/g;
const tokens: string[] = []; const tokens: string[] = [];
let match; let match;
let style: any = {}; let style: any = {};
let reverse = false;
let fg: string | undefined = defaultColors?.fg;
let bg: string | undefined = defaultColors?.bg;
while ((match = regex.exec(text)) !== null) { while ((match = regex.exec(text)) !== null) {
const [, , codeStr, , text] = match; const [, , codeStr, , text] = match;
if (codeStr) { if (codeStr) {
@ -29,11 +34,28 @@ export function ansi2html(text: string): string {
case 2: style['opacity'] = '0.8'; break; case 2: style['opacity'] = '0.8'; break;
case 3: style['font-style'] = 'italic'; break; case 3: style['font-style'] = 'italic'; break;
case 4: style['text-decoration'] = 'underline'; break; case 4: style['text-decoration'] = 'underline'; break;
case 7:
reverse = true;
break;
case 8: style.display = 'none'; break; case 8: style.display = 'none'; break;
case 9: style['text-decoration'] = 'line-through'; 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 22:
case 23: style = { ...style, 'font-weight': undefined, 'font-style': undefined, 'opacity': undefined }; break; delete style['font-weight'];
case 24: style = { ...style, 'text-decoration': undefined }; break; 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 30:
case 31: case 31:
case 32: case 32:
@ -41,8 +63,12 @@ export function ansi2html(text: string): string {
case 34: case 34:
case 35: case 35:
case 36: case 36:
case 37: style.color = ansiColors[code - 30]; break; case 37:
case 39: style = { ...style, color: undefined }; break; fg = ansiColors[code - 30];
break;
case 39:
fg = defaultColors?.fg;
break;
case 40: case 40:
case 41: case 41:
case 42: case 42:
@ -50,8 +76,12 @@ export function ansi2html(text: string): string {
case 44: case 44:
case 45: case 45:
case 46: case 46:
case 47: style['background-color'] = ansiColors[code - 40]; break; case 47:
case 49: style = { ...style, 'background-color': undefined }; break; bg = ansiColors[code - 40];
break;
case 49:
bg = defaultColors?.bg;
break;
case 53: style['text-decoration'] = 'overline'; break; case 53: style['text-decoration'] = 'overline'; break;
case 90: case 90:
case 91: case 91:
@ -60,7 +90,9 @@ export function ansi2html(text: string): string {
case 94: case 94:
case 95: case 95:
case 96: case 96:
case 97: style.color = brightAnsiColors[code - 90]; break; case 97:
fg = brightAnsiColors[code - 90];
break;
case 100: case 100:
case 101: case 101:
case 102: case 102:
@ -68,10 +100,19 @@ export function ansi2html(text: string): string {
case 104: case 104:
case 105: case 105:
case 106: case 106:
case 107: style['background-color'] = brightAnsiColors[code - 100]; break; case 107:
bg = brightAnsiColors[code - 100];
break;
} }
} else if (text) { } 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(''); return tokens.join('');

View file

@ -472,7 +472,7 @@ for (const useIntermediateMergeReport of [true, false] as const) {
await showReport(); await showReport();
await page.click('text=fails'); 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 }) => { 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); expect(result.exitCode).toBe(1);
await showReport(); await showReport();
await page.click('text="is a test"'); 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 }) => { 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); expect(result.exitCode).toBe(1);
await showReport(); await showReport();
await page.click('text="is a test"'); await page.click('text="is a test"');
const stricken = await page.locator('css=strike').innerText(); await expect(page.locator('.test-error-view').getByText('old')).toHaveCSS('text-decoration', 'line-through solid rgb(205, 49, 49)');
expect(stricken).toBe('old'); 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 }) => { 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); expect(result.exitCode).toBe(1);
await showReport(); await showReport();
await page.locator('text=sample').first().click(); await page.getByText('sample').first().click();
await expect(page.locator('text=ouch')).toHaveCount(1); await expect(page.getByText('ouch')).toHaveCount(2);
await page.locator('text=All').first().click(); await page.getByText('All').first().click();
await page.locator('text=sample').nth(1).click(); await page.getByText('sample').nth(1).click();
await expect(page.locator('text=Before Hooks')).toBeVisible(); await expect(page.getByText('Before Hooks')).toBeVisible();
await expect(page.locator('text=ouch')).toBeHidden(); await expect(page.getByText('ouch')).toBeHidden();
}); });
test('should group similar / loop steps', async ({ runInlineTest, showReport, page }) => { test('should group similar / loop steps', async ({ runInlineTest, showReport, page }) => {