Oregami
Repositories/oxedyne/daimond

oxedyne/daimond/dev/verify_storage.mjs

7.0 KiB, 1 run

created by r2519314175:703, which is this file's identity for as long as the history lasts, whatever it is later renamed to

download · who wrote it · its history

1// verify_storage.mjs — what the workspace costs, and the one figure the browser will not give.
2//
3// The sandbox row used to say "Workspace · evictable 2.0 MB", which is a number with nothing to
4// compare it to and a word with no explanation. The browser DOES know the rest — `estimate()`
5// gives usage and quota, `persisted()` says whether the storage may be thrown away, and
6// `persist()` fixes it — so the row now says how much of what it is allowed, and offers the fix.
7//
8// A real folder is the opposite: the browser tells us NOTHING. A FileSystemDirectoryHandle has no
9// size, no quota, and there is no web API for free disk space. So that row cannot show a
10// percentage, and rather than invent one it offers to go and COUNT — which on a large tree reads
11// every file, and therefore has to be abandonable. That is the part worth testing: not that the
12// walk works, but that it stops when it is told to.
13import { open, signInAs, shot } from './harness.mjs';
14
15const ok = [], bad = [];
16const check = (name, pass, detail) => {
17 (pass ? ok : bad).push(name + (detail ? ' — ' + detail : ''));
18 console.log((pass ? ' ok ' : ' FAIL ') + name + (detail ? ' — ' + detail : ''));
19};
20
21const s = await open({ name: 'storage', connect: false });
22const p = s.page;
23await p.waitForTimeout(1800);
24
25// ── The sandbox row ─────────────────────────────────────────────────────
26
27const sandbox = await p.evaluate(async () => {
28 const r = document.getElementById('astat-store');
29 const e = await navigator.storage.estimate();
30 return {
31 text: r ? r.textContent.trim() : '(none)',
32 title: r ? r.title : '',
33 quota: e.quota,
34 persisted: await navigator.storage.persisted(),
35 };
36});
37// The word is "Browser", not "OPFS": the mode chip in the files panel reads
38// "🗄 Browser", and the row has to name the same place in the same words or the
39// two read as two different stores. (The row said "OPFS" until the chips were
40// de-jargoned; this asserts the current word, and that it matches the chip.)
41check('the sandbox row names the browser store, in the chip’s own words',
42 /Browser/.test(sandbox.text), sandbox.text);
43check('and says how much of what it is allowed',
44 /%/.test(sandbox.text) && /of/.test(sandbox.title),
45 sandbox.text + ' · ' + sandbox.title.slice(0, 48));
46check('evictable is explained, not just asserted',
47 /evict/i.test(sandbox.title), sandbox.persisted ? '(already persistent)' : 'offers the fix');
48
49// ── The CREDIT cell is gone from Session / Week / Month ─────────────────
50
51const spend = await p.evaluate(() => {
52 const el = document.getElementById('spend-row');
53 return {
54 cells: [...(el ? el.querySelectorAll('.spend-label') : [])].map(e => e.textContent),
55 creditCell: !!(el && el.querySelector('.credit-cell')),
56 };
57});
58check('Session / Week / Month no longer carries a fourth "Credits" cell',
59 spend.creditCell === false, spend.cells.join(' · ') || '(row hidden — no spend yet)');
60
61// ── The native row, and the walk that can be stopped ────────────────────
62//
63// The folder is stood in for by an OPFS subdirectory, which is the same handle type the picker
64// returns (see verify_fsa.mjs). Enough files are put in it that the walk cannot finish instantly,
65// so there is something to interrupt.
66
67const FILES = 400;
68await p.evaluate(async ({ n }) => {
69 const mod = await import('../pkg/oxedyne_daimond.js');
70 const root = await navigator.storage.getDirectory();
71 const dir = await root.getDirectoryHandle('bigfolder', { create: true });
72 for (let i = 0; i < n; i++) {
73 const fh = await dir.getFileHandle('f' + i + '.txt', { create: true });
74 const w = await fh.createWritable();
75 await w.write('x'.repeat(1024)); // 1 KiB each, so the total is knowable
76 await w.close();
77 }
78 mod.set_workspace_dir(dir);
79 // The Files module owns the handle the status row reads, so it has to be told, the same way
80 // activateFolder tells it when a folder is really picked.
81 window.__testFolder = dir;
82}, { n: FILES });
83
84// Drive the real path: seed the handle where tryReconnect looks, and reload.
85await p.evaluate(async () => {
86 const root = await navigator.storage.getDirectory();
87 const dir = await root.getDirectoryHandle('bigfolder', { create: true });
88 const db = await new Promise((res, rej) => {
89 const q = indexedDB.open('daimond-fsa', 1);
90 q.onupgradeneeded = () => q.result.createObjectStore('handles');
91 q.onsuccess = () => res(q.result);
92 q.onerror = () => rej(q.error);
93 });
94 await new Promise((res, rej) => {
95 const t = db.transaction('handles', 'readwrite');
96 t.objectStore('handles').put(dir, 'workspace');
97 t.oncomplete = res; t.onerror = () => rej(t.error);
98 });
99});
100await p.reload({ waitUntil: 'domcontentloaded' });
101await signInAs(s, 'storage');
102await p.waitForTimeout(2500);
103
104const nat = await p.evaluate(() => {
105 const r = document.getElementById('astat-store-native');
106 return { shown: r && r.style.display !== 'none', text: r ? r.textContent.trim() : '', title: r ? r.title : '' };
107});
108check('a real folder gets a row of its own', nat.shown === true, nat.text);
109check('and it offers to count, warning that it may take a while',
110 /count/i.test(nat.title) && /stop/i.test(nat.title), nat.title.slice(0, 72));
111
112// Start the walk and STOP it almost immediately. The walk yields to the event loop, which is
113// precisely what makes the stop reachable — a loop that never yielded could not hear it.
114const stopped = await p.evaluate(async () => {
115 const r = document.getElementById('astat-store-native');
116 r.click(); // start counting
117 await new Promise(res => setTimeout(res, 30));
118 const mid = r.textContent.trim();
119 r.click(); // …and stop it
120 await new Promise(res => setTimeout(res, 600));
121 return { mid, after: r.textContent.trim(), title: r.title };
122});
123check('while counting it reports what it has counted so far',
124 /files/.test(stopped.mid), stopped.mid);
125check('and it stops when told to, keeping what it counted',
126 /\(part\)/.test(stopped.after) && /stopped it/i.test(stopped.title),
127 stopped.after + ' · ' + stopped.title.slice(0, 56));
128
129// And a walk left alone finishes, with the right total.
130const full = await p.evaluate(async () => {
131 const r = document.getElementById('astat-store-native');
132 r.click();
133 for (let i = 0; i < 100; i++) {
134 await new Promise(res => setTimeout(res, 100));
135 if (!/files/.test(r.textContent) && !/Counting/i.test(r.title)) break;
136 }
137 return { text: r.textContent.trim(), title: r.title };
138});
139check('a walk left alone finishes and reports the real total',
140 /400 files/.test(full.title) && !/\(part\)/.test(full.text),
141 full.text + ' · ' + full.title.slice(0, 40));
142
143await shot(s, 'storage');
144const errs = s.errs.filter(e => !/favicon|404|401|net::ERR/.test(e));
145console.log('\nconsole errors:', errs.slice(0, 4));
146await s.close();
147
148console.log(`\n${ok.length} passed, ${bad.length} failed`);
149if (bad.length) console.log('FAILED:\n ' + bad.join('\n '));
150process.exit(bad.length ? 1 : 0);