Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/playwright/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"./lib/mcp/index": "./lib/mcp/index.js",
"./lib/program": "./lib/program.js",
"./lib/runner": "./lib/runner/index.js",
"./lib/isomorphic": "./lib/isomorphic/index.js",
"./lib/transform/babelBundle": "./lib/transform/babelBundle.js",
"./jsx-runtime": {
"import": "./jsx-runtime.mjs",
Expand Down
17 changes: 17 additions & 0 deletions packages/playwright/src/isomorphic/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/**
* 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.
*/

export * from './testServerConnection';
56 changes: 2 additions & 54 deletions packages/playwright/src/reporters/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,13 @@
import path from 'path';

import realColors from 'colors/safe';
import * as getEastAsianWidth from 'get-east-asian-width';
import { noColors } from '@isomorphic/colors';
import { msToString } from '@isomorphic/formatUtils';
import { parseErrorStack } from '@isomorphic/stackTrace';
import { getPackageManagerExecCommand } from '@utils/env';
import { fitToWidth } from '@utils/stringWidth';

import { ansiRegex, resolveReporterOutputPath, stripAnsiEscapes } from '../util';
import { resolveReporterOutputPath, stripAnsiEscapes } from '../util';

import type { ReporterV2 } from './reporterV2';
import type { FullConfig, FullResult, Location, Suite, TestCase, TestError, TestResult, TestStep } from '../../types/testReporter';
Expand Down Expand Up @@ -618,58 +618,6 @@ export function prepareErrorStack(stack: string): {
return parseErrorStack(stack, path.sep, !!process.env.PWDEBUGIMPL);
}

function characterWidth(c: string) {
return getEastAsianWidth.eastAsianWidth(c.codePointAt(0)!);
}

function stringWidth(v: string) {
let width = 0;
for (const { segment } of new Intl.Segmenter(undefined, { granularity: 'grapheme' }).segment(v))
width += characterWidth(segment);
return width;
}

function suffixOfWidth(v: string, width: number) {
const segments = [...new Intl.Segmenter(undefined, { granularity: 'grapheme' }).segment(v)];
let suffixBegin = v.length;
for (const { segment, index } of segments.reverse()) {
const segmentWidth = stringWidth(segment);
if (segmentWidth > width)
break;
width -= segmentWidth;
suffixBegin = index;
}
return v.substring(suffixBegin);
}

// Leaves enough space for the "prefix" to also fit.
export function fitToWidth(line: string, width: number, prefix?: string): string {
const prefixLength = prefix ? stripAnsiEscapes(prefix).length : 0;
width -= prefixLength;
if (stringWidth(line) <= width)
return line;

// 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 = suffixOfWidth(parts[i], width);
const wasTruncated = part.length < parts[i].length;
if (wasTruncated && parts[i].length > 0) {
// Add ellipsis if we are truncating.
part = '\u2026' + suffixOfWidth(parts[i], width - 1);
}
taken.push(part);
width -= stringWidth(part);
}
}
return taken.reverse().join('');
}

function resolveFromEnv(name: string): string | undefined {
const value = process.env[name];
if (value)
Expand Down
24 changes: 2 additions & 22 deletions packages/playwright/src/reporters/html.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import { MultiMap } from '@isomorphic/multimap';
import { calculateSha1 } from '@utils/crypto';
import { copyFileAndMakeWritable, removeFolders, sanitizeForFilePath, toPosixPath } from '@utils/fileUtils';
import { getPackageManagerExecCommand } from '@utils/env';
import { HttpServer } from '@utils/httpServer';
import { serveFolder } from '@utils/httpServer';
import { gracefullyProcessExitDoNotHang } from '@utils/processLauncher';

import { CommonReporterOptions, formatError, formatResultFailure, internalScreen } from './base';
Expand Down Expand Up @@ -222,7 +222,7 @@ export async function showHTMLReport(reportFolder: string | undefined, host: str
gracefullyProcessExitDoNotHang(1);
return;
}
const server = startHtmlReportServer(folder);
const server = serveFolder(folder);
await server.start({ port, host, preferredPort: port ? undefined : 9323 });
let url = server.urlPrefix('human-readable');
writeLine('');
Expand All @@ -234,26 +234,6 @@ export async function showHTMLReport(reportFolder: string | undefined, host: str
await new Promise(() => {});
}

export function startHtmlReportServer(folder: string): HttpServer {
const server = new HttpServer();
server.routePrefix('/', (request, response) => {
let relativePath = new URL('http://localhost' + request.url).pathname;
if (relativePath.startsWith('/trace/file')) {
const url = new URL('http://localhost' + request.url!);
try {
return server.serveFile(request, response, url.searchParams.get('path')!);
} catch (e) {
return false;
}
}
if (relativePath === '/')
relativePath = '/index.html';
const absolutePath = path.join(folder, ...relativePath.split('/'));
return server.serveFile(request, response, absolutePath);
});
return server;
}

type DataMap = Map<string, { testFile: TestFile, testFileSummary: TestFileSummary }>;

class HtmlBuilder {
Expand Down
20 changes: 20 additions & 0 deletions packages/utils/httpServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -220,3 +220,23 @@ export class HttpServer {
}
}
}

