Skip to content

Commit

Permalink
#9156: tab synchronized mod variable storage (3/3) (#9181)
Browse files Browse the repository at this point in the history
  • Loading branch information
twschiller authored Sep 23, 2024
1 parent 8021e8c commit 9d4cf78
Show file tree
Hide file tree
Showing 19 changed files with 670 additions and 163 deletions.
54 changes: 54 additions & 0 deletions end-to-end-tests/tests/runtime/modVariables/variableSync.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,4 +92,58 @@ test.describe("Mod Variable Sync", () => {
await expect(frameLocator.getByText("Local: 0")).toBeVisible();
});
});

test("tab variable sync", async ({ page, extensionId }) => {
await test.step("activate mod", async () => {
const modId = "@e2e-testing/tab-state-sync";
const modActivationPage = new ActivateModPage(page, extensionId, modId);
await modActivationPage.goto();
await modActivationPage.clickActivateAndWaitForModsPageRedirect();

await page.goto("/frames-builder.html");
});

// Waiting for the mod to be ready before opening sidebar
await expect(page.getByText("Local: 0")).toBeVisible();

// The mod contains a trigger to open the sidebar on h1
await page.click("h1");
const sideBarPage = await getSidebarPage(page, extensionId);
await expect(
sideBarPage.getByRole("heading", { name: "State Sync Demo" }),
).toBeVisible();

await test.step("verify same tab increment", async () => {
await sideBarPage.getByRole("button", { name: "Increment" }).click();

await expect(sideBarPage.getByText("Sync: 1")).toBeVisible();
await expect(sideBarPage.getByText("Local: 1")).toBeVisible();

await expect(page.getByText("Sync: 1")).toBeVisible();
await expect(page.getByText("Local: 1")).toBeVisible();

const frameLocator = page.frameLocator("iframe");
await expect(frameLocator.getByText("Sync: 1")).toBeVisible();
await expect(frameLocator.getByText("Local: 0")).toBeVisible();
});

await test.step("persist on navigation", async () => {
await page.goto("/frames-builder.html");

await expect(page.getByText("Local: 0")).toBeVisible();
await expect(sideBarPage.getByText("Sync: 1")).toBeVisible();

// Tab variables sync within the same tab
const frameLocator = page.frameLocator("iframe");
await expect(frameLocator.getByText("Local: 0")).toBeVisible();
await expect(frameLocator.getByText("Sync: 1")).toBeVisible();
});

const otherPage = await page.context().newPage();
await otherPage.goto(page.url());

await expect(otherPage.getByText("Local: 0")).toBeVisible();
// Tab variables should not sync
await expect(otherPage.getByText("Sync: 1")).toBeHidden();
});
});
2 changes: 2 additions & 0 deletions src/background/background.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ import { initFeatureFlagBackgroundListeners } from "@/auth/featureFlagStorage";
import initTabListener from "./tabs";
import { initApiClient } from "@/data/service/apiClient";
import initTeamTrialUpdater from "@/background/teamTrialUpdater";
import { initStateControllerListeners } from "@/background/stateControllerListeners";

// The background "platform" currently is used to execute API requests from Google Sheets/Automation Anywhere.
// In the future, it might also run other background tasks from mods (e.g., background intervals)
Expand Down Expand Up @@ -86,3 +87,4 @@ initLogSweep();
initModUpdater();
initWalkthroughModalTrigger();
void initRestrictUnauthenticatedUrlAccess();
initStateControllerListeners();
6 changes: 6 additions & 0 deletions src/background/messenger/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,11 +126,17 @@ export const initTelemetry = getNotifier("INIT_TELEMETRY", bg);
export const sendDeploymentAlert = getNotifier("SEND_DEPLOYMENT_ALERT", bg);

export const ensureContextMenu = getMethod("ENSURE_CONTEXT_MENU", bg);

/**
* Uninstall context menu and return whether the context menu was uninstalled.
*/
export const uninstallContextMenu = getMethod("UNINSTALL_CONTEXT_MENU", bg);

export const deleteSynchronizedModVariables = getMethod(
"DELETE_SYNCHRONIZED_MOD_VARIABLES",
bg,
);

