diff --git a/.gitignore b/.gitignore index 0035d70c2cfab2a247a93613b879c88a6b2fba6b..247cd6166cca2da2e9aa9cdf1bdad6338f02b627 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ .DS_Store node_modules /dist +coverage # local env files diff --git a/package-lock.json b/package-lock.json index 6c90cbdf471980e235f8a00ec59d5448b42f5298..b00e266c5d8bf882654b8e90bb2188ae5fe3869b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -40,6 +40,7 @@ "eslint-plugin-prettier": "^4.0.0", "eslint-plugin-vue": "^8.0.3", "jest": "^27.0.5", + "jest-serializer-vue": "^2.0.2", "postcss": "^8.4.12", "prettier": "^2.4.1", "tailwindcss": "^3.0.24" diff --git a/package.json b/package.json index fe4e8182e70eaa9150b6c4bd6aa9e414408ee7a9..cce55e6cd844586f7c09af08b82704f174640fc8 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "eslint-plugin-prettier": "^4.0.0", "eslint-plugin-vue": "^8.0.3", "jest": "^27.0.5", + "jest-serializer-vue": "^2.0.2", "postcss": "^8.4.12", "prettier": "^2.4.1", "tailwindcss": "^3.0.24" diff --git a/src/components/NewPasswordForm.vue b/src/components/NewPasswordForm.vue index 5800624d849148ac6043af8f6f908c002f55d61e..c86145ce10db183a6c8781b18c95c99c0e7c53b0 100644 --- a/src/components/NewPasswordForm.vue +++ b/src/components/NewPasswordForm.vue @@ -58,15 +58,14 @@ export default { rules: { required: (value) => !!value || "Feltet er påkrevd", min: (v) => v.length >= 8 || "Minimum 8 tegn", - passwordConfirmation: (v) => v === this.user.password || "Passordene må være like" + passwordConfirmation: (v) => + v === this.user.password || "Passordene må være like", }, }; }, methods: { - async setNewPassword() { - - }, + async setNewPassword() {}, validate() { this.$refs.form.validate(); }, diff --git a/src/components/RegisterFormComponent.vue b/src/components/RegisterFormComponent.vue index b69d38827a90245b00a8c961b4ef816511074bd6..240895bffbe2fe98be228f19e2d7549f3bc24227 100644 --- a/src/components/RegisterFormComponent.vue +++ b/src/components/RegisterFormComponent.vue @@ -1,155 +1,250 @@ <template> - <v-form ref="form" v-model="valid" lazy-validation> - <v-text-field - v-model="email" - :rules="emailRules" - label="E-mail" - required - ></v-text-field> - - <v-text-field - v-model="password" - :counter="32" - :rules="passwordRules" - label="Passord" - :append-icon="passwordHidden ? 'mdi-eye' : 'mdi-eye-off'" - :type="passwordHidden ? 'text' : 'password'" - @click:append="passwordHidden = !passwordHidden" - required - ></v-text-field> - - <v-container class="grey lighten-5"> - <v-row> - <v-text-field - class="pr-2" - v-model="firstName" - :counter="32" - :rules="firstNameRules" - label="Fornavn" - required - ></v-text-field> - - <v-text-field - class="pl-2" - v-model="lastName" - :counter="32" - :rules="lastNameRules" - label="Etternavn" - required - ></v-text-field> - </v-row> - </v-container> - - <v-text-field - v-model="address" - :counter="32" - :rules="addressRules" - label="Addresse" - required - ></v-text-field> - - <!-- <v-text-field - v-model="confirmPassword" - :rules="confirmPasswordRules" - label="Bekreft passord" - :append-icon="confirmPasswordHidden ? 'mdi-eye' : 'mdi-eye-off'" - :type="confirmPasswordHidden ? 'text' : 'password'" - @click:append="confirmPasswordHidden = !confirmPasswordHidden" - required - ></v-text-field> --> - - <!-- <v-select - v-model="select" - :items="items" - :rules="[(v) => !!v || 'Item is required']" - label="Item" - required - ></v-select> --> - - <!-- <v-checkbox - v-model="checkbox" - :rules="[(v) => !!v || 'You must agree to continue!']" - label="Do you agree?" - required - ></v-checkbox> --> - - <v-btn :disabled="!valid" color="success" class="mr-4" @click="submit()" - >Registrer</v-btn - > - - <v-btn color="error" class="mr-4" @click="reset()">Tøm felter</v-btn> - </v-form> + <section + class="max-w-4xl p-6 mx-auto bg-white rounded-md shadow-md dark:bg-gray-800" + > + <h2 class="text-lg font-semibold text-gray-700 capitalize dark:text-white"> + Opprett ny bruker + </h2> + + <form @submit.prevent> + <div class="grid grid-cols-1 gap-6 mt-4 sm:grid-cols-2"> + <div> + <label class="text-gray-700 dark:text-gray-200" for="email" + >E-mail</label + > + <input + v-model="email" + id="email" + type="email" + class="block w-full px-4 py-2 mt-2 text-gray-700 bg-white border border-gray-200 rounded-md dark:bg-gray-800 dark:text-gray-300 dark:border-gray-600 focus:border-blue-400 focus:ring-blue-300 focus:ring-opacity-40 dark:focus:border-blue-300 focus:outline-none focus:ring" + /> + </div> + + <div> + <label class="text-gray-700 dark:text-gray-200" for="password" + >Passord</label + > + <input + v-model="password" + id="password" + type="password" + class="block w-full px-4 py-2 mt-2 text-gray-700 bg-white border border-gray-200 rounded-md dark:bg-gray-800 dark:text-gray-300 dark:border-gray-600 focus:border-blue-400 focus:ring-blue-300 focus:ring-opacity-40 dark:focus:border-blue-300 focus:outline-none focus:ring" + /> + </div> + + <div> + <label class="text-gray-700 dark:text-gray-200" for="confirmPassword" + >Bekreft Passord</label + > + <input + v-model="confirmPassword" + id="confirmPassword" + type="password" + class="block w-full px-4 py-2 mt-2 text-gray-700 bg-white border border-gray-200 rounded-md dark:bg-gray-800 dark:text-gray-300 dark:border-gray-600 focus:border-blue-400 focus:ring-blue-300 focus:ring-opacity-40 dark:focus:border-blue-300 focus:outline-none focus:ring" + /> + </div> + + <div> + <label class="text-gray-700 dark:text-gray-200" for="firstName" + >Fornavn</label + > + <input + data-test="firstNameTest" + v-model="firstName" + id="firstName" + type="text" + class="block w-full px-4 py-2 mt-2 text-gray-700 bg-white border border-gray-200 rounded-md dark:bg-gray-800 dark:text-gray-300 dark:border-gray-600 focus:border-blue-400 focus:ring-blue-300 focus:ring-opacity-40 dark:focus:border-blue-300 focus:outline-none focus:ring" + /> + </div> + + <div> + <label class="text-gray-700 dark:text-gray-200" for="lastName" + >Etternavn</label + > + <input + v-model="lastName" + id="lastName" + type="text" + class="block w-full px-4 py-2 mt-2 text-gray-700 bg-white border border-gray-200 rounded-md dark:bg-gray-800 dark:text-gray-300 dark:border-gray-600 focus:border-blue-400 focus:ring-blue-300 focus:ring-opacity-40 dark:focus:border-blue-300 focus:outline-none focus:ring" + /> + </div> + + <div> + <label class="text-gray-700 dark:text-gray-200" for="address" + >Addresse</label + > + <input + v-model="address" + id="address" + type="text" + class="block w-full px-4 py-2 mt-2 text-gray-700 bg-white border border-gray-200 rounded-md dark:bg-gray-800 dark:text-gray-300 dark:border-gray-600 focus:border-blue-400 focus:ring-blue-300 focus:ring-opacity-40 dark:focus:border-blue-300 focus:outline-none focus:ring" + /> + </div> + </div> + + <div class="flex justify-end mt-6"> + <button + class="px-6 py-2 leading-5 text-white transition-colors duration-200 transform bg-gray-700 rounded-md hover:bg-gray-600 focus:outline-none focus:bg-gray-600" + @click="submit()" + type="submit" + :disabled="loading" + > + <div v-if="loading"> + <div v-if="loading" class="lds-ring"> + <div></div> + <div></div> + <div></div> + <div></div> + </div> + </div> + <div v-else>Lagre</div> + </button> + </div> + </form> + </section> + <ul data-test="errorMessageList"> + <li v-if="errorMessage" data-test="customErrorMsg">{{ errorMessage }}</li> + <li v-for="error of v$.$errors" :key="error.$uid"> + <!-- {{ error.$validator }} --> + Field + {{ error.$property }} + has error: + {{ error.$message }} + </li> + </ul> </template> + <script> -import axios from "axios"; +import useVuelidate from "@vuelidate/core"; +import { doLogin, registerUser } from "@/utils/apiutil"; +import { required, email, minLength, sameAs } from "@vuelidate/validators"; -export default { - data: () => ({ - passwordHidden: false, - // confirmPasswordHidden: false, - valid: true, - firstName: "", - firstNameRules: [ - (v) => !!v || "Fornavn er påkrevd", - (v) => (v && v.length <= 32) || "Fornavn må være mindre enn 32 bokstaver", - ], - lastName: "", - lastNameRules: [ - (v) => !!v || "Etternavn er påkrevd", - (v) => - (v && v.length <= 32) || "Etternavn må være mindre enn 32 bokstaver", - ], - address: "", - addressRules: [ - (v) => !!v || "Addresse er påkrevd", - (v) => - (v && v.length <= 32) || "Addresse må være mindre enn 32 bokstaver", - ], - password: "", - passwordRules: [ - (v) => !!v || "Passord er påkrevd", - (v) => (v && v.length <= 32) || "Passord må være mindre enn 32 tegn", - (v) => (v && v.length >= 8) || "Passord må være større enn 8 tegn", - ], - // confirmPassword: "", - // confirmPasswordRules: [ - // (v) => !!v || "Passord er påkrevd", - // (v) => (v && v.length <= 32) || "Passord må være mindre enn 32 bokstaver", - // // (v) => v === this.password || "Passordene må være like", - // ], - email: "", - emailRules: [ - (v) => !!v || "E-mail is required", - (v) => /.+@.+\..+/.test(v) || "E-mail must be valid", - ], - // select: null, - // items: ["Item 1", "Item 2", "Item 3", "Item 4"], - // checkbox: false, - }), +// const isEmailTaken = (value) => +// fetch(`/api/unique/${value}`).then((r) => r.json()); // check the email in the server +export default { + setup: () => ({ v$: useVuelidate() }), + data() { + return { + errorMessage: "", + loading: false, + email: "", + password: "", + confirmPassword: "", + firstName: "", + lastName: "", + address: "", + }; + }, + validations() { + return { + email: { + required, + email, + // isUnique: helpers.withAsync(isEmailTaken), + }, + password: { + required, + minLength: minLength(8), + }, + confirmPassword: { sameAs: sameAs(this.password) }, + firstName: { required }, + lastName: { required }, + address: { required }, + }; + }, methods: { - submit() { - console.log("Attempting to register user"); - this.valid = this.$refs.form.validate(); - if (!this.valid) return; - this.valid = false; - console.log("User is validated"); - axios - .post("http://localhost:3000/api/register", { - email: this.email, - firstName: this.firstName, - lastname: this.lastName, - password: this.password, - address: this.address, - }) - .then(console.log("Sent")) - .catch((e) => console.log(e)); + async submit() { + //Display loading symbol + this.loading = true; + + //Validate form + const result = await this.v$.$validate(); + if (!result) { + this.loading = false; + return; + } + + //Send a request to create a user and save success as a bool + const userCreated = await this.sendRegisterRequest(); + + //If a user is created succsessfully, try to login + //If we get this far, we will be pushed anyway so there is no point updating "loading" + if (!userCreated) { + this.errorMessage = "Could not create user."; + return; + } + + const loginRequest = { + email: this.email, + password: this.password, + }; + + const loginResponse = await doLogin(loginRequest); + + if (loginResponse === "Failed login") { + this.errorMessage = "Failed to log in with new user"; + this.$store.commit("logout"); + this.$router.push("/login"); + return; + } + + this.$store.commit("saveToken", loginResponse); + this.$router.push("/"); }, - reset() { - this.$refs.form.reset(); - this.$refs.form.resetValidation(); - this.valid = true; + async sendRegisterRequest() { + const registerInfo = { + email: this.email, + firstName: this.firstName, + lastname: this.lastName, + password: this.password, + address: this.address, + }; + + const response = await registerUser(registerInfo); + + if (response.status === 200) return true; + return false; }, }, }; </script> + +<style scoped> +/* https://loading.io/css/ */ +.lds-ring { + display: inline-block; + position: relative; + width: 20px; + height: 20px; +} +.lds-ring div { + box-sizing: border-box; + display: block; + position: absolute; + width: 16px; + height: 16px; + margin: 2px; + border: 2px solid #fff; + border-radius: 50%; + animation: lds-ring 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite; + border-color: #fff transparent transparent transparent; +} +.lds-ring div:nth-child(1) { + animation-delay: -0.45s; +} +.lds-ring div:nth-child(2) { + animation-delay: -0.3s; +} +.lds-ring div:nth-child(3) { + animation-delay: -0.15s; +} +@keyframes lds-ring { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } +} +</style> diff --git a/src/utils/apiutil.js b/src/utils/apiutil.js index 8812b429588b3974d65292f39fcc60bd4dc3647c..a438e020751965e604551955bafcece3294f2e7a 100644 --- a/src/utils/apiutil.js +++ b/src/utils/apiutil.js @@ -1,8 +1,10 @@ import axios from "axios"; +const API_URL = process.env.VUE_APP_BASEURL; + export function doLogin(loginRequest) { return axios - .post(process.env.VUE_APP_BASEURL + "login/authentication", loginRequest) + .post(API_URL + "login/authentication", loginRequest) .then((response) => { return response.data; }) @@ -11,3 +13,18 @@ export function doLogin(loginRequest) { return error.response; }); } + +export function registerUser(registerInfo) { + return axios + .post(API_URL + "register", { + email: registerInfo.email, + firstName: registerInfo.firstName, + lastname: registerInfo.lastname, + password: registerInfo.password, + address: registerInfo.address, + }) + .then((response) => { + return response; + }) + .catch((err) => console.log(err)); +} diff --git a/src/views/RegisterView.vue b/src/views/RegisterView.vue index 9d2ee691f97a923ea3fde5a7d1881792a252b801..c1478be27a6919308ca75adad5a1df3aca8303a3 100644 --- a/src/views/RegisterView.vue +++ b/src/views/RegisterView.vue @@ -1,5 +1,7 @@ <template> - <register-form-component id="form" class="pa-8" /> + <div class="h-screen bg-gray-200 content-center grid place-items-center"> + <RegisterFormComponent /> + </div> </template> <script> @@ -11,10 +13,3 @@ export default { }, }; </script> - -<style scoped> -#form { - max-width: 600px; - margin: auto; -} -</style> diff --git a/tailwind.config.js b/tailwind.config.js index f521631db48dc78b5a6597a6552d9d95ecaee4a5..7cd4f37efd95627b8f362e1b210fd12b384925d9 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -1,4 +1,5 @@ module.exports = { + darkMode: "class", content: ["./index.html", "./src/**/*.{vue,js,ts,jsx,tsx}"], theme: { extend: {}, diff --git a/tests/unit/RegisterUserComponent.spec.js b/tests/unit/RegisterUserComponent.spec.js new file mode 100644 index 0000000000000000000000000000000000000000..a13d18e437230618a1d38e19b9c7bb00d27c1d7f --- /dev/null +++ b/tests/unit/RegisterUserComponent.spec.js @@ -0,0 +1,50 @@ +import { mount } from "@vue/test-utils"; +import RegisterFormComponent from "@/components/RegisterFormComponent"; + +describe("RegisterFormComponent", () => { + let wrapper; + + beforeEach(() => { + wrapper = mount(RegisterFormComponent); + }); + + it("renders correctly", () => { + expect(wrapper.element).toMatchSnapshot(); + }); + + it("is instantiated", () => { + expect(wrapper.exists()).toBeTruthy(); + }); + + it("renders error message to user", async () => { + await wrapper.setData({ errorMessage: "test message" }); + expect(wrapper.find('li[data-test="customErrorMsg"]').text()).toBe( + "test message" + ); + }); + + it("renders the h2 text correctly", () => { + expect(wrapper.find("h2").text()).toBe("Opprett ny bruker"); + }); + + it("has a button", () => { + expect(wrapper.exists("button")).toBe(true); + }); + + it("updates data when field is updated", async () => { + await wrapper.find('input[data-test="firstNameTest"]').setValue("Gunnar"); + expect(wrapper.vm.firstName).toBe("Gunnar"); + }); + + it("displays 5 error messages when submit is clicked with no data", async () => { + await wrapper.find("button").trigger("click"); + expect(wrapper.findAll("li").length).toBe(5); + }); + + /* it("button click with correct sum", () => { + wrapper.setData({ guess: "15" }); + const button = wrapper.find("button"); + button.trigger("click"); + expect(wrapper.vm.message).toBe("SUCCESS!"); + }); */ +}); diff --git a/tests/unit/__snapshots__/RegisterUserComponent.spec.js.snap b/tests/unit/__snapshots__/RegisterUserComponent.spec.js.snap new file mode 100644 index 0000000000000000000000000000000000000000..f1a63d05fc1ff431b8677b83e799442934955b68 --- /dev/null +++ b/tests/unit/__snapshots__/RegisterUserComponent.spec.js.snap @@ -0,0 +1,123 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`RegisterFormComponent renders correctly 1`] = ` +<div + data-v-app="" +> + + <section + class="max-w-4xl p-6 mx-auto bg-white rounded-md shadow-md dark:bg-gray-800" + > + <h2 + class="text-lg font-semibold text-gray-700 capitalize dark:text-white" + > + Opprett ny bruker + </h2> + <form> + <div + class="grid grid-cols-1 gap-6 mt-4 sm:grid-cols-2" + > + <div> + <label + class="text-gray-700 dark:text-gray-200" + for="email" + > + E-mail + </label> + <input + class="block w-full px-4 py-2 mt-2 text-gray-700 bg-white border border-gray-200 rounded-md dark:bg-gray-800 dark:text-gray-300 dark:border-gray-600 focus:border-blue-400 focus:ring-blue-300 focus:ring-opacity-40 dark:focus:border-blue-300 focus:outline-none focus:ring" + id="email" + type="email" + /> + </div> + <div> + <label + class="text-gray-700 dark:text-gray-200" + for="password" + > + Passord + </label> + <input + class="block w-full px-4 py-2 mt-2 text-gray-700 bg-white border border-gray-200 rounded-md dark:bg-gray-800 dark:text-gray-300 dark:border-gray-600 focus:border-blue-400 focus:ring-blue-300 focus:ring-opacity-40 dark:focus:border-blue-300 focus:outline-none focus:ring" + id="password" + type="password" + /> + </div> + <div> + <label + class="text-gray-700 dark:text-gray-200" + for="confirmPassword" + > + Bekreft Passord + </label> + <input + class="block w-full px-4 py-2 mt-2 text-gray-700 bg-white border border-gray-200 rounded-md dark:bg-gray-800 dark:text-gray-300 dark:border-gray-600 focus:border-blue-400 focus:ring-blue-300 focus:ring-opacity-40 dark:focus:border-blue-300 focus:outline-none focus:ring" + id="confirmPassword" + type="password" + /> + </div> + <div> + <label + class="text-gray-700 dark:text-gray-200" + for="firstName" + > + Fornavn + </label> + <input + class="block w-full px-4 py-2 mt-2 text-gray-700 bg-white border border-gray-200 rounded-md dark:bg-gray-800 dark:text-gray-300 dark:border-gray-600 focus:border-blue-400 focus:ring-blue-300 focus:ring-opacity-40 dark:focus:border-blue-300 focus:outline-none focus:ring" + data-test="firstNameTest" + id="firstName" + type="text" + /> + </div> + <div> + <label + class="text-gray-700 dark:text-gray-200" + for="lastName" + > + Etternavn + </label> + <input + class="block w-full px-4 py-2 mt-2 text-gray-700 bg-white border border-gray-200 rounded-md dark:bg-gray-800 dark:text-gray-300 dark:border-gray-600 focus:border-blue-400 focus:ring-blue-300 focus:ring-opacity-40 dark:focus:border-blue-300 focus:outline-none focus:ring" + id="lastName" + type="text" + /> + </div> + <div> + <label + class="text-gray-700 dark:text-gray-200" + for="address" + > + Addresse + </label> + <input + class="block w-full px-4 py-2 mt-2 text-gray-700 bg-white border border-gray-200 rounded-md dark:bg-gray-800 dark:text-gray-300 dark:border-gray-600 focus:border-blue-400 focus:ring-blue-300 focus:ring-opacity-40 dark:focus:border-blue-300 focus:outline-none focus:ring" + id="address" + type="text" + /> + </div> + </div> + <div + class="flex justify-end mt-6" + > + <button + class="px-6 py-2 leading-5 text-white transition-colors duration-200 transform bg-gray-700 rounded-md hover:bg-gray-600 focus:outline-none focus:bg-gray-600" + type="submit" + > + <div> + Lagre + </div> + </button> + </div> + </form> + </section> + <ul + data-test="errorMessageList" + > + <!--v-if--> + + + </ul> + +</div> +`;