I can add tables
Refactoring DbEngine Fixing unit tests Fixing unit tests Fixing unit tests Refactored DbManager for datagrid Improving front end performance I can add new table Fixed sidebar closing when clicking on it Fix drag event rebinding, improve listener options, and add debug Prevent duplicate drag event bindings with a dataset flag and ensure consistent scrollbar functionality. Change wheel event listener to passive mode for better performance. Refactor function naming for consistency, and add debug logs for event handling. Refactor Datagrid bindings and default state handling. Updated Javascript to conditionally rebind Datagrid on specific events. Improved Python components by handling empty DataFrame cases and removing redundant code. Revised default state initialization in settings for better handling of mutable fields. Added Rowindex visualisation support Working on Debugger with own implementation of JsonViewer Working on JsonViewer.py Fixed unit tests Adding unit tests I can fold and unfold fixed unit tests Adding css for debugger Added tooltip management Adding debugger functionalities Refactor serializers and improve error handling in DB engine Fixed error where tables were overwritten I can display footer menu Working on footer. Refactoring how heights are managed Refactored scrollbars management Working on footer menu I can display footer menu + fixed unit tests Fixed unit tests Updated click management I can display aggregations in footers Added docker management Refactor input handling and improve config defaults Fixed scrollbars colors Refactored tooltip management Improved tooltip management Improving FilterAll
This commit is contained in:
107
tests/helpers.py
107
tests/helpers.py
@@ -8,7 +8,7 @@ import pandas as pd
|
||||
from bs4 import BeautifulSoup
|
||||
from fastcore.basics import NotStr
|
||||
from fastcore.xml import to_xml
|
||||
from fasthtml.components import html2ft, Div
|
||||
from fasthtml.components import html2ft, Div, Span
|
||||
|
||||
pattern = r"""(?P<tag>\w+)(?:#(?P<id>[\w-]+))?(?P<attributes>(?:\[\w+=['"]?[\w_-]+['"]?\])*)"""
|
||||
attr_pattern = r"""\[(?P<name>\w+)=['"]?(?P<value>[\w_-]+)['"]?\]"""
|
||||
@@ -17,6 +17,7 @@ compiled_pattern = re.compile(pattern)
|
||||
compiled_attr_pattern = re.compile(attr_pattern)
|
||||
compiled_svg_pattern = re.compile(svg_pattern)
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class DoNotCheck:
|
||||
desc: str = None
|
||||
@@ -33,6 +34,7 @@ class StartsWith:
|
||||
"""
|
||||
s: str
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class Contains:
|
||||
"""
|
||||
@@ -40,6 +42,7 @@ class Contains:
|
||||
"""
|
||||
s: str
|
||||
|
||||
|
||||
Empty = EmptyElement()
|
||||
|
||||
|
||||
@@ -141,7 +144,10 @@ def search_elements_by_name(ft, tag: str = None, attrs: dict = None, comparison_
|
||||
# Recursive case: search through the children
|
||||
for child in _ft.children:
|
||||
result.extend(_search_elements_by_name(child))
|
||||
|
||||
elif isinstance(_ft, (list, tuple)):
|
||||
for _item in _ft:
|
||||
result.extend(_search_elements_by_name(_item))
|
||||
|
||||
return result
|
||||
|
||||
if isinstance(ft, list):
|
||||
@@ -156,7 +162,7 @@ def search_elements_by_name(ft, tag: str = None, attrs: dict = None, comparison_
|
||||
def search_elements_by_path(ft, path: str, attrs: dict = None):
|
||||
"""
|
||||
Selects elements that match a given path. The path is a dot-separated list of elements.
|
||||
One the path if found, the optional attributes are compared against the last element's
|
||||
Once the path if found, the optional attributes are compared against the last element's
|
||||
attributes.
|
||||
Note the path may not start at the root node of the tree structure.
|
||||
|
||||
@@ -188,10 +194,10 @@ def search_elements_by_path(ft, path: str, attrs: dict = None):
|
||||
return _find(ft, "")
|
||||
|
||||
|
||||
def search_first_with_attribute(ft, tag, attribute
|
||||
):
|
||||
def search_first_with_attribute(ft, tag, attribute):
|
||||
"""
|
||||
Browse ft and its children to find the first element that matches the tag and has the attribute defined
|
||||
We do not care about the value of the attribute, just the presence of it.
|
||||
if tag is None, it will return the first element with the attribute
|
||||
:param ft:
|
||||
:param tag:
|
||||
@@ -266,6 +272,83 @@ def matches(actual, expected, path=""):
|
||||
|
||||
return type(x)
|
||||
|
||||
def _debug(_actual, _expected):
|
||||
str_actual = _debug_print_actual(_actual, _expected, "", 3)
|
||||
str_expected = _debug_print_expected(_expected, "", 2)
|
||||
return f"\nactual={str_actual}\nexpected={str_expected}"
|
||||
|
||||
def _debug_value(x):
|
||||
if x in ("** NOT FOUND **", "** NONE **", "** NO MORE CHILDREN **"):
|
||||
return x
|
||||
elif isinstance(x, str):
|
||||
return f"'{x}'" if "'" not in x else f'"{x}"'
|
||||
else:
|
||||
return x
|
||||
|
||||
def _debug_print_actual(_actual, _expected, indent, max_level):
|
||||
# debug print both actual and expected, showing only expected elements
|
||||
if max_level == 0:
|
||||
return ""
|
||||
|
||||
if _actual is None:
|
||||
return f"{indent}** NONE **"
|
||||
|
||||
if not hasattr(_actual, "tag") or not hasattr(_expected, "tag"):
|
||||
return f"{indent}{_actual}"
|
||||
|
||||
str_actual = f"{indent}({_actual.tag}"
|
||||
first_attr = True
|
||||
for attr in _expected.attrs:
|
||||
comma = " " if first_attr else ", "
|
||||
str_actual += f"{comma}{attr}={_debug_value(_actual.attrs.get(attr, '** NOT FOUND **'))}"
|
||||
first_attr = False
|
||||
|
||||
if len(_expected.children) == 0 and len(_actual.children) and max_level > 1:
|
||||
# force recursion to see sub levels
|
||||
for _actual_child in _actual.children:
|
||||
str_child_a = _debug_print_actual(_actual_child, _actual_child, indent + " ", max_level - 1)
|
||||
str_actual += "\n" + str_child_a if str_child_a else ""
|
||||
|
||||
else:
|
||||
|
||||
for index, _expected_child in enumerate(_expected.children):
|
||||
if len(_actual.children) > index:
|
||||
_actual_child = _actual.children[index]
|
||||
else:
|
||||
_actual_child = "** NO MORE CHILDREN **"
|
||||
|
||||
str_child_a = _debug_print_actual(_actual_child, _expected_child, indent + " ", max_level - 1)
|
||||
str_actual += "\n" + str_child_a if str_child_a else ""
|
||||
|
||||
str_actual += ")"
|
||||
|
||||
return str_actual
|
||||
|
||||
def _debug_print_expected(_expected, indent, max_level):
|
||||
if max_level == 0:
|
||||
return ""
|
||||
|
||||
if _expected is None:
|
||||
return f"{indent}** NONE **"
|
||||
|
||||
if not hasattr(_expected, "tag"):
|
||||
return f"{indent}{_expected}"
|
||||
|
||||
str_expected = f"{indent}({_expected.tag}"
|
||||
first_attr = True
|
||||
for attr in _expected.attrs:
|
||||
comma = " " if first_attr else ", "
|
||||
str_expected += f"{comma}{attr}={_expected.attrs[attr]}"
|
||||
first_attr = False
|
||||
|
||||
for _expected_child in _expected.children:
|
||||
str_child_e = _debug_print_expected(_expected_child, indent + " ", max_level - 1)
|
||||
str_expected += "\n" + str_child_e if str_child_e else ""
|
||||
|
||||
str_expected += ")"
|
||||
|
||||
return str_expected
|
||||
|
||||
if actual is None and expected is not None:
|
||||
assert False, f"{print_path(path)}actual is None !"
|
||||
|
||||
@@ -278,11 +361,11 @@ def matches(actual, expected, path=""):
|
||||
return True
|
||||
|
||||
assert _type(actual) == _type(expected) or (hasattr(actual, "tag") and hasattr(expected, "tag")), \
|
||||
f"{print_path(path)}The types are different: {type(actual)} != {type(expected)}, ({actual} != {expected})."
|
||||
f"{print_path(path)}The types are different: {type(actual)} != {type(expected)}{_debug(actual, expected)}."
|
||||
|
||||
if isinstance(expected, (list, tuple)):
|
||||
assert len(actual) >= len(expected), \
|
||||
f"{print_path(path)}Some required elements are missing: {actual} != {expected}."
|
||||
f"{print_path(path)}Some required elements are missing: {len(actual)=} < {len(expected)}, \n{_debug(actual, expected)}."
|
||||
|
||||
for actual_child, expected_child in zip(actual, expected):
|
||||
assert matches(actual_child, expected_child)
|
||||
@@ -329,7 +412,7 @@ def matches(actual, expected, path=""):
|
||||
pass
|
||||
else:
|
||||
assert len(actual.children) >= len(expected.children), \
|
||||
f"{print_path(path)}Some required elements are missing: actual={actual.children} != expected={expected.children}."
|
||||
f"{print_path(path)}Some required elements are missing: len(actual)={len(actual.children)} < len(expected)={len(expected.children)}{_debug(actual, expected)}."
|
||||
|
||||
for actual_child, expected_child in zip(actual.children, expected.children):
|
||||
matches(actual_child, expected_child, path)
|
||||
@@ -547,3 +630,11 @@ def icon(name: str):
|
||||
|
||||
def div_icon(name: str):
|
||||
return Div(NotStr(f'<svg name="{name}"'))
|
||||
|
||||
|
||||
def span_icon(name: str):
|
||||
return Span(NotStr(f'<svg name="{name}"'))
|
||||
|
||||
|
||||
def div_ellipsis(text: str):
|
||||
return Div(text, cls="truncate", data_tooltip=text)
|
||||
|
||||
@@ -11,7 +11,7 @@ from helpers import matches, search_elements_by_path
|
||||
|
||||
COLUMNS_SETTINGS_ID = "columns_settings_id"
|
||||
TEST_GRID_ID = "test_grid_id"
|
||||
TEST_GRID_KEY = "test_grid_key"
|
||||
TEST_GRID_KEY = ("RepositoryName", "test_grid_key")
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
|
||||
@@ -5,11 +5,11 @@ import pytest
|
||||
from fasthtml.components import *
|
||||
|
||||
from components.datagrid_new.components.DataGrid import DataGrid
|
||||
from components.datagrid_new.constants import ColumnType, ViewType
|
||||
from components.datagrid_new.constants import ColumnType, ViewType, FooterAggregation
|
||||
from components.datagrid_new.settings import DataGridColumnState, DatagridView
|
||||
from core.settings_management import SettingsManager, MemoryDbEngine
|
||||
from helpers import matches, search_elements_by_path, extract_table_values_new, search_elements_by_name, div_icon, \
|
||||
Contains
|
||||
Contains, div_ellipsis
|
||||
|
||||
TEST_GRID_ID = "testing_grid_id"
|
||||
TEST_GRID_KEY = "testing_grid_key"
|
||||
@@ -20,7 +20,8 @@ def empty_dg(session):
|
||||
return DataGrid(session,
|
||||
_id=TEST_GRID_ID,
|
||||
settings_manager=SettingsManager(MemoryDbEngine()),
|
||||
key=TEST_GRID_KEY)
|
||||
key=TEST_GRID_KEY,
|
||||
boundaries={"height": 500, "width": 800})
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
@@ -53,14 +54,18 @@ def test_i_can_render_datagrid(empty_dg):
|
||||
actual = empty_dg.__ft__()
|
||||
|
||||
expected = Div(
|
||||
Div(id=f"tt_{TEST_GRID_ID}"),
|
||||
Div(), # menu
|
||||
Div(
|
||||
Div(id=f"t_{TEST_GRID_ID}", ), # table
|
||||
Div(id=f"sb_{TEST_GRID_ID}"), # sidebar
|
||||
cls="dt2-main",
|
||||
Div(), # menu
|
||||
Div(
|
||||
Div(
|
||||
Div(id=f"scb_{TEST_GRID_ID}"), # scrollbar
|
||||
Div(id=f"t_{TEST_GRID_ID}"), # table
|
||||
id=f"tc_{TEST_GRID_ID}"), # table container
|
||||
Div(id=f"sb_{TEST_GRID_ID}"), # sidebar
|
||||
cls="dt2-main",
|
||||
),
|
||||
Script()
|
||||
),
|
||||
Script(),
|
||||
id=TEST_GRID_ID
|
||||
)
|
||||
|
||||
@@ -73,15 +78,15 @@ def test_i_can_render_dataframe(dg):
|
||||
expected = Div(
|
||||
Div(id=f"tsm_{TEST_GRID_ID}"), # selection manager
|
||||
Div(id=f"tdd_{TEST_GRID_ID}"), # cell drop down
|
||||
Div(id=f"tcdd_{TEST_GRID_ID}"), # cell drop down
|
||||
Div(id=f"tcm_{TEST_GRID_ID}"), # cell drop down
|
||||
Div(), # Keyboard navigation
|
||||
Div(
|
||||
Div(id=f"scb_{TEST_GRID_ID}"), # container for the scroll bars
|
||||
Div(id=f"th_{TEST_GRID_ID}"), # header
|
||||
Div(id=f"tb_{TEST_GRID_ID}"), # table
|
||||
Div(id=f"tf_{TEST_GRID_ID}"), # footer
|
||||
cls="dt2-inner-table"
|
||||
)
|
||||
),
|
||||
id=f"t_{TEST_GRID_ID}"
|
||||
)
|
||||
|
||||
assert matches(to_compare, expected)
|
||||
@@ -140,14 +145,13 @@ def test_i_can_render_boolean_cells(dg):
|
||||
assert matches(to_compare, expected)
|
||||
|
||||
|
||||
def test_i_can_render_when_not_visible(dg):
|
||||
def test_i_can_render_when_column_not_visible(dg):
|
||||
updates = [{"col_id": "name", "visible": False}]
|
||||
dg.update_columns_state(updates)
|
||||
|
||||
actual = dg.__ft__()
|
||||
to_compare = search_elements_by_name(actual, "div", attrs={"class": "dt2-inner-table"})[0]
|
||||
expected = Div(
|
||||
Div(id=f"scb_{TEST_GRID_ID}"),
|
||||
Div(
|
||||
Div(data_col='name',
|
||||
data_tooltip="Show column 'Name'",
|
||||
@@ -196,14 +200,13 @@ def test_i_can_render_when_not_visible(dg):
|
||||
assert matches(to_compare, expected)
|
||||
|
||||
|
||||
def test_i_can_render_when_not_usable(dg):
|
||||
def test_i_can_render_when_column_not_usable(dg):
|
||||
updates = [{"col_id": "name", "usable": False}]
|
||||
dg.update_columns_state(updates)
|
||||
|
||||
actual = dg.__ft__()
|
||||
to_compare = search_elements_by_name(actual, "div", attrs={"class": "dt2-inner-table"})[0]
|
||||
expected = Div(
|
||||
Div(id=f"scb_{TEST_GRID_ID}"),
|
||||
Div(
|
||||
None,
|
||||
Div(data_col='age',
|
||||
@@ -451,4 +454,58 @@ def test_change_view_to_nonexistent_view(dg_with_views):
|
||||
with pytest.raises(ValueError) as e:
|
||||
dg_with_views.change_view("Non Existent View")
|
||||
|
||||
assert str(e.value) == "View 'Non Existent View' does not exist"
|
||||
assert str(e.value) == "View 'Non Existent View' does not exist."
|
||||
|
||||
|
||||
def test_i_can_render_footer_menu_emtpy(dg):
|
||||
menu = dg.mk_footer_menu(None)
|
||||
expected = Div(
|
||||
cls="dt2-footer-menu menu menu-sm rounded-box shadow-sm ",
|
||||
id=f"tcm_{TEST_GRID_ID}",
|
||||
)
|
||||
|
||||
assert matches(menu, expected)
|
||||
|
||||
|
||||
def test_i_can_render_footer_menu_with_items(dg):
|
||||
boundaries = {"x": 0, "y": 0, "height": 10, "width": 50}
|
||||
row_index = 0
|
||||
menu = dg.mk_footer_menu("name", row_index, boundaries) # just give any col_id
|
||||
|
||||
expected = Div(
|
||||
*[Div(
|
||||
div_ellipsis(agg.value),
|
||||
cls=Contains("dt2-footer-menu-item"),
|
||||
) for agg in FooterAggregation],
|
||||
id=f"tcm_{TEST_GRID_ID}",
|
||||
)
|
||||
|
||||
assert matches(menu, expected)
|
||||
|
||||
|
||||
def test_i_can_compute_footer_menu_position_when_enough_space(dg):
|
||||
# when enough space at the bottom, the menu is display below the footer cell (below boundaries['y'])
|
||||
boundaries = {"x": 0, "y": 0, "height": 10, "width": 50}
|
||||
row_index = 0
|
||||
menu = dg.mk_footer_menu("name", row_index, boundaries) # just give any col_id
|
||||
|
||||
expected = Div(
|
||||
style=f"left:{boundaries['x'] + 10}px;top:{boundaries['y'] + boundaries['height']}px;",
|
||||
id=f"tcm_{TEST_GRID_ID}",
|
||||
)
|
||||
|
||||
assert matches(menu, expected)
|
||||
|
||||
|
||||
def test_i_can_compute_footer_menu_position_when_not_enough_space(dg):
|
||||
# when not enough space at the bottom, the menu is display above the footer cell (above boundaries['y'])
|
||||
boundaries = {"x": 0, "y": dg.get_state().boundaries["container_height"], "height": 10, "width": 50}
|
||||
row_index = 0
|
||||
menu = dg.mk_footer_menu("name", row_index, boundaries) # just give any col_id
|
||||
|
||||
expected = Div(
|
||||
style=f"left:10px;top:296px;",
|
||||
id=f"tcm_{TEST_GRID_ID}",
|
||||
)
|
||||
|
||||
assert matches(menu, expected)
|
||||
|
||||
@@ -1,108 +0,0 @@
|
||||
import os.path
|
||||
|
||||
import pytest
|
||||
|
||||
from core.dbengine import DbException
|
||||
from core.settings_management import DummyDbEngine
|
||||
from core.settings_objects import BudgetTrackerSettings
|
||||
|
||||
settings_file = DummyDbEngine().db_path
|
||||
|
||||
FAKE_USER_ID = "FakeUserId"
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def setup_and_finalize():
|
||||
if os.path.exists(settings_file):
|
||||
os.remove(settings_file)
|
||||
|
||||
yield
|
||||
|
||||
if os.path.exists(settings_file):
|
||||
os.remove(settings_file)
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def engine():
|
||||
return DummyDbEngine()
|
||||
|
||||
|
||||
def test_i_can_save_and_load(engine):
|
||||
obj = BudgetTrackerSettings(
|
||||
spread_sheet="spread_sheet",
|
||||
col_row_num="row_number",
|
||||
col_project="project",
|
||||
col_owner="owner",
|
||||
col_capex="capex",
|
||||
col_details="details",
|
||||
col_supplier="supplier",
|
||||
col_budget_amt="budget",
|
||||
col_actual_amt="actual",
|
||||
col_forecast5_7_amt="forecast5_7",
|
||||
)
|
||||
|
||||
engine.save(FAKE_USER_ID, "MyEntry", obj)
|
||||
|
||||
res = engine.load(FAKE_USER_ID, "MyEntry")
|
||||
assert isinstance(res, BudgetTrackerSettings)
|
||||
|
||||
assert res.spread_sheet == obj.spread_sheet
|
||||
assert res.col_row_num == obj.col_row_num
|
||||
assert res.col_project == obj.col_project
|
||||
assert res.col_owner == obj.col_owner
|
||||
assert res.col_capex == obj.col_capex
|
||||
assert res.col_details == obj.col_details
|
||||
assert res.col_supplier == obj.col_supplier
|
||||
assert res.col_budget_amt == obj.col_budget_amt
|
||||
assert res.col_actual_amt == obj.col_actual_amt
|
||||
assert res.col_forecast5_7_amt == obj.col_forecast5_7_amt
|
||||
|
||||
|
||||
def test_i_can_save_and_modify(engine):
|
||||
obj = BudgetTrackerSettings()
|
||||
engine.save(FAKE_USER_ID, "MyEntry", obj)
|
||||
|
||||
obj = BudgetTrackerSettings(
|
||||
spread_sheet="modified_spread_sheet",
|
||||
col_row_num="modified_row_number",
|
||||
col_project="modified_project",
|
||||
col_owner="modified_owner",
|
||||
col_capex="modified_capex",
|
||||
col_details="modified_details",
|
||||
col_supplier="modified_supplier",
|
||||
col_budget_amt="modified_budget",
|
||||
col_actual_amt="modified_actual",
|
||||
col_forecast5_7_amt="forecast5_7",
|
||||
)
|
||||
engine.save(FAKE_USER_ID, "MyEntry", obj)
|
||||
|
||||
res = engine.load(FAKE_USER_ID, "MyEntry")
|
||||
|
||||
assert isinstance(res, BudgetTrackerSettings)
|
||||
assert res.spread_sheet == obj.spread_sheet
|
||||
assert res.col_row_num == obj.col_row_num
|
||||
assert res.col_project == obj.col_project
|
||||
assert res.col_owner == obj.col_owner
|
||||
assert res.col_capex == obj.col_capex
|
||||
assert res.col_details == obj.col_details
|
||||
assert res.col_supplier == obj.col_supplier
|
||||
assert res.col_budget_amt == obj.col_budget_amt
|
||||
assert res.col_actual_amt == obj.col_actual_amt
|
||||
assert res.col_forecast5_7_amt == obj.col_forecast5_7_amt
|
||||
|
||||
|
||||
def test_i_cannot_load_if_no_setting_file(engine):
|
||||
with pytest.raises(DbException) as ex:
|
||||
engine.load(FAKE_USER_ID, "MyEntry")
|
||||
|
||||
assert str(ex.value) == f"Entry 'MyEntry' is not found."
|
||||
|
||||
|
||||
def test_i_cannot_load_if_no_entry_found(engine):
|
||||
obj = BudgetTrackerSettings()
|
||||
engine.save(FAKE_USER_ID, "AnotherEntry", obj)
|
||||
|
||||
with pytest.raises(DbException) as ex:
|
||||
engine.load(FAKE_USER_ID, "MyEntry")
|
||||
|
||||
assert str(ex.value) == f"Entry 'MyEntry' is not found."
|
||||
@@ -32,13 +32,13 @@ def sample_structure():
|
||||
|
||||
@pytest.mark.parametrize("value, expected, expected_error", [
|
||||
(Div(), "value",
|
||||
"The types are different: <class 'fastcore.xml.FT'> != <class 'str'>, (div((),{}) != value)."),
|
||||
"The types are different: <class 'fastcore.xml.FT'> != <class 'str'>\nactual=div((),{})\nexpected=value."),
|
||||
(Div(), A(),
|
||||
"The elements are different: 'div' != 'a'."),
|
||||
(Div(Div()), Div(A()),
|
||||
"Path 'div':\n\tThe elements are different: 'div' != 'a'."),
|
||||
(Div(A(Span())), Div(A("element")),
|
||||
"Path 'div.a':\n\tThe types are different: <class 'fastcore.xml.FT'> != <class 'str'>, (span((),{}) != element)."),
|
||||
"Path 'div.a':\n\tThe types are different: <class 'fastcore.xml.FT'> != <class 'str'>\nactual=span((),{})\nexpected=element."),
|
||||
(Div(attr="one"), Div(attr="two"),
|
||||
"Path 'div':\n\tThe values are different for 'attr' : 'one' != 'two'."),
|
||||
(Div(A(attr="alpha")), Div(A(attr="beta")),
|
||||
|
||||
159
tests/test_jsonviewer.py
Normal file
159
tests/test_jsonviewer.py
Normal file
@@ -0,0 +1,159 @@
|
||||
import pytest
|
||||
from fasthtml.components import *
|
||||
|
||||
from components.debugger.components.JsonViewer import JsonViewer, DictNode, ListNode, ValueNode
|
||||
from helpers import matches, span_icon, search_elements_by_name
|
||||
|
||||
JSON_VIEWER_INSTANCE_ID = "json_viewer"
|
||||
ML_20 = "margin-left: 20px;"
|
||||
CLS_PREFIX = "mmt-jsonviewer"
|
||||
USER_ID = "user_id"
|
||||
|
||||
dn = DictNode
|
||||
ln = ListNode
|
||||
n = ValueNode
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def json_viewer(session):
|
||||
return JsonViewer(session, JSON_VIEWER_INSTANCE_ID, None, USER_ID, {})
|
||||
|
||||
|
||||
def jv_id(x):
|
||||
return f"{JSON_VIEWER_INSTANCE_ID}-{x}"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("data, expected_node", [
|
||||
({}, dn({}, jv_id(0), 0, {})),
|
||||
([], ln([], jv_id(0), 0, [])),
|
||||
(1, n(1)),
|
||||
("value", n("value")),
|
||||
(True, n(True)),
|
||||
(None, n(None)),
|
||||
([1, 2, 3], ln([1, 2, 3], jv_id(0), 0, [n(1), n(2), n(3)])),
|
||||
({"a": 1, "b": 2}, dn({"a": 1, "b": 2}, jv_id(0), 0, {"a": n(1), "b": n(2)})),
|
||||
({"a": [1, 2]}, dn({"a": [1, 2]}, jv_id(0), 0, {"a": ln([1, 2], jv_id(1), 1, [n(1), n(2)])})),
|
||||
([{"a": [1, 2]}],
|
||||
ln([{"a": [1, 2]}], jv_id(0), 0, [dn({"a": [1, 2]}, jv_id(1), 1, {"a": ln([1, 2], jv_id(2), 2, [n(1), n(2)])})]))
|
||||
])
|
||||
def test_i_can_create_node(data, expected_node):
|
||||
json_viewer_ = JsonViewer(None, JSON_VIEWER_INSTANCE_ID, None, USER_ID, data)
|
||||
assert json_viewer_.node == expected_node
|
||||
|
||||
|
||||
def test_i_can_render(json_viewer):
|
||||
actual = json_viewer.__ft__()
|
||||
expected = Div(
|
||||
Div(Div(id=f"{jv_id('0')}"), id=f"{jv_id('root')}"), # root debug
|
||||
cls=f"{CLS_PREFIX}",
|
||||
id=JSON_VIEWER_INSTANCE_ID)
|
||||
|
||||
assert matches(actual, expected)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("value, expected_inner", [
|
||||
("hello world", Span('"hello world"', cls=f"{CLS_PREFIX}-string")),
|
||||
(1, Span("1", cls=f"{CLS_PREFIX}-number")),
|
||||
(True, Span("true", cls=f"{CLS_PREFIX}-bool")),
|
||||
(False, Span("false", cls=f"{CLS_PREFIX}-bool")),
|
||||
(None, Span("null", cls=f"{CLS_PREFIX}-null")),
|
||||
])
|
||||
def test_i_can_render_simple_value(session, value, expected_inner):
|
||||
jsonv = JsonViewer(session, JSON_VIEWER_INSTANCE_ID, None, USER_ID, value)
|
||||
actual = jsonv.__ft__()
|
||||
to_compare = search_elements_by_name(actual, "div", attrs={"id": f"{jv_id("root")}"})[0]
|
||||
expected = Div(
|
||||
|
||||
Div(
|
||||
None, # no folding
|
||||
None, # # 'key :' is missing for the first node
|
||||
expected_inner,
|
||||
style=ML_20),
|
||||
|
||||
id=f"{jv_id("root")}")
|
||||
|
||||
assert matches(to_compare, expected)
|
||||
|
||||
|
||||
def test_i_can_render_expanded_list_node(session):
|
||||
value = [1, "hello", True]
|
||||
jsonv = JsonViewer(session, JSON_VIEWER_INSTANCE_ID, None, USER_ID, value)
|
||||
actual = jsonv.__ft__()
|
||||
to_compare = search_elements_by_name(actual, "div", attrs={"id": f"{jv_id("root")}"})[0]
|
||||
to_compare = to_compare.children[0] # I want to compare what is inside the div
|
||||
|
||||
expected_inner = Span("[",
|
||||
Div(None, Span("0 : "), Span('1'), style=ML_20),
|
||||
Div(None, Span("1 : "), Span('"hello"'), style=ML_20),
|
||||
Div(None, Span("2 : "), Span('true'), style=ML_20),
|
||||
Div("]")),
|
||||
|
||||
expected = Div(
|
||||
span_icon("expanded"),
|
||||
None, # 'key :' is missing for the first node
|
||||
expected_inner,
|
||||
style=ML_20)
|
||||
|
||||
assert matches(to_compare, expected)
|
||||
|
||||
|
||||
def test_i_can_render_expanded_dict_node(session):
|
||||
value = {"a": 1, "b": "hello", "c": True}
|
||||
jsonv = JsonViewer(session, JSON_VIEWER_INSTANCE_ID, None, USER_ID, value)
|
||||
actual = jsonv.__ft__()
|
||||
to_compare = search_elements_by_name(actual, "div", attrs={"id": f"{jv_id("root")}"})[0]
|
||||
to_compare = to_compare.children[0] # I want to compare what is inside the div
|
||||
|
||||
expected_inner = Span("{",
|
||||
Div(None, Span("a : "), Span('1'), style=ML_20),
|
||||
Div(None, Span("b : "), Span('"hello"'), style=ML_20),
|
||||
Div(None, Span("c : "), Span('true'), style=ML_20),
|
||||
Div("}"))
|
||||
|
||||
expected = Div(
|
||||
span_icon("expanded"),
|
||||
None, # 'key :' is missing for the first node
|
||||
expected_inner,
|
||||
style=ML_20)
|
||||
|
||||
assert matches(to_compare, expected)
|
||||
|
||||
|
||||
def test_i_can_render_expanded_list_of_dict_node(session):
|
||||
value = [{"a": 1, "b": "hello"}]
|
||||
jsonv = JsonViewer(session, JSON_VIEWER_INSTANCE_ID, None, USER_ID, value)
|
||||
actual = jsonv.__ft__()
|
||||
to_compare = search_elements_by_name(actual, "div", attrs={"id": f"{jv_id("root")}"})[0]
|
||||
to_compare = to_compare.children[0] # I want to compare what is inside the div
|
||||
|
||||
expected_inner = Span("[",
|
||||
|
||||
Div(span_icon("expanded"),
|
||||
Span("0 : "),
|
||||
Span("{",
|
||||
Div(None, Span("a : "), Span('1'), style=ML_20),
|
||||
Div(None, Span("b : "), Span('"hello"'), style=ML_20),
|
||||
Div("}")),
|
||||
id=f"{jv_id(1)}"),
|
||||
|
||||
Div("]"))
|
||||
|
||||
expected = Div(
|
||||
span_icon("expanded"),
|
||||
None, # 'key :' is missing for the first node
|
||||
expected_inner,
|
||||
style=ML_20)
|
||||
|
||||
assert matches(to_compare, expected)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("input_value, expected_output", [
|
||||
('Hello World', '"Hello World"'), # No quotes in input
|
||||
('Hello "World"', "'Hello \"World\"'"), # Contains double quotes
|
||||
("Hello 'World'", '"Hello \'World\'"'), # Contains single quotes
|
||||
('Hello "World" and \'Universe\'', '"Hello \\"World\\" and \'Universe\'"'), # both single and double quotes
|
||||
('', '""'), # Empty string
|
||||
])
|
||||
def test_add_quotes(input_value, expected_output):
|
||||
result = JsonViewer.add_quotes(input_value)
|
||||
assert result == expected_output
|
||||
@@ -1,15 +1,14 @@
|
||||
import pytest
|
||||
from fasthtml.components import *
|
||||
|
||||
from components.addstuff.constants import ROUTE_ROOT, Routes
|
||||
from components.addstuff.settings import Repository, RepositoriesSettings
|
||||
from components.repositories.components.Repositories import Repositories
|
||||
from components.repositories.constants import ROUTE_ROOT, Routes
|
||||
from core.settings_management import SettingsManager, MemoryDbEngine
|
||||
from helpers import matches, StartsWith, div_icon, find_first_match, search_elements_by_path
|
||||
from src.components.addstuff.components.Repositories import Repositories
|
||||
|
||||
USER_EMAIL = "test@mail.com"
|
||||
USER_ID = "test_user"
|
||||
TEST_REPOSITORIES_ID = "testing_grid_id"
|
||||
TEST_REPOSITORIES_ID = "testing_repositories_id"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@@ -48,14 +47,9 @@ def tabs_manager():
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def db_engine():
|
||||
return MemoryDbEngine()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def repositories(session, tabs_manager, db_engine):
|
||||
def repositories(session, tabs_manager):
|
||||
return Repositories(session=session, _id=TEST_REPOSITORIES_ID,
|
||||
settings_manager=SettingsManager(engine=db_engine),
|
||||
settings_manager=SettingsManager(engine=MemoryDbEngine()),
|
||||
tabs_manager=tabs_manager)
|
||||
|
||||
|
||||
@@ -63,22 +57,18 @@ def test_render_no_repository(repositories):
|
||||
actual = repositories.__ft__()
|
||||
expected = (
|
||||
Div(
|
||||
Div(id=f"tt_{repositories.get_id()}"),
|
||||
Div(cls="divider"),
|
||||
Div("Repositories"),
|
||||
Div(id=repositories.get_id()),
|
||||
Script()
|
||||
)
|
||||
)
|
||||
|
||||
assert matches(actual, expected)
|
||||
|
||||
|
||||
def test_render_when_repo_and_tables(db_engine, repositories):
|
||||
db_engine.init_db(USER_ID, 'AddStuffSettings', RepositoriesSettings([
|
||||
Repository("repo 1", [MyTable("table 1"), MyTable("table 2")]),
|
||||
Repository("repo 2", [MyTable("table 3")]),
|
||||
]))
|
||||
def test_render_when_repo_and_tables(repositories):
|
||||
repositories.db.add_repository("repo 1", ["table 1", "table 2"])
|
||||
repositories.db.add_repository("repo 2", ["table 3"])
|
||||
|
||||
actual = repositories.__ft__()
|
||||
to_compare = search_elements_by_path(actual, "div", {"id": repositories.get_id()})[0]
|
||||
@@ -109,8 +99,9 @@ def test_i_can_add_new_repository(repositories):
|
||||
form_id = "form_id"
|
||||
repository_name = "repository_name"
|
||||
table_name = "table_name"
|
||||
boundaries = {"height": 600, "width": 800}
|
||||
|
||||
res = repositories.add_new_repository(tab_id, form_id, repository_name, table_name)
|
||||
res = repositories.add_new_repository(tab_id, form_id, repository_name, table_name, boundaries)
|
||||
expected = (
|
||||
Div(
|
||||
Input(type="radio"),
|
||||
@@ -125,10 +116,8 @@ def test_i_can_add_new_repository(repositories):
|
||||
assert matches(res, expected)
|
||||
|
||||
|
||||
def test_i_can_click_on_repo(db_engine, repositories):
|
||||
db_engine.init_db(USER_ID, 'AddStuffSettings', RepositoriesSettings([
|
||||
Repository("repo 1", [])
|
||||
]))
|
||||
def test_i_can_click_on_repo(repositories):
|
||||
repositories.db.add_repository("repo 1", [])
|
||||
|
||||
actual = repositories.__ft__()
|
||||
expected = Input(
|
||||
@@ -140,17 +129,15 @@ def test_i_can_click_on_repo(db_engine, repositories):
|
||||
assert matches(to_compare, expected)
|
||||
|
||||
|
||||
def test_render_i_can_click_on_table(db_engine, repositories, tabs_manager):
|
||||
db_engine.init_db(USER_ID, 'AddStuffSettings', RepositoriesSettings([
|
||||
Repository("repo 1", [MyTable("table 1")])
|
||||
]))
|
||||
def test_render_i_can_click_on_table(repositories, tabs_manager):
|
||||
repositories.db.add_repository("repo 1", ["table 1"])
|
||||
|
||||
actual = repositories.__ft__()
|
||||
expected = Div(name="repo-table",
|
||||
hx_get=f"{ROUTE_ROOT}{Routes.ShowTable}",
|
||||
hx_target=f"#{repositories.tabs_manager.get_id()}",
|
||||
hx_swap="outerHTML",
|
||||
hx_vals=f'{{"_id": "{repositories.get_id()}", "repository": "repo 1", "table": "table 1"}}',
|
||||
hx_vals=f'js:{{"_id": "{repositories.get_id()}", "repository": "repo 1", "table": "table 1", "tab_boundaries": getTabContentBoundaries("tabs_id")}}',
|
||||
cls="flex")
|
||||
|
||||
to_compare = find_first_match(actual, "div.div.div.div.div[name='repo-table']")
|
||||
|
||||
@@ -1,33 +1,44 @@
|
||||
import pytest
|
||||
|
||||
from components.addstuff.settings import RepositoriesDbManager, RepositoriesSettings, Repository, \
|
||||
from components.repositories.db_management import RepositoriesDbManager, RepositoriesSettings, Repository, \
|
||||
REPOSITORIES_SETTINGS_ENTRY
|
||||
from core.settings_management import SettingsManager, MemoryDbEngine
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def settings_manager():
|
||||
return SettingsManager(MemoryDbEngine())
|
||||
return SettingsManager(MemoryDbEngine())
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def db(session, settings_manager):
|
||||
return RepositoriesDbManager(session, settings_manager)
|
||||
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def settings_manager_with_existing_repo(db, settings_manager):
|
||||
settings = RepositoriesSettings()
|
||||
repo = Repository(name="ExistingRepo", tables=["Table1"])
|
||||
settings.repositories.append(repo)
|
||||
settings_manager.save(db.session, REPOSITORIES_SETTINGS_ENTRY, settings)
|
||||
return settings_manager
|
||||
|
||||
|
||||
def test_add_new_repository(db, settings_manager):
|
||||
"""Test adding a new repository with valid data."""
|
||||
db.add_repository("NewRepo", ["Table1", "Table2"])
|
||||
|
||||
settings = settings_manager.get(db.session, REPOSITORIES_SETTINGS_ENTRY)
|
||||
assert len(settings.repositories) == 1
|
||||
assert settings.repositories[0].name == "NewRepo"
|
||||
assert settings.repositories[0].tables == ["Table1", "Table2"]
|
||||
"""Test adding a new repository with valid data."""
|
||||
db.add_repository("NewRepo", ["Table1", "Table2"])
|
||||
|
||||
settings = settings_manager.load(db.session, REPOSITORIES_SETTINGS_ENTRY)
|
||||
assert len(settings.repositories) == 1
|
||||
assert settings.repositories[0].name == "NewRepo"
|
||||
assert settings.repositories[0].tables == ["Table1", "Table2"]
|
||||
|
||||
|
||||
def test_add_repository_duplicate_name(db, settings_manager):
|
||||
"""Test adding a repository with an existing name."""
|
||||
settings = RepositoriesSettings()
|
||||
settings.repositories.append(Repository(name="ExistingRepo", tables=[]))
|
||||
settings_manager.put(db.session, REPOSITORIES_SETTINGS_ENTRY, settings)
|
||||
settings_manager.save(db.session, REPOSITORIES_SETTINGS_ENTRY, settings)
|
||||
|
||||
with pytest.raises(ValueError, match="Repository 'ExistingRepo' already exists."):
|
||||
db.add_repository("ExistingRepo")
|
||||
@@ -49,30 +60,25 @@ def test_add_repository_no_tables(db, settings_manager):
|
||||
"""Test adding a repository without specifying tables."""
|
||||
db.add_repository("RepoWithoutTables")
|
||||
|
||||
settings = settings_manager.get(db.session, "Repositories")
|
||||
settings = settings_manager.load(db.session, REPOSITORIES_SETTINGS_ENTRY)
|
||||
assert len(settings.repositories) == 1
|
||||
assert settings.repositories[0].name == "RepoWithoutTables"
|
||||
assert settings.repositories[0].tables == []
|
||||
|
||||
|
||||
def test_get_existing_repository(db, settings_manager_with_existing_repo):
|
||||
"""Test retrieving an existing repository."""
|
||||
# Retrieve the repository
|
||||
retrieved_repo = db.get_repository("ExistingRepo")
|
||||
|
||||
def test_get_existing_repository(db, settings_manager):
|
||||
"""Test retrieving an existing repository."""
|
||||
# Pre-populate settings with a repository
|
||||
settings = RepositoriesSettings()
|
||||
repo = Repository(name="ExistingRepo", tables=["Table1"])
|
||||
settings.repositories.append(repo)
|
||||
settings_manager.put(db.session, "Repositories", settings)
|
||||
|
||||
# Retrieve the repository
|
||||
retrieved_repo = db.get_repository("ExistingRepo")
|
||||
|
||||
# Verify the repository is correctly returned
|
||||
assert retrieved_repo.name == "ExistingRepo"
|
||||
assert retrieved_repo.tables == ["Table1"]
|
||||
# Verify the repository is correctly returned
|
||||
assert retrieved_repo.name == "ExistingRepo"
|
||||
assert retrieved_repo.tables == ["Table1"]
|
||||
|
||||
|
||||
def test_get_repository_not_found(db):
|
||||
"""Test retrieving a repository that does not exist."""
|
||||
with pytest.raises(ValueError, match="Repository 'NonExistentRepo' does not exists."):
|
||||
with pytest.raises(ValueError, match="Repository 'NonExistentRepo' does not exist."):
|
||||
db.get_repository("NonExistentRepo")
|
||||
|
||||
|
||||
@@ -87,3 +93,183 @@ def test_get_repository_none_name(db):
|
||||
with pytest.raises(ValueError, match="Repository name cannot be empty."):
|
||||
db.get_repository(None)
|
||||
|
||||
|
||||
def test_modify_repository_valid(db, settings_manager_with_existing_repo):
|
||||
"""Test modifying an existing repository with valid data."""
|
||||
modified_repo = db.modify_repository("ExistingRepo", "ModifiedRepo", ["UpdatedTable1", "UpdatedTable2"])
|
||||
|
||||
assert modified_repo.name == "ModifiedRepo"
|
||||
assert modified_repo.tables == ["UpdatedTable1", "UpdatedTable2"]
|
||||
|
||||
updated_settings = settings_manager_with_existing_repo.load(db.session, REPOSITORIES_SETTINGS_ENTRY)
|
||||
assert len(updated_settings.repositories) == 1
|
||||
assert updated_settings.repositories[0].name == "ModifiedRepo"
|
||||
assert updated_settings.repositories[0].tables == ["UpdatedTable1", "UpdatedTable2"]
|
||||
|
||||
|
||||
def test_modify_repository_not_found(db):
|
||||
"""Test modifying a repository that does not exist."""
|
||||
with pytest.raises(ValueError, match="Repository 'NonExistentRepo' not found."):
|
||||
db.modify_repository("NonExistentRepo", "NewName", ["Table1"])
|
||||
|
||||
|
||||
@pytest.mark.parametrize("repo_to_modify, new_repo", [
|
||||
("", "NewName"),
|
||||
(None, "NewName"),
|
||||
("ExistingRepo", ""),
|
||||
("ExistingRepo", None),
|
||||
])
|
||||
def test_modify_repository_empty_repo_to_modify(db, repo_to_modify, new_repo):
|
||||
"""Test modifying a repository with an empty name for repo_to_modify."""
|
||||
with pytest.raises(ValueError, match="Repository name cannot be empty."):
|
||||
db.modify_repository(repo_to_modify, new_repo, ["Table1"])
|
||||
|
||||
|
||||
def test_modify_repository_empty_tables_list(db, settings_manager_with_existing_repo):
|
||||
"""Test modifying an existing repository to have an empty list of tables."""
|
||||
modified_repo = db.modify_repository("ExistingRepo", "RepoWithEmptyTables", [])
|
||||
|
||||
assert modified_repo.name == "RepoWithEmptyTables"
|
||||
assert modified_repo.tables == []
|
||||
|
||||
updated_settings = settings_manager_with_existing_repo.load(db.session, REPOSITORIES_SETTINGS_ENTRY)
|
||||
assert len(updated_settings.repositories) == 1
|
||||
assert updated_settings.repositories[0].name == "RepoWithEmptyTables"
|
||||
assert updated_settings.repositories[0].tables == []
|
||||
|
||||
|
||||
def test_remove_repository_success(db, settings_manager_with_existing_repo):
|
||||
"""Test successfully removing an existing repository."""
|
||||
db.remove_repository("ExistingRepo")
|
||||
|
||||
settings = settings_manager_with_existing_repo.load(db.session, REPOSITORIES_SETTINGS_ENTRY)
|
||||
assert len(settings.repositories) == 0
|
||||
|
||||
|
||||
def test_remove_repository_not_found(db):
|
||||
"""Test removing a repository that does not exist."""
|
||||
with pytest.raises(ValueError, match="Repository 'NonExistentRepo' does not exist."):
|
||||
db.remove_repository("NonExistentRepo")
|
||||
|
||||
|
||||
def test_remove_repository_empty_name(db):
|
||||
"""Test removing a repository with an empty name."""
|
||||
with pytest.raises(ValueError, match="Repository name cannot be empty."):
|
||||
db.remove_repository("")
|
||||
|
||||
|
||||
def test_remove_repository_none_name(db):
|
||||
"""Test removing a repository with a None name."""
|
||||
with pytest.raises(ValueError, match="Repository name cannot be empty."):
|
||||
db.remove_repository(None)
|
||||
|
||||
|
||||
def test_add_table_success(db, settings_manager_with_existing_repo):
|
||||
"""Test successfully adding a new table to an existing repository."""
|
||||
db.add_table("ExistingRepo", "NewTable")
|
||||
|
||||
settings = settings_manager_with_existing_repo.load(db.session, REPOSITORIES_SETTINGS_ENTRY)
|
||||
assert len(settings.repositories) == 1
|
||||
assert "NewTable" in settings.repositories[0].tables
|
||||
assert "Table1" in settings.repositories[0].tables
|
||||
|
||||
|
||||
def test_add_table_repository_not_found(db):
|
||||
"""Test adding a table to a non-existent repository."""
|
||||
with pytest.raises(ValueError, match="Repository 'NonExistentRepo' does not exist."):
|
||||
db.add_table("NonExistentRepo", "NewTable")
|
||||
|
||||
|
||||
def test_add_table_empty_name(db, settings_manager_with_existing_repo):
|
||||
"""Test adding a table with an empty name."""
|
||||
with pytest.raises(ValueError, match="Table name cannot be empty."):
|
||||
db.add_table("ExistingRepo", "")
|
||||
|
||||
|
||||
def test_add_table_none_name(db, settings_manager_with_existing_repo):
|
||||
"""Test adding a table with a None name."""
|
||||
with pytest.raises(ValueError, match="Table name cannot be empty."):
|
||||
db.add_table("ExistingRepo", None)
|
||||
|
||||
|
||||
def test_add_table_duplicate(db, settings_manager_with_existing_repo):
|
||||
"""Test adding a duplicate table name."""
|
||||
with pytest.raises(ValueError, match="Table 'Table1' already exists in repository 'ExistingRepo'."):
|
||||
db.add_table("ExistingRepo", "Table1")
|
||||
|
||||
|
||||
def test_modify_table_success(db, settings_manager_with_existing_repo):
|
||||
"""Test successfully modifying an existing table."""
|
||||
db.modify_table("ExistingRepo", "Table1", "ModifiedTable")
|
||||
|
||||
settings = settings_manager_with_existing_repo.load(db.session, REPOSITORIES_SETTINGS_ENTRY)
|
||||
assert len(settings.repositories) == 1
|
||||
assert "ModifiedTable" in settings.repositories[0].tables
|
||||
assert "Table1" not in settings.repositories[0].tables
|
||||
|
||||
|
||||
def test_modify_table_repository_not_found(db):
|
||||
"""Test modifying a table in a non-existent repository."""
|
||||
with pytest.raises(ValueError, match="Repository 'NonExistentRepo' does not exist."):
|
||||
db.modify_table("NonExistentRepo", "Table1", "NewTable")
|
||||
|
||||
|
||||
def test_modify_table_not_found(db, settings_manager_with_existing_repo):
|
||||
"""Test modifying a non-existent table."""
|
||||
with pytest.raises(ValueError, match="Table 'NonExistentTable' does not exist in repository 'ExistingRepo'."):
|
||||
db.modify_table("ExistingRepo", "NonExistentTable", "NewTable")
|
||||
|
||||
|
||||
@pytest.mark.parametrize("old_table, new_table", [
|
||||
("", "NewTable"),
|
||||
(None, "NewTable"),
|
||||
("Table1", ""),
|
||||
("Table1", None),
|
||||
])
|
||||
def test_modify_table_empty_names(db, settings_manager_with_existing_repo, old_table, new_table):
|
||||
"""Test modifying a table with empty/None names."""
|
||||
with pytest.raises(ValueError, match="Table name cannot be empty."):
|
||||
db.modify_table("ExistingRepo", old_table, new_table)
|
||||
|
||||
|
||||
def test_modify_table_empty_repository_name(db):
|
||||
"""Test modifying a table with empty/None names."""
|
||||
with pytest.raises(ValueError, match="Repository name cannot be empty."):
|
||||
db.modify_table(None, "old_table", "new_table")
|
||||
|
||||
|
||||
def test_remove_table_success(db, settings_manager_with_existing_repo):
|
||||
"""Test successfully removing an existing table."""
|
||||
db.remove_table("ExistingRepo", "Table1")
|
||||
|
||||
settings = settings_manager_with_existing_repo.load(db.session, REPOSITORIES_SETTINGS_ENTRY)
|
||||
assert len(settings.repositories) == 1
|
||||
assert "Table1" not in settings.repositories[0].tables
|
||||
|
||||
|
||||
def test_remove_table_repository_not_found(db):
|
||||
"""Test removing a table from a non-existent repository."""
|
||||
with pytest.raises(ValueError, match="Repository 'NonExistentRepo' does not exist."):
|
||||
db.remove_table("NonExistentRepo", "Table1")
|
||||
|
||||
|
||||
def test_remove_table_not_found(db, settings_manager_with_existing_repo):
|
||||
"""Test removing a non-existent table."""
|
||||
with pytest.raises(ValueError, match="Table 'NonExistentTable' does not exist in repository 'ExistingRepo'."):
|
||||
db.remove_table("ExistingRepo", "NonExistentTable")
|
||||
|
||||
|
||||
def test_remove_table_empty_name(db, settings_manager_with_existing_repo):
|
||||
"""Test removing a table with empty/None name."""
|
||||
with pytest.raises(ValueError, match="Table name cannot be empty."):
|
||||
db.remove_table("ExistingRepo", "")
|
||||
with pytest.raises(ValueError, match="Table name cannot be empty."):
|
||||
db.remove_table("ExistingRepo", None)
|
||||
|
||||
|
||||
def test_remove_table_empty_repository_name(db):
|
||||
"""Test removing a table with empty/None repository name."""
|
||||
with pytest.raises(ValueError, match="Repository name cannot be empty."):
|
||||
db.remove_table("", "Table1")
|
||||
with pytest.raises(ValueError, match="Repository name cannot be empty."):
|
||||
db.remove_table(None, "Table1")
|
||||
|
||||
@@ -1,116 +1,88 @@
|
||||
from unittest.mock import MagicMock
|
||||
import dataclasses
|
||||
|
||||
import pytest
|
||||
|
||||
from core.settings_management import SettingsManager, DummyDbEngine
|
||||
from core.settings_objects import BudgetTrackerSettings, BudgetTrackerMappings, BUDGET_TRACKER_MAPPINGS_ENTRY
|
||||
from core.settings_management import SettingsManager, MemoryDbEngine
|
||||
|
||||
FAKE_USER_ID = "FakeUserId"
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class DummyObject:
|
||||
a: int
|
||||
b: str
|
||||
c: bool
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class DummySettings:
|
||||
prop1: DummyObject
|
||||
prop2: str
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def manager():
|
||||
return SettingsManager(DummyDbEngine("settings_from_unit_testing.json"))
|
||||
return SettingsManager(MemoryDbEngine())
|
||||
|
||||
|
||||
def test_i_can_save_and_load_settings(manager):
|
||||
settings = BudgetTrackerSettings()
|
||||
manager.save(FAKE_USER_ID, "MyEntry", settings)
|
||||
|
||||
from_db = manager.load(FAKE_USER_ID, "MyEntry")
|
||||
|
||||
assert isinstance(from_db, BudgetTrackerSettings)
|
||||
assert from_db.spread_sheet == settings.spread_sheet
|
||||
assert from_db.col_row_num == settings.col_row_num
|
||||
assert from_db.col_project == settings.col_project
|
||||
assert from_db.col_owner == settings.col_owner
|
||||
assert from_db.col_capex == settings.col_capex
|
||||
assert from_db.col_details == settings.col_details
|
||||
assert from_db.col_supplier == settings.col_supplier
|
||||
assert from_db.col_budget_amt == settings.col_budget_amt
|
||||
assert from_db.col_actual_amt == settings.col_actual_amt
|
||||
assert from_db.col_forecast5_7_amt == settings.col_forecast5_7_amt
|
||||
@pytest.fixture()
|
||||
def settings():
|
||||
return DummySettings(
|
||||
prop1=DummyObject(1, "2", True),
|
||||
prop2="prop2_new",
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_db_engine():
|
||||
"""Fixture to mock the _db_engine instance."""
|
||||
return MagicMock()
|
||||
def test_i_can_save_and_load_settings(session, manager, settings):
|
||||
manager.save(session, "MyEntry", settings)
|
||||
|
||||
from_db = manager.load(session, "MyEntry")
|
||||
|
||||
assert isinstance(from_db, DummySettings)
|
||||
assert from_db.prop1.a == 1
|
||||
assert from_db.prop1.b == "2"
|
||||
assert from_db.prop1.c == True
|
||||
assert from_db.prop2 == "prop2_new"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def settings_manager(mock_db_engine):
|
||||
"""Fixture to provide an instance of SettingsManager with a mocked db engine."""
|
||||
return SettingsManager(engine=mock_db_engine)
|
||||
def test_i_can_have_two_entries(session, manager, settings):
|
||||
manager.save(session, "MyEntry", settings)
|
||||
manager.save(session, "MyOtherEntry", settings)
|
||||
|
||||
from_db = manager.load(session, "MyEntry")
|
||||
from_db_other = manager.load(session, "MyOtherEntry")
|
||||
|
||||
assert isinstance(from_db, DummySettings)
|
||||
assert isinstance(from_db_other, DummySettings)
|
||||
|
||||
|
||||
def test_get_successful(settings_manager, mock_db_engine):
|
||||
"""Test successful retrieval of a value."""
|
||||
# Arrange
|
||||
session = {"user_id": "user123", "user_email": "user@example.com"}
|
||||
mock_db_engine.get.return_value = "mock_value"
|
||||
|
||||
# Act
|
||||
result = settings_manager.get(session=session, key="theme")
|
||||
|
||||
# Assert
|
||||
assert result == "mock_value"
|
||||
mock_db_engine.get.assert_called_once_with("user@example.com", "user123", "theme")
|
||||
def test_i_can_put_many_items_dict(session, manager):
|
||||
manager.save(session, "TestEntry", {})
|
||||
|
||||
items = {
|
||||
'key1': 'value1',
|
||||
'key2': 'value2',
|
||||
'key3': 'value3'
|
||||
}
|
||||
manager.put_many(session, "TestEntry", items)
|
||||
|
||||
loaded = manager.load(session, "TestEntry")
|
||||
assert loaded['key1'] == 'value1'
|
||||
assert loaded['key2'] == 'value2'
|
||||
assert loaded['key3'] == 'value3'
|
||||
|
||||
|
||||
def test_get_key_error_no_default(settings_manager, mock_db_engine):
|
||||
"""Test KeyError is raised if key doesn't exist and default is NoDefault."""
|
||||
# Arrange
|
||||
session = {"user_id": "user123", "user_email": "user@example.com"}
|
||||
mock_db_engine.get.side_effect = KeyError # Simulate missing key
|
||||
|
||||
# Act & Assert
|
||||
with pytest.raises(KeyError):
|
||||
settings_manager.get(session=session, key="theme")
|
||||
|
||||
|
||||
def test_get_key_error_with_default(settings_manager, mock_db_engine):
|
||||
"""Test default value is returned if key doesn't exist and default is provided."""
|
||||
# Arrange
|
||||
session = {"user_id": "user123", "user_email": "user@example.com"}
|
||||
mock_db_engine.get.side_effect = KeyError # Simulate missing key
|
||||
|
||||
# Act
|
||||
result = settings_manager.get(session=session, key="theme", default="default_value")
|
||||
|
||||
# Assert
|
||||
assert result == "default_value"
|
||||
mock_db_engine.get.assert_called_once_with("user@example.com", "user123", "theme")
|
||||
|
||||
|
||||
def test_get_key_none(settings_manager, mock_db_engine):
|
||||
"""Test behavior when key is None."""
|
||||
# Arrange
|
||||
session = {"user_id": "user123", "user_email": "user@example.com"}
|
||||
mock_db_engine.get.return_value = {"example_key": "example_value"}
|
||||
|
||||
# Act
|
||||
result = settings_manager.get(session=session, key=None)
|
||||
|
||||
# Assert
|
||||
assert result == {"example_key": "example_value"}
|
||||
mock_db_engine.get.assert_called_once_with("user@example.com", "user123", None)
|
||||
#
|
||||
# def test_i_can_save_and_load_mapping_settings(manager):
|
||||
# """
|
||||
# I test 'BudgetTrackerMappings' because there is an object inside an object
|
||||
# :param manager:
|
||||
# :return:
|
||||
# """
|
||||
# settings = BudgetTrackerMappings(mappings=[
|
||||
# BudgetTrackerMappings.Mapping(1, "p1", "o1", "d1", "s1", "l1_1", "l2_1", "l3_1", 0),
|
||||
# BudgetTrackerMappings.Mapping(1, "p2", "o2", "d2", "s2", "l1_2", "l2_2", "l3_2", 10)])
|
||||
#
|
||||
# manager.save(FAKE_USER_ID, BUDGET_TRACKER_MAPPINGS_ENTRY, settings)
|
||||
# from_db = manager.load(FAKE_USER_ID, BUDGET_TRACKER_MAPPINGS_ENTRY)
|
||||
#
|
||||
# assert isinstance(from_db, BudgetTrackerMappings)
|
||||
# assert len(from_db.mappings) == 2
|
||||
# assert isinstance(from_db.mappings[0], BudgetTrackerMappings.Mapping)
|
||||
# assert from_db.mappings[0].col_project == "p1"
|
||||
# assert from_db.mappings[1].col_project == "p2"
|
||||
def test_i_can_put_many_items_list(session, manager):
|
||||
manager.save(session, "TestEntry", {})
|
||||
|
||||
items = [
|
||||
('key1', 'value1'),
|
||||
('key2', 'value2'),
|
||||
('key3', 'value3')
|
||||
]
|
||||
manager.put_many(session, "TestEntry", items)
|
||||
|
||||
loaded = manager.load(session, "TestEntry")
|
||||
assert loaded['key1'] == 'value1'
|
||||
assert loaded['key2'] == 'value2'
|
||||
assert loaded['key3'] == 'value3'
|
||||
|
||||
@@ -4,7 +4,7 @@ from fasthtml.components import *
|
||||
|
||||
from components.tabs.components.MyTabs import Tab, MyTabs
|
||||
from components.tabs.constants import ROUTE_ROOT, Routes
|
||||
from tests.helpers import matches, find_first_match
|
||||
from tests.helpers import matches, find_first_match, search_elements_by_name, div_ellipsis
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@@ -105,6 +105,7 @@ def test_add_tab_with_icon_attribute(tabs_instance):
|
||||
assert tabs_instance.tabs[0].id == tab_id
|
||||
assert tabs_instance.tabs[0].icon == icon
|
||||
|
||||
|
||||
def test_remove_tab(tabs_instance):
|
||||
"""Test the remove_tab method."""
|
||||
# Add some tabs
|
||||
@@ -171,41 +172,48 @@ def test_do_no_change_the_active_tab_if_another_tab_is_removed(tabs_instance):
|
||||
|
||||
|
||||
def test_render_empty_when_empty(tabs_instance):
|
||||
expected = Div(id=tabs_instance._id)
|
||||
actual = tabs_instance.__ft__()
|
||||
assert matches(tabs_instance.__ft__(), expected)
|
||||
expected = Div(id=tabs_instance._id)
|
||||
assert matches(actual, expected)
|
||||
|
||||
|
||||
def test_render_empty_when_multiple_tabs(tabs_instance):
|
||||
def test_render_when_multiple_tabs(tabs_instance):
|
||||
tabs_instance.tabs = [
|
||||
Tab("1", "Tab1", "Content 1"),
|
||||
Tab("2", "Tab2", "Content 2", active=True),
|
||||
Tab("3", "Tab3", "Content 3"),
|
||||
]
|
||||
|
||||
actual = tabs_instance.__ft__()
|
||||
to_compare = search_elements_by_name(actual, "div", {"id": tabs_instance._id})
|
||||
|
||||
expected = Div(
|
||||
Span(cls="tabs-tab "),
|
||||
Span(cls="tabs-tab tabs-active"),
|
||||
Span(cls="tabs-tab "),
|
||||
Div("Content 2", cls="tabs-content"),
|
||||
Div(
|
||||
Span(cls="mmt-tabs-tab "),
|
||||
Span(cls="mmt-tabs-tab mmt-tabs-active"),
|
||||
Span(cls="mmt-tabs-tab "),
|
||||
cls="mmt-tabs-header"
|
||||
),
|
||||
Div("Content 2", cls="mmt-tabs-content"),
|
||||
id=tabs_instance._id,
|
||||
cls="tabs",
|
||||
cls="mmt-tabs",
|
||||
)
|
||||
|
||||
actual = tabs_instance.__ft__()
|
||||
assert matches(actual, expected)
|
||||
assert matches(to_compare[0], expected)
|
||||
|
||||
|
||||
def test_render_a_tab_has_label_and_a_cross_with_correct_hx_posts(tabs_instance):
|
||||
def test_render_a_tab_header_with_its_name_and_the_cross_to_close(tabs_instance):
|
||||
tabs_instance.tabs = [
|
||||
Tab("1", "Tab1", "Content 1"),
|
||||
]
|
||||
|
||||
actual = tabs_instance.__ft__()
|
||||
|
||||
expected = Span(
|
||||
Label("Tab1", hx_post=f"{ROUTE_ROOT}{Routes.SelectTab}"),
|
||||
Label(div_ellipsis("Tab1"), hx_post=f"{ROUTE_ROOT}{Routes.SelectTab}"),
|
||||
Div(NotStr('<svg name="close"'), hx_post=f"{ROUTE_ROOT}{Routes.RemoveTab}"),
|
||||
cls="tabs-tab "
|
||||
cls="mmt-tabs-tab "
|
||||
)
|
||||
|
||||
actual = find_first_match(tabs_instance.__ft__(), "div.span")
|
||||
assert matches(actual, expected)
|
||||
to_compare = find_first_match(actual, "div.div.span")
|
||||
assert matches(to_compare, expected)
|
||||
|
||||
Reference in New Issue
Block a user