linkding/bookmarks/tests/test_bookmark_details_modal.py
Sascha Ißbrücker 9c48085829
Add bookmark details view (#665)
* Experiment with bookmark details

* Add basic tests

* Refactor details into modal

* Implement edit and delete button

* Remove slide down animation

* Add fallback details view

* Add status actions

* Improve dark theme

* Improve return URLs

* Make bookmark details sharable

* Fix E2E tests
2024-03-29 12:37:20 +01:00

562 lines
20 KiB
Python

from django.test import TestCase
from django.urls import reverse
from django.utils import formats
from bookmarks.models import UserProfile
from bookmarks.tests.helpers import BookmarkFactoryMixin, HtmlTestMixin
class BookmarkDetailsModalTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
def setUp(self):
user = self.get_or_create_test_user()
self.client.force_login(user)
def get_base_url(self, bookmark):
return reverse("bookmarks:details_modal", args=[bookmark.id])
def get_details(self, bookmark, return_url=""):
url = self.get_base_url(bookmark)
if return_url:
url += f"?return_url={return_url}"
response = self.client.get(url)
soup = self.make_soup(response.content)
return soup
def find_section(self, soup, section_name):
dt = soup.find("dt", string=section_name)
dd = dt.find_next_sibling("dd") if dt else None
return dd
def get_section(self, soup, section_name):
dd = self.find_section(soup, section_name)
self.assertIsNotNone(dd)
return dd
def find_weblink(self, soup, url):
return soup.find("a", {"class": "weblink", "href": url})
def test_access(self):
# own bookmark
bookmark = self.setup_bookmark()
response = self.client.get(
reverse("bookmarks:details_modal", args=[bookmark.id])
)
self.assertEqual(response.status_code, 200)
# other user's bookmark
other_user = self.setup_user()
bookmark = self.setup_bookmark(user=other_user)
response = self.client.get(
reverse("bookmarks:details_modal", args=[bookmark.id])
)
self.assertEqual(response.status_code, 404)
# non-existent bookmark
response = self.client.get(reverse("bookmarks:details_modal", args=[9999]))
self.assertEqual(response.status_code, 404)
# guest user
self.client.logout()
response = self.client.get(
reverse("bookmarks:details_modal", args=[bookmark.id])
)
self.assertEqual(response.status_code, 404)
def test_access_with_sharing(self):
# shared bookmark, sharing disabled
other_user = self.setup_user()
bookmark = self.setup_bookmark(shared=True, user=other_user)
response = self.client.get(
reverse("bookmarks:details_modal", args=[bookmark.id])
)
self.assertEqual(response.status_code, 404)
# shared bookmark, sharing enabled
profile = other_user.profile
profile.enable_sharing = True
profile.save()
response = self.client.get(
reverse("bookmarks:details_modal", args=[bookmark.id])
)
self.assertEqual(response.status_code, 200)
# shared bookmark, guest user, no public sharing
self.client.logout()
response = self.client.get(
reverse("bookmarks:details_modal", args=[bookmark.id])
)
self.assertEqual(response.status_code, 404)
# shared bookmark, guest user, public sharing
profile.enable_public_sharing = True
profile.save()
response = self.client.get(
reverse("bookmarks:details_modal", args=[bookmark.id])
)
self.assertEqual(response.status_code, 200)
def test_displays_title(self):
# with title
bookmark = self.setup_bookmark(title="Test title")
soup = self.get_details(bookmark)
title = soup.find("h2")
self.assertIsNotNone(title)
self.assertEqual(title.text.strip(), bookmark.title)
# with website title
bookmark = self.setup_bookmark(title="", website_title="Website title")
soup = self.get_details(bookmark)
title = soup.find("h2")
self.assertIsNotNone(title)
self.assertEqual(title.text.strip(), bookmark.website_title)
# with URL only
bookmark = self.setup_bookmark(title="", website_title="")
soup = self.get_details(bookmark)
title = soup.find("h2")
self.assertIsNotNone(title)
self.assertEqual(title.text.strip(), bookmark.url)
def test_website_link(self):
# basics
bookmark = self.setup_bookmark()
soup = self.get_details(bookmark)
link = self.find_weblink(soup, bookmark.url)
self.assertIsNotNone(link)
self.assertEqual(link["href"], bookmark.url)
self.assertEqual(link.text.strip(), bookmark.url)
# favicons disabled
bookmark = self.setup_bookmark(favicon_file="example.png")
soup = self.get_details(bookmark)
link = self.find_weblink(soup, bookmark.url)
image = link.select_one("img")
self.assertIsNone(image)
# favicons enabled, no favicon
profile = self.get_or_create_test_user().profile
profile.enable_favicons = True
profile.save()
bookmark = self.setup_bookmark(favicon_file="")
soup = self.get_details(bookmark)
link = self.find_weblink(soup, bookmark.url)
image = link.select_one("img")
self.assertIsNone(image)
# favicons enabled, favicon present
bookmark = self.setup_bookmark(favicon_file="example.png")
soup = self.get_details(bookmark)
link = self.find_weblink(soup, bookmark.url)
image = link.select_one("img")
self.assertIsNotNone(image)
self.assertEqual(image["src"], "/static/example.png")
def test_internet_archive_link(self):
# without snapshot url
bookmark = self.setup_bookmark()
soup = self.get_details(bookmark)
link = self.find_weblink(soup, bookmark.web_archive_snapshot_url)
self.assertIsNone(link)
# with snapshot url
bookmark = self.setup_bookmark(web_archive_snapshot_url="https://example.com/")
soup = self.get_details(bookmark)
link = self.find_weblink(soup, bookmark.web_archive_snapshot_url)
self.assertIsNotNone(link)
self.assertEqual(link["href"], bookmark.web_archive_snapshot_url)
self.assertEqual(link.text.strip(), "View on Internet Archive")
# favicons disabled
bookmark = self.setup_bookmark(
web_archive_snapshot_url="https://example.com/", favicon_file="example.png"
)
soup = self.get_details(bookmark)
link = self.find_weblink(soup, bookmark.web_archive_snapshot_url)
image = link.select_one("svg")
self.assertIsNone(image)
# favicons enabled, no favicon
profile = self.get_or_create_test_user().profile
profile.enable_favicons = True
profile.save()
bookmark = self.setup_bookmark(
web_archive_snapshot_url="https://example.com/", favicon_file=""
)
soup = self.get_details(bookmark)
link = self.find_weblink(soup, bookmark.web_archive_snapshot_url)
image = link.select_one("svg")
self.assertIsNone(image)
# favicons enabled, favicon present
bookmark = self.setup_bookmark(
web_archive_snapshot_url="https://example.com/", favicon_file="example.png"
)
soup = self.get_details(bookmark)
link = self.find_weblink(soup, bookmark.web_archive_snapshot_url)
image = link.select_one("svg")
self.assertIsNotNone(image)
def test_weblinks_respect_target_setting(self):
bookmark = self.setup_bookmark(web_archive_snapshot_url="https://example.com/")
# target blank
profile = self.get_or_create_test_user().profile
profile.bookmark_link_target = UserProfile.BOOKMARK_LINK_TARGET_BLANK
profile.save()
soup = self.get_details(bookmark)
website_link = self.find_weblink(soup, bookmark.url)
self.assertIsNotNone(website_link)
self.assertEqual(website_link["target"], UserProfile.BOOKMARK_LINK_TARGET_BLANK)
web_archive_link = self.find_weblink(soup, bookmark.web_archive_snapshot_url)
self.assertIsNotNone(web_archive_link)
self.assertEqual(
web_archive_link["target"], UserProfile.BOOKMARK_LINK_TARGET_BLANK
)
# target self
profile.bookmark_link_target = UserProfile.BOOKMARK_LINK_TARGET_SELF
profile.save()
soup = self.get_details(bookmark)
website_link = self.find_weblink(soup, bookmark.url)
self.assertIsNotNone(website_link)
self.assertEqual(website_link["target"], UserProfile.BOOKMARK_LINK_TARGET_SELF)
web_archive_link = self.find_weblink(soup, bookmark.web_archive_snapshot_url)
self.assertIsNotNone(web_archive_link)
self.assertEqual(
web_archive_link["target"], UserProfile.BOOKMARK_LINK_TARGET_SELF
)
def test_status(self):
# renders form
bookmark = self.setup_bookmark()
soup = self.get_details(bookmark)
section = self.get_section(soup, "Status")
form = section.find("form")
self.assertIsNotNone(form)
self.assertEqual(
form["action"], reverse("bookmarks:details", args=[bookmark.id])
)
self.assertEqual(form["method"], "post")
# sharing disabled
bookmark = self.setup_bookmark()
soup = self.get_details(bookmark)
section = self.get_section(soup, "Status")
archived = section.find("input", {"type": "checkbox", "name": "is_archived"})
self.assertIsNotNone(archived)
unread = section.find("input", {"type": "checkbox", "name": "unread"})
self.assertIsNotNone(unread)
shared = section.find("input", {"type": "checkbox", "name": "shared"})
self.assertIsNone(shared)
# sharing enabled
profile = self.get_or_create_test_user().profile
profile.enable_sharing = True
profile.save()
bookmark = self.setup_bookmark()
soup = self.get_details(bookmark)
section = self.get_section(soup, "Status")
archived = section.find("input", {"type": "checkbox", "name": "is_archived"})
self.assertIsNotNone(archived)
unread = section.find("input", {"type": "checkbox", "name": "unread"})
self.assertIsNotNone(unread)
shared = section.find("input", {"type": "checkbox", "name": "shared"})
self.assertIsNotNone(shared)
# unchecked
bookmark = self.setup_bookmark()
soup = self.get_details(bookmark)
section = self.get_section(soup, "Status")
archived = section.find("input", {"type": "checkbox", "name": "is_archived"})
self.assertFalse(archived.has_attr("checked"))
unread = section.find("input", {"type": "checkbox", "name": "unread"})
self.assertFalse(unread.has_attr("checked"))
shared = section.find("input", {"type": "checkbox", "name": "shared"})
self.assertFalse(shared.has_attr("checked"))
# checked
bookmark = self.setup_bookmark(is_archived=True, unread=True, shared=True)
soup = self.get_details(bookmark)
section = self.get_section(soup, "Status")
archived = section.find("input", {"type": "checkbox", "name": "is_archived"})
self.assertTrue(archived.has_attr("checked"))
unread = section.find("input", {"type": "checkbox", "name": "unread"})
self.assertTrue(unread.has_attr("checked"))
shared = section.find("input", {"type": "checkbox", "name": "shared"})
self.assertTrue(shared.has_attr("checked"))
def test_status_visibility(self):
# own bookmark
bookmark = self.setup_bookmark()
soup = self.get_details(bookmark)
section = self.find_section(soup, "Status")
form_action = reverse("bookmarks:details", args=[bookmark.id])
form = soup.find("form", {"action": form_action})
self.assertIsNotNone(section)
self.assertIsNotNone(form)
# other user's bookmark
other_user = self.setup_user(enable_sharing=True)
bookmark = self.setup_bookmark(user=other_user, shared=True)
soup = self.get_details(bookmark)
section = self.find_section(soup, "Status")
form_action = reverse("bookmarks:details", args=[bookmark.id])
form = soup.find("form", {"action": form_action})
self.assertIsNone(section)
self.assertIsNone(form)
# guest user
self.client.logout()
bookmark = self.setup_bookmark(user=other_user, shared=True)
soup = self.get_details(bookmark)
section = self.find_section(soup, "Status")
form_action = reverse("bookmarks:details", args=[bookmark.id])
form = soup.find("form", {"action": form_action})
self.assertIsNone(section)
self.assertIsNone(form)
def test_status_update(self):
bookmark = self.setup_bookmark()
# update status
response = self.client.post(
self.get_base_url(bookmark),
{"is_archived": "on", "unread": "on", "shared": "on"},
)
self.assertEqual(response.status_code, 302)
bookmark.refresh_from_db()
self.assertTrue(bookmark.is_archived)
self.assertTrue(bookmark.unread)
self.assertTrue(bookmark.shared)
# update individual status
response = self.client.post(
self.get_base_url(bookmark),
{"is_archived": "", "unread": "on", "shared": ""},
)
self.assertEqual(response.status_code, 302)
bookmark.refresh_from_db()
self.assertFalse(bookmark.is_archived)
self.assertTrue(bookmark.unread)
self.assertFalse(bookmark.shared)
def test_status_update_access(self):
# no sharing
other_user = self.setup_user()
bookmark = self.setup_bookmark(user=other_user)
response = self.client.post(
self.get_base_url(bookmark),
{"is_archived": "on", "unread": "on", "shared": "on"},
)
self.assertEqual(response.status_code, 404)
# shared, sharing disabled
bookmark = self.setup_bookmark(user=other_user, shared=True)
response = self.client.post(
self.get_base_url(bookmark),
{"is_archived": "on", "unread": "on", "shared": "on"},
)
self.assertEqual(response.status_code, 404)
# shared, sharing enabled
bookmark = self.setup_bookmark(user=other_user, shared=True)
profile = other_user.profile
profile.enable_sharing = True
profile.save()
response = self.client.post(
self.get_base_url(bookmark),
{"is_archived": "on", "unread": "on", "shared": "on"},
)
self.assertEqual(response.status_code, 404)
# shared, public sharing enabled
bookmark = self.setup_bookmark(user=other_user, shared=True)
profile = other_user.profile
profile.enable_public_sharing = True
profile.save()
response = self.client.post(
self.get_base_url(bookmark),
{"is_archived": "on", "unread": "on", "shared": "on"},
)
self.assertEqual(response.status_code, 404)
# guest user
self.client.logout()
bookmark = self.setup_bookmark(user=other_user, shared=True)
response = self.client.post(
self.get_base_url(bookmark),
{"is_archived": "on", "unread": "on", "shared": "on"},
)
self.assertEqual(response.status_code, 404)
def test_date_added(self):
bookmark = self.setup_bookmark()
soup = self.get_details(bookmark)
section = self.get_section(soup, "Date added")
expected_date = formats.date_format(bookmark.date_added, "DATETIME_FORMAT")
date = section.find("span", string=expected_date)
self.assertIsNotNone(date)
def test_tags(self):
# without tags
bookmark = self.setup_bookmark()
soup = self.get_details(bookmark)
section = self.find_section(soup, "Tags")
self.assertIsNone(section)
# with tags
bookmark = self.setup_bookmark(tags=[self.setup_tag(), self.setup_tag()])
soup = self.get_details(bookmark)
section = self.get_section(soup, "Tags")
for tag in bookmark.tags.all():
tag_link = section.find("a", string=f"#{tag.name}")
self.assertIsNotNone(tag_link)
expected_url = reverse("bookmarks:index") + f"?q=%23{tag.name}"
self.assertEqual(tag_link["href"], expected_url)
def test_description(self):
# without description
bookmark = self.setup_bookmark(description="", website_description="")
soup = self.get_details(bookmark)
section = self.find_section(soup, "Description")
self.assertIsNone(section)
# with description
bookmark = self.setup_bookmark(description="Test description")
soup = self.get_details(bookmark)
section = self.get_section(soup, "Description")
self.assertEqual(section.text.strip(), bookmark.description)
# with website description
bookmark = self.setup_bookmark(
description="", website_description="Website description"
)
soup = self.get_details(bookmark)
section = self.get_section(soup, "Description")
self.assertEqual(section.text.strip(), bookmark.website_description)
def test_notes(self):
# without notes
bookmark = self.setup_bookmark()
soup = self.get_details(bookmark)
section = self.find_section(soup, "Notes")
self.assertIsNone(section)
# with notes
bookmark = self.setup_bookmark(notes="Test notes")
soup = self.get_details(bookmark)
section = self.get_section(soup, "Notes")
self.assertEqual(section.decode_contents(), "<p>Test notes</p>")
def test_edit_link(self):
bookmark = self.setup_bookmark()
# with default return URL
soup = self.get_details(bookmark)
edit_link = soup.find("a", string="Edit")
self.assertIsNotNone(edit_link)
details_url = reverse("bookmarks:details", args=[bookmark.id])
expected_url = (
reverse("bookmarks:edit", args=[bookmark.id]) + "?return_url=" + details_url
)
self.assertEqual(edit_link["href"], expected_url)
# with custom return URL
soup = self.get_details(bookmark, return_url="/custom")
edit_link = soup.find("a", string="Edit")
self.assertIsNotNone(edit_link)
expected_url = (
reverse("bookmarks:edit", args=[bookmark.id]) + "?return_url=/custom"
)
self.assertEqual(edit_link["href"], expected_url)
def test_delete_button(self):
bookmark = self.setup_bookmark()
# basics
soup = self.get_details(bookmark)
delete_button = soup.find("button", {"type": "submit", "name": "remove"})
self.assertIsNotNone(delete_button)
self.assertEqual(delete_button.text.strip(), "Delete...")
self.assertEqual(delete_button["value"], str(bookmark.id))
form = delete_button.find_parent("form")
self.assertIsNotNone(form)
expected_url = reverse("bookmarks:index.action") + f"?return_url=/bookmarks"
self.assertEqual(form["action"], expected_url)
# with custom return URL
soup = self.get_details(bookmark, return_url="/custom")
delete_button = soup.find("button", {"type": "submit", "name": "remove"})
form = delete_button.find_parent("form")
expected_url = reverse("bookmarks:index.action") + f"?return_url=/custom"
self.assertEqual(form["action"], expected_url)
def test_actions_visibility(self):
# with sharing
other_user = self.setup_user(enable_sharing=True)
bookmark = self.setup_bookmark(user=other_user, shared=True)
soup = self.get_details(bookmark)
edit_link = soup.find("a", string="Edit")
delete_button = soup.find("button", {"type": "submit", "name": "remove"})
self.assertIsNone(edit_link)
self.assertIsNone(delete_button)
# with public sharing
profile = other_user.profile
profile.enable_public_sharing = True
profile.save()
bookmark = self.setup_bookmark(user=other_user, shared=True)
soup = self.get_details(bookmark)
edit_link = soup.find("a", string="Edit")
delete_button = soup.find("button", {"type": "submit", "name": "remove"})
self.assertIsNone(edit_link)
self.assertIsNone(delete_button)
# guest user
self.client.logout()
bookmark = self.setup_bookmark(user=other_user, shared=True)
soup = self.get_details(bookmark)
edit_link = soup.find("a", string="Edit")
delete_button = soup.find("button", {"type": "submit", "name": "remove"})
self.assertIsNone(edit_link)
self.assertIsNone(delete_button)