Skip to content

Commit f0eff84

Browse files
author
Yueying Lu
committed
Implement Flashbar and Alert persistence
1 parent 773369f commit f0eff84

File tree

15 files changed

+1027
-28
lines changed

15 files changed

+1027
-28
lines changed

pages/alert/persistence.page.tsx

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
import * as React from 'react';
4+
import { useState } from 'react';
5+
6+
import Alert, { AlertProps } from '~components/alert';
7+
import Button from '~components/button';
8+
import { setPersistenceFunctionsForTesting } from '~components/internal/persistence';
9+
import SpaceBetween from '~components/space-between';
10+
11+
import FocusTarget from '../common/focus-target';
12+
import ScreenshotArea from '../utils/screenshot-area';
13+
14+
const params = new URLSearchParams(window.location.hash.split('?')[1] || '');
15+
setPersistenceFunctionsForTesting({
16+
retrieveAlertDismiss: async (persistenceConfig: AlertProps.PersistenceConfig) => {
17+
const dismissed = Boolean(params.get('dismissedKeys')?.includes(persistenceConfig.uniqueKey));
18+
const result = await new Promise<boolean>(resolve =>
19+
setTimeout(() => resolve(dismissed), Math.min(parseInt(params.get('mockRetrieveDelay') || '0'), 150))
20+
);
21+
return result;
22+
},
23+
});
24+
25+
export default function AlertPersistenceTest() {
26+
const [alerts, setAlerts] = useState<{ id: string; alert: React.ReactElement }[]>([
27+
{
28+
id: 'alert_1',
29+
alert: (
30+
<Alert
31+
type="success"
32+
dismissible={true}
33+
onDismiss={() => setAlerts(alerts => alerts.filter(item => item.id !== 'alert_1'))}
34+
>
35+
Success alert without persistence
36+
</Alert>
37+
),
38+
},
39+
{
40+
id: 'alert_2',
41+
alert: (
42+
<Alert
43+
type="warning"
44+
dismissible={true}
45+
onDismiss={() => setAlerts(alerts => alerts.filter(item => item.id !== 'alert_2'))}
46+
>
47+
Warning alert without persistence
48+
</Alert>
49+
),
50+
},
51+
{
52+
id: 'alert_3',
53+
alert: (
54+
<Alert
55+
type="info"
56+
dismissible={true}
57+
onDismiss={() => setAlerts(alerts => alerts.filter(item => item.id !== 'alert_3'))}
58+
persistenceConfig={{ uniqueKey: 'persistence_1' }}
59+
>
60+
Info alert with persistence with uniqueKey persistence_1
61+
</Alert>
62+
),
63+
},
64+
{
65+
id: 'alert_4',
66+
alert: (
67+
<Alert
68+
type="warning"
69+
dismissible={true}
70+
onDismiss={() => setAlerts(alerts => alerts.filter(item => item.id !== 'alert_4'))}
71+
persistenceConfig={{ uniqueKey: 'persistence_2' }}
72+
>
73+
Warning alert with persistence with uniqueKey persistence_2
74+
</Alert>
75+
),
76+
},
77+
]);
78+
79+
const addAlert = (withPersistence: boolean) => {
80+
const id = `alert_${Date.now()}`;
81+
const newAlert = {
82+
id,
83+
alert: (
84+
<Alert
85+
type="info"
86+
dismissible={true}
87+
onDismiss={() => setAlerts(alerts => alerts.filter(item => item.id !== id))}
88+
{...(withPersistence && { persistenceConfig: { uniqueKey: `new_${id}` } })}
89+
>
90+
New alert {withPersistence ? 'with' : 'without'} persistence
91+
</Alert>
92+
),
93+
};
94+
setAlerts(alerts => [...alerts, newAlert]);
95+
};
96+
97+
return (
98+
<>
99+
<h1>Alert test with Persistence</h1>
100+
<SpaceBetween size="xs">
101+
<div>This page is to test Alert Persistence with retrieval delay (the maximum possible delay is 150ms)</div>
102+
<SpaceBetween direction="horizontal" size="xs">
103+
<Button data-id="add-no-persistence-item" onClick={() => addAlert(false)}>
104+
Add without persistence
105+
</Button>
106+
<Button data-id="add-persistence-item" onClick={() => addAlert(true)}>
107+
Add with persistence
108+
</Button>
109+
</SpaceBetween>
110+
<FocusTarget />
111+
</SpaceBetween>
112+
<ScreenshotArea>
113+
<SpaceBetween size="xs">
114+
{alerts.map(({ id, alert }) => (
115+
<div key={id}>{alert}</div>
116+
))}
117+
</SpaceBetween>
118+
</ScreenshotArea>
119+
</>
120+
);
121+
}
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
import * as React from 'react';
4+
import { useState } from 'react';
5+
6+
import Button from '~components/button';
7+
import Flashbar, { FlashbarProps } from '~components/flashbar';
8+
import { setPersistenceFunctionsForTesting } from '~components/internal/persistence';
9+
import SpaceBetween from '~components/space-between';
10+
import Toggle from '~components/toggle';
11+
12+
import FocusTarget from '../common/focus-target';
13+
import ScreenshotArea from '../utils/screenshot-area';
14+
15+
const params = new URLSearchParams(window.location.hash.split('?')[1] || '');
16+
setPersistenceFunctionsForTesting({
17+
retrieveFlashbarDismiss: async (persistenceConfig: FlashbarProps.PersistenceConfig) => {
18+
const dismissed = Boolean(params.get('dismissedKeys')?.includes(persistenceConfig.uniqueKey));
19+
const result = await new Promise<boolean>(resolve =>
20+
setTimeout(() => resolve(dismissed), Math.min(parseInt(params.get('mockRetrieveDelay') || '0'), 150))
21+
);
22+
return result;
23+
},
24+
});
25+
26+
export default function FlashbarTest() {
27+
const [items, setItems] = useState<FlashbarProps.MessageDefinition[]>([
28+
{
29+
type: 'success',
30+
dismissible: true,
31+
dismissLabel: 'Dismiss message',
32+
content: 'Success flash message without persistence',
33+
id: 'message_1',
34+
onDismiss: () => setItems(items => items.filter(item => item.id !== 'message_1')),
35+
},
36+
{
37+
type: 'warning',
38+
dismissible: true,
39+
dismissLabel: 'Dismiss message',
40+
content: 'Warning flash message without persistence',
41+
id: 'message_2',
42+
onDismiss: () => setItems(items => items.filter(item => item.id !== 'message_2')),
43+
},
44+
{
45+
type: 'info',
46+
dismissible: true,
47+
dismissLabel: 'Dismiss message',
48+
content: 'Notification flash message 1 with persistence',
49+
id: 'message_3',
50+
onDismiss: () => setItems(items => items.filter(item => item.id !== 'message_3')),
51+
persistenceConfig: {
52+
uniqueKey: 'persistence_1',
53+
},
54+
},
55+
{
56+
type: 'in-progress',
57+
dismissible: true,
58+
dismissLabel: 'Dismiss message',
59+
content: 'Notification flash message 2 with persistence',
60+
id: 'message_4',
61+
onDismiss: () => setItems(items => items.filter(item => item.id !== 'message_4')),
62+
persistenceConfig: {
63+
uniqueKey: 'persistence_2',
64+
},
65+
},
66+
]);
67+
const [stackItems, setStackItems] = useState(params.get('stackItems') === 'true');
68+
69+
const addFlashItem = (withPersistence: boolean) => {
70+
const id = `message_${Date.now()}`;
71+
const newItem: FlashbarProps.MessageDefinition = {
72+
type: 'info',
73+
dismissible: true,
74+
dismissLabel: 'Dismiss message',
75+
content: `New flash message ${withPersistence ? 'with' : 'without'} persistence`,
76+
id,
77+
ariaRole: 'status',
78+
onDismiss: () => setItems(items => items.filter(item => item.id !== id)),
79+
...(withPersistence && {
80+
persistenceConfig: {
81+
uniqueKey: `new_${id}`,
82+
},
83+
}),
84+
};
85+
setItems(items => [...items, newItem]);
86+
};
87+
88+
return (
89+
<>
90+
<h1>Flashbar test with Persistence</h1>
91+
<SpaceBetween size="xs">
92+
<div>This page is to test Persistence with retrival delay (the maximum possible delay is 150ms)</div>
93+
<SpaceBetween direction="horizontal" size="xs">
94+
<Button data-id="add-no-persistence-item" onClick={() => addFlashItem(false)}>
95+
Add without persistence
96+
</Button>
97+
<Button data-id="add-persistence-item" onClick={() => addFlashItem(true)}>
98+
Add with persistence
99+
</Button>
100+
<Toggle
101+
data-id="stack-items"
102+
checked={stackItems}
103+
onChange={({ detail }) => {
104+
setStackItems(detail.checked);
105+
const url = new URL(window.location.href);
106+
const params = new URLSearchParams(url.hash.split('?')[1] || '');
107+
params.set('stackItems', detail.checked.toString());
108+
window.history.replaceState({}, '', `${url.pathname}${url.hash.split('?')[0]}?${params}`);
109+
}}
110+
>
111+
Stack items
112+
</Toggle>
113+
</SpaceBetween>
114+
<FocusTarget />
115+
</SpaceBetween>
116+
<ScreenshotArea>
117+
<Flashbar items={items} stackItems={stackItems} />
118+
</ScreenshotArea>
119+
</>
120+
);
121+
}

