@@ -121,6 +121,206 @@ test('logs events to console - listener added to window', (assert) => {
121121 clearGlobalProps ( agLogAddEventListenerProp ) ;
122122} ) ;
123123
124+ test ( 'forwards addEventListener options' , ( assert ) => {
125+ assert . expect ( 6 ) ;
126+
127+ const useCaptureElement = document . createElement ( 'div' ) ;
128+ const onceElement = document . createElement ( 'div' ) ;
129+ const passiveCaptureElement = document . createElement ( 'div' ) ;
130+ const combinedOptionsElement = document . createElement ( 'div' ) ;
131+ let useCaptureCallCount = 0 ;
132+ let onceCallCount = 0 ;
133+ let passiveCaptureCallCount = 0 ;
134+ let combinedCallCount = 0 ;
135+
136+ runScriptlet ( name ) ;
137+
138+ useCaptureElement . addEventListener ( 'click' , ( ) => {
139+ useCaptureCallCount += 1 ;
140+ } , true ) ;
141+
142+ onceElement . addEventListener ( 'click' , ( ) => {
143+ onceCallCount += 1 ;
144+ } , { once : true } ) ;
145+
146+ passiveCaptureElement . addEventListener ( 'click' , ( ) => {
147+ passiveCaptureCallCount += 1 ;
148+ } , { capture : true , passive : true } ) ;
149+
150+ combinedOptionsElement . addEventListener ( 'click' , ( ) => {
151+ combinedCallCount += 1 ;
152+ } , { capture : true , passive : true , once : true } ) ;
153+
154+ useCaptureElement . click ( ) ;
155+ useCaptureElement . click ( ) ;
156+ onceElement . click ( ) ;
157+ onceElement . click ( ) ;
158+ passiveCaptureElement . click ( ) ;
159+ combinedOptionsElement . click ( ) ;
160+ combinedOptionsElement . click ( ) ;
161+
162+ assert . strictEqual ( window . hit , 'FIRED' , 'hit function fired' ) ;
163+ assert . strictEqual (
164+ useCaptureCallCount ,
165+ 2 ,
166+ 'boolean capture option should be forwarded to native addEventListener' ,
167+ ) ;
168+ assert . strictEqual (
169+ onceCallCount ,
170+ 1 ,
171+ 'once option should be forwarded to native addEventListener' ,
172+ ) ;
173+ assert . strictEqual (
174+ passiveCaptureCallCount ,
175+ 1 ,
176+ 'capture and passive options should be forwarded to native addEventListener' ,
177+ ) ;
178+ assert . strictEqual (
179+ combinedCallCount ,
180+ 1 ,
181+ 'combined options object should be forwarded to native addEventListener' ,
182+ ) ;
183+ assert . strictEqual (
184+ typeof onceElement . addEventListener ,
185+ 'function' ,
186+ 'wrapped addEventListener should stay callable' ,
187+ ) ;
188+ } ) ;
189+
190+ test ( 'noProtect parameter allows subsequent override of addEventListener' , ( assert ) => {
191+ assert . expect ( 9 ) ;
192+
193+ const scriptletArgs = [ 'true' ] ;
194+ runScriptlet ( name , scriptletArgs ) ;
195+
196+ const elementId = 'noProtectElement' ;
197+ const elementProp = 'elementProp' ;
198+ const elementEventName = 'click' ;
199+ const callback = function callback ( ) {
200+ window [ elementProp ] = 'clicked' ;
201+ } ;
202+
203+ const element = document . createElement ( 'div' ) ;
204+ element . setAttribute ( 'id' , elementId ) ;
205+ console . log = function log ( ...args ) {
206+ const input = args [ 0 ] ;
207+ const elementArg = args [ 1 ] ;
208+ if ( input . includes ( 'trace' ) ) {
209+ return ;
210+ }
211+
212+ if ( input . includes ( 'log-addEventListener Element:' ) ) {
213+ assert . true ( elementArg . matches ( `div#${ elementId } ` ) , 'target element should match the noProtect element' ) ;
214+ } else {
215+ assert . ok ( input . includes ( elementEventName ) , 'event name should be logged for noProtect' ) ;
216+ assert . ok ( input . includes ( callback . toString ( ) ) , 'callback should be logged for noProtect' ) ;
217+ assert . ok (
218+ input . includes ( `Element: div[id="${ elementId } "]` ) ,
219+ 'target element should be logged for noProtect' ,
220+ ) ;
221+ assert . notOk (
222+ input . includes ( INVALID_MESSAGE_START ) ,
223+ 'Invalid message should not be displayed for noProtect' ,
224+ ) ;
225+ }
226+
227+ nativeConsole ( ...args ) ;
228+ } ;
229+
230+ element . addEventListener ( elementEventName , callback ) ;
231+ element . click ( ) ;
232+
233+ assert . strictEqual ( window . hit , 'FIRED' , 'hit function fired' ) ;
234+ assert . strictEqual ( window [ elementProp ] , 'clicked' , 'element listener should still be logged and called' ) ;
235+ clearGlobalProps ( 'hit' , elementProp ) ;
236+
237+ let overrideWorked = false ;
238+ window . EventTarget . prototype . addEventListener = function customWrapper ( ) {
239+ overrideWorked = true ;
240+ } ;
241+
242+ const elementAfterOverride = document . createElement ( 'div' ) ;
243+ elementAfterOverride . addEventListener ( 'click' , ( ) => { } ) ;
244+
245+ assert . strictEqual ( overrideWorked , true , 'addEventListener should be overridable with noProtect' ) ;
246+ assert . strictEqual ( window . hit , undefined , 'hit should NOT fire' ) ;
247+ } ) ;
248+
249+ test ( 'default behavior (no noProtect) protects addEventListener from override' , ( assert ) => {
250+ assert . expect ( 12 ) ;
251+
252+ runScriptlet ( name ) ;
253+
254+ const descriptor = Object . getOwnPropertyDescriptor (
255+ window . EventTarget . prototype ,
256+ 'addEventListener' ,
257+ ) ;
258+
259+ assert . strictEqual ( typeof descriptor . get , 'function' , 'descriptor should have a getter' ) ;
260+ assert . strictEqual ( typeof descriptor . set , 'function' , 'descriptor should have a setter' ) ;
261+ assert . strictEqual ( descriptor . configurable , true , 'descriptor should be configurable' ) ;
262+
263+ const originalWrapper = descriptor . get ( ) ;
264+
265+ window . EventTarget . prototype . addEventListener = function maliciousWrapper ( ) {
266+ throw new Error ( 'This should not be called' ) ;
267+ } ;
268+
269+ const currentAddEventListener = window . EventTarget . prototype . addEventListener ;
270+ assert . strictEqual (
271+ currentAddEventListener ,
272+ originalWrapper ,
273+ 'addEventListener should still be the scriptlet wrapper after attempted override' ,
274+ ) ;
275+
276+ assert . strictEqual (
277+ descriptor . get ( ) ,
278+ originalWrapper ,
279+ 'getter should still return original wrapper after setter was called' ,
280+ ) ;
281+
282+ const elementId = 'protectedElement' ;
283+ const protectedProp = 'protectedProp' ;
284+ const eventName = 'click' ;
285+ const callback = function callback ( ) {
286+ window [ protectedProp ] = 'clicked' ;
287+ } ;
288+
289+ const element = document . createElement ( 'div' ) ;
290+ element . setAttribute ( 'id' , elementId ) ;
291+ console . log = function log ( ...args ) {
292+ const input = args [ 0 ] ;
293+ const elementArg = args [ 1 ] ;
294+ if ( input . includes ( 'trace' ) ) {
295+ return ;
296+ }
297+
298+ if ( input . includes ( 'log-addEventListener Element:' ) ) {
299+ assert . true ( elementArg . matches ( `div#${ elementId } ` ) , 'target element should match the protected element' ) ;
300+ } else {
301+ assert . ok ( input . includes ( eventName ) , 'event name should be logged after blocked override' ) ;
302+ assert . ok ( input . includes ( callback . toString ( ) ) , 'callback should be logged after blocked override' ) ;
303+ assert . ok (
304+ input . includes ( `Element: div[id="${ elementId } "]` ) ,
305+ 'target element should be logged after blocked override' ,
306+ ) ;
307+ assert . notOk (
308+ input . includes ( INVALID_MESSAGE_START ) ,
309+ 'Invalid message should not be displayed after blocked override' ,
310+ ) ;
311+ }
312+
313+ nativeConsole ( ...args ) ;
314+ } ;
315+
316+ element . addEventListener ( eventName , callback ) ;
317+ element . click ( ) ;
318+
319+ assert . strictEqual ( window . hit , 'FIRED' , 'hit function fired after blocked override' ) ;
320+ assert . strictEqual ( window [ protectedProp ] , 'clicked' , 'listener should still use the original protected wrapper' ) ;
321+ clearGlobalProps ( protectedProp ) ;
322+ } ) ;
323+
124324test ( 'logs events to console - listener is null' , ( assert ) => {
125325 const eventName = 'click' ;
126326 const listener = null ;
0 commit comments