Skip to content
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 @@ -57,6 +57,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- [1867](https://github.com/microsoft/BotFramework-Emulator/pull/1867)
- [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)

- [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
60 changes: 60 additions & 0 deletions packages/app/client/src/ui/a11y/ariaAlertService.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
//
// 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 { ariaAlertService } from './ariaAlertService';

describe('AriaAlertService', () => {
it('should create an aria alert and only one at a time', () => {
ariaAlertService.alert('I am an alert!');
const alertElement = document.querySelector('span#alert-from-service') as HTMLSpanElement;

expect(alertElement).toBeTruthy();
expect(alertElement.innerText).toBe('I am an alert!');

ariaAlertService.alert('I am another alert!');

const alertElements = document.querySelectorAll('span#alert-from-service');

expect(alertElements.length).toBe(1);
});

it('should not create an aria alert if there is no message', () => {
// make sure there are no leftover alerts from previous test(s)
const preExistingAlerts = document.querySelectorAll('span#alert-from-service');
preExistingAlerts.forEach(alert => alert.remove());
ariaAlertService.alert(undefined);
const alertElement = document.querySelector('span#alert-from-service') as HTMLSpanElement;

expect(alertElement).toBeFalsy();
});
});
57 changes: 57 additions & 0 deletions packages/app/client/src/ui/a11y/ariaAlertService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
//
// 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.
//

let singleton: AriaAlertService;
class AriaAlertService {
constructor() {
singleton = this;
}

/** Creates an alert and inserts it into the DOM */
public alert(msg: string): void {
if (!msg) {
return;
}
const prevAlert = document.querySelector('span#alert-from-service');
prevAlert && prevAlert.remove();
const alert = document.createElement('span');
alert.innerText = msg;
alert.setAttribute('id', 'alert-from-service');
alert.setAttribute('role', 'alert');
alert.setAttribute('style', 'position: absolute; top: -9999px; overflow: hidden;');
document.body.appendChild(alert);
}
}

/** Creates invisible alerts to be read by screen reader technologies */
export const ariaAlertService = singleton || new AriaAlertService();
34 changes: 34 additions & 0 deletions packages/app/client/src/ui/a11y/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
//
// 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.
//

export * from './ariaAlertService';
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,12 @@
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//

import { mount } from 'enzyme';
import { mount, ReactWrapper } from 'enzyme';
import * as React from 'react';
import { CommandServiceImpl, CommandServiceInstance } from '@bfemulator/sdk-shared';

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

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

Expand Down Expand Up @@ -85,7 +86,7 @@ describe('BotCreationDialog tests', () => {
commandService = descriptor.descriptor.get();
});

let testWrapper;
let testWrapper: ReactWrapper<any, any, any>;
beforeEach(() => {
testWrapper = mount(<BotCreationDialog />);
});
Expand Down Expand Up @@ -141,9 +142,11 @@ describe('BotCreationDialog tests', () => {
});
(window.document.getElementById as any) = mockGetElementById;
window.document.execCommand = mockExec;
const alertServiceSpy = jest.spyOn(ariaAlertService, 'alert').mockReturnValueOnce(undefined);

(testWrapper.instance() as any).onCopyClick();
expect(mockExec).toHaveBeenCalledWith('copy');
expect(alertServiceSpy).toHaveBeenCalledWith('Secret copied to clipboard.');

// restore window functions
window.document.execCommand = backupExec;
Expand Down Expand Up @@ -238,4 +241,24 @@ describe('BotCreationDialog tests', () => {
null
);
});

it('should toggle the visibility of the secret', () => {
const spy = jest.spyOn(ariaAlertService, 'alert');
testWrapper.setState({ encryptKey: true, revealSecret: false });
testWrapper.instance().onRevealSecretClick();

expect(spy).toHaveBeenCalledWith('Secret showing.');
expect(testWrapper.instance().state.revealSecret).toBe(true);

testWrapper.instance().onRevealSecretClick();

expect(spy).toHaveBeenCalledWith('Secret hidden.');
});

it('should not toggle the visibility of the secret if the encryption is disabled', () => {
testWrapper.setState({ encryptKey: false, revealSecret: false });
testWrapper.instance().onRevealSecretClick();

expect(testWrapper.instance().state.revealSecret).toBe(false);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ import { store } from '../../../state/store';
import { generateBotSecret } from '../../../utils';
import { ActiveBotHelper } from '../../helpers/activeBotHelper';
import { DialogService } from '../service';
import { ariaAlertService } from '../../a11y';

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

Expand All @@ -70,6 +71,8 @@ export class BotCreationDialog extends React.Component<{}, BotCreationDialogStat
@CommandServiceInstance()
public commandService: CommandServiceImpl;

private secretInputRef: HTMLInputElement;

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

Expand Down Expand Up @@ -171,12 +174,13 @@ export class BotCreationDialog extends React.Component<{}, BotCreationDialogStat
</Row>

<TextField
disabled={!encryptKey}
inputContainerClassName={styles.key}
label="Secret "
inputRef={this.setSecretInputRef}
label="Secret"
value={secret}
placeholder="Your keys are not encrypted"
disabled={true}
id="key-input"
readOnly={true}
type={revealSecret ? 'text' : 'password'}
/>
<ul className={styles.actionsList}>
Expand Down Expand Up @@ -261,21 +265,22 @@ export class BotCreationDialog extends React.Component<{}, BotCreationDialogStat
if (!this.state.encryptKey) {
return null;
}
this.setState({ revealSecret: !this.state.revealSecret });
const revealSecret = !this.state.revealSecret;
ariaAlertService.alert(`Secret ${revealSecret ? 'showing' : 'hidden'}.`);
this.setState({ revealSecret });
};

private onCopyClick = (): void => {
if (!this.state.encryptKey) {
return null;
}
const input: HTMLInputElement = window.document.getElementById('key-input') as HTMLInputElement;
input.removeAttribute('disabled');
const { type } = input;
input.type = 'text';
input.select();
const { secretInputRef } = this;
const { type } = secretInputRef;
secretInputRef.type = 'text';
secretInputRef.select();
window.document.execCommand('copy');
input.type = type;
input.setAttribute('disabled', '');
secretInputRef.type = type;
ariaAlertService.alert('Secret copied to clipboard.');
};

// TODO: Re-enable ability to re-generate secret after 4.1
Expand Down Expand Up @@ -364,4 +369,8 @@ export class BotCreationDialog extends React.Component<{}, BotCreationDialogStat
const controllerRegEx = /api\/messages\/?$/;
return controllerRegEx.test(endpoint) ? '' : `Please include route if necessary: "/api/messages"`;
}

private setSecretInputRef = (ref: HTMLInputElement): void => {
this.secretInputRef = ref;
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import { clientAwareSettingsChanged } from '../../../state/actions/clientAwareSe
import { bot } from '../../../state/reducers/bot';
import { clientAwareSettings } from '../../../state/reducers/clientAwareSettings';
import { DialogService } from '../service';
import { ariaAlertService } from '../../a11y';

import { OpenBotDialog } from './openBotDialog';
import { OpenBotDialogContainer } from './openBotDialogContainer';
Expand Down Expand Up @@ -243,4 +244,14 @@ describe('The OpenBotDialog', () => {

expect(instance.state.botUrl).toBe('http://localhost:3978');
});

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

expect(spy).toHaveBeenCalledWith('Invalid bot url.');
});
});
11 changes: 11 additions & 0 deletions packages/app/client/src/ui/dialogs/openBotDialog/openBotDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ import { EmulatorMode } from '@bfemulator/sdk-shared';
import * as openBotStyles from './openBotDialog.scss';

