I need to use an alias I got after intercepting a cy.wait(#..) in other tests (in different files), but I am not sure how I could do it.
However, seems like it is possible if save the data in plugins/config space and then fetch using cy.task But I am not sure how to do it. Maybe someone could help me?
I am intercepting this request
cy.intercept('POST', '**/quotes').as('abccreateQuote')
and as well I am getting the quote Id that comes in the response body.
cy.wait('#createQuote').then(({ response }) => {
if (response?.statusCode === 201) {
cy.wrap(response.body.data.id).as('abcQuoteId')
}
})
I need to use this abcQuoteId alias in different tests and located it in different files.
to visit the page
cy.visit(`quotes/${abcQuoteId}/show`)
A task would do it, but less code if you write to a fixture file
cy.wait('#createQuote').then(({ response }) => {
if (response?.statusCode === 201) {
cy.writeFile('./cypress/fixtures/abcQuoteId.json', {abcQuoteId: response.body.data.id})
}
})
Advantage over task, you can check the fixture file manually in case of typos, should look like this:
{
"abcQuoteId": 123
}
and use it like this
cy.fixture('abcQuoteId.json')
.then(fixture => {
const url = `quotes/${fixture.abcQuoteId}/show`
console.log(url) // quotes/123/show
cy.visit(url)
})
This will allow you to create infinite stores with values that will be available all the time while Cypress is running.
Each Store with it's values is available between all spec files.
Usage:
Saving some value:
// spec.file1.js
cy.wait('#createQuote').then(({ response }) => {
if (response?.statusCode === 201) {
cy.task('setItem', {
storeId: 'Global',
item: {
name: 'createQuoteResponse',
value: response.body.data.id,
},
})
}
})
Getting the above value inside another spec file:
// spec.file2.js
cy.task('getItem', {
storeId: 'Global',
item: {
name: 'createQuoteResponse',
},
}).then((item) => {
console.log(item) // Your response code
})
How to implement?
Edit:
Install cypress-store-plugin
npm install #optimumqa/cypress-store
End of Edit
May seem like a lot of code, and it is. But once setup, you won't have to modify or worry about it.
Create a ./cypress/plugins/Store.js file and paste following code:
// `./cypress/plugins/Store.js`
const StoreHelper = require('../support/Store')
const stores = {}
class Store {
constructor(on, config, pluginConfig) {
this.CONFIG = {
...{
logging: false,
},
...(pluginConfig || {}),
}
this.init(on, config, pluginConfig)
}
init(on, config, pluginConfig) {
on('task', {
/**
* #description - Store items to specific store. If store does not exist, it will be created
*
* #param {String} data.id - Store id
* #param {Object} data.item - Object containing item info
* #param {String} data.item.name - Item name
* #param {Any} data.item.value - Item value
*
* #returns {Store.Item|Null}
*/
setItem: (data) => {
let store = stores[data.storeId]
if (!store) {
stores[data.storeId] = new StoreHelper()
store = stores[data.storeId]
}
return store.setItem(data.item) || null
},
/**
* #description - Get items from specific store
*
* #param {String} data.id - Store id
* #param {Object} data.item - Object containing item info
* #param {String} data.item.name - Item name
*
* #returns {Store.Item|Null}
*/
getItem: (data) => {
const store = stores[data.storeId]
if (store) {
return store.getItem(data.item)
}
return null
},
})
}
}
module.exports = Store
Then create one more other file ./cypress/support/Store.js and paste following code in it:
// `./cypress/support/Store.js`
class Store {
constructor() {
/** #type {object} */
this.items = {}
return this
}
getItem(data = {}) {
return this.items[data.name] || null
}
setItem(data = {}) {
this.items[data.name] = new Item(data)
return this.items[data.name]
}
}
class Item {
constructor(data = {}) {
this.name = data.name || ''
this.value = data.value || undefined
return this
}
}
module.exports = Store
Cypress < v10
Inside your ./cypress/plugins/index.js require the plugin like this:
You need to require the Store file from plugins/.
// `./cypress/plugins/index.js`
module.exports = (on, config) => {
require('./Store')(on, config)
}
Cypress >= v10
// `./cypress.config.js`
const { defineConfig } = require('cypress')
const Store = require('./cypress/plugins/Store')
module.exports = defineConfig({
e2e: {
setupNodeEvents(on, config) {
Store(on, config)
},
},
})
This is by default enabled in the cypress-boilerplate
Related
How can I pass a variable between plugins in Rollup?
What I've tried:
// plugin-a.js
const pluginA = () => {
return {
name: 'pluginA',
async options(options) {
options.define = options.define || {};
options.define['foo'] = 'bar';
}
}
}
// plugin-b.js
const pluginB = (options = {}) => {
return {
name: 'pluginB',
buildStart: async (options) => {
console.log(options)
}
}
}
I'm getting a warning:
(!) You have passed an unrecognized option
Unknown input options: define. Allowed options: acorn, acornInjectPlugins, cache, context, experimentalCacheExpiry, external, inlineDynamicImports, input, makeAbsoluteExternalsRelative, manualChunks, maxParallelFileOps, maxParallelFileReads, moduleContext, onwarn, perf, plugins, preserveEntrySignatures, preserveModules, preserveSymlinks, shimMissingExports, strictDeprecations, treeshake, watch
It seems passing data should be done by what Rollup refers to as Direct plugin communication. This is working for me. I feel this is very hard coupled though.
function parentPlugin() {
return {
name: 'parent',
api: {
//...methods and properties exposed for other plugins
doSomething(...args) {
// do something interesting
}
}
// ...plugin hooks
};
}
function dependentPlugin() {
let parentApi;
return {
name: 'dependent',
buildStart({ plugins }) {
const parentName = 'parent';
const parentPlugin = plugins.find(plugin => plugin.name === parentName);
if (!parentPlugin) {
// or handle this silently if it is optional
throw new Error(`This plugin depends on the "${parentName}" plugin.`);
}
// now you can access the API methods in subsequent hooks
parentApi = parentPlugin.api;
},
transform(code, id) {
if (thereIsAReasonToDoSomething(id)) {
parentApi.doSomething(id);
}
}
};
}
There's also Custom module meta-data, however when I read the meta I always get null.
Doing a little experiment to reduce code smell and in that effort I'm trying to write a typed function that would allow me to define my endpoints in one location within my application. The thought is, if I ever change or add routes, I can define them in one area which is then dispersed across the application.
The example below is where I'm at currently. While it does works, it's still a bit messy and not exactly DRY (repeating enum value as the path in the getApiAddress() function.)
Any thoughts how I can create a function that allows me to add n[] unique id:string params while ago getting rid of the {target: '', path: ''} and just have one value that can accept those id string values?
const apiAddress =
process.env === 'PRODUCTION'
? 'https://some-address.com'
: 'http://localhost:5000';
export enum targetsEnum {
transcribe = '/services/transcribe',
transcribeId = '/services/transcribe/:id',
}
export type ApiTargets = targetsEnum.transcribe | targetsEnum.transcribeId;
export const getApiAddress = (target: ApiTargets, id?: string[]): string => {
const endpoints = [
{ target: targetsEnum.transcribe, path: '/services/transcribe' },
{
target: targetsEnum.transcribeId,
path: `/services/transcribe/${id[0]}`,
},
];
const matchingEndpoint = endpoints.find(
endpoint => endpoint.target === target
);
return `${apiAddress}${matchingEndpoint.path}`;
};
continued to work on this some more.
With the approach below, you can defined you endpoints in one location (targetsEnum) and pass that in anywhere in your application. Then, the second argument is an array of strings. They are injected into the enum's value using injectPathVariables().
example:
const endpoint = getApiAddress(ApiEndpointsEnum.youtubeId,['${youtubeId}']);
const apiAddress =
process.env === 'PRODUCTION'
? 'https://some-endpoint.com'
: 'http://localhost:5000';
export enum testApiEndpointsEnum {
test = '/test',
testId = '/test/:id',
testIdtestId = '/test/:id/test/:id',
}
export enum ApiEndpointsEnum {
youtube = '/services/youtube',
youtubeId = '/services/youtube/:id',
transcribe = '/services/transcribe',
transcribeId = '/services/transcribe/:id',
}
export type ApiTargets =
| testApiEndpointsEnum.test
| testApiEndpointsEnum.testId
| testApiEndpointsEnum.testIdtestId
// * ^ the TWO above this comment are for testing.
| ApiEndpointsEnum.youtube
| ApiEndpointsEnum.youtubeId
| ApiEndpointsEnum.transcribe
| ApiEndpointsEnum.transcribeId
const injectPathVariables = (
target: ApiTargets[1],
ids: string[]
): string => {
let newStr = target;
ids.forEach(i => {
newStr = newStr.replace(':id', i);
});
return newStr;
};
const getApiAddress = (target: ApiTargets, ids?: string[]): string => {
// * no ids provided + confirmation target doesn't need ids.
if (ids === undefined && target.includes(':id') === false) {
return `${apiAddress}${target}`;
}
// * no ids provided, but target does need ids.
if (
(ids === undefined || ids.length === 0) &&
target.includes(':id') === true
) {
return `${apiAddress}`;
}
// * ids have been provided.
const injectedOutput = injectPathVariables(target, ids);
if (injectedOutput.includes(':id')) {
// * but not enough ids were provided for complete injection.
return apiAddress;
}
// * sufficient ids have been provided
return `${apiAddress}${injectedOutput}`;
};
I'm trying to make a relationship between two tables.
My relationship is belongsToMany between user => user_bet_match => matchs.
A user can have many user_bet_match and matchs can have many user_bet_match.
My database migration is :
Matchs table :
this.create('matchs', (table) => {
table.increments()
table.integer('round_id').unsigned()
table.integer('league_id').unsigned()
table.integer('hometeam_id').unsigned()
table.integer('awayteam_id').unsigned()
table.string('final_score_hometeam_goal')
table.string('final_score_awayteam_goal')
table.string('halftime_score_hometeam_goal')
table.string('halftime_score_awayteam_goal')
table.date('event_date')
table.integer('event_timestamp')
table.boolean('betailable').defaultTo(false)
table.boolean('is_finish').defaultTo(false)
table.timestamps()
})
User table:
this.create('users', (table) => {
table.increments()
table.string('username', 80).notNullable().unique()
table.string('email', 254).notNullable().unique()
table.string('password', 60).notNullable()
table.timestamps()
})
user_bet_match table :
this.create('user_bet_match', (table) => {
table.increments()
table.integer('user_id').unsigned()
table.integer('match_id').unsigned()
table.string('choice').notNullable()
table.timestamps()
})
My user model:
class User extends Model {
static boot () {
super.boot()
this.addHook('beforeSave', async (userInstance) => {
if (userInstance.dirty.password) {
userInstance.password = await Hash.make(userInstance.password)
}
})
}
tokens () {
return this.hasMany('App/Models/Token')
}
match () {
return this.belongsToMany('App/Models/Match').pivotTable('user_bet_match')
}
My user bet match module:
'use strict'
/** #type {typeof import('#adonisjs/lucid/src/Lucid/Model')} */
const Model = use('Model')
const Database = use('Database')
class UserBetMatch extends Model {
user () {
return this.hasOne('App/Models/User')
}
matchs () {
return this.hasOne('App/Models/Match')
}
}
module.exports = UserBetMatch
And my matchs module:
'use strict'
/** #type {typeof import('#adonisjs/lucid/src/Lucid/Model')} */
const Model = use('Model')
class Match extends Model {
userbetmatchs () {
return this.hasMany('App/Models/UserBetMatch')
}
}
module.exports = Match
And when I make :
let k = user.match().fetch()
With this relation :
match () {
return this.belongsToMany('App/Models/Match').pivotTable('user_bet_match')
}
It's returning me sqlMessage: "Table 'bo7jjjccwliucibms5pf.matches' doesn't exist"
But I never mention of a table "matches"..
I don't know why..
I noticed that you changed the name of the tables in the migration (by default with adonis cli : matches; user_bet_matches)
Try to use this in your models:
static get table () {
return 'matchs' // Your table name
}
^ https://adonisjs.com/docs/4.0/lucid#_table
Lucid does not take into account the migrations.
It's therefore necessary to specify the name of the table if it is not the default one (with adonis cli).
Don't hesitate to tell me if it's not fair.
Here is the service that I am trying to test:
#Injectable()
export class BomRevisiosnsService {
constructor(
private baseService: BaseService,
private appConstants: AppConstants,
private dmConstants: DMConstants
) { }
public getRevisionsData(): any {
var itemId = this.appConstants.userPreferences.modelData['basicDetails']['itemId'];
let url = this.dmConstants.URLs.GETBOMREVISIONS + itemId + "/GetRevisionsAsync";
let headers = {
"Content-Type": "application/json",
UserExecutionContext: JSON.stringify(this.appConstants.userPreferences.UserBasicDetails),
}
if (itemId != null || itemId != undefined) {
return this.baseService.getData(url, headers).map(response => {
return response;
});
}
}
}
spec file
describe('bom-revisions.service ',()=>{
let bomRevisiosnsService:BomRevisiosnsService;
let baseService: BaseService;
let appConstants: AppConstants;
let dmConstants: DMConstants;
beforeEach(()=>{
baseService=new BaseService(null,null);
appConstants=null;
dmConstants=null;
bomRevisiosnsService=new BomRevisiosnsService(baseService,appConstants,dmConstants);
});
it('getRevisionsData() call base service getData()',()=>{
let spy = spyOn(baseService, 'getData').and.returnValue(Observable.of())
bomRevisiosnsService.getRevisionsData();
expect(baseService.getData).toHaveBeenCalled();
});
})
Error: TypeError: Cannot read property 'userPreferences' of null
I believe I need to provide some mock value for this.appConstants.userPreferences.modelData['basicDetails']['itemId'];
and this.dmConstants.URLs.GETBOMREVISIONS + itemId + "/GetRevisionsAsync";
Yes, indeed, you need to provide a valid value for appConstants and dmConstants because the call to bomRevisiosnsService.getRevisionsData() uses that information internally.
So, instead of assigning null to appConstants and dmConstants, you could create an objects with some valid data, like this:
appConstants = {
userPreferences: {
modelData: {
basicDetails: {
itemId: 3 // some other valid value here is fine
}
},
UserBasicDetails: {
// some valid values here, maybe
}
}
};
dmConstants = {
URLs: {
GETBOMREVISIONS: 'revisions' // or just some valid value according to the use case
}
};
And the same goes to baseService.
In general, you need to create valid stub, mock, etc for all object, services, etc that are used internally by the service you're testing.
I am currently writing tests for some services using karma with jasmine, and I was wondering if I had to mock a service's service dependency that uses $http, as described below.
PS: I'm already using $httpBackend to mock any GET request, and I plan on using $httpBackend.expect* if I don't mock the service ApiProvider
This is the service I am testing
.factory('CRUDService', ['ApiProvider', function (ApiProvider) {
'use strict';
var CRUD = function CRUD(modelName) {
this.getModelName = function () {
return modelName;
};
},
overridableMethods = {
save: null
};
CRUD.prototype = {
save: function () {
// ABSTRACT
},
/**
* Deletes instance from id property
* #return http promise
*/
remove: function () {
return ApiProvider.delete(this.getModelName(), this.id);
}
};
return {
/**
* Function creating a class extending CRUD
* #param {string} modelName
* #param {Object} methods an object with methods to override, ex: save
* return {classD} the extended class
*/
build: function (modelName, methods) {
var key,
Model = function () {
};
// Class extending CRUD
Model.prototype = new CRUD(modelName);
// Apply override on methods allowed for override
for (key in methods) {
if (key in overridableMethods &&
typeof methods[key] === 'function') {
Model.prototype[key] = methods[key];
}
}
/**
* Static method
* Gets an entity of a model
* #param {Object} config #see ApiProvider config
* #return {CRUD} the reference to the entity
*/
Model.get = function (config, success) {
var entity = new Model();
ApiProvider.get(modelName, config)
.success(function (data) {
angular.extend(entity, data);
if (success) {
success();
}
});
return entity;
};
/**
* Static method
* Gets entities of a model
* #param {Object} config #see ApiProvider config
* #return {CRUD[]} the reference to the entity
*/
Model.query = function (config, success) {
var entities = [];
ApiProvider.get(modelName, config)
.success(function (data) {
data.map(function (model) {
var entity = new Model();
angular.extend(entity, model);
return entity;
});
Array.prototype.push.apply(entities, data);
if (success) {
success();
}
});
return entities;
};
return Model;
},
// Direct link to ApiProvider.post method
post: ApiProvider.post,
// Direct link to ApiProvider.put method
put: ApiProvider.put
};
}]);
And this is the service's service dependency ApiProvider
.service('ApiProvider', function ($http) {
/**
* Private
* #param {string}
* #param {object}
* #return {string} Example: /service/[config.id[/config.relatedModel], /?config.params.key1=config.params.value1&config.params.key2=config.params.value2]
*/
var buildUrl = function (service, config) {
var push = Array.prototype.push,
url = [apiRoot, service],
params = [],
param = null;
// if a key id is defined, we want to target a specific resource
if ('id' in config) {
push.apply(url, ['/', config.id]);
// a related model might be defined for this specific resource
if ('relatedModel' in config) {
push.apply(url, ['/', config.relatedModel]);
}
}
// Build query string parameters
// Please note that you can use both an array or a string for each param
// Example as an array:
// {
// queryString: {
// fields: ['field1', 'field2']
// }
// }
// Example as a string
// {
// queryString: {
// fields: 'field1,field2'
// }
// }
if ('queryString' in config) {
// loop through each key in config.params
for (paramName in config.queryString) {
// this gives us something like [my_key]=[my_value]
// and we push that string in params array
push.call(params, [paramName, '=', config.queryString[paramName]].join(''));
}
// now that all params are in an array we glue it to separate them
// so that it looks like
// ?[my_first_key]=[my_first_value]&[my_second_key]=[my_second_value]
push.apply(url, ['?', params.join('&')]);
}
return url.join('');
},
request = function (method, url, methodSpecificArgs) {
trace({
method: method,
url: url,
methodSpecificArgs: methodSpecificArgs
}, 'ApiProvider request');
return $http[method].apply($http, [url].concat(methodSpecificArgs));
},
methods = {
'get': function (url, config) {
config.cache = false;
return request('get', url, [config]);
},
'post': function (url, data, config) {
config.cache = false;
return request('post', url, [data, config]);
},
'put': function (url, data, config) {
config.cache = false;
return request('put', url, [data, config]);
},
'delete': function (url, config) {
config.cache = false;
return request('delete', url, [config]);
}
};
return {
'get': function (service, config) {
config = config || {};
return methods.get(buildUrl(service, config), config);
},
'post': function (service, data, config) {
config = config || {};
return methods.post(buildUrl(service, config), data, config);
},
'put': function (service, data, config) {
config = config || {};
return methods.put(buildUrl(service, config), data, config);
},
'delete': function (service, config) {
config = config || {};
return methods.delete(buildUrl(service, config), config);
}
};
});
Finally this is how I tested the CRUDService so far
describe('CRUDServiceTest', function () {
'use strict';
var CRUDService;
beforeEach(function () {
inject(function ($injector) {
CRUDService = $injector.get('CRUDService');
});
});
it('should have a method build', function () {
expect(CRUDService).toHaveMethod('build');
});
it('should ensure that an instance of a built service has a correct value for getModelName method',
function () {
var expectedModelName = 'myService',
BuiltService = CRUDService.build(expectedModelName),
instanceOfBuiltService = new BuiltService();
expect(instanceOfBuiltService).toHaveMethod('getModelName');
expect(instanceOfBuiltService.getModelName()).toEqual(expectedModelName);
});
// TEST get
it('should ensure build returns a class with static method get', function () {
expect(CRUDService.build()).toHaveMethod('get');
});
it('should ensure get returns an instance of CRUD', function() {
var BuiltService = CRUDService.build(),
instanceOfBuiltService = new BuiltService();
expect((BuiltService.get()).constructor).toBe(instanceOfBuiltService.constructor);
});
// TEST query
it('should ensure build returns a class with static method query', function () {
expect(CRUDService.build()).toHaveMethod('query');
});
it('should a collection of CRUD', function () {
expect(CRUDService.build()).toHaveMethod('query');
});
it('should have a static method post', function () {
expect(CRUDService).toHaveMethod('post');
});
it('should have a static method put', function () {
expect(CRUDService).toHaveMethod('put');
});
});
TLDR;
To mock or not to mock a depending service depending itself on $http ?
In general I think it's a good idea to mock out your services. If you keep up on doing it, then it makes isolating the behavior of any service you add really easy.
That being said, you don't have to at all, you can simply use Jasmine spy's.
for instance if you were testing your CRUDService which had a method like this:
remove: function () {
return ApiProvider.delete(this.getModelName(), this.id);
}
You could, in your test write something like:
var spy = spyOn(ApiProvider, 'delete').andCallFake(function(model, id) {
var def = $q.defer();
$timeout(function() { def.resolve('something'); }, 1000)
return def.promise;
});
Then if you called it:
var promise = CRUDService.remove();
expect(ApiProvider.delete).toHaveBeenCalledWith(CRUDService.getModelName(), CRUDService.id);
So basically you can mock out the functionality you need in your test, without fully mocking out the service. You can read about it more here
Hope this helped!