Skip to content

Fixed #37154 -- Switched from Selenium to Playwright for integration testing.#7

Draft
varunkasyap wants to merge 1 commit into
mainfrom
playwright-migration
Draft

Fixed #37154 -- Switched from Selenium to Playwright for integration testing.#7
varunkasyap wants to merge 1 commit into
mainfrom
playwright-migration

Conversation

@varunkasyap

@varunkasyap varunkasyap commented May 28, 2026

Copy link
Copy Markdown
Owner

Notes:

  • selenium find element refer to actual DOM element, but playwright locator is not DOM reference, its lazy selector (lazy evaluation model).

so, Extracting the Tag Name from an Element tag_name selenium possible
example

element = driver.find_element(By.ID, "submit-button")
element_tag = element.tag_name
print(element_tag)  # Output: e.g., "button" or "input"

due to architecture change in Playwright, it doesn't natively expose DOM properties.

playwright translation looks like


element_tag = locator.evaluate("el => el.tagName").lower()

run: python -m pip install --upgrade pip wheel
- run: python -m pip install -r tests/requirements/py3.txt -e .
- name: Install Playwright browsers
run: python -m playwright install --with-deps chromium

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@varunkasyap

Copy link
Copy Markdown
Owner Author

In Playright:
wait_for_load_state() - Returns when the required load state has been reached.

Arguments

  1. state "load" | "domcontentloaded" | "networkidle" (optional)#

example-

  • page.wait_for_load_state("load")
  • page.wait_for_load_state("domcontentloaded")
  • page.wait_for_load_state("networkidle")

Optional load state to wait for, defaults to load. If the state has been already reached while loading current document, the method resolves immediately. Can be one of:

'load' - wait for the load event to be fired.
'domcontentloaded' - wait for the DOMContentLoaded event to be fired.
'networkidle' - DISCOURAGED wait until there are no network connections for at least 500 ms. Don't use this method for testing, rely on web assertions to assess readiness instead.

timeout float (optional)#

@varunkasyap

Copy link
Copy Markdown
Owner Author

In playwright:

expect() is used for assetions

reference - https://playwright.dev/python/docs/test-assertions#list-of-assertions

@varunkasyap

Copy link
Copy Markdown
Owner Author

Playwright uses milliseconds, whereas, selenium used seconds

Comment thread django/test/playwright.py
Comment on lines +126 to +127
cls._old_async_unsafe = os.environ.get("DJANGO_ALLOW_ASYNC_UNSAFE")
os.environ["DJANGO_ALLOW_ASYNC_UNSAFE"] = "true"

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

otherwise, getting this error

File "C:\Users\varun\OneDrive\Desktop\os\django\django\utils\asyncio.py", line 24, in inner
raise SynchronousOnlyOperation(message)
django.core.exceptions.SynchronousOnlyOperation: You cannot call this from an async context - use a thread or sync_to_async.

Comment thread django/test/playwright.py Outdated
cls._browser_context = cls._browser.new_context()
cls._browser_logs = []
cls.page = cls._browser_context.new_page()
# Set up CDP session for browser-level logs (Chromium only).

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

temporary workaround for now, TODO - should make this compatible for all browsers.

@sarahboyce sarahboyce left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you!
I've added a few suggestions from an initial look at the tests

