For the new version of a product I decided to try a page objects approach instead of using views and probably I started to use it wrong.
We have a custom command that simply waits for an element and clicks (waitAndClick.js):
exports.command = function(selector, callback) {
return this
.waitForElementPresent(selector, 30000)
.click(selector, callback);
};
It works perfectly inside the test:
const {client} = require('nightwatch-cucumber');
const {defineSupportCode} = require('cucumber');
defineSupportCode(({Given, Then, When}) => {
Given(/^I enable Dashboard management$/, () => {
return client.waitAndClick('[id=enableManagement]');
});
});
But when I am trying to use it inside the page object it throws an error:
module.exports = {
url() {
return this.api.launchUrl;
},
elements: {
username: '[name="_Nitro_Login_username"]',
password: '[name="_Nitro_Login_password"]',
enter_button: '[title="Enter"]'
},
commands: [
{
loginAs(username, password) {
return this.waitForElementVisible('#username', 50000)
.setValue('#username', username)
.setValue('#password', password)
.waitAndClick('#enter_button')
.waitForElementNotPresent('#enter_button', 50000);
}
}
]
};
I also tried with .api.waitAndClick('#enter_button'), the same result.
And the error message:
Error while running click command: Provided locating strategy is not
supported: [title="enter"]. It must be one of the following:
class name, css selector, id, name, link text, partial link text, tag
name, xpath
at Object.exports.command (/Users/eaflor/dev/jive-next/test/ui/commands/waitAndClick.js:9:63)
at Object.F.command (/Users/eaflor/dev/jive-next/node_modules/nightwatch/lib/core/api.js:274:31)
at Object.commandFn (/Users/eaflor/dev/jive-next/node_modules/nightwatch/lib/core/api.js:287:24)
at AsyncTree.runCommand (/Users/eaflor/dev/jive-next/node_modules/nightwatch/lib/core/queue.js:154:30)
at AsyncTree.runChildNode (/Users/eaflor/dev/jive-next/node_modules/nightwatch/lib/core/queue.js:114:8)
at AsyncTree.walkDown (/Users/eaflor/dev/jive-next/node_modules/nightwatch/lib/core/queue.js:80:10)
at AsyncTree.walkUp (/Users/eaflor/dev/jive-next/node_modules/nightwatch/lib/core/queue.js:97:8)
at AsyncTree.walkDown (/Users/eaflor/dev/jive-next/node_modules/nightwatch/lib/core/queue.js:90:12)
at AsyncTree.traverse (/Users/eaflor/dev/jive-next/node_modules/nightwatch/lib/core/queue.js:73:8)
at F.onCommandComplete (/Users/eaflor/dev/jive-next/node_modules/nightwatch/lib/core/queue.js:131:12)
at F.g (events.js:291:16)
at emitNone (events.js:86:13)
at F.emit (events.js:185:7)
at /Users/eaflor/dev/jive-next/node_modules/nightwatch/lib/api/client-commands/_locateStrategy.js:18:10
at _combinedTickCallback (internal/process/next_tick.js:67:7)
at process._tickCallback (internal/process/next_tick.js:98:9)
Is it even possible to use custom commands inside the page object?
I found the way to fix it. In order to use custom commands in page objects you have to write them in class-style: http://nightwatchjs.org/guide#writing-custom-commands
Here how it should look like:
var util = require('util');
var events = require('events');
function waitAndClick() {
events.EventEmitter.call(this);
}
util.inherits(waitAndClick, events.EventEmitter);
waitAndClick.prototype.command = function(selector) {
const api = this.client.api;
api
.waitForElementPresent(selector)
.click(selector, () => {
this.emit('complete');
})
;
return this;
};
module.exports = waitAndClick;
Hope it will help someone.
I'm still new with Nightwatch, but I'm using commands in Page Objects like this:
commands: [
{
login: function() {
this.api
.waitForElementVisible('body', 2000)
.setValue(this.elements.username.selector, user.loginUser) //here I'm inputting username from json file into element that I've defined in this page object
.setValue(this.elements.password.selector, pass.loginPass) //here I did same thing with password
.pause(500)
.click(this.elements.submitButton.selector) //here I'm clicking on predefined button element
}
}
]
Works perfectly, and it's very readable. This is simple login command. Hope this helps.
Cheers
Too late for an answer here but might help others facing similar issue. Returning this could fix the chaining issue from the page object.
exports.command = function(selector, callback) {
this
.waitForElementPresent(selector, 30000)
.click(selector, callback);
return this;
};
Related
I'm new on electronjs and developing a small application that reads a json file and build a small html form and return the values entered by the user.
So I've developed small scripts in javascript that link to html 'button' tags to call dialogs so that a user can enter directories, files and save the final form. Everything works nicely... on electronjs "^3.1.13". But if I'm updating to a recent version of the lib ("^8.2.5"), then all my cool ShowOpenDialog don't work at all. Any clue of what happens?
Here is the script to open a folder if it helps:
{
let myName = document.currentScript.getAttribute('name');
const ipc = require('electron').ipcRenderer;
let asyncBtn = document.querySelector('#folder-selector-'+myName);
let replyField = document.querySelector('#folder-selector-content-'+myName);
let onButtonClick = function() {
const { dialog } = require('electron').remote;
let dialogOptions = {
title: "Choisir un dossier:",
properties: ['openDirectory','promptToCreate'],
};
dialog.showOpenDialog(
dialogOptions,
fileNames => {
if (fileNames === undefined) {
console.log("No file selected");
} else {
console.log('file:', fileNames[0]);
replyField.value = fileNames[0];
}
})
};
asyncBtn.addEventListener("click", onButtonClick);
}
Thanks a lot for any help.
Apart from the fact that the call to dialog.showOpenDialog has indeed been updated in recent versions of Electron, and returns a promise instead of making use of a callback function, there is another flaw in your updated code: reading the above-mentioned documentation page shows that getCurrentWindow() is not a method of dialog; it can be obtained from remote instead, so you have to add it explicitely:
const { dialog, getCurrentWindow } = require('electron').remote;
then simply call it from inside dialog.showOpenDialog:
dialog.showOpenDialog( getCurrentWindow(), dialogOptions).then(result => {
but this is an error you could have caught yourself by looking at the DevTools's console, which would display:
TypeError: dialog.getCurrentWindow is not a function
Recent version of showOpenDialog receives two arguments: optional BrowserWindow, and options as second argument. It returns promise and not requires callback.
https://github.com/electron/electron/blob/8-x-y/docs/api/dialog.md#dialogshowopendialogbrowserwindow-options
So you need to change you callback logic to promises.
let onButtonClick = function() {
const { dialog } = require('electron').remote;
let dialogOptions = {
title: "Choisir un dossier:",
properties: ['openDirectory','promptToCreate'],
};
dialog.showOpenDialog(
dialogOptions
).then((fileNames)=>{
if (fileNames === undefined) {
console.log("No file selected");
} else {
console.log('file:', fileNames[0]);
replyField.value = fileNames[0];
}
}).catch(err=>console.log('Handle Error',err))
};
asyncBtn.addEventListener("click", onButtonClick);
thanks a lot Vladimir. So I've tried to update my code as explained, updating electron package to version 8.2.5 and modifying the script as you explained but it's not going any better. If I got it well, this code should be correct, but doesn't work on electron 8.2.5. Any error you still see on this?
{
let myName = document.currentScript.getAttribute('name');
const ipc = require('electron').ipcRenderer;
let asyncBtn = document.querySelector('#folder-selector-'+myName);
let replyField = document.querySelector('#folder-selector-content-'+myName);
let onButtonClick = function() {
const { dialog } = require('electron').remote;
let dialogOptions = {
title: "Choisir un dossier:",
properties: ['openDirectory','promptToCreate']
};
dialog.showOpenDialog( dialog.getCurrentWindow(), dialogOptions).then(result => {
if(!result.canceled) {
replyField.value = result.filePaths[0];
}
}).catch(err => {
console.log(err)
})
};
asyncBtn.addEventListener("click", onButtonClick);
}
Ok, finally got it. Apart from the most appreciated help I had, I missed
"webPreferences": {
nodeIntegration: true
}
in the main.js to make it work.
The discovering of the Developer Tools were of great help as well :)
Now everything is fine again. Thanks a lot!
Useage of 'request-native-promise' not correctly chaining to it's subsequent 'then' and 'catch' handlers.
My Protractor Test
// There's a bunch of other imports here
import { browser } from "protractor";
const RequestPromise = require('request-promise-native');
describe('spec mapper app', () => {
let specMapperPage: SpecMapperPage;
let specMapperFVRPage: SpecMapperFieldsValuesReviewPage;
let loginLogoutWorkflow: LoginLogoutWorkflow;
let apiToken: LoginToken;
let tokenUtil: TokenUtil;
let projectRecordsToBeDeleted = [];
let requestHandler;
let logger = new CustomLogger("spec mapper app");
let speccyEndpoints = new SpeccyEndpoints();
beforeAll( () => {
logger.debug("Before All")
loginLogoutWorkflow = new LoginLogoutWorkflow();
loginLogoutWorkflow.login();
tokenUtil = new TokenUtil();
tokenUtil.getToken().then((token:LoginToken) => {
apiToken = token;
requestHandler = new SpeccyRequestHandler(apiToken);
});
});
describe('import/export page', () => {
it('TC2962: I'm a test case', () => {
let testLogger = new CustomLogger("TC2955");
// Test Var setup
... // removed for brevity
// Test Setup
browser.waitForAngularEnabled(false);
// Setup the record to be on the mapper page
let body = speccyEndpoints.generateRevitPostBody(PROJECT_ID, fileName);
requestHandler.postToSpeccy(speccyEndpoints.DELITE_REVIT_POST_URI, body).then((response) => { // EDIT: removed non-existant argument "rejection"
// --> The then handler the promise is STILL not resolving into
// Only made it here like once
console.log("Response is: ");
console.log(response);
// I got this to work once, but now it's not
console.log("Response body is: ");
console.log(response.body);
}).catch(error => {
// --> The catch handler is ALSO NOT resolving
console.log("catch handler executed!");
console.log(error);
});
});
});
});
The test case where things are going wrong. My console.log("Response is: "); is NOT being outputted. I'm not getting error messages as to why.
My Speccy Request Handler Wrapper Class
import * as RequestPromise from "request-promise-native";
import {LoginToken} from "../testObjects/LoginToken";
import {CustomLogger} from "../logging/CustomLogger";
export class SpeccyRequestHandler {
_baseAPIURL = 'http://myapi.net/';
_options = {
method: '',
uri: '',
auth: {
'bearer': ''
},
headers: {
'User-Agent': 'client'
},
"resolveWithFullResponse": true,
body: {},
json: true
};
_logger;
constructor(apiToken: LoginToken) {
this._options.auth.bearer = apiToken.idToken;
this._logger = new CustomLogger(SpeccyRequestHandler.name);
}
getOptions() {
return this._options;
}
postToSpeccy(uri:string, body?) {
this._options.method = 'POST';
this._options.uri = this._baseAPIURL + uri;
if(body) {
this._options.body = body;
}
return RequestPromise(this._options);
}
getFromSpeccy(uri) {
this._options.method = 'GET';
this._options.uri = this._baseAPIURL + uri;
return RequestPromise(this._options);
}
}
This is my Request Handler specific to one of my APIs, the Speccy one, and has some custom aspects to it in the URL and the token passing.
Sources
Request-Promise-Native Github Page
Request-Promise Github page, documentation location
Update Number 1
After the fix #tomalak brought to my attention, my console.log's in the .then(... handler were being executed, the first 5-ish times after I changed over to this, I was getting a roughly 150+ line console log of the response object that contained a body of response I would expect from my request URI. I even got the body out by using response.body. I thought things were fixed and I wasn't using my logger that logs out to file, so I lost my proof. Now when I run this test and this request I do not go into the .then(... handler at all. I'm also not going into the catch. My request is working though, as my resource is created when the post endpoint is hit. Any further suggestions are appreciated.
What would cause something to work sometimes and not others? My only thought is maybe the generic post name in my request handler wasn't being used in lieu of another method higher up the build chain being caught.
Update Number 2
Removed a bunch of stuff to shorten my question. If you need more clarification, ask and I'll add it.
It ended up being a timeout on the end of the API. My response was simply taking too long to get back to me. It wasn't failing, so it never went into the catch. And I had it working at one point because the response taking so long is due to an overabundance of a certain resource in our system in particular. I thought it was me and something I had written wrong. Long story short, suspect your system, even if you think it's perfect or if your devs swear up and down nothing could be broken.
Also, the request-debug module was a nice thing to have to prove that other endpoints, such as the rest testing endpoints at https://jsonplaceholder.typicode.com/ , do work with your code.
Heroku recently posted a list of some good tips for postgres. I was most intreged by the Track the Source of Your Queries section. I was curious if this was something that's possible to use with Sequelize. I know that sequelize has hooks, but wasn't sure if hooks could be used to make actual query string adjustments.
I'm curious if it's possible to use a hook or another Sequelize method to append a comment to Sequelize query (without using .raw) to keep track of where the query was called from.
(Appending and prepending to queries would also be helpful for implementing row-level security, specifically set role / reset role)
Edit: Would it be possible to use sequelize.fn() for this?
If you want to just insert a "tag" into the SQL query you could use Sequelize.literal() to pass a literal string to the query generator. Adding this to options.attributes.include will add it, however it will also need an alias so you would have to pass some kind of value as well.
Model.findById(id, {
attributes: {
include: [
[Sequelize.literal('/* your comment */ 1'), 'an_alias'],
],
},
});
This would produce SQL along the lines of
SELECT `model`.`id`, /* your comment */ 1 as `an_alias`
FROM `model` as `model`
WHERE `model`.`id` = ???
I played around with automating this a bit and it probably goes beyond the scope of this answer, but you could modify the Sequelize.Model.prototype before you create a connection using new Sequelize() to tweak the handling of the methods. You would need to do this for all the methods you want to "tag".
// alias findById() so we can call it once we fiddle with the input
Sequelize.Model.prototype.findById_untagged = Sequelize.Model.prototype.findById;
// override the findbyId() method so we can intercept the options.
Sequelize.Model.prototype.findById = function findById(id, options) {
// get the caller somehow (I was having trouble accessing the call stack properly)
const caller = ???;
// you need to make sure it's defined and you aren't overriding settings, etc
options.attributes.include.push([Sequelize.literal('/* your comment */ 1'), 'an_alias']);
// pass it off to the aliased method to continue as normal
return this.findById_untagged(id, options);
}
// create the connection
const connection = new Sequelize(...);
Note: it may not be possible to do this automagically as Sequelize has use strict so the arguments.caller and arguments.callee properties are not accessible.
2nd Note: if you don't care about modifying the Sequelize.Model prototypes you can also abstract your calls to the Sequelize methods and tweak the options there.
function Wrapper(model) {
return {
findById(id, options) {
// do your stuff
return model.findById(id, options);
},
};
}
Wrapper(Model).findById(id, options);
3rd Note: You can also submit a pull request to add this functionality to Sequelize under a new option value, like options.comment, which is added at the end of the query.
This overrides the sequelize.query() method that's internally used by Sequelize for all queries to add a comment showing the location of the query in the code. It also adds the stack trace to errors thrown.
const excludeLineTexts = ['node_modules', 'internal/process', ' anonymous ', 'runMicrotasks', 'Promise.'];
// overwrite the query() method that Sequelize uses internally for all queries so the error shows where in the code the query is from
sequelize.query = function () {
let stack;
const getStack = () => {
if (!stack) {
const o = {};
Error.captureStackTrace(o, sequelize.query);
stack = o.stack;
}
return stack;
};
const lines = getStack().split(/\n/g).slice(1);
const line = lines.find((l) => !excludeLineTexts.some((t) => l.includes(t)));
if (line) {
const methodAndPath = line.replace(/(\s+at (async )?|[^a-z0-9.:/\\\-_ ]|:\d+\)?$)/gi, '');
if (methodAndPath) {
const comment = `/* ${methodAndPath} */`;
if (arguments[0]?.query) {
arguments[0].query = `${comment} ${arguments[0].query}`;
} else {
arguments[0] = `${comment} ${arguments[0]}`;
}
}
}
return Sequelize.prototype.query.apply(this, arguments).catch((err) => {
err.fullStack = getStack();
throw err;
});
};
I'm trying to use the Steam Community (steamcommunity) npm package along with meteorhacks:npm Meteor package to retreive a user's inventory. My code is as follows:
lib/methods.js:
Meteor.methods({
getSteamInventory: function(steamId) {
// Check arguments for validity
check(steamId, String);
// Require Steam Community module
var SteamCommunity = Meteor.npmRequire('steamcommunity');
var community = new SteamCommunity();
// Get the inventory (730 = CSGO App ID, 2 = Valve Inventory Context)
var inventory = Async.runSync(function(done) {
community.getUserInventory(steamId, 730, 2, true, function(error, inventory, currency) {
done(error, inventory);
});
});
if (inventory.error) {
throw new Meteor.Error('steam-error', inventory.error);
} else {
return inventory.results;
}
}
});
client/views/inventory.js:
Template.Trade.helpers({
inventory: function() {
if (Meteor.user() && !Meteor.loggingIn()) {
var inventory;
Meteor.call('getSteamInventory', Meteor.user().services.steam.id, function(error, result) {
if (!error) {
inventory = result;
}
});
return inventory;
}
}
});
When trying to access the results of the call, nothing is displayed on the client or through the console.
I can add console.log(inventory) inside the callback of the community.getUserInventory function and receive the results on the server.
Relevant docs:
https://github.com/meteorhacks/npm
https://github.com/DoctorMcKay/node-steamcommunity/wiki/CSteamUser#getinventoryappid-contextid-tradableonly-callback
You have to use a reactive data source inside your inventory helper. Otherwise, Meteor doesn't know when to rerun it. You could create a ReactiveVar in the template:
Template.Trade.onCreated(function() {
this.inventory = new ReactiveVar;
});
In the helper, you establish a reactive dependency by getting its value:
Template.Trade.helpers({
inventory() {
return Template.instance().inventory.get();
}
});
Setting the value happens in the Meteor.call callback. You shouldn't call the method inside the helper, by the way. See David Weldon's blog post on common mistakes for details (section Overworked Helpers).
Meteor.call('getSteamInventory', …, function(error, result) {
if (! error) {
// Set the `template` variable in the closure of this handler function.
template.inventory.set(result);
}
});
I think the issue here is you're calling an async function inside your getSteamInventory Meteor method, and thus it will always try to return the result before you actually have the result from the community.getUserInventory call. Luckily, Meteor has WrapAsync for this case, so your method then simply becomes:
Meteor.methods({
getSteamInventory: function(steamId) {
// Check arguments for validity
check(steamId, String);
var community = new SteamCommunity();
var loadInventorySync = Meteor.wrapAsync(community.getUserInventory, community);
//pass in variables to getUserInventory
return loadInventorySync(steamId,730,2, false);
}
});
Note: I moved the SteamCommunity = Npm.require('SteamCommunity') to a global var, so that I wouldn't have to declare it every method call.
You can then just call this method on the client as you have already done in the way chris has outlined.
I try to develop a simple flash function for expressjs (please no links to existing libraries, I know them).
One single issue does headache. If i assign req.session.flash to res.locals.messages I want to make it that the array is set to [] if you read it. Background: If a jade template iterate through it, after the iteration it shall be empty, otherwise you see old messages again.
Code so far:
app.use(function(req, res, next) {
assert(req.session, 'req.session is required!')
if (!util.isArray(req.session.flash)) {
req.session.flash = []
}
console.log(req.session.flash)
res.locals.messages = function() {
var flash = req.session.flash
req.session.flash = []
return flash
}
res.flash = function(type, msg) {
console.log('res.flash')
req.session.flash.push({
type: type,
msg: msg
})
}
next()
})
Its gona work, but the problem is I have to execute a function in jade like
for flash in messages()
.alert.alert-danger
strong= flash.msg
but I want it more like that:
for flash in messages
.alert.alert-danger
strong= flash.msg
without the ()
Any ideas how to get it works like that?
Edit:
One approach is near to the solution, but not complete yet.
if (!res.locals.messages) {
Object.defineProperty(res.locals, 'messages', {
get: function() {
var flash = req.session.flash
req.session.flash = []
console.log('flash in getter', flash)
return flash
}
})
}
The prolem is, I get the error
Cannot read property 'length' of undefined
I think you might just need a undefined check:
Object.defineProperty(res.locals, 'messages', {
get: function() {
var flash = req.session.flash || [];
req.session.flash = []
console.log('flash in getter', flash)
return flash
}
})
But in all honesty I would prefer the messages() approach. Javascript isn't a land where people will typically expect object attributes to behave as if they were C# attributes with getters/setters and what not. Making messages a function makes this clearer to people reading your code.