mirror of
https://github.com/mealie-recipes/mealie.git
synced 2026-06-01 22:50:26 -04:00
dev: Improve support for front end unit tests (#7163)
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
import type { IngredientFood, RecipeSummary, ShoppingListItemOut, ShoppingListMultiPurposeLabelOut, ShoppingListOut } from "~/lib/api/types/household";
|
||||
|
||||
export const MOCK_ITEM: ShoppingListItemOut = {
|
||||
shoppingListId: "",
|
||||
id: "",
|
||||
groupId: "",
|
||||
householdId: "",
|
||||
display: "MOCK_ITEM",
|
||||
updatedAt: "100",
|
||||
position: 1,
|
||||
checked: false,
|
||||
createdAt: "100",
|
||||
};
|
||||
|
||||
export const MOCK_RECIPE: RecipeSummary = {
|
||||
id: "recipe-id",
|
||||
name: "Recipe!",
|
||||
};
|
||||
|
||||
export const MOCK_RECIPE2: RecipeSummary = {
|
||||
...MOCK_RECIPE,
|
||||
id: undefined,
|
||||
name: "Recipe 2!",
|
||||
};
|
||||
|
||||
export const MOCK_FOOD: IngredientFood = {
|
||||
id: "1",
|
||||
name: "food 1",
|
||||
};
|
||||
|
||||
export const MOCK_FOOD2: IngredientFood = {
|
||||
id: "2",
|
||||
name: "food 2",
|
||||
};
|
||||
|
||||
export const MOCK_LABEL: ShoppingListMultiPurposeLabelOut = {
|
||||
shoppingListId: "",
|
||||
labelId: "",
|
||||
id: "",
|
||||
label: {
|
||||
name: "MOCK_LABEL",
|
||||
groupId: "",
|
||||
id: "",
|
||||
},
|
||||
};
|
||||
|
||||
export const MOCK_LABEL2: ShoppingListMultiPurposeLabelOut = {
|
||||
shoppingListId: "",
|
||||
labelId: "",
|
||||
id: "",
|
||||
label: {
|
||||
name: "MOCK_LABEL2",
|
||||
groupId: "",
|
||||
id: "",
|
||||
},
|
||||
};
|
||||
|
||||
export const MOCK_SHOPPING_LIST: ShoppingListOut = {
|
||||
groupId: "",
|
||||
userId: "",
|
||||
id: "",
|
||||
householdId: "",
|
||||
labelSettings: [
|
||||
MOCK_LABEL,
|
||||
MOCK_LABEL2,
|
||||
],
|
||||
listItems: [
|
||||
MOCK_ITEM,
|
||||
],
|
||||
recipeReferences: [{
|
||||
id: "",
|
||||
shoppingListId: "",
|
||||
recipeId: "",
|
||||
recipeQuantity: 0,
|
||||
recipe: MOCK_RECIPE,
|
||||
}, {
|
||||
id: "",
|
||||
shoppingListId: "",
|
||||
recipeId: "",
|
||||
recipeQuantity: 0,
|
||||
recipe: MOCK_RECIPE2,
|
||||
}],
|
||||
};
|
||||
@@ -0,0 +1,89 @@
|
||||
import * as vueusecore from "@vueuse/core";
|
||||
import { describe, expect, test, vi } from "vitest";
|
||||
import type { ShoppingListItemOut } from "~/lib/api/types/household";
|
||||
import { makeWrapper } from "~/tests/utils";
|
||||
import { useShoppingListCopy } from "../use-shopping-list-copy";
|
||||
import { MOCK_ITEM } from "./mocks";
|
||||
|
||||
vi.mock("@vueuse/core", { spy: true });
|
||||
|
||||
const mockCopy = vi.fn().mockImplementation(args => new Promise(resolve => resolve(args)));
|
||||
|
||||
vi.mocked(vueusecore.useClipboard).mockImplementation(() => {
|
||||
return {
|
||||
isSupported: computed(() => true),
|
||||
copied: computed(() => true),
|
||||
text: computed(() => ""),
|
||||
copy: mockCopy,
|
||||
};
|
||||
});
|
||||
const wrapper = () => makeWrapper(useShoppingListCopy);
|
||||
|
||||
const TEST_HEADER = "SPECIAL HEADER!";
|
||||
|
||||
const MOCK_LIST: { [key: string]: ShoppingListItemOut[] } = {
|
||||
[TEST_HEADER]: [MOCK_ITEM],
|
||||
[TEST_HEADER + "2"]: [MOCK_ITEM],
|
||||
};
|
||||
|
||||
describe("Shopping list copy composable", () => {
|
||||
describe("copyListItems", () => {
|
||||
test("copies markdown lists correctly", () => {
|
||||
const { copyListItems } = wrapper();
|
||||
copyListItems(MOCK_LIST, "markdown");
|
||||
const expected = [
|
||||
"# SPECIAL HEADER!",
|
||||
"- [ ] MOCK_ITEM",
|
||||
"",
|
||||
"# SPECIAL HEADER!2",
|
||||
"- [ ] MOCK_ITEM",
|
||||
].join("\n");
|
||||
|
||||
expect(mockCopy).toBeCalledWith(expected);
|
||||
});
|
||||
test("copies plain text lists correctly", () => {
|
||||
const { copyListItems } = wrapper();
|
||||
copyListItems(MOCK_LIST, "plain");
|
||||
const expected = [
|
||||
"[SPECIAL HEADER!]",
|
||||
"MOCK_ITEM",
|
||||
"",
|
||||
"[SPECIAL HEADER!2]",
|
||||
"MOCK_ITEM",
|
||||
].join("\n");
|
||||
|
||||
expect(mockCopy).toBeCalledWith(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe("formatCopiedLabelHeading", () => {
|
||||
test("copies markdown headers correctly", () => {
|
||||
const { formatCopiedLabelHeading } = wrapper();
|
||||
const header = formatCopiedLabelHeading("markdown", TEST_HEADER);
|
||||
expect(header).toEqual(`# ${TEST_HEADER}`);
|
||||
});
|
||||
test("copies plain text headers correctly", () => {
|
||||
const { formatCopiedLabelHeading } = wrapper();
|
||||
const header = formatCopiedLabelHeading("plain", TEST_HEADER);
|
||||
expect(header).toEqual(`[${TEST_HEADER}]`);
|
||||
});
|
||||
});
|
||||
|
||||
describe("formatCopiedListItem", () => {
|
||||
test("copies markdown items correctly", () => {
|
||||
const { formatCopiedListItem } = wrapper();
|
||||
const header = formatCopiedListItem("markdown", MOCK_ITEM);
|
||||
expect(header).toEqual(`- [ ] ${MOCK_ITEM.display}`);
|
||||
});
|
||||
test("copies plain text items correctly", () => {
|
||||
const { formatCopiedListItem } = wrapper();
|
||||
const header = formatCopiedListItem("plain", MOCK_ITEM);
|
||||
expect(header).toEqual(MOCK_ITEM.display);
|
||||
});
|
||||
test("copies items without a display as empty", () => {
|
||||
const { formatCopiedListItem } = wrapper();
|
||||
const header = formatCopiedListItem("plain", { ...MOCK_ITEM, display: undefined });
|
||||
expect(header).toEqual("");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,175 @@
|
||||
import { describe, expect, test } from "vitest";
|
||||
import type { ShoppingListOut } from "~/lib/api/types/household";
|
||||
import { makeWrapper } from "~/tests/utils";
|
||||
import { useShoppingListSorting } from "../use-shopping-list-sorting";
|
||||
import { MOCK_FOOD, MOCK_FOOD2, MOCK_ITEM, MOCK_LABEL, MOCK_LABEL2, MOCK_SHOPPING_LIST } from "./mocks";
|
||||
|
||||
const wrapper = () => makeWrapper(() => {
|
||||
const { t } = useI18n();
|
||||
return {
|
||||
t,
|
||||
...useShoppingListSorting(),
|
||||
};
|
||||
});
|
||||
|
||||
describe("use-shopping-list-sorting", () => {
|
||||
describe("sortItems", () => {
|
||||
const { sortItems } = wrapper();
|
||||
test("sorts by position first", () => {
|
||||
const result = sortItems(MOCK_ITEM, { ...MOCK_ITEM, position: 0 });
|
||||
const result2 = sortItems({ ...MOCK_ITEM, position: 0 }, MOCK_ITEM);
|
||||
expect(result).toBe(1);
|
||||
expect(result2).toBe(-1);
|
||||
});
|
||||
test("sorts by createdAt next", () => {
|
||||
const result = sortItems(MOCK_ITEM, { ...MOCK_ITEM, createdAt: "0" });
|
||||
const result2 = sortItems({ ...MOCK_ITEM, createdAt: "0" }, MOCK_ITEM);
|
||||
expect(result).toBe(1);
|
||||
expect(result2).toBe(-1);
|
||||
});
|
||||
test("sorts similar items into the same spot", () => {
|
||||
const result = sortItems(MOCK_ITEM, MOCK_ITEM);
|
||||
expect(result).toBe(0);
|
||||
});
|
||||
test("handles nulls", () => {
|
||||
const result = sortItems(MOCK_ITEM, { ...MOCK_ITEM, position: undefined });
|
||||
const result2 = sortItems({ ...MOCK_ITEM, position: undefined }, MOCK_ITEM);
|
||||
expect(result).toBe(1);
|
||||
expect(result2).toBe(-1);
|
||||
});
|
||||
test("handles nulls", () => {
|
||||
const result = sortItems(MOCK_ITEM, { ...MOCK_ITEM, createdAt: undefined });
|
||||
const result2 = sortItems({ ...MOCK_ITEM, createdAt: undefined }, MOCK_ITEM);
|
||||
expect(result).toBe(1);
|
||||
expect(result2).toBe(-1);
|
||||
});
|
||||
});
|
||||
describe("sortListItems", () => {
|
||||
const { sortListItems } = wrapper();
|
||||
test("sorts by position first", () => {
|
||||
const sortedList = { ...MOCK_SHOPPING_LIST, listItems: [MOCK_ITEM, { ...MOCK_ITEM, position: 0 }, { ...MOCK_ITEM, createdAt: "0" }] };
|
||||
sortListItems(sortedList);
|
||||
expect(sortedList.listItems).toEqual([
|
||||
{ ...MOCK_ITEM, position: 0 },
|
||||
{ ...MOCK_ITEM, createdAt: "0" },
|
||||
MOCK_ITEM,
|
||||
]);
|
||||
});
|
||||
test("handles nulls", () => {
|
||||
const sortedList = { ...MOCK_SHOPPING_LIST, listItems: undefined };
|
||||
sortListItems(sortedList);
|
||||
expect(sortedList.listItems).toEqual(undefined);
|
||||
});
|
||||
});
|
||||
describe("updateItemsByLabel", () => {
|
||||
const { updateItemsByLabel, t } = wrapper();
|
||||
test("sorts by group", () => {
|
||||
const sortedList = {
|
||||
...MOCK_SHOPPING_LIST, listItems: [
|
||||
MOCK_ITEM,
|
||||
{ ...MOCK_ITEM, label: MOCK_LABEL2.label, labelId: "2" },
|
||||
{ ...MOCK_ITEM, label: MOCK_LABEL.label, labelId: "1" },
|
||||
{ ...MOCK_ITEM, label: MOCK_LABEL.label, labelId: "1" },
|
||||
],
|
||||
};
|
||||
const result = updateItemsByLabel(sortedList);
|
||||
expect(result).toEqual({
|
||||
[t("shopping-list.no-label")]: [
|
||||
MOCK_ITEM,
|
||||
],
|
||||
[MOCK_LABEL.label.name]: [
|
||||
{ ...MOCK_ITEM, label: MOCK_LABEL.label, labelId: "1" },
|
||||
{ ...MOCK_ITEM, label: MOCK_LABEL.label, labelId: "1" },
|
||||
],
|
||||
[MOCK_LABEL2.label.name]: [
|
||||
{ ...MOCK_ITEM, label: MOCK_LABEL2.label, labelId: "2" },
|
||||
],
|
||||
});
|
||||
});
|
||||
test("ignores checked items", () => {
|
||||
const sortedList = {
|
||||
...MOCK_SHOPPING_LIST, listItems: [
|
||||
MOCK_ITEM,
|
||||
{ ...MOCK_ITEM, label: MOCK_LABEL2.label, labelId: "2" },
|
||||
{ ...MOCK_ITEM, label: MOCK_LABEL.label, labelId: "1" },
|
||||
{ ...MOCK_ITEM, label: MOCK_LABEL.label, labelId: "1", checked: true },
|
||||
],
|
||||
};
|
||||
const result = updateItemsByLabel(sortedList);
|
||||
expect(result).toEqual({
|
||||
[t("shopping-list.no-label")]: [
|
||||
MOCK_ITEM,
|
||||
],
|
||||
[MOCK_LABEL.label.name]: [
|
||||
{ ...MOCK_ITEM, label: MOCK_LABEL.label, labelId: "1" },
|
||||
],
|
||||
[MOCK_LABEL2.label.name]: [
|
||||
{ ...MOCK_ITEM, label: MOCK_LABEL2.label, labelId: "2" },
|
||||
],
|
||||
});
|
||||
});
|
||||
test("returns unordered labels if no ordering is specified", () => {
|
||||
const sortedList = {
|
||||
...MOCK_SHOPPING_LIST,
|
||||
labelSettings: undefined,
|
||||
listItems: [
|
||||
MOCK_ITEM,
|
||||
{ ...MOCK_ITEM, label: MOCK_LABEL2.label, labelId: "2" },
|
||||
{ ...MOCK_ITEM, label: MOCK_LABEL.label, labelId: "1" },
|
||||
{ ...MOCK_ITEM, label: MOCK_LABEL.label, labelId: "1", checked: true },
|
||||
],
|
||||
};
|
||||
const result = updateItemsByLabel(sortedList);
|
||||
expect(result).toEqual({
|
||||
[t("shopping-list.no-label")]: [
|
||||
MOCK_ITEM,
|
||||
],
|
||||
[MOCK_LABEL2.label.name]: [
|
||||
{ ...MOCK_ITEM, label: MOCK_LABEL2.label, labelId: "2" },
|
||||
],
|
||||
[MOCK_LABEL.label.name]: [
|
||||
{ ...MOCK_ITEM, label: MOCK_LABEL.label, labelId: "1" },
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
describe("groupAndSortListItemsByFood", () => {
|
||||
const { groupAndSortListItemsByFood } = wrapper();
|
||||
test("sorts by group", () => {
|
||||
const sortedList = { ...MOCK_SHOPPING_LIST };
|
||||
groupAndSortListItemsByFood(sortedList);
|
||||
expect(sortedList.listItems).toEqual(MOCK_SHOPPING_LIST.listItems);
|
||||
});
|
||||
test("groups checked items together", () => {
|
||||
const sortedList: ShoppingListOut = {
|
||||
...MOCK_SHOPPING_LIST, listItems: [
|
||||
{ ...MOCK_ITEM, checked: true, food: MOCK_FOOD },
|
||||
{ ...MOCK_ITEM, checked: true, food: MOCK_FOOD2 },
|
||||
],
|
||||
};
|
||||
groupAndSortListItemsByFood(sortedList);
|
||||
expect(sortedList.listItems).toEqual([
|
||||
{ ...MOCK_ITEM, checked: true, food: MOCK_FOOD },
|
||||
{ ...MOCK_ITEM, checked: true, food: MOCK_FOOD2, position: 1 },
|
||||
]);
|
||||
});
|
||||
test("populates position and created at if not present", () => {
|
||||
const sortedList: ShoppingListOut = {
|
||||
...MOCK_SHOPPING_LIST, listItems: [
|
||||
{ ...MOCK_ITEM, food: MOCK_FOOD, position: undefined },
|
||||
{ ...MOCK_ITEM, food: MOCK_FOOD2, createdAt: undefined },
|
||||
],
|
||||
};
|
||||
groupAndSortListItemsByFood(sortedList);
|
||||
expect(sortedList.listItems).toEqual([
|
||||
{ ...MOCK_ITEM, food: MOCK_FOOD2, createdAt: undefined },
|
||||
{ ...MOCK_ITEM, food: MOCK_FOOD, position: 1 },
|
||||
]);
|
||||
});
|
||||
test("handles nulls", () => {
|
||||
const sortedList: ShoppingListOut = { ...MOCK_SHOPPING_LIST, listItems: undefined };
|
||||
groupAndSortListItemsByFood(sortedList);
|
||||
expect(sortedList.listItems).toEqual(undefined);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,63 @@
|
||||
import { describe, expect, test } from "vitest";
|
||||
import type { ShoppingListOut } from "~/lib/api/types/household";
|
||||
import { makeWrapper } from "~/tests/utils";
|
||||
import { useShoppingListState } from "../use-shopping-list-state";
|
||||
import { MOCK_ITEM, MOCK_RECIPE, MOCK_RECIPE2, MOCK_SHOPPING_LIST } from "./mocks";
|
||||
|
||||
const wrapper = (list: ShoppingListOut = MOCK_SHOPPING_LIST) => makeWrapper(() => {
|
||||
const { shoppingList, ...state } = useShoppingListState();
|
||||
shoppingList.value = list;
|
||||
return {
|
||||
shoppingList,
|
||||
...state,
|
||||
};
|
||||
});
|
||||
|
||||
describe("use-shopping-list-state", () => {
|
||||
describe("checked items are sorted", () => {
|
||||
const { sortCheckedItems } = wrapper();
|
||||
|
||||
test("by timestamp", () => {
|
||||
const sorted = sortCheckedItems(MOCK_ITEM, { ...MOCK_ITEM, updatedAt: "200" });
|
||||
const sorted2 = sortCheckedItems(MOCK_ITEM, { ...MOCK_ITEM, updatedAt: "0" });
|
||||
expect(sorted).toBe(1);
|
||||
expect(sorted2).toBe(-1);
|
||||
});
|
||||
test("by position if timestamps match", () => {
|
||||
const sorted = sortCheckedItems(MOCK_ITEM, { ...MOCK_ITEM, position: 2 });
|
||||
const sorted2 = sortCheckedItems(MOCK_ITEM, { ...MOCK_ITEM, position: 0 });
|
||||
const sorted3 = sortCheckedItems({ ...MOCK_ITEM, position: undefined }, { ...MOCK_ITEM, position: undefined });
|
||||
expect(sorted).toBe(1);
|
||||
expect(sorted2).toBe(-1);
|
||||
expect(sorted3).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("recipeMap", () => {
|
||||
test("Updates to match shopping list recipe references", () => {
|
||||
const { recipeMap } = wrapper();
|
||||
expect(recipeMap).toEqual(new Map([
|
||||
[MOCK_RECIPE.id, MOCK_RECIPE],
|
||||
["", MOCK_RECIPE2],
|
||||
]));
|
||||
});
|
||||
test("handles nulls", () => {
|
||||
const { recipeMap } = wrapper({ ...MOCK_SHOPPING_LIST, recipeReferences: undefined });
|
||||
expect(recipeMap).toEqual(new Map([]));
|
||||
});
|
||||
});
|
||||
|
||||
describe("checked and unchecked items", () => {
|
||||
test("update appropriately", () => {
|
||||
const mockCheckedItem = { ...MOCK_ITEM, checked: true };
|
||||
const { listItems: { checked, unchecked } } = wrapper({
|
||||
...MOCK_SHOPPING_LIST, listItems: [
|
||||
MOCK_ITEM,
|
||||
mockCheckedItem,
|
||||
],
|
||||
});
|
||||
expect(unchecked[0]).toEqual(MOCK_ITEM);
|
||||
expect(checked[0]).toEqual(mockCheckedItem);
|
||||
});
|
||||
});
|
||||
});
|
||||
18
frontend/app/tests/setup.ts
Normal file
18
frontend/app/tests/setup.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { config } from "@vue/test-utils";
|
||||
import { createI18n } from "vue-i18n";
|
||||
|
||||
function loadEnLocales() {
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
return require("../lang/messages/en-US.json") as Record<string, string>;
|
||||
}
|
||||
|
||||
const i18n = createI18n({
|
||||
locale: "en-US",
|
||||
messages: {
|
||||
"en-US": loadEnLocales(),
|
||||
},
|
||||
});
|
||||
|
||||
config.global.plugins = [...(config.global.plugins ?? []), i18n];
|
||||
|
||||
export { i18n };
|
||||
@@ -1,3 +1,4 @@
|
||||
import { mount } from "@vue/test-utils";
|
||||
import { createI18n } from "vue-i18n";
|
||||
|
||||
function loadEnLocales() {
|
||||
@@ -14,3 +15,12 @@ export function stubI18n() {
|
||||
});
|
||||
return i18n.global;
|
||||
}
|
||||
|
||||
export const makeWrapper = <T>(setup: () => T) => {
|
||||
const Wrapper = {
|
||||
template: "<div />",
|
||||
setup,
|
||||
};
|
||||
const { vm } = mount(Wrapper);
|
||||
return vm as unknown as ReturnType<typeof Wrapper.setup>;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user