added test suite for chatbox, use accept/reject buttons for ToS instead of typing answer

This commit is contained in:
Sharon Kennedy
2020-03-12 13:08:57 -04:00
parent 1c96d11443
commit a97696f687
11 changed files with 324 additions and 262 deletions

View File

@@ -1,197 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<Widget /> open/close 1`] = `
<Widget
bodyText="Body"
footerText="Footer"
headerText="Header"
>
<div
className="docked-widget"
>
<Transition
appear={false}
enter={true}
exit={true}
in={false}
mountOnEnter={false}
onEnter={[Function]}
onEntered={[Function]}
onEntering={[Function]}
onExit={[Function]}
onExited={[Function]}
onExiting={[Function]}
timeout={250}
unmountOnExit={false}
>
<div
className="widget widget-exited"
>
<div
className="widget-header"
>
<div
className="widget-header-title"
>
Header
</div>
<button
className="widget-header-icon"
onClick={[Function]}
onKeyPress={[Function]}
type="button"
>
X
</button>
</div>
<div
className="widget-body"
>
Body
</div>
<div
className="widget-footer"
>
Footer
</div>
</div>
</Transition>
<button
className="dock"
onClick={[Function]}
onKeyPress={[Function]}
type="button"
>
^ OPEN ^
</button>
</div>
</Widget>
`;
exports[`<Widget /> open/close 2`] = `
<Widget
bodyText="Body"
footerText="Footer"
headerText="Header"
>
<div
className="docked-widget"
>
<Transition
appear={false}
enter={true}
exit={true}
in={true}
mountOnEnter={false}
onEnter={[Function]}
onEntered={[Function]}
onEntering={[Function]}
onExit={[Function]}
onExited={[Function]}
onExiting={[Function]}
timeout={250}
unmountOnExit={false}
>
<div
className="widget widget-entering"
>
<div
className="widget-header"
>
<div
className="widget-header-title"
>
Header
</div>
<button
className="widget-header-icon"
onClick={[Function]}
onKeyPress={[Function]}
type="button"
>
X
</button>
</div>
<div
className="widget-body"
>
Body
</div>
<div
className="widget-footer"
>
Footer
</div>
</div>
</Transition>
</div>
</Widget>
`;
exports[`<Widget /> open/close 3`] = `
<Widget
bodyText="Body"
footerText="Footer"
headerText="Header"
>
<div
className="docked-widget"
>
<Transition
appear={false}
enter={true}
exit={true}
in={false}
mountOnEnter={false}
onEnter={[Function]}
onEntered={[Function]}
onEntering={[Function]}
onExit={[Function]}
onExited={[Function]}
onExiting={[Function]}
timeout={250}
unmountOnExit={false}
>
<div
className="widget widget-exited"
>
<div
className="widget-header"
>
<div
className="widget-header-title"
>
Header
</div>
<button
className="widget-header-icon"
onClick={[Function]}
onKeyPress={[Function]}
type="button"
>
X
</button>
</div>
<div
className="widget-body"
>
Body
</div>
<div
className="widget-footer"
>
Footer
</div>
</div>
</Transition>
<button
className="dock"
onClick={[Function]}
onKeyPress={[Function]}
type="button"
>
^ OPEN ^
</button>
</div>
</Widget>
`;

View File

@@ -270,6 +270,42 @@
width: fit-content;
}
.buttons {
display: flex;
align-items: center;
font-size: 1rem;
button {
background-color: transparent;
padding: 0.25rem 0.5rem;
font-size: 0.9rem;
color: inherit;
font-weight: bold;
font-family: $theme-font;
cursor: pointer;
display: flex;
flex: 0 1 auto;
border: 1px solid $theme-color;
transition: all 0.2s ease-in-out;
border-radius: 10px;
margin-left: 4px;
&:hover {
border: 1px solid $dark-color;
box-shadow: inset 0px 0px 0px 1px $dark-color;
}
&:focus {
outline: none;
color: $white;
border: 1px solid $dark-color;
box-shadow: inset 0px 0px 0px 1px $dark-color;
background-color: $theme-highlight-color;
}
}
}
&.from-bot {
color: $gray-color;
font-size: 0.9rem;

View File

@@ -24,11 +24,14 @@ const ENCRYPTION_NOTICE = "Messages in this chat are secured with end-to-end enc
const UNENCRYPTION_NOTICE = "End-to-end message encryption is not available on this browser."
const RESTARTING_UNENCRYPTED_CHAT_MESSAGE = "Restarting chat without encryption."
const DEFAULT_MATRIX_SERVER = "https://matrix.rhok.space/"
const DEFAULT_BOT_USERNAME = "@help-bot:rhok.space"
const DEFAULT_TERMS_URL = "https://tosdr.org/"
const DEFAULT_ROOM_NAME = "Support Chat"
const DEFAULT_INTRO_MESSAGE = "This chat application does not collect any of your personal data or any data from your use of this service."
const DEFAULT_AGREEMENT_MESSAGE = "👉 Do you want to continue? Type yes or no."
const DEFAULT_AGREEMENT_MESSAGE = "Do you want to continue?"
const DEFAULT_CONFIRMATION_MESSAGE = "Waiting for a facilitator to join the chat..."
const DEFAULT_EXIT_MESSAGE = "The chat was not started."
const DEFAULT_EXIT_MESSAGE = "The chat is closed. You may close this window."
const DEFAULT_ANONYMOUS_DISPLAY_NAME="Anonymous"
const DEFAULT_CHAT_UNAVAILABLE_MESSAGE = "The chat service is not available right now. Please try again later."
@@ -36,7 +39,6 @@ const DEFAULT_CHAT_UNAVAILABLE_MESSAGE = "The chat service is not available righ
class ChatBox extends React.Component {
constructor(props) {
super(props)
const client = matrix.createClient(this.props.matrixServerUrl)
this.initialState = {
opened: false,
showDock: true,
@@ -46,38 +48,17 @@ class ChatBox extends React.Component {
userId: null,
password: null,
localStorage: null,
messages: [
{
id: 'intro-msg-id',
type: 'm.room.message',
sender: this.props.botUsername,
content: { body: this.props.introMessage },
},
{
id: 'terms-msg-id',
type: 'm.room.message',
sender: this.props.botUsername,
content: {
body: `Please read the full terms and conditions at ${this.props.termsUrl}.`,
formatted_body: `Please read the full <a href="${this.props.termsUrl}">terms and conditions</a>.`
}
},
{
id: 'agreement-msg-id',
type: 'm.room.message',
sender: this.props.botUsername,
content: { body: this.props.agreementMessage }, },
],
messages: [],
inputValue: "",
errors: [],
roomId: null,
typingStatus: null,
awaitingAgreement: true,
awaitingFacilitator: false,
}
this.state = this.initialState
this.chatboxInput = React.createRef();
this.messageWindow = React.createRef();
this.termsUrl = React.createRef();
}
handleToggleOpen = () => {
@@ -100,7 +81,11 @@ class ChatBox extends React.Component {
}
handleWidgetEnter = () => {
this.chatboxInput.current.focus()
if (this.state.awaitingAgreement) {
this.termsUrl.current.focus()
} else {
this.chatboxInput.current.focus()
}
}
handleExitChat = () => {
@@ -442,24 +427,20 @@ class ChatBox extends React.Component {
this.setState({ inputValue: e.currentTarget.value })
}
handleAcceptTerms = () => {
this.setState({ awaitingAgreement: false })
this.initializeChat()
}
handleRejectTerms = () => {
this.exitChat()
this.displayBotMessage({ body: this.props.exitMessage })
}
handleSubmit = e => {
e.preventDefault()
if (!Boolean(this.state.inputValue)) return null;
if (this.state.awaitingAgreement && !this.state.client) {
if (this.state.inputValue.toLowerCase() === 'yes') {
this.displayFakeMessage({ body: this.state.inputValue }, 'from-me')
this.setState({ inputValue: "" })
return this.initializeChat()
} else {
this.displayFakeMessage({ body: this.state.inputValue }, 'from-me')
this.displayBotMessage({ body: this.props.exitMessage })
return this.setState({ inputValue: "" })
}
}
if (this.state.client && this.state.roomId) {
return this.sendMessage()
}
@@ -480,6 +461,26 @@ class ChatBox extends React.Component {
<div className="message-window" ref={this.messageWindow}>
<div className="messages">
<div className={`message from-bot`}>
<div className="text">{ this.props.introMessage }</div>
</div>
<div className={`message from-bot`}>
<div className="text">Please read the full <a href={this.props.termsUrl} ref={this.termsUrl}>terms and conditions</a>. By using this chat, you agree to these terms.</div>
</div>
<div className={`message from-bot`}>
<div className="text">{ this.props.agreementMessage }</div>
</div>
<div className={`message from-bot`}>
<div className="text buttons">
{`👉`}
<button id="accept" onClick={this.handleAcceptTerms}>YES</button>
<button id="reject" onClick={this.handleRejectTerms}>NO</button>
</div>
</div>
{
messages.map((message, index) => {
return(
@@ -537,6 +538,9 @@ ChatBox.propTypes = {
}
ChatBox.defaultProps = {
matrixServerUrl: DEFAULT_MATRIX_SERVER,
botUsername: DEFAULT_BOT_USERNAME,
termsUrl: DEFAULT_TERMS_URL,
roomName: DEFAULT_ROOM_NAME,
introMessage: DEFAULT_INTRO_MESSAGE,
agreementMessage: DEFAULT_AGREEMENT_MESSAGE,

View File

@@ -0,0 +1,167 @@
import React from 'react';
import Chatbox from './chatbox';
import mockMatrix, { mockCreateClient } from "matrix-js-sdk";
import { mount, shallow } from 'enzyme';
import { createWaitForElement } from 'enzyme-wait';
import { config } from 'react-transition-group';
config.disabled = true
const testConfig = {
matrixServerUrl: 'https://matrix.rhok.space',
botUsername: '@help-bot:rhok.space',
roomName: 'Support Chat',
termsUrl: 'https://tosdr.org/',
introMessage: 'This chat application does not collect any of your personal data or any data from your use of this service.',
agreementMessage: '👉 Do you want to continue? Type yes or no.',
confirmationMessage: 'Waiting for a facilitator to join the chat...',
exitMessage: 'The chat was not started.',
chatUnavailableMessage: 'The chat service is not available right now. Please try again later.',
anonymousDisplayName: 'Anonymous',
}
describe('Chatbox', () => {
test('chat window should open and close', async () => {
const chatbox = mount(<Chatbox {...testConfig} />)
let dock = chatbox.find('button.dock')
let chatWindow = chatbox.find('.widget')
expect(dock.length).toEqual(1)
expect(chatWindow.hasClass('widget-exited')).toEqual(true)
// open chat window
dock.simulate('click')
const openChatWindow = await createWaitForElement('.widget-entered')(chatbox)
dock = chatbox.find('button.dock')
expect(openChatWindow.length).toEqual(1)
expect(dock.length).toEqual(0)
// close chat window
const closeButton = chatbox.find('button.widget-header-close')
closeButton.simulate('click')
chatWindow = chatbox.find('.widget')
dock = chatbox.find('button.dock')
expect(dock.length).toEqual(1)
expect(chatWindow.hasClass('widget-exited')).toEqual(true)
})
test('chat window should contain the right messages', () => {
const chatbox = mount(<Chatbox {...testConfig} />)
const props = chatbox.props()
const messages = chatbox.find('.messages')
expect(messages.text()).toContain(props.introMessage)
expect(messages.html()).toContain(props.termsUrl)
expect(messages.text()).toContain(props.agreementMessage)
});
test('#handleExitChat should call exitChat if the client has been initialized', () => {
})
test('#exitChat should leave the room and destroy client', () => {
// leave room
// deactivate account
// stop client
// clear stores
// reset initial state
})
test('agreeing to terms should start encrypted chat', async () => {
const chatbox = mount(<Chatbox {...testConfig} />)
const dock = chatbox.find('button.dock')
dock.simulate('click')
const yesButton = chatbox.find('#accept')
yesButton.simulate('click')
expect(mockCreateClient).toHaveBeenCalled()
})
// test('rejecting terms should not start chat', async () => {
// const chatbox = mount(<Chatbox {...testConfig} />)
// const dock = chatbox.find('button.dock')
// dock.simulate('click')
// const noButton = chatbox.find('#reject')
// noButton.simulate('click')
// expect(mockMatrix.mockCreateClient.mock.calls.length).toEqual(0)
// })
test('#initializeChat should notify user if client fails to initialize', () => {
// handleInitError
})
test('#initializeChat should create unencypted chat if initCrypto fails', () => {
// initializeUnencryptedChat
})
test('#initializeUnencryptedChat should initialize an unencrypted client', () => {
// initializeUnencryptedChat
})
test('#handleDecryptionError should restart client without encryption and notify user', () => {
// initializeUnencryptedChat
})
test('#verifyAllRoomDevices should mark all devices in the room as verified devices', () => {
})
test('#createRoom should create a new encrypted room with bot as admin', () => {
})
test('#createRoom should create a new unencrypted room if encryption is not enabled', () => {
})
test('#sendMessage should send text message with input value', () => {
})
test('#sendMessage should mark devices as known and retry sending on UnknownDeviceError', () => {
})
test('#sendMessage should mark devices as known and retry sending on UnknownDeviceError', () => {
})
test('#displayFakeMessage should add a message object to message list', () => {
})
test('#displayBotMessage should add a message object with bot as sender to message list', () => {
})
test('#handleMessageEvent should add received message to message list', () => {
})
test('#componentDidUpdate should set state listeners', () => {
})
test('#handleSubmit should listen for yes if awaiting agreement and initialize client', () => {
})
test('#handleSubmit should listen for no if awaiting agreement and do nothing', () => {
})
test('#handleSubmit should send message if awaitingAgreement is false', () => {
})
});

View File

@@ -6,12 +6,20 @@ export default function bookmarklet() {
}
window.EmbeddableChatbox = EmbeddableChatbox;
EmbeddableChatbox.mount({
termsUrl: 'https://tosdr.org/',
privacyStatement: 'This chat application does not collect any of your personal data or any data from your use of this service.',
var config = {
matrixServerUrl: 'https://matrix.rhok.space',
botUsername: '@help-bot:rhok.space',
roomName: 'Support Chat',
});
termsUrl: 'https://tosdr.org/',
introMessage: 'This chat application does not collect any of your personal data or any data from your use of this service.',
agreementMessage: '👉 Do you want to continue? Type yes or no.',
confirmationMessage: 'Waiting for a facilitator to join the chat...',
exitMessage: 'The chat was not started.',
chatUnavailableMessage: 'The chat service is not available right now. Please try again later.',
anonymousDisplayName: 'Anonymous',
}
EmbeddableChatbox.mount(config);
}
bookmarklet();

View File

@@ -6,11 +6,11 @@ describe('bookmarklet', () => {
const el = document.querySelectorAll('body > div');
ReactDOM.unmountComponentAtNode(el[0]);
el[0].parentNode.removeChild(el[0]);
window.EmbeddableWidget = null;
window.EmbeddableChatbox = null;
});
test('#mount document becomes ready', async () => {
expect(window.EmbeddableWidget).not.toBeNull();
expect(window.EmbeddableChatbox).not.toBeNull();
bookmarklet();
const el = document.querySelectorAll('body > div');
expect(el).toHaveLength(1);

View File

@@ -1,23 +1,24 @@
import EmbeddableWidget from './embeddable-widget';
import EmbeddableChatbox from './embeddable-chatbox';
import { waitForSelection } from '../test-helpers';
describe('EmbeddableWidget', () => {
describe('EmbeddableChatbox', () => {
afterEach(() => {
document.readyState = 'complete';
if (EmbeddableWidget.el) {
EmbeddableWidget.unmount();
if (EmbeddableChatbox.el) {
EmbeddableChatbox.unmount();
}
});
test('#mount document becomes ready', async () => {
document.readyState = 'loading';
EmbeddableWidget.mount();
EmbeddableChatbox.mount();
window.dispatchEvent(new Event('load', {}));
await waitForSelection(document, 'div');
});
test('#mount document complete', async () => {
EmbeddableWidget.mount();
EmbeddableChatbox.mount();
await waitForSelection(document, 'div');
});
@@ -26,7 +27,7 @@ describe('EmbeddableWidget', () => {
newElement.setAttribute('id', 'widget-mount');
document.body.appendChild(newElement);
EmbeddableWidget.mount({
EmbeddableChatbox.mount({
parentElement: '#widget-mount',
});
@@ -36,8 +37,8 @@ describe('EmbeddableWidget', () => {
});
test('#mount twice', async () => {
EmbeddableWidget.mount();
expect(() => EmbeddableWidget.mount()).toThrow('already mounted');
EmbeddableChatbox.mount();
expect(() => EmbeddableChatbox.mount()).toThrow('already mounted');
});
test('#unmount', async () => {
@@ -45,13 +46,13 @@ describe('EmbeddableWidget', () => {
document.body.appendChild(el);
expect(document.querySelectorAll('div')).toHaveLength(1);
EmbeddableWidget.el = el;
EmbeddableWidget.unmount();
EmbeddableChatbox.el = el;
EmbeddableChatbox.unmount();
expect(document.querySelectorAll('div')).toHaveLength(0);
});
test('#unmount without mounting', async () => {
expect(() => EmbeddableWidget.unmount()).toThrow('not mounted');
expect(() => EmbeddableChatbox.unmount()).toThrow('not mounted');
});
});