mirror of
https://github.com/mealie-recipes/mealie.git
synced 2025-08-05 13:35:23 +02:00
chore: frontend testing setup (#1739)
* add vitest * initialize lib w/ tests * move to dev dep * run tests in CI * update file names * move api folder to lib * move api and api types to same folder * update generator outpath * rm husky * i guess i _did_ need those types * reorg types * extract validators into testable components * (WIP) start composable testing * fix import type * fix linter complaint * simplify icon type def * fix linter errors (maybe?) * rename client file for sorting
This commit is contained in:
parent
9f6bcc83d5
commit
fcc5d99d40
182 changed files with 902 additions and 487 deletions
|
@ -1,9 +1,8 @@
|
|||
import { AxiosResponse } from "axios";
|
||||
import { useContext } from "@nuxtjs/composition-api";
|
||||
import type { NuxtAxiosInstance } from "@nuxtjs/axios";
|
||||
import { AdminAPI, Api } from "~/api";
|
||||
import { ApiRequestInstance, RequestResponse } from "~/types/api";
|
||||
import { PublicApi } from "~/api/public-api";
|
||||
import { ApiRequestInstance, RequestResponse } from "~/lib/api/types/non-generated";
|
||||
import { AdminAPI, PublicApi, UserApi } from "~/lib/api";
|
||||
|
||||
const request = {
|
||||
async safe<T, U>(
|
||||
|
@ -66,12 +65,12 @@ export const useAdminApi = function (): AdminAPI {
|
|||
return new AdminAPI(requests);
|
||||
};
|
||||
|
||||
export const useUserApi = function (): Api {
|
||||
export const useUserApi = function (): UserApi {
|
||||
const { $axios, i18n } = useContext();
|
||||
$axios.setHeader("Accept-Language", i18n.locale);
|
||||
|
||||
const requests = getRequests($axios);
|
||||
return new Api(requests);
|
||||
return new UserApi(requests);
|
||||
};
|
||||
|
||||
export const usePublicApi = function (): PublicApi {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { ref, Ref, useAsync, useContext } from "@nuxtjs/composition-api";
|
||||
import { useAsyncKey } from "../use-utils";
|
||||
import { AppInfo } from "~/types/api-types/admin";
|
||||
import { AppInfo } from "~/lib/api/types/admin";
|
||||
|
||||
export function useAppInfo(): Ref<AppInfo | null> {
|
||||
const appInfo = ref<null | AppInfo>(null);
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { Ref, useAsync } from "@nuxtjs/composition-api";
|
||||
import { useAsyncKey } from "../use-utils";
|
||||
import { BaseCRUDAPI } from "~/api/_base";
|
||||
import { BaseCRUDAPI } from "~/lib/api/base/base-clients";
|
||||
|
||||
type BoundT = {
|
||||
id?: string | number;
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { computed, ComputedRef, ref, Ref, useContext } from "@nuxtjs/composition-api";
|
||||
import { UserOut } from "~/types/api-types/user";
|
||||
import { UserOut } from "~/lib/api/types/user";
|
||||
|
||||
export enum PageMode {
|
||||
EDIT = "EDIT",
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import DOMPurify from "isomorphic-dompurify";
|
||||
import { useFraction } from "./use-fraction";
|
||||
import { RecipeIngredient } from "~/types/api-types/recipe";
|
||||
import { RecipeIngredient } from "~/lib/api/types/recipe";
|
||||
const { frac } = useFraction();
|
||||
|
||||
function sanitizeIngredientHTML(rawHtml: string) {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { Ref } from "@nuxtjs/composition-api";
|
||||
import { useStaticRoutes } from "~/composables/api";
|
||||
import { Recipe } from "~/types/api-types/recipe";
|
||||
import { Recipe } from "~/lib/api/types/recipe";
|
||||
|
||||
export interface RecipeMeta {
|
||||
title?: string;
|
||||
|
@ -53,6 +53,6 @@ export const useRecipeMeta = () => {
|
|||
},
|
||||
],
|
||||
};
|
||||
};
|
||||
}
|
||||
return { recipeMeta };
|
||||
};
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { computed, reactive, ref, Ref } from "@nuxtjs/composition-api";
|
||||
import Fuse from "fuse.js";
|
||||
import { Recipe } from "~/types/api-types/recipe";
|
||||
import { Recipe } from "~/lib/api/types/recipe";
|
||||
|
||||
export const useRecipeSearch = (recipes: Ref<Recipe[] | null>) => {
|
||||
const localState = reactive({
|
||||
|
|
|
@ -2,7 +2,7 @@ import { reactive, ref, useAsync } from "@nuxtjs/composition-api";
|
|||
import { useAsyncKey } from "../use-utils";
|
||||
import { useUserApi } from "~/composables/api";
|
||||
import { VForm } from "~/types/vuetify";
|
||||
import { RecipeTool } from "~/types/api-types/user";
|
||||
import { RecipeTool } from "~/lib/api/types/user";
|
||||
|
||||
export const useTools = function (eager = true) {
|
||||
const workingToolData = reactive<RecipeTool>({
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { ref, onMounted } from "@nuxtjs/composition-api";
|
||||
import { useUserApi } from "~/composables/api";
|
||||
import { Recipe } from "~/types/api-types/recipe";
|
||||
import { Recipe } from "~/lib/api/types/recipe";
|
||||
|
||||
export const useRecipe = function (slug: string, eager = true) {
|
||||
const api = useUserApi();
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { useAsync, ref } from "@nuxtjs/composition-api";
|
||||
import { useAsyncKey } from "../use-utils";
|
||||
import { useUserApi } from "~/composables/api";
|
||||
import { Recipe } from "~/types/api-types/recipe";
|
||||
import { Recipe } from "~/lib/api/types/recipe";
|
||||
|
||||
export const allRecipes = ref<Recipe[]>([]);
|
||||
export const recentRecipes = ref<Recipe[]>([]);
|
||||
|
@ -21,7 +21,14 @@ export const useLazyRecipes = function () {
|
|||
tag: string | null = null,
|
||||
tool: string | null = null
|
||||
) {
|
||||
const { data } = await api.recipes.getAll(page, perPage, { orderBy, orderDirection, cookbook, "categories": category, "tags": tag, "tools": tool });
|
||||
const { data } = await api.recipes.getAll(page, perPage, {
|
||||
orderBy,
|
||||
orderDirection,
|
||||
cookbook,
|
||||
categories: category,
|
||||
tags: tag,
|
||||
tools: tool,
|
||||
});
|
||||
return data ? data.items : [];
|
||||
}
|
||||
|
||||
|
@ -54,7 +61,7 @@ export const useLazyRecipes = function () {
|
|||
appendRecipes,
|
||||
assignSorted,
|
||||
removeRecipe,
|
||||
replaceRecipes
|
||||
replaceRecipes,
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { reactive, ref, Ref } from "@nuxtjs/composition-api";
|
||||
import { useStoreActions } from "../partials/use-actions-factory";
|
||||
import { useUserApi } from "~/composables/api";
|
||||
import { RecipeCategory } from "~/types/api-types/admin";
|
||||
import { RecipeCategory } from "~/lib/api/types/admin";
|
||||
|
||||
const categoryStore: Ref<RecipeCategory[]> = ref([]);
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { ref, reactive, Ref } from "@nuxtjs/composition-api";
|
||||
import { useStoreActions } from "../partials/use-actions-factory";
|
||||
import { useUserApi } from "~/composables/api";
|
||||
import { IngredientFood } from "~/types/api-types/recipe";
|
||||
import { IngredientFood } from "~/lib/api/types/recipe";
|
||||
|
||||
let foodStore: Ref<IngredientFood[] | null> | null = null;
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { reactive, ref, Ref } from "@nuxtjs/composition-api";
|
||||
import { useStoreActions } from "../partials/use-actions-factory";
|
||||
import { MultiPurposeLabelOut } from "~/types/api-types/labels";
|
||||
import { MultiPurposeLabelOut } from "~/lib/api/types/labels";
|
||||
import { useUserApi } from "~/composables/api";
|
||||
|
||||
let labelStore: Ref<MultiPurposeLabelOut[] | null> | null = null;
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { reactive, ref, Ref } from "@nuxtjs/composition-api";
|
||||
import { useStoreActions } from "../partials/use-actions-factory";
|
||||
import { useUserApi } from "~/composables/api";
|
||||
import { RecipeTag } from "~/types/api-types/admin";
|
||||
import { RecipeTag } from "~/lib/api/types/admin";
|
||||
|
||||
const items: Ref<RecipeTag[]> = ref([]);
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { reactive, ref, Ref } from "@nuxtjs/composition-api";
|
||||
import { useStoreActions } from "../partials/use-actions-factory";
|
||||
import { useUserApi } from "~/composables/api";
|
||||
import { RecipeTool } from "~/types/api-types/recipe";
|
||||
import { RecipeTool } from "~/lib/api/types/recipe";
|
||||
|
||||
const toolStore: Ref<RecipeTool[]> = ref([]);
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { ref, reactive, Ref } from "@nuxtjs/composition-api";
|
||||
import { useStoreActions } from "../partials/use-actions-factory";
|
||||
import { useUserApi } from "~/composables/api";
|
||||
import { IngredientUnit } from "~/types/api-types/recipe";
|
||||
import { IngredientUnit } from "~/lib/api/types/recipe";
|
||||
|
||||
let unitStore: Ref<IngredientUnit[] | null> | null = null;
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { useAsync, ref, reactive } from "@nuxtjs/composition-api";
|
||||
import { toastLoading, loader } from "./use-toast";
|
||||
import { AllBackups, BackupOptions } from "~/types/api-types/admin";
|
||||
import { AllBackups, BackupOptions } from "~/lib/api/types/admin";
|
||||
import { useUserApi } from "~/composables/api";
|
||||
|
||||
interface ImportBackup {
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
* with the food, units, quantity, and notes.
|
||||
*/
|
||||
|
||||
import { IngredientFood, IngredientUnit } from "~/types/api-types/recipe";
|
||||
import { IngredientFood, IngredientUnit } from "~/lib/api/types/recipe";
|
||||
|
||||
export function getDisplayText(
|
||||
notes = "",
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { useAsync, ref, Ref } from "@nuxtjs/composition-api";
|
||||
import { useAsyncKey } from "./use-utils";
|
||||
import { useUserApi } from "~/composables/api";
|
||||
import { ReadCookBook, UpdateCookBook } from "~/types/api-types/cookbook";
|
||||
import { ReadCookBook, UpdateCookBook } from "~/lib/api/types/cookbook";
|
||||
|
||||
let cookbookStore: Ref<ReadCookBook[] | null> | null = null;
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@ import { useAsync, ref, Ref, watch } from "@nuxtjs/composition-api";
|
|||
import { format } from "date-fns";
|
||||
import { useAsyncKey } from "./use-utils";
|
||||
import { useUserApi } from "~/composables/api";
|
||||
import { CreatePlanEntry, PlanEntryType, UpdatePlanEntry } from "~/types/api-types/meal-plan";
|
||||
import { CreatePlanEntry, PlanEntryType, UpdatePlanEntry } from "~/lib/api/types/meal-plan";
|
||||
|
||||
export const planTypeOptions = [
|
||||
{ text: "Breakfast", value: "breakfast" },
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { useAsync, ref } from "@nuxtjs/composition-api";
|
||||
import { useAsyncKey } from "./use-utils";
|
||||
import { useUserApi } from "~/composables/api";
|
||||
import { ReadWebhook } from "~/types/api-types/group";
|
||||
import { ReadWebhook } from "~/lib/api/types/group";
|
||||
|
||||
export const useGroupWebhooks = function () {
|
||||
const api = useUserApi();
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { useAsync, ref } from "@nuxtjs/composition-api";
|
||||
import { useAsyncKey } from "./use-utils";
|
||||
import { useUserApi } from "~/composables/api";
|
||||
import { GroupBase } from "~/types/api-types/user";
|
||||
import { GroupBase } from "~/lib/api/types/user";
|
||||
|
||||
export const useGroupSelf = function () {
|
||||
const api = useUserApi();
|
||||
|
|
32
frontend/composables/use-passwords.test.ts
Normal file
32
frontend/composables/use-passwords.test.ts
Normal file
|
@ -0,0 +1,32 @@
|
|||
import { ref } from "@nuxtjs/composition-api";
|
||||
import { describe, expect, test } from "vitest";
|
||||
import { usePasswordStrength } from "./use-passwords";
|
||||
|
||||
// test("test usePasswordField", () => {
|
||||
// const { inputType, togglePasswordShow, passwordIcon } = usePasswordField();
|
||||
// expect(inputType.value).toBe("password");
|
||||
// expect(passwordIcon.value).toBe("mdi-eye");
|
||||
// togglePasswordShow();
|
||||
// expect(inputType.value).toBe("text");
|
||||
// expect(passwordIcon.value).toBe("mdi-eye-off");
|
||||
// });
|
||||
|
||||
describe("test usePasswordStrength", () => {
|
||||
test("weak password", () => {
|
||||
const password = ref("123456");
|
||||
const { score, strength, color } = usePasswordStrength(password);
|
||||
expect(score.value).toBeGreaterThan(0);
|
||||
expect(score.value).toBeLessThan(40);
|
||||
expect(strength.value).toBe("Weak");
|
||||
expect(color.value).toBe("error");
|
||||
});
|
||||
|
||||
test("very strong password", () => {
|
||||
const password = ref("My~Secret~Not~So~Secret?123");
|
||||
const { score, strength, color } = usePasswordStrength(password);
|
||||
expect(score.value).toBeGreaterThan(90);
|
||||
expect(score.value).toBe(100);
|
||||
expect(strength.value).toBe("Very Strong");
|
||||
expect(color.value).toBe("success");
|
||||
});
|
||||
});
|
|
@ -1,4 +1,5 @@
|
|||
import { computed, Ref, ref, useContext } from "@nuxtjs/composition-api";
|
||||
import { scorePassword } from "~/lib/validators";
|
||||
|
||||
export function usePasswordField() {
|
||||
const show = ref(false);
|
||||
|
@ -21,46 +22,6 @@ export function usePasswordField() {
|
|||
};
|
||||
}
|
||||
|
||||
function scorePassword(pass: string): number {
|
||||
let score = 0;
|
||||
if (!pass) return score;
|
||||
|
||||
const flaggedWords = ["password", "mealie", "admin", "qwerty", "login"];
|
||||
|
||||
if (pass.length < 6) return score;
|
||||
|
||||
// Check for flagged words
|
||||
for (const word of flaggedWords) {
|
||||
if (pass.toLowerCase().includes(word)) {
|
||||
score -= 100;
|
||||
}
|
||||
}
|
||||
|
||||
// award every unique letter until 5 repetitions
|
||||
const letters: { [key: string]: number } = {};
|
||||
|
||||
for (let i = 0; i < pass.length; i++) {
|
||||
letters[pass[i]] = (letters[pass[i]] || 0) + 1;
|
||||
score += 5.0 / letters[pass[i]];
|
||||
}
|
||||
|
||||
// bonus points for mixing it up
|
||||
const variations: { [key: string]: boolean } = {
|
||||
digits: /\d/.test(pass),
|
||||
lower: /[a-z]/.test(pass),
|
||||
upper: /[A-Z]/.test(pass),
|
||||
nonWords: /\W/.test(pass),
|
||||
};
|
||||
|
||||
let variationCount = 0;
|
||||
for (const check in variations) {
|
||||
variationCount += variations[check] === true ? 1 : 0;
|
||||
}
|
||||
score += (variationCount - 1) * 10;
|
||||
|
||||
return score;
|
||||
}
|
||||
|
||||
export const usePasswordStrength = (password: Ref<string>) => {
|
||||
const score = computed(() => {
|
||||
return scorePassword(password.value);
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { useAsync, ref } from "@nuxtjs/composition-api";
|
||||
import { useUserApi } from "~/composables/api";
|
||||
import { UserIn, UserOut } from "~/types/api-types/user";
|
||||
import { UserIn, UserOut } from "~/lib/api/types/user";
|
||||
|
||||
/*
|
||||
TODO: Potentially combine useAllUsers and useUser by delaying the get all users functionality
|
||||
|
|
|
@ -1,19 +1,15 @@
|
|||
import { ref, Ref } from "@nuxtjs/composition-api";
|
||||
import { RequestResponse } from "~/types/api";
|
||||
import { ValidationResponse } from "~/types/api-types/response";
|
||||
|
||||
const EMAIL_REGEX =
|
||||
/^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@(([[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
|
||||
|
||||
const URL_REGEX = /[-a-zA-Z0-9@:%._+~#=]{1,256}.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&//=]*)/;
|
||||
import { RequestResponse } from "~/lib/api/types/non-generated";
|
||||
import { ValidationResponse } from "~/lib/api/types/response";
|
||||
import { required, email, whitespace, url, minLength, maxLength } from "~/lib/validators";
|
||||
|
||||
export const validators = {
|
||||
required: (v: string) => !!v || "This Field is Required",
|
||||
email: (v: string) => !v || EMAIL_REGEX.test(v) || "Email Must Be Valid",
|
||||
whitespace: (v: string) => !v || v.split(" ").length <= 1 || "No Whitespace Allowed",
|
||||
url: (v: string) => !v || URL_REGEX.test(v) || "Must Be A Valid URL",
|
||||
minLength: (min: number) => (v: string) => !v || v.length >= min || `Must Be At Least ${min} Characters`,
|
||||
maxLength: (max: number) => (v: string) => !v || v.length <= max || `Must Be At Most ${max} Characters`,
|
||||
required,
|
||||
email,
|
||||
whitespace,
|
||||
url,
|
||||
minLength,
|
||||
maxLength,
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue