Skip to content

Commit ba0873c

Browse files
author
Yueying Lu
committed
Address comments
1 parent 7b5dce3 commit ba0873c

File tree

6 files changed

+129
-33
lines changed

6 files changed

+129
-33
lines changed

pages/flashbar/persistence.page.tsx

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
22
// SPDX-License-Identifier: Apache-2.0
33
import * as React from 'react';
4-
import { useState } from 'react';
4+
import { useContext, useState } from 'react';
55

66
import Button from '~components/button';
77
import Flashbar, { FlashbarProps } from '~components/flashbar';
88
import { setPersistenceFunctionsForTesting } from '~components/internal/persistence';
99
import SpaceBetween from '~components/space-between';
1010
import Toggle from '~components/toggle';
1111

12+
import AppContext, { AppContextType } from '../app/app-context';
1213
import ScreenshotArea from '../utils/screenshot-area';
1314

1415
const params = new URLSearchParams(window.location.hash.split('?')[1] || '');
@@ -22,7 +23,10 @@ setPersistenceFunctionsForTesting({
2223
},
2324
});
2425

26+
type PageContext = React.Context<AppContextType<{ stackItems: boolean }>>;
27+
2528
export default function FlashbarTest() {
29+
const { urlParams, setUrlParams } = useContext(AppContext as PageContext);
2630
const [items, setItems] = useState<FlashbarProps.MessageDefinition[]>([
2731
{
2832
type: 'success',
@@ -63,7 +67,7 @@ export default function FlashbarTest() {
6367
},
6468
},
6569
]);
66-
const [stackItems, setStackItems] = useState(params.get('stackItems') === 'true');
70+
const stackItems = urlParams.stackItems ?? false;
6771

6872
const addFlashItem = (withPersistence: boolean) => {
6973
const id = `message_${Date.now()}`;
@@ -100,11 +104,7 @@ export default function FlashbarTest() {
100104
data-id="stack-items"
101105
checked={stackItems}
102106
onChange={({ detail }) => {
103-
setStackItems(detail.checked);
104-
const url = new URL(window.location.href);
105-
const params = new URLSearchParams(url.hash.split('?')[1] || '');
106-
params.set('stackItems', detail.checked.toString());
107-
window.history.replaceState({}, '', `${url.pathname}${url.hash.split('?')[0]}?${params}`);
107+
setUrlParams({ stackItems: detail.checked });
108108
}}
109109
>
110110
Stack items

src/alert/__tests__/persistence-unit.test.tsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ describe('Alert Persistence', () => {
3535
renderAlert({ persistenceConfig, dismissible: true });
3636

3737
await waitFor(() => {
38-
expect(mockRetrieveAlertDismiss).toHaveBeenCalledWith(persistenceConfig);
38+
expect(mockRetrieveAlertDismiss).toHaveBeenCalledWith({ uniqueKey: 'test-alert' });
3939
});
4040
});
4141

@@ -88,7 +88,7 @@ describe('Alert Persistence', () => {
8888

8989
getWrapper()!.findDismissButton()!.click();
9090

91-
expect(mockPersistAlertDismiss).toHaveBeenCalledWith(persistenceConfig);
91+
expect(mockPersistAlertDismiss).toHaveBeenCalledWith({ uniqueKey: 'test-alert' });
9292
expect(onDismiss).toHaveBeenCalled();
9393
});
9494

@@ -123,7 +123,10 @@ describe('Alert Persistence', () => {
123123

124124
getWrapper()!.findDismissButton()!.click();
125125

126-
expect(mockPersistAlertDismiss).toHaveBeenCalledWith(persistenceConfig);
126+
expect(mockPersistAlertDismiss).toHaveBeenCalledWith({
127+
uniqueKey: 'test-alert',
128+
crossServicePersistence: true,
129+
});
127130
});
128131
});
129132

src/alert/internal.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -130,16 +130,17 @@ const InternalAlert = React.forwardRef(
130130
);
131131

132132
useEffect(() => {
133-
if (persistenceConfig?.uniqueKey && persistenceConfig.uniqueKey.trim()) {
133+
if (persistenceConfig?.uniqueKey) {
134134
retrieveAlertDismiss(persistenceConfig).then(dismissed => {
135135
setIsPersistentlyDismissed(!!dismissed);
136136
});
137137
}
138-
}, [persistenceConfig]);
138+
// eslint-disable-next-line react-hooks/exhaustive-deps
139+
}, [persistenceConfig?.uniqueKey]);
139140

