diff --git a/packages/html-reporter/src/testCaseView.tsx b/packages/html-reporter/src/testCaseView.tsx
index aa85522a8fd01..2e8e71cf11b2c 100644
--- a/packages/html-reporter/src/testCaseView.tsx
+++ b/packages/html-reporter/src/testCaseView.tsx
@@ -25,7 +25,7 @@ import { statusIcon } from './statusIcon';
import './testCaseView.css';
import { TestResultView } from './testResultView';
import { linkifyText } from '@web/renderUtils';
-import { msToString } from './utils';
+import { msToString } from '@isomorphic/formatUtils';
import { clsx } from '@web/uiUtils';
import { CopyToClipboardContainer } from './copyToClipboard';
import { HeaderView } from './headerView';
diff --git a/packages/html-reporter/src/testFileView.tsx b/packages/html-reporter/src/testFileView.tsx
index 4022094bf6700..688e1b2f8b799 100644
--- a/packages/html-reporter/src/testFileView.tsx
+++ b/packages/html-reporter/src/testFileView.tsx
@@ -16,7 +16,7 @@
import type { TestCaseSummary, TestFileSummary } from './types';
import * as React from 'react';
-import { msToString } from './utils';
+import { msToString } from '@isomorphic/formatUtils';
import { Chip } from './chip';
import { Link, LinkBadge, testResultHref, TraceLink, useSearchParams } from './links';
import { statusIcon } from './statusIcon';
diff --git a/packages/html-reporter/src/testFilesView.tsx b/packages/html-reporter/src/testFilesView.tsx
index cf6825108ef7c..84dbc988093bd 100644
--- a/packages/html-reporter/src/testFilesView.tsx
+++ b/packages/html-reporter/src/testFilesView.tsx
@@ -19,7 +19,7 @@ import * as React from 'react';
import { TestFileView } from './testFileView';
import './testFileView.css';
import './chip.css';
-import { msToString } from './utils';
+import { msToString } from '@isomorphic/formatUtils';
import { Chip } from './chip';
import { CodeSnippet } from './testErrorView';
import * as icons from './icons';
diff --git a/packages/html-reporter/src/testResultView.tsx b/packages/html-reporter/src/testResultView.tsx
index cb8e6313978c6..11ebc4835a724 100644
--- a/packages/html-reporter/src/testResultView.tsx
+++ b/packages/html-reporter/src/testResultView.tsx
@@ -17,7 +17,8 @@
import type { TestAttachment, TestCase, TestCaseSummary, TestResult, TestStep } from './types';
import * as React from 'react';
import { TreeItem } from './treeItem';
-import { formatUrl, msToString } from './utils';
+import { formatUrl } from './utils';
+import { msToString } from '@isomorphic/formatUtils';
import { AutoChip } from './chip';
import { traceImage } from './images';
import { Anchor, AttachmentLink, generateTraceUrl, testResultHref, useSearchParams } from './links';
diff --git a/packages/html-reporter/src/utils.ts b/packages/html-reporter/src/utils.ts
index 2737b28311de0..9ea7a26c1ef12 100644
--- a/packages/html-reporter/src/utils.ts
+++ b/packages/html-reporter/src/utils.ts
@@ -14,32 +14,6 @@
limitations under the License.
*/
-export function msToString(ms: number): string {
- if (!isFinite(ms))
- return '-';
-
- if (ms === 0)
- return '0ms';
-
- if (ms < 1000)
- return ms.toFixed(0) + 'ms';
-
- const seconds = ms / 1000;
- if (seconds < 60)
- return seconds.toFixed(1) + 's';
-
- const minutes = seconds / 60;
- if (minutes < 60)
- return minutes.toFixed(1) + 'm';
-
- const hours = minutes / 60;
- if (hours < 24)
- return hours.toFixed(1) + 'h';
-
- const days = hours / 24;
- return days.toFixed(1) + 'd';
-}
-
// hash string to integer in range [0, 6] for color index, to get same color for same tag
export function hashStringToInt(str: string) {
let hash = 0;
diff --git a/packages/playwright-core/src/tools/trace/traceCli.ts b/packages/playwright-core/src/tools/trace/traceCli.ts
index e08111364937e..f51ece2e534a5 100644
--- a/packages/playwright-core/src/tools/trace/traceCli.ts
+++ b/packages/playwright-core/src/tools/trace/traceCli.ts
@@ -23,6 +23,7 @@ import { TraceModel, buildActionTree } from '../../utils/isomorphic/trace/traceM
import { TraceLoader } from '../../utils/isomorphic/trace/traceLoader';
import { renderTitleForCall } from '../../utils/isomorphic/protocolFormatter';
import { asLocatorDescription } from '../../utils/isomorphic/locatorGenerators';
+import { msToString, bytesToString } from '../../utils/isomorphic/formatUtils';
import { ZipTraceLoaderBackend } from './traceParser';
import type { ActionTraceEventInContext } from '@isomorphic/trace/traceModel';
@@ -149,43 +150,6 @@ export async function loadTraceModel(traceFile: string): Promise {
return (await loadTrace(traceFile)).model;
}
-function msToString(ms: number): string {
- if (ms < 0 || !isFinite(ms))
- return '-';
- if (ms === 0)
- return '0';
- if (ms < 1000)
- return ms.toFixed(0) + 'ms';
- const seconds = ms / 1000;
- if (seconds < 60)
- return seconds.toFixed(1) + 's';
- const minutes = seconds / 60;
- if (minutes < 60)
- return minutes.toFixed(1) + 'm';
- const hours = minutes / 60;
- if (hours < 24)
- return hours.toFixed(1) + 'h';
- const days = hours / 24;
- return days.toFixed(1) + 'd';
-}
-
-function bytesToString(bytes: number): string {
- if (bytes < 0 || !isFinite(bytes))
- return '-';
- if (bytes === 0)
- return '0';
- if (bytes < 1000)
- return bytes.toFixed(0);
- const kb = bytes / 1024;
- if (kb < 1000)
- return kb.toFixed(1) + 'K';
- const mb = kb / 1024;
- if (mb < 1000)
- return mb.toFixed(1) + 'M';
- const gb = mb / 1024;
- return gb.toFixed(1) + 'G';
-}
-
function formatTimestamp(ms: number, base: number): string {
const relative = ms - base;
if (relative < 0)
diff --git a/packages/playwright-core/src/utils.ts b/packages/playwright-core/src/utils.ts
index a1ef2907198e4..405ea86d5d95b 100644
--- a/packages/playwright-core/src/utils.ts
+++ b/packages/playwright-core/src/utils.ts
@@ -29,6 +29,7 @@ export * from './utils/isomorphic/rtti';
export * from './utils/isomorphic/semaphore';
export * from './utils/isomorphic/stackTrace';
export * from './utils/isomorphic/stringUtils';
+export * from './utils/isomorphic/formatUtils';
export * from './utils/isomorphic/time';
export * from './utils/isomorphic/timeoutRunner';
export * from './utils/isomorphic/urlMatch';
diff --git a/packages/playwright-core/src/utils/isomorphic/formatUtils.ts b/packages/playwright-core/src/utils/isomorphic/formatUtils.ts
new file mode 100644
index 0000000000000..b03a9e3bb180a
--- /dev/null
+++ b/packages/playwright-core/src/utils/isomorphic/formatUtils.ts
@@ -0,0 +1,63 @@
+/**
+ * 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 function msToString(ms: number): string {
+ if (ms < 0 || !isFinite(ms))
+ return '-';
+
+ if (ms === 0)
+ return '0ms';
+
+ if (ms < 1000)
+ return ms.toFixed(0) + 'ms';
+
+ const seconds = ms / 1000;
+ if (seconds < 60)
+ return seconds.toFixed(1) + 's';
+
+ const minutes = seconds / 60;
+ if (minutes < 60)
+ return minutes.toFixed(1) + 'm';
+
+ const hours = minutes / 60;
+ if (hours < 24)
+ return hours.toFixed(1) + 'h';
+
+ const days = hours / 24;
+ return days.toFixed(1) + 'd';
+}
+
+export function bytesToString(bytes: number): string {
+ if (bytes < 0 || !isFinite(bytes))
+ return '-';
+
+ if (bytes === 0)
+ return '0';
+
+ if (bytes < 1000)
+ return bytes.toFixed(0);
+
+ const kb = bytes / 1024;
+ if (kb < 1000)
+ return kb.toFixed(1) + 'K';
+
+ const mb = kb / 1024;
+ if (mb < 1000)
+ return mb.toFixed(1) + 'M';
+
+ const gb = mb / 1024;
+ return gb.toFixed(1) + 'G';
+}
diff --git a/packages/playwright-core/src/utilsBundle.ts b/packages/playwright-core/src/utilsBundle.ts
index 2eab1d9872899..e22bf61a8b820 100644
--- a/packages/playwright-core/src/utilsBundle.ts
+++ b/packages/playwright-core/src/utilsBundle.ts
@@ -39,29 +39,3 @@ export const yaml: typeof import('../bundles/utils/node_modules/yaml') = require
export type { Range as YAMLRange, Scalar as YAMLScalar, YAMLError, YAMLMap, YAMLSeq } from '../bundles/utils/node_modules/yaml';
export type { Command } from '../bundles/utils/node_modules/commander';
export type { EventEmitter as WebSocketEventEmitter, RawData as WebSocketRawData, WebSocket, WebSocketServer } from '../bundles/utils/node_modules/@types/ws';
-
-export function ms(ms: number): string {
- if (!isFinite(ms))
- return '-';
-
- if (ms === 0)
- return '0ms';
-
- if (ms < 1000)
- return ms.toFixed(0) + 'ms';
-
- const seconds = ms / 1000;
- if (seconds < 60)
- return seconds.toFixed(1) + 's';
-
- const minutes = seconds / 60;
- if (minutes < 60)
- return minutes.toFixed(1) + 'm';
-
- const hours = minutes / 60;
- if (hours < 24)
- return hours.toFixed(1) + 'h';
-
- const days = hours / 24;
- return days.toFixed(1) + 'd';
-}
diff --git a/packages/playwright/src/reporters/base.ts b/packages/playwright/src/reporters/base.ts
index 200a975740972..f00437c477cdc 100644
--- a/packages/playwright/src/reporters/base.ts
+++ b/packages/playwright/src/reporters/base.ts
@@ -16,8 +16,7 @@
import path from 'path';
-import { getPackageManagerExecCommand, parseErrorStack } from 'playwright-core/lib/utils';
-import { ms as milliseconds } from 'playwright-core/lib/utilsBundle';
+import { getPackageManagerExecCommand, msToString as milliseconds, parseErrorStack } from 'playwright-core/lib/utils';
import { colors as realColors, noColors } from 'playwright-core/lib/utils';
import { ansiRegex, resolveReporterOutputPath, stripAnsiEscapes } from '../util';
diff --git a/packages/playwright/src/reporters/github.ts b/packages/playwright/src/reporters/github.ts
index ceac0d89147b5..c19c7ab621347 100644
--- a/packages/playwright/src/reporters/github.ts
+++ b/packages/playwright/src/reporters/github.ts
@@ -16,8 +16,7 @@
import path from 'path';
-import { noColors } from 'playwright-core/lib/utils';
-import { ms as milliseconds } from 'playwright-core/lib/utilsBundle';
+import { msToString as milliseconds, noColors } from 'playwright-core/lib/utils';
import { TerminalReporter, formatResultFailure, formatRetry } from './base';
import { stripAnsiEscapes } from '../util';
diff --git a/packages/playwright/src/reporters/list.ts b/packages/playwright/src/reporters/list.ts
index 9315e3bfa38c6..26216edababd2 100644
--- a/packages/playwright/src/reporters/list.ts
+++ b/packages/playwright/src/reporters/list.ts
@@ -14,8 +14,7 @@
* limitations under the License.
*/
-import { getAsBooleanFromENV } from 'playwright-core/lib/utils';
-import { ms as milliseconds } from 'playwright-core/lib/utilsBundle';
+import { getAsBooleanFromENV, msToString as milliseconds } from 'playwright-core/lib/utils';
import { markErrorsAsReported, TerminalReporter, stepSuffix } from './base';
import { stripAnsiEscapes } from '../util';
diff --git a/packages/recorder/src/callLog.tsx b/packages/recorder/src/callLog.tsx
index eafc9f9826816..bc28d82a673f5 100644
--- a/packages/recorder/src/callLog.tsx
+++ b/packages/recorder/src/callLog.tsx
@@ -17,7 +17,8 @@
import './callLog.css';
import * as React from 'react';
import type { CallLog } from './recorderTypes';
-import { clsx, msToString } from '@web/uiUtils';
+import { clsx } from '@web/uiUtils';
+import { msToString } from '@isomorphic/formatUtils';
import { asLocator } from '@isomorphic/locatorGenerators';
import type { Language } from '@isomorphic/locatorGenerators';
diff --git a/packages/trace-viewer/src/ui/actionList.tsx b/packages/trace-viewer/src/ui/actionList.tsx
index 56fb5e0e16319..7d666db23b648 100644
--- a/packages/trace-viewer/src/ui/actionList.tsx
+++ b/packages/trace-viewer/src/ui/actionList.tsx
@@ -15,7 +15,8 @@
*/
import type { ActionTraceEvent } from '@trace/trace';
-import { clsx, msToString } from '@web/uiUtils';
+import { clsx } from '@web/uiUtils';
+import { msToString } from '@isomorphic/formatUtils';
import * as React from 'react';
import './actionList.css';
import { stats, buildActionTree } from '@isomorphic/trace/traceModel';
diff --git a/packages/trace-viewer/src/ui/callTab.tsx b/packages/trace-viewer/src/ui/callTab.tsx
index 223027fc8c01a..943ec4a897d4e 100644
--- a/packages/trace-viewer/src/ui/callTab.tsx
+++ b/packages/trace-viewer/src/ui/callTab.tsx
@@ -16,7 +16,8 @@
import type { SerializedValue } from '@protocol/channels';
import type { ActionTraceEvent } from '@trace/trace';
-import { clsx, msToString } from '@web/uiUtils';
+import { clsx } from '@web/uiUtils';
+import { msToString } from '@isomorphic/formatUtils';
import * as React from 'react';
import './callTab.css';
import { CopyToClipboard } from './copyToClipboard';
diff --git a/packages/trace-viewer/src/ui/consoleTab.tsx b/packages/trace-viewer/src/ui/consoleTab.tsx
index 8194e973e4d44..1b88a15b956d5 100644
--- a/packages/trace-viewer/src/ui/consoleTab.tsx
+++ b/packages/trace-viewer/src/ui/consoleTab.tsx
@@ -20,7 +20,8 @@ import './consoleTab.css';
import type { TraceModel } from '@isomorphic/trace/traceModel';
import { ListView } from '@web/components/listView';
import type { Boundaries } from './geometry';
-import { clsx, msToString } from '@web/uiUtils';
+import { clsx } from '@web/uiUtils';
+import { msToString } from '@isomorphic/formatUtils';
import { ansi2html } from '@web/ansi2html';
import { PlaceholderPanel } from './placeholderPanel';
diff --git a/packages/trace-viewer/src/ui/logTab.tsx b/packages/trace-viewer/src/ui/logTab.tsx
index a98c433855ff0..7f0ac6acfa9b6 100644
--- a/packages/trace-viewer/src/ui/logTab.tsx
+++ b/packages/trace-viewer/src/ui/logTab.tsx
@@ -18,7 +18,7 @@ import type { ActionTraceEventInContext } from '@isomorphic/trace/traceModel';
import * as React from 'react';
import { ListView } from '@web/components/listView';
import { PlaceholderPanel } from './placeholderPanel';
-import { msToString } from '@web/uiUtils';
+import { msToString } from '@isomorphic/formatUtils';
import './logTab.css';
const LogList = ListView<{ message: string, time: string }>;
diff --git a/packages/trace-viewer/src/ui/metadataView.tsx b/packages/trace-viewer/src/ui/metadataView.tsx
index 1a863f4c4ce53..4fc57799f021d 100644
--- a/packages/trace-viewer/src/ui/metadataView.tsx
+++ b/packages/trace-viewer/src/ui/metadataView.tsx
@@ -14,7 +14,7 @@
limitations under the License.
*/
-import { msToString } from '@web/uiUtils';
+import { msToString } from '@isomorphic/formatUtils';
import * as React from 'react';
import type { TraceModel } from '@isomorphic/trace/traceModel';
import './callTab.css';
diff --git a/packages/trace-viewer/src/ui/networkResourceDetails.tsx b/packages/trace-viewer/src/ui/networkResourceDetails.tsx
index 4ca5efc71038f..63be975e26953 100644
--- a/packages/trace-viewer/src/ui/networkResourceDetails.tsx
+++ b/packages/trace-viewer/src/ui/networkResourceDetails.tsx
@@ -25,7 +25,8 @@ import { CopyToClipboardTextButton } from './copyToClipboard';
import { getAPIRequestCodeGen } from './codegen';
import type { Language } from '@isomorphic/locatorGenerators';
import { isJsonMimeType, isXmlMimeType } from '@isomorphic/mimeType';
-import { msToString, useAsyncMemo, useSetting } from '@web/uiUtils';
+import { useAsyncMemo, useSetting } from '@web/uiUtils';
+import { msToString } from '@isomorphic/formatUtils';
import type { Entry } from '@trace/har';
import { useTraceModel } from './traceModelContext';
import { Expandable } from '@web/components/expandable';
diff --git a/packages/trace-viewer/src/ui/networkTab.tsx b/packages/trace-viewer/src/ui/networkTab.tsx
index af5bd13297eb1..4d02cc558e710 100644
--- a/packages/trace-viewer/src/ui/networkTab.tsx
+++ b/packages/trace-viewer/src/ui/networkTab.tsx
@@ -18,7 +18,7 @@ import * as React from 'react';
import type { Boundaries } from './geometry';
import './networkTab.css';
import { NetworkResourceDetails } from './networkResourceDetails';
-import { bytesToString, msToString } from '@web/uiUtils';
+import { bytesToString, msToString } from '@isomorphic/formatUtils';
import { PlaceholderPanel } from './placeholderPanel';
import { context, type ResourceEntry } from '@isomorphic/trace/traceModel';
import type { TraceModel } from '@isomorphic/trace/traceModel';
diff --git a/packages/trace-viewer/src/ui/timeline.tsx b/packages/trace-viewer/src/ui/timeline.tsx
index 8da798d424ea4..855a6fa85fa27 100644
--- a/packages/trace-viewer/src/ui/timeline.tsx
+++ b/packages/trace-viewer/src/ui/timeline.tsx
@@ -14,7 +14,8 @@
* limitations under the License.
*/
-import { useSetting, msToString, useMeasure } from '@web/uiUtils';
+import { useSetting, useMeasure } from '@web/uiUtils';
+import { msToString } from '@isomorphic/formatUtils';
import { GlassPane } from '@web/shared/glassPane';
import * as React from 'react';
import type { Boundaries } from './geometry';
diff --git a/packages/trace-viewer/src/ui/uiModeTestListView.tsx b/packages/trace-viewer/src/ui/uiModeTestListView.tsx
index fe76b8a744c03..2caacf3bb2692 100644
--- a/packages/trace-viewer/src/ui/uiModeTestListView.tsx
+++ b/packages/trace-viewer/src/ui/uiModeTestListView.tsx
@@ -22,7 +22,7 @@ import { ToolbarButton } from '@web/components/toolbarButton';
import type { TreeState } from '@web/components/treeView';
import { TreeView } from '@web/components/treeView';
import '@web/third_party/vscode/codicon.css';
-import { msToString } from '@web/uiUtils';
+import { msToString } from '@isomorphic/formatUtils';
import type * as reporterTypes from 'playwright/types/testReporter';
import React from 'react';
import type { SourceLocation } from '@isomorphic/trace/traceModel';
diff --git a/packages/trace-viewer/src/ui/workbench.tsx b/packages/trace-viewer/src/ui/workbench.tsx
index 77a4a290b6c2d..e54b490d0256c 100644
--- a/packages/trace-viewer/src/ui/workbench.tsx
+++ b/packages/trace-viewer/src/ui/workbench.tsx
@@ -35,7 +35,8 @@ import { AnnotationsTab } from './annotationsTab';
import type { Boundaries } from './geometry';
import { InspectorTab } from './inspectorTab';
import { ToolbarButton } from '@web/components/toolbarButton';
-import { useSetting, msToString, clsx, usePartitionedState, togglePartition } from '@web/uiUtils';
+import { useSetting, clsx, usePartitionedState, togglePartition } from '@web/uiUtils';
+import { msToString } from '@isomorphic/formatUtils';
import './workbench.css';
import { testStatusIcon, testStatusText } from './testUtils';
import type { UITestStatus } from './testUtils';
diff --git a/packages/web/src/DEPS.list b/packages/web/src/DEPS.list
index 78980e8898bce..2903883d573fa 100644
--- a/packages/web/src/DEPS.list
+++ b/packages/web/src/DEPS.list
@@ -1,3 +1,4 @@
[*]
+@isomorphic/**
@playwright/experimental-ct-react
third_party/**
diff --git a/packages/web/src/uiUtils.ts b/packages/web/src/uiUtils.ts
index 1c9818f3fbac2..48b68f91e0a8a 100644
--- a/packages/web/src/uiUtils.ts
+++ b/packages/web/src/uiUtils.ts
@@ -16,6 +16,7 @@
import React from 'react';
+
// Recalculates the value when dependencies change.
export function useAsyncMemo(fn: () => Promise, deps: React.DependencyList, initialValue: T, resetValue?: T) {
const [value, setValue] = React.useState(initialValue);
@@ -66,54 +67,6 @@ export function useMeasureForRef(ref?: React.RefObject(array: S[], object: T, comparator: (object: T, b: S) => number, left?: number, right?: number): number {
let l = left || 0;
let r = right !== undefined ? right : array.length;
diff --git a/tests/playwright-test/reporter-html.spec.ts b/tests/playwright-test/reporter-html.spec.ts
index cc996651d1543..fc9be4c718ffe 100644
--- a/tests/playwright-test/reporter-html.spec.ts
+++ b/tests/playwright-test/reporter-html.spec.ts
@@ -20,7 +20,7 @@ import url from 'url';
import { test as baseTest, expect as baseExpect, createImage } from './playwright-test-fixtures';
import type { HttpServer } from '../../packages/playwright-core/lib/server/utils/httpServer';
import { startHtmlReportServer } from '../../packages/playwright/lib/reporters/html';
-import { msToString } from '../../packages/web/src/uiUtils';
+import { msToString } from '../../packages/playwright-core/src/utils/isomorphic/formatUtils';
const { spawnAsync } = require('../../packages/playwright-core/lib/utils');
const test = baseTest.extend<{ showReport: (reportFolder?: string) => Promise }>({