-
Notifications
You must be signed in to change notification settings - Fork 481
fix(ui): prevent ng-event listener accumulation on iframe reload #37113
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -219,7 +219,7 @@ describe('IframeComponent', () => { | |
| }; | ||
| }); | ||
|
|
||
| it('should remove and add listener on load', () => { | ||
| it('should remove and add listener on load using the same stable function references', () => { | ||
| iframeEl.triggerEventHandler('load', { | ||
| target: { | ||
| contentDocument: { | ||
|
|
@@ -228,19 +228,26 @@ describe('IframeComponent', () => { | |
| } | ||
| }); | ||
|
|
||
| // eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
| const keyDownRef = (comp as any).boundEmitKeyDown; | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would deriving these from the mock call history work just as well here? Something like: const [, keyDownRef] = comp.iframeElement.nativeElement.contentWindow.addEventListener.mock.calls[0];That asserts the same invariant without depending on the private field names, so a later rename wouldn't break the test. It would also leave the door open to switching these to |
||
| // eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
| const customEventRef = (comp as any).boundEmitCustomEvent; | ||
|
|
||
| // remove must receive the exact same reference as add so the browser | ||
| // can actually deregister the handler instead of silently failing | ||
| expect( | ||
| comp.iframeElement.nativeElement.contentWindow.removeEventListener | ||
| ).toHaveBeenCalledWith('keydown', expect.any(Function)); | ||
| ).toHaveBeenCalledWith('keydown', keyDownRef); | ||
| expect( | ||
| comp.iframeElement.nativeElement.contentWindow.document.removeEventListener | ||
| ).toHaveBeenCalledWith('ng-event', expect.any(Function)); | ||
| ).toHaveBeenCalledWith('ng-event', customEventRef); | ||
|
|
||
| expect( | ||
| comp.iframeElement.nativeElement.contentWindow.addEventListener | ||
| ).toHaveBeenCalledWith('keydown', expect.any(Function)); | ||
| ).toHaveBeenCalledWith('keydown', keyDownRef); | ||
| expect( | ||
| comp.iframeElement.nativeElement.contentWindow.document.addEventListener | ||
| ).toHaveBeenCalledWith('ng-event', expect.any(Function)); | ||
| ).toHaveBeenCalledWith('ng-event', customEventRef); | ||
| }); | ||
|
|
||
| it('should set the colors to the jsp on load', () => { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -76,6 +76,13 @@ export class IframeComponent implements OnInit, OnDestroy { | |
|
|
||
| private destroy$: Subject<boolean> = new Subject<boolean>(); | ||
|
|
||
| // Stable bound references required so removeEventListener can match the | ||
| // exact function object that was passed to addEventListener. Using | ||
| // .bind(this) inline creates a new object each call, making removal a | ||
| // no-op and causing listeners to accumulate on every iframe load event. | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Small wording question on this comment. The first half is exactly right — As far as I can tell it only holds for Would it be worth splitting that out so the next person reading this doesn't assume both listeners leak the same way? |
||
| private readonly boundEmitKeyDown = this.emitKeyDown.bind(this); | ||
| private readonly boundEmitCustomEvent = this.emitCustonEvent.bind(this); | ||
|
|
||
| ngOnInit(): void { | ||
| this.iframeOverlayService.overlay | ||
| .pipe(takeUntil(this.destroy$)) | ||
|
|
@@ -264,17 +271,11 @@ export class IframeComponent implements OnInit, OnDestroy { | |
| } | ||
|
|
||
| private handleIframeEvents($event): void { | ||
| this.getIframeWindow().removeEventListener('keydown', this.emitKeyDown.bind(this)); | ||
| this.getIframeWindow().document.removeEventListener( | ||
| 'ng-event', | ||
| this.emitCustonEvent.bind(this) | ||
| ); | ||
|
|
||
| this.getIframeWindow().addEventListener('keydown', this.emitKeyDown.bind(this)); | ||
| this.getIframeWindow().document.addEventListener( | ||
| 'ng-event', | ||
| this.emitCustonEvent.bind(this) | ||
| ); | ||
| this.getIframeWindow().removeEventListener('keydown', this.boundEmitKeyDown); | ||
| this.getIframeWindow().document.removeEventListener('ng-event', this.boundEmitCustomEvent); | ||
|
|
||
| this.getIframeWindow().addEventListener('keydown', this.boundEmitKeyDown); | ||
| this.getIframeWindow().document.addEventListener('ng-event', this.boundEmitCustomEvent); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I traced the save path for this one and wanted to check the root cause with you. Since the duplicate save runs through Have you been able to confirm in the Network tab that the two |
||
| this.charge.emit($event); | ||
|
|
||
| const doc = this.getIframeDocument(); | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would it be worth adding a test that exercises the symptom directly? Right now the assertions stop at which references were handed to
add/removeEventListenerafter a singleload, so nothing covers the property that actually matters here: one dispatchedng-eventproducing exactly one emission after several reloads.Something like triggering
loadtwo or three times, grabbing the handler fromcontentWindow.document.addEventListener.mock.calls, invoking it once, and assertingcustom.emitfired once would pin the regression down. As it stands, a refactor could bring the duplicate back without turning the suite red.