diff --git a/launchable/test_runners/playwright.py b/launchable/test_runners/playwright.py index 87e803213..b7d0c25fb 100644 --- a/launchable/test_runners/playwright.py +++ b/launchable/test_runners/playwright.py @@ -3,13 +3,14 @@ # https://playwright.dev/ # import json +from pathlib import Path from typing import Dict, Generator, List import click 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 = " › " @@ -173,13 +174,35 @@ 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) + 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 = str(s.get("title", "")) + test_file = prepend_path_if_missing(str(s.get("title", "")), root_dir_relpath) for event in self._parse_suites(test_file, s, []): yield event + 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 + (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 "" + + 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/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) 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()