feat: manage private device storage (Closes #998)
This commit is contained in:
parent
f30143b5ea
commit
70c8ea6584
|
|
@ -60,6 +60,10 @@ button { background: linear-gradient(180deg,#1f3a5f,#15324d); border:1px solid #
|
||||||
.device-setup-deadline-controls label { font-size:12px; color:#bfdbfe; }
|
.device-setup-deadline-controls label { font-size:12px; color:#bfdbfe; }
|
||||||
.device-setup-deadline-controls select, .device-setup-deadline-controls button, #push-deadline-hour, #push-deadline-days { min-height:44px; }
|
.device-setup-deadline-controls select, .device-setup-deadline-controls button, #push-deadline-hour, #push-deadline-days { min-height:44px; }
|
||||||
.device-setup-ready { margin:0; padding:12px; border-radius:10px; background:#0f2237; color:#bfdbfe; font-weight:700; }
|
.device-setup-ready { margin:0; padding:12px; border-radius:10px; background:#0f2237; color:#bfdbfe; font-weight:700; }
|
||||||
|
.device-storage { display:grid; gap:12px; margin:14px 0; padding:12px; border:1px solid #31577f; border-radius:12px; background:#0d1d31; }
|
||||||
|
.device-storage h3, .device-storage p { margin:0 0 5px; }
|
||||||
|
.device-storage-actions { display:flex; gap:8px; flex-wrap:wrap; }
|
||||||
|
.device-storage-actions button { min-height:44px; flex:1 1 180px; }
|
||||||
.device-readiness-card { display:none; min-width:0; margin:10px 0; padding:12px; border:1px solid #3b82b8; border-radius:12px; background:#102b46; overflow-x:hidden; }
|
.device-readiness-card { display:none; min-width:0; margin:10px 0; padding:12px; border:1px solid #3b82b8; border-radius:12px; background:#102b46; overflow-x:hidden; }
|
||||||
.device-readiness-card p { margin:5px 0 0; overflow-wrap:anywhere; }
|
.device-readiness-card p { margin:5px 0 0; overflow-wrap:anywhere; }
|
||||||
.device-readiness-summary { color:#bfdbfe; font-weight:700; }
|
.device-readiness-summary { color:#bfdbfe; font-weight:700; }
|
||||||
|
|
|
||||||
|
|
@ -7744,6 +7744,7 @@
|
||||||
await controller.enableDeadline();
|
await controller.enableDeadline();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
await createDeviceStorage.mount(document).start();
|
||||||
await deviceSetup.start();
|
await deviceSetup.start();
|
||||||
return deviceSetup;
|
return deviceSetup;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
96
frontend/device-storage.js
Normal file
96
frontend/device-storage.js
Normal file
|
|
@ -0,0 +1,96 @@
|
||||||
|
(function (root, factory) {
|
||||||
|
if (typeof module === 'object' && module.exports) module.exports = factory;
|
||||||
|
else {
|
||||||
|
root.createDeviceStorage = factory;
|
||||||
|
root.createDeviceStorage.mount = document => factory({
|
||||||
|
summary:document.querySelector('#device-storage-summary'),
|
||||||
|
detail:document.querySelector('#device-storage-detail'),
|
||||||
|
clearCachesButton:document.querySelector('#clear-device-caches'),
|
||||||
|
clearAllButton:document.querySelector('#clear-private-device-data'),
|
||||||
|
localStorage:root.localStorage,
|
||||||
|
sessionStorage:root.sessionStorage,
|
||||||
|
caches:root.caches,
|
||||||
|
storageManager:root.navigator?.storage,
|
||||||
|
privateDatabases:root.stackchainPrivateDatabases,
|
||||||
|
clearPrivateDeviceData:root.stackchainPrivateDeviceData,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})(typeof self !== 'undefined' ? self : this, function createDeviceStorage(options) {
|
||||||
|
let privateItemCount = 0;
|
||||||
|
let fullClearArmed = false;
|
||||||
|
const ownedCaches = async () => (await options.caches?.keys?.() || [])
|
||||||
|
.filter(name => name.startsWith('stackchain-dashboard-'));
|
||||||
|
|
||||||
|
function ownedStorageCount(storage) {
|
||||||
|
if (!storage) return 0;
|
||||||
|
let count = 0;
|
||||||
|
for (let index = 0; index < storage.length; index += 1) {
|
||||||
|
if (storage.key(index)?.startsWith('stackchain.')) count += 1;
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
function megabytes(value) {
|
||||||
|
return Math.round((Number(value) || 0) / (1024 * 1024));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refresh() {
|
||||||
|
let estimate = null;
|
||||||
|
try { estimate = await options.storageManager?.estimate?.(); }
|
||||||
|
catch (_error) { /* Storage estimates are optional. */ }
|
||||||
|
options.summary.textContent = estimate?.quota
|
||||||
|
? `${megabytes(estimate.usage)} MB of ${megabytes(estimate.quota)} MB browser storage used.`
|
||||||
|
: 'Browser storage usage is unavailable.';
|
||||||
|
const itemCount = ownedStorageCount(options.localStorage)
|
||||||
|
+ (options.sessionStorage === options.localStorage ? 0 : ownedStorageCount(options.sessionStorage));
|
||||||
|
privateItemCount = itemCount;
|
||||||
|
const cacheCount = (await ownedCaches()).length;
|
||||||
|
options.detail.textContent = `${itemCount} private browser item${itemCount === 1 ? '' : 's'} · `
|
||||||
|
+ `${options.privateDatabases.length} private work stores · `
|
||||||
|
+ `${cacheCount} cached app cop${cacheCount === 1 ? 'y' : 'ies'}`;
|
||||||
|
return { itemCount, cacheCount };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function clearCaches() {
|
||||||
|
options.clearCachesButton.disabled = true;
|
||||||
|
try {
|
||||||
|
await Promise.all((await ownedCaches()).map(name => options.caches.delete(name)));
|
||||||
|
await refresh();
|
||||||
|
options.summary.textContent = 'Cached app copies cleared. Private work was kept.';
|
||||||
|
} catch (error) {
|
||||||
|
options.summary.textContent = `Cached copies could not be cleared: ${error.message}`;
|
||||||
|
} finally {
|
||||||
|
options.clearCachesButton.disabled = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function clearAll() {
|
||||||
|
if (privateItemCount > 0 && !fullClearArmed) {
|
||||||
|
fullClearArmed = true;
|
||||||
|
options.clearAllButton.textContent = 'Confirm: clear private work';
|
||||||
|
options.summary.textContent = 'Private drafts or queued work may not be synced. Press confirm to clear them from this device.';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
options.clearAllButton.disabled = true;
|
||||||
|
try {
|
||||||
|
await options.clearPrivateDeviceData();
|
||||||
|
privateItemCount = 0;
|
||||||
|
fullClearArmed = false;
|
||||||
|
options.clearAllButton.textContent = 'Clear all private data';
|
||||||
|
options.detail.textContent = '0 private browser items · 0 private work stores · 0 cached app copies';
|
||||||
|
options.summary.textContent = 'All Stackchain private data was cleared from this device.';
|
||||||
|
} catch (error) {
|
||||||
|
options.summary.textContent = `Private data was not fully cleared: ${error.message}`;
|
||||||
|
} finally {
|
||||||
|
options.clearAllButton.disabled = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function start() {
|
||||||
|
options.clearCachesButton.addEventListener('click', clearCaches);
|
||||||
|
options.clearAllButton.addEventListener('click', clearAll);
|
||||||
|
return refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
return { refresh, start };
|
||||||
|
});
|
||||||
|
|
@ -108,6 +108,17 @@
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
</ol>
|
</ol>
|
||||||
|
<section class="device-storage" aria-labelledby="device-storage-heading">
|
||||||
|
<div>
|
||||||
|
<h3 id="device-storage-heading">Private device storage</h3>
|
||||||
|
<p class="small" id="device-storage-summary" role="status" aria-live="polite">Checking private storage…</p>
|
||||||
|
<p class="small muted" id="device-storage-detail"></p>
|
||||||
|
</div>
|
||||||
|
<div class="device-storage-actions">
|
||||||
|
<button id="clear-device-caches" type="button">Clear cached copies</button>
|
||||||
|
<button id="clear-private-device-data" type="button">Clear all private data</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
<p id="device-setup-ready-status" class="device-setup-ready" role="status" aria-live="polite"></p>
|
<p id="device-setup-ready-status" class="device-setup-ready" role="status" aria-live="polite"></p>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -1638,6 +1649,8 @@
|
||||||
<script src="static/mobile-insights.js"></script>
|
<script src="static/mobile-insights.js"></script>
|
||||||
<script src="static/mobile-app-shortcuts.js"></script>
|
<script src="static/mobile-app-shortcuts.js"></script>
|
||||||
<script src="static/install-app.js"></script>
|
<script src="static/install-app.js"></script>
|
||||||
|
<script src="static/private-device-data.js"></script>
|
||||||
|
<script src="static/device-storage.js"></script>
|
||||||
<script src="static/mobile-device-setup.js"></script>
|
<script src="static/mobile-device-setup.js"></script>
|
||||||
<script src="static/mobile-search-viewport.js"></script>
|
<script src="static/mobile-search-viewport.js"></script>
|
||||||
<script src="static/mobile-composer-viewport.js"></script>
|
<script src="static/mobile-composer-viewport.js"></script>
|
||||||
|
|
|
||||||
|
|
@ -55,9 +55,9 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
return async function clearPrivateDeviceData() {
|
return async function clearPrivateDeviceData() {
|
||||||
|
await stopWorkerOutbox();
|
||||||
removeOwnedStorage(localStorage);
|
removeOwnedStorage(localStorage);
|
||||||
if (sessionStorage !== localStorage) removeOwnedStorage(sessionStorage);
|
if (sessionStorage !== localStorage) removeOwnedStorage(sessionStorage);
|
||||||
await stopWorkerOutbox();
|
|
||||||
for (const name of privateDatabases) await deletePrivateDatabase(name);
|
for (const name of privateDatabases) await deletePrivateDatabase(name);
|
||||||
const keys = await caches?.keys?.() || [];
|
const keys = await caches?.keys?.() || [];
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
|
|
|
||||||
|
|
@ -118,6 +118,8 @@ const SHELL = [
|
||||||
BASE + 'static/mobile-insights.js',
|
BASE + 'static/mobile-insights.js',
|
||||||
BASE + 'static/mobile-app-shortcuts.js',
|
BASE + 'static/mobile-app-shortcuts.js',
|
||||||
BASE + 'static/install-app.js',
|
BASE + 'static/install-app.js',
|
||||||
|
BASE + 'static/private-device-data.js',
|
||||||
|
BASE + 'static/device-storage.js',
|
||||||
BASE + 'static/mobile-device-setup.js',
|
BASE + 'static/mobile-device-setup.js',
|
||||||
BASE + 'static/mobile-search-viewport.js',
|
BASE + 'static/mobile-search-viewport.js',
|
||||||
BASE + 'static/mobile-composer-viewport.js',
|
BASE + 'static/mobile-composer-viewport.js',
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,10 @@ FEATURE_SOURCES = {
|
||||||
),
|
),
|
||||||
"pull-workflow": ("static/pull-sheet.js", "static/review-sheet.js"),
|
"pull-workflow": ("static/pull-sheet.js", "static/review-sheet.js"),
|
||||||
"push-notifications": ("static/push-notifications.js",),
|
"push-notifications": ("static/push-notifications.js",),
|
||||||
"device-setup": ("static/install-app.js", "static/mobile-device-setup.js"),
|
"device-setup": (
|
||||||
|
"static/install-app.js", "static/private-device-data.js",
|
||||||
|
"static/device-storage.js", "static/mobile-device-setup.js",
|
||||||
|
),
|
||||||
"security-center": ("static/security-center.js",),
|
"security-center": ("static/security-center.js",),
|
||||||
"today-timer": (
|
"today-timer": (
|
||||||
"static/conversation.js", "static/voice-transcript-store.js", "static/voice-conversation-capture.js", "static/mobile-launch.js", "static/mobile-insights.js", "static/mobile-app-shortcuts.js", "static/mobile-plan-today-nav.js", "static/mobile-find-work-nav.js",
|
"static/conversation.js", "static/voice-transcript-store.js", "static/voice-conversation-capture.js", "static/mobile-launch.js", "static/mobile-insights.js", "static/mobile-app-shortcuts.js", "static/mobile-plan-today-nav.js", "static/mobile-find-work-nav.js",
|
||||||
|
|
|
||||||
|
|
@ -80,6 +80,18 @@ def test_release_artifact_bootstraps_mobile_home_and_returns_from_insights(
|
||||||
expect(page.locator("#my-work")).to_be_visible()
|
expect(page.locator("#my-work")).to_be_visible()
|
||||||
expect(dock).to_be_visible()
|
expect(dock).to_be_visible()
|
||||||
|
|
||||||
|
page.locator("#app-menu-toggle").click()
|
||||||
|
page.locator("#open-device-setup").click()
|
||||||
|
expect(page.locator("#device-setup-sheet")).to_be_visible()
|
||||||
|
expect(page.locator("#device-storage-heading")).to_have_text("Private device storage")
|
||||||
|
expect(page.locator("#device-storage-detail")).to_contain_text("private work stores")
|
||||||
|
for selector in ("#clear-device-caches", "#clear-private-device-data"):
|
||||||
|
bounds = page.locator(selector).bounding_box()
|
||||||
|
assert bounds and bounds["height"] >= 44
|
||||||
|
assert page.evaluate("document.documentElement.scrollWidth <= window.innerWidth")
|
||||||
|
page.locator("#close-device-setup").click()
|
||||||
|
expect(page.locator("#device-setup-sheet")).to_be_hidden()
|
||||||
|
|
||||||
assert len(workspace_requests) == 1, workspace_requests
|
assert len(workspace_requests) == 1, workspace_requests
|
||||||
assert page.evaluate("document.documentElement.scrollWidth <= window.innerWidth")
|
assert page.evaluate("document.documentElement.scrollWidth <= window.innerWidth")
|
||||||
assert browser_errors == []
|
assert browser_errors == []
|
||||||
|
|
|
||||||
108
tests/test_device_storage.py
Normal file
108
tests/test_device_storage.py
Normal file
|
|
@ -0,0 +1,108 @@
|
||||||
|
import json
|
||||||
|
import subprocess
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
MODULE = Path(__file__).parents[1] / "frontend" / "device-storage.js"
|
||||||
|
|
||||||
|
|
||||||
|
def run_scenario(script: str) -> dict:
|
||||||
|
harness = r"""
|
||||||
|
const createDeviceStorage = require(__MODULE__);
|
||||||
|
class Target {
|
||||||
|
constructor() { this.listeners={}; this.textContent=''; this.hidden=false; this.disabled=false; this.dataset={}; }
|
||||||
|
addEventListener(name, callback) { (this.listeners[name] ||= []).push(callback); }
|
||||||
|
async dispatch(name) { for (const callback of this.listeners[name] || []) await callback(); }
|
||||||
|
}
|
||||||
|
const summary=new Target(), detail=new Target(), clearCachesButton=new Target(), clearAllButton=new Target();
|
||||||
|
let purgeCalls=0, purgeError='';
|
||||||
|
const localStorage={
|
||||||
|
values:new Map([['stackchain.draft.1','private'],['other.preference','keep']]),
|
||||||
|
get length(){return this.values.size;},
|
||||||
|
key(index){return Array.from(this.values.keys())[index] || null;},
|
||||||
|
};
|
||||||
|
const caches={
|
||||||
|
values:new Set(['stackchain-dashboard-shell-a1','other-app']),
|
||||||
|
async keys(){return Array.from(this.values);},
|
||||||
|
async delete(key){return this.values.delete(key);},
|
||||||
|
};
|
||||||
|
const controller=createDeviceStorage({
|
||||||
|
summary, detail, clearCachesButton, clearAllButton, localStorage, caches,
|
||||||
|
privateDatabases:['stackchain-background-outbox-v1','stackchain-offline-work-v2'],
|
||||||
|
storageManager:{estimate:async()=>({usage:2 * 1024 * 1024, quota:10 * 1024 * 1024})},
|
||||||
|
clearPrivateDeviceData:async()=>{purgeCalls++;if(purgeError)throw new Error(purgeError);},
|
||||||
|
});
|
||||||
|
(async()=>{ __SCENARIO__ })().catch(error=>{console.error(error);process.exit(1);});
|
||||||
|
""".replace("__MODULE__", json.dumps(str(MODULE))).replace("__SCENARIO__", script)
|
||||||
|
completed = subprocess.run(["node", "-e", harness], capture_output=True, text=True)
|
||||||
|
assert completed.returncode == 0, completed.stderr
|
||||||
|
return json.loads(completed.stdout)
|
||||||
|
|
||||||
|
|
||||||
|
def test_inventory_reports_private_categories_and_quota_without_content():
|
||||||
|
result = run_scenario("""
|
||||||
|
await controller.refresh();
|
||||||
|
process.stdout.write(JSON.stringify({summary:summary.textContent,detail:detail.textContent}));
|
||||||
|
""")
|
||||||
|
|
||||||
|
assert result == {
|
||||||
|
"summary": "2 MB of 10 MB browser storage used.",
|
||||||
|
"detail": "1 private browser item · 2 private work stores · 1 cached app copy",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_cache_only_cleanup_preserves_private_work_and_other_apps():
|
||||||
|
result = run_scenario("""
|
||||||
|
await controller.start();
|
||||||
|
await clearCachesButton.dispatch('click');
|
||||||
|
process.stdout.write(JSON.stringify({
|
||||||
|
caches:Array.from(caches.values),
|
||||||
|
storage:Array.from(localStorage.values.keys()),
|
||||||
|
status:summary.textContent,
|
||||||
|
detail:detail.textContent,
|
||||||
|
}));
|
||||||
|
""")
|
||||||
|
|
||||||
|
assert result == {
|
||||||
|
"caches": ["other-app"],
|
||||||
|
"storage": ["stackchain.draft.1", "other.preference"],
|
||||||
|
"status": "Cached app copies cleared. Private work was kept.",
|
||||||
|
"detail": "1 private browser item · 2 private work stores · 0 cached app copies",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_full_cleanup_requires_explicit_second_press_when_private_work_exists():
|
||||||
|
result = run_scenario("""
|
||||||
|
await controller.start();
|
||||||
|
await clearAllButton.dispatch('click');
|
||||||
|
const warning={calls:purgeCalls,label:clearAllButton.textContent,status:summary.textContent};
|
||||||
|
await clearAllButton.dispatch('click');
|
||||||
|
process.stdout.write(JSON.stringify({warning,calls:purgeCalls,status:summary.textContent}));
|
||||||
|
""")
|
||||||
|
|
||||||
|
assert result == {
|
||||||
|
"warning": {
|
||||||
|
"calls": 0,
|
||||||
|
"label": "Confirm: clear private work",
|
||||||
|
"status": "Private drafts or queued work may not be synced. Press confirm to clear them from this device.",
|
||||||
|
},
|
||||||
|
"calls": 1,
|
||||||
|
"status": "All Stackchain private data was cleared from this device.",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_full_cleanup_reports_blocked_deletion_without_claiming_success():
|
||||||
|
result = run_scenario("""
|
||||||
|
purgeError='IndexedDB deletion was blocked.';
|
||||||
|
await controller.start();
|
||||||
|
await clearAllButton.dispatch('click');
|
||||||
|
let escaped='';
|
||||||
|
try { await clearAllButton.dispatch('click'); } catch (error) { escaped=error.message; }
|
||||||
|
process.stdout.write(JSON.stringify({escaped,status:summary.textContent,disabled:clearAllButton.disabled}));
|
||||||
|
""")
|
||||||
|
|
||||||
|
assert result == {
|
||||||
|
"escaped": "",
|
||||||
|
"status": "Private data was not fully cleared: IndexedDB deletion was blocked.",
|
||||||
|
"disabled": False,
|
||||||
|
}
|
||||||
|
|
@ -197,6 +197,7 @@ def test_mobile_dashboard_mounts_phone_safe_device_setup_flow():
|
||||||
root = MODULE.parents[1]
|
root = MODULE.parents[1]
|
||||||
html = (root / "frontend" / "index.html").read_text()
|
html = (root / "frontend" / "index.html").read_text()
|
||||||
dashboard = (root / "frontend" / "dashboard.js").read_text()
|
dashboard = (root / "frontend" / "dashboard.js").read_text()
|
||||||
|
storage_module = (root / "frontend" / "device-storage.js").read_text()
|
||||||
worker = (root / "frontend" / "service-worker.js").read_text()
|
worker = (root / "frontend" / "service-worker.js").read_text()
|
||||||
css = (root / "frontend" / "dashboard.css").read_text()
|
css = (root / "frontend" / "dashboard.css").read_text()
|
||||||
|
|
||||||
|
|
@ -208,9 +209,17 @@ def test_mobile_dashboard_mounts_phone_safe_device_setup_flow():
|
||||||
assert 'id="device-setup-sheet"' in html
|
assert 'id="device-setup-sheet"' in html
|
||||||
assert 'aria-labelledby="device-setup-heading"' in html
|
assert 'aria-labelledby="device-setup-heading"' in html
|
||||||
assert all(f'id="device-setup-{step}"' in html for step in ("install", "offline", "push", "deadline"))
|
assert all(f'id="device-setup-{step}"' in html for step in ("install", "offline", "push", "deadline"))
|
||||||
|
assert 'id="device-storage-summary"' in html
|
||||||
|
assert 'id="device-storage-detail"' in html
|
||||||
|
assert 'id="clear-device-caches"' in html
|
||||||
|
assert 'id="clear-private-device-data"' in html
|
||||||
|
assert '<script src="static/private-device-data.js"></script>' in html
|
||||||
|
assert '<script src="static/device-storage.js"></script>' in html
|
||||||
assert 'id="device-setup-deadline-hour"' in html
|
assert 'id="device-setup-deadline-hour"' in html
|
||||||
assert '<script src="static/mobile-device-setup.js"></script>' in html
|
assert '<script src="static/mobile-device-setup.js"></script>' in html
|
||||||
assert "createMobileDeviceSetup.mount({" in dashboard
|
assert "createMobileDeviceSetup.mount({" in dashboard
|
||||||
|
assert "createDeviceStorage.mount(document)" in dashboard
|
||||||
|
assert "clearPrivateDeviceData:root.stackchainPrivateDeviceData" in storage_module
|
||||||
assert "promptStorage:localStorage" in dashboard
|
assert "promptStorage:localStorage" in dashboard
|
||||||
assert "pushControllerReady.then(ensureDeviceSetup)" in dashboard
|
assert "pushControllerReady.then(ensureDeviceSetup)" in dashboard
|
||||||
assert "BASE + 'static/mobile-device-setup.js'" in worker
|
assert "BASE + 'static/mobile-device-setup.js'" in worker
|
||||||
|
|
|
||||||
|
|
@ -9,12 +9,12 @@ PRIVATE_DATA_JS = Path(__file__).resolve().parents[1] / "frontend" / "private-de
|
||||||
def test_private_device_data_purger_waits_for_owned_outbox_and_cache_deletion():
|
def test_private_device_data_purger_waits_for_owned_outbox_and_cache_deletion():
|
||||||
harness = f"""
|
harness = f"""
|
||||||
const createPrivateDeviceDataPurger = require({json.dumps(str(PRIVATE_DATA_JS))});
|
const createPrivateDeviceDataPurger = require({json.dumps(str(PRIVATE_DATA_JS))});
|
||||||
const state = {{removed:[], databases:[], caches:[], workerMessages:[], complete:false}};
|
const state = {{removed:[], databases:[], caches:[], workerMessages:[], order:[], complete:false}};
|
||||||
const storage = {{
|
const storage = {{
|
||||||
values:new Map([['stackchain.draft','private'],['other.preference','keep']]),
|
values:new Map([['stackchain.draft','private'],['other.preference','keep']]),
|
||||||
get length(){{return this.values.size;}},
|
get length(){{return this.values.size;}},
|
||||||
key(index){{return Array.from(this.values.keys())[index] || null;}},
|
key(index){{return Array.from(this.values.keys())[index] || null;}},
|
||||||
removeItem(key){{state.removed.push(key);this.values.delete(key);}},
|
removeItem(key){{state.order.push('storage');state.removed.push(key);this.values.delete(key);}},
|
||||||
}};
|
}};
|
||||||
const clear = createPrivateDeviceDataPurger({{
|
const clear = createPrivateDeviceDataPurger({{
|
||||||
localStorage:storage, sessionStorage:storage,
|
localStorage:storage, sessionStorage:storage,
|
||||||
|
|
@ -25,7 +25,7 @@ const clear = createPrivateDeviceDataPurger({{
|
||||||
return request;
|
return request;
|
||||||
}}}},
|
}}}},
|
||||||
caches:{{keys:async()=>['stackchain-dashboard-shell-v37','other-app'],delete:async key=>state.caches.push(key)}},
|
caches:{{keys:async()=>['stackchain-dashboard-shell-v37','other-app'],delete:async key=>state.caches.push(key)}},
|
||||||
serviceWorker:{{ready:Promise.resolve({{active:{{postMessage:(message,ports)=>{{state.workerMessages.push(message);ports[0].postMessage({{ok:true}});}}}}}})}},
|
serviceWorker:{{ready:Promise.resolve({{active:{{postMessage:(message,ports)=>{{state.order.push('worker');state.workerMessages.push(message);ports[0].postMessage({{ok:true}});}}}}}})}},
|
||||||
MessageChannel:class{{constructor(){{
|
MessageChannel:class{{constructor(){{
|
||||||
const first={{onmessage:null,postMessage:data=>queueMicrotask(()=>second.onmessage?.({{data}}))}};
|
const first={{onmessage:null,postMessage:data=>queueMicrotask(()=>second.onmessage?.({{data}}))}};
|
||||||
const second={{onmessage:null,postMessage:data=>queueMicrotask(()=>first.onmessage?.({{data}}))}};
|
const second={{onmessage:null,postMessage:data=>queueMicrotask(()=>first.onmessage?.({{data}}))}};
|
||||||
|
|
@ -53,4 +53,5 @@ const clear = createPrivateDeviceDataPurger({{
|
||||||
]
|
]
|
||||||
assert state["caches"] == ["stackchain-dashboard-shell-v37"]
|
assert state["caches"] == ["stackchain-dashboard-shell-v37"]
|
||||||
assert state["workerMessages"] == [{"type": "stackchain-purge-outbox"}]
|
assert state["workerMessages"] == [{"type": "stackchain-purge-outbox"}]
|
||||||
|
assert state["order"][0] == "worker"
|
||||||
assert state["complete"] is True
|
assert state["complete"] is True
|
||||||
|
|
|
||||||
|
|
@ -1055,6 +1055,8 @@ def test_install_precaches_complete_subpath_scoped_app_shell():
|
||||||
"/dashboard/static/mobile-insights.js",
|
"/dashboard/static/mobile-insights.js",
|
||||||
"/dashboard/static/mobile-app-shortcuts.js",
|
"/dashboard/static/mobile-app-shortcuts.js",
|
||||||
"/dashboard/static/install-app.js",
|
"/dashboard/static/install-app.js",
|
||||||
|
"/dashboard/static/private-device-data.js",
|
||||||
|
"/dashboard/static/device-storage.js",
|
||||||
"/dashboard/static/mobile-device-setup.js",
|
"/dashboard/static/mobile-device-setup.js",
|
||||||
"/dashboard/static/mobile-search-viewport.js",
|
"/dashboard/static/mobile-search-viewport.js",
|
||||||
"/dashboard/static/mobile-composer-viewport.js",
|
"/dashboard/static/mobile-composer-viewport.js",
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user