-
Notifications
You must be signed in to change notification settings - Fork 162
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Add alert content replacement runtime API
- Loading branch information
1 parent
bce2ba0
commit 5c61041
Showing
10 changed files
with
935 additions
and
31 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,77 @@ | ||
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. | ||
// SPDX-License-Identifier: Apache-2.0 | ||
import React, { useState } from 'react'; | ||
import { render, unmountComponentAtNode } from 'react-dom'; | ||
|
||
import { Checkbox, TextContent } from '~components'; | ||
import Alert, { AlertProps } from '~components/alert'; | ||
import Button from '~components/button'; | ||
import awsuiPlugins from '~components/internal/plugins'; | ||
|
||
import createPermutations from '../utils/permutations'; | ||
import PermutationsView from '../utils/permutations-view'; | ||
import ScreenshotArea from '../utils/screenshot-area'; | ||
|
||
awsuiPlugins.alertContent.registerContent({ | ||
id: 'awsui/alert-test-action', | ||
mountContent: (container, context) => { | ||
if (context.type === 'error' && context.contentRef.current?.textContent?.match('Access denied')) { | ||
render( | ||
<div> | ||
Access was denied | ||
<TextContent> | ||
<code>Some more details</code> | ||
</TextContent> | ||
</div>, | ||
container | ||
); | ||
return true; | ||
} | ||
return null; | ||
}, | ||
mountHeader: (container, context) => { | ||
if (context.type === 'error' && context.contentRef.current?.textContent?.match('Access denied')) { | ||
container.replaceChildren(); | ||
return false; | ||
} | ||
return null; | ||
}, | ||
unmountContent: container => unmountComponentAtNode(container), | ||
unmountHeader: () => {}, | ||
}); | ||
|
||
/* eslint-disable react/jsx-key */ | ||
const permutations = createPermutations<AlertProps>([ | ||
{ | ||
header: [null, 'Alert'], | ||
children: ['Content', 'There was an error: Access denied because of XYZ'], | ||
type: ['success', 'error'], | ||
action: [null, <Button>Action</Button>], | ||
}, | ||
]); | ||
/* eslint-enable react/jsx-key */ | ||
|
||
export default function () { | ||
const [loading, setLoading] = useState(true); | ||
return ( | ||
<> | ||
<h1>Alert runtime actions</h1> | ||
<Checkbox onChange={e => setLoading(e.detail.checked)} checked={loading}> | ||
Loading | ||
</Checkbox> | ||
<ScreenshotArea> | ||
<PermutationsView | ||
permutations={permutations} | ||
render={permutation => ( | ||
<> | ||
<Alert statusIconAriaLabel={permutation.type} dismissAriaLabel="Dismiss" {...permutation} /> | ||
<Alert statusIconAriaLabel={permutation.type} dismissAriaLabel="Dismiss" {...permutation}> | ||
{loading ? 'Loading' : permutation.children} | ||
</Alert> | ||
</> | ||
)} | ||
/> | ||
</ScreenshotArea> | ||
</> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,275 @@ | ||
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. | ||
// SPDX-License-Identifier: Apache-2.0 | ||
import * as React from 'react'; | ||
import { act, render, screen } from '@testing-library/react'; | ||
|
||
import Alert from '../../../lib/components/alert'; | ||
import Button from '../../../lib/components/button'; | ||
import awsuiPlugins from '../../../lib/components/internal/plugins'; | ||
import { awsuiPluginsInternal } from '../../../lib/components/internal/plugins/api'; | ||
import { AlertFlashContentConfig } from '../../../lib/components/internal/plugins/controllers/alert-flash-content'; | ||
import { AlertWrapper } from '../../../lib/components/test-utils/dom'; | ||
|
||
const pause = (timeout: number) => new Promise(resolve => setTimeout(resolve, timeout)); | ||
|
||
const defaultContent: AlertFlashContentConfig = { | ||
id: 'test-content', | ||
mountContent: container => { | ||
const content = document.createElement('div'); | ||
content.append('New content'); | ||
content.dataset.testid = 'test-content'; | ||
container.replaceChildren(content); | ||
return true; | ||
}, | ||
mountHeader: container => { | ||
const content = document.createElement('div'); | ||
content.append('New header'); | ||
content.dataset.testid = 'test-header'; | ||
container.replaceChildren(content); | ||
return true; | ||
}, | ||
unmountContent: jest.fn(), | ||
unmountHeader: jest.fn(), | ||
}; | ||
|
||
function delay(advanceBy = 1) { | ||
const promise = act(() => new Promise(resolve => setTimeout(resolve))); | ||
jest.advanceTimersByTime(advanceBy); | ||
return promise; | ||
} | ||
|
||
beforeEach(() => { | ||
jest.useFakeTimers(); | ||
}); | ||
afterEach(() => { | ||
awsuiPluginsInternal.alertContent.clearRegisteredContent(); | ||
jest.useRealTimers(); | ||
jest.resetAllMocks(); | ||
}); | ||
|
||
test('renders runtime content initially', async () => { | ||
awsuiPlugins.alertContent.registerContent(defaultContent); | ||
const { container } = render(<Alert>Alert content</Alert>); | ||
const alertWrapper = new AlertWrapper(container); | ||
await delay(); | ||
expect(screen.queryByTestId('test-content')).toBeTruthy(); | ||
expect(alertWrapper.findContent().getElement().textContent).toBe('New content'); | ||
}); | ||
|
||
test('renders runtime content asynchronously', async () => { | ||
render(<Alert />); | ||
await delay(); | ||
expect(screen.queryByTestId('test-content')).toBeFalsy(); | ||
awsuiPlugins.alertContent.registerContent(defaultContent); | ||
await delay(); | ||
expect(screen.queryByTestId('test-content')).toBeTruthy(); | ||
}); | ||
|
||
describe.each([true, false])('existing header:%p', existingHeader => { | ||
test('renders runtime header initially', async () => { | ||
awsuiPlugins.alertContent.registerContent(defaultContent); | ||
const { container } = render(<Alert header={existingHeader ? 'Header content' : undefined}>Alert content</Alert>); | ||
const alertWrapper = new AlertWrapper(container); | ||
await delay(); | ||
expect(screen.queryByTestId('test-header')).toBeTruthy(); | ||
expect(alertWrapper.findHeader()!.getElement().textContent).toBe('New header'); | ||
}); | ||
|
||
test('renders runtime header asynchronously', async () => { | ||
const { container } = render(<Alert header={existingHeader ? 'Header content' : undefined}>Alert content</Alert>); | ||
const alertWrapper = new AlertWrapper(container); | ||
await delay(); | ||
expect(screen.queryByTestId('test-header')).toBeFalsy(); | ||
awsuiPlugins.alertContent.registerContent(defaultContent); | ||
await delay(); | ||
expect(screen.queryByTestId('test-header')).toBeTruthy(); | ||
expect(alertWrapper.findHeader()!.getElement().textContent).toBe('New header'); | ||
}); | ||
}); | ||
|
||
test('removes header styling if runtime header is explicitly empty', async () => { | ||
const plugin: AlertFlashContentConfig = { | ||
id: 'test-content', | ||
mountHeader(container) { | ||
container.replaceChildren(); | ||
return false; | ||
}, | ||
}; | ||
awsuiPlugins.alertContent.registerContent(plugin); | ||
const { container } = render(<Alert header="Initial header content" />); | ||
const alertWrapper = new AlertWrapper(container); | ||
expect(alertWrapper.findHeader()).toBeTruthy(); | ||
await delay(); | ||
expect(alertWrapper.findHeader()).toBeFalsy(); | ||
}); | ||
|
||
describe('mountContent arguments', () => { | ||
const mountContent = jest.fn(); | ||
beforeEach(() => { | ||
const plugin: AlertFlashContentConfig = { | ||
id: 'test-content', | ||
mountContent, | ||
}; | ||
awsuiPlugins.alertContent.registerContent(plugin); | ||
}); | ||
test('refs', async () => { | ||
render( | ||
<Alert header="Alert header" action={<Button>Action button</Button>}> | ||
Alert content | ||
</Alert> | ||
); | ||
await delay(); | ||
expect(mountContent.mock.lastCall[1].headerRef.current).toHaveTextContent('Alert header'); | ||
expect(mountContent.mock.lastCall[1].contentRef.current).toHaveTextContent('Alert content'); | ||
expect(mountContent.mock.lastCall[1].actionsRef.current).toHaveTextContent('Action button'); | ||
}); | ||
test('type - default', async () => { | ||
render(<Alert />); | ||
await delay(); | ||
expect(mountContent.mock.lastCall[1].type).toBe('info'); | ||
}); | ||
test('type - custom', async () => { | ||
render(<Alert type="error" />); | ||
await delay(); | ||
expect(mountContent.mock.lastCall[1].type).toBe('error'); | ||
}); | ||
}); | ||
|
||
describe('unmounting', () => { | ||
test('unmounts content and header', async () => { | ||
const plugin: AlertFlashContentConfig = { | ||
id: 'test-content', | ||
mountContent: () => true, | ||
unmountContent: jest.fn(), | ||
unmountHeader: jest.fn(), | ||
}; | ||
awsuiPlugins.alertContent.registerContent(plugin); | ||
const { unmount } = render(<Alert>Alert content</Alert>); | ||
await delay(); | ||
expect(plugin.unmountContent).toBeCalledTimes(0); | ||
expect(plugin.unmountHeader).toBeCalledTimes(0); | ||
unmount(); | ||
await delay(); | ||
expect(plugin.unmountContent).toBeCalledTimes(1); | ||
expect(plugin.unmountHeader).toBeCalledTimes(1); | ||
}); | ||
|
||
test('does not unmount if not rendered', async () => { | ||
const noRender: AlertFlashContentConfig = { | ||
id: 'test-content', | ||
mountContent: () => null, | ||
mountHeader: () => null, | ||
unmountContent: jest.fn(), | ||
unmountHeader: jest.fn(), | ||
}; | ||
awsuiPlugins.alertContent.registerContent(noRender); | ||
const { unmount } = render(<Alert>Alert content</Alert>); | ||
await delay(); | ||
unmount(); | ||
await delay(); | ||
expect(noRender.unmountContent).toBeCalledTimes(0); | ||
expect(noRender.unmountHeader).toBeCalledTimes(0); | ||
}); | ||
}); | ||
|
||
describe('asynchronous rendering', () => { | ||
test('allows asynchronous rendering of content', async () => { | ||
const asyncContent: AlertFlashContentConfig = { | ||
id: 'test-content-async', | ||
mountContent: async container => { | ||
await pause(1000); | ||
const content = document.createElement('div'); | ||
content.append('New content'); | ||
content.dataset.testid = 'test-content-async'; | ||
container.replaceChildren(content); | ||
return true; | ||
}, | ||
}; | ||
awsuiPlugins.alertContent.registerContent(asyncContent); | ||
const { container } = render(<Alert>Alert content</Alert>); | ||
const alertWrapper = new AlertWrapper(container); | ||
await delay(); | ||
expect(screen.queryByTestId('test-content-async')).toBeFalsy(); | ||
expect(alertWrapper.findContent().getElement().textContent).toBe('Alert content'); | ||
await delay(1000); | ||
expect(screen.queryByTestId('test-content-async')).toBeTruthy(); | ||
expect(alertWrapper.findContent().getElement().textContent).toBe('New content'); | ||
}); | ||
|
||
test('cancels asynchronous rendering when unmounting', async () => { | ||
let rendered = false; | ||
const asyncContent: AlertFlashContentConfig = { | ||
id: 'test-content-async', | ||
mountContent: async (container, { signal }) => { | ||
await pause(1000); | ||
if (!signal.aborted) { | ||
rendered = true; | ||
return true; | ||
} | ||
return false; | ||
}, | ||
}; | ||
awsuiPlugins.alertContent.registerContent(asyncContent); | ||
const { unmount } = render(<Alert>Alert content</Alert>); | ||
await delay(500); | ||
unmount(); | ||
await delay(1000); | ||
expect(rendered).toBeFalsy(); | ||
}); | ||
|
||
test('waits for first async plugin before trying next', async () => { | ||
const asyncContent: AlertFlashContentConfig = { | ||
id: 'test-content-async-1', | ||
mountContent: jest.fn(async () => { | ||
await pause(1000); | ||
return null; | ||
}), | ||
}; | ||
const asyncContent2: AlertFlashContentConfig = { | ||
id: 'test-content-async-2', | ||
mountContent: jest.fn(async () => { | ||
await pause(1000); | ||
return true; | ||
}), | ||
mountHeader: jest.fn(), | ||
}; | ||
const asyncContent3: AlertFlashContentConfig = { | ||
id: 'test-content-async-3', | ||
mountContent: jest.fn(), | ||
}; | ||
awsuiPlugins.alertContent.registerContent(asyncContent); | ||
awsuiPlugins.alertContent.registerContent(asyncContent2); | ||
render(<Alert>Alert content</Alert>); | ||
await delay(); | ||
expect(asyncContent.mountContent).toBeCalled(); | ||
expect(asyncContent2.mountContent).not.toBeCalled(); | ||
expect(asyncContent2.mountHeader).not.toBeCalled(); | ||
awsuiPlugins.alertContent.registerContent(asyncContent3); | ||
expect(asyncContent3.mountContent).not.toBeCalled(); | ||
await delay(1000); | ||
expect(asyncContent2.mountContent).toBeCalled(); | ||
expect(asyncContent2.mountHeader).toBeCalled(); | ||
expect(asyncContent3.mountContent).not.toBeCalled(); | ||
await delay(1000); | ||
expect(asyncContent3.mountContent).not.toBeCalled(); | ||
}); | ||
}); | ||
|
||
test('only uses first plugin to return a result', async () => { | ||
const first: AlertFlashContentConfig = { | ||
id: 'test-content-1', | ||
mountContent: jest.fn(() => false), | ||
}; | ||
const second: AlertFlashContentConfig = { | ||
id: 'test-content-2', | ||
mountContent: jest.fn(), | ||
mountHeader: jest.fn(), | ||
}; | ||
awsuiPlugins.alertContent.registerContent(first); | ||
awsuiPlugins.alertContent.registerContent(second); | ||
render(<Alert />); | ||
await delay(); | ||
expect(first.mountContent).toBeCalled(); | ||
expect(second.mountContent).not.toBeCalled(); | ||
expect(second.mountHeader).not.toBeCalled(); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.