fix(spa): error handling on store actions + exact-active-class + matchmedia setup
This commit is contained in:
@@ -26,22 +26,9 @@ const apiMod = api.modules as unknown as Record<string, ReturnType<typeof vi.fn>
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia());
|
||||
Object.values(apiMod).forEach((fn) => fn.mockReset());
|
||||
// jsdom does not implement matchMedia; PrimeVue's TieredMenu (used by
|
||||
// SplitButton) binds a matchMedia listener on mount.
|
||||
vi.stubGlobal("matchMedia", vi.fn().mockReturnValue({
|
||||
matches: false,
|
||||
media: "",
|
||||
onchange: null,
|
||||
addListener: vi.fn(),
|
||||
removeListener: vi.fn(),
|
||||
addEventListener: vi.fn(),
|
||||
removeEventListener: vi.fn(),
|
||||
dispatchEvent: vi.fn(),
|
||||
}));
|
||||
});
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
function makeRouter() {
|
||||
|
||||
@@ -2,6 +2,7 @@ import { mount, flushPromises } from "@vue/test-utils";
|
||||
import { setActivePinia, createPinia } from "pinia";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import PrimeVue from "primevue/config";
|
||||
import ToastService from "primevue/toastservice";
|
||||
|
||||
vi.mock("../api/client", () => ({
|
||||
api: {
|
||||
@@ -17,25 +18,21 @@ import TestRunner from "../views/TestRunner.vue";
|
||||
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia());
|
||||
vi.stubGlobal("matchMedia", vi.fn().mockReturnValue({
|
||||
matches: false, addEventListener: vi.fn(), removeEventListener: vi.fn(),
|
||||
}));
|
||||
});
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
describe("TestRunner.vue", () => {
|
||||
it("renders heading and no histogram before run", async () => {
|
||||
const wrap = mount(TestRunner, { global: { plugins: [PrimeVue] } });
|
||||
const wrap = mount(TestRunner, { global: { plugins: [PrimeVue, ToastService] } });
|
||||
await flushPromises();
|
||||
expect(wrap.text()).toContain("Test runner");
|
||||
expect(wrap.text()).not.toContain("Histogram");
|
||||
});
|
||||
|
||||
it("Run button disabled when no module selected", async () => {
|
||||
const wrap = mount(TestRunner, { global: { plugins: [PrimeVue] } });
|
||||
const wrap = mount(TestRunner, { global: { plugins: [PrimeVue, ToastService] } });
|
||||
await flushPromises();
|
||||
const btn = wrap.findAll("button").find((b) => b.text().includes("Run"));
|
||||
// PrimeVue Button forwards :disabled to the button element
|
||||
|
||||
@@ -26,14 +26,9 @@ beforeEach(() => {
|
||||
setActivePinia(createPinia());
|
||||
Object.values(apiMod).forEach((fn) => fn.mockReset());
|
||||
apiCat.list.mockResolvedValue({ items: [] });
|
||||
// jsdom lacks matchMedia, used by some PrimeVue overlays
|
||||
vi.stubGlobal("matchMedia", vi.fn().mockReturnValue({
|
||||
matches: false, addEventListener: vi.fn(), removeEventListener: vi.fn(),
|
||||
}));
|
||||
});
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
function makeRouter() {
|
||||
|
||||
@@ -11,7 +11,7 @@ defineProps<{
|
||||
<RouterLink
|
||||
:to="item.to"
|
||||
class="flex items-center gap-2 px-4 py-2 text-wp-text hover:bg-wp-bg2 border-l-2 border-transparent"
|
||||
active-class="!bg-wp-accent/15 !border-l-[color:var(--wp-accent)] !text-white"
|
||||
exact-active-class="!bg-wp-accent/15 !border-l-[color:var(--wp-accent)] !text-white"
|
||||
>
|
||||
<i :class="['pi', item.icon, 'text-wp-text2']" aria-hidden="true" />
|
||||
<span class="text-sm">{{ item.label }}</span>
|
||||
|
||||
@@ -31,8 +31,13 @@ async function add() {
|
||||
}
|
||||
|
||||
async function remove(row: CategoryRow) {
|
||||
await store.remove(row.id);
|
||||
toast.add({ severity: "success", summary: "Deleted", life: 2000 });
|
||||
try {
|
||||
await store.remove(row.id);
|
||||
toast.add({ severity: "success", summary: "Deleted", life: 2000 });
|
||||
} catch (e) {
|
||||
const msg = e instanceof ApiError ? e.message : String(e);
|
||||
toast.add({ severity: "error", summary: "Delete failed", detail: msg, life: 4000 });
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -28,13 +28,18 @@ const isEdit = computed(() => !!props.id);
|
||||
onMounted(async () => {
|
||||
await categoryStore.fetchAll();
|
||||
if (props.id) {
|
||||
const row = await moduleStore.get(props.id);
|
||||
name.value = row.name;
|
||||
description.value = row.description;
|
||||
categoryId.value = row.category_id;
|
||||
tags.value = row.tags;
|
||||
const payload = row.payload as { values?: NamedValue[] };
|
||||
values.value = payload.values ?? [];
|
||||
try {
|
||||
const row = await moduleStore.get(props.id);
|
||||
name.value = row.name;
|
||||
description.value = row.description;
|
||||
categoryId.value = row.category_id;
|
||||
tags.value = row.tags;
|
||||
const payload = row.payload as { values?: NamedValue[] };
|
||||
values.value = payload.values ?? [];
|
||||
} catch {
|
||||
toast.add({ severity: "error", summary: "Module not found", life: 3000 });
|
||||
router.replace("/modules");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -27,16 +27,30 @@ const newOptions = [
|
||||
{ label: "Fixed values", icon: "pi pi-tag", command: () => router.push("/modules/fixed-values/new") },
|
||||
];
|
||||
|
||||
onMounted(async () => { await store.fetchAll(); });
|
||||
onMounted(async () => {
|
||||
try {
|
||||
await store.fetchAll();
|
||||
} catch (e) {
|
||||
toast.add({ severity: "error", summary: "Load failed", detail: String(e), life: 4000 });
|
||||
}
|
||||
});
|
||||
|
||||
watch(search, async (q) => {
|
||||
store.filter.q = q || undefined;
|
||||
await store.fetchAll();
|
||||
try {
|
||||
await store.fetchAll();
|
||||
} catch (e) {
|
||||
toast.add({ severity: "error", summary: "Search failed", detail: String(e), life: 4000 });
|
||||
}
|
||||
});
|
||||
|
||||
async function setTypeFilter(type: ModuleType | undefined) {
|
||||
store.filter.type = type;
|
||||
await store.fetchAll();
|
||||
try {
|
||||
await store.fetchAll();
|
||||
} catch (e) {
|
||||
toast.add({ severity: "error", summary: "Load failed", detail: String(e), life: 4000 });
|
||||
}
|
||||
}
|
||||
|
||||
function rowClass(row: ModuleRow) {
|
||||
@@ -49,12 +63,20 @@ function edit(row: ModuleRow) {
|
||||
}
|
||||
|
||||
async function dup(row: ModuleRow) {
|
||||
await store.duplicate(row.id);
|
||||
toast.add({ severity: "success", summary: "Duplicated", detail: row.name, life: 2000 });
|
||||
try {
|
||||
await store.duplicate(row.id);
|
||||
toast.add({ severity: "success", summary: "Duplicated", detail: row.name, life: 2000 });
|
||||
} catch (e) {
|
||||
toast.add({ severity: "error", summary: "Duplicate failed", detail: String(e), life: 4000 });
|
||||
}
|
||||
}
|
||||
|
||||
async function fav(row: ModuleRow) {
|
||||
await store.toggleFavorite(row.id);
|
||||
try {
|
||||
await store.toggleFavorite(row.id);
|
||||
} catch (e) {
|
||||
toast.add({ severity: "error", summary: "Favorite failed", detail: String(e), life: 4000 });
|
||||
}
|
||||
}
|
||||
|
||||
function del(row: ModuleRow) {
|
||||
@@ -64,8 +86,12 @@ function del(row: ModuleRow) {
|
||||
icon: "pi pi-exclamation-triangle",
|
||||
acceptClass: "p-button-danger",
|
||||
accept: async () => {
|
||||
await store.remove(row.id);
|
||||
toast.add({ severity: "success", summary: "Deleted", detail: row.name, life: 2000 });
|
||||
try {
|
||||
await store.remove(row.id);
|
||||
toast.add({ severity: "success", summary: "Deleted", detail: row.name, life: 2000 });
|
||||
} catch (e) {
|
||||
toast.add({ severity: "error", summary: "Delete failed", detail: String(e), life: 4000 });
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -4,11 +4,13 @@ import Button from "primevue/button";
|
||||
import InputNumber from "primevue/inputnumber";
|
||||
import InputText from "primevue/inputtext";
|
||||
import Select from "primevue/select";
|
||||
import { useToast } from "primevue/usetoast";
|
||||
import { api } from "../api/client";
|
||||
import { useModuleStore } from "../stores/moduleStore";
|
||||
import type { TestResponse } from "../api/types";
|
||||
|
||||
const store = useModuleStore();
|
||||
const toast = useToast();
|
||||
const moduleId = ref<string | null>(null);
|
||||
const samples = ref(50);
|
||||
const variableBinding = ref("$x");
|
||||
@@ -39,6 +41,8 @@ async function run() {
|
||||
instance,
|
||||
samples: samples.value,
|
||||
});
|
||||
} catch (e) {
|
||||
toast.add({ severity: "error", summary: "Test failed", detail: String(e), life: 4000 });
|
||||
} finally {
|
||||
running.value = false;
|
||||
}
|
||||
|
||||
@@ -29,13 +29,18 @@ const isEdit = computed(() => !!props.id);
|
||||
onMounted(async () => {
|
||||
await categoryStore.fetchAll();
|
||||
if (props.id) {
|
||||
const row = await moduleStore.get(props.id);
|
||||
name.value = row.name;
|
||||
description.value = row.description;
|
||||
categoryId.value = row.category_id;
|
||||
tags.value = row.tags;
|
||||
const payload = row.payload as { options?: Option[] };
|
||||
options.value = payload.options ?? [];
|
||||
try {
|
||||
const row = await moduleStore.get(props.id);
|
||||
name.value = row.name;
|
||||
description.value = row.description;
|
||||
categoryId.value = row.category_id;
|
||||
tags.value = row.tags;
|
||||
const payload = row.payload as { options?: Option[] };
|
||||
options.value = payload.options ?? [];
|
||||
} catch {
|
||||
toast.add({ severity: "error", summary: "Module not found", life: 3000 });
|
||||
router.replace("/modules");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -17,3 +17,18 @@ if (typeof globalThis.ResizeObserver === "undefined") {
|
||||
disconnect() {}
|
||||
} as never;
|
||||
}
|
||||
|
||||
// PrimeVue overlays (TieredMenu, Select dropdown, etc.) call window.matchMedia.
|
||||
// jsdom doesn't ship with it, so we stub it for every test.
|
||||
if (typeof window !== "undefined" && !window.matchMedia) {
|
||||
window.matchMedia = vi.fn().mockImplementation((query: string) => ({
|
||||
matches: false,
|
||||
media: query,
|
||||
onchange: null,
|
||||
addListener: vi.fn(),
|
||||
removeListener: vi.fn(),
|
||||
addEventListener: vi.fn(),
|
||||
removeEventListener: vi.fn(),
|
||||
dispatchEvent: vi.fn(),
|
||||
})) as unknown as typeof window.matchMedia;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user