Skip to content

Commit

Permalink
add SaveScreenshot Action.
Browse files Browse the repository at this point in the history
  • Loading branch information
perrygoy committed Feb 20, 2024
1 parent aba843c commit d904ea1
Show file tree
Hide file tree
Showing 5 changed files with 190 additions and 1 deletion.
6 changes: 6 additions & 0 deletions docs/extended_api/actions.rst
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,9 @@ Open

.. autoclass:: Open
:members:

SaveScreenshot
--------------

.. autoclass:: SaveScreenshot
:members:
2 changes: 2 additions & 0 deletions screenpy_playwright/actions/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -13,4 +14,5 @@
"Enter",
"Open",
"Visit",
"SaveScreenshot",
]
108 changes: 108 additions & 0 deletions screenpy_playwright/actions/save_screenshot.py
Original file line number Diff line number Diff line change
@@ -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
73 changes: 72 additions & 1 deletion tests/test_actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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)
2 changes: 2 additions & 0 deletions tests/test_namespace.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ def test_screenpy_playwright() -> None:
"Number",
"Open",
"PageObject",
"SaveScreenshot",
"Target",
"TargetingError",
"Text",
Expand All @@ -29,6 +30,7 @@ def test_actions() -> None:
"Click",
"Enter",
"Open",
"SaveScreenshot",
"Visit",
]
assert sorted(screenpy_playwright.actions.__all__) == sorted(expected)
Expand Down

0 comments on commit d904ea1

Please sign in to comment.