Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#8131 Auto-deactivate unassigned mods on Mods Page #8164

Merged
merged 60 commits into from
Apr 5, 2024
Merged
Show file tree
Hide file tree
Changes from 59 commits
Commits
Show all changes
60 commits
Select commit Hold shift + click to select a range
d7ad836
refactor rename extensions -> activatedModComponents
mnholtz Apr 2, 2024
cf524da
refactor rename uninstallAllDeployments -> deactivateAllDeployedMods
mnholtz Apr 2, 2024
9e9ef90
refactor rename vars in deactivateAllDeployedMods
mnholtz Apr 2, 2024
0b6c1a9
refactor rename uninstallExtensionsAndSaveState -> deactivateModCompo…
mnholtz Apr 2, 2024
1048c30
refactor rename vars in deactivateModComponentsAndSaveState
mnholtz Apr 2, 2024
6355e9f
reactor rename uninstallExtensinoFromStates -> deactivateModComponent…
mnholtz Apr 2, 2024
743096d
refactor rename setExtensionsState -> saveModComponentStateAndReactiv…
mnholtz Apr 2, 2024
240238b
refactor rename uninstallUmnatchedDeployments -> deactivateUnassigned…
mnholtz Apr 2, 2024
df41426
refactor rename uninstallRecipe -> deactivateMod
mnholtz Apr 2, 2024
bbb01f2
refactor rename installDeployment -> activateDeployment
mnholtz Apr 2, 2024
792b051
refactor rename installDeployments -> activateDeployments
mnholtz Apr 2, 2024
ec5cf41
refacto rename canAutomaticallyInstall -> canAutoActivate
mnholtz Apr 2, 2024
b55f544
refactor rename vars in activateDeploymentsInBackground
mnholtz Apr 2, 2024
be64708
refactor clarify manual/auto activation logic in activateDeploymentsI…
mnholtz Apr 2, 2024
fb4d12f
refactor consolidate resetUpdateTimestamp func
mnholtz Apr 2, 2024
651256a
replace straggling references to 'install' with 'activate'
mnholtz Apr 2, 2024
e840dfc
add test for changing mod id
mnholtz Apr 2, 2024
5443136
fix test inteference in deploymentUpdater
mnholtz Apr 3, 2024
f4b0a28
add empty test file for activateDeployments
mnholtz Apr 3, 2024
32dc724
fix name and docs for updatePromptTimestamp
mnholtz Apr 3, 2024
7aca896
refactor integrate graham suggestion
mnholtz Apr 3, 2024
448b221
Merge branch 'feature/7475_refactor_deploymentUpdater_test' into bugf…
mnholtz Apr 3, 2024
294c6d6
Merge branch 'main' into bugfix/7475_fix_switch_deployed_mod
mnholtz Apr 3, 2024
b186dda
add empty test to DeploymentsContext
mnholtz Apr 3, 2024
85dc080
add basic test body
mnholtz Apr 4, 2024
b3cfce7
update deployment timestamps
mnholtz Apr 4, 2024
fcfa90d
fix type errors
mnholtz Apr 4, 2024
4d4f35e
fix registry id in test
mnholtz Apr 4, 2024
bf1725e
finish replication test
mnholtz Apr 4, 2024
1f332e5
refactor rename activateDeployment param installed -> activatedModCom…
mnholtz Apr 4, 2024
b9564a4
refactor rename activateDeployments param installed -> activatedModCo…
mnholtz Apr 4, 2024
edd5316
refactor consolidate redundant prop
mnholtz Apr 4, 2024
adf690c
refactor rename extension -> modComponent
mnholtz Apr 4, 2024
e90ad50
bugfix deactivate mods based on deloyment id instead of recipe id
mnholtz Apr 4, 2024
d9d8137
Refactor improve deploymentOverride spread
mnholtz Apr 4, 2024
565860b
refactor extract mockDeploymentActivationRequests
mnholtz Apr 4, 2024
7ec9f5a
add test for unmanaged activated mod becoming managed
mnholtz Apr 4, 2024
099ef85
fix type errors
mnholtz Apr 4, 2024
bb2de0d
fix modcomponentstate tyep errors
mnholtz Apr 4, 2024
827bfbf
misc refactors
mnholtz Apr 4, 2024
9f3cd5e
fix lint errors
mnholtz Apr 4, 2024
2e01199
add restricted flags mock
mnholtz Apr 4, 2024
81954f0
make fix for deactivate unmananged mod components
mnholtz Apr 4, 2024
df55f43
remove deactivate unassigned
mnholtz Apr 5, 2024
ac7dcae
remove todos in tests
mnholtz Apr 5, 2024
806de9a
add unassignedModComponents to deploymentUpdateState
mnholtz Apr 5, 2024
c56b111
refactor rename activeExtensions -> activeModComponents
mnholtz Apr 5, 2024
6fb900d
refactor rename installedExtensions -> activatedModComponents
mnholtz Apr 5, 2024
59f8e5e
add unassignedModComponents to useAutoDeploy
mnholtz Apr 5, 2024
baa6a21
deactivate unassigned mod components in useAutoDeploy effect
mnholtz Apr 5, 2024
c3c16db
fix autoDeploy tests
mnholtz Apr 5, 2024
9866ee0
remove unnecessary async
mnholtz Apr 5, 2024
d5db378
fix lint errors'
mnholtz Apr 5, 2024
f34313b
add try catch to deactivateUnassignedModComponents
mnholtz Apr 5, 2024
683f4a3
fix lint errors
mnholtz Apr 5, 2024
6cf04ab
create useDeactivateUnassignedDeploymentsEffect
mnholtz Apr 5, 2024
ca06d21
replace useAutoDeploy deactivateUnassignedModComponents with effect
mnholtz Apr 5, 2024
671c4b5
replace useAutoDeploy deactivateUnassignedModComponents with effect
mnholtz Apr 5, 2024
de2b1d5
remove from strictnull
mnholtz Apr 5, 2024
08e8ead
Merge branch 'main' into bugfix/8131_auto_deactivate_unassigned_mods
mnholtz Apr 5, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
68 changes: 68 additions & 0 deletions src/background/deploymentUpdater.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ beforeEach(async () => {
} as any);

