mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2025-02-16 02:09:33 +01:00
Merge pull request #2853 from mikiher/nuxt-unit-tests
Add client component testing framework and tests
This commit is contained in:
commit
7929f3dc42
1
.gitignore
vendored
1
.gitignore
vendored
@ -19,3 +19,4 @@
|
|||||||
sw.*
|
sw.*
|
||||||
.DS_STORE
|
.DS_STORE
|
||||||
.idea/*
|
.idea/*
|
||||||
|
tailwind.compiled.css
|
@ -1,34 +1,34 @@
|
|||||||
<template>
|
<template>
|
||||||
<nuxt-link :to="`/author/${author.id}`">
|
<nuxt-link :to="`/author/${author?.id}`">
|
||||||
<div @mouseover="mouseover" @mouseleave="mouseleave">
|
<div cy-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 cy-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 cy-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 cy-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 cy-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 cy-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 cy-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>
|
||||||
|
@ -1,28 +1,28 @@
|
|||||||
<template>
|
<template>
|
||||||
<div ref="card" :id="`series-card-${index}`" :style="{ width: width + 'px', height: height + 'px' }" class="rounded-sm z-30 cursor-pointer" @mousedown.prevent @mouseup.prevent @mousemove.prevent @mouseover="mouseover" @mouseleave="mouseleave" @click="clickCard">
|
<div cy-id="card" ref="card" :id="`series-card-${index}`" :style="{ width: width + 'px', height: height + 'px' }" class="absolute rounded-sm z-30 cursor-pointer" @mousedown.prevent @mouseup.prevent @mousemove.prevent @mouseover="mouseover" @mouseleave="mouseleave" @click="clickCard">
|
||||||
<div class="absolute top-0 left-0 w-full box-shadow-book shadow-height" />
|
<div class="absolute top-0 left-0 w-full box-shadow-book shadow-height" />
|
||||||
<div class="w-full h-full bg-primary relative rounded overflow-hidden z-0">
|
<div class="w-full h-full bg-primary relative rounded overflow-hidden z-0">
|
||||||
<covers-group-cover v-if="series" ref="cover" :id="seriesId" :name="displayTitle" :book-items="books" :width="width" :height="height" :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
<covers-group-cover v-if="series" ref="cover" :id="seriesId" :name="displayTitle" :book-items="books" :width="width" :height="height" :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="absolute z-10 top-1.5 right-1.5 rounded-md leading-3 text-sm p-1 font-semibold text-white flex items-center justify-center" style="background-color: #cd9d49dd">{{ books.length }}</div>
|
<div cy-id="seriesLengthMarker" class="absolute z-10 top-1.5 right-1.5 rounded-md leading-3 text-sm p-1 font-semibold text-white flex items-center justify-center" style="background-color: #cd9d49dd">{{ books.length }}</div>
|
||||||
|
|
||||||
<div v-if="seriesPercentInProgress > 0" class="absolute bottom-0 left-0 h-1 shadow-sm max-w-full z-10 rounded-b w-full" :class="isSeriesFinished ? 'bg-success' : 'bg-yellow-400'" :style="{ width: seriesPercentInProgress * 100 + '%' }" />
|
<div cy-id="seriesProgressBar" v-if="seriesPercentInProgress > 0" class="absolute bottom-0 left-0 h-1 shadow-sm max-w-full z-10 rounded-b w-full" :class="isSeriesFinished ? 'bg-success' : 'bg-yellow-400'" :style="{ width: seriesPercentInProgress * 100 + '%' }" />
|
||||||
|
|
||||||
<div v-if="hasValidCovers" class="bg-black bg-opacity-60 absolute top-0 left-0 w-full h-full flex items-center justify-center text-center transition-opacity" :class="isHovering ? '' : 'opacity-0'" :style="{ padding: `${sizeMultiplier}rem` }">
|
<div cy-id="hoveringDisplayTitle" v-if="hasValidCovers" class="bg-black bg-opacity-60 absolute top-0 left-0 w-full h-full flex items-center justify-center text-center transition-opacity" :class="isHovering ? '' : 'opacity-0'" :style="{ padding: `${sizeMultiplier}rem` }">
|
||||||
<p :style="{ fontSize: 1.2 * sizeMultiplier + 'rem' }">{{ displayTitle }}</p>
|
<p :style="{ fontSize: 1.2 * sizeMultiplier + 'rem' }">{{ displayTitle }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<span v-if="!isHovering && rssFeed" class="absolute z-10 material-icons text-success" :style="{ top: 0.5 * sizeMultiplier + 'rem', left: 0.5 * sizeMultiplier + 'rem', fontSize: 1.5 * sizeMultiplier + 'rem' }">rss_feed</span>
|
<span cy-id="rssFeedMarker" v-if="!isHovering && rssFeed" class="absolute z-10 material-icons text-success" :style="{ top: 0.5 * sizeMultiplier + 'rem', left: 0.5 * sizeMultiplier + 'rem', fontSize: 1.5 * sizeMultiplier + 'rem' }">rss_feed</span>
|
||||||
|
|
||||||
<div v-if="!isAlternativeBookshelfView" class="categoryPlacard absolute z-10 left-0 right-0 mx-auto -bottom-6 h-6 rounded-md text-center" :style="{ width: Math.min(200, width) + 'px' }">
|
<div cy-id="standardBottomText" v-if="!isAlternativeBookshelfView" class="categoryPlacard absolute z-10 left-0 right-0 mx-auto -bottom-6 h-6 rounded-md text-center" :style="{ width: Math.min(200, width) + 'px' }">
|
||||||
<div class="w-full h-full shinyBlack flex items-center justify-center rounded-sm border" :style="{ padding: `0rem ${0.5 * sizeMultiplier}rem` }">
|
<div class="w-full h-full shinyBlack flex items-center justify-center rounded-sm border" :style="{ padding: `0rem ${0.5 * sizeMultiplier}rem` }">
|
||||||
<p class="truncate" :style="{ fontSize: labelFontSize + 'rem' }">{{ displayTitle }}</p>
|
<p cy-id="standardBottomDisplayTitle" class="truncate" :style="{ fontSize: labelFontSize + 'rem' }">{{ displayTitle }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="absolute z-30 left-0 right-0 mx-auto -bottom-8 h-8 py-1 rounded-md text-center">
|
<div cy-id="detailBottomText" v-else class="absolute z-30 left-0 right-0 mx-auto -bottom-8 h-8 py-1 rounded-md text-center">
|
||||||
<p class="truncate" :style="{ fontSize: labelFontSize * sizeMultiplier + 'rem' }">{{ displayTitle }}</p>
|
<p cy-id="detailBottomDisplayTitle" class="truncate" :style="{ fontSize: labelFontSize * sizeMultiplier + 'rem' }">{{ displayTitle }}</p>
|
||||||
<p v-if="displaySortLine" class="truncate text-gray-400" :style="{ fontSize: 0.8 * sizeMultiplier + 'rem' }">{{ displaySortLine }}</p>
|
<p cy-id="detailBottomSortLine" v-if="displaySortLine" class="truncate text-gray-400" :style="{ fontSize: 0.8 * sizeMultiplier + 'rem' }">{{ displaySortLine }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -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 cy-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 cy-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 cy-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
|
||||||
|
11
client/cypress.config.js
Normal file
11
client/cypress.config.js
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
const { defineConfig } = require("cypress")
|
||||||
|
|
||||||
|
module.exports = defineConfig({
|
||||||
|
component: {
|
||||||
|
devServer: {
|
||||||
|
framework: "nuxt",
|
||||||
|
bundler: "webpack"
|
||||||
|
},
|
||||||
|
specPattern: "cypress/tests/**/*.cy.js"
|
||||||
|
}
|
||||||
|
})
|
BIN
client/cypress/fixtures/images/book_placeholder.jpg
Normal file
BIN
client/cypress/fixtures/images/book_placeholder.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 105 KiB |
31
client/cypress/support/commands.js
Normal file
31
client/cypress/support/commands.js
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
// ***********************************************
|
||||||
|
// This example commands.js shows you how to
|
||||||
|
// create various custom commands and overwrite
|
||||||
|
// existing commands.
|
||||||
|
//
|
||||||
|
// For more comprehensive examples of custom
|
||||||
|
// commands please read more here:
|
||||||
|
// https://on.cypress.io/custom-commands
|
||||||
|
// ***********************************************
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// -- This is a parent command --
|
||||||
|
// Cypress.Commands.add('login', (email, password) => { ... })
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// -- This is a child command --
|
||||||
|
// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... })
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// -- This is a dual command --
|
||||||
|
// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... })
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// -- This will overwrite an existing command --
|
||||||
|
// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
|
||||||
|
Cypress.Commands.overwriteQuery('get', function (originalFn, ...args) {
|
||||||
|
if (args.length > 0 && typeof args[0] === 'string' && args[0].startsWith('&')) {
|
||||||
|
args[0] = `[cy-id="${args[0].substring(1)}"]`
|
||||||
|
}
|
||||||
|
return originalFn.apply(this, args)
|
||||||
|
})
|
13
client/cypress/support/component-index.html
Normal file
13
client/cypress/support/component-index.html
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||||
|
<link rel="stylesheet" href="tailwind.compiled.css">
|
||||||
|
<title>Components App</title>
|
||||||
|
</head>
|
||||||
|
<body class="text-white bg-bg">
|
||||||
|
<div data-cy-root></div>
|
||||||
|
</body>
|
||||||
|
</html>
|
38
client/cypress/support/component.js
Normal file
38
client/cypress/support/component.js
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
// ***********************************************************
|
||||||
|
// This example support/component.js is processed and
|
||||||
|
// loaded automatically before your test files.
|
||||||
|
//
|
||||||
|
// This is a great place to put global configuration and
|
||||||
|
// behavior that modifies Cypress.
|
||||||
|
//
|
||||||
|
// You can change the location of this file or turn off
|
||||||
|
// automatically serving support files with the
|
||||||
|
// 'supportFile' configuration option.
|
||||||
|
//
|
||||||
|
// You can read more here:
|
||||||
|
// https://on.cypress.io/configuration
|
||||||
|
// ***********************************************************
|
||||||
|
import '../../assets/app.css'
|
||||||
|
import './tailwind.compiled.css'
|
||||||
|
// Import commands.js using ES2015 syntax:
|
||||||
|
import './commands'
|
||||||
|
import Vue from 'vue'
|
||||||
|
|
||||||
|
import { Constants } from '../../plugins/constants'
|
||||||
|
import Strings from '../../strings/en-us.json'
|
||||||
|
import '../../plugins/utils'
|
||||||
|
import '../../plugins/init.client'
|
||||||
|
|
||||||
|
import { mount } from 'cypress/vue2'
|
||||||
|
|
||||||
|
//Cypress.Commands.add('mount', mount)
|
||||||
|
Cypress.Commands.add('mount', (component, options = {}) => {
|
||||||
|
|
||||||
|
Vue.prototype.$constants = Constants
|
||||||
|
Vue.prototype.$strings = Strings
|
||||||
|
|
||||||
|
return mount(component, options)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Example use:
|
||||||
|
// cy.mount(MyComponent)
|
191
client/cypress/tests/components/cards/AuthorCard.cy.js
Normal file
191
client/cypress/tests/components/cards/AuthorCard.cy.js
Normal file
@ -0,0 +1,191 @@
|
|||||||
|
// Import the necessary dependencies
|
||||||
|
import AuthorCard from '@/components/cards/AuthorCard.vue'
|
||||||
|
import AuthorImage from '@/components/covers/AuthorImage.vue'
|
||||||
|
import Tooltip from '@/components/ui/Tooltip.vue'
|
||||||
|
import LoadingSpinner from '@/components/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().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.calledOnceWithExactly', 'Author John Doe was updated (no image found)')
|
||||||
|
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().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.calledOnceWithExactly', 'Author John Doe was updated')
|
||||||
|
cy.get('@error').should('not.have.been.called')
|
||||||
|
cy.get('@info').should('not.have.been.called')
|
||||||
|
})
|
||||||
|
})
|
216
client/cypress/tests/components/cards/LazySeriesCard.cy.js
Normal file
216
client/cypress/tests/components/cards/LazySeriesCard.cy.js
Normal file
@ -0,0 +1,216 @@
|
|||||||
|
import LazySeriesCard from '@/components/cards/LazySeriesCard.vue'
|
||||||
|
import GroupCover from '@/components/covers/GroupCover.vue'
|
||||||
|
|
||||||
|
describe('LazySeriesCard', () => {
|
||||||
|
const series = {
|
||||||
|
id: 1,
|
||||||
|
name: 'The Lord of the Rings',
|
||||||
|
nameIgnorePrefix: 'Lord of the Rings',
|
||||||
|
books: [
|
||||||
|
{ id: 1, updatedAt: /* 04/14/2024 */ 1713099600000, addedAt: 1713099600000, media: { coverPath: 'cover1.jpg' }, title: 'The Fellowship of the Ring' },
|
||||||
|
{ id: 2, updatedAt: /* 04/15/2024 */ 1713186000000, addedAt: 1713186000000, media: { coverPath: 'cover2.jpg' }, title: 'The Two Towers' },
|
||||||
|
{ id: 3, updatedAt: /* 04/16/2024 */ 1713272400000, addedAt: 1713272400000, media: { coverPath: 'cover3.jpg' }, title: 'The Return of the King' }
|
||||||
|
],
|
||||||
|
addedAt: /* 04/17/2024 */ 1713358800000,
|
||||||
|
totalDuration: /* 7h 30m */ 3600 * 7 + 60 * 30,
|
||||||
|
rssFeed: 'https://example.com/feed.rss'
|
||||||
|
}
|
||||||
|
|
||||||
|
const propsData = {
|
||||||
|
index: 0,
|
||||||
|
width: 192 * 2,
|
||||||
|
height: 192,
|
||||||
|
sizeMultiplier: 1,
|
||||||
|
bookCoverAspectRatio: 1,
|
||||||
|
bookshelfView: 1,
|
||||||
|
isCategorized: false,
|
||||||
|
seriesMount: series,
|
||||||
|
sortingIgnorePrefix: false,
|
||||||
|
orderBy: 'addedAt',
|
||||||
|
}
|
||||||
|
|
||||||
|
const stubs = {
|
||||||
|
'covers-group-cover': GroupCover
|
||||||
|
}
|
||||||
|
|
||||||
|
const mocks = {
|
||||||
|
$store: {
|
||||||
|
getters: {
|
||||||
|
'user/getUserCanUpdate': true,
|
||||||
|
'user/getUserMediaProgress': (id) => null,
|
||||||
|
'libraries/getLibraryProvider': () => 'audible.us',
|
||||||
|
'globals/getLibraryItemCoverSrc': () => 'https://my.server.com/book_placeholder.jpg'
|
||||||
|
},
|
||||||
|
state: {
|
||||||
|
libraries: {
|
||||||
|
currentLibraryId: 'library-123'
|
||||||
|
},
|
||||||
|
serverSettings: {
|
||||||
|
dateFormat: 'MM/dd/yyyy'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
before(() => {
|
||||||
|
cy.intercept('GET', 'https://my.server.com/book_placeholder.jpg', { fixture: 'images/book_placeholder.jpg' }).as('bookCover')
|
||||||
|
cy.mount(LazySeriesCard, { propsData, stubs, mocks })
|
||||||
|
cy.wait('@bookCover')
|
||||||
|
// Now the placeholder image is in the browser cache
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders the component', () => {
|
||||||
|
cy.mount(LazySeriesCard, { propsData, stubs, mocks })
|
||||||
|
|
||||||
|
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)
|
||||||
|
})
|
||||||
|
cy.get('&seriesLengthMarker').should('be.visible').and('have.text', propsData.seriesMount.books.length)
|
||||||
|
cy.get('&seriesProgressBar').should('not.exist')
|
||||||
|
cy.get('&hoveringDisplayTitle').should('be.hidden')
|
||||||
|
cy.get('&rssFeedMarker').should('be.visible')
|
||||||
|
cy.get('&standardBottomDisplayTitle').should('not.exist')
|
||||||
|
cy.get('&detailBottomDisplayTitle').should('be.visible')
|
||||||
|
cy.get('&detailBottomDisplayTitle').should('have.text', 'The Lord of the Rings')
|
||||||
|
cy.get('&detailBottomSortLine').should('have.text', 'Added 04/17/2024')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('shows series name and hides rss feed marker on mouseover', () => {
|
||||||
|
cy.mount(LazySeriesCard, { propsData, stubs, mocks })
|
||||||
|
cy.get('&card').trigger('mouseover')
|
||||||
|
|
||||||
|
cy.get('&hoveringDisplayTitle').should('be.visible').should('have.text', 'The Lord of the Rings')
|
||||||
|
cy.get('&rssFeedMarker').should('not.exist')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('routes properly when clicked', () => {
|
||||||
|
const updatedMocks = {
|
||||||
|
...mocks,
|
||||||
|
$router: {
|
||||||
|
push: cy.stub().as('routerPush')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cy.mount(LazySeriesCard, { propsData, stubs, mocks: updatedMocks })
|
||||||
|
cy.get('&card').click()
|
||||||
|
|
||||||
|
cy.get('@routerPush').should('have.been.calledOnceWithExactly', '/library/library-123/series/1')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('shows progress bar when progress is available', () => {
|
||||||
|
const updatedMocks = {
|
||||||
|
...mocks,
|
||||||
|
$store: {
|
||||||
|
...mocks.$store,
|
||||||
|
getters: {
|
||||||
|
...mocks.$store.getters,
|
||||||
|
'user/getUserMediaProgress': (id) => {
|
||||||
|
switch (id) {
|
||||||
|
case 1:
|
||||||
|
return { isFinished: true }
|
||||||
|
case 2:
|
||||||
|
return { progress: 0.5 }
|
||||||
|
default:
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cy.mount(LazySeriesCard, { propsData, stubs, mocks: updatedMocks })
|
||||||
|
|
||||||
|
cy.get('&seriesProgressBar')
|
||||||
|
.should('be.visible')
|
||||||
|
.and('have.class', 'bg-yellow-400')
|
||||||
|
.and(($el) => {
|
||||||
|
const width = $el.width()
|
||||||
|
expect(width).to.be.closeTo((2 / 3) * propsData.width, 0.01)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('shows full green progress bar when all books are finished', () => {
|
||||||
|
const updatedMocks = {
|
||||||
|
...mocks,
|
||||||
|
$store: {
|
||||||
|
...mocks.$store,
|
||||||
|
getters: {
|
||||||
|
...mocks.$store.getters,
|
||||||
|
'user/getUserMediaProgress': (id) => { return { isFinished: true } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cy.mount(LazySeriesCard, { propsData, stubs, mocks: updatedMocks })
|
||||||
|
|
||||||
|
cy.get('&seriesProgressBar')
|
||||||
|
.should('be.visible')
|
||||||
|
.and('have.class', 'bg-success')
|
||||||
|
.and(($el) => {
|
||||||
|
const width = $el.width()
|
||||||
|
expect(width).to.equal(propsData.width)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('hides the rss feed marker when there is no rss feed', () => {
|
||||||
|
const updatedPropsData = {
|
||||||
|
...propsData,
|
||||||
|
seriesMount: { ...series, rssFeed: null }
|
||||||
|
}
|
||||||
|
cy.mount(LazySeriesCard, { propsData: updatedPropsData, stubs, mocks })
|
||||||
|
|
||||||
|
cy.get('&rssFeedMarker').should('not.exist')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('shows the standard bottom display when bookshelf view is 0', () => {
|
||||||
|
const updatedPropsData = {
|
||||||
|
...propsData,
|
||||||
|
bookshelfView: 0
|
||||||
|
}
|
||||||
|
cy.mount(LazySeriesCard, { propsData: updatedPropsData, stubs, mocks })
|
||||||
|
|
||||||
|
cy.get('&standardBottomDisplayTitle').should('be.visible')
|
||||||
|
cy.get('&detailBottomDisplayTitle').should('not.exist')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('shows total duration in sort line when orderBy is totalDuration', () => {
|
||||||
|
const updatedPropsData = {
|
||||||
|
...propsData,
|
||||||
|
orderBy: 'totalDuration'
|
||||||
|
}
|
||||||
|
cy.mount(LazySeriesCard, { propsData: updatedPropsData, stubs, mocks })
|
||||||
|
|
||||||
|
cy.get('&detailBottomSortLine').should('have.text', 'Duration 7h 30m')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('shows last book updated date in sort line when orderBy is lastBookUpdated', () => {
|
||||||
|
const updatedPropsData = {
|
||||||
|
...propsData,
|
||||||
|
orderBy: 'lastBookUpdated'
|
||||||
|
}
|
||||||
|
cy.mount(LazySeriesCard, { propsData: updatedPropsData, stubs, mocks })
|
||||||
|
|
||||||
|
cy.get('&detailBottomSortLine').should('have.text', 'Last Book Updated 04/16/2024')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('shows last book added date in sort line when orderBy is lastBookAdded', () => {
|
||||||
|
const updatedPropsData = {
|
||||||
|
...propsData,
|
||||||
|
orderBy: 'lastBookAdded'
|
||||||
|
}
|
||||||
|
cy.mount(LazySeriesCard, { propsData: updatedPropsData, stubs, mocks })
|
||||||
|
|
||||||
|
cy.get('&detailBottomSortLine').should('have.text', 'Last Book Added 04/16/2024')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('shows nameIgnorePrefix when sortingIgnorePrefix is true', () => {
|
||||||
|
const updatedPropsData = {
|
||||||
|
...propsData,
|
||||||
|
sortingIgnorePrefix: true
|
||||||
|
}
|
||||||
|
cy.mount(LazySeriesCard, { propsData: updatedPropsData, stubs, mocks })
|
||||||
|
|
||||||
|
cy.get('&detailBottomDisplayTitle').should('have.text', 'Lord of the Rings')
|
||||||
|
})
|
||||||
|
|
||||||
|
})
|
85
client/cypress/tests/components/cards/NarratorCard.cy.js
Normal file
85
client/cypress/tests/components/cards/NarratorCard.cy.js
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
import NarratorCard from '@/components/cards/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
|
||||||
|
})
|
||||||
|
})
|
@ -153,4 +153,6 @@ module.exports = {
|
|||||||
* See: [Issue tracker](https://github.com/nuxt-community/tailwindcss-module/issues/480)
|
* See: [Issue tracker](https://github.com/nuxt-community/tailwindcss-module/issues/480)
|
||||||
*/
|
*/
|
||||||
devServerHandlers: [],
|
devServerHandlers: [],
|
||||||
|
|
||||||
|
ignore: ["**/*.test.*", "**/*.cy.*"]
|
||||||
}
|
}
|
||||||
|
1070
client/package-lock.json
generated
1070
client/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -9,7 +9,10 @@
|
|||||||
"dev2": "nuxt --hostname localhost --port 1337",
|
"dev2": "nuxt --hostname localhost --port 1337",
|
||||||
"build": "nuxt build",
|
"build": "nuxt build",
|
||||||
"start": "nuxt start",
|
"start": "nuxt start",
|
||||||
"generate": "nuxt generate"
|
"generate": "nuxt generate",
|
||||||
|
"test": "npm run compile-tailwind && cypress run --component --browser chrome",
|
||||||
|
"test-visually": "npm run compile-tailwind && cypress open --component --browser chrome",
|
||||||
|
"compile-tailwind": "tailwindcss -i ./assets/tailwind.css -o ./cypress/support/tailwind.compiled.css"
|
||||||
},
|
},
|
||||||
"author": "advplyr",
|
"author": "advplyr",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
@ -33,6 +36,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@nuxtjs/pwa": "^3.3.5",
|
"@nuxtjs/pwa": "^3.3.5",
|
||||||
"autoprefixer": "^10.4.7",
|
"autoprefixer": "^10.4.7",
|
||||||
|
"cypress": "^13.7.3",
|
||||||
"postcss": "^8.3.6",
|
"postcss": "^8.3.6",
|
||||||
"tailwindcss": "^3.4.1"
|
"tailwindcss": "^3.4.1"
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user