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

fix: copy issue details button on insecure origin target page #4170

Merged
merged 11 commits into from
May 12, 2021
10 changes: 8 additions & 2 deletions src/common/components/copy-issue-details-button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export type CopyIssueDetailsButtonProps = {
deps: CopyIssueDetailsButtonDeps;
issueDetailsData: CreateIssueDetailsTextData;
onClick: (clickEvent: React.MouseEvent<any>) => void;
hasSecureTargetPage: boolean;
};

export class CopyIssueDetailsButton extends React.Component<CopyIssueDetailsButtonProps> {
Expand Down Expand Up @@ -52,8 +53,13 @@ export class CopyIssueDetailsButton extends React.Component<CopyIssueDetailsButt
this.getIssueDetailsText(this.props.issueDetailsData),
);
} catch (error) {
toast.show('Failed to copy failure details. Please try again.');
return;
if (this.props.hasSecureTargetPage) {
toast.show('Failed to copy failure details. Please try again.');
return;
} else {
toast.show('Failed to copy failure details.');
return;
}
}
toast.show('Failure details copied.');
};
Expand Down
4 changes: 4 additions & 0 deletions src/common/window-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,10 @@ export class WindowUtils {
return window.navigator.platform;
}

public isSecureOrigin(): boolean {
return window.isSecureContext;
}

public getRandomValueArray(length: number): Uint8Array {
return window.crypto.getRandomValues(new Uint8Array(length));
}
Expand Down
30 changes: 25 additions & 5 deletions src/injected/components/command-bar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ export type CommandBarProps = {
userConfigurationStoreData: UserConfigurationStoreData;
shouldShowInspectButtonMessage: () => boolean;
devToolsShortcut: string;
hasSecureTargetPage: boolean;
shouldShowInsecureOriginPageMessage: boolean;
};

