124 lines
4.8 KiB
Python
124 lines
4.8 KiB
Python
import json
|
|
import subprocess
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from tests.dashboard_bundle import dashboard
|
|
|
|
|
|
DOCK = Path(__file__).resolve().parents[1] / "frontend" / "mobile-task-dock.js"
|
|
|
|
|
|
def test_mobile_task_dock_routes_actions_hides_for_overlays_and_restores_focus():
|
|
script = f"""
|
|
const createDock = require({json.dumps(str(DOCK))});
|
|
class FakeElement {{
|
|
constructor() {{
|
|
this.listeners = {{}};
|
|
this.attributes = {{}};
|
|
this.classList = {{ values:new Set(), contains:value => this.classList.values.has(value) }};
|
|
this.hidden = false;
|
|
this.focuses = 0;
|
|
}}
|
|
addEventListener(name, callback) {{ this.listeners[name] = callback; }}
|
|
click() {{ this.listeners.click({{currentTarget:this}}); }}
|
|
setAttribute(name, value) {{ this.attributes[name] = value; }}
|
|
removeAttribute(name) {{ delete this.attributes[name]; }}
|
|
focus() {{ this.focuses += 1; }}
|
|
}}
|
|
const nav = new FakeElement();
|
|
const buttons = Object.fromEntries(['work','find','new','search','drafts'].map(name => [name, new FakeElement()]));
|
|
const overlay = new FakeElement();
|
|
const calls = [];
|
|
let observerCallback;
|
|
const dock = createDock({{
|
|
nav, buttons, overlays:[overlay],
|
|
actions: Object.fromEntries(Object.keys(buttons).map(name => [name, () => calls.push(name)])),
|
|
observe(callback) {{ observerCallback = callback; return {{disconnect() {{}}}}; }},
|
|
}});
|
|
dock.start();
|
|
buttons.work.click();
|
|
buttons.find.click();
|
|
overlay.classList.values.add('open'); observerCallback();
|
|
const hiddenWhileOpen = nav.hidden;
|
|
overlay.classList.values.delete('open'); observerCallback();
|
|
buttons.drafts.click();
|
|
process.stdout.write(JSON.stringify({{
|
|
calls, hiddenWhileOpen, hiddenAfterClose:nav.hidden,
|
|
findFocuses:buttons.find.focuses,
|
|
current:Object.fromEntries(Object.entries(buttons).map(([name, button]) => [name, button.attributes['aria-current'] || null])),
|
|
}}));
|
|
"""
|
|
result = subprocess.run(
|
|
["node", "-e", script], capture_output=True, text=True
|
|
)
|
|
|
|
assert result.returncode == 0, result.stderr
|
|
assert json.loads(result.stdout) == {
|
|
"calls": ["work", "find", "drafts"],
|
|
"hiddenWhileOpen": True,
|
|
"hiddenAfterClose": False,
|
|
"findFocuses": 1,
|
|
"current": {
|
|
"work": None,
|
|
"find": None,
|
|
"new": None,
|
|
"search": None,
|
|
"drafts": "page",
|
|
},
|
|
}
|
|
|
|
|
|
def test_mobile_task_dock_exposes_and_hides_deduplicated_attention_count():
|
|
script = f"""
|
|
const createDock = require({json.dumps(str(DOCK))});
|
|
const work = {{ attributes: {{}}, setAttribute(name, value) {{ this.attributes[name] = value; }} }};
|
|
const badge = {{ textContent:'', hidden:true }};
|
|
const dock = createDock({{ nav:{{}}, buttons:{{work}}, attentionBadge:badge }});
|
|
dock.updateAttention(3);
|
|
const pending = {{ count:badge.textContent, hidden:badge.hidden, label:work.attributes['aria-label'] }};
|
|
dock.updateAttention(0);
|
|
process.stdout.write(JSON.stringify({{
|
|
pending,
|
|
empty:{{ count:badge.textContent, hidden:badge.hidden, label:work.attributes['aria-label'] }},
|
|
}}));
|
|
"""
|
|
result = subprocess.run(
|
|
["node", "-e", script], capture_output=True, text=True
|
|
)
|
|
|
|
assert result.returncode == 0, result.stderr
|
|
assert json.loads(result.stdout) == {
|
|
"pending": {"count": "3", "hidden": False, "label": "Work, 3 items need attention"},
|
|
"empty": {"count": "0", "hidden": True, "label": "Work"},
|
|
}
|
|
|
|
|
|
@pytest.mark.anyio
|
|
async def test_dashboard_renders_and_wires_phone_safe_task_dock():
|
|
html = await dashboard()
|
|
|
|
assert '<nav class="mobile-task-dock" id="mobile-task-dock" aria-label="Primary tasks">' in html
|
|
assert html.count('class="mobile-task-action" data-mobile-task=') == 5
|
|
for task in ("work", "find", "new", "search", "drafts"):
|
|
assert f'data-mobile-task="{task}"' in html
|
|
assert 'id="mobile-draft-count"' in html
|
|
assert 'data-work-filter="attention"' in html
|
|
assert 'id="mobile-attention-count"' in html
|
|
assert '.mobile-task-dock { display:none;' in html
|
|
assert 'grid-template-columns:repeat(5,minmax(0,1fr))' in html
|
|
assert 'padding-bottom:env(safe-area-inset-bottom)' in html
|
|
assert '.mobile-task-action { min-width:0; min-height:44px;' in html
|
|
assert '<script src="static/mobile-task-dock.js"></script>' in html
|
|
assert "createMobileTaskDock({" in html
|
|
assert "work: openMobileWork" in html
|
|
assert "counts.attention ? 'attention' : 'all'" in html
|
|
assert "qs('[data-work-filter=\"' + filter + '\"]').click()" in html
|
|
assert "find: () => qs('#find-work').click()" in html
|
|
assert "new: () => qs('#new-issue').click()" in html
|
|
assert "search: () => qs('#open-palette').click()" in html
|
|
assert "drafts: () => qs('[data-work-filter=\"draft\"]').click()" in html
|
|
assert "draftCount.textContent = sourceDraftCount.textContent" in html
|
|
assert "mobileTaskDock.updateAttention(counts.attention)" in html
|