From 3ed81b88f6ea4c8d32ead6ded57e04929e4b8980 Mon Sep 17 00:00:00 2001 From: Sridhar Bala Date: Sun, 13 Sep 2026 01:40:34 +0530 Subject: [PATCH] fix: cancel pending style manager updates on destroy --- packages/core/src/style_manager/index.ts | 12 +++-- .../core/test/specs/style_manager/destroy.ts | 52 +++++++++++++++++++ 2 files changed, 60 insertions(+), 4 deletions(-) create mode 100644 packages/core/test/specs/style_manager/destroy.ts diff --git a/packages/core/src/style_manager/index.ts b/packages/core/src/style_manager/index.ts index 3e8eb5380..309ed4fa5 100644 --- a/packages/core/src/style_manager/index.ts +++ b/packages/core/src/style_manager/index.ts @@ -79,6 +79,8 @@ export default class StyleManager extends ItemManagerModule< > { builtIn: PropertyFactory; upAll: Debounced; + private upProps: Debounced & (() => void); + private trgCustom: Debounced & (() => void); properties: typeof Properties; events = StyleManagerEvents; sectors: Sectors; @@ -121,15 +123,15 @@ export default class StyleManager extends ItemManagerModule< model.listenTo(em, ComponentsEvents.toggled, this.__clearStateTarget); // Triggers only for properties (avoid selection refresh) - const upProps = debounce(() => { + this.upProps = debounce(() => { this.__upProps(); this.__trgCustom(); }, 0); - model.listenTo(em, 'styleable:change undo redo', upProps); + model.listenTo(em, 'styleable:change undo redo', this.upProps); // Triggers only custom event - const trgCustom = debounce(() => this.__trgCustom(), 0); - model.listenTo(em, `${events.layerSelect} ${events.target}`, trgCustom); + this.trgCustom = debounce(() => this.__trgCustom(), 0); + model.listenTo(em, `${events.layerSelect} ${events.target}`, this.trgCustom); // Other listeners model.on('change:lastTarget', () => em.trigger(events.target, this.getSelected())); @@ -836,5 +838,7 @@ export default class StyleManager extends ItemManagerModule< this.SectView?.remove(); this.model.stopListening(); this.upAll.cancel(); + this.upProps.cancel(); + this.trgCustom.cancel(); } } diff --git a/packages/core/test/specs/style_manager/destroy.ts b/packages/core/test/specs/style_manager/destroy.ts new file mode 100644 index 000000000..917272be9 --- /dev/null +++ b/packages/core/test/specs/style_manager/destroy.ts @@ -0,0 +1,52 @@ +import Editor from '../../../src/editor'; + +describe('StyleManager pending updates on destroy', () => { + beforeEach(() => { + jest.useFakeTimers(); + }); + + afterEach(() => { + jest.clearAllTimers(); + jest.useRealTimers(); + }); + + test('does not refresh properties after destroying the editor', () => { + const editor = new Editor({}); + const em = editor.getModel(); + em.Pages.onLoad(); + const component = editor.addComponents({ type: 'text', content: 'Text', style: { padding: '5px' } })[0]; + em.Styles.select(component); + jest.runOnlyPendingTimers(); + const update = jest.spyOn(em.Styles, '__upProps'); + component.setStyle({ padding: '10px' }); + editor.destroy(); + expect(() => jest.runOnlyPendingTimers()).not.toThrow(); + expect(update).not.toHaveBeenCalled(); + }); + + test('does not send pending custom events after destroying the module', () => { + const editor = new Editor({}); + const em = editor.getModel(); + const styles = em.Styles; + const custom = jest.fn(); + em.on(styles.events.custom, custom); + em.trigger(styles.events.target, undefined); + styles.destroy(); + jest.runOnlyPendingTimers(); + expect(custom).not.toHaveBeenCalled(); + editor.destroy(); + }); + + test('still refreshes properties and sends custom events while alive', () => { + const editor = new Editor({}); + const em = editor.getModel(); + const update = jest.spyOn(em.Styles, '__upProps'); + const custom = jest.fn(); + em.on(em.Styles.events.custom, custom); + em.trigger('styleable:change'); + jest.runOnlyPendingTimers(); + expect(update).toHaveBeenCalledTimes(1); + expect(custom).toHaveBeenCalled(); + editor.destroy(); + }); +});