export const setPartnerCopilotData = getNotifier(
"SET_PARTNER_COPILOT_DATA",
bg,
Expand Down
7 changes: 7 additions & 0 deletions src/background/messenger/registration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ import { launchAuthIntegration } from "@/background/auth/partnerIntegrations/lau
import { getPartnerPrincipals } from "@/background/auth/partnerIntegrations/getPartnerPrincipals";
import refreshPartnerAuthentication from "@/background/auth/partnerIntegrations/refreshPartnerAuthentication";
import { getMe } from "@/data/service/backgroundApi";
import { deleteSynchronizedModVariablesForMod } from "@/background/stateControllerListeners";

expectContext("background");

Expand Down Expand Up @@ -151,6 +152,9 @@ declare global {

ENSURE_CONTEXT_MENU: typeof ensureContextMenu;
UNINSTALL_CONTEXT_MENU: typeof uninstallContextMenu;

DELETE_SYNCHRONIZED_MOD_VARIABLES: typeof deleteSynchronizedModVariablesForMod;

SET_PARTNER_COPILOT_DATA: typeof setCopilotProcessData;

REQUEST_RUN_IN_OPENER: typeof requestRunInOpener;
Expand Down Expand Up @@ -239,6 +243,9 @@ export default function registerMessenger(): void {

ENSURE_CONTEXT_MENU: ensureContextMenu,
UNINSTALL_CONTEXT_MENU: uninstallContextMenu,

DELETE_SYNCHRONIZED_MOD_VARIABLES: deleteSynchronizedModVariablesForMod,

SET_PARTNER_COPILOT_DATA: setCopilotProcessData,

REQUEST_RUN_IN_OPENER: requestRunInOpener,
Expand Down
135 changes: 135 additions & 0 deletions src/background/stateControllerListeners.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
/*
* Copyright (C) 2024 PixieBrix, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

import { modComponentRefFactory } from "@/testUtils/factories/modComponentFactories";
import {
getState,
setState,
TEST_resetStateController,
} from "@/contentScript/stateController/stateController";
import { MergeStrategies, StateNamespaces } from "@/platform/state/stateTypes";
import type { JSONSchema7Definition } from "json-schema";
import {
deleteSynchronizedModVariablesForMod,
deleteSynchronizedModVariablesForTab,
} from "@/background/stateControllerListeners";
import { getThisFrame } from "webext-messenger";
import { registerModVariables } from "@/contentScript/stateController/modVariablePolicyController";

beforeEach(async () => {
await TEST_resetStateController();
});

describe("stateControllerListeners", () => {
it("deletes mod variables", async () => {
const modComponentRef = modComponentRefFactory();

registerModVariables(modComponentRef.modId, {
schema: {
type: "object",
properties: {
sessionVariable: {
type: "string",
"x-sync-policy": "session",
// Cast required because types don't support custom `x-` variables yet
} as JSONSchema7Definition,
tabVariable: {
type: "string",
"x-sync-policy": "tab",
// Cast required because types don't support custom `x-` variables yet
} as JSONSchema7Definition,
},
},
});

await setState({
namespace: StateNamespaces.MOD,
data: {
sessionVariable: "sessionValue",
tabVariable: "tabValue",
},
mergeStrategy: MergeStrategies.DEEP,
modComponentRef,
});

let values = await browser.storage.session.get();
expect(Object.keys(values)).toHaveLength(2);

// Delete unrelated mod variables
await deleteSynchronizedModVariablesForMod(modComponentRefFactory().modId);
values = await browser.storage.session.get();
expect(Object.keys(values)).toHaveLength(2);

// Delete mod variables
await deleteSynchronizedModVariablesForMod(modComponentRef.modId);
values = await browser.storage.session.get();
expect(Object.keys(values)).toHaveLength(0);
});

it("deletes tab variables", async () => {
const modComponentRef = modComponentRefFactory();

registerModVariables(modComponentRef.modId, {
schema: {
type: "object",
properties: {
sessionVariable: {
type: "string",
"x-sync-policy": "session",
// Cast required because types don't support custom `x-` variables yet
} as JSONSchema7Definition,
tabVariable: {
type: "string",
"x-sync-policy": "tab",
// Cast required because types don't support custom `x-` variables yet
} as JSONSchema7Definition,
},
},
});

await setState({
namespace: StateNamespaces.MOD,
data: {
sessionVariable: "sessionValue",
tabVariable: "tabValue",
},
mergeStrategy: MergeStrategies.DEEP,
modComponentRef,
});

let values = await browser.storage.session.get();
expect(Object.keys(values)).toHaveLength(2);

// Delete unrelated mod variables
const { tabId } = await getThisFrame();
await deleteSynchronizedModVariablesForTab(tabId);

// Smoke test for raw state
values = await browser.storage.session.get();
expect(Object.keys(values)).toHaveLength(1);

// State controller value check
const state = await getState({
namespace: StateNamespaces.MOD,
modComponentRef,
});

expect(state).toStrictEqual({
sessionVariable: "sessionValue",
});
});
});
53 changes: 53 additions & 0 deletions src/background/stateControllerListeners.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/*
* Copyright (C) 2024 PixieBrix, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

import { tryParseSessionStorageKey } from "@/platform/state/stateHelpers";
import type { RegistryId } from "@/types/registryTypes";

/**
* Delete synchronized mod variables for the given tab id to free up memory.
*/
export async function deleteSynchronizedModVariablesForTab(
tabId: number,
): Promise<void> {
const values = await browser.storage.session.get();
const matches = Object.keys(values).filter(
(x) => tryParseSessionStorageKey(x)?.tabId === tabId,
);
if (matches.length > 0) {
await browser.storage.session.remove(matches);
}
}

/**
* Delete synchronized mod variables for the given tab mod to free up memory.
*/
export async function deleteSynchronizedModVariablesForMod(
modId: RegistryId,
): Promise<void> {
const values = await browser.storage.session.get();
const matches = Object.keys(values).filter(
(x) => tryParseSessionStorageKey(x)?.modId === modId,
);
if (matches.length > 0) {
await browser.storage.session.remove(matches);
}
}

export function initStateControllerListeners(): void {
browser.tabs.onRemoved.addListener(deleteSynchronizedModVariablesForTab);
}
6 changes: 3 additions & 3 deletions src/bricks/effects/pageState.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,11 @@ import { unsafeAssumeValidArg } from "@/runtime/runtimeTypes";
import { brickOptionsFactory } from "@/testUtils/factories/runtimeFactories";
import { toExpression } from "@/utils/expressionUtils";
import { GetPageState, SetPageState } from "@/bricks/effects/pageState";
import { TEST_resetState } from "@/contentScript/stateController";
import { TEST_resetStateController } from "@/contentScript/stateController/stateController";
import { MergeStrategies, StateNamespaces } from "@/platform/state/stateTypes";

beforeEach(() => {
TEST_resetState();
beforeEach(async () => {
await TEST_resetStateController();
});

describe("@pixiebrix/state/get", () => {
Expand Down
8 changes: 4 additions & 4 deletions src/bricks/renderers/customForm.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,10 @@ import { templates } from "@/components/formBuilder/RjsfTemplates";
import { toExpression } from "@/utils/expressionUtils";
import { unsafeAssumeValidArg } from "@/runtime/runtimeTypes";
import {
TEST_resetState,
TEST_resetStateController,
getState,
setState,
} from "@/contentScript/stateController";
} from "@/contentScript/stateController/stateController";
import type { Target } from "@/types/messengerTypes";
import { StateNamespaces } from "@/platform/state/stateTypes";

Expand Down Expand Up @@ -224,8 +224,8 @@ describe("form data normalization", () => {
});

describe("CustomFormRenderer", () => {
beforeEach(() => {
TEST_resetState();
beforeEach(async () => {
await TEST_resetStateController();
jest.clearAllMocks();
});

Expand Down
4 changes: 2 additions & 2 deletions src/contentScript/contentScriptPlatform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,8 @@ import {
} from "@/background/messenger/api";
import {
getState,
registerModVariables,
setState,
} from "@/contentScript/stateController";
} from "@/contentScript/stateController/stateController";
import quickBarRegistry from "@/components/quickBar/quickBarRegistry";
import { expectContext } from "@/utils/expectContext";
import type { PlatformCapability } from "@/platform/capabilities";
Expand Down Expand Up @@ -62,6 +61,7 @@ import { InteractiveLoginRequiredError } from "@/errors/authErrors";
import { deferLogin } from "@/contentScript/integrations/deferredLoginController";
import { selectionMenuActionRegistry } from "@/contentScript/textSelectionMenu/selectionMenuController";
import { getExtensionVersion } from "@/utils/extensionUtils";
import { registerModVariables } from "@/contentScript/stateController/modVariablePolicyController";

/**
* @file Platform definition for mods running in a content script
Expand Down
5 changes: 4 additions & 1 deletion src/contentScript/messenger/registration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,10 @@ import {
import { getProcesses, initRobot } from "@/contentScript/uipath";
import { checkAvailable } from "@/bricks/available";
import notify from "@/utils/notify";
import { getState, setState } from "@/contentScript/stateController";
import {
getState,
setState,
} from "@/contentScript/stateController/stateController";
import {
cancelTemporaryPanels,
getPanelDefinition,
Expand Down
Loading

0 comments on commit 9d4cf78

Please sign in to comment.