1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-01-20 00:08:02 +01:00

fix: announce navigation to screen readers (#911)

* refactor: unify page titles

* refactor: update page title on navigation

* refactor: add AnnouncerContext to test contexts

* fix: announce navigation to screen readers
This commit is contained in:
olav 2022-04-27 09:14:44 +02:00 committed by GitHub
parent 06232a5522
commit f7266cde10
22 changed files with 913 additions and 794 deletions

View File

@ -8,7 +8,7 @@
<meta name="cdnPrefix" content="::cdnPrefix::" /> <meta name="cdnPrefix" content="::cdnPrefix::" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="description" content="unleash" /> <meta name="description" content="unleash" />
<title>Unleash - Enterprise ready feature toggles</title> <title>Unleash</title>
<link rel="preconnect" href="https://fonts.googleapis.com" /> <link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin /> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link <link

View File

@ -1,8 +1,8 @@
import { render } from 'utils/testRenderer'; import { render } from 'utils/testRenderer';
import { AnnouncerProvider } from 'component/common/Announcer/AnnouncerProvider/AnnouncerProvider';
import { AnnouncerContext } from 'component/common/Announcer/AnnouncerContext/AnnouncerContext'; import { AnnouncerContext } from 'component/common/Announcer/AnnouncerContext/AnnouncerContext';
import { useContext, useEffect } from 'react'; import { useContext, useEffect } from 'react';
import { screen } from '@testing-library/react'; import { screen } from '@testing-library/react';
import { ANNOUNCER_ELEMENT_TEST_ID } from 'utils/testIds';
test('AnnouncerContext', async () => { test('AnnouncerContext', async () => {
const TestComponent = () => { const TestComponent = () => {
@ -16,12 +16,9 @@ test('AnnouncerContext', async () => {
return null; return null;
}; };
render( render(<TestComponent />);
<AnnouncerProvider>
<TestComponent />
</AnnouncerProvider>
);
expect(screen.getByRole('status')).not.toHaveTextContent('Foo'); const el = screen.getByTestId(ANNOUNCER_ELEMENT_TEST_ID);
expect(screen.getByRole('status')).toHaveTextContent('Bar'); expect(el).not.toHaveTextContent('Foo');
expect(el).toHaveTextContent('Bar');
}); });

View File

@ -1,5 +1,6 @@
import React, { ReactElement } from 'react'; import React, { ReactElement } from 'react';
import { useStyles } from 'component/common/Announcer/AnnouncerElement/AnnouncerElement.styles'; import { useStyles } from 'component/common/Announcer/AnnouncerElement/AnnouncerElement.styles';
import { ANNOUNCER_ELEMENT_TEST_ID } from 'utils/testIds';
interface IAnnouncerElementProps { interface IAnnouncerElementProps {
announcement?: string; announcement?: string;
@ -16,6 +17,7 @@ export const AnnouncerElement = ({
aria-live="polite" aria-live="polite"
aria-atomic aria-atomic
className={styles.container} className={styles.container}
data-testid={ANNOUNCER_ELEMENT_TEST_ID}
> >
{announcement} {announcement}
</div> </div>

View File

@ -6,6 +6,7 @@ import { Typography } from '@material-ui/core';
import ConditionallyRender from '../ConditionallyRender/ConditionallyRender'; import ConditionallyRender from '../ConditionallyRender/ConditionallyRender';
import { useStyles } from './styles'; import { useStyles } from './styles';
import { usePageTitle } from 'hooks/usePageTitle';
const HeaderTitle = ({ const HeaderTitle = ({
title, title,
@ -18,6 +19,8 @@ const HeaderTitle = ({
const styles = useStyles(); const styles = useStyles();
const headerClasses = classnames({ skeleton: loading }); const headerClasses = classnames({ skeleton: loading });
usePageTitle(title);
return ( return (
<div className={styles.headerTitleContainer}> <div className={styles.headerTitleContainer}>
<div className={headerClasses} data-loading> <div className={headerClasses} data-loading>

View File

@ -96,12 +96,12 @@ const FeatureToggleList = ({
}; };
const searchResultsHeader = filter.query const searchResultsHeader = filter.query
? `(${features.length} matches)` ? ` (${features.length} matches)`
: ''; : '';
const headerTitle = archive const headerTitle = archive
? `Archived Features ${searchResultsHeader}` ? `Archived feature toggles${searchResultsHeader}`
: `Features ${searchResultsHeader}`; : `Feature toggles${searchResultsHeader}`;
return ( return (
<div className={styles.featureContainer}> <div className={styles.featureContainer}>

View File

@ -6,6 +6,7 @@ import renderer from 'react-test-renderer';
import theme from 'themes/mainTheme'; import theme from 'themes/mainTheme';
import { CREATE_FEATURE } from 'component/providers/AccessProvider/permissions'; import { CREATE_FEATURE } from 'component/providers/AccessProvider/permissions';
import AccessProvider from 'component/providers/AccessProvider/AccessProvider'; import AccessProvider from 'component/providers/AccessProvider/AccessProvider';
import { AnnouncerProvider } from 'component/common/Announcer/AnnouncerProvider/AnnouncerProvider';
jest.mock('./FeatureToggleListItem/FeatureToggleListItem', () => ({ jest.mock('./FeatureToggleListItem/FeatureToggleListItem', () => ({
__esModule: true, __esModule: true,
@ -24,18 +25,22 @@ test('renders correctly with one feature', () => {
const tree = renderer.create( const tree = renderer.create(
<MemoryRouter> <MemoryRouter>
<ThemeProvider theme={theme}> <ThemeProvider theme={theme}>
<AccessProvider permissions={[{ permission: CREATE_FEATURE }]}> <AnnouncerProvider>
<FeatureToggleList <AccessProvider
updateSetting={jest.fn()} permissions={[{ permission: CREATE_FEATURE }]}
filter={{}} >
setFilter={jest.fn()} <FeatureToggleList
sort={{}} updateSetting={jest.fn()}
setSort={jest.fn()} filter={{}}
features={features} setFilter={jest.fn()}
fetcher={jest.fn()} sort={{}}
flags={{}} setSort={jest.fn()}
/> features={features}
</AccessProvider> fetcher={jest.fn()}
flags={{}}
/>
</AccessProvider>
</AnnouncerProvider>
</ThemeProvider> </ThemeProvider>
</MemoryRouter> </MemoryRouter>
); );
@ -52,17 +57,21 @@ test('renders correctly with one feature without permissions', () => {
const tree = renderer.create( const tree = renderer.create(
<MemoryRouter> <MemoryRouter>
<ThemeProvider theme={theme}> <ThemeProvider theme={theme}>
<AccessProvider permissions={[{ permission: CREATE_FEATURE }]}> <AnnouncerProvider>
<FeatureToggleList <AccessProvider
filter={{}} permissions={[{ permission: CREATE_FEATURE }]}
setFilter={jest.fn()} >
sort={{}} <FeatureToggleList
setSort={jest.fn()} filter={{}}
features={features} setFilter={jest.fn()}
fetcher={jest.fn()} sort={{}}
flags={{}} setSort={jest.fn()}
/> features={features}
</AccessProvider> fetcher={jest.fn()}
flags={{}}
/>
</AccessProvider>
</AnnouncerProvider>
</ThemeProvider> </ThemeProvider>
</MemoryRouter> </MemoryRouter>
); );

View File

@ -1,348 +1,368 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders correctly with one feature 1`] = ` exports[`renders correctly with one feature 1`] = `
<div> Array [
<div <div>
className="makeStyles-searchBarContainer-3"
>
<form
className="makeStyles-container-6"
role="search"
>
<div
className="makeStyles-search-7 makeStyles-searchBar-4"
>
<svg
aria-hidden={true}
className="MuiSvgIcon-root makeStyles-searchIcon-8"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"
/>
</svg>
<div
className="MuiInputBase-root makeStyles-inputRoot-9"
onClick={[Function]}
onKeyPress={[Function]}
>
<input
aria-label="search"
className="MuiInputBase-input MuiInputBase-inputTypeSearch"
onAnimationStart={[Function]}
onBlur={[Function]}
onChange={[Function]}
onFocus={[Function]}
placeholder="Search..."
type="search"
value=""
/>
</div>
</div>
</form>
<a
href="/archive"
onClick={[Function]}
>
Archive
</a>
</div>
<div
className="MuiPaper-root MuiPaper-elevation1 MuiPaper-rounded"
style={
Object {
"borderRadius": "10px",
"boxShadow": "none",
}
}
>
<div <div
className="makeStyles-headerContainer-10" className="makeStyles-searchBarContainer-3"
> >
<div <form
className="makeStyles-headerTitleContainer-14" className="makeStyles-container-6"
role="search"
> >
<div <div
className="" className="makeStyles-search-7 makeStyles-searchBar-4"
data-loading={true}
> >
<h1 <svg
className="MuiTypography-root makeStyles-headerTitle-15 MuiTypography-h1" aria-hidden={true}
className="MuiSvgIcon-root makeStyles-searchIcon-8"
focusable="false"
viewBox="0 0 24 24"
> >
Features <path
</h1> d="M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"
/>
</svg>
<div
className="MuiInputBase-root makeStyles-inputRoot-9"
onClick={[Function]}
onKeyPress={[Function]}
>
<input
aria-label="search"
className="MuiInputBase-input MuiInputBase-inputTypeSearch"
onAnimationStart={[Function]}
onBlur={[Function]}
onChange={[Function]}
onFocus={[Function]}
placeholder="Search..."
type="search"
value=""
/>
</div>
</div> </div>
</form>
<a
href="/archive"
onClick={[Function]}
>
Archive
</a>
</div>
<div
className="MuiPaper-root MuiPaper-elevation1 MuiPaper-rounded"
style={
Object {
"borderRadius": "10px",
"boxShadow": "none",
}
}
>
<div
className="makeStyles-headerContainer-10"
>
<div <div
className="makeStyles-headerActions-16" className="makeStyles-headerTitleContainer-14"
> >
<div <div
className="makeStyles-actionsContainer-1" className=""
data-loading={true}
>
<h1
className="MuiTypography-root makeStyles-headerTitle-15 MuiTypography-h1"
>
Feature toggles
</h1>
</div>
<div
className="makeStyles-headerActions-16"
> >
<div <div
className="makeStyles-actions-17" className="makeStyles-actionsContainer-1"
> >
<p <div
className="MuiTypography-root MuiTypography-body2" className="makeStyles-actions-17"
data-loading={true}
> >
Sorted by: <p
</p> className="MuiTypography-root MuiTypography-body2"
<button data-loading={true}
aria-controls="sorting" >
aria-haspopup="true" Sorted by:
className="MuiButtonBase-root MuiButton-root MuiButton-text" </p>
data-loading={true} <button
disabled={false} aria-controls="sorting"
id="sorting" aria-haspopup="true"
onBlur={[Function]} className="MuiButtonBase-root MuiButton-root MuiButton-text"
onClick={[Function]} data-loading={true}
onDragLeave={[Function]} disabled={false}
onFocus={[Function]} id="sorting"
onKeyDown={[Function]} onBlur={[Function]}
onKeyUp={[Function]} onClick={[Function]}
onMouseDown={[Function]} onDragLeave={[Function]}
onMouseLeave={[Function]} onFocus={[Function]}
onMouseUp={[Function]} onKeyDown={[Function]}
onTouchEnd={[Function]} onKeyUp={[Function]}
onTouchMove={[Function]} onMouseDown={[Function]}
onTouchStart={[Function]} onMouseLeave={[Function]}
style={ onMouseUp={[Function]}
Object { onTouchEnd={[Function]}
"fontWeight": "normal", onTouchMove={[Function]}
"textTransform": "lowercase", onTouchStart={[Function]}
} style={
} Object {
tabIndex={0} "fontWeight": "normal",
title="Sort by" "textTransform": "lowercase",
type="button" }
> }
<span tabIndex={0}
className="MuiButton-label" title="Sort by"
type="button"
> >
By Name
<span <span
className="MuiButton-endIcon MuiButton-iconSizeMedium" className="MuiButton-label"
> >
By Name
<span <span
aria-hidden={true} className="MuiButton-endIcon MuiButton-iconSizeMedium"
className="material-icons MuiIcon-root"
> >
<svg <span
className="MuiSvgIcon-root" aria-hidden={true}
focusable="false" className="material-icons MuiIcon-root"
role="img"
viewBox="0 0 24 24"
> >
<path <svg
d="M7 10l5 5 5-5z" className="MuiSvgIcon-root"
/> focusable="false"
<title> role="img"
Toggle viewBox="0 0 24 24"
</title> >
</svg> <path
d="M7 10l5 5 5-5z"
/>
<title>
Toggle
</title>
</svg>
</span>
</span> </span>
</span> </span>
</span> </button>
</button> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> <div
<div className="makeStyles-bodyContainer-11"
className="makeStyles-bodyContainer-11"
>
<ul
className="MuiList-root MuiList-padding"
> >
<ListItem <ul
feature={ className="MuiList-root MuiList-padding"
Object { >
"name": "Another", <ListItem
"reviveName": "Another", feature={
Object {
"name": "Another",
"reviveName": "Another",
}
} }
} flags={Object {}}
flags={Object {}} hasAccess={[Function]}
hasAccess={[Function]} />
/> </ul>
</ul> </div>
</div> </div>
</div> </div>,
</div> <div
aria-atomic={true}
aria-live="polite"
className="makeStyles-container-18"
data-testid="ANNOUNCER_ELEMENT_TEST_ID"
role="status"
/>,
]
`; `;
exports[`renders correctly with one feature without permissions 1`] = ` exports[`renders correctly with one feature without permissions 1`] = `
<div> Array [
<div <div>
className="makeStyles-searchBarContainer-3"
>
<form
className="makeStyles-container-6"
role="search"
>
<div
className="makeStyles-search-7 makeStyles-searchBar-4"
>
<svg
aria-hidden={true}
className="MuiSvgIcon-root makeStyles-searchIcon-8"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"
/>
</svg>
<div
className="MuiInputBase-root makeStyles-inputRoot-9"
onClick={[Function]}
onKeyPress={[Function]}
>
<input
aria-label="search"
className="MuiInputBase-input MuiInputBase-inputTypeSearch"
onAnimationStart={[Function]}
onBlur={[Function]}
onChange={[Function]}
onFocus={[Function]}
placeholder="Search..."
type="search"
value=""
/>
</div>
</div>
</form>
<a
href="/archive"
onClick={[Function]}
>
Archive
</a>
</div>
<div
className="MuiPaper-root MuiPaper-elevation1 MuiPaper-rounded"
style={
Object {
"borderRadius": "10px",
"boxShadow": "none",
}
}
>
<div <div
className="makeStyles-headerContainer-10" className="makeStyles-searchBarContainer-3"
> >
<div <form
className="makeStyles-headerTitleContainer-14" className="makeStyles-container-6"
role="search"
> >
<div <div
className="" className="makeStyles-search-7 makeStyles-searchBar-4"
data-loading={true}
> >
<h1 <svg
className="MuiTypography-root makeStyles-headerTitle-15 MuiTypography-h1" aria-hidden={true}
className="MuiSvgIcon-root makeStyles-searchIcon-8"
focusable="false"
viewBox="0 0 24 24"
> >
Features <path
</h1> d="M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"
/>
</svg>
<div
className="MuiInputBase-root makeStyles-inputRoot-9"
onClick={[Function]}
onKeyPress={[Function]}
>
<input
aria-label="search"
className="MuiInputBase-input MuiInputBase-inputTypeSearch"
onAnimationStart={[Function]}
onBlur={[Function]}
onChange={[Function]}
onFocus={[Function]}
placeholder="Search..."
type="search"
value=""
/>
</div>
</div> </div>
</form>
<a
href="/archive"
onClick={[Function]}
>
Archive
</a>
</div>
<div
className="MuiPaper-root MuiPaper-elevation1 MuiPaper-rounded"
style={
Object {
"borderRadius": "10px",
"boxShadow": "none",
}
}
>
<div
className="makeStyles-headerContainer-10"
>
<div <div
className="makeStyles-headerActions-16" className="makeStyles-headerTitleContainer-14"
> >
<div <div
className="makeStyles-actionsContainer-1" className=""
data-loading={true}
>
<h1
className="MuiTypography-root makeStyles-headerTitle-15 MuiTypography-h1"
>
Feature toggles
</h1>
</div>
<div
className="makeStyles-headerActions-16"
> >
<div <div
className="makeStyles-actions-17" className="makeStyles-actionsContainer-1"
> >
<p <div
className="MuiTypography-root MuiTypography-body2" className="makeStyles-actions-17"
data-loading={true}
> >
Sorted by: <p
</p> className="MuiTypography-root MuiTypography-body2"
<button data-loading={true}
aria-controls="sorting" >
aria-haspopup="true" Sorted by:
className="MuiButtonBase-root MuiButton-root MuiButton-text" </p>
data-loading={true} <button
disabled={false} aria-controls="sorting"
id="sorting" aria-haspopup="true"
onBlur={[Function]} className="MuiButtonBase-root MuiButton-root MuiButton-text"
onClick={[Function]} data-loading={true}
onDragLeave={[Function]} disabled={false}
onFocus={[Function]} id="sorting"
onKeyDown={[Function]} onBlur={[Function]}
onKeyUp={[Function]} onClick={[Function]}
onMouseDown={[Function]} onDragLeave={[Function]}
onMouseLeave={[Function]} onFocus={[Function]}
onMouseUp={[Function]} onKeyDown={[Function]}
onTouchEnd={[Function]} onKeyUp={[Function]}
onTouchMove={[Function]} onMouseDown={[Function]}
onTouchStart={[Function]} onMouseLeave={[Function]}
style={ onMouseUp={[Function]}
Object { onTouchEnd={[Function]}
"fontWeight": "normal", onTouchMove={[Function]}
"textTransform": "lowercase", onTouchStart={[Function]}
} style={
} Object {
tabIndex={0} "fontWeight": "normal",
title="Sort by" "textTransform": "lowercase",
type="button" }
> }
<span tabIndex={0}
className="MuiButton-label" title="Sort by"
type="button"
> >
By Name
<span <span
className="MuiButton-endIcon MuiButton-iconSizeMedium" className="MuiButton-label"
> >
By Name
<span <span
aria-hidden={true} className="MuiButton-endIcon MuiButton-iconSizeMedium"
className="material-icons MuiIcon-root"
> >
<svg <span
className="MuiSvgIcon-root" aria-hidden={true}
focusable="false" className="material-icons MuiIcon-root"
role="img"
viewBox="0 0 24 24"
> >
<path <svg
d="M7 10l5 5 5-5z" className="MuiSvgIcon-root"
/> focusable="false"
<title> role="img"
Toggle viewBox="0 0 24 24"
</title> >
</svg> <path
d="M7 10l5 5 5-5z"
/>
<title>
Toggle
</title>
</svg>
</span>
</span> </span>
</span> </span>
</span> <span
<span className="MuiTouchRipple-root"
className="MuiTouchRipple-root" />
/> </button>
</button> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> <div
<div className="makeStyles-bodyContainer-11"
className="makeStyles-bodyContainer-11"
>
<ul
className="MuiList-root MuiList-padding"
> >
<ListItem <ul
feature={ className="MuiList-root MuiList-padding"
Object { >
"name": "Another", <ListItem
"reviveName": "Another", feature={
Object {
"name": "Another",
"reviveName": "Another",
}
} }
} flags={Object {}}
flags={Object {}} hasAccess={[Function]}
hasAccess={[Function]} />
/> </ul>
</ul> </div>
</div> </div>
</div> </div>,
</div> <div
aria-atomic={true}
aria-live="polite"
className="makeStyles-container-18"
data-testid="ANNOUNCER_ELEMENT_TEST_ID"
role="status"
>
Navigated to Feature toggles
</div>,
]
`; `;

View File

@ -16,12 +16,14 @@ import { FeatureMetricsChips } from './FeatureMetricsChips/FeatureMetricsChips';
import { useFeature } from 'hooks/api/getters/useFeature/useFeature'; import { useFeature } from 'hooks/api/getters/useFeature/useFeature';
import ConditionallyRender from 'component/common/ConditionallyRender'; import ConditionallyRender from 'component/common/ConditionallyRender';
import { useStyles } from './FeatureMetrics.styles'; import { useStyles } from './FeatureMetrics.styles';
import { usePageTitle } from 'hooks/usePageTitle';
export const FeatureMetrics = () => { export const FeatureMetrics = () => {
const { projectId, featureId } = useParams<IFeatureViewParams>(); const { projectId, featureId } = useParams<IFeatureViewParams>();
const environments = useFeatureMetricsEnvironments(projectId, featureId); const environments = useFeatureMetricsEnvironments(projectId, featureId);
const applications = useFeatureMetricsApplications(featureId); const applications = useFeatureMetricsApplications(featureId);
const styles = useStyles(); const styles = useStyles();
usePageTitle('Metrics');
const [hoursBack = FEATURE_METRIC_HOURS_BACK_MAX, setHoursBack] = const [hoursBack = FEATURE_METRIC_HOURS_BACK_MAX, setHoursBack] =
useQueryStringNumberState('hoursBack'); useQueryStringNumberState('hoursBack');

View File

@ -10,15 +10,16 @@ import {
formatFeaturePath, formatFeaturePath,
} from 'component/feature/FeatureStrategy/FeatureStrategyEdit/FeatureStrategyEdit'; } from 'component/feature/FeatureStrategy/FeatureStrategyEdit/FeatureStrategyEdit';
import { useRequiredPathParam } from 'hooks/useRequiredPathParam'; import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
import { usePageTitle } from 'hooks/usePageTitle';
const FeatureOverview = () => { const FeatureOverview = () => {
const styles = useStyles(); const styles = useStyles();
const { push } = useHistory(); const { push } = useHistory();
const projectId = useRequiredPathParam('projectId'); const projectId = useRequiredPathParam('projectId');
const featureId = useRequiredPathParam('featureId'); const featureId = useRequiredPathParam('featureId');
const featurePath = formatFeaturePath(projectId, featureId); const featurePath = formatFeaturePath(projectId, featureId);
const onSidebarClose = () => push(featurePath); const onSidebarClose = () => push(featurePath);
usePageTitle(featureId);
return ( return (
<div className={styles.container}> <div className={styles.container}>

View File

@ -1,8 +1,10 @@
import { useStyles } from './FeatureVariants.styles'; import { useStyles } from './FeatureVariants.styles';
import FeatureOverviewVariants from './FeatureVariantsList/FeatureVariantsList'; import FeatureOverviewVariants from './FeatureVariantsList/FeatureVariantsList';
import { usePageTitle } from 'hooks/usePageTitle';
const FeatureVariants = () => { const FeatureVariants = () => {
const styles = useStyles(); const styles = useStyles();
usePageTitle('Variants');
return ( return (
<div className={styles.container}> <div className={styles.container}>

View File

@ -8,5 +8,5 @@ export const EventHistory = () => {
return null; return null;
} }
return <EventLog history={events} title="Recent changes" />; return <EventLog history={events} title="Event log" />;
}; };

View File

@ -10,7 +10,7 @@ export const FeatureEventHistory = ({ toggleName }) => {
} }
return ( return (
<EventLog history={events} hideName title="Change log" displayInline /> <EventLog history={events} hideName title="Event log" displayInline />
); );
}; };

View File

@ -23,6 +23,13 @@ exports[`renders correctly with empty version 1`] = `
</small> </small>
<br /> <br />
</section> </section>
<div
aria-atomic="true"
aria-live="polite"
class="makeStyles-container-2"
data-testid="ANNOUNCER_ELEMENT_TEST_ID"
role="status"
/>
</div> </div>
</body> </body>
`; `;
@ -34,7 +41,7 @@ exports[`renders correctly with ui-config 1`] = `
title="API details" title="API details"
> >
<h2 <h2
class="makeStyles-title-2" class="makeStyles-title-3"
> >
Unleash 1.1.0 Unleash 1.1.0
@ -50,6 +57,13 @@ exports[`renders correctly with ui-config 1`] = `
</small> </small>
<br /> <br />
</section> </section>
<div
aria-atomic="true"
aria-live="polite"
class="makeStyles-container-4"
data-testid="ANNOUNCER_ELEMENT_TEST_ID"
role="status"
/>
</div> </div>
</body> </body>
`; `;
@ -61,7 +75,7 @@ exports[`renders correctly with versionInfo 1`] = `
title="API details" title="API details"
> >
<h2 <h2
class="makeStyles-title-4" class="makeStyles-title-7"
> >
Unleash 1.2.3 Unleash 1.2.3
@ -77,6 +91,13 @@ exports[`renders correctly with versionInfo 1`] = `
1 1
</small> </small>
</section> </section>
<div
aria-atomic="true"
aria-live="polite"
class="makeStyles-container-8"
data-testid="ANNOUNCER_ELEMENT_TEST_ID"
role="status"
/>
</div> </div>
</body> </body>
`; `;
@ -88,7 +109,7 @@ exports[`renders correctly without uiConfig 1`] = `
title="API details" title="API details"
> >
<h2 <h2
class="makeStyles-title-3" class="makeStyles-title-5"
> >
Unleash 1.1.0 Unleash 1.1.0
@ -97,6 +118,13 @@ exports[`renders correctly without uiConfig 1`] = `
<small /> <small />
<br /> <br />
</section> </section>
<div
aria-atomic="true"
aria-live="polite"
class="makeStyles-container-6"
data-testid="ANNOUNCER_ELEMENT_TEST_ID"
role="status"
/>
</div> </div>
</body> </body>
`; `;

View File

@ -46,7 +46,7 @@ Array [
"menu": Object {}, "menu": Object {},
"parent": "/projects", "parent": "/projects",
"path": "/projects/:projectId/features/:featureId/edit", "path": "/projects/:projectId/features/:featureId/edit",
"title": "Edit Feature", "title": "Edit feature",
"type": "protected", "type": "protected",
}, },
Object { Object {
@ -122,7 +122,7 @@ Array [
"mobile": true, "mobile": true,
}, },
"path": "/features", "path": "/features",
"title": "Feature Toggles", "title": "Feature toggles",
"type": "protected", "type": "protected",
}, },
Object { Object {
@ -169,7 +169,7 @@ Array [
"mobile": true, "mobile": true,
}, },
"path": "/context", "path": "/context",
"title": "Context Fields", "title": "Context fields",
"type": "protected", "type": "protected",
}, },
Object { Object {
@ -331,14 +331,14 @@ Array [
"adminSettings": true, "adminSettings": true,
}, },
"path": "/history", "path": "/history",
"title": "Event History", "title": "Event log",
"type": "protected", "type": "protected",
}, },
Object { Object {
"component": [Function], "component": [Function],
"menu": Object {}, "menu": Object {},
"path": "/archive", "path": "/archive",
"title": "Archived Toggles", "title": "Archived toggles",
"type": "protected", "type": "protected",
}, },
Object { Object {
@ -402,7 +402,7 @@ Array [
}, },
"parent": "/admin", "parent": "/admin",
"path": "/admin/roles", "path": "/admin/roles",
"title": "Project Roles", "title": "Project roles",
"type": "protected", "type": "protected",
}, },
Object { Object {
@ -412,7 +412,7 @@ Array [
}, },
"parent": "/admin", "parent": "/admin",
"path": "/admin/auth", "path": "/admin/auth",
"title": "Single Sign-On", "title": "Single sign-on",
"type": "protected", "type": "protected",
}, },
Object { Object {

View File

@ -98,7 +98,7 @@ export const routes: IRoute[] = [
{ {
path: '/projects/:projectId/features/:featureId/edit', path: '/projects/:projectId/features/:featureId/edit',
parent: '/projects', parent: '/projects',
title: 'Edit Feature', title: 'Edit feature',
component: EditFeature, component: EditFeature,
type: 'protected', type: 'protected',
menu: {}, menu: {},
@ -172,7 +172,7 @@ export const routes: IRoute[] = [
}, },
{ {
path: '/features', path: '/features',
title: 'Feature Toggles', title: 'Feature toggles',
component: FeatureToggleListContainer, component: FeatureToggleListContainer,
type: 'protected', type: 'protected',
menu: { mobile: true }, menu: { mobile: true },
@ -216,7 +216,7 @@ export const routes: IRoute[] = [
}, },
{ {
path: '/context', path: '/context',
title: 'Context Fields', title: 'Context fields',
component: ContextList, component: ContextList,
type: 'protected', type: 'protected',
flag: C, flag: C,
@ -372,7 +372,7 @@ export const routes: IRoute[] = [
}, },
{ {
path: '/history', path: '/history',
title: 'Event History', title: 'Event log',
component: EventHistoryPage, component: EventHistoryPage,
type: 'protected', type: 'protected',
menu: { adminSettings: true }, menu: { adminSettings: true },
@ -381,7 +381,7 @@ export const routes: IRoute[] = [
// Archive // Archive
{ {
path: '/archive', path: '/archive',
title: 'Archived Toggles', title: 'Archived toggles',
component: ArchiveListContainer, component: ArchiveListContainer,
type: 'protected', type: 'protected',
menu: {}, menu: {},
@ -447,7 +447,7 @@ export const routes: IRoute[] = [
{ {
path: '/admin/roles', path: '/admin/roles',
parent: '/admin', parent: '/admin',
title: 'Project Roles', title: 'Project roles',
component: ProjectRoles, component: ProjectRoles,
type: 'protected', type: 'protected',
flag: RE, flag: RE,
@ -456,7 +456,7 @@ export const routes: IRoute[] = [
{ {
path: '/admin/auth', path: '/admin/auth',
parent: '/admin', parent: '/admin',
title: 'Single Sign-On', title: 'Single sign-on',
component: AuthSettings, component: AuthSettings,
type: 'protected', type: 'protected',
menu: { adminSettings: true }, menu: { adminSettings: true },

View File

@ -49,7 +49,7 @@ export const ProjectFeatureToggles = ({
headerContent={ headerContent={
<HeaderTitle <HeaderTitle
className={styles.title} className={styles.title}
title={`Feature toggles (${filteredFeatures.length})`} title={`Project features (${filteredFeatures.length})`}
actions={ actions={
<div className={styles.actionsContainer}> <div className={styles.actionsContainer}>
<SearchField <SearchField

View File

@ -3,12 +3,15 @@ import ApiError from 'component/common/ApiError/ApiError';
import ConditionallyRender from 'component/common/ConditionallyRender'; import ConditionallyRender from 'component/common/ConditionallyRender';
import ReportToggleList from 'component/Reporting/ReportToggleList/ReportToggleList'; import ReportToggleList from 'component/Reporting/ReportToggleList/ReportToggleList';
import { ReportCard } from 'component/Reporting/ReportCard/ReportCard'; import { ReportCard } from 'component/Reporting/ReportCard/ReportCard';
import { usePageTitle } from 'hooks/usePageTitle';
interface IProjectHealthProps { interface IProjectHealthProps {
projectId: string; projectId: string;
} }
const ProjectHealth = ({ projectId }: IProjectHealthProps) => { const ProjectHealth = ({ projectId }: IProjectHealthProps) => {
usePageTitle('Project health');
const { healthReport, refetchHealthReport, error } = const { healthReport, refetchHealthReport, error } =
useHealthReport(projectId); useHealthReport(projectId);

View File

@ -6,6 +6,7 @@ import theme from 'themes/mainTheme';
import AccessProvider from 'component/providers/AccessProvider/AccessProvider'; import AccessProvider from 'component/providers/AccessProvider/AccessProvider';
import { ADMIN } from 'component/providers/AccessProvider/permissions'; import { ADMIN } from 'component/providers/AccessProvider/permissions';
import UIProvider from 'component/providers/UIProvider/UIProvider'; import UIProvider from 'component/providers/UIProvider/UIProvider';
import { AnnouncerProvider } from 'component/common/Announcer/AnnouncerProvider/AnnouncerProvider';
test('renders correctly with one strategy', () => { test('renders correctly with one strategy', () => {
const strategy = { const strategy = {
@ -15,17 +16,19 @@ test('renders correctly with one strategy', () => {
const tree = renderer.create( const tree = renderer.create(
<MemoryRouter> <MemoryRouter>
<ThemeProvider theme={theme}> <ThemeProvider theme={theme}>
<UIProvider> <AnnouncerProvider>
<AccessProvider permissions={[{ permission: ADMIN }]}> <UIProvider>
<StrategiesList <AccessProvider permissions={[{ permission: ADMIN }]}>
strategies={[strategy]} <StrategiesList
fetchStrategies={jest.fn()} strategies={[strategy]}
removeStrategy={jest.fn()} fetchStrategies={jest.fn()}
deprecateStrategy={jest.fn()} removeStrategy={jest.fn()}
reactivateStrategy={jest.fn()} deprecateStrategy={jest.fn()}
/> reactivateStrategy={jest.fn()}
</AccessProvider> />
</UIProvider> </AccessProvider>
</UIProvider>
</AnnouncerProvider>
</ThemeProvider> </ThemeProvider>
</MemoryRouter> </MemoryRouter>
); );
@ -41,17 +44,19 @@ test('renders correctly with one strategy without permissions', () => {
const tree = renderer.create( const tree = renderer.create(
<MemoryRouter> <MemoryRouter>
<ThemeProvider theme={theme}> <ThemeProvider theme={theme}>
<UIProvider> <AnnouncerProvider>
<AccessProvider permissions={[{ permission: ADMIN }]}> <UIProvider>
<StrategiesList <AccessProvider permissions={[{ permission: ADMIN }]}>
strategies={[strategy]} <StrategiesList
fetchStrategies={jest.fn()} strategies={[strategy]}
removeStrategy={jest.fn()} fetchStrategies={jest.fn()}
deprecateStrategy={jest.fn()} removeStrategy={jest.fn()}
reactivateStrategy={jest.fn()} deprecateStrategy={jest.fn()}
/> reactivateStrategy={jest.fn()}
</AccessProvider> />
</UIProvider> </AccessProvider>
</UIProvider>
</AnnouncerProvider>
</ThemeProvider> </ThemeProvider>
</MemoryRouter> </MemoryRouter>
); );

View File

@ -1,139 +1,42 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders correctly with one strategy 1`] = ` exports[`renders correctly with one strategy 1`] = `
<div Array [
className="MuiPaper-root MuiPaper-elevation1 MuiPaper-rounded"
style={
Object {
"borderRadius": "10px",
"boxShadow": "none",
}
}
>
<div <div
className="makeStyles-headerContainer-2" className="MuiPaper-root MuiPaper-elevation1 MuiPaper-rounded"
style={
Object {
"borderRadius": "10px",
"boxShadow": "none",
}
}
> >
<div <div
className="makeStyles-headerTitleContainer-6" className="makeStyles-headerContainer-2"
> >
<div <div
className="" className="makeStyles-headerTitleContainer-6"
data-loading={true}
>
<h1
className="MuiTypography-root makeStyles-headerTitle-7 MuiTypography-h1"
>
Strategies
</h1>
</div>
<div
className="makeStyles-headerActions-8"
>
<span
id="useId-0"
>
<button
aria-describedby="useId-0"
className="MuiButtonBase-root MuiButton-root MuiButton-contained MuiButton-containedPrimary"
data-testid="ADD_NEW_STRATEGY_ID"
disabled={false}
onBlur={[Function]}
onClick={[Function]}
onDragLeave={[Function]}
onFocus={[Function]}
onKeyDown={[Function]}
onKeyUp={[Function]}
onMouseDown={[Function]}
onMouseLeave={[Function]}
onMouseUp={[Function]}
onTouchEnd={[Function]}
onTouchMove={[Function]}
onTouchStart={[Function]}
tabIndex={0}
type="button"
>
<span
className="MuiButton-label"
>
New strategy
<span
className="MuiButton-endIcon MuiButton-iconSizeMedium"
/>
</span>
<span
className="MuiTouchRipple-root"
/>
</button>
</span>
</div>
</div>
</div>
<div
className="makeStyles-bodyContainer-3"
>
<ul
className="MuiList-root MuiList-padding"
>
<li
className="MuiListItem-root makeStyles-listItem-1 MuiListItem-gutters"
disabled={false}
> >
<div <div
className="MuiListItemAvatar-root" className=""
data-loading={true}
> >
<svg <h1
aria-hidden={true} className="MuiTypography-root makeStyles-headerTitle-7 MuiTypography-h1"
className="MuiSvgIcon-root"
focusable="false"
style={
Object {
"color": "#0000008a",
}
}
viewBox="0 0 24 24"
> >
<path Strategies
d="M20.5 11H19V7c0-1.1-.9-2-2-2h-4V3.5C13 2.12 11.88 1 10.5 1S8 2.12 8 3.5V5H4c-1.1 0-1.99.9-1.99 2v3.8H3.5c1.49 0 2.7 1.21 2.7 2.7s-1.21 2.7-2.7 2.7H2V20c0 1.1.9 2 2 2h3.8v-1.5c0-1.49 1.21-2.7 2.7-2.7 1.49 0 2.7 1.21 2.7 2.7V22H17c1.1 0 2-.9 2-2v-4h1.5c1.38 0 2.5-1.12 2.5-2.5S21.88 11 20.5 11z" </h1>
/>
</svg>
</div> </div>
<div <div
className="MuiListItemText-root MuiListItemText-multiline" className="makeStyles-headerActions-8"
> >
<span <span
className="MuiTypography-root MuiListItemText-primary MuiTypography-body1 MuiTypography-displayBlock" id="useId-0"
>
<a
href="/strategies/flexibleRollout"
onClick={[Function]}
>
<strong>
Gradual rollout
</strong>
</a>
</span>
<p
className="MuiTypography-root MuiListItemText-secondary MuiTypography-body2 MuiTypography-colorTextSecondary MuiTypography-displayBlock"
>
Roll out to a percentage of your userbase, and ensure that the experience is the same for the user on each visit.
</p>
</div>
<div>
<span
aria-describedby={null}
className=""
id="useId-1"
onBlur={[Function]}
onFocus={[Function]}
onMouseLeave={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
onTouchStart={[Function]}
title="Deprecate strategy"
> >
<button <button
aria-labelledby="useId-1" aria-describedby="useId-0"
className="MuiButtonBase-root MuiIconButton-root" className="MuiButtonBase-root MuiButton-root MuiButton-contained MuiButton-containedPrimary"
data-testid="ADD_NEW_STRATEGY_ID"
disabled={false} disabled={false}
onBlur={[Function]} onBlur={[Function]}
onClick={[Function]} onClick={[Function]}
@ -151,18 +54,12 @@ exports[`renders correctly with one strategy 1`] = `
type="button" type="button"
> >
<span <span
className="MuiIconButton-label" className="MuiButton-label"
> >
<svg New strategy
aria-hidden={true} <span
className="MuiSvgIcon-root" className="MuiButton-endIcon MuiButton-iconSizeMedium"
focusable="false" />
viewBox="0 0 24 24"
>
<path
d="M12 4.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5zM12 17c-2.76 0-5-2.24-5-5s2.24-5 5-5 5 2.24 5 5-2.24 5-5 5zm0-8c-1.66 0-3 1.34-3 3s1.34 3 3 3 3-1.34 3-3-1.34-3-3-3z"
/>
</svg>
</span> </span>
<span <span
className="MuiTouchRipple-root" className="MuiTouchRipple-root"
@ -170,240 +67,257 @@ exports[`renders correctly with one strategy 1`] = `
</button> </button>
</span> </span>
</div> </div>
<div </div>
aria-describedby={null} </div>
className="" <div
onBlur={[Function]} className="makeStyles-bodyContainer-3"
onFocus={[Function]} >
onMouseLeave={[Function]} <ul
onMouseOver={[Function]} className="MuiList-root MuiList-padding"
onTouchEnd={[Function]} >
onTouchStart={[Function]} <li
title="You cannot delete a built-in strategy" className="MuiListItem-root makeStyles-listItem-1 MuiListItem-gutters"
disabled={false}
> >
<button <div
className="MuiButtonBase-root MuiIconButton-root Mui-disabled Mui-disabled" className="MuiListItemAvatar-root"
disabled={true} >
onBlur={[Function]} <svg
onDragLeave={[Function]} aria-hidden={true}
onFocus={[Function]} className="MuiSvgIcon-root"
onKeyDown={[Function]} focusable="false"
onKeyUp={[Function]} style={
onMouseDown={[Function]} Object {
onMouseLeave={[Function]} "color": "#0000008a",
onMouseUp={[Function]} }
onTouchEnd={[Function]} }
onTouchMove={[Function]} viewBox="0 0 24 24"
onTouchStart={[Function]} >
tabIndex={-1} <path
type="button" d="M20.5 11H19V7c0-1.1-.9-2-2-2h-4V3.5C13 2.12 11.88 1 10.5 1S8 2.12 8 3.5V5H4c-1.1 0-1.99.9-1.99 2v3.8H3.5c1.49 0 2.7 1.21 2.7 2.7s-1.21 2.7-2.7 2.7H2V20c0 1.1.9 2 2 2h3.8v-1.5c0-1.49 1.21-2.7 2.7-2.7 1.49 0 2.7 1.21 2.7 2.7V22H17c1.1 0 2-.9 2-2v-4h1.5c1.38 0 2.5-1.12 2.5-2.5S21.88 11 20.5 11z"
/>
</svg>
</div>
<div
className="MuiListItemText-root MuiListItemText-multiline"
> >
<span <span
className="MuiIconButton-label" className="MuiTypography-root MuiListItemText-primary MuiTypography-body1 MuiTypography-displayBlock"
> >
<svg <a
className="MuiSvgIcon-root" href="/strategies/flexibleRollout"
focusable="false" onClick={[Function]}
role="img"
viewBox="0 0 24 24"
> >
<path <strong>
d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34a.9959.9959 0 00-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z" Gradual rollout
/> </strong>
<title> </a>
Edit strategy
</title>
</svg>
</span> </span>
</button> <p
</div> className="MuiTypography-root MuiListItemText-secondary MuiTypography-body2 MuiTypography-colorTextSecondary MuiTypography-displayBlock"
<div >
aria-describedby={null} Roll out to a percentage of your userbase, and ensure that the experience is the same for the user on each visit.
className="" </p>
onBlur={[Function]} </div>
onFocus={[Function]} <div>
onMouseLeave={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
onTouchStart={[Function]}
title="You cannot delete a built-in strategy"
>
<button
className="MuiButtonBase-root MuiIconButton-root Mui-disabled Mui-disabled"
disabled={true}
onBlur={[Function]}
onDragLeave={[Function]}
onFocus={[Function]}
onKeyDown={[Function]}
onKeyUp={[Function]}
onMouseDown={[Function]}
onMouseLeave={[Function]}
onMouseUp={[Function]}
onTouchEnd={[Function]}
onTouchMove={[Function]}
onTouchStart={[Function]}
tabIndex={-1}
type="button"
>
<span <span
className="MuiIconButton-label" aria-describedby={null}
className=""
id="useId-1"
onBlur={[Function]}
onFocus={[Function]}
onMouseLeave={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
onTouchStart={[Function]}
title="Deprecate strategy"
> >
<svg <button
className="MuiSvgIcon-root" aria-labelledby="useId-1"
focusable="false" className="MuiButtonBase-root MuiIconButton-root"
role="img" disabled={false}
viewBox="0 0 24 24" onBlur={[Function]}
onClick={[Function]}
onDragLeave={[Function]}
onFocus={[Function]}
onKeyDown={[Function]}
onKeyUp={[Function]}
onMouseDown={[Function]}
onMouseLeave={[Function]}
onMouseUp={[Function]}
onTouchEnd={[Function]}
onTouchMove={[Function]}
onTouchStart={[Function]}
tabIndex={0}
type="button"
> >
<path <span
d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z" className="MuiIconButton-label"
>
<svg
aria-hidden={true}
className="MuiSvgIcon-root"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M12 4.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5zM12 17c-2.76 0-5-2.24-5-5s2.24-5 5-5 5 2.24 5 5-2.24 5-5 5zm0-8c-1.66 0-3 1.34-3 3s1.34 3 3 3 3-1.34 3-3-1.34-3-3-3z"
/>
</svg>
</span>
<span
className="MuiTouchRipple-root"
/> />
<title> </button>
Delete strategy
</title>
</svg>
</span> </span>
</button> </div>
</div> <div
</li> aria-describedby={null}
</ul> className=""
</div> onBlur={[Function]}
</div> onFocus={[Function]}
onMouseLeave={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
onTouchStart={[Function]}
title="You cannot delete a built-in strategy"
>
<button
className="MuiButtonBase-root MuiIconButton-root Mui-disabled Mui-disabled"
disabled={true}
onBlur={[Function]}
onDragLeave={[Function]}
onFocus={[Function]}
onKeyDown={[Function]}
onKeyUp={[Function]}
onMouseDown={[Function]}
onMouseLeave={[Function]}
onMouseUp={[Function]}
onTouchEnd={[Function]}
onTouchMove={[Function]}
onTouchStart={[Function]}
tabIndex={-1}
type="button"
>
<span
className="MuiIconButton-label"
>
<svg
className="MuiSvgIcon-root"
focusable="false"
role="img"
viewBox="0 0 24 24"
>
<path
d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34a.9959.9959 0 00-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z"
/>
<title>
Edit strategy
</title>
</svg>
</span>
</button>
</div>
<div
aria-describedby={null}
className=""
onBlur={[Function]}
onFocus={[Function]}
onMouseLeave={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
onTouchStart={[Function]}
title="You cannot delete a built-in strategy"
>
<button
className="MuiButtonBase-root MuiIconButton-root Mui-disabled Mui-disabled"
disabled={true}
onBlur={[Function]}
onDragLeave={[Function]}
onFocus={[Function]}
onKeyDown={[Function]}
onKeyUp={[Function]}
onMouseDown={[Function]}
onMouseLeave={[Function]}
onMouseUp={[Function]}
onTouchEnd={[Function]}
onTouchMove={[Function]}
onTouchStart={[Function]}
tabIndex={-1}
type="button"
>
<span
className="MuiIconButton-label"
>
<svg
className="MuiSvgIcon-root"
focusable="false"
role="img"
viewBox="0 0 24 24"
>
<path
d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"
/>
<title>
Delete strategy
</title>
</svg>
</span>
</button>
</div>
</li>
</ul>
</div>
</div>,
<div
aria-atomic={true}
aria-live="polite"
className="makeStyles-container-11"
data-testid="ANNOUNCER_ELEMENT_TEST_ID"
role="status"
>
Navigated to Strategies
</div>,
]
`; `;
exports[`renders correctly with one strategy without permissions 1`] = ` exports[`renders correctly with one strategy without permissions 1`] = `
<div Array [
className="MuiPaper-root MuiPaper-elevation1 MuiPaper-rounded"
style={
Object {
"borderRadius": "10px",
"boxShadow": "none",
}
}
>
<div <div
className="makeStyles-headerContainer-2" className="MuiPaper-root MuiPaper-elevation1 MuiPaper-rounded"
style={
Object {
"borderRadius": "10px",
"boxShadow": "none",
}
}
> >
<div <div
className="makeStyles-headerTitleContainer-6" className="makeStyles-headerContainer-2"
> >
<div <div
className="" className="makeStyles-headerTitleContainer-6"
data-loading={true}
>
<h1
className="MuiTypography-root makeStyles-headerTitle-7 MuiTypography-h1"
>
Strategies
</h1>
</div>
<div
className="makeStyles-headerActions-8"
>
<span
id="useId-2"
>
<button
aria-describedby="useId-2"
className="MuiButtonBase-root MuiButton-root MuiButton-contained MuiButton-containedPrimary"
data-testid="ADD_NEW_STRATEGY_ID"
disabled={false}
onBlur={[Function]}
onClick={[Function]}
onDragLeave={[Function]}
onFocus={[Function]}
onKeyDown={[Function]}
onKeyUp={[Function]}
onMouseDown={[Function]}
onMouseLeave={[Function]}
onMouseUp={[Function]}
onTouchEnd={[Function]}
onTouchMove={[Function]}
onTouchStart={[Function]}
tabIndex={0}
type="button"
>
<span
className="MuiButton-label"
>
New strategy
<span
className="MuiButton-endIcon MuiButton-iconSizeMedium"
/>
</span>
<span
className="MuiTouchRipple-root"
/>
</button>
</span>
</div>
</div>
</div>
<div
className="makeStyles-bodyContainer-3"
>
<ul
className="MuiList-root MuiList-padding"
>
<li
className="MuiListItem-root makeStyles-listItem-1 MuiListItem-gutters"
disabled={false}
> >
<div <div
className="MuiListItemAvatar-root" className=""
data-loading={true}
> >
<svg <h1
aria-hidden={true} className="MuiTypography-root makeStyles-headerTitle-7 MuiTypography-h1"
className="MuiSvgIcon-root"
focusable="false"
style={
Object {
"color": "#0000008a",
}
}
viewBox="0 0 24 24"
> >
<path Strategies
d="M20.5 11H19V7c0-1.1-.9-2-2-2h-4V3.5C13 2.12 11.88 1 10.5 1S8 2.12 8 3.5V5H4c-1.1 0-1.99.9-1.99 2v3.8H3.5c1.49 0 2.7 1.21 2.7 2.7s-1.21 2.7-2.7 2.7H2V20c0 1.1.9 2 2 2h3.8v-1.5c0-1.49 1.21-2.7 2.7-2.7 1.49 0 2.7 1.21 2.7 2.7V22H17c1.1 0 2-.9 2-2v-4h1.5c1.38 0 2.5-1.12 2.5-2.5S21.88 11 20.5 11z" </h1>
/>
</svg>
</div> </div>
<div <div
className="MuiListItemText-root MuiListItemText-multiline" className="makeStyles-headerActions-8"
> >
<span <span
className="MuiTypography-root MuiListItemText-primary MuiTypography-body1 MuiTypography-displayBlock" id="useId-2"
>
<a
href="/strategies/flexibleRollout"
onClick={[Function]}
>
<strong>
Gradual rollout
</strong>
</a>
</span>
<p
className="MuiTypography-root MuiListItemText-secondary MuiTypography-body2 MuiTypography-colorTextSecondary MuiTypography-displayBlock"
>
Roll out to a percentage of your userbase, and ensure that the experience is the same for the user on each visit.
</p>
</div>
<div>
<span
aria-describedby={null}
className=""
id="useId-3"
onBlur={[Function]}
onFocus={[Function]}
onMouseLeave={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
onTouchStart={[Function]}
title="Deprecate strategy"
> >
<button <button
aria-labelledby="useId-3" aria-describedby="useId-2"
className="MuiButtonBase-root MuiIconButton-root" className="MuiButtonBase-root MuiButton-root MuiButton-contained MuiButton-containedPrimary"
data-testid="ADD_NEW_STRATEGY_ID"
disabled={false} disabled={false}
onBlur={[Function]} onBlur={[Function]}
onClick={[Function]} onClick={[Function]}
@ -421,18 +335,12 @@ exports[`renders correctly with one strategy without permissions 1`] = `
type="button" type="button"
> >
<span <span
className="MuiIconButton-label" className="MuiButton-label"
> >
<svg New strategy
aria-hidden={true} <span
className="MuiSvgIcon-root" className="MuiButton-endIcon MuiButton-iconSizeMedium"
focusable="false" />
viewBox="0 0 24 24"
>
<path
d="M12 4.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5zM12 17c-2.76 0-5-2.24-5-5s2.24-5 5-5 5 2.24 5 5-2.24 5-5 5zm0-8c-1.66 0-3 1.34-3 3s1.34 3 3 3 3-1.34 3-3-1.34-3-3-3z"
/>
</svg>
</span> </span>
<span <span
className="MuiTouchRipple-root" className="MuiTouchRipple-root"
@ -440,102 +348,216 @@ exports[`renders correctly with one strategy without permissions 1`] = `
</button> </button>
</span> </span>
</div> </div>
<div </div>
aria-describedby={null} </div>
className="" <div
onBlur={[Function]} className="makeStyles-bodyContainer-3"
onFocus={[Function]} >
onMouseLeave={[Function]} <ul
onMouseOver={[Function]} className="MuiList-root MuiList-padding"
onTouchEnd={[Function]} >
onTouchStart={[Function]} <li
title="You cannot delete a built-in strategy" className="MuiListItem-root makeStyles-listItem-1 MuiListItem-gutters"
disabled={false}
> >
<button <div
className="MuiButtonBase-root MuiIconButton-root Mui-disabled Mui-disabled" className="MuiListItemAvatar-root"
disabled={true} >
onBlur={[Function]} <svg
onDragLeave={[Function]} aria-hidden={true}
onFocus={[Function]} className="MuiSvgIcon-root"
onKeyDown={[Function]} focusable="false"
onKeyUp={[Function]} style={
onMouseDown={[Function]} Object {
onMouseLeave={[Function]} "color": "#0000008a",
onMouseUp={[Function]} }
onTouchEnd={[Function]} }
onTouchMove={[Function]} viewBox="0 0 24 24"
onTouchStart={[Function]} >
tabIndex={-1} <path
type="button" d="M20.5 11H19V7c0-1.1-.9-2-2-2h-4V3.5C13 2.12 11.88 1 10.5 1S8 2.12 8 3.5V5H4c-1.1 0-1.99.9-1.99 2v3.8H3.5c1.49 0 2.7 1.21 2.7 2.7s-1.21 2.7-2.7 2.7H2V20c0 1.1.9 2 2 2h3.8v-1.5c0-1.49 1.21-2.7 2.7-2.7 1.49 0 2.7 1.21 2.7 2.7V22H17c1.1 0 2-.9 2-2v-4h1.5c1.38 0 2.5-1.12 2.5-2.5S21.88 11 20.5 11z"
/>
</svg>
</div>
<div
className="MuiListItemText-root MuiListItemText-multiline"
> >
<span <span
className="MuiIconButton-label" className="MuiTypography-root MuiListItemText-primary MuiTypography-body1 MuiTypography-displayBlock"
> >
<svg <a
className="MuiSvgIcon-root" href="/strategies/flexibleRollout"
focusable="false" onClick={[Function]}
role="img"
viewBox="0 0 24 24"
> >
<path <strong>
d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34a.9959.9959 0 00-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z" Gradual rollout
/> </strong>
<title> </a>
Edit strategy
</title>
</svg>
</span> </span>
</button> <p
</div> className="MuiTypography-root MuiListItemText-secondary MuiTypography-body2 MuiTypography-colorTextSecondary MuiTypography-displayBlock"
<div >
aria-describedby={null} Roll out to a percentage of your userbase, and ensure that the experience is the same for the user on each visit.
className="" </p>
onBlur={[Function]} </div>
onFocus={[Function]} <div>
onMouseLeave={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
onTouchStart={[Function]}
title="You cannot delete a built-in strategy"
>
<button
className="MuiButtonBase-root MuiIconButton-root Mui-disabled Mui-disabled"
disabled={true}
onBlur={[Function]}
onDragLeave={[Function]}
onFocus={[Function]}
onKeyDown={[Function]}
onKeyUp={[Function]}
onMouseDown={[Function]}
onMouseLeave={[Function]}
onMouseUp={[Function]}
onTouchEnd={[Function]}
onTouchMove={[Function]}
onTouchStart={[Function]}
tabIndex={-1}
type="button"
>
<span <span
className="MuiIconButton-label" aria-describedby={null}
className=""
id="useId-3"
onBlur={[Function]}
onFocus={[Function]}
onMouseLeave={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
onTouchStart={[Function]}
title="Deprecate strategy"
> >
<svg <button
className="MuiSvgIcon-root" aria-labelledby="useId-3"
focusable="false" className="MuiButtonBase-root MuiIconButton-root"
role="img" disabled={false}
viewBox="0 0 24 24" onBlur={[Function]}
onClick={[Function]}
onDragLeave={[Function]}
onFocus={[Function]}
onKeyDown={[Function]}
onKeyUp={[Function]}
onMouseDown={[Function]}
onMouseLeave={[Function]}
onMouseUp={[Function]}
onTouchEnd={[Function]}
onTouchMove={[Function]}
onTouchStart={[Function]}
tabIndex={0}
type="button"
> >
<path <span
d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z" className="MuiIconButton-label"
>
<svg
aria-hidden={true}
className="MuiSvgIcon-root"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M12 4.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5zM12 17c-2.76 0-5-2.24-5-5s2.24-5 5-5 5 2.24 5 5-2.24 5-5 5zm0-8c-1.66 0-3 1.34-3 3s1.34 3 3 3 3-1.34 3-3-1.34-3-3-3z"
/>
</svg>
</span>
<span
className="MuiTouchRipple-root"
/> />
<title> </button>
Delete strategy
</title>
</svg>
</span> </span>
</button> </div>
</div> <div
</li> aria-describedby={null}
</ul> className=""
</div> onBlur={[Function]}
</div> onFocus={[Function]}
onMouseLeave={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
onTouchStart={[Function]}
title="You cannot delete a built-in strategy"
>
<button
className="MuiButtonBase-root MuiIconButton-root Mui-disabled Mui-disabled"
disabled={true}
onBlur={[Function]}
onDragLeave={[Function]}
onFocus={[Function]}
onKeyDown={[Function]}
onKeyUp={[Function]}
onMouseDown={[Function]}
onMouseLeave={[Function]}
onMouseUp={[Function]}
onTouchEnd={[Function]}
onTouchMove={[Function]}
onTouchStart={[Function]}
tabIndex={-1}
type="button"
>
<span
className="MuiIconButton-label"
>
<svg
className="MuiSvgIcon-root"
focusable="false"
role="img"
viewBox="0 0 24 24"
>
<path
d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34a.9959.9959 0 00-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z"
/>
<title>
Edit strategy
</title>
</svg>
</span>
</button>
</div>
<div
aria-describedby={null}
className=""
onBlur={[Function]}
onFocus={[Function]}
onMouseLeave={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
onTouchStart={[Function]}
title="You cannot delete a built-in strategy"
>
<button
className="MuiButtonBase-root MuiIconButton-root Mui-disabled Mui-disabled"
disabled={true}
onBlur={[Function]}
onDragLeave={[Function]}
onFocus={[Function]}
onKeyDown={[Function]}
onKeyUp={[Function]}
onMouseDown={[Function]}
onMouseLeave={[Function]}
onMouseUp={[Function]}
onTouchEnd={[Function]}
onTouchMove={[Function]}
onTouchStart={[Function]}
tabIndex={-1}
type="button"
>
<span
className="MuiIconButton-label"
>
<svg
className="MuiSvgIcon-root"
focusable="false"
role="img"
viewBox="0 0 24 24"
>
<path
d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"
/>
<title>
Delete strategy
</title>
</svg>
</span>
</button>
</div>
</li>
</ul>
</div>
</div>,
<div
aria-atomic={true}
aria-live="polite"
className="makeStyles-container-11"
data-testid="ANNOUNCER_ELEMENT_TEST_ID"
role="status"
>
Navigated to Strategies
</div>,
]
`; `;

View File

@ -0,0 +1,21 @@
import { useEffect, useContext } from 'react';
import { AnnouncerContext } from 'component/common/Announcer/AnnouncerContext/AnnouncerContext';
export const usePageTitle = (title: string) => {
const { setAnnouncement } = useContext(AnnouncerContext);
useEffect(() => {
document.title = title;
return () => {
document.title = DEFAULT_PAGE_TITLE;
};
}, [title]);
useEffect(() => {
if (title !== DEFAULT_PAGE_TITLE) {
setAnnouncement(`Navigated to ${title}`);
}
}, [setAnnouncement, title]);
};
const DEFAULT_PAGE_TITLE = 'Unleash';

View File

@ -49,3 +49,4 @@ export const INPUT_ERROR_TEXT = 'INPUT_ERROR_TEXT';
export const HEADER_USER_AVATAR = 'HEADER_USER_AVATAR'; export const HEADER_USER_AVATAR = 'HEADER_USER_AVATAR';
export const SIDEBAR_MODAL_ID = 'SIDEBAR_MODAL_ID'; export const SIDEBAR_MODAL_ID = 'SIDEBAR_MODAL_ID';
export const AUTH_PAGE_ID = 'AUTH_PAGE_ID'; export const AUTH_PAGE_ID = 'AUTH_PAGE_ID';
export const ANNOUNCER_ELEMENT_TEST_ID = 'ANNOUNCER_ELEMENT_TEST_ID';

View File

@ -3,6 +3,7 @@ import { BrowserRouter as Router } from 'react-router-dom';
import { render as rtlRender, RenderOptions } from '@testing-library/react'; import { render as rtlRender, RenderOptions } from '@testing-library/react';
import { SWRConfig } from 'swr'; import { SWRConfig } from 'swr';
import { MainThemeProvider } from 'themes/MainThemeProvider'; import { MainThemeProvider } from 'themes/MainThemeProvider';
import { AnnouncerProvider } from 'component/common/Announcer/AnnouncerProvider/AnnouncerProvider';
export const render = ( export const render = (
ui: JSX.Element, ui: JSX.Element,
@ -23,7 +24,9 @@ const Wrapper: FC = ({ children }) => {
return ( return (
<SWRConfig value={{ provider: () => new Map() }}> <SWRConfig value={{ provider: () => new Map() }}>
<MainThemeProvider> <MainThemeProvider>
<Router>{children}</Router> <AnnouncerProvider>
<Router>{children}</Router>
</AnnouncerProvider>
</MainThemeProvider> </MainThemeProvider>
</SWRConfig> </SWRConfig>
); );