140141
const dismiss = () => {
141142
fireNonCancelableEvent(onDismiss);
142-
if (persistenceConfig && persistenceConfig.uniqueKey && persistenceConfig.uniqueKey.trim()) {
143+
if (persistenceConfig && persistenceConfig.uniqueKey) {
143144
persistAlertDismiss(persistenceConfig);
144145
}
145146
};

src/flashbar/__tests__/persistence-unit.test.tsx

Lines changed: 85 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,9 +62,15 @@ describe('Flashbar persistence', () => {
6262
});
6363

6464
it('handles mixed persistent and non-persistent items', async () => {
65-
mockRetrieveFlashbarDismiss
66-
.mockResolvedValueOnce(true) // key-0: hidden
67-
.mockResolvedValueOnce(false); // key-2: visible
65+
mockRetrieveFlashbarDismiss.mockImplementation(config => {
66+
if (config.uniqueKey === 'key-0') {
67+
return Promise.resolve(true);
68+
}
69+
if (config.uniqueKey === 'key-2') {
70+
return Promise.resolve(false);
71+
}
72+
throw new Error(`Unknown key ${config.uniqueKey}`);
73+
});
6874

6975
const items = createItems([true, false, true]);
7076

@@ -196,5 +202,81 @@ describe('Flashbar persistence', () => {
196202
expect(mockRetrieveFlashbarDismiss).toHaveBeenLastCalledWith({ uniqueKey: 'new-key' });
197203
});
198204
});
205+
206+
it('checks persistence only for newly added items when start with empty array', async () => {
207+
mockRetrieveFlashbarDismiss.mockResolvedValue(false);
208+
209+
// Start with empty array
210+
const { rerender } = render(<Flashbar items={[]} />);
211+
212+
expect(mockRetrieveFlashbarDismiss).not.toHaveBeenCalled();
213+
214+
// Add one item, check that mockRetrieveFlashbarDismiss was called with that item id
215+
const items = createItems([true]);
216+
rerender(<Flashbar items={items} />);
217+
218+
await waitFor(() => {
219+
expect(mockRetrieveFlashbarDismiss).toHaveBeenCalledTimes(1);
220+
expect(mockRetrieveFlashbarDismiss).toHaveBeenCalledWith({ uniqueKey: 'key-0' });
221+
});
222+
223+
mockRetrieveFlashbarDismiss.mockClear();
224+
225+
// Add one more item, check that mockRetrieveFlashbarDismiss was called with only the 2nd item id
226+
const secondItem = { ...createItems([true])[0], id: 'item-1', persistenceConfig: { uniqueKey: 'key-1' } };
227+
rerender(<Flashbar items={[...items, secondItem]} />);
228+
229+
await waitFor(() => {
230+
expect(mockRetrieveFlashbarDismiss).toHaveBeenCalledTimes(1);
231+
expect(mockRetrieveFlashbarDismiss).toHaveBeenCalledWith({ uniqueKey: 'key-1' });
232+
});
233+
});
234+
235+
it('handles concurrent async calls without race conditions', async () => {
236+
let resolveFirst: (value: boolean) => void;
237+
let resolveSecond: (value: boolean) => void;
238+
239+
const firstPromise = new Promise<boolean>(resolve => {
240+
resolveFirst = resolve;
241+
});
242+
const secondPromise = new Promise<boolean>(resolve => {
243+
resolveSecond = resolve;
244+
});
245+
246+
mockRetrieveFlashbarDismiss.mockImplementation(config => {
247+
if (config.uniqueKey === 'key-0') {
248+
return firstPromise;
249+
}
250+
if (config.uniqueKey === 'key-2') {
251+
return secondPromise;
252+
}
253+
throw new Error(`Unknown key ${config.uniqueKey}`);
254+
});
255+
256+
const initialItems = createItems([true, false]); // Create item 0 with persistence, item 1 without persistence
257+
const { rerender, container } = render(<Flashbar items={initialItems} />);
258+
259+
// Add third item while first item still loading
260+
const updatedItems = [
261+
...initialItems,
262+
{ ...createItems([true])[0], content: 'Item 2', id: 'item-second', persistenceConfig: { uniqueKey: 'key-2' } },
263+
];
264+
rerender(<Flashbar items={updatedItems} />);
265+
266+
// Resolve second call first (race condition scenario)
267+
resolveSecond!(false); // Item 2 should be visible
268+
await waitFor(() => {});
269+
270+
// Then resolve first call
271+
resolveFirst!(true); // Item 0 should be hidden
272+
273+
const wrapper = createWrapper(container).findFlashbar()!;
274+
await waitFor(() => {
275+
const items = wrapper.findItems();
276+
expect(items).toHaveLength(2);
277+
expect(items[0].findContent()!.getElement()).toHaveTextContent('Item 1');
278+
expect(items[1].findContent()!.getElement()).toHaveTextContent('Item 2');
279+
});
280+
});
199281
});
200282
});