await resetManagedStorage();
refreshRegistriesMock.mockReset();
});

afterEach(async () => {
Expand Down Expand Up @@ -796,4 +797,71 @@ describe("syncDeployments", () => {
expect(elements).toBeArrayOfSize(1);
expect(elements[0]).toEqual(personalElement);
});

test("deactivates old mod when deployed mod id is changed", async () => {
isLinkedMock.mockResolvedValue(true);

const { deployment, modDefinition } = activatableDeploymentFactory();

appApiMock.onGet("/api/me/").reply(200, {
flags: [],
});

appApiMock.onPost("/api/deployments/").reply(201, [deployment]);

appApiMock
.onGet(
`/api/registry/bricks/${encodeURIComponent(
deployment.package.package_id,
)}/`,
)
.reply(
200,
packageConfigDetailFactory({
modDefinition,
packageVersionUUID: deployment.package.id,
}),
);

await syncDeployments();
const { extensions: activatedModComponents } = await getModComponentState();
expect(activatedModComponents).toHaveLength(1);
expect(activatedModComponents[0]._recipe.id).toBe(
deployment.package.package_id,
);

// Remove package from the deployment so that it can be updated
delete deployment.package;
const {
deployment: updatedDeployment,
modDefinition: updatedModDefinition,
} = activatableDeploymentFactory({
deploymentOverride: {
...deployment,
},
});

appApiMock.onPost("/api/deployments/").reply(201, [updatedDeployment]);

appApiMock
.onGet(
`/api/registry/bricks/${encodeURIComponent(
updatedDeployment.package.package_id,
)}/`,
)
.reply(
200,
packageConfigDetailFactory({
modDefinition: updatedModDefinition,
packageVersionUUID: updatedDeployment.package.id,
}),
);

await syncDeployments();
const { extensions: expectedModComponents } = await getModComponentState();
expect(expectedModComponents).toHaveLength(1);
expect(expectedModComponents[0]._recipe.id).toBe(
updatedDeployment.package.package_id,
);
});
});
10 changes: 6 additions & 4 deletions src/background/deploymentUpdater.ts
Original file line number Diff line number Diff line change
Expand Up @@ -186,25 +186,27 @@ async function deactivateUnassignedDeployments(
assignedDeployments.map((deployment) => deployment.package.package_id),
);