export function serveFolder(folder: string): HttpServer {
const server = new HttpServer();
server.routePrefix('/', (request, response) => {
let relativePath = new URL('http://localhost' + request.url).pathname;
if (relativePath.startsWith('/trace/file')) {
const url = new URL('http://localhost' + request.url!);
try {
return server.serveFile(request, response, url.searchParams.get('path')!);
} catch (e) {
return false;
}
}
if (relativePath === '/')
relativePath = '/index.html';
const absolutePath = path.join(folder, ...relativePath.split('/'));
return server.serveFile(request, response, absolutePath);
});
return server;
}
1 change: 1 addition & 0 deletions packages/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export * from './profiler';
export * from './serializedFS';
export * from './socksProxy';
export * from './spawnAsync';
export * from './stringWidth';
export * from './task';
export * from './wsServer';
export * from './zipFile';
Expand Down
69 changes: 69 additions & 0 deletions packages/utils/stringWidth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/**
* 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 * as getEastAsianWidth from 'get-east-asian-width';
import { ansiRegex, stripAnsiEscapes } from '@isomorphic/stringUtils';

function characterWidth(c: string) {
return getEastAsianWidth.eastAsianWidth(c.codePointAt(0)!);
}

export function stringWidth(v: string) {
let width = 0;
for (const { segment } of new Intl.Segmenter(undefined, { granularity: 'grapheme' }).segment(v))
width += characterWidth(segment);
return width;
}

function suffixOfWidth(v: string, width: number) {
const segments = [...new Intl.Segmenter(undefined, { granularity: 'grapheme' }).segment(v)];
let suffixBegin = v.length;
for (const { segment, index } of segments.reverse()) {
const segmentWidth = stringWidth(segment);
if (segmentWidth > width)
break;
width -= segmentWidth;
suffixBegin = index;
}
return v.substring(suffixBegin);
}

export function fitToWidth(line: string, width: number, prefix?: string): string {
const prefixLength = prefix ? stripAnsiEscapes(prefix).length : 0;
width -= prefixLength;
if (stringWidth(line) <= width)
return line;

// 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 = suffixOfWidth(parts[i], width);
const wasTruncated = part.length < parts[i].length;
if (wasTruncated && parts[i].length > 0) {
// Add ellipsis if we are truncating.
part = '\u2026' + suffixOfWidth(parts[i], width - 1);
}
taken.push(part);
width -= stringWidth(part);
}
}
return taken.reverse().join('');
}
21 changes: 10 additions & 11 deletions tests/playwright-test/fit-to-width.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,21 +14,20 @@
* limitations under the License.
*/

import { base } from '../../packages/playwright/lib/runner';
const { fitToWidth } = base;
import { utils } from '../../packages/playwright-core/lib/coreBundle';
import { test, expect } from './playwright-test-fixtures';

test('chinese characters', () => {
expect(fitToWidth('你你好', 3)).toBe('…好');
expect(fitToWidth('你好你好', 4)).toBe('…好');
expect(utils.fitToWidth('你你好', 3)).toBe('…好');
expect(utils.fitToWidth('你好你好', 4)).toBe('…好');
});

