- Modernize the `RegisterPanel` component and turn it into a Lit element.
- Improve CSS and move into plugin.
- Fix button click handler not being registered.
- Fix switching between login/register form after logging out (Fixes #1556)
This commit is contained in:
JC Brand 2023-02-16 22:10:14 +01:00
parent 8035084e8e
commit 7b8b32638c
12 changed files with 296 additions and 209 deletions

View File

@ -1,5 +1,10 @@
# Changelog # Changelog
## 10.1.2 (Unreleased)
- #1556: Can't switch to registration form afrer logout
- #3137: Various UI/UX bugfixes regarding the registration form
## 10.1.1 (2023-02-15) ## 10.1.1 (2023-02-15)
- #1851: Sort open groupchats alphabetically - #1851: Sort open groupchats alphabetically

View File

@ -60,7 +60,7 @@ converse.plugins.add('converse-controlbox', {
sticky_controlbox: false sticky_controlbox: false
}); });
api.promises.add('controlBoxInitialized'); api.promises.add('controlBoxInitialized', false);
Object.assign(api, controlbox_api); Object.assign(api, controlbox_api);
_converse.ControlBoxView = ControlBoxView; _converse.ControlBoxView = ControlBoxView;

View File

@ -109,56 +109,6 @@
font-weight: bold; font-weight: bold;
} }
#converse-register {
@include fade-in;
background-color: var(--controlbox-pane-background-color);
.title {
font-weight: bold;
}
.info {
color: green;
font-size: 90%;
margin: 1.5em 0;
}
.form-errors {
color: var(--error-color);
margin: 1em 0;
}
.provider-title {
font-size: var(--font-size-huge);
margin: 0;
}
.provider-score {
width: 178px;
margin-bottom: 8px;
}
.form-help .url {
font-weight: bold;
color: var(--link-color);
}
.input-group {
display: table;
margin: auto;
width: 100%;
span {
overflow-x: hidden;
text-overflow: ellipsis;
max-width: 110px;
}
span, input[name=username] {
display: table-cell;
text-align: left;
}
}
.instructions {
color: gray;
font-size: 85%;
&:hover {
color: var(--controlbox-text-color);
}
}
}
.conn-feedback { .conn-feedback {
color: var(--controlbox-head-color); color: var(--controlbox-head-color);
&.error { &.error {

View File

@ -60,7 +60,7 @@ const password_input = () => {
`; `;
} }
const register_link = () => { const tplRegisterLink = () => {
const i18n_create_account = __("Create an account"); const i18n_create_account = __("Create an account");
const i18n_hint_no_account = __("Don't have a chat account?"); const i18n_hint_no_account = __("Don't have a chat account?");
return html` return html`
@ -71,7 +71,7 @@ const register_link = () => {
`; `;
} }
const show_register_link = () => { const tplShowRegisterLink = () => {
return api.settings.get('allow_registration') && return api.settings.get('allow_registration') &&
!api.settings.get("auto_login") && !api.settings.get("auto_login") &&
_converse.pluggable.plugins['converse-register'].enabled(_converse); _converse.pluggable.plugins['converse-register'].enabled(_converse);
@ -106,7 +106,7 @@ const auth_fields = (el) => {
<fieldset class="form-group buttons"> <fieldset class="form-group buttons">
<input class="btn btn-primary" type="submit" value="${i18n_login}"/> <input class="btn btn-primary" type="submit" value="${i18n_login}"/>
</fieldset> </fieldset>
${ show_register_link() ? register_link() : '' } ${ tplShowRegisterLink() ? tplRegisterLink(el) : '' }
`; `;
} }

View File

@ -9,6 +9,7 @@
import './panel.js'; import './panel.js';
import { __ } from 'i18n'; import { __ } from 'i18n';
import { _converse, api, converse } from '@converse/headless/core'; import { _converse, api, converse } from '@converse/headless/core';
import { setActiveForm } from './utils.js';
// Strophe methods for building stanzas // Strophe methods for building stanzas
const { Strophe } = converse.env; const { Strophe } = converse.env;
@ -32,6 +33,8 @@ converse.plugins.add('converse-register', {
}, },
initialize () { initialize () {
const { router } = _converse;
_converse.CONNECTION_STATUS[Strophe.Status.REGIFAIL] = 'REGIFAIL'; _converse.CONNECTION_STATUS[Strophe.Status.REGIFAIL] = 'REGIFAIL';
_converse.CONNECTION_STATUS[Strophe.Status.REGISTERED] = 'REGISTERED'; _converse.CONNECTION_STATUS[Strophe.Status.REGISTERED] = 'REGISTERED';
_converse.CONNECTION_STATUS[Strophe.Status.CONFLICT] = 'CONFLICT'; _converse.CONNECTION_STATUS[Strophe.Status.CONFLICT] = 'CONFLICT';
@ -44,17 +47,7 @@ converse.plugins.add('converse-register', {
'registration_domain': '' 'registration_domain': ''
}); });
async function setActiveForm (value) { router.route('converse/login', () => setActiveForm('login'));
await api.waitUntil('controlBoxInitialized'); router.route('converse/register', () => setActiveForm('register'));
const controlbox = _converse.chatboxes.get('controlbox');
controlbox.set({ 'active-form': value });
}
_converse.router.route('converse/login', () => setActiveForm('login'));
_converse.router.route('converse/register', () => setActiveForm('register'));
api.listen.on('controlBoxInitialized', view => {
view.model.on('change:active-form', view.showLoginOrRegisterForm, view);
});
} }
}); });

View File