const modComponentsToDeactivate = activatedModComponents.filter(
const unassignedModComponents = activatedModComponents.filter(
(activatedModComponent) =>
!isEmpty(activatedModComponent._deployment) &&
!deployedModIds.has(activatedModComponent._recipe?.id),
);

if (modComponentsToDeactivate.length === 0) {
if (unassignedModComponents.length === 0) {
// Short-circuit to skip reporting telemetry
return;
}

await deactivateModComponentsAndSaveState(modComponentsToDeactivate, {
await deactivateModComponentsAndSaveState(unassignedModComponents, {
editorState,
optionsState,
});

reportEvent(Events.DEPLOYMENT_DEACTIVATE_UNASSIGNED, {
auto: true,
deployments: modComponentsToDeactivate.map((x) => x._deployment.id),
deployments: unassignedModComponents.map(
(modComponent) => modComponent._deployment.id,
),
});
}

Expand Down
175 changes: 135 additions & 40 deletions src/extensionConsole/pages/deployments/DeploymentsContext.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,14 +34,38 @@ import {
} from "@/testUtils/factories/deploymentFactories";
import { packageConfigDetailFactory } from "@/testUtils/factories/brickFactories";
import { ExtensionNotLinkedError } from "@/errors/genericErrors";
import extensionsSlice from "@/store/extensionsSlice";
import { validateTimestamp } from "@/types/helpers";
import { type ModDefinition } from "@/types/modDefinitionTypes";
import { type Deployment } from "@/types/contract";

const axiosMock = new MockAdapter(axios);
axiosMock.onGet("/api/me/").reply(200, { flags: [] });

const getLinkedApiClientMock = jest.mocked(getLinkedApiClient);

const requestPermissionsMock = jest.mocked(browser.permissions.request);

const mockDeploymentActivationRequests = (
deployment: Deployment,
modDefinition: ModDefinition,
) => {
axiosMock.onPost("/api/deployments/").reply(200, [deployment]);
axiosMock
.onGet(
`/api/registry/bricks/${encodeURIComponent(
deployment.package.package_id,
)}/`,
)
.reply(
200,
packageConfigDetailFactory({
modDefinition,
packageVersionUUID: deployment.package.id,
}),
);
requestPermissionsMock.mockResolvedValue(true);
};

const Component: React.FC = () => {
const deployments = useContext(DeploymentsContext);
return (
Expand All @@ -59,6 +83,7 @@ const Component: React.FC = () => {

describe("DeploymentsContext", () => {
beforeEach(() => {
axiosMock.onGet("/api/me/").reply(200, { flags: [] });
jest.clearAllMocks();
axiosMock.resetHistory();

Expand Down Expand Up @@ -97,19 +122,7 @@ describe("DeploymentsContext", () => {

it("activate single deployment from empty state", async () => {
const { deployment, modDefinition } = activatableDeploymentFactory();
const registryId = deployment.package.package_id;

axiosMock.onPost("/api/deployments/").reply(200, [deployment]);
axiosMock
.onGet(`/api/registry/bricks/${encodeURIComponent(registryId)}/`)
.reply(
200,
packageConfigDetailFactory({
modDefinition,
packageVersionUUID: deployment.package.id,
}),
);
requestPermissionsMock.mockResolvedValue(true);
mockDeploymentActivationRequests(deployment, modDefinition);

const { getReduxStore } = render(
<DeploymentsProvider>
Expand Down Expand Up @@ -139,21 +152,115 @@ describe("DeploymentsContext", () => {
expect((options as ModComponentState).extensions).toHaveLength(1);
});

it("updating deployment mod id deactivates old mod", async () => {
const { deployment, modDefinition: oldModDefinition } =
activatableDeploymentFactory({
deploymentOverride: {
updated_at: validateTimestamp("2021-01-01T12:52:16.189Z"),
created_at: validateTimestamp("2021-01-01T12:52:16.189Z"),
},
});

// Remove package from the deployment so that the mod can be changed entirely
const { package: _, ...deploymentOverride } = deployment;
const {
deployment: updatedDeployment,
modDefinition: expectedModDefinition,
} = activatableDeploymentFactory({
deploymentOverride: {
...deploymentOverride,
updated_at: validateTimestamp("2021-02-02T12:52:16.189Z"),
},
});

mockDeploymentActivationRequests(updatedDeployment, expectedModDefinition);

const { getReduxStore } = render(
<DeploymentsProvider>
<Component />
</DeploymentsProvider>,
{
setupRedux(dispatch) {
dispatch(
extensionsSlice.actions.activateMod({
modDefinition: oldModDefinition,
deployment,
screen: "extensionConsole",
isReactivate: false,
}),
);
},
},
);

expect(screen.queryAllByTestId("Component")).toHaveLength(1);
expect(screen.queryAllByTestId("Error")).toHaveLength(0);

await waitFor(() => {
// Initial fetch with old activated deployed mod
expect(axiosMock.history.post).toHaveLength(1);
});

await userEvent.click(screen.getByText("Update"));

await waitFor(() => {
// Refetch after deployment activation
expect(axiosMock.history.post).toHaveLength(3);
});

const { options } = getReduxStore().getState();
expect((options as ModComponentState).extensions).toHaveLength(1);
});

it("updating deployment reactivates mod that was previously unmanaged for restricted user", async () => {
axiosMock.onGet("/api/me/").reply(200, { flags: ["restricted-uninstall"] });
const { deployment, modDefinition } = activatableDeploymentFactory();
mockDeploymentActivationRequests(deployment, modDefinition);

const { getReduxStore } = render(
<DeploymentsProvider>
<Component />
</DeploymentsProvider>,
{
setupRedux(dispatch) {
dispatch(
extensionsSlice.actions.activateMod({
modDefinition,
// No deployment, so that the mod is unmanaged
screen: "extensionConsole",
isReactivate: false,
}),
);
},
},
);

const {
options: { extensions: initialActivatedModComponents },
} = getReduxStore().getState() as { options: ModComponentState };
expect(initialActivatedModComponents).toHaveLength(1);
expect(initialActivatedModComponents[0]._deployment).toBeUndefined();

expect(screen.queryAllByTestId("Component")).toHaveLength(1);
expect(screen.queryAllByTestId("Error")).toHaveLength(0);

await waitFor(() => {
// Initial fetch with old activated deployed mod
expect(axiosMock.history.post).toHaveLength(1);
});

await userEvent.click(screen.getByText("Update"));

const {
options: { extensions: activatedModComponents },
} = getReduxStore().getState() as { options: ModComponentState };
expect(activatedModComponents).toHaveLength(1);
expect(activatedModComponents[0]._deployment?.id).toBe(deployment.id);
});

it("remounting the DeploymentsProvider doesn't refetch the deployments", async () => {
const { deployment, modDefinition } = activatableDeploymentFactory();
const registryId = deployment.package.package_id;

axiosMock.onPost("/api/deployments/").reply(200, [deployment]);
axiosMock
.onGet(`/api/registry/bricks/${encodeURIComponent(registryId)}/`)
.reply(
200,
packageConfigDetailFactory({
modDefinition,
packageVersionUUID: deployment.package.id,
}),
);
requestPermissionsMock.mockResolvedValue(true);
mockDeploymentActivationRequests(deployment, modDefinition);

const { rerender } = render(
<DeploymentsProvider>
Expand Down Expand Up @@ -203,19 +310,7 @@ describe("DeploymentsContext", () => {
const user = userEvent.setup({ advanceTimers: jest.advanceTimersByTime });

const { deployment, modDefinition } = activatableDeploymentFactory();
const registryId = deployment.package.package_id;

axiosMock.onPost("/api/deployments/").reply(200, [deployment]);
axiosMock
.onGet(`/api/registry/bricks/${encodeURIComponent(registryId)}/`)
.reply(
200,
packageConfigDetailFactory({
modDefinition,
packageVersionUUID: deployment.package.id,
}),
);
requestPermissionsMock.mockResolvedValue(true);
mockDeploymentActivationRequests(deployment, modDefinition);

const { rerender } = render(
<DeploymentsProvider>
Expand Down