mirror of
				https://github.com/advplyr/audiobookshelf.git
				synced 2025-10-27 11:18:14 +01:00 
			
		
		
		
	Add NarratorCard and AuthorCard component tests
This commit is contained in:
		
							parent
							
								
									d638a328d8
								
							
						
					
					
						commit
						9e1c907591
					
				
							
								
								
									
										192
									
								
								client/components/cards/AuthorCard.cy.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										192
									
								
								client/components/cards/AuthorCard.cy.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,192 @@ | |||||||
|  | // Import the necessary dependencies
 | ||||||
|  | import AuthorCard from './AuthorCard.vue' | ||||||
|  | import AuthorImage from '../covers/AuthorImage.vue' | ||||||
|  | import Tooltip from '../ui/Tooltip.vue' | ||||||
|  | import LoadingSpinner from '../widgets/LoadingSpinner.vue' | ||||||
|  | 
 | ||||||
|  | describe('AuthorCard', () => { | ||||||
|  |   const author = { | ||||||
|  |     id: 1, | ||||||
|  |     name: 'John Doe', | ||||||
|  |     numBooks: 5 | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   const propsData = { | ||||||
|  |     author, | ||||||
|  |     width: 192 * 0.8, | ||||||
|  |     height: 192, | ||||||
|  |     sizeMultiplier: 1, | ||||||
|  |     nameBelow: false | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   const mocks = { | ||||||
|  |     $strings: { | ||||||
|  |       LabelBooks: 'Books', | ||||||
|  |       ButtonQuickMatch: 'Quick Match' | ||||||
|  |     }, | ||||||
|  |     $store : { | ||||||
|  |       getters: { | ||||||
|  |         'user/getUserCanUpdate': true, | ||||||
|  |         'libraries/getLibraryProvider': () => 'audible.us'         | ||||||
|  |       }, | ||||||
|  |       state: { | ||||||
|  |         libraries: { | ||||||
|  |           currentLibraryId: 'library-123' | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |     $eventBus: { | ||||||
|  |       $on: () => {}, | ||||||
|  |       $off: () => {}, | ||||||
|  |     }, | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   const stubs = { | ||||||
|  |     'covers-author-image': AuthorImage, | ||||||
|  |     'ui-tooltip': Tooltip, | ||||||
|  |     'widgets-loading-spinner': LoadingSpinner | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   const mountOptions = { propsData, mocks, stubs } | ||||||
|  | 
 | ||||||
|  |   it('renders the component', () => { | ||||||
|  |     cy.mount(AuthorCard, mountOptions) | ||||||
|  |      | ||||||
|  |     cy.get('#textInline').should('be.visible') | ||||||
|  |     cy.get('#match').should('be.hidden') | ||||||
|  |     cy.get('#edit').should('be.hidden') | ||||||
|  |     cy.get('#nameBelow').should('be.hidden') | ||||||
|  |     cy.get('#card').should(($el) => { | ||||||
|  |       const width = $el.width() | ||||||
|  |       const height = $el.height() | ||||||
|  |       expect(width).to.be.closeTo(propsData.width, 0.01) | ||||||
|  |       expect(height).to.be.closeTo(propsData.height, 0.01) | ||||||
|  |     }) | ||||||
|  |   }) | ||||||
|  | 
 | ||||||
|  |   it('renders the component with the author name below', () => { | ||||||
|  |     const updatedPropsData = { ...propsData, nameBelow: true } | ||||||
|  |     cy.mount(AuthorCard, { ...mountOptions, propsData: updatedPropsData } ) | ||||||
|  | 
 | ||||||
|  |     cy.get('#textInline').should('be.hidden') | ||||||
|  |     cy.get('#match').should('be.hidden') | ||||||
|  |     cy.get('#edit').should('be.hidden') | ||||||
|  |     let nameBelowHeight | ||||||
|  |     cy.get('#nameBelow') | ||||||
|  |         .should('be.visible') | ||||||
|  |         .and('have.text', 'John Doe') | ||||||
|  |         .and(($el) => { | ||||||
|  |           const height = $el.height() | ||||||
|  |           const width = $el.width() | ||||||
|  |           const sizeMultiplier = propsData.sizeMultiplier | ||||||
|  |           const defaultFontSize = 16 | ||||||
|  |           const defaultLineHeight = 1.5 | ||||||
|  |           const fontSizeMultiplier = 0.75 | ||||||
|  |           const px2 = 16 | ||||||
|  |           expect(height).to.be.closeTo(defaultFontSize * fontSizeMultiplier * sizeMultiplier * defaultLineHeight, 0.01) | ||||||
|  |           nameBelowHeight = height  | ||||||
|  |           expect(width).to.be.closeTo(propsData.width - px2, 0.01)          | ||||||
|  |       }) | ||||||
|  |     cy.get('#card').should(($el) => { | ||||||
|  |       const width = $el.width() | ||||||
|  |       const height = $el.height() | ||||||
|  |       const py1 = 8 | ||||||
|  |       expect(width).to.be.closeTo(propsData.width, 0.01) | ||||||
|  |       expect(height).to.be.closeTo(propsData.height + nameBelowHeight + py1, 0.01) | ||||||
|  |     }) | ||||||
|  | 
 | ||||||
|  |   }) | ||||||
|  | 
 | ||||||
|  |   it('renders quick-match and edit buttons on mouse hover', () => { | ||||||
|  |     cy.mount(AuthorCard, mountOptions ) | ||||||
|  | 
 | ||||||
|  |     // before mouseover
 | ||||||
|  |     cy.get('#match').should('be.hidden') | ||||||
|  |     cy.get('#edit').should('be.hidden') | ||||||
|  |     // after mouseover
 | ||||||
|  |     cy.get('#card').trigger('mouseover') | ||||||
|  |     cy.get('#match').should('be.visible') | ||||||
|  |     cy.get('#edit').should('be.visible') | ||||||
|  |     // after mouseleave
 | ||||||
|  |     cy.get('#card').trigger('mouseleave') | ||||||
|  |     cy.get('#match').should('be.hidden') | ||||||
|  |     cy.get('#edit').should('be.hidden') | ||||||
|  | 
 | ||||||
|  |   }) | ||||||
|  | 
 | ||||||
|  |   it('renders the component with spinner while searching', () => { | ||||||
|  |     const data = () => { return { searching: true, isHovering: false } } | ||||||
|  |     cy.mount(AuthorCard, { ...mountOptions, data } ) | ||||||
|  | 
 | ||||||
|  |     cy.get('#textInline').should('be.hidden') | ||||||
|  |     cy.get('#match').should('be.hidden') | ||||||
|  |     cy.get('#edit').should('be.hidden') | ||||||
|  |     cy.get('#spinner').should('be.visible') | ||||||
|  |   }) | ||||||
|  | 
 | ||||||
|  |   it ('toasts after quick match with no updates', () => { | ||||||
|  |     const updatedMocks = { | ||||||
|  |       ...mocks, | ||||||
|  |       $axios: { | ||||||
|  |         $post: cy.stub().resolves({ updated: false, author: { name: 'John Doe' } }) | ||||||
|  |       }, | ||||||
|  |       $toast: { | ||||||
|  |         success: cy.spy().as('success'), | ||||||
|  |         error: cy.spy().as('error'), | ||||||
|  |         info: cy.spy().as('info') | ||||||
|  |       }   | ||||||
|  |     } | ||||||
|  |     cy.mount(AuthorCard, { ...mountOptions, mocks: updatedMocks } ) | ||||||
|  |     cy.get('#card').trigger('mouseover') | ||||||
|  |     cy.get('#match').click() | ||||||
|  | 
 | ||||||
|  |     cy.get("#spinner").should('be.hidden') | ||||||
|  |     cy.get('@success').should('not.have.been.called') | ||||||
|  |     cy.get('@error').should('not.have.been.called') | ||||||
|  |     cy.get('@info').should('have.been.called') | ||||||
|  |   }) | ||||||
|  | 
 | ||||||
|  |   it ('toasts after quick match with updates and no image', () => { | ||||||
|  |     const updatedMocks = { | ||||||
|  |       ...mocks, | ||||||
|  |       $axios: { | ||||||
|  |         $post: cy.stub().resolves({ updated: true, author: { name: 'John Doe' } }) | ||||||
|  |       }, | ||||||
|  |       $toast: { | ||||||
|  |         success: cy.stub().withArgs('Author John Doe was updated (no image found)').as('success'), | ||||||
|  |         error: cy.spy().as('error'), | ||||||
|  |         info: cy.spy().as('info') | ||||||
|  |       }   | ||||||
|  |     } | ||||||
|  |     cy.mount(AuthorCard, { ...mountOptions, mocks: updatedMocks } ) | ||||||
|  |     cy.get('#card').trigger('mouseover') | ||||||
|  |     cy.get('#match').click() | ||||||
|  | 
 | ||||||
|  |     cy.get("#spinner").should('be.hidden') | ||||||
|  |     cy.get('@success').should('have.been.called') | ||||||
|  |     cy.get('@error').should('not.have.been.called') | ||||||
|  |     cy.get('@info').should('not.have.been.called') | ||||||
|  |   }) | ||||||
|  | 
 | ||||||
|  |   it ('toasts after quick match with updates including image', () => { | ||||||
|  |     const updatedMocks = { | ||||||
|  |       ...mocks, | ||||||
|  |       $axios: { | ||||||
|  |         $post: cy.stub().resolves({ updated: true, author: { name: 'John Doe', imagePath: "path/to/image" } }) | ||||||
|  |       }, | ||||||
|  |       $toast: { | ||||||
|  |         success: cy.stub().withArgs('Author John Doe was updated').as('success'), | ||||||
|  |         error: cy.spy().as('error'), | ||||||
|  |         info: cy.spy().as('info') | ||||||
|  |       }   | ||||||
|  |     } | ||||||
|  |     cy.mount(AuthorCard, { ...mountOptions, mocks: updatedMocks } ) | ||||||
|  |     cy.get('#card').trigger('mouseover') | ||||||
|  |     cy.get('#match').click() | ||||||
|  | 
 | ||||||
|  |     cy.get("#spinner").should('be.hidden') | ||||||
|  |     cy.get('@success').should('have.been.called') | ||||||
|  |     cy.get('@error').should('not.have.been.called') | ||||||
|  |     cy.get('@info').should('not.have.been.called') | ||||||
|  |   }) | ||||||
|  | }) | ||||||
| @ -1,35 +1,35 @@ | |||||||
| <template> | <template> | ||||||
|   <nuxt-link :to="`/author/${author.id}`"> |   <nuxt-link :to="`/author/${author?.id}`"> | ||||||
|     <div @mouseover="mouseover" @mouseleave="mouseleave"> |     <div id="card" :style="{ width: width + 'px'}" @mouseover="mouseover" @mouseleave="mouseleave"> | ||||||
|       <div :style="{ width: width + 'px', height: height + 'px' }" class="bg-primary box-shadow-book rounded-md relative overflow-hidden"> |       <div id="imageArea" :style="{ height: height + 'px' }" class=" bg-primary box-shadow-book rounded-md relative overflow-hidden"> | ||||||
|         <!-- Image or placeholder --> |         <!-- Image or placeholder --> | ||||||
|         <covers-author-image :author="author" /> |         <covers-author-image :author="author"/> | ||||||
| 
 | 
 | ||||||
|         <!-- Author name & num books overlay --> |         <!-- Author name & num books overlay --> | ||||||
|         <div v-show="!searching && !nameBelow" class="absolute bottom-0 left-0 w-full py-1 bg-black bg-opacity-60 px-2"> |         <div id="textInline" v-show="!searching && !nameBelow" class="absolute bottom-0 left-0 w-full py-1 bg-black bg-opacity-60 px-2"> | ||||||
|           <p class="text-center font-semibold truncate" :style="{ fontSize: sizeMultiplier * 0.75 + 'rem' }">{{ name }}</p> |           <p class="text-center font-semibold truncate" :style="{ fontSize: sizeMultiplier * 0.75 + 'rem' }">{{ name }}</p> | ||||||
|           <p class="text-center text-gray-200" :style="{ fontSize: sizeMultiplier * 0.65 + 'rem' }">{{ numBooks }} {{ $strings.LabelBooks }}</p> |           <p class="text-center text-gray-200" :style="{ fontSize: sizeMultiplier * 0.65 + 'rem' }">{{ numBooks }} {{ $strings.LabelBooks }}</p> | ||||||
|         </div> |         </div> | ||||||
| 
 | 
 | ||||||
|         <!-- Search icon btn --> |         <!-- Search icon btn --> | ||||||
|         <div v-show="!searching && isHovering && userCanUpdate" class="absolute top-0 left-0 p-2 cursor-pointer hover:text-white text-gray-200 transform hover:scale-125 duration-150" @click.prevent.stop="searchAuthor"> |         <div id="match" v-show="!searching && isHovering && userCanUpdate" class="absolute top-0 left-0 p-2 cursor-pointer hover:text-white text-gray-200 transform hover:scale-125 duration-150" @click.prevent.stop="searchAuthor"> | ||||||
|           <ui-tooltip :text="$strings.ButtonQuickMatch" direction="bottom"> |           <ui-tooltip :text="$strings.ButtonQuickMatch" direction="bottom"> | ||||||
|             <span class="material-icons text-lg">search</span> |             <span class="material-icons text-lg">search</span> | ||||||
|           </ui-tooltip> |           </ui-tooltip> | ||||||
|         </div> |         </div> | ||||||
|         <div v-show="isHovering && !searching && userCanUpdate" class="absolute top-0 right-0 p-2 cursor-pointer hover:text-white text-gray-200 transform hover:scale-125 duration-150" @click.prevent.stop="$emit('edit', author)"> |         <div id="edit" v-show="isHovering && !searching && userCanUpdate" class="absolute top-0 right-0 p-2 cursor-pointer hover:text-white text-gray-200 transform hover:scale-125 duration-150" @click.prevent.stop="$emit('edit', author)"> | ||||||
|           <ui-tooltip :text="$strings.LabelEdit" direction="bottom"> |           <ui-tooltip :text="$strings.LabelEdit" direction="bottom"> | ||||||
|             <span class="material-icons text-lg">edit</span> |             <span class="material-icons text-lg">edit</span> | ||||||
|           </ui-tooltip> |           </ui-tooltip> | ||||||
|         </div> |         </div> | ||||||
| 
 | 
 | ||||||
|         <!-- Loading spinner --> |         <!-- Loading spinner --> | ||||||
|         <div v-show="searching" class="absolute top-0 left-0 z-10 w-full h-full bg-black bg-opacity-50 flex items-center justify-center"> |         <div id="spinner" v-show="searching" class="absolute top-0 left-0 z-10 w-full h-full bg-black bg-opacity-50 flex items-center justify-center"> | ||||||
|           <widgets-loading-spinner size="" /> |           <widgets-loading-spinner size="" /> | ||||||
|         </div> |         </div> | ||||||
|       </div> |       </div> | ||||||
|       <div v-show="nameBelow" class="w-full py-1 px-2"> |       <div id="nameBelow" v-show="nameBelow" class="w-full py-1 px-2"> | ||||||
|         <p class="text-center font-semibold truncate text-gray-200" :style="{ fontSize: sizeMultiplier * 0.75 + 'rem' }">{{ name }}</p> |           <p class="text-center font-semibold truncate text-gray-200" :style="{ fontSize: sizeMultiplier * 0.75 + 'rem' }">{{ name }}</p> | ||||||
|       </div> |       </div> | ||||||
|     </div> |     </div> | ||||||
|   </nuxt-link> |   </nuxt-link> | ||||||
|  | |||||||
							
								
								
									
										86
									
								
								client/components/cards/NarratorCard.cy.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										86
									
								
								client/components/cards/NarratorCard.cy.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,86 @@ | |||||||
|  | import NarratorCard from './NarratorCard.vue' | ||||||
|  | 
 | ||||||
|  | describe('<NarratorCard />', () => { | ||||||
|  |   const narrator = { | ||||||
|  |     name: 'John Doe', | ||||||
|  |     numBooks: 5 | ||||||
|  |   } | ||||||
|  |   const propsData = { | ||||||
|  |     narrator, | ||||||
|  |     width: 200, | ||||||
|  |     height: 150, | ||||||
|  |     sizeMultiplier: 1.2 | ||||||
|  |   } | ||||||
|  |   const mocks = { | ||||||
|  |     $store: { | ||||||
|  |       getters: { | ||||||
|  |         'user/getUserCanUpdate': true | ||||||
|  |       }, | ||||||
|  |       state: { | ||||||
|  |         libraries: { | ||||||
|  |           currentLibraryId: 'library-123' | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |     $encode: (value) => value | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   it('renders the component', () => { | ||||||
|  |     let mountOptions = { propsData, mocks } | ||||||
|  |     // see: https://on.cypress.io/mounting-vue
 | ||||||
|  |     cy.mount(NarratorCard, mountOptions) | ||||||
|  |   }) | ||||||
|  | 
 | ||||||
|  |   it('renders the narrator name correctly', () => { | ||||||
|  |     let mountOptions = { propsData, mocks } | ||||||
|  |     cy.mount(NarratorCard, mountOptions) | ||||||
|  | 
 | ||||||
|  |     cy.get('#name').should('have.text', 'John Doe') | ||||||
|  |   }) | ||||||
|  | 
 | ||||||
|  |   it('renders the number of books correctly', () => { | ||||||
|  |     let mountOptions = { propsData, mocks } | ||||||
|  |     cy.mount(NarratorCard, mountOptions) | ||||||
|  | 
 | ||||||
|  |     cy.get('#numbooks').should('have.text', '5 Books') | ||||||
|  |   }) | ||||||
|  | 
 | ||||||
|  |   it('renders 1 book correctly', () => { | ||||||
|  |     let propsData = { narrator: { name: 'John Doe', numBooks: 1 }, width: 200, height: 150, sizeMultiplier: 1.2 } | ||||||
|  |     let mountOptions = { propsData, mocks } | ||||||
|  |     cy.mount(NarratorCard, mountOptions) | ||||||
|  | 
 | ||||||
|  |     cy.get('#numbooks').should('have.text', '1 Book') | ||||||
|  | 
 | ||||||
|  |   }) | ||||||
|  | 
 | ||||||
|  |   it('renders the default name and num-books when narrator is not provided', () => { | ||||||
|  |     let propsData = { width: 200, height: 150, sizeMultiplier: 1.2 } | ||||||
|  |     let mountOptions = { propsData, mocks } | ||||||
|  |     cy.mount(NarratorCard, mountOptions) | ||||||
|  |     cy.get('#name').should('have.text', '') | ||||||
|  |     cy.get('#numbooks').should('have.text', '0 Books') | ||||||
|  |   }) | ||||||
|  | 
 | ||||||
|  |   it('has the correct width and height', () => { | ||||||
|  |     let mountOptions = { propsData, mocks } | ||||||
|  |     cy.mount(NarratorCard, mountOptions) | ||||||
|  |     cy.get('#card').should('have.css', 'width', '200px') | ||||||
|  |     cy.get('#card').should('have.css', 'height', '150px') | ||||||
|  |   }) | ||||||
|  | 
 | ||||||
|  |   it('has the correct width and height when not provided', () => { | ||||||
|  |     let propsData = { narrator, sizeMultiplier: 1.2 } | ||||||
|  |     let mountOptions = { propsData, mocks } | ||||||
|  |     cy.mount(NarratorCard, mountOptions) | ||||||
|  |     cy.get('#card').should('have.css', 'width', '150px') | ||||||
|  |     cy.get('#card').should('have.css', 'height', '100px') | ||||||
|  |   }) | ||||||
|  | 
 | ||||||
|  |   it ('has the correct font sizes', () => { | ||||||
|  |     let mountOptions = { propsData, mocks } | ||||||
|  |     cy.mount(NarratorCard, mountOptions) | ||||||
|  |     cy.get('#name').should('have.css', 'font-size', '14.4px') // 0.75 * 1.2 * 16
 | ||||||
|  |     cy.get('#numbooks').should('have.css', 'font-size', '12.48px') // 0.65 * 1.2 * 16
 | ||||||
|  |   }) | ||||||
|  | }) | ||||||
| @ -1,14 +1,14 @@ | |||||||
| <template> | <template> | ||||||
|   <nuxt-link :to="`/library/${currentLibraryId}/bookshelf?filter=narrators.${$encode(narrator.name)}`"> |   <nuxt-link :to="`/library/${currentLibraryId}/bookshelf?filter=narrators.${$encode(name)}`"> | ||||||
|     <div :style="{ width: width + 'px', height: height + 'px' }" class="bg-primary box-shadow-book rounded-md relative overflow-hidden"> |     <div id="card" :style="{ width: width + 'px', height: height + 'px' }" class="bg-primary box-shadow-book rounded-md relative overflow-hidden"> | ||||||
|       <div class="absolute inset-0 w-full h-full flex items-center justify-center pointer-events-none opacity-40"> |       <div class="absolute inset-0 w-full h-full flex items-center justify-center pointer-events-none opacity-40"> | ||||||
|         <span class="material-icons-outlined text-[10rem]">record_voice_over</span> |         <span class="material-icons-outlined text-[10rem]">record_voice_over</span> | ||||||
|       </div> |       </div> | ||||||
| 
 | 
 | ||||||
|       <!-- Narrator name & num books overlay --> |       <!-- Narrator name & num books overlay --> | ||||||
|       <div class="absolute bottom-0 left-0 w-full py-1 bg-black bg-opacity-60 px-2"> |       <div class="absolute bottom-0 left-0 w-full py-1 bg-black bg-opacity-60 px-2"> | ||||||
|         <p class="text-center font-semibold truncate" :style="{ fontSize: sizeMultiplier * 0.75 + 'rem' }">{{ name }}</p> |         <p id="name" class="text-center font-semibold truncate text-gray-200" :style="{ fontSize: sizeMultiplier * 0.75 + 'rem' }">{{ name }}</p> | ||||||
|         <p class="text-center text-gray-200" :style="{ fontSize: sizeMultiplier * 0.65 + 'rem' }">{{ numBooks }} Book{{ numBooks === 1 ? '' : 's' }}</p> |         <p id="numbooks" class="text-center text-gray-200" :style="{ fontSize: sizeMultiplier * 0.65 + 'rem' }">{{ numBooks }} Book{{ numBooks === 1 ? '' : 's' }}</p> | ||||||
|       </div> |       </div> | ||||||
|     </div> |     </div> | ||||||
|   </nuxt-link> |   </nuxt-link> | ||||||
| @ -21,8 +21,14 @@ export default { | |||||||
|       type: Object, |       type: Object, | ||||||
|       default: () => {} |       default: () => {} | ||||||
|     }, |     }, | ||||||
|     width: Number, |     width:  { | ||||||
|     height: Number, |       type: Number, | ||||||
|  |       default: 150 | ||||||
|  |     }, | ||||||
|  |     height: {  | ||||||
|  |       type: Number, | ||||||
|  |       default: 100 | ||||||
|  |     }, | ||||||
|     sizeMultiplier: { |     sizeMultiplier: { | ||||||
|       type: Number, |       type: Number, | ||||||
|       default: 1 |       default: 1 | ||||||
|  | |||||||
		Loading…
	
		Reference in New Issue
	
	Block a user