Skip to content

Commit 819bdb1

Browse files
ccups101claude
andcommitted
e2e: poll for saved data instead of sleeping a fixed time
"File > New Notebook still works" failed intermittently (3 of 5 runs on 48a6025, so it predates the Tab change; it was red in the run before 28c1c10 was pushed). The app is fine: measured over 6 runs the new notebook is in the DOM within ~10 ms and on disk ~550 ms later. The check read the file once after 800 ms. Saves are debounced 500 ms and any further edit restarts the timer; the new notebook's rename input commits on blur, which fires when the test window loses OS focus, so sometimes the write landed after the read. waitForData(pred) polls the saved file (tolerating a read mid-rename) until the predicate holds or 4 s pass. Every sleep-then-read check now uses it. Three full e2e runs: 54/54 each; menuExport 8/8. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 28c1c10 commit 819bdb1

2 files changed

Lines changed: 29 additions & 12 deletions

File tree

‎test/e2e/app/harness.js‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,19 @@ function makeCtx(win) {
7070
menu: (action) => win.webContents.send("menu-action", action),
7171
file: (name) => path.join(tmp, name),
7272
readData: () => JSON.parse(fs.readFileSync(path.join(tmp, "noteforge-data.json"), "utf-8")),
73+
// Polls the saved file until pred(data) holds (or ms passes) and returns the last data
74+
// read. Saves are debounced 500 ms and any further edit (e.g. a rename input committing
75+
// on window blur) restarts the timer, so a fixed sleep before reading is racy.
76+
async waitForData(pred, ms = 4000) {
77+
const t0 = Date.now();
78+
let data = null;
79+
while (Date.now() - t0 < ms) {
80+
try { data = JSON.parse(fs.readFileSync(path.join(tmp, "noteforge-data.json"), "utf-8")); if (pred(data)) return data; }
81+
catch { /* mid-rename or not written yet */ }
82+
await sleep(100);
83+
}
84+
return data;
85+
},
7386
};
7487
}
7588

‎test/e2e/app/scenarios.js‎

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -62,9 +62,9 @@ const SCENARIOS = {
6262
await c.waitFor(`document.querySelector(".nf-editor")`);
6363
await c.js(`(()=>{const ed=document.querySelector(".nf-editor");ed.innerHTML="<p>edited body</p>";ed.dispatchEvent(new Event("input",{bubbles:true}))})()`);
6464
await c.sleep(1000);
65-
const pg = c.readData().notebooks[0].sections[0].pages[0];
66-
c.check("edit saved to disk after debounce", pg.content === "<p>edited body</p>", pg.content);
67-
c.check("schema version stamped", c.readData().version === 1);
65+
const d = await c.waitForData((d) => d.notebooks[0].sections[0].pages[0].content === "<p>edited body</p>");
66+
c.check("edit saved to disk after debounce", d.notebooks[0].sections[0].pages[0].content === "<p>edited body</p>", d.notebooks[0].sections[0].pages[0].content);
67+
c.check("schema version stamped", d.version === 1);
6868
},
6969
},
7070

@@ -132,12 +132,12 @@ SCENARIOS.removePassword = {
132132
await openRemove();
133133
await c.js(`${R.button(".nf-modal-btn", "Cancel")}.click()`);
134134
await c.sleep(800);
135-
let nb = c.readData().notebooks.find((n) => n.id === "nb-l");
135+
let nb = (await c.waitForData(() => true)).notebooks.find((n) => n.id === "nb-l");
136136
c.check("cancel keeps the notebook locked", nb.locked === true && !!nb.encSections);
137137
await openRemove();
138138
await c.js(`${R.button(".nf-modal-btn", "Remove Password")}.click()`);
139139
await c.sleep(1000);
140-
nb = c.readData().notebooks.find((n) => n.id === "nb-l");
140+
nb = (await c.waitForData((d) => d.notebooks.find((n) => n.id === "nb-l").locked === false)).notebooks.find((n) => n.id === "nb-l");
141141
c.check("confirm removes the lock", nb.locked === false && !nb.encSections);
142142
c.check("pages survive removing the lock", JSON.stringify(nb.sections).includes("TOPSECRET"));
143143
},
@@ -158,7 +158,8 @@ SCENARIOS.saveFailureShown = {
158158
await edit("saved now");
159159
await c.sleep(1000);
160160
c.check("next successful write shows 'Saved'", (await status()) === "Saved", await status());
161-
c.check("content on disk after recovery", c.readData().notebooks[0].sections[0].pages[0].content === "<p>saved now</p>");
161+
c.check("content on disk after recovery", (await c.waitForData((d) => d.notebooks[0].sections[0].pages[0].content === "<p>saved now</p>"))
162+
.notebooks[0].sections[0].pages[0].content === "<p>saved now</p>");
162163
},
163164
};
164165

