Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[#1730] Fixed endpoint validation a11y issues. #1880

Merged
merged 1 commit into from
Sep 26, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- [1871](https://github.com/microsoft/BotFramework-Emulator/pull/1871)
- [1872](https://github.com/microsoft/BotFramework-Emulator/pull/1872)
- [1873](https://github.com/microsoft/BotFramework-Emulator/pull/1873)
- [1880](https://github.com/microsoft/BotFramework-Emulator/pull/1880)

- [client] Fixed an issue with the transcripts path input inside of the resource settings dialog in PR [1836](https://github.com/microsoft/BotFramework-Emulator/pull/1836)

Expand Down
4 changes: 2 additions & 2 deletions packages/app/client/src/commands/uiCommands.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ import { store } from '../state/store';
import {
AzureLoginPromptDialogContainer,
AzureLoginSuccessDialogContainer,
BotCreationDialog,
BotCreationDialogContainer,
DialogService,
OpenBotDialogContainer,
SecretPromptDialogContainer,
Expand Down Expand Up @@ -97,7 +97,7 @@ describe('the uiCommands', () => {
it('should call DialogService.showDialog when the ShowBotCreationDialog command is dispatched', async () => {
const spy = jest.spyOn(DialogService, 'showDialog').mockResolvedValueOnce(true);
const result = await registry.getCommand(Commands.ShowBotCreationDialog)();
expect(spy).toHaveBeenCalledWith(BotCreationDialog);
expect(spy).toHaveBeenCalledWith(BotCreationDialogContainer);
expect(result).toBe(true);
});

Expand Down
4 changes: 2 additions & 2 deletions packages/app/client/src/commands/uiCommands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ import {
AzureLoginFailedDialogContainer,
AzureLoginPromptDialogContainer,
AzureLoginSuccessDialogContainer,
BotCreationDialog,
BotCreationDialogContainer,
DialogService,
OpenBotDialogContainer,
OpenUrlDialogContainer,
Expand Down Expand Up @@ -113,7 +113,7 @@ export class UiCommands {
// Shows a bot creation dialog
@Command(UI.ShowBotCreationDialog)
protected async showBotCreationPage() {
return await DialogService.showDialog(BotCreationDialog);
return await DialogService.showDialog(BotCreationDialogContainer);
}

// ---------------------------------------------------------------------------
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,14 +34,17 @@
import { mount, ReactWrapper } from 'enzyme';
import * as React from 'react';
import { CommandServiceImpl, CommandServiceInstance } from '@bfemulator/sdk-shared';
import { createStore } from 'redux';

import { ActiveBotHelper } from '../../helpers/activeBotHelper';
import { ariaAlertService } from '../../a11y';

import { BotCreationDialog, BotCreationDialogState } from './botCreationDialog';
import { BotCreationDialogContainer } from './botCreationDialogContainer';

jest.mock('../index', () => null);
jest.mock('../../../utils', () => ({
debounce: (func: () => any) => func,
generateBotSecret: () => {
return Math.random() + '';
},
Expand Down Expand Up @@ -88,7 +91,8 @@ describe('BotCreationDialog tests', () => {

let testWrapper: ReactWrapper<any, any, any>;
beforeEach(() => {
testWrapper = mount(<BotCreationDialog />);
const parent = mount(<BotCreationDialogContainer store={createStore((_state, _action) => ({}))} />);
testWrapper = parent.find(BotCreationDialog);
});

it('should render without throwing an error', () => {
Expand Down Expand Up @@ -200,6 +204,7 @@ describe('BotCreationDialog tests', () => {
});

it('should validate the endpoint', () => {
expect((testWrapper.instance() as any).validateEndpoint('')).toBe('');
expect((testWrapper.instance() as any).validateEndpoint('http://localhost:3000/api/messages')).toBe('');
expect((testWrapper.instance() as any).validateEndpoint('http://localhost:3000')).toBe(
`Please include route if necessary: "/api/messages"`
Expand Down Expand Up @@ -261,4 +266,14 @@ describe('BotCreationDialog tests', () => {

expect(testWrapper.instance().state.revealSecret).toBe(false);
});

it('should announce any validation warning messages', () => {
// make sure there are no leftover alerts from previous test(s)
const preExistingAlerts = document.querySelectorAll('body > span#alert-from-service');
preExistingAlerts.forEach(alert => alert.remove());
const spy = jest.spyOn(ariaAlertService, 'alert').mockReturnValueOnce(undefined);
testWrapper.instance().announceEndpointWarning('Invalid bot url.');

expect(spy).toHaveBeenCalledWith('For Endpoint URL, Invalid bot url.');
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -51,13 +51,17 @@ import { CommandServiceImpl, CommandServiceInstance } from '@bfemulator/sdk-shar

import { beginAdd } from '../../../state/actions/notificationActions';
import { store } from '../../../state/store';
import { generateBotSecret } from '../../../utils';
import { generateBotSecret, debounce } from '../../../utils';
import { ActiveBotHelper } from '../../helpers/activeBotHelper';
import { DialogService } from '../service';
import { ariaAlertService } from '../../a11y';

import * as styles from './botCreationDialog.scss';

export interface BotCreationDialogProps {
createAriaAlert: (msg: string) => void;
}

export interface BotCreationDialogState {
bot: BotConfigWithPath;
endpoint: IEndpointService;
Expand All @@ -67,13 +71,13 @@ export interface BotCreationDialogState {
isAzureGov: boolean;
}

export class BotCreationDialog extends React.Component<{}, BotCreationDialogState> {
export class BotCreationDialog extends React.Component<BotCreationDialogProps, BotCreationDialogState> {
@CommandServiceInstance()
public commandService: CommandServiceImpl;

private secretInputRef: HTMLInputElement;

public constructor(props: {}, context: BotCreationDialogState) {
public constructor(props: BotCreationDialogProps, context: BotCreationDialogState) {
super(props, context);

this.state = {
Expand Down Expand Up @@ -106,6 +110,7 @@ export class BotCreationDialog extends React.Component<{}, BotCreationDialogStat
const requiredFieldsCompleted = bot && endpoint.endpoint && bot.name && secretCriteria;

const endpointWarning = this.validateEndpoint(endpoint.endpoint);
endpointWarning && this.announceEndpointWarning(endpointWarning);
const endpointPlaceholder = "Your bot's endpoint (ex: http://localhost:3978/api/messages)";

// TODO - localization
Expand Down Expand Up @@ -366,11 +371,19 @@ export class BotCreationDialog extends React.Component<{}, BotCreationDialogStat

/** Checks the endpoint to see if it has the correct route syntax at the end (/api/messages) */
private validateEndpoint(endpoint: string): string {
if (!endpoint) {
// allow empty
return '';
}
const controllerRegEx = /api\/messages\/?$/;
return controllerRegEx.test(endpoint) ? '' : `Please include route if necessary: "/api/messages"`;
}

private setSecretInputRef = (ref: HTMLInputElement): void => {
this.secretInputRef = ref;
};

private announceEndpointWarning = debounce((msg: string) => {
this.props.createAriaAlert(`For Endpoint URL, ${msg}`);
}, 2000);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license.
//
// Microsoft Bot Framework: http://botframework.com
//
// Bot Framework Emulator Github:
// https://github.com/Microsoft/BotFramwork-Emulator
//
// Copyright (c) Microsoft Corporation
// All rights reserved.
//
// MIT License:
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED ""AS IS"", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//

import { connect } from 'react-redux';

import { ariaAlertService } from '../../a11y';

import { BotCreationDialog, BotCreationDialogProps } from './botCreationDialog';

const mapDispatchToProps = (_dispatch): BotCreationDialogProps => {
return {
createAriaAlert: (msg: string) => {
ariaAlertService.alert(msg);
},
};
};

export const BotCreationDialogContainer = connect(
undefined,
mapDispatchToProps
)(BotCreationDialog);
2 changes: 1 addition & 1 deletion packages/app/client/src/ui/dialogs/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//

export * from './botCreationDialog/botCreationDialog';
export * from './botCreationDialog/botCreationDialogContainer';
export * from './host/hostContainer';
export * from './secretPromptDialog/secretPromptDialogContainer';
export * from './tabManager/tabManagerContainer';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -247,11 +247,11 @@ describe('The OpenBotDialog', () => {

it('should announce any validation error messages', () => {
// make sure there are no leftover alerts from previous test(s)
const preExistingAlerts = document.querySelectorAll('body > span');
const preExistingAlerts = document.querySelectorAll('body > span#alert-from-service');
preExistingAlerts.forEach(alert => alert.remove());
const spy = jest.spyOn(ariaAlertService, 'alert').mockReturnValueOnce(undefined);
instance.announceErrorMessage('Invalid bot url.');

expect(spy).toHaveBeenCalledWith('Invalid bot url.');
expect(spy).toHaveBeenCalledWith('For Bot URL, Invalid bot url.');
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ import * as React from 'react';
import { ChangeEvent, Component, MouseEvent, ReactNode } from 'react';
import { EmulatorMode } from '@bfemulator/sdk-shared';

import { debounce } from '../../../utils';

import * as openBotStyles from './openBotDialog.scss';

export interface OpenBotDialogProps {
Expand Down Expand Up @@ -258,11 +260,8 @@ export class OpenBotDialog extends Component<OpenBotDialogProps, OpenBotDialogSt
}

/** Announces the error message to screen reader technologies */
private announceErrorMessage(msg: string): void {
private announceErrorMessage = debounce((msg: string): void => {
// ensure that we aren't spamming aria alerts each time the input is validated
const existingAlerts = document.querySelectorAll('span#alert-from-service');
if (!existingAlerts.length) {
this.props.createAriaAlert(msg);
}
}
this.props.createAriaAlert(`For Bot URL, ${msg}`);
}, 2000);
}