diff --git a/playwright/package.json b/playwright/package.json index 9c69245..a749c3d 100644 --- a/playwright/package.json +++ b/playwright/package.json @@ -1,6 +1,6 @@ { "name": "testsolar-oss-playwright", - "version": "0.2.22", + "version": "0.2.23", "description": "TestSolar support for playwright tool", "main": "main.js", "scripts": { diff --git a/playwright/src/playwrightx/executor.ts b/playwright/src/playwrightx/executor.ts index f511c21..812d28a 100644 --- a/playwright/src/playwrightx/executor.ts +++ b/playwright/src/playwrightx/executor.ts @@ -21,6 +21,9 @@ export async function runTestCase(runParamFile: string): Promise { const testSelectors = data.TestSelectors || []; let projPath = data.ProjectPath; const taskId = data.TaskId; + + // 默认true, 有值则为false + const fileMode = process.env.TESTSOLAR_TTP_FILEMODE == "1"; // 创建附件目录 const attachmentsPath = path.join(projPath, "attachments"); @@ -93,29 +96,63 @@ export async function runTestCase(runParamFile: string): Promise { } }); - // 按照文件对用例进行分组 - const caseLists = groupTestCasesByPath(newSelectors); - - // 对每个文件生成命令行 - for (const [casePath, testcases] of Object.entries(caseLists)) { - // 上报用例运行状态 - createRunningTestResults(casePath, testcases, reporter); - - // 执行命令并解析用例生成的 JSON 文件 - log.info(`当前进程ID: ${process.pid}`) - const jsonName = casePath.replace(/\//g, "_") + "_pid_" + process.pid + ".json"; - const { command, testIdentifiers } = generateCommands(casePath, testcases, jsonName); - const testResults = await executeCommands( - projPath, - command, - testIdentifiers, - jsonName, - attachmentsPath, - ); + if (fileMode) { + // fileMode: 直接运行文件,不解析具体测试用例 + log.info("TESTSOLAR_TTP_FILEMODE is set, running files directly"); + + // 按照文件对用例进行分组,但在fileMode下每个文件运行所有测试 + const caseLists = groupTestCasesByPath(newSelectors); + + // 对每个文件生成命令行 + for (const [casePath, testcases] of Object.entries(caseLists)) { + // 上报用例运行状态 + createRunningTestResults(casePath, testcases, reporter); + + // 执行命令并解析用例生成的 JSON 文件 + log.info(`当前进程ID: ${process.pid}`) + const jsonName = casePath.replace(/\//g, "_") + "_pid_" + process.pid + ".json"; + // 在fileMode下,只运行文件,不指定具体测试用例 + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { command, testIdentifiers } = generateCommands(casePath, [], jsonName); + // 注意:在fileMode下,testIdentifiers不会被使用,但仍需要解构以保持函数接口一致性 + const testResults = await executeCommands( + projPath, + command, + [casePath], // 在fileMode下,使用文件路径作为标识符 + jsonName, + attachmentsPath, + ); + + const results = createTestResults(testResults, [casePath]); + for (const result of results) { + await reporter.reportTestResult(result); + } + } + } else { + // 按照文件对用例进行分组 + const caseLists = groupTestCasesByPath(newSelectors); + + // 对每个文件生成命令行 + for (const [casePath, testcases] of Object.entries(caseLists)) { + // 上报用例运行状态 + createRunningTestResults(casePath, testcases, reporter); - const results = createTestResults(testResults, testIdentifiers); - for (const result of results) { - await reporter.reportTestResult(result); + // 执行命令并解析用例生成的 JSON 文件 + log.info(`当前进程ID: ${process.pid}`) + const jsonName = casePath.replace(/\//g, "_") + "_pid_" + process.pid + ".json"; + const { command, testIdentifiers } = generateCommands(casePath, testcases, jsonName); + const testResults = await executeCommands( + projPath, + command, + testIdentifiers, + jsonName, + attachmentsPath, + ); + + const results = createTestResults(testResults, testIdentifiers); + for (const result of results) { + await reporter.reportTestResult(result); + } } } } diff --git a/playwright/src/playwrightx/parser.ts b/playwright/src/playwrightx/parser.ts index 75a2382..d26814c 100644 --- a/playwright/src/playwrightx/parser.ts +++ b/playwright/src/playwrightx/parser.ts @@ -7,6 +7,7 @@ import { filterTestcases, getTestcasePrefix, parsePlaywrightReport, + scanPlaywrightTestFiles, } from "./utils"; import { @@ -38,24 +39,35 @@ export async function collectTestCases( } const filePath = path.join(projPath, "load.json"); + // 默认false + const fileMode = process.env.TESTSOLAR_TTP_FILEMODE == "1"; - // 执行命令获取output.json文件内容 - const command = `npx playwright test --list --reporter=json > ${filePath}`; - log.info("Run Command: ", command); - const { stdout, stderr } = await executeCommand(command); - log.info("stdout:", stdout); - log.info("stderr:", stderr); - - //TODO 解析output.json文件内容, 待完善,重跑用例加上数据驱动 - const fileContent = fs.readFileSync(filePath, "utf-8"); - const testData = JSON.parse(fileContent); - - // 解析所有用例 - const loadCaseResult = parseTestcase(projPath, testData); + let loadCaseResult; + if (fileMode) { + log.info("TESTSOLAR_TTP_FILEMODE is set, using file paths directly without parsing"); + // 扫描Playwright测试文件 + loadCaseResult = scanPlaywrightTestFiles(projPath); + } else { + // 如果环境变量未设置,则按原来的方式解析用例 + // 执行命令获取output.json文件内容 + const command = `npx playwright test --list --reporter=json > ${filePath}`; + log.info("Run Command: ", command); + const { stdout, stderr } = await executeCommand(command); + log.info("stdout:", stdout); + log.info("stderr:", stderr); + + //TODO 解析output.json文件内容, 待完善,重跑用例加上数据驱动 + const fileContent = fs.readFileSync(filePath, "utf-8"); + const testData = JSON.parse(fileContent); + + // 解析所有用例 + loadCaseResult = parseTestcase(projPath, testData); + } log.info("PlayWright testtool parse all testcases: \n", loadCaseResult); // 如果用例为空,则通过解析json来获取错误信息 - if (loadCaseResult.length === 0) { + if (loadCaseResult.length === 0 && !fileMode) { + const fileContent = fs.readFileSync(filePath, "utf-8"); const errors = parsePlaywrightReport(fileContent); result.LoadErrors.push(...errors); // 使用LoadResult中定义的属性名 } @@ -85,8 +97,15 @@ export async function collectTestCases( // 提取用例数据 filterResult.forEach((filteredTestCase: string) => { - const [path, descAndName] = filteredTestCase.split("?"); - const test = new TestCase(`${testcasePrefix}${path}?${descAndName}`, {}); + let test; + if (fileMode) { + // 去掉前缀 projectPath,获取相对路径 + const relativePath = path.relative(projPath, filteredTestCase); + test = new TestCase(`${testcasePrefix}${relativePath}`, {}); + } else { + const [path, descAndName] = filteredTestCase.split("?"); + test = new TestCase(`${testcasePrefix}${path}?${descAndName}`, {}); + } result.Tests.push(test); }); } catch (error: unknown) { diff --git a/playwright/src/playwrightx/utils.ts b/playwright/src/playwrightx/utils.ts index 59a9c57..b3dbc74 100644 --- a/playwright/src/playwrightx/utils.ts +++ b/playwright/src/playwrightx/utils.ts @@ -371,9 +371,12 @@ export function generateCommands( const hash = createHash('md5').update(input).digest('hex').substring(0, 10); const outputOption = `--output=test-results-${hash}`; - // 获取 grep 模式 + // 检查是否为 fileMode + const fileMode = process.env.TESTSOLAR_TTP_FILEMODE == "1"; + + // 获取 grep 模式(在 fileMode 下不使用 grep) let grepPattern = ""; - if (testCases.length > 0) { + if (testCases.length > 0 && !fileMode) { grepPattern = `--grep="${decodeURIComponent(testCases.join("|"))}"`; } @@ -382,14 +385,24 @@ export function generateCommands( if (useEnvJsonFile) { // 使用环境变量设置 JSON 输出 if (testCases.length === 0) { - command = `export PLAYWRIGHT_JSON_OUTPUT_NAME=${jsonName} && npx playwright test --reporter=json ${traceOption} ${workersOption} ${outputOption} ${extraArgs}`; + // 在 fileMode 下,即使没有具体测试用例,也要指定文件路径 + if (fileMode) { + command = `export PLAYWRIGHT_JSON_OUTPUT_NAME=${jsonName} && npx playwright test ${casePath} --reporter=json ${traceOption} ${workersOption} ${outputOption} ${extraArgs}`; + } else { + command = `export PLAYWRIGHT_JSON_OUTPUT_NAME=${jsonName} && npx playwright test --reporter=json ${traceOption} ${workersOption} ${outputOption} ${extraArgs}`; + } } else { command = `export PLAYWRIGHT_JSON_OUTPUT_NAME=${jsonName} && npx playwright test ${casePath} ${grepPattern} --reporter=json ${traceOption} ${workersOption} ${outputOption} ${extraArgs}`; } } else { // 使用原始的重定向方式 if (testCases.length === 0) { - command = `npx playwright test --reporter=json ${traceOption} ${workersOption} ${outputOption} ${extraArgs} > ${jsonName}`; + // 在 fileMode 下,即使没有具体测试用例,也要指定文件路径 + if (fileMode) { + command = `npx playwright test ${casePath} --reporter=json ${traceOption} ${workersOption} ${outputOption} ${extraArgs} > ${jsonName}`; + } else { + command = `npx playwright test --reporter=json ${traceOption} ${workersOption} ${outputOption} ${extraArgs} > ${jsonName}`; + } } else { command = `npx playwright test ${casePath} ${grepPattern} --reporter=json ${traceOption} ${workersOption} ${outputOption} ${extraArgs} > ${jsonName}`; } @@ -871,6 +884,45 @@ export function parsePlaywrightReport(jsonData: string): LoadError[] { } } +// 扫描目录中的Playwright测试文件(排除node_modules) +export function scanPlaywrightTestFiles(directory: string): string[] { + const testCases: string[] = []; + // 匹配Playwright测试文件的模式 + const testPattern = /test\(['"`]/; + + function readDirRecursive(dir: string) { + // 如果路径中包含node_modules,则跳过该目录 + if (dir.includes("node_modules")) { + return; + } + + try { + const files = fs.readdirSync(dir); + + files.forEach((file) => { + const fullPath = path.join(dir, file); + const stat = fs.statSync(fullPath); + + if (stat.isDirectory()) { + readDirRecursive(fullPath); + } else if (file.endsWith(".spec.js") || file.endsWith(".spec.ts") || + file.endsWith(".test.js") || file.endsWith(".test.ts") || + file.endsWith(".e2e.js") || file.endsWith(".e2e.ts")) { + const content = fs.readFileSync(fullPath, "utf-8"); + if (testPattern.test(content)) { + testCases.push(fullPath); + } + } + }); + } catch (error) { + console.error(`Error reading directory ${dir}:`, error); + } + } + + readDirRecursive(directory); + return testCases; +} + export async function createRunningTestResults( path: string, names: string[], diff --git a/playwright/tests/filemode-simple.test.ts b/playwright/tests/filemode-simple.test.ts new file mode 100644 index 0000000..de3567f --- /dev/null +++ b/playwright/tests/filemode-simple.test.ts @@ -0,0 +1,162 @@ +import { describe, expect, test, beforeEach, afterEach } from "@jest/globals"; +import * as process from "process"; + +let originalEnv: NodeJS.ProcessEnv; + +describe("FileMode Environment Variable Tests", () => { + beforeEach(() => { + originalEnv = { ...process.env }; + }); + + afterEach(() => { + Object.keys(process.env).forEach(key => { + delete process.env[key]; + }); + Object.assign(process.env, originalEnv); + }); + + describe("Environment variable behavior", () => { + test("fileMode should be true when TESTSOLAR_TTP_FILEMODE is not set", () => { + delete process.env.TESTSOLAR_TTP_FILEMODE; + const fileMode = !process.env.TESTSOLAR_TTP_FILEMODE; + expect(fileMode).toBe(true); + }); + + test("fileMode should be false when TESTSOLAR_TTP_FILEMODE is set to '1'", () => { + process.env.TESTSOLAR_TTP_FILEMODE = "1"; + const fileMode = !process.env.TESTSOLAR_TTP_FILEMODE; + expect(fileMode).toBe(false); + }); + + test("fileMode should be false when TESTSOLAR_TTP_FILEMODE is set to 'true'", () => { + process.env.TESTSOLAR_TTP_FILEMODE = "true"; + const fileMode = !process.env.TESTSOLAR_TTP_FILEMODE; + expect(fileMode).toBe(false); + }); + + test("fileMode should be false when TESTSOLAR_TTP_FILEMODE is set to any value", () => { + process.env.TESTSOLAR_TTP_FILEMODE = "any_value"; + const fileMode = !process.env.TESTSOLAR_TTP_FILEMODE; + expect(fileMode).toBe(false); + }); + }); + + describe("File extension matching logic", () => { + test("should match correct Playwright test file extensions", () => { + const testFiles = [ + 'example.spec.ts', + 'example.spec.js', + 'example.test.ts', + 'example.test.js', + 'example.e2e.ts', + 'example.e2e.js' + ]; + + const nonTestFiles = [ + 'example.ts', + 'example.js', + 'example.component.ts', + 'example.service.js', + 'README.md' + ]; + + // 测试文件应该匹配 + testFiles.forEach(file => { + const isTestFile = file.endsWith(".spec.js") || file.endsWith(".spec.ts") || + file.endsWith(".test.js") || file.endsWith(".test.ts") || + file.endsWith(".e2e.js") || file.endsWith(".e2e.ts"); + expect(isTestFile).toBe(true); + }); + + // 非测试文件不应该匹配 + nonTestFiles.forEach(file => { + const isTestFile = file.endsWith(".spec.js") || file.endsWith(".spec.ts") || + file.endsWith(".test.js") || file.endsWith(".test.ts") || + file.endsWith(".e2e.js") || file.endsWith(".e2e.ts"); + expect(isTestFile).toBe(false); + }); + }); + }); + + describe("Test pattern matching logic", () => { + test("should match test pattern in file content", () => { + const testPattern = /test\(['"`]/; + + const validTestContent = [ + 'test("should work", () => {})', + "test('should work', () => {})", + 'test(`should work`, () => {})', + 'describe("suite", () => { test("case", () => {}); })' + ]; + + const invalidTestContent = [ + 'function test() {}', + 'const testData = {}', + 'testing = true', + 'describe("suite", () => {})', + 'it("should work", () => {})' // playwright uses test(), not it() + ]; + + validTestContent.forEach(content => { + expect(testPattern.test(content)).toBe(true); + }); + + invalidTestContent.forEach(content => { + expect(testPattern.test(content)).toBe(false); + }); + }); + }); + + describe("Path handling logic", () => { + test("should correctly identify node_modules paths", () => { + const paths = [ + '/project/node_modules/package', + '/project/src/node_modules/lib', + '/project/tests/example.spec.ts', + '/project/src/utils.ts' + ]; + + const nodeModulesPaths = paths.filter(p => p.includes('node_modules')); + const regularPaths = paths.filter(p => !p.includes('node_modules')); + + expect(nodeModulesPaths.length).toBe(2); + expect(regularPaths.length).toBe(2); + expect(nodeModulesPaths.every(p => p.includes('node_modules'))).toBe(true); + expect(regularPaths.every(p => !p.includes('node_modules'))).toBe(true); + }); + }); + + describe("FileMode integration logic", () => { + test("should demonstrate fileMode behavior difference", () => { + // 模拟正常模式(fileMode = true) + delete process.env.TESTSOLAR_TTP_FILEMODE; + const normalMode = !process.env.TESTSOLAR_TTP_FILEMODE; + + // 模拟 fileMode(fileMode = false) + process.env.TESTSOLAR_TTP_FILEMODE = "1"; + const fileModeEnabled = !process.env.TESTSOLAR_TTP_FILEMODE; + + expect(normalMode).toBe(true); // 正常模式:解析具体测试用例 + expect(fileModeEnabled).toBe(false); // fileMode:只输出文件路径 + + // 验证两种模式的行为不同 + expect(normalMode).not.toBe(fileModeEnabled); + }); + + test("should simulate test case vs file path output", () => { + const testFilePath = "tests/example.spec.ts"; + const testCaseName = "should work correctly"; + + // 正常模式:输出 "文件路径?测试用例名" + const normalModeOutput = `${testFilePath}?${testCaseName}`; + + // fileMode:只输出文件路径 + const fileModeOutput = testFilePath; + + expect(normalModeOutput).toBe("tests/example.spec.ts?should work correctly"); + expect(fileModeOutput).toBe("tests/example.spec.ts"); + expect(normalModeOutput).toContain("?"); + expect(fileModeOutput).not.toContain("?"); + }); + }); +}); \ No newline at end of file diff --git a/playwright/tests/utils.test.ts b/playwright/tests/utils.test.ts index 3e77a54..e545aca 100644 --- a/playwright/tests/utils.test.ts +++ b/playwright/tests/utils.test.ts @@ -110,7 +110,7 @@ describe("isFileOrDirectory", () => { expect(result).toBe(-1); }, 10000); - test("should r置超时时eturn 0 for neither file nor directory", async () => { + test("should return 0 for neither file nor directory", async () => { log.info("Testing unknown path..."); const testUnknown = path.join(__dirname, "unknown"); const result = isFileOrDirectory(testUnknown); diff --git a/playwright/testtool.yaml b/playwright/testtool.yaml index 1d2de75..0ad0ebf 100644 --- a/playwright/testtool.yaml +++ b/playwright/testtool.yaml @@ -2,7 +2,7 @@ schemaVersion: 1.0 name: playwright nameZh: Playwright自动化测试 lang: javascript -version: '0.2.22' +version: '0.2.23' defaultBaseImage: node:18 langType: INTERPRETED description: |-