@@ -225,7 +226,8 @@ SCENARIOS.menuExport = {
225226
c.check("File > Export as Text exports the current page", c.dialogs.includes("save:Page_B.txt"), c.dialogs.join(", "));
226227
c.menu("new-notebook");
227228
await c.sleep(800);
228-
c.check("File > New Notebook still works", c.readData().notebooks.length === 2);
229+
const afterNew = await c.waitForData((d) => d.notebooks.length === 2);
230+
c.check("File > New Notebook still works", afterNew.notebooks.length === 2, JSON.stringify(afterNew.notebooks.map((n) => n.name)));
229231
},
230232
};
231233

@@ -247,7 +249,7 @@ SCENARIOS.contextMenuPaste = {
247249
await c.sleep(1000);
248250
const text = await c.js(R.editorText);
249251
c.check("context-menu Paste inserts clipboard text at the caret", /alpha body\s*PASTED-FROM-CLIPBOARD/.test(text), JSON.stringify(text));
250-
c.check("pasted text saved", JSON.stringify(c.readData()).includes("PASTED-FROM-CLIPBOARD"));
252+
c.check("pasted text saved", JSON.stringify(await c.waitForData((d) => JSON.stringify(d).includes("PASTED-FROM-CLIPBOARD"))).includes("PASTED-FROM-CLIPBOARD"));
251253
} finally {
252254
await clipboard.writeText(saved);
253255
}
@@ -267,7 +269,8 @@ SCENARIOS.replaceAll = {
267269
await c.sleep(800);
268270
const text = await c.js(R.editorText);
269271
c.check("Replace All inserts the replacement literally", text === "cost 5 US$& ($1) $$, US$& ($1) $$", JSON.stringify(text));
270-
c.check("replacement saved", c.readData().notebooks[0].sections[0].pages[0].content.includes("US$&amp; ($1) $$"));
272+
const replaced = (await c.waitForData((d) => d.notebooks[0].sections[0].pages[0].content.includes("US$&amp;"))).notebooks[0].sections[0].pages[0].content;
273+
c.check("replacement saved", replaced.includes("US$&amp; ($1) $$"), replaced);
271274
},
272275
};
273276

@@ -300,7 +303,7 @@ SCENARIOS.trashToggleKeepsPage = {
300303
await c.js(`(()=>{const ed=document.querySelector(".nf-editor");ed.focus();const r=document.createRange();r.selectNodeContents(ed);r.collapse(false);
301304
const s=getSelection();s.removeAllRanges();s.addRange(r);document.execCommand("insertText",false," typed")})()`);
302305
await c.sleep(1000);
303-
const saved = c.readData().notebooks[0].sections[0].pages[0].content;
306+
const saved = (await c.waitForData((d) => d.notebooks[0].sections[0].pages[0].content.includes("typed"))).notebooks[0].sections[0].pages[0].content;
304307
c.check("typing afterwards keeps the existing content", saved.includes("alpha body") && saved.includes("typed"), saved);
305308
},
306309
};
@@ -355,8 +358,9 @@ SCENARIOS.listTabIndent = {
355358
await press();
356359
c.check("Tab in a numbered list nests it", await nested("second"), await c.js(`document.querySelector(".nf-editor").innerHTML`));
357360
await c.sleep(800);
358-
c.check("nested list saved", /<ol>\s*<li>first<\/li>\s*<ol>|<li>first<ol>/.test(c.readData().notebooks[0].sections[0].pages[0].content),
359-
c.readData().notebooks[0].sections[0].pages[0].content);
361+
const nestedRe = /<ol>\s*<li>first<\/li>\s*<ol>|<li>first<ol>/;
362+
const listHtml = (await c.waitForData((d) => nestedRe.test(d.notebooks[0].sections[0].pages[0].content))).notebooks[0].sections[0].pages[0].content;
363+
c.check("nested list saved", nestedRe.test(listHtml), listHtml);
360364
},
361365
};
362366

0 commit comments

Comments
 (0)