From 2f9607d8cd1cbd797f12faa9957df191491855cc Mon Sep 17 00:00:00 2001 From: Kendall Roth Date: Wed, 1 Jul 2020 19:03:08 -0400 Subject: [PATCH] GitHub config (#1) * Add tests for FormLeaveGuardMixin * Add basic GitLab CI * Remove GitLab CI (using GitHub...) * Update CHANGELOG * Update CHANGELOG * 0.2.2 --- CHANGELOG.md | 8 +- babel.config.js | 11 +- package.json | 14 ++- src/formLeaveGuardMixin.js | 9 +- test/formCreateMixin.test.js | 36 +++---- test/formLeaveGuardMixin.test.js | 175 ++++++++++++++++++++++++++++++- 6 files changed, 223 insertions(+), 30 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 242724d..fb83dbe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,13 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [Unreleased] +## [0.2.2] - 2020-07-01 +### Added +- GitLab testing and release pipeline (#1) +- Test suite implementation and coverage tests (#1) + +### Fixed +- Only allow setting form flags that have been defined (#1) ## [0.2.1] - 2020-06-30 ### Fixed diff --git a/babel.config.js b/babel.config.js index 51b0ae6..55df36e 100644 --- a/babel.config.js +++ b/babel.config.js @@ -9,7 +9,16 @@ module.exports = { plugins: ["add-module-exports"], }, test: { - presets: ["@babel/preset-env"], + presets: [ + [ + "@babel/preset-env", + { + targets: { + node: "current", + }, + }, + ], + ], plugins: ["add-module-exports"], }, }, diff --git a/package.json b/package.json index 16332b6..402c7fe 100644 --- a/package.json +++ b/package.json @@ -1,15 +1,16 @@ { "name": "@kendallroth/vue-simple-forms", - "version": "0.2.1", + "version": "0.2.2", "description": "Simple Vue form state management library", "main": "./lib/index.js", "scripts": { "clean": "rimraf lib", "format": "prettier --write {src,test}/**/*.js", - "test": "npm run lint && jest --verbose", - "test:cover": "npm test -- --coverage", + "test": "npm run lint && npm run test:only", + "test:cover": "npm run test:only -- --coverage", "test:prod": "cross-env BABEL_ENV=production npm run test", - "test:watch": "npm test -- --watch", + "test:only": "jest --verbose", + "test:watch": "npm run test:only -- --watch", "lint": "eslint src test", "build": "cross-env BABEL_ENV=production babel src --out-dir lib", "prepublish": "npm run clean && npm run test && npm run build" @@ -64,6 +65,9 @@ } }, "lint-staged": { - "{src,test}/**/*.js": ["eslint --fix", "prettier --write"] + "{src,test}/**/*.js": [ + "eslint --fix", + "prettier --write" + ] } } diff --git a/src/formLeaveGuardMixin.js b/src/formLeaveGuardMixin.js index 7200283..3817e80 100644 --- a/src/formLeaveGuardMixin.js +++ b/src/formLeaveGuardMixin.js @@ -48,6 +48,7 @@ const FormLeaveGuardMixin = (formKeys, options = {}) => { ); if (areAllClean) return next(); } else { + /* istanbul ignore next - Uncommon case */ return next(); } @@ -70,7 +71,11 @@ const FormLeaveGuardMixin = (formKeys, options = {}) => { if (shouldContinue) { // Reset the form before leaving (otherwise it sometimes retains data) - formKeys.forEach((key) => resetForm.call(this, key)); + if (Array.isArray(formKeys)) { + formKeys.forEach((key) => resetForm.call(this, key)); + } else { + resetForm.call(this, formKeys); + } return next(); } @@ -78,7 +83,7 @@ const FormLeaveGuardMixin = (formKeys, options = {}) => { // Set the callback and pass the reference to the "onPrevent" callback this[callbackKey] = callback; - onPrevent(callback); + onPrevent && onPrevent(callback); }, }; }; diff --git a/test/formCreateMixin.test.js b/test/formCreateMixin.test.js index ec0e681..a8e0256 100644 --- a/test/formCreateMixin.test.js +++ b/test/formCreateMixin.test.js @@ -6,6 +6,9 @@ import { createForm, FormCreateMixin } from "../src"; describe("Form Create Mixin", () => { let wrapper = null; + const Component = Vue.component("formComponent", { + template: "
", + }); const formName = "form"; const fields = { @@ -22,10 +25,6 @@ describe("Form Create Mixin", () => { // Setup the component with mixin before each test const beforeHandler = () => { - const Component = Vue.component("formComponent", { - template: "
", - }); - wrapper = shallowMount(Component, { mixins: [ // NOTE: Must spread setup values to avoid mutating by reference! @@ -34,11 +33,12 @@ describe("Form Create Mixin", () => { }); }; - beforeEach(beforeHandler); - - afterEach(() => { + const afterHandler = () => { wrapper.destroy(); - }); + }; + + beforeEach(beforeHandler); + afterEach(afterHandler); it("should run mixin in component", () => { // Should import successfully @@ -64,7 +64,8 @@ describe("Form Create Mixin", () => { }); describe("should handle form data methods", () => { - beforeAll(beforeHandler); + beforeEach(beforeHandler); + afterEach(afterHandler); it("'should get form values", () => { expect(wrapper.vm[formName].getValues()).toEqual(fields); @@ -96,7 +97,8 @@ describe("Form Create Mixin", () => { }); describe("should handle form flag methods", () => { - beforeAll(beforeHandler); + beforeEach(beforeHandler); + afterEach(afterHandler); it("should not set invalid flags", () => { wrapper.vm[formName].setFlag("locked", true); @@ -121,7 +123,8 @@ describe("Form Create Mixin", () => { }); describe("should handle form computed flags", () => { - beforeAll(beforeHandler); + beforeEach(beforeHandler); + afterEach(afterHandler); it("should calculate computed 'changed' flag", () => { // Should start unchanged @@ -147,19 +150,17 @@ describe("Form Create Mixin", () => { let wrapperStatic = null; beforeEach(() => { - // NOTE: Create separate Vue instance to test these options (behavior changes) - const ComponentStatic = Vue.component("staticFormComponent", { - template: "
", - }); - const options = { calculateChanged: false, flags: { locked: true } }; - wrapperStatic = shallowMount(ComponentStatic, { + wrapperStatic = shallowMount(Component, { mixins: [ // NOTE: Must spread setup values to avoid mutating by reference! FormCreateMixin(formName, { ...fields }, options), ], }); }); + afterEach(() => { + wrapperStatic.destroy(); + }); it("should use custom flags", () => { // Should have custom flag set by mixin @@ -205,7 +206,6 @@ describe("Form Create Function", () => { }; beforeEach(beforeHandler); - afterEach(() => { wrapper.destroy(); }); diff --git a/test/formLeaveGuardMixin.test.js b/test/formLeaveGuardMixin.test.js index d276ff3..500e587 100644 --- a/test/formLeaveGuardMixin.test.js +++ b/test/formLeaveGuardMixin.test.js @@ -1,9 +1,178 @@ +import Vue from "vue"; +import { shallowMount } from "@vue/test-utils"; + // Utilities -import { FormLeaveGuardMixin } from "../src"; +import { FormCreateMixin, FormLeaveGuardMixin } from "../src"; + +const nextFn = jest.fn(); +const onPreventFn = jest.fn(); +const Component = Vue.component("formComponent", { + template: "
", +}); + +/** + * Shallow mount a component with the mixin (and custom options) + * @param {string} formKey - Form key + * @param {Object} mixinOptions - Mixin options + * @return - Vue wrapper + */ +const mountComponent = (formKey, mixinOptions = {}) => + shallowMount(Component, { + mixins: [ + // NOTE: Must spread setup values to avoid mutating by reference! + FormCreateMixin(formName, { ...fields }), + FormLeaveGuardMixin(formKey, { + activeKey, + callbackKey, + onPrevent: onPreventFn, + ...mixinOptions, + }), + ], + }); + +const formName = "form"; +const fields = { + email: "test@example.com", + password: "******", +}; +const fieldChanges = { + email: "noone@example.com", +}; + +const activeKey = "isLeavingForm"; +const callbackKey = "onFormLeave"; describe("Form Leave Guard Mixin", () => { - it("should import mixin", () => { - // Assert + let wrapper = null; + let beforeRouteLeave = null; + + // Setup the component with mixin before each test + const beforeHandler = () => { + wrapper = mountComponent(formName); + ({ beforeRouteLeave } = wrapper.vm.$options); + }; + + const afterHandler = () => { + wrapper.destroy(); + nextFn.mockReset(); + onPreventFn.mockReset(); + }; + + beforeEach(beforeHandler); + afterEach(afterHandler); + + it("should run mixin in component", () => { + // Should import successfully expect(FormLeaveGuardMixin).toBeTruthy(); + // Should have active key from mixin options + expect(wrapper.vm[activeKey]).toBe(false); + // Should have callback key from mixin options + expect(wrapper.vm[callbackKey]).toBeNull(); + }); + + it("should handle leaving clean form", () => { + beforeRouteLeave.call(wrapper.vm, "toObj", "fromObj", nextFn); + + // Should call "next" when leaving clean form + expect(nextFn).toHaveBeenCalled(); + // Should not set active key when leaving clean form + expect(wrapper.vm[activeKey]).toBe(false); + // Should not set callback handler when leaving clean form + expect(wrapper.vm[callbackKey]).toBeNull(); + // Should not call custom prevent handler when leaving clean form + expect(onPreventFn).not.toHaveBeenCalled(); + }); + + it("should handle leaving clean forms (multiple)", () => { + const wrapperMulti = mountComponent([formName]); + const beforeRouteLeaveMulti = wrapperMulti.vm.$options.beforeRouteLeave; + beforeRouteLeaveMulti.call(wrapperMulti.vm, "toObj", "fromObj", nextFn); + + // Should call "next" when leaving clean forms + expect(nextFn).toHaveBeenCalled(); + // Should not set active key when leaving clean forms + expect(wrapperMulti.vm[activeKey]).toBe(false); + // Should not set callback handler when leaving clean forms + expect(wrapperMulti.vm[callbackKey]).toBeNull(); + // Should not call custom prevent handler when leaving clean forms + expect(onPreventFn).not.toHaveBeenCalled(); + }); + + describe("should handle leaving dirty form", () => { + beforeEach(() => { + beforeHandler(); + + // Make changes to form (to trigger "changed" flag) + wrapper.vm[formName].setValues({ ...fieldChanges }, false); + beforeRouteLeave.call(wrapper.vm, "toObj", "fromObj", nextFn); + }); + afterEach(afterHandler); + + it("should prevent leaving dirty form", async () => { + // Should not call "next" when leaving dirty form + expect(nextFn).not.toHaveBeenCalled(); + // Should set "is leaving" active getter + expect(wrapper.vm[activeKey]).toBe(true); + // Should set form leave confirmation callback + expect(wrapper.vm[callbackKey]).not.toBeNull(); + + // Should remove callback after it is the setter is called (v-model issue) + wrapper.vm[activeKey] = false; + expect(wrapper.vm[callbackKey]).not.toBeNull(); + await Vue.nextTick(); + expect(wrapper.vm[callbackKey]).toBeNull(); + }); + + it("should remain on dirty form after cancellation", () => { + wrapper.vm[callbackKey](); + + // Should reset callback key when staying on dirty form + expect(wrapper.vm[callbackKey]).toBeNull(); + // Should not call "next" when staying on dirty form + expect(nextFn).not.toHaveBeenCalled(); + }); + + it("should leave dirty form after confirmation", () => { + wrapper.vm[callbackKey](true); + + // Should reset callback key when leaving dirty form + expect(wrapper.vm[callbackKey]).toBeNull(); + // Should call "next" when leaving dirty form + expect(nextFn).toHaveBeenCalled(); + // Should reset form state when leaving dirty form + expect(wrapper.vm[formName].getValues()).toEqual(fields); + }); + + it("should leave dirty forms (multiple) after confirmation", () => { + const wrapperMulti = mountComponent([formName]); + const beforeRouteLeaveMulti = wrapperMulti.vm.$options.beforeRouteLeave; + wrapperMulti.vm[formName].setValues({ ...fieldChanges }, false); + beforeRouteLeaveMulti.call(wrapperMulti.vm, "toObj", "fromObj", nextFn); + wrapperMulti.vm[callbackKey](true); + + // Should reset callback key when leaving dirty form + expect(wrapperMulti.vm[callbackKey]).toBeNull(); + // Should call "next" when leaving dirty form + expect(nextFn).toHaveBeenCalled(); + // Should reset form state when leaving dirty form + expect(wrapperMulti.vm[formName].getValues()).toEqual(fields); + }); + }); + + it("should prevent leaving dirty form (without callbacks) if specified", () => { + const wrapperPrevent = mountComponent(formName, { + onlyPrevent: true, + }); + const beforeRouteLeavePrevent = wrapperPrevent.vm.$options.beforeRouteLeave; + + wrapperPrevent.vm[formName].setValues({ ...fieldChanges }, false); + beforeRouteLeavePrevent.call(wrapperPrevent.vm, "toObj", "fromObj", nextFn); + + // Should not call "next" when only preventing leaving dirty form + expect(nextFn).not.toBeCalled(); + // Should not set callback when only preventing leaving dirty form + expect(wrapperPrevent.vm[callbackKey]).toBeNull(); + // Should call custom prevent handler when only preventing leaving dirty form + expect(onPreventFn).toBeCalled(); }); });