Skip to content

Commit f5612a2

Browse files
authored
fix: protect internal methods (#422)
1 parent 83772ef commit f5612a2

4 files changed

Lines changed: 109 additions & 1 deletion

File tree

src/flash/object.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,18 @@ export type FlashObjectOptions<
3535
onDidExitMessage?: () => void;
3636
} & T;
3737

38+
const INTERNAL_KEYS = new Set([
39+
// Lifecycle callbacks — already assigned to `this` above the loop, but
40+
// listed here as a safety net in case future refactors change that order.
41+
'onDestroy',
42+
'onDidDestroyMessage',
43+
'onDidExitMessage',
44+
// Options consumed by the constructor but not stored as class properties.
45+
'testHelperDisableTimeout',
46+
// Stored in a private field (#flashService), so `key in this` won't catch it.
47+
'flashService',
48+
]);
49+
3850
export default class FlashObject<
3951
T extends Record<string, unknown> = Record<string, unknown>,
4052
> {
@@ -99,7 +111,7 @@ export default class FlashObject<
99111

100112
// Copy any custom properties from T
101113
for (const [key, value] of Object.entries(options)) {
102-
if (!(key in this) && key !== 'flashService' && !key.startsWith('on')) {
114+
if (!(key in this) && !INTERNAL_KEYS.has(key)) {
103115
(this as Record<string, unknown>)[key] = value;
104116
}
105117
}

src/services/flash-messages.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,11 @@ export default class FlashMessagesService<
214214
): boolean {
215215
const message = this.findBy(key, value);
216216
if (message) {
217+
// Remove from queue immediately so the message disappears from the UI
218+
// without waiting for the extendedTimeout exit-animation delay.
219+
// destroyMessage() still handles timer cleanup and destroyable teardown.
220+
this.queue = this.queue.filter((flash) => flash !== message);
221+
this.#guidSet.delete(message._guid);
217222
message.destroyMessage();
218223
return true;
219224
}

tests/unit/flash/object-test.ts

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -344,4 +344,71 @@ module('Unit | Flash | object', function (hooks) {
344344

345345
assert.notOk(flash.timerTaskInstance, 'no timer when timeout is undefined');
346346
});
347+
348+
test('custom properties starting with "on" are preserved', function (assert) {
349+
interface CustomFields extends Record<string, unknown> {
350+
onAction: () => void;
351+
onRetry: () => void;
352+
}
353+
354+
let actionCalled = false;
355+
let retryCalled = false;
356+
357+
const flash = new FlashMessage<CustomFields>({
358+
message: 'test',
359+
sticky: true,
360+
onAction: () => {
361+
actionCalled = true;
362+
},
363+
onRetry: () => {
364+
retryCalled = true;
365+
},
366+
});
367+
368+
const typedFlash = flash as FlashMessage<CustomFields> & CustomFields;
369+
370+
assert.strictEqual(
371+
typeof typedFlash.onAction,
372+
'function',
373+
'onAction is preserved on the flash object',
374+
);
375+
assert.strictEqual(
376+
typeof typedFlash.onRetry,
377+
'function',
378+
'onRetry is preserved on the flash object',
379+
);
380+
381+
typedFlash.onAction();
382+
typedFlash.onRetry();
383+
384+
assert.true(actionCalled, 'onAction callback is callable');
385+
assert.true(retryCalled, 'onRetry callback is callable');
386+
});
387+
388+
test('internal callbacks are not duplicated as custom properties', function (assert) {
389+
let destroyCallCount = 0;
390+
391+
const flash = new FlashMessage({
392+
message: 'test',
393+
sticky: true,
394+
onDestroy: () => {
395+
destroyCallCount++;
396+
},
397+
});
398+
399+
// onDestroy should be set as the class property, not duplicated
400+
assert.strictEqual(
401+
typeof flash.onDestroy,
402+
'function',
403+
'onDestroy is set as class callback',
404+
);
405+
406+
// Verify the callback works
407+
flash.onDestroy?.();
408+
assert.strictEqual(
409+
destroyCallCount,
410+
1,
411+
'onDestroy callback works correctly',
412+
);
413+
});
347414
});

tests/unit/services/flash-messages-test.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -567,6 +567,30 @@ module('Unit | Service | flash-messages', function (hooks) {
567567
assert.strictEqual(service.queue.length, 0, 'it removes the message');
568568
});
569569

570+
test('#removeBy immediately removes from queue even with extendedTimeout', function (assert) {
571+
const service = this.owner.lookup(
572+
'service:flash-messages',
573+
) as FlashMessagesService<{ id: string }>;
574+
575+
service.add({
576+
message: 'system-notification',
577+
id: 'test-notification',
578+
sticky: true,
579+
extendedTimeout: 5000,
580+
});
581+
582+
assert.strictEqual(service.queue.length, 1, 'message is in queue');
583+
584+
const removed = service.removeBy('id', 'test-notification');
585+
586+
assert.true(removed, 'removeBy returns true');
587+
assert.strictEqual(
588+
service.queue.length,
589+
0,
590+
'message is immediately removed from queue without waiting for extendedTimeout',
591+
);
592+
});
593+
570594
test('it supports custom fields via generics', function (assert) {
571595
const service = this.owner.lookup(
572596
'service:flash-messages',

0 commit comments

Comments
 (0)