src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,34 @@ use the \`id\` attribute, consider setting it on a parent element instead.",
172172
"optional": true,
173173
"type": "string",
174174
},
175+
{
176+
"description": "Config to persist dismiss state for dismissable Alert
177+
persistenceConfig contains:
178+
* \`uniqueKey\` (string) - This key to store the persistence state, it must be unique across your console.
179+
* \`crossServicePersistence\` (boolean) - (Optional) If true, the persistence state will be shared across AWS services.",
180+
"inlineType": {
181+
"name": "AlertProps.PersistenceConfig",
182+
"properties": [
183+
{
184+
"name": "crossServicePersistence",
185+
"optional": true,
186+
"type": "boolean",
187+
},
188+
{
189+
"name": "uniqueKey",
190+
"optional": false,
191+
"type": "string",
192+
},
193+
],
194+
"type": "object",
195+
},
196+
"name": "persistenceConfig",
197+
"optional": true,
198+
"systemTags": [
199+
"console",
200+
],
201+
"type": "AlertProps.PersistenceConfig",
202+
},
175203
{
176204
"deprecatedTag": "Use the label properties inside \`i18nStrings\` instead.
177205
If the label is assigned via the \`i18nStrings\` property, this label will be ignored.",
@@ -12066,7 +12094,9 @@ use the \`id\` attribute, consider setting it on a parent element instead.",
1206612094
"type": "string",
1206712095
},
1206812096
{
12069-
"analyticsTag": "",
12097+
"analyticsTag": "* \`persistenceConfig\` (FlashbarProps.PersistenceConfig) - Config to persist dismiss state for dismissable Flashbar item.
12098+
* \`uniqueKey\` (string) - This key to store the persistence state, it must be unique across your console.
12099+
* \`crossServicePersistence\` (boolean) - If true, the persistence state will be shared across AWS services.",
1207012100
"description": "Specifies flash messages that appear in the same order that they are listed.
1207112101
The value is an array of flash message definition objects.
1207212102

@@ -12098,6 +12128,9 @@ If the \`action\` property is set, this property is ignored. **Deprecated**, rep
1209812128
* \`suppressFlowMetricEvents\` - Prevent this item from generating events related to flow metrics.",
1209912129
"name": "items",
1210012130
"optional": false,
12131+
"systemTags": [
12132+
"console",
12133+
],
1210112134
"type": "ReadonlyArray<FlashbarProps.MessageDefinition>",
1210212135
},
1210312136
{
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
import { BasePageObject } from '@cloudscape-design/browser-test-tools/page-objects';
5+
import useBrowser from '@cloudscape-design/browser-test-tools/use-browser';
6+
7+
import createWrapper from '../../../lib/components/test-utils/selectors';
8+
9+
const wrapper = createWrapper();
10+
11+
class AlertPersistencePage extends BasePageObject {
12+
countAlerts() {
13+
return this.getElementsCount(wrapper.findAlert().toSelector());
14+
}
15+
16+
async dismissAlert(index: number) {
17+
await this.click(wrapper.findAlert(`[data-testid="alert-${index}"]`).findDismissButton().toSelector());
18+
}
19+
}
20+
21+
const setupTest = (
22+
params: { mockRetrieveDelay?: number; dismissedKeys?: string } = {},
23+
testFn: (page: AlertPersistencePage) => Promise<void>
24+
) => {
25+
return useBrowser(async browser => {
26+
const page = new AlertPersistencePage(browser);
27+
const urlParams = new URLSearchParams();
28+
if (params.mockRetrieveDelay !== undefined) {
29+
urlParams.set('mockRetrieveDelay', params.mockRetrieveDelay.toString());
30+
}
31+
if (params.dismissedKeys !== undefined) {
32+
urlParams.set('dismissedKeys', params.dismissedKeys);
33+
}
34+
const url = `#/light/alert/persistence${urlParams.toString() ? '?' + urlParams.toString() : ''}`;
35+
await browser.url(url);
36+
await page.waitForVisible(wrapper.findAlert().toSelector());
37+
await testFn(page);
38+
});
39+
};
40+
41+
describe('Alert Persistence Integration', () => {
42+
test(
43+
'showing 2 non-persisted items initially, then all 4 items after persistence delay',
44+
setupTest({ mockRetrieveDelay: 150 }, async page => {
45+
await expect(page.countAlerts()).resolves.toBeGreaterThanOrEqual(2);
46+
await new Promise(resolve => setTimeout(resolve, 150));
47+
await expect(page.countAlerts()).resolves.toBe(4);
48+
})
49+
);
50+
51+
test(
52+
'adding new persistent item and showing 5 total items',
53+
setupTest({ mockRetrieveDelay: 150 }, async page => {
54+
await expect(page.countAlerts()).resolves.toBeGreaterThanOrEqual(2);
55+
await page.click('[data-id="add-persistence-item"]');
56+
await new Promise(resolve => setTimeout(resolve, 150));
57+
await expect(page.countAlerts()).resolves.toBe(5);
58+
})
59+
);
60+
61+
test(
62+
'dismissing persistent alert and reducing count from 4 to 3',
63+
setupTest({ mockRetrieveDelay: 150 }, async page => {
64+
await new Promise(resolve => setTimeout(resolve, 150));
65+
await expect(page.countAlerts()).resolves.toBe(4);
66+
await page.dismissAlert(3);
67+
await expect(page.countAlerts()).resolves.toBe(3);
68+
})
69+
);
70+
71+
describe('dismissed items', () => {
72+
test(
73+
'showing 3 items when persistence_1 is dismissed',
74+
setupTest({ mockRetrieveDelay: 150, dismissedKeys: 'persistence_1' }, async page => {
75+
await expect(page.countAlerts()).resolves.toBeGreaterThanOrEqual(2);
76+
await new Promise(resolve => setTimeout(resolve, 150));
77+
await expect(page.countAlerts()).resolves.toBe(3);
78+
})
79+
);
80+
});
81+
});

0 commit comments

Comments
 (0)