Files
donutbrowser/e2e/tests/motion.test.mjs
T
2026-09-09 10:09:14 +04:00

1887 lines
73 KiB
JavaScript

import assert from "node:assert/strict";
import { mkdir, rm, writeFile } from "node:fs/promises";
import path from "node:path";
import test from "node:test";
import Color from "color";
import en from "../../src/i18n/locales/en.json" with { type: "json" };
import { THEMES } from "../../src/lib/themes.ts";
import { withApp } from "../lib/app.mjs";
import { extensionZipBase64 } from "../lib/fixtures.mjs";
const slot = (name) => `[data-slot="${name}"]`;
const profileRow = (id) => `tr[data-profile-id="${id}"]`;
const inspectTrigger = (id) =>
`${slot("profile-inspect-trigger")}[data-profile-id="${id}"]`;
const dragHandle = (id) =>
`${slot("profile-drag-handle")}[data-profile-id="${id}"]`;
const tableScroll = `${slot("profile-workspace")} > .scroll-fade`;
const modifier =
process.platform === "darwin" ? { meta: true } : { ctrl: true };
async function createProfile(app, name) {
return app.invoke("create_browser_profile_new", {
name,
browserStr: "wayfern",
version: "150.0.7871.100",
releaseType: "stable",
proxyId: null,
vpnId: null,
wayfernConfig: { fingerprint: "{}" },
groupId: null,
ephemeral: false,
dnsBlocklist: null,
launchHook: null,
});
}
async function waitForSelector(app, selector, present = true) {
return app.waitFor(
() =>
app.execute(
`return Boolean(document.querySelector(arguments[0])) === arguments[1];`,
[selector, present],
),
{ description: `${present ? "present" : "absent"} ${selector}` },
);
}
async function resize(app, width, height) {
const before = await app.session.command("GET", "/window/rect");
const viewport = await app.execute(
"return { width: innerWidth, height: innerHeight };",
);
const contentWidth = width - Math.max(0, before.width - viewport.width);
const contentHeight = height - Math.max(0, before.height - viewport.height);
await app.session.command("POST", "/window/rect", { width, height });
// The native resize response precedes WebKit layout and painting. A capture
// before those complete crops the previous frame to the new window size.
await app.waitFor(
() =>
app.execute(
"return innerWidth === arguments[0] && innerHeight === arguments[1];",
[contentWidth, contentHeight],
),
{ description: `WebView resized to ${contentWidth} by ${contentHeight}` },
);
await app.execute(
"return new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(() => resolve(true))));",
);
}
async function tabForward(app) {
// tauri-wd's /actions dispatches an untrusted Tab event without its default.
// /element/value implements focus traversal, and honors a canceled keydown
// before moveFocus(), including Radix's modal boundary trap.
const active = await app.execute("return document.activeElement;");
await app.session.sendKeys(active, "\uE004");
}
async function activateFocusedByKeyboard(app, key = "\uE006") {
// tauri-wd 0.1.11 does not implement Enter/Space's default button click in
// either keyboard endpoint. Exercise the handlers, then supply only the
// uncanceled missing HTML activation. This is keyboard-path simulation;
// pointer activation is covered separately through actual WebDriver clicks.
await app.execute(`
const state = { target: document.activeElement, events: [], clicks: 0 };
state.recordKey = (event) => state.events.push(event);
state.recordClick = (event) => { if (state.target === event.target || state.target.contains(event.target)) state.clicks++; };
document.addEventListener("keydown", state.recordKey, true);
document.addEventListener("keyup", state.recordKey, true);
document.addEventListener("click", state.recordClick, true);
window.__donutMotionKeyboard = state;
`);
try {
await app.pressShortcut({ key });
await app.execute(`
const state = window.__donutMotionKeyboard;
if (!state.clicks && !state.events.some((event) => event.defaultPrevented) && state.target.isConnected) state.target.click();
`);
} finally {
await app.execute(`
const state = window.__donutMotionKeyboard;
if (!state) return;
document.removeEventListener("keydown", state.recordKey, true);
document.removeEventListener("keyup", state.recordKey, true);
document.removeEventListener("click", state.recordClick, true);
delete window.__donutMotionKeyboard;
`);
}
}
async function emit(app, event, payload) {
await app.invoke("plugin:event|emit", { event, payload });
}
async function assertContained(app, selector) {
const bounds = await app.execute(
`
const element = document.querySelector(arguments[0]);
if (!element) return null;
const rect = element.getBoundingClientRect();
return { left: rect.left, top: rect.top, right: rect.right, bottom: rect.bottom,
width: innerWidth, height: innerHeight };
`,
[selector],
);
assert.ok(bounds, selector);
assert.ok(
bounds.left >= -1 && bounds.right <= bounds.width + 1,
JSON.stringify(bounds),
);
assert.ok(
bounds.top >= -1 && bounds.bottom <= bounds.height + 1,
JSON.stringify(bounds),
);
}
async function exampleStates(app) {
return app.execute(`return Object.fromEntries(
[...document.querySelectorAll('[data-slot="isolation-example-profile"]')].map((profile) =>
[profile.dataset.profileId, { signedIn: profile.dataset.signedIn, route: profile.dataset.route }])
);`);
}
async function assertExamples(app, expected) {
await app.waitFor(
async () => {
const actual = await exampleStates(app);
return (
Object.keys(actual).length === Object.keys(expected).length &&
Object.entries(expected).every(
([id, profile]) =>
actual[id]?.signedIn === profile.signedIn &&
actual[id]?.route === profile.route,
)
);
},
{
description: `independent example state ${JSON.stringify(expected)}`,
},
);
}
const emptyExamples = {
research: { signedIn: "false", route: "direct" },
shopping: { signedIn: "false", route: "direct" },
};
async function exerciseIsolation(app) {
await waitForSelector(app, slot("profile-isolation-demo"));
const realProfiles = await app.invoke("list_browser_profiles");
await assertExamples(app, emptyExamples);
await app.clickSelector(slot("isolation-toggle-cookie"));
await assertExamples(app, {
...emptyExamples,
research: { signedIn: "true", route: "direct" },
});
await app.clickSelector(slot("isolation-toggle-route"));
await assertExamples(app, {
...emptyExamples,
research: { signedIn: "true", route: "proxy" },
});
await app.clickSelector(
`${slot("isolation-example-profile")}[data-profile-id="shopping"] ${slot("isolation-select-profile")}`,
);
await app.clickSelector(slot("isolation-toggle-cookie"));
await assertExamples(app, {
research: { signedIn: "true", route: "proxy" },
shopping: { signedIn: "true", route: "direct" },
});
await app.clickSelector(slot("isolation-toggle-route"));
await app.clickSelector(slot("isolation-toggle-cookie"));
await assertExamples(app, {
research: { signedIn: "true", route: "proxy" },
shopping: { signedIn: "false", route: "proxy" },
});
// Move focus with the real keyboard, then activate the currently focused
// route button. The example changes without waiting for a pointer animation.
await app.execute(
`document.querySelector('[data-slot="isolation-toggle-cookie"]').focus();`,
);
await tabForward(app);
assert.equal(
await app.execute(`return document.activeElement?.dataset.slot;`),
"isolation-toggle-route",
);
await activateFocusedByKeyboard(app);
await assertExamples(app, {
research: { signedIn: "true", route: "proxy" },
shopping: { signedIn: "false", route: "direct" },
});
await app.clickSelector(slot("isolation-reset"));
await assertExamples(app, emptyExamples);
assert.deepEqual(
await app.invoke("list_browser_profiles"),
realProfiles,
"the demonstration never mutates real profiles",
);
}
test("first-run profile cutaway is interactive, isolated, and readable at both window sizes", async () => {
await withApp(
"motion-first-run",
async (app) => {
await app.waitForText(en.welcome.title);
await resize(app, 1080, 900);
await exerciseIsolation(app);
await app.capture("onboarding-isolation-wide");
await resize(app, 640, 480);
await assertContained(app, '[role="dialog"]');
await app.clickSelector(`${slot("welcome-features")} summary`);
await app.waitFor(
() =>
app.execute(
`return document.querySelector('[data-slot="welcome-features"]')?.open === true;`,
),
{
description:
"welcome feature disclosure to open from a real pointer click",
},
);
assert.equal(
await app.visibleTextIncludes(en.welcome.features.items.cookies),
true,
);
await app.clickSelector(`${slot("welcome-features")} summary`);
await app.capture("onboarding-isolation-narrow");
await app.clickText(en.welcome.next, { roles: ["button"] });
await app.waitForText(en.welcome.license.title);
assert.equal(
await app.invoke("get_onboarding_completed"),
false,
"the example does not complete actual onboarding",
);
},
{ onboardingCompleted: false },
);
});
async function openReplay(app) {
await app.clickSelector(`[aria-label="${en.rail.more.label}"]`);
await waitForSelector(app, '[role="menu"]');
await app.clickText(en.rail.more.about, {
exact: false,
roles: ["menuitem"],
});
await app.clickSelector(slot("isolation-demo-replay"));
await waitForSelector(app, slot("profile-isolation-demo"));
}
async function assertPopupReadable(app, selector) {
await app.waitFor(
() =>
app.execute(
`
const content = document.querySelector(arguments[0]);
if (!content || content.getBoundingClientRect().height <= 0) return false;
for (let node = content; node instanceof Element; node = node.parentElement) {
const style = getComputedStyle(node);
if (Number(style.opacity) < 0.99 || style.visibility === "hidden" || style.display === "none") return false;
}
return true;
`,
[selector],
),
{ timeoutMs: 2000, description: `fully readable popup ${selector}` },
);
}
test("paused CSS animations cannot retain dismissed selects or dropdowns", async () => {
await withApp(
"motion-popup-dismissal",
async (app) => {
await resize(app, 1100, 760);
await createProfile(app, "Motion popup Alpha");
await createProfile(app, "Motion popup Beta");
await app.clickSelector(`[aria-label="${en.rail.settings}"]`);
await app.clickSelector("#theme-select");
await assertPopupReadable(app, slot("select-content"));
// Freeze CSS only after the first menu is fully open. A closed Radix
// Presence must not wait for animationend, which cannot arrive here.
await app.execute(`
const style = document.createElement("style");
style.id = "donut-motion-paused-css";
style.textContent = "*, *::before, *::after { animation-play-state: paused !important; }";
document.head.append(style);
window.__donutPausedSelect = document.querySelector('[data-slot="select-content"]');
`);
try {
assert.equal(
await app.execute(
`return getComputedStyle(window.__donutPausedSelect).animationPlayState;`,
),
"paused",
);
await app.clickText(en.common.labels.custom, { roles: ["option"] });
await app.waitFor(
() =>
app.execute(
`return !window.__donutPausedSelect.isConnected && !document.querySelector('[data-slot="select-content"]');`,
),
{
timeoutMs: 2000,
description:
"selected menu to unmount without a CSS animationend event",
},
);
// The next popup also opens under the paused clock: both its content
// and its pointer targets must be available at the initial frame.
await app.clickSelector("#theme-preset-select");
await assertPopupReadable(app, slot("select-content"));
await app.capture("paused-css-preset-select");
const preset = THEMES.find((theme) => theme.id === "dracula");
assert.ok(preset);
await app.clickText(preset.name, { roles: ["option"] });
await waitForSelector(app, slot("select-content"), false);
assert.equal(
await app.execute(
`return document.querySelector("#theme-preset-select")?.textContent.trim();`,
),
preset.name,
);
await app.clickText(en.common.buttons.saveSettings, {
roles: ["button"],
});
await app.waitFor(
() => app.visibleTextIncludes(en.common.buttons.saved),
{ description: "saved settings feedback" },
);
await waitForSelector(app, "#theme-select", true);
await app.clickSelector(`[aria-label="${en.rail.profiles}"]`);
const openSort = () =>
app.clickTextIn("thead", en.common.labels.name, {
roles: ["button"],
});
await openSort();
await assertPopupReadable(app, slot("dropdown-menu-content"));
await app.capture("paused-css-profile-sort");
await app.clickText(en.profiles.sort.nameDesc, {
roles: ["menuitem"],
});
await waitForSelector(app, slot("dropdown-menu-content"), false);
await app.waitFor(
() =>
app.execute(
`return document.querySelector('tr[data-profile-id]')?.textContent.includes("Motion popup Beta");`,
),
{ description: "pointer-selected descending profile order" },
);
await openSort();
await assertPopupReadable(app, slot("dropdown-menu-content"));
await app.clickText(en.profiles.sort.nameAsc, {
roles: ["menuitem"],
});
await waitForSelector(app, slot("dropdown-menu-content"), false);
await app.clickSelector(`[aria-label="${en.rail.more.label}"]`);
await assertPopupReadable(app, '[role="menu"]');
await app.clickText(en.rail.more.about, {
exact: false,
roles: ["menuitem"],
});
await waitForSelector(app, '[role="menu"]', false);
await app.clickSelector(slot("isolation-demo-replay"));
await waitForSelector(app, slot("profile-isolation-demo"));
await app.capture("paused-css-about-replay");
} finally {
await app.execute(`
document.getElementById("donut-motion-paused-css")?.remove();
delete window.__donutPausedSelect;
`);
}
},
{ seedDownloadedBrowser: true },
);
});
async function clickVisible(app, selector) {
// tauri-wd's element click always scrollIntoView(center), including a raw
// session.click. Pointer actions preserve the position of an in-view row.
const point = await pointIn(app, selector);
try {
await pointerActions(app, [
{ type: "pointerMove", x: point.x, y: point.y, origin: "viewport" },
{ type: "pointerDown", button: 0 },
{ type: "pointerUp", button: 0 },
]);
} finally {
await app.session.command("DELETE", "/actions");
}
}
async function visibleProfileIds(app, count = 3) {
return app.waitFor(
() =>
app.execute(
`
const scroller = document.querySelector(arguments[0]);
if (!scroller) return null;
const bounds = scroller.getBoundingClientRect();
const rows = [...scroller.querySelectorAll('tr[data-profile-id]')].filter((row) => {
const rect = row.getBoundingClientRect();
return rect.top > bounds.top + 45 && rect.bottom < bounds.bottom - 110;
});
return rows.length >= arguments[1] ? rows.slice(0, arguments[1]).map((row) => row.dataset.profileId) : null;
`,
[tableScroll, count],
),
{ description: `${count} fully visible profile rows` },
);
}
async function toggleProfile(app, id) {
const selector = `${profileRow(id)} [role="checkbox"], ${profileRow(id)} [aria-label="${en.common.aria.selectProfile}"]`;
await clickVisible(app, selector);
}
async function pointIn(app, selector) {
const point = await app.execute(
`
const node = document.querySelector(arguments[0]);
if (!node) return null;
const rect = node.getBoundingClientRect();
const x = Math.round(rect.left + rect.width / 2);
const y = Math.round(rect.top + rect.height / 2);
const hit = document.elementFromPoint(x, y);
return hit && (hit === node || node.contains(hit)) ? { x, y, left: rect.left, top: rect.top } : null;
`,
[selector],
);
assert.ok(point, `pointer-interactable ${selector}`);
return point;
}
async function pointerActions(app, actions) {
await app.session.command("POST", "/actions", {
actions: [
{
type: "pointer",
id: "motion-profile-pointer",
parameters: { pointerType: "mouse" },
actions,
},
],
});
}
async function beginDrag(app, id) {
const start = await pointIn(app, dragHandle(id));
await pointerActions(app, [
{ type: "pointerMove", x: start.x, y: start.y, origin: "viewport" },
{ type: "pointerDown", button: 0 },
{ type: "pause", duration: 50 },
{
type: "pointerMove",
x: start.x + 16,
y: start.y + 10,
duration: 100,
origin: "viewport",
},
]);
await waitForSelector(
app,
`${slot("profile-drag-preview")}[data-phase="dragging"]`,
);
const preview = await app.execute(`
const rect = document.querySelector('[data-slot="profile-drag-preview"]').getBoundingClientRect();
return { left: rect.left, top: rect.top };
`);
assert.ok(
Math.abs(preview.left - start.left - 16) <= 3,
"drag preserves the horizontal grab offset",
);
assert.ok(
Math.abs(preview.top - start.top - 10) <= 3,
"drag preserves the vertical grab offset",
);
}
async function groupAssignments(app) {
const profiles = await app.invoke("list_browser_profiles");
return Object.fromEntries(
profiles.map((profile) => [profile.id, profile.group_id ?? null]),
);
}
async function installSynchronizerFixture(app) {
// This test supplies a paid user's UI state without changing authentication,
// entitlement checks, or synchronizer state in the backend. The only
// operation being simulated is the readiness promise of start_sync_session.
// Tauri defines invoke/ipc/postMessage as non-writable. Like the native folder
// picker fixture in ui.test.mjs, this wraps only their IPC fetch transport.
await app.execute(`
const fixture = { originalFetch: window.fetch, requests: [], pending: [] };
window.__donutMotionSynchronizer = fixture;
const response = (value, ok = true) => new Response(JSON.stringify(value), {
status: 200, headers: { "content-type": "application/json", "Tauri-Response": ok ? "ok" : "error" }
});
window.fetch = function (input, init) {
let command = "";
try {
const url = new URL(typeof input === "string" ? input : input.url);
if ((url.protocol === "ipc:" && url.hostname === "localhost") ||
(["http:", "https:"].includes(url.protocol) && url.hostname === "ipc.localhost")) {
command = decodeURIComponent(url.pathname.split("/").pop() || "");
}
} catch {}
if (command === "cloud_get_user") return Promise.resolve(response({
logged_in_at: "2026-09-01T00:00:00Z",
user: {
id: "motion-ui-fixture", email: "motion@example.test", plan: "pro",
planPeriod: "monthly", subscriptionStatus: "active", profileLimit: 50,
cloudProfilesUsed: 0, proxyBandwidthLimitMb: 0, proxyBandwidthUsedMb: 0,
proxyBandwidthExtraMb: 0, isPrimaryDevice: true
}
}));
if (command === "start_sync_session") {
fixture.requests.push(JSON.parse(init.body));
return new Promise((resolve) => fixture.pending.push({
resolve: (value) => resolve(response(value)),
// Reject at the Tauri response layer. Rejecting fetch itself would
// trigger its native-transport fallback and launch real browsers.
reject: (error) => resolve(response(error, false))
}));
}
return fixture.originalFetch.apply(window, arguments);
};
`);
assert.equal(
(await app.invoke("cloud_get_user")).user?.id,
"motion-ui-fixture",
"scoped IPC fixture is active before opening paid UI",
);
await emit(app, "cloud-auth-changed", null);
}
async function restoreSynchronizerFixture(app) {
await app.execute(`
const fixture = window.__donutMotionSynchronizer;
if (!fixture) return;
window.fetch = fixture.originalFetch;
for (const pending of fixture.pending) pending.reject("Motion fixture disposed");
delete window.__donutMotionSynchronizer;
`);
await emit(app, "cloud-auth-expired", null);
}
test("synchronizer rehearsal targets selected profiles and waits for actual command readiness", async () => {
await withApp(
"motion-synchronizer",
async (app) => {
await resize(app, 1080, 900);
const leader = await createProfile(app, "Motion rehearsal leader");
const first = await createProfile(app, "Motion rehearsal follower one");
const second = await createProfile(
app,
"Motion rehearsal follower with a longer profile name",
);
await installSynchronizerFixture(app);
const session = {
id: "motion-ui-rehearsal-session",
leader_profile_id: leader.id,
leader_profile_name: leader.name,
followers: [
{
profile_id: first.id,
profile_name: first.name,
failed_at_url: null,
},
],
};
try {
await app.clickSelector(inspectTrigger(leader.id));
await app.clickSelector(
`${slot("profile-info-section")}[data-section="automation"]`,
);
await app.waitFor(
() =>
app.execute(
`return document.querySelector('[data-slot="profile-start-synchronizer"]')?.disabled === false;`,
),
{
description:
"paid fixture can open the real synchronizer selection interface",
},
);
await app.clickSelector(slot("profile-start-synchronizer"));
await waitForSelector(app, slot("synchronizer-follower-dialog"));
const option = (id) =>
`${slot("synchronizer-follower-option")}[data-profile-id="${id}"]`;
const checkbox = (id) => `${option(id)} [role="checkbox"]`;
const checked = (id, value) =>
app.waitFor(
() =>
app.execute(
`return document.querySelector(arguments[0])?.getAttribute("aria-checked") === arguments[1];`,
[checkbox(id), String(value)],
),
{
description: `${id} checkbox becomes ${value}`,
},
);
await app.clickSelector(`${option(first.id)} > span`);
await checked(first.id, true);
await app.clickSelector(checkbox(first.id));
await checked(first.id, false);
await app.execute(`document.querySelector(arguments[0]).focus();`, [
checkbox(first.id),
]);
await activateFocusedByKeyboard(app, "\uE00D");
await checked(first.id, true);
await app.clickSelector(`${option(second.id)} > span`);
await checked(second.id, true);
await app.clickSelector(slot("synchronizer-preview-send"));
const received = () =>
app.execute(
`return [...document.querySelectorAll('[data-slot="synchronizer-preview-follower"][data-received="true"]')].map((node) => node.dataset.profileId).sort();`,
);
await app.waitFor(
async () =>
JSON.stringify(await received()) ===
JSON.stringify([first.id, second.id].sort()),
{
description:
"example click reaches exactly the two selected real profile names",
},
);
assert.equal(
await app.execute(
`return window.__donutMotionSynchronizer.requests.length;`,
),
0,
"rehearsal never starts browsers",
);
await app.capture("synchronizer-rehearsal-wide");
await app.clickSelector(checkbox(second.id));
await checked(second.id, false);
assert.deepEqual(
await received(),
[],
"changing selection resets the previous rehearsal result",
);
await app.execute(
`document.querySelector('[data-slot="synchronizer-preview-send"]').focus();`,
);
await activateFocusedByKeyboard(app);
await app.waitFor(
async () =>
JSON.stringify(await received()) === JSON.stringify([first.id]),
{
description:
"keyboard rehearsal acknowledges only the selected follower",
},
);
assert.equal(
await app.execute(
`return document.querySelectorAll('[data-slot="synchronizer-rehearsal"] path[stroke-width="2.5"]').length;`,
),
0,
"keyboard preview is static",
);
await resize(app, 640, 700);
await assertContained(app, '[role="dialog"]');
await app.capture("synchronizer-rehearsal-narrow");
await app.clickSelector(slot("synchronizer-start"));
await waitForSelector(app, slot("synchronizer-starting"));
assert.deepEqual(
await app.execute(
`return window.__donutMotionSynchronizer.requests;`,
),
[
{
leaderProfileId: leader.id,
followerProfileIds: [first.id],
},
],
);
await emit(app, "sync-session-changed", session);
await waitForSelector(app, slot("synchronizer-starting"));
assert.equal(
await app.execute(
`return document.querySelector('[data-slot="synchronizer-start"]')?.disabled;`,
),
true,
);
await assert.rejects(
app.session.click(
await app.session.findCss(slot("synchronizer-start")),
),
/element does not receive pointer events/,
);
await app.pressShortcut({ key: "Escape" });
assert.equal(
await app.execute(
`return window.__donutMotionSynchronizer.requests.length;`,
),
1,
"pending startup cannot be duplicated",
);
await waitForSelector(app, slot("synchronizer-follower-dialog"));
await app.capture("synchronizer-awaiting-readiness");
await app.execute(
`window.__donutMotionSynchronizer.pending.shift().reject(JSON.stringify({ code: "PROFILE_RUNNING" }));`,
);
await waitForSelector(app, slot("synchronizer-start-error"));
await checked(first.id, true);
await checked(second.id, false);
assert.equal(
await app.execute(
`return document.querySelector('[data-slot="synchronizer-start"]')?.disabled;`,
),
false,
"failed startup can be retried",
);
await app.capture("synchronizer-retry");
await emit(app, "sync-session-ended", session.id);
await app.clickSelector(slot("synchronizer-start"));
await waitForSelector(app, slot("synchronizer-starting"));
assert.equal(
await app.execute(
`return window.__donutMotionSynchronizer.requests.length;`,
),
2,
);
await app.execute(
`window.__donutMotionSynchronizer.pending.shift().resolve(arguments[0]);`,
[session],
);
await waitForSelector(app, slot("synchronizer-follower-dialog"), false);
assert.deepEqual(
await app.invoke("get_sync_sessions"),
[],
"the readiness fixture never creates a backend session",
);
const profiles = await app.invoke("list_browser_profiles");
assert.ok(
profiles.every((profile) => profile.process_id == null),
"rehearsal and simulated readiness launch no browser processes",
);
} finally {
await emit(app, "sync-session-ended", session.id);
await restoreSynchronizerFixture(app);
}
},
{ seedDownloadedBrowser: true },
);
});
async function freezeAnimations(app) {
// Motion captures requestAnimationFrame at module initialization, so replacing
// the global afterward does not stall its engine. Its JS batcher reads
// performance.now on every batch; native animations need a separate pause.
await app.execute(`
const state = {
now: performance.now(),
nowDescriptor: Object.getOwnPropertyDescriptor(performance, "now"),
animateDescriptor: Object.getOwnPropertyDescriptor(Element.prototype, "animate"),
animations: []
};
window.__donutFrozenMotion = state;
Object.defineProperty(performance, "now", { configurable: true, value: () => state.now });
if (state.animateDescriptor?.value) {
Object.defineProperty(Element.prototype, "animate", {
...state.animateDescriptor,
value: function (...args) {
const animation = state.animateDescriptor.value.apply(this, args);
const play = animation.play;
const pause = animation.pause;
state.animations.push({ animation, play, playDescriptor: Object.getOwnPropertyDescriptor(animation, "play") });
// NativeAnimation may explicitly play after construction. Keep that
// path paused too, so its first keyframe remains the rendered frame.
Object.defineProperty(animation, "play", {
configurable: true,
value: function () { play.call(this); pause.call(this); this.currentTime = 0; }
});
pause.call(animation);
animation.currentTime = 0;
return animation;
}
});
}
`);
}
async function resumeAnimations(app) {
await app.execute(`
const state = window.__donutFrozenMotion;
if (!state) return;
if (state.nowDescriptor) Object.defineProperty(performance, "now", state.nowDescriptor);
else delete performance.now;
if (state.animateDescriptor) Object.defineProperty(Element.prototype, "animate", state.animateDescriptor);
for (const { animation, play, playDescriptor } of state.animations) {
if (playDescriptor) Object.defineProperty(animation, "play", playDescriptor);
else delete animation.play;
if (animation.playState === "paused") play.call(animation);
}
delete window.__donutFrozenMotion;
`);
}
test("profile replay, inspector, group gestures, and remote handoff preserve context", async () => {
await withApp(
"motion-workspace",
async (app) => {
await resize(app, 1480, 900);
await openReplay(app);
await exerciseIsolation(app);
await app.capture("isolation-replay-wide");
await resize(app, 640, 480);
await assertContained(app, '[role="dialog"]');
await app.capture("isolation-replay-narrow");
await app.clickText(en.common.buttons.back, { roles: ["button"] });
await waitForSelector(app, slot("isolation-demo-replay"));
assert.equal(
await app.execute(`return document.activeElement?.dataset.slot;`),
"isolation-demo-replay",
);
await app.pressShortcut({ key: "Escape" });
await waitForSelector(app, '[role="dialog"]', false);
await resize(app, 1480, 900);
const group = await app.invoke("create_profile_group", {
name: "Motion research group",
});
const proxy = await app.invoke("create_stored_proxy", {
name: "Motion inspector proxy",
proxySettings: {
proxy_type: "http",
host: "127.0.0.1",
port: 9,
username: null,
password: null,
},
});
for (let index = 0; index < 36; index += 1) {
await createProfile(
app,
`Motion profile ${String(index + 1).padStart(2, "0")}`,
);
}
await app.waitFor(
async () => (await app.invoke("list_browser_profiles")).length === 36,
{ description: "all motion fixture profiles" },
);
await waitForSelector(app, slot("profile-inspect-trigger"));
await app.execute(
`document.querySelector(arguments[0]).scrollTop = 250;`,
[tableScroll],
);
const [first, second] = await visibleProfileIds(app, 2);
await toggleProfile(app, first);
const scrollBefore = await app.execute(
`return document.querySelector(arguments[0]).scrollTop;`,
[tableScroll],
);
assert.ok(scrollBefore > 0);
try {
await freezeAnimations(app);
await clickVisible(app, inspectTrigger(first));
await waitForSelector(
app,
`${slot("profile-inspector")}[data-profile-id="${first}"]`,
);
assert.equal(
await app.execute(`
const content = document.querySelector('[data-slot="profile-info-content"]');
const control = document.querySelector('[data-slot="profile-info-section"][data-section="network"]');
return [content, control].every((element) => {
if (!element || element.getBoundingClientRect().height <= 0) return false;
for (let node = element; node instanceof Element; node = node.parentElement) {
const style = getComputedStyle(node);
if (Number(style.opacity) === 0 || style.visibility === "hidden" || style.display === "none") return false;
}
return true;
});
`),
true,
"inspector text and controls remain visible with the animation clock frozen at its initial frame",
);
assert.equal(
await app.execute(
`return performance.now() === window.__donutFrozenMotion.now && window.__donutFrozenMotion.animations.every(({ animation }) => animation.playState !== "running" && (animation.currentTime === null || animation.currentTime === 0));`,
),
true,
"both JavaScript and newly created native animations remain frozen",
);
await app.capture("profile-inspector-animation-frozen");
} finally {
await resumeAnimations(app);
}
assert.equal(
await app.execute(
`return document.querySelector('[data-slot="dialog-overlay"]') === null;`,
),
true,
"wide inspector is nonmodal",
);
await app.clickSelector(
`${slot("profile-info-section")}[data-section="network"]`,
);
await clickVisible(app, inspectTrigger(second));
await waitForSelector(
app,
`${slot("profile-info-content")}[data-profile-id="${second}"][data-section="network"]`,
);
assert.equal(
await app.execute(
`return document.querySelector(arguments[0]).scrollTop;`,
[tableScroll],
),
scrollBefore,
"profile switching preserves table scroll",
);
assert.equal(
await app.execute(
`return document.querySelector(arguments[0])?.getAttribute("data-state");`,
[profileRow(first)],
),
"selected",
"profile switching preserves selection",
);
await app.capture("profile-inspector-wide");
await app.pressShortcut({ key: "Escape" });
await waitForSelector(app, slot("profile-inspector"), false);
assert.equal(
await app.execute(`return document.activeElement?.dataset.profileId;`),
second,
"Escape restores the last inspection trigger",
);
await activateFocusedByKeyboard(app);
await waitForSelector(app, slot("profile-inspector"));
await app.clickSelector(
`${slot("profile-info-section")}[data-section="network"]`,
);
await app.clickSelector(
`${slot("profile-info-content")}[data-section="network"] [role="combobox"]`,
);
await app.waitFor(
() =>
app.execute(`
const popup = document.querySelector('[data-slot="select-content"]');
return popup?.contains(document.activeElement) && !popup.closest('[aria-hidden="true"]');
`),
{
description: "focused, accessibility-visible inspector proxy picker",
},
);
// Radix Select intentionally closes on window.resize. Its ownership
// cleanup must restore the inspector's accessibility exposure.
await resize(app, 720, 600);
await waitForSelector(app, slot("profile-inspector"), false);
await waitForSelector(app, slot("select-content"), false);
const inspectorAccessible = () =>
app.execute(`
const content = document.querySelector('[data-slot="profile-info-content"]');
return content && !content.closest('[aria-hidden="true"]');
`);
await app.waitFor(inspectorAccessible, {
description: "inspector is accessible after Select closes on resize",
});
await app.clickSelector(
`${slot("profile-info-content")}[data-section="network"] [role="combobox"]`,
);
await app.clickText(proxy.name, { roles: ["option"] });
await waitForSelector(app, slot("select-content"), false);
await app.waitFor(
async () =>
(await app.invoke("list_browser_profiles")).find(
(profile) => profile.id === second,
)?.proxy_id === proxy.id,
{ description: "real pointer selection persists the proxy assignment" },
);
// The proxy is an inert local fixture. Selecting it neither starts a
// browser nor claims the endpoint is reachable.
await resize(app, 1480, 900);
await waitForSelector(app, slot("profile-inspector"));
await app.clickSelector(
`${slot("profile-info-section")}[data-section="overview"]`,
);
const colorTrigger = `[aria-label="${en.profileInfo.fields.windowColor}"]`;
await app.clickSelector(colorTrigger);
await assertPopupReadable(app, slot("popover-content"));
await app.clickSelector(`${slot("popover-content")} input`);
const colorBefore = await app.execute(`
const popup = document.querySelector('[data-slot="popover-content"]');
window.__donutMotionOwnedPopup = { popup, focused: document.activeElement };
return popup.querySelector('input').value;
`);
assert.equal(
await app.execute(
`return document.querySelector(arguments[0]).getAttribute('aria-controls') === window.__donutMotionOwnedPopup.popup.id;`,
[colorTrigger],
),
true,
"the color trigger identifies its actual owned portal",
);
try {
await resize(app, 720, 600);
await waitForSelector(app, slot("profile-inspector"), false);
await assertPopupReadable(app, slot("popover-content"));
assert.deepEqual(
await app.execute(`
const popup = document.querySelector('[data-slot="popover-content"]');
return {
samePopup: popup === window.__donutMotionOwnedPopup.popup,
sameFocus: document.activeElement === window.__donutMotionOwnedPopup.focused,
focusInside: popup?.contains(document.activeElement),
accessible: Boolean(popup && !popup.closest('[aria-hidden="true"]'))
};
`),
{
samePopup: true,
sameFocus: true,
focusInside: true,
accessible: true,
},
"wide-to-narrow resize preserves the owned portal, focus, and accessibility exposure",
);
await app.capture("profile-inspector-owned-popup-narrow");
const colorPoint = await pointIn(
app,
`${slot("popover-content")} .h-32`,
);
try {
await pointerActions(app, [
{
type: "pointerMove",
x: colorPoint.x,
y: colorPoint.y,
origin: "viewport",
},
{ type: "pointerDown", button: 0 },
{
type: "pointerMove",
x: colorPoint.x + 20,
y: colorPoint.y + 10,
duration: 120,
origin: "viewport",
},
{ type: "pointerUp", button: 0 },
]);
} finally {
await app.session.command("DELETE", "/actions");
}
await app.waitFor(
() =>
app.execute(
`return document.querySelector('[data-slot="popover-content"] input').value !== arguments[0];`,
[colorBefore],
),
{
description:
"resized color popup responds to real pointer selection",
},
);
// The picker renders its HSL input separately from its effect-driven
// onColorChange. The controlled swatch is the color committed on close.
const chosenColor = Color(
await app.waitFor(
() =>
app.execute(
`
const color = getComputedStyle(document.querySelector(arguments[0])).backgroundColor;
const before = document.createElement('span').style;
before.backgroundColor = arguments[1];
return color !== before.backgroundColor ? color : null;
`,
[colorTrigger, colorBefore],
),
{
description:
"selected color reaches the controlled profile swatch",
},
),
).hex();
await app.clickSelector(colorTrigger);
await waitForSelector(app, slot("popover-content"), false);
await app.waitFor(
async () =>
(await app.invoke("list_browser_profiles"))
.find((profile) => profile.id === second)
?.window_color?.toLowerCase() === chosenColor.toLowerCase(),
{
description:
"closing the color popup persists its real selected color",
},
);
await app.waitFor(inspectorAccessible, {
description:
"inspector remains accessible after its owned popup closes",
});
} finally {
await app.execute(`delete window.__donutMotionOwnedPopup;`);
}
await resize(app, 1480, 900);
await waitForSelector(app, slot("profile-inspector"));
await app.clickSelector(
`${slot("profile-info-section")}[data-section="automation"]`,
);
const draft = "https://example.com/inspector-draft";
await app.fillSelector(slot("profile-launch-hook-input"), draft);
await app.execute(
`window.__donutMotionDraftInput = document.querySelector('[data-slot="profile-launch-hook-input"]');`,
);
await resize(app, 720, 600);
await waitForSelector(app, slot("profile-inspector"), false);
await waitForSelector(
app,
`${slot("profile-info-content")}[data-profile-id="${second}"][data-section="automation"]`,
);
assert.deepEqual(
await app.execute(`
const input = document.querySelector('[data-slot="profile-launch-hook-input"]');
return { value: input?.value, sameNode: input === window.__donutMotionDraftInput };
`),
{ value: draft, sameNode: true },
"wide-to-narrow transition preserves the actual unsaved editor",
);
await assertContained(app, '[role="dialog"]');
await app.capture("profile-inspector-narrow");
assert.equal(
await app.execute(
`return document.activeElement === document.querySelector('[data-slot="profile-launch-hook-input"]');`,
),
true,
"resizing keeps focus in the unsaved editor",
);
for (let index = 0; index < 14; index += 1) {
await tabForward(app);
assert.equal(
await app.execute(
`return document.querySelector('[role="dialog"]')?.contains(document.activeElement);`,
),
true,
"compact inspector traps keyboard focus inside the modal",
);
}
await resize(app, 1480, 900);
await waitForSelector(app, slot("profile-inspector"));
assert.deepEqual(
await app.execute(`
const input = document.querySelector('[data-slot="profile-launch-hook-input"]');
return { value: input?.value, sameNode: input === window.__donutMotionDraftInput };
`),
{ value: draft, sameNode: true },
"narrow-to-wide transition also preserves the unsaved editor",
);
await app.execute(`delete window.__donutMotionDraftInput;`);
await resize(app, 720, 600);
await waitForSelector(app, slot("profile-inspector"), false);
await app.pressShortcut({ key: "Escape" });
await waitForSelector(app, slot("profile-info-content"), false);
await app.pressShortcut({ key: "k", ...modifier });
await waitForSelector(app, "[cmdk-input]");
await app.session.sendKeys(
await app.session.findCss("[cmdk-input]"),
en.rail.settings,
);
assert.equal(await app.visibleTextIncludes(en.rail.settings), true);
await app.pressShortcut({ key: "Escape" });
await waitForSelector(app, "[cmdk-input]", false);
await resize(app, 1480, 900);
await app.execute(`document.querySelector(arguments[0]).scrollTop = 0;`, [
tableScroll,
]);
// Clear any earlier selection through the actual table checkbox.
const selected = await app.execute(
`return [...document.querySelectorAll('tr[data-state="selected"]')].map((row) => row.dataset.profileId);`,
);
if (selected.includes(first)) await toggleProfile(app, first);
else {
// The selected profile may be virtualized outside the top of the table.
await app.clickSelector(`[aria-label="${en.common.aria.selectAll}"]`);
await app.clickSelector(`[aria-label="${en.common.aria.selectAll}"]`);
}
const [moveFirst, moveSecond, cancelId] = await visibleProfileIds(app);
await toggleProfile(app, moveFirst);
await toggleProfile(app, moveSecond);
try {
await beginDrag(app, moveFirst);
const target = await pointIn(
app,
`[data-profile-group-drop="${group.id}"]`,
);
await pointerActions(app, [
{
type: "pointerMove",
x: target.x,
y: target.y,
duration: 180,
origin: "viewport",
},
]);
await waitForSelector(
app,
`[data-profile-group-drop="${group.id}"][data-drop-state="target"]`,
);
await app.capture("profile-group-drag");
await pointerActions(app, [{ type: "pointerUp", button: 0 }]);
await app.waitFor(
async () => {
const assignments = await groupAssignments(app);
return (
assignments[moveFirst] === group.id &&
assignments[moveSecond] === group.id
);
},
{
description:
"both selected profiles persisted into the drop target group",
},
);
} finally {
await app.session.command("DELETE", "/actions");
}
await waitForSelector(app, slot("profile-drag-preview"), false);
const beforeCancel = await groupAssignments(app);
try {
await beginDrag(app, cancelId);
await app.pressShortcut({ key: "Escape" });
} finally {
await app.session.command("DELETE", "/actions");
}
await waitForSelector(app, slot("profile-drag-preview"), false);
assert.deepEqual(
await groupAssignments(app),
beforeCancel,
"Escape never writes a group assignment",
);
await app.execute(`document.querySelector(arguments[0]).focus();`, [
dragHandle(cancelId),
]);
await activateFocusedByKeyboard(app);
await app.waitForText(en.groupAssignment.title);
await app.pressShortcut({ key: "Escape" });
await waitForSelector(app, '[role="dialog"]', false);
await clickVisible(app, inspectTrigger(cancelId));
await app.clickSelector(
`${slot("profile-info-section")}[data-section="overview"]`,
);
const handoff = `${slot("profile-handoff-detail")}[data-profile-id="${cancelId}"]`;
// These are frontend event-contract checks. No remote browser or real
// sync transfer is started, and no networking success is inferred.
await emit(app, "remote-handoff-changed", { [cancelId]: "running" });
await waitForSelector(app, `${handoff}[data-handoff-state="running"]`);
await emit(app, "remote-handoff-changed", {});
await waitForSelector(app, handoff, false);
await emit(app, "remote-handoff-changed", { [cancelId]: "pending_sync" });
await waitForSelector(
app,
`${handoff}[data-handoff-state="pending_sync"]`,
);
await emit(app, "profile-sync-status", {
profile_id: cancelId,
status: "error",
error: "Motion fixture sync failure",
});
await app.waitFor(
() =>
app.execute(
`return document.querySelector(arguments[0])?.textContent.includes(arguments[1]);`,
[handoff, en.profileMotion.handoffError],
),
{
description:
"failed return keeps its pending state and explains the error",
},
);
await waitForSelector(
app,
`${handoff}[data-handoff-state="pending_sync"]`,
);
await app.capture("profile-handoff-retry");
await emit(app, "profile-sync-status", {
profile_id: cancelId,
status: "synced",
});
await waitForSelector(
app,
`${handoff}[data-handoff-state="pending_sync"]`,
);
await emit(app, "remote-handoff-changed", {});
await waitForSelector(app, `${handoff}[data-handoff-state="returned"]`);
await app.capture("profile-handoff-returned");
await app.clickTextIn(handoff, en.common.buttons.close, {
roles: ["button"],
});
await waitForSelector(app, handoff, false);
},
{ seedDownloadedBrowser: true },
);
});
test("settings save a RAM-only edit, preserve location, and expose review and discard", async () => {
await withApp("motion-settings-feedback", async (app) => {
await resize(app, 1000, 720);
await app.clickSelector(`[aria-label="${en.rail.settings}"]`);
await waitForSelector(app, slot("settings-search"));
await app.fillSelector(slot("settings-search"), "decrypted");
assert.deepEqual(
await app.execute(
`return [...document.querySelectorAll('[data-settings-section]')].filter(node => !node.hidden).map(node => node.dataset.settingsSection);`,
),
["advanced"],
);
const initial = await app.invoke("get_app_settings");
const selector = "#keep-decrypted-profiles-in-ram";
await app.clickSelector(selector);
await app.clickSelector(`${slot("settings-feedback")} summary`);
assert.equal(
await app.visibleTextIncludes(en.settings.keepDecryptedProfilesInRam),
true,
);
await app.capture("review-ram-change");
await app.clickSelector(slot("settings-discard"));
assert.equal(
await app.execute(
`return document.querySelector(arguments[0]).getAttribute("data-state") === "checked";`,
[selector],
),
Boolean(initial.keep_decrypted_profiles_in_ram),
);
await app.clickSelector(selector);
await app.clickText(en.common.buttons.saveSettings, { roles: ["button"] });
await app.waitFor(
async () =>
(await app.invoke("get_app_settings"))
.keep_decrypted_profiles_in_ram !==
Boolean(initial.keep_decrypted_profiles_in_ram),
{ description: "RAM preference persisted" },
);
await waitForSelector(app, slot("settings-search"));
assert.equal(
await app.execute(`return document.querySelector(arguments[0]).value;`, [
slot("settings-search"),
]),
"decrypted",
);
await app.waitFor(() => app.visibleTextIncludes(en.common.buttons.saved), {
description: "inline save confirmation",
});
await resize(app, 780, 580);
await app.capture("settings-small-saved");
assert.equal(
await app.execute(
`const el = [...document.querySelectorAll("button")].find(button => button.textContent.trim() === arguments[0]); const r = el.getBoundingClientRect(); return r.bottom <= innerHeight && r.right <= innerWidth && r.top >= 0;`,
[en.common.buttons.saveSettings],
),
true,
);
await app.pressShortcut({ ...modifier, key: "/" });
await waitForSelector(app, slot("shortcuts-search"));
await app.fillSelector(slot("shortcuts-search"), en.shortcuts.goProfiles);
assert.equal(await app.visibleTextIncludes(en.shortcuts.goProfiles), true);
assert.equal(await app.visibleTextIncludes(en.shortcuts.goGroups), false);
await app.capture("shortcuts-search");
});
});
test("profile notes save through the inspector and real launch failures retain a receipt", async () => {
await withApp("motion-profile-feedback", async (app) => {
await resize(app, 1200, 800);
const profile = await createProfile(app, "Feedback profile");
await waitForSelector(app, inspectTrigger(profile.id));
await app.clickSelector(inspectTrigger(profile.id));
await app.clickSelector(slot("profile-edit-note"));
await app.fillSelector(
`textarea[aria-label="${en.profileInfo.fields.note}"]`,
"Remember the launch context",
);
await app.clickTextIn(slot("popover-content"), en.common.buttons.save, {
roles: ["button"],
});
await app.waitFor(
async () =>
(await app.invoke("list_browser_profiles")).find(
(item) => item.id === profile.id,
)?.note === "Remember the launch context",
{ description: "note saved" },
);
// A cross-platform launch is rejected before starting any process.
const otherOs = process.platform === "darwin" ? "linux" : "macos";
await app.invokeError("launch_browser_profile", {
profile: { ...profile, host_os: otherOs },
url: null,
});
await waitForSelector(
app,
`${slot("profile-launch-activity")}[data-stage="failed"]`,
);
await app.clickSelector(`${slot("profile-launch-activity")} summary`);
assert.equal(
await app.visibleTextIncludes(en.appFeedback.launch.preparing),
true,
);
assert.equal(
await app.visibleTextIncludes(en.appFeedback.launch.failed),
true,
);
await app.capture("profile-launch-receipt");
});
});
test("schedule displays every timezone and slot, and skipped run details work by keyboard", async () => {
await withApp("motion-schedule-feedback", async (app) => {
await resize(app, 1200, 800);
const profile = await createProfile(app, "Two daily slots");
const other = await createProfile(app, "Separate timezone");
await installSynchronizerFixture(app);
const schedule = (item, timezone, slots) => ({
profile_id: item.id,
profile_name: item.name,
owner_user_id: "motion-ui-fixture",
enabled: true,
timezone,
slots,
run_at_minute: slots[0].run_at_minute,
days_mask: slots[0].days_mask,
max_minutes: 30,
platform: "linux",
preset: "light",
sites: [],
template_id: null,
next_run_at: "2026-09-09T02:00:00Z",
last_run_at: null,
});
const schedules = [
schedule(profile, "UTC", [
{ days_mask: 127, run_at_minute: 120 },
{ days_mask: 127, run_at_minute: 720 },
]),
schedule(other, "Asia/Yerevan", [{ days_mask: 127, run_at_minute: 120 }]),
];
await app.execute(
`
const schedules = arguments[0], id = arguments[1];
const previous = window.fetch;
window.__donutScheduleCalls = [];
const response = value => Promise.resolve(new Response(JSON.stringify(value), { status: 200, headers: { "content-type": "application/json", "Tauri-Response": "ok" } }));
window.fetch = function(input, init) {
let command = "";
try { const url = new URL(typeof input === "string" ? input : input.url); if (url.hostname === "localhost" && url.protocol === "ipc:" || url.hostname === "ipc.localhost") command = decodeURIComponent(url.pathname.split("/").pop() || ""); } catch {}
if (command === "get_cookie_bot_schedules") return response({ schedules });
if (command === "get_remote_hours_quota") return response({ granted_hours: 200, remaining_hours: 199.8, used_hours: 0.2, seats: 1, per_seat_hours: 200, members: [] });
if (command === "get_cookie_bot_runs") return response({ runs: [{ id: "skipped-fixture", profile_id: id, profile_name: "Two daily slots", status: "skipped", scheduled_for: "2026-09-08T02:00:00Z", max_minutes: 30, chunks_total: 1, chunk_index: 0, sites_total: 0, sites_visited: 0, sites_failed: 0, consent_dismissed: 0, billed_seconds: 0, outcome_code: "profile_locked" }], next_before: null });
return previous.apply(window, arguments);
};
`,
[schedules, profile.id],
);
await app.pressShortcut({ ...modifier, key: "b" });
await app.clickText(en.cookieBot.tabs.schedule, { roles: ["tab"] });
await waitForSelector(app, slot("schedule-lane"));
assert.deepEqual(
await app.execute(
`return [...document.querySelectorAll('[data-slot="schedule-lane"]')].map(node => Number(node.dataset.minute)).sort((a, b) => a - b);`,
),
[120, 120, 720],
);
assert.equal(await app.visibleTextIncludes("Asia/Yerevan"), true);
assert.equal(await app.visibleTextIncludes("199.8"), true);
await app.capture("every-schedule-slot");
await app.clickText(en.cookieBot.tabs.activity, { roles: ["tab"] });
await app.clickSelector('[role="combobox"]');
await app.clickText(en.cookieBot.runStatus.skipped, { roles: ["option"] });
await waitForSelector(app, slot("run-details-toggle"));
await app.execute(`document.querySelector(arguments[0]).focus();`, [
slot("run-details-toggle"),
]);
await activateFocusedByKeyboard(app);
assert.equal(
await app.execute(
`return document.querySelector(arguments[0]).getAttribute("aria-expanded");`,
[slot("run-details-toggle")],
),
"true",
);
await app.capture("skipped-run-details");
await restoreSynchronizerFixture(app);
});
});
test("connection diagnostics and extension impact expose real assigned profiles", async () => {
await withApp("motion-connection-impact", async (app) => {
await resize(app, 1120, 800);
const profile = await createProfile(app, "Assigned feedback profile");
const proxy = await app.invoke("create_stored_proxy", {
name: "Unavailable local route",
proxySettings: {
proxy_type: "http",
host: "127.0.0.1",
port: 9,
username: null,
password: null,
},
});
await app.invoke("update_profile_proxy", {
profileId: profile.id,
proxyId: proxy.id,
});
await app.pressShortcut({ ...modifier, key: "n" });
await waitForSelector(app, slot("profile-usage"));
await app.clickSelector(slot("profile-usage"));
await app.waitForText(profile.name);
await app.capture("proxy-assignment-list");
await app.pressShortcut({ key: "Escape" });
await app.clickSelector(`[aria-label="${en.proxyCheck.tooltipDefault}"]`);
await waitForSelector(app, slot("proxy-route-details"));
await app.waitFor(
() => app.visibleTextIncludes(en.proxyCheck.tooltipFailedTitle),
{ description: "actual failed proxy check" },
);
assert.equal(
await app.visibleTextIncludes(en.appFeedback.notVerified),
true,
);
await app.capture("proxy-route-failure");
await app.pressShortcut({ key: "Escape" });
await app.invoke("update_stored_proxy", {
proxyId: proxy.id,
name: proxy.name,
proxySettings: { ...proxy.proxy_settings, port: 19 },
});
assert.equal(
await app.invoke("get_cached_proxy_check", { proxyId: proxy.id }),
null,
);
const extension = await app.invoke("add_extension", {
name: "Fixture",
fileName: "fixture.zip",
fileData: [...Buffer.from(extensionZipBase64(), "base64")],
});
const group = await app.invoke("create_extension_group", {
name: "Feedback extensions",
});
await app.invoke("add_extension_to_group", {
groupId: group.id,
extensionId: extension.id,
});
await app.invoke("assign_extension_group_to_profile", {
profileId: profile.id,
extensionGroupId: group.id,
});
await app.invoke("update_extension", {
extensionId: extension.id,
name: "My chosen name",
fileName: null,
fileData: null,
});
await app.pressShortcut({ ...modifier, key: "e" });
await app.clickText("My chosen name", { roles: ["button"], exact: false });
await waitForSelector(app, slot("assignment-impact"));
const disclosures = await app.execute(
`return [...document.querySelectorAll('[data-slot="assignment-impact"] summary')].map(node => node.textContent.trim());`,
);
assert.ok(disclosures.some((text) => text.includes("1")));
await app.clickSelector(
`${slot("assignment-impact")} li:last-child summary`,
);
await app.waitForText(profile.name);
await app.waitFor(
() =>
app.execute(`
const impact = document.querySelector('[data-slot="assignment-impact"]');
const marker = impact.querySelector('[data-slot="operation-marker"]')?.getBoundingClientRect();
const label = impact.querySelector('li[aria-current="step"]')?.getBoundingClientRect();
if (!marker || !label || Math.abs(marker.left + marker.width / 2 - label.left - label.width / 2) > 1) return false;
let node = impact;
while (node && node !== document.body) {
if (node.scrollLeft || node.scrollWidth > node.clientWidth + 1) return false;
node = node.parentElement;
}
return true;
`),
{ description: "extension flow aligned without horizontal overflow" },
);
await app.capture("extension-assignment-impact");
await app.clickText(en.appFeedback.useManifestName, { roles: ["button"] });
await app.clickTextIn('[role="dialog"]', en.common.buttons.save, {
roles: ["button"],
});
await app.waitFor(
async () =>
(await app.invoke("list_extensions"))[0].name === extension.name,
{ description: "explicit manifest-name choice persists" },
);
await app.pressShortcut({ ...modifier, key: "i" });
await waitForSelector(app, slot("integration-diagnostics"));
await app.clickTextIn(
slot("integration-diagnostics"),
en.appFeedback.testConnection,
{ roles: ["button"] },
);
await app.waitFor(
() =>
app.execute(
`return document.querySelector('[data-slot="integration-diagnostics"] [role="status"]').textContent.includes(arguments[0]);`,
[en.proxyCheck.tooltipChecked.split("{{")[0]],
),
{ description: "real local API probe receipt" },
);
await app.capture("integration-connection-receipt");
});
});
test("partial import receipts survive retry without duplicating successful profiles", async () => {
await withApp(
"motion-import-receipts",
async (app) => {
const root = path.join(app.root, "import-sources");
const sources = [
path.join(root, "Default"),
path.join(root, "Profile 1"),
];
async function writeSource(index) {
await mkdir(sources[index], { recursive: true });
await writeFile(
path.join(sources[index], "Preferences"),
JSON.stringify({ profile: { name: `Receipt source ${index + 1}` } }),
);
await writeFile(
path.join(sources[index], "Bookmarks"),
JSON.stringify({
roots: {
bookmark_bar: {
type: "folder",
children: [
{ type: "url", name: "Example", url: "https://example.com/" },
],
},
},
}),
);
}
await writeSource(0);
await writeSource(1);
// Only fingerprint generation is fixed for this offline UI suite. Scanning,
// file copying, reports, progress events and retries run through real IPC.
await app.execute(`
window.__donutImportFetch = window.fetch;
window.__donutImportBatches = [];
window.fetch = function(input, init) {
let command = "";
try { const url = new URL(typeof input === "string" ? input : input.url); if (url.hostname === "localhost" && url.protocol === "ipc:" || url.hostname === "ipc.localhost") command = decodeURIComponent(url.pathname.split("/").pop() || ""); } catch {}
if (command === "import_browser_profiles") {
const body = JSON.parse(init.body);
window.__donutImportBatches.push(body.items.map(item => item.source_path));
body.wayfernConfig = { fingerprint: "{}" };
return window.__donutImportFetch.call(this, input, { ...init, body: JSON.stringify(body) });
}
return window.__donutImportFetch.apply(this, arguments);
};
`);
try {
await app.pressShortcut({ ...modifier, key: "o" });
await app.clickText(en.importProfile.manualImport, { roles: ["tab"] });
await app.fillSelector("#manual-profile-path", root);
await app.clickText(en.importProfile.scanButton, { roles: ["button"] });
await app.waitFor(
() =>
app.execute(
`return document.querySelectorAll('[role="checkbox"][data-state="checked"]').length === 3;`,
),
{ description: "both scanned sources selected" },
);
await app.clickText(en.importProfile.nextButton, { roles: ["button"] });
await rm(sources[1], { recursive: true, force: true });
await app.clickText(
en.importProfile.importButtonCount.replace("{{count}}", "2"),
{ roles: ["button"], exact: false },
);
await app.waitFor(
() =>
app.execute(
`return document.querySelectorAll('[data-slot="import-receipt"][data-status="imported"]').length === 1 && document.querySelectorAll('[data-slot="import-receipt"][data-status="failed"]').length === 1;`,
),
{ description: "real partial-import receipts" },
);
const first = await app.invoke("list_browser_profiles");
assert.equal(first.length, 1);
await app.capture("partial-import-receipts");
await writeSource(1);
await app.clickSelector(slot("import-retry"));
await app.waitFor(
() =>
app.execute(
`return document.querySelectorAll('[data-slot="import-receipt"][data-status="imported"]').length === 2;`,
),
{ description: "both import receipts after retry" },
);
const profiles = await app.invoke("list_browser_profiles");
assert.equal(profiles.length, 2);
assert.ok(profiles.some((profile) => profile.id === first[0].id));
await app.waitFor(
() =>
app.execute(
`return [...document.querySelectorAll('[data-sonner-toast]')].some(node => node.getAttribute('data-type') === 'success' && node.textContent.includes(arguments[0]));`,
[
en.importProfile.resultsSummary
.replace("{{imported}}", "2")
.replace("{{skipped}}", "0")
.replace("{{failed}}", "0"),
],
),
{ description: "retry toast agrees with the cumulative receipts" },
);
const batches = await app.execute(
`return window.__donutImportBatches;`,
);
assert.equal(batches[0].length, 2);
assert.deepEqual(batches[1], [sources[1]]);
await resize(app, 760, 620);
await assertContained(app, slot("import-receipts"));
await app.capture("import-retry-receipts-small");
} finally {
await app.execute(
`window.fetch = window.__donutImportFetch; delete window.__donutImportFetch; delete window.__donutImportBatches;`,
);
}
},
{ seedDownloadedBrowser: true },
);
});
test("About hides a snack drawer that responds to bites and resets on close", async () => {
await withApp("motion-donut-snack", async (app) => {
await resize(app, 760, 620);
const openAbout = async () => {
await app.clickSelector(`[aria-label="${en.rail.more.label}"]`);
await app.clickText(en.rail.more.about, {
roles: ["menuitem"],
exact: false,
});
await waitForSelector(app, slot("about-logo"));
};
await openAbout();
await app.clickSelector(slot("about-logo"));
await waitForSelector(app, slot("donut-snack"), false);
await app.execute(
`document.querySelector('[data-slot="about-logo"]').focus();`,
);
await app.pressShortcut({ key: "\uE006", shift: true });
await waitForSelector(app, slot("donut-snack"));
const biteCount = () =>
app.execute(
`return Number(document.querySelector('[data-slot="donut-snack"]').dataset.bites);`,
);
assert.equal(await biteCount(), 0);
await app.capture("secret-snack-drawer");
for (const count of [1, 2, 3]) {
await app.clickSelector(slot("donut-snack-bite"));
assert.equal(await biteCount(), count);
await assertContained(app, slot("donut-snack"));
if (count === 2) await app.capture("secret-snack-two-bites");
}
await app.waitForText(en.about.snack.finished);
await app.capture("secret-snack-crumbs");
await app.clickSelector(slot("donut-snack-bite"));
assert.equal(await biteCount(), 0);
await activateFocusedByKeyboard(app);
assert.equal(await biteCount(), 1);
await freezeAnimations(app);
try {
await app.clickSelector(slot("donut-snack-bite"));
assert.equal(await biteCount(), 2);
await assertPopupReadable(app, slot("donut-snack"));
} finally {
await resumeAnimations(app);
}
assert.deepEqual(
await app.invoke("list_browser_profiles"),
[],
"the snack never touches profiles",
);
await app.pressShortcut({ key: "Escape" });
await waitForSelector(app, slot("donut-snack"), false);
await openAbout();
await waitForSelector(app, slot("donut-snack"), false);
});
});
test("the Wayfern terms gate lifts when the backend announces acceptance", async () => {
// No Wayfern binary in this suite, so the marker is written the way the
// binary writes it and the backend's announcement is replayed. The dialog
// must close on that event alone: a REST or automation acceptance never
// presses the dialog's own button.
await withApp(
"motion-terms-gate",
async (app) => {
const termsDialogVisible = () =>
app.execute(
`return [...document.querySelectorAll('[role="dialog"]')].some(node => node.textContent.includes(arguments[0]));`,
[en.wayfernTerms.title],
);
await app.waitFor(termsDialogVisible, {
description: "the Wayfern terms dialog before acceptance",
});
await app.capture("terms-gate-closed");
await mkdir(path.dirname(app.wayfernTermsFile), { recursive: true });
await writeFile(
app.wayfernTermsFile,
`${Math.floor(Date.now() / 1000)}\n`,
);
assert.equal(await app.invoke("check_wayfern_terms_accepted"), true);
await emit(app, "wayfern-terms-accepted", null);
await app.waitFor(async () => !(await termsDialogVisible()), {
description: "the Wayfern terms dialog to close on the event",
});
},
{ wayfernTermsAccepted: false, seedDownloadedBrowser: true },
);
});
test("the cheat code pays out sprinkles and never touches a profile", async () => {
await withApp("motion-cheat-code", async (app) => {
await resize(app, 1000, 700);
const code = ["", "", "", "", "", "", "", "", "b", "a"];
const toastVisible = () =>
app.execute(
`return [...document.querySelectorAll('[data-sonner-toast]')].some(node => node.textContent.includes(arguments[0]));`,
[en.easterEgg.konami.title],
);
// Arrow keys type nothing; the letters land in a focused field as text.
const typedLetters = code.filter((key) => /^[a-z]$/.test(key)).join("");
// Typed into a field, the code is text, not a command.
await app.pressShortcut({ ...modifier, key: "/" });
await waitForSelector(app, slot("shortcuts-search"));
await app.clickSelector(slot("shortcuts-search"));
for (const key of code) await app.pressShortcut({ key });
assert.equal(await toastVisible(), false);
assert.equal(
await app.execute(`return document.querySelector(arguments[0]).value;`, [
slot("shortcuts-search"),
]),
typedLetters,
);
// Escape empties the filter first; only an empty filter lets it through.
await app.pressShortcut({ key: "Escape" });
assert.equal(
await app.execute(`return document.querySelector(arguments[0]).value;`, [
slot("shortcuts-search"),
]),
"",
);
await app.execute("document.activeElement?.blur();");
for (const key of code) await app.pressShortcut({ key });
await app.waitFor(toastVisible, { description: "the cheat-code toast" });
await app.capture("cheat-code");
assert.deepEqual(
await app.invoke("list_browser_profiles"),
[],
"the cheat code creates nothing",
);
});
});