mirror of
https://github.com/penpot/penpot.git
synced 2026-04-29 13:18:29 +00:00
* 🐛 Handle plugin errors gracefully without crashing the UI Plugin errors (like 'Set is not a constructor') were propagating to the global error handler and showing the exception page. This fix: - Uses a WeakMap to track plugin errors (works in SES hardened environment) - Wraps setTimeout/setInterval handlers to mark errors and re-throw them - Frontend global handler checks isPluginError and logs to console Plugin errors are now logged to console with 'Plugin Error' prefix but don't crash the main application or show the exception page. Signed-off-by: AI Agent <agent@penpot.app> * ✨ Improved handling of plugin errors on initialization * ✨ Fix test and linter --------- Signed-off-by: AI Agent <agent@penpot.app> Co-authored-by: alonso.torres <alonso.torres@kaleidos.net>
158 lines
3.8 KiB
TypeScript
158 lines
3.8 KiB
TypeScript
import { describe, it, vi, expect, beforeEach, afterEach } from 'vitest';
|
|
import {
|
|
loadPlugin,
|
|
ɵloadPlugin,
|
|
ɵloadPluginByUrl,
|
|
setContextBuilder,
|
|
getPlugins,
|
|
} from './load-plugin';
|
|
import { loadManifest } from './parse-manifest';
|
|
import { createPlugin } from './create-plugin';
|
|
import type { Context } from '@penpot/plugin-types';
|
|
import type { Manifest } from './models/manifest.model.js';
|
|
|
|
vi.mock('./parse-manifest', () => ({
|
|
loadManifest: vi.fn(),
|
|
}));
|
|
|
|
vi.mock('./create-plugin', () => ({
|
|
createPlugin: vi.fn(),
|
|
}));
|
|
|
|
vi.mock('./create-sandbox.js', () => ({
|
|
markPluginError: vi.fn(),
|
|
}));
|
|
|
|
vi.mock('./ses.js', () => ({
|
|
ses: {
|
|
harden: vi.fn().mockImplementation((obj) => obj),
|
|
},
|
|
}));
|
|
|
|
describe('plugin-loader', () => {
|
|
let mockContext: Context;
|
|
let manifest: Manifest;
|
|
let mockPluginApi: Awaited<ReturnType<typeof createPlugin>>;
|
|
let mockClose: ReturnType<typeof vi.fn>;
|
|
|
|
beforeEach(() => {
|
|
manifest = {
|
|
pluginId: 'test-plugin',
|
|
name: 'Test Plugin',
|
|
host: '',
|
|
code: '',
|
|
permissions: [
|
|
'content:read',
|
|
'content:write',
|
|
'library:read',
|
|
'library:write',
|
|
'user:read',
|
|
'comment:read',
|
|
'comment:write',
|
|
'allow:downloads',
|
|
'allow:localstorage',
|
|
],
|
|
};
|
|
|
|
mockClose = vi.fn();
|
|
mockPluginApi = {
|
|
plugin: {
|
|
close: mockClose,
|
|
sendMessage: vi.fn(),
|
|
},
|
|
} as unknown as Awaited<ReturnType<typeof createPlugin>>;
|
|
|
|
mockContext = {
|
|
addListener: vi.fn(),
|
|
removeListener: vi.fn(),
|
|
} as unknown as Context;
|
|
|
|
vi.mocked(createPlugin).mockResolvedValue(mockPluginApi);
|
|
setContextBuilder(() => mockContext);
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
it('should load and initialize a plugin', async () => {
|
|
await loadPlugin(manifest);
|
|
|
|
expect(createPlugin).toHaveBeenCalledWith(
|
|
mockContext,
|
|
manifest,
|
|
expect.any(Function),
|
|
undefined,
|
|
);
|
|
expect(mockPluginApi.plugin.close).not.toHaveBeenCalled();
|
|
expect(getPlugins()).toHaveLength(1);
|
|
});
|
|
|
|
it('should close all plugins before loading a new one', async () => {
|
|
await loadPlugin(manifest);
|
|
await loadPlugin(manifest);
|
|
|
|
expect(mockClose).toHaveBeenCalledTimes(1);
|
|
expect(createPlugin).toHaveBeenCalledTimes(2);
|
|
});
|
|
|
|
it('should remove the plugin from the list on close', async () => {
|
|
await loadPlugin(manifest);
|
|
|
|
const closeCallback = vi.mocked(createPlugin).mock.calls[0][2];
|
|
closeCallback();
|
|
|
|
expect(getPlugins()).toHaveLength(0);
|
|
});
|
|
|
|
it('should handle errors and close all plugins', async () => {
|
|
vi.mocked(createPlugin).mockRejectedValue(
|
|
new Error('Plugin creation failed'),
|
|
);
|
|
|
|
try {
|
|
await loadPlugin(manifest);
|
|
} catch (err) {
|
|
expect.assert(err);
|
|
}
|
|
|
|
expect(getPlugins()).toHaveLength(0);
|
|
});
|
|
|
|
it('should handle messages sent to plugins', async () => {
|
|
await loadPlugin(manifest);
|
|
|
|
window.dispatchEvent(new MessageEvent('message', { data: 'test-message' }));
|
|
|
|
expect(mockPluginApi.plugin.sendMessage).toHaveBeenCalledWith(
|
|
'test-message',
|
|
);
|
|
});
|
|
|
|
it('should load plugin using ɵloadPlugin', async () => {
|
|
await ɵloadPlugin(manifest);
|
|
|
|
expect(createPlugin).toHaveBeenCalledWith(
|
|
mockContext,
|
|
manifest,
|
|
expect.any(Function),
|
|
undefined,
|
|
);
|
|
});
|
|
|
|
it('should load plugin by URL using ɵloadPluginByUrl', async () => {
|
|
const manifestUrl = 'https://example.com/manifest.json';
|
|
vi.mocked(loadManifest).mockResolvedValue(manifest);
|
|
|
|
await ɵloadPluginByUrl(manifestUrl);
|
|
|
|
expect(loadManifest).toHaveBeenCalledWith(manifestUrl);
|
|
expect(createPlugin).toHaveBeenCalledWith(
|
|
mockContext,
|
|
manifest,
|
|
expect.any(Function),
|
|
undefined,
|
|
);
|
|
});
|
|
});
|