How would you simulate typing via Jest and Vue-Test-Utils? - vue.js

For my test, I am expecting it to start with a value of length 39 (maxlength: 40), and when I press a key (ie: 'a'), the length should then be 40. However, anything I try or look up doesn't seem to allow me to trigger a keypress/keydown/keyup event.
My Component:
<q-input
v-model="name"
class="my-class"
maxlength="40"
\>
My Test:
it('Input should change', async() => {
let wrapper = mount(Component, { name: 'M'.repeat(39) })
let name = wrapper.find('.my-class')
console.log(name.vm.$options.propsData.value) // prints 39 M's as expected
name.trigger('click') // I try clicking the input. I've also tried triggering 'focus'
await wrapper.vm.$nextTick() // I've also tried with and without this
// I've also tried await name.vm.$nextTick()
name.trigger('keydown', { key: 'a' }) // According to their testing docs, this should press 'a'
// I've also tried name.trigger('keyup', { key: 'a' })
// and name.trigger('keypress', { key: 'a' })
await wrapper.vm.$nextTick()
console.log(name.vm.$options.propsData.value) // prints 39 M's without the additional 'a'
expect(name.vm.$options.propsData.value.length).toBe(40)
})

Why does not the value change after triggering keydown?
In short: you can't change the value of input/texarea with dispatching KeyboardEvent programmatically.
How actually do chars come into input? On MDN you can find the description of Keyboardevent sequence (assuming that preventDefault is not called):
A keydown event is first fired. If the key is held down further and the key produces a character key, then the event continues to be emitted in a platform implementation dependent interval and the KeyboardEvent.repeat read only property is set to true.
If the key produces a character key that would result in a character being inserted into possibly an <input>, <textarea> or an element with HTMLElement.contentEditable set to true, the beforeinput and input event types are fired in that order. Note that some other implementations may fire keypress event if supported. The events will be fired repeatedly while the key is held down.
A keyup event is fired once the key is released. This completes the process.
So, keydown leads to input event by default. But that is true only for trusted events:
Most untrusted events will not trigger default actions, with the exception of the click event... All other untrusted events behave as if the preventDefault() method had been called on that event.
Basically trusted events are those initiated by a user and untrusted events are initiated with a script. In most browsers, each event has an attribute isTrusted indicating if the event is trusted or not.
And how to test KeyboardEvents on inputs then?
Well, it depends on what is going on in your KeyboardEvent handler. E.g. if you call preventDefault in certain conditions then you can check if it was called or not in a test using a spy. Here is an example with sinon as a spy and chai as assertion library.
const myEvent = new KeyboardEvent('keydown', { key: 'a' })
sinon.spy(myEvent, 'preventDefault')
await wrapper.find('input').element.dispatchEvent(myEvent)
expect(myEvent.preventDefault.calledOnce).to.equal(true)
But sometimes you even don't need a KeyboardEvent handler. E.g. in your question you write about an input with maximum value length 40 characters. Actually you can just use maxlength HTML attribute for the input to prevent input of more than 40 characters. But just for a study example, we can just cut the value in input handler (it will control the length not only when a user types but also when a user copy-and-pastes a string):
onInput (event) {
event.target.value = event.target.value.substr(0, 40)
...
}
The test for this may look like this:
await wrapper.find('input').setValue('M'.repeat(41))
expect(wrapper.find('input').element.value).to.equal('M'.repeat(40))
So here, we directly change the value of input, not by KeyboardEvent.

I don't know where dialog comes from, but should you not be searching for the element inside wrapper?
wrapper.find('.my-class')
Also most examples I see use mount instead of factory
const wrapper = mount(component)
Here is an example from the docs:
it('Magic character "a" sets quantity to 13', () => {
const wrapper = mount(QuantityComponent)
wrapper.trigger('keydown', {
key: 'a'
})
expect(wrapper.vm.quantity).toBe(13)
})
})

Related

Unable to use short cut keys (hotKeys)

I am writing e2e tests in NightWatch v2.1.3 using page objects.
There are list of items, and an item can be selected either by click or by hotKey of its index.
Example: second element can be selected by click or shift+2.
Following code i have written, taking reference from the docs, but it is not working
browser.perform(function () {
const actions = this.actions({async: true});
console.log('performing hotKeys');
actions
.click('#option1')
.keyDown(Keys.SHIFT)
.keyDown(Keys.NUMPAD2)
.keyUp(Keys.NUMPAD2)
.keyUp(Keys.SHIFT);
});
Console is happening but the click and keyUp, keyDown is not working, when kept inside the .perform method.
What need to be fixed here?
return keyword is important. (silly mistake here)
Use 'a', 'b', '1', '2' for normal keys (single quotes are important, even for numbers)
click is not working, inside actions api. Better use the api click instead of userActions click. No idea why this click is added under new user-actions. The documentations does not have enough examples, one need to find out through hit and trial method.
Example:
For SHIFT + 1
browser
.pause(3000)
.perform(function () {
const actions = this.actions({ async: true });
return actions
.keyDown(Keys.SHIFT)
.keyDown('1')
.keyUp('1')
.keyUp(Keys.SHIFT);
})
.pause(2000);
For Shift + 10
(This depends on the way feature is developed, if app need both 1 and 0 to be pressed or not)
browser
.pause(3000)
.perform(function () {
const actions = this.actions({ async: true });
return actions
.keyDown(Keys.SHIFT)
.keyDown('1')
.keyUp('1')
.keyDown('0')
.keyUp('0')
.keyUp(Keys.SHIFT);
})
.pause(2000);
// keyUp('10') wont work, it is not a valid key in your keyboard
For simple 'a'
(This depends on the way feature is developed, if app need both 1 and 0 to be pressed or not)
browser
.pause(3000)
.perform(function () {
const actions = this.actions({ async: true });
return actions
.keyDown('a')
.keyUp('a')
})
.pause(2000);

What does 'the key input state' mean in the Webdriver spec?

I've been trying to digest the Webdriver spec and its more friendly version. And I'm having trouble understanding what do these words mean (in the description of the 'Element Send Keys' command):
The key input state used for input may be cleared mid-way through "typing" by sending the null key, which is U+E000 (NULL)
I had several ideas of what it might mean, I mention some below as a sort of evidence of my 'prior research'*).
Could somebody, please, explain what does it mean and, if possible, give an example, preferably in JavaScript?
*Attempts to figure it out myself:
I thought, one may skip calling releaseActions() if he previously pressed, say, the Shift key, like:
await browser.performActions([
{
type: 'key',
id: 'key1',
actions: [
{ type: 'keyDown', value: '\u0010', },
],
},
]);
await browser.elementSendKeys(elemUUID, '\uE000ABC');
But no, the shift key was still pressed, when the elementSendKeys() was called.
Also I thought the null character clears text in the element, no, it doesn't.
My original idea about what the words mean was correct, only the example to test it had errors.
From an abstract point of view, one need to understand, how the spec defines the Actions API. Simplified, it's this:
In the Webdriver implementation environment there are certain input sources, like null, keyboard, pointer (probably others). And each input source has an associated input state object, which (simplified) is the current state of the source, like (for a keyboard source) which keys are now being held down, or (for a pointer source) where is the cursor now on the screen etc.
So if one performs a keyDownkeyboard action the key will remain pressed until a keyUp action for the key is performed, or the state is reset.
The null unicode code point may be used in string literals to reset the state of the keyboard input source, namely release all currently held down keys.
Here is an example:
// elemUUID can be taken from the return values
// of methods like `findElement()` etc.
await browser.performActions([
{
type: 'key',
id: 'key1',
actions: [
// \u0008 is used for a Shift key in Webdriver
// I don't actually know, where it's specified
{ type: 'keyDown', value: '\u0008', },
],
},
]);
// Here the remote end (the automated browser)
// will type a capital A, since the shift key
// is still being pressed
await browser.elementSendKeys(elemUUID, 'a');
// One can include the null char somewhere in
// the string, and all subsequent chars will be
// 'pressed' without the pressed Shift
// This will type Aa
await browser.elementSendKeys(elemUUID, 'a\uE000a');
In my question's example errors were: the wrong unicode code point value for the Shift key, and the capital letters in the text argument to the elementSendKeys() method, which made the driver use the Shift key, despite the null character had been provided, i.e. in:
await browser.elementSendKeys(elemUUID, '\uE000A');
The shift key will always be pressed, since 'A' implies it.

