117 lines
4.6 KiB
Python
117 lines
4.6 KiB
Python
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
|
|
|
|
|
|
@pytest.mark.anyio
|
|
async def test_initial_http_outage_hydrates_saved_work_and_recovers_on_live_snapshot():
|
|
html = await dashboard()
|
|
|
|
assert "if (!hasContextSnapshot && hydrateOfflineWork('outage')) return;" in html
|
|
assert "Outage · saved " in html
|
|
assert "Server unavailable · showing private My Work saved " in html
|
|
assert "Live details and actions will return automatically." in html
|
|
assert "function renderLiveSnapshot(snapshot, changedSections" in html
|
|
assert "setOfflineWorkMode(false);\n offlineStatus.hidden = true;" in html
|