From 7b46a7bc8ab7ba16305a813926f4b9ccea9c3b48 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 25 Feb 2026 04:40:46 +0000 Subject: [PATCH 01/14] [tagpr] prepare for the next release From c3624f609cd274e95c69fe07181288e19619dcd0 Mon Sep 17 00:00:00 2001 From: Naoto Ono Date: Wed, 4 Mar 2026 11:06:36 +0900 Subject: [PATCH 02/14] playwright: add support for test file prefixes in JSON reports --- launchable/test_runners/playwright.py | 45 ++++++++++++- tests/data/playwright/report_with_prefix.json | 64 +++++++++++++++++++ tests/test_runners/test_playwright.py | 16 +++++ 3 files changed, 124 insertions(+), 1 deletion(-) create mode 100644 tests/data/playwright/report_with_prefix.json diff --git a/launchable/test_runners/playwright.py b/launchable/test_runners/playwright.py index 87e803213..b18c53262 100644 --- a/launchable/test_runners/playwright.py +++ b/launchable/test_runners/playwright.py @@ -3,7 +3,9 @@ # https://playwright.dev/ # import json +import os from typing import Dict, Generator, List +from pathlib import Path import click from junitparser import TestCase, TestSuite # type: ignore @@ -173,13 +175,54 @@ def parse_func(self, report_file: str) -> Generator[CaseEvent, None, None]: click.echo("Can't find test results from {}. Make sure to confirm report file.".format( report_file), err=True) + test_prefix = self._compute_test_prefix(data) for s in suites: # The title of the root suite object contains the file name. - test_file = str(s.get("title", "")) + test_file = self._resolve_test_file(str(s.get("title", "")), test_prefix) for event in self._parse_suites(test_file, s, []): yield event + def _compute_test_prefix(self, report: Dict) -> str: + """ + Playwright JSON stores test `file` paths relative to `config.rootDir`. + Our CLI wants paths relative to the Playwright config directory + (usually the project/repo root), so we compute: + relpath(root_dir, base_dir) + where base_dir = dirname(configFile). + + Example: + configFile = /repo/playwright.config.ts + rootDir = /repo/tests + relpath(...) -> "tests" + """ + config: Dict = report.get("config", {}) + config_file = str(config.get("configFile", "")) + root_dir = str(config.get("rootDir", "")) + if not config_file or not root_dir: + return "" + + base_dir = Path(config_file).parent + try: + test_prefix = Path(root_dir).relative_to(base_dir).as_posix() + except ValueError: + return "" + + if test_prefix == ".": + return "" + + return test_prefix + + def _resolve_test_file(self, test_file: str, test_prefix: str) -> str: + if not test_prefix or not test_file: + return test_file + + # Guard against duplicate paths when report data is already prefixed. + if test_file.startswith(test_prefix): + return test_file + + return Path(test_prefix, test_file).as_posix() + def _parse_suites(self, test_file: str, suite: Dict[str, Dict], test_case_names: List[str] = []) -> List: events = [] diff --git a/tests/data/playwright/report_with_prefix.json b/tests/data/playwright/report_with_prefix.json new file mode 100644 index 000000000..870a9ba21 --- /dev/null +++ b/tests/data/playwright/report_with_prefix.json @@ -0,0 +1,64 @@ +{ + "config": { + "configFile": "/repo/playwright.config.ts", + "rootDir": "/repo/packages/e2e" + }, + "suites": [ + { + "title": "tests/a.spec.ts", + "specs": [], + "suites": [ + { + "title": "smoke", + "specs": [ + { + "title": "passes", + "line": 10, + "tests": [ + { + "results": [ + { + "status": "passed", + "duration": 12, + "stdout": [], + "errors": [] + } + ] + } + ] + } + ], + "suites": [] + } + ] + }, + { + "title": "packages/e2e/tests/b.spec.ts", + "specs": [], + "suites": [ + { + "title": "smoke", + "specs": [ + { + "title": "already prefixed", + "line": 20, + "tests": [ + { + "results": [ + { + "status": "passed", + "duration": 15, + "stdout": [], + "errors": [] + } + ] + } + ] + } + ], + "suites": [] + } + ] + } + ] +} diff --git a/tests/test_runners/test_playwright.py b/tests/test_runners/test_playwright.py index 95ab8751d..6e361950a 100644 --- a/tests/test_runners/test_playwright.py +++ b/tests/test_runners/test_playwright.py @@ -64,3 +64,19 @@ def _test_test_path_status(payload, test_path: str, status: CaseEvent) -> bool: 'playwright', '--json', str(self.test_files_dir.joinpath("report.json"))) json_payload = json.loads(gzip.decompress(self.find_request('/events', 1).request.body).decode()) self.assertEqual(_test_test_path_status(json_payload, target_test_path, CaseEvent.TEST_FAILED), True) + + @responses.activate + @mock.patch.dict(os.environ, + {"LAUNCHABLE_TOKEN": CliTestCase.launchable_token}) + def test_record_test_with_json_option_adds_prefix_from_config(self): + report_file = str(self.test_files_dir.joinpath("report_with_prefix.json")) + + result = self.cli('record', 'tests', '--session', self.session, + 'playwright', '--json', report_file) + + self.assert_success(result) + + payload = json.loads(gzip.decompress(self.find_request('/events').request.body).decode()) + test_paths = [unparse_test_path(event.get("testPath")) for event in payload.get("events")] + self.assertIn("file=packages/e2e/tests/a.spec.ts#testcase=smoke › passes", test_paths) + self.assertIn("file=packages/e2e/tests/b.spec.ts#testcase=smoke › already prefixed", test_paths) From a392c8608d63f45c30d6ea1937683f261a29f71b Mon Sep 17 00:00:00 2001 From: Naoto Ono Date: Wed, 4 Mar 2026 15:52:27 +0900 Subject: [PATCH 03/14] Remove unused imports --- launchable/test_runners/playwright.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/launchable/test_runners/playwright.py b/launchable/test_runners/playwright.py index b18c53262..939920b3c 100644 --- a/launchable/test_runners/playwright.py +++ b/launchable/test_runners/playwright.py @@ -3,9 +3,8 @@ # https://playwright.dev/ # import json -import os -from typing import Dict, Generator, List from pathlib import Path +from typing import Dict, Generator, List import click from junitparser import TestCase, TestSuite # type: ignore From dcd742f6749dc0ac38815859b039f9f21a21c025 Mon Sep 17 00:00:00 2001 From: Naoto Ono Date: Wed, 4 Mar 2026 19:06:24 +0900 Subject: [PATCH 04/14] Cleanup naming --- launchable/test_runners/playwright.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/launchable/test_runners/playwright.py b/launchable/test_runners/playwright.py index 939920b3c..d30205d76 100644 --- a/launchable/test_runners/playwright.py +++ b/launchable/test_runners/playwright.py @@ -174,15 +174,15 @@ def parse_func(self, report_file: str) -> Generator[CaseEvent, None, None]: click.echo("Can't find test results from {}. Make sure to confirm report file.".format( report_file), err=True) - test_prefix = self._compute_test_prefix(data) + root_dir_relpath = self._compute_root_dir_relpath(data) for s in suites: # The title of the root suite object contains the file name. - test_file = self._resolve_test_file(str(s.get("title", "")), test_prefix) + test_file = self._resolve_test_file(str(s.get("title", "")), root_dir_relpath) for event in self._parse_suites(test_file, s, []): yield event - def _compute_test_prefix(self, report: Dict) -> str: + def _compute_root_dir_relpath(self, report: Dict) -> str: """ Playwright JSON stores test `file` paths relative to `config.rootDir`. Our CLI wants paths relative to the Playwright config directory @@ -203,24 +203,24 @@ def _compute_test_prefix(self, report: Dict) -> str: base_dir = Path(config_file).parent try: - test_prefix = Path(root_dir).relative_to(base_dir).as_posix() + root_dir_relpath = Path(root_dir).relative_to(base_dir).as_posix() except ValueError: return "" - if test_prefix == ".": + if root_dir_relpath == ".": return "" - return test_prefix + return root_dir_relpath - def _resolve_test_file(self, test_file: str, test_prefix: str) -> str: - if not test_prefix or not test_file: + def _resolve_test_file(self, test_file: str, root_dir_relpath: str) -> str: + if not root_dir_relpath or not test_file: return test_file # Guard against duplicate paths when report data is already prefixed. - if test_file.startswith(test_prefix): + if test_file.startswith(root_dir_relpath): return test_file - return Path(test_prefix, test_file).as_posix() + return Path(root_dir_relpath, test_file).as_posix() def _parse_suites(self, test_file: str, suite: Dict[str, Dict], test_case_names: List[str] = []) -> List: events = [] From 521fcfc228ebc04ad5ebc8d8ad9e35b9f6efe20c Mon Sep 17 00:00:00 2001 From: Naoto Ono Date: Wed, 4 Mar 2026 19:22:10 +0900 Subject: [PATCH 05/14] Extract test path logic --- launchable/test_runners/playwright.py | 25 +++----------------- launchable/testpath.py | 25 ++++++++++++++++++++ tests/test_testpath.py | 34 ++++++++++++++++++++++++++- 3 files changed, 61 insertions(+), 23 deletions(-) diff --git a/launchable/test_runners/playwright.py b/launchable/test_runners/playwright.py index d30205d76..b7d0c25fb 100644 --- a/launchable/test_runners/playwright.py +++ b/launchable/test_runners/playwright.py @@ -10,7 +10,7 @@ from junitparser import TestCase, TestSuite # type: ignore from ..commands.record.case_event import CaseEvent -from ..testpath import TestPath +from ..testpath import TestPath, prepend_path_if_missing, relative_subpath from . import launchable TEST_CASE_DELIMITER = " › " @@ -177,7 +177,7 @@ def parse_func(self, report_file: str) -> Generator[CaseEvent, None, None]: root_dir_relpath = self._compute_root_dir_relpath(data) for s in suites: # The title of the root suite object contains the file name. - test_file = self._resolve_test_file(str(s.get("title", "")), root_dir_relpath) + test_file = prepend_path_if_missing(str(s.get("title", "")), root_dir_relpath) for event in self._parse_suites(test_file, s, []): yield event @@ -201,26 +201,7 @@ def _compute_root_dir_relpath(self, report: Dict) -> str: if not config_file or not root_dir: return "" - base_dir = Path(config_file).parent - try: - root_dir_relpath = Path(root_dir).relative_to(base_dir).as_posix() - except ValueError: - return "" - - if root_dir_relpath == ".": - return "" - - return root_dir_relpath - - def _resolve_test_file(self, test_file: str, root_dir_relpath: str) -> str: - if not root_dir_relpath or not test_file: - return test_file - - # Guard against duplicate paths when report data is already prefixed. - if test_file.startswith(root_dir_relpath): - return test_file - - return Path(root_dir_relpath, test_file).as_posix() + return relative_subpath(root_dir, str(Path(config_file).parent)) def _parse_suites(self, test_file: str, suite: Dict[str, Dict], test_case_names: List[str] = []) -> List: events = [] diff --git a/launchable/testpath.py b/launchable/testpath.py index 5a9456fc6..a362a7914 100644 --- a/launchable/testpath.py +++ b/launchable/testpath.py @@ -95,6 +95,31 @@ def _relative_to(p: pathlib.Path, base: str) -> pathlib.Path: return resolved.relative_to(base) +def relative_subpath(path: str, base_path: str) -> str: + if not path or not base_path: + return "" + + try: + relpath = pathlib.Path(path).relative_to(pathlib.Path(base_path)).as_posix() + except ValueError: + return "" + + if relpath == ".": + return "" + + return relpath + + +def prepend_path_if_missing(path: str, prefix: str) -> str: + if not path or not prefix: + return path + + if path.startswith(prefix): + return path + + return pathlib.Path(prefix, path).as_posix() + + class FilePathNormalizer: """Normalize file paths based on the Git repository root diff --git a/tests/test_testpath.py b/tests/test_testpath.py index 32d6253ad..635290d78 100644 --- a/tests/test_testpath.py +++ b/tests/test_testpath.py @@ -4,8 +4,9 @@ import sys import tempfile import unittest + # hello smart tests -from launchable.testpath import FilePathNormalizer, parse_test_path, unparse_test_path +from launchable.testpath import FilePathNormalizer, parse_test_path, prepend_path_if_missing, relative_subpath, unparse_test_path class TestPathEncodingTest(unittest.TestCase): @@ -136,5 +137,36 @@ def _run_command(self, args, cwd=None): self.fail("Failed to execute a command: {}\nSTDOUT: {}\nSTDERR: {}\n". format(e, e.stdout, e.stderr)) +class TestPathHelpers(unittest.TestCase): + def test_relative_subpath(self): + self.assertEqual( + "tests", + relative_subpath(str(pathlib.Path("repo", "tests")), str(pathlib.Path("repo")))) + + def test_relative_subpath_returns_empty_when_same_path(self): + self.assertEqual( + "", + relative_subpath(str(pathlib.Path("repo")), str(pathlib.Path("repo")))) + + def test_relative_subpath_returns_empty_when_not_under_base(self): + self.assertEqual( + "", + relative_subpath(str(pathlib.Path("repo", "tests")), str(pathlib.Path("other")))) + + def test_prepend_path_if_missing(self): + self.assertEqual( + "tests/a.spec.ts", + prepend_path_if_missing("a.spec.ts", "tests")) + + def test_prepend_path_if_missing_when_already_prefixed(self): + self.assertEqual( + "tests/a.spec.ts", + prepend_path_if_missing("tests/a.spec.ts", "tests")) + + def test_prepend_path_if_missing_when_empty_input(self): + self.assertEqual("", prepend_path_if_missing("", "tests")) + self.assertEqual("a.spec.ts", prepend_path_if_missing("a.spec.ts", "")) + + if __name__ == '__main__': unittest.main() From 413719f9be1516ad4977bcdb5566b27f16b4ce21 Mon Sep 17 00:00:00 2001 From: Naoto Ono Date: Mon, 20 Apr 2026 08:33:22 +0900 Subject: [PATCH 06/14] playwright: respect --base for JSON report paths --- launchable/test_runners/playwright.py | 26 ++++++++++++++++++++++---- tests/test_runners/test_playwright.py | 27 +++++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 4 deletions(-) diff --git a/launchable/test_runners/playwright.py b/launchable/test_runners/playwright.py index b7d0c25fb..b619828dd 100644 --- a/launchable/test_runners/playwright.py +++ b/launchable/test_runners/playwright.py @@ -175,9 +175,10 @@ def parse_func(self, report_file: str) -> Generator[CaseEvent, None, None]: report_file), err=True) root_dir_relpath = self._compute_root_dir_relpath(data) + config_dir = self._config_dir(data) for s in suites: # The title of the root suite object contains the file name. - test_file = prepend_path_if_missing(str(s.get("title", "")), root_dir_relpath) + test_file = self._resolve_test_file(str(s.get("title", "")), root_dir_relpath, config_dir) for event in self._parse_suites(test_file, s, []): yield event @@ -196,12 +197,29 @@ def _compute_root_dir_relpath(self, report: Dict) -> str: relpath(...) -> "tests" """ config: Dict = report.get("config", {}) - config_file = str(config.get("configFile", "")) + config_dir = self._config_dir(report) root_dir = str(config.get("rootDir", "")) - if not config_file or not root_dir: + if not config_dir or not root_dir: + return "" + + return relative_subpath(root_dir, config_dir) + + def _config_dir(self, report: Dict) -> str: + config: Dict = report.get("config", {}) + config_file = str(config.get("configFile", "")) + if not config_file: return "" - return relative_subpath(root_dir, str(Path(config_file).parent)) + return str(Path(config_file).parent) + + def _resolve_test_file(self, test_file: str, root_dir_relpath: str, config_dir: str) -> str: + test_file = prepend_path_if_missing(test_file, root_dir_relpath) + if not self.client.base_path or not config_dir: + return test_file + + # When --base is set, hand an absolute path to make_file_path_component() + # so its existing base_path relativization works as intended. + return str(Path(config_dir, test_file).absolute()) def _parse_suites(self, test_file: str, suite: Dict[str, Dict], test_case_names: List[str] = []) -> List: events = [] diff --git a/tests/test_runners/test_playwright.py b/tests/test_runners/test_playwright.py index 6e361950a..dbc63c286 100644 --- a/tests/test_runners/test_playwright.py +++ b/tests/test_runners/test_playwright.py @@ -3,6 +3,7 @@ import os import sys import unittest +from pathlib import Path from unittest import mock import responses # type: ignore @@ -80,3 +81,29 @@ def test_record_test_with_json_option_adds_prefix_from_config(self): test_paths = [unparse_test_path(event.get("testPath")) for event in payload.get("events")] self.assertIn("file=packages/e2e/tests/a.spec.ts#testcase=smoke › passes", test_paths) self.assertIn("file=packages/e2e/tests/b.spec.ts#testcase=smoke › already prefixed", test_paths) + + @responses.activate + @mock.patch.dict(os.environ, + {"LAUNCHABLE_TOKEN": CliTestCase.launchable_token}) + def test_record_test_with_json_option_respects_base_path(self): + project_root = Path(self.dir, "repo") + base_path = project_root / "packages" / "e2e" + base_path.mkdir(parents=True, exist_ok=True) + + report = self.load_json_from_file(self.test_files_dir.joinpath("report_with_prefix.json")) + report["config"]["configFile"] = str(project_root / "playwright.config.ts") + report["config"]["rootDir"] = str(base_path) + + report_file = Path(self.dir, "report_with_prefix_base.json") + with report_file.open("w") as f: + json.dump(report, f) + + result = self.cli('record', 'tests', '--session', self.session, '--base', str(base_path), + 'playwright', '--json', str(report_file)) + + self.assert_success(result) + + payload = json.loads(gzip.decompress(self.find_request('/events').request.body).decode()) + test_paths = [unparse_test_path(event.get("testPath")) for event in payload.get("events")] + self.assertIn("file=tests/a.spec.ts#testcase=smoke › passes", test_paths) + self.assertIn("file=tests/b.spec.ts#testcase=smoke › already prefixed", test_paths) From c856b5045176b0f0c865a50e68863ceffd244b94 Mon Sep 17 00:00:00 2001 From: Naoto Ono Date: Mon, 20 Apr 2026 08:58:21 +0900 Subject: [PATCH 07/14] Refactor if expression --- launchable/test_runners/playwright.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/launchable/test_runners/playwright.py b/launchable/test_runners/playwright.py index b619828dd..958bfa6f0 100644 --- a/launchable/test_runners/playwright.py +++ b/launchable/test_runners/playwright.py @@ -199,10 +199,10 @@ def _compute_root_dir_relpath(self, report: Dict) -> str: config: Dict = report.get("config", {}) config_dir = self._config_dir(report) root_dir = str(config.get("rootDir", "")) - if not config_dir or not root_dir: - return "" + if config_dir and root_dir: + return relative_subpath(root_dir, config_dir) - return relative_subpath(root_dir, config_dir) + return "" def _config_dir(self, report: Dict) -> str: config: Dict = report.get("config", {}) @@ -214,12 +214,12 @@ def _config_dir(self, report: Dict) -> str: def _resolve_test_file(self, test_file: str, root_dir_relpath: str, config_dir: str) -> str: test_file = prepend_path_if_missing(test_file, root_dir_relpath) - if not self.client.base_path or not config_dir: - return test_file + if self.client.base_path and config_dir: + # When --base is set, hand an absolute path to make_file_path_component() + # so its existing base_path relativization works as intended. + return str(Path(config_dir, test_file).absolute()) - # When --base is set, hand an absolute path to make_file_path_component() - # so its existing base_path relativization works as intended. - return str(Path(config_dir, test_file).absolute()) + return test_file def _parse_suites(self, test_file: str, suite: Dict[str, Dict], test_case_names: List[str] = []) -> List: events = [] From af4762ea41abea125d265d93506c36d778c8e674 Mon Sep 17 00:00:00 2001 From: Naoto Ono Date: Mon, 20 Apr 2026 08:58:59 +0900 Subject: [PATCH 08/14] Refactor if expression --- launchable/test_runners/playwright.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/launchable/test_runners/playwright.py b/launchable/test_runners/playwright.py index 958bfa6f0..963ffb1d7 100644 --- a/launchable/test_runners/playwright.py +++ b/launchable/test_runners/playwright.py @@ -207,10 +207,10 @@ def _compute_root_dir_relpath(self, report: Dict) -> str: def _config_dir(self, report: Dict) -> str: config: Dict = report.get("config", {}) config_file = str(config.get("configFile", "")) - if not config_file: - return "" + if config_file: + str(Path(config_file).parent) - return str(Path(config_file).parent) + return "" def _resolve_test_file(self, test_file: str, root_dir_relpath: str, config_dir: str) -> str: test_file = prepend_path_if_missing(test_file, root_dir_relpath) From c7d15cc272e4d30242b56b33c3335475b237700a Mon Sep 17 00:00:00 2001 From: Naoto Ono Date: Mon, 20 Apr 2026 09:05:23 +0900 Subject: [PATCH 09/14] Refactor if expression --- launchable/test_runners/playwright.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/launchable/test_runners/playwright.py b/launchable/test_runners/playwright.py index 963ffb1d7..b2a9bc1d7 100644 --- a/launchable/test_runners/playwright.py +++ b/launchable/test_runners/playwright.py @@ -208,7 +208,7 @@ def _config_dir(self, report: Dict) -> str: config: Dict = report.get("config", {}) config_file = str(config.get("configFile", "")) if config_file: - str(Path(config_file).parent) + return str(Path(config_file).parent) return "" From 4c3a355c86f0ecd508a3443b29f8977608167f3f Mon Sep 17 00:00:00 2001 From: Naoto Ono Date: Mon, 20 Apr 2026 09:20:43 +0900 Subject: [PATCH 10/14] Fix tests --- launchable/test_runners/playwright.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/launchable/test_runners/playwright.py b/launchable/test_runners/playwright.py index b2a9bc1d7..704cd4c41 100644 --- a/launchable/test_runners/playwright.py +++ b/launchable/test_runners/playwright.py @@ -208,7 +208,7 @@ def _config_dir(self, report: Dict) -> str: config: Dict = report.get("config", {}) config_file = str(config.get("configFile", "")) if config_file: - return str(Path(config_file).parent) + return Path(config_file).parent.as_posix() return "" @@ -217,7 +217,7 @@ def _resolve_test_file(self, test_file: str, root_dir_relpath: str, config_dir: if self.client.base_path and config_dir: # When --base is set, hand an absolute path to make_file_path_component() # so its existing base_path relativization works as intended. - return str(Path(config_dir, test_file).absolute()) + return Path(config_dir, test_file).absolute().as_posix() return test_file From 4f073f2daf71daca96cb452c255b6371de2879ae Mon Sep 17 00:00:00 2001 From: Naoto Ono Date: Mon, 20 Apr 2026 12:16:54 +0900 Subject: [PATCH 11/14] Refactor tests --- .../record_test_result_with_json_base.json | 49 +++++++++++++++++++ .../record_test_result_with_prefix.json | 49 +++++++++++++++++++ tests/test_runners/test_playwright.py | 12 +---- 3 files changed, 100 insertions(+), 10 deletions(-) create mode 100644 tests/data/playwright/record_test_result_with_json_base.json create mode 100644 tests/data/playwright/record_test_result_with_prefix.json diff --git a/tests/data/playwright/record_test_result_with_json_base.json b/tests/data/playwright/record_test_result_with_json_base.json new file mode 100644 index 000000000..394417be0 --- /dev/null +++ b/tests/data/playwright/record_test_result_with_json_base.json @@ -0,0 +1,49 @@ +{ + "events": [ + { + "type": "case", + "testPath": [ + { + "type": "file", + "name": "tests/a.spec.ts" + }, + { + "type": "testcase", + "name": "smoke \u203a passes" + } + ], + "duration": 0.012, + "status": 1, + "stdout": "", + "stderr": "", + "data": { + "lineNumber": 10 + } + }, + { + "type": "case", + "testPath": [ + { + "type": "file", + "name": "tests/b.spec.ts" + }, + { + "type": "testcase", + "name": "smoke \u203a already prefixed" + } + ], + "duration": 0.015, + "status": 1, + "stdout": "", + "stderr": "", + "data": { + "lineNumber": 20 + } + } + ], + "testRunner": "playwright", + "group": "", + "noBuild": false, + "flavors": [], + "testSuite": "" +} diff --git a/tests/data/playwright/record_test_result_with_prefix.json b/tests/data/playwright/record_test_result_with_prefix.json new file mode 100644 index 000000000..69a6fcf3c --- /dev/null +++ b/tests/data/playwright/record_test_result_with_prefix.json @@ -0,0 +1,49 @@ +{ + "events": [ + { + "type": "case", + "testPath": [ + { + "type": "file", + "name": "packages/e2e/tests/a.spec.ts" + }, + { + "type": "testcase", + "name": "smoke \u203a passes" + } + ], + "duration": 0.012, + "status": 1, + "stdout": "", + "stderr": "", + "data": { + "lineNumber": 10 + } + }, + { + "type": "case", + "testPath": [ + { + "type": "file", + "name": "packages/e2e/tests/b.spec.ts" + }, + { + "type": "testcase", + "name": "smoke \u203a already prefixed" + } + ], + "duration": 0.015, + "status": 1, + "stdout": "", + "stderr": "", + "data": { + "lineNumber": 20 + } + } + ], + "testRunner": "playwright", + "group": "", + "noBuild": false, + "flavors": [], + "testSuite": "" +} diff --git a/tests/test_runners/test_playwright.py b/tests/test_runners/test_playwright.py index dbc63c286..86200e93a 100644 --- a/tests/test_runners/test_playwright.py +++ b/tests/test_runners/test_playwright.py @@ -76,11 +76,7 @@ def test_record_test_with_json_option_adds_prefix_from_config(self): 'playwright', '--json', report_file) self.assert_success(result) - - payload = json.loads(gzip.decompress(self.find_request('/events').request.body).decode()) - test_paths = [unparse_test_path(event.get("testPath")) for event in payload.get("events")] - self.assertIn("file=packages/e2e/tests/a.spec.ts#testcase=smoke › passes", test_paths) - self.assertIn("file=packages/e2e/tests/b.spec.ts#testcase=smoke › already prefixed", test_paths) + self.assert_record_tests_payload('record_test_result_with_prefix.json') @responses.activate @mock.patch.dict(os.environ, @@ -102,8 +98,4 @@ def test_record_test_with_json_option_respects_base_path(self): 'playwright', '--json', str(report_file)) self.assert_success(result) - - payload = json.loads(gzip.decompress(self.find_request('/events').request.body).decode()) - test_paths = [unparse_test_path(event.get("testPath")) for event in payload.get("events")] - self.assertIn("file=tests/a.spec.ts#testcase=smoke › passes", test_paths) - self.assertIn("file=tests/b.spec.ts#testcase=smoke › already prefixed", test_paths) + self.assert_record_tests_payload('record_test_result_with_json_base.json') From 201c07a7579620ae9dc2f083c2d3b9fa1cab6962 Mon Sep 17 00:00:00 2001 From: Naoto Ono Date: Mon, 20 Apr 2026 13:12:14 +0900 Subject: [PATCH 12/14] Refactor tests --- tests/data/playwright/playwright.config.ts | 1 + .../playwright/report_with_json_base.json | 64 +++++++++++++++++++ tests/test_runners/test_playwright.py | 21 ++---- 3 files changed, 72 insertions(+), 14 deletions(-) create mode 100644 tests/data/playwright/playwright.config.ts create mode 100644 tests/data/playwright/report_with_json_base.json diff --git a/tests/data/playwright/playwright.config.ts b/tests/data/playwright/playwright.config.ts new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/tests/data/playwright/playwright.config.ts @@ -0,0 +1 @@ + diff --git a/tests/data/playwright/report_with_json_base.json b/tests/data/playwright/report_with_json_base.json new file mode 100644 index 000000000..0608a1360 --- /dev/null +++ b/tests/data/playwright/report_with_json_base.json @@ -0,0 +1,64 @@ +{ + "config": { + "configFile": "tests/data/playwright/playwright.config.ts", + "rootDir": "tests/data/playwright/packages/e2e" + }, + "suites": [ + { + "title": "tests/a.spec.ts", + "specs": [], + "suites": [ + { + "title": "smoke", + "specs": [ + { + "title": "passes", + "line": 10, + "tests": [ + { + "results": [ + { + "status": "passed", + "duration": 12, + "stdout": [], + "errors": [] + } + ] + } + ] + } + ], + "suites": [] + } + ] + }, + { + "title": "packages/e2e/tests/b.spec.ts", + "specs": [], + "suites": [ + { + "title": "smoke", + "specs": [ + { + "title": "already prefixed", + "line": 20, + "tests": [ + { + "results": [ + { + "status": "passed", + "duration": 15, + "stdout": [], + "errors": [] + } + ] + } + ] + } + ], + "suites": [] + } + ] + } + ] +} diff --git a/tests/test_runners/test_playwright.py b/tests/test_runners/test_playwright.py index 86200e93a..cff26d296 100644 --- a/tests/test_runners/test_playwright.py +++ b/tests/test_runners/test_playwright.py @@ -81,21 +81,14 @@ def test_record_test_with_json_option_adds_prefix_from_config(self): @responses.activate @mock.patch.dict(os.environ, {"LAUNCHABLE_TOKEN": CliTestCase.launchable_token}) + @unittest.skipIf( + sys.platform.startswith("win"), + "The base path fixture uses POSIX-style paths" + ) def test_record_test_with_json_option_respects_base_path(self): - project_root = Path(self.dir, "repo") - base_path = project_root / "packages" / "e2e" - base_path.mkdir(parents=True, exist_ok=True) - - report = self.load_json_from_file(self.test_files_dir.joinpath("report_with_prefix.json")) - report["config"]["configFile"] = str(project_root / "playwright.config.ts") - report["config"]["rootDir"] = str(base_path) - - report_file = Path(self.dir, "report_with_prefix_base.json") - with report_file.open("w") as f: - json.dump(report, f) - - result = self.cli('record', 'tests', '--session', self.session, '--base', str(base_path), - 'playwright', '--json', str(report_file)) + result = self.cli('record', 'tests', '--session', self.session, + '--base', str(self.test_files_dir.joinpath("packages", "e2e")), + 'playwright', '--json', str(self.test_files_dir.joinpath("report_with_json_base.json"))) self.assert_success(result) self.assert_record_tests_payload('record_test_result_with_json_base.json') From f73e181ea8d88bd1dbd789d68339b7a953aa55a4 Mon Sep 17 00:00:00 2001 From: Naoto Ono Date: Mon, 20 Apr 2026 13:18:54 +0900 Subject: [PATCH 13/14] Add empty dir --- tests/data/playwright/packages/e2e/.gitkeep | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 tests/data/playwright/packages/e2e/.gitkeep diff --git a/tests/data/playwright/packages/e2e/.gitkeep b/tests/data/playwright/packages/e2e/.gitkeep new file mode 100644 index 000000000..e69de29bb From 28e5a4f8a90915abc160c4426c52cb2c5944b38e Mon Sep 17 00:00:00 2001 From: Naoto Ono Date: Mon, 20 Apr 2026 15:26:12 +0900 Subject: [PATCH 14/14] Add TODO comments --- launchable/test_runners/playwright.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/launchable/test_runners/playwright.py b/launchable/test_runners/playwright.py index 704cd4c41..2b306b185 100644 --- a/launchable/test_runners/playwright.py +++ b/launchable/test_runners/playwright.py @@ -199,6 +199,9 @@ def _compute_root_dir_relpath(self, report: Dict) -> str: config: Dict = report.get("config", {}) config_dir = self._config_dir(report) root_dir = str(config.get("rootDir", "")) + # TODO: We currently haven't supported the following sibling/parent path cases. + # configFile = /repo/foo/playwright.config.ts + # rootDir = /repo/tests if config_dir and root_dir: return relative_subpath(root_dir, config_dir)