I want to test file uploader component using Jest and vue/test-utils.
I have this:
describe('show progress bar of uploading file', () => {
const wrapper = mount(FileUploaderComponent)
// create csv file
let csv = new Blob([''], { type: 'text/csv;charset=utf-8;' })
csv.name = 'myFile.csv'
let input = wrapper.find('input')
input.element.value = csv // || csv.error value, Error here
input.trigger('change')
// Update current status
})
Where in FileUploaderComponent i have:
<template>
<form action="POST" enctype="multipart/form-data">
<label class="btn btn-primary" for="input-file">
<input class="input-file" id="input-file" name="file" type="file" accept=".xlsx, .xls, .csv">
UPLOAD FILE
</label>
</form>
</template>
Throws this error:
InvalidStateError: This input element accepts a filename, which may
only be programmatically set to the empty string.
49 |
50 | let input = wrapper.find('input')
> 51 | input.element.value = csv
52 | input.trigger('change')
53 |
54 | // Update current status
So, the question is: How can i trigger event change with file input value? in this case, a csv file as value ?
You could do this using the DataTransfer object. Unfortunately, it hasn't been added to JSDOM, so you can't test in Jest. There's an open issue to add the object—https://github.com/jsdom/jsdom/issues/1568
If you ran your tests in a browser using Karma, you could test like this:
const wrapper = shallow(FormComponent)
const input = wrapper.find('input[type="file"]')
const dT = new ClipboardEvent('').clipboardData || new DataTransfer()
dT.items.add(new File(['foo'], 'programmatically_created.txt'))
input.element.files = dT.files
await input.trigger('change')
If you're just wanting to simulate a value in input.element.files and changes to input.element.value in Jest, but not necessarily accurately simulating every DOM behavior, you can do it by defining a getter/setter for those fields. This works for me:
let localImageInput
let localImageInputFiles
let localImageInputValueGet
let localImageInputValueSet
let localImageInputValue = ''
beforeEach(function() {
localImageInput = wrapper.find('#local-image-input')
localImageInputFilesGet = jest.fn()
localImageInputValueGet = jest.fn().mockReturnValue(localImageInputValue)
localImageInputValueSet = jest.fn().mockImplementation(v => {
localImageInputValue = v
})
Object.defineProperty(localImageInput.element, 'files', {
get: localImageInputFilesGet
})
Object.defineProperty(localImageInput.element, 'value', {
get: localImageInputValueGet,
set: localImageInputValueSet
})
})
it('should do the thing', function() {
localImageInputValue = 'some-image.gif'
localImageInputFilesGet.mockReturnValue([{
size: 12345,
blob: 'some-blob',
width: 300,
height: 200
}])
localImageInput.trigger('change')
return Vue.nextTick().then(() => {
// Assuming the component sets input.value = '' when this event is triggered
// and calls someFn with the image data
expect(localImageInputValue).toEqual('')
expect(someFn.mock.calls[0][0]).toEqual({
size: 12345,
blob: 'some-blob',
width: 300,
height: 200
})
})
}
Related
I am using ember.js and handlebars. I need to pass a string to a component and that string needs to have line breaks. I am struggling trying to get the line breaks to work. I have tried using
\n, \ , + `' + none of these seem to work
Here is my computed property where the string is getting returned:
scoreMessage: Ember.computed(function scoreMessage () {
const model = this.get('model')
return Ember.String.htmlSafe(`Advocacy Post: ${model.total_advocacy_posts_count}\n
Assets Submitted: ${model.total_assets_submitted_count}\n Engagement from asset:
${model.total_engagement_from_assets_count}`);
}),
Here is the handlebars file where the scoreMessage is passed into the ui-tooltip-on-mouseover component
<li class="item-cell flex-end score">
{{#ui-tooltip-on-mouseover
message=scoreMessage
tooltipLocation="top"
class="action"
}}
Score
{{/ui-tooltip-on-mouseover}}
</li>
Hmm, can you try white-space: pre-line; CSS to the tooltip? with this approach, you don't need to specify \n, you just press enter.
i.e.
scoreMessage: Ember.computed(function scoreMessage () {
const model = this.get('model')
return Ember.String.htmlSafe(
`Advocacy Post: ${model.total_advocacy_posts_count}
Assets Submitted: ${model.total_assets_submitted_count}
Engagement from asset: ${model.total_engagement_from_assets_count}`
);
}),
var model = { total_advocacy_posts_count: 3, total_assets_submitted_count: 4, total_engagement_from_assets_count: 7 }
var text = `Advocacy Post: ${model.total_advocacy_posts_count}
Assets Submitted: ${model.total_assets_submitted_count}
Engagement from asset: ${model.total_engagement_from_assets_count}`
window.onload = function() {
document.querySelector('.tooltip.other').innerHTML = text;
}
.tooltip {
white-space: pre-line;
padding: 20px;
}
body {
background: white;
}
<div class="tooltip">
Some
Text that does take
line breaks without specifying them
</div>
<div class="tooltip other"></div>
`
I am testing that form data are sent after submit.
ContactForm.spec.js
import Vue from "vue";
import Vuex from "vuex";
import { mount, shallowMount } from "#vue/test-utils";
import VeeValidate from "vee-validate";
import i18n from "#/locales";
import Vuetify from "vuetify";
import ContactForm from "#/components/Home/ContactForm.vue";
Vue.use(VeeValidate, { errorBagName: "errors" });
Vue.use(Vuetify);
Vue.use(Vuex);
describe("ContactForm.vue", () => {
let store;
let wrapper;
let options;
let input;
const v = new VeeValidate.Validator();
beforeEach(() => {
const el = document.createElement("div");
el.setAttribute("data-app", true);
document.body.appendChild(el);
});
it("should sendMessage - valid form", async () => {
// given
store = new Vuex.Store({
state: {
language: "en",
loading: false
}
})
options = {
sync: false,
provide: {
$validator () {
return new VeeValidate.Validator();
}
},
i18n,
store
};
wrapper = mount(ContactForm, options);
// when
const radioInput = wrapper.findAll('input[type="radio"]');
radioInput.at(1).setChecked(); // input element value is changed, v-model is not
radioInput.at(1).trigger("change"); // v-model updated
input = wrapper.find('input[name="givenName"]');
input.element.value = "John"; // input element value is changed, v-model is not
input.trigger("input"); // v-model updated
input = wrapper.find('input[name="familyName"]');
input.element.value = "Doe"; // input element value is changed, v-model is not
input.trigger("input"); // v-model updated
input = wrapper.find('input[name="email"]');
input.element.value = "john.doe#example.com"; // input element value is changed, v-model is not
input.trigger("input"); // v-model updated
input = wrapper.find('textarea[name="messageContent"]');
input.element.value = "Hello World!"; // input element value is changed, v-model is not
input.trigger("input"); // v-model updated
const contactForm = wrapper.find("form");
contactForm.trigger("submit");
await wrapper.vm.$nextTick();
// then
console.log("DATA: ", wrapper.vm.$data.contactLang);
expect(wrapper.vm.validForm).toEqual(true);
});
});
Validation is successful ( so validForm is set to true in the component )
BUT the test does not pass
console.log
✕ should sendMessage - valid form (476ms)
● ContactForm.vue › should sendMessage - valid form
expect(received).toEqual(expected)
Expected value to equal:
true
Received:
false
The component vue is
ContactForm.vue
<template>
<form id="contactForm" #submit="sendMessage()">
<input v-model="contactLang" type='hidden' data-vv-name="contactLang" v-validate="'required'" name='contactLang'>
<v-layout row wrap align-center>
<v-flex xs12 sm3 md3 lg3>
<v-radio-group row :mandatory="false" v-model="gender" name="gender">
<v-radio :label='genderLabel("f")' value="f" name="female"></v-radio>
<v-radio :label='genderLabel("m")' value="m" name="male"></v-radio>
</v-radio-group>
</v-flex>
<v-flex xs12 sm4 md4 lg4>
<v-text-field
v-model="givenName"
browser-autocomplete="off"
:label="$t('lang.views.home.contactForm.givenName')"
data-vv-name="givenName"
:error-messages="errors.collect('givenName')"
v-validate="'required'"
name="givenName">
</v-text-field>
</v-flex>
<v-flex xs12 sm5 md5 lg5>
<v-text-field
v-model="familyName"
browser-autocomplete="off"
:label="$t('lang.views.home.contactForm.familyName')"
data-vv-name="familyName"
:error-messages="errors.collect('familyName')"
v-validate="'required'"
name="familyName">
</v-text-field>
</v-flex>
</v-layout>
<v-text-field
browser-autocomplete="off"
v-model="email"
:label="$t('lang.views.home.contactForm.email')"
data-vv-name="email"
:error-messages="errors.collect('email')"
v-validate="'required|email'"
name="email">
</v-text-field>
<v-textarea v-model="messageContent" :label="$t('lang.views.home.contactForm.message')" :error-messages="errors.collect('messageContent')" :rules="[(v) => v.length <= 200 || 'Max 200 characters']" :counter="200" v-validate="'required'" data-vv-name="messageContent" name="messageContent"></v-textarea>
<v-btn id="btnClear" round #click.native="clear">{{ $t('lang.views.global.clear') }}</v-btn>
<v-btn round large color="primary" type="submit">{{ $t('lang.views.global.send') }}
<v-icon right>email</v-icon><span slot="loader" class="custom-loader"><v-icon light>cached</v-icon></span>
</v-btn>
</form>
</template>
<script>
import swal from "sweetalert2";
import { mapState } from "vuex";
import appValidationDictionarySetup from "#/locales/appValidationDictionary";
export default {
name: "contactForm",
$_veeValidate: { validator: "new" },
data() {
return {
contactLang: "",
gender: "f",
givenName: "",
familyName: "",
email: "",
messageContent: "",
validForm: false
};
},
...
methods: {
...
sendMessage: function() {
console.log("sendMessage()...");
this.$validator
.validateAll()
.then(isValid => {
console.log("VALIDATION RESULT: ", isValid);
this.validForm = isValid;
if (!isValid) {
console.log("VALIDATION ERROR");
// console.log("Errors: ", this.$validator.errors.items.length);
const alertTitle = this.$validator.dictionary.container[
this.language
].custom.error;
const textMsg = this.$validator.dictionary.container[this.language]
.custom.correct_all;
swal({
title: alertTitle,
text: textMsg,
type: "error",
confirmButtonText: "OK"
});
return;
}
console.log("validation success, form submitted validForm: ", this.validForm);
return;
})
.catch(e => {
// catch error from validateAll() promise
console.log("error validation promise: ", e);
this.validForm = false;
return;
});
},
clear: function() {
this.contactLang = "";
this.gender = "f";
this.givenName = "";
this.familyName = "";
this.email = "";
this.messageContent = "";
this.validForm = false;
this.$validator.reset();
}
},
mounted() {
appValidationDictionarySetup(this.$validator);
this.$validator.localize(this.language);
this.contactLang = this.language;
}
};
</script>
</style>
and the console.log debugging is
console.log src/components/Home/ContactForm.vue:90
sendMessage()...
console.log tests/unit/ContactForm.spec.js:242
DATA: en
console.log src/components/Home/ContactForm.vue:94
VALIDATION RESULT: true
It's weird to see that the DATA ( contactLang ) value is false in the console.log from the spec ... displayed before the validation result
console.log src/components/Home/ContactForm.vue:90
sendMessage()...
console.log tests/unit/ContactForm.spec.js:242
DATA: en
console.log src/components/Home/ContactForm.vue:94
VALIDATION RESULT: true
console.log src/components/Home/ContactForm.vue:112
validation success, form submitted validForm: true
I guess there is async problem ... timout ?
thanks for feedback
SOLVED
It's actually a timeout issue
expect.assertions(1); // Otherwise jest will give a false positive
await contactForm.trigger("submit");
// then
setTimeout(() => {
expect(wrapper.vm.validForm).toEqual(true);
}, 2000);
I propose to use jest faketimers
jest.useFakeTimers()
contactForm.trigger("submit");
await wrapper.vm.$nextTick();
// then
jest.runTimersToTime(2000)
expect(wrapper.vm.validForm).toEqual(true);
I suggest to first make the test fail to avoid false positives
for more information about jest faketimers
https://jestjs.io/docs/en/timer-mocks.html
i did a simple test for my login form of component, in case it helps someone
it("submit form call method login", () => {
const login = jest.fn()
const wrapper = shallowMount(Login, {
methods: {
login
}
})
wrapper.findAll("v-form").at(0).trigger("submit")
expect(login).toBeCalled()
})
I'm using Telerik for MVC and trying to get the multi-select to populate with the initial values in an Edit scenario.
<script>
function filterProducts() {
return {
manufacturerId: $("#ServiceBulletinItem_ManufacturerId").val()
};
}
function onManufacturerChange(e) {
var v = e.sender.dataItem().Value;
$.post("#Url.Action("GetCascadeProducts", "Components")", { manufacturerId: v }, function (result) {
var grid = $("#ServiceBulletinItem_ApplicableProducts").data("kendoMultiSelect")
grid.setDataSource(result)
});
}
function InitialPopulate(manId) {
$.post("#Url.Action("GetCascadeProducts", "Components")", { manufacturerId: manId }, function (result) {
var grid = $("#ServiceBulletinItem_ApplicableProducts").data("kendoMultiSelect")
grid.setDataSource(result)
});
}
$(document).ready(function () {
$('.control-datepicker').Zebra_DatePicker();
var m = $("#ServiceBulletinItem_ManufacturerId").val();
InitialPopulate(m);
});
</script>
<div class="form-group">
#Html.LabelFor(m => m.ManufacturerList, "Manufacturer", htmlAttributes: new { #class = "control-label col-md-2" })
<div class="col-md-10">
#(Html.Kendo().DropDownListFor(m => m.ServiceBulletinItem.ManufacturerId)
.HtmlAttributes(new { #class = "col-md-6 form-control" })
.Filter("contains")
.DataValueField("Value")
.DataTextField("Text")
.BindTo((IEnumerable<SelectListItem>)Model.ManufacturerSelectList)
.HtmlAttributes(new { style = "width:70%;" }).Events(e =>
{
e.Change("onManufacturerChange");
})
)
</div >
</div >
<div class="form-group">
#Html.LabelFor(m => m.ProductList, "Product", htmlAttributes: new { #class = "control-label col-md-2" })
<div class="col-md-10">
#(Html.Kendo().MultiSelectFor(m => m.ServiceBulletinItem.ApplicableProducts)
.AutoClose(false)
.DataTextField("ProductName")
.DataValueField("ProductId")
.Placeholder("Select products...")
)
</div>
</div>
I'm trying to populate the manufacturer drop down and the Product multiSelect. The ApplicableProducts item is an IEnumerable representing the ProductId's of all those previously selected and I know that when I select the manufacturer and it calls the GetCascadeProducts controller method it will return back a collection of ProductId and ProductName for all the manufacturers products of which those productId is the ApplicableProducts property should exist.
On document.ready I can call the InitialPopulate method with the manufacturerID which will populate the multiSelect items but can't seem to populate the initial values.
I couldnt get the binding working correctly so ended up using
#(Html.Kendo().MultiSelect()
.Name("ServiceBulletinItem_ApplicableProducts")
.AutoClose(false)
.DataTextField("ProductName")
.DataValueField("ProductId")
.Placeholder("Select products 2...")
.AutoBind(false)
)
and then on the using the following code on document ready to make an ajax call to populate the manufacturer and product controls
function PopulateProductsInitial(manId) {
$.post("#Url.Action("GetCascadeProducts", "Components")", { manufacturerId: manId }, function (result) {
var grid = $("#ServiceBulletinItem_ApplicableProducts").data("kendoMultiSelect")
grid.setDataSource(result);
var s = $("#ServiceBulletinItem_Id").val();
$.post("#Url.Action("GetSBProducts", "ServiceBulletins")", { Id: s}, function (result) {
var arr = [];
result.forEach(function (element) {
arr.push(element.ProductId);
});
var grid = $("#ServiceBulletinItem_ApplicableProducts").data("kendoMultiSelect")
grid.value(arr);
});
});
}
}
$(document).ready(function () {
//Populate Initial Values
PopulateProductsInitial($("#ServiceBulletinItem_ManufacturerId").val());
$('#YourButton').click(SendForm);
});
The problem then became sending the selected items back to the controller when the edit was complete which again seemed convoluted because the control was not bound and therefore I had to make an Ajax call to submit the data.
function SendForm() {
var items = $("#ServiceBulletinItem_ApplicableProducts").data("kendoMultiSelect").value();
//Manipulate into ServiceBulletinViewModel for the save
var data = {
Id: $("#ServiceBulletinItem_Id").val(),
ServiceBulletinItem: {
Id: $("#ServiceBulletinItem_Id").val(),
ManufacturerId: $("#ServiceBulletinItem_ManufacturerId").val(),
IssueDate: $('#ServiceBulletinItem_IssueDate').val(),
Heading: $('#ServiceBulletinItem_Heading').val(),
Details: $('#ServiceBulletinItem_Details').val(),
Url: $('#ServiceBulletinItem_Url').val(),
SelectedProducts: items
}
}
$.ajax({
type: 'POST',
url: '/ServiceBulletins/Edit',
contentType: 'application/json',
data: JSON.stringify(data),
success: function (result) {
//Your success code here..
if (result.redirectUrl != null) {
window.location = result.redirectUrl;
}
},
error: function (jqXHR) {
if (jqXHR.status === 200) {
alert("Value Not found");
}
}
});
}
It all seemed a lot more convoluted than any of the demo's that teleriks and I couldnt find any good examples of binding from remote sources which looked similar.
As the binding is convention based I'm wondering if its possible to simplify the ajax calling for the post functionality with the correct naming of the controls so that I can simply get the selected items on the multiselect control or if the ajax post is the way to go.
I am trying out Reason-React. I am facing a problem when I try to add a key to one of the components.
I have a TodoApp that takes a list of TodoItem as state. The app works fine when I don't have a key for the TodoItem. When I add it, however I am getting a compilation error. I am adding the files here for reference:
TodoItem.re:
type item = {
id: int,
title: string,
completed: bool
};
let lastId = ref(0);
let newItem = () => {
lastId := lastId^ + 1;
{id: lastId^, title: "Click a button", completed: false}
};
let toString = ReasonReact.stringToElement;
let component = ReasonReact.statelessComponent("TodoItem");
let make = (~item, children) => {
...component,
render: (self) =>
<div className="item">
<input _type="checkbox" checked=(Js.Boolean.to_js_boolean(item.completed)) />
(toString(item.title))
</div>
};
TodoApp.re:
let toString = ReasonReact.stringToElement;
type state = {items: list(TodoItem.item)};
type action =
| AddItem;
let component = ReasonReact.reducerComponent("TodoApp");
let currentItems = [TodoItem.{id: 0, title: "ToDo", completed: false}];
let make = (children) => {
...component,
initialState: () => {items: currentItems},
reducer: (action, {items}) =>
switch action {
| AddItem => ReasonReact.Update({items: [TodoItem.newItem(), ...items]})
},
render: ({state: {items}, reduce}) => {
let numOfItems = List.length(items);
<div className="app">
<div className="title">
(toString("What to do"))
<button onClick=(reduce((_evt) => AddItem))> (toString("Add Something")) </button>
</div>
<div className="items">
(
ReasonReact.arrayToElement(
Array.of_list(
(List.map((item) => <TodoItem key=(string_of_int(item.id)) item />, items))
/*List.map((item) => <TodoItem item />, items) This works but the above line of code with the key does not*/
)
)
)
</div>
<div className="footer"> (toString(string_of_int(numOfItems) ++ " items")) </div>
</div>
}
};
I've added a comment near the line where the error occurs.
The error reads as Unbound record field id, but I am not able to figure out how it is not bound. What am I missing here?
Type inference is unfortunately a bit limited when it comes to inferring the type of a record from another module based on the usage of record fields, so you need to give it some help. Two options that should work are:
Annotating the type of ìtem:
List.map((item: TodoItem.item) => <TodoItem key=(string_of_int(item.id)) item />)
or locally opening the module where the record field is used:
List.map((item) => <TodoItem key=(string_of_int(item.TodoItem.id)) item />)
This is my test code
import { shallow } from 'enzyme';
import ApplicationStatus
from 'models/ApplicationStatus';
import ApplicationStatusLabel
from 'component/ApplicationStatus';
describe('<ApplicationStatusLabel />', () => {
it(`should render a label if a user have submitted an
application`, () => {
const status = new ApplicationStatus({ status: 'pending' });
const wrapper = shallow(
<ApplicationStatusLabel
planApplicationStatus={status}
/>
);
console.log(wrapper.text())
expect(wrapper.contains('received your application')).to.equal(true);
});
and there is my label react code
const PlanApplicationReceivedStatus = () => (
<div className={styles['plan-trial']}>
<div className={styles['plan-trial-headline']}>
{"We've received your application."}
</div>
<div className={styles['plan-trial-text']}>
{
`We'll get back to you via email within 24 hours.
`
}
</div>
</div>
);
const ApplicationStatusLabel = (
{ planApplicationStatus }
) => {
if (
planApplicationStatus &&
planApplicationStatus.status === 'pending'
) {
return PlanApplicationReceivedStatus();
}
return null;
};
However the test always reports failure
AssertionError: expected false to equal true
+ expected - actual
-false
+true
at Assertion.assertEqual (node_modules/chai/lib/chai/core/assertions.js:487:12)
at Assertion.ctx.(anonymous function) [as equal] (node_modules/chai/lib/chai/utils/addMethod.js:41:25)
at Context.<anonymous> (test/components/PlanApplicationStatus.spec.js:19:
I have put in a console.log statement to print out the content. I can see the output as such:
We've received your application.We'll get back to you via email within 24 hours.
So it seems to me the react component works as expected. So why the contains test failed?