diff --git a/packages/playwright-core/src/utils/fileUtils.ts b/packages/playwright-core/src/utils/fileUtils.ts index 751878a869..0b2f28143b 100644 --- a/packages/playwright-core/src/utils/fileUtils.ts +++ b/packages/playwright-core/src/utils/fileUtils.ts @@ -52,3 +52,7 @@ export async function copyFileAndMakeWritable(from: string, to: string) { export function sanitizeForFilePath(s: string) { return s.replace(/[\x00-\x2C\x2E-\x2F\x3A-\x40\x5B-\x60\x7B-\x7F]+/g, '-'); } + +export function toPosixPath(aPath: string): string { + return aPath.split(path.sep).join(path.posix.sep); +} diff --git a/packages/playwright/src/common/suiteUtils.ts b/packages/playwright/src/common/suiteUtils.ts index 7994c10e04..6c3015772f 100644 --- a/packages/playwright/src/common/suiteUtils.ts +++ b/packages/playwright/src/common/suiteUtils.ts @@ -15,7 +15,7 @@ */ import path from 'path'; -import { calculateSha1 } from 'playwright-core/lib/utils'; +import { calculateSha1, toPosixPath } from 'playwright-core/lib/utils'; import type { Suite, TestCase } from './test'; import type { FullProjectInternal } from './config'; import type { Matcher, TestFileFilter } from '../util'; @@ -39,9 +39,10 @@ export function filterTestsRemoveEmptySuites(suite: Suite, filter: (test: TestCa suite._entries = suite._entries.filter(e => entries.has(e)); // Preserve the order. return !!suite._entries.length; } + export function bindFileSuiteToProject(project: FullProjectInternal, suite: Suite): Suite { - const relativeFile = path.relative(project.project.testDir, suite.location!.file).split(path.sep).join('/'); - const fileId = calculateSha1(relativeFile).slice(0, 20); + const relativeFile = path.relative(project.project.testDir, suite.location!.file); + const fileId = calculateSha1(toPosixPath(relativeFile)).slice(0, 20); // Clone suite. const result = suite._deepClone(); @@ -51,7 +52,8 @@ export function bindFileSuiteToProject(project: FullProjectInternal, suite: Suit result.forEachTest((test, suite) => { suite._fileId = fileId; // At the point of the query, suite is not yet attached to the project, so we only get file, describe and test titles. - const testIdExpression = `[project=${project.id}]${test.titlePath().join('\x1e')}`; + const [file, ...titles] = test.titlePath(); + const testIdExpression = `[project=${project.id}]${toPosixPath(file)}\x1e${titles.join('\x1e')}`; const testId = fileId + '-' + calculateSha1(testIdExpression).slice(0, 20); test.id = testId; test._projectId = project.id; diff --git a/packages/playwright/src/reporters/html.ts b/packages/playwright/src/reporters/html.ts index dbebacc176..74bf628932 100644 --- a/packages/playwright/src/reporters/html.ts +++ b/packages/playwright/src/reporters/html.ts @@ -20,11 +20,10 @@ import fs from 'fs'; import path from 'path'; import type { TransformCallback } from 'stream'; import { Transform } from 'stream'; -import { toPosixPath } from './json'; import { codeFrameColumns } from '../transform/babelBundle'; import type { FullResult, FullConfig, Location, Suite, TestCase as TestCasePublic, TestResult as TestResultPublic, TestStep as TestStepPublic, TestError } from '../../types/testReporter'; import type { SuitePrivate } from '../../types/reporterPrivate'; -import { HttpServer, assert, calculateSha1, copyFileAndMakeWritable, gracefullyProcessExitDoNotHang, removeFolders, sanitizeForFilePath } from 'playwright-core/lib/utils'; +import { HttpServer, assert, calculateSha1, copyFileAndMakeWritable, gracefullyProcessExitDoNotHang, removeFolders, sanitizeForFilePath, toPosixPath } from 'playwright-core/lib/utils'; import { colors, formatError, formatResultFailure, stripAnsiEscapes } from './base'; import { resolveReporterOutputPath } from '../util'; import type { Metadata } from '../../types/test'; diff --git a/packages/playwright/src/reporters/json.ts b/packages/playwright/src/reporters/json.ts index d677079ab5..41c26b653c 100644 --- a/packages/playwright/src/reporters/json.ts +++ b/packages/playwright/src/reporters/json.ts @@ -18,15 +18,10 @@ import fs from 'fs'; import path from 'path'; import type { FullConfig, TestCase, Suite, TestResult, TestError, TestStep, FullResult, Location, JSONReport, JSONReportSuite, JSONReportSpec, JSONReportTest, JSONReportTestResult, JSONReportTestStep, JSONReportError } from '../../types/testReporter'; import { formatError, prepareErrorStack } from './base'; -import { MultiMap } from 'playwright-core/lib/utils'; -import { assert } from 'playwright-core/lib/utils'; +import { MultiMap, assert, toPosixPath } from 'playwright-core/lib/utils'; import { getProjectId } from '../common/config'; import EmptyReporter from './empty'; -export function toPosixPath(aPath: string): string { - return aPath.split(path.sep).join(path.posix.sep); -} - class JSONReporter extends EmptyReporter { config!: FullConfig; suite!: Suite; diff --git a/tests/playwright-test/reporter.spec.ts b/tests/playwright-test/reporter.spec.ts index 4157310dd9..653728fd78 100644 --- a/tests/playwright-test/reporter.spec.ts +++ b/tests/playwright-test/reporter.spec.ts @@ -797,4 +797,30 @@ var import_test = __toModule(require("@playwright/test")); 4 | `); }); }); -} \ No newline at end of file +} + +test('should report a stable test.id', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'reporter.ts': ` + class Reporter { + onTestBegin(test) { + console.log('\\n%%testbegin-' + test.id); + } + } + export default Reporter; + `, + 'playwright.config.ts': ` + module.exports = { reporter: [[ './reporter.ts' ]] }; + `, + 'a.test.ts': ` + import { test, expect } from '@playwright/test'; + test('example test', async ({}) => { + }); + ` + }, { reporter: '', workers: 1 }); + + expect(result.exitCode).toBe(0); + expect(result.outputLines).toEqual([ + 'testbegin-20289bcdad95a5e18c38-8b63c3695b9c8bd62d98', + ]); +});