How to test computed properties in Vue.js? Can't mock "data" - javascript

I wonder how to test computed properties in Vue.js's unit tests.
I have create a new project via vue-cli (webpack based).
For example here are my Component:
<script>
export default {
data () {
return {
source: []
}
},
methods: {
removeDuplicates (arr) {
return [...new Set(arr)]
}
},
computed: {
types () {
return this.removeDuplicates(this.source))
}
}
}
</script>
I've tried to test it like this
it('should remove duplicates from array', () => {
const arr = [1, 2, 1, 2, 3]
const result = FiltersList.computed.types()
const expectedLength = 3
expect(result).to.have.length(expectedLength)
})
QUESTION (two problems):
this.source is undefined. How to mock or set value to it? (FiltersList.data is a function);
Perhaps I don't wan't to call removeDuplicates method, but how to mock(stub) this call?

Okay. I've found a dumb solution. Dumb but works.
You have been warned =)
The idea: To use .call({}) to replace this inside that calls:
it('should remove duplicates from array', () => {
const mockSource = {
source: [1, 2, 1, 2, 3],
getUniq (arr) {
return FiltersList.methods.removeDuplicates(arr)
}
}
const result = FiltersList.computed.types.call(mockSource)
const expectedLength = 3
expect(result).to.have.length(expectedLength)
})
So basically you can specify your own this with any kind of data.
and call YourComponent.computed.foo.call(mockSource). Same for methods

Just change the variable from which depends computed property and expect it.
This is my work example for component computed prop:
import Vue from 'vue'
import Zoom from 'src/components/Zoom'
import $ from 'jquery'
/* eslint-disable no-unused-vars */
/**
* get template for wrapper Vue object make Vue with Zoom component and that template
* #param wrapperTemplate
* #returns {Vue$2}
*/
const makeWrapper = (wrapperTemplate = '<div><zoom ref="component"></zoom></div>') => {
return new Vue({
template: wrapperTemplate,
components: {Zoom}
})
}
const startWrapperWidth = 1093
const startWrapperHeight = 289
const startImageWidth = 1696
const startImageHeight = 949
const padding = 15
/**
* gets vueWrapper and return component from it
* #param vueWrapper
* #param useOffset
* #returns {'Zoom component'}
*/
const setSizesForComponent = (vueWrapper) => {
vueWrapper.$mount()
var cmp = vueWrapper.$refs.component
var $elWrapper = $(cmp.$el)
var $elImage = $elWrapper.find(cmp.selectors.image)
$elWrapper.width(startWrapperWidth)
$elWrapper.height(startWrapperHeight)
$elWrapper.css({padding: padding})
$elImage.width(startImageWidth)
$elImage.height(startImageHeight)
cmp.calculateSizesAndProportions()
return cmp
}
describe('onZoom method (run on mousemove)', () => {
sinon.spy(Zoom.methods, 'onZoom')
let vueWrapper = makeWrapper()
let cmp = setSizesForComponent(vueWrapper)
let e = document.createEvent('HTMLEvents')
e.initEvent('mousemove', true, true)
e.pageX = 150
e.pageY = 250
let callsOnZoomBeforeMousemove = Zoom.methods.onZoom.callCount
cmp.$el.dispatchEvent(e)
describe('left and top computed props', () => {
it('left', () => {
expect(cmp.left).is.equal(-74)
})
it('top', () => {
expect(cmp.top).is.equal(-536)
})
})
})

Related

Cannot set properties of undefined when using eval but works without eval

