From d904ea1f7e4de29340a77e6cad9625c52531baa9 Mon Sep 17 00:00:00 2001 From: perrygoy Date: Tue, 20 Feb 2024 14:21:24 -0600 Subject: [PATCH] add SaveScreenshot Action. --- docs/extended_api/actions.rst | 6 + screenpy_playwright/actions/__init__.py | 2 + .../actions/save_screenshot.py | 108 ++++++++++++++++++ tests/test_actions.py | 73 +++++++++++- tests/test_namespace.py | 2 + 5 files changed, 190 insertions(+), 1 deletion(-) create mode 100644 screenpy_playwright/actions/save_screenshot.py diff --git a/docs/extended_api/actions.rst b/docs/extended_api/actions.rst index befa8db..4e5ccbc 100644 --- a/docs/extended_api/actions.rst +++ b/docs/extended_api/actions.rst @@ -27,3 +27,9 @@ Open .. autoclass:: Open :members: + +SaveScreenshot +-------------- + +.. autoclass:: SaveScreenshot + :members: diff --git a/screenpy_playwright/actions/__init__.py b/screenpy_playwright/actions/__init__.py index 91cb179..ba466fe 100644 --- a/screenpy_playwright/actions/__init__.py +++ b/screenpy_playwright/actions/__init__.py @@ -3,6 +3,7 @@ from .click import Click from .enter import Enter from .open import Open +from .save_screenshot import SaveScreenshot # Natural-language-enabling syntactic sugar Visit = Open @@ -13,4 +14,5 @@ "Enter", "Open", "Visit", + "SaveScreenshot", ] diff --git a/screenpy_playwright/actions/save_screenshot.py b/screenpy_playwright/actions/save_screenshot.py new file mode 100644 index 0000000..64e10ef --- /dev/null +++ b/screenpy_playwright/actions/save_screenshot.py @@ -0,0 +1,108 @@ +"""Save a screenshot of the current page.""" + +from __future__ import annotations + +import os +from pathlib import Path +from typing import TYPE_CHECKING, Any + +from screenpy import AttachTheFile, UnableToAct, beat + +from ..abilities import BrowseTheWebSynchronously + +if TYPE_CHECKING: + from screenpy import Actor + from typing_extensions import Self + + +class SaveScreenshot: + """Save a screenshot of the Actor's current page. + + Use the :meth:`~screenpy_playwright.actions.save_screenshot.and_attach_it` + method to indicate that this screenshot should be attached to all reports + through the Narrator's adapters. This method also accepts any keyword + arguments those adapters might require. + + Abilities Required: + :class:`~screenpy_playwright.abilities.BrowseTheWebSynchronously` + + Examples:: + + the_actor.attempts_to(SaveScreenshot("screenshot.png")) + + the_actor.attempts_to(SaveScreenshot.as_(filepath)) + + # attach file to the Narrator's reports (behavior depends on adapter). + the_actor.attempts_to(SaveScreenshot.as_(filepath).and_attach_it()) + + # using screenpy_adapter_allure plugin! + from allure_commons.types import AttachmentTypes + the_actor.attempts_to( + SaveScreenshot.as_(filepath).and_attach_it_with( + attachment_type=AttachmentTypes.PNG, + ), + ) + """ + + attach_kwargs: dict | None + path: str + filename: str + + def describe(self) -> str: + """Describe the Action in present tense.""" + return f"Save screenshot as {self.filename}" + + @classmethod + def as_(cls, path: str) -> Self: + """Supply the name and/or filepath for the screenshot. + + If only a name is supplied, the screenshot will appear in the current + working directory. + + Args: + path: The filepath for the screenshot, including its name. + + Returns: + Self + """ + return cls(path=path) + + def and_attach_it(self, **kwargs: Any) -> Self: # noqa: ANN401 + """Indicate the screenshot should be attached to any reports. + + This method accepts any additional keywords needed by any adapters + attached for :external+screenpy:ref:`Narration`. + + Args: + **kwargs: keyword arguments for the adapters used by the narrator. + + Returns: + Self + """ + self.attach_kwargs = kwargs + return self + + and_attach_it_with = and_attach_it + + @beat("{} saves a screenshot as {filename}") + def perform_as(self, the_actor: Actor) -> None: + """Direct the actor to save a screenshot. + + Args: + the_actor: The actor who will perform this Action. + """ + current_page = the_actor.ability_to(BrowseTheWebSynchronously).current_page + if current_page is None: + msg = "No page has been opened! Cannot save a screenshot." + raise UnableToAct(msg) + + screenshot = current_page.screenshot(path=self.path) + Path(self.path).write_bytes(screenshot) + + if self.attach_kwargs is not None: + the_actor.attempts_to(AttachTheFile(self.path, **self.attach_kwargs)) + + def __init__(self, path: str) -> None: + self.path = path + self.filename = path.split(os.path.sep)[-1] + self.attach_kwargs = None diff --git a/tests/test_actions.py b/tests/test_actions.py index 6804dd1..05434cd 100644 --- a/tests/test_actions.py +++ b/tests/test_actions.py @@ -5,7 +5,13 @@ import pytest from screenpy import Actor, Describable, Performable, UnableToAct -from screenpy_playwright import BrowseTheWebSynchronously, Click, Enter, Visit +from screenpy_playwright import ( + BrowseTheWebSynchronously, + Click, + Enter, + SaveScreenshot, + Visit, +) from .useful_mocks import get_mocked_target_and_element @@ -139,3 +145,68 @@ def test_perform_visit(self, Tester: Actor) -> None: mock_page.goto.assert_called_once_with(url) assert mock_ability.current_page == mock_page assert mock_page in mock_ability.pages + + +class TestSaveScreenshot: + + class_path = "screenpy_playwright.actions.save_screenshot" + + def test_can_be_instantiated(self) -> None: + ss1 = SaveScreenshot("./screenshot.png") + ss2 = SaveScreenshot.as_("./screenshot.png") + ss3 = SaveScreenshot.as_("./screenshot.png").and_attach_it() + ss4 = SaveScreenshot.as_("./a_witch.png").and_attach_it(me="newt") + + assert isinstance(ss1, SaveScreenshot) + assert isinstance(ss2, SaveScreenshot) + assert isinstance(ss3, SaveScreenshot) + assert isinstance(ss4, SaveScreenshot) + + def test_implements_protocol(self) -> None: + ss = SaveScreenshot("./screenshot.png") + + assert isinstance(ss, Describable) + assert isinstance(ss, Performable) + + def test_filepath_vs_filename(self) -> None: + test_name = "mmcmanus.png" + test_path = f"boondock/saints/{test_name}" + + ss = SaveScreenshot.as_(test_path) + + assert ss.path == test_path + assert ss.filename == test_name + + @mock.patch(f"{class_path}.AttachTheFile", autospec=True) + def test_perform_sends_kwargs_to_attach( + self, mocked_attachthefile: mock.Mock, Tester: Actor + ) -> None: + test_path = "souiiie.png" + test_kwargs = {"color": "Red", "weather": "Tornado"} + current_page = mock.Mock() + btws = Tester.ability_to(BrowseTheWebSynchronously) + btws.pages.append(current_page) + btws.current_page = current_page + + with mock.patch(f"{self.class_path}.Path") as mocked_path: + SaveScreenshot(test_path).and_attach_it(**test_kwargs).perform_as(Tester) + + mocked_attachthefile.assert_called_once_with(test_path, **test_kwargs) + mocked_path().write.assert_called_once() + + def test_describe(self) -> None: + assert SaveScreenshot("pth").describe() == "Save screenshot as pth" + + def test_subclass(self) -> None: + """test code for mypy to scan without issue""" + + class SubSaveScreenshot(SaveScreenshot): + pass + + sss1 = SubSaveScreenshot("./screenshot.png") + sss2 = SubSaveScreenshot.as_("./screenshot.png") + sss3 = SubSaveScreenshot.as_("./screenshot.png").and_attach_it() + + assert isinstance(sss1, SubSaveScreenshot) + assert isinstance(sss2, SubSaveScreenshot) + assert isinstance(sss3, SubSaveScreenshot) diff --git a/tests/test_namespace.py b/tests/test_namespace.py index a0ec127..adc4e1d 100644 --- a/tests/test_namespace.py +++ b/tests/test_namespace.py @@ -9,6 +9,7 @@ def test_screenpy_playwright() -> None: "Number", "Open", "PageObject", + "SaveScreenshot", "Target", "TargetingError", "Text", @@ -29,6 +30,7 @@ def test_actions() -> None: "Click", "Enter", "Open", + "SaveScreenshot", "Visit", ] assert sorted(screenpy_playwright.actions.__all__) == sorted(expected)