- 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
## 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)
- #1851: Sort open groupchats alphabetically

View File

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

View File

@ -109,56 +109,6 @@
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 {
color: var(--controlbox-head-color);
&.error {

View File

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

View File

@ -9,6 +9,7 @@
import './panel.js';
import { __ } from 'i18n';
import { _converse, api, converse } from '@converse/headless/core';
import { setActiveForm } from './utils.js';
// Strophe methods for building stanzas
const { Strophe } = converse.env;
@ -32,6 +33,8 @@ converse.plugins.add('converse-register', {
},
initialize () {
const { router } = _converse;
_converse.CONNECTION_STATUS[Strophe.Status.REGIFAIL] = 'REGIFAIL';
_converse.CONNECTION_STATUS[Strophe.Status.REGISTERED] = 'REGISTERED';
_converse.CONNECTION_STATUS[Strophe.Status.CONFLICT] = 'CONFLICT';
@ -44,17 +47,7 @@ converse.plugins.add('converse-register', {
'registration_domain': ''
});
async function setActiveForm (value) {
await api.waitUntil('controlBoxInitialized');
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);
});
router.route('converse/login', () => setActiveForm('login'));
router.route('converse/register', () => setActiveForm('register'));
}
});

View File

@ -1,16 +1,18 @@
import log from "@converse/headless/log";
import pick from "lodash-es/pick";
import tplFormInput from "templates/form_input.js";
import tplFormUrl from "templates/form_url.js";
import tplFormUsername from "templates/form_username.js";
import tplRegisterPanel from "./templates/register_panel.js";
import tplSpinner from "templates/spinner.js";
import { webForm2xForm } from "@converse/headless/utils/form";
import { ElementView } from "@converse/skeletor/src/element";
import { CustomElement } from 'shared/components/element.js';
import { __ } from 'i18n';
import { _converse, api, converse } from "@converse/headless/core.js";
import { initConnection } from '@converse/headless/utils/init.js';
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
const { Strophe, sizzle, $iq } = converse.env;
@ -27,38 +29,29 @@ const REGISTRATION_FORM = 2;
* @namespace _converse.RegisterPanel
* @memberOf _converse
*/
class RegisterPanel extends ElementView {
id = "converse-register-panel"
className = 'controlbox-pane fade-in'
events = {
'submit form#converse-register': 'onFormSubmission',
'click .button-cancel': 'renderProviderChoiceForm',
class RegisterPanel extends CustomElement {
static get properties () {
return {
status : { type: String },
error_message: { type: String },
}
}
initialize () {
this.reset();
const controlbox = _converse.chatboxes.get('controlbox');
this.model = controlbox;
this.listenTo(_converse, 'connectionInitialized', this.registerHooks);
this.listenTo(this.model, 'change:registration_status', this.render);
this.listenTo(_converse, 'connectionInitialized', () => this.registerHooks());
const domain = api.settings.get('registration_domain');
if (domain) {
this.fetchRegistrationForm(domain);
} else {
this.model.set('registration_status', CHOOSE_PROVIDER);
this.status = CHOOSE_PROVIDER;
}
}
render () {
render(tplRegisterPanel({
'domain': this.domain,
'fields': this.fields,
'form_fields': this.form_fields,
'instructions': this.instructions,
'model': this.model,
'title': this.title,
}), this);
return tplRegisterPanel(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.
* @private
@ -98,9 +86,8 @@ class RegisterPanel extends ElementView {
const body = conn._proto._reqToData(req);
if (!body) { return; }
if (conn._proto._connect_cb(body) === Strophe.Status.CONNFAIL) {
this.showValidationError(
__("Sorry, we're unable to connect to your chosen provider.")
);
this.status = CHOOSE_PROVIDER;
this.error_message = __("Sorry, we're unable to connect to your chosen provider.");
return false;
}
const register = body.getElementsByTagName("register");
@ -111,14 +98,14 @@ class RegisterPanel extends ElementView {
}
if (register.length === 0) {
conn._changeConnectStatus(Strophe.Status.REGIFAIL);
this.showValidationError(
this.error_message =
__("Sorry, the given provider does not support in "+
"band account registration. Please try with a "+
"different provider."))
"different provider.");
return true;
}
// 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();
stanza.setAttribute("id", conn.getUniqueId("sendIQ"));
conn.send(stanza);
@ -149,7 +136,7 @@ class RegisterPanel extends ElementView {
return false;
}
this.setFields(stanza);
if (this.model.get('registration_status') === FETCHING_FORM) {
if (this.status === FETCHING_FORM) {
this.renderRegistrationForm(stanza);
}
return false;
@ -167,9 +154,7 @@ class RegisterPanel extends ElementView {
form_type: null
};
Object.assign(this, defaults);
if (settings) {
Object.assign(this, pick(settings, Object.keys(defaults)));
}
if (settings) Object.assign(this, settings);
}
/**
@ -179,7 +164,7 @@ class RegisterPanel extends ElementView {
* @param { Event } ev
*/
onFormSubmission (ev) {
if (ev && ev.preventDefault) { ev.preventDefault(); }
ev?.preventDefault?.();
if (ev.target.querySelector('input[name=domain]') === null) {
this.submitRegistrationForm(ev.target);
} else {
@ -195,15 +180,8 @@ class RegisterPanel extends ElementView {
* @param { HTMLElement } form - The form that was submitted
*/
onProviderChosen (form) {
const domain_input = form.querySelector('input[name=domain]'),
domain = domain_input?.value;
if (!domain) {
// TODO: add validation message
domain_input.classList.add('error');
return;
}
form.querySelector('input[type=submit]').classList.add('hidden');
this.fetchRegistrationForm(domain.trim());
const domain = form.querySelector('input[name=domain]')?.value;
if (domain) this.fetchRegistrationForm(domain.trim());
}
/**
@ -213,7 +191,7 @@ class RegisterPanel extends ElementView {
* @param { String } domain_name - XMPP server domain
*/
fetchRegistrationForm (domain_name) {
this.model.set('registration_status', FETCHING_FORM);
this.status = FETCHING_FORM;
this.reset({
'domain': Strophe.getDomainFromJid(domain_name),
'_registering': true
@ -221,7 +199,7 @@ class RegisterPanel extends ElementView {
initConnection(this.domain);
// When testing, the test tears down before the async function
// 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;
}
@ -281,6 +259,8 @@ class RegisterPanel extends ElementView {
this.fields.password,
_converse.onConnectStatusChanged
);
setActiveForm('login');
_converse.router.navigate('');
this.giveFeedback(__('Now logging you in'), 'info');
} else {
_converse.giveFeedback(__('Registered successfully'));
@ -334,29 +314,7 @@ class RegisterPanel extends ElementView {
*/
renderRegistrationForm (stanza) {
this.form_fields = this.getFormFields(stanza);
this.model.set('registration_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');
this.status = REGISTRATION_FORM;
}
/**
@ -367,31 +325,31 @@ class RegisterPanel extends ElementView {
* @param { XMLElement } stanza - The IQ stanza received from the XMPP server
*/
reportErrors (stanza) {
const errors = stanza.querySelectorAll('error');
errors.forEach(e => this.showValidationError(e.textContent));
if (!errors.length) {
const message = __('The provider rejected your registration attempt. '+
const errors = Array.from(stanza.querySelectorAll('error'));
if (errors.length) {
this.error_message = errors.reduce((result, e) => `${result}\n${e.textContent}`, '');
} else {
this.error_message = __('The provider rejected your registration attempt. '+
'Please check the values you entered for correctness.');
this.showValidationError(message);
}
}
renderProviderChoiceForm (ev) {
if (ev && ev.preventDefault) { ev.preventDefault(); }
ev?.preventDefault?.();
_converse.connection._proto._abortAllRequests();
_converse.connection.reset();
this.render();
this.status = CHOOSE_PROVIDER;
}
abortRegistration () {
_converse.connection._proto._abortAllRequests();
_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')) {
this.fetchRegistrationForm(api.settings.get('registration_domain'));
}
} else {
this.render();
this.requestUpdate();
}
}
@ -403,16 +361,6 @@ class RegisterPanel extends ElementView {
* @param { HTMLElement } form - The HTML form that was submitted
*/
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 iq = $iq({'type': 'set', 'id': u.getUniqueId()})
.c("query", {xmlns:Strophe.NS.REGISTER});
@ -425,7 +373,7 @@ class RegisterPanel extends ElementView {
} else {
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);
this.setFields(iq.tree());
}
@ -462,8 +410,8 @@ class RegisterPanel extends ElementView {
}
_setFieldsFromXForm (xform) {
this.title = xform.querySelector('title')?.textContent;
this.instructions = xform.querySelector('instructions')?.textContent;
this.title = xform.querySelector('title')?.textContent ?? '';
this.instructions = xform.querySelector('instructions')?.textContent ?? '';
xform.querySelectorAll('field').forEach(field => {
const _var = field.getAttribute('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 { html } from 'lit';
const tplFormRequest = () => {
const tplFormRequest = (el) => {
const default_domain = api.settings.get('registration_domain');
const i18n_fetch_form = __("Hold tight, we're fetching the registration form…");
const i18n_cancel = __('Cancel');
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' })}
<p class="info">${i18n_fetch_form}</p>
${default_domain
? ''
: 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>
`;
@ -50,19 +51,21 @@ const tplFetchFormButtons = () => {
`;
};
const tplChooseProvider = () => {
const tplChooseProvider = (el) => {
const default_domain = api.settings.get('registration_domain');
const i18n_create_account = __('Create your account');
const i18n_choose_provider = __('Please enter the XMPP provider to register with:');
const show_form_buttons = !default_domain && el.status === CHOOSE_PROVIDER;
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>
<div class="form-group">
<label>${i18n_choose_provider}</label>
<div class="form-errors hidden"></div>
${default_domain ? default_domain : tplDomainInput()}
</div>
${default_domain ? '' : tplFetchFormButtons()}
${show_form_buttons ? tplFetchFormButtons() : ''}
</form>
`;
};
@ -71,11 +74,12 @@ const CHOOSE_PROVIDER = 0;
const FETCHING_FORM = 1;
const REGISTRATION_FORM = 2;
export default o => {
export default (el) => {
return html`
<converse-brand-logo></converse-brand-logo>
${o.model.get('registration_status') === CHOOSE_PROVIDER ? tplChooseProvider() : ''}
${o.model.get('registration_status') === FETCHING_FORM ? tplFormRequest() : ''}
${o.model.get('registration_status') === REGISTRATION_FORM ? tplRegistrationForm(o) : ''}
${ el.error_message ? html`<div class="alert alert-danger" role="alert">${el.error_message}</div>` : '' }
${el.status === CHOOSE_PROVIDER ? tplChooseProvider(el) : ''}
${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 { html } from 'lit';
export default o => {
export default (el) => {
const i18n_choose_provider = __('Choose a different provider');
const i18n_has_account = __('Already have a chat account?');
const i18n_legend = __('Account Registration:');
@ -11,15 +11,15 @@ export default o => {
const registration_domain = api.settings.get('registration_domain');
return html`
<form id="converse-register" class="converse-form">
<legend class="col-form-label">${i18n_legend} ${o.domain}</legend>
<p class="title">${o.title}</p>
<p class="form-help instructions">${o.instructions}</p>
<form id="converse-register" class="converse-form" @submit=${ev => el.onFormSubmission(ev)}>
<legend class="col-form-label">${i18n_legend} ${el.domain}</legend>
<p class="title">${el.title}</p>
<p class="form-help instructions">${el.instructions}</p>
<div class="form-errors hidden"></div>
${o.form_fields}
${el.form_fields}
<fieldset class="buttons form-group">
${o.fields
${el.fields
? html`
<input type="submit" class="btn btn-primary" value="${i18n_register}" />
`
@ -31,6 +31,7 @@ export default o => {
type="button"
class="btn btn-secondary button-cancel"
value="${i18n_choose_provider}"
@click=${ev => el.renderProviderChoiceForm(ev)}
/>
`}
<div class="switch-form">

View File

@ -1,9 +1,7 @@
/*global mock, converse */
const Strophe = converse.env.Strophe;
const $iq = converse.env.$iq;
const { sizzle} = converse.env;
const u = converse.env.utils;
const { stx, Strophe, $iq, sizzle, u } = converse.env;
describe("The Registration Panel", function () {
@ -88,6 +86,61 @@ describe("The Registration Panel", function () {
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",
mock.initConverse(
['chatBoxesInitialized'],
@ -108,7 +161,6 @@ describe("The Registration Panel", function () {
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
expect(registerview._registering).toBeFalsy();
expect(_converse.api.connection.connected()).toBeFalsy();
@ -140,7 +192,8 @@ describe("The Registration Panel", function () {
_converse.connection._dataRecv(mock.createRequest(stanza));
expect(registerview.onRegistrationFields).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=button]').length).toBe(1);
}));
@ -168,7 +221,6 @@ describe("The Registration Panel", function () {
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[type=submit]').click();
@ -192,7 +244,9 @@ describe("The Registration Panel", function () {
_converse.connection._dataRecv(mock.createRequest(stanza));
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=email]').value = 'test@email.local';
@ -229,7 +283,6 @@ describe("The Registration Panel", function () {
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[type=submit]').click();
@ -255,7 +308,9 @@ describe("The Registration Panel", function () {
_converse.connection._dataRecv(mock.createRequest(stanza));
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=email]').value = 'test@email.local';
@ -304,12 +359,6 @@ describe("The Registration Panel", function () {
const cbview = _converse.chatboxviews.get('controlbox');
cbview.querySelector('.toggle-register-login').click();
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[type=submit]').click();
@ -321,7 +370,7 @@ describe("The Registration Panel", function () {
.c('mechanisms', {xmlns: "urn:ietf:params:xml:ns:xmpp-sasl"});
_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">
<query xmlns="jabber:iq:register">
<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>
<instructions>You need a client that supports x:data and CAPTCHA to register</instructions>
</query>
</iq>`);
</iq>`;
_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.querySelectorAll('#converse-register input[required]').length).toBe(3);
// Hide the controlbox so that we can see whether the test
// passed or failed
u.addClass('hidden', _converse.chatboxviews.get('controlbox').el);
// Hide the controlbox so that we can see whether the test passed or failed
u.addClass('hidden', _converse.chatboxviews.get('controlbox'));
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;
}));
@ -385,7 +497,7 @@ describe("The Registration Panel", function () {
.c('mechanisms', {xmlns: "urn:ietf:params:xml:ns:xmpp-sasl"});
_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">
<query xmlns="jabber:iq:register">
<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>
<instructions>You need a client that supports x:data and CAPTCHA to register</instructions>
</query>
</iq>`);
</iq>`;
_converse.connection._dataRecv(mock.createRequest(stanza));
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';
const password_input = view.querySelector('[name="password"]');
password_input.value = 'secret';
@ -421,16 +534,20 @@ describe("The Registration Panel", function () {
view.querySelector('[type="submit"]').click();
expect(view.submitRegistrationForm).toHaveBeenCalled();
const response_IQ = u.toStanza(`
<iq xml:lang='en' from='conversejs.org' type='error' id='d9917b7a-588f-4ef6-8a56-0d6d3ad538ae:sendIQ'>
const response_IQ = stx`
<iq xml:lang='en' from='conversejs.org' type='error' id='d9917b7a-588f-4ef6-8a56-0d6d3ad538ae:sendIQ' xmlns="jabber:client">
<query xmlns='jabber:iq:register'/>
<error code='500' type='wait'>
<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>
</error>
</iq>`);
</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;
}));
});

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">
${ o.label ? html`<label>${o.label}</label>` : '' }
<div class="input-group">
<div class="input-group-prepend">
<input name="${o.name}"
class="form-control"
type="${o.type}"
value="${o.value || ''}"
?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>`;