I'm using the last version of nightwatch I had some issues when a Page Object can have another Page Object as 'field'.
I develop PageObjects for the header, footer and for html elements related for a login action as below:
menu.js
var commands = {
// commands for the menu
}
module.exports = {
elements: {
// menu html elements selectors, i.e.
someElementInHeader : '.cssSelector'
},
commands: [commands]
}
footer.js
module.exports = {
elements: {
// footer html elements selectors
}
}
But I need to embed these two Page Objects in a third Page Object, as example, a login page that contains the same menu and the same footer.
loginPage.js
// import the menu and footer js some way
module.exports = {
url: function() {
// get the url from configuration
return this.api....
}
// should have the elements from menu and footer, along with this page elements
elements: { ... },
// should contain the commands from menu and some commands from the page itself
commands: [{ ... }]
}
I tried to use a constructor for the loginPage Page Object, but it's not working.
Now I'm using this approach for the loginPage.js, but I don't think that is the better one:
// the header.js is a Page Object for nightwatch because it's located on the page objects path
const header = require('../common/header.js');
/**
* commands related to this page
*/
let _commands = [];
// add comands from dependencies
_commands = _commands.concat(header.commands);
/**
* elements related to this page
*/
let _elements = {};
// add elements from dependencies
Object.assign(_elements, header.elements);
/**
* page object configuration
*/
module.exports = {
url: function() {
return this.api.globals.baseUrl + '/login';
},
elements: _elements,
commands: _commands
};
And this is an example for tests (simplelogin.spec.js):
var customer = null;
module.exports = {
// set tests parameters
before : function (browser) {
var loginPage = browser.page.pages.loginPage();
browser
.useCss()
.pause(1000);
customer = loginPage.navigate();
},
// check for a basic element in header is present because it's needed
'a basic element should be present' : function (browser) {
customer.expect.element('#someElementInHeader').to.be.present;
},
after : function (browser) {
browser.end();
}
}
Even though I'm reusing the code for the the header, I didn't like the design and the readibility of the code for the loginPage.js... also, if a lot of page objects is needed in this page object for further testing, it will became very verbose.
Any ideas?
Related
I'm currently creating a 'smartobject' widget. In the widgets dialog, the user can choose a 'smartobject', which simply put, generates some html, which should be added to the editor. Here comes the tricky part: the html sometimes div elements and sometimes simply span elements. In the case of the div variant, the widget should be wrapped in a div 'template'. In the case of a span variant, the widget should be wrapped in a span and the html should be added 'inline'.
In the widgets API I see the following way to define a template:
editor.widgets.add('smartobject', {
dialog: 'smartobject',
pathName: lang.pathName,
template: '<div class="cke_smartobject"></div>', // <------
upcast: function(element) {
return element.hasClass('smartObject');
},
init: function() {
this.setData('editorHtml', this.element.getOuterHtml());
},
data: function() {
var editorHtml = this.data.editorHtml;
var newElement = new CKEDITOR.dom.element.createFromHtml(editorHtml);
newElement.copyAttributes(this.element);
this.element.setText(newElement.getText());
}
});
But in my case, the template is more dynamic: sometimes a div and sometimes the span will do the correct thing..
How can I fix this without needing to create two widgets which will do the exact same thing, with only the wrapping element as difference?
I've already tried to replace the entire element in the 'data' method, like:
newElement.replace(this.element);
this.element = newElement;
But this seemed not supported: resulted in undefined errors after calling editor.getData().
I'm using ckeditor v4.5.9
Thanks for your help!
It seems I got it working (with a workaround).
The code:
CKEDITOR.dialog.add('smartobject', this.path + 'dialogs/smartobject.js');
editor.widgets.add('smartobject', {
pathName: lang.pathName,
// This template is needed, to activate the widget logic, but does nothing.
// The entire widgets html is defined and created in the dialog.
template: '<div class="cke_smartobject"></div>',
init: function() {
var widget = this;
widget.on('doubleclick', function(evt) {
editor.execCommand('smartobject');
}, null, null, 5);
},
upcast: function(element) {
return element.hasClass('smartObject');
}
});
// Add a custom command, instead of using the default widget command,
// otherwise multiple smartobject variants (div / span / img) are not supported.
editor.addCommand('smartobject', new CKEDITOR.dialogCommand('smartobject'));
editor.ui.addButton && editor.ui.addButton('CreateSmartobject', {
label: lang.toolbar,
command: 'smartobject',
toolbar: 'insert,5',
icon: 'smartobject'
});
And in the dialog, to insert code looks like:
return {
title: lang.title,
minWidth: 300,
minHeight: 80,
onOk: function() {
var element = CKEDITOR.dom.element.createFromHtml(smartobjectEditorHtml);
editor.insertElement(element);
// Trigge the setData method, so the widget html is transformed,
// to an actual widget!
editor.setData(editor.getData());
},
...etc.
UPDATE
I made the 'onOk' method a little bit better: the smartobject element is now selected after the insertion.
onOk: function() {
var element = CKEDITOR.dom.element.createFromHtml(smartobjectEditorHtml);
var elementId = "ckeditor-element-" + element.getUniqueId();
element.setAttribute("id", elementId);
editor.insertElement(element);
// Trigger the setData method, so the widget html is transformed,
// to an actual widget!
editor.setData(editor.getData());
// Get the element 'fresh' by it's ID, because the setData method,
// makes the element change into a widget, and thats the element which should be selected,
// after adding.
var refreshedElement = CKEDITOR.document.getById(elementId);
var widgetWrapperElement = CKEDITOR.document.getById(elementId).getParent();
// Better safe then sorry: if the fresh element doesn't have a parent, simply select the element itself.
var elementToSelect = widgetWrapperElement != null ? widgetWrapperElement : refreshedElement;
// Normally the 'insertElement' makes sure the inserted element is selected,
// but because we call the setData method (to ensure the element is transformed to a widget)
// the selection is cleared and the cursor points to the start of the editor.
editor.getSelection().selectElement(elementToSelect);
},
So in short, I partially used the widget API for the parts I wanted:
- Make the html of the widget not editable
- Make it moveable
But I created a custom dialog command, which simply bypasses the default widget insertion, so I can entirely decide my own html structure for the widget.
All seems to work like this.
Any suggestions, to make it better are appreciated:)!
As suggested in this ckeditor forum thread, the best approach would be to set the template to include all possible content elements. Then, in the data function, remove the unnecessary parts according to your specific logic.
I am lost between the possibilities offered to handle this case: let's say we have the following constraints:
Knockout
SPA with Sammy.js - Html loaded via Ajax
My page:
+-------------------------------+
| #navigation |
+---------+---------------------+
| #sidebar| #content |
| | |
| | |
| | |
+---------+---------------------+
Currently, I have one appViewModel which handle the data-bind for all the shared elements of my website: #navigation and #sidebar. This appViewModel has observable used on every pages of my website.
appViewModel = function () {
var self = this;
self.sidebarItemArray = ko.observableArray([x, y, z]);
self.currentRoute = ko.observable();
...
self.updateView = function(path, currentRoute) {
return $.get(path, function( data ) {
var $data = $(data);
// Updates #content, TITLE and update the currentRoute observable.
$( '#content' ).replaceWith($data.find('#content'));
document.title = $data.filter('title').text();
self.currentRoute(currentRoute);
}, 'html');
}
Sammy(function() {
this.get(':link'', function() {
self.updateView(this.path, this.params.link);
});
}).run();
}
ko.applyBindings(new appViewModel());
Now, let's say that #content is a piece of DOM loaded through an Ajax Call. Each time a user click a link inside #navigation or #sidebar, Sammy.js intercept it and then update #content. The problem is that the new DOM inside #content has data-bindings itself.
1) First, should I use the html data-bind on #content, replaceWith(as above) or the template binding with custom function to get the template?
(http://knockoutjs.com/documentation/template-binding.html#note-5-dynamically-choosing-which-template-is-used)? What is the best practice here?
2) Should Sammy necessary lives inside the appViewModel as in the documentation or elsewhere is just fine?
3) Once the updateView method is completed, how would you bind the new DOM? Like below? Isn't there a risk of rebinding some DOM because ko.applyBindings has already been called without second argument?
ko.applyBindings(new routeSpecificViewModel() , document.getElementById("content"));
I am thankful for your help.
One simple solution is to make the page's viewmodel an observable, and load it ondemand. Use a variable to record if ko.applyBindings has been called. Example from the knockout-spa framework:
/*! knockout-spa (https://github.com/onlyurei/knockout-spa) * Copyright 2015-2016 Cheng Fan * MIT Licensed (https://raw.githubusercontent.com/onlyurei/knockout-spa/master/LICENSE) */
define(['app/shared/root-bindings', 'framework/page-disposer', 'ko', 'sugar'], function (
RootBindings, PageDisposer, ko) {
var initialRun = true;
var Page = {
init: function (name, data, controller, path) {
Page.loading(false);
name = name.toLowerCase();
if ((Page.page().name == name) && (Page.page().data == data)) { // if the requested page is the same page, immediately call controller without going further
if (controller) {
controller(data);
}
document.title = Page.title();
if (Page.initExtra) {
Page.initExtra(name, data, controller);
}
return data;
}
var autoDispose = (Page.page().data.dispose && Page.page().data.dispose(Page)) || true; // if the requested page is not the same page, dispose current page first before swap to the new page
if (autoDispose !== false) {
// auto-dispose page's exposed observables and primitive properties to initial values. if not desired, return
// false in dispose function to suppress auto-disposal for all public properties of the page, or make the
// particular properties private
PageDisposer.dispose(Page.page().data);
}
PageDisposer.init(data); //store initial observable and primitive properties values of the page
var initialized = (data.init && data.init(Page)) || true; // init view model and call controller (optional) before template is swapped-in
if (initialized === false) {
return false; // stop initialization if page's init function return false (access control, etc.)
}
if (controller) {
controller(data);
}
Page.pageClass([name, ('ontouchstart' in document.documentElement) ? 'touch' : 'no-touch'].join(' '));
Page.page({
name: name,
data: data,
path: path
}); // to test if template finished rendering, use afterRender binding in the template binding
document.title = Page.title();
if (Page.initExtra) {
Page.initExtra(name, data, controller); // useful for common init tasks for all pages such as anaylitics page view tracking, can be set in RootBindings
}
if (initialRun) {
ko.applyBindings(Page, document.getElementsByTagName('html')[0]); // apply binding at root node to be able to bind to anywhere
initialRun = false;
}
return data;
},
page: ko.observable({
name: '', // name of the page - auto-set by the framework, no need to worry
data: {
init: function () {}, // preparation before the page's template is rendered, such as checking access control, init/instantiate modules used by the page, etc.
dispose: function () {} // properly dispose the page to prevent memory leaks and UI leftovers (important for SPA since page doesn't refresh between page views) - remove DOM element event listeners, dispose knockout manual subscriptions, etc.
}
}),
pageClass: ko.observable(''),
loading: ko.observable(false),
title: function () {
return Page.page().name.titleize(); // override in RootBindings as needed
}
};
Object.merge(Page, RootBindings); // additional root bindings as needed by the app
return Page;
});
A mini but full-fledged SPA framework built on top of Knockout, Require, Director, jQuery, Sugar.
https://github.com/onlyurei/knockout-spa
Live Demo: https://knockout-spa.mybluemix.net
The following function is called when my page loads to set the default src and data attributes for an image element, and can also be called in the following click event handler to change the elements src and data attributes on the fly. The function sets the default attributes correctly when the page loads, and also in the click handler (when I inspect the image element the attributes have been changed) however when I select the element the default attributes will not change no matter how many times I run the click event. Does this have something to do with my callback?
function changepics(newbreed) {
var pic = "<?php echo $breedpic ?>";
var path = "../Mazi/images/";
var getpic = path+pic;
// set default
$('#breedimage').attr({
"src":getpic,
"data":pic
});
//change default
if(newbreed) {
$.post('myquery.php', {"newpic" : newbreed}, function(result) {
var newpic = result;
var newpic = path+newpic;
$('#breedimage').attr({
"src":newpic,
"data":newbreed
});
});
setfilter(currpage, newbreed);
}
}
$('#table2').on("click", ".breed", function() {
newbreed = $(this).text();
changepics(newbreed);
});
When I made my selection inside the success handler of $.post I was able to retrieve the attributes of the new image.
I've this code to load content in a div #target with some animation. Works fine but i don't know how implement code to change link and url with #hash!
How can I do this?
the code:
$(document).ready(function(){
$("#target").addClass('hide');
$('.ajaxtrigger').click(function() {
var pagina = $(this).attr('href');
if ($('#target').is(':visible')) {
}
$("#target").removeClass('animated show page fadeInRightBig').load(pagina,
function() {
$("#target").delay(10).transition({ opacity: 1 })
.addClass('animated show page fadeInRightBig');
}
);
return false;
});
});
Try to use any javascript router. For example, router.js.
Modify you code like this(I didn't check if this code work, but I think idea should be clear):
$(document).ready(function(){
var router = new Router();
//Define route for your link
router.route('/loadpath/:href', function(href) {
console.log(href);
if ($('#target').is(':visible')) {
$("#target").removeClass('animated show page fadeInRightBig').load(href,
function() {
$("#target").delay(10).transition({ opacity: 1 })
.addClass('animated show page fadeInRightBig');
}
);
}
});
router.route('', function(){ console.log("default route")});
$("#target").addClass('hide');
// Instead of loading content in click handler,
// we just go to the url from href attribute with our custom prefix ('/loadpath/').
// Router will do rest of job for us. It will trigger an event when url hash is
// changes and will call our handler, that will load contents
// from appropriate url.
$('.ajaxtrigger').click(function() {
router.navigate('/loadpath/' + $(this).attr('href'));
return false;
});
});
As far as I can tell, Backbone.js view represents DOM element. I take it from existing DOM or create it on the fly in el attribute.
In my case, I want to take it from server with AJAX request because I'm using Django templates and don't want to rewrite everything to JavaScript templates.
So I define el function that performs AJAX request.
el: function() {
model.fetch().success(function(response) {
return response.template
})
}
Of course, it does NOT work because AJAX request is executed asynchronous.
This means that I don't have el attribute and events does NOT work neither. Can I fix it?
Maybe the Backbone.js framework isn't the right tool for my needs? The reason I want to use that was to have some structure for the code.
P.S. I'm new to Backbone.js.
Do your ajax request from another view, or directly after the page load using jquery directly, and after you've downloaded your template, THEN instantiate your backbone view class with the proper id/el or whatever (depending on where you stored your ajax fetched template). Depending on your use-case, this may or may not be a sensible approach.
Another, perhaps more typical approach, would be to set up your view with some placeholder element (saying "loading" or whatever), then fire off the ajax, and after the updated template has been retrieved, then update your view accordingly (replace the placeholder with the actual template you requested).
When/if you update your view with new/other DOM elements, you need to call the view's delegateEvents method to rebind your events to the new elements, see:
http://backbonejs.org/#View-delegateEvents
I came across a similar requirement. In my instance, I was running asp.net and wanted to pull my templates from user controls. The first thing I would recommend is looking into Marionette because it will save you from writing a lot of boiler plate code in Backbone. The next step is to override how your templates are loaded. In this case I created a function that uses Ajax to retrieve the HTML from the server. I found an example of this function where they were using it to pull down html pages so I did a little modification so I can make MVC type requests. I can't remember where I found the idea from; otherwise, I would give the link here.
function JackTemplateLoader(params) {
if (typeof params === 'undefined') params = {};
var TEMPLATE_DIR = params.dir || '';
var file_cache = {};
function get_filename(name) {
if (name.indexOf('-') > -1) name = name.substring(0, name.indexOf('-'));
return TEMPLATE_DIR + name;
}
this.get_template = function (name) {
var template;
var file = get_filename(name);
var file_content;
var result;
if (!(file_content = file_cache[name])) {
$.ajax({
url: file,
async: false,
success: function (data) {
file_content = data; // wrap top-level templates for selection
file_cache[name] = file_content;
}
});
}
//return file_content.find('#' + name).html();
return file_content;
}
this.clear_cache = function () {
template_cache = {};
};
}
The third step would be to override Marionette's method to load templates. I did this in the app.addInitializer method. Here I am initializing my template loader and setting it's directory to a route handler. So when I want to load a template, I just set the template: "templatename" in my view and Backbone will load the template from api/ApplicationScreens/templatename. I am also overriding my template compiling to use Handlebars because ASP.net is not impressed with the <%= %> syntax.
app.JackTemplateLoader = new JackTemplateLoader({ dir: "/api/ApplicationScreens/", ext: '' });
Backbone.Marionette.TemplateCache.prototype.loadTemplate = function (name) {
if (name == undefined) {
return "";
} else {
var template = app.JackTemplateLoader.get_template(name);
return template;
}
};
// compiling
Backbone.Marionette.TemplateCache.prototype.compileTemplate = function (rawTemplate) {
var compiled = Handlebars.compile(rawTemplate);
return compiled;
};
// rendering
Backbone.Marionette.Renderer.render = function (template, data) {
var template = Marionette.TemplateCache.get(template);
return template(data);
}
Hopefully this helps. I've been working on a large dynamic website and it is coming along very nicely. I am constantly being surprised by the overall functionality and flow of using Marionette and Backbone.js.