export interface OpenBotDialogProps {
createAriaAlert?: (msg: string) => void;
mode?: EmulatorMode;
isDebug?: boolean;
onDialogCancel?: () => void;
Expand Down Expand Up @@ -127,6 +128,7 @@ export class OpenBotDialog extends Component<OpenBotDialogProps, OpenBotDialogSt
const { botUrl, appId, appPassword, mode, isDebug, isAzureGov } = this.state;
const validationResult = OpenBotDialog.validateEndpoint(botUrl);
const errorMessage = OpenBotDialog.getErrorMessage(validationResult);
errorMessage && this.announceErrorMessage(errorMessage);
const shouldBeDisabled =
validationResult === ValidationResult.Invalid || validationResult === ValidationResult.Empty;
const botUrlLabel = 'Bot URL';
Expand Down Expand Up @@ -254,4 +256,13 @@ export class OpenBotDialog extends Component<OpenBotDialogProps, OpenBotDialogSt
}
return null;
}

/** Announces the error message to screen reader technologies */
private announceErrorMessage(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);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,15 @@ import { Action } from 'redux';
import { openBotViaFilePathAction, openBotViaUrlAction } from '../../../state/actions/botActions';
import { DialogService } from '../service';
import { RootState } from '../../../state/store';
import { ariaAlertService } from '../../a11y';

import { OpenBotDialog, OpenBotDialogProps, OpenBotDialogState } from './openBotDialog';

const mapDispatchToProps = (dispatch: (action: Action) => void): OpenBotDialogProps => {
return {
createAriaAlert: (msg: string) => {
ariaAlertService.alert(msg);
},
openBot: (componentState: OpenBotDialogState) => {
DialogService.hideDialog();
const { appId = '', appPassword = '', botUrl = '', mode = 'livechat-url', isAzureGov } = componentState;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import * as EditorActions from '../../../state/actions/editorActions';
import { setFrameworkSettings, saveFrameworkSettings } from '../../../state/actions/frameworkSettingsActions';
import { getTabGroupForDocument } from '../../../state/helpers/editorHelpers';
import { framework } from '../../../state/reducers/framework';
import { ariaAlertService } from '../../a11y';

import { AppSettingsEditor } from './appSettingsEditor';
import { AppSettingsEditorContainer } from './appSettingsEditorContainer';
Expand Down Expand Up @@ -163,6 +164,8 @@ describe('The AppSettingsEditorContainer', () => {
});

it('should save the framework settings then get them again from main when the "onSaveClick" handler is called', async () => {
const alertServiceSpy = jest.spyOn(ariaAlertService, 'alert').mockReturnValueOnce(undefined);

await (instance as any).onSaveClick();

const keys = Object.keys(frameworkDefault).sort();
Expand All @@ -172,6 +175,8 @@ describe('The AppSettingsEditorContainer', () => {
...saveSettingsAction.payload,
hash: jasmine.any(String),
};

expect(mockDispatch).toHaveBeenLastCalledWith(saveFrameworkSettings(savedSettings));
expect(alertServiceSpy).toHaveBeenCalledWith('App settings saved.');
});
});
Loading