diff --git a/common/COMPONENT_IMPROVEMENTS.md b/common/COMPONENT_IMPROVEMENTS.md
index 1b5a0d2..1e53c67 100644
--- a/common/COMPONENT_IMPROVEMENTS.md
+++ b/common/COMPONENT_IMPROVEMENTS.md
@@ -39,3 +39,5 @@ Zero test coverage for the entire component system.
**Fix**: Add unit tests for each component function — basic rendering, edge cases,
and cache hit/miss verification.
+
+**Done**: 96 unit tests covering all component functions (`Component`, `randomid`, `Popover`, `PopoverTruncated`, `A`, `Button`, `Div`, `Icon`, `Form`, `Input`, `NameWithIcon`, `LinkedPurchase`, `PurchasePrice`, `_render_cached`, `enable_cache`). Includes template rendering, deterministic ID generation, LRU cache behavior, HTML output validation, edge cases, error handling, and model-dependent integration tests.
diff --git a/games/static/base.css b/games/static/base.css
index ab291cb..e377fbd 100644
--- a/games/static/base.css
+++ b/games/static/base.css
@@ -1403,6 +1403,84 @@
font-size: 0.875rem;
}
}
+ .form-input {
+ appearance: none;
+ background-color: #fff;
+ border-color: oklch(55.1% 0.027 264.364);
+ border-width: 1px;
+ border-radius: 0px;
+ padding-top: 0.5rem;
+ padding-right: 0.75rem;
+ padding-bottom: 0.5rem;
+ padding-left: 0.75rem;
+ font-size: 1rem;
+ line-height: 1.5rem;
+ --tw-shadow: 0 0 #0000;
+ &:focus {
+ outline: 2px solid transparent;
+ outline-offset: 2px;
+ --tw-ring-inset: var(--tw-empty,/*!*/ /*!*/);
+ --tw-ring-offset-width: 0px;
+ --tw-ring-offset-color: #fff;
+ --tw-ring-color: oklch(54.6% 0.245 262.881);
+ --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
+ --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);
+ box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
+ border-color: oklch(54.6% 0.245 262.881);
+ }
+ &::placeholder {
+ color: oklch(55.1% 0.027 264.364);
+ opacity: 1;
+ }
+ &::-webkit-datetime-edit-fields-wrapper {
+ padding: 0;
+ }
+ &::-webkit-date-and-time-value {
+ min-height: 1.5em;
+ }
+ &::-webkit-date-and-time-value {
+ text-align: inherit;
+ }
+ &::-webkit-datetime-edit {
+ display: inline-flex;
+ }
+ &::-webkit-datetime-edit {
+ padding-top: 0;
+ padding-bottom: 0;
+ }
+ &::-webkit-datetime-edit-year-field {
+ padding-top: 0;
+ padding-bottom: 0;
+ }
+ &::-webkit-datetime-edit-month-field {
+ padding-top: 0;
+ padding-bottom: 0;
+ }
+ &::-webkit-datetime-edit-day-field {
+ padding-top: 0;
+ padding-bottom: 0;
+ }
+ &::-webkit-datetime-edit-hour-field {
+ padding-top: 0;
+ padding-bottom: 0;
+ }
+ &::-webkit-datetime-edit-minute-field {
+ padding-top: 0;
+ padding-bottom: 0;
+ }
+ &::-webkit-datetime-edit-second-field {
+ padding-top: 0;
+ padding-bottom: 0;
+ }
+ &::-webkit-datetime-edit-millisecond-field {
+ padding-top: 0;
+ padding-bottom: 0;
+ }
+ &::-webkit-datetime-edit-meridiem-field {
+ padding-top: 0;
+ padding-bottom: 0;
+ }
+ }
.block {
display: block;
}
diff --git a/tests/test_components.py b/tests/test_components.py
index 92620b8..b8999e0 100644
--- a/tests/test_components.py
+++ b/tests/test_components.py
@@ -12,6 +12,7 @@ from django.template import TemplateDoesNotExist
from django.utils.safestring import SafeText
from common import components
+from games.models import Platform, Game, Purchase, Session
class RenderCachedImplTest(unittest.TestCase):
@@ -415,5 +416,391 @@ class ComponentReturnTypeTest(unittest.TestCase):
self.assertNotIsInstance(result, tuple)
+class ComponentEdgeCasesTest(unittest.TestCase):
+ """Test Component() edge cases and error handling."""
+
+ def test_no_template_or_tag_name_raises(self):
+ with self.assertRaises(ValueError) as ctx:
+ components.Component(children="hello")
+ self.assertIn("template or tag_name", str(ctx.exception))
+
+ def test_single_string_children_wrapped(self):
+ result = components.Component(tag_name="span", children="hello")
+ self.assertIn("hello", result)
+
+ def test_multiple_children_joined_with_newlines(self):
+ result = components.Component(
+ tag_name="div", children=["a", "b"]
+ )
+ self.assertIn("a", result)
+ self.assertIn("b", result)
+ self.assertIn("
", result)
+ self.assertIn("
", result)
+
+ def test_attributes_serialized_correctly(self):
+ result = components.Component(
+ tag_name="div", attributes=[("class", "foo"), ("id", "bar")]
+ )
+ self.assertIn('class="foo"', result)
+ self.assertIn('id="bar"', result)
+
+ def test_empty_attributes_no_extra_space(self):
+ result = components.Component(tag_name="span", children="x")
+ self.assertEqual(result, "x")
+ self.assertNotIn(" ", result)
+
+ def test_unavailable_icon_falls_back(self):
+ result = components.Icon("zzz_nonexistent_platform")
+ self.assertIsInstance(result, SafeText)
+ self.assertIn("