Run tests in CI in parallel (#1254)

* 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
This commit is contained in:
Sascha Ißbrücker
2026-01-01 01:46:31 +01:00
committed by GitHub
parent df595f2219
commit 38d450a916
23 changed files with 1073 additions and 1141 deletions

View File

@@ -30,7 +30,7 @@ jobs:
uv sync
mkdir data
- name: Run tests
run: uv run manage.py test bookmarks.tests
run: uv run pytest -n auto
e2e_tests:
name: E2E Tests
runs-on: ubuntu-latest
@@ -59,4 +59,10 @@ jobs:
npm run build
uv run manage.py collectstatic
- name: Run tests
run: uv run manage.py test bookmarks.tests_e2e --pattern="e2e_test_*.py"
run: uv run pytest bookmarks/tests_e2e -n auto -o "python_files=e2e_test_*.py"
- name: Upload screenshots
if: failure()
uses: actions/upload-artifact@v4
with:
name: e2e-screenshots
path: test-results/screenshots

1
.gitignore vendored
View File

@@ -60,6 +60,7 @@ coverage.xml
*.cover
.hypothesis/
.pytest_cache/
test-results/
# Translations
*.mo

View File

@@ -1,73 +1,69 @@
from django.urls import reverse
from playwright.sync_api import sync_playwright, expect
from playwright.sync_api import expect
from bookmarks.tests_e2e.helpers import LinkdingE2ETestCase
class A11yNavigationFocusTest(LinkdingE2ETestCase):
def test_initial_page_load_focus(self):
with sync_playwright() as p:
# First page load should keep focus on the body
page = self.open(reverse("linkding:bookmarks.index"), p)
focused_tag = page.evaluate("document.activeElement?.tagName")
self.assertEqual("BODY", focused_tag)
# First page load should keep focus on the body
page = self.open(reverse("linkding:bookmarks.index"))
focused_tag = page.evaluate("document.activeElement?.tagName")
self.assertEqual("BODY", focused_tag)
page.goto(self.live_server_url + reverse("linkding:bookmarks.archived"))
focused_tag = page.evaluate("document.activeElement?.tagName")
self.assertEqual("BODY", focused_tag)
page.goto(self.live_server_url + reverse("linkding:bookmarks.archived"))
focused_tag = page.evaluate("document.activeElement?.tagName")
self.assertEqual("BODY", focused_tag)
page.goto(self.live_server_url + reverse("linkding:settings.general"))
focused_tag = page.evaluate("document.activeElement?.tagName")
self.assertEqual("BODY", focused_tag)
page.goto(self.live_server_url + reverse("linkding:settings.general"))
focused_tag = page.evaluate("document.activeElement?.tagName")
self.assertEqual("BODY", focused_tag)
# Bookmark form views should focus the URL input
page.goto(self.live_server_url + reverse("linkding:bookmarks.new"))
page.wait_for_timeout(timeout=1000)
focused_tag = page.evaluate(
"document.activeElement?.tagName + '|' + document.activeElement?.name"
)
self.assertEqual("INPUT|url", focused_tag)
# Bookmark form views should focus the URL input
page.goto(self.live_server_url + reverse("linkding:bookmarks.new"))
page.wait_for_timeout(timeout=1000)
focused_tag = page.evaluate(
"document.activeElement?.tagName + '|' + document.activeElement?.name"
)
self.assertEqual("INPUT|url", focused_tag)
def test_page_navigation_focus(self):
bookmark = self.setup_bookmark()
with sync_playwright() as p:
page = self.open(reverse("linkding:bookmarks.index"), p)
page = self.open(reverse("linkding:bookmarks.index"))
# Subsequent navigation should move focus to main content
self.reset_focus()
self.navigate_menu("Bookmarks", "Active")
focused = page.locator("main:focus")
expect(focused).to_be_visible()
# Subsequent navigation should move focus to main content
self.reset_focus()
self.navigate_menu("Bookmarks", "Active")
focused = page.locator("main:focus")
expect(focused).to_be_visible()
self.reset_focus()
self.navigate_menu("Bookmarks", "Archived")
focused = page.locator("main:focus")
expect(focused).to_be_visible()
self.reset_focus()
self.navigate_menu("Bookmarks", "Archived")
focused = page.locator("main:focus")
expect(focused).to_be_visible()
self.reset_focus()
self.navigate_menu("Settings", "General")
focused = page.locator("main:focus")
expect(focused).to_be_visible()
self.reset_focus()
self.navigate_menu("Settings", "General")
focused = page.locator("main:focus")
expect(focused).to_be_visible()
# Bookmark form views should focus the URL input
self.reset_focus()
self.navigate_menu("Add bookmark")
focused = page.locator("input[name='url']:focus")
expect(focused).to_be_visible()
# Bookmark form views should focus the URL input
self.reset_focus()
self.navigate_menu("Add bookmark")
focused = page.locator("input[name='url']:focus")
expect(focused).to_be_visible()
# Opening details modal should move focus to close button
self.navigate_menu("Bookmarks", "Active")
self.open_details_modal(bookmark)
focused = page.locator(".modal button.close:focus")
expect(focused).to_be_visible()
# Opening details modal should move focus to close button
self.navigate_menu("Bookmarks", "Active")
self.open_details_modal(bookmark)
focused = page.locator(".modal button.close:focus")
expect(focused).to_be_visible()
# Closing modal should move focus back to the bookmark item
page.keyboard.press("Escape")
focused = self.locate_bookmark(bookmark.title).locator(
"a.view-action:focus"
)
expect(focused).to_be_visible()
# Closing modal should move focus back to the bookmark item
page.keyboard.press("Escape")
focused = self.locate_bookmark(bookmark.title).locator("a.view-action:focus")
expect(focused).to_be_visible()
def reset_focus(self):
self.page.evaluate("document.activeElement.blur()")

View File

@@ -1,6 +1,6 @@
from django.test import override_settings
from django.urls import reverse
from playwright.sync_api import sync_playwright, expect
from playwright.sync_api import expect
from bookmarks.tests_e2e.helpers import LinkdingE2ETestCase
from bookmarks.models import Bookmark
@@ -10,78 +10,74 @@ class BookmarkDetailsModalE2ETestCase(LinkdingE2ETestCase):
def test_show_details(self):
bookmark = self.setup_bookmark()
with sync_playwright() as p:
self.open(reverse("linkding:bookmarks.index"), p)
self.open(reverse("linkding:bookmarks.index"))
details_modal = self.open_details_modal(bookmark)
title = details_modal.locator("h2")
expect(title).to_have_text(bookmark.title)
details_modal = self.open_details_modal(bookmark)
title = details_modal.locator("h2")
expect(title).to_have_text(bookmark.title)
def test_close_details(self):
bookmark = self.setup_bookmark()
with sync_playwright() as p:
self.open(reverse("linkding:bookmarks.index"), p)
self.open(reverse("linkding:bookmarks.index"))
# close with close button
details_modal = self.open_details_modal(bookmark)
details_modal.locator("button.close").click()
expect(details_modal).to_be_hidden()
# close with close button
details_modal = self.open_details_modal(bookmark)
details_modal.locator("button.close").click()
expect(details_modal).to_be_hidden()
# close with backdrop
details_modal = self.open_details_modal(bookmark)
overlay = details_modal.locator(".modal-overlay")
overlay.click(position={"x": 0, "y": 0})
expect(details_modal).to_be_hidden()
# close with backdrop
details_modal = self.open_details_modal(bookmark)
overlay = details_modal.locator(".modal-overlay")
overlay.click(position={"x": 0, "y": 0})
expect(details_modal).to_be_hidden()
# close with escape
details_modal = self.open_details_modal(bookmark)
self.page.keyboard.press("Escape")
expect(details_modal).to_be_hidden()
# close with escape
details_modal = self.open_details_modal(bookmark)
self.page.keyboard.press("Escape")
expect(details_modal).to_be_hidden()
def test_toggle_archived(self):
bookmark = self.setup_bookmark()
with sync_playwright() as p:
# archive
url = reverse("linkding:bookmarks.index")
self.open(url, p)
# archive
url = reverse("linkding:bookmarks.index")
self.open(url)
details_modal = self.open_details_modal(bookmark)
details_modal.get_by_text("Archived", exact=False).click()
expect(self.locate_bookmark(bookmark.title)).not_to_be_visible()
self.assertReloads(0)
details_modal = self.open_details_modal(bookmark)
details_modal.get_by_text("Archived", exact=False).click()
expect(self.locate_bookmark(bookmark.title)).not_to_be_visible()
self.assertReloads(0)
# unarchive
url = reverse("linkding:bookmarks.archived")
self.page.goto(self.live_server_url + url)
self.resetReloads()
# unarchive
url = reverse("linkding:bookmarks.archived")
self.page.goto(self.live_server_url + url)
self.resetReloads()
details_modal = self.open_details_modal(bookmark)
details_modal.get_by_text("Archived", exact=False).click()
expect(self.locate_bookmark(bookmark.title)).not_to_be_visible()
self.assertReloads(0)
details_modal = self.open_details_modal(bookmark)
details_modal.get_by_text("Archived", exact=False).click()
expect(self.locate_bookmark(bookmark.title)).not_to_be_visible()
self.assertReloads(0)
def test_toggle_unread(self):
bookmark = self.setup_bookmark()
with sync_playwright() as p:
# mark as unread
url = reverse("linkding:bookmarks.index")
self.open(url, p)
# mark as unread
url = reverse("linkding:bookmarks.index")
self.open(url)
details_modal = self.open_details_modal(bookmark)
details_modal = self.open_details_modal(bookmark)
details_modal.get_by_text("Unread").click()
bookmark_item = self.locate_bookmark(bookmark.title)
expect(bookmark_item.get_by_text("Unread")).to_be_visible()
self.assertReloads(0)
details_modal.get_by_text("Unread").click()
bookmark_item = self.locate_bookmark(bookmark.title)
expect(bookmark_item.get_by_text("Unread")).to_be_visible()
self.assertReloads(0)
# mark as read
details_modal.get_by_text("Unread").click()
bookmark_item = self.locate_bookmark(bookmark.title)
expect(bookmark_item.get_by_text("Unread")).not_to_be_visible()
self.assertReloads(0)
# mark as read
details_modal.get_by_text("Unread").click()
bookmark_item = self.locate_bookmark(bookmark.title)
expect(bookmark_item.get_by_text("Unread")).not_to_be_visible()
self.assertReloads(0)
def test_toggle_shared(self):
profile = self.get_or_create_test_user().profile
@@ -90,61 +86,58 @@ class BookmarkDetailsModalE2ETestCase(LinkdingE2ETestCase):
bookmark = self.setup_bookmark()
with sync_playwright() as p:
# share bookmark
url = reverse("linkding:bookmarks.index")
self.open(url, p)
# share bookmark
url = reverse("linkding:bookmarks.index")
self.open(url)
details_modal = self.open_details_modal(bookmark)
details_modal = self.open_details_modal(bookmark)
details_modal.get_by_text("Shared").click()
bookmark_item = self.locate_bookmark(bookmark.title)
expect(bookmark_item.get_by_text("Shared")).to_be_visible()
self.assertReloads(0)
details_modal.get_by_text("Shared").click()
bookmark_item = self.locate_bookmark(bookmark.title)
expect(bookmark_item.get_by_text("Shared")).to_be_visible()
self.assertReloads(0)
# unshare bookmark
details_modal.get_by_text("Shared").click()
bookmark_item = self.locate_bookmark(bookmark.title)
expect(bookmark_item.get_by_text("Shared")).not_to_be_visible()
self.assertReloads(0)
# unshare bookmark
details_modal.get_by_text("Shared").click()
bookmark_item = self.locate_bookmark(bookmark.title)
expect(bookmark_item.get_by_text("Shared")).not_to_be_visible()
self.assertReloads(0)
def test_edit_return_url(self):
bookmark = self.setup_bookmark()
with sync_playwright() as p:
url = reverse("linkding:bookmarks.index") + f"?q={bookmark.title}"
self.open(url, p)
url = reverse("linkding:bookmarks.index") + f"?q={bookmark.title}"
self.open(url)
details_modal = self.open_details_modal(bookmark)
details_modal = self.open_details_modal(bookmark)
# Navigate to edit page
with self.page.expect_navigation():
details_modal.get_by_text("Edit").click()
# Navigate to edit page
with self.page.expect_navigation():
details_modal.get_by_text("Edit").click()
# Cancel edit, verify return to details url
details_url = url + f"&details={bookmark.id}"
with self.page.expect_navigation(url=self.live_server_url + details_url):
self.page.get_by_text("Cancel").click()
# Cancel edit, verify return to details url
details_url = url + f"&details={bookmark.id}"
with self.page.expect_navigation(url=self.live_server_url + details_url):
self.page.get_by_text("Cancel").click()
def test_delete(self):
bookmark = self.setup_bookmark()
with sync_playwright() as p:
url = reverse("linkding:bookmarks.index") + f"?q={bookmark.title}"
self.open(url, p)
url = reverse("linkding:bookmarks.index") + f"?q={bookmark.title}"
self.open(url)
details_modal = self.open_details_modal(bookmark)
details_modal = self.open_details_modal(bookmark)
# Wait for confirm button to be initialized
self.page.wait_for_timeout(1000)
# Wait for confirm button to be initialized
self.page.wait_for_timeout(1000)
# Delete bookmark, verify return url
with self.page.expect_navigation(url=self.live_server_url + url):
details_modal.get_by_text("Delete").click()
self.locate_confirm_dialog().get_by_text("Confirm").click()
# Delete bookmark, verify return url
with self.page.expect_navigation(url=self.live_server_url + url):
details_modal.get_by_text("Delete").click()
self.locate_confirm_dialog().get_by_text("Confirm").click()
# verify bookmark is deleted
expect(self.locate_bookmark(bookmark.title)).not_to_be_visible()
# verify bookmark is deleted
expect(self.locate_bookmark(bookmark.title)).not_to_be_visible()
self.assertEqual(Bookmark.objects.count(), 0)
@@ -152,28 +145,27 @@ class BookmarkDetailsModalE2ETestCase(LinkdingE2ETestCase):
def test_create_snapshot_remove_snapshot(self):
bookmark = self.setup_bookmark()
with sync_playwright() as p:
url = reverse("linkding:bookmarks.index") + f"?q={bookmark.title}"
self.open(url, p)
url = reverse("linkding:bookmarks.index") + f"?q={bookmark.title}"
self.open(url)
details_modal = self.open_details_modal(bookmark)
asset_list = details_modal.locator(".assets")
details_modal = self.open_details_modal(bookmark)
asset_list = details_modal.locator(".assets")
# No snapshots initially
snapshot = asset_list.get_by_text("HTML snapshot from", exact=False)
expect(snapshot).not_to_be_visible()
# No snapshots initially
snapshot = asset_list.get_by_text("HTML snapshot from", exact=False)
expect(snapshot).not_to_be_visible()
# Create snapshot
details_modal.get_by_text("Create HTML snapshot", exact=False).click()
self.assertReloads(0)
# Create snapshot
details_modal.get_by_text("Create HTML snapshot", exact=False).click()
self.assertReloads(0)
# Has new snapshots
expect(snapshot).to_be_visible()
# Has new snapshots
expect(snapshot).to_be_visible()
# Remove snapshot
asset_list.get_by_text("Remove", exact=False).click()
self.locate_confirm_dialog().get_by_text("Confirm", exact=False).click()
# Remove snapshot
asset_list.get_by_text("Remove", exact=False).click()
self.locate_confirm_dialog().get_by_text("Confirm", exact=False).click()
# Snapshot is removed
expect(snapshot).not_to_be_visible()
self.assertReloads(0)
# Snapshot is removed
expect(snapshot).not_to_be_visible()
self.assertReloads(0)

View File

@@ -1,7 +1,7 @@
from unittest import skip
from django.urls import reverse
from playwright.sync_api import sync_playwright, expect
from playwright.sync_api import expect
from bookmarks.tests_e2e.helpers import LinkdingE2ETestCase
@@ -11,15 +11,14 @@ class BookmarkItemE2ETestCase(LinkdingE2ETestCase):
def test_toggle_notes_should_show_hide_notes(self):
bookmark = self.setup_bookmark(notes="Test notes")
with sync_playwright() as p:
page = self.open(reverse("linkding:bookmarks.index"), p)
page = self.open(reverse("linkding:bookmarks.index"))
notes = self.locate_bookmark(bookmark.title).locator(".notes")
expect(notes).to_be_hidden()
notes = self.locate_bookmark(bookmark.title).locator(".notes")
expect(notes).to_be_hidden()
toggle_notes = page.locator("li button.toggle-notes")
toggle_notes.click()
expect(notes).to_be_visible()
toggle_notes = page.locator("li button.toggle-notes")
toggle_notes.click()
expect(notes).to_be_visible()
toggle_notes.click()
expect(notes).to_be_hidden()
toggle_notes.click()
expect(notes).to_be_hidden()

View File

@@ -1,5 +1,5 @@
from django.urls import reverse
from playwright.sync_api import sync_playwright, expect
from playwright.sync_api import expect
from bookmarks.tests_e2e.helpers import LinkdingE2ETestCase
from bookmarks.models import Bookmark
@@ -36,19 +36,18 @@ class BookmarkPageBulkEditE2ETestCase(LinkdingE2ETestCase):
def test_active_bookmarks_bulk_select_across(self):
self.setup_test_data()
with sync_playwright() as p:
self.open(reverse("linkding:bookmarks.index"), p)
self.open(reverse("linkding:bookmarks.index"))
bookmark_list = self.locate_bookmark_list().element_handle()
self.locate_bulk_edit_toggle().click()
self.locate_bulk_edit_select_all().click()
self.locate_bulk_edit_select_across().click()
bookmark_list = self.locate_bookmark_list().element_handle()
self.locate_bulk_edit_toggle().click()
self.locate_bulk_edit_select_all().click()
self.locate_bulk_edit_select_across().click()
self.select_bulk_action("Delete")
self.locate_bulk_edit_bar().get_by_text("Execute").click()
self.locate_confirm_dialog().get_by_text("Confirm").click()
# Wait until bookmark list is updated (old reference becomes invisible)
bookmark_list.wait_for_element_state("hidden", timeout=1000)
self.select_bulk_action("Delete")
self.locate_bulk_edit_bar().get_by_text("Execute").click()
self.locate_confirm_dialog().get_by_text("Confirm").click()
# Wait until bookmark list is updated (old reference becomes invisible)
bookmark_list.wait_for_element_state("hidden", timeout=1000)
self.assertEqual(
0,
@@ -74,19 +73,18 @@ class BookmarkPageBulkEditE2ETestCase(LinkdingE2ETestCase):
def test_archived_bookmarks_bulk_select_across(self):
self.setup_test_data()
with sync_playwright() as p:
self.open(reverse("linkding:bookmarks.archived"), p)
self.open(reverse("linkding:bookmarks.archived"))
bookmark_list = self.locate_bookmark_list().element_handle()
self.locate_bulk_edit_toggle().click()
self.locate_bulk_edit_select_all().click()
self.locate_bulk_edit_select_across().click()
bookmark_list = self.locate_bookmark_list().element_handle()
self.locate_bulk_edit_toggle().click()
self.locate_bulk_edit_select_all().click()
self.locate_bulk_edit_select_across().click()
self.select_bulk_action("Delete")
self.locate_bulk_edit_bar().get_by_text("Execute").click()
self.locate_confirm_dialog().get_by_text("Confirm").click()
# Wait until bookmark list is updated (old reference becomes invisible)
bookmark_list.wait_for_element_state("hidden", timeout=1000)
self.select_bulk_action("Delete")
self.locate_bulk_edit_bar().get_by_text("Execute").click()
self.locate_confirm_dialog().get_by_text("Confirm").click()
# Wait until bookmark list is updated (old reference becomes invisible)
bookmark_list.wait_for_element_state("hidden", timeout=1000)
self.assertEqual(
50,
@@ -112,19 +110,18 @@ class BookmarkPageBulkEditE2ETestCase(LinkdingE2ETestCase):
def test_active_bookmarks_bulk_select_across_respects_query(self):
self.setup_test_data()
with sync_playwright() as p:
self.open(reverse("linkding:bookmarks.index") + "?q=foo", p)
self.open(reverse("linkding:bookmarks.index") + "?q=foo")
bookmark_list = self.locate_bookmark_list().element_handle()
self.locate_bulk_edit_toggle().click()
self.locate_bulk_edit_select_all().click()
self.locate_bulk_edit_select_across().click()
bookmark_list = self.locate_bookmark_list().element_handle()
self.locate_bulk_edit_toggle().click()
self.locate_bulk_edit_select_all().click()
self.locate_bulk_edit_select_across().click()
self.select_bulk_action("Delete")
self.locate_bulk_edit_bar().get_by_text("Execute").click()
self.locate_confirm_dialog().get_by_text("Confirm").click()
# Wait until bookmark list is updated (old reference becomes invisible)
bookmark_list.wait_for_element_state("hidden", timeout=1000)
self.select_bulk_action("Delete")
self.locate_bulk_edit_bar().get_by_text("Execute").click()
self.locate_confirm_dialog().get_by_text("Confirm").click()
# Wait until bookmark list is updated (old reference becomes invisible)
bookmark_list.wait_for_element_state("hidden", timeout=1000)
self.assertEqual(
50,
@@ -150,19 +147,18 @@ class BookmarkPageBulkEditE2ETestCase(LinkdingE2ETestCase):
def test_archived_bookmarks_bulk_select_across_respects_query(self):
self.setup_test_data()
with sync_playwright() as p:
self.open(reverse("linkding:bookmarks.archived") + "?q=foo", p)
self.open(reverse("linkding:bookmarks.archived") + "?q=foo")
bookmark_list = self.locate_bookmark_list().element_handle()
self.locate_bulk_edit_toggle().click()
self.locate_bulk_edit_select_all().click()
self.locate_bulk_edit_select_across().click()
bookmark_list = self.locate_bookmark_list().element_handle()
self.locate_bulk_edit_toggle().click()
self.locate_bulk_edit_select_all().click()
self.locate_bulk_edit_select_across().click()
self.select_bulk_action("Delete")
self.locate_bulk_edit_bar().get_by_text("Execute").click()
self.locate_confirm_dialog().get_by_text("Confirm").click()
# Wait until bookmark list is updated (old reference becomes invisible)
bookmark_list.wait_for_element_state("hidden", timeout=1000)
self.select_bulk_action("Delete")
self.locate_bulk_edit_bar().get_by_text("Execute").click()
self.locate_confirm_dialog().get_by_text("Confirm").click()
# Wait until bookmark list is updated (old reference becomes invisible)
bookmark_list.wait_for_element_state("hidden", timeout=1000)
self.assertEqual(
50,
@@ -188,148 +184,138 @@ class BookmarkPageBulkEditE2ETestCase(LinkdingE2ETestCase):
def test_select_all_toggles_all_checkboxes(self):
self.setup_numbered_bookmarks(5)
with sync_playwright() as p:
url = reverse("linkding:bookmarks.index")
page = self.open(url, p)
url = reverse("linkding:bookmarks.index")
page = self.open(url)
self.locate_bulk_edit_toggle().click()
self.locate_bulk_edit_toggle().click()
checkboxes = page.locator("label.bulk-edit-checkbox input")
self.assertEqual(6, checkboxes.count())
for i in range(checkboxes.count()):
expect(checkboxes.nth(i)).not_to_be_checked()
checkboxes = page.locator("label.bulk-edit-checkbox input")
self.assertEqual(6, checkboxes.count())
for i in range(checkboxes.count()):
expect(checkboxes.nth(i)).not_to_be_checked()
self.locate_bulk_edit_select_all().click()
self.locate_bulk_edit_select_all().click()
for i in range(checkboxes.count()):
expect(checkboxes.nth(i)).to_be_checked()
for i in range(checkboxes.count()):
expect(checkboxes.nth(i)).to_be_checked()
self.locate_bulk_edit_select_all().click()
self.locate_bulk_edit_select_all().click()
for i in range(checkboxes.count()):
expect(checkboxes.nth(i)).not_to_be_checked()
for i in range(checkboxes.count()):
expect(checkboxes.nth(i)).not_to_be_checked()
def test_select_all_shows_select_across(self):
self.setup_numbered_bookmarks(5)
with sync_playwright() as p:
url = reverse("linkding:bookmarks.index")
self.open(url, p)
url = reverse("linkding:bookmarks.index")
self.open(url)
self.locate_bulk_edit_toggle().click()
self.locate_bulk_edit_toggle().click()
expect(self.locate_bulk_edit_select_across()).not_to_be_visible()
expect(self.locate_bulk_edit_select_across()).not_to_be_visible()
self.locate_bulk_edit_select_all().click()
expect(self.locate_bulk_edit_select_across()).to_be_visible()
self.locate_bulk_edit_select_all().click()
expect(self.locate_bulk_edit_select_across()).to_be_visible()
self.locate_bulk_edit_select_all().click()
expect(self.locate_bulk_edit_select_across()).not_to_be_visible()
self.locate_bulk_edit_select_all().click()
expect(self.locate_bulk_edit_select_across()).not_to_be_visible()
def test_select_across_is_unchecked_when_toggling_all(self):
self.setup_numbered_bookmarks(5)
with sync_playwright() as p:
url = reverse("linkding:bookmarks.index")
self.open(url, p)
url = reverse("linkding:bookmarks.index")
self.open(url)
self.locate_bulk_edit_toggle().click()
self.locate_bulk_edit_toggle().click()
# Show select across, check it
self.locate_bulk_edit_select_all().click()
self.locate_bulk_edit_select_across().click()
expect(self.locate_bulk_edit_select_across()).to_be_checked()
# Show select across, check it
self.locate_bulk_edit_select_all().click()
self.locate_bulk_edit_select_across().click()
expect(self.locate_bulk_edit_select_across()).to_be_checked()
# Hide select across by toggling select all
self.locate_bulk_edit_select_all().click()
expect(self.locate_bulk_edit_select_across()).not_to_be_visible()
# Hide select across by toggling select all
self.locate_bulk_edit_select_all().click()
expect(self.locate_bulk_edit_select_across()).not_to_be_visible()
# Show select across again, verify it is unchecked
self.locate_bulk_edit_select_all().click()
expect(self.locate_bulk_edit_select_across()).not_to_be_checked()
# Show select across again, verify it is unchecked
self.locate_bulk_edit_select_all().click()
expect(self.locate_bulk_edit_select_across()).not_to_be_checked()
def test_select_across_is_unchecked_when_toggling_bookmark(self):
self.setup_numbered_bookmarks(5)
with sync_playwright() as p:
url = reverse("linkding:bookmarks.index")
self.open(url, p)
url = reverse("linkding:bookmarks.index")
self.open(url)
self.locate_bulk_edit_toggle().click()
self.locate_bulk_edit_toggle().click()
# Show select across, check it
self.locate_bulk_edit_select_all().click()
self.locate_bulk_edit_select_across().click()
expect(self.locate_bulk_edit_select_across()).to_be_checked()
# Show select across, check it
self.locate_bulk_edit_select_all().click()
self.locate_bulk_edit_select_across().click()
expect(self.locate_bulk_edit_select_across()).to_be_checked()
# Hide select across by toggling a single bookmark
self.locate_bookmark("Bookmark 1").locator(
"label.bulk-edit-checkbox"
).click()
expect(self.locate_bulk_edit_select_across()).not_to_be_visible()
# Hide select across by toggling a single bookmark
self.locate_bookmark("Bookmark 1").locator("label.bulk-edit-checkbox").click()
expect(self.locate_bulk_edit_select_across()).not_to_be_visible()
# Show select across again, verify it is unchecked
self.locate_bookmark("Bookmark 1").locator(
"label.bulk-edit-checkbox"
).click()
expect(self.locate_bulk_edit_select_across()).not_to_be_checked()
# Show select across again, verify it is unchecked
self.locate_bookmark("Bookmark 1").locator("label.bulk-edit-checkbox").click()
expect(self.locate_bulk_edit_select_across()).not_to_be_checked()
def test_execute_resets_all_checkboxes(self):
self.setup_numbered_bookmarks(100)
with sync_playwright() as p:
url = reverse("linkding:bookmarks.index")
page = self.open(url, p)
url = reverse("linkding:bookmarks.index")
page = self.open(url)
bookmark_list = self.locate_bookmark_list().element_handle()
bookmark_list = self.locate_bookmark_list().element_handle()
# Select all bookmarks, enable select across
self.locate_bulk_edit_toggle().click()
self.locate_bulk_edit_select_all().click()
self.locate_bulk_edit_select_across().click()
# Select all bookmarks, enable select across
self.locate_bulk_edit_toggle().click()
self.locate_bulk_edit_select_all().click()
self.locate_bulk_edit_select_across().click()
# Execute bulk action
self.select_bulk_action("Mark as unread")
self.locate_bulk_edit_bar().get_by_text("Execute").click()
self.locate_confirm_dialog().get_by_text("Confirm").click()
# Execute bulk action
self.select_bulk_action("Mark as unread")
self.locate_bulk_edit_bar().get_by_text("Execute").click()
self.locate_confirm_dialog().get_by_text("Confirm").click()
# Wait until bookmark list is updated (old reference becomes invisible)
bookmark_list.wait_for_element_state("hidden", timeout=1000)
# Wait until bookmark list is updated (old reference becomes invisible)
bookmark_list.wait_for_element_state("hidden", timeout=1000)
# Verify bulk edit checkboxes are reset
checkboxes = page.locator("label.bulk-edit-checkbox input")
self.assertEqual(31, checkboxes.count())
for i in range(checkboxes.count()):
expect(checkboxes.nth(i)).not_to_be_checked()
# Verify bulk edit checkboxes are reset
checkboxes = page.locator("label.bulk-edit-checkbox input")
self.assertEqual(31, checkboxes.count())
for i in range(checkboxes.count()):
expect(checkboxes.nth(i)).not_to_be_checked()
# Toggle select all and verify select across is reset
self.locate_bulk_edit_select_all().click()
expect(self.locate_bulk_edit_select_across()).not_to_be_checked()
# Toggle select all and verify select across is reset
self.locate_bulk_edit_select_all().click()
expect(self.locate_bulk_edit_select_across()).not_to_be_checked()
def test_update_select_across_bookmark_count(self):
self.setup_numbered_bookmarks(100)
with sync_playwright() as p:
url = reverse("linkding:bookmarks.index")
self.open(url, p)
url = reverse("linkding:bookmarks.index")
self.open(url)
bookmark_list = self.locate_bookmark_list().element_handle()
self.locate_bulk_edit_toggle().click()
self.locate_bulk_edit_select_all().click()
bookmark_list = self.locate_bookmark_list().element_handle()
self.locate_bulk_edit_toggle().click()
self.locate_bulk_edit_select_all().click()
expect(
self.locate_bulk_edit_bar().get_by_text("All pages (100 bookmarks)")
).to_be_visible()
expect(
self.locate_bulk_edit_bar().get_by_text("All pages (100 bookmarks)")
).to_be_visible()
self.select_bulk_action("Delete")
self.locate_bulk_edit_bar().get_by_text("Execute").click()
self.locate_confirm_dialog().get_by_text("Confirm").click()
# Wait until bookmark list is updated (old reference becomes invisible)
bookmark_list.wait_for_element_state("hidden", timeout=1000)
self.select_bulk_action("Delete")
self.locate_bulk_edit_bar().get_by_text("Execute").click()
self.locate_confirm_dialog().get_by_text("Confirm").click()
# Wait until bookmark list is updated (old reference becomes invisible)
bookmark_list.wait_for_element_state("hidden", timeout=1000)
expect(self.locate_bulk_edit_select_all()).not_to_be_checked()
self.locate_bulk_edit_select_all().click()
expect(self.locate_bulk_edit_select_all()).not_to_be_checked()
self.locate_bulk_edit_select_all().click()
expect(
self.locate_bulk_edit_bar().get_by_text("All pages (70 bookmarks)")
).to_be_visible()
expect(
self.locate_bulk_edit_bar().get_by_text("All pages (70 bookmarks)")
).to_be_visible()

View File

@@ -1,7 +1,7 @@
from typing import List
from django.urls import reverse
from playwright.sync_api import sync_playwright, expect
from playwright.sync_api import expect
from bookmarks.tests_e2e.helpers import LinkdingE2ETestCase
@@ -43,91 +43,85 @@ class BookmarkPagePartialUpdatesE2ETestCase(LinkdingE2ETestCase):
self.setup_numbered_bookmarks(5, prefix="foo")
self.setup_numbered_bookmarks(5, prefix="bar")
with sync_playwright() as p:
url = reverse("linkding:bookmarks.index") + "?q=foo"
self.open(url, p)
url = reverse("linkding:bookmarks.index") + "?q=foo"
self.open(url)
self.assertVisibleBookmarks(["foo 1", "foo 2", "foo 3", "foo 4", "foo 5"])
self.assertVisibleBookmarks(["foo 1", "foo 2", "foo 3", "foo 4", "foo 5"])
self.locate_bookmark("foo 2").get_by_text("Archive").click()
self.assertVisibleBookmarks(["foo 1", "foo 3", "foo 4", "foo 5"])
self.locate_bookmark("foo 2").get_by_text("Archive").click()
self.assertVisibleBookmarks(["foo 1", "foo 3", "foo 4", "foo 5"])
def test_partial_update_respects_sort(self):
self.setup_numbered_bookmarks(5, prefix="foo")
with sync_playwright() as p:
url = reverse("linkding:bookmarks.index") + "?sort=title_asc"
page = self.open(url, p)
url = reverse("linkding:bookmarks.index") + "?sort=title_asc"
page = self.open(url)
first_item = page.locator("ul.bookmark-list > li").first
expect(first_item).to_contain_text("foo 1")
first_item = page.locator("ul.bookmark-list > li").first
expect(first_item).to_contain_text("foo 1")
first_item.get_by_text("Archive").click()
first_item.get_by_text("Archive").click()
first_item = page.locator("ul.bookmark-list > li").first
expect(first_item).to_contain_text("foo 2")
first_item = page.locator("ul.bookmark-list > li").first
expect(first_item).to_contain_text("foo 2")
def test_partial_update_respects_page(self):
# add a suffix, otherwise 'foo 1' also matches 'foo 10'
self.setup_numbered_bookmarks(50, prefix="foo", suffix="-")
with sync_playwright() as p:
url = reverse("linkding:bookmarks.index") + "?q=foo&page=2"
self.open(url, p)
url = reverse("linkding:bookmarks.index") + "?q=foo&page=2"
self.open(url)
# with descending sort, page two has 'foo 1' to 'foo 20'
expected_titles = [f"foo {i}-" for i in range(1, 21)]
self.assertVisibleBookmarks(expected_titles)
# with descending sort, page two has 'foo 1' to 'foo 20'
expected_titles = [f"foo {i}-" for i in range(1, 21)]
self.assertVisibleBookmarks(expected_titles)
self.locate_bookmark("foo 20-").get_by_text("Archive").click()
self.locate_bookmark("foo 20-").get_by_text("Archive").click()
expected_titles = [f"foo {i}-" for i in range(1, 20)]
self.assertVisibleBookmarks(expected_titles)
expected_titles = [f"foo {i}-" for i in range(1, 20)]
self.assertVisibleBookmarks(expected_titles)
def test_multiple_partial_updates(self):
self.setup_numbered_bookmarks(5)
with sync_playwright() as p:
url = reverse("linkding:bookmarks.index")
self.open(url, p)
url = reverse("linkding:bookmarks.index")
self.open(url)
self.locate_bookmark("Bookmark 1").get_by_text("Archive").click()
self.assertVisibleBookmarks(
["Bookmark 2", "Bookmark 3", "Bookmark 4", "Bookmark 5"]
)
self.locate_bookmark("Bookmark 1").get_by_text("Archive").click()
self.assertVisibleBookmarks(
["Bookmark 2", "Bookmark 3", "Bookmark 4", "Bookmark 5"]
)
self.locate_bookmark("Bookmark 2").get_by_text("Archive").click()
self.assertVisibleBookmarks(["Bookmark 3", "Bookmark 4", "Bookmark 5"])
self.locate_bookmark("Bookmark 2").get_by_text("Archive").click()
self.assertVisibleBookmarks(["Bookmark 3", "Bookmark 4", "Bookmark 5"])
self.locate_bookmark("Bookmark 3").get_by_text("Archive").click()
self.assertVisibleBookmarks(["Bookmark 4", "Bookmark 5"])
self.locate_bookmark("Bookmark 3").get_by_text("Archive").click()
self.assertVisibleBookmarks(["Bookmark 4", "Bookmark 5"])
self.assertReloads(0)
self.assertReloads(0)
def test_active_bookmarks_partial_update_on_archive(self):
self.setup_fixture()
with sync_playwright() as p:
self.open(reverse("linkding:bookmarks.index"), p)
self.open(reverse("linkding:bookmarks.index"))
self.locate_bookmark("Bookmark 2").get_by_text("Archive").click()
self.locate_bookmark("Bookmark 2").get_by_text("Archive").click()
self.assertVisibleBookmarks(["Bookmark 1", "Bookmark 3"])
self.assertVisibleTags(["Tag 1", "Tag 3"])
self.assertReloads(0)
self.assertVisibleBookmarks(["Bookmark 1", "Bookmark 3"])
self.assertVisibleTags(["Tag 1", "Tag 3"])
self.assertReloads(0)
def test_active_bookmarks_partial_update_on_delete(self):
self.setup_fixture()
with sync_playwright() as p:
self.open(reverse("linkding:bookmarks.index"), p)
self.open(reverse("linkding:bookmarks.index"))
self.locate_bookmark("Bookmark 2").get_by_text("Remove").click()
self.locate_confirm_dialog().get_by_text("Confirm").click()
self.locate_bookmark("Bookmark 2").get_by_text("Remove").click()
self.locate_confirm_dialog().get_by_text("Confirm").click()
self.assertVisibleBookmarks(["Bookmark 1", "Bookmark 3"])
self.assertVisibleTags(["Tag 1", "Tag 3"])
self.assertReloads(0)
self.assertVisibleBookmarks(["Bookmark 1", "Bookmark 3"])
self.assertVisibleTags(["Tag 1", "Tag 3"])
self.assertReloads(0)
def test_active_bookmarks_partial_update_on_mark_as_read(self):
self.setup_fixture()
@@ -135,15 +129,14 @@ class BookmarkPagePartialUpdatesE2ETestCase(LinkdingE2ETestCase):
bookmark2.unread = True
bookmark2.save()
with sync_playwright() as p:
self.open(reverse("linkding:bookmarks.index"), p)
self.open(reverse("linkding:bookmarks.index"))
expect(self.locate_bookmark("Bookmark 2")).to_have_class("unread")
self.locate_bookmark("Bookmark 2").get_by_text("Unread").click()
self.locate_confirm_dialog().get_by_text("Confirm").click()
expect(self.locate_bookmark("Bookmark 2")).to_have_class("unread")
self.locate_bookmark("Bookmark 2").get_by_text("Unread").click()
self.locate_confirm_dialog().get_by_text("Confirm").click()
expect(self.locate_bookmark("Bookmark 2")).not_to_have_class("unread")
self.assertReloads(0)
expect(self.locate_bookmark("Bookmark 2")).not_to_have_class("unread")
self.assertReloads(0)
def test_active_bookmarks_partial_update_on_unshare(self):
self.setup_fixture()
@@ -151,112 +144,101 @@ class BookmarkPagePartialUpdatesE2ETestCase(LinkdingE2ETestCase):
bookmark2.shared = True
bookmark2.save()
with sync_playwright() as p:
self.open(reverse("linkding:bookmarks.index"), p)
self.open(reverse("linkding:bookmarks.index"))
expect(self.locate_bookmark("Bookmark 2")).to_have_class("shared")
self.locate_bookmark("Bookmark 2").get_by_text("Shared").click()
self.locate_confirm_dialog().get_by_text("Confirm").click()
expect(self.locate_bookmark("Bookmark 2")).to_have_class("shared")
self.locate_bookmark("Bookmark 2").get_by_text("Shared").click()
self.locate_confirm_dialog().get_by_text("Confirm").click()
expect(self.locate_bookmark("Bookmark 2")).not_to_have_class("shared")
self.assertReloads(0)
expect(self.locate_bookmark("Bookmark 2")).not_to_have_class("shared")
self.assertReloads(0)
def test_active_bookmarks_partial_update_on_bulk_archive(self):
self.setup_fixture()
with sync_playwright() as p:
self.open(reverse("linkding:bookmarks.index"), p)
self.open(reverse("linkding:bookmarks.index"))
self.locate_bulk_edit_toggle().click()
self.locate_bookmark("Bookmark 2").locator(
"label.bulk-edit-checkbox"
).click()
self.select_bulk_action("Archive")
self.locate_bulk_edit_bar().get_by_text("Execute").click()
self.locate_confirm_dialog().get_by_text("Confirm").click()
self.locate_bulk_edit_toggle().click()
self.locate_bookmark("Bookmark 2").locator("label.bulk-edit-checkbox").click()
self.select_bulk_action("Archive")
self.locate_bulk_edit_bar().get_by_text("Execute").click()
self.locate_confirm_dialog().get_by_text("Confirm").click()
self.assertVisibleBookmarks(["Bookmark 1", "Bookmark 3"])
self.assertVisibleTags(["Tag 1", "Tag 3"])
self.assertReloads(0)
self.assertVisibleBookmarks(["Bookmark 1", "Bookmark 3"])
self.assertVisibleTags(["Tag 1", "Tag 3"])
self.assertReloads(0)
def test_active_bookmarks_partial_update_on_bulk_delete(self):
self.setup_fixture()
with sync_playwright() as p:
self.open(reverse("linkding:bookmarks.index"), p)
self.open(reverse("linkding:bookmarks.index"))
self.locate_bulk_edit_toggle().click()
self.locate_bookmark("Bookmark 2").locator(
"label.bulk-edit-checkbox"
).click()
self.select_bulk_action("Delete")
self.locate_bulk_edit_bar().get_by_text("Execute").click()
self.locate_confirm_dialog().get_by_text("Confirm").click()
self.locate_bulk_edit_toggle().click()
self.locate_bookmark("Bookmark 2").locator("label.bulk-edit-checkbox").click()
self.select_bulk_action("Delete")
self.locate_bulk_edit_bar().get_by_text("Execute").click()
self.locate_confirm_dialog().get_by_text("Confirm").click()
self.assertVisibleBookmarks(["Bookmark 1", "Bookmark 3"])
self.assertVisibleTags(["Tag 1", "Tag 3"])
self.assertReloads(0)
self.assertVisibleBookmarks(["Bookmark 1", "Bookmark 3"])
self.assertVisibleTags(["Tag 1", "Tag 3"])
self.assertReloads(0)
def test_archived_bookmarks_partial_update_on_unarchive(self):
self.setup_fixture()
with sync_playwright() as p:
self.open(reverse("linkding:bookmarks.archived"), p)
self.open(reverse("linkding:bookmarks.archived"))
self.locate_bookmark("Archived Bookmark 2").get_by_text("Unarchive").click()
self.locate_bookmark("Archived Bookmark 2").get_by_text("Unarchive").click()
self.assertVisibleBookmarks(["Archived Bookmark 1", "Archived Bookmark 3"])
self.assertVisibleTags(["Archived Tag 1", "Archived Tag 3"])
self.assertReloads(0)
self.assertVisibleBookmarks(["Archived Bookmark 1", "Archived Bookmark 3"])
self.assertVisibleTags(["Archived Tag 1", "Archived Tag 3"])
self.assertReloads(0)
def test_archived_bookmarks_partial_update_on_delete(self):
self.setup_fixture()
with sync_playwright() as p:
self.open(reverse("linkding:bookmarks.archived"), p)
self.open(reverse("linkding:bookmarks.archived"))
self.locate_bookmark("Archived Bookmark 2").get_by_text("Remove").click()
self.locate_confirm_dialog().get_by_text("Confirm").click()
self.locate_bookmark("Archived Bookmark 2").get_by_text("Remove").click()
self.locate_confirm_dialog().get_by_text("Confirm").click()
self.assertVisibleBookmarks(["Archived Bookmark 1", "Archived Bookmark 3"])
self.assertVisibleTags(["Archived Tag 1", "Archived Tag 3"])
self.assertReloads(0)
self.assertVisibleBookmarks(["Archived Bookmark 1", "Archived Bookmark 3"])
self.assertVisibleTags(["Archived Tag 1", "Archived Tag 3"])
self.assertReloads(0)
def test_archived_bookmarks_partial_update_on_bulk_unarchive(self):
self.setup_fixture()
with sync_playwright() as p:
self.open(reverse("linkding:bookmarks.archived"), p)
self.open(reverse("linkding:bookmarks.archived"))
self.locate_bulk_edit_toggle().click()
self.locate_bookmark("Archived Bookmark 2").locator(
"label.bulk-edit-checkbox"
).click()
self.select_bulk_action("Unarchive")
self.locate_bulk_edit_bar().get_by_text("Execute").click()
self.locate_confirm_dialog().get_by_text("Confirm").click()
self.locate_bulk_edit_toggle().click()
self.locate_bookmark("Archived Bookmark 2").locator(
"label.bulk-edit-checkbox"
).click()
self.select_bulk_action("Unarchive")
self.locate_bulk_edit_bar().get_by_text("Execute").click()
self.locate_confirm_dialog().get_by_text("Confirm").click()
self.assertVisibleBookmarks(["Archived Bookmark 1", "Archived Bookmark 3"])
self.assertVisibleTags(["Archived Tag 1", "Archived Tag 3"])
self.assertReloads(0)
self.assertVisibleBookmarks(["Archived Bookmark 1", "Archived Bookmark 3"])
self.assertVisibleTags(["Archived Tag 1", "Archived Tag 3"])
self.assertReloads(0)
def test_archived_bookmarks_partial_update_on_bulk_delete(self):
self.setup_fixture()
with sync_playwright() as p:
self.open(reverse("linkding:bookmarks.archived"), p)
self.open(reverse("linkding:bookmarks.archived"))
self.locate_bulk_edit_toggle().click()
self.locate_bookmark("Archived Bookmark 2").locator(
"label.bulk-edit-checkbox"
).click()
self.select_bulk_action("Delete")
self.locate_bulk_edit_bar().get_by_text("Execute").click()
self.locate_confirm_dialog().get_by_text("Confirm").click()
self.locate_bulk_edit_toggle().click()
self.locate_bookmark("Archived Bookmark 2").locator(
"label.bulk-edit-checkbox"
).click()
self.select_bulk_action("Delete")
self.locate_bulk_edit_bar().get_by_text("Execute").click()
self.locate_confirm_dialog().get_by_text("Confirm").click()
self.assertVisibleBookmarks(["Archived Bookmark 1", "Archived Bookmark 3"])
self.assertVisibleTags(["Archived Tag 1", "Archived Tag 3"])
self.assertReloads(0)
self.assertVisibleBookmarks(["Archived Bookmark 1", "Archived Bookmark 3"])
self.assertVisibleTags(["Archived Tag 1", "Archived Tag 3"])
self.assertReloads(0)
def test_shared_bookmarks_partial_update_on_unarchive(self):
self.setup_fixture()
@@ -264,24 +246,23 @@ class BookmarkPagePartialUpdatesE2ETestCase(LinkdingE2ETestCase):
3, shared=True, prefix="My Bookmark", with_tags=True
)
with sync_playwright() as p:
self.open(reverse("linkding:bookmarks.shared"), p)
self.open(reverse("linkding:bookmarks.shared"))
self.locate_bookmark("My Bookmark 2").get_by_text("Archive").click()
self.locate_bookmark("My Bookmark 2").get_by_text("Archive").click()
# Shared bookmarks page also shows archived bookmarks, though it probably shouldn't
self.assertVisibleBookmarks(
[
"My Bookmark 1",
"My Bookmark 2",
"My Bookmark 3",
"Joe's Bookmark 1",
"Joe's Bookmark 2",
"Joe's Bookmark 3",
]
)
self.assertVisibleTags(["Shared Tag 1", "Shared Tag 2", "Shared Tag 3"])
self.assertReloads(0)
# Shared bookmarks page also shows archived bookmarks, though it probably shouldn't
self.assertVisibleBookmarks(
[
"My Bookmark 1",
"My Bookmark 2",
"My Bookmark 3",
"Joe's Bookmark 1",
"Joe's Bookmark 2",
"Joe's Bookmark 3",
]
)
self.assertVisibleTags(["Shared Tag 1", "Shared Tag 2", "Shared Tag 3"])
self.assertReloads(0)
def test_shared_bookmarks_partial_update_on_delete(self):
self.setup_fixture()
@@ -289,20 +270,19 @@ class BookmarkPagePartialUpdatesE2ETestCase(LinkdingE2ETestCase):
3, shared=True, prefix="My Bookmark", with_tags=True
)
with sync_playwright() as p:
self.open(reverse("linkding:bookmarks.shared"), p)
self.open(reverse("linkding:bookmarks.shared"))
self.locate_bookmark("My Bookmark 2").get_by_text("Remove").click()
self.locate_confirm_dialog().get_by_text("Confirm").click()
self.locate_bookmark("My Bookmark 2").get_by_text("Remove").click()
self.locate_confirm_dialog().get_by_text("Confirm").click()
self.assertVisibleBookmarks(
[
"My Bookmark 1",
"My Bookmark 3",
"Joe's Bookmark 1",
"Joe's Bookmark 2",
"Joe's Bookmark 3",
]
)
self.assertVisibleTags(["Shared Tag 1", "Shared Tag 3"])
self.assertReloads(0)
self.assertVisibleBookmarks(
[
"My Bookmark 1",
"My Bookmark 3",
"Joe's Bookmark 1",
"Joe's Bookmark 2",
"Joe's Bookmark 3",
]
)
self.assertVisibleTags(["Shared Tag 1", "Shared Tag 3"])
self.assertReloads(0)

View File

@@ -1,5 +1,5 @@
from django.urls import reverse
from playwright.sync_api import sync_playwright, expect
from playwright.sync_api import expect
from bookmarks.tests_e2e.helpers import LinkdingE2ETestCase
@@ -9,39 +9,38 @@ class BookmarkItemE2ETestCase(LinkdingE2ETestCase):
group1 = self.setup_numbered_bookmarks(3, prefix="foo")
group2 = self.setup_numbered_bookmarks(3, prefix="bar")
with sync_playwright() as p:
# shows all bookmarks initially
page = self.open(reverse("linkding:bundles.new"), p)
# shows all bookmarks initially
page = self.open(reverse("linkding:bundles.new"))
expect(
page.get_by_text(f"Found 6 bookmarks matching this bundle")
).to_be_visible()
self.assertVisibleBookmarks(group1 + group2)
expect(
page.get_by_text(f"Found 6 bookmarks matching this bundle")
).to_be_visible()
self.assertVisibleBookmarks(group1 + group2)
# filter by group1
search = page.get_by_label("Search")
search.fill("foo")
# filter by group1
search = page.get_by_label("Search")
search.fill("foo")
expect(
page.get_by_text(f"Found 3 bookmarks matching this bundle")
).to_be_visible()
self.assertVisibleBookmarks(group1)
expect(
page.get_by_text(f"Found 3 bookmarks matching this bundle")
).to_be_visible()
self.assertVisibleBookmarks(group1)
# filter by group2
search.fill("bar")
# filter by group2
search.fill("bar")
expect(
page.get_by_text(f"Found 3 bookmarks matching this bundle")
).to_be_visible()
self.assertVisibleBookmarks(group2)
expect(
page.get_by_text(f"Found 3 bookmarks matching this bundle")
).to_be_visible()
self.assertVisibleBookmarks(group2)
# filter by invalid group
search.fill("invalid")
# filter by invalid group
search.fill("invalid")
expect(
page.get_by_text(f"No bookmarks match the current bundle")
).to_be_visible()
self.assertVisibleBookmarks([])
expect(
page.get_by_text(f"No bookmarks match the current bundle")
).to_be_visible()
self.assertVisibleBookmarks([])
def assertVisibleBookmarks(self, bookmarks):
self.assertEqual(len(bookmarks), self.count_bookmarks())

View File

@@ -1,5 +1,5 @@
from django.urls import reverse
from playwright.sync_api import sync_playwright, expect
from playwright.sync_api import expect
from bookmarks.tests_e2e.helpers import LinkdingE2ETestCase
@@ -22,31 +22,25 @@ class CollapseSidePanelE2ETestCase(LinkdingE2ETestCase):
).to_be_visible()
def test_side_panel_should_be_visible_by_default(self):
with sync_playwright() as p:
self.open(reverse("linkding:bookmarks.index"), p)
self.assertSidePanelIsVisible()
self.open(reverse("linkding:bookmarks.index"))
self.assertSidePanelIsVisible()
self.page.goto(
self.live_server_url + reverse("linkding:bookmarks.archived")
)
self.assertSidePanelIsVisible()
self.page.goto(self.live_server_url + reverse("linkding:bookmarks.archived"))
self.assertSidePanelIsVisible()
self.page.goto(self.live_server_url + reverse("linkding:bookmarks.shared"))
self.assertSidePanelIsVisible()
self.page.goto(self.live_server_url + reverse("linkding:bookmarks.shared"))
self.assertSidePanelIsVisible()
def test_side_panel_should_be_hidden_when_collapsed(self):
user = self.get_or_create_test_user()
user.profile.collapse_side_panel = True
user.profile.save()
with sync_playwright() as p:
self.open(reverse("linkding:bookmarks.index"), p)
self.assertSidePanelIsHidden()
self.open(reverse("linkding:bookmarks.index"))
self.assertSidePanelIsHidden()
self.page.goto(
self.live_server_url + reverse("linkding:bookmarks.archived")
)
self.assertSidePanelIsHidden()
self.page.goto(self.live_server_url + reverse("linkding:bookmarks.archived"))
self.assertSidePanelIsHidden()
self.page.goto(self.live_server_url + reverse("linkding:bookmarks.shared"))
self.assertSidePanelIsHidden()
self.page.goto(self.live_server_url + reverse("linkding:bookmarks.shared"))
self.assertSidePanelIsHidden()

View File

@@ -1,5 +1,5 @@
from django.urls import reverse
from playwright.sync_api import sync_playwright, expect
from playwright.sync_api import expect
from bookmarks.tests_e2e.helpers import LinkdingE2ETestCase
@@ -18,111 +18,105 @@ class DropdownE2ETestCase(LinkdingE2ETestCase):
return self.locate_dropdown().locator(".menu")
def test_click_toggle_opens_and_closes_dropdown(self):
with sync_playwright() as p:
self.open(reverse("linkding:bookmarks.index"), p)
self.open(reverse("linkding:bookmarks.index"))
toggle = self.locate_dropdown_toggle()
menu = self.locate_dropdown_menu()
toggle = self.locate_dropdown_toggle()
menu = self.locate_dropdown_menu()
# Open dropdown
toggle.click()
expect(menu).to_be_visible()
# Open dropdown
toggle.click()
expect(menu).to_be_visible()
# Click toggle again to close
toggle.click()
expect(menu).not_to_be_visible()
# Click toggle again to close
toggle.click()
expect(menu).not_to_be_visible()
def test_outside_click_closes_dropdown(self):
with sync_playwright() as p:
self.open(reverse("linkding:bookmarks.index"), p)
self.open(reverse("linkding:bookmarks.index"))
toggle = self.locate_dropdown_toggle()
menu = self.locate_dropdown_menu()
toggle = self.locate_dropdown_toggle()
menu = self.locate_dropdown_menu()
# Open dropdown
toggle.click()
expect(menu).to_be_visible()
# Open dropdown
toggle.click()
expect(menu).to_be_visible()
# Click outside the dropdown (on the page body)
self.page.locator("main").click()
expect(menu).not_to_be_visible()
# Click outside the dropdown (on the page body)
self.page.locator("main").click()
expect(menu).not_to_be_visible()
def test_escape_closes_dropdown_and_restores_focus(self):
with sync_playwright() as p:
self.open(reverse("linkding:bookmarks.index"), p)
self.open(reverse("linkding:bookmarks.index"))
toggle = self.locate_dropdown_toggle()
menu = self.locate_dropdown_menu()
toggle = self.locate_dropdown_toggle()
menu = self.locate_dropdown_menu()
# Open dropdown
toggle.click()
expect(menu).to_be_visible()
# Open dropdown
toggle.click()
expect(menu).to_be_visible()
# Press Escape
self.page.keyboard.press("Escape")
# Press Escape
self.page.keyboard.press("Escape")
# Menu should be closed
expect(menu).not_to_be_visible()
# Menu should be closed
expect(menu).not_to_be_visible()
# Focus should be back on toggle
expect(toggle).to_be_focused()
# Focus should be back on toggle
expect(toggle).to_be_focused()
def test_focus_out_closes_dropdown(self):
with sync_playwright() as p:
self.open(reverse("linkding:bookmarks.index"), p)
self.open(reverse("linkding:bookmarks.index"))
toggle = self.locate_dropdown_toggle()
menu = self.locate_dropdown_menu()
toggle = self.locate_dropdown_toggle()
menu = self.locate_dropdown_menu()
# Open dropdown
toggle.click()
expect(menu).to_be_visible()
# Open dropdown
toggle.click()
expect(menu).to_be_visible()
# Shift+Tab to move focus out of the dropdown
self.page.keyboard.press("Shift+Tab")
# Shift+Tab to move focus out of the dropdown
self.page.keyboard.press("Shift+Tab")
# Menu should be closed after focus leaves
expect(menu).not_to_be_visible()
# Menu should be closed after focus leaves
expect(menu).not_to_be_visible()
def test_aria_expanded_attribute(self):
with sync_playwright() as p:
self.open(reverse("linkding:bookmarks.index"), p)
self.open(reverse("linkding:bookmarks.index"))
toggle = self.locate_dropdown_toggle()
menu = self.locate_dropdown_menu()
toggle = self.locate_dropdown_toggle()
menu = self.locate_dropdown_menu()
# Initially aria-expanded should be false
expect(toggle).to_have_attribute("aria-expanded", "false")
# Initially aria-expanded should be false
expect(toggle).to_have_attribute("aria-expanded", "false")
# Open dropdown
toggle.click()
expect(menu).to_be_visible()
# Open dropdown
toggle.click()
expect(menu).to_be_visible()
# aria-expanded should be true
expect(toggle).to_have_attribute("aria-expanded", "true")
# aria-expanded should be true
expect(toggle).to_have_attribute("aria-expanded", "true")
# Close dropdown
toggle.click()
expect(menu).not_to_be_visible()
# Close dropdown
toggle.click()
expect(menu).not_to_be_visible()
# aria-expanded should be false again
expect(toggle).to_have_attribute("aria-expanded", "false")
# aria-expanded should be false again
expect(toggle).to_have_attribute("aria-expanded", "false")
def test_can_click_menu_item(self):
with sync_playwright() as p:
self.open(reverse("linkding:bookmarks.index"), p)
self.open(reverse("linkding:bookmarks.index"))
toggle = self.locate_dropdown_toggle()
menu = self.locate_dropdown_menu()
toggle = self.locate_dropdown_toggle()
menu = self.locate_dropdown_menu()
# Open dropdown
toggle.click()
expect(menu).to_be_visible()
# Open dropdown
toggle.click()
expect(menu).to_be_visible()
# Click on "Archived" menu item
menu.get_by_text("Archived", exact=True).click()
# Click on "Archived" menu item
menu.get_by_text("Archived", exact=True).click()
# Should navigate to archived page
expect(self.page).to_have_url(
self.live_server_url + reverse("linkding:bookmarks.archived")
)
# Should navigate to archived page
expect(self.page).to_have_url(
self.live_server_url + reverse("linkding:bookmarks.archived")
)

View File

@@ -1,7 +1,7 @@
from unittest.mock import patch
from django.urls import reverse
from playwright.sync_api import sync_playwright, expect
from playwright.sync_api import expect
from bookmarks.tests_e2e.helpers import LinkdingE2ETestCase
from bookmarks.services import website_loader
@@ -24,51 +24,47 @@ class BookmarkFormE2ETestCase(LinkdingE2ETestCase):
self.website_loader_patch.start()
def tearDown(self) -> None:
super().tearDown()
self.website_loader_patch.stop()
super().tearDown()
def test_should_not_check_for_existing_bookmark(self):
bookmark = self.setup_bookmark()
with sync_playwright() as p:
page = self.open(reverse("linkding:bookmarks.edit", args=[bookmark.id]), p)
page = self.open(reverse("linkding:bookmarks.edit", args=[bookmark.id]))
page.wait_for_timeout(timeout=1000)
page.get_by_text("This URL is already bookmarked.").wait_for(state="hidden")
page.wait_for_timeout(timeout=1000)
page.get_by_text("This URL is already bookmarked.").wait_for(state="hidden")
def test_should_not_prefill_title_and_description(self):
bookmark = self.setup_bookmark(
title="Initial title", description="Initial description"
)
with sync_playwright() as p:
page = self.open(reverse("linkding:bookmarks.edit", args=[bookmark.id]), p)
page.wait_for_timeout(timeout=1000)
page = self.open(reverse("linkding:bookmarks.edit", args=[bookmark.id]))
page.wait_for_timeout(timeout=1000)
title = page.get_by_label("Title")
description = page.get_by_label("Description")
expect(title).to_have_value(bookmark.title)
expect(description).to_have_value(bookmark.description)
title = page.get_by_label("Title")
description = page.get_by_label("Description")
expect(title).to_have_value(bookmark.title)
expect(description).to_have_value(bookmark.description)
def test_enter_url_should_not_prefill_title_and_description(self):
bookmark = self.setup_bookmark()
with sync_playwright() as p:
page = self.open(reverse("linkding:bookmarks.edit", args=[bookmark.id]), p)
page = self.open(reverse("linkding:bookmarks.edit", args=[bookmark.id]))
page.get_by_label("URL").fill("https://example.com")
page.wait_for_timeout(timeout=1000)
page.get_by_label("URL").fill("https://example.com")
page.wait_for_timeout(timeout=1000)
title = page.get_by_label("Title")
description = page.get_by_label("Description")
expect(title).to_have_value(bookmark.title)
expect(description).to_have_value(bookmark.description)
title = page.get_by_label("Title")
description = page.get_by_label("Description")
expect(title).to_have_value(bookmark.title)
expect(description).to_have_value(bookmark.description)
def test_refresh_button_should_be_visible_when_editing(self):
bookmark = self.setup_bookmark()
with sync_playwright() as p:
page = self.open(reverse("linkding:bookmarks.edit", args=[bookmark.id]), p)
page = self.open(reverse("linkding:bookmarks.edit", args=[bookmark.id]))
refresh_button = page.get_by_text("Refresh from website")
expect(refresh_button).to_be_visible()
refresh_button = page.get_by_text("Refresh from website")
expect(refresh_button).to_be_visible()

View File

@@ -1,5 +1,5 @@
from django.urls import reverse
from playwright.sync_api import sync_playwright, expect
from playwright.sync_api import expect
from bookmarks.tests_e2e.helpers import LinkdingE2ETestCase
@@ -9,62 +9,60 @@ class FilterDrawerE2ETestCase(LinkdingE2ETestCase):
self.setup_bookmark(tags=[self.setup_tag(name="cooking")])
self.setup_bookmark(tags=[self.setup_tag(name="hiking")])
with sync_playwright() as p:
page = self.open(reverse("linkding:bookmarks.index"), p)
page = self.open(reverse("linkding:bookmarks.index"))
# use smaller viewport to make filter button visible
page.set_viewport_size({"width": 375, "height": 812})
# use smaller viewport to make filter button visible
page.set_viewport_size({"width": 375, "height": 812})
# open drawer
drawer_trigger = page.locator(".main").get_by_role("button", name="Filters")
drawer_trigger.click()
# open drawer
drawer_trigger = page.locator(".main").get_by_role("button", name="Filters")
drawer_trigger.click()
# verify drawer is visible
drawer = page.locator("ld-filter-drawer")
expect(drawer).to_be_visible()
expect(drawer.locator("h2")).to_have_text("Filters")
# verify drawer is visible
drawer = page.locator("ld-filter-drawer")
expect(drawer).to_be_visible()
expect(drawer.locator("h2")).to_have_text("Filters")
# close with close button
drawer.locator("button.close").click()
expect(drawer).to_be_hidden()
# close with close button
drawer.locator("button.close").click()
expect(drawer).to_be_hidden()
# open drawer again
drawer_trigger.click()
# open drawer again
drawer_trigger.click()
# close with backdrop
backdrop = drawer.locator(".modal-overlay")
backdrop.click(position={"x": 0, "y": 0})
expect(drawer).to_be_hidden()
# close with backdrop
backdrop = drawer.locator(".modal-overlay")
backdrop.click(position={"x": 0, "y": 0})
expect(drawer).to_be_hidden()
def test_select_tag(self):
self.setup_bookmark(tags=[self.setup_tag(name="cooking")])
self.setup_bookmark(tags=[self.setup_tag(name="hiking")])
with sync_playwright() as p:
page = self.open(reverse("linkding:bookmarks.index"), p)
page = self.open(reverse("linkding:bookmarks.index"))
# use smaller viewport to make filter button visible
page.set_viewport_size({"width": 375, "height": 812})
# use smaller viewport to make filter button visible
page.set_viewport_size({"width": 375, "height": 812})
# open tag cloud modal
drawer_trigger = page.locator(".main").get_by_role("button", name="Filters")
drawer_trigger.click()
# open tag cloud modal
drawer_trigger = page.locator(".main").get_by_role("button", name="Filters")
drawer_trigger.click()
# verify tags are displayed
drawer = page.locator("ld-filter-drawer")
unselected_tags = drawer.locator(".unselected-tags")
expect(unselected_tags.get_by_text("cooking")).to_be_visible()
expect(unselected_tags.get_by_text("hiking")).to_be_visible()
# verify tags are displayed
drawer = page.locator("ld-filter-drawer")
unselected_tags = drawer.locator(".unselected-tags")
expect(unselected_tags.get_by_text("cooking")).to_be_visible()
expect(unselected_tags.get_by_text("hiking")).to_be_visible()
# select tag
unselected_tags.get_by_text("cooking").click()
# select tag
unselected_tags.get_by_text("cooking").click()
# open drawer again
drawer_trigger.click()
# open drawer again
drawer_trigger.click()
# verify tag is selected, other tag is not visible anymore
selected_tags = drawer.locator(".selected-tags")
expect(selected_tags.get_by_text("cooking")).to_be_visible()
# verify tag is selected, other tag is not visible anymore
selected_tags = drawer.locator(".selected-tags")
expect(selected_tags.get_by_text("cooking")).to_be_visible()
expect(unselected_tags.get_by_text("cooking")).not_to_be_visible()
expect(unselected_tags.get_by_text("hiking")).not_to_be_visible()
expect(unselected_tags.get_by_text("cooking")).not_to_be_visible()
expect(unselected_tags.get_by_text("hiking")).not_to_be_visible()

View File

@@ -1,32 +1,24 @@
from django.urls import reverse
from playwright.sync_api import sync_playwright, expect
from playwright.sync_api import expect
from bookmarks.tests_e2e.helpers import LinkdingE2ETestCase
class GlobalShortcutsE2ETestCase(LinkdingE2ETestCase):
def test_focus_search(self):
with sync_playwright() as p:
browser = self.setup_browser(p)
page = browser.new_page()
page.goto(self.live_server_url + reverse("linkding:bookmarks.index"))
self.open(reverse("linkding:bookmarks.index"))
page.press("body", "s")
self.page.press("body", "s")
expect(page.get_by_placeholder("Search for words or #tags")).to_be_focused()
browser.close()
expect(
self.page.get_by_placeholder("Search for words or #tags")
).to_be_focused()
def test_add_bookmark(self):
with sync_playwright() as p:
browser = self.setup_browser(p)
page = browser.new_page()
page.goto(self.live_server_url + reverse("linkding:bookmarks.index"))
self.open(reverse("linkding:bookmarks.index"))
page.press("body", "n")
self.page.press("body", "n")
expect(page).to_have_url(
self.live_server_url + reverse("linkding:bookmarks.new")
)
browser.close()
expect(self.page).to_have_url(
self.live_server_url + reverse("linkding:bookmarks.new")
)

View File

@@ -2,7 +2,7 @@ from unittest.mock import patch
from urllib.parse import quote
from django.urls import reverse
from playwright.sync_api import sync_playwright, expect
from playwright.sync_api import expect
from bookmarks.models import Bookmark
from bookmarks.services import website_loader
@@ -26,74 +26,69 @@ class BookmarkFormE2ETestCase(LinkdingE2ETestCase):
self.website_loader_mock = self.website_loader_patch.start()
def tearDown(self) -> None:
super().tearDown()
self.website_loader_patch.stop()
super().tearDown()
def test_enter_url_prefills_title_and_description(self):
with sync_playwright() as p:
page = self.open(reverse("linkding:bookmarks.new"), p)
url = page.get_by_label("URL")
title = page.get_by_label("Title")
description = page.get_by_label("Description")
page = self.open(reverse("linkding:bookmarks.new"))
url = page.get_by_label("URL")
title = page.get_by_label("Title")
description = page.get_by_label("Description")
url.fill("https://example.com")
expect(title).to_have_value("Example Domain")
expect(description).to_have_value(
"This domain is for use in illustrative examples in documents. You may use this domain in literature without prior coordination or asking for permission."
)
url.fill("https://example.com")
expect(title).to_have_value("Example Domain")
expect(description).to_have_value(
"This domain is for use in illustrative examples in documents. You may use this domain in literature without prior coordination or asking for permission."
)
def test_enter_url_does_not_overwrite_modified_title_and_description(self):
with sync_playwright() as p:
page = self.open(reverse("linkding:bookmarks.new"), p)
url = page.get_by_label("URL")
title = page.get_by_label("Title")
description = page.get_by_label("Description")
page = self.open(reverse("linkding:bookmarks.new"))
url = page.get_by_label("URL")
title = page.get_by_label("Title")
description = page.get_by_label("Description")
title.fill("Modified title")
description.fill("Modified description")
url.fill("https://example.com")
page.wait_for_timeout(timeout=1000)
title.fill("Modified title")
description.fill("Modified description")
url.fill("https://example.com")
page.wait_for_timeout(timeout=1000)
expect(title).to_have_value("Modified title")
expect(description).to_have_value("Modified description")
expect(title).to_have_value("Modified title")
expect(description).to_have_value("Modified description")
def test_with_initial_url_prefills_title_and_description(self):
with sync_playwright() as p:
page_url = (
reverse("linkding:bookmarks.new")
+ f"?url={quote('https://example.com')}"
)
page = self.open(page_url, p)
url = page.get_by_label("URL")
title = page.get_by_label("Title")
description = page.get_by_label("Description")
page_url = (
reverse("linkding:bookmarks.new") + f"?url={quote('https://example.com')}"
)
page = self.open(page_url)
url = page.get_by_label("URL")
title = page.get_by_label("Title")
description = page.get_by_label("Description")
page.wait_for_timeout(timeout=1000)
page.wait_for_timeout(timeout=1000)
expect(url).to_have_value("https://example.com")
expect(title).to_have_value("Example Domain")
expect(description).to_have_value(
"This domain is for use in illustrative examples in documents. You may use this domain in literature without prior coordination or asking for permission."
)
expect(url).to_have_value("https://example.com")
expect(title).to_have_value("Example Domain")
expect(description).to_have_value(
"This domain is for use in illustrative examples in documents. You may use this domain in literature without prior coordination or asking for permission."
)
def test_with_initial_url_title_description_does_not_overwrite_title_and_description(
self,
):
with sync_playwright() as p:
page_url = (
reverse("linkding:bookmarks.new")
+ f"?url={quote('https://example.com')}&title=Initial+title&description=Initial+description"
)
page = self.open(page_url, p)
url = page.get_by_label("URL")
title = page.get_by_label("Title")
description = page.get_by_label("Description")
page_url = (
reverse("linkding:bookmarks.new")
+ f"?url={quote('https://example.com')}&title=Initial+title&description=Initial+description"
)
page = self.open(page_url)
url = page.get_by_label("URL")
title = page.get_by_label("Title")
description = page.get_by_label("Description")
page.wait_for_timeout(timeout=1000)
page.wait_for_timeout(timeout=1000)
expect(url).to_have_value("https://example.com")
expect(title).to_have_value("Initial title")
expect(description).to_have_value("Initial description")
expect(url).to_have_value("https://example.com")
expect(title).to_have_value("Initial title")
expect(description).to_have_value("Initial description")
def test_create_should_check_for_existing_bookmark(self):
existing_bookmark = self.setup_bookmark(
@@ -105,170 +100,164 @@ class BookmarkFormE2ETestCase(LinkdingE2ETestCase):
)
tag_names = " ".join(existing_bookmark.tag_names)
with sync_playwright() as p:
page = self.open(reverse("linkding:bookmarks.new"), p)
page = self.open(reverse("linkding:bookmarks.new"))
# Enter bookmarked URL
page.get_by_label("URL").fill(existing_bookmark.url)
# Already bookmarked hint should be visible
page.get_by_text("This URL is already bookmarked.").wait_for(timeout=2000)
# Form should be pre-filled with data from existing bookmark
self.assertEqual(
existing_bookmark.title, page.get_by_label("Title").input_value()
)
self.assertEqual(
existing_bookmark.description,
page.get_by_label("Description").input_value(),
)
self.assertEqual(
existing_bookmark.notes, page.get_by_label("Notes").input_value()
)
self.assertEqual(tag_names, page.get_by_label("Tags").input_value())
self.assertTrue(tag_names, page.get_by_label("Mark as unread").is_checked())
# Enter bookmarked URL
page.get_by_label("URL").fill(existing_bookmark.url)
# Already bookmarked hint should be visible
page.get_by_text("This URL is already bookmarked.").wait_for(timeout=2000)
# Form should be pre-filled with data from existing bookmark
self.assertEqual(
existing_bookmark.title, page.get_by_label("Title").input_value()
)
self.assertEqual(
existing_bookmark.description,
page.get_by_label("Description").input_value(),
)
self.assertEqual(
existing_bookmark.notes, page.get_by_label("Notes").input_value()
)
self.assertEqual(tag_names, page.get_by_label("Tags").input_value())
self.assertTrue(tag_names, page.get_by_label("Mark as unread").is_checked())
# Enter non-bookmarked URL
page.get_by_label("URL").fill("https://example.com/unknown")
# Already bookmarked hint should be hidden
page.get_by_text("This URL is already bookmarked.").wait_for(
state="hidden", timeout=2000
)
# Enter non-bookmarked URL
page.get_by_label("URL").fill("https://example.com/unknown")
# Already bookmarked hint should be hidden
page.get_by_text("This URL is already bookmarked.").wait_for(
state="hidden", timeout=2000
)
def test_enter_url_of_existing_bookmark_should_show_notes(self):
bookmark = self.setup_bookmark(
notes="Existing notes", description="Existing description"
)
with sync_playwright() as p:
page = self.open(reverse("linkding:bookmarks.new"), p)
page = self.open(reverse("linkding:bookmarks.new"))
details = page.locator("details.notes")
expect(details).not_to_have_attribute("open", value="")
details = page.locator("details.notes")
expect(details).not_to_have_attribute("open", value="")
page.get_by_label("URL").fill(bookmark.url)
expect(details).to_have_attribute("open", value="")
page.get_by_label("URL").fill(bookmark.url)
expect(details).to_have_attribute("open", value="")
def test_create_should_preview_auto_tags(self):
profile = self.get_or_create_test_user().profile
profile.auto_tagging_rules = "github.com dev github"
profile.save()
with sync_playwright() as p:
# Open page with URL that should have auto tags
url = (
reverse("linkding:bookmarks.new")
+ "?url=https%3A%2F%2Fgithub.com%2Fsissbruecker%2Flinkding"
)
page = self.open(url, p)
# Open page with URL that should have auto tags
url = (
reverse("linkding:bookmarks.new")
+ "?url=https%3A%2F%2Fgithub.com%2Fsissbruecker%2Flinkding"
)
page = self.open(url)
auto_tags_hint = page.locator(".form-input-hint.auto-tags")
expect(auto_tags_hint).to_be_visible()
expect(auto_tags_hint).to_have_text("Auto tags: dev github")
auto_tags_hint = page.locator(".form-input-hint.auto-tags")
expect(auto_tags_hint).to_be_visible()
expect(auto_tags_hint).to_have_text("Auto tags: dev github")
# Change to URL without auto tags
page.get_by_label("URL").fill("https://example.com")
# Change to URL without auto tags
page.get_by_label("URL").fill("https://example.com")
expect(auto_tags_hint).to_be_hidden()
expect(auto_tags_hint).to_be_hidden()
def test_clear_buttons_only_shown_when_fields_have_content(self):
with sync_playwright() as p:
page = self.open(reverse("linkding:bookmarks.new"), p)
page = self.open(reverse("linkding:bookmarks.new"))
title_field = page.get_by_label("Title")
title_clear_button = page.locator("ld-clear-button[data-for='id_title']")
description_field = page.get_by_label("Description")
description_clear_button = page.locator(
"ld-clear-button[data-for='id_description']"
)
title_field = page.get_by_label("Title")
title_clear_button = page.locator("ld-clear-button[data-for='id_title']")
description_field = page.get_by_label("Description")
description_clear_button = page.locator(
"ld-clear-button[data-for='id_description']"
)
# Initially, clear buttons should be hidden because fields are empty
expect(title_clear_button).to_be_hidden()
expect(description_clear_button).to_be_hidden()
# Initially, clear buttons should be hidden because fields are empty
expect(title_clear_button).to_be_hidden()
expect(description_clear_button).to_be_hidden()
# Add content to title field, its clear button should become visible
title_field.fill("Test title")
expect(title_clear_button).to_be_visible()
# Add content to title field, its clear button should become visible
title_field.fill("Test title")
expect(title_clear_button).to_be_visible()
# Add content to description field, its clear button should become visible
description_field.fill("Test description")
expect(description_clear_button).to_be_visible()
# Add content to description field, its clear button should become visible
description_field.fill("Test description")
expect(description_clear_button).to_be_visible()
# Clear title field, its clear button should be hidden again
title_field.fill("")
expect(title_clear_button).to_be_hidden()
# Clear title field, its clear button should be hidden again
title_field.fill("")
expect(title_clear_button).to_be_hidden()
# Clear description field, its clear button should be hidden again
description_field.fill("")
expect(description_clear_button).to_be_hidden()
# Clear description field, its clear button should be hidden again
description_field.fill("")
expect(description_clear_button).to_be_hidden()
def test_refresh_button_only_shown_for_existing_bookmarks(self):
existing_bookmark = self.setup_bookmark(
title="Existing title", description="Existing description"
)
with sync_playwright() as p:
page = self.open(reverse("linkding:bookmarks.new"), p)
refresh_button = page.locator("#refresh-button")
page = self.open(reverse("linkding:bookmarks.new"))
refresh_button = page.locator("#refresh-button")
# Initially, refresh button should be hidden
expect(refresh_button).to_be_hidden()
# Initially, refresh button should be hidden
expect(refresh_button).to_be_hidden()
# Enter a URL that is not bookmarked yet
page.get_by_label("URL").fill("https://example.com/not-bookmarked")
page.wait_for_timeout(timeout=1000)
# Enter a URL that is not bookmarked yet
page.get_by_label("URL").fill("https://example.com/not-bookmarked")
page.wait_for_timeout(timeout=1000)
expect(refresh_button).to_be_hidden()
expect(refresh_button).to_be_hidden()
# Enter a URL that is already bookmarked
page.get_by_label("URL").fill(existing_bookmark.url)
# Enter a URL that is already bookmarked
page.get_by_label("URL").fill(existing_bookmark.url)
expect(refresh_button).to_be_visible()
expect(refresh_button).to_be_visible()
# Change back to non-bookmarked URL
page.get_by_label("URL").fill("https://example.com/another-not-bookmarked")
# Change back to non-bookmarked URL
page.get_by_label("URL").fill("https://example.com/another-not-bookmarked")
expect(refresh_button).to_be_hidden()
expect(refresh_button).to_be_hidden()
def test_refresh_from_website_button_updates_title_and_description(self):
existing_bookmark = self.setup_bookmark(
title="Existing title", description="Existing description"
)
with sync_playwright() as p:
page = self.open(reverse("linkding:bookmarks.new"), p)
url_field = page.get_by_label("URL")
title_field = page.get_by_label("Title")
description_field = page.get_by_label("Description")
refresh_button = page.locator("#refresh-button")
page = self.open(reverse("linkding:bookmarks.new"))
url_field = page.get_by_label("URL")
title_field = page.get_by_label("Title")
description_field = page.get_by_label("Description")
refresh_button = page.locator("#refresh-button")
# Enter the URL of the existing bookmark to make refresh button visible
url_field.fill(existing_bookmark.url)
# Enter the URL of the existing bookmark to make refresh button visible
url_field.fill(existing_bookmark.url)
# Wait for metadata to be loaded and for refresh button to be visible
expect(refresh_button).to_be_visible()
expect(title_field).to_have_value("Existing title")
expect(description_field).to_have_value("Existing description")
# Wait for metadata to be loaded and for refresh button to be visible
expect(refresh_button).to_be_visible()
expect(title_field).to_have_value("Existing title")
expect(description_field).to_have_value("Existing description")
# Update the mock to return different metadata when refresh is requested
self.website_loader_mock.reset_mock()
self.website_loader_mock.return_value = website_loader.WebsiteMetadata(
url="https://example.com",
title="Updated Example Domain",
description="This is a refreshed description for the example domain.",
preview_image=None,
)
# Update the mock to return different metadata when refresh is requested
self.website_loader_mock.reset_mock()
self.website_loader_mock.return_value = website_loader.WebsiteMetadata(
url="https://example.com",
title="Updated Example Domain",
description="This is a refreshed description for the example domain.",
preview_image=None,
)
# Click the refresh button
refresh_button.click()
# Click the refresh button
refresh_button.click()
# Verify that title and description have been updated with new values
expect(title_field).to_have_value("Updated Example Domain")
expect(description_field).to_have_value(
"This is a refreshed description for the example domain."
)
# Verify that title and description have been updated with new values
expect(title_field).to_have_value("Updated Example Domain")
expect(description_field).to_have_value(
"This is a refreshed description for the example domain."
)
# Verify that the fields are visually marked as modified
expect(title_field).to_have_class("form-input modified")
expect(description_field).to_have_class("form-input modified")
# Verify that the fields are visually marked as modified
expect(title_field).to_have_class("form-input modified")
expect(description_field).to_have_class("form-input modified")
def test_refresh_from_website_button_does_not_modify_fields_if_metadata_is_same(
self,
@@ -277,59 +266,57 @@ class BookmarkFormE2ETestCase(LinkdingE2ETestCase):
title="Existing title", description="Existing description"
)
with sync_playwright() as p:
page = self.open(reverse("linkding:bookmarks.new"), p)
url_field = page.get_by_label("URL")
title_field = page.get_by_label("Title")
description_field = page.get_by_label("Description")
refresh_button = page.locator("#refresh-button")
page = self.open(reverse("linkding:bookmarks.new"))
url_field = page.get_by_label("URL")
title_field = page.get_by_label("Title")
description_field = page.get_by_label("Description")
refresh_button = page.locator("#refresh-button")
# Enter the URL of the existing bookmark to make refresh button visible
url_field.fill(existing_bookmark.url)
# Enter the URL of the existing bookmark to make refresh button visible
url_field.fill(existing_bookmark.url)
# Wait for metadata to be loaded and for refresh button to be visible
expect(refresh_button).to_be_visible()
expect(title_field).to_have_value("Existing title")
expect(description_field).to_have_value("Existing description")
# Wait for metadata to be loaded and for refresh button to be visible
expect(refresh_button).to_be_visible()
expect(title_field).to_have_value("Existing title")
expect(description_field).to_have_value("Existing description")
# Update the mock to return same metadata when refresh is requested
self.website_loader_mock.reset_mock()
self.website_loader_mock.return_value = website_loader.WebsiteMetadata(
url="https://example.com",
title="Existing title",
description="Existing description",
preview_image=None,
)
# Update the mock to return same metadata when refresh is requested
self.website_loader_mock.reset_mock()
self.website_loader_mock.return_value = website_loader.WebsiteMetadata(
url="https://example.com",
title="Existing title",
description="Existing description",
preview_image=None,
)
# Click the refresh button
refresh_button.click()
page.wait_for_timeout(timeout=1000)
# Click the refresh button
refresh_button.click()
page.wait_for_timeout(timeout=1000)
# Verify that title and description values are still the same
expect(title_field).to_have_value("Existing title")
expect(description_field).to_have_value("Existing description")
# Verify that title and description values are still the same
expect(title_field).to_have_value("Existing title")
expect(description_field).to_have_value("Existing description")
# Verify that the fields are NOT visually marked as modified
expect(title_field).to_have_class("form-input")
expect(description_field).to_have_class("form-input")
# Verify that the fields are NOT visually marked as modified
expect(title_field).to_have_class("form-input")
expect(description_field).to_have_class("form-input")
def test_ctrl_enter_submits_form_from_description(self):
with sync_playwright() as p:
page = self.open(reverse("linkding:bookmarks.new"), p)
url_field = page.get_by_label("URL")
description_field = page.get_by_label("Description")
page = self.open(reverse("linkding:bookmarks.new"))
url_field = page.get_by_label("URL")
description_field = page.get_by_label("Description")
url_field.fill("https://example.com")
description_field.fill("Test description")
description_field.focus()
url_field.fill("https://example.com")
description_field.fill("Test description")
description_field.focus()
# Press Ctrl+Enter to submit form
description_field.press("Control+Enter")
# Press Ctrl+Enter to submit form
description_field.press("Control+Enter")
# Should navigate away from new bookmark page after successful submission
expect(page).not_to_have_url(
self.live_server_url + reverse("linkding:bookmarks.new")
)
# Should navigate away from new bookmark page after successful submission
expect(page).not_to_have_url(
self.live_server_url + reverse("linkding:bookmarks.new")
)
self.assertEqual(1, Bookmark.objects.count())
bookmark = Bookmark.objects.first()

View File

@@ -1,5 +1,5 @@
from django.urls import reverse
from playwright.sync_api import sync_playwright, expect
from playwright.sync_api import expect
from bookmarks.tests_e2e.helpers import LinkdingE2ETestCase
from bookmarks.models import UserProfile
@@ -7,54 +7,51 @@ from bookmarks.models import UserProfile
class SettingsGeneralE2ETestCase(LinkdingE2ETestCase):
def test_should_only_enable_public_sharing_if_sharing_is_enabled(self):
with sync_playwright() as p:
browser = self.setup_browser(p)
page = browser.new_page()
page.goto(self.live_server_url + reverse("linkding:settings.general"))
self.open(reverse("linkding:settings.general"))
enable_sharing = page.get_by_label("Enable bookmark sharing")
enable_sharing_label = page.get_by_text("Enable bookmark sharing")
enable_public_sharing = page.get_by_label("Enable public bookmark sharing")
enable_public_sharing_label = page.get_by_text(
"Enable public bookmark sharing"
)
default_mark_shared = page.get_by_label(
"Create bookmarks as shared by default"
)
default_mark_shared_label = page.get_by_text(
"Create bookmarks as shared by default"
)
enable_sharing = self.page.get_by_label("Enable bookmark sharing")
enable_sharing_label = self.page.get_by_text("Enable bookmark sharing")
enable_public_sharing = self.page.get_by_label("Enable public bookmark sharing")
enable_public_sharing_label = self.page.get_by_text(
"Enable public bookmark sharing"
)
default_mark_shared = self.page.get_by_label(
"Create bookmarks as shared by default"
)
default_mark_shared_label = self.page.get_by_text(
"Create bookmarks as shared by default"
)
# Public sharing and default shared are disabled by default
expect(enable_sharing).not_to_be_checked()
expect(enable_public_sharing).not_to_be_checked()
expect(enable_public_sharing).to_be_disabled()
expect(default_mark_shared).not_to_be_checked()
expect(default_mark_shared).to_be_disabled()
# Public sharing and default shared are disabled by default
expect(enable_sharing).not_to_be_checked()
expect(enable_public_sharing).not_to_be_checked()
expect(enable_public_sharing).to_be_disabled()
expect(default_mark_shared).not_to_be_checked()
expect(default_mark_shared).to_be_disabled()
# Enable sharing
enable_sharing_label.click()
expect(enable_sharing).to_be_checked()
expect(enable_public_sharing).not_to_be_checked()
expect(enable_public_sharing).to_be_enabled()
expect(default_mark_shared).not_to_be_checked()
expect(default_mark_shared).to_be_enabled()
# Enable sharing
enable_sharing_label.click()
expect(enable_sharing).to_be_checked()
expect(enable_public_sharing).not_to_be_checked()
expect(enable_public_sharing).to_be_enabled()
expect(default_mark_shared).not_to_be_checked()
expect(default_mark_shared).to_be_enabled()
# Enable public sharing and default shared
enable_public_sharing_label.click()
default_mark_shared_label.click()
expect(enable_public_sharing).to_be_checked()
expect(enable_public_sharing).to_be_enabled()
expect(default_mark_shared).to_be_checked()
expect(default_mark_shared).to_be_enabled()
# Enable public sharing and default shared
enable_public_sharing_label.click()
default_mark_shared_label.click()
expect(enable_public_sharing).to_be_checked()
expect(enable_public_sharing).to_be_enabled()
expect(default_mark_shared).to_be_checked()
expect(default_mark_shared).to_be_enabled()
# Disable sharing
enable_sharing_label.click()
expect(enable_sharing).not_to_be_checked()
expect(enable_public_sharing).not_to_be_checked()
expect(enable_public_sharing).to_be_disabled()
expect(default_mark_shared).not_to_be_checked()
expect(default_mark_shared).to_be_disabled()
# Disable sharing
enable_sharing_label.click()
expect(enable_sharing).not_to_be_checked()
expect(enable_public_sharing).not_to_be_checked()
expect(enable_public_sharing).to_be_disabled()
expect(default_mark_shared).not_to_be_checked()
expect(default_mark_shared).to_be_disabled()
def test_should_not_show_bookmark_description_max_lines_when_display_inline(self):
profile = self.get_or_create_test_user().profile
@@ -63,13 +60,10 @@ class SettingsGeneralE2ETestCase(LinkdingE2ETestCase):
)
profile.save()
with sync_playwright() as p:
browser = self.setup_browser(p)
page = browser.new_page()
page.goto(self.live_server_url + reverse("linkding:settings.general"))
self.open(reverse("linkding:settings.general"))
max_lines = page.get_by_label("Bookmark description max lines")
expect(max_lines).to_be_hidden()
max_lines = self.page.get_by_label("Bookmark description max lines")
expect(max_lines).to_be_hidden()
def test_should_show_bookmark_description_max_lines_when_display_separate(self):
profile = self.get_or_create_test_user().profile
@@ -78,26 +72,20 @@ class SettingsGeneralE2ETestCase(LinkdingE2ETestCase):
)
profile.save()
with sync_playwright() as p:
browser = self.setup_browser(p)
page = browser.new_page()
page.goto(self.live_server_url + reverse("linkding:settings.general"))
self.open(reverse("linkding:settings.general"))
max_lines = page.get_by_label("Bookmark description max lines")
expect(max_lines).to_be_visible()
max_lines = self.page.get_by_label("Bookmark description max lines")
expect(max_lines).to_be_visible()
def test_should_update_bookmark_description_max_lines_when_changing_display(self):
with sync_playwright() as p:
browser = self.setup_browser(p)
page = browser.new_page()
page.goto(self.live_server_url + reverse("linkding:settings.general"))
self.open(reverse("linkding:settings.general"))
max_lines = page.get_by_label("Bookmark description max lines")
expect(max_lines).to_be_hidden()
max_lines = self.page.get_by_label("Bookmark description max lines")
expect(max_lines).to_be_hidden()
display = page.get_by_label("Bookmark description", exact=True)
display.select_option("separate")
expect(max_lines).to_be_visible()
display = self.page.get_by_label("Bookmark description", exact=True)
display.select_option("separate")
expect(max_lines).to_be_visible()
display.select_option("inline")
expect(max_lines).to_be_hidden()
display.select_option("inline")
expect(max_lines).to_be_hidden()

