1
0
Fork 0
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:
Hayden 2022-10-22 11:51:07 -08:00 committed by GitHub
parent 9f6bcc83d5
commit fcc5d99d40
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
182 changed files with 902 additions and 487 deletions

View file

@ -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 {

View file

@ -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);

View file

@ -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;

View file

@ -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",

View file

@ -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) {

View file

@ -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 };
};

View file

@ -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({

View file

@ -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>({

View file

@ -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();

View file

@ -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,
};
};

View file

@ -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([]);

View file

@ -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;

View file

@ -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;

View file

@ -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([]);

View file

@ -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([]);

View file

@ -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;

View file

@ -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 {

View file

@ -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 = "",

View file

@ -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;

View file

@ -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" },

View file

@ -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();

View file

@ -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();

View 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");
});
});

View file

@ -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);

View file

@ -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

View file

@ -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,
};
/**