Add NarratorCard and AuthorCard component tests

This commit is contained in:
mikiher 2024-04-16 00:00:35 +03:00
parent d638a328d8
commit 9e1c907591
4 changed files with 300 additions and 16 deletions

View 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')
})
})

View File

@ -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>

View 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
})
})

View File

@ -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