I was trying to use the default template in a rest query using this:
/company/somecompanyid?template=default
But I still get all the data from my mongodb including fields and collections of related tables that are not in the templates.
This is also defined as the defaultTemplate in the model but it doesn't seems to impact the results in any way.
Can someone explain what I am doing wrong and how to apply the template?
If I want to include just the object Id rather than a whole related object in the reply, how do I specify it in the template?
company.js:
'use strict';
/**
* Module dependencies
*/
// Node.js core.
const path = require('path');
// Public node modules.
const _ = require('lodash');
const anchor = require('anchor');
// Local dependencies.
const WLValidationError = require('../../../node_modules/waterline/lib/waterline/error/WLValidationError');
// Settings for the Company model.
const settings = require('./Company.settings.json');
/**
* Export the Company model
*/
module.exports = {
/**
* Basic settings
*/
// The identity to use.
identity: settings.identity,
// The connection to use.
connection: settings.connection,
// Do you want to respect schema?
schema: settings.schema,
// Limit for a get request on the list.
limit: settings.limit,
// Merge simple attributes from settings with those ones.
attributes: _.merge(settings.attributes, {
}),
// Do you automatically want to have time data?
autoCreatedAt: settings.autoCreatedAt,
autoUpdatedAt: settings.autoUpdatedAt,
/**
* Lifecycle callbacks on validate
*/
// Before validating value
beforeValidate: function (values, next) {
// WARNING: Don't remove this part of code if you don't know what you are doing
const api = path.basename(__filename, '.js').toLowerCase();
if (strapi.api.hasOwnProperty(api) && _.size(strapi.api[api].templates)) {
const template = strapi.api[api].templates.hasOwnProperty(values.template) ? values.template : strapi.models[api].defaultTemplate;
const templateAttributes = _.merge(_.pick(strapi.models[api].attributes, 'lang'), strapi.api[api].templates[template].attributes);
// Handle Waterline double validate
if (_.size(_.keys(values)) === 1 && !_.includes(_.keys(templateAttributes), _.keys(values)[0])) {
next();
} else {
const errors = {};
// Set template with correct value
values.template = template;
values.lang = _.includes(strapi.config.i18n.locales, values.lang) ? values.lang : strapi.config.i18n.defaultLocale;
_.forEach(templateAttributes, function (rules, key) {
if (values.hasOwnProperty(key)) {
// Check rules
const test = anchor(values[key]).to(rules);
if (test) {
errors[key] = test;
}
} else if (rules.required) {
errors[key] = [{
rule: 'required',
message: 'Missing attributes ' + key
}];
}
});
// Go next step or not
_.isEmpty(errors) ? next() : next(new WLValidationError({
invalidAttributes: errors,
model: api
}));
}
} else if (strapi.api.hasOwnProperty(api) && !_.size(strapi.api[api].templates)) {
next();
} else {
next(new Error('Unknow API or no template detected'));
}
}
/**
* Lifecycle callbacks on create
*/
// Before creating a value.
// beforeCreate: function (values, next) {
// next();
// },
// After creating a value.
// afterCreate: function (newlyInsertedRecord, next) {
// next();
// },
/**
* Lifecycle callbacks on update
*/
// Before updating a value.
// beforeUpdate: function (valuesToUpdate, next) {
// next();
// },
// After updating a value.
// afterUpdate: function (updatedRecord, next) {
// next();
// },
/**
* Lifecycle callbacks on destroy
*/
// Before updating a value.
// beforeDestroy: function (criteria, next) {
// next();
// },
// After updating a value.
// afterDestroy: function (destroyedRecords, next) {
// next();
// }
};
the template which is a subset of the company attributes (CompanyDefault.template.json):
{
"default": {
"attributes": {
"name": {
"required": true
},
"address": {},
"phone": {},
"email": {}
},
"displayedAttribute": "name"
}
}
Can you show us your default template file? Then, to use the template system, you have to generate your model from the Studio or the CLI. The template logic is located in the "/api/company/models/Company.js" file in your case. It will be interesting to show us this file too.
You're using the right way to apply the template. Your URL seems good! However, this is not possible to include only the object ID rather than the whole related object for now. To do this, I advise you to use GraphQL, or an RAW query...
In the V2, we will support the JSON API specifications in Strapi, this will fit with your problematics for sure!
PS: I'm one of the authors of Strapi.
Related
I have records in strapi. I am using strapi content API. In my front-end, I need to display only 2 records randomly. For limiting, I have used limit query from content API. But random fetching what keyword I need to use. The official documentation doesn't provide any details regarding this - https://strapi.io/documentation/v3.x/content-api/parameters.html#available-operators
There's no official Strapi API parameter for random. You have to implement your own. Below is what I've done previously, using Strapi v3:
1 - Make a service function
File: api/mymodel/services/mymodel.js
This will contain our actual random query (SQL), and wrapping it in a service is handy because it can be used in many places (cron jobs, inside other models, etc).
module.exports = {
serviceGetRandom() {
return new Promise( (resolve, reject) => {
// There's a few ways to query data.
// This example uses Knex.
const knex = strapi.connections.default
let query = knex('mydatatable')
// Add more .select()'s if you want other fields
query.select('id')
// These rules enable us to get one random post
query.orderByRaw('RAND()')
query.limit(1)
// Initiate the query and do stuff
query
.then(record => {
console.log("getRandom() record: %O", record[0])
resolve(record[0])
})
.catch(error => {
reject(error)
})
})
}
}
2 - Use the service somewhere, like a controller:
File: api/mymodel/controllers/mymodel.js
module.exports = {
//(untested)
getRandom: async (ctx) => {
await strapi.services.mymodel.serviceGetRandom()
.then(output => {
console.log("getRandom output is %O", output.id)
ctx.send({
randomPost: output
}, 200)
})
.catch( () => {
ctx.send({
message: 'Oops! Some error message'
}, 204) // Place a proper error code here
})
}
}
3 - Create a route that points to this controller
File: api/mymodel/config/routes.json
...
{
"method": "GET",
"path": "/mymodelrandom",
"handler": "mymodel.getRandom",
"config": {
"policies": []
}
},
...
4 - In your front-end, access the route
(However you access your API)
e.g. ajax call to /api/mymodelrandom
There is no API parameter for getting a random result.
So: FrontEnd is the recommended solution for your question.
You need to create a random request range and then get some random item from this range.
function getRandomInt(max) {
return Math.floor(Math.random() * Math.floor(max));
}
const firstID = getRandomInt(restaurants.length);
const secondID = getRandomInt(3);
const query = qs.stringify({
id_in:[firstID,secondID ]
});
// request query should be something like GET /restaurants?id_in=3&id_in=6
One way you can do this reliably is by two steps:
Get the total number of records
Fetch the number of records using _start and _limit parameters
// Untested code but you get the idea
// Returns a random number between min (inclusive) and max (exclusive)
function getRandomArbitrary(min, max) {
return Math.random() * (max - min) + min;
}
const { data: totalNumberPosts } = await axios.get('/posts/count');
// Fetch 20 posts
const _limit = 20;
// We need to be sure that we are not fetching less than 20 posts
// e.g. we only have 40 posts. We generate a random number that is 30.
// then we would start on 30 and would only fetch 10 posts (because we only have 40)
const _start = getRandomArbitrary(0, totalNumberPosts - _limit);
const { data: randomPosts } = await axios.get('/posts', { params: { _limit, _start } })
The problem with this approach is that it requires two network requests but for my needs, this is not a problem.
This seem to work for me with Strapi v.4 REST API
Controller, Get 6 random entries
"use strict";
/**
* artwork controller
*/
const { createCoreController } = require("#strapi/strapi").factories;
module.exports = createCoreController("api::artwork.artwork", ({ strapi }) => {
const numberOfEntries = 6;
return {
async random(ctx) {
const entries = await strapi.entityService.findMany(
"api::artwork.artwork",
{
populate: ["image", "pageHeading", "seo", "socialMedia", "artist"],
}
);
const randomEntries = [...entries].sort(() => 0.5 - Math.random());
ctx.body = randomEntries.slice(0, numberOfEntries);
},
};
});
Route
random.js
"use strict";
module.exports = {
routes: [
{
method: "GET",
path: "/artwork/random",
handler: "artwork.random",
config: {
auth: false,
},
},
],
};
API
http://localhost:1337/api/artwork/random
To match default data structure of Strapi
"use strict";
/**
* artwork controller
*/
const { createCoreController } = require("#strapi/strapi").factories;
module.exports = createCoreController("api::artwork.artwork", ({ strapi }) => {
const numberOfEntries = 6;
return {
async random(ctx) {
const entries = await strapi.entityService.findMany(
"api::artwork.artwork",
{
populate: ["image", "pageHeading", "seo", "socialMedia", "artist"],
}
);
const randomEntries = [...entries]
.sort(() => 0.5 - Math.random())
.slice(0, numberOfEntries);
const structureRandomEntries = {
data: randomEntries.map((entry) => {
return {
id: entry.id,
attributes: entry,
};
}),
};
ctx.body = structureRandomEntries;
},
};
});
There is also a random sort plugin.
https://www.npmjs.com/package/strapi-plugin-random-sort
This seem to work for me with Strapi v4.3.8 and graphql
src/index.js
"use strict";
module.exports = {
register({ strapi }) {
const extensionService = strapi.service("plugin::graphql.extension");
const extension = ({ strapi }) => ({
typeDefs: `
type Query {
randomTestimonial: Testimonial
}
`,
resolvers: {
Query: {
randomTestimonial: async (parent, args) => {
const entries = await strapi.entityService.findMany(
"api::testimonial.testimonial"
);
const sanitizedRandomEntry =
entries[Math.floor(Math.random() * entries.length)];
return sanitizedRandomEntry;
},
},
},
resolversConfig: {
"Query.randomTestimonial": {
auth: false,
},
},
});
extensionService.use(extension);
},
bootstrap({ strapi }) {},
};
graphql query:
query GetRandomTestimonial {
randomTestimonial {
__typename
name
position
location
description
}
}
generate random testimonial on route change/refresh
https://jungspooner.com/biography
I have the following code:
export const myEpic = (action$, store) =>
action$.ofType("SOME_ACTION")
.switchMap(action => {
const {siteId, selectedProgramId} = action;
const state = store.getState();
const siteProgram$ = Observable.fromPromise(axios.get(`/url/${siteId}/programs`))
.catch(error =>{
return Observable.of({
type: 'PROGRAM_FAILURE'
error
});
});
const programType$ = Observable.fromPromise(axios.get('url2'))
.catch(error =>{
return Observable.of({
type: "OTHER_FAILURE",
error
});
});
so far so good, when there is an error I catch it, and (maybe this is wrong) map it to an action (indicating something failed).
now the question begins, I have another observable which is the result of the zip operator of the two observables from above:
const siteProgram$result$ = Observable.zip(siteProgram$, programType$)
.map(siteProgramsAndProgramTypes => siteProgramsAndProgramTypesToFinalSiteProgramsActionMapper(siteProgramsAndProgramTypes, siteId));
the problem is that I still get to this observable as if everything is fine.
is there a way to "understand" that one of the "zipped" observables errored and then not get to the "next" of siteProgram$result$.
I think I am missing something trivial...
I don't want to have to perform this check:
const siteProgramsAndProgramTypesToFinalSiteProgramsActionMapper = (siteProgramsAndProgramTypesArray, siteId) => {
const [programsResponse, programTypesResponse] = siteProgramsAndProgramTypesArray;
if (programsResponse.error || programTypesResponse.error){
return {
type: 'GENERAL_ERROR',
};
}
everytime I have an observable which is a result of an operator on other observable that might have errored.
in pure rxjs (not in redux observable) I think I could subscribe to it passing it an object
{
next: val => some logic,
error: err => do what ever I want :) //this is what I am missing in redux observable,
complete: () => some logic
}
// some more logic
return Observable.concat(programType$Result$, selectedProgramId$, siteProgram$result$);
What is the right way to attack this in redux observable?
Thanks.
Here is a detailed example with an API wrapper to facilitate what you're trying to achieve.
The gist is available on GitHub here
Here is the API wrapper which wraps Observable.ajax and lets you dispatch single actions or array of actions and handles both XHR and Application level generated errors that stem from the requests made with Observable.ajax
import * as Rx from 'rxjs';
import queryString from 'query-string';
/**
* This function simply transforms any actions into an array of actions
* This enables us to use the synthax Observable.of(...actions)
* If an array is passed to this function it will be returned automatically instead
* Example: mapObservables({ type: ACTION_1 }) -> will return: [{ type: ACTION_1 }]
* Example2: mapObservables([{ type: ACTION_1 }, { type: ACTION_2 }]) -> will return: [{ type: ACTION_1 }, { type: ACTION_2 }]
*/
function mapObservables(observables) {
if (observables === null) {
return null;
} else if (Array.isArray(observables)) {
return observables;
}
return [observables];
}
/**
* Possible Options:
* params (optional): Object of parameters to be appended to query string of the uri e.g: { foo: bar } (Used with GET requests)
* headers (optional): Object of headers to be appended to the request headers
* data (optional): Any type of data you want to be passed to the body of the request (Used for POST, PUT, PATCH, DELETE requests)
* uri (required): Uri to be appended to our API base url
*/
function makeRequest(method, options) {
let uri = options.uri;
if (method === 'get' && options.params) {
uri += `?${queryString.stringify(options.params)}`;
}
return Rx.Observable.ajax({
headers: {
'Content-Type': 'application/json;charset=utf-8',
...options.headers,
},
responseType: 'json',
timeout: 60000,
body: options.data || null,
method,
url: `http://www.website.com/api/v1/${uri}`,
// Most often you have a fixed API url so we just append a URI here to our fixed URL instead of repeating the API URL everywhere.
})
.flatMap(({ response }) => {
/**
* Here we handle our success callback, anyt actions returned from it will be dispatched.
* You can return a single action or an array of actions to be dispatched eg. [{ type: ACTION_1 }, { type: ACTION_2 }].
*/
if (options.onSuccess) {
const observables = mapObservables(options.onSuccess(response));
if (observables) {
// This is only being called if our onSuccess callback returns any actions in which case we have to dispatch them
return Rx.Observable.of(...observables);
}
}
return Rx.Observable.of();
})
.catch((error) => {
/**
* This if case is to handle non-XHR errors gracefully that may be coming from elsewhere in our application when we fire
* an Observable.ajax request
*/
if (!error.xhr) {
if (options.onError) {
const observables = mapObservables(options.onError(null)); // Note we pass null to our onError callback because it's not an XHR error
if (observables) {
// This is only being called if our onError callback returns any actions in which case we have to dispatch them
return Rx.Observable.of(...observables);
}
}
// You always have to ensure that you return an Observable, even if it's empty from all your Observables.
return Rx.Observable.of();
}
const { xhr } = error;
const { response } = error.xhr;
const actions = [];
const resArg = response || null;
let message = null;
if (xhr.status === 0) {
message = 'Server is not responding.';
} else if (xhr.status === 401) {
// For instance we handle a 401 here, if you use react-router-redux you can simply push actions here to your router
actions.push(
replace('/login'),
);
} else if (
response
&& response.errorMessage
) {
/*
* In this case the errorMessage parameter would refer to SampleApiResponse.json 400 example
* { "errorMessage": "Invalid parameter." }
*/
message = response.errorMessage;
}
if (options.onError) {
// Here if our options contain an onError callback we can map the returned Actions and push them into our action payload
mapObservables(options.onError(resArg)).forEach(o => actions.push(o));
}
if (message) {
actions.push(showMessageAction(message));
}
/**
* You can return multiple actions in one observable by adding arguments Rx.Observable.of(action1, action2, ...)
* The actions always have to have a type { type: 'ACTION_1' }
*/
return Rx.Observable.of(...actions);
});
}
const API = {
get: options => makeRequest('get', options),
post: options => makeRequest('post', options),
put: options => makeRequest('put', options),
patch: options => makeRequest('patch', options),
delete: options => makeRequest('delete', options),
};
export default API;
Here are the actions, action creators and epics:
import API from 'API';
const FETCH_PROFILE = 'FETCH_PROFILE';
const FETCH_PROFILE_SUCCESS = 'FETCH_PROFILE_SUCCESS';
const FETCH_PROFILE_ERROR = 'FETCH_PROFILE_ERROR';
const FETCH_OTHER_THING = 'FETCH_OTHER_THING';
const FETCH_OTHER_THING_SUCCESS = 'FETCH_OTHER_THING_SUCCESS';
const FETCH_OTHER_THING_ERROR = 'FETCH_OTHER_THING_ERROR';
function fetchProfile(id) {
return {
type: FETCH_PROFILE,
id,
};
}
function fetchProfileSuccess(data) {
return {
type: FETCH_PROFILE_SUCCESS,
data,
};
}
function fetchProfileError(error) {
return {
type: FETCH_PROFILE_ERROR,
error,
};
}
function fetchOtherThing(id) {
return {
type: FETCH_OTHER_THING,
id,
};
}
const fetchProfileEpic = action$ => action$.
ofType(FETCH_PROFILE)
.switchMap(({ id }) => API.get({
uri: 'profile',
params: {
id,
},
/*
* We could also dispatch multiple actions using an array here you could dispatch another API request if needed
* We can redispatch another action to fire another epic if we want also.
* In both onSuccess and onError you can return a single action, an array of actions or null
* Note here we fire fetchOtherThing(data.someOtherThingId) which will trigger our fetchOtherThingEpic!
* In this case the data parameter would refer to SampleApiResponse.json 200 example
* { firstName: "John", lastName: "Doe" }
*/
onSuccess: ({ data }) => [fetchProfileSuccess(data), fetchOtherThing(data.someOtherThingId)],
onError: error => fetchProfileError(error),
})
const fetchOtherThingEpic = action$ => action$.
ofType(FETCH_OTHER_THING)
.switchMap(({ id }) => API.get({
uri: 'other-thing',
params: {
id,
},
onSuccess: ...
onError: ...
});
Here is a data sample that works with the examples shown above:
/*
* If possible, you should standardize your API response which will make error/data handling a lot easier on the client side
* Note, this data format is to work with the example code above
*/
/**
* Status code: 401
* This error would be caught in the .catch() method of our API wrapper
*/
{
"errorMessage": "Please login to perform this operation",
"data": null,
}
/**
* Status code: 400
* This error would be caught in the .catch() method of our API wrapper
*
*/
{
"errorMessage": "Invalid parameter.",
"data": null
}
/**
* Status code: 200
* This would be passed to our onSuccess function specified in our API options
*/
{
"errorMessage": 'Please login to perform this operation',
"data": {
"firstName": "John",
"lastName": "Doe"
}
}
I'm having a real hard time understanding how to implement aor-realtime (trying to use it with firebase; reads only, no write).
The first place I get stuck: This library generates a saga, right? How do I connect that with a restClient/resource? I have a few custom sagas that alert me on errors, but there is a main restClient/resource backing those. Those sagas just handles some side-effects. In this case, I just don't understand what the role of the client is, and how it interacts with the generated saga (or visa-versa)
The other question is with persistence: Updates stream in and the initial set of records is not loaded in one go. Should I be calling observer.next() with each update? or cache the updated records and call next() with the entire collection to-date.
Here's my current attempt at doing the later, but I'm still lost with how to connect it to my Admin/Resource.
import realtimeSaga from 'aor-realtime';
import { client, getToken } from '../firebase';
import { union } from 'lodash'
let cachedToken
const observeRequest = path => (type, resource, params) => {
// Filtering so that only chats are updated in real time
if (resource !== 'chat') return;
let results = {}
let ids = []
return {
subscribe(observer) {
let databaseRef = client.database().ref(path).orderByChild('at')
let events = [ 'child_added', 'child_changed' ]
events.forEach(e => {
databaseRef.on(e, ({ key, val }) => {
results[key] = val()
ids = union([ key ], ids)
observer.next(ids.map(id => results[id]))
})
})
const subscription = {
unsubscribe() {
// Clean up after ourselves
databaseRef.off()
results = {}
ids = []
// Notify the saga that we cleaned up everything
observer.complete();
}
};
return subscription;
},
};
};
export default path => realtimeSaga(observeRequest(path));
How do I connect that with a restClient/resource?
Just add the created saga to the custom sagas of your Admin component.
About the restClient, if you need it in your observer, then pass it the function which return your observer as you did with path. That's actually how it's done in the readme.
Should I be calling observer.next() with each update? or cache the updated records and call next() with the entire collection to-date.
It depends on the type parameter which is one of the admin-on-rest fetch types:
CRUD_GET_LIST: you should return the entire collection, updated
CRUD_GET_ONE: you should return the resource specified in params (which should contains its id)
Here's the solution I came up with, with guidance by #gildas:
import realtimeSaga from "aor-realtime";
import { client } from "../../../clients/firebase";
import { union } from "lodash";
const observeRequest = path => {
return (type, resource, params) => {
// Filtering so that only chats are updated in real time
if (resource !== "chats") return;
let results = {}
let ids = []
const updateItem = res => {
results[res.key] = { ...res.val(), id: res.key }
ids = Object.keys(results).sort((a, b) => results[b].at - results[a].at)
}
return {
subscribe(observer) {
const { page, perPage } = params.pagination
const offset = perPage * (page - 1)
const databaseRef = client
.database()
.ref(path)
.orderByChild("at")
.limitToLast(offset + perPage)
const notify = () => observer.next({ data: ids.slice(offset, offset + perPage).map(e => results[e]), total: ids.length + 1 })
databaseRef.once('value', snapshot => {
snapshot.forEach(updateItem)
notify()
})
databaseRef.on('child_changed', res => {
updateItem(res)
notify()
})
const subscription = {
unsubscribe() {
// Clean up after ourselves
databaseRef.off();
// Notify the saga that we cleaned up everything
observer.complete();
}
};
return subscription;
}
};
}
};
export default path => realtimeSaga(observeRequest(path));
I'm trying to update the Angular2 Forms Validation example to handle an Async Validation response. This way I can hit an HTTP endpoint to validate a username.
Looking at their code they currently aren't currently using a Promise and it's working just fine:
/** A hero's name can't match the given regular expression */
export function forbiddenNameValidator(nameRe: RegExp): ValidatorFn {
return (control: AbstractControl): {[key: string]: any} => {
const name = control.value;
const no = nameRe.test(name);
return no ? {'forbiddenName': {name}} : null;
};
}
I'm trying to update to return a Promise. Something like:
/** A hero's name can't match the given regular expression */
export function forbiddenNameValidator(nameRe: RegExp): ValidatorFn {
return (control: AbstractControl) => {
const name = control.value;
return new Promise( resolve => {
resolve({'forbiddenName': {name}});
});
};
}
However, the result I get doesn't display the error message, it's showing undefined.
My thought is it has something to do with the way they are handling displaying the errors:
onValueChanged(data?: any) {
if (!this.heroForm) { return; }
const form = this.heroForm;
for (const field in this.formErrors) {
// clear previous error message (if any)
this.formErrors[field] = '';
const control = form.get(field);
if (control && control.dirty && !control.valid) {
const messages = this.validationMessages[field];
for (const key in control.errors) {
this.formErrors[field] += messages[key] + ' ';
}
}
}
}
However I'm not sure of a better way of doing this.
Angular2 example:
https://angular.io/docs/ts/latest/cookbook/form-validation.html#!#live-example
Link to my example attempting to return Promise:
https://plnkr.co/edit/sDs9pNQ1Bs2knp6tasgI?p=preview
The problem is that you add the AsyncValidator to the SyncValidator Array. AsyncValidators are added in a separate array after the SyncValidators:
this.heroForm = this.fb.group({
'name': [this.hero.name, [
Validators.required,
Validators.minLength(4),
Validators.maxLength(24)
],
[forbiddenNameValidator(/bob/i)] // << separate array
],
'alterEgo': [this.hero.alterEgo],
'power': [this.hero.power, Validators.required]
});
I am trying to run Jasmine inside PhantomJS. After a lot of effort I am down to:
page.injectJs("/jasmine-standalone-2.0.0/lib/jasmine-2.0.0/jasmine.js");
page.injectJs("/jasmine-standalone-2.0.0/lib/jasmine-2.0.0/jasmine-html.js");
page.injectJs("/jasmine-standalone-2.0.0/lib/jasmine-2.0.0/boot.js");
page.injectJs("/jasmine-standalone-2.0.0/lib/jasmine-2.0.0/console.js");
var result = page.evaluate(function (done) {
var ConsoleReporter = jasmineRequire.ConsoleReporter();
var options = {
timer: new jasmine.Timer,
print: function () {
console.log.apply(console, arguments)
}
};
consoleReporter = new ConsoleReporter(options);
jasmine.getEnv().addReporter(consoleReporter);
describe("test1", function () {
console.log("in test 1");
it("should do something", function () {
console.log("NEVER GETS HERE"); // <-- never gets there
});
});
});
The code executes all the way up to it() but the callback never executes :|
[edit]
I'm trying to use phantom with jasmine for end to end testing. I already have an app server and I'm using Karma for unit tests. So I don't think a testRunner.html is going to help. PhantomJS should login to my app and do some stuff, which I will test with Jasmine.
Got it with the dumb luck brute force approach. I never found helpful documentation but this helped a little
My files are like this:
testrunner.js
var system = require("system");
var page = require("webpage").create();
var loginUrl = system.args[1];
page.onConsoleMessage = function (msg) {
console.log("FROM PAGE: " + msg);
};
page.onError = function (err) {
console.log("ERR FROM PAGE: " + JSON.stringify(err));
}
var loggedIn = false;
function onLogin_atHomePage() {
if (loggedIn)
return; // dont run twice if a developer is programatically logging in
page.injectJs("/jasmine-standalone-2.0.0/lib/jasmine-2.0.0/jasmine.js");
page.injectJs("/jasmine-standalone-2.0.0/lib/jasmine-2.0.0/boot.js");
page.injectJs("/jasmine-standalone-2.0.0/lib/jasmine-2.0.0/console.js");
var result = page.evaluate(function (done) {
var ConsoleReporter = jasmineRequire.ConsoleReporter();
var options = {
timer: new jasmine.Timer,
print: function () {
console.log.apply(console, arguments)
}
};
consoleReporter = new ConsoleReporter(options); // initialize ConsoleReporter
jasmine.getEnv().addReporter(consoleReporter); //add reporter to execution environment
describe("test1", function () {
console.log("in test 1");
it("should do something", function () {
console.log("NEV");
});
});
jasmine.getEnv().execute();
});
}
page.open(loginUrl, function () {
console.log("LOGGING IN...");
page.onLoadFinished = onLogin_atHomePage;
page.evaluate(function () {
$(document).ready(function () {
var $u = $("#login_id");
var $p = $("[name='password']");
$u.val("me");
$p.val("666");
submitForm();
});
});
});
boot.js
/**
Starting with version 2.0, this file "boots" Jasmine, performing all of the necessary initialization before executing the loaded environment and all of a project's specs. This file should be loaded after `jasmine.js`, but before any project source files or spec files are loaded. Thus this file can also be used to customize Jasmine for a project.
If a project is using Jasmine via the standalone distribution, this file can be customized directly. If a project is using Jasmine via the [Ruby gem][jasmine-gem], this file can be copied into the support directory via `jasmine copy_boot_js`. Other environments (e.g., Python) will have different mechanisms.
The location of `boot.js` can be specified and/or overridden in `jasmine.yml`.
[jasmine-gem]: http://github.com/pivotal/jasmine-gem
*/
(function() {
/**
* ## Require & Instantiate
*
* Require Jasmine's core files. Specifically, this requires and attaches all of Jasmine's code to the `jasmine` reference.
*/
window.jasmine = jasmineRequire.core(jasmineRequire);
/**
* Since this is being run in a browser and the results should populate to an HTML page, require the HTML-specific Jasmine code, injecting the same reference.
*/
//jasmineRequire.html(jasmine);
/**
* Create the Jasmine environment. This is used to run all specs in a project.
*/
var env = jasmine.getEnv();
/**
* ## The Global Interface
*
* Build up the functions that will be exposed as the Jasmine public interface. A project can customize, rename or alias any of these functions as desired, provided the implementation remains unchanged.
*/
var jasmineInterface = {
describe: function(description, specDefinitions) {
return env.describe(description, specDefinitions);
},
xdescribe: function(description, specDefinitions) {
return env.xdescribe(description, specDefinitions);
},
it: function(desc, func) {
return env.it(desc, func);
},
xit: function(desc, func) {
return env.xit(desc, func);
},
beforeEach: function(beforeEachFunction) {
return env.beforeEach(beforeEachFunction);
},
afterEach: function(afterEachFunction) {
return env.afterEach(afterEachFunction);
},
expect: function(actual) {
return env.expect(actual);
},
pending: function() {
return env.pending();
},
spyOn: function(obj, methodName) {
return env.spyOn(obj, methodName);
},
jsApiReporter: new jasmine.JsApiReporter({
timer: new jasmine.Timer()
})
};
/**
* Add all of the Jasmine global/public interface to the proper global, so a project can use the public interface directly. For example, calling `describe` in specs instead of `jasmine.getEnv().describe`.
*/
if (typeof window == "undefined" && typeof exports == "object") {
extend(exports, jasmineInterface);
} else {
extend(window, jasmineInterface);
}
/**
* Expose the interface for adding custom equality testers.
*/
jasmine.addCustomEqualityTester = function(tester) {
env.addCustomEqualityTester(tester);
};
/**
* Expose the interface for adding custom expectation matchers
*/
jasmine.addMatchers = function(matchers) {
return env.addMatchers(matchers);
};
/**
* Expose the mock interface for the JavaScript timeout functions
*/
jasmine.clock = function() {
return env.clock;
};
/**
* ## Runner Parameters
*
* More browser specific code - wrap the query string in an object and to allow for getting/setting parameters from the runner user interface.
*/
/*var queryString = new jasmine.QueryString({
getWindowLocation: function() { return window.location; }
});
var catchingExceptions = queryString.getParam("catch");
env.catchExceptions(typeof catchingExceptions === "undefined" ? true : catchingExceptions);
*/
/**
* ## Reporters
* The `HtmlReporter` builds all of the HTML UI for the runner page. This reporter paints the dots, stars, and x's for specs, as well as all spec names and all failures (if any).
*/
/*
var htmlReporter = new jasmine.HtmlReporter({
env: env,
onRaiseExceptionsClick: function() { queryString.setParam("catch", !env.catchingExceptions()); },
getContainer: function() { return document.body; },
createElement: function() { return document.createElement.apply(document, arguments); },
createTextNode: function() { return document.createTextNode.apply(document, arguments); },
timer: new jasmine.Timer()
});
*/
/**
* The `jsApiReporter` also receives spec results, and is used by any environment that needs to extract the results from JavaScript.
*/
env.addReporter(jasmineInterface.jsApiReporter);
//env.addReporter(htmlReporter);
/**
* Filter which specs will be run by matching the start of the full name against the `spec` query param.
*/
/*var specFilter = new jasmine.HtmlSpecFilter({
filterString: function() { return queryString.getParam("spec"); }
});
env.specFilter = function(spec) {
return specFilter.matches(spec.getFullName());
};*/
/**
* Setting up timing functions to be able to be overridden. Certain browsers (Safari, IE 8, phantomjs) require this hack.
*/
window.setTimeout = window.setTimeout;
window.setInterval = window.setInterval;
window.clearTimeout = window.clearTimeout;
window.clearInterval = window.clearInterval;
/**
* ## Execution
*
* Replace the browser window's `onload`, ensure it's called, and then run all of the loaded specs. This includes initializing the `HtmlReporter` instance and then executing the loaded Jasmine environment. All of this will happen after all of the specs are loaded.
*/
var currentWindowOnload = window.onload;
window.onload = function() {
if (currentWindowOnload) {
currentWindowOnload();
}
htmlReporter.initialize();
env.execute();
};
/**
* Helper function for readability above.
*/
function extend(destination, source) {
for (var property in source) destination[property] = source[property];
return destination;
}
}());