Merge pull request #1025 from Safelite/feature/CSR-1214-release

Post Tech Review changes for CSR-1214
This commit is contained in:
chloeherdsafelite 2023-03-28 14:26:16 -04:00 committed by GitHub
commit 6519745d9d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 365 additions and 172 deletions

View file

@ -198,139 +198,6 @@ describe("textboxQuestion.vue", () => {
expect(wrapper.vm.handleChange).toHaveBeenCalled;
});
it("Should call this.handleChange when image is submitted with valid value.", async () => {
// Arrange
const inputId = "test";
const responseValue = "testResponse";
const storeMixin = {
methods: {
dispatchStoreAction: jest.fn().mockImplementation(() => {
return new Promise((resolve, reject) =>
resolve({
data: [responseValue],
})
);
}),
},
};
const wrapper = shallowMount(textboxQuestion, {
global: {
directives: {
maska: maska,
},
},
propsData: {
modelValue: "",
includeCameraIcon: true,
inputId: inputId,
},
mixins: [mockMixin, storeMixin],
attachTo: document.body,
});
wrapper.vm.handleChange = jest.fn().mockImplementation(() => {});
// Act
const imageUploadField = wrapper.find('[data-test="image-upload"]');
await imageUploadField.trigger("change");
await wrapper.vm.$nextTick();
// Assert
expect(wrapper.vm.handleChange).toHaveBeenCalled();
expect(wrapper.emitted()).toHaveProperty("update:modelValue");
});
it("Should emit image-lookup-error when image is submitted with invalid value", async () => {
// Arrange
const inputId = "test";
const storeMixin = {
methods: {
dispatchStoreAction: jest.fn().mockImplementation(() => {
return new Promise((resolve, reject) =>
resolve({
data: [],
})
);
}),
},
};
const wrapper = shallowMount(textboxQuestion, {
global: {
directives: {
maska: maska,
},
},
propsData: {
modelValue: "",
includeCameraIcon: true,
inputId: inputId,
},
mixins: [mockMixin, storeMixin],
attachTo: document.body,
});
wrapper.vm.handleChange = jest.fn().mockImplementation(() => {});
// Act
const imageUploadField = wrapper.find('[data-test="image-upload"]');
await imageUploadField.trigger("change");
await wrapper.vm.$nextTick();
// Assert
expect(wrapper.vm.handleChange).not.toHaveBeenCalled();
expect(wrapper.emitted()).toHaveProperty("imageLookupError");
});
it("Should emit image-lookup-error when image endpoint responds with an error", async () => {
// Arrange
const inputId = "test";
const storeMixin = {
methods: {
dispatchStoreAction: jest.fn().mockImplementation(() => {
return new Promise((resolve, reject) => reject());
}),
},
};
const wrapper = shallowMount(textboxQuestion, {
global: {
directives: {
maska: maska,
},
},
propsData: {
modelValue: "",
includeCameraIcon: true,
inputId: inputId,
},
mixins: [mockMixin, storeMixin],
attachTo: document.body,
});
wrapper.vm.handleChange = jest.fn().mockImplementation(() => {});
// Act
const imageUploadField = wrapper.find('[data-test="image-upload"]');
await imageUploadField.trigger("change");
await wrapper.vm.$nextTick();
// Assert
expect(wrapper.vm.handleChange).not.toHaveBeenCalled();
expect(wrapper.emitted()).toHaveProperty("imageLookupError");
});
it("Should not display camera or spinner icon when disabled", () => {
// Arrange
const inputId = "test";
@ -343,7 +210,7 @@ describe("textboxQuestion.vue", () => {
},
propsData: {
modelValue: "",
includeCameraIcon: true,
includeImageQuestion: true,
inputId: inputId,
isDisabled: true,
},
@ -359,4 +226,220 @@ describe("textboxQuestion.vue", () => {
expect(cameraIcon.exists()).toBe(false);
expect(loadingIcon.exists()).toBe(false);
});
describe("Image Uploading", () => {
it("Should call this.handleChange & emit update when fully valid image is submitted.", async () => {
// Arrange
const inputId = "test";
const responseValue = "testResponse";
const imageHandler = jest.fn().mockImplementation(
() =>
new Promise((resolve, reject) => {
resolve({
data: [responseValue],
});
})
);
const wrapper = shallowMount(textboxQuestion, {
global: {
directives: {
maska: maska,
},
},
propsData: {
modelValue: "",
"onUpdate:modelValue": (v) => wrapper.setProps({ modelValue: v }),
includeImageQuestion: true,
inputId: inputId,
imageQuestionSubmitHandler: imageHandler,
},
mixins: [mockMixin],
attachTo: document.body,
});
wrapper.vm.handleChange = jest.fn().mockImplementation(() => {});
wrapper.vm.isImageValid = jest.fn().mockImplementation(() => true);
// Act
const imageUploadField = wrapper.find('[data-test="image-upload"]');
await imageUploadField.trigger("change");
await wrapper.vm.$nextTick();
// Assert
expect(wrapper.vm.imageQuestionSubmitHandler).toHaveBeenCalled();
expect(wrapper.vm.handleChange).toHaveBeenCalled();
expect(wrapper.emitted()).toHaveProperty("update:modelValue");
});
it("Should not call handler and emit image-lookup-error when image is invalid.", async () => {
// Arrange
const inputId = "test";
const responseValue = "testResponse";
const imageHandler = jest.fn().mockImplementation(
() =>
new Promise((resolve, reject) => {
resolve({
data: [responseValue],
});
})
);
const wrapper = shallowMount(textboxQuestion, {
global: {
directives: {
maska: maska,
},
},
propsData: {
modelValue: "",
"onUpdate:modelValue": (v) => wrapper.setProps({ modelValue: v }),
includeImageQuestion: true,
inputId: inputId,
imageQuestionSubmitHandler: imageHandler,
},
mixins: [mockMixin],
attachTo: document.body,
});
wrapper.vm.handleChange = jest.fn().mockImplementation(() => {});
wrapper.vm.isImageValid = jest.fn().mockImplementation(() => false);
// Act
const imageUploadField = wrapper.find('[data-test="image-upload"]');
await imageUploadField.trigger("change");
await wrapper.vm.$nextTick();
// Assert
expect(wrapper.vm.imageQuestionSubmitHandler).not.toHaveBeenCalled();
expect(wrapper.vm.handleChange).not.toHaveBeenCalled();
expect(wrapper.emitted()).toHaveProperty("imageValidityError");
});
it("Should emit image-lookup-error when image lookup responds with an error.", async () => {
// Arrange
const inputId = "test";
const imageHandler = jest.fn().mockImplementation(
() =>
new Promise((resolve, reject) => {
reject({});
})
);
const wrapper = shallowMount(textboxQuestion, {
global: {
directives: {
maska: maska,
},
},
propsData: {
modelValue: "",
"onUpdate:modelValue": (v) => wrapper.setProps({ modelValue: v }),
includeImageQuestion: true,
inputId: inputId,
imageQuestionSubmitHandler: imageHandler,
},
mixins: [mockMixin],
attachTo: document.body,
});
wrapper.vm.handleChange = jest.fn().mockImplementation(() => {});
wrapper.vm.isImageValid = jest.fn().mockImplementation(() => true);
// Act
const imageUploadField = wrapper.find('[data-test="image-upload"]');
await imageUploadField.trigger("change");
await wrapper.vm.$nextTick();
// Assert
expect(wrapper.vm.imageQuestionSubmitHandler).toHaveBeenCalled();
expect(wrapper.vm.handleChange).not.toHaveBeenCalled();
expect(wrapper.emitted()).toHaveProperty("imageLookupError");
});
});
describe("Image Validation", () => {
it("Should return true if a valid image is tested.", () => {
// Arrange
const wrapper = shallowMount(textboxQuestion, {
global: {
directives: {
maska: maska,
},
},
propsData: {
inputId: "input ID",
},
mixins: [mockMixin],
});
const image = {
size: 100 * 1028,
};
// Act
const result = wrapper.vm.isImageValid(image);
// Assert
expect(result).toBeTruthy();
});
it("Should return false if null image is tested.", () => {
// Arrange
const wrapper = shallowMount(textboxQuestion, {
global: {
directives: {
maska: maska,
},
},
propsData: {
inputId: "input ID",
},
mixins: [mockMixin],
});
const image = null;
// Act
const result = wrapper.vm.isImageValid(image);
// Assert
expect(result).toBeFalsy();
});
it("Should return false if oversized image is tested.", () => {
// Arrange
const wrapper = shallowMount(textboxQuestion, {
global: {
directives: {
maska: maska,
},
},
propsData: {
inputId: "input ID",
},
mixins: [mockMixin],
});
const image = {
size: 10000000 * 1028,
};
// Act
const result = wrapper.vm.isImageValid(image);
// Assert
expect(result).toBeFalsy();
});
});
});

View file

@ -10,7 +10,7 @@
class="input-wrapper"
:class="[
includeSearchIcon ? 'has-search-icon' : '',
includeCameraIcon ? 'has-camera-icon' : '',
includeImageQuestion ? 'has-camera-icon' : '',
]">
<input
class="form-control"
@ -36,20 +36,22 @@
:maxlength="maxLength ? maxLength : '999'"
@focus="$emit('focus', $event.target.value)" />
<button v-if="includeSearchIcon" type="submit" aria-label="Search button" />
<template v-if="!isDisabled">
<label class="camera-icon-input" v-show="includeCameraIcon && !isLoading">
<input
type="file"
id="vin-input"
accept="image/jpeg,image/png"
aria-label="Camera icon/button"
data-test="image-upload"
@change="submitImage" />
</label>
<loader
v-show="includeCameraIcon && isLoading"
loaderColor="blue"
class="loading-icon"></loader>
<template v-if="includeImageQuestion">
<template v-if="!isDisabled">
<label class="camera-icon-input" v-show="!isImageProcessing">
<input
type="file"
id="vin-input"
accept="image/jpeg,image/png"
aria-label="Camera icon/button"
data-test="image-upload"
@change="imageChanged" />
</label>
<loader
v-show="isImageProcessing"
loaderColor="blue"
class="loading-icon"></loader>
</template>
</template>
</div>
<div v-show="errorMessage" class="row my-2 form-test-error">
@ -92,13 +94,14 @@ export default {
questionAlignment: String, // Left or center. Left is default.
cornerStyle: String, // Rounded or square. Square is default.
includeSearchIcon: Boolean,
includeCameraIcon: Boolean,
includeImageQuestion: Boolean,
imageQuestionSubmitHandler: Function,
},
setup(props) {
const propsClone = Object.assign({}, props);
const modelValue = propsClone.modelValue;
let initialValue;
let isLoading = ref(false);
let isImageProcessing = ref(false);
switch (typeof modelValue) {
case "number":
@ -128,30 +131,35 @@ export default {
validate,
meta,
errors,
isLoading,
isImageProcessing,
};
},
methods: {
async submitImage(e) {
this.isLoading = true;
await this.dispatchStoreAction(storeActions.LOOKUP_VIN_BY_IMAGE, e.target.files[0])
.then(async (response) => {
if (response.data.length > 0) {
this.value = response.data[0];
async imageChanged(e) {
let file = e.target.files[0];
// Wait for DOM to update before firing change event to handle validation.
await this.$nextTick();
if (!this.isImageValid(file)) {
this.$emit("imageValidityError");
return;
}
document.getElementById(this.inputId).dispatchEvent(new Event("change"));
} else {
this.$emit("imageLookupError");
}
})
.catch((error) => {
this.$emit("imageLookupError");
});
this.isLoading = false;
e.target.value = null;
this.isImageProcessing = true;
try {
this.value = await this.imageQuestionSubmitHandler(file);
await this.$nextTick();
document.getElementById(this.inputId).dispatchEvent(new Event("change"));
} catch (error) {
this.$emit("imageLookupError");
}
this.isImageProcessing = false;
},
isImageValid(imageFile) {
return imageFile && imageFile.size < this.maxFileSize;
},
},
computed: {
@ -190,6 +198,12 @@ export default {
return questionText;
},
},
maxFileSize() {
let kiloBytes = 5140;
return kiloBytes * 1028;
},
},
watch: {
async value(newValue) {

View file

@ -256,6 +256,84 @@ describe("vin-lookup.vue", () => {
expect(wrapper.findAllComponents({ name: "alert" }).length).toBe(1);
});
});
describe("getVinFromImage", () => {
test("GetVinFromImage resolves with first valid VIN when any vins are returned.", async () => {
// Arrange
const responseValue = "testResponse";
const lookup = jest.fn().mockImplementation(() => {
return new Promise((resolve, reject) => resolve([responseValue]));
});
const image = {};
const storeMixin = {
methods: {
dispatchStoreAction: lookup,
},
};
const { wrapper } = setupMocks({
mixins: [storeMixin],
});
// Act
const response = await wrapper.vm.getVinFromImage(image);
// Assert
expect(lookup).toHaveBeenCalled();
expect(response).toEqual(responseValue);
});
test("GetVinFromImage rejects if no vins are returned.", async () => {
// Arrange
const lookup = jest.fn().mockImplementation(() => {
return new Promise((resolve, reject) => resolve([]));
});
const image = {};
const storeMixin = {
methods: {
dispatchStoreAction: lookup,
},
};
const { wrapper } = setupMocks({
mixins: [storeMixin],
});
// Act
const promise = wrapper.vm.getVinFromImage(image);
// Assert
await expect(promise).rejects.toEqual("No VINs detected.");
});
test("GetVinFromImage rejects if an error occurs.", async () => {
const lookup = jest.fn().mockImplementation(() => {
return new Promise((resolve, reject) => reject());
});
const image = {};
const storeMixin = {
methods: {
dispatchStoreAction: lookup,
},
};
const { wrapper } = setupMocks({
mixins: [storeMixin],
});
// Act
const promise = wrapper.vm.getVinFromImage(image);
// Assert
await expect(promise).rejects.toEqual("An error occurred during the lookup.");
});
});
});
function setupMocks({ customMountOptions }) {

View file

@ -19,10 +19,13 @@
validationRules="vin-required|vin-format"
:isDisabled="vinPopulatedOnPageLoad"
maxLength="17"
includeCameraIcon
:mask="vinMask"
includeImageQuestion
:imageQuestionSubmitHandler="getVinFromImage"
@image-lookup-error="displayVinScanAlert"
data-test="vin-lookup-field" />
@image-validity-error="displayVinScanAlert"
data-test="vin-lookup-field"
ref="vinLookupQuestion" />
</div>
</div>
<div class="row">
@ -395,6 +398,21 @@ export default {
displayVinScanAlert() {
this.displayVinScanFailedAlert = true;
},
getVinFromImage(image) {
return new Promise((resolve, reject) => {
this.dispatchStoreAction(storeActions.LOOKUP_VIN_BY_IMAGE, image)
.then((response) => {
if (response.data.length > 0) {
resolve(response.data[0]);
} else {
reject("No VINs detected.");
}
})
.catch(() => {
reject("An error occurred during the lookup.");
});
});
},
resetAlerts() {
this.displayMatchedDifferentVehicleAlert = false;
this.displayNonServiceableZipAlert = false;