diff --git a/ci/pipeline.yml b/ci/pipeline.yml
index 7366662de..80ea954b6 100644
--- a/ci/pipeline.yml
+++ b/ci/pipeline.yml
@@ -378,6 +378,9 @@ jobs:
- task: code-coverage-diff
image: node
file: src/ci/partials/code-coverage-diff.yml
+ params:
+ APP_ENV: ((deploy-env))
+ APP_HOSTNAME: https://((((deploy-env))-pages-domain))
#@ if/end env != 'production':
- name: e2e-test
diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md
index 00ee2c408..2ae5d8697 100644
--- a/docs/DEVELOPMENT.md
+++ b/docs/DEVELOPMENT.md
@@ -561,3 +561,4 @@ Best practices for writing tests:
- Prefer click events to direct function calls
- [Query for elements](https://testing-library.com/docs/queries/about/#priority) based on role, label, or text before falling back to `test ids` (and never classes or ids)
- Simulated data needed for tests ("fixtures") should be located outside the test file itself, except in the case of single values (e.g. fake `Build` model data should be in a separate file but having an inline variable like `const testText = 'sample-data'` is fine)
+- User events should use `@testing-library/user-event` since it simulates user interactions by dispatching the events that would happen if the interaction took place in a browser. Using the built-in `fireEvent` provides a browser's low-level `dispatchEvent` API but it cannot properly simulate more complex interactions like typing into a text box.
diff --git a/frontend/shared/GithubAuthButton.jsx b/frontend/shared/GithubAuthButton.jsx
index f7d9c3c47..45f155a38 100644
--- a/frontend/shared/GithubAuthButton.jsx
+++ b/frontend/shared/GithubAuthButton.jsx
@@ -53,7 +53,7 @@ function authorize(revokeFirst) {
return authPromise();
}
-const GithubAuthButton = ({ onFailure, onSuccess, text, revokeFirst }) => (
+const GithubAuthButton = ({ onFailure, onSuccess, text, revokeFirst = false }) => (
{text}
(
className="usa-button github-auth-button"
onClick={() => authorize(revokeFirst).then(onSuccess).catch(onFailure)}
>
- Connect with Github
+ Connect with GitHub
);
GithubAuthButton.propTypes = {
- onFailure: PropTypes.func,
- onSuccess: PropTypes.func,
- text: PropTypes.string.isRequired,
+ onFailure: PropTypes.func.isRequired,
+ onSuccess: PropTypes.func.isRequired,
revokeFirst: PropTypes.bool,
-};
-
-GithubAuthButton.defaultProps = {
- onFailure: () => {},
- onSuccess: () => {},
- revokeFirst: false,
+ text: PropTypes.string.isRequired,
};
export default GithubAuthButton;
diff --git a/frontend/shared/GithubAuthButton.test.jsx b/frontend/shared/GithubAuthButton.test.jsx
new file mode 100644
index 000000000..17d1805d1
--- /dev/null
+++ b/frontend/shared/GithubAuthButton.test.jsx
@@ -0,0 +1,193 @@
+import React from 'react';
+import { render, screen, waitFor } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import { spy, stub, restore } from 'sinon';
+import '@testing-library/jest-dom';
+import globals from '../globals';
+
+import api from '@util/federalistApi';
+import GithubAuthButton from './GithubAuthButton';
+
+const BUTTON_TEXT = `Connect with GitHub`;
+const text = 'Hello World';
+
+// We need to create the MessageEvent to set the origin
+// and data when testing
+// https://github.com/jsdom/jsdom/issues/2745#issuecomment-1207414024
+function createPostMessageEvent(data, origin) {
+ return new MessageEvent('message', {
+ source: window,
+ origin,
+ data,
+ });
+}
+
+function createStubs() {
+ const onFailure = spy();
+ const onSuccess = spy();
+ const apiStub = stub(api, 'revokeApplicationGrant').resolves();
+ const focusStub = stub().resolves();
+ const closeStub = stub().returns();
+ const windowStub = stub(window, 'open').returns({
+ focus: focusStub,
+ close: closeStub,
+ });
+
+ return {
+ onFailure,
+ onSuccess,
+ apiStub,
+ focusStub,
+ closeStub,
+ windowStub,
+ };
+}
+
+describe(' ', () => {
+ let onFailure;
+ let onSuccess;
+ let apiStub;
+ let focusStub;
+ let closeStub;
+ let windowStub;
+
+ const user = userEvent.setup();
+
+ beforeAll(() => {
+ // Set window origin based on globals APP_HOSTNAME
+ window['origin'] = globals.APP_HOSTNAME;
+
+ // Set screen height and width
+ window['screen'] = {
+ width: 1200,
+ height: 1000,
+ };
+ });
+
+ beforeEach(() => {
+ ({ onFailure, onSuccess, apiStub, focusStub, closeStub, windowStub } = createStubs());
+ });
+
+ afterEach(() => restore());
+
+ it('renders', () => {
+ const props = { onFailure, onSuccess, text };
+ render( );
+
+ expect(screen.getByText(text)).toBeTruthy();
+
+ const button = screen.getByRole('button');
+ expect(button).toHaveTextContent(BUTTON_TEXT);
+
+ expect(onFailure.notCalled).toBe(true);
+ expect(onSuccess.notCalled).toBe(true);
+ });
+
+ it('opens window, calls the revokeFirst and then succeeds', async () => {
+ const props = { onFailure, onSuccess, text, revokeFirst: true };
+ render( );
+
+ await user.click(screen.getByRole('button'));
+
+ expect(apiStub.calledOnce).toBe(true);
+ await waitFor(() => expect(windowStub.calledOnce).toBe(true));
+ await waitFor(() => expect(focusStub.calledOnce).toBe(true));
+ await waitFor(() =>
+ window.dispatchEvent(createPostMessageEvent('success', window.origin)),
+ );
+
+ expect(closeStub.calledOnce).toBe(true);
+ expect(onSuccess.calledOnce).toBe(true);
+ expect(onFailure.notCalled).toBe(true);
+ });
+
+ it('opens window with revokeFirst and fails authorize', async () => {
+ const props = { onFailure, onSuccess, text, revokeFirst: true };
+ render( );
+
+ await user.click(screen.getByRole('button'));
+
+ expect(apiStub.calledOnce).toBe(true);
+ await waitFor(() => expect(windowStub.calledOnce).toBe(true));
+ await waitFor(() => expect(focusStub.calledOnce).toBe(true));
+ await waitFor(() =>
+ window.dispatchEvent(createPostMessageEvent('fail', window.origin)),
+ );
+
+ expect(closeStub.notCalled).toBe(true);
+ expect(onSuccess.notCalled).toBe(true);
+ expect(onFailure.calledOnce).toBe(true);
+ });
+
+ it('opens window with revokeFirst and fails origin', async () => {
+ const props = { onFailure, onSuccess, text, revokeFirst: true };
+ render( );
+
+ await user.click(screen.getByRole('button'));
+
+ expect(apiStub.calledOnce).toBe(true);
+ await waitFor(() => expect(windowStub.calledOnce).toBe(true));
+ await waitFor(() => expect(focusStub.calledOnce).toBe(true));
+ await waitFor(() =>
+ window.dispatchEvent(createPostMessageEvent('success', 'http://wrongorigin')),
+ );
+
+ expect(closeStub.notCalled).toBe(true);
+ expect(onSuccess.notCalled).toBe(true);
+ expect(onFailure.calledOnce).toBe(true);
+ });
+
+ it('opens window without revokeFirst and then succeeds', async () => {
+ const props = { onFailure, onSuccess, text };
+ render( );
+
+ await user.click(screen.getByRole('button'));
+
+ expect(apiStub.notCalled).toBe(true);
+ await waitFor(() => expect(windowStub.calledOnce).toBe(true));
+ await waitFor(() => expect(focusStub.calledOnce).toBe(true));
+ await waitFor(() =>
+ window.dispatchEvent(createPostMessageEvent('success', window.origin)),
+ );
+
+ expect(closeStub.calledOnce).toBe(true);
+ expect(onSuccess.calledOnce).toBe(true);
+ expect(onFailure.notCalled).toBe(true);
+ });
+
+ it('opens window without revokeFirst and fails authorize', async () => {
+ const props = { onFailure, onSuccess, text };
+ render( );
+
+ await user.click(screen.getByRole('button'));
+
+ expect(apiStub.notCalled).toBe(true);
+ await waitFor(() => expect(windowStub.calledOnce).toBe(true));
+ await waitFor(() => expect(focusStub.calledOnce).toBe(true));
+ await waitFor(() =>
+ window.dispatchEvent(createPostMessageEvent('fail', window.origin)),
+ );
+
+ expect(closeStub.notCalled).toBe(true);
+ expect(onSuccess.notCalled).toBe(true);
+ expect(onFailure.calledOnce).toBe(true);
+ });
+
+ it('opens window without revokeFirst and fails origin', async () => {
+ const props = { onFailure, onSuccess, text };
+ render( );
+
+ await user.click(screen.getByRole('button'));
+
+ expect(apiStub.notCalled).toBe(true);
+ await waitFor(() => expect(windowStub.calledOnce).toBe(true));
+ await waitFor(() => expect(focusStub.calledOnce).toBe(true));
+ await waitFor(() =>
+ window.dispatchEvent(createPostMessageEvent('success', 'http://wrongorigin')),
+ );
+
+ expect(closeStub.notCalled).toBe(true);
+ expect(onSuccess.notCalled).toBe(true);
+ expect(onFailure.calledOnce).toBe(true);
+ });
+});