feat: US-006 - Frontend: Icon picker as compact dropdown
This commit is contained in:
@@ -626,7 +626,84 @@
|
|||||||
box-shadow: 0 0 0 2px var(--bg-surface), 0 0 0 4px var(--accent);
|
box-shadow: 0 0 0 2px var(--bg-surface), 0 0 0 4px var(--accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Icon picker */
|
/* Icon picker dropdown */
|
||||||
|
.icon-picker-dropdown {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-picker-trigger {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-2);
|
||||||
|
padding: var(--space-2) var(--space-3);
|
||||||
|
background: var(--bg-surface);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all var(--transition-base);
|
||||||
|
min-height: 44px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-picker-trigger:hover {
|
||||||
|
background: var(--bg-hover);
|
||||||
|
border-color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-picker-trigger svg {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-picker-trigger span {
|
||||||
|
flex: 1;
|
||||||
|
text-align: left;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.trigger-chevron {
|
||||||
|
transition: transform var(--transition-base);
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-picker-trigger.open .trigger-chevron {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-picker-content {
|
||||||
|
position: absolute;
|
||||||
|
top: calc(100% + var(--space-1));
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
background: var(--bg-surface);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||||
|
z-index: 100;
|
||||||
|
display: none;
|
||||||
|
max-height: 300px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-picker-content.visible {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-search-input {
|
||||||
|
width: 100%;
|
||||||
|
padding: var(--space-2);
|
||||||
|
border: none;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
background: var(--bg-base);
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-search-input:focus {
|
||||||
|
border-bottom-color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
.icon-picker-grid {
|
.icon-picker-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(6, 1fr);
|
grid-template-columns: repeat(6, 1fr);
|
||||||
@@ -634,9 +711,6 @@
|
|||||||
max-height: 250px;
|
max-height: 250px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
padding: var(--space-2);
|
padding: var(--space-2);
|
||||||
border: 1px solid var(--border);
|
|
||||||
border-radius: var(--radius-md);
|
|
||||||
background: var(--bg-base);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-option {
|
.icon-option {
|
||||||
@@ -649,6 +723,7 @@
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all var(--transition-base);
|
transition: all var(--transition-base);
|
||||||
background: var(--bg-surface);
|
background: var(--bg-surface);
|
||||||
|
min-height: 44px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-option:hover {
|
.icon-option:hover {
|
||||||
@@ -1125,8 +1200,18 @@
|
|||||||
<!-- Icon -->
|
<!-- Icon -->
|
||||||
<div class="form-field">
|
<div class="form-field">
|
||||||
<label class="form-label">Icon</label>
|
<label class="form-label">Icon</label>
|
||||||
|
<div class="icon-picker-dropdown">
|
||||||
|
<button type="button" class="icon-picker-trigger" id="iconPickerTrigger" onclick="toggleIconPicker()">
|
||||||
|
<i data-lucide="smile" id="selectedIconDisplay"></i>
|
||||||
|
<span>Select Icon</span>
|
||||||
|
<i data-lucide="chevron-down" class="trigger-chevron" id="iconPickerChevron"></i>
|
||||||
|
</button>
|
||||||
|
<div class="icon-picker-content" id="iconPickerContent">
|
||||||
|
<input type="text" id="iconSearch" class="icon-search-input" placeholder="Search icons..." oninput="filterIcons()">
|
||||||
<div class="icon-picker-grid" id="iconPicker"></div>
|
<div class="icon-picker-grid" id="iconPicker"></div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Priority -->
|
<!-- Priority -->
|
||||||
<div class="form-field">
|
<div class="form-field">
|
||||||
@@ -1855,6 +1940,69 @@
|
|||||||
<i data-lucide="${icon}"></i>
|
<i data-lucide="${icon}"></i>
|
||||||
</div>`
|
</div>`
|
||||||
).join('');
|
).join('');
|
||||||
|
|
||||||
|
// Update trigger button with selected icon
|
||||||
|
const selectedIconDisplay = document.getElementById('selectedIconDisplay');
|
||||||
|
selectedIconDisplay.setAttribute('data-lucide', selectedIcon);
|
||||||
|
|
||||||
|
lucide.createIcons();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Toggle icon picker dropdown
|
||||||
|
function toggleIconPicker() {
|
||||||
|
const content = document.getElementById('iconPickerContent');
|
||||||
|
const trigger = document.getElementById('iconPickerTrigger');
|
||||||
|
const isOpen = content.classList.contains('visible');
|
||||||
|
|
||||||
|
if (isOpen) {
|
||||||
|
closeIconPicker();
|
||||||
|
} else {
|
||||||
|
openIconPicker();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open icon picker dropdown
|
||||||
|
function openIconPicker() {
|
||||||
|
const content = document.getElementById('iconPickerContent');
|
||||||
|
const trigger = document.getElementById('iconPickerTrigger');
|
||||||
|
const search = document.getElementById('iconSearch');
|
||||||
|
|
||||||
|
content.classList.add('visible');
|
||||||
|
trigger.classList.add('open');
|
||||||
|
|
||||||
|
// Reset search
|
||||||
|
search.value = '';
|
||||||
|
filterIcons();
|
||||||
|
|
||||||
|
// Focus search input
|
||||||
|
setTimeout(() => search.focus(), 50);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close icon picker dropdown
|
||||||
|
function closeIconPicker() {
|
||||||
|
const content = document.getElementById('iconPickerContent');
|
||||||
|
const trigger = document.getElementById('iconPickerTrigger');
|
||||||
|
|
||||||
|
content.classList.remove('visible');
|
||||||
|
trigger.classList.remove('open');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter icons based on search query
|
||||||
|
function filterIcons() {
|
||||||
|
const searchQuery = document.getElementById('iconSearch').value.toLowerCase();
|
||||||
|
const iconPickerContainer = document.getElementById('iconPicker');
|
||||||
|
|
||||||
|
const filteredIcons = commonIcons.filter(icon =>
|
||||||
|
icon.toLowerCase().includes(searchQuery)
|
||||||
|
);
|
||||||
|
|
||||||
|
iconPickerContainer.innerHTML = filteredIcons.map(icon =>
|
||||||
|
`<div class="icon-option ${icon === selectedIcon ? 'selected' : ''}"
|
||||||
|
onclick="selectIcon('${icon}')">
|
||||||
|
<i data-lucide="${icon}"></i>
|
||||||
|
</div>`
|
||||||
|
).join('');
|
||||||
|
|
||||||
lucide.createIcons();
|
lucide.createIcons();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1864,13 +2012,20 @@
|
|||||||
|
|
||||||
// Update icon options
|
// Update icon options
|
||||||
const iconOptions = document.querySelectorAll('.icon-option');
|
const iconOptions = document.querySelectorAll('.icon-option');
|
||||||
iconOptions.forEach((option, index) => {
|
iconOptions.forEach(option => {
|
||||||
if (commonIcons[index] === icon) {
|
|
||||||
option.classList.add('selected');
|
|
||||||
} else {
|
|
||||||
option.classList.remove('selected');
|
option.classList.remove('selected');
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Add selected class to clicked option
|
||||||
|
event.target.closest('.icon-option')?.classList.add('selected');
|
||||||
|
|
||||||
|
// Update trigger button display
|
||||||
|
const selectedIconDisplay = document.getElementById('selectedIconDisplay');
|
||||||
|
selectedIconDisplay.setAttribute('data-lucide', icon);
|
||||||
|
lucide.createIcons();
|
||||||
|
|
||||||
|
// Close dropdown after selection
|
||||||
|
closeIconPicker();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update frequency params based on selected type
|
// Update frequency params based on selected type
|
||||||
@@ -2472,6 +2627,18 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Click outside icon picker to close
|
||||||
|
document.addEventListener('click', (e) => {
|
||||||
|
const dropdown = document.querySelector('.icon-picker-dropdown');
|
||||||
|
const content = document.getElementById('iconPickerContent');
|
||||||
|
|
||||||
|
if (content && content.classList.contains('visible')) {
|
||||||
|
if (!dropdown.contains(e.target)) {
|
||||||
|
closeIconPicker();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Restore collapsed/expanded state from localStorage
|
// Restore collapsed/expanded state from localStorage
|
||||||
restoreWeeklySummaryState();
|
restoreWeeklySummaryState();
|
||||||
|
|
||||||
|
|||||||
@@ -1767,9 +1767,23 @@ def run_all_tests():
|
|||||||
test_css_transition_300ms,
|
test_css_transition_300ms,
|
||||||
test_height_constraint_collapsed,
|
test_height_constraint_collapsed,
|
||||||
test_typecheck_us005,
|
test_typecheck_us005,
|
||||||
|
# US-006 tests
|
||||||
|
test_icon_picker_is_dropdown,
|
||||||
|
test_icon_picker_has_search_input,
|
||||||
|
test_icon_picker_max_height_300px,
|
||||||
|
test_icon_picker_toggle_functions,
|
||||||
|
test_icon_picker_visible_class_toggle,
|
||||||
|
test_icon_picker_filter_function,
|
||||||
|
test_icon_picker_closes_on_selection,
|
||||||
|
test_icon_picker_click_outside_closes,
|
||||||
|
test_icon_picker_updates_trigger_display,
|
||||||
|
test_icon_picker_css_dropdown_styles,
|
||||||
|
test_icon_picker_chevron_rotation,
|
||||||
|
test_icon_picker_search_autofocus,
|
||||||
|
test_typecheck_us006,
|
||||||
]
|
]
|
||||||
|
|
||||||
print(f"\nRunning {len(tests)} frontend tests for US-002, US-003, US-005, US-006 through US-014...\n")
|
print(f"\nRunning {len(tests)} frontend tests for US-002, US-003, US-004, US-005, US-006 through US-014...\n")
|
||||||
|
|
||||||
failed = []
|
failed = []
|
||||||
for test in tests:
|
for test in tests:
|
||||||
@@ -2530,3 +2544,190 @@ def test_typecheck_us005():
|
|||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
run_all_tests()
|
run_all_tests()
|
||||||
|
|
||||||
|
# US-006: Frontend: Icon picker as compact dropdown
|
||||||
|
def test_icon_picker_is_dropdown():
|
||||||
|
"""Test 151: Icon picker renders as dropdown with trigger button"""
|
||||||
|
habits_path = Path(__file__).parent.parent / 'habits.html'
|
||||||
|
html = habits_path.read_text()
|
||||||
|
|
||||||
|
# Check for dropdown structure
|
||||||
|
assert 'icon-picker-dropdown' in html, "Icon picker should be a dropdown"
|
||||||
|
assert 'icon-picker-trigger' in html, "Dropdown should have trigger button"
|
||||||
|
assert 'icon-picker-content' in html, "Dropdown should have content container"
|
||||||
|
|
||||||
|
# Check trigger button has icon display and chevron
|
||||||
|
assert 'selectedIconDisplay' in html, "Trigger should display selected icon"
|
||||||
|
assert 'iconPickerChevron' in html, "Trigger should have chevron icon"
|
||||||
|
assert 'chevron-down' in html, "Chevron should be down arrow"
|
||||||
|
|
||||||
|
print("✓ Test 151 passed: Icon picker is dropdown structure")
|
||||||
|
|
||||||
|
def test_icon_picker_has_search_input():
|
||||||
|
"""Test 152: Dropdown has search input for filtering icons"""
|
||||||
|
habits_path = Path(__file__).parent.parent / 'habits.html'
|
||||||
|
html = habits_path.read_text()
|
||||||
|
|
||||||
|
# Check for search input
|
||||||
|
assert 'icon-search-input' in html, "Dropdown should have search input"
|
||||||
|
assert 'id="iconSearch"' in html, "Search input should have iconSearch id"
|
||||||
|
assert 'placeholder="Search icons..."' in html, "Search input should have placeholder"
|
||||||
|
assert 'oninput="filterIcons()"' in html, "Search input should trigger filterIcons()"
|
||||||
|
|
||||||
|
print("✓ Test 152 passed: Dropdown has search input")
|
||||||
|
|
||||||
|
def test_icon_picker_max_height_300px():
|
||||||
|
"""Test 153: Dropdown content has max-height 300px with scroll"""
|
||||||
|
habits_path = Path(__file__).parent.parent / 'habits.html'
|
||||||
|
html = habits_path.read_text()
|
||||||
|
|
||||||
|
# Check for max-height in CSS
|
||||||
|
assert 'max-height: 300px' in html, "Icon picker content should have max-height 300px"
|
||||||
|
assert 'overflow-y: auto' in html or 'overflow: hidden' in html, "Should have overflow handling"
|
||||||
|
|
||||||
|
print("✓ Test 153 passed: Max-height 300px with scroll")
|
||||||
|
|
||||||
|
def test_icon_picker_toggle_functions():
|
||||||
|
"""Test 154: Toggle functions exist for opening/closing dropdown"""
|
||||||
|
habits_path = Path(__file__).parent.parent / 'habits.html'
|
||||||
|
html = habits_path.read_text()
|
||||||
|
|
||||||
|
# Check for toggle functions
|
||||||
|
assert 'function toggleIconPicker()' in html, "toggleIconPicker function should exist"
|
||||||
|
assert 'function openIconPicker()' in html, "openIconPicker function should exist"
|
||||||
|
assert 'function closeIconPicker()' in html, "closeIconPicker function should exist"
|
||||||
|
assert 'onclick="toggleIconPicker()"' in html, "Trigger should call toggleIconPicker"
|
||||||
|
|
||||||
|
print("✓ Test 154 passed: Toggle functions exist")
|
||||||
|
|
||||||
|
def test_icon_picker_visible_class_toggle():
|
||||||
|
"""Test 155: Dropdown uses visible class to show/hide content"""
|
||||||
|
habits_path = Path(__file__).parent.parent / 'habits.html'
|
||||||
|
html = habits_path.read_text()
|
||||||
|
|
||||||
|
# Check for visible class handling in CSS
|
||||||
|
assert '.icon-picker-content.visible' in html, "CSS should have visible class rule"
|
||||||
|
assert 'display: block' in html, "Visible class should display content"
|
||||||
|
assert 'display: none' in html, "Hidden state should have display none"
|
||||||
|
|
||||||
|
# Check for visible class toggling in JS
|
||||||
|
assert "classList.add('visible')" in html, "Should add visible class to open"
|
||||||
|
assert "classList.remove('visible')" in html, "Should remove visible class to close"
|
||||||
|
|
||||||
|
print("✓ Test 155 passed: Visible class toggle logic")
|
||||||
|
|
||||||
|
def test_icon_picker_filter_function():
|
||||||
|
"""Test 156: filterIcons function filters icons based on search"""
|
||||||
|
habits_path = Path(__file__).parent.parent / 'habits.html'
|
||||||
|
html = habits_path.read_text()
|
||||||
|
|
||||||
|
# Check for filterIcons function
|
||||||
|
assert 'function filterIcons()' in html, "filterIcons function should exist"
|
||||||
|
|
||||||
|
# Check filter logic
|
||||||
|
search_section = html[html.find('function filterIcons()'):html.find('function filterIcons()') + 1000]
|
||||||
|
assert 'toLowerCase()' in search_section, "Should use case-insensitive search"
|
||||||
|
assert 'filter' in search_section, "Should use filter method"
|
||||||
|
assert 'includes' in search_section, "Should check if icon name includes query"
|
||||||
|
|
||||||
|
print("✓ Test 156 passed: filterIcons function implementation")
|
||||||
|
|
||||||
|
def test_icon_picker_closes_on_selection():
|
||||||
|
"""Test 157: Selecting an icon closes the dropdown"""
|
||||||
|
habits_path = Path(__file__).parent.parent / 'habits.html'
|
||||||
|
html = habits_path.read_text()
|
||||||
|
|
||||||
|
# Check that selectIcon calls closeIconPicker
|
||||||
|
select_icon_section = html[html.find('function selectIcon('):html.find('function selectIcon(') + 1500]
|
||||||
|
assert 'closeIconPicker()' in select_icon_section, "selectIcon should call closeIconPicker"
|
||||||
|
|
||||||
|
print("✓ Test 157 passed: Selection closes dropdown")
|
||||||
|
|
||||||
|
def test_icon_picker_click_outside_closes():
|
||||||
|
"""Test 158: Clicking outside dropdown closes it"""
|
||||||
|
habits_path = Path(__file__).parent.parent / 'habits.html'
|
||||||
|
html = habits_path.read_text()
|
||||||
|
|
||||||
|
# Check for click outside listener comment and handler
|
||||||
|
assert "Click outside icon picker to close" in html, "Should have icon picker click-outside comment"
|
||||||
|
|
||||||
|
# Find the icon picker click handler (after the comment)
|
||||||
|
click_handler_start = html.find("Click outside icon picker to close")
|
||||||
|
if click_handler_start > 0:
|
||||||
|
click_handler = html[click_handler_start:click_handler_start + 500]
|
||||||
|
assert 'icon-picker-dropdown' in click_handler, "Should reference icon-picker-dropdown"
|
||||||
|
assert 'contains' in click_handler, "Should check if click is inside dropdown"
|
||||||
|
assert 'closeIconPicker()' in click_handler, "Should close if clicked outside"
|
||||||
|
|
||||||
|
print("✓ Test 158 passed: Click outside closes dropdown")
|
||||||
|
|
||||||
|
def test_icon_picker_updates_trigger_display():
|
||||||
|
"""Test 159: Selected icon updates trigger button display"""
|
||||||
|
habits_path = Path(__file__).parent.parent / 'habits.html'
|
||||||
|
html = habits_path.read_text()
|
||||||
|
|
||||||
|
# Check that selectIcon updates the trigger display
|
||||||
|
select_icon_section = html[html.find('function selectIcon('):html.find('function selectIcon(') + 1500]
|
||||||
|
assert 'selectedIconDisplay' in select_icon_section, "Should update selectedIconDisplay element"
|
||||||
|
assert 'setAttribute' in select_icon_section, "Should set icon attribute"
|
||||||
|
assert 'data-lucide' in select_icon_section, "Should update data-lucide attribute"
|
||||||
|
assert 'lucide.createIcons()' in select_icon_section, "Should refresh Lucide icons"
|
||||||
|
|
||||||
|
print("✓ Test 159 passed: Trigger display updates on selection")
|
||||||
|
|
||||||
|
def test_icon_picker_css_dropdown_styles():
|
||||||
|
"""Test 160: CSS includes dropdown-specific styles"""
|
||||||
|
habits_path = Path(__file__).parent.parent / 'habits.html'
|
||||||
|
html = habits_path.read_text()
|
||||||
|
|
||||||
|
# Check for dropdown-specific CSS
|
||||||
|
assert '.icon-picker-dropdown' in html, "Should have dropdown container styles"
|
||||||
|
assert '.icon-picker-trigger' in html, "Should have trigger button styles"
|
||||||
|
assert '.icon-picker-content' in html, "Should have content container styles"
|
||||||
|
assert 'position: absolute' in html, "Content should be absolutely positioned"
|
||||||
|
assert 'z-index: 100' in html, "Content should have high z-index"
|
||||||
|
|
||||||
|
# Check for hover states
|
||||||
|
assert '.icon-picker-trigger:hover' in html, "Should have trigger hover state"
|
||||||
|
|
||||||
|
print("✓ Test 160 passed: Dropdown CSS styles present")
|
||||||
|
|
||||||
|
def test_icon_picker_chevron_rotation():
|
||||||
|
"""Test 161: Chevron rotates when dropdown opens"""
|
||||||
|
habits_path = Path(__file__).parent.parent / 'habits.html'
|
||||||
|
html = habits_path.read_text()
|
||||||
|
|
||||||
|
# Check for chevron rotation in CSS
|
||||||
|
assert 'trigger-chevron' in html, "Chevron should have trigger-chevron class"
|
||||||
|
assert 'transform: rotate(180deg)' in html, "Chevron should rotate 180deg when open"
|
||||||
|
assert '.open .trigger-chevron' in html or '.icon-picker-trigger.open .trigger-chevron' in html, "Should rotate when trigger has open class"
|
||||||
|
|
||||||
|
# Check that open class is toggled
|
||||||
|
assert "classList.add('open')" in html, "Should add open class"
|
||||||
|
assert "classList.remove('open')" in html, "Should remove open class"
|
||||||
|
|
||||||
|
print("✓ Test 161 passed: Chevron rotation logic")
|
||||||
|
|
||||||
|
def test_icon_picker_search_autofocus():
|
||||||
|
"""Test 162: Search input gets focus when dropdown opens"""
|
||||||
|
habits_path = Path(__file__).parent.parent / 'habits.html'
|
||||||
|
html = habits_path.read_text()
|
||||||
|
|
||||||
|
# Check that openIconPicker focuses the search input
|
||||||
|
open_icon_picker = html[html.find('function openIconPicker()'):html.find('function openIconPicker()') + 800]
|
||||||
|
assert 'focus()' in open_icon_picker, "Should focus search input when opening"
|
||||||
|
assert 'setTimeout' in open_icon_picker, "Should use setTimeout for focus after animation"
|
||||||
|
|
||||||
|
print("✓ Test 162 passed: Search input autofocus")
|
||||||
|
|
||||||
|
def test_typecheck_us006():
|
||||||
|
"""Test 163: Typecheck passes"""
|
||||||
|
result = subprocess.run(
|
||||||
|
['python3', '-m', 'py_compile', 'dashboard/api.py', 'dashboard/habits_helpers.py'],
|
||||||
|
cwd='/home/moltbot/clawd',
|
||||||
|
capture_output=True,
|
||||||
|
text=True
|
||||||
|
)
|
||||||
|
assert result.returncode == 0, f"Typecheck failed: {result.stderr}"
|
||||||
|
print("✓ Test 163 passed: Typecheck successful")
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user