dev: Improve support for front end unit tests (#7163)

This commit is contained in:
miah
2026-05-31 10:41:52 -05:00
committed by GitHub
parent 7b0d1fde64
commit 364af97060
11 changed files with 719 additions and 8 deletions

View File

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

View File

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

View File

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

View File

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

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

View File

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