diff --git a/src/components/__tests__/ProfileQuestionnaires.cy.js b/src/components/__tests__/ProfileQuestionnaires.cy.js new file mode 100644 index 000000000..832ceee6d --- /dev/null +++ b/src/components/__tests__/ProfileQuestionnaires.cy.js @@ -0,0 +1,128 @@ +import { colors } from 'quasar'; +import ProfileQuestionnaires from 'components/profile/ProfileQuestionnaires.vue'; +import { i18n } from '../../boot/i18n'; +import { + failOnStatusCode, + httpSuccessfullStatus, + httpTooManyRequestsStatus, + httpTooManyRequestsStatusMessage, +} from '../../../test/cypress/support/commonTests'; + +// colors +const { getPaletteColor } = colors; +const primary = getPaletteColor('primary'); + +// selectors +const selectorProfileQuestionnaire = 'profile-questionnaires'; +const selectorQuestionnaireItem = 'questionnaire-item'; +const selectorQuestionnaireTitle = 'questionnaire-title'; +const selectorQuestionnaireButton = 'questionnaire-button'; +const selectorQuestionnaireButtonIcon = 'questionnaire-button-icon'; + +// variables +const iconSize = 18; + +describe('', () => { + let questionnaires; + + before(() => { + cy.fixture('listQuestionnaires').then((fixtureData) => { + questionnaires = fixtureData; + }); + }); + + it('has translation for all strings', () => { + cy.testLanguageStringsInContext( + ['buttonFillQuestionnaire'], + 'profile', + i18n, + ); + }); + + context('desktop', () => { + beforeEach(() => { + cy.mount(ProfileQuestionnaires, { + props: {}, + }); + cy.viewport('macbook-16'); + }); + + coreTests(); + }); + + context('mobile', () => { + beforeEach(() => { + cy.mount(ProfileQuestionnaires, { + props: {}, + }); + cy.viewport('iphone-6'); + }); + + coreTests(); + }); + + function coreTests() { + it('renders component', () => { + cy.dataCy(selectorProfileQuestionnaire).should('be.visible'); + }); + + it('renders correct number of questionnaire items', () => { + cy.dataCy(selectorQuestionnaireItem).should( + 'have.length', + questionnaires.length, + ); + }); + + it('displays correct title and button URL for each questionnaire item', () => { + questionnaires.forEach((questionnaire, index) => { + cy.dataCy(selectorQuestionnaireItem) + .eq(index) + .within(() => { + // link + cy.dataCy(selectorQuestionnaireTitle).should( + 'contain', + questionnaire.title, + ); + // link url + cy.dataCy(selectorQuestionnaireButton) + .should('have.attr', 'href', questionnaire.link.url) + .and('have.attr', 'target', questionnaire.link.target); + // successful response from url + cy.request({ + url: questionnaire.link.url, + failOnStatusCode: failOnStatusCode, + }).then((resp) => { + if (resp.status === httpTooManyRequestsStatus) { + cy.log(httpTooManyRequestsStatusMessage); + return; + } + expect(resp.status).to.eq(httpSuccessfullStatus); + }); + // icon external link + if (questionnaire.link.target === '_blank') { + cy.dataCy(selectorQuestionnaireButtonIcon) + .should('be.visible') + .and('have.color', primary); + cy.dataCy(selectorQuestionnaireButtonIcon) + .invoke('height') + .should('be.eq', iconSize); + cy.dataCy(selectorQuestionnaireButtonIcon) + .invoke('width') + .should('be.eq', iconSize); + } else { + cy.dataCy(selectorQuestionnaireButtonIcon).should('not.exist'); + } + }); + }); + }); + + it('renders button link with correct text for each item', () => { + cy.dataCy(selectorQuestionnaireButton).each(($button) => { + cy.wrap($button).should( + 'contain', + i18n.global.t('profile.buttonFillQuestionnaire'), + ); + }); + }); + } +}); diff --git a/src/components/__tests__/ProfileTabs.cy.js b/src/components/__tests__/ProfileTabs.cy.js index fa3438549..96c62c2fb 100644 --- a/src/components/__tests__/ProfileTabs.cy.js +++ b/src/components/__tests__/ProfileTabs.cy.js @@ -5,13 +5,14 @@ import { routesConf } from 'src/router/routes_conf'; // selectors const selectorProfileTabs = 'profile-tabs'; const selectorButtonDetails = 'profile-tabs-button-details'; -const selectorButtonForms = 'profile-tabs-button-forms'; +const selectorButtonQuestionnaires = 'profile-tabs-button-questionnaires'; const selectorButtonNewsletter = 'profile-tabs-button-newsletter'; const selectorButtonNotifications = 'profile-tabs-button-notifications'; const selectorPanelDetails = 'profile-tabs-panel-details'; -const selectorPanelForms = 'profile-tabs-panel-forms'; +const selectorPanelQuestionnaires = 'profile-tabs-panel-questionnaires'; const selectorPanelNewsletter = 'profile-tabs-panel-newsletter'; const selectorPanelNotifications = 'profile-tabs-panel-notifications'; +const selectorProfileQuestionnaires = 'profile-questionnaires'; describe('', () => { it('has translation for all strings', () => { @@ -48,7 +49,7 @@ function coreTests() { 'contain', i18n.global.t('profile.tabDetails'), ); - cy.dataCy(selectorButtonForms).and( + cy.dataCy(selectorButtonQuestionnaires).and( 'contain', i18n.global.t('profile.tabForms'), ); @@ -63,14 +64,14 @@ function coreTests() { cy.dataCy(selectorButtonDetails).click(); cy.dataCy(selectorPanelDetails).should('be.visible'); - cy.dataCy(selectorPanelForms).should('not.exist'); + cy.dataCy(selectorPanelQuestionnaires).should('not.exist'); cy.dataCy(selectorPanelNewsletter).should('not.exist'); cy.dataCy(selectorPanelNotifications).should('not.exist'); }); it('allows to switch tabs', () => { - cy.dataCy(selectorButtonForms).click(); - cy.dataCy(selectorPanelForms).should('be.visible'); + cy.dataCy(selectorButtonQuestionnaires).click(); + cy.dataCy(selectorPanelQuestionnaires).should('be.visible'); cy.dataCy(selectorButtonNewsletter).click(); cy.dataCy(selectorPanelNewsletter).should('be.visible'); cy.dataCy(selectorButtonNotifications).click(); @@ -83,7 +84,7 @@ function coreTests() { // initial state cy.url().should('include', routesConf['profile_details'].path); // switch to details tab - cy.dataCy(selectorButtonForms).click(); + cy.dataCy(selectorButtonQuestionnaires).click(); cy.url().should('not.include', routesConf['profile_details'].path); cy.url().should('include', routesConf['profile_forms'].path); // switch to forms tab @@ -99,4 +100,10 @@ function coreTests() { cy.url().should('include', routesConf['profile_newsletter'].path); cy.dataCy(selectorPanelNewsletter).should('be.visible'); }); + + it('renders questionnaires tab', () => { + cy.dataCy(selectorButtonQuestionnaires).click(); + cy.dataCy(selectorPanelQuestionnaires).should('be.visible'); + cy.dataCy(selectorProfileQuestionnaires).should('be.visible'); + }); } diff --git a/src/components/__tests__/QuestionnaireItem.cy.js b/src/components/__tests__/QuestionnaireItem.cy.js new file mode 100644 index 000000000..aa5abcf3a --- /dev/null +++ b/src/components/__tests__/QuestionnaireItem.cy.js @@ -0,0 +1,153 @@ +import { colors } from 'quasar'; +import QuestionnaireItem from 'components/profile/QuestionnaireItem.vue'; +import { i18n } from '../../boot/i18n'; + +// colors +const { getPaletteColor } = colors; +const primary = getPaletteColor('primary'); +const white = getPaletteColor('white'); + +// selectors +const questionnaireItem = 'questionnaire-item'; +const questionnaireTitle = 'questionnaire-title'; +const questionnaireButton = 'questionnaire-button'; +const questionnaireButtonIcon = 'questionnaire-button-icon'; +const questionnaireAvatar = 'questionnaire-avatar'; +const questionnaireImage = 'questionnaire-image'; + +// variables +const avatarSize = 48; +const iconSize = 18; + +describe('', () => { + let questionnaires; + + before(() => { + cy.fixture('listQuestionnaires').then((fixtureData) => { + questionnaires = fixtureData; + }); + }); + + it('has translation for all strings', () => { + cy.testLanguageStringsInContext( + ['buttonFillQuestionnaire'], + 'profile', + i18n, + ); + }); + + context('desktop - target _blank', () => { + beforeEach(() => { + cy.wrap(questionnaires[0]).as('questionnaire'); + cy.get('@questionnaire').then((questionnaire) => { + cy.mount(QuestionnaireItem, { + props: { + questionnaire: questionnaire, + }, + }); + }); + cy.viewport('macbook-16'); + }); + + coreTests(); + iconTests(); + }); + + context('desktop - target _self', () => { + beforeEach(() => { + cy.wrap(questionnaires[1]).as('questionnaire'); + cy.get('@questionnaire').then((questionnaire) => { + cy.mount(QuestionnaireItem, { + props: { + questionnaire: questionnaire, + }, + }); + }); + cy.viewport('macbook-16'); + }); + + coreTests(); + + it('does not render an external link icon in the button', () => { + cy.dataCy(questionnaireButtonIcon).should('not.exist'); + }); + }); + + context('mobile', () => { + beforeEach(() => { + cy.wrap(questionnaires[0]).as('questionnaire'); + cy.get('@questionnaire').then((questionnaire) => { + cy.mount(QuestionnaireItem, { + props: { + questionnaire: questionnaire, + }, + }); + }); + cy.viewport('iphone-6'); + }); + + coreTests(); + iconTests(); + }); + + function coreTests() { + it('renders component', () => { + cy.dataCy(questionnaireItem).should('be.visible'); + }); + + it('displays the questionnaire title', () => { + cy.get('@questionnaire').then((questionnaire) => { + cy.dataCy(questionnaireTitle) + .should('have.css', 'font-size', '16px') + .and('have.css', 'font-weight', '400') + .and('contain', questionnaire.title); + }); + }); + + it('renders the questionnaire image', () => { + cy.get('@questionnaire').then((questionnaire) => { + cy.dataCy(questionnaireAvatar) + .should('be.visible') + .invoke('height') + .should('be.eq', avatarSize); + cy.dataCy(questionnaireAvatar) + .invoke('width') + .should('be.eq', avatarSize); + cy.dataCy(questionnaireImage) + .find('img') + .should('have.attr', 'src', questionnaire.image.src) + .and('have.attr', 'alt', questionnaire.image.alt); + }); + }); + + it('renders a button with correct attributes and text', () => { + cy.get('@questionnaire').then((questionnaire) => { + cy.dataCy(questionnaireButton) + .should('be.visible') + .and('have.attr', 'href', questionnaire.link.url) + .and('have.attr', 'target', questionnaire.link.target) + .and('contain', i18n.global.t('profile.buttonFillQuestionnaire')); + }); + }); + + it('applies correct styles to the component', () => { + cy.dataCy(questionnaireItem) + .should('have.css', 'padding', '16px') + .and('have.backgroundColor', white); + }); + } + + function iconTests() { + it('renders an external link icon in the button', () => { + cy.dataCy(questionnaireButtonIcon) + .should('be.visible') + .and('have.color', primary); + cy.dataCy(questionnaireButtonIcon) + .invoke('height') + .should('be.eq', iconSize); + cy.dataCy(questionnaireButtonIcon) + .invoke('width') + .should('be.eq', iconSize); + }); + } +}); diff --git a/src/components/profile/ProfileQuestionnaires.vue b/src/components/profile/ProfileQuestionnaires.vue new file mode 100644 index 000000000..35a0842f1 --- /dev/null +++ b/src/components/profile/ProfileQuestionnaires.vue @@ -0,0 +1,55 @@ + + + diff --git a/src/components/profile/ProfileTabs.vue b/src/components/profile/ProfileTabs.vue index 378f72449..300d0588b 100644 --- a/src/components/profile/ProfileTabs.vue +++ b/src/components/profile/ProfileTabs.vue @@ -8,6 +8,7 @@ * @components * - `ProfileDetails`: Component to display a ProfileDetails section. * - `TableNotifications`: Component to display a table of notifications. + * - `ProfileQuestionnaires`: Component to display a table of questionnaires. * * @example * @@ -21,13 +22,14 @@ import { defineComponent, ref } from 'vue'; // components import ProfileDetails from './ProfileDetails.vue'; import TableNotifications from './TableNotifications.vue'; +import ProfileQuestionnaires from './ProfileQuestionnaires.vue'; // routes import { routesConf } from '../../router/routes_conf'; enum tabsProfile { details = 'details', - forms = 'forms', + questionnaires = 'questionnaires', newsletter = 'newsletter', notifications = 'notifications', none = '', @@ -38,6 +40,7 @@ export default defineComponent({ components: { ProfileDetails, TableNotifications, + ProfileQuestionnaires, }, setup() { const activeTab = ref(tabsProfile.none); @@ -71,9 +74,9 @@ export default defineComponent({ /> - - - + + + +/** + * QuestionnaireItem Component + * + * @description * Use this component to render a link to a questionnaire. + * + * Used in `ProfileQuestionnaires` component. + * + * @props + * - `questionnaire` (QuestionnaireLink, required): Questionnaire object. + * It should be of type `QuestionnaireLink`. + * + * @example + * + * + * @see [Figma Design](https://www.figma.com/design/L8dVREySVXxh3X12TcFDdR/Do-pr%C3%A1ce-na-kole?node-id=6726-57401&t=T5wPNRBro9elerFm-1) + */ + +// libraries +import { defineComponent } from 'vue'; + +// config +import { rideToWorkByBikeConfig } from '../../boot/global_vars'; + +// types +import type { QuestionnaireLink } from '../types/Questionnaire'; + +export default defineComponent({ + name: 'QuestionnaireItem', + props: { + questionnaire: { + type: Object as () => QuestionnaireLink, + required: true, + }, + }, + setup() { + const borderRadius = rideToWorkByBikeConfig.borderRadiusCard; + const borderColor = rideToWorkByBikeConfig.colorGrayMiddle; + + return { + borderColor, + borderRadius, + }; + }, +}); + + + diff --git a/src/components/types/Questionnaire.ts b/src/components/types/Questionnaire.ts new file mode 100644 index 000000000..fd0dfce98 --- /dev/null +++ b/src/components/types/Questionnaire.ts @@ -0,0 +1,11 @@ +import type { Image } from './Image'; + +export interface QuestionnaireLink { + id: string; + title: string; + image: Image; + link: { + url: string; + target?: '_blank' | '_self'; + }; +} diff --git a/src/i18n/cs.toml b/src/i18n/cs.toml index 07a6582b4..d5818c88e 100755 --- a/src/i18n/cs.toml +++ b/src/i18n/cs.toml @@ -471,6 +471,7 @@ titleStep2 = "Přizvěte své blízké do výzvy" [profile] buttonDownloadInvoice = "Stáhnout potvrzení o platbě" +buttonFillQuestionnaire = "Vyplnit dotazník" descriptionNickname = "Zobrazí se ve veřejných výsledcích místo vašeho jména" labelAddressDivision = "Adresa / pobočka:" labelAllowContactPhone = "Chci vědět více o udržitelné mobilitě a souhlasím, že se mi můžete ozvat telefonicky ohledně podpory aktivní dopravy a veřejného prostoru." diff --git a/src/i18n/en.toml b/src/i18n/en.toml index 8e53512db..95fdf5043 100755 --- a/src/i18n/en.toml +++ b/src/i18n/en.toml @@ -468,6 +468,7 @@ titleStep2 = "Invite your loved ones to the challenge" [profile] buttonDownloadInvoice = "Download payment confirmation" +buttonFillQuestionnaire = "Fill in questionnaire" descriptionNickname = "Appears in public results instead of your name" labelAddressDivision = "Address / Division:" labelAllowContactPhone = "I want to know more about sustainable mobility and agree to be contacted by phone about promoting active transport and public space." diff --git a/src/i18n/sk.toml b/src/i18n/sk.toml index 6baeb649e..20d940b48 100755 --- a/src/i18n/sk.toml +++ b/src/i18n/sk.toml @@ -468,6 +468,7 @@ titleStep2 = "Pozvite na výzvu svojich blízkych" [profile] buttonDownloadInvoice = "Stiahnutie potvrzenia o platbe" +buttonFillQuestionnaire = "Vyplniť dotazník" descriptionNickname = "Zobrazuje sa vo verejných výsledkoch namiesto vášho mena" labelAddressDivision = "Adresa / pobočka:" labelAllowContactPhone = "Chcem sa dozvedieť viac o udržateľnej mobilite a súhlasím s telefonickým kontaktovaním v súvislosti s podporou aktívnej dopravy a verejného priestoru." diff --git a/test/cypress/fixtures/listQuestionnaires.json b/test/cypress/fixtures/listQuestionnaires.json new file mode 100644 index 000000000..633cbc499 --- /dev/null +++ b/test/cypress/fixtures/listQuestionnaires.json @@ -0,0 +1,38 @@ +[ + { + "id": "1", + "title": "Závěrečný dotazník", + "image": { + "src": "https://picsum.photos/id/70/160/160", + "alt": "road lined with trees" + }, + "link": { + "url": "https://dopracenakole.cz/do-prace-na-kole-pro-firmy/cyklozamestnavatel-roku", + "target": "_blank" + } + }, + { + "id": "2", + "title": "Cyklozaměstnavatel", + "image": { + "src": "https://picsum.photos/id/70/160/160", + "alt": "road lined with trees" + }, + "link": { + "url": "https://dopracenakole.cz/do-prace-na-kole-pro-firmy/cyklozamestnavatel-roku", + "target": "_self" + } + }, + { + "id": "3", + "title": "Cykloměsto a Cykloskokan", + "image": { + "src": "https://picsum.photos/id/70/160/160", + "alt": "road lined with trees" + }, + "link": { + "url": "https://dopracenakole.cz/do-prace-na-kole-pro-firmy/cyklozamestnavatel-roku", + "target": "_blank" + } + } +]