Verify P2P pane in a fresh mobile session

This commit is contained in:
vorotamoroz
2026-07-26 08:06:48 +00:00
parent 4194a0067c
commit 51a749099d
6 changed files with 404 additions and 52 deletions
+65 -11
View File
@@ -1,3 +1,5 @@
import { mkdir } from "node:fs/promises";
import { join } from "node:path";
import {
assertLocatorHasMinimumTouchTarget,
assertLocatorWithinSafeArea,
@@ -12,13 +14,20 @@ export const desktopViewport = { width: 1024, height: 768 } as const;
export const iPhoneSafeArea = { top: 47, right: 0, bottom: 34, left: 0 } as const;
type ObsidianTestApp = {
isMobile?: boolean;
emulateMobile?: (mobile: boolean) => void;
plugins?: { plugins: Record<string, unknown> };
workspace?: { layoutReady?: boolean };
};
type ObsidianTestGlobal = typeof globalThis & { app?: ObsidianTestApp };
export async function setObsidianMobileTestMode(port: number, enabled: boolean, timeoutMs: number): Promise<void> {
async function applyObsidianMobileTestMode(
port: number,
enabled: boolean,
timeoutMs: number,
waitForLiveSync: boolean
): Promise<void> {
await withObsidianPage(port, async (page) => {
await page.setViewportSize(enabled ? mobileViewport : desktopViewport);
await page.evaluate((nextEnabled) => {
@@ -28,17 +37,49 @@ export async function setObsidianMobileTestMode(port: number, enabled: boolean,
}
obsidianApp.emulateMobile(nextEnabled);
}, enabled);
await page.waitForFunction(
(nextEnabled) => {
// Obsidian reopens its workspace layout when platform emulation
// changes. Loading a controlled plug-in before that transition has
// completed can leave the plug-in enabled but absent from the active
// renderer.
try {
await page.waitForFunction(
({ nextEnabled, waitForLiveSync }) => {
const obsidianApp = (globalThis as ObsidianTestGlobal).app;
return (
document.body.classList.contains("is-mobile") === nextEnabled &&
obsidianApp?.workspace?.layoutReady === true &&
(!waitForLiveSync || obsidianApp?.plugins?.plugins["obsidian-livesync"] !== undefined)
);
},
{ nextEnabled: enabled, waitForLiveSync },
{ timeout: timeoutMs }
);
} catch (error) {
const state = await page.evaluate(() => {
const obsidianApp = (globalThis as ObsidianTestGlobal).app;
return (
document.body.classList.contains("is-mobile") === nextEnabled &&
obsidianApp?.plugins?.plugins["obsidian-livesync"] !== undefined
);
},
enabled,
{ timeout: timeoutMs }
);
return {
appIsMobile: obsidianApp?.isMobile ?? null,
bodyClasses: document.body.className,
documentReadyState: document.readyState,
liveSyncLoaded: obsidianApp?.plugins?.plugins["obsidian-livesync"] !== undefined,
viewport: { width: window.innerWidth, height: window.innerHeight },
workspaceLayoutReady: obsidianApp?.workspace?.layoutReady ?? null,
};
});
const outputDirectory = process.env.E2E_OBSIDIAN_DIAGNOSTICS_DIR ?? "/tmp/obsidian-livesync-e2e";
await mkdir(outputDirectory, { recursive: true });
const screenshotPath = join(
outputDirectory,
waitForLiveSync
? "mobile-mode-transition.failure.png"
: "mobile-mode-before-plugin-start.failure.png"
);
await page.screenshot({ path: screenshotPath, fullPage: true });
const detail = error instanceof Error ? error.message : String(error);
throw new Error(
`Obsidian mobile-mode transition did not settle: ${JSON.stringify(state)}; screenshot=${screenshotPath}; cause=${detail}`
);
}
await page.evaluate(
(safeArea) => {
for (const edge of ["top", "right", "bottom", "left"] as const) {
@@ -52,6 +93,19 @@ export async function setObsidianMobileTestMode(port: number, enabled: boolean,
});
}
/** Enters mobile emulation before LiveSync's first load in a controlled session. */
export async function setObsidianMobileTestModeBeforePluginStart(
port: number,
enabled: boolean,
timeoutMs: number
): Promise<void> {
await applyObsidianMobileTestMode(port, enabled, timeoutMs, false);
}
export async function setObsidianMobileTestMode(port: number, enabled: boolean, timeoutMs: number): Promise<void> {
await applyObsidianMobileTestMode(port, enabled, timeoutMs, true);
}
export async function assertMobileDialogueLayout(page: Page, container: Locator, label: string): Promise<void> {
const dialogue = container.locator(".modal").last();
const closeButton = dialogue.locator(".modal-close-button");
+32
View File
@@ -52,4 +52,36 @@ describe("LiveSync real-Obsidian session", () => {
})
);
});
it("forwards instance-scoped lifecycle hooks and the selected plug-in start mode", async () => {
const beforePluginStart = vi.fn(async () => undefined);
const vault = {
path: "/tmp/mobile-vault",
statePath: "/tmp/mobile-state",
name: "mobile-vault",
id: "mobile-vault-id",
homePath: "/tmp/mobile-state/home",
xdgConfigPath: "/tmp/mobile-state/xdg-config",
xdgCachePath: "/tmp/mobile-state/xdg-cache",
xdgDataPath: "/tmp/mobile-state/xdg-data",
userDataPath: "/tmp/mobile-state/user-data",
processMarker: "/tmp/mobile-state",
dispose: vi.fn(async () => undefined),
};
await startObsidianLiveSyncSession({
binary: "/Applications/Obsidian",
cliBinary: "obsidian-cli",
vault,
pluginStartup: "controlled",
lifecycle: { beforePluginStart },
});
expect(startObsidianPluginSession).toHaveBeenCalledWith(
expect.objectContaining({
lifecycle: { beforePluginStart },
pluginStartup: "controlled",
})
);
});
});
+10 -1
View File
@@ -1,4 +1,9 @@
import { startObsidianPluginSession, type ObsidianPluginSession } from "@vrtmrz/obsidian-test-session";
import {
startObsidianPluginSession,
type ObsidianPluginSession,
type ObsidianPluginSessionLifecycle,
type ObsidianPluginStartupMode,
} from "@vrtmrz/obsidian-test-session";
import type { TemporaryVault } from "./vault.ts";
export type ObsidianLiveSyncSession = ObsidianPluginSession;
@@ -11,6 +16,8 @@ export type StartObsidianLiveSyncSessionOptions = {
startupGraceMs?: number;
pluginData?: Record<string, unknown>;
localStorageEntries?: Readonly<Record<string, string>>;
pluginStartup?: ObsidianPluginStartupMode;
lifecycle?: ObsidianPluginSessionLifecycle;
env?: NodeJS.ProcessEnv;
};
@@ -26,6 +33,8 @@ export async function startObsidianLiveSyncSession(
startupGraceMs: options.startupGraceMs,
pluginData: options.pluginData,
localStorageEntries: options.localStorageEntries,
pluginStartup: options.pluginStartup,
lifecycle: options.lifecycle,
env: options.env,
});
}