feat: keep My Work available offline (#228)
This commit is contained in:
1 parent
a579b95f47
commit
1b2a3c83af
6 files changed
+274
-3
No files matched your search
@@ -0,0 +1,104 @@
|
||||
import json
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from src.views import dashboard
|
||||
|
||||
|
||||
OFFLINE_WORK = Path(__file__).parents[1] / "frontend" / "offline-work.js"
|
||||
|
||||
|
||||
def run_scenario(scenario: str) -> dict:
|
||||
script = f"""
|
||||
const createOfflineWorkStore = require({json.dumps(str(OFFLINE_WORK))});
|
||||
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),
|
||||
}};
|
||||
{scenario}
|
||||
"""
|
||||
result = subprocess.run(
|
||||
["node", "-e", script], check=True, capture_output=True, text=True
|
||||
)
|
||||
return json.loads(result.stdout)
|
||||
|
||||
|
||||
def test_opted_in_snapshot_survives_restart_with_only_queue_card_fields():
|
||||
result = run_scenario("""
|
||||
const store = createOfflineWorkStore({storage, now:() => new Date('2026-08-07T12:00:00Z')});
|
||||
store.setEnabled(true);
|
||||
store.save({
|
||||
user:{login:'timmy', full_name:'Timmy', token:'secret'},
|
||||
issues:[{id:1, number:8, title:'Ship offline work', body:'private body', state:'open',
|
||||
repository:'stackchain/dashboard', labels:['P0'], assignees:['timmy'],
|
||||
updated_at:'2026-08-07T11:00:00Z', url:'https://forge.example/issues/8'}],
|
||||
pull_requests:[],
|
||||
notifications:[{id:9, number:8, title:'Updated', unread:true, subject_body:'private comment',
|
||||
repository:'stackchain/dashboard', subject_type:'Issue', updated_at:'2026-08-07T11:30:00Z',
|
||||
url:'https://forge.example/issues/8'}],
|
||||
work_pagination:{issue:{page:1,total:1,has_more:false}},
|
||||
repos:[{private:true}], events:[{body:'private event'}],
|
||||
});
|
||||
const restarted = createOfflineWorkStore({storage, now:() => new Date('2026-08-07T12:01:00Z')});
|
||||
process.stdout.write(JSON.stringify({loaded:restarted.load('timmy'), raw:[...values.values()].join(' ')}));
|
||||
""")
|
||||
|
||||
assert result["loaded"]["user"] == {"login": "timmy", "full_name": "Timmy"}
|
||||
assert result["loaded"]["issues"][0]["title"] == "Ship offline work"
|
||||
assert result["loaded"]["notifications"][0]["unread"] is True
|
||||
assert result["loaded"]["saved_at"] == "2026-08-07T12:00:00.000Z"
|
||||
assert "private body" not in result["raw"]
|
||||
assert "private comment" not in result["raw"]
|
||||
assert "private event" not in result["raw"]
|
||||
assert "secret" not in result["raw"]
|
||||
|
||||
|
||||
def test_expired_snapshot_is_deleted_and_wrong_user_cannot_load_it():
|
||||
result = run_scenario("""
|
||||
const writer = createOfflineWorkStore({storage, now:() => new Date('2026-08-01T12:00:00Z'), maxAgeMs:1000});
|
||||
writer.setEnabled(true);
|
||||
writer.save({user:{login:'timmy'}, issues:[], pull_requests:[], notifications:[]});
|
||||
const wrongUser = writer.load('alexander');
|
||||
const expired = createOfflineWorkStore({storage, now:() => new Date('2026-08-01T12:00:02Z'), maxAgeMs:1000});
|
||||
const expiredValue = expired.load('timmy');
|
||||
process.stdout.write(JSON.stringify({wrongUser, expiredValue, snapshotStillStored:values.has('stackchain.offline-work.snapshot.v1')}));
|
||||
""")
|
||||
|
||||
assert result == {
|
||||
"wrongUser": None,
|
||||
"expiredValue": None,
|
||||
"snapshotStillStored": False,
|
||||
}
|
||||
|
||||
|
||||
def test_opting_out_deletes_the_persisted_snapshot():
|
||||
result = run_scenario("""
|
||||
const store = createOfflineWorkStore({storage});
|
||||
store.setEnabled(true);
|
||||
store.save({user:{login:'timmy'}, issues:[], pull_requests:[], notifications:[]});
|
||||
store.setEnabled(false);
|
||||
process.stdout.write(JSON.stringify({enabled:store.enabled(), loaded:store.load()}));
|
||||
""")
|
||||
|
||||
assert result == {"enabled": False, "loaded": None}
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_dashboard_offers_private_offline_work_controls_and_read_only_hydration():
|
||||
html = await dashboard()
|
||||
|
||||
assert '<script src="static/offline-work.js"></script>' in html
|
||||
assert 'id="keep-work-offline"' in html
|
||||
assert 'Keep My Work available offline' in html
|
||||
assert 'id="clear-offline-work"' in html
|
||||
assert 'id="offline-work-status"' in html
|
||||
assert 'offlineWorkStore.save({' in html
|
||||
assert 'offlineWorkStore.load()' in html
|
||||
assert 'Offline · saved ' in html
|
||||
assert "setOfflineWorkMode(true)" in html
|
||||
assert "if (offlineWorkMode)" in html
|
||||
assert '.offline-work-controls button { min-height:44px;' in html
|
||||
@@ -81,6 +81,7 @@ def test_install_precaches_complete_subpath_scoped_app_shell():
|
||||
"/dashboard/static/search-preview.js",
|
||||
"/dashboard/static/widgets.js",
|
||||
"/dashboard/static/drafts.js",
|
||||
"/dashboard/static/offline-work.js",
|
||||
"/dashboard/static/my-work.js",
|
||||
"/dashboard/static/pick-work.js",
|
||||
"/dashboard/static/conversation.js",
|
||||
|
||||
Reference in new issue
Block a user