Skip to content

Commit

Permalink
Fixed issue with endpoint validation narration.
Browse files Browse the repository at this point in the history
  • Loading branch information
tonyanziano committed Sep 26, 2019
1 parent 97d88a1 commit e73b856
Show file tree
Hide file tree
Showing 9 changed files with 96 additions and 17 deletions.
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);
}

0 comments on commit e73b856

Please sign in to comment.