export const CommandBar = NamedFC<CommandBarProps>('CommandBar', props => {
Expand Down Expand Up @@ -67,16 +69,23 @@ export const CommandBar = NamedFC<CommandBarProps>('CommandBar', props => {

return (
<>
<CopyIssueDetailsButton
deps={props.deps}
issueDetailsData={issueData}
onClick={props.onClickCopyIssueDetailsButton}
/>
{renderCopyIssueDetailsButton(issueData)}
{renderFileIssueButton(issueData)}
</>
);
};

const renderCopyIssueDetailsButton = (issueData: CreateIssueDetailsTextData): JSX.Element => {
return (
<CopyIssueDetailsButton
deps={props.deps}
issueDetailsData={issueData}
onClick={props.onClickCopyIssueDetailsButton}
hasSecureTargetPage={props.hasSecureTargetPage}
/>
);
};

const renderFileIssueButton = (issueData: CreateIssueDetailsTextData): JSX.Element => {
return (
<IssueFilingButton
Expand All @@ -98,11 +107,22 @@ export const CommandBar = NamedFC<CommandBarProps>('CommandBar', props => {
}
};

const renderCopyIssueDetailsMessage = (): JSX.Element => {
if (props.shouldShowInsecureOriginPageMessage) {
return (
<div role="alert" className="copy-issue-details-button-help">
To copy failure details, first open the Accessibility Insights for Web page.
</div>
);
}
};

return (
<div className="insights-dialog-target-button-container">
{renderInspectButton()}
{renderIssueButtons()}
{renderInspectMessage()}
{renderCopyIssueDetailsMessage()}
</div>
);
});
17 changes: 14 additions & 3 deletions src/injected/components/details-dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ export interface DetailsDialogState {
canInspect: boolean;
userConfigurationStoreData: UserConfigurationStoreData;
showInspectMessage: boolean;
showInsecureOriginPageMessage: boolean;
}

export class DetailsDialog extends React.Component<DetailsDialogProps, DetailsDialogState> {
Expand All @@ -65,6 +66,8 @@ export class DetailsDialog extends React.Component<DetailsDialogProps, DetailsDi
public isBackButtonDisabled: () => boolean;
public isNextButtonDisabled: () => boolean;
public isInspectButtonDisabled: () => boolean;
public onClickCopyIssueDetailsButton: (ev) => void;
public shouldShowInsecureOriginPageMessage: () => boolean;

constructor(props: DetailsDialogProps) {
super(props);
Expand Down Expand Up @@ -105,7 +108,12 @@ export class DetailsDialog extends React.Component<DetailsDialogProps, DetailsDi
this.isInspectButtonDisabled = () => {
return this.props.dialogHandler.isInspectButtonDisabled(this);
};

this.onClickCopyIssueDetailsButton = (ev: React.MouseEvent<MouseEvent>) => {
this.props.dialogHandler.copyIssueDetailsButtonClickHandler(this, ev);
};
this.shouldShowInsecureOriginPageMessage = () => {
return this.props.dialogHandler.shouldShowInsecureOriginPageMessage(this);
};
this.state = {
showDialog: true,
currentRuleIndex: 0,
Expand All @@ -114,6 +122,8 @@ export class DetailsDialog extends React.Component<DetailsDialogProps, DetailsDi
// eslint-disable-next-line react/no-unused-state
showInspectMessage: false,
userConfigurationStoreData: props.userConfigStore.getState(),
// eslint-disable-next-line react/no-unused-state
showInsecureOriginPageMessage: false,
};
}

Expand Down Expand Up @@ -161,12 +171,13 @@ export class DetailsDialog extends React.Component<DetailsDialogProps, DetailsDi
deps: this.props.deps,
devToolsShortcut: this.props.devToolsShortcut,
failedRules: this.props.failedRules,
onClickCopyIssueDetailsButton: this.props.deps.targetPageActionMessageCreator
.copyIssueDetailsClicked,
onClickCopyIssueDetailsButton: this.onClickCopyIssueDetailsButton,
onClickInspectButton: this.onClickInspectButton,
shouldShowInspectButtonMessage: () =>
this.props.dialogHandler.shouldShowInspectButtonMessage(this),
userConfigurationStoreData: this.state.userConfigurationStoreData,
hasSecureTargetPage: this.props.dialogHandler.isTargetPageOriginSecure(),
shouldShowInsecureOriginPageMessage: this.shouldShowInsecureOriginPageMessage(),
};

return <CommandBar {...props} />;
Expand Down
23 changes: 22 additions & 1 deletion src/injected/details-dialog-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@
// Licensed under the MIT License.
import * as React from 'react';
import { HTMLElementUtils } from '../common/html-element-utils';
import { WindowUtils } from '../common/window-utils';
import { DetailsDialog } from './components/details-dialog';

export class DetailsDialogHandler {
private onDevToolChangedHandler: () => void;
private onUserConfigChangedHandler: () => void;

constructor(private htmlElementUtils: HTMLElementUtils) {}
constructor(private htmlElementUtils: HTMLElementUtils, private windowUtils: WindowUtils) {}

public backButtonClickHandler = (dialog: DetailsDialog): void => {
const currentRuleIndex = dialog.state.currentRuleIndex;
Expand Down Expand Up @@ -82,6 +83,26 @@ export class DetailsDialogHandler {
} for this target`;
};

public isTargetPageOriginSecure = (): boolean => {
return this.windowUtils.isSecureOrigin();
};

public copyIssueDetailsButtonClickHandler = (
dialog: DetailsDialog,
event: React.MouseEvent<MouseEvent>,
): void => {
dialog.props.deps.targetPageActionMessageCreator.copyIssueDetailsClicked(event);
if (!this.isTargetPageOriginSecure()) {
dialog.setState({ showInsecureOriginPageMessage: true });
} else {
dialog.setState({ showInsecureOriginPageMessage: false });
}
};

public shouldShowInsecureOriginPageMessage = (dialog: DetailsDialog): boolean => {
return dialog.state.showInsecureOriginPageMessage;
};

public onLayoutDidMount = (): void => {
const dialogContainer = this.htmlElementUtils.querySelector(
'.insights-dialog-main-override',
Expand Down
8 changes: 8 additions & 0 deletions src/injected/styles/injected.scss
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@

.insights-dialog-main-override .insights-dialog-inspect-disabled {
margin-top: 4px !important;
margin-bottom: 8px !important;
font-size: 12px !important;
color: $negative-outcome !important;
font-weight: $fontWeightSemiBold !important;
Expand All @@ -150,6 +151,13 @@
font-weight: $fontWeightSemiBold;
}

.insights-dialog-main-override .copy-issue-details-button-help {
margin-top: 4px !important;
font-size: 12px !important;
color: $negative-outcome !important;
font-weight: $fontWeightSemiBold !important;
}

.insights-dialog-main-override {
.insights-dialog-title {
margin-bottom: 16px !important;
Expand Down
2 changes: 1 addition & 1 deletion src/injected/window-initializer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ export class WindowInitializer {
this.frameMessenger,
this.browserAdapter,
getRTL,
new DetailsDialogHandler(htmlElementUtils),
new DetailsDialogHandler(htmlElementUtils, this.windowUtils),
);
this.drawingController = new DrawingController(
this.frameMessenger,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,34 +49,78 @@ describe('CopyIssueDetailsButtonTest', () => {
},
issueDetailsData: {} as CreateIssueDetailsTextData,
onClick: onClickMock.object,
hasSecureTargetPage: true,
};
});

test('render', () => {
const result = Enzyme.shallow(<CopyIssueDetailsButton {...props} />);
expect(result.debug()).toMatchSnapshot();
});
describe('toast message', () => {
test('render after click shows copy success message', async () => {
navigatorUtilsMock
.setup(navigatorUtils => navigatorUtils.copyToClipboard(issueDetailsText))
.returns(() => {
return Promise.resolve();
})
.verifiable(Times.once());

test('render after click shows toast', async () => {
navigatorUtilsMock
.setup(navigatorUtils => navigatorUtils.copyToClipboard(issueDetailsText))
.returns(() => {
return Promise.resolve();
})
.verifiable(Times.once());
const result = Enzyme.mount(<CopyIssueDetailsButton {...props} />);
const button = result.find(DefaultButton);
onClickMock.setup(m => m(It.isAny())).verifiable(Times.once());
// tslint:disable-next-line: await-promise
await button.simulate('click');

const result = Enzyme.mount(<CopyIssueDetailsButton {...props} />);
const button = result.find(DefaultButton);
onClickMock.setup(m => m(It.isAny())).verifiable(Times.once());
// tslint:disable-next-line: await-promise
await button.simulate('click');
const toast = result.find(Toast);

const toast = result.find(Toast);
expect(toast.state().toastVisible).toBe(true);
expect(toast.state().content).toBe('Failure details copied.');

expect(toast.state().toastVisible).toBe(true);
expect(toast.state().content).toBe('Failure details copied.');
verifyMocks();
});
test('render after click shows copy failure message', async () => {
navigatorUtilsMock
.setup(navigatorUtils => navigatorUtils.copyToClipboard(issueDetailsText))
.returns(() => {
return Promise.reject();
})
.verifiable(Times.once());

verifyMocks();
const result = Enzyme.mount(<CopyIssueDetailsButton {...props} />);
const button = result.find(DefaultButton);
onClickMock.setup(m => m(It.isAny())).verifiable(Times.once());
// tslint:disable-next-line: await-promise
await button.simulate('click');

const toast = result.find(Toast);

expect(toast.state().toastVisible).toBe(true);
expect(toast.state().content).toBe('Failed to copy failure details. Please try again.');

verifyMocks();
});
test('shows copy failure message for insecure origin', async () => {
navigatorUtilsMock
.setup(navigatorUtils => navigatorUtils.copyToClipboard(issueDetailsText))
.returns(() => {
return Promise.reject();
})
.verifiable(Times.once());
props.hasSecureTargetPage = false;
const result = Enzyme.mount(<CopyIssueDetailsButton {...props} />);
const button = result.find(DefaultButton);
onClickMock.setup(m => m(It.isAny())).verifiable(Times.once());
// tslint:disable-next-line: await-promise
await button.simulate('click');

const toast = result.find(Toast);

expect(toast.state().toastVisible).toBe(true);
expect(toast.state().content).toBe('Failed to copy failure details.');

verifyMocks();
});
});

function verifyMocks(): void {
Expand Down
Loading