I am trying to use eval() to dynamically update a variable that I have to access by path like myArray[0][0][1][0]... But for some reason it is giving me this error:
Uncaught TypeError: Cannot set properties of undefined (setting '$folded')
If I do it without eval like this productCategoriesTree[1].$folded = false - then it works.
I am using Vue 3 so perhaps it might be related to Vue somehow.
addNewProductCategory() function is the problem:
// useProductCategoriesTree.ts
import { v4 as uuidv4 } from 'uuid'
import TreeItem from '#/types/TreeItem'
import { try as tryCatch } from 'radash'
import { getCurrentInstance } from 'vue'
import { ProductCategory, ProductCategoryTreeInput } from '#/types/graphql/graphql'
import productCategoryApi, { FRAGMENT_PRODUCT_CATEGORY_TREE } from '#/api/productCategoryApi'
export function useProductCategoriesTree() {
let productCategoriesTree = $ref<TreeItem<ProductCategory>[]>([])
const { emit } = getCurrentInstance() as any
const getProductCategoriesTree = async () => {
const [error, productCategories] = await tryCatch(productCategoryApi.getProductCategoryList)(FRAGMENT_PRODUCT_CATEGORY_TREE)
productCategoriesTree = buildProductCategoriesTree(productCategories)
}
const buildProductCategoriesTree = (productCategories: ProductCategory[]): TreeItem<ProductCategory>[] => {
return productCategories.map(productCategory => ({
data: productCategory,
children: (productCategory.children && productCategory.children.length)
? buildProductCategoriesTree(productCategory.children)
: undefined
}))
}
const selectProductCategory = (productCategoryUuid: string) => {
emit('select', productCategoryUuid)
}
// treePath could be [0, 1, 0, 0, 1] - it is dynamic
const addNewProductCategory = (parentCategoryUuid: string, treePath: number[]) => {
const newProductCategory = {
uuid: `__NEW-${uuidv4()}`,
namePlural: 'new',
parent: { uuid: parentCategoryUuid }
}
if (!productCategoriesTree[1].children) {
productCategoriesTree[1].children = []
}
productCategoriesTree[1].children?.push({
data: newProductCategory as ProductCategory,
children: [],
})
console.log(`productCategoriesTree[${treePath.join('][')}].$folded = false`)
// productCategoriesTree[1].$folded = false
// this works
productCategoriesTree[1].$folded = false
// this does not work
eval(`productCategoriesTree[${treePath.join('][')}].$folded = false`)
}
getProductCategoriesTree()
return $$({
productCategoriesTree,
selectProductCategory,
addNewProductCategory,
})
}
I'd try any other approach that didn't use eval. For example:
let thingToUpdate = productCategoriesTree;
treePath.forEach(el => thingToUpdate = thingToUpdate[el]);
thingToUpdate.$folded = false;
Similar to this question: Javascript: Get deep value from object by passing path to it as string

Vue 3 ref array doesnt update values

Description
I'm new to Vue 3 and the composition API.
When i try to delete values in an array declared with ref, values do not delete.
Code preview
Here is my code
<script setup lang="ts">
import { IPartnerCategory, IPartners } from '~~/shared/types'
const selectedPartnershipCategories = ref([])
const props = withDefaults(
defineProps<{
partnershipCategories?: IPartnerCategory[]
partnerships?: IPartners[]
freelancer?: boolean
}>(),
{
partnershipCategories: () => [],
partnerships: () => [],
freelancer: false,
}
)
const emit =
defineEmits<{
(e: 'update:value', partnership: IPartnerCategory): void
(e: 'update:selected', select: boolean): void
}>()
const updateSelectedPartnership = (partnershipId: string, categorySelected: boolean) => {
if (categorySelected && !selectedPartnershipCategories.value.includes(partnershipId)) {
return selectedPartnershipCategories.value.push(partnershipId)
}
if (!categorySelected && selectedPartnershipCategories.value.includes(partnershipId)) {
const clearedArray = selectedPartnershipCategories.value.filter((i) => {
return i !== partnershipId
})
console.log(clearedArray)
}
}
const select = (event) => {
updateSelectedPartnership(event.fieldId, event.isSelected)
}
</script>
My array is declared as selectedPartnershipCategories
I've a function named updateSelectedPartnesh, called everytime when i update a value in the selectedPartnership array
When i log clearedArray values are only pushed but not deleted.
Thanks in advance for your help :)
This is because filter creates a shallow copy and does not modify the original array.
const words = ['spray', 'limit', 'elite', 'exuberant', 'destruction', 'present'];
const result = words.filter(word => word.length > 6);
console.log(result);
console.log(words);
If you want to modify selectedPartnership, you should use splice or another method, or simply selectedPartnership = selectedPartnership.filter(...).

Updating a two level nested object using immutability-helper in react and typescript