Comment thread tests/runtests.py
Comment on lines +803 to +810
if (
multiprocessing.get_start_method() in {"spawn", "forkserver"}
and options.parallel != 1
):
parser.error(
"You cannot use --playwright with parallel tests on this system. "
"Pass --parallel=1 to use --playwright."
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (
multiprocessing.get_start_method() in {"spawn", "forkserver"}
and options.parallel != 1
):
parser.error(
"You cannot use --playwright with parallel tests on this system. "
"Pass --parallel=1 to use --playwright."
)

I can remove this without errors. I think ideally our tests don't need to have a shared state so would be interested if we can have this be removed and only added if really needed

Comment thread tests/runtests.py Outdated
options.tags = ["playwright"]
elif "playwright" not in options.tags:
options.tags.append("playwright")
# TODO VK external_host and playwright_hub ??

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we will need a playwright_hub

Comment thread tests/runtests.py Outdated
Comment on lines +652 to +657
parser.add_argument(
"--playwright-headless",
action="store_true",
help="Run Playwright tests in headless mode, if the browser supports "
"the option.",
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think let's reuse the existing headless option

Comment on lines +54 to +55
self.page.click(f"input[value='{login_text}']")
self.page.wait_for_load_state("load")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
self.page.click(f"input[value='{login_text}']")
self.page.wait_for_load_state("load")
with self.page.expect_navigation():
self.page.get_by_role("button", name=login_text).click()

Perhaps?

@varunkasyap varunkasyap Jun 4, 2026

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the review @sarahboyce

expect_navigation() seems depricated

Playwight is suggesting to use page.wait_for_url() instead.

so,

Suggested change
self.page.click(f"input[value='{login_text}']")
self.page.wait_for_load_state("load")
self.page.get_by_role("button", name=login_text).click()
self.page.wait_for_url(f"{self.live_server_url}{login_url}")

Perhaps?

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can have expect(page).to_have_url(...) and I think the url is the admin index page
I'm not sure what's best practice really, it would be good for us to agree what we should do in most navigation cases

Comment on lines +38 to +44
size = self.page.viewport_size
self.page.set_viewport_size(
{"width": size["width"] + 1, "height": size["height"]}
)
self.page.wait_for_load_state("domcontentloaded")
self.page.set_viewport_size(size)
self.page.wait_for_load_state("domcontentloaded")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
size = self.page.viewport_size
self.page.set_viewport_size(
{"width": size["width"] + 1, "height": size["height"]}
)
self.page.wait_for_load_state("domcontentloaded")
self.page.set_viewport_size(size)
self.page.wait_for_load_state("domcontentloaded")
self.page.evaluate(
"window.dispatchEvent(new Event('resize'))"
)

And perhaps the selenium version should be:

        self.selenium.execute_script(
            "window.dispatchEvent(new Event('resize'));"
        )

I also think we barely use this so wondering if it is really worth being a helper method 🤔

self.page.click(f"input[value='{login_text}']")
self.page.wait_for_load_state("load")

def select_option(self, selector, value):

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As agreed, we should decide if each of these helpers are needed

Comment on lines +179 to +181
is_valid = self.page.evaluate(
"document.getElementById('id_number').checkValidity()"
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
is_valid = self.page.evaluate(
"document.getElementById('id_number').checkValidity()"
)
is_valid = number_input.evaluate("el => el.checkValidity()")

Perhaps

Comment thread django/test/playwright.py

@tag("playwright")
class PlaywrightTestCase(LiveServerTestCase, metaclass=PlaywrightTestCaseBase):
default_timeout = 10000 # milliseconds

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the default_timout logic required? Not sure if we were hacking in things for selenium which playwright solves for us

Comment on lines +20 to +21
self.page.locator("#submit").click()
self.page.wait_for_load_state("load")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
self.page.locator("#submit").click()
self.page.wait_for_load_state("load")
with self.page.expect_navigation():
self.page.locator("#submit").click()

Comment thread tests/view_tests/tests/test_i18n.py Outdated
from playwright.sync_api import expect

self.page.goto(self.live_server_url + "/jsi18n_template/")
self.page.wait_for_load_state("domcontentloaded")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this needed?

@varunkasyap
varunkasyap force-pushed the playwright-migration branch 7 times, most recently from 52b738f to 4144053 Compare June 10, 2026 05:57
@varunkasyap
varunkasyap force-pushed the playwright-migration branch from 4144053 to 342dae8 Compare June 12, 2026 10:26
@varunkasyap varunkasyap changed the title Added Playwright base classes and CI. Fixed #37154 -- Switched from Selenium to Playwright for integration testing. Jun 12, 2026
@varunkasyap
varunkasyap force-pushed the playwright-migration branch from 342dae8 to c4a0493 Compare June 14, 2026 15:22
# Confirm they're selected after clicking inactive buttons: ticket
# #26575
self.assertSelectedOptions(from_box, [str(self.peter.id), str(self.lisa.id)])
self.page.locator(remove_button).click(force=True)

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Selenium test intentionally clicks disabled buttons

Playwright refuses to click disabled elements by default. Hence, added force=True

@varunkasyap
varunkasyap force-pushed the playwright-migration branch from c4a0493 to 2884c14 Compare June 14, 2026 17:15

def setUp(self):
if self.browser == "webkit":
self.skipTest("WebKit Tab key only focuses form controls, not links.")

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Safari User Guide says:

Highlight the next field or pop-up menu on a web page: Tab
Highlight the next field, pop-up menu or clickable item (such as a link) on a web page: Option-Tab

reference:
https://support.apple.com/en-in/guide/safari/cpsh003/mac#:~:text=Highlight%20the%20next,of%20Safari%20settings.

# `Skip link` is not present.
skip_link = self.page.locator(".skip-to-content-link")
self.assertFalse(
skip_link.evaluate("el => el.getBoundingClientRect().top >= 0")

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The skip link hides via top: -999px CSS, which is off-screen but Playwright's is_visible() still considers it visible. Checking getBoundingClientRect().top >= 0 can detects whether the element is on-screen.

alternatives:
• self.assertFalse(...) → expect(skip_link).not_to_be_in_viewport()
• self.assertTrue(...) → expect(skip_link).to_be_in_viewport()

@varunkasyap
varunkasyap force-pushed the playwright-migration branch 5 times, most recently from 2575577 to 07f99f2 Compare June 19, 2026 08:27
@varunkasyap
varunkasyap marked this pull request as draft June 19, 2026 10:48
@varunkasyap
varunkasyap force-pushed the playwright-migration branch 2 times, most recently from f3415ea to 68477c4 Compare June 21, 2026 17:24
@varunkasyap
varunkasyap marked this pull request as ready for review June 22, 2026 16:31
@varunkasyap
varunkasyap force-pushed the playwright-migration branch from 82018a6 to ae8d708 Compare June 22, 2026 16:36
@varunkasyap
varunkasyap marked this pull request as draft June 22, 2026 16:37
@varunkasyap
varunkasyap force-pushed the playwright-migration branch 7 times, most recently from 8cdb706 to da7fdf5 Compare June 30, 2026 08:22
@varunkasyap
varunkasyap force-pushed the playwright-migration branch 10 times, most recently from a9b85d9 to b287baf Compare July 7, 2026 17:28
@varunkasyap
varunkasyap force-pushed the playwright-migration branch 6 times, most recently from 942774b to e27b977 Compare July 11, 2026 08:55
@varunkasyap
varunkasyap force-pushed the playwright-migration branch from e27b977 to cfbafac Compare July 15, 2026 03:47
@varunkasyap
varunkasyap force-pushed the playwright-migration branch from cfbafac to d902339 Compare July 16, 2026 05:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants