In this article, I'll walk you through a simple project using Nuxt 3 with Pinia for state management and Cypress for end-to-end testing. This project implements an interactive counter with actions and tests to ensure everything works as expected. If you're looking to explore these technologies or enhance your skills, this article is for you!
ποΈ Project Structure
Hereβs an overview of the main files in the project:
/layouts
default.vue
/pages
index.vue
/stores
counter.ts
/tests
main.cy.js
β¨ Global Layout: layouts/default.vue
This layout provides the base structure for the application, including a header, a footer, and a slot for the page content.
<template>
<header>
<h1>Pinia</h1>
<p>The intuitive store for Vue</p>
</header>
<template v-if="route.path !== '/'">
<router-link to="/">β Back Home</router-link>
<hr/>
</template>
<slot/>
<hr/>
<footer>
<a href="https://github.com/posva/pinia">
<svg class="logo" fill="none" viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg">
<path
clip-rule="evenodd"
d="M7.02751 0.333496C..."
fill="currentColor"
fill-rule="evenodd"
/>
</svg>
Github
</a> - by
<a href="https://github.com/posva">@posva</a> 2021
</footer>
</template>
<script lang="ts" setup>
import { useRoute } from "#app";
const route = useRoute();
</script>
<style scoped>
.logo {
width: 1.5rem;
height: 1.5rem;
color: white;
}
</style>
Β πΌοΈ Home Page: pages/index.vue
The home page uses the Pinia store to display and interact with the counter.
<template>
<div>
<div style="margin: 1rem 0">
<PiniaLogo/>
</div>
<p>
This is an example store to test out devtools.
</p>
<h2>Counter Store</h2>
<p data-testid="counter-values">Counter: {{ counter.n }}. Double: {{ counter.double }}</p>
<p>Increment the Store:</p>
<button @click="counter.increment()" data-testid="increment">+1</button>
<button @click="counter.increment(10)">+10</button>
<button @click="counter.increment(100)">+100</button>
</div>
</template>
<script setup lang="ts">
import { useCounter } from "~/stores/counter";
import PiniaLogo from "~/components/PiniaLogo.vue";
const counter = useCounter();
</script>
<style scoped>
button {
margin-right: 0.5rem;
margin-left: 0.5rem;
}
</style>
π¦ Pinia Store: stores/counter.ts
The store manages the application state, along with its actions and getters.
import {acceptHMRUpdate, defineStore} from "pinia";
const delay = (t: number) => new Promise(resolve => setTimeout(resolve, t))
export const useCounter = defineStore('counter', {
state: () => ({
n: 2,
incrementedTimes: 0,
decrementedTimes: 0,
numbers: [] as number[]
}),
getters: {
double: (state) => state.n * 2
},
actions: {
increment(amount: number = 1) {
this.incrementedTimes++
this.n += amount
},
changeMe() {
console.log('Change me to test HMR')
},
async fail() {
const n = this.n
await delay(1000)
this.numbers.push(n)
await delay(1000)
if (this.n !== n) {
throw new Error('Someone changed n !')
}
},
async decrementToZero(interval: number = 300) {
if (this.n <= 0) return
while (this.n > 0) {
this.$patch((state) => {
this.n--
state.decrementedTimes++
})
await delay(interval)
}
}
}
})
if (import.meta.hot) {
import.meta.hot.accept(acceptHMRUpdate(useCounter, import.meta.hot))
}
Β π οΈ Testing with Cypress: cypress/e2e/main.cy.js
The tests verify that the counter works properly and that the Pinia store actions are correctly reflected in the UI.
const PORT = process.env.PORT || 3000;
describe("Pinia demo with counters", () => {
beforeEach(() => {
cy.visit(`http://localhost:${PORT}`);
});
it("works", () => {
cy.get("[data-testid=counter-values]")
.should("contain.text", "Counter: 2. Double: 4")
.wait(500)
.get("[data-testid=increment]")
.click()
.get("[data-testid=counter-values]")
.should("contain.text", "Counter: 3. Double: 6")
.get("[data-testid=increment]")
.click();
});
});
Β π― Results
Running the tests with npx cypress open
validates the behavior of the Pinia store and its interactions with the UI.
Top comments (0)