Injecting data into a vue3 vitest - vuejs3

I've written a test for vue3 for one of my components:
import { describe} from 'vitest';
import { shallowMount } from '#vue/test-utils';
import ProgressBar from '#/components/ProgressBar.vue';
const messages = {
"en-US" : {
strings: {
a: "A",
b: "B",
c: "C"
}
},
"fr-CA": {
strings: {
a: "AA",
b: "BB",
c: "CC"
}
}
};
const locale = "en-US";
try {
describe("ProgressBar.vue", () => {
function stepClick() {
console.log("step click");
}
const stepData = {
steps: [
{ index: 1, completed: false, clickActive: false },
{ index: 2, completed: false, clickActive: false },
{ index: 3, completed: false, clickActive: false }
],
currentIndex: 1
};
const component = shallowMount(ProgressBar, {
props: {
stepDataSource: "stepData",
onClick: stepClick,
locPage: "strings",
locKeys:['a', 'b', 'c' ]
},
provide: {
stepData() { return stepData }
},
mocks: {
'$t': (key) => {
const params = key.split('.');
return messages[locale][params[0]][params[1]];
}
}
});
});
} catch (e) {
console.log(e);
}
The component injects the specified data in properties:
stepDataSource: "stepData"
It is loaded by a method in the component:
async getStepData() {
console.log('getStepData');
const stepData = inject(this.stepDataSource) as StepData;
this.data.stepData = stepData;
const instance = getCurrentInstance();
instance?.proxy?.$forceUpdate();
},
Which works in normal running but it doesn't seem to get passed by the test. AFAIK, my mock of locale is working.
So what am I doing wrong to inject the step data for the test? I understand that I don't actually have any tests yet, but I need to get it to mount first.

This is a working test. It mocks $t successfully. I gave up trying to write them in typescript, this is a javascript file in the src/tests folder. If you need additional info about how to setup the rest of the project, go ahead and ask here.
import { describe, test, it, expect } from 'vitest';
import { ref } from 'vue';
import ProgressBar from '#/components/ProgressBar.vue';
import { mount } from "#vue/test-utils";
describe("ProgressBar.vue", () => {
const messages = {
"en-US" : {
strings: {
a: "A",
b: "B",
c: "C"
}
},
"fr-CA": {
strings: {
a: "AA",
b: "BB",
c: "CC"
}
}
};
const locale = "en-US";
const clickedItems = {1: false, 2: false, 3: false};
function stepClick(index) {
console.log("index:" + index);
clickedItems[index] = true;
}
const stepData = ref({
steps: [
{ index: 1, completed: true, clickActive: true },
{ index: 2, completed: false, clickActive: true },
{ index: 3, completed: false, clickActive: false }
],
currentIndex: 2
});
test ("Arrange ProgressBar", async () => {
const component = mount(ProgressBar, {
props: {
stepData: stepData,
onClick: stepClick,
locPage: "strings",
locKeys:['a', 'b', 'c']
},
global: {
mocks: {
$t: (msg) => {
const params = msg.split('.');
return messages[locale][params[0]][params[1]];
}
}
}
});
let multipleActive = false;
const progItems = component.findAll('.prog-item');
let selectedIndex = -1;
progItems.forEach((item, index) => {
if (item.element.className.indexOf("current") > -1) {
if (selectedIndex < 0) {
selectedIndex = index + 1; //indexes are 1 based for this control
} else {
multipleActive = true;
}
}
item.find("span.prog-item-outer").trigger('click');
});
expect(!multipleActive, "Only one Progbar item is selected").toBeTruthy();
expect(selectedIndex === 2, "Correct Progbar Item selected,").toBeTruthy();
expect(clickedItems[1] && !clickedItems[2] && !clickedItems[3], "Correct progBar indexes are return and correct items are click-active").toBeTruthy();
});
});

Related

How to watch the whole pinia state inside the store?

