mirror of
https://github.com/sissbruecker/linkding.git
synced 2026-02-28 06:53:12 +08:00
* Run tests in CI in parallel * make tests automatically open/close playwright * fix parallel tests and screenshots * fix capturing screenshots for non-failing tests * cleanup * cleanup * format * log js errors * provide screenshots as artifacts * remove old scripts
152 lines
5.2 KiB
Python
152 lines
5.2 KiB
Python
import os
|
|
|
|
from django.contrib.staticfiles.testing import LiveServerTestCase
|
|
from playwright.sync_api import BrowserContext, Page, sync_playwright
|
|
from playwright.sync_api import expect
|
|
|
|
from bookmarks.tests.helpers import BookmarkFactoryMixin
|
|
|
|
SCREENSHOT_DIR = "test-results/screenshots"
|
|
|
|
# Allow Django ORM operations within Playwright's async context
|
|
os.environ.setdefault("DJANGO_ALLOW_ASYNC_UNSAFE", "true")
|
|
|
|
|
|
class LinkdingE2ETestCase(LiveServerTestCase, BookmarkFactoryMixin):
|
|
def setUp(self) -> None:
|
|
self.client.force_login(self.get_or_create_test_user())
|
|
self.cookie = self.client.cookies["sessionid"]
|
|
self.playwright = None
|
|
self.browser = None
|
|
self.context = None
|
|
self.page = None
|
|
|
|
def tearDown(self) -> None:
|
|
if self.page and self._test_has_failed():
|
|
self._capture_screenshot()
|
|
if self.browser:
|
|
self.browser.close()
|
|
if self.playwright:
|
|
self.playwright.stop()
|
|
super().tearDown()
|
|
|
|
def _test_has_failed(self) -> bool:
|
|
"""Detect if the current test has failed. Works with both Django/unittest and pytest."""
|
|
# Check _outcome for failure info
|
|
if self._outcome is not None:
|
|
result = self._outcome.result
|
|
if result:
|
|
# pytest stores exception info in _excinfo
|
|
if hasattr(result, "_excinfo") and result._excinfo:
|
|
return True
|
|
# Django/unittest stores failures and errors in the result
|
|
# Check if THIS test is in failures/errors (not just any test)
|
|
if hasattr(result, "failures"):
|
|
for failed_test, _ in result.failures:
|
|
if failed_test is self:
|
|
return True
|
|
return False
|
|
|
|
def _ensure_playwright(self):
|
|
if not self.playwright:
|
|
self.playwright = sync_playwright().start()
|
|
self.browser = self.playwright.chromium.launch(headless=True)
|
|
|
|
def _capture_screenshot(self):
|
|
os.makedirs(SCREENSHOT_DIR, exist_ok=True)
|
|
filename = f"{self.__class__.__name__}_{self._testMethodName}.png"
|
|
filepath = os.path.join(SCREENSHOT_DIR, filename)
|
|
self.page.screenshot(path=filepath, full_page=True)
|
|
|
|
def setup_browser(self) -> BrowserContext:
|
|
self._ensure_playwright()
|
|
context = self.browser.new_context()
|
|
context.add_cookies(
|
|
[
|
|
{
|
|
"name": "sessionid",
|
|
"value": self.cookie.value,
|
|
"domain": self.live_server_url.replace("http:", ""),
|
|
"path": "/",
|
|
}
|
|
]
|
|
)
|
|
return context
|
|
|
|
def open(self, url: str) -> Page:
|
|
self.context = self.setup_browser()
|
|
self.page = self.context.new_page()
|
|
self.page.on("pageerror", self.on_page_error)
|
|
self.page.goto(self.live_server_url + url)
|
|
self.page.on("load", self.on_load)
|
|
self.num_loads = 0
|
|
return self.page
|
|
|
|
def on_page_error(self, error):
|
|
print(f"[JS ERROR] {error}")
|
|
if hasattr(error, "stack"):
|
|
print(f"[JS STACK] {error.stack}")
|
|
|
|
def on_load(self):
|
|
self.num_loads += 1
|
|
|
|
def assertReloads(self, count: int):
|
|
self.assertEqual(self.num_loads, count)
|
|
|
|
def resetReloads(self):
|
|
self.num_loads = 0
|
|
|
|
def locate_bookmark_list(self):
|
|
return self.page.locator("ul.bookmark-list")
|
|
|
|
def locate_bookmark(self, title: str):
|
|
bookmark_tags = self.page.locator("ul.bookmark-list > li")
|
|
return bookmark_tags.filter(has_text=title)
|
|
|
|
def count_bookmarks(self):
|
|
bookmark_tags = self.page.locator("ul.bookmark-list > li")
|
|
return bookmark_tags.count()
|
|
|
|
def locate_details_modal(self):
|
|
return self.page.locator("ld-details-modal")
|
|
|
|
def open_details_modal(self, bookmark):
|
|
details_button = self.locate_bookmark(bookmark.title).get_by_text("View")
|
|
details_button.click()
|
|
|
|
details_modal = self.locate_details_modal()
|
|
expect(details_modal).to_be_visible()
|
|
|
|
return details_modal
|
|
|
|
def locate_bulk_edit_bar(self):
|
|
return self.page.locator(".bulk-edit-bar")
|
|
|
|
def locate_bulk_edit_select_all(self):
|
|
return self.locate_bulk_edit_bar().locator("label.bulk-edit-checkbox.all")
|
|
|
|
def locate_bulk_edit_select_across(self):
|
|
return self.locate_bulk_edit_bar().locator("label.select-across")
|
|
|
|
def locate_bulk_edit_toggle(self):
|
|
return self.page.get_by_title("Bulk edit")
|
|
|
|
def select_bulk_action(self, value: str):
|
|
return (
|
|
self.locate_bulk_edit_bar()
|
|
.locator('select[name="bulk_action"]')
|
|
.select_option(value)
|
|
)
|
|
|
|
def navigate_menu(self, main_menu_item: str, sub_menu_item: str | None = None):
|
|
if sub_menu_item:
|
|
self.page.locator("nav").get_by_role("button", name=main_menu_item).click()
|
|
self.page.locator("nav ul.menu").get_by_text(
|
|
sub_menu_item, exact=True
|
|
).click()
|
|
else:
|
|
self.page.locator("nav").get_by_text(main_menu_item, exact=True).click()
|
|
|
|
def locate_confirm_dialog(self):
|
|
return self.page.locator("ld-confirm-dropdown")
|