django-spire 0.22.4__py3-none-any.whl → 0.23.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- django_spire/auth/group/forms.py +3 -4
- django_spire/auth/group/utils.py +1 -2
- django_spire/auth/group/views/form_views.py +22 -14
- django_spire/auth/group/views/page_views.py +1 -1
- django_spire/auth/mfa/utils.py +1 -1
- django_spire/auth/permissions/consts.py +2 -1
- django_spire/auth/permissions/decorators.py +1 -2
- django_spire/auth/permissions/permissions.py +1 -3
- django_spire/comment/factories.py +1 -1
- django_spire/comment/mixins.py +1 -1
- django_spire/comment/views.py +1 -1
- django_spire/consts.py +1 -1
- django_spire/contrib/breadcrumb/breadcrumbs.py +1 -1
- django_spire/contrib/form/confirmation_forms.py +1 -1
- django_spire/contrib/form/utils.py +26 -16
- django_spire/contrib/generic_views/modal_views.py +1 -1
- django_spire/contrib/generic_views/portal_views.py +18 -55
- django_spire/contrib/ordering/validators.py +4 -8
- django_spire/contrib/queryset/enums.py +2 -3
- django_spire/contrib/queryset/mixins.py +17 -6
- django_spire/contrib/session/controller.py +0 -1
- django_spire/core/context_processors.py +1 -1
- django_spire/core/management/commands/spire_startapp_pkg/user_input.py +1 -1
- django_spire/core/middleware/maintenance.py +2 -1
- django_spire/core/middleware.py +2 -1
- django_spire/core/redirect/generic_redirect.py +1 -1
- django_spire/core/redirect/safe_redirect.py +2 -1
- django_spire/core/shortcuts.py +4 -2
- django_spire/core/table/__init__.py +0 -0
- django_spire/core/table/enums.py +18 -0
- django_spire/core/templates/django_spire/card/infinite_scroll_card.html +3 -137
- django_spire/core/templates/django_spire/container/infinite_scroll_container.html +64 -0
- django_spire/core/templates/django_spire/infinite_scroll/base.html +348 -0
- django_spire/core/templates/django_spire/infinite_scroll/element/footer.html +11 -0
- django_spire/core/templates/django_spire/infinite_scroll/scroll.html +142 -0
- django_spire/core/templates/django_spire/item/infinite_scroll_item.html +33 -0
- django_spire/core/templates/django_spire/lazy_tab/element/lazy_tab_section_element.html +19 -0
- django_spire/core/templates/django_spire/lazy_tab/element/lazy_tab_trigger_element.html +15 -0
- django_spire/core/templates/django_spire/lazy_tab/lazy_tab.html +157 -0
- django_spire/core/templates/django_spire/page/infinite_scroll_list_page.html +7 -0
- django_spire/core/templates/django_spire/table/base.html +185 -373
- django_spire/core/templates/django_spire/table/element/footer.html +7 -15
- django_spire/core/templates/django_spire/table/element/header.html +1 -1
- django_spire/core/templates/django_spire/table/element/row.html +15 -7
- django_spire/core/templatetags/spire_core_tags.py +1 -2
- django_spire/file/fields.py +1 -1
- django_spire/file/interfaces.py +1 -1
- django_spire/file/views.py +1 -1
- django_spire/history/activity/utils.py +1 -1
- django_spire/history/mixins.py +0 -4
- django_spire/notification/app/context_data.py +3 -1
- django_spire/notification/app/views/json_views.py +1 -1
- django_spire/notification/app/views/page_views.py +2 -1
- django_spire/notification/app/views/template_views.py +2 -2
- django_spire/profiling/middleware/profiling.py +2 -2
- django_spire/profiling/panel.py +2 -2
- django_spire/testing/__init__.py +0 -0
- django_spire/testing/playwright/__init__.py +64 -0
- django_spire/testing/playwright/components/__init__.py +45 -0
- django_spire/testing/playwright/components/accordion.py +55 -0
- django_spire/testing/playwright/components/attribute_element.py +73 -0
- django_spire/testing/playwright/components/base_session_filter_form.py +57 -0
- django_spire/testing/playwright/components/breadcrumb_element.py +56 -0
- django_spire/testing/playwright/components/card.py +102 -0
- django_spire/testing/playwright/components/dropdown.py +87 -0
- django_spire/testing/playwright/components/infinite_scroll.py +158 -0
- django_spire/testing/playwright/components/lazy_tab.py +92 -0
- django_spire/testing/playwright/components/modal.py +101 -0
- django_spire/testing/playwright/components/navigation.py +119 -0
- django_spire/testing/playwright/components/notification_bell.py +59 -0
- django_spire/testing/playwright/components/theme_selector.py +46 -0
- django_spire/testing/playwright/components/toast.py +72 -0
- django_spire/testing/playwright/fixtures.py +54 -0
- django_spire/testing/playwright/pages/__init__.py +6 -0
- django_spire/testing/playwright/pages/base.py +24 -0
- django_spire/theme/models.py +1 -1
- django_spire/theme/tests/test_context_processor.py +0 -1
- django_spire/theme/views/json_views.py +1 -1
- django_spire/theme/views/page_views.py +1 -1
- {django_spire-0.22.4.dist-info → django_spire-0.23.2.dist-info}/METADATA +4 -1
- {django_spire-0.22.4.dist-info → django_spire-0.23.2.dist-info}/RECORD +84 -54
- {django_spire-0.22.4.dist-info → django_spire-0.23.2.dist-info}/WHEEL +0 -0
- {django_spire-0.22.4.dist-info → django_spire-0.23.2.dist-info}/licenses/LICENSE.md +0 -0
- {django_spire-0.22.4.dist-info → django_spire-0.23.2.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
if TYPE_CHECKING:
|
|
6
|
+
from playwright.sync_api import Locator
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Dropdown:
|
|
10
|
+
"""Playwright component for django_spire/dropdown/dropdown.html"""
|
|
11
|
+
|
|
12
|
+
menu_selector: str = '.position-absolute.shadow-lg.card'
|
|
13
|
+
trigger_selector: str = '[x-bind="trigger"]'
|
|
14
|
+
|
|
15
|
+
def __init__(self, parent_locator: Locator) -> None:
|
|
16
|
+
self.parent = parent_locator
|
|
17
|
+
|
|
18
|
+
@property
|
|
19
|
+
def menu(self) -> Locator:
|
|
20
|
+
return self.parent.page.locator(self.menu_selector).filter(
|
|
21
|
+
has=self.parent.page.locator(':visible')
|
|
22
|
+
).first
|
|
23
|
+
|
|
24
|
+
@property
|
|
25
|
+
def trigger(self) -> Locator:
|
|
26
|
+
return self.parent.locator(self.trigger_selector)
|
|
27
|
+
|
|
28
|
+
def click_option(self, text: str) -> None:
|
|
29
|
+
self.menu.locator(f'text={text}').click()
|
|
30
|
+
|
|
31
|
+
def close(self) -> None:
|
|
32
|
+
if self.is_open():
|
|
33
|
+
self.trigger.click()
|
|
34
|
+
|
|
35
|
+
def get_option(self, text: str) -> Locator:
|
|
36
|
+
return self.menu.locator(f'text={text}')
|
|
37
|
+
|
|
38
|
+
def has_option(self, text: str) -> bool:
|
|
39
|
+
return self.get_option(text).is_visible()
|
|
40
|
+
|
|
41
|
+
def is_open(self) -> bool:
|
|
42
|
+
return self.menu.is_visible()
|
|
43
|
+
|
|
44
|
+
def open(self) -> None:
|
|
45
|
+
if not self.is_open():
|
|
46
|
+
self.trigger.click()
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class EllipsisDropdown(Dropdown):
|
|
50
|
+
"""Playwright component for django_spire/dropdown/ellipsis_dropdown.html"""
|
|
51
|
+
|
|
52
|
+
trigger_selector: str = '.bi-three-dots-vertical'
|
|
53
|
+
|
|
54
|
+
def click_delete(self) -> None:
|
|
55
|
+
self.click_option('Delete')
|
|
56
|
+
|
|
57
|
+
def click_edit(self) -> None:
|
|
58
|
+
self.click_option('Edit')
|
|
59
|
+
|
|
60
|
+
def click_view(self) -> None:
|
|
61
|
+
self.click_option('View')
|
|
62
|
+
|
|
63
|
+
def has_delete_option(self) -> bool:
|
|
64
|
+
return self.has_option('Delete')
|
|
65
|
+
|
|
66
|
+
def has_edit_option(self) -> bool:
|
|
67
|
+
return self.has_option('Edit')
|
|
68
|
+
|
|
69
|
+
def has_view_option(self) -> bool:
|
|
70
|
+
return self.has_option('View')
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class EllipsisModalDropdown(EllipsisDropdown):
|
|
74
|
+
"""
|
|
75
|
+
Playwright component for django_spire/dropdown/ellipsis_modal_dropdown.html
|
|
76
|
+
Dropdown options trigger modals via dispatch_modal_view()
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
pass
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class EllipsisTableDropdown(EllipsisDropdown):
|
|
83
|
+
"""
|
|
84
|
+
Playwright component for django_spire/dropdown/ellipsis_table_dropdown.html
|
|
85
|
+
Used in table rows, positioned start-0 instead of end-0
|
|
86
|
+
"""
|
|
87
|
+
trigger_selector: str = 'td .bi-three-dots-vertical'
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
if TYPE_CHECKING:
|
|
6
|
+
from playwright.sync_api import Locator, Page
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class InfiniteScroll:
|
|
10
|
+
"""
|
|
11
|
+
Playwright component for django_spire/infinite_scroll/base.html
|
|
12
|
+
and django_spire/infinite_scroll/scroll.html
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
def __init__(self, page: Page, container_selector: str = '[x-ref="scroll_container"]') -> None:
|
|
16
|
+
self.container_selector = container_selector
|
|
17
|
+
self.page = page
|
|
18
|
+
|
|
19
|
+
@property
|
|
20
|
+
def content_container(self) -> Locator:
|
|
21
|
+
return self.page.locator('[x-ref="content_container"]')
|
|
22
|
+
|
|
23
|
+
@property
|
|
24
|
+
def loaded_count_text(self) -> Locator:
|
|
25
|
+
return self.page.locator('[x-text="loaded_count"]')
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
def scroll_container(self) -> Locator:
|
|
29
|
+
return self.page.locator(self.container_selector)
|
|
30
|
+
|
|
31
|
+
@property
|
|
32
|
+
def spinner(self) -> Locator:
|
|
33
|
+
return self.page.locator('.spinner-border')
|
|
34
|
+
|
|
35
|
+
@property
|
|
36
|
+
def total_count_text(self) -> Locator:
|
|
37
|
+
return self.page.locator('[x-text="total_count"]')
|
|
38
|
+
|
|
39
|
+
def get_loaded_count(self) -> int:
|
|
40
|
+
return int(self.loaded_count_text.inner_text())
|
|
41
|
+
|
|
42
|
+
def get_total_count(self) -> int:
|
|
43
|
+
return int(self.total_count_text.inner_text())
|
|
44
|
+
|
|
45
|
+
def is_loading(self) -> bool:
|
|
46
|
+
return self.spinner.is_visible()
|
|
47
|
+
|
|
48
|
+
def scroll_to_bottom(self) -> None:
|
|
49
|
+
self.scroll_container.evaluate('el => el.scrollTop = el.scrollHeight')
|
|
50
|
+
|
|
51
|
+
def scroll_to_top(self) -> None:
|
|
52
|
+
self.scroll_container.evaluate('el => el.scrollTop = 0')
|
|
53
|
+
|
|
54
|
+
def wait_for_count_to_increase(self, initial_count: int, timeout: int = 5000) -> None:
|
|
55
|
+
self.page.wait_for_function(
|
|
56
|
+
f'() => parseInt(document.querySelector("[x-text=\\"loaded_count\\"]").textContent) > {initial_count}',
|
|
57
|
+
timeout=timeout
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
def wait_for_items_to_load(self) -> None:
|
|
61
|
+
self.loaded_count_text.wait_for()
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class InfiniteScrollList(InfiniteScroll):
|
|
65
|
+
"""Playwright component for infinite scroll with list items"""
|
|
66
|
+
|
|
67
|
+
item_selector: str = '[data-row-id]'
|
|
68
|
+
|
|
69
|
+
@property
|
|
70
|
+
def items(self) -> Locator:
|
|
71
|
+
return self.page.locator(self.item_selector)
|
|
72
|
+
|
|
73
|
+
def get_item(self, index: int) -> Locator:
|
|
74
|
+
return self.items.nth(index)
|
|
75
|
+
|
|
76
|
+
def get_item_count(self) -> int:
|
|
77
|
+
return self.items.count()
|
|
78
|
+
|
|
79
|
+
def get_item_ids(self) -> list[str]:
|
|
80
|
+
items = self.items
|
|
81
|
+
return [items.nth(i).get_attribute('data-row-id') for i in range(items.count())]
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class InfiniteScrollTable(InfiniteScroll):
|
|
85
|
+
"""Playwright component for django_spire/table/base.html"""
|
|
86
|
+
|
|
87
|
+
row_selector: str = 'tbody tr[data-row-id]'
|
|
88
|
+
skeleton_selector: str = '.skeleton-box'
|
|
89
|
+
|
|
90
|
+
def __init__(self, page: Page, container_selector: str = '.table-container[x-ref="scroll_container"]') -> None:
|
|
91
|
+
super().__init__(page, container_selector)
|
|
92
|
+
|
|
93
|
+
@property
|
|
94
|
+
def rows(self) -> Locator:
|
|
95
|
+
return self.page.locator(self.row_selector)
|
|
96
|
+
|
|
97
|
+
@property
|
|
98
|
+
def select_all_checkbox(self) -> Locator:
|
|
99
|
+
return self.page.locator('thead input[type="checkbox"]')
|
|
100
|
+
|
|
101
|
+
@property
|
|
102
|
+
def selected_count_text(self) -> Locator:
|
|
103
|
+
return self.page.locator('[x-text="selected_rows.size"]')
|
|
104
|
+
|
|
105
|
+
@property
|
|
106
|
+
def skeleton_rows(self) -> Locator:
|
|
107
|
+
return self.page.locator(self.skeleton_selector)
|
|
108
|
+
|
|
109
|
+
@property
|
|
110
|
+
def table(self) -> Locator:
|
|
111
|
+
return self.page.locator('table')
|
|
112
|
+
|
|
113
|
+
def click_header(self, header_text: str) -> None:
|
|
114
|
+
self.get_header(header_text).click()
|
|
115
|
+
|
|
116
|
+
def deselect_all_rows(self) -> None:
|
|
117
|
+
self.select_all_checkbox.click()
|
|
118
|
+
|
|
119
|
+
def deselect_row(self, index: int) -> None:
|
|
120
|
+
self.get_row(index).locator('input[type="checkbox"]').click()
|
|
121
|
+
|
|
122
|
+
def get_first_row_text(self) -> str:
|
|
123
|
+
return self.rows.first.inner_text()
|
|
124
|
+
|
|
125
|
+
def get_header(self, header_text: str) -> Locator:
|
|
126
|
+
return self.page.locator(f'th:has-text("{header_text}")')
|
|
127
|
+
|
|
128
|
+
def get_row(self, index: int) -> Locator:
|
|
129
|
+
return self.rows.nth(index)
|
|
130
|
+
|
|
131
|
+
def get_row_count(self) -> int:
|
|
132
|
+
return self.rows.count()
|
|
133
|
+
|
|
134
|
+
def get_selected_count(self) -> int:
|
|
135
|
+
return int(self.selected_count_text.inner_text())
|
|
136
|
+
|
|
137
|
+
def get_sort_icon(self, header_text: str) -> Locator:
|
|
138
|
+
return self.get_header(header_text).locator('i.bi')
|
|
139
|
+
|
|
140
|
+
def is_sorted_ascending(self, header_text: str) -> bool:
|
|
141
|
+
icon = self.get_sort_icon(header_text)
|
|
142
|
+
return 'bi-chevron-up' in (icon.get_attribute('class') or '')
|
|
143
|
+
|
|
144
|
+
def is_sorted_descending(self, header_text: str) -> bool:
|
|
145
|
+
icon = self.get_sort_icon(header_text)
|
|
146
|
+
return 'bi-chevron-down' in (icon.get_attribute('class') or '')
|
|
147
|
+
|
|
148
|
+
def select_all_rows(self) -> None:
|
|
149
|
+
self.select_all_checkbox.click()
|
|
150
|
+
|
|
151
|
+
def select_row(self, index: int) -> None:
|
|
152
|
+
self.get_row(index).locator('input[type="checkbox"]').click()
|
|
153
|
+
|
|
154
|
+
def wait_for_rows_to_load(self) -> None:
|
|
155
|
+
self.rows.first.wait_for()
|
|
156
|
+
|
|
157
|
+
def wait_for_table(self) -> None:
|
|
158
|
+
self.table.wait_for()
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from playwright.sync_api import Locator, Page
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class LazyTab:
|
|
12
|
+
"""Playwright component for django_spire/lazy_tab/lazy_tab.html"""
|
|
13
|
+
|
|
14
|
+
selected_class: str = 'tab-item'
|
|
15
|
+
|
|
16
|
+
def __init__(self, page: Page, container_selector: str, tab_id: str | None = None) -> None:
|
|
17
|
+
self.container_selector = container_selector
|
|
18
|
+
self.page = page
|
|
19
|
+
self.tab_id = tab_id
|
|
20
|
+
|
|
21
|
+
@property
|
|
22
|
+
def container(self) -> Locator:
|
|
23
|
+
return self.page.locator(self.container_selector)
|
|
24
|
+
|
|
25
|
+
@property
|
|
26
|
+
def sections(self) -> Locator:
|
|
27
|
+
return self.container.locator('[role="tabpanel"]')
|
|
28
|
+
|
|
29
|
+
@property
|
|
30
|
+
def triggers(self) -> Locator:
|
|
31
|
+
return self.container.locator('[role="tab"]')
|
|
32
|
+
|
|
33
|
+
def click_tab(self, index: int) -> None:
|
|
34
|
+
self.get_trigger(index).click()
|
|
35
|
+
|
|
36
|
+
def get_section(self, index: int) -> Locator:
|
|
37
|
+
return self.sections.nth(index)
|
|
38
|
+
|
|
39
|
+
def get_section_count(self) -> int:
|
|
40
|
+
return self.sections.count()
|
|
41
|
+
|
|
42
|
+
def get_trigger(self, index: int) -> Locator:
|
|
43
|
+
return self.triggers.nth(index)
|
|
44
|
+
|
|
45
|
+
def get_trigger_count(self) -> int:
|
|
46
|
+
return self.triggers.count()
|
|
47
|
+
|
|
48
|
+
def get_url_param(self) -> str | None:
|
|
49
|
+
if not self.tab_id:
|
|
50
|
+
return None
|
|
51
|
+
|
|
52
|
+
url = self.page.url
|
|
53
|
+
match = re.search(f'{self.tab_id}=([^&]+)', url)
|
|
54
|
+
|
|
55
|
+
if match:
|
|
56
|
+
return match.group(1)
|
|
57
|
+
|
|
58
|
+
return None
|
|
59
|
+
|
|
60
|
+
def get_visible_section(self) -> Locator | None:
|
|
61
|
+
for i in range(self.sections.count()):
|
|
62
|
+
section = self.sections.nth(i)
|
|
63
|
+
|
|
64
|
+
if section.is_visible():
|
|
65
|
+
return section
|
|
66
|
+
|
|
67
|
+
return None
|
|
68
|
+
|
|
69
|
+
def get_visible_section_text(self) -> str:
|
|
70
|
+
section = self.get_visible_section()
|
|
71
|
+
|
|
72
|
+
if section:
|
|
73
|
+
return section.inner_text()
|
|
74
|
+
|
|
75
|
+
return ''
|
|
76
|
+
|
|
77
|
+
def is_loading(self, index: int) -> bool:
|
|
78
|
+
section = self.get_section(index)
|
|
79
|
+
spinner = section.locator('.spinner-border')
|
|
80
|
+
return spinner.is_visible()
|
|
81
|
+
|
|
82
|
+
def is_tab_selected(self, index: int) -> bool:
|
|
83
|
+
trigger = self.get_trigger(index)
|
|
84
|
+
classes = trigger.get_attribute('class') or ''
|
|
85
|
+
return self.selected_class in classes
|
|
86
|
+
|
|
87
|
+
def wait_for_section_content(self, index: int, timeout: int = 5000) -> None:
|
|
88
|
+
section = self.get_section(index)
|
|
89
|
+
section.locator('.spinner-border').wait_for(state='hidden', timeout=timeout)
|
|
90
|
+
|
|
91
|
+
def wait_for_tabs_to_load(self) -> None:
|
|
92
|
+
self.triggers.first.wait_for()
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
if TYPE_CHECKING:
|
|
6
|
+
from playwright.sync_api import Locator, Page
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Modal:
|
|
10
|
+
"""Playwright component for django_spire/modal/modal.html"""
|
|
11
|
+
|
|
12
|
+
def __init__(self, page: Page) -> None:
|
|
13
|
+
self.page = page
|
|
14
|
+
|
|
15
|
+
@property
|
|
16
|
+
def close_button(self) -> Locator:
|
|
17
|
+
return self.modal.locator('[\\@click*="close_modal"]').first
|
|
18
|
+
|
|
19
|
+
@property
|
|
20
|
+
def content(self) -> Locator:
|
|
21
|
+
return self.modal.locator('.bg-app-layer-one.card')
|
|
22
|
+
|
|
23
|
+
@property
|
|
24
|
+
def modal(self) -> Locator:
|
|
25
|
+
return self.page.locator('[x-show="show_modal"]').last
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
def overlay(self) -> Locator:
|
|
29
|
+
return self.page.locator('[\\@click="close_modal"]').first
|
|
30
|
+
|
|
31
|
+
def close(self) -> None:
|
|
32
|
+
self.close_button.click()
|
|
33
|
+
|
|
34
|
+
def close_by_overlay(self) -> None:
|
|
35
|
+
self.overlay.click()
|
|
36
|
+
|
|
37
|
+
def is_open(self) -> bool:
|
|
38
|
+
return self.modal.is_visible()
|
|
39
|
+
|
|
40
|
+
def wait_for_close(self, timeout: int = 5000) -> None:
|
|
41
|
+
self.modal.wait_for(state='hidden', timeout=timeout)
|
|
42
|
+
|
|
43
|
+
def wait_for_open(self, timeout: int = 5000) -> None:
|
|
44
|
+
self.modal.wait_for(state='visible', timeout=timeout)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class TitleModal(Modal):
|
|
48
|
+
"""Playwright component for django_spire/modal/title_modal.html"""
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def title(self) -> Locator:
|
|
52
|
+
return self.modal.locator('.text-uppercase.text-app-secondary-dark.h6').first
|
|
53
|
+
|
|
54
|
+
def get_title_text(self) -> str:
|
|
55
|
+
return self.title.inner_text()
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class FormModal(TitleModal):
|
|
59
|
+
"""Playwright component for modals containing forms"""
|
|
60
|
+
|
|
61
|
+
@property
|
|
62
|
+
def cancel_button(self) -> Locator:
|
|
63
|
+
return self.modal.locator('button:has-text("Cancel"), [type="button"]:has-text("Cancel")').first
|
|
64
|
+
|
|
65
|
+
@property
|
|
66
|
+
def form(self) -> Locator:
|
|
67
|
+
return self.modal.locator('form')
|
|
68
|
+
|
|
69
|
+
@property
|
|
70
|
+
def submit_button(self) -> Locator:
|
|
71
|
+
return self.modal.locator('button[type="submit"], input[type="submit"]').first
|
|
72
|
+
|
|
73
|
+
def cancel(self) -> None:
|
|
74
|
+
self.cancel_button.click()
|
|
75
|
+
|
|
76
|
+
def fill_field(self, name: str, value: str) -> None:
|
|
77
|
+
self.form.locator(f'[name="{name}"]').fill(value)
|
|
78
|
+
|
|
79
|
+
def get_field_value(self, name: str) -> str:
|
|
80
|
+
return self.form.locator(f'[name="{name}"]').input_value()
|
|
81
|
+
|
|
82
|
+
def submit(self) -> None:
|
|
83
|
+
self.submit_button.click()
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class DeleteModal(TitleModal):
|
|
87
|
+
"""Playwright component for delete confirmation modals"""
|
|
88
|
+
|
|
89
|
+
@property
|
|
90
|
+
def cancel_button(self) -> Locator:
|
|
91
|
+
return self.modal.locator('button:has-text("Cancel")').first
|
|
92
|
+
|
|
93
|
+
@property
|
|
94
|
+
def confirm_button(self) -> Locator:
|
|
95
|
+
return self.modal.locator('button:has-text("Delete"), button:has-text("Confirm")').first
|
|
96
|
+
|
|
97
|
+
def cancel(self) -> None:
|
|
98
|
+
self.cancel_button.click()
|
|
99
|
+
|
|
100
|
+
def confirm(self) -> None:
|
|
101
|
+
self.confirm_button.click()
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
if TYPE_CHECKING:
|
|
6
|
+
from playwright.sync_api import Locator, Page
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class SideNavigation:
|
|
10
|
+
"""Playwright component for django_spire/navigation/side_navigation.html"""
|
|
11
|
+
|
|
12
|
+
def __init__(self, page: Page) -> None:
|
|
13
|
+
self.page = page
|
|
14
|
+
|
|
15
|
+
@property
|
|
16
|
+
def container(self) -> Locator:
|
|
17
|
+
return self.page.locator('.side-navigation')
|
|
18
|
+
|
|
19
|
+
@property
|
|
20
|
+
def links(self) -> Locator:
|
|
21
|
+
return self.container.locator('a.nav-link')
|
|
22
|
+
|
|
23
|
+
@property
|
|
24
|
+
def scroll_container(self) -> Locator:
|
|
25
|
+
return self.container.locator('[x-ref="scroll_container"]')
|
|
26
|
+
|
|
27
|
+
def click_link(self, text: str) -> None:
|
|
28
|
+
self.get_link_by_text(text).click()
|
|
29
|
+
|
|
30
|
+
def get_link_by_text(self, text: str) -> Locator:
|
|
31
|
+
return self.container.locator(f'a.nav-link:has-text("{text}")')
|
|
32
|
+
|
|
33
|
+
def get_link_count(self) -> int:
|
|
34
|
+
return self.links.count()
|
|
35
|
+
|
|
36
|
+
def get_link_texts(self) -> list[str]:
|
|
37
|
+
return [self.links.nth(i).inner_text() for i in range(self.get_link_count())]
|
|
38
|
+
|
|
39
|
+
def has_link(self, text: str) -> bool:
|
|
40
|
+
return self.get_link_by_text(text).count() > 0
|
|
41
|
+
|
|
42
|
+
def is_visible(self) -> bool:
|
|
43
|
+
return self.container.is_visible()
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class TopNavigation:
|
|
47
|
+
"""Playwright component for django_spire/navigation/top_navigation.html"""
|
|
48
|
+
|
|
49
|
+
def __init__(self, page: Page) -> None:
|
|
50
|
+
self.page = page
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
def container(self) -> Locator:
|
|
54
|
+
return self.page.locator('.top-navigation')
|
|
55
|
+
|
|
56
|
+
@property
|
|
57
|
+
def notification_bell(self) -> Locator:
|
|
58
|
+
return self.container.locator('.bi-bell')
|
|
59
|
+
|
|
60
|
+
@property
|
|
61
|
+
def theme_selector(self) -> Locator:
|
|
62
|
+
return self.container.locator('.bi-sun-fill, .bi-moon-fill')
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def title(self) -> Locator:
|
|
66
|
+
return self.container.locator('.fs-5.fw-bold, .fs-md-1.fw-bold').first
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
def user_menu(self) -> Locator:
|
|
70
|
+
return self.container.locator('.bi-person-circle')
|
|
71
|
+
|
|
72
|
+
def click_notification_bell(self) -> None:
|
|
73
|
+
self.notification_bell.click()
|
|
74
|
+
|
|
75
|
+
def click_theme_selector(self) -> None:
|
|
76
|
+
self.theme_selector.click()
|
|
77
|
+
|
|
78
|
+
def click_user_menu(self) -> None:
|
|
79
|
+
self.user_menu.click()
|
|
80
|
+
|
|
81
|
+
def get_title_text(self) -> str:
|
|
82
|
+
return self.title.inner_text()
|
|
83
|
+
|
|
84
|
+
def is_visible(self) -> bool:
|
|
85
|
+
return self.container.is_visible()
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class UserMenu:
|
|
89
|
+
"""Playwright component for user dropdown menu in top navigation"""
|
|
90
|
+
|
|
91
|
+
def __init__(self, page: Page) -> None:
|
|
92
|
+
self.page = page
|
|
93
|
+
|
|
94
|
+
@property
|
|
95
|
+
def menu(self) -> Locator:
|
|
96
|
+
return self.page.locator('.dropdown-menu[aria-labelledby="dropdownMenuButton1"]')
|
|
97
|
+
|
|
98
|
+
@property
|
|
99
|
+
def trigger(self) -> Locator:
|
|
100
|
+
return self.page.locator('#dropdownMenuButton1')
|
|
101
|
+
|
|
102
|
+
def click_admin_panel(self) -> None:
|
|
103
|
+
self.menu.locator('a:has-text("Admin Panel")').click()
|
|
104
|
+
|
|
105
|
+
def click_change_password(self) -> None:
|
|
106
|
+
self.menu.locator('a:has-text("Change Password")').click()
|
|
107
|
+
|
|
108
|
+
def click_logout(self) -> None:
|
|
109
|
+
self.menu.locator('a:has-text("Logout")').click()
|
|
110
|
+
|
|
111
|
+
def click_theme_dashboard(self) -> None:
|
|
112
|
+
self.menu.locator('a:has-text("Theme Dashboard")').click()
|
|
113
|
+
|
|
114
|
+
def is_open(self) -> bool:
|
|
115
|
+
return self.menu.is_visible()
|
|
116
|
+
|
|
117
|
+
def open(self) -> None:
|
|
118
|
+
if not self.is_open():
|
|
119
|
+
self.trigger.click()
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
if TYPE_CHECKING:
|
|
6
|
+
from playwright.sync_api import Locator, Page
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class NotificationBell:
|
|
10
|
+
"""Playwright component for django_spire/notification/app/element/notification_bell.html"""
|
|
11
|
+
|
|
12
|
+
def __init__(self, page: Page) -> None:
|
|
13
|
+
self.page = page
|
|
14
|
+
|
|
15
|
+
@property
|
|
16
|
+
def badge(self) -> Locator:
|
|
17
|
+
return self.bell.locator('.badge, .position-absolute')
|
|
18
|
+
|
|
19
|
+
@property
|
|
20
|
+
def bell(self) -> Locator:
|
|
21
|
+
return self.page.locator('.bi-bell').first
|
|
22
|
+
|
|
23
|
+
@property
|
|
24
|
+
def dropdown(self) -> Locator:
|
|
25
|
+
return self.page.locator('.notification-dropdown, [x-show*="notification"]')
|
|
26
|
+
|
|
27
|
+
def click(self) -> None:
|
|
28
|
+
self.bell.click()
|
|
29
|
+
|
|
30
|
+
def get_badge_count(self) -> int:
|
|
31
|
+
if not self.has_badge():
|
|
32
|
+
return 0
|
|
33
|
+
|
|
34
|
+
text = self.badge.inner_text()
|
|
35
|
+
|
|
36
|
+
if text.isdigit():
|
|
37
|
+
return int(text)
|
|
38
|
+
|
|
39
|
+
return 0
|
|
40
|
+
|
|
41
|
+
def has_badge(self) -> bool:
|
|
42
|
+
return self.badge.count() > 0 and self.badge.is_visible()
|
|
43
|
+
|
|
44
|
+
def has_notifications(self) -> bool:
|
|
45
|
+
return self.has_badge() and self.get_badge_count() > 0
|
|
46
|
+
|
|
47
|
+
def is_dropdown_open(self) -> bool:
|
|
48
|
+
return self.dropdown.is_visible()
|
|
49
|
+
|
|
50
|
+
def is_visible(self) -> bool:
|
|
51
|
+
return self.bell.is_visible()
|
|
52
|
+
|
|
53
|
+
def open_dropdown(self) -> None:
|
|
54
|
+
if not self.is_dropdown_open():
|
|
55
|
+
self.click()
|
|
56
|
+
|
|
57
|
+
def close_dropdown(self) -> None:
|
|
58
|
+
if self.is_dropdown_open():
|
|
59
|
+
self.click()
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
if TYPE_CHECKING:
|
|
6
|
+
from playwright.sync_api import Locator, Page
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ThemeSelector:
|
|
10
|
+
"""Playwright component for django_spire/theme/element/theme_selector.html"""
|
|
11
|
+
|
|
12
|
+
def __init__(self, page: Page) -> None:
|
|
13
|
+
self.page = page
|
|
14
|
+
|
|
15
|
+
@property
|
|
16
|
+
def icon(self) -> Locator:
|
|
17
|
+
return self.page.locator('.bi-sun-fill, .bi-moon-fill').first
|
|
18
|
+
|
|
19
|
+
def click(self) -> None:
|
|
20
|
+
self.icon.click()
|
|
21
|
+
|
|
22
|
+
def get_current_mode(self) -> str:
|
|
23
|
+
html = self.page.locator('html')
|
|
24
|
+
return html.get_attribute('data-theme') or 'light'
|
|
25
|
+
|
|
26
|
+
def get_current_theme_family(self) -> str:
|
|
27
|
+
html = self.page.locator('html')
|
|
28
|
+
return html.get_attribute('data-theme-family') or ''
|
|
29
|
+
|
|
30
|
+
def is_dark_mode(self) -> bool:
|
|
31
|
+
return self.get_current_mode() == 'dark'
|
|
32
|
+
|
|
33
|
+
def is_light_mode(self) -> bool:
|
|
34
|
+
return self.get_current_mode() == 'light'
|
|
35
|
+
|
|
36
|
+
def is_visible(self) -> bool:
|
|
37
|
+
return self.icon.is_visible()
|
|
38
|
+
|
|
39
|
+
def toggle(self) -> None:
|
|
40
|
+
self.click()
|
|
41
|
+
|
|
42
|
+
def wait_for_theme_change(self, expected_mode: str, timeout: int = 5000) -> None:
|
|
43
|
+
self.page.wait_for_function(
|
|
44
|
+
f'() => document.documentElement.getAttribute("data-theme") === "{expected_mode}"',
|
|
45
|
+
timeout=timeout
|
|
46
|
+
)
|