From 2d9e13f3bc037c303c13205c6e421e6c85d7642b Mon Sep 17 00:00:00 2001 From: generaltony007 <191973953+hakarrr@users.noreply.github.com> Date: Mon, 26 Oct 2026 23:33:29 +0530 Subject: [PATCH 1/2] Fix plugin comparison in state change notification Co-authored-by: generaltony007 <144252170+GeneralTony007@users.noreply.github.com> --- src/core/core.plugins.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/core.plugins.js b/src/core/core.plugins.js index c73a2a1c2a2..83f20097da8 100644 --- a/src/core/core.plugins.js +++ b/src/core/core.plugins.js @@ -113,7 +113,7 @@ export default class PluginService { _notifyStateChanges(chart) { const previousDescriptors = this._oldCache || []; const descriptors = this._cache; - const diff = (a, b) => a.filter(x => !b.some(y => x.plugin.id === y.plugin.id)); + const diff = (a, b) => a.filter(x => !b.some(y => x.plugin === y.plugin)); this._notify(diff(previousDescriptors, descriptors), chart, 'stop'); this._notify(diff(descriptors, previousDescriptors), chart, 'start'); } From 4806fd0f34a1293bce9b0d7c477f4847c2d942cf Mon Sep 17 00:00:00 2001 From: generaltony007 <191973953+hakarrr@users.noreply.github.com> Date: Mon, 26 Oct 2026 23:33:39 +0530 Subject: [PATCH 2/2] Add tests for plugin lifecycle and event handling Co-authored-by: generaltony007 <144252170+GeneralTony007@users.noreply.github.com> --- test/specs/core.plugin.tests.js | 168 ++++++++++++++++++++++++++++++++ 1 file changed, 168 insertions(+) diff --git a/test/specs/core.plugin.tests.js b/test/specs/core.plugin.tests.js index 76f7b7109b1..952b3efd3d0 100644 --- a/test/specs/core.plugin.tests.js +++ b/test/specs/core.plugin.tests.js @@ -508,3 +508,171 @@ describe('Chart.plugins', function() { }); }); }); + +describe('Plugin object lifecycle', function() { + ['same', 'anonymous', 'distinct'].forEach(function(identity) { + [false, true].forEach(function(disableReplacement) { + it('should stop the old object and start the replacement (' + identity + ', ' + disableReplacement + ')', function() { + const calls = []; + const makePlugin = function(name) { + return { + ...(identity === 'anonymous' ? {} : {id: identity === 'same' ? 'resource' : name}), + start: function() { + calls.push(name + ':start'); + }, + stop: function() { + calls.push(name + ':stop'); + }, + beforeUpdate: function() { + calls.push(name + ':update'); + } + }; + }; + const oldPlugin = makePlugin('old'); + const replacement = makePlugin('new'); + const config = { + type: 'line', + data: {labels: ['a', 'b'], datasets: [{data: [1, 2]}]}, + plugins: [oldPlugin] + }; + const chart = window.acquireChart(config); + expect(calls).toEqual(['old:start', 'old:update']); + calls.length = 0; + config.plugins = [replacement]; + if (disableReplacement) { + chart.options.plugins = false; + } + chart.update(); + expect(calls).toEqual(disableReplacement ? ['old:stop'] : ['old:stop', 'new:start', 'new:update']); + calls.length = 0; + chart.options.plugins = {}; + chart.update(); + expect(calls).toEqual(disableReplacement ? ['new:start', 'new:update'] : ['new:update']); + calls.length = 0; + chart.update(); + expect(calls).toEqual(['new:update']); + calls.length = 0; + window.releaseChart(chart); + expect(calls).toEqual(['new:stop']); + }); + }); + }); +}); + +describe('Plugin object lifecycle registry transitions', function() { + it('should stop an unregistered object before starting a new object with the same ID', function() { + const calls = []; + const oldPlugin = { + id: 'resource', + start: function() { + calls.push('old:start'); + }, + stop: function() { + calls.push('old:stop'); + } + }; + const replacement = { + id: 'resource', + start: function() { + calls.push('new:start'); + }, + stop: function() { + calls.push('new:stop'); + } + }; + Chart.register(oldPlugin); + const chart = window.acquireChart({}); + expect(calls).toEqual(['old:start']); + calls.length = 0; + Chart.unregister(oldPlugin); + Chart.register(replacement); + chart.update(); + expect(calls).toEqual(['old:stop', 'new:start']); + calls.length = 0; + Chart.register(replacement); + chart.update(); + expect(calls).toEqual([]); + Chart.unregister(replacement); + chart.update(); + expect(calls).toEqual(['new:stop']); + window.releaseChart(chart); + }); + + it('should start and stop distinct anonymous local plugins independently', function() { + const calls = []; + const first = { + start: function() { + calls.push('first:start'); + }, + stop: function() { + calls.push('first:stop'); + } + }; + const second = { + start: function() { + calls.push('second:start'); + }, + stop: function() { + calls.push('second:stop'); + } + }; + const config = {plugins: [first]}; + const chart = window.acquireChart(config); + calls.length = 0; + config.plugins.push(second); + chart.update(); + expect(calls).toEqual(['second:start']); + calls.length = 0; + config.plugins.splice(0, 1); + chart.update(); + expect(calls).toEqual(['first:stop']); + calls.length = 0; + window.releaseChart(chart); + expect(calls).toEqual(['second:stop']); + }); +}); + +describe('Plugin-owned event listeners', function() { + [true, false].forEach(function(withId) { + it('should transfer event handling to the replacement object (' + withId + ')', function() { + const events = []; + const listeners = new WeakMap(); + const makePlugin = function(name) { + return { + ...(withId ? {id: 'listener'} : {}), + start: function(chart) { + const listener = function() { + events.push(name); + }; + listeners.set(this, {canvas: chart.canvas, listener}); + chart.canvas.addEventListener('test-resource', listener); + }, + stop: function() { + const resource = listeners.get(this); + if (resource) { + resource.canvas.removeEventListener('test-resource', resource.listener); + } + listeners.delete(this); + }, + beforeUpdate: function() { + expect(listeners.has(this)).toBeTrue(); + } + }; + }; + const oldPlugin = makePlugin('old'); + const replacement = makePlugin('new'); + const config = {plugins: [oldPlugin]}; + const chart = window.acquireChart(config); + config.plugins = [replacement]; + chart.update(); + chart.canvas.dispatchEvent(new Event('test-resource')); + expect(events).toEqual(['new']); + expect(listeners.has(oldPlugin)).toBeFalse(); + const canvas = chart.canvas; + window.releaseChart(chart); + canvas.dispatchEvent(new Event('test-resource')); + expect(events).toEqual(['new']); + expect(listeners.has(replacement)).toBeFalse(); + }); + }); +});