test('surrogate pairs', () => {
expect(fitToWidth('🫣🤗', 2)).toBe('…');
expect(fitToWidth('🫣🤗', 3)).toBe('…🤗');
expect(fitToWidth('🚄🚄', 1)).toBe('…');
expect(fitToWidth('🚄🚄', 2)).toBe('…');
expect(fitToWidth('🚄🚄', 3)).toBe('…🚄');
expect(fitToWidth('🚄🚄', 4)).toBe('🚄🚄');
expect(fitToWidth('🧑‍🧑‍🧒🧑‍🧑‍🧒🧑‍🧑‍🧒', 4)).toBe('…🧑‍🧑‍🧒');
expect(utils.fitToWidth('🫣🤗', 2)).toBe('…');
expect(utils.fitToWidth('🫣🤗', 3)).toBe('…🤗');
expect(utils.fitToWidth('🚄🚄', 1)).toBe('…');
expect(utils.fitToWidth('🚄🚄', 2)).toBe('…');
expect(utils.fitToWidth('🚄🚄', 3)).toBe('…🚄');
expect(utils.fitToWidth('🚄🚄', 4)).toBe('🚄🚄');
expect(utils.fitToWidth('🧑‍🧑‍🧒🧑‍🧑‍🧒🧑‍🧑‍🧒', 4)).toBe('…🧑‍🧑‍🧒');
});
4 changes: 1 addition & 3 deletions tests/playwright-test/reporter-blob.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,6 @@
import * as fs from 'fs';
import type { PlaywrightTestConfig } from '@playwright/test';
import path from 'path';
import { html } from '../../packages/playwright/lib/runner';
const { startHtmlReportServer } = html;
import { expect as baseExpect, test as baseTest, stripAnsi } from './playwright-test-fixtures';
import { extractZip } from '../../packages/utils/third_party/extractZip';
import * as yazl from 'yazl';
Expand All @@ -39,7 +37,7 @@ const test = baseTest.extend<{
let server: HttpServer | undefined;
await use(async (reportFolder?: string) => {
reportFolder ??= test.info().outputPath('playwright-report');
server = startHtmlReportServer(reportFolder) as HttpServer;
server = utils.serveFolder(reportFolder);
await server.start();
await page.goto(server.urlPrefix('precise'));
});
Expand Down
4 changes: 1 addition & 3 deletions tests/playwright-test/reporter-html.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,6 @@ import fs from 'fs';
import path from 'path';
import url from 'url';
import { test as baseTest, expect as baseExpect, createImage } from './playwright-test-fixtures';
import { html } from '../../packages/playwright/lib/runner';
const { startHtmlReportServer } = html;
import { iso, utils } from '../../packages/playwright-core/lib/coreBundle';

type HttpServer = utils.HttpServer;
Expand All @@ -32,7 +30,7 @@ const test = baseTest.extend<{ showReport: (reportFolder?: string) => Promise<vo
let server: HttpServer | undefined;
await use(async (reportFolder?: string) => {
reportFolder ??= testInfo.outputPath('playwright-report');
server = startHtmlReportServer(reportFolder) as HttpServer;
server = utils.serveFolder(reportFolder);
await server.start();
await page.goto(server.urlPrefix('precise'));
});
Expand Down
24 changes: 12 additions & 12 deletions tests/playwright-test/stable-test-runner/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion tests/playwright-test/stable-test-runner/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"private": true,
"dependencies": {
"@playwright/test": "^1.60.0-alpha-2026-04-06"
"@playwright/test": "^1.60.0-alpha-2026-04-13"
}
}
2 changes: 1 addition & 1 deletion tests/playwright-test/test-server.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
// @ts-nocheck

import { test as baseTest, expect } from './ui-mode-fixtures';
import { TestServerConnection } from '../../packages/playwright/lib/runner';
import { TestServerConnection } from '../../packages/playwright/lib/isomorphic';
import { playwrightCtConfigText } from './playwright-test-fixtures';
import ws from 'ws';
import type { TestChildProcess } from '../config/commonFixtures';
Expand Down
Loading
Loading