@ -1,16 +1,18 @@
import log from "@converse/headless/log"; import log from "@converse/headless/log";
import pick from "lodash-es/pick";
import tplFormInput from "templates/form_input.js"; import tplFormInput from "templates/form_input.js";
import tplFormUrl from "templates/form_url.js"; import tplFormUrl from "templates/form_url.js";
import tplFormUsername from "templates/form_username.js"; import tplFormUsername from "templates/form_username.js";
import tplRegisterPanel from "./templates/register_panel.js"; import tplRegisterPanel from "./templates/register_panel.js";
import tplSpinner from "templates/spinner.js"; import tplSpinner from "templates/spinner.js";
import { webForm2xForm } from "@converse/headless/utils/form"; import { CustomElement } from 'shared/components/element.js';
import { ElementView } from "@converse/skeletor/src/element";
import { __ } from 'i18n'; import { __ } from 'i18n';
import { _converse, api, converse } from "@converse/headless/core.js"; import { _converse, api, converse } from "@converse/headless/core.js";
import { initConnection } from '@converse/headless/utils/init.js'; import { initConnection } from '@converse/headless/utils/init.js';
import { render } from 'lit'; import { render } from 'lit';
import { setActiveForm } from './utils.js';
import { webForm2xForm } from "@converse/headless/utils/form";
import './styles/register.scss';
// Strophe methods for building stanzas // Strophe methods for building stanzas
const { Strophe, sizzle, $iq } = converse.env; const { Strophe, sizzle, $iq } = converse.env;
@ -27,38 +29,29 @@ const REGISTRATION_FORM = 2;
* @namespace _converse.RegisterPanel * @namespace _converse.RegisterPanel
* @memberOf _converse * @memberOf _converse
*/ */
class RegisterPanel extends ElementView { class RegisterPanel extends CustomElement {
id = "converse-register-panel"
className = 'controlbox-pane fade-in' static get properties () {
events = { return {
'submit form#converse-register': 'onFormSubmission', status : { type: String },
'click .button-cancel': 'renderProviderChoiceForm', error_message: { type: String },
}
} }
initialize () { initialize () {
this.reset(); this.reset();
const controlbox = _converse.chatboxes.get('controlbox'); this.listenTo(_converse, 'connectionInitialized', () => this.registerHooks());
this.model = controlbox;
this.listenTo(_converse, 'connectionInitialized', this.registerHooks);
this.listenTo(this.model, 'change:registration_status', this.render);
const domain = api.settings.get('registration_domain'); const domain = api.settings.get('registration_domain');
if (domain) { if (domain) {
this.fetchRegistrationForm(domain); this.fetchRegistrationForm(domain);
} else { } else {
this.model.set('registration_status', CHOOSE_PROVIDER); this.status = CHOOSE_PROVIDER;
} }
} }
render () { render () {
render(tplRegisterPanel({ return tplRegisterPanel(this);
'domain': this.domain,
'fields': this.fields,
'form_fields': this.form_fields,
'instructions': this.instructions,
'model': this.model,
'title': this.title,
}), this);
} }
/** /**
@ -79,11 +72,6 @@ class RegisterPanel extends ElementView {
}; };
} }
connectedCallback () {
super.connectedCallback();
this.render();
}
/** /**
* Send an IQ stanza to the XMPP server asking for the registration fields. * Send an IQ stanza to the XMPP server asking for the registration fields.
* @private * @private
@ -98,9 +86,8 @@ class RegisterPanel extends ElementView {
const body = conn._proto._reqToData(req); const body = conn._proto._reqToData(req);
if (!body) { return; } if (!body) { return; }
if (conn._proto._connect_cb(body) === Strophe.Status.CONNFAIL) { if (conn._proto._connect_cb(body) === Strophe.Status.CONNFAIL) {
this.showValidationError( this.status = CHOOSE_PROVIDER;
__("Sorry, we're unable to connect to your chosen provider.") this.error_message = __("Sorry, we're unable to connect to your chosen provider.");
);
return false; return false;
} }
const register = body.getElementsByTagName("register"); const register = body.getElementsByTagName("register");
@ -111,14 +98,14 @@ class RegisterPanel extends ElementView {
} }
if (register.length === 0) { if (register.length === 0) {
conn._changeConnectStatus(Strophe.Status.REGIFAIL); conn._changeConnectStatus(Strophe.Status.REGIFAIL);
this.showValidationError( this.error_message =
__("Sorry, the given provider does not support in "+ __("Sorry, the given provider does not support in "+
"band account registration. Please try with a "+ "band account registration. Please try with a "+
"different provider.")) "different provider.");
return true; return true;
} }
// Send an IQ stanza to get all required data fields // Send an IQ stanza to get all required data fields
conn._addSysHandler(this.onRegistrationFields.bind(this), null, "iq", null, null); conn._addSysHandler((s) => this.onRegistrationFields(s), null, "iq", null, null);
const stanza = $iq({type: "get"}).c("query", {xmlns: Strophe.NS.REGISTER}).tree(); const stanza = $iq({type: "get"}).c("query", {xmlns: Strophe.NS.REGISTER}).tree();
stanza.setAttribute("id", conn.getUniqueId("sendIQ")); stanza.setAttribute("id", conn.getUniqueId("sendIQ"));
conn.send(stanza); conn.send(stanza);
@ -149,7 +136,7 @@ class RegisterPanel extends ElementView {
return false; return false;
} }
this.setFields(stanza); this.setFields(stanza);
if (this.model.get('registration_status') === FETCHING_FORM) { if (this.status === FETCHING_FORM) {
this.renderRegistrationForm(stanza); this.renderRegistrationForm(stanza);
} }
return false; return false;
@ -167,9 +154,7 @@ class RegisterPanel extends ElementView {
form_type: null form_type: null
}; };
Object.assign(this, defaults); Object.assign(this, defaults);
if (settings) { if (settings) Object.assign(this, settings);
Object.assign(this, pick(settings, Object.keys(defaults)));
}
} }
/** /**
@ -179,7 +164,7 @@ class RegisterPanel extends ElementView {
* @param { Event } ev * @param { Event } ev
*/ */
onFormSubmission (ev) { onFormSubmission (ev) {
if (ev && ev.preventDefault) { ev.preventDefault(); } ev?.preventDefault?.();
if (ev.target.querySelector('input[name=domain]') === null) { if (ev.target.querySelector('input[name=domain]') === null) {
this.submitRegistrationForm(ev.target); this.submitRegistrationForm(ev.target);
} else { } else {
@ -195,15 +180,8 @@ class RegisterPanel extends ElementView {
* @param { HTMLElement } form - The form that was submitted * @param { HTMLElement } form - The form that was submitted
*/ */
onProviderChosen (form) { onProviderChosen (form) {
const domain_input = form.querySelector('input[name=domain]'), const domain = form.querySelector('input[name=domain]')?.value;
domain = domain_input?.value; if (domain) this.fetchRegistrationForm(domain.trim());
if (!domain) {
// TODO: add validation message
domain_input.classList.add('error');
return;
}
form.querySelector('input[type=submit]').classList.add('hidden');
this.fetchRegistrationForm(domain.trim());
} }
/** /**
@ -213,7 +191,7 @@ class RegisterPanel extends ElementView {
* @param { String } domain_name - XMPP server domain * @param { String } domain_name - XMPP server domain
*/ */
fetchRegistrationForm (domain_name) { fetchRegistrationForm (domain_name) {
this.model.set('registration_status', FETCHING_FORM); this.status = FETCHING_FORM;
this.reset({ this.reset({
'domain': Strophe.getDomainFromJid(domain_name), 'domain': Strophe.getDomainFromJid(domain_name),
'_registering': true '_registering': true
@ -221,7 +199,7 @@ class RegisterPanel extends ElementView {
initConnection(this.domain); initConnection(this.domain);
// When testing, the test tears down before the async function // When testing, the test tears down before the async function
// above finishes. So we use optional chaining here // above finishes. So we use optional chaining here
_converse.connection?.connect(this.domain, "", status => this.onConnectStatusChanged(status)); _converse.connection?.connect(this.domain, "", (s) => this.onConnectStatusChanged(s));
return false; return false;
} }
@ -281,6 +259,8 @@ class RegisterPanel extends ElementView {
this.fields.password, this.fields.password,
_converse.onConnectStatusChanged _converse.onConnectStatusChanged
); );
setActiveForm('login');
_converse.router.navigate('');
this.giveFeedback(__('Now logging you in'), 'info'); this.giveFeedback(__('Now logging you in'), 'info');
} else { } else {
_converse.giveFeedback(__('Registered successfully')); _converse.giveFeedback(__('Registered successfully'));
@ -334,29 +314,7 @@ class RegisterPanel extends ElementView {
*/ */
renderRegistrationForm (stanza) { renderRegistrationForm (stanza) {
this.form_fields = this.getFormFields(stanza); this.form_fields = this.getFormFields(stanza);
this.model.set('registration_status', REGISTRATION_FORM); this.status = REGISTRATION_FORM;
}
showValidationError (message) {
const form = this.querySelector('form');
let flash = form.querySelector('.form-errors');
if (flash === null) {
flash = '<div class="form-errors hidden"></div>';
const instructions = form.querySelector('p.instructions');
if (instructions === null) {
form.insertAdjacentHTML('afterbegin', flash);
} else {
instructions.insertAdjacentHTML('afterend', flash);
}
flash = form.querySelector('.form-errors');
} else {
flash.innerHTML = '';
}
flash.insertAdjacentHTML(
'beforeend',
'<p class="form-help error">'+message+'</p>'
);
flash.classList.remove('hidden');
} }
/** /**
@ -367,31 +325,31 @@ class RegisterPanel extends ElementView {
* @param { XMLElement } stanza - The IQ stanza received from the XMPP server * @param { XMLElement } stanza - The IQ stanza received from the XMPP server
*/ */
reportErrors (stanza) { reportErrors (stanza) {
const errors = stanza.querySelectorAll('error'); const errors = Array.from(stanza.querySelectorAll('error'));
errors.forEach(e => this.showValidationError(e.textContent)); if (errors.length) {
if (!errors.length) { this.error_message = errors.reduce((result, e) => `${result}\n${e.textContent}`, '');
const message = __('The provider rejected your registration attempt. '+ } else {
this.error_message = __('The provider rejected your registration attempt. '+
'Please check the values you entered for correctness.'); 'Please check the values you entered for correctness.');
this.showValidationError(message);
} }
} }
renderProviderChoiceForm (ev) { renderProviderChoiceForm (ev) {
if (ev && ev.preventDefault) { ev.preventDefault(); } ev?.preventDefault?.();
_converse.connection._proto._abortAllRequests(); _converse.connection._proto._abortAllRequests();
_converse.connection.reset(); _converse.connection.reset();
this.render(); this.status = CHOOSE_PROVIDER;
} }
abortRegistration () { abortRegistration () {
_converse.connection._proto._abortAllRequests(); _converse.connection._proto._abortAllRequests();
_converse.connection.reset(); _converse.connection.reset();
if ([FETCHING_FORM, REGISTRATION_FORM].includes(this.model.get('registration_status'))) { if ([FETCHING_FORM, REGISTRATION_FORM].includes(this.status)) {
if (api.settings.get('registration_domain')) { if (api.settings.get('registration_domain')) {
this.fetchRegistrationForm(api.settings.get('registration_domain')); this.fetchRegistrationForm(api.settings.get('registration_domain'));
} }
} else { } else {
this.render(); this.requestUpdate();
} }
} }
@ -403,16 +361,6 @@ class RegisterPanel extends ElementView {
* @param { HTMLElement } form - The HTML form that was submitted * @param { HTMLElement } form - The HTML form that was submitted
*/ */
submitRegistrationForm (form) { submitRegistrationForm (form) {
const has_empty_inputs = Array.from(this.querySelectorAll('input.required'))
.reduce((result, input) => {
if (input.value === '') {
input.classList.add('error');
return result + 1;
}
return result;
}, 0);
if (has_empty_inputs) { return; }
const inputs = sizzle(':input:not([type=button]):not([type=submit])', form); const inputs = sizzle(':input:not([type=button]):not([type=submit])', form);
const iq = $iq({'type': 'set', 'id': u.getUniqueId()}) const iq = $iq({'type': 'set', 'id': u.getUniqueId()})
.c("query", {xmlns:Strophe.NS.REGISTER}); .c("query", {xmlns:Strophe.NS.REGISTER});
@ -425,7 +373,7 @@ class RegisterPanel extends ElementView {
} else { } else {
inputs.forEach(input => iq.c(input.getAttribute('name'), {}, input.value)); inputs.forEach(input => iq.c(input.getAttribute('name'), {}, input.value));
} }
_converse.connection._addSysHandler(this._onRegisterIQ.bind(this), null, "iq", null, null); _converse.connection._addSysHandler((iq) => this._onRegisterIQ(iq), null, "iq", null, null);
_converse.connection.send(iq); _converse.connection.send(iq);
this.setFields(iq.tree()); this.setFields(iq.tree());
} }
@ -462,8 +410,8 @@ class RegisterPanel extends ElementView {
} }
_setFieldsFromXForm (xform) { _setFieldsFromXForm (xform) {
this.title = xform.querySelector('title')?.textContent; this.title = xform.querySelector('title')?.textContent ?? '';
this.instructions = xform.querySelector('instructions')?.textContent; this.instructions = xform.querySelector('instructions')?.textContent ?? '';
xform.querySelectorAll('field').forEach(field => { xform.querySelectorAll('field').forEach(field => {
const _var = field.getAttribute('var'); const _var = field.getAttribute('var');
if (_var) { if (_var) {

View File

@ -0,0 +1,61 @@
@import "shared/styles/_mixins.scss";
converse-register-panel {
.alert {
margin: auto;
max-width: 50vw;
}
}
#converse-register {
@include fade-in;
background-color: var(--controlbox-pane-background-color);
.title {
font-weight: bold;
}
.input-group {
input {
height: auto;
}
.input-group-text {
color: var(--text-color);
background-color: var(--controlbox-pane-background-color);
}
}
.info {
color: green;
font-size: 90%;
margin: 1.5em 0;
}
.form-errors {
color: var(--error-color);
margin: 1em 0;
}
.provider-title {
font-size: var(--font-size-huge);
margin: 0;
}
.provider-score {
width: 178px;
margin-bottom: 8px;
}
.form-help .url {
font-weight: bold;
color: var(--link-color);
}
.instructions {
color: gray;
font-size: 85%;
&:hover {
color: var(--controlbox-text-color);
}
}
}

View File

@ -4,18 +4,19 @@ import { __ } from 'i18n';
import { api } from '@converse/headless/core'; import { api } from '@converse/headless/core';
import { html } from 'lit'; import { html } from 'lit';
const tplFormRequest = () => { const tplFormRequest = (el) => {
const default_domain = api.settings.get('registration_domain'); const default_domain = api.settings.get('registration_domain');
const i18n_fetch_form = __("Hold tight, we're fetching the registration form…"); const i18n_fetch_form = __("Hold tight, we're fetching the registration form…");
const i18n_cancel = __('Cancel'); const i18n_cancel = __('Cancel');
return html` return html`
<form id="converse-register" class="converse-form no-scrolling"> <form id="converse-register" class="converse-form no-scrolling" @submit=${ev => el.onFormSubmission(ev)}>
${tplSpinner({ 'classes': 'hor_centered' })} ${tplSpinner({ 'classes': 'hor_centered' })}
<p class="info">${i18n_fetch_form}</p> <p class="info">${i18n_fetch_form}</p>
${default_domain ${default_domain
? '' ? ''
: html` : html`
<button class="btn btn-secondary button-cancel hor_centered">${i18n_cancel}</button> <button class="btn btn-secondary button-cancel hor_centered"
@click=${ev => el.renderProviderChoiceForm(ev)}>${i18n_cancel}</button>
`} `}
</form> </form>
`; `;
@ -50,19 +51,21 @@ const tplFetchFormButtons = () => {
`; `;
}; };
const tplChooseProvider = () => { const tplChooseProvider = (el) => {
const default_domain = api.settings.get('registration_domain'); const default_domain = api.settings.get('registration_domain');
const i18n_create_account = __('Create your account'); const i18n_create_account = __('Create your account');
const i18n_choose_provider = __('Please enter the XMPP provider to register with:'); const i18n_choose_provider = __('Please enter the XMPP provider to register with:');
const show_form_buttons = !default_domain && el.status === CHOOSE_PROVIDER;
return html` return html`
<form id="converse-register" class="converse-form"> <form id="converse-register" class="converse-form" @submit=${ev => el.onFormSubmission(ev)}>
<legend class="col-form-label">${i18n_create_account}</legend> <legend class="col-form-label">${i18n_create_account}</legend>
<div class="form-group"> <div class="form-group">
<label>${i18n_choose_provider}</label> <label>${i18n_choose_provider}</label>
<div class="form-errors hidden"></div>
${default_domain ? default_domain : tplDomainInput()} ${default_domain ? default_domain : tplDomainInput()}
</div> </div>
${default_domain ? '' : tplFetchFormButtons()} ${show_form_buttons ? tplFetchFormButtons() : ''}
</form> </form>
`; `;
}; };
@ -71,11 +74,12 @@ const CHOOSE_PROVIDER = 0;
const FETCHING_FORM = 1; const FETCHING_FORM = 1;
const REGISTRATION_FORM = 2; const REGISTRATION_FORM = 2;
export default o => { export default (el) => {
return html` return html`
<converse-brand-logo></converse-brand-logo> <converse-brand-logo></converse-brand-logo>
${o.model.get('registration_status') === CHOOSE_PROVIDER ? tplChooseProvider() : ''} ${ el.error_message ? html`<div class="alert alert-danger" role="alert">${el.error_message}</div>` : '' }
${o.model.get('registration_status') === FETCHING_FORM ? tplFormRequest() : ''} ${el.status === CHOOSE_PROVIDER ? tplChooseProvider(el) : ''}
${o.model.get('registration_status') === REGISTRATION_FORM ? tplRegistrationForm(o) : ''} ${el.status === FETCHING_FORM ? tplFormRequest(el) : ''}
${el.status === REGISTRATION_FORM ? tplRegistrationForm(el) : ''}
`; `;
}; };

View File

@ -2,7 +2,7 @@ import { __ } from 'i18n';
import { api } from '@converse/headless/core'; import { api } from '@converse/headless/core';
import { html } from 'lit'; import { html } from 'lit';
export default o => { export default (el) => {
const i18n_choose_provider = __('Choose a different provider'); const i18n_choose_provider = __('Choose a different provider');
const i18n_has_account = __('Already have a chat account?'); const i18n_has_account = __('Already have a chat account?');
const i18n_legend = __('Account Registration:'); const i18n_legend = __('Account Registration:');
@ -11,15 +11,15 @@ export default o => {
const registration_domain = api.settings.get('registration_domain'); const registration_domain = api.settings.get('registration_domain');
return html` return html`
<form id="converse-register" class="converse-form"> <form id="converse-register" class="converse-form" @submit=${ev => el.onFormSubmission(ev)}>
<legend class="col-form-label">${i18n_legend} ${o.domain}</legend> <legend class="col-form-label">${i18n_legend} ${el.domain}</legend>
<p class="title">${o.title}</p> <p class="title">${el.title}</p>
<p class="form-help instructions">${o.instructions}</p> <p class="form-help instructions">${el.instructions}</p>
<div class="form-errors hidden"></div> <div class="form-errors hidden"></div>
${o.form_fields} ${el.form_fields}
<fieldset class="buttons form-group"> <fieldset class="buttons form-group">
${o.fields ${el.fields
? html` ? html`
<input type="submit" class="btn btn-primary" value="${i18n_register}" /> <input type="submit" class="btn btn-primary" value="${i18n_register}" />
` `
@ -31,6 +31,7 @@ export default o => {
type="button" type="button"
class="btn btn-secondary button-cancel" class="btn btn-secondary button-cancel"
value="${i18n_choose_provider}" value="${i18n_choose_provider}"
@click=${ev => el.renderProviderChoiceForm(ev)}
/> />
`} `}
<div class="switch-form"> <div class="switch-form">

View File

@ -1,9 +1,7 @@
/*global mock, converse */ /*global mock, converse */
const Strophe = converse.env.Strophe; const { stx, Strophe, $iq, sizzle, u } = converse.env;
const $iq = converse.env.$iq;
const { sizzle} = converse.env;
const u = converse.env.utils;
describe("The Registration Panel", function () { describe("The Registration Panel", function () {
@ -88,6 +86,61 @@ describe("The Registration Panel", function () {
delete _converse.connection; delete _converse.connection;
})); }));
it("allows the user to choose an XMPP provider's domain in fullscreen view mode",
mock.initConverse(
['chatBoxesInitialized'], {
auto_login: false,
view_mode: 'fullscreen',
discover_connection_methods: false,
allow_registration: true
},
async function (_converse) {
const cbview = _converse.api.controlbox.get();
cbview.querySelector('.toggle-register-login').click();
const registerview = await u.waitUntil(() => cbview.querySelector('converse-register-panel'));
spyOn(registerview, 'fetchRegistrationForm').and.callThrough();
spyOn(registerview, 'onProviderChosen').and.callThrough();
spyOn(registerview, 'getRegistrationFields').and.callThrough();
spyOn(registerview, 'renderRegistrationForm').and.callThrough();
expect(registerview._registering).toBeFalsy();
expect(_converse.api.connection.connected()).toBeFalsy();
registerview.querySelector('input[name=domain]').value = 'conversejs.org';
registerview.querySelector('input[type=submit]').click();
expect(registerview.onProviderChosen).toHaveBeenCalled();
expect(registerview._registering).toBeTruthy();
await u.waitUntil(() => registerview.fetchRegistrationForm.calls.count());
let stanza = new Strophe.Builder("stream:features", {
'xmlns:stream': "http://etherx.jabber.org/streams",
'xmlns': "jabber:client"
})
.c('register', {xmlns: "http://jabber.org/features/iq-register"}).up()
.c('mechanisms', {xmlns: "urn:ietf:params:xml:ns:xmpp-sasl"});
_converse.connection._connect_cb(mock.createRequest(stanza));
expect(registerview.getRegistrationFields).toHaveBeenCalled();
stanza = $iq({
'type': 'result',
'id': 'reg1'
}).c('query', {'xmlns': 'jabber:iq:register'})
.c('instructions')
.t('Please choose a username, password and provide your email address').up()
.c('username').up()
.c('password').up()
.c('email');
_converse.connection._dataRecv(mock.createRequest(stanza));
expect(registerview.renderRegistrationForm).toHaveBeenCalled();
await u.waitUntil(() => registerview.querySelectorAll('input').length === 5);
expect(registerview.querySelectorAll('input[type=submit]').length).toBe(1);
expect(registerview.querySelectorAll('input[type=button]').length).toBe(1);
}));
it("will render a registration form as received from the XMPP provider", it("will render a registration form as received from the XMPP provider",
mock.initConverse( mock.initConverse(
['chatBoxesInitialized'], ['chatBoxesInitialized'],
@ -108,7 +161,6 @@ describe("The Registration Panel", function () {
spyOn(registerview, 'getRegistrationFields').and.callThrough(); spyOn(registerview, 'getRegistrationFields').and.callThrough();
spyOn(registerview, 'onRegistrationFields').and.callThrough(); spyOn(registerview, 'onRegistrationFields').and.callThrough();
spyOn(registerview, 'renderRegistrationForm').and.callThrough(); spyOn(registerview, 'renderRegistrationForm').and.callThrough();
registerview.delegateEvents(); // We need to rebind all events otherwise our spy won't be called
expect(registerview._registering).toBeFalsy(); expect(registerview._registering).toBeFalsy();
expect(_converse.api.connection.connected()).toBeFalsy(); expect(_converse.api.connection.connected()).toBeFalsy();
@ -140,7 +192,8 @@ describe("The Registration Panel", function () {
_converse.connection._dataRecv(mock.createRequest(stanza)); _converse.connection._dataRecv(mock.createRequest(stanza));
expect(registerview.onRegistrationFields).toHaveBeenCalled(); expect(registerview.onRegistrationFields).toHaveBeenCalled();
expect(registerview.renderRegistrationForm).toHaveBeenCalled(); expect(registerview.renderRegistrationForm).toHaveBeenCalled();
expect(registerview.querySelectorAll('input').length).toBe(5);
await u.waitUntil(() => registerview.querySelectorAll('input').length === 5);
expect(registerview.querySelectorAll('input[type=submit]').length).toBe(1); expect(registerview.querySelectorAll('input[type=submit]').length).toBe(1);
expect(registerview.querySelectorAll('input[type=button]').length).toBe(1); expect(registerview.querySelectorAll('input[type=button]').length).toBe(1);
})); }));
@ -168,7 +221,6 @@ describe("The Registration Panel", function () {
spyOn(registerview, 'getRegistrationFields').and.callThrough(); spyOn(registerview, 'getRegistrationFields').and.callThrough();
spyOn(registerview, 'onRegistrationFields').and.callThrough(); spyOn(registerview, 'onRegistrationFields').and.callThrough();
spyOn(registerview, 'renderRegistrationForm').and.callThrough(); spyOn(registerview, 'renderRegistrationForm').and.callThrough();
registerview.delegateEvents(); // We need to rebind all events otherwise our spy won't be called
registerview.querySelector('input[name=domain]').value = 'conversejs.org'; registerview.querySelector('input[name=domain]').value = 'conversejs.org';
registerview.querySelector('input[type=submit]').click(); registerview.querySelector('input[type=submit]').click();
@ -192,7 +244,9 @@ describe("The Registration Panel", function () {
_converse.connection._dataRecv(mock.createRequest(stanza)); _converse.connection._dataRecv(mock.createRequest(stanza));
expect(registerview.form_type).toBe('legacy'); expect(registerview.form_type).toBe('legacy');
registerview.querySelector('input[name=username]').value = 'testusername'; const username_input = await u.waitUntil(() => registerview.querySelector('input[name=username]'));
username_input.value = 'testusername';
registerview.querySelector('input[name=password]').value = 'testpassword'; registerview.querySelector('input[name=password]').value = 'testpassword';
registerview.querySelector('input[name=email]').value = 'test@email.local'; registerview.querySelector('input[name=email]').value = 'test@email.local';
@ -229,7 +283,6 @@ describe("The Registration Panel", function () {
spyOn(registerview, 'getRegistrationFields').and.callThrough(); spyOn(registerview, 'getRegistrationFields').and.callThrough();
spyOn(registerview, 'onRegistrationFields').and.callThrough(); spyOn(registerview, 'onRegistrationFields').and.callThrough();
spyOn(registerview, 'renderRegistrationForm').and.callThrough(); spyOn(registerview, 'renderRegistrationForm').and.callThrough();
registerview.delegateEvents(); // We need to rebind all events otherwise our spy won't be called
registerview.querySelector('input[name=domain]').value = 'conversejs.org'; registerview.querySelector('input[name=domain]').value = 'conversejs.org';
registerview.querySelector('input[type=submit]').click(); registerview.querySelector('input[type=submit]').click();
@ -255,7 +308,9 @@ describe("The Registration Panel", function () {
_converse.connection._dataRecv(mock.createRequest(stanza)); _converse.connection._dataRecv(mock.createRequest(stanza));
expect(registerview.form_type).toBe('xform'); expect(registerview.form_type).toBe('xform');
registerview.querySelector('input[name=username]').value = 'testusername'; const username_input = await u.waitUntil(() => registerview.querySelector('input[name=username]'));
username_input.value = 'testusername';
registerview.querySelector('input[name=password]').value = 'testpassword'; registerview.querySelector('input[name=password]').value = 'testpassword';
registerview.querySelector('input[name=email]').value = 'test@email.local'; registerview.querySelector('input[name=email]').value = 'test@email.local';
@ -304,12 +359,6 @@ describe("The Registration Panel", function () {
const cbview = _converse.chatboxviews.get('controlbox'); const cbview = _converse.chatboxviews.get('controlbox');
cbview.querySelector('.toggle-register-login').click(); cbview.querySelector('.toggle-register-login').click();
const registerview = await u.waitUntil(() => cbview.querySelector('converse-register-panel')); const registerview = await u.waitUntil(() => cbview.querySelector('converse-register-panel'));
spyOn(registerview, 'onProviderChosen').and.callThrough();
spyOn(registerview, 'getRegistrationFields').and.callThrough();
spyOn(registerview, 'onRegistrationFields').and.callThrough();
spyOn(registerview, 'renderRegistrationForm').and.callThrough();
registerview.delegateEvents(); // We need to rebind all events otherwise our spy won't be called
registerview.querySelector('input[name=domain]').value = 'conversejs.org'; registerview.querySelector('input[name=domain]').value = 'conversejs.org';
registerview.querySelector('input[type=submit]').click(); registerview.querySelector('input[type=submit]').click();
@ -321,7 +370,7 @@ describe("The Registration Panel", function () {
.c('mechanisms', {xmlns: "urn:ietf:params:xml:ns:xmpp-sasl"}); .c('mechanisms', {xmlns: "urn:ietf:params:xml:ns:xmpp-sasl"});
_converse.connection._connect_cb(mock.createRequest(stanza)); _converse.connection._connect_cb(mock.createRequest(stanza));
stanza = u.toStanza(` stanza = stx`
<iq xmlns="jabber:client" type="result" from="conversations.im" id="ad1e0d50-5adb-4397-a997-5feab56fe418:sendIQ" xml:lang="en"> <iq xmlns="jabber:client" type="result" from="conversations.im" id="ad1e0d50-5adb-4397-a997-5feab56fe418:sendIQ" xml:lang="en">
<query xmlns="jabber:iq:register"> <query xmlns="jabber:iq:register">
<x xmlns="jabber:x:data" type="form"> <x xmlns="jabber:x:data" type="form">
@ -344,13 +393,76 @@ describe("The Registration Panel", function () {
max-age="0">iVBORw0KGgoAAAANSUhEUgAAALQAAAA8BAMAAAA9AI20AAAAMFBMVEX///8AAADf39+fn59fX19/f3+/v78fHx8/Pz9PT08bGxsvLy9jY2NTU1MXFxcnJyc84bkWAAAACXBIWXMAAA7EAAAOxAGVKw4bAAAERUlEQVRYhe1WTXMaRxDdDxY4JWpYvDinpVyxdATLin0MiRLlCHEi+7hYUcVHTSI7urhK6yr5//gn5N/4Z7inX89+CQkTcFUO6gOwS8/r7tdvesbzvoT5ROR5JJ9bB97xAK22XWAY1WznlnUr7QaAzSOsWufXQ6wH/FmO60b4D936LJr8TWRwW4SNgOsodZr8m4vZUoRt2xZ3xHXgna1FCE5+f5aWwPU//bXgg8eHjyqPp4aXJeOlwLUIt0O39zOvPWW3WfHmCCkli816FxlK0rnFGKZ484dN+eIXsw1R+G+JfjwgOpMnm+r5SxA63gS2Q8MchO1RLN8jSn4W4F5OPed2evhTthKLG3bsfjLL874XGBpWHLrU0953i/ev7JsfViHbhsWSQTunJDOppeAe0hVGokJUHBOphmjrbBlgabviJKXbIP0B//gKSBHZh2rvJnQp3wsapMFz+VsTPNhPr0Hn9N57YOjywaxFSU6S79fUF39KBDgnt6yjZOeSffk+4IXDZovbQl9E96m34EzQKMepQcbzijAGiBmDsO+LaqzqG3m3kEf+DQ2mY+vdk5c2n2Iaj5QGi6n59FHDmcuP4t8MGlRaF39P6ENyIaB2EXdpjLnQq9IgdVxfax3ilBc10u4gowX9K6BaKiZNmCC7CF/WpkJvWxN00OjuoqGYLqAnpILLE68Ymrt9M0S9hcznUJ8RykdlLalUfFaDjvA8pT2kxmsl5fuMaM6mSWUpUhDoudSucdhiZFDwphEHwsMwhEpH0jsm+/UBK2wCzFIiitalN7YjWkyIBgTNPgpDXX4rjk4UH+yPPgfK4HNZQCP/KZ0fGnrnKl8+pXl3X7FwZuwNUdwDGO+BjPUn6XaKtbkm+MJ6vtaXSnIz6wBT/m+VvZNIhz7ayabQLSeRQDmYkjt0KlmHDa555v9DzFxx+CCvCG4K3dbx6mTYtfPs1Dgdh0i3W+cl4lnnhblMKKBBA23X1Ezc3E5ZoPS5KHjPiU1rKTviYe1fTsa6e3UwXGWI4ykB8uiGqkmA6Cbf3K4JTH3LOBlbX+yPWll57LKVeH8CTEvyVPV2TXL8kPnPqtA51CaFYxOH2rJoZunSnvsSj48WiaDccl6KEgiMSarITsa+rWWBnqFloYlT1qWW2GKw9nPSbEvoVHFst967XgNQjxdA66Q6VFEUh488xfaSo7cHB52XYzA4eRlVteeT8ostWfuPea0oF6MwzlwgZE9gQI+uUV0gzK+WlpUrNI8juhhX/OyNwZnRrsDfxOqS1aDR+gC6NUPvJpvQeVZ9eiNr9aDUuddY3bLnA4tH4r/49UboznH1ia8PV/uP3WUB3dxtzj1uxfDZgbEbZx17Itwrf0Jyc8N4en+5dhivtKeYjGJ8yXgUzKvSU/uWJZmsuAYtseDku+K3zMHi4lC1h0suPmtZaEp2tm3hEV2lXwb6zu7szv6f9glF5rPGT5xR7AAAAABJRU5ErkJggg==</data> max-age="0">iVBORw0KGgoAAAANSUhEUgAAALQAAAA8BAMAAAA9AI20AAAAMFBMVEX///8AAADf39+fn59fX19/f3+/v78fHx8/Pz9PT08bGxsvLy9jY2NTU1MXFxcnJyc84bkWAAAACXBIWXMAAA7EAAAOxAGVKw4bAAAERUlEQVRYhe1WTXMaRxDdDxY4JWpYvDinpVyxdATLin0MiRLlCHEi+7hYUcVHTSI7urhK6yr5//gn5N/4Z7inX89+CQkTcFUO6gOwS8/r7tdvesbzvoT5ROR5JJ9bB97xAK22XWAY1WznlnUr7QaAzSOsWufXQ6wH/FmO60b4D936LJr8TWRwW4SNgOsodZr8m4vZUoRt2xZ3xHXgna1FCE5+f5aWwPU//bXgg8eHjyqPp4aXJeOlwLUIt0O39zOvPWW3WfHmCCkli816FxlK0rnFGKZ484dN+eIXsw1R+G+JfjwgOpMnm+r5SxA63gS2Q8MchO1RLN8jSn4W4F5OPed2evhTthKLG3bsfjLL874XGBpWHLrU0953i/ev7JsfViHbhsWSQTunJDOppeAe0hVGokJUHBOphmjrbBlgabviJKXbIP0B//gKSBHZh2rvJnQp3wsapMFz+VsTPNhPr0Hn9N57YOjywaxFSU6S79fUF39KBDgnt6yjZOeSffk+4IXDZovbQl9E96m34EzQKMepQcbzijAGiBmDsO+LaqzqG3m3kEf+DQ2mY+vdk5c2n2Iaj5QGi6n59FHDmcuP4t8MGlRaF39P6ENyIaB2EXdpjLnQq9IgdVxfax3ilBc10u4gowX9K6BaKiZNmCC7CF/WpkJvWxN00OjuoqGYLqAnpILLE68Ymrt9M0S9hcznUJ8RykdlLalUfFaDjvA8pT2kxmsl5fuMaM6mSWUpUhDoudSucdhiZFDwphEHwsMwhEpH0jsm+/UBK2wCzFIiitalN7YjWkyIBgTNPgpDXX4rjk4UH+yPPgfK4HNZQCP/KZ0fGnrnKl8+pXl3X7FwZuwNUdwDGO+BjPUn6XaKtbkm+MJ6vtaXSnIz6wBT/m+VvZNIhz7ayabQLSeRQDmYkjt0KlmHDa555v9DzFxx+CCvCG4K3dbx6mTYtfPs1Dgdh0i3W+cl4lnnhblMKKBBA23X1Ezc3E5ZoPS5KHjPiU1rKTviYe1fTsa6e3UwXGWI4ykB8uiGqkmA6Cbf3K4JTH3LOBlbX+yPWll57LKVeH8CTEvyVPV2TXL8kPnPqtA51CaFYxOH2rJoZunSnvsSj48WiaDccl6KEgiMSarITsa+rWWBnqFloYlT1qWW2GKw9nPSbEvoVHFst967XgNQjxdA66Q6VFEUh488xfaSo7cHB52XYzA4eRlVteeT8ostWfuPea0oF6MwzlwgZE9gQI+uUV0gzK+WlpUrNI8juhhX/OyNwZnRrsDfxOqS1aDR+gC6NUPvJpvQeVZ9eiNr9aDUuddY3bLnA4tH4r/49UboznH1ia8PV/uP3WUB3dxtzj1uxfDZgbEbZx17Itwrf0Jyc8N4en+5dhivtKeYjGJ8yXgUzKvSU/uWJZmsuAYtseDku+K3zMHi4lC1h0suPmtZaEp2tm3hEV2lXwb6zu7szv6f9glF5rPGT5xR7AAAAABJRU5ErkJggg==</data>
<instructions>You need a client that supports x:data and CAPTCHA to register</instructions> <instructions>You need a client that supports x:data and CAPTCHA to register</instructions>
</query> </query>
</iq>`); </iq>`;
_converse.connection._dataRecv(mock.createRequest(stanza)); _converse.connection._dataRecv(mock.createRequest(stanza));
await u.waitUntil(() => registerview.querySelectorAll('#converse-register input[required]').length === 3);
expect(registerview.form_type).toBe('xform'); expect(registerview.form_type).toBe('xform');
expect(registerview.querySelectorAll('#converse-register input[required]').length).toBe(3);
// Hide the controlbox so that we can see whether the test // Hide the controlbox so that we can see whether the test passed or failed
// passed or failed u.addClass('hidden', _converse.chatboxviews.get('controlbox'));
u.addClass('hidden', _converse.chatboxviews.get('controlbox').el); delete _converse.connection;
}));
it("lets you choose a different provider",
mock.initConverse(
['chatBoxesInitialized'],
{ auto_login: false,
view_mode: 'fullscreen',
discover_connection_methods: false,
allow_registration: true },
async function (_converse) {
const toggle = document.querySelector(".toggle-controlbox");
if (!u.isVisible(document.querySelector("#controlbox"))) {
if (!u.isVisible(toggle)) {
u.removeClass('hidden', toggle);
}
toggle.click();
}
const cbview = _converse.chatboxviews.get('controlbox');
cbview.querySelector('.toggle-register-login').click();
const registerview = await u.waitUntil(() => cbview.querySelector('converse-register-panel'));
registerview.querySelector('input[name=domain]').value = 'conversejs.org';
registerview.querySelector('input[type=submit]').click();
let stanza = new Strophe.Builder("stream:features", {
'xmlns:stream': "http://etherx.jabber.org/streams",
'xmlns': "jabber:client"
})
.c('register', {xmlns: "http://jabber.org/features/iq-register"}).up()
.c('mechanisms', {xmlns: "urn:ietf:params:xml:ns:xmpp-sasl"});
_converse.connection._connect_cb(mock.createRequest(stanza));
stanza = stx`
<iq xmlns="jabber:client" type="result" from="conversations.im" id="ad1e0d50-5adb-4397-a997-5feab56fe418:sendIQ" xml:lang="en">
<query xmlns="jabber:iq:register">
<x xmlns="jabber:x:data" type="form">
<instructions>Choose a username and password to register with this server</instructions>
<field var="FORM_TYPE" type="hidden"><value>urn:xmpp:captcha</value></field>
<field var="username" type="text-single" label="User"><required/></field>
<field var="password" type="text-private" label="Password"><required/></field>
<field var="from" type="hidden"><value>conversations.im</value></field>
<field var="challenge" type="hidden"><value>15376320046808160053</value></field>
<field var="sid" type="hidden"><value>ad1e0d50-5adb-4397-a997-5feab56fe418:sendIQ</value></field>
</x>
</query>
</iq>`;
_converse.connection._dataRecv(mock.createRequest(stanza));
await u.waitUntil(() => registerview.querySelectorAll('#converse-register input[required]').length === 2);
expect(registerview.form_type).toBe('xform');
const button = await u.waitUntil(() => registerview.querySelector('.btn-secondary'));
expect(button.value).toBe("Choose a different provider");
button.click();
await u.waitUntil(() => registerview.querySelector('input[name="domain"]'));
expect(registerview.querySelectorAll('input[required]').length).toBe(1);
// Hide the controlbox so that we can see whether the test passed or failed
u.addClass('hidden', _converse.chatboxviews.get('controlbox'));
delete _converse.connection; delete _converse.connection;
})); }));
@ -385,7 +497,7 @@ describe("The Registration Panel", function () {
.c('mechanisms', {xmlns: "urn:ietf:params:xml:ns:xmpp-sasl"}); .c('mechanisms', {xmlns: "urn:ietf:params:xml:ns:xmpp-sasl"});
_converse.connection._connect_cb(mock.createRequest(stanza)); _converse.connection._connect_cb(mock.createRequest(stanza));
stanza = u.toStanza(` stanza = stx`
<iq xmlns="jabber:client" type="result" from="conversejs.org" id="ad1e0d50-5adb-4397-a997-5feab56fe418:sendIQ" xml:lang="en"> <iq xmlns="jabber:client" type="result" from="conversejs.org" id="ad1e0d50-5adb-4397-a997-5feab56fe418:sendIQ" xml:lang="en">
<query xmlns="jabber:iq:register"> <query xmlns="jabber:iq:register">
<x xmlns="jabber:x:data" type="form"> <x xmlns="jabber:x:data" type="form">
@ -408,11 +520,12 @@ describe("The Registration Panel", function () {
max-age="0">iVBORw0KGgoAAAANSUhEUgAAALQAAAA8BAMAAAA9AI20AAAAMFBMVEX///8AAADf39+fn59fX19/f3+/v78fHx8/Pz9PT08bGxsvLy9jY2NTU1MXFxcnJyc84bkWAAAACXBIWXMAAA7EAAAOxAGVKw4bAAAERUlEQVRYhe1WTXMaRxDdDxY4JWpYvDinpVyxdATLin0MiRLlCHEi+7hYUcVHTSI7urhK6yr5//gn5N/4Z7inX89+CQkTcFUO6gOwS8/r7tdvesbzvoT5ROR5JJ9bB97xAK22XWAY1WznlnUr7QaAzSOsWufXQ6wH/FmO60b4D936LJr8TWRwW4SNgOsodZr8m4vZUoRt2xZ3xHXgna1FCE5+f5aWwPU//bXgg8eHjyqPp4aXJeOlwLUIt0O39zOvPWW3WfHmCCkli816FxlK0rnFGKZ484dN+eIXsw1R+G+JfjwgOpMnm+r5SxA63gS2Q8MchO1RLN8jSn4W4F5OPed2evhTthKLG3bsfjLL874XGBpWHLrU0953i/ev7JsfViHbhsWSQTunJDOppeAe0hVGokJUHBOphmjrbBlgabviJKXbIP0B//gKSBHZh2rvJnQp3wsapMFz+VsTPNhPr0Hn9N57YOjywaxFSU6S79fUF39KBDgnt6yjZOeSffk+4IXDZovbQl9E96m34EzQKMepQcbzijAGiBmDsO+LaqzqG3m3kEf+DQ2mY+vdk5c2n2Iaj5QGi6n59FHDmcuP4t8MGlRaF39P6ENyIaB2EXdpjLnQq9IgdVxfax3ilBc10u4gowX9K6BaKiZNmCC7CF/WpkJvWxN00OjuoqGYLqAnpILLE68Ymrt9M0S9hcznUJ8RykdlLalUfFaDjvA8pT2kxmsl5fuMaM6mSWUpUhDoudSucdhiZFDwphEHwsMwhEpH0jsm+/UBK2wCzFIiitalN7YjWkyIBgTNPgpDXX4rjk4UH+yPPgfK4HNZQCP/KZ0fGnrnKl8+pXl3X7FwZuwNUdwDGO+BjPUn6XaKtbkm+MJ6vtaXSnIz6wBT/m+VvZNIhz7ayabQLSeRQDmYkjt0KlmHDa555v9DzFxx+CCvCG4K3dbx6mTYtfPs1Dgdh0i3W+cl4lnnhblMKKBBA23X1Ezc3E5ZoPS5KHjPiU1rKTviYe1fTsa6e3UwXGWI4ykB8uiGqkmA6Cbf3K4JTH3LOBlbX+yPWll57LKVeH8CTEvyVPV2TXL8kPnPqtA51CaFYxOH2rJoZunSnvsSj48WiaDccl6KEgiMSarITsa+rWWBnqFloYlT1qWW2GKw9nPSbEvoVHFst967XgNQjxdA66Q6VFEUh488xfaSo7cHB52XYzA4eRlVteeT8ostWfuPea0oF6MwzlwgZE9gQI+uUV0gzK+WlpUrNI8juhhX/OyNwZnRrsDfxOqS1aDR+gC6NUPvJpvQeVZ9eiNr9aDUuddY3bLnA4tH4r/49UboznH1ia8PV/uP3WUB3dxtzj1uxfDZgbEbZx17Itwrf0Jyc8N4en+5dhivtKeYjGJ8yXgUzKvSU/uWJZmsuAYtseDku+K3zMHi4lC1h0suPmtZaEp2tm3hEV2lXwb6zu7szv6f9glF5rPGT5xR7AAAAABJRU5ErkJggg==</data> max-age="0">iVBORw0KGgoAAAANSUhEUgAAALQAAAA8BAMAAAA9AI20AAAAMFBMVEX///8AAADf39+fn59fX19/f3+/v78fHx8/Pz9PT08bGxsvLy9jY2NTU1MXFxcnJyc84bkWAAAACXBIWXMAAA7EAAAOxAGVKw4bAAAERUlEQVRYhe1WTXMaRxDdDxY4JWpYvDinpVyxdATLin0MiRLlCHEi+7hYUcVHTSI7urhK6yr5//gn5N/4Z7inX89+CQkTcFUO6gOwS8/r7tdvesbzvoT5ROR5JJ9bB97xAK22XWAY1WznlnUr7QaAzSOsWufXQ6wH/FmO60b4D936LJr8TWRwW4SNgOsodZr8m4vZUoRt2xZ3xHXgna1FCE5+f5aWwPU//bXgg8eHjyqPp4aXJeOlwLUIt0O39zOvPWW3WfHmCCkli816FxlK0rnFGKZ484dN+eIXsw1R+G+JfjwgOpMnm+r5SxA63gS2Q8MchO1RLN8jSn4W4F5OPed2evhTthKLG3bsfjLL874XGBpWHLrU0953i/ev7JsfViHbhsWSQTunJDOppeAe0hVGokJUHBOphmjrbBlgabviJKXbIP0B//gKSBHZh2rvJnQp3wsapMFz+VsTPNhPr0Hn9N57YOjywaxFSU6S79fUF39KBDgnt6yjZOeSffk+4IXDZovbQl9E96m34EzQKMepQcbzijAGiBmDsO+LaqzqG3m3kEf+DQ2mY+vdk5c2n2Iaj5QGi6n59FHDmcuP4t8MGlRaF39P6ENyIaB2EXdpjLnQq9IgdVxfax3ilBc10u4gowX9K6BaKiZNmCC7CF/WpkJvWxN00OjuoqGYLqAnpILLE68Ymrt9M0S9hcznUJ8RykdlLalUfFaDjvA8pT2kxmsl5fuMaM6mSWUpUhDoudSucdhiZFDwphEHwsMwhEpH0jsm+/UBK2wCzFIiitalN7YjWkyIBgTNPgpDXX4rjk4UH+yPPgfK4HNZQCP/KZ0fGnrnKl8+pXl3X7FwZuwNUdwDGO+BjPUn6XaKtbkm+MJ6vtaXSnIz6wBT/m+VvZNIhz7ayabQLSeRQDmYkjt0KlmHDa555v9DzFxx+CCvCG4K3dbx6mTYtfPs1Dgdh0i3W+cl4lnnhblMKKBBA23X1Ezc3E5ZoPS5KHjPiU1rKTviYe1fTsa6e3UwXGWI4ykB8uiGqkmA6Cbf3K4JTH3LOBlbX+yPWll57LKVeH8CTEvyVPV2TXL8kPnPqtA51CaFYxOH2rJoZunSnvsSj48WiaDccl6KEgiMSarITsa+rWWBnqFloYlT1qWW2GKw9nPSbEvoVHFst967XgNQjxdA66Q6VFEUh488xfaSo7cHB52XYzA4eRlVteeT8ostWfuPea0oF6MwzlwgZE9gQI+uUV0gzK+WlpUrNI8juhhX/OyNwZnRrsDfxOqS1aDR+gC6NUPvJpvQeVZ9eiNr9aDUuddY3bLnA4tH4r/49UboznH1ia8PV/uP3WUB3dxtzj1uxfDZgbEbZx17Itwrf0Jyc8N4en+5dhivtKeYjGJ8yXgUzKvSU/uWJZmsuAYtseDku+K3zMHi4lC1h0suPmtZaEp2tm3hEV2lXwb6zu7szv6f9glF5rPGT5xR7AAAAABJRU5ErkJggg==</data>
<instructions>You need a client that supports x:data and CAPTCHA to register</instructions> <instructions>You need a client that supports x:data and CAPTCHA to register</instructions>
</query> </query>
</iq>`); </iq>`;
_converse.connection._dataRecv(mock.createRequest(stanza)); _converse.connection._dataRecv(mock.createRequest(stanza));
spyOn(view, 'submitRegistrationForm').and.callThrough(); spyOn(view, 'submitRegistrationForm').and.callThrough();
const username_input = view.querySelector('[name="username"]');
const username_input = await u.waitUntil(() => view.querySelector('[name="username"]'));
username_input.value = 'romeo'; username_input.value = 'romeo';
const password_input = view.querySelector('[name="password"]'); const password_input = view.querySelector('[name="password"]');
password_input.value = 'secret'; password_input.value = 'secret';
@ -421,16 +534,20 @@ describe("The Registration Panel", function () {
view.querySelector('[type="submit"]').click(); view.querySelector('[type="submit"]').click();
expect(view.submitRegistrationForm).toHaveBeenCalled(); expect(view.submitRegistrationForm).toHaveBeenCalled();
const response_IQ = u.toStanza(` const response_IQ = stx`
<iq xml:lang='en' from='conversejs.org' type='error' id='d9917b7a-588f-4ef6-8a56-0d6d3ad538ae:sendIQ'> <iq xml:lang='en' from='conversejs.org' type='error' id='d9917b7a-588f-4ef6-8a56-0d6d3ad538ae:sendIQ' xmlns="jabber:client">
<query xmlns='jabber:iq:register'/> <query xmlns='jabber:iq:register'/>
<error code='500' type='wait'> <error code='500' type='wait'>
<resource-constraint xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'/> <resource-constraint xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'/>
<text xml:lang='en' xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'>Too many CAPTCHA requests</text> <text xml:lang='en' xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'>Too many CAPTCHA requests</text>
</error> </error>
</iq>`); </iq>`;
_converse.connection._dataRecv(mock.createRequest(response_IQ)); _converse.connection._dataRecv(mock.createRequest(response_IQ));
expect(view.querySelector('.error')?.textContent.trim()).toBe('Too many CAPTCHA requests');
const alert = await u.waitUntil(() => view.querySelector('.alert'));
expect(alert.textContent.trim()).toBe('Too many CAPTCHA requests');
// Hide the controlbox so that we can see whether the test passed or failed
u.addClass('hidden', _converse.chatboxviews.get('controlbox'));
delete _converse.connection; delete _converse.connection;
})); }));
}); });

View File

@ -0,0 +1,7 @@
import { _converse, api } from '@converse/headless/core';
export async function setActiveForm (value) {
await api.waitUntil('controlBoxInitialized');
const controlbox = _converse.chatboxes.get('controlbox');
controlbox.set({ 'active-form': value });
}

View File

@ -4,12 +4,13 @@ export default (o) => html`
<div class="form-group"> <div class="form-group">
${ o.label ? html`<label>${o.label}</label>` : '' } ${ o.label ? html`<label>${o.label}</label>` : '' }
<div class="input-group"> <div class="input-group">
<div class="input-group-prepend">
<input name="${o.name}" <input name="${o.name}"
class="form-control"
type="${o.type}" type="${o.type}"
value="${o.value || ''}" value="${o.value || ''}"
?required="${o.required}" /> ?required="${o.required}" />
<div class="input-group-text col" title="${o.domain}">${o.domain}</div> <div class="input-group-append">
<div class="input-group-text" title="${o.domain}">${o.domain}</div>
</div> </div>
</div> </div>
</div>`; </div>`;