Skip to content

Commit 33c984e

Browse files
smastromcursoragent
andcommitted
Core, Tests - Align notification option priority
Keep v3 notification option precedence explicit while preserving legacy promise alias config during loading normalization. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent dc87833 commit 33c984e

6 files changed

Lines changed: 137 additions & 28 deletions

File tree

packages/notivue/core/constants.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ export const DEFAULT_NOTIFICATION_OPTIONS = {
4040
[NotificationTypeKeys.LOADING_ERROR]: { ariaLive: 'assertive', ariaRole: 'alert' },
4141
[NotificationTypeKeys.PROMISE]: {},
4242
[NotificationTypeKeys.PROMISE_RESOLVE]: {},
43-
[NotificationTypeKeys.PROMISE_REJECT]: { ariaLive: 'assertive', ariaRole: 'alert' },
43+
[NotificationTypeKeys.PROMISE_REJECT]: {},
4444
} as NotivueConfigRequired['notifications']
4545

4646
export const DEFAULT_CONFIG: NotivueConfigRequired = {

packages/notivue/core/utils.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,8 @@ function notificationTypeConfigSlice(
6464
const fromLegacy = legacy ? configOptions[legacy] : undefined
6565
const fromCanon = configOptions[canonical]
6666

67-
return { ...fromLegacy, ...fromCanon } as NotificationOptions
67+
// Per-type slice: canonical defaults, then legacy `promise*` overrides (deprecated alias).
68+
return { ...fromCanon, ...fromLegacy } as NotificationOptions
6869
}
6970

7071
export function mergeNotificationOptions<T extends Obj = Obj>(
@@ -75,6 +76,7 @@ export function mergeNotificationOptions<T extends Obj = Obj>(
7576

7677
const type = toCanonicalNotificationType(pushOptions.type)
7778

79+
// global → per-type (canonical + legacy alias) → push → loading duration enforcement
7880
return {
7981
...configOptions.global,
8082
...notificationTypeConfigSlice(configOptions, type),

tests/Notivue/slot-custom-push-options.cy.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ describe('Push notification options have higher priority over config', () => {
5555

5656
.get('.Promise')
5757
.click()
58-
.checkSlotAgainst({ ...options, duration: null })
58+
.checkSlotAgainst({ ...options, duration: -1 })
5959
})
6060

6161
it('Promise - Resolve', () => {
@@ -146,7 +146,7 @@ describe('Push notification options are merged properly with config', () => {
146146

147147
.get('.Promise')
148148
.click()
149-
.checkSlotAgainst({ ...expectedOptions, duration: null })
149+
.checkSlotAgainst({ ...expectedOptions, duration: -1 })
150150
})
151151
})
152152

tests/Notivue/slot-default-options.cy.ts

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,72 +3,75 @@ import { RESOLVE_REJECT_DELAY } from '@/support/utils'
33
import { DEFAULT_NOTIFICATION_OPTIONS as DEFAULT_OPTIONS } from '@/core/constants'
44

55
describe('Default options match the slot content', () => {
6-
const { success, error, warning, info, promise } = DEFAULT_OPTIONS as Record<
7-
keyof typeof DEFAULT_OPTIONS,
8-
Record<string, unknown>
9-
>
6+
const {
7+
global,
8+
success,
9+
error,
10+
warning,
11+
info,
12+
loading,
13+
'loading-success': loadingSuccess,
14+
'loading-error': loadingError,
15+
} = DEFAULT_OPTIONS as unknown as Record<keyof typeof DEFAULT_OPTIONS, Record<string, unknown>>
1016

1117
describe('First-level notifications', () => {
1218
it('Success', () => {
1319
cy.mountNotivue()
1420

1521
.get('.Success')
1622
.click()
17-
.checkSlotAgainst(success)
23+
.checkSlotAgainst({ ...global, ...success })
1824
})
1925

2026
it('Error', () => {
2127
cy.mountNotivue()
2228

2329
.get('.Error')
2430
.click()
25-
.checkSlotAgainst(error)
31+
.checkSlotAgainst({ ...global, ...error })
2632
})
2733

2834
it('Warning', () => {
2935
cy.mountNotivue()
3036

3137
.get('.Warning')
3238
.click()
33-
.checkSlotAgainst(warning)
39+
.checkSlotAgainst({ ...global, ...warning })
3440
})
3541

3642
it('Info', () => {
3743
cy.mountNotivue()
3844

3945
.get('.Info')
4046
.click()
41-
.checkSlotAgainst(info)
47+
.checkSlotAgainst({ ...global, ...info })
4248
})
4349

4450
it('Promise', () => {
4551
cy.mountNotivue()
4652

4753
.get('.Promise')
4854
.click()
49-
.checkSlotAgainst({ ...promise, duration: -1 })
55+
.checkSlotAgainst({ ...global, ...loading, duration: -1 })
5056
})
5157
})
5258

5359
describe('Promise - Resolve / Reject', () => {
54-
const promiseResolve = DEFAULT_OPTIONS['promise-resolve'] as Record<string, unknown>
55-
const promiseReject = DEFAULT_OPTIONS['promise-reject'] as Record<string, unknown>
56-
5760
it('Promise - Resolve', () => {
5861
cy.mountNotivue()
5962

6063
.get('.PushPromiseAndResolve')
6164
.click()
6265
.wait(RESOLVE_REJECT_DELAY)
63-
.checkSlotAgainst(promiseResolve)
66+
.checkSlotAgainst({ ...global, ...loadingSuccess })
6467
})
6568

6669
it('Promise - Reject', () => {
6770
cy.mountNotivue()
6871
.get('.PushPromiseAndReject')
6972
.click()
7073
.wait(RESOLVE_REJECT_DELAY)
71-
.checkSlotAgainst(promiseReject)
74+
.checkSlotAgainst({ ...global, ...loadingError })
7275
})
7376
})
7477
})

tests/Notivue/slot-global-options.cy.ts

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ const globalOptions = getRandomOptions()
44
const options = getRandomOptions()
55
const newOptions = getRandomOptions()
66

7-
describe('Global options have higher priority over defaults', () => {
7+
describe('Per-type options override global config', () => {
88
const customConfig = {
99
config: {
1010
notifications: {
@@ -28,39 +28,39 @@ describe('Global options have higher priority over defaults', () => {
2828

2929
.get('.Success')
3030
.click()
31-
.checkSlotAgainst(globalOptions)
31+
.checkSlotAgainst(options)
3232
})
3333

3434
it('Error', () => {
3535
cy.mountNotivue(customConfig)
3636

3737
.get('.Error')
3838
.click()
39-
.checkSlotAgainst(globalOptions)
39+
.checkSlotAgainst(options)
4040
})
4141

4242
it('Warning', () => {
4343
cy.mountNotivue(customConfig)
4444

4545
.get('.Warning')
4646
.click()
47-
.checkSlotAgainst(globalOptions)
47+
.checkSlotAgainst(options)
4848
})
4949

5050
it('Info', () => {
5151
cy.mountNotivue(customConfig)
5252

5353
.get('.Info')
5454
.click()
55-
.checkSlotAgainst(globalOptions)
55+
.checkSlotAgainst(options)
5656
})
5757

5858
it('Promise - Should not override duration', () => {
5959
cy.mountNotivue(customConfig)
6060

6161
.get('.Promise')
6262
.click()
63-
.checkSlotAgainst({ ...globalOptions, duration: null })
63+
.checkSlotAgainst({ ...options, duration: -1 })
6464
})
6565
})
6666

@@ -71,7 +71,7 @@ describe('Global options have higher priority over defaults', () => {
7171
.get('.PushPromiseAndResolve')
7272
.click()
7373
.wait(RESOLVE_REJECT_DELAY)
74-
.checkSlotAgainst(globalOptions)
74+
.checkSlotAgainst(newOptions)
7575
})
7676

7777
it('Promise - Reject', () => {
@@ -80,12 +80,12 @@ describe('Global options have higher priority over defaults', () => {
8080
.get('.PushPromiseAndReject')
8181
.click()
8282
.wait(RESOLVE_REJECT_DELAY)
83-
.checkSlotAgainst(globalOptions)
83+
.checkSlotAgainst(newOptions)
8484
})
8585
})
8686
})
8787

88-
describe('Push options have higher priority over globals', () => {
88+
describe('Push options have higher priority over config', () => {
8989
const componentConf = {
9090
config: { notifications: { global: globalOptions } },
9191
props: {
@@ -133,7 +133,7 @@ describe('Push options have higher priority over globals', () => {
133133

134134
.get('.Promise')
135135
.click()
136-
.checkSlotAgainst({ ...options, duration: null })
136+
.checkSlotAgainst({ ...options, duration: -1 })
137137
})
138138
})
139139

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import type { NotivueConfig, NotivueConfigRequired, PushOptionsWithInternals } from 'notivue'
2+
3+
import { describe, expect, test } from 'vitest'
4+
5+
import { mergeNotificationOptions } from '@/core/utils'
6+
7+
import { DEFAULT_NOTIFICATION_OPTIONS } from '@/core/constants'
8+
9+
const baseConfig = DEFAULT_NOTIFICATION_OPTIONS as NotivueConfigRequired['notifications']
10+
11+
function merge<T extends Record<string, unknown> = Record<string, never>>(
12+
notifications: NotivueConfig['notifications'] = {},
13+
push: PushOptionsWithInternals<T>
14+
) {
15+
return mergeNotificationOptions(
16+
{ ...baseConfig, ...notifications } as NotivueConfigRequired['notifications'],
17+
push
18+
)
19+
}
20+
21+
describe('mergeNotificationOptions', () => {
22+
test('Per-type options override global config', () => {
23+
const result = merge(
24+
{
25+
global: { title: 'global', duration: 1000 },
26+
success: { title: 'typed', duration: 2000 },
27+
},
28+
{ id: '1', type: 'success' }
29+
)
30+
31+
expect(result.title).toBe('typed')
32+
expect(result.duration).toBe(2000)
33+
})
34+
35+
test('Push options override config', () => {
36+
const result = merge(
37+
{
38+
global: { title: 'global', duration: 1000 },
39+
success: { title: 'typed', duration: 2000 },
40+
},
41+
{ id: '1', type: 'success', title: 'push', duration: 3000 }
42+
)
43+
44+
expect(result.title).toBe('push')
45+
expect(result.duration).toBe(3000)
46+
})
47+
48+
test('Loading notifications always use unlimited duration', () => {
49+
const result = merge(
50+
{
51+
global: { duration: 1000 },
52+
promise: { duration: 5000 },
53+
},
54+
{ id: '1', type: 'loading', duration: 3000 }
55+
)
56+
57+
expect(result.duration).toBe(-1)
58+
expect(result.type).toBe('loading')
59+
})
60+
61+
test('Legacy promise* config overrides canonical loading* defaults', () => {
62+
const result = merge(
63+
{
64+
'promise-reject': { ariaLive: 'polite', ariaRole: 'status', title: 'legacy' },
65+
},
66+
{ id: '1', type: 'loading-error' }
67+
)
68+
69+
expect(result.ariaLive).toBe('polite')
70+
expect(result.ariaRole).toBe('status')
71+
expect(result.title).toBe('legacy')
72+
})
73+
74+
test('Canonical loading* config applies when legacy alias is unset', () => {
75+
const result = merge(
76+
{
77+
'loading-error': { ariaLive: 'polite', ariaRole: 'status', title: 'canonical' },
78+
},
79+
{ id: '1', type: 'loading-error' }
80+
)
81+
82+
expect(result.ariaLive).toBe('polite')
83+
expect(result.title).toBe('canonical')
84+
})
85+
86+
test('Legacy promise* config overrides canonical loading* config when both are set', () => {
87+
const result = merge(
88+
{
89+
'loading-error': { ariaLive: 'polite', ariaRole: 'status', title: 'canonical' },
90+
'promise-reject': { ariaLive: 'assertive', ariaRole: 'alert', title: 'legacy' },
91+
},
92+
{ id: '1', type: 'loading-error' }
93+
)
94+
95+
expect(result.ariaLive).toBe('assertive')
96+
expect(result.title).toBe('legacy')
97+
})
98+
99+
test('Normalizes deprecated promise types to loading variants', () => {
100+
expect(merge({}, { id: '1', type: 'promise' }).type).toBe('loading')
101+
expect(merge({}, { id: '1', type: 'promise-resolve' }).type).toBe('loading-success')
102+
expect(merge({}, { id: '1', type: 'promise-reject' }).type).toBe('loading-error')
103+
})
104+
})

0 commit comments

Comments
 (0)