mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-11-01 13:58:18 +00:00
Let the CouchDB save warning scroll with the form
This commit is contained in:
1 parent
b205b14c5f
commit
5e9531743f
4 files changed
+115
-4
No files matched your search
@@ -314,14 +314,16 @@
|
|||||||
{#if processing}
|
{#if processing}
|
||||||
{translateMessage("Checking connection... Please wait.")}
|
{translateMessage("Checking connection... Please wait.")}
|
||||||
{:else}
|
{:else}
|
||||||
<UserDecisions>
|
|
||||||
<Decision title={primaryActionTitle} important disabled={!canProceed} commit={() => checkAndCommit()} />
|
|
||||||
{#if setupMode === "settings"}
|
{#if setupMode === "settings"}
|
||||||
<InfoNote warning>
|
<InfoNote warning>
|
||||||
{translateMessage(
|
{translateMessage(
|
||||||
"Saving without a successful connection test keeps this profile, but automatic synchronisation may fail until the connection is corrected."
|
"Saving without a successful connection test keeps this profile, but automatic synchronisation may fail until the connection is corrected."
|
||||||
)}
|
)}
|
||||||
</InfoNote>
|
</InfoNote>
|
||||||
|
{/if}
|
||||||
|
<UserDecisions>
|
||||||
|
<Decision title={primaryActionTitle} important disabled={!canProceed} commit={() => checkAndCommit()} />
|
||||||
|
{#if setupMode === "settings"}
|
||||||
<Decision
|
<Decision
|
||||||
title={translateMessage("Save without connecting")}
|
title={translateMessage("Save without connecting")}
|
||||||
disabled={!canProceed}
|
disabled={!canProceed}
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ Set `showVerboseLog: true` only in isolated plug-in data when a focused investig
|
|||||||
|
|
||||||
Collect evidence before cleanup, and keep process, Vault, profile, and remote-fixture cleanup in `finally`. After `app.emulateMobile(true)`, use the active CDP renderer for fixture operations because Obsidian may remove desktop-only CLI commands. Visually inspect screenshots before copying selected images into user documentation; a passing locator assertion does not establish that a dialogue is readable or unobstructed.
|
Collect evidence before cleanup, and keep process, Vault, profile, and remote-fixture cleanup in `finally`. After `app.emulateMobile(true)`, use the active CDP renderer for fixture operations because Obsidian may remove desktop-only CLI commands. Visually inspect screenshots before copying selected images into user documentation; a passing locator assertion does not establish that a dialogue is readable or unobstructed.
|
||||||
|
|
||||||
The dialogue scenario simulates the host keyboard-height variable while the mobile CouchDB setup dialogue is open. It verifies that the space after the final action does not grow with the keyboard height and restores the original variable and scroll position afterwards. This layout fixture does not open a physical software keyboard.
|
The dialogue scenario simulates the host keyboard-height variable while the mobile CouchDB setup dialogue is open. It verifies that the space after the final action does not grow with the keyboard height and restores the original variable and scroll position afterwards. It also checks that the warning about saving without a connection test scrolls with the form and leaves room for at least two input rows above the fixed actions. This layout fixture does not open a physical software keyboard.
|
||||||
|
|
||||||
## Local Setup
|
## Local Setup
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,13 @@ import { SETTING_KEY_P2P_DEVICE_NAME } from "@vrtmrz/livesync-commonlib/compat/c
|
|||||||
import { $msg } from "../../../src/common/translation.ts";
|
import { $msg } from "../../../src/common/translation.ts";
|
||||||
import { discoverObsidianCli, requireObsidianBinary } from "../runner/environment.ts";
|
import { discoverObsidianCli, requireObsidianBinary } from "../runner/environment.ts";
|
||||||
import { createE2eCouchDbPluginData, waitForLiveSyncCoreReady } from "../runner/liveSyncWorkflow.ts";
|
import { createE2eCouchDbPluginData, waitForLiveSyncCoreReady } from "../runner/liveSyncWorkflow.ts";
|
||||||
import { assertMobileDialogueLayout, assertMobileNoticeLayout, setObsidianMobileTestMode } from "../runner/mobileUi.ts";
|
import {
|
||||||
|
assertMobileDialogueLayout,
|
||||||
|
assertMobileNoticeLayout,
|
||||||
|
iPhoneSafeArea,
|
||||||
|
mobileViewport,
|
||||||
|
setObsidianMobileTestMode,
|
||||||
|
} from "../runner/mobileUi.ts";
|
||||||
import { startObsidianLiveSyncSession, type ObsidianLiveSyncSession } from "../runner/session.ts";
|
import { startObsidianLiveSyncSession, type ObsidianLiveSyncSession } from "../runner/session.ts";
|
||||||
import {
|
import {
|
||||||
captureObsidianDialogue,
|
captureObsidianDialogue,
|
||||||
@@ -472,6 +478,106 @@ async function assertNoExtraKeyboardSpace(page: Page, modal: Locator): Promise<v
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function assertCouchDBKeyboardFormSpace(page: Page, modal: Locator): Promise<void> {
|
||||||
|
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<string> {
|
async function verifyRemoteSelectionDialogue(mode: DialogueMode): Promise<string> {
|
||||||
await openRemoteSelectionDialogue();
|
await openRemoteSelectionDialogue();
|
||||||
const screenshotPath = await captureObsidianDialogue(
|
const screenshotPath = await captureObsidianDialogue(
|
||||||
@@ -603,6 +709,7 @@ async function verifyCouchDBSettingsDialogue(mode: DialogueMode): Promise<string
|
|||||||
if (mode === "mobile") {
|
if (mode === "mobile") {
|
||||||
await assertMobileDialogueLayout(page, modal, "CouchDB settings dialogue");
|
await assertMobileDialogueLayout(page, modal, "CouchDB settings dialogue");
|
||||||
await assertNoExtraKeyboardSpace(page, modal);
|
await assertNoExtraKeyboardSpace(page, modal);
|
||||||
|
await assertCouchDBKeyboardFormSpace(page, modal);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ Earlier releases remain available in the 1.0 release history, the 1.0 preview hi
|
|||||||
|
|
||||||
- Setup dialogues no longer add a large blank area below their buttons when the software keyboard is open.
|
- Setup dialogues no longer add a large blank area below their buttons when the software keyboard is open.
|
||||||
|
|
||||||
|
- CouchDB connection fields have more room above the software keyboard. The warning about saving without a connection test now scrolls with the form.
|
||||||
|
|
||||||
- A new Join or scheduled Fetch asks for data-handling choices again, even after a previous Fetch changed the local database suffix.
|
- A new Join or scheduled Fetch asks for data-handling choices again, even after a previous Fetch changed the local database suffix.
|
||||||
- Fast Setup retains interrupted choices in an editable Markdown code block in the fetch flag. On restart, we confirm the current remote and saved choices, choose again, or cancel; a remaining flag never reapplies choices silently.
|
- Fast Setup retains interrupted choices in an editable Markdown code block in the fetch flag. On restart, we confirm the current remote and saved choices, choose again, or cancel; a remaining flag never reapplies choices silently.
|
||||||
- If the saved selection cannot be read, we can choose new options for that attempt without repairing or rewriting the flag. Completing or cancelling the operation removes the whole flag.
|
- If the saved selection cannot be read, we can choose new options for that attempt without repairing or rewriting the flag. Completing or cancelling the operation removes the whole flag.
|
||||||
|
|||||||
Reference in new issue
Block a user