Bruno Predot

Ajout et paramétrage de @nuxt/test-utils avec vitest et toutes ses dépendances.

Factorisation en ajoutant les composants :  Loader, MovieCard.
@@ -11,4 +11,6 @@ @@ -11,4 +11,6 @@
11 - Ajout composant MovieCommentForm. 11 - Ajout composant MovieCommentForm.
12 - Ajout composant MovieCommentList. 12 - Ajout composant MovieCommentList.
13 - Ajout dépendance TinyMCE. 13 - Ajout dépendance TinyMCE.
14 -- Ajout composant TinyMceFieldEditor. 14 +- Ajout composant TinyMceFieldEditor.
  15 +- Ajout composant Loader.
  16 +- Ajout composant MovieCard.
  1 +<script lang="ts" setup>
  2 +//#region --Props--.
  3 +import { useDateFormat } from "@vueuse/core";
  4 +import { FilmIcon } from "lucide-vue-next";
  5 +//#endregion
  6 +
  7 +//#region --Props--.
  8 +defineProps({
  9 + movie: {
  10 + type: Object,
  11 + required: true,
  12 + nullable: false,
  13 + },
  14 +});
  15 +//#endregion
  16 +</script>
  17 +
  18 +<template>
  19 + <section
  20 + class="bg-gray-800 rounded-lg overflow-hidden shadow-lg transition-transform duration-300 hover:scale-105 cursor-pointer"
  21 + @click="navigateTo(`/movies/${movie.id}`)"
  22 + >
  23 + <div class="relative pb-[150%]">
  24 + <img
  25 + v-if="movie.poster_path"
  26 + :alt="movie.title"
  27 + :src="`https://image.tmdb.org/t/p/w500${movie.poster_path}`"
  28 + class="absolute inset-0 w-full h-full object-cover"
  29 + />
  30 + <div v-else class="absolute inset-0 w-full h-full bg-gray-700 flex items-center justify-center">
  31 + <FilmIcon :size="48" class="text-gray-500" />
  32 + </div>
  33 + <div
  34 + class="absolute top-2 right-2 bg-primary text-white rounded-full w-10 h-10 flex items-center justify-center font-bold"
  35 + >
  36 + {{ movie.vote_average.toFixed(1) }}
  37 + </div>
  38 + </div>
  39 + <div class="p-4">
  40 + <h2 class="text-lg font-bold mb-1 line-clamp-1">{{ movie.title }}</h2>
  41 + <p class="text-sm text-gray-400">{{ useDateFormat(movie.release_date, "DD-MM-YYYY") }}</p>
  42 + </div>
  43 + </section>
  44 +</template>
  45 +
  46 +<style scoped></style>