I have an array of object called TourStop, which is two level nested. The types are as below.
TourStops = TourStop[]
TourStop = {
suggestions?: StoreSuggestion[]
id: Uuid
}
StoreSuggestion= {
id: Uuid
spaceSuggestions: SpaceSuggestion[]
}
SpaceSuggestion = {
id: Uuid
title: string
}
My goal is to remove a particular StoreSuggestion from a particular TourStop
I have written the following code(uses immutability-helper and hooks)
const [tourStopsArray, setTourStopsArray] = useState(tourStops)
// function to remove the store suggestion
const removeSuggestionFromTourStop = (index: number, tourStopId: Uuid) => {
// find the particular tourStop
const targetTourStop = tourStopsArray.find(arr => arr.id === tourStopId)
// Update the storeSuggestion in that tourstop
const filteredStoreSuggestion = targetTourStop?.suggestions?.filter(sugg => sugg.id !== index)
if (targetTourStop) {
// create a new TourStop with the updated storeSuggestion
const updatedTargetTourStop: TourStopType = {
...targetTourStop,
suggestions: filteredStoreSuggestion,
}
const targetIndex = tourStopsArray.findIndex(
tourStop => tourStop.id == updatedTargetTourStop.id,
)
// Find it by index and remove it
setTourStopsArray(
update(tourStopsArray, {
$splice: [[targetIndex, 1]],
}),
)
// add the new TourStop
setTourStopsArray(
update(tourStopsArray, {
$push: [updatedTargetTourStop],
}),
)
}
}
The push action works correctly. However the splice action doesn't work for some reason. What am I doing wrong?

waiting observable subscribe inside foreach to end

Im iterating over an array of objects and for each iteration i run a observable.subscribe, how can i ensure that the all the subscribes were completed, so i can call another function?
this is the function
calculaSimulacoesPorInscricao(){
let lista = ["2019-01-01","2020-02-02","2021-01-01","2022-01-01","2023-01-01"];
this.cliente.coberturas.forEach(cobertura => {
cobertura.MovimentosProjetados = [];
this._dataService.ObterSimulacao(cobertura.codigoInscricao,lista)
.subscribe((data:any[])=>{
data[0].simulacaoRentabilidadeEntities.forEach(simulacao =>{
let movimento = {
dataMovimento: '',
valor: 1,
imposto: 1,
percentualCarregamento: 1,
fundoCotacao: []
};
movimento.dataMovimento = simulacao.anoRentabilidade;
movimento.imposto = cobertura.totalFundos * simulacao.demonstrativo.demonstrativo[0].aliquota;
movimento.percentualCarregamento = simulacao.valorPercentualCarregamento * (cobertura.totalFundos + (cobertura.totalFundos * simulacao.percentualRentabilidade));
movimento.valor = cobertura.totalFundos + (cobertura.totalFundos * simulacao.percentualRentabilidade);
cobertura.MovimentosProjetados.push(movimento);
});
})
});
this.calcularSimulacao();
}
i need to call calcularSimulacao() after all subscribe inside the coberturas.foreach is done. Any tips?
You can try using forkJoin with onCompleted callback. See below:
calculaSimulacoesPorInscricao() {
let lista = [
'2019-01-01',
'2020-02-02',
'2021-01-01',
'2022-01-01',
'2023-01-01'
];
let all_obs = [];
this.cliente.coberturas.forEach(cobertura => {
cobertura.MovimentosProjetados = [];
all_obs.push(
this._dataService.ObterSimulacao(cobertura.codigoInscricao, lista).pipe(
map(
(data: any[]) => {
data[0].simulacaoRentabilidadeEntities.forEach(simulacao => {
let movimento = {
dataMovimento: '',
valor: 1,
imposto: 1,
percentualCarregamento: 1,
fundoCotacao: []
};
movimento.dataMovimento = simulacao.anoRentabilidade;
movimento.imposto =
cobertura.totalFundos *
simulacao.demonstrativo.demonstrativo[0].aliquota;
movimento.percentualCarregamento =
simulacao.valorPercentualCarregamento *
(cobertura.totalFundos +
cobertura.totalFundos * simulacao.percentualRentabilidade);
movimento.valor =
cobertura.totalFundos +
cobertura.totalFundos * simulacao.percentualRentabilidade;
cobertura.MovimentosProjetados.push(movimento);
});
})
)
);
});
forkJoin(all_obs).subscribe(
undefined,
undefined,
() => {
this.calcularSimulacao();
}
);
}
Just remember to import forkJoin if you're using RxJS 6.
import { forkJoin } from 'rxjs';
In RxJS 5 it could look like this. In RxJS 6 just replace Observable.forkJoin with forkJoin.
const observables = [];
this.cliente.coberturas.forEach(cobertura => {
// just create the Observable here but don't subscribe yet
observables.push(this._dataService.ObterSimulacao(...));
});
Observable.forkJoin(observables)
.subscribe(results => this.calcularSimulacao());

