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
40 changes: 36 additions & 4 deletions playwright/src/playwrightx/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -520,12 +520,17 @@ export function parseJsonContent(
}
}

// 处理错误信息
// 处理错误信息:仅在明确失败状态(failed/timedOut/interrupted)下追加"错误信息"标题,
// 避免 skipped 等非失败状态被误标记为错误。
if (result.errors) {
log.info(
`发现 errors。errors 数量: ${result.errors.length}`,
);
if (result.status !== "passed") {
const failedStatuses = ["failed", "timedout", "interrupted"];
const isFailedStatus = failedStatuses.includes(
(result.status || "").toLowerCase(),
);
if (isFailedStatus) {
specErrorCtx += "\n==== 错误信息 ====\n";
}
for (const error of result.errors) {
Expand Down Expand Up @@ -727,6 +732,33 @@ export function groupTestCasesByPath(
return groupedTestCases;
}

// Playwright 原生状态 → TestSolar ResultType 的映射关系
// Playwright 官方状态取值:'passed' | 'failed' | 'timedOut' | 'skipped' | 'interrupted'
// 其中 'skipped' 必须映射为 IGNORED,避免与 FAILED 混淆;
// 'timedOut' / 'interrupted' 归为 FAILED;其余未知状态按 UNKNOWN 处理以便定位问题。
export function mapPlaywrightStatus(status: string | undefined | null): ResultType {
switch ((status || "").toLowerCase()) {
case "passed":
return ResultType.SUCCEED;
case "skipped":
return ResultType.IGNORED;
case "failed":
case "timedout":
case "interrupted":
return ResultType.FAILED;
default:
return ResultType.UNKNOWN;
}
}

// 根据 ResultType 获取对应的日志等级,跳过/未知场景不使用 ERROR 以避免误判为失败
export function getLogLevelByResultType(resultType: ResultType): LogLevel {
if (resultType === ResultType.FAILED) {
return LogLevel.ERROR;
}
return LogLevel.INFO;
}

export function createTestResults(
output: Record<string, SpecResult[]>,
testIdentifiers: string[],
Expand Down Expand Up @@ -768,14 +800,14 @@ export function createTestResults(

const startTime = new Date(result.startTime * 1000).toISOString();
const endTime = new Date(result.endTime * 1000).toISOString();
const resultType = result.result === "passed" ? ResultType.SUCCEED : ResultType.FAILED;
const resultType = mapPlaywrightStatus(result.result);
const message = result.message || "";
const content = result.content || "";
const attachments = result.attachments || [];

const testLog = new TestCaseLog(
startTime,
result.result === "passed" ? LogLevel.INFO : LogLevel.ERROR,
getLogLevelByResultType(resultType),
content,
attachments,
undefined,
Expand Down
253 changes: 252 additions & 1 deletion playwright/tests/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,14 @@ import {
parsePlaywrightReport,
encodeQueryParams,
createRunningTestResults,
mapPlaywrightStatus,
getLogLevelByResultType,
} from "../src/playwrightx/utils";

import * as path from "path";
import log from 'testsolar-oss-sdk/src/testsolar_sdk/logger';
import Reporter from "testsolar-oss-sdk/src/testsolar_sdk/reporter";
import { Attachment, AttachmentType } from "testsolar-oss-sdk/src/testsolar_sdk/model/testresult";
import { Attachment, AttachmentType, ResultType, LogLevel } from "testsolar-oss-sdk/src/testsolar_sdk/model/testresult";


describe("parsePlaywrightReport", () => { // 更改为与测试函数名称一致
Expand Down Expand Up @@ -618,4 +620,253 @@ describe("createRunningTestResults", () => {
// 验证结果
expect(TestCase).toHaveBeenCalledTimes(0);
});
});

/**
* TAPD: playwright-错误信息中区分 skipped 状态和 failed 状态
* https://tapd.woa.com/tapd_fe/69995517/story/detail/1069995517134057272
*
* 测试目标:验证 Playwright 原生状态到 TestSolar ResultType 的正确映射,
* 确保 skipped 用例不会被误判为 failed。
*/
describe("mapPlaywrightStatus", () => {
describe("1. 核心功能测试", () => {
test("场景1: passed 状态映射为 SUCCEED", () => {
expect(mapPlaywrightStatus("passed")).toBe(ResultType.SUCCEED);
});

test("场景2: skipped 状态映射为 IGNORED(关键修复点)", () => {
expect(mapPlaywrightStatus("skipped")).toBe(ResultType.IGNORED);
});

test("场景3: failed 状态映射为 FAILED", () => {
expect(mapPlaywrightStatus("failed")).toBe(ResultType.FAILED);
});

test("场景4: timedOut 状态映射为 FAILED", () => {
expect(mapPlaywrightStatus("timedOut")).toBe(ResultType.FAILED);
});

test("场景5: interrupted 状态映射为 FAILED", () => {
expect(mapPlaywrightStatus("interrupted")).toBe(ResultType.FAILED);
});
});

describe("2. 边界条件测试", () => {
test("场景1: 大小写不敏感 - PASSED", () => {
expect(mapPlaywrightStatus("PASSED")).toBe(ResultType.SUCCEED);
});

test("场景2: 大小写不敏感 - Skipped", () => {
expect(mapPlaywrightStatus("Skipped")).toBe(ResultType.IGNORED);
});

test("场景3: 空字符串映射为 UNKNOWN", () => {
expect(mapPlaywrightStatus("")).toBe(ResultType.UNKNOWN);
});

test("场景4: null/undefined 映射为 UNKNOWN", () => {
expect(mapPlaywrightStatus(null)).toBe(ResultType.UNKNOWN);
expect(mapPlaywrightStatus(undefined)).toBe(ResultType.UNKNOWN);
});

test("场景5: 未知状态映射为 UNKNOWN", () => {
expect(mapPlaywrightStatus("weirdStatus")).toBe(ResultType.UNKNOWN);
});
});
});

describe("getLogLevelByResultType", () => {
test("场景1: FAILED 对应 ERROR 日志等级", () => {
expect(getLogLevelByResultType(ResultType.FAILED)).toBe(LogLevel.ERROR);
});

test("场景2: SUCCEED 对应 INFO 日志等级", () => {
expect(getLogLevelByResultType(ResultType.SUCCEED)).toBe(LogLevel.INFO);
});

test("场景3: IGNORED 对应 INFO 日志等级(避免 skipped 被误标为错误)", () => {
expect(getLogLevelByResultType(ResultType.IGNORED)).toBe(LogLevel.INFO);
});

test("场景4: UNKNOWN 对应 INFO 日志等级", () => {
expect(getLogLevelByResultType(ResultType.UNKNOWN)).toBe(LogLevel.INFO);
});
});

describe("createTestResults - 区分 skipped 与 failed", () => {
test("场景1: skipped 用例最终 ResultType 为 IGNORED", () => {
const output = {
"path/to/skipped": [
{
projectID: "proj1",
result: "skipped",
duration: 0,
startTime: 1610000000,
endTime: 1610000000,
message: "",
content: "test skipped",
owner: "amb",
description: "",
attachments: [],
},
],
};
const tests = ["path/to/skipped"];
const testResults = createTestResults(output, tests);
expect(testResults).toHaveLength(1);
expect(testResults[0].ResultType).toBe(ResultType.IGNORED);
});

test("场景2: failed 用例最终 ResultType 为 FAILED", () => {
const output = {
"path/to/failed": [
{
projectID: "proj1",
result: "failed",
duration: 1,
startTime: 1610000000,
endTime: 1610000001,
message: "assertion failed",
content: "stack trace",
owner: "amb",
description: "",
attachments: [],
},
],
};
const tests = ["path/to/failed"];
const testResults = createTestResults(output, tests);
expect(testResults).toHaveLength(1);
expect(testResults[0].ResultType).toBe(ResultType.FAILED);
});

test("场景3: skipped 用例不会被用作参考失败用例(不会把其它 identifier 误报为 FAILED)", () => {
// output 中只有一个 skipped 用例(不在 testIdentifiers 中)
const output = {
"other/skipped/case": [
{
projectID: "proj1",
result: "skipped",
duration: 0,
startTime: 1610000000,
endTime: 1610000000,
message: "",
content: "",
owner: null,
description: null,
attachments: [],
},
],
};
// testIdentifiers 中的 identifier 不在 output 里
const tests = ["requested/case"];
const testResults = createTestResults(output, tests);
// 由于 output 中的用例是 skipped(而非 failed),不应成为参考失败用例,
// 因此不会为 requested/case 生成兜底的 FAILED 结果
expect(testResults).toHaveLength(0);
});

test("场景4: timedOut 用例最终 ResultType 为 FAILED", () => {
const output = {
"path/to/timeout": [
{
projectID: "proj1",
result: "timedOut",
duration: 30,
startTime: 1610000000,
endTime: 1610000030,
message: "test timed out",
content: "",
owner: null,
description: null,
attachments: [],
},
],
};
const tests = ["path/to/timeout"];
const testResults = createTestResults(output, tests);
expect(testResults).toHaveLength(1);
expect(testResults[0].ResultType).toBe(ResultType.FAILED);
});
});

describe("parseJsonContent - skipped 状态不追加'错误信息'标题", () => {
test("场景1: skipped 状态下即使存在 errors,也不追加'错误信息'标题", () => {
const projPath = "/project";
const data = {
config: { rootDir: "/project/tests" },
suites: [
{
title: "Suite 1",
file: "suite1.js",
specs: [
{
title: "Spec 1",
file: "spec1.js",
tests: [
{
annotations: [],
projectId: "proj1",
results: [
{
startTime: "2023-01-01T00:00:00Z",
duration: 0,
status: "skipped",
errors: [{ message: "residual error" }],
},
],
},
],
},
],
},
],
};
const result = parseJsonContent(projPath, data);
const key = "tests/spec1.js?Suite 1 Spec 1";
expect(result[key]).toBeDefined();
expect(result[key][0].result).toBe("skipped");
// 不应出现"==== 错误信息 ===="
expect(result[key][0].content).not.toContain("==== 错误信息 ====");
});

test("场景2: failed 状态下仍正常追加'错误信息'标题", () => {
const projPath = "/project";
const data = {
config: { rootDir: "/project/tests" },
suites: [
{
title: "Suite 1",
file: "suite1.js",
specs: [
{
title: "Spec 1",
file: "spec1.js",
tests: [
{
annotations: [],
projectId: "proj1",
results: [
{
startTime: "2023-01-01T00:00:00Z",
duration: 1,
status: "failed",
errors: [{ message: "real error" }],
},
],
},
],
},
],
},
],
};
const result = parseJsonContent(projPath, data);
const key = "tests/spec1.js?Suite 1 Spec 1";
expect(result[key]).toBeDefined();
expect(result[key][0].result).toBe("failed");
expect(result[key][0].content).toContain("==== 错误信息 ====");
expect(result[key][0].content).toContain("real error");
});
});
Loading