158 lines
7.0 KiB
Python
158 lines
7.0 KiB
Python
from django.contrib.auth import get_user_model
|
|
from django.test import TestCase
|
|
from django.urls import reverse
|
|
|
|
from .models import Item, Tag, UserPreference
|
|
from .templatetags.markdown_extras import markdown
|
|
from .views import parse_due_command, parse_quick_content
|
|
|
|
|
|
class ItemMetadataTests(TestCase):
|
|
def setUp(self):
|
|
self.user = get_user_model().objects.create_user(username='tester', password='secret')
|
|
|
|
def test_code_sections_are_ignored_when_extracting_tags(self):
|
|
item = Item.objects.create(
|
|
owner=self.user,
|
|
content='Outside #real\n```python\n# fenced\n```\nInline `#inline` text',
|
|
)
|
|
item.sync_metadata()
|
|
self.assertEqual(list(item.tags.values_list('name', flat=True)), ['real'])
|
|
self.assertFalse(Tag.objects.filter(name__in=['fenced', 'inline']).exists())
|
|
|
|
def test_unused_tags_are_removed_after_sync_and_delete(self):
|
|
item = Item.objects.create(owner=self.user, content='#old')
|
|
item.sync_metadata()
|
|
item.content = '#new'
|
|
item.save()
|
|
item.sync_metadata()
|
|
self.assertFalse(Tag.objects.filter(name='old').exists())
|
|
self.assertTrue(Tag.objects.filter(name='new').exists())
|
|
|
|
item.delete()
|
|
self.assertFalse(Tag.objects.filter(name='new').exists())
|
|
|
|
|
|
class DefaultItemKindTests(TestCase):
|
|
def setUp(self):
|
|
self.user = get_user_model().objects.create_user(username='tester', password='secret')
|
|
self.client.force_login(self.user)
|
|
self.preferences = UserPreference.objects.create(user=self.user, default_item_kind=Item.Kind.TODO)
|
|
|
|
def test_quick_create_uses_user_default(self):
|
|
response = self.client.post(reverse('index'), {'content': 'Default task'})
|
|
self.assertRedirects(response, reverse('index'))
|
|
self.assertEqual(Item.objects.get().kind, Item.Kind.TODO)
|
|
|
|
def test_explicit_command_overrides_default(self):
|
|
kind, _, content, _ = parse_quick_content('/note Explicit note', Item.Kind.TODO)
|
|
self.assertEqual(kind, Item.Kind.NOTE)
|
|
self.assertEqual(content, 'Explicit note')
|
|
|
|
def test_new_item_editor_uses_user_default(self):
|
|
response = self.client.get(reverse('new_item'))
|
|
self.assertEqual(response.context['requested_kind'], Item.Kind.TODO)
|
|
|
|
|
|
class DueDateCommandTests(TestCase):
|
|
def setUp(self):
|
|
self.user = get_user_model().objects.create_user(username='dates', password='secret')
|
|
self.client.force_login(self.user)
|
|
|
|
def test_datum_command_is_removed_and_parsed(self):
|
|
cleaned, due_at, found = parse_due_command('/todo Call back /datum 20.10.26 14:30 #work')
|
|
self.assertTrue(found)
|
|
self.assertEqual(cleaned, '/todo Call back #work')
|
|
self.assertEqual((due_at.year, due_at.month, due_at.day, due_at.hour, due_at.minute), (2026, 10, 20, 14, 30))
|
|
|
|
def test_relative_command_sets_due_date_on_quick_create(self):
|
|
response = self.client.post(reverse('index'), {'content': '/todo Call back /morgen'})
|
|
self.assertRedirects(response, reverse('index'))
|
|
item = Item.objects.get()
|
|
self.assertNotIn('/morgen', item.content)
|
|
self.assertIsNotNone(item.due_at)
|
|
|
|
def test_type_and_date_commands_are_available_in_menu(self):
|
|
response = self.client.get(reverse('index'))
|
|
self.assertContains(response, "{token:'/note'")
|
|
self.assertContains(response, "{token:'/datum'")
|
|
self.assertContains(response, 'queryStart')
|
|
self.assertContains(response, "group:'Typ'")
|
|
|
|
|
|
class SlashMenuContextTests(TestCase):
|
|
def test_type_command_only_counts_at_start(self):
|
|
leading_kind, _, leading_content, _ = parse_quick_content('/todo Leading')
|
|
later_kind, _, later_content, _ = parse_quick_content('Text /todo later')
|
|
self.assertEqual(leading_kind, Item.Kind.TODO)
|
|
self.assertEqual(leading_content, 'Leading')
|
|
self.assertEqual(later_kind, Item.Kind.NOTE)
|
|
self.assertEqual(later_content, 'Text /todo later')
|
|
|
|
|
|
class MarkdownHeadingTests(TestCase):
|
|
def test_tag_at_line_start_is_not_a_heading(self):
|
|
rendered = str(markdown('#project starts here'))
|
|
self.assertIn('<p>#project starts here</p>', rendered)
|
|
self.assertNotIn('<h1>', rendered)
|
|
|
|
def test_heading_with_space_remains_a_heading(self):
|
|
self.assertIn('<h1>Project</h1>', str(markdown('# Project')))
|
|
|
|
def test_hash_in_fenced_code_is_unchanged(self):
|
|
rendered = str(markdown('```python\n# comment\n```'))
|
|
self.assertIn('# comment', rendered)
|
|
self.assertNotIn('\\# comment', rendered)
|
|
|
|
def test_underline_is_allowed(self):
|
|
self.assertIn('<u>underlined</u>', str(markdown('<u>underlined</u>')))
|
|
|
|
|
|
class FormattingToolbarTests(TestCase):
|
|
def test_toolbar_and_shortcuts_are_available(self):
|
|
user = get_user_model().objects.create_user(username='formatting', password='secret')
|
|
self.client.force_login(user)
|
|
response = self.client.get(reverse('index'))
|
|
self.assertContains(response, 'class="format-toolbar')
|
|
self.assertContains(response, "formatSelection('bold')")
|
|
self.assertContains(response, "u:'underline'")
|
|
|
|
|
|
class NavigationAndLayoutTests(TestCase):
|
|
def setUp(self):
|
|
self.user = get_user_model().objects.create_user(username='navigation', password='secret')
|
|
self.preferences = UserPreference.objects.create(user=self.user)
|
|
self.client.force_login(self.user)
|
|
|
|
def test_desktop_navigation_contains_overview_hidden_links_and_kanban(self):
|
|
self.preferences.show_notes = False
|
|
self.preferences.save(update_fields=['show_notes'])
|
|
response = self.client.get(reverse('index'))
|
|
self.assertContains(response, 'd-none d-md-flex')
|
|
self.assertContains(response, f'{reverse("index")}?kind=note')
|
|
self.assertContains(response, reverse('kanban_list'))
|
|
|
|
def test_preference_selects_are_stacked(self):
|
|
response = self.client.get(reverse('settings'))
|
|
preferences = response.content.decode().split('id="preferences"', 1)[1].split('name="due_first"', 1)[0]
|
|
self.assertEqual(preferences.count('<div class="col-12"><label class="form-label">'), 3)
|
|
|
|
def test_composer_menu_is_before_toolbar_and_file_controls(self):
|
|
response = self.client.get(reverse('index'))
|
|
html = response.content.decode()
|
|
menu_position = html.index('composer-slash-menu')
|
|
toolbar_position = html.index('format-toolbar', menu_position)
|
|
file_position = html.index('desktop-file', toolbar_position)
|
|
self.assertLess(menu_position, toolbar_position)
|
|
self.assertLess(toolbar_position, file_position)
|
|
|
|
|
|
class CopyButtonTests(TestCase):
|
|
def test_item_has_copy_button_with_original_text(self):
|
|
user = get_user_model().objects.create_user(username='tester', password='secret')
|
|
item = Item.objects.create(owner=user, content='Text to copy')
|
|
self.client.force_login(user)
|
|
response = self.client.get(reverse('item_detail', args=[item.pk]))
|
|
self.assertContains(response, 'class="copy-item')
|
|
self.assertContains(response, 'Text to copy')
|