import { fireEvent, render, screen, waitFor } from '@testing-library/react' import { expect } from 'chai' import sinon from 'sinon' import fetchMock from 'fetch-mock' import Input, { clearDomainCache, } from '../../../../../../frontend/js/features/settings/components/emails/add-email/input' import domainBlocklist from '../../../../../../frontend/js/features/settings/domain-blocklist' const testInstitutionData = [ { university: { id: 124 }, hostname: 'domain.edu' }, ] describe('', function () { const defaultProps = { onChange: (value: string) => {}, handleAddNewEmail: () => {}, } beforeEach(function () { clearDomainCache() fetchMock.reset() }) describe('on initial render', function () { it('should render an input with a placeholder', function () { render() screen.getByPlaceholderText('e.g. johndoe@mit.edu') }) it('should not dispatch any `change` event', function () { const onChangeStub = sinon.stub() render() expect(onChangeStub.called).to.equal(false) }) }) describe('when typing text that does not contain any potential domain match', function () { let onChangeStub: sinon.SinonStub let handleAddNewEmailStub: sinon.SinonStub beforeEach(function () { fetchMock.get('express:/institutions/domains', 200) onChangeStub = sinon.stub() handleAddNewEmailStub = sinon.stub() render( ) fireEvent.change(screen.getByTestId('affiliations-email'), { target: { value: 'user' }, }) }) it('should render the text being typed', function () { const input = screen.getByTestId('affiliations-email') as HTMLInputElement expect(input.value).to.equal('user') }) it('should dispatch a `change` event on every stroke', function () { expect(onChangeStub.calledWith('user')).to.equal(true) fireEvent.change(screen.getByTestId('affiliations-email'), { target: { value: 's' }, }) expect(onChangeStub.calledWith('s')).to.equal(true) }) it('should not make any request for institution domains', function () { expect(fetchMock.called()).to.be.false }) it('should submit on Enter if email looks valid', async function () { fireEvent.change(screen.getByTestId('affiliations-email'), { target: { value: 'user@domain.com' }, }) fireEvent.keyDown(screen.getByTestId('affiliations-email'), { key: 'Enter', }) expect(handleAddNewEmailStub.calledWith()).to.equal(true) }) it('should not submit on Enter if email does not look valid', async function () { fireEvent.change(screen.getByTestId('affiliations-email'), { target: { value: 'user@' }, }) fireEvent.keyDown(screen.getByTestId('affiliations-email'), { key: 'Enter', }) expect(handleAddNewEmailStub.calledWith()).to.equal(false) }) }) describe('when typing text that contains a potential domain match', function () { let onChangeStub: sinon.SinonStub beforeEach(function () { onChangeStub = sinon.stub() render() }) describe('when there are no matches', function () { beforeEach(function () { fetchMock.get('express:/institutions/domains', 200) fireEvent.change(screen.getByTestId('affiliations-email'), { target: { value: 'user@d' }, }) }) it('should render the text being typed', function () { const input = screen.getByTestId( 'affiliations-email' ) as HTMLInputElement expect(input.value).to.equal('user@d') }) }) describe('when there is a domain match', function () { beforeEach(function () { fetchMock.get('express:/institutions/domains', testInstitutionData) fireEvent.change(screen.getByTestId('affiliations-email'), { target: { value: 'user@d' }, }) }) it('should render the text being typed along with the suggestion', async function () { const input = screen.getByTestId( 'affiliations-email' ) as HTMLInputElement expect(input.value).to.equal('user@d') await waitFor(() => { const shadowInput = screen.getByTestId( 'affiliations-email-shadow' ) as HTMLInputElement expect(shadowInput.value).to.equal('user@domain.edu') }) }) it('should dispatch a `change` event with the typed text', function () { expect(onChangeStub.calledWith('user@d')).to.equal(true) }) it('should dispatch a `change` event with institution data when the typed email contains the institution domain', async function () { fireEvent.change(screen.getByTestId('affiliations-email'), { target: { value: 'user@domain.edu' }, }) await fetchMock.flush(true) expect( onChangeStub.calledWith( 'user@domain.edu', sinon.match(testInstitutionData[0]) ) ).to.equal(true) }) it('should clear the suggestion when the potential domain match is completely deleted', async function () { await waitFor(() => { const shadowInput = screen.getByTestId( 'affiliations-email-shadow' ) as HTMLInputElement expect(shadowInput.value).to.equal('user@domain.edu') }) fireEvent.change(screen.getByTestId('affiliations-email'), { target: { value: '' }, }) expect(screen.queryByText('user@domain.edu')).to.be.null }) describe('when there is a suggestion and "Tab" key is pressed', function () { beforeEach(async function () { // wait until autocompletion available await waitFor(() => { const shadowInput = screen.getByTestId( 'affiliations-email-shadow' ) as HTMLInputElement expect(shadowInput.value).to.equal('user@domain.edu') }) fireEvent.keyDown(screen.getByTestId('affiliations-email'), { key: 'Tab', }) }) it('it should autocomplete the input', async function () { const input = screen.getByTestId( 'affiliations-email' ) as HTMLInputElement expect(input.value).to.equal('user@domain.edu') }) it('should dispatch a `change` event with the domain matched', async function () { expect( onChangeStub.calledWith( 'user@domain.edu', sinon.match(testInstitutionData[0]) ) ).to.equal(true) }) }) describe('when there is a suggestion and "Enter" key is pressed', function () { beforeEach(async function () { // wait until autocompletion available await waitFor(() => { const shadowInput = screen.getByTestId( 'affiliations-email-shadow' ) as HTMLInputElement expect(shadowInput.value).to.equal('user@domain.edu') }) fireEvent.keyDown(screen.getByTestId('affiliations-email'), { key: 'Enter', }) }) it('it should autocomplete the input', async function () { const input = screen.getByTestId( 'affiliations-email' ) as HTMLInputElement expect(input.value).to.equal('user@domain.edu') }) it('should dispatch a `change` event with the domain matched', async function () { expect( onChangeStub.calledWith( 'user@domain.edu', sinon.match(testInstitutionData[0]) ) ).to.equal(true) }) }) it('should cache the result and skip subsequent requests', async function () { fetchMock.reset() // clear input fireEvent.change(screen.getByTestId('affiliations-email'), { target: { value: '' }, }) // type a hint to trigger the domain search fireEvent.change(screen.getByTestId('affiliations-email'), { target: { value: 'user@d' }, }) expect(fetchMock.called()).to.be.false expect(onChangeStub.calledWith('user@d')).to.equal(true) await waitFor(() => { const shadowInput = screen.getByTestId( 'affiliations-email-shadow' ) as HTMLInputElement expect(shadowInput.value).to.equal('user@domain.edu') }) }) }) describe('when there is a match for a blocklisted domain', function () { const [blockedDomain] = domainBlocklist afterEach(function () { clearDomainCache() fetchMock.reset() }) it('should not render the suggestion with blocked domain', async function () { const blockedInstitution = [ { university: { id: 1 }, hostname: blockedDomain }, ] fetchMock.get('express:/institutions/domains', blockedInstitution) fireEvent.change(screen.getByTestId('affiliations-email'), { target: { value: `user@${blockedDomain.split('.')[0]}` }, }) await fetchMock.flush(true) expect(screen.queryByText(`user@${blockedDomain}`)).to.be.null }) it('should not render the suggestion with blocked domain having a subdomain', async function () { const blockedInstitution = [ { university: { id: 1 }, hostname: `subdomain.${blockedDomain}`, }, ] fetchMock.get('express:/institutions/domains', blockedInstitution) fireEvent.change(screen.getByTestId('affiliations-email'), { target: { value: `user@subdomain.${blockedDomain.split('.')[0]}`, }, }) await fetchMock.flush(true) expect(screen.queryByText(`user@subdomain.${blockedDomain}`)).to.be.null }) }) describe('while waiting for a response', function () { beforeEach(async function () { // type an initial suggestion fetchMock.get('express:/institutions/domains', testInstitutionData) fireEvent.change(screen.getByTestId('affiliations-email'), { target: { value: 'user@d' }, }) await waitFor(() => { const shadowInput = screen.getByTestId( 'affiliations-email-shadow' ) as HTMLInputElement expect(shadowInput.value).to.equal('user@domain.edu') }) // make sure the next suggestions are delayed clearDomainCache() fetchMock.reset() fetchMock.get('express:/institutions/domains', 200, { delay: 1000 }) }) it('should keep the suggestion if the hint matches the previously matched domain', async function () { fireEvent.change(screen.getByTestId('affiliations-email'), { target: { value: 'user@do' }, }) const shadowInput = screen.getByTestId( 'affiliations-email-shadow' ) as HTMLInputElement expect(shadowInput.value).to.equal('user@domain.edu') }) it('should remove the suggestion if the hint does not match the previously matched domain', async function () { fireEvent.change(screen.getByTestId('affiliations-email'), { target: { value: 'user@foo' }, }) await waitFor(() => { const shadowInput = screen.getByTestId( 'affiliations-email-shadow' ) as HTMLInputElement expect(shadowInput.value).to.equal('') }) }) }) }) describe('when the request to fetch institution domains fail', function () { let onChangeStub beforeEach(async function () { // initial request populates the suggestion fetchMock.get('express:/institutions/domains', testInstitutionData) onChangeStub = sinon.stub() render() fireEvent.change(screen.getByTestId('affiliations-email'), { target: { value: 'user@d' }, }) await waitFor(() => { const shadowInput = screen.getByTestId( 'affiliations-email-shadow' ) as HTMLInputElement expect(shadowInput.value).to.equal('user@domain.edu') }) // subsequent requests fail fetchMock.reset() fetchMock.get('express:/institutions/domains', 500) }) it('should clear suggestions', async function () { fireEvent.change(screen.getByTestId('affiliations-email'), { target: { value: 'user@dom' }, }) const input = screen.getByTestId('affiliations-email') as HTMLInputElement expect(input.value).to.equal('user@dom') await waitFor(() => { const shadowInput = screen.getByTestId( 'affiliations-email-shadow' ) as HTMLInputElement expect(shadowInput.value).to.equal('') }) expect(fetchMock.called()).to.be.true // ensures `domainCache` hasn't been hit }) }) describe('when the request to fetch institution is not matching input', function () { it('should clear suggestion', async function () { fetchMock.get('express:/institutions/domains', testInstitutionData) render() fireEvent.change(screen.getByTestId('affiliations-email'), { target: { value: 'user@other' }, }) await fetchMock.flush(true) const shadowInput = screen.getByTestId( 'affiliations-email-shadow' ) as HTMLInputElement expect(shadowInput.value).to.equal('') }) }) })