I'm having some trouble understanding class scope and how to utilize objects, prototypes, etc, within the system I'm having. It seems to me that I'm doing things correctly, but it isn't working and I'm hoping someone can explain it to me or get me pointed in the right direction.
So here's the situation:
I'm trying to create a generic page object and verification object that would handle an input box with an associated text label. Things do appear to work if I have the verification in the same class as the page object structure, but I've recently learned that is bad design.
This is an Angular 2 rc1 site. I'm using protractor 3.3.0 and have ensured that the project version of selenium and chromedriver are up-to-date by using webdriver-manager.
So here's what I've done (filenames are in the comments at the start of each section):
'use strict';
/*
* InputLabelPageObject.js
*
* This object will provide basic methods for an Input box with an attached label.
* It is expected that the label will have an element called "label" and an element called "input"
*/
module.exports = InputLabelPageObject;
/**
* Create an object that will provide methods for an input/label combination of elements.
*
* #param container The selector for the __container of the input/label combination of elements.
*/
function InputLabelPageObject(container) {
this.Container = container;
}
InputLabelPageObject.prototype = {
constructor: InputLabelPageObject,
/**
* Return the element for the label of the input/label combination of elements.
*
* #returns {protractor.element}
*/
getLabel: function () {
return this.Container.$('label');
},
/**
* Return the element for the input of the input/label combination of elements.
*
* #returns {ElementFinder}
*/
getInput: function () {
return this.Container.$('input');
},
/**
* Return the text shown in the input of the input/label combination of elements.
*
* #returns {Promise}
*/
getValue: function () {
return this.getInput().getAttribute('value');
},
/**
* Get the placeholder text shown in the input of the input/label combination of elements.
*
* #returns {Promise}
*/
getPlaceholder: function () {
return this.getInput().getAttribute('placeholder');
},
/**
* Clears the input element then puts the text from data into the input element.
*
* #param data The text to be entered into the input element.
*/
sendKeys: function (data) {
var el = this.getInput();
el.clear().then(function () {
return el.sendKeys(data);
});
}
};
--
'use strict';
/*
* InputLabelVerification.js
*
* Provide verification methods associated with an Input and Label
* combination of elements.
*/
module.exports = InputLabelVerifications;
var inputLabelPageObject;
function InputLabelVerifications(inputLabelPageObject) {
this.__setPageObject(inputLabelPageObject);
}
InputLabelVerifications.prototype = {
constructor: InputLabelVerifications,
__setPageObject: function (ilpo) {
inputLabelPageObject = ilpo;
},
/**
* Verify the text on the label of the input/label combination of elements.
*
* #param expected The expected text on the label.
*/
verifyText: function (expected) {
//console.log('Asserting text [' + expected + ']');
expect(inputLabelPageObject.getLabel()).toEqual(expected);
},
/**
* Verify the text shown in the input of the input/label combination of elements.
*
* #param expected The expected text in the input element.
*/
verifyValue: function (expected) {
//console.log('Asserting input value [' + expected + ']');
expect(inputLabelPageObject.getValue()).toEqual(expected);
},
/**
* Verify the placeholder text shown in the input of the input/label combination of elements.
*
* #param expected The expected text of the placeholder.
*/
verifyPlaceholder: function (expected) {
//console.log('Verifying placeholder text [' + expected + ']');
expect(inputLabelPageObject.getPlaceholder()).toEqual(expected);
}
};
--
'use strict';
/*
* LoginPageObject.js
*
*/
var InputLabelPageObject = require('./generics/InputLabelPageObject.js');
module.exports = LoginPageObject;
var __container = $('login-component');
var username = new InputLabelPageObject(__container.$('form:nth-child(2) > div:nth-child(1)'));
var password = new InputLabelPageObject(__container.$('form:nth-child(2) > div:nth-child(2)'));
/**
* Create an object that contains the methods necessary to perform actions against the LoginPageObject page.
*
* #param url The base URL string. If not undefined, it will load the url+'/login' page.
* #constructor new LoginPageObject('http://localhost:9000');
*/
function LoginPageObject(url) {
if (url) {
this.loadPage(url)
}
}
LoginPageObject.prototype = {
constructor: LoginPageObject,
loadPage: function (url) {
url = url + '/login';
console.log('Loading page: '+ url);
browser.get(url);
},
welcome: {
/**
* Return the element for the Welcome text
*
* #returns {ElementFinder}
*/
get: function () {
return __container.$('section:first-child h1:first-child');
},
},
/**
* Return an InputLabelPageObject object specific for the username input and label elements.
*/
username: username,
/**
* Return an InputLabelPageObject object specific for the password input and label elements.
*/
password: password,
loginButton: {
/**
* Return the element for the login button.
*
* #returns {ElementFinder}
*/
get: function () {
return __container.$('form > button');
},
/**
* Click the LoginPageObject button.
* #returns {*|void|webdriver.promise.Promise<void>|ActionSequence|!webdriver.promise.Promise.<void>}
*/
click: function () {
return this.get().click();
}
}
};
--
'use strict';
/*
* LoginPageVerifications.js
*/
var LoginPageObject = require('../pageObjects/LoginPageObject');
var verifyText = require('./generics/VerifyText');
var inputLabelVerifications = require('./generics/InputLabelVerifications');
module.exports = LoginPageVerifications;
var __loginPageObject = new LoginPageObject();
function LoginPageVerifications(url) {
if (url) {
__loginPageObject = new LoginPageObject(url);
}
}
LoginPageVerifications.prototype = {
constructor: LoginPageVerifications,
loginPageObject: new LoginPageObject(),
welcome: {
verifyText: function (expected) {
verifyText(__loginPageObject.welcome.get(), expected);
}
},
username: new inputLabelVerifications(__loginPageObject.username),
password: new inputLabelVerifications(__loginPageObject.password),
loginButton: {
verifyText: function (expected) {
verifyText(__loginPageObject.loginButton.get(), expected);
}
},
/**
* Performs the actions of logging in. That is, enter the username and password values,
* then click the LoginPageObject button. This does *not* verify page load.
*
* #param username The username to login with.
* #param password The password to login with.
*/
doLogin: function (username, password) {
var uPromise = __loginPageObject.username.sendKeys(username);
var pPromise = __loginPageObject.password.sendKeys(password);
protractor.promise.asap(this.username.verifyValue(username));
protractor.promise.asap(this.password.verifyValue(password));
protractor.promise.all([uPromise, pPromise]).then(this.loginButton.click());
},
/**
* Verifies all page elements' text or other default attributes.
*
* #param welcomeText The expected Welcome text
* #param userText The expected username label text.
* #param userPlaceholder The expected username's input element's placeholder text.
* #param passText The expected password label text.
* #param passPlaceholder The expected password's input element's placeholder text.
* #param loginText The expected login button text.
*/
verifyPage: function (welcomeText, userText, userPlaceholder, passText, passPlaceholder, loginText) {
this.welcome.verifyText(welcomeText);
this.username.verifyText(userText);
this.username.verifyPlaceholder(userPlaceholder);
this.password.verifyText(passText);
this.password.verifyPlaceholder(passPlaceholder);
this.loginButton.verifyText(loginText);
}
};
--
'use strict';
/*
* login-spec.js
*/
var LoginPageVerifications = require('../components/actions/LoginPageVerifications');
var myUrl = 'http://localhost:3000';
describe('My Login Page test', function() {
var loginPage;
beforeAll(function() {
loginPage = new LoginPageVerifications(myUrl);
});
it('should verify username input and label values', function() {
var welcomeText = 'Thank you for visiting my login page';
var userText = 'Username';
var userPlaceholder = 'Enter your username';
var passText = 'Password';
var passPlaceholder = 'Enter your password';
var loginText = 'Login';
loginPage.username.verifyText(userText);
// loginPage.verifyPage(welcomeText, userText, userPlaceholder, passText, passPlaceholder, loginText);
});
});
The results that I'm usually seeing:
If in InputLabelVerification.js I leave out var inputLabelPageObject or try to set the value only in the constructor function, I get Failed: Cannot read property 'getLabel' of undefined. So, I've figured that I must set it the way I have it shown above.
The closest I seem to get is when I get the following response:
A Jasmine spec timed out. Resetting the WebDriver Control Flow.
F
Failures:
1) My Login Page test should verify username input and label values
Expected ({ ptor_: ({ controlFlow: Function, schedule: Function,
setFileDetector: Function, getSession: Function, getCapabilities: Function,
quit: Function, actions: Function, touchActions: Function,
executeScript: Function, executeAsyncScript: Function, call: Function,
wait: Function, sleep: Function, getWindowHandle... }) }) to equal 'Username'.
In the LoginPageVerification.js, I've tested and ensured that the other verifications (welcome and loginButton) work fine.
Also, if from login-spec.js I add this line:
expect(loginPage.loginPageObject.username.getLabel().getText()).toEqual(userText);
this expect passes the test.
I've got the answer. And now I feel silly. I'll post as the answer when I can in 2 days. (This only took me several days to get it right.)
Essentially, I'm calling .getLabel() from the InputLabelPageObject from InputLabelVerifications, which returns an ElementFinder... it does not return a string as is needed. (oops)
In InputLabelPageObject, I added the following line:
getText: function() {
this.getLabel().getText();
}
and then called that function in the InputLabelVerifications class,
verifyText: function (expected) {
//console.log('Asserting text [' + expected + ']');
expect(inputLabelPageObject.getText()).toEqual(expected);
},
and all is working as expected.
Related
Here is the log:
build.js:8325 Uncaught DOMException: Failed to read the 'localStorage' property from 'Window': Access is denied for this document.
Source code :
/**
* LocalStorageMemory save and load js objects in localStorage.
*/
var localStorageMemory = {
localStorage: window.localStorage,
/**
* save Put the object into storage.
* #example Usage : save("MyObjectKey", myObject )
* #method save
* #param {String} Name Name of localstorage key
* #param {object} Value Any object we can store.
* #return {false | object} What ever we are stored intro localStorage.
*/
save: function(name, obj) {
try {
return localStorage.setItem(name, JSON.stringify(obj));
} catch (e) {
console.log("Something wrong in LocalStorageMemory class , method save! -> ", e);
return false;
}
},
/**
* Load saved object from storage. Retrieve the object from storage or
* return false.
* #example Usage : var giveMeMyObject = load("MyObjectKey")
* #function load
* #param {String} Name Name of localstorage key
* #return {false | object} What ever we are stored intro localStorage.
*/
load: function(name) {
if (localStorage.getItem(name) === "undefined" || localStorage.getItem(name) == null || localStorage.getItem(name) === "") {
console.warn("LocalStorageMemory method load return's: ", localStorage.getItem(name));
return false;
} else {
return JSON.parse(localStorage.getItem(name));
}
}
}
/**********CREATE INSTANCE***********/
var saveAnyDataObj = {
name: "wins",
numberOfWins: c++
};
localStorageMemory.save("MyObject1", saveAnyDataObj);
var getMyMemory = localStorageMemory.load("MyObject1");
console.log("MyObject1 = ", getMyMemory);
var c = 0;
setInterval(function() {
var saveAnyDataObj = {
name: "wins",
numberOfWins: c++
};
localStorageMemory.save("MyObject1", saveAnyDataObj);
getMyMemory = localStorageMemory.load("MyObject1");
document.getElementById("result").innerHTML = getMyMemory.numberOfWins;
}, 1000);
Try it :
https://codepen.io/zlatnaspirala/pen/NRYKzJ
It's one of many sandbox values for iframes: allow-same-origin. There is no way you can defeat it.
My guess why JS Fiddle and Codepen don't allow it since one script could put some form of secrets into localstorage and another (potentially malicious) script could read those as they are on the same origin.
You can read why Stack Exchange disallows it here: https://meta.stackoverflow.com/a/345386/23528
My method returns a a bill object with all of User object.
I would like that I return only bill object and User with two attributes in entity. I use TypeORM
/**
* Returns a bills by account bill
*/
async getByAccountBill(
accountBill: string,
id?: number
): Promise<Object | undefined> {
const userService = new UserService();
const user = await userService.getById(id);
const bills = await this.billRepository.find({
select: ["accountBill"],
where: {
accountBill: Like(`${accountBill}%`),
user: Not(`${user.id}`)
},
relations: ["user"] // I get All object Entity (userId, password, login...) I want to only name and surname
});
if (bills) {
return bills;
} else {
return undefined;
}
}
It's bit late but for all others who visit this page may be helpful
there is a option available in typeorm so we can get the result how we want.
return this.repository.find({
relations: ['user'],
loadRelationIds: true,
where: { ... },
order: { ... }
});
You can use querybuilder which is one of the most powerful tool of TypeOrm, to do so.
const values = this.billRepository.createQueryBuilder("bill")
.leftJoinAndSelect("bill.user", "user")
.where("bill.accountBill LIKE :accountBill", {accountBill})
.andWhere("user.id = :userId", {userId: user.id})
.select(["user.name", "user.surname"])
.execute();
// NOTE
// .execute() will return raw results.
// To return objects, use .getMany()
In case someone interested, short list of code related to relation and links to repo...
https://github.com/typeorm/typeorm/blob/master/src/find-options/FindOptionsUtils.ts
/**
* Applies give find options to the given query builder.
*/
static applyOptionsToQueryBuilder<T>(qb: SelectQueryBuilder<T>, options: FindOneOptions<T> | FindManyOptions<T> | undefined): SelectQueryBuilder<T>;
...
if (options.loadRelationIds === true) {
qb.loadAllRelationIds();
}
else if (options.loadRelationIds instanceof Object) {
qb.loadAllRelationIds(options.loadRelationIds);
}
https://github.com/typeorm/typeorm/blob/master/src/query-builder/SelectQueryBuilder.ts
/**
* Loads all relation ids for all relations of the selected entity.
* All relation ids will be mapped to relation property themself.
* If array of strings is given then loads only relation ids of the given properties.
*/
loadAllRelationIds(options?: { relations?: string[], disableMixedMap?: boolean }): this { // todo: add skip relations
this.expressionMap.mainAlias!.metadata.relations.forEach(relation => {
if (options !== undefined && options.relations !== undefined && options.relations.indexOf(relation.propertyPath) === -1)
return;
this.loadRelationIdAndMap(
this.expressionMap.mainAlias!.name + "." + relation.propertyPath,
this.expressionMap.mainAlias!.name + "." + relation.propertyPath,
options
);
});
return this;
}
/**
* LEFT JOINs relation id and maps it into some entity's property.
* Optionally, you can add condition and parameters used in condition.
*/
loadRelationIdAndMap(mapToProperty: string, relationName: string, options?: { disableMixedMap?: boolean }): this;
/**
* LEFT JOINs relation id and maps it into some entity's property.
* Optionally, you can add condition and parameters used in condition.
*/
loadRelationIdAndMap(mapToProperty: string, relationName: string, alias: string, queryBuilderFactory: (qb: SelectQueryBuilder<any>) => SelectQueryBuilder<any>): this;
/**
* LEFT JOINs relation id and maps it into some entity's property.
* Optionally, you can add condition and parameters used in condition.
*/
loadRelationIdAndMap(mapToProperty: string,
relationName: string,
aliasNameOrOptions?: string|{ disableMixedMap?: boolean },
queryBuilderFactory?: (qb: SelectQueryBuilder<any>) => SelectQueryBuilder<any>): this {
const relationIdAttribute = new RelationIdAttribute(this.expressionMap);
relationIdAttribute.mapToProperty = mapToProperty;
relationIdAttribute.relationName = relationName;
if (typeof aliasNameOrOptions === "string")
relationIdAttribute.alias = aliasNameOrOptions;
if (aliasNameOrOptions instanceof Object && (aliasNameOrOptions as any).disableMixedMap)
relationIdAttribute.disableMixedMap = true;
relationIdAttribute.queryBuilderFactory = queryBuilderFactory;
this.expressionMap.relationIdAttributes.push(relationIdAttribute);
if (relationIdAttribute.relation.junctionEntityMetadata) {
this.expressionMap.createAlias({
type: "other",
name: relationIdAttribute.junctionAlias,
metadata: relationIdAttribute.relation.junctionEntityMetadata
});
}
return this;
}
https://github.com/typeorm/typeorm/blob/master/src/query-builder/relation-id/RelationIdAttribute.ts
/**
* Stores all join relation id attributes which will be used to build a JOIN query.
*/
export class RelationIdAttribute {
// -------------------------------------------------------------------------
// Public Properties
// -------------------------------------------------------------------------
/**
* Alias of the joined (destination) table.
*/
alias?: string;
/**
* Name of relation.
*/
relationName: string;
/**
* Property + alias of the object where to joined data should be mapped.
*/
mapToProperty: string;
/**
* Extra condition applied to "ON" section of join.
*/
queryBuilderFactory?: (qb: SelectQueryBuilder<any>) => SelectQueryBuilder<any>;
/**
* Indicates if relation id should NOT be loaded as id map.
*/
disableMixedMap = false;
...
I have the following code "js", and the browser puts this error "in line 8, "undefined is not a function".
The line 8 is: $('.wysiwyg', context).once('wysiwyg', function () {
I have no idea about what happens!
Any suggestions? Thank you.
Drupal.behaviors.attachWysiwyg = {
attach: function (context, settings) {
// This breaks in Konqueror. Prevent it from running.
if (/KDE/.test(navigator.vendor)) {
return;
}
$('.wysiwyg', context).once('wysiwyg', function () {
if (!this.id || typeof Drupal.settings.wysiwyg.triggers[this.id] === 'undefined') {
return;
}
var $this = $(this);
var params = Drupal.settings.wysiwyg.triggers[this.id];
for (var format in params) {
params[format].format = format;
params[format].trigger = this.id;
params[format].field = params.field;
}
var format = 'format' + this.value;
// Directly attach this editor, if the input format is enabled or there is
// only one input format at all.
if ($this.is(':input')) {
Drupal.wysiwygAttach(context, params[format]);
}
// Attach onChange handlers to input format selector elements.
if ($this.is('select')) {
$this.change(function() {
// If not disabled, detach the current and attach a new editor.
Drupal.wysiwygDetach(context, params[format]);
format = 'format' + this.value;
Drupal.wysiwygAttach(context, params[format]);
});
}
// Detach any editor when the containing form is submitted.
$('#' + params.field).parents('form').submit(function (event) {
// Do not detach if the event was cancelled.
if (event.isDefaultPrevented()) {
return;
}
Drupal.wysiwygDetach(context, params[format], 'serialize');
});
});
},
detach: function (context, settings, trigger) {
var wysiwygs;
// The 'serialize' trigger indicates that we should simply update the
// underlying element with the new text, without destroying the editor.
if (trigger == 'serialize') {
// Removing the wysiwyg-processed class guarantees that the editor will
// be reattached. Only do this if we're planning to destroy the editor.
wysiwygs = $('.wysiwyg-processed', context);
}
else {
wysiwygs = $('.wysiwyg', context).removeOnce('wysiwyg');
}
wysiwygs.each(function () {
var params = Drupal.settings.wysiwyg.triggers[this.id];
Drupal.wysiwygDetach(context, params, trigger);
});
}
};
/**
* Attach an editor to a target element.
*
* This tests whether the passed in editor implements the attach hook and
* invokes it if available. Editor profile settings are cloned first, so they
* cannot be overridden. After attaching the editor, the toggle link is shown
* again, except in case we are attaching no editor.
*
* #param context
* A DOM element, supplied by Drupal.attachBehaviors().
* #param params
* An object containing input format parameters.
*/
Drupal.wysiwygAttach = function(context, params) {
if (typeof Drupal.wysiwyg.editor.attach[params.editor] == 'function') {
// (Re-)initialize field instance.
Drupal.wysiwyg.instances[params.field] = {};
// Provide all input format parameters to editor instance.
jQuery.extend(Drupal.wysiwyg.instances[params.field], params);
// Provide editor callbacks for plugins, if available.
if (typeof Drupal.wysiwyg.editor.instance[params.editor] == 'object') {
jQuery.extend(Drupal.wysiwyg.instances[params.field], Drupal.wysiwyg.editor.instance[params.editor]);
}
// Store this field id, so (external) plugins can use it.
// #todo Wrong point in time. Probably can only supported by editors which
// support an onFocus() or similar event.
Drupal.wysiwyg.activeId = params.field;
// Attach or update toggle link, if enabled.
if (params.toggle) {
Drupal.wysiwygAttachToggleLink(context, params);
}
// Otherwise, ensure that toggle link is hidden.
else {
$('#wysiwyg-toggle-' + params.field).hide();
}
// Attach editor, if enabled by default or last state was enabled.
if (params.status) {
Drupal.wysiwyg.editor.attach[params.editor](context, params, (Drupal.settings.wysiwyg.configs[params.editor] ? jQuery.extend(true, {}, Drupal.settings.wysiwyg.configs[params.editor][params.format]) : {}));
}
// Otherwise, attach default behaviors.
else {
Drupal.wysiwyg.editor.attach.none(context, params);
Drupal.wysiwyg.instances[params.field].editor = 'none';
}
}
};
/**
* Detach all editors from a target element.
*
* #param context
* A DOM element, supplied by Drupal.attachBehaviors().
* #param params
* An object containing input format parameters.
* #param trigger
* A string describing what is causing the editor to be detached.
*
* #see Drupal.detachBehaviors
*/
Drupal.wysiwygDetach = function (context, params, trigger) {
// Do not attempt to detach an unknown editor instance (Ajax).
if (typeof Drupal.wysiwyg.instances[params.field] == 'undefined') {
return;
}
trigger = trigger || 'unload';
var editor = Drupal.wysiwyg.instances[params.field].editor;
if (jQuery.isFunction(Drupal.wysiwyg.editor.detach[editor])) {
Drupal.wysiwyg.editor.detach[editor](context, params, trigger);
}
};
/**
* Append or update an editor toggle link to a target element.
*
* #param context
* A DOM element, supplied by Drupal.attachBehaviors().
* #param params
* An object containing input format parameters.
*/
Drupal.wysiwygAttachToggleLink = function(context, params) {
if (!$('#wysiwyg-toggle-' + params.field).length) {
var text = document.createTextNode(params.status ? Drupal.settings.wysiwyg.disable : Drupal.settings.wysiwyg.enable);
var a = document.createElement('a');
$(a).attr({ id: 'wysiwyg-toggle-' + params.field, href: 'javascript:void(0);' }).append(text);
var div = document.createElement('div');
$(div).addClass('wysiwyg-toggle-wrapper').append(a);
$('#' + params.field).after(div);
}
$('#wysiwyg-toggle-' + params.field)
.html(params.status ? Drupal.settings.wysiwyg.disable : Drupal.settings.wysiwyg.enable).show()
.unbind('click.wysiwyg', Drupal.wysiwyg.toggleWysiwyg)
.bind('click.wysiwyg', { params: params, context: context }, Drupal.wysiwyg.toggleWysiwyg);
// Hide toggle link in case no editor is attached.
if (params.editor == 'none') {
$('#wysiwyg-toggle-' + params.field).hide();
}
};
/**
* Callback for the Enable/Disable rich editor link.
*/
Drupal.wysiwyg.toggleWysiwyg = function (event) {
var context = event.data.context;
var params = event.data.params;
if (params.status) {
// Detach current editor.
params.status = false;
Drupal.wysiwygDetach(context, params);
// After disabling the editor, re-attach default behaviors.
// #todo We HAVE TO invoke Drupal.wysiwygAttach() here.
Drupal.wysiwyg.editor.attach.none(context, params);
Drupal.wysiwyg.instances[params.field] = Drupal.wysiwyg.editor.instance.none;
Drupal.wysiwyg.instances[params.field].editor = 'none';
Drupal.wysiwyg.instances[params.field].field = params.field;
$(this).html(Drupal.settings.wysiwyg.enable).blur();
}
else {
// Before enabling the editor, detach default behaviors.
Drupal.wysiwyg.editor.detach.none(context, params);
// Attach new editor using parameters of the currently selected input format.
params = Drupal.settings.wysiwyg.triggers[params.trigger]['format' + $('#' + params.trigger).val()];
params.status = true;
Drupal.wysiwygAttach(context, params);
$(this).html(Drupal.settings.wysiwyg.disable).blur();
}
}
/**
* Parse the CSS classes of an input format DOM element into parameters.
*
* Syntax for CSS classes is "wysiwyg-name-value".
*
* #param element
* An input format DOM element containing CSS classes to parse.
* #param params
* (optional) An object containing input format parameters to update.
*/
Drupal.wysiwyg.getParams = function(element, params) {
var classes = element.className.split(' ');
var params = params || {};
for (var i = 0; i < classes.length; i++) {
if (classes[i].substr(0, 8) == 'wysiwyg-') {
var parts = classes[i].split('-');
var value = parts.slice(2).join('-');
params[parts[1]] = value;
}
}
// Convert format id into string.
params.format = 'format' + params.format;
// Convert numeric values.
params.status = parseInt(params.status, 10);
params.toggle = parseInt(params.toggle, 10);
params.resizable = parseInt(params.resizable, 10);
return params;
};
/**
* Allow certain editor libraries to initialize before the DOM is loaded.
*/
Drupal.wysiwygInit();
// Respond to CTools detach behaviors event.
$(document).bind('CToolsDetachBehaviors', function(event, context) {
Drupal.behaviors.attachWysiwyg.detach(context, {}, 'unload');
});
})(jQuery);
Sounds like you might be missing a jquery file defining the "once" function. Check with Firebug for Firefox (or a similiar development tool) where you can see the requested files for each page - clear the browser cache, then check Firebug's Net tab when reloading the page. If any files are not listed with a HTTP status code of "200 OK", you'll need to check that the files exist and have read permissions so the server can hand them out.
Anyway try to change line 8 as:
$('.wysiwyg:not(.processed)', context).addClass('processed').each(function() {
Since the release of iOS6, my web app has hit a series of bugs, one of the worst being what I'm almost 100% positive is websql transactions being queued. When I first load the app in mobile safari( ipad ), the transactions work fine. Then, if I close safari and open it again the transactions seem to be queued and never execute.
If I open the dev tools and run a simple alert, the methods will fire, if I just hit reload the transactions work fine as well, or if I delay the running of the db transactions by 1sec or something it works fine as well.
I do not want to run a setTimeout to run the transactions.
Is this a caching issue that safari has now since implemented?
If anyone has ANY good ideas on how to fix this please answer below.
Thanks in advance.
It may not be bug. You may be using series of transaction unnecessarily. You could use mulitple requests per transaction. onsuccess callback, you can reuse the transaction. It should work. At the same time, limit number of requests per transaction. setTimeout should never necessary.
Here is how a single transaction is used to insert multiple objects
/**
* #param {goog.async.Deferred} df
* #param {string} store_name table name.
* #param {!Array.<!Object>} objects object to put.
* #param {!Array.<(Array|string|number)>=} opt_keys
*/
ydn.db.req.WebSql.prototype.putObjects = function (df, store_name, objects, opt_keys) {
var table = this.schema.getStore(store_name);
if (!table) {
throw new ydn.db.NotFoundError(store_name);
}
var me = this;
var result_keys = [];
var result_count = 0;
/**
* Put and item at i. This ydn.db.core.Storage will invoke callback to df if all objects
* have been put, otherwise recursive call to itself at next i+1 item.
* #param {number} i
* #param {SQLTransaction} tx
*/
var put = function (i, tx) {
// todo: handle undefined or null object
var out;
if (goog.isDef(opt_keys)) {
out = table.getIndexedValues(objects[i], opt_keys[i]);
} else {
out = table.getIndexedValues(objects[i]);
}
//console.log([obj, JSON.stringify(obj)]);
var sql = 'INSERT OR REPLACE INTO ' + table.getQuotedName() +
' (' + out.columns.join(', ') + ') ' +
'VALUES (' + out.slots.join(', ') + ');';
/**
* #param {SQLTransaction} transaction transaction.
* #param {SQLResultSet} results results.
*/
var success_callback = function (transaction, results) {
result_count++;
result_keys[i] = goog.isDef(out.key) ? out.key : results.insertId;
if (result_count == objects.length) {
df.callback(result_keys);
} else {
var next = i + ydn.db.req.WebSql.RW_REQ_PER_TX;
if (next < objects.length) {
put(next, transaction);
}
}
};
/**
* #param {SQLTransaction} tr transaction.
* #param {SQLError} error error.
*/
var error_callback = function (tr, error) {
if (ydn.db.req.WebSql.DEBUG) {
window.console.log([sql, out, tr, error]);
}
df.errback(error);
return true; // roll back
};
//console.log([sql, out.values]);
tx.executeSql(sql, out.values, success_callback, error_callback);
};
if (objects.length > 0) {
// send parallel requests
for (var i = 0; i < ydn.db.req.WebSql.RW_REQ_PER_TX && i < objects.length; i++) {
put(i, this.getTx());
}
} else {
df.callback([]);
}
};
Regarding transaction queue, it is better to handle by the application rather than by the SQLite for robustness. Basically we can watch transaction complete event before starting a new transaction. It is also fine to run multiple transactions as long as they are under control. Out of control will be opening transactions under a loop. Generally I will open only couple of transactions.
Here is how transaction is queued:
/**
* Create a new isolated transaction. After creating a transaction, use
* {#link #getTx} to received an active transaction. If transaction is not
* active, it return null. In this case a new transaction must re-create.
* #export
* #param {Function} trFn function that invoke in the transaction.
* #param {!Array.<string>} store_names list of keys or
* store name involved in the transaction.
* #param {ydn.db.TransactionMode=} opt_mode mode, default to 'readonly'.
* #param {function(ydn.db.TransactionEventTypes, *)=} oncompleted
* #param {...} opt_args
* #override
*/
ydn.db.tr.TxStorage.prototype.transaction = function (trFn, store_names, opt_mode, oncompleted, opt_args) {
//console.log('tr starting ' + trFn.name);
var scope_name = trFn.name || '';
var names = store_names;
if (goog.isString(store_names)) {
names = [store_names];
} else if (!goog.isArray(store_names) ||
(store_names.length > 0 && !goog.isString(store_names[0]))) {
throw new ydn.error.ArgumentException("storeNames");
}
var mode = goog.isDef(opt_mode) ? opt_mode : ydn.db.TransactionMode.READ_ONLY;
var outFn = trFn;
if (arguments.length > 4) { // handle optional parameters
var args = Array.prototype.slice.call(arguments, 4);
outFn = function () {
// Prepend the bound arguments to the current arguments.
var newArgs = Array.prototype.slice.call(arguments);
//newArgs.unshift.apply(newArgs, args); // pre-apply
newArgs = newArgs.concat(args); // post-apply
return trFn.apply(this, newArgs);
}
}
outFn.name = scope_name;
var me = this;
if (this.mu_tx_.isActive()) {
//console.log(this + ' active')
this.pushTxQueue(outFn, store_names, mode, oncompleted);
} else {
//console.log(this + ' not active')
var transaction_process = function (tx) {
me.mu_tx_.up(tx, scope_name);
// now execute transaction process
outFn(me);
me.mu_tx_.out(); // flag transaction callback scope is over.
// transaction is still active and use in followup request handlers
};
var completed_handler = function (type, event) {
me.mu_tx_.down(type, event);
/**
* #preserve_try
*/
try {
if (goog.isFunction(oncompleted)) {
oncompleted(type, event);
}
} catch (e) {
// swallow error. document it publicly.
// this is necessary to continue transaction queue
if (goog.DEBUG) {
throw e;
}
} finally {
me.popTxQueue_();
}
};
if (ydn.db.tr.TxStorage.DEBUG) {
window.console.log(this + ' transaction ' + mode + ' open for ' + JSON.stringify(names) + ' in ' + scope_name);
}
this.storage_.newTransaction(transaction_process, names, mode, completed_handler);
}
};
As it turns out, initializing Facebook before websql was causing the problem. After commenting out FB the app behaved properly, which is why setTimeout solved the issue as well; the fb api was ready. How the thread of execution gets blocked, I don't know.
So, to anyone using FB and then trying to execute websql transactions...delay FB!
Though, websql is still running a bit slow on safari load...
UPDATE: I found a solution
I had to do exactly as follows:
In the extension dir I created a new dir named "components".
Inside that dir I create a js file with my custom auto-complete name, in this case "simpleautocomplete.js".
The content of that js file is:
/*
* https://developer.mozilla.org/en/How_to_implement_custom_autocomplete_search_component
*/
const Ci = Components.interfaces;
const CLASS_ID = Components.ID("6224daa1-71a2-4d1a-ad90-01ca1c08e323");
const CLASS_NAME = "Simple AutoComplete";
const CONTRACT_ID = "#mozilla.org/autocomplete/search;1?name=simple-autocomplete";
try{
Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
} catch (x) {
}
// Implements nsIAutoCompleteResult
function SimpleAutoCompleteResult(searchString, searchResult,
defaultIndex, errorDescription,
results, comments) {
this._searchString = searchString;
this._searchResult = searchResult;
this._defaultIndex = defaultIndex;
this._errorDescription = errorDescription;
this._results = results;
this._comments = comments;
}
SimpleAutoCompleteResult.prototype = {
_searchString: "",
_searchResult: 0,
_defaultIndex: 0,
_errorDescription: "",
_results: [],
_comments: [],
getLabelAt: function(index) { return this._results[index]; },
/**
* The original search string
*/
get searchString() {
return this._searchString;
},
/**
* The result code of this result object, either:
* RESULT_IGNORED (invalid searchString)
* RESULT_FAILURE (failure)
* RESULT_NOMATCH (no matches found)
* RESULT_SUCCESS (matches found)
*/
get searchResult() {
return this._searchResult;
},
/**
* Index of the default item that should be entered if none is selected
*/
get defaultIndex() {
return this._defaultIndex;
},
/**
* A string describing the cause of a search failure
*/
get errorDescription() {
return this._errorDescription;
},
/**
* The number of matches
*/
get matchCount() {
return this._results.length;
},
/**
* Get the value of the result at the given index
*/
getValueAt: function(index) {
return this._results[index];
},
/**
* Get the comment of the result at the given index
*/
getCommentAt: function(index) {
return this._comments[index];
},
/**
* Get the style hint for the result at the given index
*/
getStyleAt: function(index) {
if (!this._comments[index])
return null; // not a category label, so no special styling
if (index == 0)
return "suggestfirst"; // category label on first line of results
return "suggesthint"; // category label on any other line of results
},
/**
* Get the image for the result at the given index
* The return value is expected to be an URI to the image to display
*/
getImageAt : function (index) {
return "";
},
/**
* Remove the value at the given index from the autocomplete results.
* If removeFromDb is set to true, the value should be removed from
* persistent storage as well.
*/
removeValueAt: function(index, removeFromDb) {
this._results.splice(index, 1);
this._comments.splice(index, 1);
},
QueryInterface: function(aIID) {
if (!aIID.equals(Ci.nsIAutoCompleteResult) && !aIID.equals(Ci.nsISupports))
throw Components.results.NS_ERROR_NO_INTERFACE;
return this;
}
};
// Implements nsIAutoCompleteSearch
function SimpleAutoCompleteSearch() {
}
SimpleAutoCompleteSearch.prototype = {
classID: CLASS_ID,
classDescription: CLASS_NAME,
contractID: CONTRACT_ID,
/*
* Search for a given string and notify a listener (either synchronously
* or asynchronously) of the result
*
* #param searchString - The string to search for
* #param searchParam - An extra parameter
* #param previousResult - A previous result to use for faster searchinig
* #param listener - A listener to notify when the search is complete
*/
startSearch: function(searchString, searchParam, result, listener) {
// This autocomplete source assumes the developer attached a JSON string
// to the the "autocompletesearchparam" attribute or "searchParam" property
// of the <textbox> element. The JSON is converted into an array and used
// as the source of match data. Any values that match the search string
// are moved into temporary arrays and passed to the AutoCompleteResult
if (searchParam.length > 0) {
var nativeJSON = Components.classes["#mozilla.org/dom/json;1"].createInstance(Ci.nsIJSON);
var searchResults = nativeJSON.decode(searchParam);
var results = [];
var comments = [];
for (i=0; i<searchResults.length; i++) {
if (searchResults[i].value.indexOf(searchString) == 0) {
results.push(searchResults[i].value);
if (searchResults[i].comment)
comments.push(searchResults[i].comment);
else
comments.push(null);
}
}
var newResult = new SimpleAutoCompleteResult(searchString, Ci.nsIAutoCompleteResult.RESULT_SUCCESS, 0, "", results, comments);
listener.onSearchResult(this, newResult);
}
},
/*
* Stop an asynchronous search that is in progress
*/
stopSearch: function() {
},
QueryInterface: function(aIID) {
if (!aIID.equals(Ci.nsIAutoCompleteSearch) && !aIID.equals(Ci.nsISupports))
throw Components.results.NS_ERROR_NO_INTERFACE;
return this;
},
_QueryInterface: XPCOMUtils.generateQI([Components.interfaces.nsIAutoCompleteSearch])
};
// Factory
var SimpleAutoCompleteSearchFactory = {
singleton: null,
createInstance: function (aOuter, aIID) {
if (aOuter != null)
throw Components.results.NS_ERROR_NO_AGGREGATION;
if (this.singleton == null)
this.singleton = new SimpleAutoCompleteSearch();
return this.singleton.QueryInterface(aIID);
}
};
// Module
var SimpleAutoCompleteSearchModule = {
registerSelf: function(aCompMgr, aFileSpec, aLocation, aType) {
aCompMgr = aCompMgr.QueryInterface(Components.interfaces.nsIComponentRegistrar);
aCompMgr.registerFactoryLocation(CLASS_ID, CLASS_NAME, CONTRACT_ID, aFileSpec, aLocation, aType);
},
unregisterSelf: function(aCompMgr, aLocation, aType) {
aCompMgr = aCompMgr.QueryInterface(Components.interfaces.nsIComponentRegistrar);
aCompMgr.unregisterFactoryLocation(CLASS_ID, aLocation);
},
getClassObject: function(aCompMgr, aCID, aIID) {
if (!aIID.equals(Components.interfaces.nsIFactory))
throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
if (aCID.equals(CLASS_ID))
return SimpleAutoCompleteSearchFactory;
throw Components.results.NS_ERROR_NO_INTERFACE;
},
canUnload: function(aCompMgr) { return true; }
};
// Module initialization
function NSGetModule(aCompMgr, aFileSpec) { return SimpleAutoCompleteSearchModule; }
if (XPCOMUtils.generateNSGetFactory){
var NSGetFactory = XPCOMUtils.generateNSGetFactory([SimpleAutoCompleteSearch]);
}
I added this code to chrome.manifest:
component {6224daa1-71a2-4d1a-ad90-01ca1c08e323} components/simpleautocomplete.js
contract #mozilla.org/autocomplete/search;1?name=simple-autocomplete {6224daa1-71a2-4d1a-ad90-01ca1c08e323}
In the xul file I added:
<textbox type="autocomplete" autocompletesearch="simple-autocomplete"
autocompletesearchparam='[{"value":"param1"},{"value":"param2"}]' />
important note: when setting the autocompletesearchparam attribute the assignment has to be insind two single quotes and not double quotes. only the values inside has to be in double quotes as in the xul above.
and that's it.
When I tested my extension and typed 'p' in the auto-complete textbox the words "param1" and "param2" poped-up.
I could also set the params in the js file as follows:
var searchTextField = document.getElementById("searchTextField");
var param1 = "Param1", param2 = "Param2";
paramsToSet = "[";
paramsToSet += "{\"value\" : \"" + param1 + "\"},";
paramsToSet += "{\"value\" : \"" + param2 + "\"},";
paramsToSet = paramsToSet.substring(0, paramsToSet.length-1); // to remove the last ","
paramsToSet += "]";
paramsToSet = paramsToSet.toLowerCase(); // important!
searchTextField.setAttribute("autocompletesearchparam", paramsToSet);
note1: notice that the auto-complete textbox only accepts text in small letters.
note2: if setting the params to the textbox is dynamically and taking some time, for example - when the client is typing in the textbox and the typed text is sent to a server so the response containing the params will be attached to the textbox, and this process is taking some time (about a second or half), then the popup auto-complete won't popup because it's autocompletesearchparam was empty when the client started to type. In this case it is possible to force the auto-complete to popup when you have the params to set in the textbox like this:
searchTextField.open = true;
I Found the solution by myself, hope it will helps someone.
I had to do exactly as follows:
In the extension dir I created a new dir named "components".
Inside that dir I create a js file with my custom auto-complete name, in this case "simpleautocomplete.js". The content of that js file is:
/*
* https://developer.mozilla.org/en/How_to_implement_custom_autocomplete_search_component
*/
const Ci = Components.interfaces;
const CLASS_ID = Components.ID("6224daa1-71a2-4d1a-ad90-01ca1c08e323");
const CLASS_NAME = "Simple AutoComplete";
const CONTRACT_ID = "#mozilla.org/autocomplete/search;1?name=simple-autocomplete";
try{
Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
} catch (x) {
}
// Implements nsIAutoCompleteResult
function SimpleAutoCompleteResult(searchString, searchResult,
defaultIndex, errorDescription,
results, comments) {
this._searchString = searchString;
this._searchResult = searchResult;
this._defaultIndex = defaultIndex;
this._errorDescription = errorDescription;
this._results = results;
this._comments = comments;
}
SimpleAutoCompleteResult.prototype = {
_searchString: "",
_searchResult: 0,
_defaultIndex: 0,
_errorDescription: "",
_results: [],
_comments: [],
getLabelAt: function(index) { return this._results[index]; },
/**
* The original search string
*/
get searchString() {
return this._searchString;
},
/**
* The result code of this result object, either:
* RESULT_IGNORED (invalid searchString)
* RESULT_FAILURE (failure)
* RESULT_NOMATCH (no matches found)
* RESULT_SUCCESS (matches found)
*/
get searchResult() {
return this._searchResult;
},
/**
* Index of the default item that should be entered if none is selected
*/
get defaultIndex() {
return this._defaultIndex;
},
/**
* A string describing the cause of a search failure
*/
get errorDescription() {
return this._errorDescription;
},
/**
* The number of matches
*/
get matchCount() {
return this._results.length;
},
/**
* Get the value of the result at the given index
*/
getValueAt: function(index) {
return this._results[index];
},
/**
* Get the comment of the result at the given index
*/
getCommentAt: function(index) {
return this._comments[index];
},
/**
* Get the style hint for the result at the given index
*/
getStyleAt: function(index) {
if (!this._comments[index])
return null; // not a category label, so no special styling
if (index == 0)
return "suggestfirst"; // category label on first line of results
return "suggesthint"; // category label on any other line of results
},
/**
* Get the image for the result at the given index
* The return value is expected to be an URI to the image to display
*/
getImageAt : function (index) {
return "";
},
/**
* Remove the value at the given index from the autocomplete results.
* If removeFromDb is set to true, the value should be removed from
* persistent storage as well.
*/
removeValueAt: function(index, removeFromDb) {
this._results.splice(index, 1);
this._comments.splice(index, 1);
},
QueryInterface: function(aIID) {
if (!aIID.equals(Ci.nsIAutoCompleteResult) && !aIID.equals(Ci.nsISupports))
throw Components.results.NS_ERROR_NO_INTERFACE;
return this;
}
};
// Implements nsIAutoCompleteSearch
function SimpleAutoCompleteSearch() {
}
SimpleAutoCompleteSearch.prototype = {
classID: CLASS_ID,
classDescription: CLASS_NAME,
contractID: CONTRACT_ID,
/*
* Search for a given string and notify a listener (either synchronously
* or asynchronously) of the result
*
* #param searchString - The string to search for
* #param searchParam - An extra parameter
* #param previousResult - A previous result to use for faster searchinig
* #param listener - A listener to notify when the search is complete
*/
startSearch: function(searchString, searchParam, result, listener) {
// This autocomplete source assumes the developer attached a JSON string
// to the the "autocompletesearchparam" attribute or "searchParam" property
// of the <textbox> element. The JSON is converted into an array and used
// as the source of match data. Any values that match the search string
// are moved into temporary arrays and passed to the AutoCompleteResult
if (searchParam.length > 0) {
var nativeJSON = Components.classes["#mozilla.org/dom/json;1"].createInstance(Ci.nsIJSON);
var searchResults = nativeJSON.decode(searchParam);
var results = [];
var comments = [];
for (i=0; i<searchResults.length; i++) {
if (searchResults[i].value.indexOf(searchString) == 0) {
results.push(searchResults[i].value);
if (searchResults[i].comment)
comments.push(searchResults[i].comment);
else
comments.push(null);
}
}
var newResult = new SimpleAutoCompleteResult(searchString, Ci.nsIAutoCompleteResult.RESULT_SUCCESS, 0, "", results, comments);
listener.onSearchResult(this, newResult);
}
},
/*
* Stop an asynchronous search that is in progress
*/
stopSearch: function() {
},
QueryInterface: function(aIID) {
if (!aIID.equals(Ci.nsIAutoCompleteSearch) && !aIID.equals(Ci.nsISupports))
throw Components.results.NS_ERROR_NO_INTERFACE;
return this;
},
_QueryInterface: XPCOMUtils.generateQI([Components.interfaces.nsIAutoCompleteSearch])
};
// Factory
var SimpleAutoCompleteSearchFactory = {
singleton: null,
createInstance: function (aOuter, aIID) {
if (aOuter != null)
throw Components.results.NS_ERROR_NO_AGGREGATION;
if (this.singleton == null)
this.singleton = new SimpleAutoCompleteSearch();
return this.singleton.QueryInterface(aIID);
}
};
// Module
var SimpleAutoCompleteSearchModule = {
registerSelf: function(aCompMgr, aFileSpec, aLocation, aType) {
aCompMgr = aCompMgr.QueryInterface(Components.interfaces.nsIComponentRegistrar);
aCompMgr.registerFactoryLocation(CLASS_ID, CLASS_NAME, CONTRACT_ID, aFileSpec, aLocation, aType);
},
unregisterSelf: function(aCompMgr, aLocation, aType) {
aCompMgr = aCompMgr.QueryInterface(Components.interfaces.nsIComponentRegistrar);
aCompMgr.unregisterFactoryLocation(CLASS_ID, aLocation);
},
getClassObject: function(aCompMgr, aCID, aIID) {
if (!aIID.equals(Components.interfaces.nsIFactory))
throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
if (aCID.equals(CLASS_ID))
return SimpleAutoCompleteSearchFactory;
throw Components.results.NS_ERROR_NO_INTERFACE;
},
canUnload: function(aCompMgr) { return true; }
};
// Module initialization
function NSGetModule(aCompMgr, aFileSpec) { return SimpleAutoCompleteSearchModule; }
if (XPCOMUtils.generateNSGetFactory){
var NSGetFactory = XPCOMUtils.generateNSGetFactory([SimpleAutoCompleteSearch]);
}
I added this code to chrome.manifest:
component {6224daa1-71a2-4d1a-ad90-01ca1c08e323} components/simpleautocomplete.js
contract #mozilla.org/autocomplete/search;1?name=simple-autocomplete {6224daa1-71a2-4d1a-ad90-01ca1c08e323}
In the xul file I added:
<textbox type="autocomplete" autocompletesearch="simple-autocomplete"
autocompletesearchparam='[{"value":"param1"},{"value":"param2"}]' />
important note: when setting the autocompletesearchparam attribute the assignment has to be insind two single quotes and not double quotes. only the values inside has to be in double quotes as in the xul above.
and that's it.
When I tested my extension and typed 'p' in the auto-complete textbox the words "param1" and "param2" poped-up.
I could also set the params in the js file as follows:
var searchTextField = document.getElementById("searchTextField");
var param1 = "Param1", param2 = "Param2";
paramsToSet = "[";
paramsToSet += "{\"value\" : \"" + param1 + "\"},";
paramsToSet += "{\"value\" : \"" + param2 + "\"},";
paramsToSet = paramsToSet.substring(0, paramsToSet.length-1); // to remove the last ","
paramsToSet += "]";
paramsToSet = paramsToSet.toLowerCase(); // important!
searchTextField.setAttribute("autocompletesearchparam", paramsToSet);
note1: notice that the auto-complete textbox only accepts text in small letters.
note2: if setting the params to the textbox is dynamically and taking some time, for example - when the client is typing in the textbox and the typed text is sent to a server so the response containing the params will be attached to the textbox, and this process is taking some time (about a second or half), then the popup auto-complete won't popup because it's autocompletesearchparam was empty when the client started to type. In this case it is possible to force the auto-complete to popup when you have the params to set in the textbox like this:
searchTextField.open = true;