diff --git a/.gitignore b/.gitignore index bc8a14de4..421fa4f59 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ .DS_Store .thumbs.db +.env node_modules # Quasar core related directories diff --git a/public/icons/email_confirmation/icons.svg b/public/icons/email_confirmation/icons.svg new file mode 100644 index 000000000..55aed3799 --- /dev/null +++ b/public/icons/email_confirmation/icons.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/ride_to_work_by_bike_config.toml b/ride_to_work_by_bike_config.toml index 61e455d53..8df29fc12 100644 --- a/ride_to_work_by_bike_config.toml +++ b/ride_to_work_by_bike_config.toml @@ -72,5 +72,7 @@ apiBase = "https://test.dopracenakole.cz/rest/" apiVersion = "1.0" apiDefaultLang = "cs" +urlApiHasUserVerifiedEmail = "auth/registration/has-user-verified-email-address/" urlApiLogin = "auth/login/" urlApiRefresh = "auth/token/refresh/" +urlApiRegister = "auth/registration/" diff --git a/src/components/__tests__/EmailVerification.cy.js b/src/components/__tests__/EmailVerification.cy.js new file mode 100644 index 000000000..12bb939a3 --- /dev/null +++ b/src/components/__tests__/EmailVerification.cy.js @@ -0,0 +1,207 @@ +import { colors } from 'quasar'; +import { createPinia, setActivePinia } from 'pinia'; +import EmailVerification from 'components/register/EmailVerification.vue'; +import { i18n } from '../../boot/i18n'; +import { useRegisterStore } from '../../stores/register'; +import { routesConf } from '../../router/routes_conf'; +import { rideToWorkByBikeConfig } from '../../boot/global_vars'; +import { getApiBaseUrlWithLang } from '../../utils/get_api_base_url_with_lang'; +import { httpSuccessfullStatus } from '../../../test/cypress/support/commonTests'; + +// colors +const { getPaletteColor, changeAlpha } = colors; +const white = getPaletteColor('white'); +const whiteOpacity20 = changeAlpha(white, 0.2); + +// selectors +const selectorEmailVerification = 'email-verification'; +const selectorEmailVerificationTitle = 'email-verification-title'; +const selectorEmailVerificationText = 'email-verification-text'; +const selectorEmailVerificationWrongEmailHint = + 'email-verification-wrong-email-hint'; +const selectorEmailVerificationRegisterLink = + 'email-verification-register-link'; +const selectorEmailVerificationGraphics = 'email-verification-graphics'; +const selectorEmailVerificationAvatar = 'email-verification-avatar'; +const selectorEmailVerificationIcon = 'email-verification-icon'; + +// variables +const fontSizeTitle = 24; +const fontWeightTitle = 700; +const fontSizeText = 14; +const fontWeightText = 400; +const avatarSize = 64; +const iconSize = 40; +const testEmail = 'test@test.cz'; + +describe('', () => { + it('has translation for all strings', () => { + cy.testLanguageStringsInContext( + [ + 'linkRegister', + 'hintWrongEmail', + 'textEmailVerification', + 'titleEmailVerification', + ], + 'register.form', + i18n, + ); + }); + + context('desktop', () => { + beforeEach(() => { + setActivePinia(createPinia()); + cy.viewport('macbook-16'); + // variables + const { apiBase, apiDefaultLang, urlApiHasUserVerifiedEmail } = + rideToWorkByBikeConfig; + const apiBaseUrl = getApiBaseUrlWithLang( + null, + apiBase, + apiDefaultLang, + i18n, + ); + const apiEmailVerificationUrl = `${apiBaseUrl}${urlApiHasUserVerifiedEmail}`; + // intercept email verification request + cy.intercept('GET', apiEmailVerificationUrl, { + statusCode: httpSuccessfullStatus, + body: { has_user_verified_email_address: true }, + }) + .as('emailVerificationRequest') + .then(() => { + // mount after intercept + cy.mount(EmailVerification, { + props: {}, + }); + }); + }); + + coreTests(); + }); + + context('mobile', () => { + beforeEach(() => { + setActivePinia(createPinia()); + cy.viewport('iphone-6'); + // variables + const { apiBase, apiDefaultLang, urlApiHasUserVerifiedEmail } = + rideToWorkByBikeConfig; + const apiBaseUrl = getApiBaseUrlWithLang( + null, + apiBase, + apiDefaultLang, + i18n, + ); + const apiEmailVerificationUrl = `${apiBaseUrl}${urlApiHasUserVerifiedEmail}`; + // intercept email verification request + cy.intercept('GET', apiEmailVerificationUrl, { + statusCode: httpSuccessfullStatus, + body: { has_user_verified_email_address: true }, + }) + .as('emailVerificationRequest') + .then(() => { + // mount after intercept + cy.mount(EmailVerification, { + props: {}, + }); + }); + }); + + coreTests(); + }); +}); + +function coreTests() { + it('renders component', () => { + cy.dataCy(selectorEmailVerification).should('be.visible'); + // title + cy.dataCy(selectorEmailVerificationTitle) + .should('be.visible') + .and('have.css', 'font-size', `${fontSizeTitle}px`) + .and('have.css', 'font-weight', `${fontWeightTitle}`) + .and('have.color', white) + .and('contain', i18n.global.t('register.form.titleEmailVerification')); + // text + const store = useRegisterStore(); + store.setEmail(testEmail); + cy.dataCy(selectorEmailVerificationText) + .should('be.visible') + .and('contain', testEmail); + // check inner html + cy.dataCy(selectorEmailVerificationText) + .should('be.visible') + .and('have.css', 'font-size', `${fontSizeText}px`) + .and('have.css', 'font-weight', `${fontWeightText}`) + .then(($el) => { + const content = $el.text(); + cy.stripHtmlTags( + i18n.global.t('register.form.textEmailVerification', { + email: testEmail, + }), + ).then((text) => { + expect(content).to.equal(text); + }); + }); + // wrong email hint + cy.dataCy(selectorEmailVerificationWrongEmailHint) + .should('be.visible') + .and('have.css', 'font-size', `${fontSizeText}px`) + .and('have.css', 'font-weight', `${fontWeightText}`) + .and('have.color', white) + .and('contain', i18n.global.t('register.form.hintWrongEmail')); + // register link + cy.dataCy(selectorEmailVerificationRegisterLink) + .should('be.visible') + .and('have.css', 'font-size', `${fontSizeText}px`) + .and('have.css', 'font-weight', `${fontWeightText}`) + .and('have.color', white) + .and('contain', i18n.global.t('register.form.linkRegister')) + .invoke('attr', 'href') + .should('contain', routesConf['register']['path']); + // graphics + cy.dataCy(selectorEmailVerificationGraphics).should('be.visible'); + // avatar + cy.dataCy(selectorEmailVerificationAvatar) + .should('be.visible') + .and('have.backgroundColor', whiteOpacity20) + .invoke('height') + .should('eq', avatarSize); + cy.dataCy(selectorEmailVerificationAvatar) + .invoke('width') + .should('eq', avatarSize); + // icon + cy.dataCy(selectorEmailVerificationIcon) + .should('be.visible') + .and('have.color', white) + .invoke('height') + .should('eq', iconSize); + cy.dataCy(selectorEmailVerificationIcon) + .invoke('width') + .should('eq', iconSize); + }); + + it('makes an email verification request', () => { + // check that email verification request is made + cy.wait('@emailVerificationRequest') + .then((interception) => { + expect(interception.response.statusCode).to.equal( + httpSuccessfullStatus, + ); + expect( + interception.response.body.has_user_verified_email_address, + ).to.equal(true); + }) + .then(() => { + // prevent race condition between modifying and accessing store + return new Cypress.Promise((resolve) => { + setTimeout(() => { + resolve(); + }, 500); + }); + }) + .then(() => { + const store = useRegisterStore(); + expect(store.getIsEmailVerified).to.equal(true); + }); + }); +} diff --git a/src/components/__tests__/FormLogin.cy.js b/src/components/__tests__/FormLogin.cy.js index 40944887e..ae3c13ee3 100644 --- a/src/components/__tests__/FormLogin.cy.js +++ b/src/components/__tests__/FormLogin.cy.js @@ -21,6 +21,8 @@ const contactEmail = rideToWorkByBikeConfig.contactEmail; // selectors const classSelectorQNotificationMessage = '.q-notification__message'; +const selectorLoginPromptNoAccount = 'login-prompt-no-account'; +const selectorLoginLinkRegister = 'login-link-register'; // variables const { apiBase, apiDefaultLang, urlApiLogin, urlApiRefresh } = @@ -158,6 +160,36 @@ describe('', () => { .and('have.text', i18n.global.t('login.form.submitLogin')); }); + it('renders a no account prompt and a link to register', () => { + cy.dataCy(selectorLoginPromptNoAccount) + .should('be.visible') + .and('have.css', 'font-size', '14px') + .and('have.css', 'font-weight', '400') + .and('have.color', white) + .then(($el) => { + const content = $el.text(); + cy.stripHtmlTags(i18n.global.t('login.form.promptNoAccount')).then( + (text) => { + expect(content).to.contain(text); + }, + ); + }); + // register + cy.dataCy(selectorLoginLinkRegister) + .should('be.visible') + .and('have.css', 'font-size', '14px') + .and('have.css', 'font-weight', '400') + .and('have.color', white) + .then(($el) => { + const content = $el.text(); + cy.stripHtmlTags(i18n.global.t('login.form.linkRegister')).then( + (text) => { + expect(content).to.contain(text); + }, + ); + }); + }); + it('allows to navigate between states', () => { cy.dataCy('form-login-forgotten-password').should('be.visible').click(); cy.dataCy('form-password-reset').should('be.visible'); diff --git a/src/components/__tests__/FormRegister.cy.js b/src/components/__tests__/FormRegister.cy.js index 337bd53f7..3c680b202 100644 --- a/src/components/__tests__/FormRegister.cy.js +++ b/src/components/__tests__/FormRegister.cy.js @@ -1,34 +1,84 @@ import { colors } from 'quasar'; - +import { createPinia, setActivePinia } from 'pinia'; import FormRegister from '../register/FormRegister.vue'; import { i18n } from '../../boot/i18n'; import { rideToWorkByBikeConfig } from '../../boot/global_vars'; import route from '../../../src/router'; import { testPasswordInputReveal } from '../../../test/cypress/support/commonTests'; +import { useChallengeStore } from '../../stores/challenge'; +import { useRegisterStore } from '../../stores/register'; +import { + httpSuccessfullStatus, + httpInternalServerErrorStatus, +} from '../../../test/cypress/support/commonTests'; +import { getApiBaseUrlWithLang } from '../../../src/utils/get_api_base_url_with_lang'; +// colors const { getPaletteColor } = colors; - -const router = route(); - -const grey10 = getPaletteColor('grey-10'); const white = getPaletteColor('white'); -const colorPrimary = rideToWorkByBikeConfig.colorPrimary; -const colorWhiteOpacity = rideToWorkByBikeConfig.colorWhiteOpacity; -const borderRadiusCardSmall = rideToWorkByBikeConfig.borderRadiusCardSmall; +// selectors +const selectorFormRegisterTitle = 'form-register-title'; +const selectorFormRegisterEmail = 'form-register-email'; +const selectorFormRegisterPassword = 'form-register-password'; +const selectorFormRegisterPasswordInput = 'form-register-password-input'; +const selectorFormRegisterPasswordIcon = 'form-register-password-icon'; +const selectorFormRegisterPasswordConfirm = 'form-register-password-confirm'; +const selectorFormRegisterPasswordConfirmInput = + 'form-register-password-confirm-input'; +const selectorFormRegisterPasswordConfirmIcon = + 'form-register-password-confirm-icon'; +const selectorFormRegisterSubmit = 'form-register-submit'; +const selectorFormRegisterCoordinator = 'form-register-coordinator'; +const selectorFormRegisterCoordinatorDescription = + 'form-register-coordinator-description'; +const selectorFormRegisterCoordinatorLinkWrapper = + 'form-register-coordinator-link-wrapper'; +const selectorFormRegisterCoordinatorLink = 'form-register-coordinator-link'; +const selectorFormRegisterLogin = 'form-register-login'; +const selectorFormRegisterLoginLink = 'form-register-login-link'; +const selectorFormRegisterTextNoActiveChallenge = + 'form-register-text-no-active-challenge'; +const selectorFormRegisterPrivacyConsent = 'form-register-privacy-consent'; +const selectorFormRegisterNewsletterSubscription = + 'form-register-newsletter-subscription'; + +// variables +const iconSize = 18; +const fontSizeText = 14; +const fontWeightText = 400; +const router = route(); +const testEmail = 'test@test.com'; +const testPassword = '12345a'; +const { + apiBase, + apiDefaultLang, + colorWhiteOpacity, + borderRadiusCardSmall, + urlApiRegister, +} = rideToWorkByBikeConfig; describe('', () => { it('has translation for all strings', () => { cy.testLanguageStringsInContext( [ - 'titleRegister', + 'hintLogin', 'hintPassword', + 'hintRegisterAsCoordinator', + 'labelEmail', + 'labelPassword', + 'labelPasswordConfirm', + 'linkLogin', + 'linkRegisterAsCoordinator', 'messageEmailReqired', 'messageEmailInvalid', 'messagePasswordRequired', 'messagePasswordStrong', 'messagePasswordConfirmRequired', 'messagePasswordConfirmNotMatch', + 'submitRegister', + 'textNoActiveChallenge', + 'titleRegister', ], 'register.form', i18n, @@ -37,6 +87,7 @@ describe('', () => { context('desktop', () => { beforeEach(() => { + setActivePinia(createPinia()); cy.mount(FormRegister, { props: {}, }); @@ -44,37 +95,37 @@ describe('', () => { }); it('renders title', () => { - cy.dataCy('form-register-title') + cy.dataCy(selectorFormRegisterTitle) .should('be.visible') - .and('have.color', grey10) + .and('have.color', white) .and('have.css', 'font-size', '24px') .and('have.css', 'font-weight', '700') .and('contain', i18n.global.t('register.form.titleRegister')); }); it('renders email field', () => { - cy.dataCy('form-register-email').should('be.visible'); + cy.dataCy(selectorFormRegisterEmail).should('be.visible'); }); it('renders password field', () => { - cy.dataCy('form-register-password') + cy.dataCy(selectorFormRegisterPassword) .should('be.visible') .find('label[for="form-register-password"]') .should('be.visible') .and('have.text', i18n.global.t('register.form.labelPassword')); - cy.dataCy('form-register-password') + cy.dataCy(selectorFormRegisterPassword) .find('.q-field__control') .should('be.visible') .and('have.css', 'border-radius', '8px'); }); it('renders password confirm field', () => { - cy.dataCy('form-register-password-confirm') + cy.dataCy(selectorFormRegisterPasswordConfirm) .should('be.visible') .find('label[for="form-register-password-confirm"]') .should('be.visible') .and('have.text', i18n.global.t('register.form.labelPasswordConfirm')); - cy.dataCy('form-register-password-confirm') + cy.dataCy(selectorFormRegisterPasswordConfirm) .find('.q-field__control') .should('be.visible') .and('have.css', 'border-radius', '8px'); @@ -82,112 +133,127 @@ describe('', () => { it('renders show/hide icons for password inputs', () => { // password - cy.dataCy('form-register-password-icon') + cy.dataCy(selectorFormRegisterPasswordIcon) .should('contain', 'visibility') - .and('have.color', `${colorPrimary}`); - cy.dataCy('form-register-password-icon') + .and('have.color', white); + cy.dataCy(selectorFormRegisterPasswordIcon) .invoke('height') - .should('be.equal', 18); - cy.dataCy('form-register-password-icon') + .should('be.equal', iconSize); + cy.dataCy(selectorFormRegisterPasswordIcon) .invoke('width') - .should('be.equal', 18); + .should('be.equal', iconSize); // password confirm - cy.dataCy('form-register-password-confirm-icon') + cy.dataCy(selectorFormRegisterPasswordConfirmIcon) .should('contain', 'visibility') - .and('have.color', `${colorPrimary}`); - cy.dataCy('form-register-password-confirm-icon') + .and('have.color', white); + cy.dataCy(selectorFormRegisterPasswordConfirmIcon) .invoke('height') - .should('be.equal', 18); - cy.dataCy('form-register-password-confirm-icon') + .should('be.equal', iconSize); + cy.dataCy(selectorFormRegisterPasswordConfirmIcon) .invoke('width') - .should('be.equal', 18); + .should('be.equal', iconSize); }); - testPasswordInputReveal('form-register-password'); - testPasswordInputReveal('form-register-password-confirm'); + testPasswordInputReveal(selectorFormRegisterPassword); + testPasswordInputReveal(selectorFormRegisterPasswordConfirm); it('validates password correctly', () => { // fill in email input to be able to test password - cy.dataCy('form-register-email').find('input').type('qw123@qw.com'); + cy.dataCy(selectorFormRegisterEmail).find('input').type('qw123@qw.com'); // test password - cy.dataCy('form-register-submit').should('be.visible').click(); - cy.dataCy('form-register-password') + cy.dataCy(selectorFormRegisterSubmit).should('be.visible').click(); + cy.dataCy(selectorFormRegisterPassword) .find('.q-field__messages') .should( 'contain', i18n.global.t('register.form.messagePasswordRequired'), ); - cy.dataCy('form-register-password-input').clear(); - cy.dataCy('form-register-password-input').type('12345'); - cy.dataCy('form-register-submit').should('be.visible').click(); - cy.dataCy('form-register-password') + cy.dataCy(selectorFormRegisterPasswordInput).clear(); + cy.dataCy(selectorFormRegisterPasswordInput).type('12345'); + cy.dataCy(selectorFormRegisterSubmit).should('be.visible').click(); + cy.dataCy(selectorFormRegisterPassword) .find('.q-field__messages') .should( 'contain', i18n.global.t('register.form.messagePasswordStrong'), ); - cy.dataCy('form-register-password-input').clear(); - cy.dataCy('form-register-password-input').type('123456789'); - cy.dataCy('form-register-password') + cy.dataCy(selectorFormRegisterPasswordInput).clear(); + cy.dataCy(selectorFormRegisterPasswordInput).type('123456789'); + cy.dataCy(selectorFormRegisterPassword) .find('.q-field__messages') .should( 'contain', i18n.global.t('register.form.messagePasswordStrong'), ); - cy.dataCy('form-register-password-input').clear(); - cy.dataCy('form-register-password-input').type('12345a'); - cy.dataCy('form-register-password-input').blur(); - cy.dataCy('form-register-password') + cy.dataCy(selectorFormRegisterPasswordInput).clear(); + cy.dataCy(selectorFormRegisterPasswordInput).type('12345a'); + cy.dataCy(selectorFormRegisterPasswordInput).blur(); + cy.dataCy(selectorFormRegisterPassword) .find('.q-field__messages') .should('contain', i18n.global.t('register.form.hintPassword')); }); it('validates password confirm correctly', () => { // fill in email input to be able to test password - cy.dataCy('form-register-email').find('input').type('qw123@qw.com'); + cy.dataCy(selectorFormRegisterEmail).find('input').type('qw123@qw.com'); // fill in password input to be able to test password confirm - cy.dataCy('form-register-password-input').type('12345a'); + cy.dataCy(selectorFormRegisterPasswordInput).type('12345a'); // test password confirm empty - cy.dataCy('form-register-submit').should('be.visible').click(); - cy.dataCy('form-register-password-confirm') + cy.dataCy(selectorFormRegisterSubmit).should('be.visible').click(); + cy.dataCy(selectorFormRegisterPasswordConfirm) .find('.q-field__messages') .should( 'contain', i18n.global.t('register.form.messagePasswordConfirmRequired'), ); - cy.dataCy('form-register-password-confirm-input').clear(); + cy.dataCy(selectorFormRegisterPasswordConfirmInput).clear(); // test password confirm not matching - cy.dataCy('form-register-password-confirm-input').type('12345b'); - cy.dataCy('form-register-password-confirm-input').blur(); - cy.dataCy('form-register-password-confirm') + cy.dataCy(selectorFormRegisterPasswordConfirmInput).type('12345b'); + cy.dataCy(selectorFormRegisterPasswordConfirmInput).blur(); + cy.dataCy(selectorFormRegisterPasswordConfirm) .find('.q-field__messages') .should( 'contain', i18n.global.t('register.form.messagePasswordConfirmNotMatch'), ); - cy.dataCy('form-register-password-confirm-input').clear(); + cy.dataCy(selectorFormRegisterPasswordConfirmInput).clear(); // test password confirm matching - cy.dataCy('form-register-password-confirm-input').type('12345a'); - cy.dataCy('form-register-password-confirm-input').blur(); + cy.dataCy(selectorFormRegisterPasswordConfirmInput).type('12345a'); + cy.dataCy(selectorFormRegisterPasswordConfirmInput).blur(); // testing non-existence of element fails on .find() method cy.get( '*[data-cy="form-register-coordinator-terms] .q-field__messages', ).should('not.exist'); }); + it('validates privacy policy correctly', () => { + // fill in email input to be able to test password + cy.dataCy(selectorFormRegisterEmail).find('input').type('qw123@qw.com'); + // fill in password input to be able to test password confirm + cy.dataCy(selectorFormRegisterPasswordInput).type('12345a'); + cy.dataCy(selectorFormRegisterPasswordConfirmInput).type('12345a'); + // validate privacy policy + cy.dataCy(selectorFormRegisterSubmit).should('be.visible').click(); + cy.dataCy(selectorFormRegisterPrivacyConsent).within(() => { + cy.contains( + i18n.global.t('register.form.messagePrivacyConsentRequired'), + ).should('be.visible'); + }); + }); + it('renders box with coordinator registration link', () => { const urlRegisterCoordinator = router.resolve({ name: 'register-coordinator', }).href; // wrapper - cy.dataCy('form-register-coordinator') + cy.dataCy(selectorFormRegisterCoordinator) .should('have.css', 'padding', '16px') .and('have.backgroundColor', colorWhiteOpacity) .and('have.css', 'border-radius', borderRadiusCardSmall); // description - cy.dataCy('form-register-coordinator-description') - .should('have.css', 'font-size', '14px') - .and('have.css', 'font-weight', '400') + cy.dataCy(selectorFormRegisterCoordinatorDescription) + .should('have.css', 'font-size', `${fontSizeText}px`) + .and('have.css', 'font-weight', `${fontWeightText}`) .and('have.css', 'margin-top', '0px') .and('have.css', 'margin-bottom', '16px') .and('have.color', white) @@ -201,11 +267,11 @@ describe('', () => { ); }); // spacing - cy.dataCy('form-register-coordinator-link-wrapper') + cy.dataCy(selectorFormRegisterCoordinatorLinkWrapper) .should('have.css', 'margin-top', '16px') .and('have.css', 'margin-bottom', '0px'); // link - cy.dataCy('form-register-coordinator-link') + cy.dataCy(selectorFormRegisterCoordinatorLink) .should('have.color', white) .and('have.attr', 'href', urlRegisterCoordinator) .and( @@ -217,26 +283,238 @@ describe('', () => { it('renders link to login page', () => { const urlLogin = router.resolve({ name: 'login' }).href; // wrapper - cy.dataCy('form-register-login') + cy.dataCy(selectorFormRegisterLogin) .should('have.color', white) - .and('have.css', 'font-size', '14px') + .and('have.css', 'font-size', `${fontSizeText}px`) + .and('have.css', 'font-weight', `${fontWeightText}`) .and('have.css', 'margin-top', '24px') .and('contain', i18n.global.t('register.form.hintLogin')); // link - cy.dataCy('form-register-login-link') + cy.dataCy(selectorFormRegisterLoginLink) .should('have.color', white) - .and('have.css', 'font-size', '14px') + .and('have.css', 'font-size', `${fontSizeText}px`) + .and('have.css', 'font-weight', `${fontWeightText}`) .and('have.attr', 'href', urlLogin) .and('contain', i18n.global.t('register.form.linkLogin')); }); + + it('shows an error if the registration fails', () => { + const registerStore = useRegisterStore(); + // default store state + expect(registerStore.getEmail).to.equal(''); + expect(registerStore.getIsEmailVerified).to.equal(false); + // variables + const apiBaseUrl = getApiBaseUrlWithLang( + null, + apiBase, + apiDefaultLang, + i18n, + ); + const apiRegisterUrl = `${apiBaseUrl}${urlApiRegister}`; + // intercept registration API call + cy.intercept('POST', apiRegisterUrl, { + statusCode: httpInternalServerErrorStatus, + }).as('registerRequest'); + // register + cy.wrap(registerStore.register(testEmail, testPassword)).then( + (response) => { + expect(response).to.deep.equal(null); + // state does not change + expect(registerStore.getEmail).to.equal(''); + expect(registerStore.getIsEmailVerified).to.equal(false); + // error is shown + cy.contains( + i18n.global.t('register.apiMessageErrorWithMessage'), + ).should('be.visible'); + }, + ); + }); + + it('allows to register with email and password', () => { + const registerStore = useRegisterStore(); + // default store state + expect(registerStore.getEmail).to.equal(''); + expect(registerStore.getIsEmailVerified).to.equal(false); + // variables + const apiBaseUrl = getApiBaseUrlWithLang( + null, + apiBase, + apiDefaultLang, + i18n, + ); + const apiRegisterUrl = `${apiBaseUrl}${urlApiRegister}`; + cy.fixture('registerResponse.json').then((registerResponse) => { + // intercept registration API call + cy.intercept('POST', apiRegisterUrl, { + statusCode: httpSuccessfullStatus, + body: registerResponse, + }).then(() => { + // register + cy.wrap(registerStore.register(testEmail, testPassword)).then( + (response) => { + // test function return value + expect(response).to.deep.equal(registerResponse); + // store state + expect(registerStore.getEmail).to.equal( + registerResponse.user.email, + ); + expect(registerStore.getIsEmailVerified).to.equal(false); + }, + ); + }); + }); + }); + }); + + context('no active challenge', () => { + beforeEach(() => { + setActivePinia(createPinia()); + cy.mount(FormRegister, { + props: {}, + }); + cy.viewport('iphone-6'); + }); + + it('shows a text with no active challenge', () => { + const challengeStore = useChallengeStore(); + challengeStore.setIsChallengeActive(false); + expect(challengeStore.getIsChallengeActive).to.equal(false); + cy.dataCy(selectorFormRegisterTextNoActiveChallenge) + .should('be.visible') + .and('have.css', 'font-size', `${fontSizeText}px`) + .and('have.css', 'font-weight', `${fontWeightText}`) + .and('have.color', white) + .and('contain', i18n.global.t('register.form.textNoActiveChallenge')); + }); + + it('shows checkboxes for privacy policy and newsletter subscription', () => { + // privacy policy + cy.dataCy(selectorFormRegisterPrivacyConsent) + .should('be.visible') + .and('have.css', 'font-size', `${fontSizeText}px`) + .and('have.css', 'font-weight', `${fontWeightText}`) + .and('have.color', white) + .and('contain', i18n.global.t('register.form.labelPrivacyConsent1')) + .and('contain', i18n.global.t('register.form.labelPrivacyConsentLink')) + .and('contain', i18n.global.t('register.form.labelPrivacyConsent2')); + // newsletter subscription + cy.dataCy(selectorFormRegisterNewsletterSubscription) + .should('be.visible') + .and('have.css', 'font-size', `${fontSizeText}px`) + .and('have.css', 'font-weight', `${fontWeightText}`) + .and('have.color', white) + .and( + 'contain', + i18n.global.t('register.form.labelNewsletterSubscription'), + ); + }); + + it('allows to submit form after filling fields', () => { + // variables + const apiBaseUrl = getApiBaseUrlWithLang( + null, + apiBase, + apiDefaultLang, + i18n, + ); + const apiRegisterUrl = `${apiBaseUrl}${urlApiRegister}`; + cy.fixture('registerResponse.json').then((registerResponse) => { + // intercept registration API call + cy.intercept('POST', apiRegisterUrl, { + statusCode: httpSuccessfullStatus, + body: registerResponse, + }).as('registerRequest'); + // fill in form + cy.dataCy(selectorFormRegisterEmail).find('input').type(testEmail); + cy.dataCy(selectorFormRegisterPasswordInput).type(testPassword); + cy.dataCy(selectorFormRegisterPasswordConfirmInput).type(testPassword); + // accept privacy policy + cy.dataCy(selectorFormRegisterPrivacyConsent) + .should('be.visible') + .click('topLeft'); + // submit form + cy.dataCy(selectorFormRegisterSubmit).should('be.visible').click(); + // check that form is submitted + cy.wait('@registerRequest') + .its('response.statusCode') + .should('be.equal', httpSuccessfullStatus) + .then(() => { + cy.contains(i18n.global.t('register.apiMessageSuccess')).should( + 'be.visible', + ); + const registerStore = useRegisterStore(); + expect(registerStore.getEmail).to.equal( + registerResponse.user.email, + ); + expect(registerStore.getIsEmailVerified).to.equal(false); + }); + }); + }); }); - context('mobile', () => { + context('active challenge', () => { beforeEach(() => { + setActivePinia(createPinia()); cy.mount(FormRegister, { props: {}, }); cy.viewport('iphone-6'); }); + + it('does not show a text with no active challenge', () => { + const challengeStore = useChallengeStore(); + challengeStore.setIsChallengeActive(true); + expect(challengeStore.getIsChallengeActive).to.equal(true); + cy.dataCy(selectorFormRegisterTextNoActiveChallenge).should('not.exist'); + }); + + it('does not show checkboxes for privacy policy and newsletter subscription', () => { + const challengeStore = useChallengeStore(); + challengeStore.setIsChallengeActive(true); + expect(challengeStore.getIsChallengeActive).to.equal(true); + cy.dataCy(selectorFormRegisterPrivacyConsent).should('not.exist'); + cy.dataCy(selectorFormRegisterNewsletterSubscription).should('not.exist'); + }); + + it('allows to submit form after filling fields and accepting privacy policy', () => { + const challengeStore = useChallengeStore(); + challengeStore.setIsChallengeActive(true); + expect(challengeStore.getIsChallengeActive).to.equal(true); + // variables + const apiBaseUrl = getApiBaseUrlWithLang( + null, + apiBase, + apiDefaultLang, + i18n, + ); + const apiRegisterUrl = `${apiBaseUrl}${urlApiRegister}`; + cy.fixture('registerResponse.json').then((registerResponse) => { + // intercept registration API call + cy.intercept('POST', apiRegisterUrl, { + statusCode: httpSuccessfullStatus, + body: registerResponse, + }).as('registerRequest'); + // fill in form + cy.dataCy(selectorFormRegisterEmail).find('input').type(testEmail); + cy.dataCy(selectorFormRegisterPasswordInput).type(testPassword); + cy.dataCy(selectorFormRegisterPasswordConfirmInput).type(testPassword); + // submit form + cy.dataCy(selectorFormRegisterSubmit).should('be.visible').click(); + // check that form is submitted + cy.wait('@registerRequest') + .its('response.statusCode') + .should('be.equal', httpSuccessfullStatus) + .then(() => { + cy.contains(i18n.global.t('register.apiMessageSuccess')).should( + 'be.visible', + ); + const registerStore = useRegisterStore(); + expect(registerStore.getEmail).to.equal( + registerResponse.user.email, + ); + expect(registerStore.getIsEmailVerified).to.equal(false); + }); + }); + }); }); }); diff --git a/src/components/global/FormFieldEmail.vue b/src/components/global/FormFieldEmail.vue index 431590859..cb688fb89 100644 --- a/src/components/global/FormFieldEmail.vue +++ b/src/components/global/FormFieldEmail.vue @@ -12,6 +12,13 @@ * @props * - `value` (string, required): The object representing user input. * It should be of type `string`. + * - `color` (string, optional): The color of the input. Defaults to `white`. + * - `bgColor` (string, optional): The background color of the input. + * Defaults to `transparent`. + * - `dark` (boolean, optional): Whether the input should be dark. + * Defaults to `false`. + * - `testing` (boolean, optional): Wheter this is a testing environment. + * Defaults to `false`. * * @events * - `update:modelValue`: Emitted as a part of v-model structure. @@ -31,10 +38,18 @@ import { useValidation } from 'src/composables/useValidation'; export default defineComponent({ name: 'FormFieldEmail', props: { + dark: { + type: Boolean, + default: false, + }, modelValue: { type: String, required: true, }, + color: { + type: String, + default: 'grey-10', + }, bgColor: { type: String as () => 'white' | 'transparent', default: 'transparent', @@ -76,6 +91,9 @@ export default defineComponent({ -
-