View File

@@ -1,67 +1,65 @@
from django.urls import reverse
from playwright.sync_api import sync_playwright, expect
from playwright.sync_api import expect
from bookmarks.tests_e2e.helpers import LinkdingE2ETestCase
class SettingsIntegrationsE2ETestCase(LinkdingE2ETestCase):
def test_create_api_token(self):
with sync_playwright() as p:
self.open(reverse("linkding:settings.integrations"), p)
self.open(reverse("linkding:settings.integrations"))
# Click create API token button
self.page.get_by_text("Create API token").click()
# Click create API token button
self.page.get_by_text("Create API token").click()
# Wait for modal to appear
modal = self.page.locator(".modal")
expect(modal).to_be_visible()
# Wait for modal to appear
modal = self.page.locator(".modal")
expect(modal).to_be_visible()
# Enter custom token name
token_name_input = modal.locator("#token-name")
token_name_input.fill("")
token_name_input.fill("My Test Token")
# Enter custom token name
token_name_input = modal.locator("#token-name")
token_name_input.fill("")
token_name_input.fill("My Test Token")
# Confirm the dialog
modal.page.get_by_role("button", name="Create Token").click()
# Confirm the dialog
modal.page.get_by_role("button", name="Create Token").click()
# Verify the API token key is shown in the input
new_token_input = self.page.locator("#new-token-key")
expect(new_token_input).to_be_visible()
token_value = new_token_input.input_value()
self.assertTrue(len(token_value) > 0)
# Verify the API token key is shown in the input
new_token_input = self.page.locator("#new-token-key")
expect(new_token_input).to_be_visible()
token_value = new_token_input.input_value()
self.assertTrue(len(token_value) > 0)
# Verify the API token is now listed in the table
token_table = self.page.locator("table.crud-table")
expect(token_table).to_be_visible()
expect(token_table.get_by_text("My Test Token")).to_be_visible()
# Verify the API token is now listed in the table
token_table = self.page.locator("table.crud-table")
expect(token_table).to_be_visible()
expect(token_table.get_by_text("My Test Token")).to_be_visible()
# Verify the dialog is gone
expect(modal).to_be_hidden()
# Verify the dialog is gone
expect(modal).to_be_hidden()
# Reload the page to verify the API token key is only shown once
self.page.reload()
# Reload the page to verify the API token key is only shown once
self.page.reload()
# Token key input should no longer be visible
expect(new_token_input).not_to_be_visible()
# Token key input should no longer be visible
expect(new_token_input).not_to_be_visible()
# But the token should still be listed in the table
expect(token_table.get_by_text("My Test Token")).to_be_visible()
# But the token should still be listed in the table
expect(token_table.get_by_text("My Test Token")).to_be_visible()
def test_delete_api_token(self):
self.setup_api_token(name="Token To Delete")
with sync_playwright() as p:
self.open(reverse("linkding:settings.integrations"), p)
self.open(reverse("linkding:settings.integrations"))
token_table = self.page.locator("table.crud-table")
expect(token_table.get_by_text("Token To Delete")).to_be_visible()
token_table = self.page.locator("table.crud-table")
expect(token_table.get_by_text("Token To Delete")).to_be_visible()
# Click delete button for the token
token_row = token_table.locator("tr").filter(has_text="Token To Delete")
token_row.get_by_role("button", name="Delete").click()
# Click delete button for the token
token_row = token_table.locator("tr").filter(has_text="Token To Delete")
token_row.get_by_role("button", name="Delete").click()
# Confirm deletion
self.locate_confirm_dialog().get_by_text("Confirm").click()
# Confirm deletion
self.locate_confirm_dialog().get_by_text("Confirm").click()
# Verify the token is removed from the table
expect(token_table.get_by_text("Token To Delete")).not_to_be_visible()
# Verify the token is removed from the table
expect(token_table.get_by_text("Token To Delete")).not_to_be_visible()

