diff --git a/tables/js/main.js b/tables/js/main.js index d395b52..a1b687e 100644 --- a/tables/js/main.js +++ b/tables/js/main.js @@ -72,15 +72,27 @@ // clicked a header link, the URL bar, etc. without moving to // another row first). focusout fires for cell-to-cell moves // too — relatedTarget being outside #table-root distinguishes. + // + // Deferred to next tick (setTimeout 0): the editor's commit + // path tears down its input element and then refocuses the + // owning cell. The remove fires focusout BEFORE the refocus + // runs, with relatedTarget=null (body briefly), so the naive + // sync check would mis-detect a "left the grid" event and + // fire flushAll redundantly alongside the selection-change + // save. Checking document.activeElement on the next tick + // gives the refocus time to settle. const tableRoot = document.getElementById('table-root'); if (tableRoot) { tableRoot.addEventListener('focusout', function (ev) { const next = ev.relatedTarget; if (next && tableRoot.contains(next)) return; - const save = app.modules.save; - if (save && typeof save.flushAll === 'function' && save.dirtyCount() > 0) { - save.flushAll(); - } + setTimeout(function () { + if (tableRoot.contains(document.activeElement)) return; + const save = app.modules.save; + if (save && typeof save.flushAll === 'function' && save.dirtyCount() > 0) { + save.flushAll(); + } + }, 0); }); } diff --git a/tests/browse.spec.js b/tests/browse.spec.js index c3810ba..854bea1 100644 --- a/tests/browse.spec.js +++ b/tests/browse.spec.js @@ -149,30 +149,42 @@ test.describe('Browse', () => { await expect(page.locator('#previewBody')).toContainText('a note inside the zip'); }); - test('Download (zip) bundles the current folder offline', async ({ page }) => { + test('Download (zip) bundles a folder via right-click → Download ZIP', async ({ page }) => { await page.goto(`file://${HTML_PATH}`, { waitUntil: 'load' }); await page.waitForSelector('#addDirectoryBtn', { timeout: 15000 }); await page.evaluate(() => { window.__setMockDirectoryTree('mock-folder', { 'a.txt': 'AAA', - 'sub': { 'b.txt': 'BBB', 'deep': { 'c.txt': 'CCC' } }, - '.zddc': 'acl: { permissions: { "*": r } }', // hidden — must not be in the zip - '_template': { 'scaffold.txt': 'x' }, // hidden dir — must not be in the zip + 'sub': { + 'b.txt': 'BBB', + 'deep': { 'c.txt': 'CCC' }, + '.zddc': 'acl: { permissions: { "*": r } }', // hidden — must not be in the zip + '_template': { 'scaffold.txt': 'x' }, // hidden dir — must not be in the zip + }, }); }); await page.locator('#addDirectoryBtn').click(); await page.waitForSelector('#browseRoot:not(.hidden)', { timeout: 10000 }); - // The Download (zip) button appears once a directory is loaded. - const dlBtn = page.locator('#downloadZipBtn'); - await expect(dlBtn).toBeVisible(); - + // Download ZIP lives in the row's right-click context menu — + // the standalone toolbar button was retired when the context + // menu became the canonical action surface (SPA overhaul, + // commit 94b2e29). The picked-root folder doesn't render as + // a row (only its CONTENTS do), so we test the next-level + // folder: right-click sub/, download sub.zip. + const subRow = page.locator('.tree-row', { has: page.locator('.tree-name__label', { hasText: /^sub$/ }) }); + await subRow.waitFor({ state: 'visible', timeout: 5000 }); const [download] = await Promise.all([ page.waitForEvent('download'), - dlBtn.click(), + (async () => { + await subRow.click({ button: 'right' }); + await page.locator('.zddc-menu__item', { hasText: 'Download ZIP' }) + .first() + .click(); + })(), ]); - expect(download.suggestedFilename()).toBe('mock-folder.zip'); + expect(download.suggestedFilename()).toBe('sub.zip'); const file = await download.path(); const buf = await fs.readFile(file); @@ -190,9 +202,8 @@ test.describe('Browse', () => { return Object.keys(z.files).filter((n) => !z.files[n].dir).sort(); }, b64); expect(entries).toEqual([ - 'mock-folder/a.txt', - 'mock-folder/sub/b.txt', - 'mock-folder/sub/deep/c.txt', + 'sub/b.txt', + 'sub/deep/c.txt', ]); }); }); diff --git a/tests/build-label.spec.js b/tests/build-label.spec.js index 247fe28..17c9cc2 100644 --- a/tests/build-label.spec.js +++ b/tests/build-label.spec.js @@ -45,15 +45,16 @@ for (const tool of tools) { const match = html.match(/class="build-timestamp">(?:]*>)?([^<]+?)(?:<\/span>)?
ZDDC Table - v0.0.20 + v0.0.21-dev · 2026-05-21 16:22:47 · 736f422-dirty
@@ -6686,15 +6686,27 @@ body.is-elevated::after { // clicked a header link, the URL bar, etc. without moving to // another row first). focusout fires for cell-to-cell moves // too — relatedTarget being outside #table-root distinguishes. + // + // Deferred to next tick (setTimeout 0): the editor's commit + // path tears down its input element and then refocuses the + // owning cell. The remove fires focusout BEFORE the refocus + // runs, with relatedTarget=null (body briefly), so the naive + // sync check would mis-detect a "left the grid" event and + // fire flushAll redundantly alongside the selection-change + // save. Checking document.activeElement on the next tick + // gives the refocus time to settle. const tableRoot = document.getElementById('table-root'); if (tableRoot) { tableRoot.addEventListener('focusout', function (ev) { const next = ev.relatedTarget; if (next && tableRoot.contains(next)) return; - const save = app.modules.save; - if (save && typeof save.flushAll === 'function' && save.dirtyCount() > 0) { - save.flushAll(); - } + setTimeout(function () { + if (tableRoot.contains(document.activeElement)) return; + const save = app.modules.save; + if (save && typeof save.flushAll === 'function' && save.dirtyCount() > 0) { + save.flushAll(); + } + }, 0); }); }