mirror of
https://github.com/penpot/penpot.git
synced 2026-10-31 15:00:32 +00:00
Unloading or replacing a plugin while its code was still being fetched could leave a late instance running, or block reopening a plugin that closed during startup. Each load now carries an AbortController. Unloading, a newer load of the same plugin or logout aborts it, and the plugin manager closes itself on abort, also while it fetches its code. createPlugin returns nothing for a plugin closed before its sandbox exists, and failed loads clear their pending state so the plugin can be loaded again.
84 lines
2.5 KiB
TypeScript
84 lines
2.5 KiB
TypeScript
import { describe, it, vi, expect, beforeEach } from 'vitest';
|
|
import { loadPlugin, setContextBuilder, getPlugins } from './load-plugin';
|
|
import { createPlugin } from './create-plugin';
|
|
import { ses } from './ses.js';
|
|
import type { Context } from '@penpot/plugin-types';
|
|
import type { Manifest } from './models/manifest.model.js';
|
|
|
|
vi.mock('./create-plugin', () => ({
|
|
createPlugin: vi.fn(),
|
|
}));
|
|
|
|
// NOTE: `./ses.js` is intentionally NOT mocked here: the test spies on the
|
|
// real `ses.harden` to assert that `loadPlugin` never hardens the host
|
|
// context.
|
|
|
|
describe('loadPlugin host context boundary (regression for #11001)', () => {
|
|
let manifest: Manifest;
|
|
|
|
beforeEach(() => {
|
|
manifest = {
|
|
pluginId: 'test-plugin',
|
|
name: 'Test Plugin',
|
|
host: '',
|
|
code: '',
|
|
permissions: ['content:read'],
|
|
};
|
|
|
|
vi.mocked(createPlugin).mockResolvedValue({
|
|
plugin: {
|
|
close: vi.fn(),
|
|
sendMessage: vi.fn(),
|
|
},
|
|
} as unknown as NonNullable<Awaited<ReturnType<typeof createPlugin>>>);
|
|
});
|
|
|
|
it('does not freeze host-owned functions reachable through the context', async () => {
|
|
const hostListener = function hostListener() {
|
|
return 'host-value';
|
|
};
|
|
const nestedHostObject = {
|
|
nestedFn() {
|
|
return 'nested';
|
|
},
|
|
};
|
|
const hostContext = {
|
|
addListener: hostListener,
|
|
nested: nestedHostObject,
|
|
} as unknown as Context;
|
|
|
|
setContextBuilder(() => hostContext);
|
|
|
|
const hardenSpy = vi.spyOn(ses, 'harden');
|
|
|
|
await loadPlugin(manifest);
|
|
|
|
// The host context itself must be passed through untouched so the host
|
|
// can keep modifying its own runtime objects (e.g. on page navigation).
|
|
expect(createPlugin).toHaveBeenCalledWith(
|
|
hostContext,
|
|
manifest,
|
|
expect.any(Function),
|
|
undefined,
|
|
expect.any(AbortSignal),
|
|
);
|
|
|
|
// Host-owned functions must remain extensible: page navigation and
|
|
// runtime code may patch/augment them (e.g. assigning `toString` on a
|
|
// wrapped listener). A deep `ses.harden(context)` here would freeze
|
|
// them and turn such later assignments into
|
|
// `TypeError: Cannot assign to read only property 'toString'`.
|
|
expect(Object.isFrozen(hostListener)).toBe(false);
|
|
expect(Object.isExtensible(hostListener)).toBe(true);
|
|
expect(Object.isFrozen(nestedHostObject)).toBe(false);
|
|
expect(() => {
|
|
hostListener.toString = () => 'patched-by-host';
|
|
}).not.toThrow();
|
|
|
|
expect(hardenSpy).not.toHaveBeenCalled();
|
|
expect(getPlugins()).toHaveLength(1);
|
|
|
|
hardenSpy.mockRestore();
|
|
});
|
|
});
|