@@ -3,9 +3,8 @@ @@ -3,9 +3,8 @@
3 import { onBeforeUnmount, ref } from "vue"; 3 import { onBeforeUnmount, ref } from "vue";
4 import { useTMDB } from "~/composables/tMDB"; 4 import { useTMDB } from "~/composables/tMDB";
5 import { Movie } from "~/models/movie"; 5 import { Movie } from "~/models/movie";
6 -import { FilmIcon, SearchXIcon } from "lucide-vue-next"; 6 +import { SearchXIcon } from "lucide-vue-next";
7 import type { MovieInterface } from "~/interfaces/movie"; 7 import type { MovieInterface } from "~/interfaces/movie";
8 -import { useDateFormat } from "@vueuse/core";  
9 //#endregion 8 //#endregion
10 9
11 //#region --Declaration--. 10 //#region --Declaration--.
@@ -99,7 +98,7 @@ function createIntersectionObserver() { @@ -99,7 +98,7 @@ function createIntersectionObserver() {
99 if (entry.isIntersecting && !isLoadingMore.value && currentPage.value < totalPages.value) { 98 if (entry.isIntersecting && !isLoadingMore.value && currentPage.value < totalPages.value) {
100 if (searchQuery.value) { 99 if (searchQuery.value) {
101 // Continue searching query if already active. 100 // Continue searching query if already active.
102 - search(searchQuery.value, currentPage.value + 1) 101 + search(searchQuery.value, currentPage.value + 1);
103 } else { 102 } else {
104 // Continue fetching popular movies. 103 // Continue fetching popular movies.
105 fetchMovies(currentPage.value + 1); 104 fetchMovies(currentPage.value + 1);
@@ -117,7 +116,7 @@ function handleSearchEvent(event: string) { @@ -117,7 +116,7 @@ function handleSearchEvent(event: string) {
117 } 116 }
118 117
119 function handleClearSearchEvent() { 118 function handleClearSearchEvent() {
120 - searchQuery.value = ''; 119 + searchQuery.value = "";
121 currentPage.value = 1; 120 currentPage.value = 1;
122 // Fetch popular movies after clear. 121 // Fetch popular movies after clear.
123 fetchMovies(1); 122 fetchMovies(1);
@@ -159,36 +158,18 @@ onBeforeUnmount(() => { @@ -159,36 +158,18 @@ onBeforeUnmount(() => {
159 @event:search="handleSearchEvent" 158 @event:search="handleSearchEvent"
160 @event:clear_search="handleClearSearchEvent" 159 @event:clear_search="handleClearSearchEvent"
161 /> 160 />
  161 +
162 <!-- Loading Skeleton --> 162 <!-- Loading Skeleton -->
163 - <ui-components-skeleton-movies-loader v-if="isInitialLoading" :is-initial-loading="isInitialLoading" :skeleton-number="20" /> 163 + <ui-components-skeleton-movies-loader
  164 + v-if="isInitialLoading"
  165 + :is-initial-loading="isInitialLoading"
  166 + :skeleton-number="20"
  167 + />
  168 +
164 <!-- Liste des films --> 169 <!-- Liste des films -->
165 <div v-else-if="movies.length > 0" class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6"> 170 <div v-else-if="movies.length > 0" class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6">
166 - <div 171 + <div v-for="movie in movies" :key="movie.id">
167 - v-for="movie in movies" 172 + <movie-card :movie="movie" />
168 - :key="movie.id"  
169 - class="bg-gray-800 rounded-lg overflow-hidden shadow-lg transition-transform duration-300 hover:scale-105 cursor-pointer"  
170 - @click="navigateTo(`/movies/${movie.id}`)"  
171 - >  
172 - <div class="relative pb-[150%]">  
173 - <img  
174 - v-if="movie.poster_path"  
175 - :alt="movie.title"  
176 - :src="`https://image.tmdb.org/t/p/w500${movie.poster_path}`"  
177 - class="absolute inset-0 w-full h-full object-cover"  
178 - />  
179 - <div v-else class="absolute inset-0 w-full h-full bg-gray-700 flex items-center justify-center">  
180 - <FilmIcon :size="48" class="text-gray-500" />  
181 - </div>  
182 - <div  
183 - class="absolute top-2 right-2 bg-primary text-white rounded-full w-10 h-10 flex items-center justify-center font-bold"  
184 - >  
185 - {{ movie.vote_average.toFixed(1) }}  
186 - </div>  
187 - </div>  
188 - <div class="p-4">  
189 - <h2 class="text-lg font-bold mb-1 line-clamp-1">{{ movie.title }}</h2>  
190 - <p class="text-sm text-gray-400">{{ useDateFormat(movie.release_date, "DD-MM-YYYY") }}</p>  
191 - </div>  
192 </div> 173 </div>
193 </div> 174 </div>
194 175
@@ -200,9 +181,7 @@ onBeforeUnmount(() => { @@ -200,9 +181,7 @@ onBeforeUnmount(() => {
200 </section> 181 </section>
201 182
202 <!-- Loader pour le chargement de plus de films --> 183 <!-- Loader pour le chargement de plus de films -->
203 - <section v-if="isLoadingMore && !isInitialLoading" class="flex justify-center mt-8"> 184 + <ui-components-loader :is-initial-loading="isInitialLoading" :is-loading="isLoadingMore" />
204 - <div class="w-10 h-10 border-4 border-primary border-t-transparent rounded-full animate-spin" />  
205 - </section>  
206 185
207 <!-- Élément observé pour le défilement infini --> 186 <!-- Élément observé pour le défilement infini -->
208 <div ref="loadMoreTrigger" class="h-10 mt-4" /> 187 <div ref="loadMoreTrigger" class="h-10 mt-4" />
  1 +import { describe, it, expect } from 'vitest'
  2 +import { mount } from '@vue/test-utils'
  3 +
  4 +import HelloWorld from './HelloWorld.vue'
  5 +
  6 +describe('HelloWorld', () => {
  7 + it('component renders Hello world properly', () => {
  8 + const wrapper = mount(HelloWorld)
  9 + expect(wrapper.text()).toContain('Hello world')
  10 + })
  11 +})
  1 +<script setup lang="ts">
  2 +
  3 +</script>
  4 +
  5 +<template>
  6 + <p>Hello world</p>
  7 +</template>
  8 +
  9 +<style scoped lang="scss">
  10 +
  11 +</style>
  1 +<script lang="ts" setup>
  2 +//#region --Props--.
  3 +defineProps({
  4 + isLoading: {
  5 + type: Boolean,
  6 + required: true,
  7 + nullable: false,
  8 + },
  9 + isInitialLoading: {
  10 + type: Boolean,
  11 + required: false,
  12 + nullable: false,
  13 + default: false,
  14 + },
  15 +});
  16 +//#endregion
  17 +</script>
  18 +
  19 +<template>
  20 + <section v-if="isLoading && !isInitialLoading" class="flex justify-center mt-8">
  21 + <div class="w-10 h-10 border-4 border-primary border-t-transparent rounded-full animate-spin" />
  22 + </section>
  23 +</template>
  24 +
  25 +<style scoped></style>
@@ -25,6 +25,7 @@ export default defineNuxtConfig({ @@ -25,6 +25,7 @@ export default defineNuxtConfig({
25 "@nuxt/eslint", 25 "@nuxt/eslint",
26 "@nuxt/icon", 26 "@nuxt/icon",
27 "@nuxt/image", 27 "@nuxt/image",
  28 + "@nuxt/test-utils/module",
28 [ 29 [
29 "@pinia/nuxt", 30 "@pinia/nuxt",
30 { 31 {
This diff is collapsed. Click to expand it.
@@ -12,20 +12,21 @@ @@ -12,20 +12,21 @@
12 "lint:js": "eslint --ext \".ts,.vue\" .", 12 "lint:js": "eslint --ext \".ts,.vue\" .",
13 "lint:prettier": "prettier --write .", 13 "lint:prettier": "prettier --write .",
14 "lint": "npm run lint:js && npm run lint:prettier", 14 "lint": "npm run lint:js && npm run lint:prettier",
15 - "format": "prettier --write \"{components,pages,plugins,middleware,layouts,composables,assets}/**/*.{js,jsx,ts,tsx,vue,html,css,scss,json,md}\"" 15 + "format": "prettier --write \"{components,pages,plugins,middleware,layouts,composables,assets}/**/*.{js,jsx,ts,tsx,vue,html,css,scss,json,md}\"",
  16 + "test": "vitest"
16 }, 17 },
17 "dependencies": { 18 "dependencies": {
18 "@nuxt/eslint": "^1.3.0", 19 "@nuxt/eslint": "^1.3.0",
19 "@nuxt/icon": "^1.12.0", 20 "@nuxt/icon": "^1.12.0",
20 "@nuxt/image": "^1.10.0", 21 "@nuxt/image": "^1.10.0",
21 "@nuxt/scripts": "^0.11.6", 22 "@nuxt/scripts": "^0.11.6",
22 - "@nuxt/test-utils": "^3.17.2",  
23 "@nuxt/ui": "^2.22.0", 23 "@nuxt/ui": "^2.22.0",
24 "@pinia-orm/nuxt": "^1.10.2", 24 "@pinia-orm/nuxt": "^1.10.2",
25 "@pinia/nuxt": "^0.9.0", 25 "@pinia/nuxt": "^0.9.0",
26 "@tinymce/tinymce-vue": "^5.1.1", 26 "@tinymce/tinymce-vue": "^5.1.1",
27 "@types/vuelidate": "^0.7.22", 27 "@types/vuelidate": "^0.7.22",
28 "@unhead/vue": "^2.0.8", 28 "@unhead/vue": "^2.0.8",
  29 + "@vitejs/plugin-vue": "^5.2.3",
29 "@vuelidate/core": "^2.0.3", 30 "@vuelidate/core": "^2.0.3",
30 "@vuelidate/validators": "^2.0.4", 31 "@vuelidate/validators": "^2.0.4",
31 "@vueuse/core": "^13.1.0", 32 "@vueuse/core": "^13.1.0",
@@ -40,9 +41,15 @@ @@ -40,9 +41,15 @@
40 "vuetify-nuxt-module": "^0.18.6" 41 "vuetify-nuxt-module": "^0.18.6"
41 }, 42 },
42 "devDependencies": { 43 "devDependencies": {
  44 + "@nuxt/test-utils": "^3.17.2",
43 "@nuxtjs/tailwindcss": "^6.13.2", 45 "@nuxtjs/tailwindcss": "^6.13.2",
  46 + "@vue/test-utils": "^2.4.6",
44 "eslint-config-prettier": "^10.1.2", 47 "eslint-config-prettier": "^10.1.2",
45 "eslint-plugin-prettier": "^5.2.6", 48 "eslint-plugin-prettier": "^5.2.6",
46 - "prettier": "^3.5.3" 49 + "happy-dom": "^17.4.4",
  50 + "jsdom": "^26.1.0",
  51 + "playwright-core": "^1.52.0",
  52 + "prettier": "^3.5.3",
  53 + "vitest": "^3.1.2"
47 } 54 }
48 } 55 }
@@ -200,10 +200,11 @@ onMounted(() => { @@ -200,10 +200,11 @@ onMounted(() => {
200 <span class="font-semibold">Têtes d'affiche:</span> 200 <span class="font-semibold">Têtes d'affiche:</span>
201 {{ 201 {{
202 movie.credit.cast 202 movie.credit.cast
203 - .slice(0, 10) 203 + .slice(0, 15)
204 .map((person) => person.name) 204 .map((person) => person.name)
205 .join(", ") 205 .join(", ")
206 }} 206 }}
  207 + <span v-if="movie.credit.cast.length > 15">..</span>
207 </div> 208 </div>
208 </div> 209 </div>
209 <!-- Comments form. --> 210 <!-- Comments form. -->
  1 +// vite.config.js
  2 +import vue from '@vitejs/plugin-vue'
  3 +
  4 +export default {
  5 + plugins: [vue()],
  6 + test: {
  7 + globals: true,
  8 + environment: "jsdom",
  9 + // Additional test configurations can be added here
  10 + },
  11 +}
  1 +import { defineVitestConfig } from '@nuxt/test-utils/config'
  2 +
  3 +export default defineVitestConfig({
  4 + /**
  5 + * Documentation here : https://nuxt.com/docs/getting-started/testing
  6 + * any custom Vitest config you require
  7 + */
  8 + test: {
  9 + environment: 'nuxt',
  10 + // you can optionally set Nuxt-specific environment options
  11 + // environmentOptions: {
  12 + // nuxt: {
  13 + // rootDir: fileURLToPath(new URL('./playground', import.meta.url)),
  14 + // domEnvironment: 'happy-dom', // 'happy-dom' (default) or 'jsdom'
  15 + // overrides: {
  16 + // // other Nuxt config you want to pass
  17 + // }
  18 + // }
  19 + // }
  20 + }
  21 +})