From 5e9531743f41df71603de7fe3b8d74d90db5beaa Mon Sep 17 00:00:00 2001 From: vorotamoroz Date: Fri, 30 Oct 2026 11:39:38 +0000 Subject: [PATCH] Let the CouchDB save warning scroll with the form --- .../dialogs/SetupRemoteCouchDB.svelte | 12 +- test/e2e-obsidian/README.md | 2 +- test/e2e-obsidian/scripts/dialog-mounts.ts | 109 +++++++++++++++++- updates.md | 2 + 4 files changed, 118 insertions(+), 7 deletions(-) diff --git a/src/modules/features/SetupWizard/dialogs/SetupRemoteCouchDB.svelte b/src/modules/features/SetupWizard/dialogs/SetupRemoteCouchDB.svelte index a163ceee..2c640ee0 100644 --- a/src/modules/features/SetupWizard/dialogs/SetupRemoteCouchDB.svelte +++ b/src/modules/features/SetupWizard/dialogs/SetupRemoteCouchDB.svelte @@ -314,14 +314,16 @@ {#if processing} {translateMessage("Checking connection... Please wait.")} {:else} + {#if setupMode === "settings"} + + {translateMessage( + "Saving without a successful connection test keeps this profile, but automatic synchronisation may fail until the connection is corrected." + )} + + {/if} checkAndCommit()} /> {#if setupMode === "settings"} - - {translateMessage( - "Saving without a successful connection test keeps this profile, but automatic synchronisation may fail until the connection is corrected." - )} - { + const originalViewport = await page.evaluate(() => ({ width: innerWidth, height: innerHeight })); + const properties = ["--keyboard-height", "--safe-area-inset-top", "--safe-area-inset-bottom"]; + const original = await page.evaluate( + (names) => + names.map((name) => ({ + name, + value: document.body.style.getPropertyValue(name), + priority: document.body.style.getPropertyPriority(name), + })), + properties + ); + const content = modal.locator(".modal-content"); + const originalScroll = await content.evaluate((element) => element.scrollTop); + const keyboardHeight = Math.round(mobileViewport.height * 0.42); + try { + await page.setViewportSize(mobileViewport); + await page.evaluate( + ({ keyboardHeight, top }) => { + document.body.style.setProperty("--keyboard-height", `${keyboardHeight}px`); + document.body.style.setProperty("--safe-area-inset-bottom", `${keyboardHeight}px`); + document.body.style.setProperty("--safe-area-inset-top", `${top}px`); + }, + { keyboardHeight, top: iPhoneSafeArea.top } + ); + await modal.locator('input[name="couchdb-username"]').focus(); + await page.waitForTimeout(350); + const warning = modal.locator(".sls-info-note").filter({ + hasText: + "Saving without a successful connection test keeps this profile, but automatic synchronisation may fail until the connection is corrected.", + }); + if ((await warning.count()) !== 1) throw new Error("Expected the untested-save warning to remain available."); + const pinned = await warning.evaluate((element) => { + for (let parent = element.parentElement; parent !== null; parent = parent.parentElement) { + if (parent.classList.contains("modal-content")) break; + const position = getComputedStyle(parent).position; + if (position === "sticky" || position === "fixed") return true; + } + return false; + }); + if (pinned) throw new Error("The untested-save warning still occupies the fixed action area."); + const bounds = await modal.evaluate((element) => { + const host = element.querySelector(".dialog-host"); + const content = element.querySelector(".modal-content"); + const footerElement = host?.querySelector(":scope > .button-group"); + const rowElement = host?.querySelector(".row"); + if (!host || !content || !footerElement || !rowElement) + throw new Error("CouchDB form layout is unavailable."); + const footer = footerElement.getBoundingClientRect(); + const row = rowElement.getBoundingClientRect(); + const modalElement = element.querySelector(".modal"); + if (!modalElement) throw new Error("The CouchDB modal is unavailable."); + const modalRect = modalElement.getBoundingClientRect(); + const contentRect = content.getBoundingClientRect(); + const headerBottom = + element.querySelector(".modal-header")?.getBoundingClientRect().bottom ?? modalRect.top; + const formTop = Math.max(contentRect.top, modalRect.top, headerBottom); + return { + width: innerWidth, + height: innerHeight, + formTop, + footerTop: footer.top, + formHeight: Math.min(footer.top, contentRect.bottom, modalRect.bottom) - formTop, + rowHeight: row.height + (parseFloat(getComputedStyle(host).rowGap) || 0), + footerBottom: footer.bottom, + }; + }); + console.log(JSON.stringify({ event: "couchdb-keyboard-form-space", keyboardHeight, bounds })); + if ( + bounds.width !== mobileViewport.width || + bounds.height !== mobileViewport.height || + bounds.formHeight < bounds.rowHeight * 2 || + bounds.footerBottom > bounds.height - keyboardHeight + 1 + ) { + throw new Error( + `Expected room for at least two input rows above keyboard actions: ${JSON.stringify(bounds)}` + ); + } + await page.screenshot({ + path: join( + process.env.E2E_OBSIDIAN_DIAGNOSTICS_DIR ?? "/tmp/obsidian-livesync-e2e", + "setup-couchdb-keyboard-form.png" + ), + animations: "disabled", + style: ".notice-container { visibility: hidden !important; }", + }); + } finally { + await page.evaluate((entries) => { + for (const { name, value, priority } of entries) { + if (value === "") document.body.style.removeProperty(name); + else document.body.style.setProperty(name, value, priority); + } + }, original); + await page.setViewportSize(originalViewport); + await content.evaluate((element, top) => { + element.scrollTop = top; + }, originalScroll); + } +} + async function verifyRemoteSelectionDialogue(mode: DialogueMode): Promise { await openRemoteSelectionDialogue(); const screenshotPath = await captureObsidianDialogue( @@ -603,6 +709,7 @@ async function verifyCouchDBSettingsDialogue(mode: DialogueMode): Promise