From 7b8b32638ccf29f7c8d3c07ca50fbb56398b3afd Mon Sep 17 00:00:00 2001 From: JC Brand Date: Thu, 16 Feb 2023 22:10:14 +0100 Subject: [PATCH] Fixes #3137 - 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) --- CHANGES.md | 5 + src/plugins/controlbox/index.js | 2 +- .../controlbox/styles/_controlbox.scss | 50 ----- src/plugins/controlbox/templates/loginform.js | 6 +- src/plugins/register/index.js | 17 +- src/plugins/register/panel.js | 136 +++++--------- src/plugins/register/styles/register.scss | 61 ++++++ .../register/templates/register_panel.js | 26 +-- .../register/templates/registration_form.js | 15 +- src/plugins/register/tests/register.js | 175 +++++++++++++++--- src/plugins/register/utils.js | 7 + src/templates/form_username.js | 5 +- 12 files changed, 296 insertions(+), 209 deletions(-) create mode 100644 src/plugins/register/styles/register.scss create mode 100644 src/plugins/register/utils.js diff --git a/CHANGES.md b/CHANGES.md index 6b6787bfc..744e7e00e 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -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 diff --git a/src/plugins/controlbox/index.js b/src/plugins/controlbox/index.js index 42a68687b..3026d3302 100644 --- a/src/plugins/controlbox/index.js +++ b/src/plugins/controlbox/index.js @@ -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; diff --git a/src/plugins/controlbox/styles/_controlbox.scss b/src/plugins/controlbox/styles/_controlbox.scss index b1932af43..f162a03d3 100644 --- a/src/plugins/controlbox/styles/_controlbox.scss +++ b/src/plugins/controlbox/styles/_controlbox.scss @@ -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 { diff --git a/src/plugins/controlbox/templates/loginform.js b/src/plugins/controlbox/templates/loginform.js index a6794ed75..92de74c42 100644 --- a/src/plugins/controlbox/templates/loginform.js +++ b/src/plugins/controlbox/templates/loginform.js @@ -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) => {
- ${ show_register_link() ? register_link() : '' } + ${ tplShowRegisterLink() ? tplRegisterLink(el) : '' } `; } diff --git a/src/plugins/register/index.js b/src/plugins/register/index.js index 7cf067276..28e845eaa 100644 --- a/src/plugins/register/index.js +++ b/src/plugins/register/index.js @@ -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')); } }); diff --git a/src/plugins/register/panel.js b/src/plugins/register/panel.js index b61efde61..195cb567a 100644 --- a/src/plugins/register/panel.js +++ b/src/plugins/register/panel.js @@ -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 = ''; - 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', - '

'+message+'

' - ); - 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) { diff --git a/src/plugins/register/styles/register.scss b/src/plugins/register/styles/register.scss new file mode 100644 index 000000000..aec367618 --- /dev/null +++ b/src/plugins/register/styles/register.scss @@ -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); + } + } +} diff --git a/src/plugins/register/templates/register_panel.js b/src/plugins/register/templates/register_panel.js index deef665eb..6407fd90a 100644 --- a/src/plugins/register/templates/register_panel.js +++ b/src/plugins/register/templates/register_panel.js @@ -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` -
+ el.onFormSubmission(ev)}> ${tplSpinner({ 'classes': 'hor_centered' })}

${i18n_fetch_form}

${default_domain ? '' : html` - + `}
`; @@ -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` -
+ el.onFormSubmission(ev)}> ${i18n_create_account}
- + ${default_domain ? default_domain : tplDomainInput()}
- ${default_domain ? '' : tplFetchFormButtons()} + ${show_form_buttons ? tplFetchFormButtons() : ''}
`; }; @@ -71,11 +74,12 @@ const CHOOSE_PROVIDER = 0; const FETCHING_FORM = 1; const REGISTRATION_FORM = 2; -export default o => { +export default (el) => { return html` - ${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`` : '' } + ${el.status === CHOOSE_PROVIDER ? tplChooseProvider(el) : ''} + ${el.status === FETCHING_FORM ? tplFormRequest(el) : ''} + ${el.status === REGISTRATION_FORM ? tplRegistrationForm(el) : ''} `; }; diff --git a/src/plugins/register/templates/registration_form.js b/src/plugins/register/templates/registration_form.js index 31ad3a4b7..d15290381 100644 --- a/src/plugins/register/templates/registration_form.js +++ b/src/plugins/register/templates/registration_form.js @@ -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` -
- ${i18n_legend} ${o.domain} -

${o.title}

-

${o.instructions}

+ el.onFormSubmission(ev)}> + ${i18n_legend} ${el.domain} +

${el.title}

+

${el.instructions}

- ${o.form_fields} + ${el.form_fields}
- ${o.fields + ${el.fields ? html` ` @@ -31,6 +31,7 @@ export default o => { type="button" class="btn btn-secondary button-cancel" value="${i18n_choose_provider}" + @click=${ev => el.renderProviderChoiceForm(ev)} /> `}
diff --git a/src/plugins/register/tests/register.js b/src/plugins/register/tests/register.js index 0ae677cce..2ed05cceb 100644 --- a/src/plugins/register/tests/register.js +++ b/src/plugins/register/tests/register.js @@ -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` @@ -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== You need a client that supports x:data and CAPTCHA to register - `); + `; _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` + + + + Choose a username and password to register with this server + urn:xmpp:captcha + + + conversations.im + 15376320046808160053 + ad1e0d50-5adb-4397-a997-5feab56fe418:sendIQ + + + `; + _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` @@ -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== You need a client that supports x:data and CAPTCHA to register - `); + `; _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(` - + const response_IQ = stx` + Too many CAPTCHA requests - `); + `; _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; })); }); diff --git a/src/plugins/register/utils.js b/src/plugins/register/utils.js new file mode 100644 index 000000000..250b72208 --- /dev/null +++ b/src/plugins/register/utils.js @@ -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 }); +} diff --git a/src/templates/form_username.js b/src/templates/form_username.js index 897712e3d..742a6e7d9 100644 --- a/src/templates/form_username.js +++ b/src/templates/form_username.js @@ -4,12 +4,13 @@ export default (o) => html`
${ o.label ? html`` : '' }
-
-
${o.domain}
+
+
${o.domain}
`;