Files
paseo/packages/desktop/src/features/browser-profile.test.ts
T
Mohamed Boudra 328361667f Keep browser sign-ins across tabs and restarts (#2089)
* feat(desktop): share persistent browser data across tabs

Keep browser sign-ins and site storage across tabs and app restarts, with a Settings action to clear the shared profile.

* fix(settings): prevent duplicate browser data clears

* fix(desktop): complete browser data cleanup

Clear legacy per-tab profiles after upgrades and localize the browser data settings in every supported language.

* fix(desktop): close browser profile cleanup races
2026-07-16 00:40:24 +08:00

209 lines
6.2 KiB
TypeScript

import { describe, expect, test } from "vitest";
import {
clearPaseoBrowserProfile,
getLegacyPaseoBrowserProfileSession,
getPaseoBrowserProfileSessions,
listPaseoBrowserProfileGuests,
readLegacyPaseoBrowserIds,
} from "./browser-profile.js";
class FakeProfileSession {
public readonly storageClears: unknown[] = [];
public cacheClears = 0;
public authClears = 0;
public storageClear: Promise<void> = Promise.resolve();
public clearStorageData(options: unknown): Promise<void> {
this.storageClears.push(options);
return this.storageClear;
}
public clearCache(): Promise<void> {
this.cacheClears += 1;
return Promise.resolve();
}
public clearAuthCache(): Promise<void> {
this.authClears += 1;
return Promise.resolve();
}
}
class FakeLiveGuest {
public reloads = 0;
public constructor(
public readonly id: number,
private readonly destroyed = false,
private readonly reloadError: Error | null = null,
) {}
public isDestroyed(): boolean {
return this.destroyed;
}
public reload(): void {
if (this.reloadError) {
throw this.reloadError;
}
this.reloads += 1;
}
}
class FakeWebContents extends FakeLiveGuest {
public constructor(
id: number,
public readonly session: object,
private readonly type: string,
destroyed = false,
) {
super(id, destroyed);
}
public getType(): string {
return this.type;
}
}
describe("listPaseoBrowserProfileGuests", () => {
test("returns every live webview in the shared profile without deduplicating tabs", () => {
const profileSession = {};
const firstWindowGuest = new FakeWebContents(1, profileSession, "webview");
const secondWindowGuest = new FakeWebContents(2, profileSession, "webview");
const foreignProfileGuest = new FakeWebContents(3, {}, "webview");
const mainRenderer = new FakeWebContents(4, profileSession, "window");
const destroyedGuest = new FakeWebContents(5, profileSession, "webview", true);
const guests = listPaseoBrowserProfileGuests({
profileSession,
webContents: [
firstWindowGuest,
secondWindowGuest,
foreignProfileGuest,
mainRenderer,
destroyedGuest,
],
});
expect(guests).toEqual([firstWindowGuest, secondWindowGuest]);
});
});
describe("legacy browser profiles", () => {
test("accepts only unique saved browser ids and resolves their old partitions", () => {
const uuid = "123e4567-e89b-42d3-a456-426614174000";
const fallbackId = "1700000000000-abcd";
const browserIds = readLegacyPaseoBrowserIds([uuid, fallbackId, uuid, "not-a-browser-id", 123]);
const partitions: string[] = [];
const sessions = getPaseoBrowserProfileSessions(
{
fromPartition: (partition) => {
partitions.push(partition);
return new FakeProfileSession();
},
},
browserIds,
);
expect(partitions).toEqual([
"persist:paseo-browser",
`persist:paseo-browser-${uuid}`,
`persist:paseo-browser-${fallbackId}`,
]);
expect(sessions).toHaveLength(3);
});
test("resolves one valid legacy profile for tab-close cleanup", () => {
const partitions: string[] = [];
const sessions = {
fromPartition: (partition: string) => {
partitions.push(partition);
return new FakeProfileSession();
},
};
expect(getLegacyPaseoBrowserProfileSession(sessions, "1700000000000-abcd")).not.toBeNull();
expect(getLegacyPaseoBrowserProfileSession(sessions, "invalid")).toBeNull();
expect(partitions).toEqual(["persist:paseo-browser-1700000000000-abcd"]);
});
});
describe("clearPaseoBrowserProfile", () => {
test("clears site data, HTTP cache, and auth before reloading live guests", async () => {
const profile = new FakeProfileSession();
const legacyProfile = new FakeProfileSession();
let finishStorageClear: (() => void) | null = null;
profile.storageClear = new Promise((resolve) => {
finishStorageClear = resolve;
});
const firstGuest = new FakeLiveGuest(1);
const secondGuest = new FakeLiveGuest(2);
const clearing = clearPaseoBrowserProfile({
profileSessions: [profile, legacyProfile],
listGuests: () => [firstGuest, secondGuest],
logReloadError: () => {},
});
expect(firstGuest.reloads).toBe(0);
expect(secondGuest.reloads).toBe(0);
finishStorageClear?.();
await clearing;
expect(profile.storageClears).toEqual([
{
storages: [
"cookies",
"filesystem",
"indexdb",
"localstorage",
"serviceworkers",
"cachestorage",
"websql",
],
},
]);
expect(profile.cacheClears).toBe(1);
expect(profile.authClears).toBe(1);
expect(legacyProfile.storageClears).toEqual(profile.storageClears);
expect(legacyProfile.cacheClears).toBe(1);
expect(legacyProfile.authClears).toBe(1);
expect(firstGuest.reloads).toBe(1);
expect(secondGuest.reloads).toBe(1);
});
test("skips destroyed guests and logs individual reload failures", async () => {
const profile = new FakeProfileSession();
const destroyedGuest = new FakeLiveGuest(1, true);
const reloadError = new Error("guest disappeared");
const failedGuest = new FakeLiveGuest(2, false, reloadError);
const reloadErrors: Array<{ guestId: number; error: unknown }> = [];
await clearPaseoBrowserProfile({
profileSessions: [profile],
listGuests: () => [destroyedGuest, failedGuest],
logReloadError: (guestId, error) => reloadErrors.push({ guestId, error }),
});
expect(destroyedGuest.reloads).toBe(0);
expect(failedGuest.reloads).toBe(0);
expect(reloadErrors).toEqual([{ guestId: 2, error: reloadError }]);
});
test("propagates clear failures without reloading guests", async () => {
const profile = new FakeProfileSession();
const clearError = new Error("profile locked");
profile.storageClear = Promise.reject(clearError);
const guest = new FakeLiveGuest(1);
await expect(
clearPaseoBrowserProfile({
profileSessions: [profile],
listGuests: () => [guest],
logReloadError: () => {},
}),
).rejects.toBe(clearError);
expect(guest.reloads).toBe(0);
});
});