feat: track actual time through Today (Closes #577)
This commit is contained in:
1 parent
a8da5ce949
commit
7ee10619a4
11 files changed
+288
-10
No files matched your search
@@ -25,7 +25,8 @@ def test_page_runtime_is_one_deterministic_content_addressed_bundle(tmp_path):
|
||||
assert first.runtime_gzip_bytes == second.runtime_gzip_bytes
|
||||
assert gzip.decompress(first.runtime_gzip_bytes) == first.runtime_bytes
|
||||
assert PAGE_SCRIPT.findall(first.dashboard_html) == []
|
||||
assert first.dashboard_html.count("<script src=") == 1
|
||||
assert first.dashboard_html.count("<script src=") == 2
|
||||
assert f'<script src="{first.feature_bundles["today-timer"].runtime_name}"></script>' in first.dashboard_html
|
||||
assert f'<script src="{first.runtime_name}"></script>' in first.dashboard_html
|
||||
assert first.runtime_name.startswith("runtime-")
|
||||
assert first.runtime_name.endswith(".js")
|
||||
@@ -46,8 +47,13 @@ def test_product_workflows_are_stable_lazy_feature_chunks(tmp_path):
|
||||
first = build_frontend(FRONTEND)
|
||||
|
||||
assert set(first.feature_bundles) == {
|
||||
"comment-actions", "issue-capture", "pull-workflow", "push-notifications", "device-setup"
|
||||
"comment-actions", "issue-capture", "pull-workflow", "push-notifications", "device-setup",
|
||||
"today-timer",
|
||||
}
|
||||
assert (
|
||||
f'<script src="{first.feature_bundles["today-timer"].runtime_name}"></script>\n'
|
||||
f'<script src="{first.runtime_name}"></script>'
|
||||
) in first.dashboard_html
|
||||
capture = first.feature_bundles["issue-capture"]
|
||||
pull_workflow = first.feature_bundles["pull-workflow"]
|
||||
assert b"function createIssueCapture" not in first.runtime_bytes
|
||||
|
||||
@@ -9,6 +9,7 @@ from tests.dashboard_bundle import dashboard
|
||||
|
||||
|
||||
MY_WORK = Path(__file__).parents[1] / "frontend" / "my-work.js"
|
||||
TODAY_TIMER = Path(__file__).parents[1] / "frontend" / "today-timer.js"
|
||||
LATER_WORK = Path(__file__).parents[1] / "frontend" / "later-work.js"
|
||||
DETAIL_DEFER = Path(__file__).parents[1] / "frontend" / "detail-defer.js"
|
||||
LATER_PICKER = Path(__file__).parents[1] / "frontend" / "later-picker.js"
|
||||
@@ -1931,6 +1932,130 @@ process.stdout.write(JSON.stringify({{finished,active:session.active()}}));
|
||||
assert json.loads(result.stdout) == {"finished": 1, "active": False}
|
||||
|
||||
|
||||
def test_today_timer_restores_wall_clock_time_once_and_excludes_pauses():
|
||||
script = f"""
|
||||
const createTodayTimer = require({json.dumps(str(TODAY_TIMER))});
|
||||
const values = new Map();
|
||||
const storage = {{
|
||||
getItem:key => values.has(key) ? values.get(key) : null,
|
||||
setItem:(key,value) => values.set(key,value),
|
||||
removeItem:key => values.delete(key),
|
||||
}};
|
||||
let now = 1000;
|
||||
const create = () => createTodayTimer({{
|
||||
storage, getLogin:() => 'Timmy', now:() => now,
|
||||
}});
|
||||
const first = create();
|
||||
first.activate('issue:stackchain/dashboard:577:');
|
||||
now += 10 * 60 * 1000;
|
||||
const beforeReload = first.snapshot();
|
||||
const restored = create();
|
||||
const afterReload = restored.snapshot();
|
||||
restored.pause();
|
||||
now += 5 * 60 * 1000;
|
||||
const whilePaused = restored.snapshot();
|
||||
restored.resume();
|
||||
now += 2 * 60 * 1000;
|
||||
const afterResume = restored.snapshot();
|
||||
process.stdout.write(JSON.stringify({{
|
||||
api:typeof createTodayTimer,
|
||||
beforeReload, afterReload, whilePaused, afterResume,
|
||||
}}));
|
||||
"""
|
||||
result = subprocess.run(
|
||||
["node", "-e", script], capture_output=True, text=True
|
||||
)
|
||||
|
||||
assert result.returncode == 0, result.stderr
|
||||
assert json.loads(result.stdout) == {
|
||||
"api": "function",
|
||||
"beforeReload": {"identity": "issue:stackchain/dashboard:577:", "elapsed_ms": 600000, "running": True},
|
||||
"afterReload": {"identity": "issue:stackchain/dashboard:577:", "elapsed_ms": 600000, "running": True},
|
||||
"whilePaused": {"identity": "issue:stackchain/dashboard:577:", "elapsed_ms": 600000, "running": False},
|
||||
"afterResume": {"identity": "issue:stackchain/dashboard:577:", "elapsed_ms": 720000, "running": True},
|
||||
}
|
||||
|
||||
|
||||
def test_today_timer_switches_items_retains_elapsed_time_and_is_account_bound():
|
||||
script = f"""
|
||||
const createTodayTimer = require({json.dumps(str(TODAY_TIMER))});
|
||||
const values = new Map();
|
||||
const storage = {{
|
||||
getItem:key => values.has(key) ? values.get(key) : null,
|
||||
setItem:(key,value) => values.set(key,value),
|
||||
removeItem:key => values.delete(key),
|
||||
}};
|
||||
let login = 'timmy';
|
||||
let now = 0;
|
||||
const timer = createTodayTimer({{storage,getLogin:() => login,now:() => now}});
|
||||
timer.activate('issue:repo:1:');
|
||||
now = 90000;
|
||||
timer.activate('pull:repo:2:');
|
||||
now = 120000;
|
||||
const first = timer.snapshot('issue:repo:1:');
|
||||
const second = timer.snapshot();
|
||||
timer.stop();
|
||||
now = 300000;
|
||||
const stopped = timer.snapshot();
|
||||
login = 'alexander';
|
||||
const other = createTodayTimer({{storage,getLogin:() => login,now:() => now}}).snapshot();
|
||||
process.stdout.write(JSON.stringify({{first,second,stopped,other}}));
|
||||
"""
|
||||
result = subprocess.run(["node", "-e", script], capture_output=True, text=True)
|
||||
|
||||
assert result.returncode == 0, result.stderr
|
||||
assert json.loads(result.stdout) == {
|
||||
"first": {"identity": "issue:repo:1:", "elapsed_ms": 90000, "running": False},
|
||||
"second": {"identity": "pull:repo:2:", "elapsed_ms": 30000, "running": True},
|
||||
"stopped": {"identity": "pull:repo:2:", "elapsed_ms": 30000, "running": False},
|
||||
"other": {"identity": "", "elapsed_ms": 0, "running": False},
|
||||
}
|
||||
|
||||
|
||||
def test_today_timer_can_be_initialized_before_operator_identity_is_restored():
|
||||
script = f"""
|
||||
const createTodayTimer = require({json.dumps(str(TODAY_TIMER))});
|
||||
const values = new Map();
|
||||
const storage = {{
|
||||
getItem:key => values.has(key) ? values.get(key) : null,
|
||||
setItem:(key,value) => values.set(key,value),
|
||||
removeItem:key => values.delete(key),
|
||||
}};
|
||||
let login = '';
|
||||
let now = 100;
|
||||
const timer = createTodayTimer({{storage,getLogin:() => login,now:() => now}});
|
||||
login = 'timmy';
|
||||
const activated = timer.activate('issue:repo:1:');
|
||||
now = 1100;
|
||||
process.stdout.write(JSON.stringify({{activated,snapshot:timer.snapshot()}}));
|
||||
"""
|
||||
result = subprocess.run(["node", "-e", script], capture_output=True, text=True)
|
||||
|
||||
assert result.returncode == 0, result.stderr
|
||||
assert json.loads(result.stdout) == {
|
||||
"activated": True,
|
||||
"snapshot": {"identity": "issue:repo:1:", "elapsed_ms": 1000, "running": True},
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_today_timer_is_wired_into_every_mobile_session_control():
|
||||
html = await dashboard()
|
||||
timer_source = TODAY_TIMER.read_text()
|
||||
|
||||
assert html.count('<button type="button" data-work-session-timer-toggle>') == 4
|
||||
assert 'createTodayTimer({' in html
|
||||
assert 'createTodayTimerView({' in html
|
||||
assert 'timerView.open(workIdentity(item), workSession.checkpointed(item))' in html
|
||||
assert 'active ? timer.activate(identity) : timer.stop()' in timer_source
|
||||
assert "setInterval(timerView.render, 1000)" in html
|
||||
assert 'timer.pause()' in timer_source
|
||||
assert 'timer.resume()' in timer_source
|
||||
assert "elapsed(snapshot.elapsed_ms)" in timer_source
|
||||
assert "' / ' + formatEstimate(runway.current_minutes)" in timer_source
|
||||
assert '.work-session-nav [data-work-session-timer-toggle] { min-height:44px;' in html
|
||||
|
||||
|
||||
def test_active_today_session_reopens_current_item_without_restarting_checkpoint():
|
||||
script = f"""
|
||||
const buildMyWork = require({json.dumps(str(MY_WORK))});
|
||||
|
||||
@@ -678,6 +678,7 @@ def test_install_precaches_complete_subpath_scoped_app_shell():
|
||||
"/dashboard/static/my-work.js",
|
||||
"/dashboard/static/card-planning.js",
|
||||
"/dashboard/static/today-work.js",
|
||||
"/dashboard/static/today-timer.js",
|
||||
"/dashboard/static/today-completion.js",
|
||||
"/dashboard/static/today-readiness.js",
|
||||
"/dashboard/static/comment-next.js",
|
||||
|
||||
@@ -333,7 +333,7 @@ async def test_today_session_sheets_offer_a_touch_safe_plan_only_completion_acti
|
||||
assert "document.querySelectorAll('[data-work-session-complete]')" in html
|
||||
assert "completeTodayItem(selectedSessionItem(button.dataset.workSessionComplete))" in html
|
||||
assert ".work-session-nav button { min-height:44px; width:100%; }" in html
|
||||
assert "grid-template-columns:repeat(3,minmax(0,1fr))" in html
|
||||
assert "grid-template-columns:repeat(2,minmax(0,1fr))" in html
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
|
||||
Reference in new issue
Block a user