I'm trying to 'stub' a method via testdoubleJS to do a unit test for this method (doing npm test). It is the first time I'm doing this, so it is still hard to understand for me.
For my attempt - shown below - I do get the error TypeError: mediaAddImagePoint.run is not a function
This is how my method I want to test looks like:
import { ValidatedMethod } from 'meteor/mdg:validated-method'
import { LoggedInMixin } from 'meteor/tunifight:loggedin-mixin'
import { Media } from '/imports/api/media/collection.js'
const mediaAddImagePoint = new ValidatedMethod({
name: 'media.point.add',
mixins: [LoggedInMixin],
checkLoggedInError: { error: 'notLogged' },
validate: null,
run ({ id, x, y }) {
Media.update(
{ _id: id },
{
$push: {
'meta.points': {
id: Random.id(),
x,
y
}
}
}
)
}
})
And this is how I'm trying to test this method via testdouble:
import { expect } from 'chai'
import td from 'testdouble'
describe('media.point.add', function () {
describe('mediaAddImagePoint', function () {
let Media = td.object(['update'])
let ValidatedMethod = td.function()
let LoggedInMixin = td.function()
let mediaAddImagePoint
beforeEach(function () {
td.replace('meteor/mdg:validated-method', { ValidatedMethod })
td.replace('meteor/tunifight:loggedin-mixin', { LoggedInMixin })
td.replace('/imports/api/media/collection.js', { Media })
mediaAddImagePoint = require('../../imports/api/media/methods/imagePoints.js').mediaAddImagePoint
})
afterEach(function () {
td.reset()
})
it('should add image point', function () {
const query = { id: 'sampleID', x: 12, y: 34 }
mediaAddImagePoint.run(query)
td.verify(Media.update(query))
})
})
})
Related
first of all I am using the Mockjs to simulate the backend data:
{
url: "/mockApi/system",
method: "get",
timeout: 500,
statusCode: 200,
response: { //
status: 200,
message: 'ok',
data: {
'onlineStatus|3': [{
'statusId': '#integer(1,3)',
'onlineStatusText': '#ctitle(3)',
'onlineStatusIcon': Random.image('20*20'),
'createTime': '#datetime'
}],
'websiteInfo': [{
'id|+1': 1,
}]
}
}
}
the data structure would be: https://imgur.com/a/7FqvVTK
and I retrieve this mock data in Pinia store:
import axios from "axios"
import { defineStore } from "pinia"
export const useSystem = defineStore('System', {
state: () => {
return {
systemConfig: {
onlineStatus: [],
},
}
},
actions: {
getSystemConfig() {
const axiosInstance = axios.interceptors.request.use(function (config) {
// Do something before request is sent
config.baseURL = '/mockApi'
return config
}, function (error) {
// Do something with request error
return Promise.reject(error);
})
axios.get('/system/').then(res => {
this.systemConfig.onlineStatus = res.data.data.onlineStatus
})
// console.log(res.data.data.onlineStatus)
axios.interceptors.request.eject(axiosInstance)
}
}
})
I use this store in the parent component Profile.vue:
export default {
setup() {
const systemConfigStore = useSystem()
systemConfigStore.getSystemConfig()
const { systemConfig } = storeToRefs(systemConfigStore)
return {
systemConfig,
}
},
computed: {
getUserOnlineStatusIndex() {
return this.userData.onlineStatus//this would be 1-3 int.
},
getUserOnlineStatus() {
return this.systemConfig.onlineStatus
},
showUserOnlineStatusText() {
return this.getUserOnlineStatus[this.getUserOnlineStatusIndex - 1]
},
},
components: {UserOnlineStatus }
}
template in Profile.vue I import the child component userOnlineStatus.vue
<UserOnlineStatus :userCurrentOnlineStatus="userData.onlineStatus">
{{ showUserOnlineStatusText }}
</UserOnlineStatus>
here is what I have got https://imgur.com/fq33uL8
but I only want to get the onlineStatusText property of the returned object, so I change the computed code in the parent component Profile.vue:
export default {
setup() {
const systemConfigStore = useSystem()
systemConfigStore.getSystemConfig()
const { systemConfig } = storeToRefs(systemConfigStore)
return {
systemConfig,
}
},
computed: {
getUserOnlineStatusIndex() {
return this.userData.onlineStatus//this would be 1-3 int.
},
getUserOnlineStatus() {
return this.systemConfig.onlineStatus
},
showUserOnlineStatusText() {
return this.getUserOnlineStatus[this.getUserOnlineStatusIndex - 1]['onlineStatusText']//👀I chage it here!
},
},
components: {UserOnlineStatus }
}
but I will get the error in the console and it doesn't work:
https://imgur.com/Gb68Slk
what should I do if I just want to display the specific propery of the retrived data?
I am out of my wits...
I have tried move the store function to the child components, but get the same result.
and I google this issue for two days, nothing found.
Maybe it's because of I was trying to read the value that the Profile.vue hasn't retrieved yet?
in this case, how could I make sure that I have got all the value ready before the page rendered in vue3? Or can I watch this specific property changed, then go on rendering the page?
every UX that has data is coming from remote source (async data) should has spinner or skeleton.
you can use the optional chaining for safe access (if no time to await):
return this.getUserOnlineStatus?.[this.getUserOnlineStatusIndex - 1]?.['onlineStatusText']
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
I have the following unit test for my Vue component:
import { shallowMount } from '#vue/test-utils';
import OrganizationChildren from './OrganizationChildren.vue';
describe('OrganizationChildren', () => {
beforeEach(() => {
jest.resetModules();
});
it('passes', () => {
jest.doMock('#/adonis-api', () => {
return {
organization: {
family(id) {
return {
descendants: [],
};
},
},
};
});
const wrapper = shallowMount(OrganizationChildren, {
propsData: {
org: {
id: 1,
},
},
});
});
});
And in the Vue component, it does import { organization } from '#/adonis-api';. I'm temporarily just console.logging the imported organization object, to make sure it's correct. But I can see that it's not using the mocked version that I've specified. What am I doing wrong? My goal is to mock the family method differently in each it() block to test what happens if descendants is empty, if it contains 5 items, 100 items, etc.
Solved! I had a couple issues, as it turns out:
Not properly mocking #/adonis-api. I should mention that it only mocks stuff at the top level, so I had to use a factory function in jest.mock (see below).
I needed an await flushPromises() to allow the template to re-render after its created() method evaluated my mock function and stored the result in this.children.
Full test:
import { shallowMount, config } from '#vue/test-utils';
import flushPromises from 'flush-promises';
import OrganizationChildren from './OrganizationChildren.vue';
import { organization } from '#/adonis-api';
jest.mock('#/adonis-api', () => ({
organization: {
family: jest.fn(),
},
}));
describe('OrganizationChildren', () => {
config.stubs = {
'el-tag': true,
};
it('shows nothing when there are no children', async () => {
organization.family.mockResolvedValueOnce({
descendants: [],
});
const wrapper = shallowMount(OrganizationChildren, {
propsData: {
org: {
id: 1,
},
},
});
await flushPromises();
const h4 = wrapper.find('h4');
expect(h4.exists()).toBe(false);
});
it('shows children when provided', async () => {
organization.family.mockResolvedValueOnce({
descendants: [{ name: 'One' }, { name: 'Two' }],
});
const wrapper = shallowMount(OrganizationChildren, {
propsData: {
org: {
id: 1,
},
},
});
await flushPromises();
const h4 = wrapper.find('h4');
const list = wrapper.findAll('el-tag-stub');
expect(h4.exists()).toBe(true);
expect(list.length).toBe(2);
expect(list.at(0).text()).toBe('One');
expect(list.at(1).text()).toBe('Two');
});
});
I'm trying to write a unit test for an init function and I'm getting an error where I am calling collectionReport.init() in the test....
TypeError: undefined is not an object
This is the code I am trying to test...
class CollectionsReport {
constructor({ editCollectionsId, hasCollections}) {
this.editCollectionsId = editCollectionsId;
this.hasCollections = hasCollections
}
init({ id, name }) {
this.id = id;
this.name = name;
// need to test this
if (this.hasCollections) {
this.collection = this.collections.find(c => c.staticId === 'CAR-COLLECTION');
}
}
And this is my test so far
describe('CollectionsReport', () => {
const collectionArgs = {
editCollectionsId: jasmine.createSpy(),
hasCollections: false,
};
const collections = [
{
id: 1,
name: 'foo',
staticId: 'CAR-COLLECTIONS',
},
{
id: 2,
name: 'bar',
staticId: 'TRUCK-COLLECTIONS',
},
];
let collectionReport;
beforeEach(() => {
collectionReport = new CollectionsReport(collectionArgs);
});
describe('.init()', () => {
it('should test hasCollections', () => {
collectionReport.init();
//test this.hasCollections here
});
});
});
I'm sure its a mess, so please comment on how to fix and improve it.
Not sure what is the purpose of the CollectionsReport class, but maybe this will lead you to the right direction:
class CollectionsReport {
constructor({ editCollectionsId, hasCollections}) {
this.editCollectionsId = editCollectionsId
this.hasCollections = hasCollections
}
init({ collections, staticId }) {
this.hasCollections = !!collections.find(c => c.staticId === staticId)
}
}
describe('CollectionsReport', () => {
const collectionArgs = {
editCollectionsId: jasmine.createSpy(), // Not really using it
hasCollections: false
}
const collections = [
{
id: 1,
name: 'foo',
staticId: 'CAR-COLLECTIONS'
}, {
id: 2,
name: 'bar',
staticId: 'TRUCK-COLLECTIONS'
}
]
describe('.init()', () => {
let collectionReport
beforeEach(() => {
collectionReport = new CollectionsReport(collectionArgs)
})
it('should test hasCollections', () => {
collectionReport.init({ collections, staticId: 'CAR-COLLECTIONS' })
expect(collectionReport.hasCollections).toBe(true)
})
it('should test hasCollections', () => {
collectionReport.init({ collections, staticId: 'SOMETHING-ELSE' })
expect(collectionReport.hasCollections).toBe(false)
})
})
})
I am trying to set up a mutation with modified code from the Relay Todo example.
When I try to compile I get the following error:
-- GraphQL Validation Error -- AddCampaignMutation --
File: /Users/me/docker/relay/examples/todo/js/mutations/AddCampaignMutation.js
Error: Cannot query field "addCampaign" on type "Mutation".
Source:
>
> mutation AddCampaignMutation {addCampaign}
> ^^^
-- GraphQL Validation Error -- AddCampaignMutation --
File: /Users/me/docker/relay/examples/todo/js/mutations/AddCampaignMutation.js
Error: Unknown type "AddCampaignPayload". Did you mean "AddTodoPayload" or "RenameTodoPayload"?
Source:
>
> fragment AddCampaignMutationRelayQL on AddCampaignPayload #relay(pattern: true) {
> ^^
I have duplicated the Todo code so I don't know why the Todo mutation is working correctly but my new Campaign test isn't.
This is my database.js file, I have removed the Todo related items to make the document easier to read:
export class Campaign {}
export class User {}
// Mock authenticated ID
const VIEWER_ID = 'me';
// Mock user data
const viewer = new User();
viewer.id = VIEWER_ID;
const usersById = {
[VIEWER_ID]: viewer,
};
// Mock campaign data
const campaignsById = {};
const campaignIdsByUser = {
[VIEWER_ID]: [],
};
let nextCampaignId = 0;
addCampaign('Campaign1');
addCampaign('Campaign2');
addCampaign('Campaign3');
addCampaign('Campaign4');
export function addCampaign(text) {
const campaign = new Campaign();
//campaign.complete = !!complete;
campaign.id = `${nextCampaignId++}`;
campaign.text = text;
campaignsById[campaign.id] = campaign;
campaignIdsByUser[VIEWER_ID].push(campaign.id);
return campaign.id;
}
export function getCampaign(id) {
return campaignsById[id];
}
export function getCampaigns(status = 'any') {
const campaigns = campaignIdsByUser[VIEWER_ID].map(id => campaignsById[id]);
if (status === 'any') {
return campaigns;
}
return campaigns.filter(campaign => campaign.complete === (status === 'completed'));
}
This is my schema.js file, again I have removed the Todo related items to make the document easier to read:
import {
GraphQLBoolean,
GraphQLID,
GraphQLInt,
GraphQLList,
GraphQLNonNull,
GraphQLObjectType,
GraphQLSchema,
GraphQLString,
} from 'graphql';
import {
connectionArgs,
connectionDefinitions,
connectionFromArray,
cursorForObjectInConnection,
fromGlobalId,
globalIdField,
mutationWithClientMutationId,
nodeDefinitions,
toGlobalId,
} from 'graphql-relay';
import {
Campaign,
addCampaign,
getCampaign,
getCampaigns,
User,
getViewer,
} from './database';
const {nodeInterface, nodeField} = nodeDefinitions(
(globalId) => {
const {type, id} = fromGlobalId(globalId);
if (type === 'User') {
return getUser(id);
} else if (type === 'Campaign') {
return getCampaign(id);
}
return null;
},
(obj) => {
if (obj instanceof User) {
return GraphQLUser;
} else if (obj instanceof Campaign) {
return GraphQLCampaign;
}
return null;
}
);
/**
* Define your own connection types here
*/
const GraphQLAddCampaignMutation = mutationWithClientMutationId({
name: 'AddCampaign',
inputFields: {
text: { type: new GraphQLNonNull(GraphQLString) },
},
outputFields: {
campaignEdge: {
type: GraphQLCampaignEdge,
resolve: ({localCampaignId}) => {
const campaign = getCampaign(localCampaignId);
return {
cursor: cursorForObjectInConnection(getCampaigns(), campaign),
node: campaign,
};
},
},
viewer: {
type: GraphQLUser,
resolve: () => getViewer(),
},
},
mutateAndGetPayload: ({text}) => {
const localCampaignId = addCampaign(text);
return {localCampaignId};
},
});
const GraphQLCampaign = new GraphQLObjectType({
name: 'Campaign',
description: 'Campaign integrated in our starter kit',
fields: () => ({
id: globalIdField('Campaign'),
text: {
type: GraphQLString,
description: 'Name of the campaign',
resolve: (obj) => obj.text,
}
}),
interfaces: [nodeInterface]
});
const {
connectionType: CampaignsConnection,
edgeType: GraphQLCampaignEdge,
} = connectionDefinitions({
name: 'Campaign',
nodeType: GraphQLCampaign,
});
const GraphQLUser = new GraphQLObjectType({
name: 'User',
fields: {
id: globalIdField('User'),
campaigns: {
type: CampaignsConnection,
args: {
...connectionArgs,
},
resolve: (obj, {...args}) =>
connectionFromArray(getCampaigns(), args),
},
totalCount: {
type: GraphQLInt,
resolve: () => getTodos().length,
},
completedCount: {
type: GraphQLInt,
resolve: () => getTodos('completed').length,
},
},
interfaces: [nodeInterface],
});
const Root = new GraphQLObjectType({
name: 'Root',
fields: {
viewer: {
type: GraphQLUser,
resolve: () => getViewer(),
},
node: nodeField,
},
});
This is my AddCampaignMutation.js file:
import Relay from 'react-relay';
export default class AddCampaignMutation extends Relay.Mutation {
static fragments = {
viewer: () => Relay.QL`
fragment on User {
id,
totalCount,
}
`,
};
getMutation() {
console.log('getMutation');
return Relay.QL`mutation{addCampaign}`;
}
getFatQuery() {
console.log('getFatQuery');
return Relay.QL`
fragment on AddCampaignPayload #relay(pattern: true) {
campaignEdge,
viewer {
campaigns,
},
}
`;
}
getConfigs() {
console.log('getConfigs');
return [{
type: 'RANGE_ADD',
parentName: 'viewer',
parentID: this.props.viewer.id,
connectionName: 'campaigns',
edgeName: 'campaignEdge',
rangeBehaviors: ({orderby}) => {
if (orderby === 'newest') {
return 'prepend';
} else {
return 'append';
}
},
//rangeBehaviors: ({status}) => {
// if (status === 'completed') {
// return 'ignore';
// } else {
// return 'append';
// }
//},
}];
}
getVariables() {
console.log('getVariables');
return {
text: this.props.text,
};
}
getOptimisticResponse() {
console.log('getOptimisticResponse');
return {
// FIXME: totalCount gets updated optimistically, but this edge does not
// get added until the server responds
campaignEdge: {
node: {
text: this.props.text,
},
},
viewer: {
id: this.props.viewer.id,
totalCount: this.props.viewer.totalCount + 1,
},
};
}
}
And finally this is the app file that contains my text input and the call to AddCampaignMutation:
import AddTodoMutation from '../mutations/AddTodoMutation';
import AddCampaignMutation from '../mutations/AddCampaignMutation';
import TodoListFooter from './TodoListFooter';
import TodoTextInput from './TodoTextInput';
import React from 'react';
import Relay from 'react-relay';
class TodoApp extends React.Component {
_handleTextInputSave = (text) => {
debugger;
this.props.relay.commitUpdate(
new AddTodoMutation({text, viewer: this.props.viewer})
);
};
_campaignHandleTextInputSave = (text) => {
debugger;
this.props.relay.commitUpdate(
new AddCampaignMutation({text, viewer: this.props.viewer})
);
};
render() {
const hasTodos = this.props.viewer.totalCount > 0;
return (
<div>
<section className="todoapp">
<header className="header">
<TodoTextInput
autoFocus={true}
className="new-campaign"
onSave={this._campaignHandleTextInputSave}
placeholder="Campaign name"
/>
<h1>
todos
</h1>
<TodoTextInput
autoFocus={true}
className="new-todo"
onSave={this._handleTextInputSave}
placeholder="What needs to be done?"
/>
</header>
{this.props.children}
{hasTodos &&
<TodoListFooter
todos={this.props.viewer.todos}
viewer={this.props.viewer}
/>
}
</section>
</div>
);
}
}
export default Relay.createContainer(TodoApp, {
fragments: {
viewer: () => Relay.QL`
fragment on User {
totalCount,
${AddTodoMutation.getFragment('viewer')},
${AddCampaignMutation.getFragment('viewer')},
${TodoListFooter.getFragment('viewer')},
}
`,
},
});
Well I feel a bit silly but I found out what the problem was. I didn't realise that my schema.json file was not updating!
If anyone has a similar problem make sure that the schema.json file is up to date by running the following command to rebuild it:
npm run-script update-schema