I can't watch the whole pinia state inside the store. If I do it Vue js will show a next message: [Vue warn] Avoid app logic that relies on enumerating keys on a component instance. The keys will be empty in production mode to avoid performance overhead. I know about the tip in pinia documentation, but I can't do it inside the store.
My store:
export const achievementsKey = 'achievements'
export const missCountKey = 'missCount'
export const useAchievementStore = defineStore('achievement', () => {
const store = useStore()
const skinStore = useSkinStore()
const achievements = ref<Achievements>(
localStorage.getItem(achievementsKey)
? JSON.parse(<string>localStorage.getItem(achievementsKey))
: [
{
name: 'Get into the top ten',
prize: skinStore.skins[0],
done: false,
},
{ name: 'Miss 5 times', prize: 100, done: false },
{ name: 'Earn 2000 points', prize: 150, done: false },
{ name: 'Buy a new skin', prize: 150, done: false },
]
)
const missCount = ref(
localStorage.getItem(missCountKey)
? Number(localStorage.getItem(missCountKey))
: 0
)
function accomplish(achievement: Achievement): void {
achievement.done = true
if (typeof achievement.prize === 'number') {
store.balance += achievement.prize
}
}
watch(
achievements,
() => {
localStorage.setItem(achievementsKey, JSON.stringify(achievements.value))
},
{ deep: true }
)
watch(missCount, () => {
/** Accomplish an achievement */
if (missCount.value >= 5) {
const achievement = achievements.value.find(
(item) => item.name === 'Miss 5 times'
)
if (achievement && !achievement.done) {
accomplish(achievement)
}
}
localStorage.setItem(missCountKey, JSON.stringify(missCount.value))
})
return { achievements, missCount, accomplish }
})
// Show a vue warn
// const achievementStore = useAchievementStore()
// watch(
// achievementStore,
// (state) => {
// localStorage.setItem('piniaState', JSON.stringify(state))
// },
// { deep: true }
// )

How to display data from firebase in vis.js timeline

I m using vis.js timeline and i want display date from firestore. It works when I type manually (look --> this.items), but does not work with firestore (look --> this.users).
I m using Vue framework.
<script>
export default {
data() {
return {
users: [],
items: [
{
id: '1',
content: 'London',
group: 'Mike',
start: '2021-12-20',
end: '2022-06-19',
},
],
}
},
async fetch() {
await this.loadPlaces()
},
methods: {
async loadPlaces() {
const querySnapshot = await getDocs(collection(db, 'places'))
querySnapshot.forEach((doc) => {
this.users.push({ id: doc.id, ...doc.data() })
})
this.$store.commit('places/setPlaces', this.users)
},
},
computed: {
places() {
return this.$store.state.places.places
},
},
mounted() {
let container = document.getElementById('visualization')
let options = {
moveable: true,
}
let timeline = new vis.Timeline(container)
timeline.setOptions(options)
timeline.setGroups(this.groups)
timeline.setItems(this.items)
},
}
</script>
I found a solution.
I just moved all code from mounted() to method loadPlaces (under this.$store.commit)
Save yourself trouble and use the vis datasets instead.
my pinia store in vue 3 looks like this.
import { defineStore } from 'pinia'
import { DataSet } from 'vis-data/esnext'
export const useVisData = defineStore('visData', {
state: () => ({
items: new DataSet([]),
groups: new DataSet([]),
selectedItems: [],
serializedGroupsAndItems: []
}),
actions: {
//Group actions
showAllGroups() {
this.groups.forEach(group => {
this.groups.updateOnly({ id: group.id, visible: true })
});
},
addGroup(group) {
this.groups.add(group)
},
hideGroup(group) {
this.groups.updateOnly({ id: group, visible: false })
},
//Item actions
addItem(item) {
this.items.add(item)
},
removeItem(item) {
this.items.remove(item)
},
setSelectedItems(items) {
this.selectedItems = items
},
//data add/remove
serializeData() {
this.serializedGroupsAndItems.push({
groups: JSON.stringify(this.groups.get()),
items: JSON.stringify(this.items.get())
})
},
loadSerializedData() {
this.clearGroupsAndItems()
this.serializedGroupsAndItems.forEach(data => {
this.addGroup(JSON.parse([data.groups]))
this.addItem(JSON.parse([data.items]))
})
},
//misc
clearGroupsAndItems() {
this.groups.clear()
this.items.clear()
}
},
getters: {
getHiddenGroups(state) {
return state.groups.get({
filter: (item) => {
return item.visible === false
}
})
}
}
})
Also remember to watch for changes in your options.
Might be better to wrap it in a vue component too. something like this.
this is what i did.
let timeline;
const visref = ref(null);
onMounted(async () => {
timeline = new Timeline(visref.value, props.items, props.groups, {...props.options, ...timelineOptions});
props.events.forEach(event => {
on(event, (properties) => {
// console.log(event, properties)
emits(`vis${event}`, properties);
});
});
})
<template>
<div ref="visref"></div>
</template>
then you can use it like so:
const timelineref = ref();
<Timeline
ref="timelineref"
:items="visStore.items"
:groups="visStore.groups"
:options="options"
/>
remember to expose the instance in your timeline component then you can call the functions using a ref like this.
timelineref.value.timeline.zoomOut(0.5)

