summaryrefslogtreecommitdiff
path: root/tests/playwright_tests/admin.py
blob: 85e63964efdcc206f6e2785c2829a53c8b8f4f99 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
from django.contrib.staticfiles.testing import StaticLiveServerTestCase
from django.test import modify_settings, override_settings
from django.utils.csp import CSP
from django.utils.translation import gettext as _

from .base import PlaywrightTestCase

# Make unittest ignore frames in this module when reporting failures.
__unittest = True


@modify_settings(
    MIDDLEWARE={"append": "django.middleware.csp.ContentSecurityPolicyMiddleware"}
)
@override_settings(
    SECURE_CSP={
        "default-src": [CSP.NONE],
        "connect-src": [CSP.SELF],
        "img-src": [CSP.SELF],
        "script-src": [CSP.SELF],
        "style-src": [CSP.SELF],
    },
)
class AdminPlaywrightTestCase(PlaywrightTestCase, StaticLiveServerTestCase):
    available_apps = [
        "django.contrib.admin",
        "django.contrib.auth",
        "django.contrib.contenttypes",
        "django.contrib.sessions",
        "django.contrib.sites",
    ]

    @classmethod
    def setUpClass(cls):
        super().setUpClass()
        # Track CSP violations via SecurityPolicyViolationEvent.
        cls._csp_violations = []
        cls._browser_context.expose_function(
            "cspviolation",
            lambda violation: cls._csp_violations.append(violation),
        )
        cls._browser_context.add_init_script("""
            document.addEventListener('securitypolicyviolation', (e) => {
                window.cspviolation({
                    blockedURI: e.blockedURI,
                    violatedDirective: e.violatedDirective,
                    effectiveDirective: e.effectiveDirective,
                    originalPolicy: e.originalPolicy,
                    documentURI: e.documentURI,
                });
            });
        """)

    def tearDown(self):
        try:
            # WebKit injects inline style during screenshot(), which triggers
            # a style-src-elem CSP violation.
            violations = [
                violation
                for violation in self._csp_violations
                if not (
                    self.browser == "webkit"
                    and violation["effectiveDirective"] == "style-src-elem"
                    and violation["blockedURI"] == "inline"
                )
            ]
            # Ensure that no CSP violations were logged in the browser.
            self.assertEqual(violations, [])
        finally:
            self._csp_violations.clear()
            super().tearDown()

    def admin_login(self, username, password, login_url="/admin/"):
        """
        Log in to the admin.
        """
        self.page.goto(f"{self.live_server_url}{login_url}")
        self.page.locator('[name="username"]').fill(username)
        self.page.locator('[name="password"]').fill(password)
        login_text = _("Log in")
        self.page.get_by_role("button", name=login_text).click()
        self.page.wait_for_url(f"{self.live_server_url}{login_url}")

    def click_and_expect_popup_to_close(self, locator):
        """
        Click a button that closes the popup and wait until the popup is
        closed.
        """
        from playwright.sync_api import Error

        popup = locator.page
        with popup.expect_event("close"):
            try:
                locator.click(no_wait_after=True)
            except Error:
                # The popup may close before Playwright finishes the click.
                # See https://github.com/microsoft/playwright/issues/26900
                if not popup.is_closed():
                    raise

    def _assertOptionsValues(self, options_selector, values):
        options = self.page.locator(options_selector)
        if values:
            actual_values = []
            for option in options.all():
                actual_values.append(option.get_attribute("value"))
            self.assertEqual(values, actual_values)
        else:
            # Wait until no options match the selector, as expected after a
            # DOM update that clears the <select>.
            self.expect(options).to_have_count(0)

    def assertSelectOptions(self, selector, values):
        """
        Assert that the <SELECT> widget identified by `selector` has the
        options with the given `values`.
        """
        self._assertOptionsValues("%s > option" % selector, values)

    def assertSelectedOptions(self, selector, values):
        """
        Assert that the <SELECT> widget identified by `selector` has the
        selected options with the given `values`.
        """
        self._assertOptionsValues("%s > option:checked" % selector, values)