Skip to content
Open
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
71 changes: 71 additions & 0 deletions desktop/src/app/communityViewTransition.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,49 @@ import {
completeCommunityViewTransition,
replaceCommunityDestinationRoute,
runCommunityViewTransition,
shouldSkipCommunityViewTransition,
} from "./communityViewTransition.ts";

const originalDocument = globalThis.document;
const originalWindow = globalThis.window;
const originalNavigator = Object.getOwnPropertyDescriptor(
globalThis,
"navigator",
);

const WEBKITGTK_UA =
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Safari/605.1.15";
const MAC_UA =
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.4 Safari/605.1.15";

afterEach(() => {
globalThis.document = originalDocument;
globalThis.window = originalWindow;
if (originalNavigator) {
Object.defineProperty(globalThis, "navigator", originalNavigator);
} else {
delete globalThis.navigator;
}
mock.restoreAll();
});

// `undefined` removes navigator entirely, so `typeof navigator === "undefined"`.
// afterEach puts the real one back.
function setNavigator(value) {
if (value === undefined) {
delete globalThis.navigator;
return;
}
Object.defineProperty(globalThis, "navigator", { configurable: true, value });
}

function installBrowser(startViewTransition) {
globalThis.window = { clearTimeout, setTimeout };
globalThis.document = { startViewTransition };
// node reports navigator.platform as "Linux x86_64", which is exactly what
// the crash guard skips the transition for. Default these tests to a
// platform that keeps the animation path; the linux cases set their own.
setNavigator({ platform: "MacIntel", userAgent: MAC_UA });
}

function transitionFor(callback) {
Expand All @@ -33,6 +62,48 @@ test("replaceCommunityDestinationRoute uses router history and encodes the chann
assert.deepEqual(replacements, ["/channels/channel%2Fwith%20spaces"]);
});

test("the transition is skipped on linux and kept everywhere else", () => {
setNavigator({ platform: "Linux x86_64", userAgent: WEBKITGTK_UA });
assert.equal(shouldSkipCommunityViewTransition(), true);

// Chromium on Linux is skipped too. The guard fails closed on purpose: it
// cannot tell a WebKitGTK webview from a Linux browser without sniffing the
// user agent, and losing an animation costs less than a segfault.
setNavigator({
platform: "Linux x86_64",
userAgent:
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36",
});
assert.equal(shouldSkipCommunityViewTransition(), true);

setNavigator({ platform: "MacIntel", userAgent: MAC_UA });
assert.equal(shouldSkipCommunityViewTransition(), false);

// Android reports a Linux platform but is not a WebKitGTK desktop webview.
setNavigator({
platform: "Linux armv8l",
userAgent:
"Mozilla/5.0 (Linux; Android 14) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Mobile Safari/537.36",
});
assert.equal(shouldSkipCommunityViewTransition(), false);

setNavigator(undefined);
assert.equal(shouldSkipCommunityViewTransition(), false);
});

test("linux runs the update without starting a transition", async () => {
const startViewTransition = mock.fn((callback) => transitionFor(callback));
installBrowser(startViewTransition);
setNavigator({ platform: "Linux x86_64", userAgent: WEBKITGTK_UA });

let updated = false;
await runCommunityViewTransition(async () => {
updated = true;
});
assert.equal(updated, true);
assert.equal(startViewTransition.mock.callCount(), 0);
});

test("unsupported browsers execute the update and contain rejection", async () => {
installBrowser(undefined);
const expected = new Error("navigation failed");
Expand Down
24 changes: 23 additions & 1 deletion desktop/src/app/communityViewTransition.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { isLinuxPlatform } from "@/shared/lib/platform";

const COMMUNITY_TRANSITION_TIMEOUT_MS = 5_000;

let finishPendingTransition: (() => void) | null = null;
Expand All @@ -13,11 +15,31 @@ export function replaceCommunityDestinationRoute(
history.replace(`/channels/${encodeURIComponent(channelId)}`);
}

// WebKitGTK (the engine under every Linux Tauri webview) crashes the UI
// process when a view transition is the first thing to demand accelerated
// compositing on a machine whose accelerated backing store could not be
// created (X11 sessions, dmabuf transport disabled or unavailable): the
// missing-store guard is a debug ASSERT that release builds compile out, so
// document.startViewTransition() segfaults instead of animating. Reproduced
// 100% outside Buzz on WebKitGTK 2.52 regardless of the dmabuf env vars —
// see #3488, #4142, and https://bugs.webkit.org/show_bug.cgi?id=321683.
//
// Keyed on the platform alone, deliberately: this guards a UI-process
// segfault, so it has to fail closed. Narrowing it to WebKitGTK would mean
// sniffing the user agent for the engine, and a UA test that quietly stops
// matching re-enables the crash with nothing pointing back at this line. On
// Tauri, Linux means WebKitGTK anyway, so the cost of over-matching is only a
// missing cross-fade in a Linux browser session — a crash is not a tradeoff
// worth taking for an animation.
export function shouldSkipCommunityViewTransition(): boolean {
return isLinuxPlatform();
}

export async function runCommunityViewTransition(
update: () => Promise<void> | void,
options: { timeoutMs?: number } = {},
): Promise<void> {
if (!document.startViewTransition) {
if (!document.startViewTransition || shouldSkipCommunityViewTransition()) {
try {
await update();
} catch (error) {
Expand Down