Cannot assign to read only property 'state' of object '#<Object>'

I'm using Redux Toolkit and I'm having trouble in one of my actions. Here's relevant parts of my slice:
export const initialCookingSessionState = {
recipeInfo: null as RecipeInfo | null,
instructions: [] as Instruction[],
ingredients: [] as Ingredient[],
activeTimers: [] as CookingTimer[],
currentStepIndex: 0 as number,
stepTimers: [] as StepTimer[]
};
const cookingSessionSlice = createSlice({
name: 'session',
initialState: initialCookingSessionState,
reducers: {
startRecipe(state, { payload: recipe }: PayloadAction<Recipe>) {
const { info, instructions, ingredients } = recipe;
state.recipeInfo = info;
state.ingredients = [...ingredients];
state.instructions = [...instructions]
state.stepTimers = [];
state.instructions.forEach(({ timers }, stepIndex) => {
timers.forEach(timer =>
state.stepTimers.push({ ...timer, stepIndex, state: CookingTimerState.Pending })
)
})
},
incStep(state) { state.currentStepIndex++ },
decStep(state) { state.currentStepIndex-- },
startTimer(state, { payload: timer }: PayloadAction<StepTimer>) {
timer.state = CookingTimerState.Running
},
}
});
When I dispatch startTimer, I get the error:
Cannot assign to read only property 'state' of object '#'
There must be something about what is and isn't possible with Redux Toolkit's "Mutative State Changes" that I'm missing. It seems to me that my example isn't that different from theirs in the docs, but apparently I'm wrong about that. (the other actions work fine)
In case it's helpful, here are the models, which I think are pretty simple:
export class Recipe {
info: RecipeInfo = {
id: "",
title: ""
};
instructions: Instruction[] = [];
ingredients: Ingredient[] = []
}
export class Instruction {
timers: CookingTimer[] = [];
constructor(public text: string) {}
}
export class Ingredient {
id: string = "";
state: IngredientState = { done: false };
constructor(public text: string) {}
}
export class CookingTimer {
constructor(
public durationSec = 0,
public label = "") {}
}
export enum CookingTimerState {
Pending, Paused, Running, Done
}
export type StepTimer = {
state: CookingTimerState
durationSec: number
label: string
stepIndex: number
}

Angular 6 ngrx - how do I use multiple reducers with ShopModule.forRoot