+

+

{{ $t('login.form.promptNoAccount') }} {{ $t('login.form.linkRegister') }} +/** + * EmailVerification Component + * + * @description * Renders a confirmation message after user registers, + * prompting user to confirm their address by clicking the link in the email. + * + * @example + * + * + * @see [Figma Design](https://www.figma.com/design/L8dVREySVXxh3X12TcFDdR/Do-pr%C3%A1ce-na-kole?node-id=4858-103494&t=6I4I349ASWWgGjGu-1) + */ + +// libraries +import { colors } from 'quasar'; +import { computed, defineComponent, inject, onMounted, watch } from 'vue'; +import { useRouter } from 'vue-router'; + +// config +import { routesConf } from '../../router/routes_conf'; + +// stores +import { useRegisterStore } from '../../stores/register'; + +// types +import type { Logger } from '../types/Logger'; + +export default defineComponent({ + name: 'EmailVerification', + setup() { + const logger = inject('vuejs3-logger') as Logger | null; + const registerStore = useRegisterStore(); + const email = computed(() => registerStore.getEmail); + const isEmailVerified = computed(() => registerStore.getIsEmailVerified); + // check email verification on page load + onMounted(async () => { + if (!isEmailVerified.value) { + await registerStore.checkEmailVerification(); + } + }); + const router = useRouter(); + // once email is verified, redirect to home page + watch(isEmailVerified, (newValue) => { + if (newValue) { + logger?.debug( + `Email address <${email.value}> was verified successfully, redirect to <${routesConf['home']['path']}> URL.`, + ); + router.push(routesConf['home']['path']); + } + }); + + // colors + const { getPaletteColor, changeAlpha } = colors; + const white = getPaletteColor('white'); + const whiteOpacity20 = changeAlpha(white, 0.2); + + return { + email, + white, + whiteOpacity20, + }; + }, +}); + + + diff --git a/src/components/register/FormRegister.vue b/src/components/register/FormRegister.vue index 2d696b36f..b8476d15c 100644 --- a/src/components/register/FormRegister.vue +++ b/src/components/register/FormRegister.vue @@ -15,6 +15,7 @@ * @slots * * @components + * - `FormFieldEmail`: Component to render email input. * - `LoginRegisterButtons`: Component to render third-party authentication * buttons. * @@ -25,7 +26,7 @@ */ // libraries -import { defineComponent, ref, reactive } from 'vue'; +import { defineComponent, ref, reactive, computed } from 'vue'; import { rideToWorkByBikeConfig } from '../../boot/global_vars'; // composables @@ -35,6 +36,10 @@ import { useValidation } from '../../composables/useValidation'; import FormFieldEmail from '../global/FormFieldEmail.vue'; import LoginRegisterButtons from '../global/LoginRegisterButtons.vue'; +// stores +import { useChallengeStore } from '../../stores/challenge'; +import { useRegisterStore } from '../../stores/register'; + export default defineComponent({ name: 'FormRegister', components: { @@ -45,18 +50,32 @@ export default defineComponent({ setup() { const formRegister = reactive({ email: '', - password: '', - passwordConfirm: '', + password1: '', + password2: '', }); + const registerStore = useRegisterStore(); + const challengeStore = useChallengeStore(); + const isActiveChallenge = computed( + () => challengeStore.getIsChallengeActive, + ); const isPassword = ref(true); const isPasswordConfirm = ref(true); + const isPrivacyConsent = ref(false); + const isNewsletterSubscription = ref(false); const { isEmail, isFilled, isIdentical, isStrongPassword } = useValidation(); - const onSubmitRegister = () => { - // noop + const onSubmitRegister = async (): Promise => { + // fields are already validated in the QForm + await registerStore.register(formRegister.email, formRegister.password1); + }; + + const onReset = (): void => { + formRegister.email = ''; + formRegister.password1 = ''; + formRegister.password2 = ''; }; const backgroundColor = rideToWorkByBikeConfig.colorWhiteOpacity; @@ -66,35 +85,45 @@ export default defineComponent({ backgroundColor, borderRadius, formRegister, + isActiveChallenge, isPassword, isPasswordConfirm, + isPrivacyConsent, + isNewsletterSubscription, isEmail, isFilled, isIdentical, isStrongPassword, onSubmitRegister, + onReset, }; }, });