mirror of
https://github.com/penpot/penpot-plugins.git
synced 2025-01-06 14:50:21 -05:00
fix(plugins-runtime): prevent plugin execution after close
This commit is contained in:
parent
74b11f0587
commit
b65492ae29
2 changed files with 44 additions and 37 deletions
|
@ -1,10 +1,9 @@
|
||||||
import { describe, it, vi, expect, beforeEach, afterEach, Mock } from 'vitest';
|
import { describe, it, vi, expect, beforeEach, afterEach } from 'vitest';
|
||||||
import { loadPlugin, setContextBuilder } from './load-plugin.js';
|
import { loadPlugin, setContextBuilder } from './load-plugin.js';
|
||||||
import { loadManifestCode } from './parse-manifest.js';
|
import { loadManifestCode } from './parse-manifest.js';
|
||||||
import { createApi, themeChange } from './api/index.js';
|
import { createApi, themeChange } from './api/index.js';
|
||||||
import type { PenpotContext, PenpotTheme } from '@penpot/plugin-types';
|
import type { PenpotContext, PenpotTheme } from '@penpot/plugin-types';
|
||||||
import type { Manifest } from './models/manifest.model.js';
|
import type { Manifest } from './models/manifest.model.js';
|
||||||
import { ses } from './ses.js';
|
|
||||||
|
|
||||||
vi.mock('./parse-manifest.js', () => ({
|
vi.mock('./parse-manifest.js', () => ({
|
||||||
loadManifestCode: vi.fn(),
|
loadManifestCode: vi.fn(),
|
||||||
|
@ -15,17 +14,25 @@ vi.mock('./api/index.js', () => ({
|
||||||
themeChange: vi.fn(),
|
themeChange: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
const evaluateMock = vi.fn();
|
||||||
|
|
||||||
vi.mock('./ses.js', () => ({
|
vi.mock('./ses.js', () => ({
|
||||||
ses: {
|
ses: {
|
||||||
hardenIntrinsics: vi.fn().mockReturnValue(null),
|
hardenIntrinsics: vi.fn().mockReturnValue(null),
|
||||||
createCompartment: vi.fn().mockReturnValue({
|
createCompartment: vi.fn().mockImplementation((publicApi) => {
|
||||||
evaluate: vi.fn(),
|
return {
|
||||||
|
evaluate: evaluateMock,
|
||||||
|
globalThis: publicApi,
|
||||||
|
};
|
||||||
}),
|
}),
|
||||||
harden: vi.fn().mockImplementation((obj) => obj),
|
harden: vi.fn().mockImplementation((obj) => obj),
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
describe('loadPlugin', () => {
|
describe('loadPlugin', () => {
|
||||||
|
vi.spyOn(globalThis, 'clearTimeout');
|
||||||
|
vi.spyOn(globalThis.console, 'error').mockImplementation(() => {});
|
||||||
|
|
||||||
let mockContext: PenpotContext;
|
let mockContext: PenpotContext;
|
||||||
let manifest: Manifest = {
|
let manifest: Manifest = {
|
||||||
pluginId: 'test-plugin',
|
pluginId: 'test-plugin',
|
||||||
|
@ -54,8 +61,8 @@ describe('loadPlugin', () => {
|
||||||
closePlugin: vi.fn(),
|
closePlugin: vi.fn(),
|
||||||
} as unknown as ReturnType<typeof createApi>;
|
} as unknown as ReturnType<typeof createApi>;
|
||||||
|
|
||||||
(createApi as Mock).mockReturnValue(mockApi);
|
vi.mocked(createApi).mockReturnValue(mockApi);
|
||||||
(loadManifestCode as Mock).mockResolvedValue(
|
vi.mocked(loadManifestCode).mockResolvedValue(
|
||||||
'console.log("Plugin loaded");'
|
'console.log("Plugin loaded");'
|
||||||
);
|
);
|
||||||
setContextBuilder(() => mockContext);
|
setContextBuilder(() => mockContext);
|
||||||
|
@ -96,20 +103,6 @@ describe('loadPlugin', () => {
|
||||||
expect(mockApi.closePlugin).toHaveBeenCalledTimes(1);
|
expect(mockApi.closePlugin).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should clear timeouts on plugin close', async () => {
|
|
||||||
await loadPlugin(manifest);
|
|
||||||
|
|
||||||
const clearTimeoutSpy = vi.spyOn(globalThis, 'clearTimeout');
|
|
||||||
const setTimeoutSpy = vi.spyOn(globalThis, 'setTimeout');
|
|
||||||
|
|
||||||
const timeoutCallback = vi.fn();
|
|
||||||
const timeoutId = setTimeout(timeoutCallback, 1000);
|
|
||||||
clearTimeout(timeoutId);
|
|
||||||
|
|
||||||
expect(clearTimeoutSpy).toHaveBeenCalledWith(timeoutId);
|
|
||||||
expect(setTimeoutSpy).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should remove finish event listener on plugin finish', async () => {
|
it('should remove finish event listener on plugin finish', async () => {
|
||||||
await loadPlugin(manifest);
|
await loadPlugin(manifest);
|
||||||
|
|
||||||
|
@ -123,16 +116,6 @@ describe('loadPlugin', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('shoud clean setTimeout when plugin is closed', async () => {
|
it('shoud clean setTimeout when plugin is closed', async () => {
|
||||||
vi.spyOn(globalThis, 'clearTimeout');
|
|
||||||
|
|
||||||
let closedCallback = () => {};
|
|
||||||
|
|
||||||
(createApi as Mock).mockImplementation((context, manifest, closed) => {
|
|
||||||
closedCallback = closed;
|
|
||||||
|
|
||||||
return mockApi;
|
|
||||||
});
|
|
||||||
|
|
||||||
const plugin = await loadPlugin(manifest);
|
const plugin = await loadPlugin(manifest);
|
||||||
|
|
||||||
if (!plugin) {
|
if (!plugin) {
|
||||||
|
@ -144,6 +127,7 @@ describe('loadPlugin', () => {
|
||||||
|
|
||||||
expect(plugin.timeouts.size).toBe(2);
|
expect(plugin.timeouts.size).toBe(2);
|
||||||
|
|
||||||
|
const closedCallback = vi.mocked(createApi).mock.calls[0][2];
|
||||||
closedCallback();
|
closedCallback();
|
||||||
|
|
||||||
expect(plugin.timeouts.size).toBe(0);
|
expect(plugin.timeouts.size).toBe(0);
|
||||||
|
@ -151,16 +135,32 @@ describe('loadPlugin', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should close plugin on evaluation error', async () => {
|
it('should close plugin on evaluation error', async () => {
|
||||||
ses.createCompartment = vi.fn().mockImplementation(() => {
|
evaluateMock.mockImplementationOnce(() => {
|
||||||
return {
|
throw new Error('Evaluation error');
|
||||||
evaluate: vi.fn().mockImplementation(() => {
|
|
||||||
throw new Error('Error in plugin');
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
});
|
});
|
||||||
|
|
||||||
await loadPlugin(manifest);
|
await loadPlugin(manifest);
|
||||||
|
|
||||||
expect(mockApi.closePlugin).toHaveBeenCalled();
|
expect(mockApi.closePlugin).toHaveBeenCalled();
|
||||||
|
expect(console.error).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should prevent using the api after closing the plugin', async () => {
|
||||||
|
const plugin = await loadPlugin(manifest);
|
||||||
|
|
||||||
|
if (!plugin) {
|
||||||
|
throw new Error('Plugin not loaded');
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(
|
||||||
|
Object.keys(plugin.compartment.globalThis).filter((it) => !!it).length
|
||||||
|
).toBeGreaterThan(0);
|
||||||
|
|
||||||
|
const closedCallback = vi.mocked(createApi).mock.calls[0][2];
|
||||||
|
closedCallback();
|
||||||
|
|
||||||
|
expect(
|
||||||
|
Object.keys(plugin.compartment.globalThis).filter((it) => !!it).length
|
||||||
|
).toBe(0);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -44,8 +44,15 @@ export const loadPlugin = async function (manifest: Manifest) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const pluginApi = createApi(context, manifest, () => {
|
const pluginApi = createApi(context, manifest, () => {
|
||||||
|
createdApis = createdApis.filter((api) => api !== pluginApi);
|
||||||
|
|
||||||
timeouts.forEach(clearTimeout);
|
timeouts.forEach(clearTimeout);
|
||||||
timeouts.clear();
|
timeouts.clear();
|
||||||
|
|
||||||
|
// Remove all public API from globalThis
|
||||||
|
Object.keys(publicPluginApi).forEach((key) => {
|
||||||
|
delete c.globalThis[key];
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
createdApis.push(pluginApi);
|
createdApis.push(pluginApi);
|
||||||
|
@ -53,7 +60,7 @@ export const loadPlugin = async function (manifest: Manifest) {
|
||||||
const timeouts = new Set<ReturnType<typeof setTimeout>>();
|
const timeouts = new Set<ReturnType<typeof setTimeout>>();
|
||||||
|
|
||||||
const publicPluginApi = {
|
const publicPluginApi = {
|
||||||
penpot: ses.harden(pluginApi),
|
penpot: ses.harden(pluginApi) as typeof pluginApi,
|
||||||
fetch: ses.harden((...args: Parameters<typeof fetch>) => {
|
fetch: ses.harden((...args: Parameters<typeof fetch>) => {
|
||||||
const requestArgs: RequestInit = {
|
const requestArgs: RequestInit = {
|
||||||
...args[1],
|
...args[1],
|
||||||
|
|
Loading…
Reference in a new issue