src/flashbar/common.tsx

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ export function useFlashbar({
128128

129129
const handleFlashDismissed = (dismissedId?: string, persistenceConfig?: FlashbarProps.PersistenceConfig) => {
130130
handleFlashDismissedInternal(dismissedId, items, ref.current, flashRefs.current);
131-
if (persistenceConfig?.uniqueKey && persistenceConfig.uniqueKey.trim()) {
131+
if (persistenceConfig?.uniqueKey) {
132132
persistFlashbarDismiss(persistenceConfig);
133133
}
134134
};
@@ -147,8 +147,8 @@ export function useFlashbar({
147147

148148
// Hook for managing flashbar items visibility with persistence
149149
export function useFlashbarVisibility(items: ReadonlyArray<FlashbarProps.MessageDefinition>) {
150-
const [checkedPersistenceKeys, setCheckedPersistenceKeys] = useState<Set<string>>(new Set());
151-
const [persistentItemsVisibility, setPersistentItemsVisibility] = useState<Map<string, boolean>>(new Map());
150+
const [checkedPersistenceKeys, setCheckedPersistenceKeys] = useState<Set<string>>(() => new Set());
151+
const [persistentItemsVisibility, setPersistentItemsVisibility] = useState<Map<string, boolean>>(() => new Map());
152152

153153
const visibleItems = useMemo(() => {
154154
return items.filter(
@@ -167,19 +167,27 @@ export function useFlashbarVisibility(items: ReadonlyArray<FlashbarProps.Message
167167
}
168168

169169
const checkNewPersistentItems = async () => {
170-
const newVisibility = new Map(persistentItemsVisibility);
171-
const newKeys = new Set(checkedPersistenceKeys);
172-
173-
await Promise.all(
170+
const results = await Promise.all(
174171
newPersistentItems.map(async item => {
175172
const isDismissed = await retrieveFlashbarDismiss(item.persistenceConfig!);
176-
newVisibility.set(item.persistenceConfig!.uniqueKey, !isDismissed);
177-
newKeys.add(item.persistenceConfig!.uniqueKey);
173+
return {
174+
key: item.persistenceConfig!.uniqueKey,
175+
visible: !isDismissed,
176+
};
178177
})
179178
);
180179

181-
setPersistentItemsVisibility(newVisibility);
182-
setCheckedPersistenceKeys(newKeys);
180+
setPersistentItemsVisibility(prev => {
181+
const updated = new Map(prev);
182+
results.forEach(({ key, visible }) => updated.set(key, visible));
183+
return updated;
184+
});
185+
186+
setCheckedPersistenceKeys(prev => {
187+
const updated = new Set(prev);
188+
results.forEach(({ key }) => updated.add(key));
189+
return updated;
190+
});
183191
};
184192

185193
checkNewPersistentItems();

src/internal/persistence/index.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,32 +27,34 @@ export function setPersistenceFunctionsForTesting(functions: PersistenceFunction
2727
}
2828
}
2929

30+
// eslint-disable-next-line require-await
3031
export let persistFlashbarDismiss = async function (
3132
// eslint-disable-next-line @typescript-eslint/no-unused-vars
3233
persistenceConfig: FlashbarProps.PersistenceConfig
3334
): Promise<void> {
34-
// No-op
35+
return Promise.resolve();
3536
};
3637

38+
// eslint-disable-next-line require-await
3739
export let retrieveFlashbarDismiss = async function (
3840
// eslint-disable-next-line @typescript-eslint/no-unused-vars
3941
persistenceConfig: FlashbarProps.PersistenceConfig
4042
): Promise<boolean> {
41-
const result = await new Promise<boolean>(resolve => resolve(false));
42-
return result;
43+
return Promise.resolve(false);
4344
};
4445

46+
// eslint-disable-next-line require-await
4547
export let persistAlertDismiss = async function (
4648
// eslint-disable-next-line @typescript-eslint/no-unused-vars
4749
persistenceConfig: AlertProps.PersistenceConfig
4850
): Promise<void> {
49-
// No-op
51+
return Promise.resolve();
5052
};
5153

54+
// eslint-disable-next-line require-await
5255
export let retrieveAlertDismiss = async function (
5356
// eslint-disable-next-line @typescript-eslint/no-unused-vars
5457
persistenceConfig: AlertProps.PersistenceConfig
5558
): Promise<boolean> {
56-
const result = await new Promise<boolean>(resolve => resolve(false));
57-
return result;
59+
return Promise.resolve(false);
5860
};

0 commit comments

Comments
 (0)