Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 15 additions & 9 deletions tests/e2e/external-settings-change.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -229,16 +229,22 @@ it("does not treat its own saves as external changes", async () => {
const { obsidian, plugin } = getContext();
const before = await plugin.data<QuickAddData>().read();
await obsidian.dev.evalJson(COUNT_EXTERNAL_CHANGES);

await obsidian.dev.evalJson(`(() => {
const saveShowCaptureNotification = (value: boolean) => obsidian.dev.evalJsonAsync(`(() => {
const tab = app.setting.pluginTabs.find((t) => t.id === "quickadd");
tab.setControlValue("showCaptureNotification", ${!before.showCaptureNotification});
return true;
tab.setControlValue("showCaptureNotification", ${value});
return app.plugins.plugins.quickadd.saveSettings().then(() => true);
})()`);
await obsidian.dev.evalJsonAsync("app.plugins.plugins.quickadd.saveSettings().then(() => true)");
expect((await plugin.data<QuickAddData>().read()).showCaptureNotification).toBe(!before.showCaptureNotification);

// Obsidian debounces its file watcher; give it well past that to call in.
await new Promise((resolve) => setTimeout(resolve, 2_500));
expect((await obsidian.dev.evalJson<LiveState>(LIVE_STATE)).externalChanges).toBe(0);
// The harness only restores data.json after a write through plugin.data(),
// which would be an external change here, so this test undoes its own save.
try {
await saveShowCaptureNotification(!before.showCaptureNotification);
expect((await plugin.data<QuickAddData>().read()).showCaptureNotification).toBe(!before.showCaptureNotification);

// Obsidian debounces its file watcher; give it well past that to call in.
await new Promise((resolve) => setTimeout(resolve, 2_500));
expect((await obsidian.dev.evalJson<LiveState>(LIVE_STATE)).externalChanges).toBe(0);
} finally {
await saveShowCaptureNotification(before.showCaptureNotification);
}
});
2 changes: 2 additions & 0 deletions tests/e2e/one-page-pinned-actions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,8 @@ it.each(["desktop", "is-phone"])("keeps Submit in view on a form taller than the
// footer doesn't clip its focus ring.
await typeInto(obsidian, ".onePageInputModal textarea", "Pinned");
await pressKey(obsidian, "Tab");
// The ring grows in over a transition; measure it at its full size.
await obsidian.dev.evalJsonAsync(`Promise.all(document.activeElement.getAnimations().map((animation) => animation.finished)).then(() => true)`);
const focused = await layout();
expect(focused.focused).toBe('button.mod-cta "Submit"');
expect(focused.ringFits).toBe(true);
Expand Down
4 changes: 3 additions & 1 deletion tests/e2e/package-import-result.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,10 @@ it("shows the import result in view, with a single Close", async () => {
if (!summary) return null;
const body = document.querySelector(".qa-package-body").getBoundingClientRect();
const rect = summary.getBoundingClientRect();
// Scrolling snaps to device pixels, so a summary scrolled flush with the
// edge can overhang it by a fraction of one.
return {
inView: rect.top >= body.top && rect.bottom <= body.bottom,
inView: rect.top >= body.top - 0.5 && rect.bottom <= body.bottom + 0.5,
buttons: Array.from(document.querySelectorAll(".qa-package-dialog .modal-button-container button"), (b) => b.textContent.trim()),
};
})()`), POLL_OPTS).toEqual({ inView: true, buttons: ["Close"] });
Expand Down
5 changes: 4 additions & 1 deletion tests/e2e/settings-popout-save.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { expect, it } from "vitest";
import type IChoice from "../../src/types/choices/IChoice";
import { TemplateChoice } from "../../src/types/choices/TemplateChoice";
import { createQuickAddE2EHarness } from "./e2eVault";
import { setVaultConfig } from "./uiHelpers";
import { setFocusEmulation, setVaultConfig } from "./uiHelpers";

const getContext = createQuickAddE2EHarness("settings-popout-save");

Expand All @@ -20,6 +20,9 @@ for (const mainWindow of ["visible", "hidden"] as const) {
const dataPath = path.join(await obsidian.vaultPath(), ".obsidian/plugins/quickadd/data.json");
const onDisk = () =>
(JSON.parse(readFileSync(dataPath, "utf8")).choices as IChoice[]).map((choice) => choice.name);
// The test setup renders the main window as the window in front, which
// would keep its timers running at full speed while hidden.
if (mainWindow === "hidden") await setFocusEmulation(obsidian, false);
const popout = await obsidian.dev.evalJson<boolean>("app.vault.getConfig('settingsPopoutWindow') ?? true");
try {
// Reorder by a native mouse drag in the popout, and close it 300 ms after the drop.
Expand Down
19 changes: 16 additions & 3 deletions tests/e2e/setup.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,30 @@
import { beforeEach } from "vitest";
import { createQuickAddObsidianClient } from "./e2eVault";
import { takeInputLog } from "./uiHelpers";
import { setFocusEmulation, takeInputLog } from "./uiHelpers";

const obsidian = createQuickAddObsidianClient();

beforeEach(async (ctx) => {
// Notices outlive the spec that raised them. They stack from the top-right
// corner over any modal, so a busy earlier spec can cover a button this test
// clicks, or leave a notice this test asserts on.
await obsidian.dev.evalJson(`(() => {
//
// On macOS, Obsidian shows menus as native OS menus unless the vault turns
// them off, so a spec finds no `.menu` to measure or click. Draw them in the
// page as on Linux and Windows. The vault's setting is put back, so a later
// config save cannot write this one to a real vault (#2207).
const inFront = await obsidian.dev.evalJson<boolean>(`(() => {
for (const notice of document.querySelectorAll(".notice")) notice.remove();
return true;
const nativeMenus = app.vault.config.nativeMenus;
app.vault.config.nativeMenus = false;
app.updateUseNativeMenu();
app.vault.config.nativeMenus = nativeMenus;
return document.visibilityState === "visible" && document.hasFocus();
})()`);
// Without this, transitions and scroll-driven animations never apply on a
// covered window. A spec about a backgrounded window turns it off. Sent
// only when needed, since under load a CLI call can lose its reply.
if (!inFront) await setFocusEmulation(obsidian, true);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
takeInputLog();
ctx.onTestFailed(() => {
const log = takeInputLog();
Expand Down
7 changes: 6 additions & 1 deletion tests/e2e/templater-rerun-deprecation.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { expect, it } from "vitest";
import { afterEach, expect, it } from "vitest";
import { CaptureChoice } from "../../src/types/choices/CaptureChoice";
import { MacroChoice } from "../../src/types/choices/MacroChoice";
import { TemplateChoice } from "../../src/types/choices/TemplateChoice";
Expand Down Expand Up @@ -26,6 +26,11 @@ async function watchNotices() {
})()`);
}

// A connected observer would keep filling `__qaNotices` for every later spec.
afterEach(async () => {
await getContext().obsidian.dev.evalJson("window.__qaNoticeObserver?.disconnect(), true");
});

const noticesContaining = (text: string) => getContext().obsidian.dev.evalJson<number>(
`window.__qaNotices.filter(n => n.includes(${JSON.stringify(text)})).length`,
);
Expand Down
10 changes: 10 additions & 0 deletions tests/e2e/uiHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,16 @@ async function sendInput(obsidian: ObsidianClient, label: string, target: string
}
}

/**
* Renders the main window as the window in front, or stops doing so. A window
* covered by other windows, as a test instance on macOS usually is, gets no
* animation frames and throttled timers. An app reload turns this off.
*/
export async function setFocusEmulation(obsidian: ObsidianClient, enabled: boolean) {
const reply = await obsidian.execText("dev:cdp", { method: "Emulation.setFocusEmulationEnabled", params: JSON.stringify({ enabled }) });
if (!reply.startsWith("{")) throw new Error(`dev:cdp Emulation.setFocusEmulationEnabled failed: ${reply}`);
}

/** Inserts text at the focused element, the way an IME commits it. */
export async function insertText(obsidian: ObsidianClient, text: string) {
await sendInput(obsidian, `insertText ${JSON.stringify(text)}`, INPUT_TARGET, [["Input.insertText", { text }]]);
Expand Down
Loading