View File

@@ -1,5 +1,5 @@
from django.urls import reverse
from playwright.sync_api import sync_playwright, expect
from playwright.sync_api import expect
from bookmarks.models import Tag
from bookmarks.tests_e2e.helpers import LinkdingE2ETestCase
@@ -25,30 +25,29 @@ class TagManagementE2ETestCase(LinkdingE2ETestCase):
expect(success_message).to_contain_text(text)
def test_create_tag(self):
with sync_playwright() as p:
self.open(reverse("linkding:tags.index"), p)
self.open(reverse("linkding:tags.index"))
# Click the Create Tag button to open the modal
self.page.get_by_text("Create Tag").click()
# Click the Create Tag button to open the modal
self.page.get_by_text("Create Tag").click()
modal = self.locate_tag_modal()
modal = self.locate_tag_modal()
# Fill in a tag name
name_input = modal.get_by_label("Name")
name_input.fill("test-tag")
# Fill in a tag name
name_input = modal.get_by_label("Name")
name_input.fill("test-tag")
# Submit the form
modal.get_by_text("Save").click()
# Submit the form
modal.get_by_text("Save").click()
# Verify modal is closed and we're back on the tags page
expect(modal).not_to_be_visible()
# Verify modal is closed and we're back on the tags page
expect(modal).not_to_be_visible()
# Verify the success message is shown
self.verify_success_message('Tag "test-tag" created successfully.')
# Verify the success message is shown
self.verify_success_message('Tag "test-tag" created successfully.')
# Verify the new tag is shown in the list
tag_row = self.locate_tag_row("test-tag")
expect(tag_row).to_be_visible()
# Verify the new tag is shown in the list
tag_row = self.locate_tag_row("test-tag")
expect(tag_row).to_be_visible()
# Verify the tag was actually created in the database
self.assertEqual(
@@ -60,31 +59,30 @@ class TagManagementE2ETestCase(LinkdingE2ETestCase):
def test_create_tag_validation_error(self):
existing_tag = self.setup_tag(name="existing-tag")
with sync_playwright() as p:
self.open(reverse("linkding:tags.index"), p)
self.open(reverse("linkding:tags.index"))
# Click the Create Tag button to open the modal
self.page.get_by_text("Create Tag").click()
# Click the Create Tag button to open the modal
self.page.get_by_text("Create Tag").click()
modal = self.locate_tag_modal()
modal = self.locate_tag_modal()
# Submit with empty value
modal.get_by_text("Save").click()
# Submit with empty value
modal.get_by_text("Save").click()
# Verify the error is shown (field is required)
error_hint = modal.get_by_text("This field is required")
expect(error_hint).to_be_visible()
# Verify the error is shown (field is required)
error_hint = modal.get_by_text("This field is required")
expect(error_hint).to_be_visible()
# Fill in the name of an existing tag
name_input = modal.get_by_label("Name")
name_input.fill(existing_tag.name)
# Fill in the name of an existing tag
name_input = modal.get_by_label("Name")
name_input.fill(existing_tag.name)
# Submit the form
modal.get_by_text("Save").click()
# Submit the form
modal.get_by_text("Save").click()
# Verify the error is shown (tag already exists)
error_hint = modal.get_by_text('Tag "existing-tag" already exists')
expect(error_hint).to_be_visible()
# Verify the error is shown (tag already exists)
error_hint = modal.get_by_text('Tag "existing-tag" already exists')
expect(error_hint).to_be_visible()
# Verify no additional tag was created
self.assertEqual(
@@ -94,34 +92,33 @@ class TagManagementE2ETestCase(LinkdingE2ETestCase):
def test_edit_tag(self):
tag = self.setup_tag(name="old-name")
with sync_playwright() as p:
self.open(reverse("linkding:tags.index"), p)
self.open(reverse("linkding:tags.index"))
# Click the Edit button for the tag
tag_row = self.locate_tag_row(tag.name)
tag_row.get_by_role("link", name="Edit").click()
# Click the Edit button for the tag
tag_row = self.locate_tag_row(tag.name)
tag_row.get_by_role("link", name="Edit").click()
modal = self.locate_tag_modal()
modal = self.locate_tag_modal()
# Verify the form is pre-filled with the tag name
name_input = modal.get_by_label("Name")
expect(name_input).to_have_value(tag.name)
# Verify the form is pre-filled with the tag name
name_input = modal.get_by_label("Name")
expect(name_input).to_have_value(tag.name)
# Change the tag name
name_input.fill("new-name")
# Change the tag name
name_input.fill("new-name")
# Submit the form
modal.get_by_text("Save").click()
# Submit the form
modal.get_by_text("Save").click()
# Verify modal is closed
expect(modal).not_to_be_visible()
# Verify modal is closed
expect(modal).not_to_be_visible()
# Verify the success message is shown
self.verify_success_message('Tag "new-name" updated successfully.')
# Verify the success message is shown
self.verify_success_message('Tag "new-name" updated successfully.')
# Verify the updated tag is shown in the list
expect(self.locate_tag_row("new-name")).to_be_visible()
expect(self.locate_tag_row("old-name")).not_to_be_visible()
# Verify the updated tag is shown in the list
expect(self.locate_tag_row("new-name")).to_be_visible()
expect(self.locate_tag_row("old-name")).not_to_be_visible()
# Verify the tag was updated in the database
tag.refresh_from_db()
@@ -131,31 +128,30 @@ class TagManagementE2ETestCase(LinkdingE2ETestCase):
tag = self.setup_tag(name="tag-to-edit")
other_tag = self.setup_tag(name="other-tag")
with sync_playwright() as p:
self.open(reverse("linkding:tags.index"), p)
self.open(reverse("linkding:tags.index"))
# Click the Edit button for the tag
tag_row = self.locate_tag_row(tag.name)
tag_row.get_by_role("link", name="Edit").click()
# Click the Edit button for the tag
tag_row = self.locate_tag_row(tag.name)
tag_row.get_by_role("link", name="Edit").click()
modal = self.locate_tag_modal()
modal = self.locate_tag_modal()
# Clear the name and submit
name_input = modal.get_by_label("Name")
name_input.fill("")
modal.get_by_text("Save").click()
# Clear the name and submit
name_input = modal.get_by_label("Name")
name_input.fill("")
modal.get_by_text("Save").click()
# Verify the error is shown (field is required)
error_hint = modal.get_by_text("This field is required")
expect(error_hint).to_be_visible()
# Verify the error is shown (field is required)
error_hint = modal.get_by_text("This field is required")
expect(error_hint).to_be_visible()
# Fill in the name of another existing tag
name_input.fill(other_tag.name)
modal.get_by_text("Save").click()
# Fill in the name of another existing tag
name_input.fill(other_tag.name)
modal.get_by_text("Save").click()
# Verify the error is shown (tag already exists)
error_hint = modal.get_by_text('Tag "other-tag" already exists')
expect(error_hint).to_be_visible()
# Verify the error is shown (tag already exists)
error_hint = modal.get_by_text('Tag "other-tag" already exists')
expect(error_hint).to_be_visible()
# Verify the tag was not modified
tag.refresh_from_db()
@@ -170,37 +166,36 @@ class TagManagementE2ETestCase(LinkdingE2ETestCase):
bookmark1 = self.setup_bookmark(tags=[merge_tag1])
bookmark2 = self.setup_bookmark(tags=[merge_tag2])
with sync_playwright() as p:
self.open(reverse("linkding:tags.index"), p)
self.open(reverse("linkding:tags.index"))
# Click the Merge Tags button to open the modal
self.page.get_by_text("Merge Tags", exact=True).click()
# Click the Merge Tags button to open the modal
self.page.get_by_text("Merge Tags", exact=True).click()
modal = self.locate_merge_modal()
modal = self.locate_merge_modal()
# Fill in the target tag
target_input = modal.get_by_label("Target tag")
target_input.fill(target_tag.name)
# Fill in the target tag
target_input = modal.get_by_label("Target tag")
target_input.fill(target_tag.name)
# Fill in the tags to merge
merge_input = modal.get_by_label("Tags to merge")
merge_input.fill(f"{merge_tag1.name} {merge_tag2.name}")
# Fill in the tags to merge
merge_input = modal.get_by_label("Tags to merge")
merge_input.fill(f"{merge_tag1.name} {merge_tag2.name}")
# Submit the form
modal.get_by_role("button", name="Merge Tags").click()
# Submit the form
modal.get_by_role("button", name="Merge Tags").click()
# Verify modal is closed
expect(modal).not_to_be_visible()
# Verify modal is closed
expect(modal).not_to_be_visible()
# Verify the success message is shown
self.verify_success_message(
'Successfully merged 2 tags (merge-tag1, merge-tag2) into "target-tag".'
)
# Verify the success message is shown
self.verify_success_message(
'Successfully merged 2 tags (merge-tag1, merge-tag2) into "target-tag".'
)
# Verify the merged tags are no longer in the list
expect(self.locate_tag_row("target-tag")).to_be_visible()
expect(self.locate_tag_row("merge-tag1")).not_to_be_visible()
expect(self.locate_tag_row("merge-tag2")).not_to_be_visible()
# Verify the merged tags are no longer in the list
expect(self.locate_tag_row("target-tag")).to_be_visible()
expect(self.locate_tag_row("merge-tag1")).not_to_be_visible()
expect(self.locate_tag_row("merge-tag2")).not_to_be_visible()
# Verify the merge tags were deleted
self.assertEqual(
@@ -217,44 +212,43 @@ class TagManagementE2ETestCase(LinkdingE2ETestCase):
target_tag = self.setup_tag(name="target-tag")
merge_tag = self.setup_tag(name="merge-tag")
with sync_playwright() as p:
self.open(reverse("linkding:tags.index"), p)
self.open(reverse("linkding:tags.index"))
# Click the Merge Tags button to open the modal
self.page.get_by_text("Merge Tags", exact=True).click()
# Click the Merge Tags button to open the modal
self.page.get_by_text("Merge Tags", exact=True).click()
modal = self.locate_merge_modal()
modal = self.locate_merge_modal()
# Submit with empty values
modal.get_by_role("button", name="Merge Tags").click()
# Submit with empty values
modal.get_by_role("button", name="Merge Tags").click()
# Verify the errors are shown
expect(modal.get_by_text("This field is required").first).to_be_visible()
# Verify the errors are shown
expect(modal.get_by_text("This field is required").first).to_be_visible()
# Fill in non-existent target tag
target_input = modal.get_by_label("Target tag")
target_input.fill("nonexistent-tag")
# Fill in non-existent target tag
target_input = modal.get_by_label("Target tag")
target_input.fill("nonexistent-tag")
merge_input = modal.get_by_label("Tags to merge")
merge_input.fill(merge_tag.name)
merge_input = modal.get_by_label("Tags to merge")
merge_input.fill(merge_tag.name)
modal.get_by_role("button", name="Merge Tags").click()
modal.get_by_role("button", name="Merge Tags").click()
# Verify error for non-existent target tag
expect(
modal.get_by_text('Tag "nonexistent-tag" does not exist')
).to_be_visible()
# Verify error for non-existent target tag
expect(
modal.get_by_text('Tag "nonexistent-tag" does not exist')
).to_be_visible()
# Fill in valid target but target tag in merge tags
target_input.fill(target_tag.name)
merge_input.fill(target_tag.name)
# Fill in valid target but target tag in merge tags
target_input.fill(target_tag.name)
merge_input.fill(target_tag.name)
modal.get_by_role("button", name="Merge Tags").click()
modal.get_by_role("button", name="Merge Tags").click()
# Verify error for target tag in merge tags
expect(
modal.get_by_text("The target tag cannot be selected for merging")
).to_be_visible()
# Verify error for target tag in merge tags
expect(
modal.get_by_text("The target tag cannot be selected for merging")
).to_be_visible()
# Verify no tags were deleted
self.assertEqual(

View File

@@ -1,18 +1,66 @@
import os
from django.contrib.staticfiles.testing import LiveServerTestCase
from playwright.sync_api import BrowserContext, Playwright, Page
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 setup_browser(self, playwright) -> BrowserContext:
browser = playwright.chromium.launch(headless=True)
context = browser.new_context()
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(
[
{
@@ -25,14 +73,20 @@ class LinkdingE2ETestCase(LiveServerTestCase, BookmarkFactoryMixin):
)
return context
def open(self, url: str, playwright: Playwright) -> Page:
browser = self.setup_browser(playwright)
self.page = browser.new_page()
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

View File

@@ -1,8 +0,0 @@
# Example setup for OIDC with Zitadel
export LD_ENABLE_OIDC=True
export OIDC_USE_PKCE=True
export OIDC_OP_AUTHORIZATION_ENDPOINT=http://localhost:8080/oauth/v2/authorize
export OIDC_OP_TOKEN_ENDPOINT=http://localhost:8080/oauth/v2/token
export OIDC_OP_USER_ENDPOINT=http://localhost:8080/oidc/v1/userinfo
export OIDC_OP_JWKS_ENDPOINT=http://localhost:8080/oauth/v2/keys
export OIDC_RP_CLIENT_ID=<client-id>

View File

@@ -1,12 +0,0 @@
#!/usr/bin/env bash
# Make sure Chromium is installed
uv run playwright install chromium
# Test server loads assets from static folder, so make sure files there are up-to-date
rm -rf static
npm run build
uv run manage.py collectstatic
# Run E2E tests
uv run manage.py test bookmarks.tests_e2e --pattern="e2e_test_*.py"

View File

@@ -23,7 +23,8 @@ export LD_DB_ENGINE=postgres
export LD_DB_USER=linkding
export LD_DB_PASSWORD=linkding
./scripts/test.sh
make test
make e2e
# Remove postgres container
docker rm -f linkding-postgres-test || true

View File

@@ -1 +0,0 @@
uv run manage.py test bookmarks.tests

View File

@@ -1,2 +0,0 @@
./scripts/test-unit.sh
./scripts/test-e2e.sh