Original post located at this dropbox link
Thanks to #rijin Rewrite of the code:
reducers/index2.ts:
import { ActionReducerMap } from "#ngrx/store";
import { userReducer, UserState } from "./user2.reducer";
import { cartReducer, CartState } from "./cart2.reducer";
interface AppState {
user: UserState;
cart: CartState;
}
export const reducers2: ActionReducerMap<AppState> = {
user: userReducer,
cart: cartReducer
};
Compilation error at user: userReducer:
Type '(appUserState: UserState, action: any) => User' is not assignable to type 'ActionReducer<UserState, Action>'.
Property 'user' is missing in type 'User' but required in type 'UserState'.
/reducers/user.ts:
export class User {
uName: string;
isAdmin: boolean;
ts: string;
loggedIn: boolean;
constructor(data: any) {
Object.assign(this, data);
}
}
/reducers/cart.ts:
export class Cart {
counter: number;
constructor(data: any) {
Object.assign(this, data);
}
}
/reducer/user2.reducer.ts:
import * as UserActions from "../actions/user.actions";
import { User } from "./user";
function mTstamp() {
let d = new Date();
let mMonth;
if (d.getMonth() < 10) {
mMonth = "0" + d.getMonth();
} else {
mMonth = d.getMonth();
}
let mDate;
if (d.getDate() < 10) {
mDate = "0" + d.getDate();
} else {
mDate = d.getDate();
}
let mHours;
if (d.getHours() < 10) {
mHours = "0" + d.getHours();
} else {
mHours = d.getHours();
}
let mMins;
if (d.getMinutes() < 10) {
mMins = "0" + d.getMinutes();
} else {
mMins = d.getMinutes();
}
let mSecs;
if (d.getSeconds() < 10) {
mSecs = "0" + d.getSeconds();
} else {
mSecs = d.getSeconds();
}
let mTimeStamp =
d.getFullYear() +
"-" +
mMonth +
"-" +
mDate +
" " +
mHours +
":" +
mMins +
":" +
mSecs;
console.log("mTimeStamp: ", mTimeStamp);
return mTimeStamp;
}
export interface UserState {
user: User;
}
const initialLoginState: UserState = {
user: new User({
uName: "Guest",
isAdmin: false,
ts: mTstamp(),
loggedIn: false
})
};
export function userReducer(appUserState = initialLoginState, action): User {
switch (action.type) {
case UserActions.ACTION_LOGOUT:
return {
...appUserState,
uName: "Guest",
isAdmin: false,
ts: mTstamp(),
loggedIn: false
};
case UserActions.ACTION_LOGIN:
return {
...appUserState,
uName: action.payload,
isAdmin: action.payload,
ts: action.payload,
loggedIn: action.payload
};
}
return appUserState;
}
Compilation error at return appUserState:
Type 'UserState' is missing the following properties from type 'User': uName, isAdmin, ts, loggedIn
/reducers/cart.reducer.ts:
import * as CartActions from "../actions/cart.actions";
import { Cart } from "./cart";
export interface CartState {
cart: Cart;
}
const initialCartState: CartState = {
cart: new Cart({
counter: 0
})
};
export function cartReducer(cartState = initialCartState, action): CartState {
switch (action.type) {
case CartActions.ACTION_DECREMENT:
return {
...cartState,
counter: action.payload
};
case CartActions.ACTION_INCREMENT:
return {
...cartState,
counter: action.payload
};
}
return cartState;
}
Compilation error at counter: action.payload:
Type '{ counter: any; cart: Cart; }' is not assignable to type 'CartState'.
Object literal may only specify known properties, and 'counter' does not exist in type 'CartState'.
Sorry but I can get past these errors. Please let me know what I can do to fix these issues
Combine and use a single ActionReducerMap for root.
Each state should be mapped to the corresponding reducer.
See stackblitz url : https://stackblitz.com/edit/angular-ngrx-tryout
// cart.reducer
export function cartReducer(cartState = initialCartState, action): CartState {
console.log('prev state: ', cartState);
switch (action.type) {
case CartActionTypes.ACTION_DECREMENT:
return {
...cartState, // no other properties, can be removed
cart: new Cart({ counter: action.payload.counter })
};
case CartActionTypes.ACTION_INCREMENT:
return {
...cartState, // no other properties, can be removed
cart: new Cart({ counter: action.payload.counter })
};
}
return cartState;
}
export const selectCartState = (state) => state.cartState;
export const selectCart = createSelector(selectCartState, (state) => state.cart);
// store/index.ts
import { ActionReducerMap } from "#ngrx/store";
import { userReducer, UserState } from "./user.reducer";
import { cartReducer, CartState } from "./cart.reducer";
interface AppState {
userState: UserState;
cartState: CartState;
}
export const reducers: ActionReducerMap<AppState> = {
userState: userReducer,
cartState: cartReducer
};
// and import
StoreModule.forRoot(reducers),

Redux immutable pattern

I use react with redux.
Action:
export const updateClicked = (id, section) => {
return {
type: actionTypes.UPDATE_CLICKED,
id,
section
};
};
Please advise the best way to immutable update property in nested array:
Reducer:
const initialState = {
updates: {
html: {
id: 'html',
label: 'HTML',
count: 0,
items: [
{
id: 1,
label: 'Header',
price: 10,
bought: false
},
{
id: 2,
label: 'Sidebar',
price: 50,
bought: false
}
]
}
}
};
My action:
action = {
id: 1,
bought: true
}
I want to update bought property inside items array. I.e.:
const updateClicked= (state, action) => {
const updateSections = state.updates[action.section].items;
const updatedItems = updateSections.map(el => {
if (el.id === action.id && !el.bought) {
el.bought = true;
}
return el;
});
//How to update state???
return {}
};
Will be glad if you explain 2 ways to do this:
With es6 spread operator
With some library (like immutability-helper)
Thanks!
With es6 spread operator:
export default (state = initialState, action) => {
if (action.type !== actionTypes.UPDATE_CLICKED) return state;
return {
...state,
updates: {
...state.updates,
html: {
...state.updates.html,
items: state.updates.html.items.map((item, idx) => idx === action.id
? {...item, bought: item.bought}
: item
)
}
}
}
};

Resources