In my Vue 3 app users can make pages for themselves that are multi-lingual. They can decide what languages will be available on their page. The available languages are loaded from an API.
The languages are loaded in an Pinia store and the LanguageSwitcher.vue component loads a default language based on browser settings, language variable in de URL, default locale and the available languages. All this warrants rigorous Cypress tests, one of them being making sure that the language shown as selected on the page is in fact the language selected. Yet how do I get the current selected language from i18n in Cypress?
My LanguageSwitcher.vue component (without the logic that sets the initial language)
</script>
<template>
<div data-testid="localeSwitcher">
<span v-if="getAvailableLocales().length > 1">
<span v-for="(locale, i) in getAvailableLocales()" :key="`locale-${locale}`">
<span v-if="i != 0" class="has-text-warning-dark"> | </span>
<a @click="setLocale(locale)" :class="[{ 'has-text-weight-bold' : ($i18n.locale === locale)}, 'has-text-warning-dark']">
{{ locale.toUpperCase() }}
</a>
</span>
</span>
</div>
</template>
My test that loads the i18n and should check the current language (no clue how to do that though)
__test__/LanguageSwitcher.cy.js
import LanguageSwitcher from '../items/LanguageSwitcher.vue'
import { createI18n } from 'vue-i18n'
import { mount } from '@cypress/vue'
import en from '../../locales/en.json'
import ru from '../../locales/ru.json'
import ro from '../../locales/ro.json'
describe('Test the LocaleSwitcher Languages selected',() => {
let i18n //<---thought if I declare it here maybe the test could access it
beforeEach(() => {
//list alle the available languages
const availableMessages = { ru, en, ro }
//load available languages
i18n = createI18n({
legacy: false,
fallbackLocale: ro,
locale: ro,
globalInjection: true,
messages: availableMessages
})
//mount component with the new i18n object
cy.mount(LocaleSwitcher, { global: { plugins : [ i18n ]}})
})
it('Check if bold language is active language', () => {
i18n.locale = 'ru' //<--- like so, but no
//check of all links shown are unique
cy.get('*[class^="has-text-weight-bold"]').should('have.length', 1)
cy.get('*[class^="has-text-weight-bold"]').each(($a) => {
expect($a.text()).to.have.string('RU')
})
})
})
I tried to find where the global.plugins.i18n
is, but all my searches come up empty. So how can I access the i18n in the currently mounted component in Cypress and read the i18n.locale
?
packages.json
{
"name": "web-shop",
"version": "0.0.0",
"private": true,
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"test:e2e": "start-server-and-test preview http://localhost:4173 'cypress run --e2e'",
"test:e2e:dev": "start-server-and-test 'vite dev --port 4173' http://localhost:4173 'cypress open --e2e'",
"test:unit": "cypress run --component",
"test:unit:dev": "cypress open --component",
"css-deploy": "npm run css-build && npm run css-postcss",
"css-build": "node-sass --omit-source-map-url src/assets/_sass/main.scss src/assets/css/main.css",
"css-postcss": "postcss --use autoprefixer --output src/assets/css/main.css src/assets/css/main.css",
"css-watch": "npm run css-build -- --watch",
"deploy": "npm run css-deploy",
"start": "npm-run-all --parallel css-watch"
},
"dependencies": {
"bulma": "^0.9.4",
"fork-awesome": "^1.2.0",
"pinia": "^2.1.4",
"vue": "^3.3.4",
"vue-i18n": "^9"
},
"devDependencies": {
"@cypress/vue": "^6.0.0",
"@npmcli/fs": "latest",
"@vitejs/plugin-vue": "^4.2.3",
"@vitejs/plugin-vue-jsx": "^3.0.1",
"cypress": "^13.2.0",
"node-sass": "latest",
"start-server-and-test": "^2.0.0",
"vite": "^4.4.6"
}
}
cypress.config.js
const { defineConfig } = require('cypress')
module.exports = defineConfig({
defaultCommandTimeout: 10000,
e2e: {
specPattern: 'cypress/e2e/**/*.{cy,spec}.{js,jsx,ts,tsx}',
baseUrl: 'http://localhost:4173'
},
component: {
specPattern: 'src/**/__tests__/*.{cy,spec}.{js,ts,jsx,tsx}',
devServer: {
framework: 'vue',
bundler: 'vite'
}
}
})
cypress/support/components.js
import './commands'
import { mount } from 'cypress/vue'
import { createPinia, setActivePinia } from 'pinia'
import { useBusinessInfoStore } from '../../src/stores/businessInfo.js'
import { createI18n } from 'vue-i18n'
import en from '../../src/locales/en.json'
import de from '../../src/locales/de.json'
import nl from '../../src/locales/nl.json'
let pinia = createPinia();
setActivePinia(pinia);
//list alle the available languages
const availableMessages = { en, de, nl }
const i18n = createI18n({
legacy: false,
fallbackLocale: en,
locale: nl,
globalInjection: true,
messages: availableMessages
})
Cypress.Commands.add('mount', (component, options = {}) => {
// Setup options object
options.global = options.global || {}
options.global.stubs = options.global.stubs || {}
options.global.stubs['transition'] = false
options.global.components = options.global.components || {}
options.global.plugins = options.global.plugins || []
/* Add any global plugins */
//check if array is empty, if not assume there is already an i18n object there
if (options.global.plugins[0] == undefined ) {
//for testing the language selector this needs to be inserted in that test
options.global.plugins.push(i18n)
}
options.global.plugins.push(pinia)
return mount(component, options)
})
Cypress operates outside the Vue application's context.
That means, directly accessing Vue component's internal state or instances (like your i18n plugin instance) is not straightforward.
One way to make your components more testable is to use data attributes to store state information that you can easily access in tests. For instance, you could modify your
LanguageSwitcher.vue
component to include a data attribute that reflects the current locale:In your Cypress test, you can then select the element with the correct attribute:
Since you are testing a UI component, the most end-to-end way to test it would be to simulate user interaction to change the language and then check if the UI reflects this change. That means triggering clicks on the language switcher links and verifying the active language is highlighted as expected:
Even though Vue CLI is not actively developed anymore, it is still a popular tool for scaffolding Vue.js projects.
If you are working on a project that was scaffolded with Vue CLI and uses the
@vue/cli-plugin-e2e-cypress
for E2E testing, you can usevue add e2e-cypress
to set up Cypress in your project, including default configuration and sample tests.If not, for integrating Cypress in a Vite-based project, you would manually install Cypress and configure it, as Vite does not have a plugin system like Vue CLI.
Given your application uses Vue I18n and Pinia, your tests need to interact with the UI to change languages and assert the active language. While direct access to Vue's reactivity system or plugins like Vue I18n within Cypress tests is not straightforward, you can manipulate and assert UI state changes as a user would.
You could write a Cypress test for your
LanguageSwitcher.vue
component, assuming it renders language options and highlights the active language, to simulate user interaction to change the language and then to verify that the UI updates accordingly:A
LanguageSwitcher.cy.js
would be:Testing more intricate behaviors related to Vue I18n and Pinia (like loading languages from an API) would involve setting up mock responses or initial states before running your tests. That could be done using Cypress commands to mock API calls or manipulate the browser's local storage and session storage where initial states might be stored.
In a Vue application, components interact with the
i18n
instance through the Vue instance they are part of. That is made possible by the Vue plugin system, which allows global properties or functionalities, like i18n for internationalization, to be accessible in all components viathis.$i18n
or a similar mechanism depending on the Vue version and the i18n library setup.However, when testing with Cypress, you are operating in a different context. Cypress runs outside of your Vue application, manipulating the application through the browser's DOM as a user might. Cypress tests do not have direct access to the internal JavaScript scope of your Vue components. That separation ensures tests mimic user interactions as closely as possible, rather than interacting with the application's internal state or instances directly.
Given this separation, to test language changes or the effects of interacting with the
i18n
object, you would typically:i18n
language selection.For example, after simulating a click on a language switcher button with Cypress, you would check for visible UI changes that confirm the language has indeed been switched, rather than trying to access the
i18n
object directly from the test.Your tests accurately reflect how a user would experience and interact with the application, which is the main goal of using Cypress for end-to-end testing.
However, as noted by Chloe
E2E (End-to-End) Tests: These tests simulate real user scenarios from start to finish. E2E tests interact with the application as it runs in a browser, clicking through interfaces, loading pages, etc., without direct access to the underlying JavaScript state or Vue instances. Cypress is traditionally known for these types of tests.
Component Tests: These are more granular than E2E tests, focusing on individual components in isolation. Component testing frameworks typically provide a way to mount individual components in a testing environment, allowing for direct interaction with their props, events, and state. Cypress introduced component testing capabilities that enable this level of granularity, blending the lines between traditional unit testing tools and Cypress's E2E testing strengths.
When running component tests with Cypress, the testing environment is configured to allow more direct interaction with the Vue component instances. That includes access to the Vue instance itself, which can be especially useful for manipulating or asserting against specific states or behaviors, like those provided by the
i18n
plugin.Example of accessing
i18n
in Cypress component tests:That assumes that
cy.mount
is part of Cypress's component testing API, which mounts the Vue component within the Cypress test runner, allowing for direct interaction with the component's Vue instance.Verify that your import statements for
cy.mount
, your component, and thei18n
plugin are correct. Make sure all imports (LanguageSwitcher
,createI18n
, etc.) are correctly resolved in your test files. Even though you mentioned not touchingcypress.config.js
, it is important to make sure it is properly configured for component testing, including setting up the Vue version and any necessary plugins:The use of
beforeEach
for setting up your tests should not, in itself, be an issue. However, making sure that any global setup that needs to happen before each test is correctly configured is important. If moving thecy.mount
out ofbeforeEach
makes a difference, it could be related to how the test lifecycle interacts with your component's state or thei18n
plugin's initialization. Make sure thei18n
instance is properly instantiated and passed to your component during the mount process.