How to prevent #change event when changing v-model value

I'm building an auto-complete menu in Vue.js backed by Firebase (using vue-fire). The aim is to start typing a user's display name and having match records show up in the list of divs below.
The template looks like this:
<b-form-input id="toUser"
type="text"
v-model="selectedTo"
#change="searcher">
</b-form-input>
<div v-on:click="selectToUser(user)" class="userSearchDropDownResult" v-for="user in searchResult" v-if="showSearcherDropdown">{{ user.name }}</div>
Upon clicking a potential match the intention is to set the value of the field and clear away the list of matches.
Here is the code portion of the component:
computed: {
/* method borrowed from Reddit user imGnarly: https://www.reddit.com/r/vuejs/comments/63w65c/client_side_autocomplete_search_with_vuejs/ */
searcher() {
let self = this;
let holder = [];
let rx = new RegExp(this.selectedTo, 'i');
this.users.forEach(function (val, key) {
if (rx.test(val.name) || rx.test(val.email)) {
let obj = {}
obj = val;
holder.push(obj);
} else {
self.searchResult = 'No matches found';
}
})
this.searchResult = holder;
return this.selectedTo;
},
showSearcherDropdown() {
if(this.searchResult == null) return false;
if(this.selectedTo === '') return false;
return true;
}
},
methods: {
selectToUser: function( user ) {
this.newMessage.to = user['.key'];
this.selectedTo = user.name;
this.searchResult = null;
}
}
Typeahead works well, on each change to the input field the searcher() function is called and populates the searchResult with the correct values. The v-for works and a list of divs is shown.
Upon clicking a div, I call selectToUser( user ). This correctly reports details from the user object to the console.
However, on first click I get an exception in the console and the divs don't clear away (I expect them to disappear because I'm setting searchResults to null).
[Vue warn]: Error in event handler for "change": "TypeError: fns.apply is not a function"
found in
---> <BFormInput>
<BFormGroup>
<BTab>
TypeError: fns.apply is not a function
at VueComponent.invoker (vue.esm.js?efeb:2004)
at VueComponent.Vue.$emit (vue.esm.js?efeb:2515)
at VueComponent.onChange (form-input.js?1465:138)
at boundFn (vue.esm.js?efeb:190)
at invoker (vue.esm.js?efeb:2004)
at HTMLInputElement.fn._withTask.fn._withTask (vue.esm.js?efeb:1802)
If I click the div a second time then there's no error, the input value is set and the divs disappear.
So I suspect that writing a value to this.selectedTo (which is also the v-model object for the element is triggering a #change event. On the second click the value of doesn't actually change because it's already set, so no call to searcher() and no error.
I've noticed this also happens if the element loses focus.
Question: how to prevent an #change event when changing v-model value via a method?
(other info: according to package.json I'm on vue 2.5.2)
On:
<b-form-input id="toUser"
type="text"
v-model="selectedTo"
#change="searcher">
The "searcher" should be a method. A method that will be called whenever that b-component issues a change event.
But looking at your code, it is not a method, but a computed:
computed: {
searcher() {
...
},
showSearcherDropdown() {
...
}
},
methods: {
selectToUser: function( user ) {
...
}
}
So when the change event happens, it tries to call something that is not a method (or, in other words, it tries to call a method that doesn't exist). That's why you get the error.
Now, since what you actually want is to update searcher whenever this.selectedTo changes, to get that, it is actually not needed to have that #change handler. This is due to the code of computed: { searcher() { already depending on this.selectedTo. Whenever this.selectedTo changes, Vue will calculate searcher again.
Solution: simply remove #change="searcher" from b-form. Everything else will work.
#acdcjunior, thanks for your answer.
Of course just removing the reference to searcher() just means no action is taken upon field value change so the field won’t work at all.
Moving the searcher() function into methods: {} instead of computed: {} means that it will be called on an input event and not a change even (another mystery but not one for today). A subtle difference that takes away the typeahead feature I’m aiming at.
However, it did make me remember that the result of computed: {} functions are cached and will be re-computed when any parameters change. In this case I realised that the searcher() function is dependent upon the this.selectedTo variable. So when the selectToUser() function sets this.selectedTo it triggers another call to searcher().
Fixed now. In case anyone has a similar problem in the future, I resolved this by turning to old fashioned semaphore by adding another variable.
var userMadeSelection: false
Now, searcher() begins with a check for this scenario:
computed: {
searcher() {
if(this.userMadeSelection) {
this.userMadeSelection = false;
return this.selectedTo;
}
…
and then in selectToUser():
this.userMadeSelection = true;

Mithriljs does not update DOM on change in data

I read somewhere, the DOM is updated every time an event occurs and there is changes in data which are bound to DOM. So I wished to learn more about it. I tried the code below but the DOM is not updated when data in textarea changes but its updated whenever I click or press tab key.
var app = {
controller: function () {
var self = this;
this.model = {};
this.model.errors = [];
this.break_data = function (value) {
self.model.errors = value.split(' ');
m.redraw();
}
},
view: function (ctrl) {
return m('.content', [
m('textarea', {onchange: m.withAttr('value', ctrl.break_data)}),
ctrl.model.errors.map(function (error) {
return m('.error', error);
})
]);
}
}
m.mount(document.getElementById('app'), app);
I even tried m.startComputaion(), m.stopComputation() and m.redraw() non of them works.
The redrawing timing is described here: https://stackoverflow.com/a/30728976/70894
As for your example, the problem is not Mithril but the event. You need to use oninput instead of onchange to look for immediate changes. As the Mozilla docs for the "change" event states:
The change event is fired for input, select, and textarea
elements when a change to the element's value is committed by the
user. Unlike the input event, the change event is not necessarily
fired for each change to an element's value.
Here's a fiddle with your code that uses oninput: http://jsfiddle.net/ciscoheat/LuLcra77/
Note that you don't need m.redraw in the event handler anymore now when the correct event is used, since Mithril redraws automatically after every event defined in a call to m().

How to trigger <enter> in an input in Angular scenario test?

I'm writing tests with Angular Scenario test runner. Within a traditional form, I can enter text into an input, but I need to press enter to execute the query and there is no button to click on. Surely there is some easy way to do this, but I do not know what it is.
input('query').enter('foo bar');
// ... now what?
I tried to simulate a keypress with JQuery, but as this answer indicates JQuery is not loaded in the e2e scenarios scope. So I followed his advice (as well as that of this answer) to simulate the keypress:
element('#search_input').query(function(el, done){
var press = document.createEvent('keypress');
press.which = 13;
press.trigger(evt);
done();
});
But to this Angular replies:
NotSupportedError: DOM Exception 9
Error: The implementation did not support the requested type of object or operation.
Update
I realized that a very easy workaround is to include a hidden submit input in my form:
<input id="search-submit" type="submit" style="display:none;">
Then in the scenario: element('#search-submit').click(); does what is needed.
For a purer solution which doesn't involve modifying the HTML for the sake of testing, #florian-f's answer (as well as this one) provides access to jQuery within the DSL via:
var $ = $window.$;
which can be used there or passed to the callback. However, even with this access when triggering a press of enter I was not able to submit my form in the following manner:
$(selector).trigger($.Event('keypress', { which: 13 }));
This must be another issue all together. But I did find jQuery's submit function to do the trick:
$(#the_form).submit();
You can access to the app (runner in an iframe) instance of jQuery :
angular.scenario.dsl('appElement', function() {
return function(selector, fn) {
return this.addFutureAction('element ' + selector, function($window, $document, done) {
fn.call(this, $window.angular.element(selector));
done();
});
};
});
Then you can call the trigger method of jQuery in your test :
appElement('yourSelector', function(elm) {
elm.trigger('enter');//or keypress
});
There is also another possibility to trigger a key event. While your first approach
element('#search_input').query(function(el, done){
var press = document.createEvent('keypress');
press.which = 13;
press.trigger(evt);
done();
});
will be blocked by angular, this one
element(<selector>).query(function($el, done) {
var event = new CustomEvent('keyup');
event.keyCode = 13;
$el.val(2);
$el.get(0).dispatchEvent(event);
done();
});
will pass and trigger a keyup event on the element specified by the selector (keyCode = 13 = Enter Key). See https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent for further information.