In Rx.js, how can I distinguish which stream triggers the combineLatest method?

I'm writing my own version of who to follow?. Clicking refreshButton will fetching suggestions list and refresh <Suggestion-List />, and closeButton will resue the data from suggestions list and refresh <Suggestion-List-Item />.
I want to let the closeClick$ and suggestions$ combine together to driving subscribers.
Demo code here:
var refreshClick$ = Rx.Observable
.fromEvent(document.querySelector('.refresh'), 'click')
var closeClick$ = Rx.Observable.merge(
Rx.Observable.fromEvent(document.querySelector('.close1'), 'click').mapTo(1),
Rx.Observable.fromEvent(document.querySelector('.close2'), 'click').mapTo(2),
Rx.Observable.fromEvent(document.querySelector('.close3'), 'click').mapTo(3)
)
var suggestions$ = refreshClick$
.debounceTime(250)
.map(() => `https://api.github.com/users?since=${Math.floor(Math.random()*500)}`)
.startWith('https://api.github.com/users')
.switchMap(requestUrl => Rx.Observable.fromPromise($.getJSON(requestUrl)))
Rx.Observable.combineLatest(closeClick$, suggestions$, (closeTarget, suggestions) => {
if (/* the latest stream is closeClick$ */) {
return [{
target: clickTarget,
suggestion: suggestions[Math.floor(Math.random() * suggestions.length)]
}]
}
if (/* the latest stream is suggestions$ */) {
return [1, 2, 3].map(clickTarget => ({
target: clickTarget,
suggestion: suggestions[Math.floor(Math.random() * suggestions.length)]
}))
}
})
Rx.Observable.merge(renderDataCollectionFromSuggestions$, renderDataCollectionFromCloseClick$)
.subscribe(renderDataCollection => {
renderDataCollection.forEach(renderData => {
var suggestionEl = document.querySelector('.suggestion' + renderData.target)
if (renderData.suggestion === null) {
suggestionEl.style.visibility = 'hidden'
} else {
suggestionEl.style.visibility = 'visible'
var usernameEl = suggestionEl.querySelector('.username')
usernameEl.href = renderData.suggestion.html_url
usernameEl.textContent = renderData.suggestion.login
var imgEl = suggestionEl.querySelector('img')
imgEl.src = "";
imgEl.src = renderData.suggestion.avatar_url
}
})
})
You can find it in JsFiddle.
You should note the comments in condition judgment, closeClick$ emits [{ target: x, suggestion: randomSuggestionX }], suggestions$ emits [{ target: 1, suggestion: randomSuggestion1 }, { target: 2, suggestion: randomSuggestion2 }, { target: 3, suggestion: randomSuggestion3 }]. Subsriber render interface according to the emitted data.
May there are some ways/hacks to distinguish the latest stream in combineLatest or elegant modifications?
I think the easiest way would be to use the scan() operator and always keep the previous state in an array:
Observable.combineLatest(obs1$, obs2$, obs3$)
.scan((acc, results) => {
if (acc.length === 2) {
acc.shift();
}
acc.push(results);
return acc;
}, [])
.do(states => {
// states[0] - previous state
// states[1] - current state
// here you can compare the two states to see what has triggered the change
})
Instead of do() you can use whatever operator you want of course.
Or maybe instead of the scan() operator you could use just bufferCount(2, 1) that should emit the same two arrays... (I didn't test it)

Categories

Resources