From c99486c63970a3e6a06d593a82e68e9f0e23e54c Mon Sep 17 00:00:00 2001 From: Kirthi Date: Tue, 11 Nov 2025 10:01:26 -0500 Subject: [PATCH 001/243] added some unit tests for prop logic --- .../form/text/FormTextInput.spec.ts | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/frontend/test/components/form/text/FormTextInput.spec.ts b/frontend/test/components/form/text/FormTextInput.spec.ts index ffbc4b9f7..42f8c12aa 100644 --- a/frontend/test/components/form/text/FormTextInput.spec.ts +++ b/frontend/test/components/form/text/FormTextInput.spec.ts @@ -114,4 +114,25 @@ describe("FormTextInput", () => { expectNormalLegend(legend); }); }); + + // Prop Usage Tests + it("renders even if required props are missing (graceful fallback)", async () => { + // intentionally omit required props to simulate misuse + const { container } = await render(FormTextInput, { props: {} }); + + // Should still mount and render an safely + const input = container.querySelector("input"); + expect(input).toBeTruthy(); + }); + + it("handles invalid iconLocation prop by defaulting to 'right'", async () => { + // imulate bad prop value + await render(FormTextInput, { + props: { id: "test", label: "Bad Icon", iconLocation: "middle" }, + }); + + // Should render as if 'right' was used (default location) + const leftIcon = screen.queryByTestId("icon-left"); + expect(leftIcon).toBeNull(); + }); }); From 992746e06ed33a29f263ce6509d9b66a364e8774 Mon Sep 17 00:00:00 2001 From: Kirthi Date: Tue, 11 Nov 2025 16:30:15 -0500 Subject: [PATCH 002/243] logic, edge cases, props --- .../form/text/FormTextInput.spec.ts | 97 ++++++++++++++++--- 1 file changed, 84 insertions(+), 13 deletions(-) diff --git a/frontend/test/components/form/text/FormTextInput.spec.ts b/frontend/test/components/form/text/FormTextInput.spec.ts index 42f8c12aa..c3bc3b04b 100644 --- a/frontend/test/components/form/text/FormTextInput.spec.ts +++ b/frontend/test/components/form/text/FormTextInput.spec.ts @@ -115,24 +115,95 @@ describe("FormTextInput", () => { }); }); - // Prop Usage Tests - it("renders even if required props are missing (graceful fallback)", async () => { - // intentionally omit required props to simulate misuse - const { container } = await render(FormTextInput, { props: {} }); + // Kirthiiii + // MARK: Rendering Tests + const defaultProps = { id: "test-input", label: "Test Label" }; - // Should still mount and render an safely - const input = container.querySelector("input"); + it("renders input and label correctly", async () => { + await render(TestWrapper, { props: defaultProps }); + + const input = screen.getByRole("textbox"); expect(input).toBeTruthy(); + expect(input.getAttribute("id")).toBe("test-input"); + + const label = screen.getByText("Test Label", { selector: "label" }); + expect(label).toBeTruthy(); }); - it("handles invalid iconLocation prop by defaulting to 'right'", async () => { - // imulate bad prop value - await render(FormTextInput, { - props: { id: "test", label: "Bad Icon", iconLocation: "middle" }, + // MARK: Logic + it("emits update:modelValue on user input", async () => { + const { emitted } = await render(FormTextInput, { + props: { id: "logic-id", label: "Logic Test" }, }); - // Should render as if 'right' was used (default location) - const leftIcon = screen.queryByTestId("icon-left"); - expect(leftIcon).toBeNull(); + const input = screen.getByRole("textbox"); + await fireEvent.update(input, "hello world"); + + expect(emitted("update:modelValue")).toBeTruthy(); + expect(emitted("update:modelValue")[0]).toEqual(["hello world"]); + }); + + // MARK: Edge Cases Tests + it("handles empty string id and label gracefully", async () => { + await render(TestWrapper, { props: { id: "", label: "" } }); + + const input = screen.getByRole("textbox"); + expect(input).toBeTruthy(); + }); + + it("handles special characters", async () => { + const special = "!@#$%^&*()_+{}[]|;:,.<>?"; + await render(TestWrapper, { + props: { id: "special-id", label: "Special Input" }, + }); + + const input = screen.getByRole("textbox") as HTMLInputElement; + await fireEvent.update(input, special); + expect(input.value).toBe(special); }); }); + +// Prop Usage Tests -- Kirthi added +// it("renders even if required props are missing", async () => { +// // intentionally having empty props to simulate error +// const { container } = await render(FormTextInput, { props: {} }); + +// // Should still render an input safely +// const input = container.querySelector("input"); +// expect(input).toBeTruthy(); +// }); + +// // it("handles invalid iconLocation by not rendering", async () => { +// // // intentially providing bad prop value (location can only be left or right) +// // await render(FormTextInput, { +// // props: { id: "test", label: "Bad Icon", iconLocation: "middle" }, +// // slots: {icons: "Icon",}, +// // }); + +// // // Should render as if 'right' was used (default location) +// // const leftIcon = screen.queryByTestId("left"); +// // const rightIcon = screen.queryByTestId("icon-right"); +// // expect(leftIcon).toBeNull(); +// // expect(rightIcon).toBeNull(); +// // }); + +// it("iconLocation should default to 'right' when not provided", async () => { +// const { container } = await render(FormTextInput, { +// props: { id: "test", label: "Default Icon" }, +// slots: { icons: "Icon" }, +// }); + +// const iconSpans = container.querySelectorAll("span.flex.items-center"); +// expect(iconSpans.length).toBe(1); +// expect(iconSpans[0].textContent).toContain("Icon"); +// }); + +// it("does not render icons when iconLocation prop is invalid", async () => { +// await render(FormTextInput, { +// props: { id: "test", label: "Bad Icon", iconLocation: "middle" }, // middle is the invalid value +// slots: { icons: "Icon" }, +// }); +// // (neither left nor right condition matches) +// const icon = screen.queryByText("Icon"); +// expect(icon).toBeNull(); +// }); From 717e2ab08435e58f663ce2cd3825707958bda65b Mon Sep 17 00:00:00 2001 From: Julia Date: Tue, 11 Nov 2025 16:36:41 -0500 Subject: [PATCH 003/243] delete extra file --- .../form/text/FormTextInput.spec.ts | 1 + .../form/text/FormTextInput.spect.ts | 92 +++++++++++++++++++ 2 files changed, 93 insertions(+) create mode 100644 frontend/test/components/form/text/FormTextInput.spect.ts diff --git a/frontend/test/components/form/text/FormTextInput.spec.ts b/frontend/test/components/form/text/FormTextInput.spec.ts index c3bc3b04b..17631b099 100644 --- a/frontend/test/components/form/text/FormTextInput.spec.ts +++ b/frontend/test/components/form/text/FormTextInput.spec.ts @@ -75,6 +75,7 @@ describe("FormTextInput", () => { await waitFor(() => { label = screen.getByText("test focus", { selector: "label" }); expectShrunkLabel(label); + expectNormalLabel(label); legend = screen.getByTestId("hidden-legend"); expectShrunkLegend(legend); diff --git a/frontend/test/components/form/text/FormTextInput.spect.ts b/frontend/test/components/form/text/FormTextInput.spect.ts new file mode 100644 index 000000000..157de9f7d --- /dev/null +++ b/frontend/test/components/form/text/FormTextInput.spect.ts @@ -0,0 +1,92 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +import { fireEvent, screen, waitFor } from "@testing-library/vue"; +import { describe, expect, it } from "vitest"; + +import FormTextInput from "../../../../app/components/form/text/FormTextInput.vue"; +import render from "../../../../test/render"; + +function expectNormalLabel(label: HTMLElement) { + expect(label.className, "Label should be normal size").toMatch( + "translate-y-[1.125rem] pl-[12px]" + ); +} + +function expectShrunkLabel(label: HTMLElement) { + expect(label.className, "Label should be shrunk").toMatch( + "translate-x-4 text-sm text-distinct-text" + ); +} + +function expectNormalLegend(legend: HTMLElement) { + expect(legend.classList, "Hidden legend should be normal size").not.toContain( + "max-w-[0.01px]" + ); +} + +function expectShrunkLegend(legend: HTMLElement) { + expect(legend.className, "Hidden legend should be shrunk").toMatch( + "max-w-[0.01px]" + ); +} + +describe("FormTextInput", () => { + it("shrinks label when focused", async () => { + await render(FormTextInput, { + props: { + id: "test", + label: "test focus", + }, + }); + + let label = screen.getByText("test focus"); + expectNormalLabel(label); + + let legend = screen.getByTestId("hidden-legend"); + expectNormalLegend(legend); + + const input = screen.getByLabelText("test focus"); + await fireEvent.focus(input); + + await waitFor(async () => { + label = screen.getByText("test focus"); + expectShrunkLabel(label); + + legend = screen.getByTestId("hidden-legend"); + expectShrunkLegend(legend); + }); + }); + + it("expands label when empty and blurred", async () => { + await render(FormTextInput, { + props: { + id: "test", + label: "test blur", + }, + }); + + const input = screen.getByLabelText("test blur"); + await fireEvent.focus(input); + await fireEvent.input(input, "text"); + await fireEvent.blur(input); + + await waitFor(async () => { + const label = screen.getByText("test blur"); + expectShrunkLabel(label); + + const legend = screen.getByTestId("hidden-legend"); + expectShrunkLegend(legend); + }); + + await fireEvent.focus(input); + await fireEvent.input(input, ""); + await fireEvent.blur(input); + + await waitFor(async () => { + const label = screen.getByText("test blur"); + expectNormalLabel(label); + + const legend = screen.getByTestId("hidden-legend"); + expectShrunkLegend(legend); + }); + }); +}); From dacf162bcfdd744e5ee46c67bcd6d9857d88a2ae Mon Sep 17 00:00:00 2001 From: Julia Date: Tue, 11 Nov 2025 16:41:43 -0500 Subject: [PATCH 004/243] I need to complete the commented-out code. Tests currently pass. --- .../form/text/FormTextInput.spec.ts | 108 ++++++++++-------- .../form/text/FormTextInput.spect.ts | 92 --------------- 2 files changed, 62 insertions(+), 138 deletions(-) delete mode 100644 frontend/test/components/form/text/FormTextInput.spect.ts diff --git a/frontend/test/components/form/text/FormTextInput.spec.ts b/frontend/test/components/form/text/FormTextInput.spec.ts index 17631b099..7379562ea 100644 --- a/frontend/test/components/form/text/FormTextInput.spec.ts +++ b/frontend/test/components/form/text/FormTextInput.spec.ts @@ -75,7 +75,6 @@ describe("FormTextInput", () => { await waitFor(() => { label = screen.getByText("test focus", { selector: "label" }); expectShrunkLabel(label); - expectNormalLabel(label); legend = screen.getByTestId("hidden-legend"); expectShrunkLegend(legend); @@ -162,49 +161,66 @@ describe("FormTextInput", () => { await fireEvent.update(input, special); expect(input.value).toBe(special); }); -}); -// Prop Usage Tests -- Kirthi added -// it("renders even if required props are missing", async () => { -// // intentionally having empty props to simulate error -// const { container } = await render(FormTextInput, { props: {} }); - -// // Should still render an input safely -// const input = container.querySelector("input"); -// expect(input).toBeTruthy(); -// }); - -// // it("handles invalid iconLocation by not rendering", async () => { -// // // intentially providing bad prop value (location can only be left or right) -// // await render(FormTextInput, { -// // props: { id: "test", label: "Bad Icon", iconLocation: "middle" }, -// // slots: {icons: "Icon",}, -// // }); - -// // // Should render as if 'right' was used (default location) -// // const leftIcon = screen.queryByTestId("left"); -// // const rightIcon = screen.queryByTestId("icon-right"); -// // expect(leftIcon).toBeNull(); -// // expect(rightIcon).toBeNull(); -// // }); - -// it("iconLocation should default to 'right' when not provided", async () => { -// const { container } = await render(FormTextInput, { -// props: { id: "test", label: "Default Icon" }, -// slots: { icons: "Icon" }, -// }); - -// const iconSpans = container.querySelectorAll("span.flex.items-center"); -// expect(iconSpans.length).toBe(1); -// expect(iconSpans[0].textContent).toContain("Icon"); -// }); - -// it("does not render icons when iconLocation prop is invalid", async () => { -// await render(FormTextInput, { -// props: { id: "test", label: "Bad Icon", iconLocation: "middle" }, // middle is the invalid value -// slots: { icons: "Icon" }, -// }); -// // (neither left nor right condition matches) -// const icon = screen.queryByText("Icon"); -// expect(icon).toBeNull(); -// }); + // MARK: Accessibility Test + // it("respects aria-hidden on fieldset for accessibility", async () => { + // await render(FormTextInput, { + // props: defaultProps, + // }); + + // // Locate the fieldset that visually wraps the input border + // const fieldset = screen.getByTestId("test-password-border"); + // expect(fieldset).toBeTruthy(); + + // // Accessibility rule: fieldset should be hidden from assistive tech + // expect(fieldset.getAttribute("aria-hidden")).toBe("true"); + + // // Ensure the fieldset has no accessible label + // const legend = fieldset.querySelector("legend"); + // expect(legend).toBeTruthy(); + // expect(legend?.textContent?.trim()).toBe("Password"); + + // // Now check the actual input is still accessible via label + // const input = screen.getByLabelText("Password"); + // expect(input).toBeTruthy(); + // expect(input.getAttribute("id")).toBe("test-password"); + // }); + + // MARK: - Style Coverage Tests + + // it("applies correct border color for normal and error states", async () => { + // // Normal state → border-interactive + // await render(FormTextInput, { props: defaultProps }); + // const normalBorder = screen.getByTestId("test-password-border"); + // expect(normalBorder.className).toContain("border-interactive"); + // expect(normalBorder.className).not.toContain("border-action-red"); + + // // Error state → border-action-red + // await render(FormTextInput, { + // props: { ...defaultProps, hasError: true }, + // }); + // const errorBorder = screen.getByTestId("test-password-border"); + // expect(errorBorder.className).toContain("border-action-red"); + // }); + + // it("shrinks and expands label correctly on focus and blur", async () => { + // await render(FormTextInput, { props: defaultProps }); + + // const input = screen.getByRole("textbox"); + // const border = screen.getByTestId("test-password-border"); + // const label = border.closest(".border-box")?.parentElement?.querySelector("label"); + // expect(label?.className).toMatch(/translate-y-\[0\.6rem\]/); + + // await fireEvent.focus(input); + // await waitFor(() => { + // const updated = border.closest(".border-box")?.parentElement?.querySelector("label"); + // expect(updated?.className).toMatch(/text-sm/); + // }); + + // await fireEvent.blur(input); + // await waitFor(() => { + // const updated = border.closest(".border-box")?.parentElement?.querySelector("label"); + // expect(updated?.className).toMatch(/translate-y-\[0\.6rem\]/); + // }); + // }); +}); diff --git a/frontend/test/components/form/text/FormTextInput.spect.ts b/frontend/test/components/form/text/FormTextInput.spect.ts deleted file mode 100644 index 157de9f7d..000000000 --- a/frontend/test/components/form/text/FormTextInput.spect.ts +++ /dev/null @@ -1,92 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later -import { fireEvent, screen, waitFor } from "@testing-library/vue"; -import { describe, expect, it } from "vitest"; - -import FormTextInput from "../../../../app/components/form/text/FormTextInput.vue"; -import render from "../../../../test/render"; - -function expectNormalLabel(label: HTMLElement) { - expect(label.className, "Label should be normal size").toMatch( - "translate-y-[1.125rem] pl-[12px]" - ); -} - -function expectShrunkLabel(label: HTMLElement) { - expect(label.className, "Label should be shrunk").toMatch( - "translate-x-4 text-sm text-distinct-text" - ); -} - -function expectNormalLegend(legend: HTMLElement) { - expect(legend.classList, "Hidden legend should be normal size").not.toContain( - "max-w-[0.01px]" - ); -} - -function expectShrunkLegend(legend: HTMLElement) { - expect(legend.className, "Hidden legend should be shrunk").toMatch( - "max-w-[0.01px]" - ); -} - -describe("FormTextInput", () => { - it("shrinks label when focused", async () => { - await render(FormTextInput, { - props: { - id: "test", - label: "test focus", - }, - }); - - let label = screen.getByText("test focus"); - expectNormalLabel(label); - - let legend = screen.getByTestId("hidden-legend"); - expectNormalLegend(legend); - - const input = screen.getByLabelText("test focus"); - await fireEvent.focus(input); - - await waitFor(async () => { - label = screen.getByText("test focus"); - expectShrunkLabel(label); - - legend = screen.getByTestId("hidden-legend"); - expectShrunkLegend(legend); - }); - }); - - it("expands label when empty and blurred", async () => { - await render(FormTextInput, { - props: { - id: "test", - label: "test blur", - }, - }); - - const input = screen.getByLabelText("test blur"); - await fireEvent.focus(input); - await fireEvent.input(input, "text"); - await fireEvent.blur(input); - - await waitFor(async () => { - const label = screen.getByText("test blur"); - expectShrunkLabel(label); - - const legend = screen.getByTestId("hidden-legend"); - expectShrunkLegend(legend); - }); - - await fireEvent.focus(input); - await fireEvent.input(input, ""); - await fireEvent.blur(input); - - await waitFor(async () => { - const label = screen.getByText("test blur"); - expectNormalLabel(label); - - const legend = screen.getByTestId("hidden-legend"); - expectShrunkLegend(legend); - }); - }); -}); From cb72f9aec246bb12a8bd3dabca7927d34af6bd0b Mon Sep 17 00:00:00 2001 From: Kirthi Date: Tue, 11 Nov 2025 20:50:04 -0500 Subject: [PATCH 005/243] Added 10 tests in total --- .../form/text/FormTextInput.spec.ts | 63 ++++++++++++++++--- 1 file changed, 55 insertions(+), 8 deletions(-) diff --git a/frontend/test/components/form/text/FormTextInput.spec.ts b/frontend/test/components/form/text/FormTextInput.spec.ts index 7379562ea..0713d74ca 100644 --- a/frontend/test/components/form/text/FormTextInput.spec.ts +++ b/frontend/test/components/form/text/FormTextInput.spec.ts @@ -10,7 +10,7 @@ import render from "../../../render"; // Test wrapper to enable v-model for correct reactivity in tests. const TestWrapper = defineComponent({ components: { FormTextInput }, - props: ["id", "label"], + props: ["id", "label", "hasError"], setup(_props) { const modelValue = ref(""); return { modelValue }; @@ -19,7 +19,8 @@ const TestWrapper = defineComponent({ `, }); @@ -115,10 +116,9 @@ describe("FormTextInput", () => { }); }); - // Kirthiiii - // MARK: Rendering Tests - const defaultProps = { id: "test-input", label: "Test Label" }; + // MARK: Basic Rendering + const defaultProps = { id: "test-input", label: "Test Label" }; it("renders input and label correctly", async () => { await render(TestWrapper, { props: defaultProps }); @@ -131,7 +131,8 @@ describe("FormTextInput", () => { }); // MARK: Logic - it("emits update:modelValue on user input", async () => { + + it("emits update:modelValue on user input. Reactivity logic works", async () => { const { emitted } = await render(FormTextInput, { props: { id: "logic-id", label: "Logic Test" }, }); @@ -143,7 +144,39 @@ describe("FormTextInput", () => { expect(emitted("update:modelValue")[0]).toEqual(["hello world"]); }); - // MARK: Edge Cases Tests + it("emits empty string when input is cleared", async () => { + const { emitted } = await render(FormTextInput, { + props: { id: "logic-clear", label: "Clear Test" }, + }); + + const input = screen.getByRole("textbox"); + await fireEvent.update(input, "some text"); + await fireEvent.update(input, ""); + + const events = emitted("update:modelValue")!; + expect(events.at(-1)).toEqual([""]); + }); + + // MARK: Props + + it("applies error border when hasError is true", async () => { + await render(TestWrapper, { props: { ...defaultProps, hasError: true } }); + + const border = screen.getByTestId(`${defaultProps.id}-border`); + expect(border.className).not.toContain("border-interactive"); + expect(border.className).toContain("border-action-red"); + }); + + it("uses default hasError value of false when not provided", async () => { + await render(TestWrapper, { props: { ...defaultProps } }); + + const border = screen.getByTestId(`${defaultProps.id}-border`); + expect(border.className).toContain("border-interactive"); + expect(border.className).not.toContain("border-action-red"); + }); + + // MARK: Edge Cases + it("handles empty string id and label gracefully", async () => { await render(TestWrapper, { props: { id: "", label: "" } }); @@ -162,7 +195,21 @@ describe("FormTextInput", () => { expect(input.value).toBe(special); }); + it("resets shrinkLabel to false on blur if input is empty", async () => { + await render(TestWrapper, { + props: { id: "blur-empty-id", label: "Blur Test" }, + }); + + const input = screen.getByRole("textbox"); + await fireEvent.focus(input); + await fireEvent.blur(input); // empty, should expand label + + const label = screen.getByText("Blur Test", { selector: "label" }); + expect(label.className).toMatch("translate-y-[0.6rem]"); + }); + // MARK: Accessibility Test + // it("respects aria-hidden on fieldset for accessibility", async () => { // await render(FormTextInput, { // props: defaultProps, @@ -186,7 +233,7 @@ describe("FormTextInput", () => { // expect(input.getAttribute("id")).toBe("test-password"); // }); - // MARK: - Style Coverage Tests + // MARK: Style Coverage Tests // it("applies correct border color for normal and error states", async () => { // // Normal state → border-interactive From 1c9cd0875d7cc6246cd6b74bcb9ef9e990529230 Mon Sep 17 00:00:00 2001 From: Kirthi Date: Tue, 11 Nov 2025 21:06:51 -0500 Subject: [PATCH 006/243] my tests are done. I did 16 --- .../form/text/FormTextInput.spec.ts | 73 +++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/frontend/test/components/form/text/FormTextInput.spec.ts b/frontend/test/components/form/text/FormTextInput.spec.ts index 0713d74ca..1765ffd99 100644 --- a/frontend/test/components/form/text/FormTextInput.spec.ts +++ b/frontend/test/components/form/text/FormTextInput.spec.ts @@ -144,6 +144,17 @@ describe("FormTextInput", () => { expect(emitted("update:modelValue")[0]).toEqual(["hello world"]); }); + it("does not emit update:modelValue if input value remains the same", async () => { + const { emitted } = await render(FormTextInput, { + props: { id: "emit-no-change", label: "Emit Test", modelValue: "same" }, + }); + + const input = screen.getByRole("textbox"); + await fireEvent.update(input, "same"); + + expect(emitted("update:modelValue")?.length ?? 0).toBeLessThanOrEqual(1); + }); + it("emits empty string when input is cleared", async () => { const { emitted } = await render(FormTextInput, { props: { id: "logic-clear", label: "Clear Test" }, @@ -157,6 +168,20 @@ describe("FormTextInput", () => { expect(events.at(-1)).toEqual([""]); }); + it("clears placeholder when label shrinks on focus", async () => { + await render(FormTextInput, { + props: { id: "placeholder-test", label: "Email" }, + }); + + const input = screen.getByRole("textbox"); + expect(input.getAttribute("placeholder")).toBe("Email"); + + await fireEvent.focus(input); + await waitFor(() => { + expect(input.getAttribute("placeholder")).toBe(""); + }); + }); + // MARK: Props it("applies error border when hasError is true", async () => { @@ -175,6 +200,42 @@ describe("FormTextInput", () => { expect(border.className).not.toContain("border-action-red"); }); + it("respects custom input type prop", async () => { + await render(FormTextInput, { + props: { id: "email-field", label: "Email", type: "email" }, + }); + + const input = screen.getByRole("textbox"); + expect(input.getAttribute("type")).toBe("email"); + }); + + // MARK: Icon + it("renders left icon slot correctly and adjusts label padding", async () => { + await render(FormTextInput, { + props: { id: "icon-left", label: "With Icon", iconLocation: "left" }, + slots: { icons: 'Icon' }, + }); + + const icon = screen.getByTestId("icon-left-slot"); + expect(icon).toBeTruthy(); + + const label = screen.getByText("With Icon", { selector: "label" }); + expect(label.className).toContain("pl-[3.4rem]"); + }); + + it("renders right icon slot correctly and adjusts label padding", async () => { + await render(FormTextInput, { + props: { id: "icon-right", label: "Right Icon", iconLocation: "right" }, + slots: { icons: 'Icon' }, + }); + + const icon = screen.getByTestId("icon-right-slot"); + expect(icon).toBeTruthy(); + + const label = screen.getByText("Right Icon", { selector: "label" }); + expect(label.className).toContain("pl-[12px]"); + }); + // MARK: Edge Cases it("handles empty string id and label gracefully", async () => { @@ -184,6 +245,18 @@ describe("FormTextInput", () => { expect(input).toBeTruthy(); }); + it("renders safely when id or label are undefined", async () => { + await render(FormTextInput, { + props: { + id: undefined as unknown as string, + label: undefined as unknown as string, + }, + }); + + const input = screen.getByRole("textbox"); + expect(input).toBeTruthy(); + }); + it("handles special characters", async () => { const special = "!@#$%^&*()_+{}[]|;:,.<>?"; await render(TestWrapper, { From 9eb343dd7a3641beecca2aea752ac8de4cd3bf39 Mon Sep 17 00:00:00 2001 From: Julia Date: Wed, 12 Nov 2025 14:00:54 -0500 Subject: [PATCH 007/243] all tests pass, added style + accessibility testing --- .../form/text/FormTextInput.spec.ts | 122 +++++++++--------- 1 file changed, 60 insertions(+), 62 deletions(-) diff --git a/frontend/test/components/form/text/FormTextInput.spec.ts b/frontend/test/components/form/text/FormTextInput.spec.ts index 1765ffd99..b5e08c364 100644 --- a/frontend/test/components/form/text/FormTextInput.spec.ts +++ b/frontend/test/components/form/text/FormTextInput.spec.ts @@ -281,66 +281,64 @@ describe("FormTextInput", () => { expect(label.className).toMatch("translate-y-[0.6rem]"); }); - // MARK: Accessibility Test - - // it("respects aria-hidden on fieldset for accessibility", async () => { - // await render(FormTextInput, { - // props: defaultProps, - // }); - - // // Locate the fieldset that visually wraps the input border - // const fieldset = screen.getByTestId("test-password-border"); - // expect(fieldset).toBeTruthy(); - - // // Accessibility rule: fieldset should be hidden from assistive tech - // expect(fieldset.getAttribute("aria-hidden")).toBe("true"); - - // // Ensure the fieldset has no accessible label - // const legend = fieldset.querySelector("legend"); - // expect(legend).toBeTruthy(); - // expect(legend?.textContent?.trim()).toBe("Password"); - - // // Now check the actual input is still accessible via label - // const input = screen.getByLabelText("Password"); - // expect(input).toBeTruthy(); - // expect(input.getAttribute("id")).toBe("test-password"); - // }); - - // MARK: Style Coverage Tests - - // it("applies correct border color for normal and error states", async () => { - // // Normal state → border-interactive - // await render(FormTextInput, { props: defaultProps }); - // const normalBorder = screen.getByTestId("test-password-border"); - // expect(normalBorder.className).toContain("border-interactive"); - // expect(normalBorder.className).not.toContain("border-action-red"); - - // // Error state → border-action-red - // await render(FormTextInput, { - // props: { ...defaultProps, hasError: true }, - // }); - // const errorBorder = screen.getByTestId("test-password-border"); - // expect(errorBorder.className).toContain("border-action-red"); - // }); - - // it("shrinks and expands label correctly on focus and blur", async () => { - // await render(FormTextInput, { props: defaultProps }); - - // const input = screen.getByRole("textbox"); - // const border = screen.getByTestId("test-password-border"); - // const label = border.closest(".border-box")?.parentElement?.querySelector("label"); - // expect(label?.className).toMatch(/translate-y-\[0\.6rem\]/); - - // await fireEvent.focus(input); - // await waitFor(() => { - // const updated = border.closest(".border-box")?.parentElement?.querySelector("label"); - // expect(updated?.className).toMatch(/text-sm/); - // }); - - // await fireEvent.blur(input); - // await waitFor(() => { - // const updated = border.closest(".border-box")?.parentElement?.querySelector("label"); - // expect(updated?.className).toMatch(/translate-y-\[0\.6rem\]/); - // }); - // }); + // MARK: Accessibility + + it("hides decorative fieldset for accessibility", async () => { + await render(FormTextInput, { props: defaultProps }); + + const fieldset = screen.getByTestId("test-input-border"); + expect(fieldset.getAttribute("aria-hidden")).toBe("true"); + }); + + it("associates input and label correctly via id and for attributes", async () => { + await render(FormTextInput, { props: defaultProps }); + + const input = document.getElementById("test-input"); + const label = document.querySelector(`label[for='test-input']`); + + expect(input).toBeTruthy(); + expect(label).toBeTruthy(); + expect(label?.textContent).toContain("Test Label"); + }); + + + // MARK: Style + + it("positions label correctly for left icon", async () => { + await render(FormTextInput, { + props: { id: "test-input", label: "Test Label", iconLocation: "left" }, + }); + + const label = document.querySelector(`label[for='test-input']`); + expect(label?.className).toContain("pl-[3.4rem]"); + expect(label?.className).toContain("translate-y-[0.6rem]"); + }); + + it("shrinks label on focus (adds translate-x and text-sm classes)", async () => { + await render(FormTextInput, { props: { id: "test-input", label: "Test Label" } }); + + const input = document.getElementById("test-input"); + let label = document.querySelector(`label[for='test-input']`); + + expect(label?.className).toMatch("translate-y-[0.6rem]"); + expect(label?.className).not.toMatch("translate-x-4"); + expect(label?.className).not.toMatch("text-sm"); + + await fireEvent.focus(input!); + + await waitFor(() => { + const label = document.querySelector(`label[for='test-input']`); + expect(label?.className).not.toMatch("translate-y-[0.6rem]"); + expect(label?.className).toMatch("translate-x-4"); + expect(label?.className).toMatch("text-sm"); + }); + }); + + it("renders top-level container with correct layout classes", async () => { + await render(FormTextInput, { props: { id: "layout", label: "Layout" } }); + + const container = screen.getByTestId("layout-border").closest("div")?.parentElement; + expect(container?.className).toContain("flex-col"); + expect(container?.className).toContain("w-full"); + }); }); From 5fb9392b35898815c27f244b287d75bf1b3b3347 Mon Sep 17 00:00:00 2001 From: Julia Date: Wed, 12 Nov 2025 14:12:21 -0500 Subject: [PATCH 008/243] ran yarn format and yarn lint, ignoring 1 warning --- .../form/text/FormTextInput.spec.ts | 23 +++++++++++-------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/frontend/test/components/form/text/FormTextInput.spec.ts b/frontend/test/components/form/text/FormTextInput.spec.ts index b5e08c364..85d75f4a8 100644 --- a/frontend/test/components/form/text/FormTextInput.spec.ts +++ b/frontend/test/components/form/text/FormTextInput.spec.ts @@ -300,7 +300,6 @@ describe("FormTextInput", () => { expect(label).toBeTruthy(); expect(label?.textContent).toContain("Test Label"); }); - // MARK: Style @@ -308,24 +307,26 @@ describe("FormTextInput", () => { await render(FormTextInput, { props: { id: "test-input", label: "Test Label", iconLocation: "left" }, }); - + const label = document.querySelector(`label[for='test-input']`); expect(label?.className).toContain("pl-[3.4rem]"); expect(label?.className).toContain("translate-y-[0.6rem]"); }); - + it("shrinks label on focus (adds translate-x and text-sm classes)", async () => { - await render(FormTextInput, { props: { id: "test-input", label: "Test Label" } }); - + await render(FormTextInput, { + props: { id: "test-input", label: "Test Label" }, + }); + const input = document.getElementById("test-input"); - let label = document.querySelector(`label[for='test-input']`); - + const label = document.querySelector(`label[for='test-input']`); + expect(label?.className).toMatch("translate-y-[0.6rem]"); expect(label?.className).not.toMatch("translate-x-4"); expect(label?.className).not.toMatch("text-sm"); - + await fireEvent.focus(input!); - + await waitFor(() => { const label = document.querySelector(`label[for='test-input']`); expect(label?.className).not.toMatch("translate-y-[0.6rem]"); @@ -337,7 +338,9 @@ describe("FormTextInput", () => { it("renders top-level container with correct layout classes", async () => { await render(FormTextInput, { props: { id: "layout", label: "Layout" } }); - const container = screen.getByTestId("layout-border").closest("div")?.parentElement; + const container = screen + .getByTestId("layout-border") + .closest("div")?.parentElement; expect(container?.className).toContain("flex-col"); expect(container?.className).toContain("w-full"); }); From 3959191f61471c3673d76de352fc67a6d63b4c69 Mon Sep 17 00:00:00 2001 From: Kirthi Date: Sun, 16 Nov 2025 19:31:26 -0500 Subject: [PATCH 009/243] Add pagination functionality to organizations page --- .../queries/useGetOrganizations.ts | 36 +++++++++-- frontend/app/pages/organizations/index.vue | 61 +++++++++++++++++-- .../communities/organization/organization.ts | 3 +- frontend/app/stores/organization.ts | 8 +++ 4 files changed, 97 insertions(+), 11 deletions(-) diff --git a/frontend/app/composables/queries/useGetOrganizations.ts b/frontend/app/composables/queries/useGetOrganizations.ts index 57431d7b5..bc9ce6506 100644 --- a/frontend/app/composables/queries/useGetOrganizations.ts +++ b/frontend/app/composables/queries/useGetOrganizations.ts @@ -14,16 +14,36 @@ export function useGetOrganizations( filters: Ref | ComputedRef ) { const store = useOrganizationStore(); + const page = ref(1); const { showToastError } = useToaster(); - const orgFilters = computed(() => unref(filters)); + const orgFilters = computed(() => unref({ ...filters })); // Use AsyncData for SSR, hydration, and cache. const { data, pending, error, refresh } = useAsyncData( - () => getKeyForGetOrganizations(orgFilters.value), + () => getKeyForGetOrganizations(unref(filters)), async () => { try { - const organizations = await listOrganizations(orgFilters.value); + //const organizations = await listOrganizations(orgFilters.value); + const organizations = await listOrganizations({ + ...(orgFilters.value as any), + page: page.value, + page_size: 10, + }); + const organizationsCached = store.getOrganizations(); + const pageCached = store.getPage(); + // Append new events to cached events if page > 1 + if ( + organizationsCached.length > 0 && + JSON.stringify(store.getFilters()) === + JSON.stringify(orgFilters.value) && + page.value > pageCached + ) { + store.setOrganizations([...organizationsCached, ...organizations]); + return [...organizationsCached, ...organizations] as OrganizationT[]; + } + store.setOrganizations(organizations); store.setFilters(orgFilters.value); + store.setPage(page.value); return organizations as OrganizationT[]; } catch (error) { showToastError((error as AppError).message); @@ -31,13 +51,14 @@ export function useGetOrganizations( } }, { - watch: [orgFilters.value], + watch: [filters, page], immediate: true, getCachedData: (key, nuxtApp) => { if ( store.getOrganizations().length > 0 && JSON.stringify(store.getFilters()) === - JSON.stringify(orgFilters.value) + JSON.stringify(orgFilters.value) && + page.value === store.getPage() ) { return store.getOrganizations(); } @@ -49,11 +70,16 @@ export function useGetOrganizations( } ); + const getMore = async () => { + page.value += 1; + }; + return { data, pending, error, refresh, filters: orgFilters.value, + getMore, }; } diff --git a/frontend/app/pages/organizations/index.vue b/frontend/app/pages/organizations/index.vue index 111dcca3c..1b4a61f16 100644 --- a/frontend/app/pages/organizations/index.vue +++ b/frontend/app/pages/organizations/index.vue @@ -4,6 +4,7 @@ {{ $t("i18n.pages.organizations.index.header_title") }} + - -
-
+ + + +
+
+ + +
+

Loading...

+
+
diff --git a/frontend/app/services/communities/organization/organization.ts b/frontend/app/services/communities/organization/organization.ts index da376bcbb..999130e71 100644 --- a/frontend/app/services/communities/organization/organization.ts +++ b/frontend/app/services/communities/organization/organization.ts @@ -13,6 +13,7 @@ import type { import { del, get, post } from "~/services/http"; import { defaultOrganizationText } from "~/types/communities/organization"; import { errorHandler } from "~/utils/errorHandler"; +import type { Pagination } from "~/types/http"; // MARK: Map API Response to Type @@ -54,7 +55,7 @@ export async function getOrganization(id: string): Promise { // MARK: List All export async function listOrganizations( - filters: OrganizationFilters + filters: OrganizationFilters & Partial = {} ): Promise { try { const query = new URLSearchParams( diff --git a/frontend/app/stores/organization.ts b/frontend/app/stores/organization.ts index 403f413a9..f6ca0dabd 100644 --- a/frontend/app/stores/organization.ts +++ b/frontend/app/stores/organization.ts @@ -10,6 +10,7 @@ interface OrganizationStore { organizations: Organization[]; images: ContentImage[]; filters: OrganizationFilters; + page: number; } export const useOrganizationStore = defineStore("organization", { @@ -18,9 +19,16 @@ export const useOrganizationStore = defineStore("organization", { images: [] as ContentImage[], organizations: [], filters: {} as OrganizationFilters, + page: 0, }), actions: { // MARK: Set Organizations + getPage(): number { + return this.page; + }, + setPage(page: number) { + this.page = page; + }, setOrganizations(organizations: Organization[]) { this.organizations = organizations; }, From f66c295cefc2d2a73b745fc28a650db6d3a1b165 Mon Sep 17 00:00:00 2001 From: Kirthi Date: Sun, 16 Nov 2025 19:39:25 -0500 Subject: [PATCH 010/243] quick change --- frontend/app/services/communities/organization/organization.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/app/services/communities/organization/organization.ts b/frontend/app/services/communities/organization/organization.ts index 999130e71..a9c72eb8f 100644 --- a/frontend/app/services/communities/organization/organization.ts +++ b/frontend/app/services/communities/organization/organization.ts @@ -55,7 +55,7 @@ export async function getOrganization(id: string): Promise { // MARK: List All export async function listOrganizations( - filters: OrganizationFilters & Partial = {} + filters: OrganizationFilters & Pagination = { page: 1, page_size: 10 } ): Promise { try { const query = new URLSearchParams( From 84e677c5df0208b13d6300f04beb05b19ad78cbb Mon Sep 17 00:00:00 2001 From: Kirthi Date: Sun, 16 Nov 2025 19:48:04 -0500 Subject: [PATCH 011/243] quick change --- frontend/app/pages/organizations/index.vue | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/frontend/app/pages/organizations/index.vue b/frontend/app/pages/organizations/index.vue index 1b4a61f16..7ab75e758 100644 --- a/frontend/app/pages/organizations/index.vue +++ b/frontend/app/pages/organizations/index.vue @@ -30,7 +30,7 @@
-

Loading...

+

Loading...

@@ -42,7 +42,10 @@ import { useGetOrganizations } from "~/composables/queries/useGetOrganizations"; const route = useRoute(); -const filters = computed(() => route.query); +const filters = computed(() => { + const { view, ...rest } = route.query; // omit view + return rest as unknown as orgFilters; +}); const loadingFetchMore = ref(false); const { data: organizations, pending, getMore } = useGetOrganizations(filters); From 0b99e5281e2af515278803fe58b75e41ac17d2cc Mon Sep 17 00:00:00 2001 From: Kirthi Date: Wed, 19 Nov 2025 14:53:57 -0500 Subject: [PATCH 012/243] fixed prettier --- frontend/app/composables/queries/useGetOrganizations.ts | 2 +- frontend/app/pages/organizations/index.vue | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/frontend/app/composables/queries/useGetOrganizations.ts b/frontend/app/composables/queries/useGetOrganizations.ts index be636a344..00691c9a7 100644 --- a/frontend/app/composables/queries/useGetOrganizations.ts +++ b/frontend/app/composables/queries/useGetOrganizations.ts @@ -16,7 +16,7 @@ export function useGetOrganizations( try { //const organizations = await listOrganizations(orgFilters.value); const organizations = await listOrganizations({ - ...(orgFilters.value as any), + ...orgFilters.value, page: page.value, page_size: 10, }); diff --git a/frontend/app/pages/organizations/index.vue b/frontend/app/pages/organizations/index.vue index 14f8af22d..2b6ae91d4 100644 --- a/frontend/app/pages/organizations/index.vue +++ b/frontend/app/pages/organizations/index.vue @@ -30,7 +30,9 @@
-

Loading...

+

+Loading... +

@@ -39,8 +41,9 @@ diff --git a/frontend/app/services/communities/organization/organization.ts b/frontend/app/services/communities/organization/organization.ts index 3ba0d5920..19465c695 100644 --- a/frontend/app/services/communities/organization/organization.ts +++ b/frontend/app/services/communities/organization/organization.ts @@ -3,8 +3,8 @@ // Uses services/http.ts helpers and centralizes error handling + normalization. import { del, get, post } from "~/services/http"; -// import { errorHandler } from "~/utils/errorHandler"; -// import type { Pagination } from "~/types/http"; +type OrganizationPaginatedResponse = + globalThis.PaginatedResponse; // MARK: Map API Response to Type @@ -47,7 +47,7 @@ export async function getOrganization(id: string): Promise { export async function listOrganizations( filters: OrganizationFilters & Pagination = { page: 1, page_size: 10 } -): Promise { +): Promise { try { const query = new URLSearchParams(); // Handle topics specially: arrays become repeated params (?topics=A&topics=B). @@ -67,7 +67,7 @@ export async function listOrganizations( `/communities/organizations?${query.toString()}`, { withoutAuth: true } ); - return res.results.map(mapOrganization); + return { data: res.results.map(mapOrganization), isLastPage: !res.next }; } catch (e) { throw errorHandler(e); } diff --git a/frontend/shared/types/organization.d.ts b/frontend/shared/types/organization.d.ts index 5aff800c9..9589cf631 100644 --- a/frontend/shared/types/organization.d.ts +++ b/frontend/shared/types/organization.d.ts @@ -25,6 +25,8 @@ interface OrganizationBase extends Entity { // supportingUsers?: User[]; } +export type OrganizationPaginatedResponse = PaginatedResponse; + export interface Organization extends OrganizationBase { texts: OrganizationText[]; } diff --git a/frontend/test/services/communities/organization/organization.spec.ts b/frontend/test/services/communities/organization/organization.spec.ts index c5d93a2b5..496cfda5e 100644 --- a/frontend/test/services/communities/organization/organization.spec.ts +++ b/frontend/test/services/communities/organization/organization.spec.ts @@ -93,9 +93,10 @@ describe("services/communities/organization", () => { const [, opts] = getFetchCall(fetchMock); expect(opts.headers?.Authorization).toBeUndefined(); - expect(result).toHaveLength(1); - expect(result[0].id).toBe("org-2"); - expect(result[0].texts).toEqual([]); + expect(result.data).toHaveLength(1); + expect(result.data[0].id).toBe("org-2"); + expect(result.data[0].texts).toEqual([]); + expect(result.isLastPage).toBe(true); }); // MARK: Create From dab28a10afb4af9e0cf9ca57141950a0ee840307 Mon Sep 17 00:00:00 2001 From: Ayush Anand Srivastava <66489925+ayushdoesdev@users.noreply.github.com> Date: Sat, 22 Nov 2025 20:15:01 +0530 Subject: [PATCH 023/243] test: add unit tests for Sidebar and useSidebar logic (#1652) (#1657) * test: add unit tests for Sidebar and useSidebar logic (#1652) * Fixes for tests to use sidebar composable in tests --------- Co-authored-by: Ayush Srivastava Co-authored-by: Nicole <31940739+nicki182@users.noreply.github.com> Co-authored-by: Andrew Tavis McAllister --- frontend/app/composables/useColor.ts | 4 +- .../components/sidebar/SidebarLeft.spec.ts | 205 ++++++++++++++++++ .../test/composables/useSidebarClass.spec.ts | 62 ++++++ frontend/test/setup.ts | 13 ++ frontend/test/vitest-globals.d.ts | 6 + 5 files changed, 288 insertions(+), 2 deletions(-) create mode 100644 frontend/test/components/sidebar/SidebarLeft.spec.ts create mode 100644 frontend/test/composables/useSidebarClass.spec.ts diff --git a/frontend/app/composables/useColor.ts b/frontend/app/composables/useColor.ts index 5c0be95e8..4e7d7284d 100644 --- a/frontend/app/composables/useColor.ts +++ b/frontend/app/composables/useColor.ts @@ -9,12 +9,12 @@ const colorByType: Record = { }; export const useColor = () => { + const colorMode = useColorMode(); const getColorModeImages = (path: string, ext: string = ".png") => { - return `${path}_${useColorMode().value}${ext}`; + return `${path}_${colorMode.value}${ext}`; }; const getEventColorByType = (eventType: EventType) => { - const colorMode = useColorMode(); const suffix = colorMode.preference === "light" ? "light" : "dark"; const key = `${eventType}_${suffix}` as ColorKey; return colorByType[key]; diff --git a/frontend/test/components/sidebar/SidebarLeft.spec.ts b/frontend/test/components/sidebar/SidebarLeft.spec.ts new file mode 100644 index 000000000..543407b0d --- /dev/null +++ b/frontend/test/components/sidebar/SidebarLeft.spec.ts @@ -0,0 +1,205 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +/** + * Unit tests for SidebarLeft.vue + */ +import { mount } from "@vue/test-utils"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { nextTick, ref } from "vue"; + +import SidebarLeft from "../../../app/components/sidebar/left/SidebarLeft.vue"; + +// Shared mock store object so test and component share same instance. +const mockSidebarStore = reactive({ + collapsed: false, + collapsedSwitch: false, + toggleCollapsed: vi.fn(), + toggleCollapsedSwitch: vi.fn(), +}); + +vi.mock("~/stores/sidebar", () => ({ + useSidebar: () => mockSidebarStore, +})); + +// Mock route utilities and vue-router hooks used by the component. +vi.mock("~/utils/routeUtils", () => ({ + currentRoutePathIncludes: (path: string, routeName: string) => { + // Return true for 'home' to match default map. + return routeName.includes(path); + }, + isCurrentRoutePathSubpageOf: () => false, +})); + +vi.mock("vue-router", () => ({ + useRoute: () => ({ query: {}, path: "/home", name: "home" }), + useRouter: () => ({ + currentRoute: { value: { name: "home" } }, + push: vi.fn(), + }), +})); + +// Provide a minimal global auto-import for useState used inside component. +// In the Nuxt environment useState is auto-imported; tests run without that, so we add it. +globalThis.useState = function (key: string, init?: () => T) { + const val = init ? init() : (undefined as unknown as T); + return ref(val); +}; + +describe("SidebarLeft.vue", () => { + let wrapper: ReturnType; + + beforeEach(() => { + // Reset shared mock store values before each test. + mockSidebarStore.collapsed = false; + mockSidebarStore.collapsedSwitch = false; + mockSidebarStore.toggleCollapsed.mockClear(); + mockSidebarStore.toggleCollapsedSwitch.mockClear(); + + wrapper = mount(SidebarLeft, { + global: { + // Stub child components (we don't need their implementations). + stubs: { + SidebarLeftHeader: true, + SidebarLeftMainSectionSelectors: true, + SidebarLeftContent: true, + SidebarLeftFilter: true, + SidebarLeftFooter: true, + SearchBar: true, + Icon: true, + }, + mocks: { + $t: (key: string) => key, + }, + }, + // Ensure the component mounts with DOM so refs/element manipulation works. + attachTo: document.body, + }); + }); + + it("renders aside element with navigation role and aria-label from $t", () => { + const aside = wrapper.find("aside"); + expect(aside.exists()).toBe(true); + expect(aside.attributes("role")).toBe("navigation"); + // $t mock returns the key string, so it should contain the i18n key. + expect(aside.attributes("aria-label")).toContain( + "i18n.components.sidebar_left.sidebar_left_aria_label" + ); + // Tabindex is present. + expect(aside.attributes("tabindex")).toBe("0"); + }); + + it("expands on mouseover and collapses on mouseleave (updates store)", async () => { + // Initialize store as collapsed to test opening. + mockSidebarStore.collapsed = true; + mockSidebarStore.collapsedSwitch = false; + + // Need a fresh nextTick so template uses updated store values. + await nextTick(); + + const aside = wrapper.find("aside"); + expect(aside.exists()).toBe(true); + + // mouseover: collapseSidebar(false) should set collapsed = false. + await aside.trigger("mouseover"); + // Event handlers set sidebar.collapsed synchronously in component function. + expect(mockSidebarStore.collapsed).toBe(false); + + // mouseleave: collapseSidebar(true) should set collapsed = true. + await aside.trigger("mouseleave"); + expect(mockSidebarStore.collapsed).toBe(true); + }); + + it("focus and focusout behavior: focus keeps expanded, focusout collapses unless relatedTarget inside wrapper", async () => { + // Start as collapsed. + mockSidebarStore.collapsed = true; + await nextTick(); + + const aside = wrapper.find("aside"); + // trigger focus leads to collapseSidebar(false). + await aside.trigger("focus"); + expect(mockSidebarStore.collapsed).toBe(false); + + // focusout with relatedTarget inside the aside should keep expanded (collapsed=false). + await aside.trigger("focusout", { relatedTarget: aside.element }); + // Because handleFocusOut checks sidebarWrapper.contains(relatedTarget) should call collapseSidebar(false). + expect(mockSidebarStore.collapsed).toBe(false); + + // focusout with relatedTarget outside (null) should collapse. + await aside.trigger("focusout", { relatedTarget: null }); + expect(mockSidebarStore.collapsed).toBe(true); + }); + + it("applies correct width classes depending on collapsed and collapsedSwitch states", async () => { + const aside = wrapper.find("aside"); + + expect(aside.classes()).toContain("w-56"); + + // Update reactive store state. + mockSidebarStore.collapsed = true; + mockSidebarStore.collapsedSwitch = true; + await nextTick(); + + // Vue now re-renders class list updates. + const classes = aside.classes().join(" "); + expect(classes).toMatch(/w-(16|20)/); // support both possible widths + }); + + it("content overflow class toggles based on collapsed state", async () => { + const contentDiv = wrapper + .findAll("div") + .find((n) => (n.attributes("class") || "").includes("overflow-x-hidden")); + expect(contentDiv).toBeTruthy(); + + mockSidebarStore.collapsed = false; + mockSidebarStore.collapsedSwitch = false; + await nextTick(); + expect(contentDiv!.attributes("class")).toContain("overflow-y-auto"); + + mockSidebarStore.collapsed = true; + mockSidebarStore.collapsedSwitch = true; + await nextTick(); + + // Vue updates and overflow-y-auto disappears. + expect(contentDiv!.attributes("class")).not.toContain("overflow-y-auto"); + }); + + it("detects scrollable content and updates width class when scrollable", async () => { + const aside = wrapper.find("aside"); + // Ensure expanded state. + mockSidebarStore.collapsed = false; + mockSidebarStore.collapsedSwitch = false; + await nextTick(); + + // Find content element that is referenced by ref="content". + // The DOM element is accessible as wrapper.vm.$refs.content. + const vmAny = wrapper.vm; + const contentRefEl = vmAny.$refs?.content as HTMLElement | undefined; + expect(contentRefEl).toBeDefined(); + + // Set the element to be scrollable by defining scrollHeight > clientHeight. + // Use defineProperty in case these are read-only. + Object.defineProperty(contentRefEl, "scrollHeight", { + value: 500, + configurable: true, + }); + Object.defineProperty(contentRefEl, "clientHeight", { + value: 100, + configurable: true, + }); + // Also set scrollTop to non-zero so top shadow logic would reflect being scrolled. + Object.defineProperty(contentRefEl, "scrollTop", { + value: 10, + configurable: true, + }); + + // Dispatch a resize so the component's window listener runs setSidebarContentScrollable. + window.dispatchEvent(new Event("resize")); + + // Wait for the small timeout in setSidebarContentScrollable (component uses setTimeout 50ms). + await new Promise((res) => setTimeout(res, 70)); + await nextTick(); + + // When scrollable and expanded, the class 'w-60' should be present. + const classes = aside.classes(); + expect(classes).toContain("w-60"); + }); +}); diff --git a/frontend/test/composables/useSidebarClass.spec.ts b/frontend/test/composables/useSidebarClass.spec.ts new file mode 100644 index 000000000..1696b95cc --- /dev/null +++ b/frontend/test/composables/useSidebarClass.spec.ts @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +/** + * Unit tests for the useSidebarClass functionality. + */ +import { describe, expect, it } from "vitest"; +import { ref } from "vue"; + +import { useSidebarClass } from "../../app/composables/useSidebarClass"; + +describe("useSidebarClass", () => { + it("getSidebarContentDynamicClass returns expanded classes when expanded and not scrollable", () => { + const { getSidebarContentDynamicClass } = useSidebarClass(); + const sidebarHover = ref(false); + const classes = getSidebarContentDynamicClass(false, sidebarHover).value; + + expect(classes["md:pl-16 xl:pl-56"]).toBe(true); + expect(classes["md:pl-20 xl:pl-60"]).toBe(false); + expect(classes["blur-sm xl:blur-none"]).toBe(false); + }); + + it("getSidebarContentDynamicClass returns scrollable classes when sidebarContentScrollable true", () => { + const { getSidebarContentDynamicClass } = useSidebarClass(); + + const sidebarHover = ref(false); + const classes = getSidebarContentDynamicClass(true, sidebarHover).value; + + expect(classes["md:pl-20 xl:pl-60"]).toBe(true); + expect(classes["md:pl-16 xl:pl-56"]).toBe(true); + }); + + it("getSidebarContentDynamicClass returns blur when collapsedSwitch true, not collapsed and hovered", () => { + // Set state to collapsedSwitch true but not collapsed. + globalThis.useSidebarMock.mockImplementation(() => ({ + collapsed: false, + collapsedSwitch: true, + })); + const { getSidebarContentDynamicClass } = useSidebarClass(); + const sidebarHover = ref(true); + const classes = getSidebarContentDynamicClass(false, sidebarHover).value; + + expect(classes["blur-sm xl:blur-none"]).toBe(true); + }); + + it("getSidebarFooterDynamicClass returns expected classes for footer", () => { + const { getSidebarFooterDynamicClass } = useSidebarClass(); + const sidebarHover = ref(false); + const classes = getSidebarFooterDynamicClass(sidebarHover).value; + + // Sidebar is expanded. + expect(classes["md:pl-24 xl:pl-64"]).toBe(true); + expect(classes["blur-sm xl:blur-none"]).toBe(false); + + // Simulate hovered state with collapsedSwitch true and not collapsed. + globalThis.useSidebarMock.mockImplementation(() => ({ + collapsed: true, + collapsedSwitch: false, + })); + sidebarHover.value = true; + const classesAfter = getSidebarFooterDynamicClass(sidebarHover).value; + expect(classesAfter["blur-sm xl:blur-none"]).toBe(true); + }); +}); diff --git a/frontend/test/setup.ts b/frontend/test/setup.ts index 9aae78dec..9da101b83 100644 --- a/frontend/test/setup.ts +++ b/frontend/test/setup.ts @@ -73,6 +73,19 @@ const useColorModeFn = () => ({ globalThis.useColorModeMock = vi.fn(useColorModeFn); globalThis.useColorMode = () => globalThis.useColorModeMock(); +// Set up Sidebar mock for components that use useSidebar(). +const useSidebarFn = () => ({ + collapsed: false, + collapsedSwitch: false, +}); + +globalThis.useSidebarMock = vi.fn(useSidebarFn); +globalThis.useSidebar = () => globalThis.useSidebarMock(); + +vi.mock("~/stores/sidebar", () => ({ + useSidebar: globalThis.useSidebar, +})); + // Mock the dev mode store to fix FriendlyCaptcha component. globalThis.useDevMode = () => ({ active: { value: false }, diff --git a/frontend/test/vitest-globals.d.ts b/frontend/test/vitest-globals.d.ts index dc5c3dace..d8574a465 100644 --- a/frontend/test/vitest-globals.d.ts +++ b/frontend/test/vitest-globals.d.ts @@ -70,6 +70,12 @@ declare global { const useColorMode: () => ReturnType; + const useSidebarMock: Mock< + () => { collapsed: boolean; collapsedSwitch: boolean } + >; + + const useSidebar: () => ReturnType; + const useDevMode: () => { active: { value: boolean }; check: () => void }; const data: { value: AuthUser }; From 21a67e49e129babfb7b045a4895106a065a10d2b Mon Sep 17 00:00:00 2001 From: Andrew Tavis McAllister Date: Sat, 22 Nov 2025 15:46:12 +0100 Subject: [PATCH 024/243] Minor vitest file formatting --- frontend/test/composables/useSidebarClass.spec.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/frontend/test/composables/useSidebarClass.spec.ts b/frontend/test/composables/useSidebarClass.spec.ts index 1696b95cc..22390bf8f 100644 --- a/frontend/test/composables/useSidebarClass.spec.ts +++ b/frontend/test/composables/useSidebarClass.spec.ts @@ -10,6 +10,7 @@ import { useSidebarClass } from "../../app/composables/useSidebarClass"; describe("useSidebarClass", () => { it("getSidebarContentDynamicClass returns expanded classes when expanded and not scrollable", () => { const { getSidebarContentDynamicClass } = useSidebarClass(); + const sidebarHover = ref(false); const classes = getSidebarContentDynamicClass(false, sidebarHover).value; @@ -35,6 +36,7 @@ describe("useSidebarClass", () => { collapsedSwitch: true, })); const { getSidebarContentDynamicClass } = useSidebarClass(); + const sidebarHover = ref(true); const classes = getSidebarContentDynamicClass(false, sidebarHover).value; @@ -43,6 +45,7 @@ describe("useSidebarClass", () => { it("getSidebarFooterDynamicClass returns expected classes for footer", () => { const { getSidebarFooterDynamicClass } = useSidebarClass(); + const sidebarHover = ref(false); const classes = getSidebarFooterDynamicClass(sidebarHover).value; From ef57d33c0023aaecc72af4a395fcde6ce8841951 Mon Sep 17 00:00:00 2001 From: Deep lukhi <154542863+deeplukhi@users.noreply.github.com> Date: Sat, 22 Nov 2025 20:26:16 +0530 Subject: [PATCH 025/243] test: add unit tests for useBreakpoint composable (#1695) (#1716) * test: add unit tests for useBreakpoint composable (#1695) * Add license header, formatting and remove anys --------- Co-authored-by: Andrew Tavis McAllister --- .../test/composables/useBreakpoint.spec.ts | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 frontend/test/composables/useBreakpoint.spec.ts diff --git a/frontend/test/composables/useBreakpoint.spec.ts b/frontend/test/composables/useBreakpoint.spec.ts new file mode 100644 index 000000000..52b61f463 --- /dev/null +++ b/frontend/test/composables/useBreakpoint.spec.ts @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +import useBreakpoint from "../../app/composables/generic/useBreakpoint"; + +describe("useBreakpoint composable", () => { + const originalWindow = globalThis.window; + + beforeEach(() => { + vi.stubGlobal("window", { + innerWidth: 800, + addEventListener: vi.fn(), + removeEventListener: vi.fn(), + }); + }); + + afterEach(() => { + vi.unstubAllGlobals(); + globalThis.window = originalWindow; + }); + + it("returns true when width matches small breakpoint", () => { + globalThis.window.innerWidth = 500; + const isSmall = useBreakpoint("sm"); + expect(isSmall.value).toBe(true); + }); + + it("returns true when width matches medium breakpoint", () => { + globalThis.window.innerWidth = 800; + const isMedium = useBreakpoint("md"); + expect(isMedium.value).toBe(true); + }); + + it("returns true when width matches large breakpoint", () => { + globalThis.window.innerWidth = 1200; + const isLarge = useBreakpoint("lg"); + expect(isLarge.value).toBe(true); + }); +}); From 8cac89bd50e8b2c49f1cd1d40f4d50854ed80d7d Mon Sep 17 00:00:00 2001 From: Aasim Syed Date: Sat, 22 Nov 2025 10:54:53 -0600 Subject: [PATCH 026/243] fix: remove duplicate organization-faq-list test ID from draggable component (#1743) Removes duplicate data-testid attribute from draggable component in organization FAQ page to fix Playwright strict mode violation in E2E tests. --- frontend/app/pages/organizations/[orgId]/faq.vue | 1 - 1 file changed, 1 deletion(-) diff --git a/frontend/app/pages/organizations/[orgId]/faq.vue b/frontend/app/pages/organizations/[orgId]/faq.vue index 516ef8eb6..b406125b2 100644 --- a/frontend/app/pages/organizations/[orgId]/faq.vue +++ b/frontend/app/pages/organizations/[orgId]/faq.vue @@ -29,7 +29,6 @@ :animation="150" chosen-class="sortable-chosen" class="space-y-4" - data-testid="organization-faq-list" :delay="0" :delay-on-touch-start="false" direction="vertical" From 8fcb191ca58ea9e70b2285fe431d92c01ee10e78 Mon Sep 17 00:00:00 2001 From: Aasim Syed Date: Sun, 23 Nov 2025 02:12:44 -0600 Subject: [PATCH 027/243] fix: use correct route parameter for groupId in FAQ page (#1748) Fixes FAQ creation by using params.groupId instead of params.eventId. This was causing groupId to be empty, preventing FAQ creation from working. --- .../app/pages/organizations/[orgId]/groups/[groupId]/faq.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/app/pages/organizations/[orgId]/groups/[groupId]/faq.vue b/frontend/app/pages/organizations/[orgId]/groups/[groupId]/faq.vue index f638fe0ba..cd8f653c3 100644 --- a/frontend/app/pages/organizations/[orgId]/groups/[groupId]/faq.vue +++ b/frontend/app/pages/organizations/[orgId]/groups/[groupId]/faq.vue @@ -67,7 +67,7 @@ const groupTabs = useGetGroupTabs(); const { openModal } = useModalHandlers("ModalFaqEntryGroup"); -const paramsGroupId = useRoute().params.eventId; +const paramsGroupId = useRoute().params.groupId; const groupId = typeof paramsGroupId === "string" ? paramsGroupId : ""; const { data: group } = useGetGroup(groupId); From 9462a1fccb99e6d3368a95337d5383b0cf2beebb Mon Sep 17 00:00:00 2001 From: Aasim Syed Date: Sun, 23 Nov 2025 02:30:19 -0600 Subject: [PATCH 028/243] fix: move edit/delete buttons outside FAQ disclosure button to resolve nested interactive controls (#1745) * fix: move edit/delete buttons outside FAQ disclosure button to resolve nested interactive controls Moves IconEdit and IconDelete buttons outside the DisclosureButton component to fix WCAG nested-interactive accessibility violation. Also adds tabindex="-1" to drag handle to ensure it's not focusable. * Add focus brand, IconDelete is a button, a11y fixes --------- Co-authored-by: Andrew Tavis McAllister --- frontend/app/components/card/CardFAQEntry.vue | 51 +++++++++---------- frontend/app/components/icon/IconDelete.vue | 9 ++-- frontend/app/components/icon/IconEdit.vue | 2 +- 3 files changed, 32 insertions(+), 30 deletions(-) diff --git a/frontend/app/components/card/CardFAQEntry.vue b/frontend/app/components/card/CardFAQEntry.vue index dea0554b9..892b33c88 100644 --- a/frontend/app/components/card/CardFAQEntry.vue +++ b/frontend/app/components/card/CardFAQEntry.vue @@ -15,6 +15,7 @@ data-testid="faq-drag-handle" :entity="entity" size="1em" + tabindex="-1" />
-
- - -
+
+ + +
diff --git a/frontend/app/components/modal/ModalAlert.vue b/frontend/app/components/modal/ModalAlert.vue index 9d2e264ad..a9b07c02e 100644 --- a/frontend/app/components/modal/ModalAlert.vue +++ b/frontend/app/components/modal/ModalAlert.vue @@ -3,7 +3,11 @@
- +

{{ $t(message) }}

diff --git a/frontend/app/components/modal/ModalBase.vue b/frontend/app/components/modal/ModalBase.vue index d2fd5bcc6..fb2b75251 100644 --- a/frontend/app/components/modal/ModalBase.vue +++ b/frontend/app/components/modal/ModalBase.vue @@ -43,7 +43,7 @@ :aria-label=" $t ? $t('i18n.components.modal_base.close_modal_aria_label') : '' " - class="absolute right-0 rounded-full p-1 text-distinct-text focus-brand hover:text-primary-text" + class="absolute right-0 cursor-pointer rounded-full p-1 text-distinct-text focus-brand hover:text-primary-text" data-testid="modal-close-button" role="button" > diff --git a/frontend/app/composables/mutations/useEventResourcesMutations.ts b/frontend/app/composables/mutations/useEventResourcesMutations.ts index 0810555e6..0fc761055 100644 --- a/frontend/app/composables/mutations/useEventResourcesMutations.ts +++ b/frontend/app/composables/mutations/useEventResourcesMutations.ts @@ -53,6 +53,26 @@ export function useEventResourcesMutations(eventId: MaybeRef) { } } + // Delete existing resource. + async function deleteResource(resourceId: string) { + loading.value = true; + error.value = null; + + try { + await deleteEventResource(resourceId); + + // Invalidate cache and refetch fresh data. + await refreshEventData(); + + return true; + } catch (err) { + showToastError((err as AppError).message); + return false; + } finally { + loading.value = false; + } + } + // Reorder multiple resource entries. async function reorderResources(resources: Resource[]) { loading.value = true; @@ -85,6 +105,7 @@ export function useEventResourcesMutations(eventId: MaybeRef) { error: readonly(error), createResource, updateResource, + deleteResource, reorderResources, refreshEventData, }; diff --git a/frontend/app/composables/mutations/useGroupResourcesMutations.ts b/frontend/app/composables/mutations/useGroupResourcesMutations.ts index 549a569f6..27e47c042 100644 --- a/frontend/app/composables/mutations/useGroupResourcesMutations.ts +++ b/frontend/app/composables/mutations/useGroupResourcesMutations.ts @@ -55,6 +55,26 @@ export function useGroupResourcesMutations(groupId: MaybeRef) { } } + // Delete existing resource. + async function deleteResource(resourceId: string) { + loading.value = true; + error.value = null; + + try { + await deleteGroupResource(resourceId); + + // Invalidate cache and refetch fresh data. + await refreshGroupData(); + + return true; + } catch (err) { + showToastError((err as AppError).message); + return false; + } finally { + loading.value = false; + } + } + // Reorder multiple resource entries. async function reorderResources(resources: Resource[]) { loading.value = true; @@ -89,6 +109,7 @@ export function useGroupResourcesMutations(groupId: MaybeRef) { error: readonly(error), createResource, updateResource, + deleteResource, reorderResources, refreshGroupData, }; diff --git a/frontend/app/composables/mutations/useOrganizationResourcesMutations.ts b/frontend/app/composables/mutations/useOrganizationResourcesMutations.ts index 7dcf23b81..6d208be37 100644 --- a/frontend/app/composables/mutations/useOrganizationResourcesMutations.ts +++ b/frontend/app/composables/mutations/useOrganizationResourcesMutations.ts @@ -1,6 +1,8 @@ // SPDX-License-Identifier: AGPL-3.0-or-later // Mutation composable for FAQ entries - uses direct service calls, not useAsyncData. +import { getKeyForGetOrganization } from "../queries/useGetOrganization"; + export function useOrganizationResourcesMutations( organizationId: MaybeRef ) { @@ -60,6 +62,26 @@ export function useOrganizationResourcesMutations( } } + // Delete existing resource. + async function deleteResource(resourceId: string) { + loading.value = true; + error.value = null; + + try { + await deleteOrganizationResource(resourceId); + + // Invalidate cache and refetch fresh data. + await refreshOrganizationData(); + + return true; + } catch (err) { + showToastError((err as AppError).message); + return false; + } finally { + loading.value = false; + } + } + // Reorder multiple resource entries. async function reorderResources(resources: Resource[]) { loading.value = true; @@ -99,6 +121,7 @@ export function useOrganizationResourcesMutations( error: readonly(error), createResource, updateResource, + deleteResource, reorderResources, refreshOrganizationData, }; diff --git a/frontend/app/services/communities/group/resource.ts b/frontend/app/services/communities/group/resource.ts index 02c43c2bd..69b53386e 100644 --- a/frontend/app/services/communities/group/resource.ts +++ b/frontend/app/services/communities/group/resource.ts @@ -1,5 +1,6 @@ // SPDX-License-Identifier: AGPL-3.0-or-later -import { post, put } from "~/services/http"; + +import { del, post, put } from "~/services/http"; // MARK: Create @@ -34,6 +35,17 @@ export async function updateGroupResource(input: ResourceInput): Promise { } } +// MARK: Delete + +export async function deleteGroupResource(resourceId: string): Promise { + try { + await del(`/communities/group_resources/${resourceId}`); + } catch (e) { + const err = errorHandler(e); + throw err; + } +} + // MARK: Reorder export async function reorderGroupResources( diff --git a/frontend/app/services/communities/organization/resource.ts b/frontend/app/services/communities/organization/resource.ts index 7964a1f69..63ebfc9e1 100644 --- a/frontend/app/services/communities/organization/resource.ts +++ b/frontend/app/services/communities/organization/resource.ts @@ -1,5 +1,6 @@ // SPDX-License-Identifier: AGPL-3.0-or-later -import { post, put } from "~/services/http"; + +import { del, post, put } from "~/services/http"; // MARK: Create @@ -37,6 +38,19 @@ export async function updateOrganizationResource( } } +// MARK: Delete + +export async function deleteOrganizationResource( + resourceId: string +): Promise { + try { + await del(`/communities/organization_resources/${resourceId}`); + } catch (e) { + const err = errorHandler(e); + throw err; + } +} + // MARK: Reorder export async function reorderOrganizationResources( diff --git a/frontend/app/services/event/resource.ts b/frontend/app/services/event/resource.ts index cd537740c..ea67c99b8 100644 --- a/frontend/app/services/event/resource.ts +++ b/frontend/app/services/event/resource.ts @@ -1,5 +1,6 @@ // SPDX-License-Identifier: AGPL-3.0-or-later -import { post, put } from "~/services/http"; + +import { del, post, put } from "~/services/http"; // MARK: Create @@ -37,6 +38,17 @@ export async function updateEventResource( } } +// MARK: Delete + +export async function deleteEventResource(resourceId: string): Promise { + try { + await del(`/events/event_resources/${resourceId}`); + } catch (e) { + const err = errorHandler(e); + throw err; + } +} + // MARK: Reorder export async function reorderEventResources( diff --git a/frontend/i18n/locales/en-US.json b/frontend/i18n/locales/en-US.json index ac6103e6b..2115682f2 100644 --- a/frontend/i18n/locales/en-US.json +++ b/frontend/i18n/locales/en-US.json @@ -146,6 +146,8 @@ "i18n.components.card_metrics_overview.header": "Recent activity", "i18n.components.card_metrics_overview.new_organizations": "New orgs", "i18n.components.card_org_application_vote.downvote_application_aria_label": "Vote to not support the organization joining activist", + "i18n.components.card_resource.confirm_delete": "Delete", + "i18n.components.card_resource.confirm_delete_message": "Are you sure you want to delete this resource? This action cannot be undone.", "i18n.components.card_resource.navigate_to_resource_aria_label": "Navigate to the page for this resource", "i18n.components.card_search_result_entity_event.event_img_alt_text": "The event logo of {entity_name}.", "i18n.components.card_search_result_entity_event.navigate_to_event_aria_label": "Navigate to the page for this event", diff --git a/frontend/test/services/communities/group/resource.spec.ts b/frontend/test/services/communities/group/resource.spec.ts index a28506142..1a137e550 100644 --- a/frontend/test/services/communities/group/resource.spec.ts +++ b/frontend/test/services/communities/group/resource.spec.ts @@ -107,6 +107,25 @@ describe("services/communities/group/resource", () => { }); }); + // MARK: Delete + + it("deleteGroupResource() calls DELETE endpoint", async () => { + const { fetchMock } = getMocks(); + fetchMock.mockResolvedValueOnce({ ok: true }); + await deleteGroupResource("resource-123"); + + expect(fetchMock).toHaveBeenCalledTimes(1); + const [url, opts] = getFetchCall(fetchMock, 0); + expect(url).toContain("/communities/group_resources/resource-123"); + expect(opts.method).toBe("DELETE"); + }); + + it("deleteGroupResource() handles successful deletion", async () => { + const { fetchMock } = getMocks(); + fetchMock.mockResolvedValueOnce({ ok: true }); + await expect(deleteGroupResource("resource-456")).resolves.toBeUndefined(); + }); + // MARK: Error Handling it("propagates AppError on failure", async () => { diff --git a/frontend/test/services/communities/organization/resource.spec.ts b/frontend/test/services/communities/organization/resource.spec.ts index e3dc7faf0..9b5a22979 100644 --- a/frontend/test/services/communities/organization/resource.spec.ts +++ b/frontend/test/services/communities/organization/resource.spec.ts @@ -125,6 +125,27 @@ describe("services/communities/organization/resource", () => { ); }); + // MARK: Delete + + it("deleteOrganizationResource() calls DELETE endpoint", async () => { + const { fetchMock } = getMocks(); + fetchMock.mockResolvedValueOnce({ ok: true }); + await deleteOrganizationResource("resource-123"); + + expect(fetchMock).toHaveBeenCalledTimes(1); + const [url, opts] = getFetchCall(fetchMock, 0); + expect(url).toContain("/communities/organization_resources/resource-123"); + expect(opts.method).toBe("DELETE"); + }); + + it("deleteOrganizationResource() handles successful deletion", async () => { + const { fetchMock } = getMocks(); + fetchMock.mockResolvedValueOnce({ ok: true }); + await expect( + deleteOrganizationResource("resource-456") + ).resolves.toBeUndefined(); + }); + // MARK: Error Handling it("propagates AppError on failure", async () => { diff --git a/frontend/test/services/event/resource.spec.ts b/frontend/test/services/event/resource.spec.ts index cf7d597c6..f96176b81 100644 --- a/frontend/test/services/event/resource.spec.ts +++ b/frontend/test/services/event/resource.spec.ts @@ -95,6 +95,25 @@ describe("services/event/resource", () => { }); }); + // MARK: Delete + + it("deleteEventResource() calls DELETE endpoint", async () => { + const { fetchMock } = getMocks(); + fetchMock.mockResolvedValueOnce({ ok: true }); + await deleteEventResource("resource-123"); + + expect(fetchMock).toHaveBeenCalledTimes(1); + const [url, opts] = getFetchCall(fetchMock, 0); + expect(url).toContain("/events/event_resources/resource-123"); + expect(opts.method).toBe("DELETE"); + }); + + it("deleteEventResource() handles successful deletion", async () => { + const { fetchMock } = getMocks(); + fetchMock.mockResolvedValueOnce({ ok: true }); + await expect(deleteEventResource("resource-456")).resolves.toBeUndefined(); + }); + // MARK: Error Handling it("propagates AppError on failure", async () => { From 08dd1a7ebd87b0e9c4d928c9d078d73b9d770d7f Mon Sep 17 00:00:00 2001 From: "Weblate (bot)" Date: Sun, 23 Nov 2025 14:57:27 +0100 Subject: [PATCH 031/243] Translated using Weblate (Ukrainian) (#1750) Currently translated at 38.1% (251 of 658 strings) Translation: activist/activist Translate-URL: https://hosted.weblate.org/projects/activist/activist/uk/ Co-authored-by: mayumo --- frontend/i18n/locales/uk.json | 49 +++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/frontend/i18n/locales/uk.json b/frontend/i18n/locales/uk.json index e741d1d72..9a19aa67b 100644 --- a/frontend/i18n/locales/uk.json +++ b/frontend/i18n/locales/uk.json @@ -62,8 +62,11 @@ "i18n.components._global.draggable_element": "Перетягуваний елемент сторінки", "i18n.components._global.get_involved": "Долучайтеся", "i18n.components._global.github": "GitHub", + "i18n.components._global.github_aria_label": "Відвідати репозиторій activist на GitHub", "i18n.components._global.instagram": "Instagram", + "i18n.components._global.instagram_aria_label": "Відвідати сторінку activist в Інстаграм", "i18n.components._global.matrix": "Matrix", + "i18n.components._global.participate": "Взяти участь", "i18n.components._global.roadmap": "Дорожня карта", "i18n.components._global.slash_tooltip_label": "Натисніть \"/\" для пошуку", "i18n.components._global.star": "Зірка", @@ -98,7 +101,9 @@ "i18n.components.card_date_picker.all_day": "Увесь день", "i18n.components.card_date_picker.date": "Дата", "i18n.components.card_date_picker.multiple_days": "Кілька днів", + "i18n.components.card_details.header": "Деталі", "i18n.components.card_discussion_input.comment": "Коментар", + "i18n.components.card_discussion_input.enable_markdown_support": "Увімкнути підтримку Markdown", "i18n.components.card_discussion_input.leave_comment": "Залишити публічний коментар", "i18n.components.card_discussion_input.preview": "Попередній перегляд", "i18n.components.card_discussion_input.preview_aria_label": "Попередній перегляд тексту який буде надіслано", @@ -106,9 +111,30 @@ "i18n.components.card_get_involved_organization.view_all_groups": "Переглянути всі групи", "i18n.components.card_get_involved_organization.view_all_groups_aria_label": "Переглянути всі групи в цій організації", "i18n.components.card_search_result_entity_event.view_video": "Переглянути відео", + "i18n.components.dropdown_info.info": "Інформація", "i18n.components.dropdown_theme.dark": "Темна", "i18n.components.dropdown_theme.label": "Тема", "i18n.components.dropdown_theme.light": "Світла", + "i18n.components.dropdown_user_options.sign_out": "Вийти", + "i18n.components.dropdown_user_options.your_events": "Ваші події", + "i18n.components.dropdown_user_options.your_orgs": "Ваші організації", + "i18n.components.dropdown_user_options.your_profile": "Ваш профіль", + "i18n.components.empty_state.create_event": "Створити подію", + "i18n.components.empty_state.create_event_aria_label": "Перейдіть до форми щоб створити нову подію", + "i18n.components.empty_state.create_group_aria_label": "Перейдіть до форми щоб створити нову групу", + "i18n.components.empty_state.create_organization": "Створити організацію", + "i18n.components.empty_state.create_organization_aria_label": "Перейдіть до форми щоб створити нову організацію", + "i18n.components.empty_state.create_resource_aria_label": "Перейдіть до форми щоб створити новий ресурс", + "i18n.components.footer.flex._global.copyright": "Ліцензія: AGPL-3.0; CC BY-SA 4.0.", + "i18n.components.footer.flex._global.powered_by_netlify": "Цей сайт працює на Netlify.", + "i18n.components.footer_website.about_aria_label": "Дізнатися більше про спільноту activist", + "i18n.components.footer_website.documentation_aria_label": "Дослідіть нашу документацію", + "i18n.components.footer_website.privacy_policy": "Політика конфіденційності", + "i18n.components.footer_website.privacy_policy_aria_label": "Ознайомтеся з нашою політикою конфіденційності", + "i18n.components.footer_website.trademark_policy": "Політика щодо торговельних марок", + "i18n.components.footer_website.trademark_policy_aria_label": "Ознайомтеся з нашою політикою щодо торговельних марок", + "i18n.components.footer_website.version_number": "v0.0.1", + "i18n.components.footer_website.version_number_aria_label": "Переглянути релізи на github", "i18n.components.form_faq_entry.answer": "Відповідь", "i18n.components.form_faq_entry.answer_required": "Відповідь необхідна", "i18n.components.form_faq_entry.question": "Питання", @@ -123,12 +149,15 @@ "i18n.components.grid_app_shields.f_droid": "F-Droid", "i18n.components.grid_app_shields.google_play": "Google Play", "i18n.components.image_multiple_file_drop_zone.number_of_files": "Кількість файлів", + "i18n.components.indicator_password_strength.invalid": "некоректний", "i18n.components.indicator_password_strength.medium": "середній", "i18n.components.indicator_password_strength.strong": "сильний", "i18n.components.indicator_password_strength.title": "Надійність пароля", "i18n.components.indicator_password_strength.very_strong": "дуже сильний", "i18n.components.indicator_password_strength.very_weak": "дуже слабкий", "i18n.components.indicator_password_strength.weak": "слабкий", + "i18n.components.landing_splash.request_access": "Запит на доступ", + "i18n.components.landing_splash.request_access_aria_label": "Запит на доступ до activist.org", "i18n.components.modal_qr_code.aria_label": "Відкрити QR-код у новій вкладці", "i18n.components.modal_qr_code.download_qr_code": "Завантажити QR-код", "i18n.components.modal_qr_code.download_qr_code_aria_label": "Завантажити QR-код з посиланням на цю сторінку", @@ -146,6 +175,8 @@ "i18n.components.modal_share_page.messenger": "Messenger", "i18n.components.modal_share_page.other": "Інше", "i18n.components.modal_share_page.signal": "Signal", + "i18n.components.modal_share_page.suggested_mastodon": "Децентралізований мікроблогінг", + "i18n.components.modal_share_page.suggested_matrix": "Децентралізовано та зашифровано", "i18n.components.modal_share_page.telegram": "Telegram", "i18n.components.modal_share_page.twitter": "Twitter", "i18n.components.tooltip_password_requirements.capital_letters": "Містить великі літери", @@ -178,11 +209,29 @@ "i18n.pages._global.tasks.new_task": "Нове завдання", "i18n.pages._global.tasks.new_task_aria_label": "Додати нове завдання на дошку", "i18n.pages._global.tasks.tasks_lower": "завдання", + "i18n.pages._global.team.team_lower": "команда", "i18n.pages._global.terms_of_service_pt_1": "Я прочитав та погоджуюсь з", "i18n.pages._global.terms_of_service_pt_2": "умовами та положеннями активісту", + "i18n.pages.contact.email_placeholder": "приклад{'@'}mail.com", + "i18n.pages.contact.error_empty": "Не може бути порожнім.", + "i18n.pages.events.create.event_type": "Тип", + "i18n.pages.events.create.events_name": "Назва події", + "i18n.pages.events.create.events_name_placeholder": "Назва події", + "i18n.pages.events.create.format": "Формат", + "i18n.pages.events.create.format_placeholder": "Формат події (наприклад, протест, семінар)", + "i18n.pages.events.create.header_1": "Тип події та ролі", + "i18n.pages.events.create.header_2": "Місце та час", "i18n.pages.events.create.link_placeholder": "https://приклад.com", + "i18n.pages.events.create.organizer": "Організатор", + "i18n.pages.events.create.organizer_instructions": "Ви можете вибрати серед усіх організацій у яких ви адміністратор та запросити інших приєднатися пізніше в процесі.", + "i18n.pages.events.create.organizer_placeholder": "Організатор події", + "i18n.pages.events.create.roles": "Ролі", "i18n.pages.events.create.subtext_2": "У цьому розділі ви можете визначити час та місце вашого заходу. Чи це особисто чи онлайн? Чи це регулярна зустріч або разова дія? Встановіть час для вашого заходу щоб учасники знали коли вони можуть взяти участь. Тип локації може бути офлайн, онлайн, або обидві. Щодо локації, дайте учасникам знати де зустрітися для заходу.", "i18n.pages.events.team.tagline": "Організатори події", + "i18n.pages.groups.create.group_name": "Назва групи", + "i18n.pages.groups.create.group_name_placeholder": "Назва групи", + "i18n.pages.groups.create.header": "Створити групу", + "i18n.pages.index.about_us": "Про нас", "i18n.pages.index.title": "Ласкаво просимо", "i18n.pages.index.view_all_supporters": "Переглянути всіх прихильників", "i18n.pages.organizations._global.events_lower": "події", From f0be70c3ea3dc41132866b8b93baa428b5c85779 Mon Sep 17 00:00:00 2001 From: Rohit Yadav Date: Tue, 25 Nov 2025 07:19:10 +0530 Subject: [PATCH 032/243] Break populate_db file into smaller modules (#1728) * break populate_db in small module * moved get topic label to orgs_py * added proper naming for variables * Mark out test files and expand function docstrings --------- Co-authored-by: Andrew Tavis McAllister --- .../core/management/commands/populate_db.py | 807 ++---------------- .../commands/populate_db_utils/__init__.py | 0 .../populate_db_utils/populate_org_events.py | 166 ++++ .../populate_org_group_event.py | 179 ++++ .../populate_db_utils/populate_org_groups.py | 177 ++++ .../populate_db_utils/populate_orgs.py | 174 ++++ backend/requirements-dev.txt | 20 +- backend/requirements.txt | 4 +- 8 files changed, 788 insertions(+), 739 deletions(-) create mode 100644 backend/core/management/commands/populate_db_utils/__init__.py create mode 100644 backend/core/management/commands/populate_db_utils/populate_org_events.py create mode 100644 backend/core/management/commands/populate_db_utils/populate_org_group_event.py create mode 100644 backend/core/management/commands/populate_db_utils/populate_org_groups.py create mode 100644 backend/core/management/commands/populate_db_utils/populate_orgs.py diff --git a/backend/core/management/commands/populate_db.py b/backend/core/management/commands/populate_db.py index 9eb3e25be..3ff053116 100644 --- a/backend/core/management/commands/populate_db.py +++ b/backend/core/management/commands/populate_db.py @@ -7,61 +7,24 @@ import json import random from argparse import ArgumentParser -from typing import Any, List, TypedDict +from typing import Any, TypedDict from django.core.management.base import BaseCommand from typing_extensions import Unpack from authentication.factories import UserFactory from authentication.models import UserModel -from communities.groups.factories import ( - GroupFactory, - GroupFaqFactory, - GroupResourceFactory, - GroupSocialLinkFactory, - GroupTextFactory, -) from communities.groups.models import Group -from communities.organizations.factories import ( - OrganizationFactory, - OrganizationFaqFactory, - OrganizationResourceFactory, - OrganizationSocialLinkFactory, - OrganizationTextFactory, -) from communities.organizations.models import Organization from content.models import Topic -from events.factories import ( - EventFactory, - EventFaqFactory, - EventResourceFactory, - EventSocialLinkFactory, - EventTextFactory, -) from events.models import Event -# MARK: Utils and Types - - -def get_topic_label(topic: Topic) -> str: - """ - Return the label of a topic from the object. +from .populate_db_utils.populate_org_events import create_org_events +from .populate_db_utils.populate_org_group_event import create_group_events +from .populate_db_utils.populate_org_groups import create_org_groups +from .populate_db_utils.populate_orgs import create_organization, get_topic_label - Parameters - ---------- - topic : Topic - The topic object that the label should be derived for. - - Returns - ------- - str - The human readable name of the topic. - """ - return ( - " ".join([t[0] + t[1:].lower() for t in topic.type.split("_")]) - .replace("Womens", "Women's") - .replace("Lgbtqia", "LGBTQIA+") - ) +# MARK: Utils and Types class Options(TypedDict): @@ -124,7 +87,7 @@ def handle(self, *args: str, **options: Unpack[Options]) -> None: num_orgs_per_user = options["orgs_per_user"] num_groups_per_org = options["groups_per_org"] num_events_per_org = options["events_per_org"] - num_events_per_group = options["events_per_org"] + num_events_per_group = options["events_per_group"] num_resources_per_entity = options["resources_per_entity"] num_faq_entries_per_entity = options["faq_entries_per_entity"] @@ -150,7 +113,6 @@ def handle(self, *args: str, **options: Unpack[Options]) -> None: # MARK: Set Data Totals - n_orgs = 0 n_social_links = 0 n_faq_entries = 0 n_resources = 0 @@ -169,711 +131,102 @@ def handle(self, *args: str, **options: Unpack[Options]) -> None: users[0].set_password("password") # ensure password is set users[0].save() - for u, user in enumerate(users): + for user in users: user_topic = random.choice(topics) user.topics.set([user_topic]) user_topic_name = get_topic_label(topic=user_topic) - for o in range(num_orgs_per_user): - # MARK: Org + for _ in range(num_orgs_per_user): + # MARK: Orgs - org_id = ( - assigned_org_fields[n_orgs]["org_name"] - if n_orgs < len(assigned_org_fields) - and "org_name" in assigned_org_fields[n_orgs] - else f"organization_u{u}_o{o}" - ) - org_name = ( - assigned_org_fields[n_orgs]["name"] - if n_orgs < len(assigned_org_fields) - and "name" in assigned_org_fields[n_orgs] - else f"{user_topic_name} Organization" - ) - tagline = ( - assigned_org_fields[n_orgs]["tagline"] - if n_orgs < len(assigned_org_fields) - and "tagline" in assigned_org_fields[n_orgs] - else f"Fighting for {user_topic_name.lower()}" - ) - - user_org = OrganizationFactory( - created_by=user, - org_name=org_id, - name=org_name, - tagline=tagline, + user_org, s_links, resources, faqs, assigned_org_spec = ( + create_organization( + user=user, + user_topic=user_topic, + assigned_org_fields=assigned_org_fields, # pass the list (may be empty) + num_faq_entries_per_entity=num_faq_entries_per_entity, + num_resources_per_entity=num_resources_per_entity, + ) ) - user_org.topics.set([user_topic]) - - # MARK: Org Texts + n_social_links += s_links + n_faq_entries += faqs + n_resources += resources - assigned_org_text_fields = ( - assigned_org_fields[n_orgs]["texts"] - if n_orgs < len(assigned_org_fields) - and "texts" in assigned_org_fields[n_orgs] - else {} - ) + # MARK: Org Events - org_texts = OrganizationTextFactory( - iso="en", primary=True, **assigned_org_text_fields + assigned_events = ( + assigned_org_spec.get("events", []) if assigned_org_spec else [] ) - user_org.texts.set([org_texts]) - - # MARK: Org Links - - org_social_links: List[OrganizationSocialLinkFactory] = [] - - assigned_org_link_fields = [] - if ( - n_orgs < len(assigned_org_fields) - and "social_links" in assigned_org_fields[n_orgs] - ): - assigned_org_link_fields = assigned_org_fields[n_orgs][ - "social_links" - ] - - if assigned_org_link_fields: - org_social_links.extend( - OrganizationSocialLinkFactory(**assigned_org_link_fields[s]) - for s in range(len(assigned_org_link_fields)) + org_events_social_links, org_events_resources, org_events_faqs = ( + create_org_events( + user=user, + user_topic=user_topic, + user_topic_name=user_topic_name, + user_org=user_org, + assigned_events=assigned_events, + num_events_per_org=num_events_per_org, + num_faq_entries_per_entity=num_faq_entries_per_entity, + num_resources_per_entity=num_resources_per_entity, ) + ) - n_social_links += len(assigned_org_link_fields) - - else: - org_social_links.extend( - OrganizationSocialLinkFactory( - label=f"Social Link {s}", order=s - ) - for s in range(3) - ) - - n_social_links += 3 - - user_org.social_links.set(org_social_links) - - # MARK: Org FAQs - - org_faqs: List[OrganizationFaqFactory] = [] - - assigned_org_faq_fields = [] - if ( - n_orgs < len(assigned_org_fields) - and "faqs" in assigned_org_fields[n_orgs] - ): - assigned_org_faq_fields = assigned_org_fields[n_orgs]["faqs"] - - if assigned_org_faq_fields: - org_faqs.extend( - OrganizationFaqFactory( - org=user_org, order=f, **assigned_org_faq_fields[f] - ) - for f in range(len(assigned_org_faq_fields)) - ) + n_social_links += org_events_social_links + n_resources += org_events_resources + n_faq_entries += org_events_faqs - n_faq_entries += len(assigned_org_faq_fields) + # MARK: Org Groups - else: - org_faqs.extend( - OrganizationFaqFactory(org=user_org, order=f) - for f in range(num_faq_entries_per_entity) + assigned_groups = ( + assigned_org_spec.get("groups", []) if assigned_org_spec else [] + ) + groups, groups_social_links, groups_resources, groups_faqs = ( + create_org_groups( + user=user, + user_topic=user_topic, + user_topic_name=user_topic_name, + user_org=user_org, + assigned_groups=assigned_groups, + num_groups_per_org=num_groups_per_org, + num_faq_entries_per_entity=num_faq_entries_per_entity, + num_resources_per_entity=num_resources_per_entity, ) + ) - n_faq_entries += num_faq_entries_per_entity - - user_org.faqs.set(org_faqs) - - # MARK: Org Resources - - assigned_org_resource_fields = [] - if ( - n_orgs < len(assigned_org_fields) - and "resources" in assigned_org_fields[n_orgs] - ): - assigned_org_resource_fields = assigned_org_fields[n_orgs][ - "resources" - ] - - if assigned_org_resource_fields: - for r in range(len(assigned_org_resource_fields)): - user_org.resources.add( - OrganizationResourceFactory( - created_by=user, - org=user_org, - order=r, - **assigned_org_resource_fields[r], - ) - ) - - n_resources += 1 - - else: - for r in range(num_resources_per_entity): - user_org_resource = OrganizationResourceFactory( - created_by=user, org=user_org, order=r - ) - user_org.resources.add(user_org_resource) - user_org_resource.topics.set([user_topic]) - - n_resources += 1 + n_social_links += groups_social_links + n_resources += groups_resources + n_faq_entries += groups_faqs - # MARK: Org Events + # MARK: Org Group Events - assigned_org_event_fields = [] - if ( - n_orgs < len(assigned_org_fields) - and "events" in assigned_org_fields[n_orgs] - ): - assigned_org_event_fields = assigned_org_fields[n_orgs][ - "events" - ] - - for eo in range(num_events_per_org): - if assigned_org_event_fields and eo < len( - assigned_org_event_fields - ): - event_name = assigned_org_event_fields[eo]["name"] - event_tagline = assigned_org_event_fields[eo]["tagline"] - event_type = assigned_org_event_fields[eo]["type"] - user_org_event = EventFactory( - name=event_name, - tagline=event_tagline, - type=event_type, - created_by=user, - orgs=user_org, - groups=None, - ) - - else: - event_type = random.choice(["learn", "action"]) - event_type_verb = ( - "Learning about" - if event_type == "learn" - else "Fighting for" - ) - - user_org_event = EventFactory( - name=f"{user_topic_name} Event [u{u}:o{o}:e{eo}]", - tagline=f"{event_type_verb} {user_topic_name}", - type=event_type, - created_by=user, - orgs=user_org, - groups=None, - ) - - user_org_event.topics.set([user_topic]) - - # MARK: Org Event Texts - - assigned_org_event_text_fields = ( - assigned_org_event_fields[eo]["texts"] - if n_orgs < len(assigned_org_fields) - and "events" in assigned_org_fields[n_orgs] - and eo < len(assigned_org_event_fields) - and "texts" in assigned_org_event_fields[eo] + for g_index, user_org_group in enumerate(groups): + group_spec = ( + assigned_groups[g_index] + if g_index < len(assigned_groups) else {} ) - - event_texts = EventTextFactory( - iso="en", primary=True, **assigned_org_event_text_fields + assigned_group_events = ( + group_spec.get("events", []) if group_spec else [] ) - user_org_event.texts.set([event_texts]) - - # MARK: Org Event Links - - org_event_social_links: List[EventSocialLinkFactory] = [] - - assigned_org_event_link_fields = [] - if ( - n_orgs < len(assigned_org_fields) - and "events" in assigned_org_fields[n_orgs] - and eo < len(assigned_org_event_fields) - and "social_links" in assigned_org_event_fields[eo] - ): - assigned_org_event_link_fields = assigned_org_fields[ - n_orgs - ]["events"][eo]["social_links"] - - if assigned_org_event_link_fields: - org_event_social_links.extend( - EventSocialLinkFactory( - **assigned_org_event_link_fields[s] - ) - for s in range(len(assigned_org_event_link_fields)) - ) - - n_social_links += len(assigned_org_event_link_fields) - - else: - org_event_social_links.extend( - EventSocialLinkFactory( - label=f"Social Link {s}", order=s - ) - for s in range(3) - ) - - n_social_links += 3 - - user_org_event.social_links.set(org_event_social_links) - - # MARK: Org Event FAQs - - user_org_event_faqs: List[EventFaqFactory] = [] - - assigned_org_event_faq_fields = [] - if ( - n_orgs < len(assigned_org_fields) - and "events" in assigned_org_fields[n_orgs] - and eo < len(assigned_org_event_fields) - and "faqs" in assigned_org_event_fields[eo] - ): - assigned_org_event_faq_fields = assigned_org_fields[n_orgs][ - "events" - ][eo]["faqs"] - - if assigned_org_event_faq_fields: - user_org_event_faqs.extend( - EventFaqFactory( - event=user_org_event, - order=f, - **assigned_org_event_faq_fields[f], - ) - for f in range(len(assigned_org_event_faq_fields)) - ) - - n_faq_entries += len(assigned_org_event_faq_fields) - - else: - user_org_event_faqs.extend( - EventFaqFactory(event=user_org_event, order=f) - for f in range(num_faq_entries_per_entity) - ) - - n_faq_entries += num_faq_entries_per_entity - - user_org_event.faqs.set(user_org_event_faqs) - - # MARK: Org Event Resources - - assigned_org_event_resource_fields = [] - if ( - n_orgs < len(assigned_org_fields) - and "events" in assigned_org_fields[n_orgs] - and eo < len(assigned_org_event_fields) - and "resources" in assigned_org_event_fields[eo] - ): - assigned_org_event_resource_fields = assigned_org_fields[ - n_orgs - ]["events"][eo]["resources"] - - if assigned_org_event_resource_fields: - for r in range(len(assigned_org_event_resource_fields)): - user_org_event.resources.add( - EventResourceFactory( - created_by=user, - event=user_org_event, - order=r, - **assigned_org_event_resource_fields[r], - ) - ) - - n_resources += 1 - - else: - for r in range(num_resources_per_entity): - user_org_event_resource = EventResourceFactory( - created_by=user, event=user_org_event, order=r - ) - user_org_event.resources.add(user_org_event_resource) - user_org_event_resource.topics.set([user_topic]) - - n_resources += 1 - - # MARK: Org Groups - - assigned_org_group_fields = [] - if ( - n_orgs < len(assigned_org_fields) - and "groups" in assigned_org_fields[n_orgs] - ): - assigned_org_group_fields = assigned_org_fields[n_orgs][ - "groups" - ] - - for g in range(num_groups_per_org): - group_id = ( - assigned_org_group_fields[g]["group_name"] - if g < len(assigned_org_group_fields) - and "group_name" in assigned_org_group_fields[g] - else f"org_u{u}_o{o}:g{g}" - ) - group_name = ( - assigned_org_group_fields[g]["name"] - if g < len(assigned_org_group_fields) - and "name" in assigned_org_group_fields[g] - else f"{user_topic_name} Group {g}" - ) - tagline = ( - assigned_org_group_fields[g]["tagline"] - if g < len(assigned_org_group_fields) - and "tagline" in assigned_org_group_fields[g] - else f"Fighting for {user_topic_name.lower()}" + ( + group_events_social_links, + group_events_resources, + group_events_faqs, + ) = create_group_events( + user=user, + user_topic=user_topic, + user_topic_name=user_topic_name, + user_org=user_org, + user_org_group=user_org_group, + assigned_group_events=assigned_group_events, + num_events_per_group=num_events_per_group, + num_faq_entries_per_entity=num_faq_entries_per_entity, + num_resources_per_entity=num_resources_per_entity, ) - user_org_group = GroupFactory( - created_by=user, - group_name=group_id, - name=group_name, - org=user_org, - tagline=tagline, - ) - - # MARK: Org Group Texts - - assigned_org_group_text_fields = ( - assigned_org_group_fields[g]["texts"] - if n_orgs < len(assigned_org_fields) - and "groups" in assigned_org_fields[n_orgs] - and g < len(assigned_org_group_fields) - and "texts" in assigned_org_group_fields[g] - else {} - ) - - group_texts = GroupTextFactory( - iso="en", primary=True, **assigned_org_group_text_fields - ) - user_org_group.texts.set([group_texts]) - - # MARK: Org Group Links - - org_group_social_links: List[GroupSocialLinkFactory] = [] - - assigned_org_group_link_fields = [] - if ( - n_orgs < len(assigned_org_fields) - and "groups" in assigned_org_fields[n_orgs] - and g < len(assigned_org_group_fields) - and "social_links" in assigned_org_group_fields[g] - ): - assigned_org_group_link_fields = assigned_org_fields[ - n_orgs - ]["groups"][g]["social_links"] - - if assigned_org_group_link_fields: - org_group_social_links.extend( - GroupSocialLinkFactory( - **assigned_org_group_link_fields[s] - ) - for s in range(len(assigned_org_group_link_fields)) - ) - - n_social_links += len(assigned_org_group_link_fields) - - else: - org_group_social_links.extend( - GroupSocialLinkFactory( - label=f"Social Link {s}", order=s - ) - for s in range(3) - ) - - n_social_links += 3 - - user_org_group.social_links.set(org_group_social_links) - - # MARK: Org Group FAQs - - user_org_group_faqs: List[GroupFaqFactory] = [] - - assigned_org_group_faq_fields = [] - if ( - n_orgs < len(assigned_org_fields) - and "groups" in assigned_org_fields[n_orgs] - and g < len(assigned_org_group_fields) - and "faqs" in assigned_org_group_fields[g] - ): - assigned_org_group_faq_fields = assigned_org_fields[n_orgs][ - "groups" - ][g]["faqs"] - - if assigned_org_group_faq_fields: - user_org_group_faqs.extend( - GroupFaqFactory( - group=user_org_group, - order=f, - **assigned_org_group_faq_fields[f], - ) - for f in range(len(assigned_org_group_faq_fields)) - ) - - n_faq_entries += len(assigned_org_group_faq_fields) - - else: - user_org_group_faqs.extend( - GroupFaqFactory(group=user_org_group, order=f) - for f in range(num_faq_entries_per_entity) - ) - - n_faq_entries += num_faq_entries_per_entity - - user_org_group.faqs.set(user_org_group_faqs) - - # MARK: Org Group Resources - - assigned_org_group_resource_fields = [] - if ( - n_orgs < len(assigned_org_fields) - and "groups" in assigned_org_fields[n_orgs] - and g < len(assigned_org_group_fields) - and "resources" in assigned_org_group_fields[g] - ): - assigned_org_group_resource_fields = assigned_org_fields[ - n_orgs - ]["groups"][g]["resources"] - - if assigned_org_group_resource_fields: - for r in range(len(assigned_org_group_resource_fields)): - user_org_group.resources.add( - GroupResourceFactory( - created_by=user, - group=user_org_group, - order=r, - **assigned_org_group_resource_fields[r], - ) - ) - - n_resources += 1 - - else: - for r in range(num_resources_per_entity): - user_org_group_resource = GroupResourceFactory( - created_by=user, group=user_org_group, order=r - ) - user_org_group.resources.add(user_org_group_resource) - user_org_group_resource.topics.set([user_topic]) - - n_resources += 1 - - # MARK: Org Group Events - - assigned_org_group_event_fields = [] - if ( - n_orgs < len(assigned_org_fields) - and "groups" in assigned_org_fields[n_orgs] - and g < len(assigned_org_fields[n_orgs]["groups"]) - and "events" in assigned_org_fields[n_orgs]["groups"][g] - ): - assigned_org_group_event_fields = assigned_org_fields[ - n_orgs - ]["groups"][g]["events"] - - for eg in range(num_events_per_group): - if assigned_org_group_event_fields and eg < len( - assigned_org_group_event_fields - ): - org_group_event_name = assigned_org_group_event_fields[ - eg - ]["name"] - org_group_event_tagline = ( - assigned_org_group_event_fields[eg]["tagline"] - ) - org_group_event_type = assigned_org_group_event_fields[ - eg - ]["type"] - user_org_group_event = EventFactory( - name=org_group_event_name, - tagline=org_group_event_tagline, - type=org_group_event_type, - created_by=user, - orgs=user_org, - groups=user_org_group, - ) - - else: - event_type = random.choice(["learn", "action"]) - event_type_verb = ( - "Learning about" - if event_type == "learn" - else "Fighting for" - ) - - user_org_group_event = EventFactory( - name=f"{user_topic_name} Event [u{u}:o{o}:g{g}:e{eg}]", - tagline=f"{event_type_verb} {user_topic_name}", - type=event_type, - created_by=user, - orgs=user_org, - groups=user_org_group, - ) - - user_org_group_event.topics.set([user_topic]) - - # MARK: Org Group Event Texts - - assigned_org_group_event_text_fields = ( - assigned_org_group_event_fields[eg]["texts"] - if n_orgs < len(assigned_org_fields) - and "groups" in assigned_org_fields[n_orgs] - and g < len(assigned_org_fields[n_orgs]["groups"]) - and "events" in assigned_org_fields[n_orgs]["groups"][g] - and eg < len(assigned_org_group_event_fields) - and "texts" in assigned_org_group_event_fields[eg] - else {} - ) - - event_texts = EventTextFactory( - iso="en", - primary=True, - **assigned_org_group_event_text_fields, - ) - user_org_group_event.texts.set([event_texts]) - - # MARK: Org Group Event Links - - org_group_event_social_links: List[ - EventSocialLinkFactory - ] = [] - - assigned_org_group_event_link_fields = [] - if ( - n_orgs < len(assigned_org_fields) - and "groups" in assigned_org_fields[n_orgs] - and g < len(assigned_org_fields[n_orgs]["groups"]) - and "events" in assigned_org_fields[n_orgs]["groups"][g] - and eg < len(assigned_org_group_event_fields) - and "social_links" - in assigned_org_group_event_fields[eg] - ): - assigned_org_group_event_link_fields = ( - assigned_org_fields[n_orgs]["events"][eg][ - "social_links" - ] - ) - - if assigned_org_group_event_link_fields: - org_group_event_social_links.extend( - EventSocialLinkFactory( - **assigned_org_group_event_link_fields[s] - ) - for s in range( - len(assigned_org_group_event_link_fields) - ) - ) - - n_social_links += len( - assigned_org_group_event_link_fields - ) - - else: - org_group_event_social_links.extend( - EventSocialLinkFactory( - label=f"Social Link {s}", order=s - ) - for s in range(3) - ) - - n_social_links += 3 - - user_org_group_event.social_links.set( - org_group_event_social_links - ) - - # MARK: Org Group Event FAQs - - user_org_group_event_faqs: List[EventFaqFactory] = [] - - assigned_org_group_event_faq_fields = [] - if ( - n_orgs < len(assigned_org_fields) - and "groups" in assigned_org_fields[n_orgs] - and g < len(assigned_org_fields[n_orgs]["groups"]) - and "events" in assigned_org_fields[n_orgs]["groups"][g] - and eg < len(assigned_org_group_event_fields) - and "faqs" in assigned_org_group_event_fields[eg] - ): - assigned_org_group_event_faq_fields = ( - assigned_org_fields[n_orgs]["events"][eg]["faqs"] - ) - - if assigned_org_group_event_faq_fields: - user_org_group_event_faqs.extend( - EventFaqFactory( - event=user_org_group_event, - order=f, - **assigned_org_group_event_faq_fields[f], - ) - for f in range( - len(assigned_org_group_event_faq_fields) - ) - ) - - n_faq_entries += len( - assigned_org_group_event_faq_fields - ) - - else: - user_org_group_event_faqs.extend( - EventFaqFactory(event=user_org_group_event, order=f) - for f in range(num_faq_entries_per_entity) - ) - - n_faq_entries += num_faq_entries_per_entity - - user_org_group_event.faqs.set(user_org_group_event_faqs) - - # MARK: Org Group Event Resources - - assigned_org_group_event_resource_fields = [] - if ( - n_orgs < len(assigned_org_fields) - and "groups" in assigned_org_fields[n_orgs] - and g < len(assigned_org_fields[n_orgs]["groups"]) - and "events" in assigned_org_fields[n_orgs]["groups"][g] - and eg < len(assigned_org_group_event_fields) - and "resources" in assigned_org_group_event_fields[eg] - ): - assigned_org_group_event_resource_fields = ( - assigned_org_fields[n_orgs]["events"][eg][ - "resources" - ] - ) - - if assigned_org_group_event_resource_fields: - for r in range( - len(assigned_org_group_event_resource_fields) - ): - user_org_group_event.resources.add( - EventResourceFactory( - created_by=user, - event=user_org_group_event, - order=r, - **assigned_org_group_event_resource_fields[ - r - ], - ) - ) - - n_resources += 1 - - else: - for r in range(num_resources_per_entity): - user_org_group_event_resource = ( - EventResourceFactory( - created_by=user, - event=user_org_group_event, - order=r, - ) - ) - user_org_group_event.resources.add( - user_org_group_event_resource - ) - user_org_group_event_resource.topics.set( - [user_topic] - ) - - n_resources += 1 - - n_orgs += 1 + n_social_links += group_events_social_links + n_resources += group_events_resources + n_faq_entries += group_events_faqs # MARK: Print Output diff --git a/backend/core/management/commands/populate_db_utils/__init__.py b/backend/core/management/commands/populate_db_utils/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/backend/core/management/commands/populate_db_utils/populate_org_events.py b/backend/core/management/commands/populate_db_utils/populate_org_events.py new file mode 100644 index 000000000..2149ee85c --- /dev/null +++ b/backend/core/management/commands/populate_db_utils/populate_org_events.py @@ -0,0 +1,166 @@ +# SPDX-License-Identifier: AGPL-3.0-or-later +""" +Populate the database with events for organizations. +""" + +# mypy: ignore-errors +import random +from typing import Any, Dict, List, Tuple + +from authentication.models import UserModel +from communities.organizations.models import Organization +from content.models import Topic +from events.factories import ( + EventFactory, + EventFaqFactory, + EventResourceFactory, + EventSocialLinkFactory, + EventTextFactory, +) + + +def create_org_events( + *, + user: UserModel, + user_topic: Topic, + user_topic_name: str, + user_org: Organization, + assigned_events: List[Dict[str, Any]], + num_events_per_org: int, + num_faq_entries_per_entity: int, + num_resources_per_entity: int, +) -> Tuple[int, int, int]: + """ + Create organization-level events for `user_org`. + + Parameters + ---------- + user : UserModel + The user for which entities are being generated for. + + user_topic : Topic + The topic of the user. + + user_topic_name : str + The name of the user's topic. + + user_org : Organization + The organization for which events are being generated for. + + assigned_events : List[Dict[str, Any]] + The data to assign to the generated events. + + num_events_per_org : int + The number of events that should be assigned to each organization from populate_db. + + num_faq_entries_per_entity : int + The number of FAQ entries that should be assigned to each entity from populate_db. + + num_resources_per_entity : int + The number of resources that should be assigned to each entity from populate_db. + + Returns + ------- + Tuple[int, int, int] + The number of social links, resources and faq entries created for organization events. + """ + n_social = 0 + n_resources = 0 + n_faq = 0 + + for eo in range(num_events_per_org): + spec = ( + assigned_events[eo] + if (assigned_events and eo < len(assigned_events)) + else None + ) + + if spec: + user_org_event = EventFactory( + name=spec["name"], + tagline=spec["tagline"], + type=spec["type"], + created_by=user, + orgs=user_org, + groups=None, + ) + + else: + event_type = random.choice(["learn", "action"]) + verb = "Learning about" if event_type == "learn" else "Fighting for" + user_org_event = EventFactory( + name=f"{user_topic_name} Event", + tagline=f"{verb} {user_topic_name}", + type=event_type, + created_by=user, + orgs=user_org, + groups=None, + ) + + # MARK: Topics + + user_org_event.topics.set([user_topic]) + + # MARK: Texts + + texts = spec.get("texts", {}) if spec else {} + event_texts = EventTextFactory(iso="en", primary=True, **texts) + user_org_event.texts.set([event_texts]) + + # MARK: Social Links + + links_spec = spec.get("social_links", []) if spec else [] + if links_spec: + links = [EventSocialLinkFactory(**link) for link in links_spec] + n_social += len(links_spec) + + else: + links = [ + EventSocialLinkFactory(label=f"Social Link {s}", order=s) + for s in range(3) + ] + n_social += 3 + + user_org_event.social_links.set(links) + + # MARK: FAQs + + faqs_spec = spec.get("faqs", []) if spec else [] + if faqs_spec: + faqs = [ + EventFaqFactory(event=user_org_event, order=i, **faqs_spec[i]) + for i in range(len(faqs_spec)) + ] + n_faq += len(faqs_spec) + + else: + faqs = [ + EventFaqFactory(event=user_org_event, order=i) + for i in range(num_faq_entries_per_entity) + ] + n_faq += num_faq_entries_per_entity + + user_org_event.faqs.set(faqs) + + # MARK: Resources + + res_spec = spec.get("resources", []) if spec else [] + if res_spec: + for i in range(len(res_spec)): + user_org_event.resources.add( + EventResourceFactory( + created_by=user, event=user_org_event, order=i, **res_spec[i] + ) + ) + n_resources += 1 + + else: + for i in range(num_resources_per_entity): + res = EventResourceFactory( + created_by=user, event=user_org_event, order=i + ) + user_org_event.resources.add(res) + res.topics.set([user_topic]) + n_resources += 1 + + return n_social, n_resources, n_faq diff --git a/backend/core/management/commands/populate_db_utils/populate_org_group_event.py b/backend/core/management/commands/populate_db_utils/populate_org_group_event.py new file mode 100644 index 000000000..9b0f7d8fa --- /dev/null +++ b/backend/core/management/commands/populate_db_utils/populate_org_group_event.py @@ -0,0 +1,179 @@ +# SPDX-License-Identifier: AGPL-3.0-or-later +""" +Populate the database with events for organization groups. +""" + +# mypy: ignore-errors +import contextlib +import random +from typing import Any, Dict, List, Tuple + +from authentication.models import UserModel +from communities.groups.models import Group +from communities.organizations.models import Organization +from content.models import Topic +from events.factories import ( + EventFactory, + EventFaqFactory, + EventResourceFactory, + EventSocialLinkFactory, + EventTextFactory, +) + + +def create_group_events( + *, + user: UserModel, + user_topic: Topic, + user_topic_name: str, + user_org: Organization, + user_org_group: Group, + assigned_group_events: List[Dict[str, Any]], + num_events_per_group: int, + num_faq_entries_per_entity: int, + num_resources_per_entity: int, +) -> Tuple[int, int, int]: + """ + Create group-level events for `user_org_group` (belonging to `user_org`). + + Parameters + ---------- + user : UserModel + The user for which entities are being generated for. + + user_topic : Topic + The topic of the user. + + user_topic_name : str + The name of the user's topic. + + user_org : Organization + The organization for which events are being generated for. + + user_org_group : Group + The group that should be created for the organization. + + assigned_group_events : List[Dict[str, Any]] + The data to assign to the generated events. + + num_events_per_group : int + The number of events that should be assigned to each group from populate_db. + + num_faq_entries_per_entity : int + The number of FAQ entries that should be assigned to each entity from populate_db. + + num_resources_per_entity : int + The number of resources that should be assigned to each entity from populate_db. + + Returns + ------- + Tuple[int, int, int] + The number of social links, resources and faq entries created for organization group events. + """ + n_social = 0 + n_resources = 0 + n_faq = 0 + + for eg in range(num_events_per_group): + spec = ( + assigned_group_events[eg] + if (assigned_group_events and eg < len(assigned_group_events)) + else None + ) + + if spec: + user_org_group_event = EventFactory( + name=spec["name"], + tagline=spec["tagline"], + type=spec["type"], + created_by=user, + orgs=user_org, + groups=user_org_group, + ) + + else: + event_type = random.choice(["learn", "action"]) + event_type_verb = ( + "Learning about" if event_type == "learn" else "Fighting for" + ) + user_org_group_event = EventFactory( + name=f"{user_topic_name} Event", + tagline=f"{event_type_verb} {user_topic_name}", + type=event_type, + created_by=user, + orgs=user_org, + groups=user_org_group, + ) + + # MARK: Topics + + user_org_group_event.topics.set([user_topic]) + + # MARK: Texts + + texts = spec.get("texts", {}) if spec else {} + event_texts = EventTextFactory(iso="en", primary=True, **texts) + user_org_group_event.texts.set([event_texts]) + + # MARK: Social Links + + links_spec = spec.get("social_links", []) if spec else [] + if links_spec: + links = [EventSocialLinkFactory(**link) for link in links_spec] + n_social += len(links_spec) + + else: + links = [ + EventSocialLinkFactory(label=f"Social Link {s}", order=s) + for s in range(3) + ] + n_social += 3 + + user_org_group_event.social_links.set(links) + + # MARK: FAQs + faqs_spec = spec.get("faqs", []) if spec else [] + if faqs_spec: + faqs = [ + EventFaqFactory(event=user_org_group_event, order=i, **faqs_spec[i]) + for i in range(len(faqs_spec)) + ] + n_faq += len(faqs_spec) + + else: + faqs = [ + EventFaqFactory(event=user_org_group_event, order=i) + for i in range(num_faq_entries_per_entity) + ] + n_faq += num_faq_entries_per_entity + + user_org_group_event.faqs.set(faqs) + + # MARK: Resources + + resources_spec = spec.get("resources", []) if spec else [] + if resources_spec: + for i in range(len(resources_spec)): + user_org_group_event.resources.add( + EventResourceFactory( + created_by=user, + event=user_org_group_event, + order=i, + **resources_spec[i], + ) + ) + n_resources += 1 + + else: + for i in range(num_resources_per_entity): + res = EventResourceFactory( + created_by=user, event=user_org_group_event, order=i + ) + user_org_group_event.resources.add(res) + + with contextlib.suppress(Exception): + res.topics.set([user_topic]) + + n_resources += 1 + + return n_social, n_resources, n_faq diff --git a/backend/core/management/commands/populate_db_utils/populate_org_groups.py b/backend/core/management/commands/populate_db_utils/populate_org_groups.py new file mode 100644 index 000000000..0f3fedf42 --- /dev/null +++ b/backend/core/management/commands/populate_db_utils/populate_org_groups.py @@ -0,0 +1,177 @@ +# SPDX-License-Identifier: AGPL-3.0-or-later +""" +Populate the database with groups for organizations. +""" + +# mypy: ignore-errors +import contextlib +from typing import Any, Dict, List, Tuple + +from authentication.models import UserModel +from communities.groups.factories import ( + GroupFactory, + GroupFaqFactory, + GroupResourceFactory, + GroupSocialLinkFactory, + GroupTextFactory, +) +from communities.groups.models import Group +from communities.organizations.models import Organization +from content.models import Topic + + +def create_org_groups( + *, + user: UserModel, + user_topic: Topic, + user_topic_name: str, + user_org: Organization, + assigned_groups: List[Dict[str, Any]], + num_groups_per_org: int, + num_faq_entries_per_entity: int, + num_resources_per_entity: int, +) -> Tuple[List[Group], int, int, int]: + """ + Create groups for `user_org`. + + Parameters + ---------- + user : UserModel + The user for which entities are being generated for. + + user_topic : Topic + The topic of the user. + + user_topic_name : str + The name of the user's topic. + + user_org : Organization + The organization for which events are being generated for. + + assigned_groups : List[Dict[str, Any]] + The per-org 'groups' list from the org spec (may be []). + + - If not empty, this function will consume (pop) the first item and use values from it where provided. + - If empty, deterministic fallback names are used. + + num_groups_per_org : int + The number of groups that should be assigned to each organization from populate_db. + + num_faq_entries_per_entity : int + The number of FAQ entries that should be assigned to each entity from populate_db. + + num_resources_per_entity : int + The number of resources that should be assigned to each entity from populate_db. + + Returns + ------- + Tuple[List[Group], int, int, int] + A list of groups created along with the number of social links, resources and faq entries created for them. + """ + groups: List[Group] = [] + n_social = 0 + n_resources = 0 + n_faq = 0 + + for g in range(num_groups_per_org): + spec = ( + assigned_groups[g] + if (assigned_groups and g < len(assigned_groups)) + else None + ) + + group_id = ( + spec.get("group_name") + if spec and "group_name" in spec + else f"{user_org.org_name}:g{g}" + ) + group_name = ( + spec.get("name") + if spec and "name" in spec + else f"{user_topic_name} Group {g}" + ) + tagline = ( + spec.get("tagline") + if spec and "tagline" in spec + else f"Fighting for {user_topic_name.lower()}" + ) + + user_org_group = GroupFactory( + created_by=user, + group_name=group_id, + name=group_name, + org=user_org, + tagline=tagline, + ) + + # MARK: Texts + + texts_spec = spec.get("texts", {}) if spec else {} + group_texts = GroupTextFactory(iso="en", primary=True, **texts_spec) + user_org_group.texts.set([group_texts]) + + # MARK: Social Links + + links_spec = spec.get("social_links", []) if spec else [] + if links_spec: + links = [GroupSocialLinkFactory(**link) for link in links_spec] + n_social += len(links_spec) + + else: + links = [ + GroupSocialLinkFactory(label=f"Social Link {s}", order=s) + for s in range(3) + ] + n_social += 3 + + user_org_group.social_links.set(links) + + # MARK: FAQs + + faqs_spec = spec.get("faqs", []) if spec else [] + if faqs_spec: + faqs = [ + GroupFaqFactory(group=user_org_group, order=i, **faqs_spec[i]) + for i in range(len(faqs_spec)) + ] + n_faq += len(faqs_spec) + + else: + faqs = [ + GroupFaqFactory(group=user_org_group, order=i) + for i in range(num_faq_entries_per_entity) + ] + n_faq += num_faq_entries_per_entity + + user_org_group.faqs.set(faqs) + + # MARK: Resources + + resources_spec = spec.get("resources", []) if spec else [] + if resources_spec: + for i in range(len(resources_spec)): + user_org_group.resources.add( + GroupResourceFactory( + created_by=user, + group=user_org_group, + order=i, + **resources_spec[i], + ) + ) + n_resources += 1 + + else: + for i in range(num_resources_per_entity): + res = GroupResourceFactory( + created_by=user, group=user_org_group, order=i + ) + user_org_group.resources.add(res) + + with contextlib.suppress(Exception): + res.topics.set([user_topic]) + + n_resources += 1 + + groups.append(user_org_group) + + return groups, n_social, n_resources, n_faq diff --git a/backend/core/management/commands/populate_db_utils/populate_orgs.py b/backend/core/management/commands/populate_db_utils/populate_orgs.py new file mode 100644 index 000000000..3b8e9d23a --- /dev/null +++ b/backend/core/management/commands/populate_db_utils/populate_orgs.py @@ -0,0 +1,174 @@ +# SPDX-License-Identifier: AGPL-3.0-or-later +""" +Populate the database with organizations. +""" + +# mypy: ignore-errors +import contextlib +from typing import Any, Dict, List, Tuple + +from authentication.models import UserModel +from communities.organizations.factories import ( + OrganizationFactory, + OrganizationFaqFactory, + OrganizationResourceFactory, + OrganizationSocialLinkFactory, + OrganizationTextFactory, +) +from communities.organizations.models import Organization +from content.models import Topic + + +def get_topic_label(topic: Topic) -> str: + """ + Return the label of a topic from the object. + + Parameters + ---------- + topic : Topic + The topic object that the label should be derived for. + + Returns + ------- + str + The human readable name of the topic. + """ + return ( + " ".join([t[0] + t[1:].lower() for t in topic.type.split("_")]) + .replace("Womens", "Women's") + .replace("Lgbtqia", "LGBTQIA+") + ) + + +def create_organization( + user: UserModel, + user_topic: Topic, + assigned_org_fields: List[Dict[str, Any]], + num_faq_entries_per_entity: int, + num_resources_per_entity: int, +) -> Tuple[Organization, int, int, int, Dict[str, Any]]: + """ + Create one organization for `user`. + + Parameters + ---------- + user : UserModel + The user for which entities are being generated for. + + user_topic : Topic + The topic of the user. + + assigned_org_fields : List[Dict[str, Any]] + The data to assign to the generated organization. + + - If not empty, this function will consume (pop) the first item and use values from it where provided. + - If empty, deterministic fallback names are used. + + num_faq_entries_per_entity : int + The number of FAQ entries that should be assigned to each entity from populate_db. + + num_resources_per_entity : int + The number of resources that should be assigned to each entity from populate_db. + + Returns + ------- + Tuple[Organization, int, int, int, Dict[str, Any]] + The organization created along with the number of social links, resources and faq entries created for it. + """ + # Consume assigned fields if present (caller can pass the global list). + assigned = assigned_org_fields.pop(0) if assigned_org_fields else {} + + # Determine per-user org index (used for stable fallback naming). + per_user_org_index = Organization.objects.filter(created_by=user).count() + + user_topic_name = get_topic_label(user_topic) + + # Basic fields with fallbacks. + org_id = ( + assigned.get("org_name") + or f"organization_{user.username}_o{per_user_org_index}" + ) + org_name = assigned.get("name") or f"{user_topic_name} Organization" + tagline = assigned.get("tagline") or f"Fighting for {user_topic_name.lower()}" + + user_org = OrganizationFactory( + created_by=user, + org_name=org_id, + name=org_name, + tagline=tagline, + ) + user_org.topics.set([user_topic]) + + # MARK: Texts + + assigned_texts = assigned.get("texts", {}) + org_texts = OrganizationTextFactory(iso="en", primary=True, **assigned_texts) + user_org.texts.set([org_texts]) + + # MARK: Social Links + + n_social_links = 0 + assigned_links = assigned.get("social_links", []) + if assigned_links: + social_objs = [ + OrganizationSocialLinkFactory(**assigned_links[i]) + for i in range(len(assigned_links)) + ] + n_social_links += len(assigned_links) + + else: + social_objs = [ + OrganizationSocialLinkFactory(label=f"Social Link {s}", order=s) + for s in range(3) + ] + n_social_links += 3 + + user_org.social_links.set(social_objs) + + # MARK: FAQs + + n_faq_entries = 0 + assigned_faqs = assigned.get("faqs", []) + if assigned_faqs: + faq_objs = [ + OrganizationFaqFactory(org=user_org, order=i, **assigned_faqs[i]) + for i in range(len(assigned_faqs)) + ] + n_faq_entries += len(assigned_faqs) + + else: + faq_objs = [ + OrganizationFaqFactory(org=user_org, order=i) + for i in range(num_faq_entries_per_entity) + ] + n_faq_entries += num_faq_entries_per_entity + + user_org.faqs.set(faq_objs) + + # MARK: Resources + + n_resources = 0 + assigned_resources = assigned.get("resources", []) + if assigned_resources: + for i in range(len(assigned_resources)): + user_org.resources.add( + OrganizationResourceFactory( + created_by=user, + org=user_org, + order=i, + **assigned_resources[i], + ) + ) + n_resources += 1 + + else: + for i in range(num_resources_per_entity): + res = OrganizationResourceFactory(created_by=user, org=user_org, order=i) + user_org.resources.add(res) + + with contextlib.suppress(Exception): + res.topics.set([user_topic]) + + n_resources += 1 + + return user_org, n_social_links, n_resources, n_faq_entries, assigned diff --git a/backend/requirements-dev.txt b/backend/requirements-dev.txt index b8dd95663..ec04aaf20 100644 --- a/backend/requirements-dev.txt +++ b/backend/requirements-dev.txt @@ -4,7 +4,7 @@ # # pip-compile requirements-dev.in # -asgiref==3.10.0 +asgiref==3.11.0 # via # -r requirements.txt # django @@ -21,16 +21,16 @@ certifi==2025.11.12 # -r requirements.txt # i18n-check # requests -cfgv==3.4.0 +cfgv==3.5.0 # via pre-commit charset-normalizer==3.4.4 # via # -r requirements.txt # i18n-check # requests -click==8.3.0 +click==8.3.1 # via pip-tools -coverage[toml]==7.11.3 +coverage[toml]==7.12.0 # via pytest-cov distlib==0.4.0 # via virtualenv @@ -81,13 +81,13 @@ drf-spectacular==0.29.0 # via -r requirements.txt factory-boy==3.3.3 # via -r requirements-dev.in -faker==38.0.0 +faker==38.2.0 # via factory-boy filelock==3.20.0 # via virtualenv gunicorn==23.0.0 # via -r requirements.txt -i18n-check==1.14.0 +i18n-check==1.15.2 # via -r requirements-dev.in icalendar==6.3.2 # via @@ -147,7 +147,7 @@ pluggy==1.6.0 # via # pytest # pytest-cov -pre-commit==4.4.0 +pre-commit==4.5.0 # via -r requirements-dev.in psycopg2-binary==2.9.11 # via -r requirements.txt @@ -198,12 +198,12 @@ requests==2.32.5 # i18n-check rich==14.2.0 # via i18n-check -rpds-py==0.28.0 +rpds-py==0.29.0 # via # -r requirements.txt # jsonschema # referencing -ruff==0.14.5 +ruff==0.14.6 # via -r requirements-dev.in six==1.17.0 # via @@ -217,7 +217,7 @@ ts-backend-check==1.1.0 # via -r requirements-dev.in types-icalendar==6.3.2.20251107 # via -r requirements-dev.in -types-python-dateutil==2.9.0.20251108 +types-python-dateutil==2.9.0.20251115 # via types-icalendar types-pytz==2025.2.0.20251108 # via types-icalendar diff --git a/backend/requirements.txt b/backend/requirements.txt index cae4be47f..eb05bcc79 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -4,7 +4,7 @@ # # pip-compile requirements.in # -asgiref==3.10.0 +asgiref==3.11.0 # via # django # django-cors-headers @@ -93,7 +93,7 @@ referencing==0.37.0 # jsonschema-specifications requests==2.32.5 # via djangorestframework-stubs -rpds-py==0.28.0 +rpds-py==0.29.0 # via # jsonschema # referencing From 3a433cfd383d848db1ca11f4afd34998cfc5364c Mon Sep 17 00:00:00 2001 From: Aasim Syed Date: Tue, 25 Nov 2025 04:20:56 -0600 Subject: [PATCH 033/243] test: add UI and configuration utility tests (#1687) * test: add UI and configuration utility tests Add comprehensive unit tests for UI-related and configuration utilities: Tests Added: - passwordRules.spec.ts: Password validation rules (7 tests) - Rule structure, defaults, uniqueness, validation - mapUtils.spec.ts: Map color utilities (7 tests) - Event type to color mapping, hex validation, enum consistency - captcha.spec.ts: Captcha configuration (6 tests) - Environment variable handling, format validation - baseURLs.spec.ts: URL configuration (2 tests) - Fixed constants, environment-derived URLs - btnUtils.spec.ts: Button styling utilities (1 test) - Dynamic class generation for CTA buttons - groupSubPages.spec.ts: Group navigation (1 test) - Tab generation with i18n labels - imageURLRegistry.s.spec.ts: Image URL registry (1 test) - URL validation for image paths - locales.spec.ts: Locale configuration (2 tests) - Locale codes, names, translation files - mentionSuggestion.spec.ts: Mention suggestions (3 tests) - Query filtering, case-insensitive matching - navMenuItems.spec.ts: Navigation menu (1 test) - Menu structure with labels and routes - sidebarUtils.spec.ts: Sidebar utilities (3 tests) - Content and footer class generation Coverage: 34 tests across 11 utility files * test: add MARK comments to btnUtils.spec.ts for minimap navigation * Fix test import paths - resolve all import errors - Fix baseURLs.spec.ts: import from app/constants/baseUrls instead of app/utils/baseURLs - Fix captcha.spec.ts: import FRIENDLY_CAPTCHA_KEY from app/constants/baseUrls - Fix groupSubPages.spec.ts: import useGetGroupTabs from app/composables/useGetGroupTabs - Fix imageURLRegistry.s.spec.ts: import from shared/utils/imageURLRegistry.s - Fix locales.spec.ts: import from shared/utils/locales - Fix mapUtils.spec.ts: import ColorByEventType from shared/types/color and colorByType from shared/utils/mapUtils - Fix mentionSuggestion.spec.ts: refactor to use useMentionSuggestion composable with Component type - Fix navMenuItems.spec.ts: import from app/constants/navMenuItems - Fix passwordRules.spec.ts: import from app/constants/passwordRules - Fix sidebarUtils.spec.ts: refactor to use useSidebarClass composable All tests now passing (60 test files, 432 tests) * Fix remaining test import paths - Fix baseURLs.spec.ts: import from app/constants/baseUrls - Fix captcha.spec.ts: import from app/constants/baseUrls - Fix groupSubPages.spec.ts: import from app/composables/useGetGroupTabs - Fix imageURLRegistry.s.spec.ts: import from shared/utils/imageURLRegistry.s - Fix locales.spec.ts: import from shared/utils/locales - Fix mapUtils.spec.ts: import from shared/types/color and shared/utils/mapUtils - Fix navMenuItems.spec.ts: import from app/constants/navMenuItems - Fix passwordRules.spec.ts: import from app/constants/passwordRules - Fix sidebarUtils.spec.ts: already fixed to use useSidebarClass composable All 10 test files now have correct import paths * Fix sidebarUtils.spec.ts import path and composable usage - Replace remaining old import path from app/utils/sidebarUtils to app/composables/useSidebarClass - Update both test cases to use useSidebarClass() composable correctly - Fix destructuring of getSidebarContentDynamicClass and getSidebarFooterDynamicClass - All tests now passing * Fix Prettier formatting in mentionSuggestion.spec.ts - Add empty line after comment for Prettier code style compliance - File now passes Prettier formatting checks * Fix missing Component type import in mentionSuggestion.spec.ts - Add explicit import for Component type from vue - Required for test files as auto-imports don't apply in test environment - Resolves TypeScript error: Cannot find name 'Component' * Minor comment fix, move composables test file, create new dirs * test: remove duplicate sidebarUtils.spec.ts The existing useSidebarClass.spec.ts already provides more comprehensive coverage (4 tests vs 2, with better assertions including edge cases like blur effects and state transitions). Removing redundant duplicate file per reviewer feedback. * test: reorganize test files to match application structure - Move composable test from utils/ to composables/ - mentionSuggestion.spec.ts -> useMentionSuggestions.spec.ts - Updated describe block to match new location - Create test/constants/ directory - Move constant tests from utils/ to constants/ - passwordRules.spec.ts -> constants/passwordRules.spec.ts - baseURLs.spec.ts -> constants/baseUrls.spec.ts - captcha.spec.ts -> constants/captcha.spec.ts - navMenuItems.spec.ts -> constants/navMenuItems.spec.ts - Updated all describe blocks to reflect new paths - Keep shared utils tests in test/utils/ (they test shared/utils/*) This reorganization aligns test directory structure with the application structure: test/composables/ mirrors app/composables/, test/constants/ mirrors app/constants/, and test/utils/ mirrors shared/utils/. --------- Co-authored-by: Andrew Tavis McAllister Co-authored-by: Nicole <31940739+nicki182@users.noreply.github.com> --- frontend/test/app/.gitkeep | 0 frontend/test/composables/useGetGroupTabs.ts | 33 ++++++++++ .../composables/useMentionSuggestions.spec.ts | 32 ++++++++++ frontend/test/constants/baseUrls.spec.ts | 39 ++++++++++++ frontend/test/constants/captcha.spec.ts | 53 ++++++++++++++++ frontend/test/constants/navMenuItems.spec.ts | 25 ++++++++ frontend/test/constants/passwordRules.spec.ts | 62 +++++++++++++++++++ frontend/test/shared/.gitkeep | 0 frontend/test/utils/btnUtils.spec.ts | 4 ++ .../test/utils/imageURLRegistry.s.spec.ts | 17 +++++ frontend/test/utils/locales.spec.ts | 35 +++++++++++ frontend/test/utils/mapUtils.spec.ts | 51 +++++++++++++++ 12 files changed, 351 insertions(+) create mode 100644 frontend/test/app/.gitkeep create mode 100644 frontend/test/composables/useGetGroupTabs.ts create mode 100644 frontend/test/composables/useMentionSuggestions.spec.ts create mode 100644 frontend/test/constants/baseUrls.spec.ts create mode 100644 frontend/test/constants/captcha.spec.ts create mode 100644 frontend/test/constants/navMenuItems.spec.ts create mode 100644 frontend/test/constants/passwordRules.spec.ts create mode 100644 frontend/test/shared/.gitkeep create mode 100644 frontend/test/utils/imageURLRegistry.s.spec.ts create mode 100644 frontend/test/utils/locales.spec.ts create mode 100644 frontend/test/utils/mapUtils.spec.ts diff --git a/frontend/test/app/.gitkeep b/frontend/test/app/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/frontend/test/composables/useGetGroupTabs.ts b/frontend/test/composables/useGetGroupTabs.ts new file mode 100644 index 000000000..14b270a08 --- /dev/null +++ b/frontend/test/composables/useGetGroupTabs.ts @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +import { describe, it, expect, vi } from "vitest"; + +describe("composables/useGetGroupTabs", () => { + // MARK: Tab Generation + + it("useGetGroupTabs builds tabs with i18n labels and org/group params", async () => { + vi.resetModules(); + vi.doMock("#imports", () => ({ + useI18n: () => ({ t: (k: string) => k.split(".").pop() ?? k }), + useRoute: () => ({ params: { orgId: "o1", groupId: "g1" } }), + })); + Object.assign(globalThis, { + useRoute: () => ({ params: { orgId: "o1", groupId: "g1" } }), + }); + const mod = await import("../../app/composables/useGetGroupTabs"); + const tabs = mod.useGetGroupTabs(); + expect(tabs).toHaveLength(4); + expect(tabs[0]!.label.toLowerCase()).toBe("about"); + expect(tabs[1]!.label.toLowerCase()).toBe("events"); + expect(tabs[2]!.label.toLowerCase()).toBe("resources"); + expect(tabs[3]!.label.toLowerCase()).toBe("faq"); + // Only assert structural shape to avoid coupling to router env. + expect(tabs[0]!.routeUrl.endsWith("/about")).toBe(true); + expect(tabs[1]!.routeUrl.endsWith("/events")).toBe(true); + expect(tabs[2]!.routeUrl.endsWith("/resources")).toBe(true); + expect(tabs[3]!.routeUrl.endsWith("/faq")).toBe(true); + for (const t of tabs) { + expect(t.routeUrl.includes("/organizations/")).toBe(true); + expect(t.routeUrl.includes("/groups/")).toBe(true); + } + }); +}); diff --git a/frontend/test/composables/useMentionSuggestions.spec.ts b/frontend/test/composables/useMentionSuggestions.spec.ts new file mode 100644 index 000000000..9610a1ba9 --- /dev/null +++ b/frontend/test/composables/useMentionSuggestions.spec.ts @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +import type { Component } from "vue"; + +import { describe, expect, it } from "vitest"; + +import { useMentionSuggestion } from "../../app/composables/useMentionSuggestions"; + +describe("useMentionSuggestions", () => { + // MARK: Query Filtering + + // Mock component for useMentionSuggestion + + const MockMentionList = {} as Component; + const { getItems } = useMentionSuggestion(MockMentionList); + + it("items filters by query prefix, case-insensitive, max 5", () => { + const out = getItems({ query: "j" } as { query: string }); + expect(out.length).toBeGreaterThan(0); + expect(out.every((n) => n.toLowerCase().startsWith("j"))).toBe(true); + expect(out.length).toBeLessThanOrEqual(5); + }); + + it("items returns all base names for empty query (limited by slice)", () => { + const out = getItems({ query: "" } as { query: string }); + expect(out).toEqual(["Jay Doe", "Jane Doe", "John Doe"]); + }); + + it("items handles non-matching query gracefully", () => { + const out = getItems({ query: "zzz" } as { query: string }); + expect(out).toEqual([]); + }); +}); diff --git a/frontend/test/constants/baseUrls.spec.ts b/frontend/test/constants/baseUrls.spec.ts new file mode 100644 index 000000000..a3eaf8511 --- /dev/null +++ b/frontend/test/constants/baseUrls.spec.ts @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +import { describe, it, expect } from "vitest"; + +import { + ACTIVIST_URL, + BASE_FRONTEND_URL, + BASE_BACKEND_URL, + BASE_BACKEND_URL_NO_V1, + REQUEST_ACCESS_URL, +} from "../../app/constants/baseUrls"; + +describe("constants/baseUrls", () => { + // MARK: Fixed Constants + + it("exports fixed constants for activist and request access", () => { + expect(ACTIVIST_URL).toBe("https://activist.org"); + expect(REQUEST_ACCESS_URL).toBe( + "https://forms.activist.org/s/cm30ujrcj0003107fqc75yke8" + ); + }); + + // MARK: Environment Variables + + it("exposes environment-derived URLs as strings or undefined", () => { + // In test, these may be undefined depending on Vite env; only assert type surface. + const candidates = [ + BASE_FRONTEND_URL, + BASE_BACKEND_URL, + BASE_BACKEND_URL_NO_V1, + ]; + for (const c of candidates) { + if (c !== undefined) { + expect(typeof c).toBe("string"); + } else { + expect(c).toBeUndefined(); + } + } + }); +}); diff --git a/frontend/test/constants/captcha.spec.ts b/frontend/test/constants/captcha.spec.ts new file mode 100644 index 000000000..d289c642d --- /dev/null +++ b/frontend/test/constants/captcha.spec.ts @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +import { describe, expect, it } from "vitest"; + +import { FRIENDLY_CAPTCHA_KEY } from "../../app/constants/baseUrls"; + +describe("constants/captcha", () => { + // MARK: Environment Variable + + it("exposes FRIENDLY_CAPTCHA_KEY as string or undefined from env", () => { + if (FRIENDLY_CAPTCHA_KEY !== undefined) { + expect(typeof FRIENDLY_CAPTCHA_KEY).toBe("string"); + } else { + expect(FRIENDLY_CAPTCHA_KEY).toBeUndefined(); + } + }); + + // MARK: Validation + + it("FRIENDLY_CAPTCHA_KEY is non-empty string when defined", () => { + if (FRIENDLY_CAPTCHA_KEY !== undefined) { + expect(FRIENDLY_CAPTCHA_KEY.length).toBeGreaterThan(0); + expect(FRIENDLY_CAPTCHA_KEY.trim()).toBe(FRIENDLY_CAPTCHA_KEY); + } else { + // Test passes if undefined (env var not set in test environment) + expect(FRIENDLY_CAPTCHA_KEY).toBeUndefined(); + } + }); + + it("FRIENDLY_CAPTCHA_KEY does not contain whitespace when defined", () => { + if (FRIENDLY_CAPTCHA_KEY !== undefined) { + expect(FRIENDLY_CAPTCHA_KEY).not.toMatch(/\s/); + } else { + expect(FRIENDLY_CAPTCHA_KEY).toBeUndefined(); + } + }); + + it("FRIENDLY_CAPTCHA_KEY type is consistent", () => { + const keyType = typeof FRIENDLY_CAPTCHA_KEY; + expect(["string", "undefined"]).toContain(keyType); + }); + + it("FRIENDLY_CAPTCHA_KEY is not null", () => { + expect(FRIENDLY_CAPTCHA_KEY).not.toBeNull(); + }); + + it("FRIENDLY_CAPTCHA_KEY is not an empty string when defined", () => { + if (typeof FRIENDLY_CAPTCHA_KEY === "string") { + expect(FRIENDLY_CAPTCHA_KEY).not.toBe(""); + } else { + expect(FRIENDLY_CAPTCHA_KEY).toBeUndefined(); + } + }); +}); diff --git a/frontend/test/constants/navMenuItems.spec.ts b/frontend/test/constants/navMenuItems.spec.ts new file mode 100644 index 000000000..559a53555 --- /dev/null +++ b/frontend/test/constants/navMenuItems.spec.ts @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +import { describe, expect, it } from "vitest"; + +import { menuItems } from "../../app/constants/navMenuItems"; + +describe("constants/navMenuItems", () => { + // MARK: Menu Structure + + it("defines three top-level nav items with labels, routes, and icons", () => { + expect(menuItems).toHaveLength(3); + expect(menuItems.map((m) => m.label)).toEqual([ + "i18n._global.home", + "i18n._global.events", + "i18n._global.organizations", + ]); + expect(menuItems.map((m) => m.routeUrl)).toEqual([ + "/home", + "/events", + "/organizations", + ]); + for (const m of menuItems) { + expect(typeof m.iconUrl).toBe("string"); + } + }); +}); diff --git a/frontend/test/constants/passwordRules.spec.ts b/frontend/test/constants/passwordRules.spec.ts new file mode 100644 index 000000000..013f467ff --- /dev/null +++ b/frontend/test/constants/passwordRules.spec.ts @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +import { describe, expect, it } from "vitest"; + +import { passwordRules } from "../../app/constants/passwordRules"; + +describe("constants/passwordRules", () => { + // MARK: Structure + + it("exports five default rules in correct order", () => { + expect(Array.isArray(passwordRules)).toBe(true); + expect(passwordRules).toHaveLength(5); + expect(passwordRules.map((r) => r.message)).toEqual([ + "number-of-chars", + "capital-letters", + "lower-case-letters", + "contains-numbers", + "contains-special-chars", + ]); + }); + + it("defaults all isValid to false", () => { + for (const rule of passwordRules) { + expect(rule.isValid).toBe(false); + } + }); + + // MARK: Validation + + it("each rule has required properties", () => { + for (const rule of passwordRules) { + expect(rule).toHaveProperty("isValid"); + expect(rule).toHaveProperty("message"); + expect(typeof rule.isValid).toBe("boolean"); + expect(typeof rule.message).toBe("string"); + } + }); + + it("rule messages are unique", () => { + const messages = passwordRules.map((r) => r.message); + const uniqueMessages = new Set(messages); + expect(uniqueMessages.size).toBe(messages.length); + }); + + it("exports specific rules in expected positions", () => { + expect(passwordRules[0].message).toBe("number-of-chars"); + expect(passwordRules[1].message).toBe("capital-letters"); + expect(passwordRules[2].message).toBe("lower-case-letters"); + expect(passwordRules[3].message).toBe("contains-numbers"); + expect(passwordRules[4].message).toBe("contains-special-chars"); + }); + + it("rule array is not empty", () => { + expect(passwordRules.length).toBeGreaterThan(0); + }); + + it("all rule messages are non-empty strings", () => { + for (const rule of passwordRules) { + expect(rule.message.length).toBeGreaterThan(0); + expect(rule.message.trim()).toBe(rule.message); + } + }); +}); diff --git a/frontend/test/shared/.gitkeep b/frontend/test/shared/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/frontend/test/utils/btnUtils.spec.ts b/frontend/test/utils/btnUtils.spec.ts index dc3065e85..2da949935 100644 --- a/frontend/test/utils/btnUtils.spec.ts +++ b/frontend/test/utils/btnUtils.spec.ts @@ -11,6 +11,7 @@ describe("utils/btnUtils", () => { expect(typeof result).toBe("object"); }); + // MARK: CTA Flag Behavior describe("CTA flag behavior", () => { it("sets style-cta to true when cta is true", () => { const result = getBtnDynamicClass(true, "base"); @@ -29,6 +30,7 @@ describe("utils/btnUtils", () => { }); }); + // MARK: Font Size Classes describe("font size classes", () => { it("applies text-xs class for xs font size", () => { const result = getBtnDynamicClass(true, "xs"); @@ -97,6 +99,7 @@ describe("utils/btnUtils", () => { }); }); + // MARK: Combined Behavior describe("combined behavior", () => { it("combines CTA true with base font size correctly", () => { const result = getBtnDynamicClass(true, "base"); @@ -141,6 +144,7 @@ describe("utils/btnUtils", () => { }); }); + // MARK: Edge Cases describe("edge cases", () => { const fontSizes = ["xs", "sm", "base", "lg", "xl", "2xl", "3xl"] as const; diff --git a/frontend/test/utils/imageURLRegistry.s.spec.ts b/frontend/test/utils/imageURLRegistry.s.spec.ts new file mode 100644 index 000000000..7d42cde15 --- /dev/null +++ b/frontend/test/utils/imageURLRegistry.s.spec.ts @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +import { describe, expect, it } from "vitest"; + +import * as reg from "../../shared/utils/imageURLRegistry.s"; + +describe("utils/imageURLRegistry.s", () => { + // MARK: URL Validation + + it("exports non-empty string URLs", () => { + for (const [, value] of Object.entries(reg)) { + if (typeof value === "string") { + expect(value.length).toBeGreaterThan(1); + expect(value.startsWith("/")).toBe(true); + } + } + }); +}); diff --git a/frontend/test/utils/locales.spec.ts b/frontend/test/utils/locales.spec.ts new file mode 100644 index 000000000..0c8e151c3 --- /dev/null +++ b/frontend/test/utils/locales.spec.ts @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +import { describe, expect, it } from "vitest"; + +import locales, { LOCALE_CODE, LOCALE_NAME } from "../../shared/utils/locales"; + +describe("utils/locales", () => { + // MARK: Locale Codes and Names + + it("exports expected locale codes and names", () => { + const codes = locales.map((l) => l.code); + expect(codes).toEqual([ + LOCALE_CODE.ARABIC, + LOCALE_CODE.ENGLISH, + LOCALE_CODE.FRENCH, + LOCALE_CODE.GERMAN, + LOCALE_CODE.ITALIAN, + LOCALE_CODE.PORTUGUESE, + LOCALE_CODE.SPANISH, + ]); + expect(locales.find((l) => l.code === LOCALE_CODE.ENGLISH)!.name).toBe( + LOCALE_NAME.ENGLISH + ); + }); + + // MARK: Translation Files + + it("has translation files assigned", () => { + for (const l of locales) { + expect(l.file).toBeDefined(); + if (typeof l.file === "string") { + expect(l.file.endsWith(".json")).toBe(true); + } + } + }); +}); diff --git a/frontend/test/utils/mapUtils.spec.ts b/frontend/test/utils/mapUtils.spec.ts new file mode 100644 index 000000000..574ea406b --- /dev/null +++ b/frontend/test/utils/mapUtils.spec.ts @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +import { describe, expect, it } from "vitest"; + +import { ColorByEventType } from "../../shared/types/color"; +import { colorByType } from "../../shared/utils/mapUtils"; + +describe("utils/mapUtils", () => { + // MARK: Color Mapping + + it("maps event types to color enum", () => { + expect(colorByType.learn).toBe(ColorByEventType.LEARN); + expect(colorByType.action).toBe(ColorByEventType.ACTION); + }); + + it("maps learn to blue color", () => { + expect(colorByType.learn).toBe("#2176AE"); + }); + + it("maps action to red color", () => { + expect(colorByType.action).toBe("#BA3D3B"); + }); + + // MARK: Structure Validation + + it("exports exactly two event type mappings", () => { + const keys = Object.keys(colorByType); + expect(keys).toHaveLength(2); + expect(keys).toContain("learn"); + expect(keys).toContain("action"); + }); + + it("all mapped colors are valid hex codes", () => { + const hexColorRegex = /^#[0-9A-F]{6}$/i; + for (const color of Object.values(colorByType)) { + expect(color).toMatch(hexColorRegex); + } + }); + + it("colorByType is an object with string keys and string values", () => { + expect(typeof colorByType).toBe("object"); + for (const [key, value] of Object.entries(colorByType)) { + expect(typeof key).toBe("string"); + expect(typeof value).toBe("string"); + } + }); + + it("color values match ColorByEventType enum values", () => { + expect(colorByType.learn).toBe(ColorByEventType.LEARN); + expect(colorByType.action).toBe(ColorByEventType.ACTION); + }); +}); From 8e86757239644642df390c8e22cb243768a1c0b4 Mon Sep 17 00:00:00 2001 From: Andrew Tavis McAllister Date: Tue, 25 Nov 2025 20:28:13 +0100 Subject: [PATCH 034/243] Misc comment fixes --- frontend/test/composables/useMentionSuggestions.spec.ts | 2 +- .../test/services/communities/organization/resource.spec.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/test/composables/useMentionSuggestions.spec.ts b/frontend/test/composables/useMentionSuggestions.spec.ts index 9610a1ba9..9ae653554 100644 --- a/frontend/test/composables/useMentionSuggestions.spec.ts +++ b/frontend/test/composables/useMentionSuggestions.spec.ts @@ -8,7 +8,7 @@ import { useMentionSuggestion } from "../../app/composables/useMentionSuggestion describe("useMentionSuggestions", () => { // MARK: Query Filtering - // Mock component for useMentionSuggestion + // Mock component for useMentionSuggestion. const MockMentionList = {} as Component; const { getItems } = useMentionSuggestion(MockMentionList); diff --git a/frontend/test/services/communities/organization/resource.spec.ts b/frontend/test/services/communities/organization/resource.spec.ts index 9b5a22979..edc33076c 100644 --- a/frontend/test/services/communities/organization/resource.spec.ts +++ b/frontend/test/services/communities/organization/resource.spec.ts @@ -83,7 +83,7 @@ describe("services/communities/organization/resource", () => { await reorderOrganizationResources("org-3", resources); - // two calls, one per resource + // Two calls, one per resource. expect(fetchMock).toHaveBeenCalledTimes(2); const [firstUrl, firstOpts] = getFetchCall(fetchMock, 0); expect(firstUrl).toBe("/communities/organization_resources/a"); From a1780d3dee9254a1ca17544d4b5278eaecb62a2f Mon Sep 17 00:00:00 2001 From: Andrew Tavis McAllister Date: Wed, 26 Nov 2025 00:47:10 +0100 Subject: [PATCH 035/243] Update frontend dependencies to close some Dependabot PRs (#1756) * Update frontend dependencies to close some dependabot PRs * Remove unneeded unkown assertions between class assertions --- .../app/components/grid/GridFilterTags.vue | 2 +- .../components/sidebar/right/SidebarRight.vue | 6 +- .../generic/useCustomInfiniteScroll.ts | 4 +- frontend/app/pages/events/index.vue | 2 +- frontend/app/pages/organizations/index.vue | 2 +- frontend/package.json | 56 +- frontend/yarn.lock | 4758 ++++++++++++----- 7 files changed, 3418 insertions(+), 1412 deletions(-) diff --git a/frontend/app/components/grid/GridFilterTags.vue b/frontend/app/components/grid/GridFilterTags.vue index 58d707c81..f8df45c8b 100644 --- a/frontend/app/components/grid/GridFilterTags.vue +++ b/frontend/app/components/grid/GridFilterTags.vue @@ -70,7 +70,7 @@ const keyboardEvent = (e: KeyboardEvent) => { (switches.value[nextIndex]?.childNodes[2] as HTMLInputElement).focus(); break; case "Enter": - currentSwitch = switches.value[currentIndex]; + currentSwitch = switches.value[currentIndex] as HTMLElement | null; if (currentSwitch && currentSwitch.childNodes.length >= 3) { const tag = currentSwitch.childNodes[2]?.textContent?.trim() || ""; selected.value[tag] = !selected.value[tag]; diff --git a/frontend/app/components/sidebar/right/SidebarRight.vue b/frontend/app/components/sidebar/right/SidebarRight.vue index 15f796ef9..44f67a603 100644 --- a/frontend/app/components/sidebar/right/SidebarRight.vue +++ b/frontend/app/components/sidebar/right/SidebarRight.vue @@ -28,6 +28,8 @@ diff --git a/frontend/app/composables/generic/useCustomInfiniteScroll.ts b/frontend/app/composables/generic/useCustomInfiniteScroll.ts index a40704a5a..28a6909e7 100644 --- a/frontend/app/composables/generic/useCustomInfiniteScroll.ts +++ b/frontend/app/composables/generic/useCustomInfiniteScroll.ts @@ -30,7 +30,9 @@ export function useCustomInfiniteScroll(options: { }, { root: null, threshold, rootMargin } ); - if (sentinel.value) observer.value.observe(sentinel.value); + if (sentinel.value && observer.value) { + observer.value.observe(sentinel.value); + } }); watch( diff --git a/frontend/app/pages/events/index.vue b/frontend/app/pages/events/index.vue index fd59fcc64..872ea76c5 100644 --- a/frontend/app/pages/events/index.vue +++ b/frontend/app/pages/events/index.vue @@ -89,7 +89,7 @@ const changeFetchMore = () => { }; useCustomInfiniteScroll({ - sentinel: bottomSentinel, + sentinel: bottomSentinel as Ref, fetchMore: getMore, canFetchMore, callback: changeFetchMore, diff --git a/frontend/app/pages/organizations/index.vue b/frontend/app/pages/organizations/index.vue index 2505b01d4..cdc1cb2bc 100644 --- a/frontend/app/pages/organizations/index.vue +++ b/frontend/app/pages/organizations/index.vue @@ -92,7 +92,7 @@ const changeFetchMore = () => { }; useCustomInfiniteScroll({ - sentinel: bottomSentinel, + sentinel: bottomSentinel as Ref, fetchMore: getMore, canFetchMore, callback: changeFetchMore, diff --git a/frontend/package.json b/frontend/package.json index 0132155a2..e0b106c3f 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -28,18 +28,18 @@ "devDependencies": { "@headlessui/vue": "1.7.23", "@nuxt/devtools": "2.6.5", - "@nuxt/eslint": "1.9.0", - "@nuxt/eslint-config": "1.9.0", - "@nuxt/icon": "2.0.0", - "@nuxt/test-utils": "3.19.2", + "@nuxt/eslint": "1.10.0", + "@nuxt/eslint-config": "1.10.0", + "@nuxt/icon": "2.1.0", + "@nuxt/test-utils": "3.20.1", "@nuxtjs/color-mode": "3.5.2", "@nuxtjs/device": "3.2.4", - "@nuxtjs/i18n": "10.1.0", - "@pinia/nuxt": "0.11.2", + "@nuxtjs/i18n": "10.2.1", + "@pinia/nuxt": "0.11.3", "@playwright/test": "1.55.1", "@sidebase/nuxt-auth": "1.1.0", "@testing-library/vue": "8.1.0", - "@types/node": "24.6.2", + "@types/node": "24.10.1", "@types/zxcvbn": "4.4.5", "@vitest/coverage-v8": "3.2.4", "@vue/test-utils": "2.4.6", @@ -49,26 +49,26 @@ "axe-html-reporter": "2.2.11", "cross-env": "10.1.0", "env-cmd": "11.0.0", - "eslint": "9.36.0", - "eslint-plugin-perfectionist": "4.15.0", - "eslint-plugin-vue": "10.5.0", + "eslint": "9.39.1", + "eslint-plugin-perfectionist": "4.15.1", + "eslint-plugin-vue": "10.6.0", "eslint-plugin-vuejs-accessibility": "2.4.1", "happy-dom": "16.8.1", "kill-port": "2.0.1", - "lint-staged": "16.1.0", + "lint-staged": "16.2.7", "next-auth": "~4.21.1", - "nuxt": "4.1.2", - "nuxt-security": "2.4.0", + "nuxt": "4.2.1", + "nuxt-security": "2.5.0", "playwright-core": "1.55.1", "prettier": "3.6.2", "prettier-plugin-tailwindcss": "0.6.14", "rollup": "4.52.3", "typescript": "5.9.3", - "vite": "7.1.12", + "vite": "7.2.4", "vitest": "3.2.4", - "vue": "3.5.22", + "vue": "3.5.25", "vue-eslint-parser": "10.2.0", - "vue-tsc": "3.1.0" + "vue-tsc": "3.1.5" }, "dependencies": { "@axe-core/playwright": "4.10.2", @@ -78,7 +78,7 @@ "@opentelemetry/api": "1.9.0", "@popperjs/core": "2.11.8", "@somushq/vue3-friendly-captcha": "1.0.2", - "@tailwindcss/vite": "4.1.14", + "@tailwindcss/vite": "4.1.17", "@tiptap/core": "3.8.0", "@tiptap/extension-link": "3.8.0", "@tiptap/extension-mention": "3.8.0", @@ -89,31 +89,31 @@ "@tiptap/suggestion": "3.8.0", "@tiptap/vue-3": "3.8.0", "@types/geojson": "7946.0.16", - "@unocss/reset": "66.5.2", + "@unocss/reset": "66.5.9", "@vee-validate/zod": "4.15.1", "@vueuse/math": "13.9.0", - "autoprefixer": "10.4.21", + "autoprefixer": "10.4.22", "axios": "1.12.2", - "commander": "14.0.1", - "dompurify": "3.2.7", + "commander": "14.0.2", + "dompurify": "3.3.0", "dotenv": "17.2.3", "eslint-config-prettier": "10.1.8", "eslint-flat-config-utils": "2.1.4", "floating-vue": "5.2.2", "maplibre-gl": "5.8.0", - "pinia": "3.0.3", + "pinia": "3.0.4", "pinia-plugin-persistedstate": "4.5.0", "postcss": "8.5.6", "postcss-custom-properties": "14.0.6", "qrcode": "1.5.4", "qrcode.vue": "3.6.0", "reduced-motion": "1.0.4", - "swiper": "12.0.2", + "swiper": "12.0.3", "tailwind-scrollbar": "4.0.2", - "tailwindcss": "4.1.14", + "tailwindcss": "4.1.17", "tippy.js": "6.3.7", "tiptap-markdown": "0.9.0", - "unocss": "66.5.2", + "unocss": "66.5.9", "uuid": "13.0.0", "v-calendar": "3.1.2", "vee-validate": "4.15.1", @@ -121,14 +121,14 @@ "vue-socials": "2.0.5", "vue-sonner": "2.0.9", "vuedraggable": "4.1.0", - "wait-on": "9.0.1", + "wait-on": "9.0.3", "zod": "3.25.76", "zxcvbn": "4.4.2" }, "resolutions": { "chokidar": "3.6.0", "playwright-core": "1.55.1", - "commander": "14.0.1" + "commander": "14.0.2" }, - "packageManager": "yarn@4.10.3" + "packageManager": "yarn@4.12.0" } diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 863eb3e36..ee32d988c 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -108,6 +108,19 @@ __metadata: languageName: node linkType: hard +"@babel/generator@npm:^7.28.5": + version: 7.28.5 + resolution: "@babel/generator@npm:7.28.5" + dependencies: + "@babel/parser": "npm:^7.28.5" + "@babel/types": "npm:^7.28.5" + "@jridgewell/gen-mapping": "npm:^0.3.12" + "@jridgewell/trace-mapping": "npm:^0.3.28" + jsesc: "npm:^3.0.2" + checksum: 10c0/9f219fe1d5431b6919f1a5c60db8d5d34fe546c0d8f5a8511b32f847569234ffc8032beb9e7404649a143f54e15224ecb53a3d11b6bb85c3203e573d91fca752 + languageName: node + linkType: hard + "@babel/helper-annotate-as-pure@npm:^7.27.3": version: 7.27.3 resolution: "@babel/helper-annotate-as-pure@npm:7.27.3" @@ -240,6 +253,13 @@ __metadata: languageName: node linkType: hard +"@babel/helper-validator-identifier@npm:^7.28.5": + version: 7.28.5 + resolution: "@babel/helper-validator-identifier@npm:7.28.5" + checksum: 10c0/42aaebed91f739a41f3d80b72752d1f95fd7c72394e8e4bd7cdd88817e0774d80a432451bcba17c2c642c257c483bf1d409dd4548883429ea9493a3bc4ab0847 + languageName: node + linkType: hard + "@babel/helper-validator-option@npm:^7.27.1": version: 7.27.1 resolution: "@babel/helper-validator-option@npm:7.27.1" @@ -279,6 +299,17 @@ __metadata: languageName: node linkType: hard +"@babel/parser@npm:^7.28.5": + version: 7.28.5 + resolution: "@babel/parser@npm:7.28.5" + dependencies: + "@babel/types": "npm:^7.28.5" + bin: + parser: ./bin/babel-parser.js + checksum: 10c0/5bbe48bf2c79594ac02b490a41ffde7ef5aa22a9a88ad6bcc78432a6ba8a9d638d531d868bd1f104633f1f6bba9905746e15185b8276a3756c42b765d131b1ef + languageName: node + linkType: hard + "@babel/plugin-syntax-jsx@npm:^7.27.1": version: 7.27.1 resolution: "@babel/plugin-syntax-jsx@npm:7.27.1" @@ -374,6 +405,16 @@ __metadata: languageName: node linkType: hard +"@babel/types@npm:^7.28.5": + version: 7.28.5 + resolution: "@babel/types@npm:7.28.5" + dependencies: + "@babel/helper-string-parser": "npm:^7.27.1" + "@babel/helper-validator-identifier": "npm:^7.28.5" + checksum: 10c0/a5a483d2100befbf125793640dec26b90b95fd233a94c19573325898a5ce1e52cdfa96e495c7dcc31b5eca5b66ce3e6d4a0f5a4a62daec271455959f208ab08a + languageName: node + linkType: hard + "@barbapapazes/plausible-tracker@npm:^0.5.6": version: 0.5.6 resolution: "@barbapapazes/plausible-tracker@npm:0.5.6" @@ -462,6 +503,26 @@ __metadata: languageName: node linkType: hard +"@dxup/nuxt@npm:^0.2.1": + version: 0.2.2 + resolution: "@dxup/nuxt@npm:0.2.2" + dependencies: + "@dxup/unimport": "npm:^0.1.2" + "@nuxt/kit": "npm:^4.2.1" + chokidar: "npm:^4.0.3" + pathe: "npm:^2.0.3" + tinyglobby: "npm:^0.2.15" + checksum: 10c0/0875be8bb644962452de4f9d5f8bcc9b5592c3bd65b963ddf0365e526e013117dbc14d34cc2d4c65c38f2b7163f9123ce3a8c730d7fd1cd11d56b280c3ed2a58 + languageName: node + linkType: hard + +"@dxup/unimport@npm:^0.1.2": + version: 0.1.2 + resolution: "@dxup/unimport@npm:0.1.2" + checksum: 10c0/acb379a689fec511559b6b5314f029b6360522ea6a8ae9f69acb00c065f17d13c371131b3b3713df1c3d4a64e05ccf4048464b269b3d478a0d59b182c800b3be + languageName: node + linkType: hard + "@emnapi/core@npm:^1.4.3, @emnapi/core@npm:^1.5.0": version: 1.5.0 resolution: "@emnapi/core@npm:1.5.0" @@ -472,6 +533,16 @@ __metadata: languageName: node linkType: hard +"@emnapi/core@npm:^1.6.0": + version: 1.7.1 + resolution: "@emnapi/core@npm:1.7.1" + dependencies: + "@emnapi/wasi-threads": "npm:1.1.0" + tslib: "npm:^2.4.0" + checksum: 10c0/f3740be23440b439333e3ae3832163f60c96c4e35337f3220ceba88f36ee89a57a871d27c94eb7a9ff98a09911ed9a2089e477ab549f4d30029f8b907f84a351 + languageName: node + linkType: hard + "@emnapi/runtime@npm:^1.4.3, @emnapi/runtime@npm:^1.5.0": version: 1.5.0 resolution: "@emnapi/runtime@npm:1.5.0" @@ -481,6 +552,15 @@ __metadata: languageName: node linkType: hard +"@emnapi/runtime@npm:^1.6.0": + version: 1.7.1 + resolution: "@emnapi/runtime@npm:1.7.1" + dependencies: + tslib: "npm:^2.4.0" + checksum: 10c0/26b851cd3e93877d8732a985a2ebf5152325bbacc6204ef5336a47359dedcc23faeb08cdfcb8bb389b5401b3e894b882bc1a1e55b4b7c1ed1e67c991a760ddd5 + languageName: node + linkType: hard + "@emnapi/wasi-threads@npm:1.1.0, @emnapi/wasi-threads@npm:^1.1.0": version: 1.1.0 resolution: "@emnapi/wasi-threads@npm:1.1.0" @@ -497,16 +577,23 @@ __metadata: languageName: node linkType: hard -"@es-joy/jsdoccomment@npm:~0.56.0": - version: 0.56.0 - resolution: "@es-joy/jsdoccomment@npm:0.56.0" +"@es-joy/jsdoccomment@npm:~0.76.0": + version: 0.76.0 + resolution: "@es-joy/jsdoccomment@npm:0.76.0" dependencies: "@types/estree": "npm:^1.0.8" - "@typescript-eslint/types": "npm:^8.42.0" + "@typescript-eslint/types": "npm:^8.46.0" comment-parser: "npm:1.4.1" esquery: "npm:^1.6.0" - jsdoc-type-pratt-parser: "npm:~5.1.0" - checksum: 10c0/ddbbe6ed235b61210c3c27c685c10700329785b9374244b9d445e8382d18e3a75d8ee26ef05a905391bd16802ba17c1f37399bee0627a1e0d5316f7803bda82e + jsdoc-type-pratt-parser: "npm:~6.10.0" + checksum: 10c0/8fe4edec7d60562787ea8c77193ebe8737a9e28ec3143d383506b63890d0ffd45a2813e913ad1f00f227cb10e3a1fb913e5a696b33d499dc564272ff1a6f3fdb + languageName: node + linkType: hard + +"@es-joy/resolve.exports@npm:1.2.0": + version: 1.2.0 + resolution: "@es-joy/resolve.exports@npm:1.2.0" + checksum: 10c0/7e4713471f5eccb17a925a12415a2d9e372a42376813a19f6abd9c35e8d01ab1403777265817da67c6150cffd4f558d9ad51e26a8de6911dad89d9cb7eedacd8 languageName: node linkType: hard @@ -517,6 +604,20 @@ __metadata: languageName: node linkType: hard +"@esbuild/aix-ppc64@npm:0.25.12": + version: 0.25.12 + resolution: "@esbuild/aix-ppc64@npm:0.25.12" + conditions: os=aix & cpu=ppc64 + languageName: node + linkType: hard + +"@esbuild/aix-ppc64@npm:0.27.0": + version: 0.27.0 + resolution: "@esbuild/aix-ppc64@npm:0.27.0" + conditions: os=aix & cpu=ppc64 + languageName: node + linkType: hard + "@esbuild/android-arm64@npm:0.25.10": version: 0.25.10 resolution: "@esbuild/android-arm64@npm:0.25.10" @@ -524,6 +625,20 @@ __metadata: languageName: node linkType: hard +"@esbuild/android-arm64@npm:0.25.12": + version: 0.25.12 + resolution: "@esbuild/android-arm64@npm:0.25.12" + conditions: os=android & cpu=arm64 + languageName: node + linkType: hard + +"@esbuild/android-arm64@npm:0.27.0": + version: 0.27.0 + resolution: "@esbuild/android-arm64@npm:0.27.0" + conditions: os=android & cpu=arm64 + languageName: node + linkType: hard + "@esbuild/android-arm@npm:0.25.10": version: 0.25.10 resolution: "@esbuild/android-arm@npm:0.25.10" @@ -531,6 +646,20 @@ __metadata: languageName: node linkType: hard +"@esbuild/android-arm@npm:0.25.12": + version: 0.25.12 + resolution: "@esbuild/android-arm@npm:0.25.12" + conditions: os=android & cpu=arm + languageName: node + linkType: hard + +"@esbuild/android-arm@npm:0.27.0": + version: 0.27.0 + resolution: "@esbuild/android-arm@npm:0.27.0" + conditions: os=android & cpu=arm + languageName: node + linkType: hard + "@esbuild/android-x64@npm:0.25.10": version: 0.25.10 resolution: "@esbuild/android-x64@npm:0.25.10" @@ -538,6 +667,20 @@ __metadata: languageName: node linkType: hard +"@esbuild/android-x64@npm:0.25.12": + version: 0.25.12 + resolution: "@esbuild/android-x64@npm:0.25.12" + conditions: os=android & cpu=x64 + languageName: node + linkType: hard + +"@esbuild/android-x64@npm:0.27.0": + version: 0.27.0 + resolution: "@esbuild/android-x64@npm:0.27.0" + conditions: os=android & cpu=x64 + languageName: node + linkType: hard + "@esbuild/darwin-arm64@npm:0.25.10": version: 0.25.10 resolution: "@esbuild/darwin-arm64@npm:0.25.10" @@ -545,6 +688,20 @@ __metadata: languageName: node linkType: hard +"@esbuild/darwin-arm64@npm:0.25.12": + version: 0.25.12 + resolution: "@esbuild/darwin-arm64@npm:0.25.12" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + +"@esbuild/darwin-arm64@npm:0.27.0": + version: 0.27.0 + resolution: "@esbuild/darwin-arm64@npm:0.27.0" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + "@esbuild/darwin-x64@npm:0.25.10": version: 0.25.10 resolution: "@esbuild/darwin-x64@npm:0.25.10" @@ -552,6 +709,20 @@ __metadata: languageName: node linkType: hard +"@esbuild/darwin-x64@npm:0.25.12": + version: 0.25.12 + resolution: "@esbuild/darwin-x64@npm:0.25.12" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + +"@esbuild/darwin-x64@npm:0.27.0": + version: 0.27.0 + resolution: "@esbuild/darwin-x64@npm:0.27.0" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + "@esbuild/freebsd-arm64@npm:0.25.10": version: 0.25.10 resolution: "@esbuild/freebsd-arm64@npm:0.25.10" @@ -559,6 +730,20 @@ __metadata: languageName: node linkType: hard +"@esbuild/freebsd-arm64@npm:0.25.12": + version: 0.25.12 + resolution: "@esbuild/freebsd-arm64@npm:0.25.12" + conditions: os=freebsd & cpu=arm64 + languageName: node + linkType: hard + +"@esbuild/freebsd-arm64@npm:0.27.0": + version: 0.27.0 + resolution: "@esbuild/freebsd-arm64@npm:0.27.0" + conditions: os=freebsd & cpu=arm64 + languageName: node + linkType: hard + "@esbuild/freebsd-x64@npm:0.25.10": version: 0.25.10 resolution: "@esbuild/freebsd-x64@npm:0.25.10" @@ -566,6 +751,20 @@ __metadata: languageName: node linkType: hard +"@esbuild/freebsd-x64@npm:0.25.12": + version: 0.25.12 + resolution: "@esbuild/freebsd-x64@npm:0.25.12" + conditions: os=freebsd & cpu=x64 + languageName: node + linkType: hard + +"@esbuild/freebsd-x64@npm:0.27.0": + version: 0.27.0 + resolution: "@esbuild/freebsd-x64@npm:0.27.0" + conditions: os=freebsd & cpu=x64 + languageName: node + linkType: hard + "@esbuild/linux-arm64@npm:0.25.10": version: 0.25.10 resolution: "@esbuild/linux-arm64@npm:0.25.10" @@ -573,6 +772,20 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-arm64@npm:0.25.12": + version: 0.25.12 + resolution: "@esbuild/linux-arm64@npm:0.25.12" + conditions: os=linux & cpu=arm64 + languageName: node + linkType: hard + +"@esbuild/linux-arm64@npm:0.27.0": + version: 0.27.0 + resolution: "@esbuild/linux-arm64@npm:0.27.0" + conditions: os=linux & cpu=arm64 + languageName: node + linkType: hard + "@esbuild/linux-arm@npm:0.25.10": version: 0.25.10 resolution: "@esbuild/linux-arm@npm:0.25.10" @@ -580,6 +793,20 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-arm@npm:0.25.12": + version: 0.25.12 + resolution: "@esbuild/linux-arm@npm:0.25.12" + conditions: os=linux & cpu=arm + languageName: node + linkType: hard + +"@esbuild/linux-arm@npm:0.27.0": + version: 0.27.0 + resolution: "@esbuild/linux-arm@npm:0.27.0" + conditions: os=linux & cpu=arm + languageName: node + linkType: hard + "@esbuild/linux-ia32@npm:0.25.10": version: 0.25.10 resolution: "@esbuild/linux-ia32@npm:0.25.10" @@ -587,6 +814,20 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-ia32@npm:0.25.12": + version: 0.25.12 + resolution: "@esbuild/linux-ia32@npm:0.25.12" + conditions: os=linux & cpu=ia32 + languageName: node + linkType: hard + +"@esbuild/linux-ia32@npm:0.27.0": + version: 0.27.0 + resolution: "@esbuild/linux-ia32@npm:0.27.0" + conditions: os=linux & cpu=ia32 + languageName: node + linkType: hard + "@esbuild/linux-loong64@npm:0.25.10": version: 0.25.10 resolution: "@esbuild/linux-loong64@npm:0.25.10" @@ -594,6 +835,20 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-loong64@npm:0.25.12": + version: 0.25.12 + resolution: "@esbuild/linux-loong64@npm:0.25.12" + conditions: os=linux & cpu=loong64 + languageName: node + linkType: hard + +"@esbuild/linux-loong64@npm:0.27.0": + version: 0.27.0 + resolution: "@esbuild/linux-loong64@npm:0.27.0" + conditions: os=linux & cpu=loong64 + languageName: node + linkType: hard + "@esbuild/linux-mips64el@npm:0.25.10": version: 0.25.10 resolution: "@esbuild/linux-mips64el@npm:0.25.10" @@ -601,6 +856,20 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-mips64el@npm:0.25.12": + version: 0.25.12 + resolution: "@esbuild/linux-mips64el@npm:0.25.12" + conditions: os=linux & cpu=mips64el + languageName: node + linkType: hard + +"@esbuild/linux-mips64el@npm:0.27.0": + version: 0.27.0 + resolution: "@esbuild/linux-mips64el@npm:0.27.0" + conditions: os=linux & cpu=mips64el + languageName: node + linkType: hard + "@esbuild/linux-ppc64@npm:0.25.10": version: 0.25.10 resolution: "@esbuild/linux-ppc64@npm:0.25.10" @@ -608,6 +877,20 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-ppc64@npm:0.25.12": + version: 0.25.12 + resolution: "@esbuild/linux-ppc64@npm:0.25.12" + conditions: os=linux & cpu=ppc64 + languageName: node + linkType: hard + +"@esbuild/linux-ppc64@npm:0.27.0": + version: 0.27.0 + resolution: "@esbuild/linux-ppc64@npm:0.27.0" + conditions: os=linux & cpu=ppc64 + languageName: node + linkType: hard + "@esbuild/linux-riscv64@npm:0.25.10": version: 0.25.10 resolution: "@esbuild/linux-riscv64@npm:0.25.10" @@ -615,6 +898,20 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-riscv64@npm:0.25.12": + version: 0.25.12 + resolution: "@esbuild/linux-riscv64@npm:0.25.12" + conditions: os=linux & cpu=riscv64 + languageName: node + linkType: hard + +"@esbuild/linux-riscv64@npm:0.27.0": + version: 0.27.0 + resolution: "@esbuild/linux-riscv64@npm:0.27.0" + conditions: os=linux & cpu=riscv64 + languageName: node + linkType: hard + "@esbuild/linux-s390x@npm:0.25.10": version: 0.25.10 resolution: "@esbuild/linux-s390x@npm:0.25.10" @@ -622,6 +919,20 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-s390x@npm:0.25.12": + version: 0.25.12 + resolution: "@esbuild/linux-s390x@npm:0.25.12" + conditions: os=linux & cpu=s390x + languageName: node + linkType: hard + +"@esbuild/linux-s390x@npm:0.27.0": + version: 0.27.0 + resolution: "@esbuild/linux-s390x@npm:0.27.0" + conditions: os=linux & cpu=s390x + languageName: node + linkType: hard + "@esbuild/linux-x64@npm:0.25.10": version: 0.25.10 resolution: "@esbuild/linux-x64@npm:0.25.10" @@ -629,6 +940,20 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-x64@npm:0.25.12": + version: 0.25.12 + resolution: "@esbuild/linux-x64@npm:0.25.12" + conditions: os=linux & cpu=x64 + languageName: node + linkType: hard + +"@esbuild/linux-x64@npm:0.27.0": + version: 0.27.0 + resolution: "@esbuild/linux-x64@npm:0.27.0" + conditions: os=linux & cpu=x64 + languageName: node + linkType: hard + "@esbuild/netbsd-arm64@npm:0.25.10": version: 0.25.10 resolution: "@esbuild/netbsd-arm64@npm:0.25.10" @@ -636,6 +961,20 @@ __metadata: languageName: node linkType: hard +"@esbuild/netbsd-arm64@npm:0.25.12": + version: 0.25.12 + resolution: "@esbuild/netbsd-arm64@npm:0.25.12" + conditions: os=netbsd & cpu=arm64 + languageName: node + linkType: hard + +"@esbuild/netbsd-arm64@npm:0.27.0": + version: 0.27.0 + resolution: "@esbuild/netbsd-arm64@npm:0.27.0" + conditions: os=netbsd & cpu=arm64 + languageName: node + linkType: hard + "@esbuild/netbsd-x64@npm:0.25.10": version: 0.25.10 resolution: "@esbuild/netbsd-x64@npm:0.25.10" @@ -643,6 +982,20 @@ __metadata: languageName: node linkType: hard +"@esbuild/netbsd-x64@npm:0.25.12": + version: 0.25.12 + resolution: "@esbuild/netbsd-x64@npm:0.25.12" + conditions: os=netbsd & cpu=x64 + languageName: node + linkType: hard + +"@esbuild/netbsd-x64@npm:0.27.0": + version: 0.27.0 + resolution: "@esbuild/netbsd-x64@npm:0.27.0" + conditions: os=netbsd & cpu=x64 + languageName: node + linkType: hard + "@esbuild/openbsd-arm64@npm:0.25.10": version: 0.25.10 resolution: "@esbuild/openbsd-arm64@npm:0.25.10" @@ -650,6 +1003,20 @@ __metadata: languageName: node linkType: hard +"@esbuild/openbsd-arm64@npm:0.25.12": + version: 0.25.12 + resolution: "@esbuild/openbsd-arm64@npm:0.25.12" + conditions: os=openbsd & cpu=arm64 + languageName: node + linkType: hard + +"@esbuild/openbsd-arm64@npm:0.27.0": + version: 0.27.0 + resolution: "@esbuild/openbsd-arm64@npm:0.27.0" + conditions: os=openbsd & cpu=arm64 + languageName: node + linkType: hard + "@esbuild/openbsd-x64@npm:0.25.10": version: 0.25.10 resolution: "@esbuild/openbsd-x64@npm:0.25.10" @@ -657,6 +1024,20 @@ __metadata: languageName: node linkType: hard +"@esbuild/openbsd-x64@npm:0.25.12": + version: 0.25.12 + resolution: "@esbuild/openbsd-x64@npm:0.25.12" + conditions: os=openbsd & cpu=x64 + languageName: node + linkType: hard + +"@esbuild/openbsd-x64@npm:0.27.0": + version: 0.27.0 + resolution: "@esbuild/openbsd-x64@npm:0.27.0" + conditions: os=openbsd & cpu=x64 + languageName: node + linkType: hard + "@esbuild/openharmony-arm64@npm:0.25.10": version: 0.25.10 resolution: "@esbuild/openharmony-arm64@npm:0.25.10" @@ -664,6 +1045,20 @@ __metadata: languageName: node linkType: hard +"@esbuild/openharmony-arm64@npm:0.25.12": + version: 0.25.12 + resolution: "@esbuild/openharmony-arm64@npm:0.25.12" + conditions: os=openharmony & cpu=arm64 + languageName: node + linkType: hard + +"@esbuild/openharmony-arm64@npm:0.27.0": + version: 0.27.0 + resolution: "@esbuild/openharmony-arm64@npm:0.27.0" + conditions: os=openharmony & cpu=arm64 + languageName: node + linkType: hard + "@esbuild/sunos-x64@npm:0.25.10": version: 0.25.10 resolution: "@esbuild/sunos-x64@npm:0.25.10" @@ -671,6 +1066,20 @@ __metadata: languageName: node linkType: hard +"@esbuild/sunos-x64@npm:0.25.12": + version: 0.25.12 + resolution: "@esbuild/sunos-x64@npm:0.25.12" + conditions: os=sunos & cpu=x64 + languageName: node + linkType: hard + +"@esbuild/sunos-x64@npm:0.27.0": + version: 0.27.0 + resolution: "@esbuild/sunos-x64@npm:0.27.0" + conditions: os=sunos & cpu=x64 + languageName: node + linkType: hard + "@esbuild/win32-arm64@npm:0.25.10": version: 0.25.10 resolution: "@esbuild/win32-arm64@npm:0.25.10" @@ -678,6 +1087,20 @@ __metadata: languageName: node linkType: hard +"@esbuild/win32-arm64@npm:0.25.12": + version: 0.25.12 + resolution: "@esbuild/win32-arm64@npm:0.25.12" + conditions: os=win32 & cpu=arm64 + languageName: node + linkType: hard + +"@esbuild/win32-arm64@npm:0.27.0": + version: 0.27.0 + resolution: "@esbuild/win32-arm64@npm:0.27.0" + conditions: os=win32 & cpu=arm64 + languageName: node + linkType: hard + "@esbuild/win32-ia32@npm:0.25.10": version: 0.25.10 resolution: "@esbuild/win32-ia32@npm:0.25.10" @@ -685,6 +1108,20 @@ __metadata: languageName: node linkType: hard +"@esbuild/win32-ia32@npm:0.25.12": + version: 0.25.12 + resolution: "@esbuild/win32-ia32@npm:0.25.12" + conditions: os=win32 & cpu=ia32 + languageName: node + linkType: hard + +"@esbuild/win32-ia32@npm:0.27.0": + version: 0.27.0 + resolution: "@esbuild/win32-ia32@npm:0.27.0" + conditions: os=win32 & cpu=ia32 + languageName: node + linkType: hard + "@esbuild/win32-x64@npm:0.25.10": version: 0.25.10 resolution: "@esbuild/win32-x64@npm:0.25.10" @@ -692,6 +1129,20 @@ __metadata: languageName: node linkType: hard +"@esbuild/win32-x64@npm:0.25.12": + version: 0.25.12 + resolution: "@esbuild/win32-x64@npm:0.25.12" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + +"@esbuild/win32-x64@npm:0.27.0": + version: 0.27.0 + resolution: "@esbuild/win32-x64@npm:0.27.0" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + "@eslint-community/eslint-utils@npm:^4.2.0, @eslint-community/eslint-utils@npm:^4.4.0, @eslint-community/eslint-utils@npm:^4.7.0, @eslint-community/eslint-utils@npm:^4.8.0, @eslint-community/eslint-utils@npm:^4.9.0": version: 4.9.0 resolution: "@eslint-community/eslint-utils@npm:4.9.0" @@ -724,67 +1175,62 @@ __metadata: languageName: node linkType: hard -"@eslint/config-array@npm:^0.21.0": - version: 0.21.0 - resolution: "@eslint/config-array@npm:0.21.0" +"@eslint/config-array@npm:^0.21.1": + version: 0.21.1 + resolution: "@eslint/config-array@npm:0.21.1" dependencies: - "@eslint/object-schema": "npm:^2.1.6" + "@eslint/object-schema": "npm:^2.1.7" debug: "npm:^4.3.1" minimatch: "npm:^3.1.2" - checksum: 10c0/0ea801139166c4aa56465b309af512ef9b2d3c68f9198751bbc3e21894fe70f25fbf26e1b0e9fffff41857bc21bfddeee58649ae6d79aadcd747db0c5dca771f + checksum: 10c0/2f657d4edd6ddcb920579b72e7a5b127865d4c3fb4dda24f11d5c4f445a93ca481aebdbd6bf3291c536f5d034458dbcbb298ee3b698bc6c9dd02900fe87eec3c languageName: node linkType: hard -"@eslint/config-helpers@npm:^0.3.1": - version: 0.3.1 - resolution: "@eslint/config-helpers@npm:0.3.1" - checksum: 10c0/f6c5b3a0b76a0d7d84cc93e310c259e6c3e0792ddd0a62c5fc0027796ffae44183432cb74b2c2b1162801ee1b1b34a6beb5d90a151632b4df7349f994146a856 +"@eslint/config-helpers@npm:^0.4.2": + version: 0.4.2 + resolution: "@eslint/config-helpers@npm:0.4.2" + dependencies: + "@eslint/core": "npm:^0.17.0" + checksum: 10c0/92efd7a527b2d17eb1a148409d71d80f9ac160b565ac73ee092252e8bf08ecd08670699f46b306b94f13d22e88ac88a612120e7847570dd7cdc72f234d50dcb4 languageName: node linkType: hard -"@eslint/config-inspector@npm:^1.2.0": - version: 1.3.0 - resolution: "@eslint/config-inspector@npm:1.3.0" +"@eslint/config-inspector@npm:^1.3.0": + version: 1.4.2 + resolution: "@eslint/config-inspector@npm:1.4.2" dependencies: - "@nodelib/fs.walk": "npm:^3.0.1" - ansis: "npm:^4.1.0" + ansis: "npm:^4.2.0" bundle-require: "npm:^5.1.0" cac: "npm:^6.7.14" chokidar: "npm:^4.0.3" - debug: "npm:^4.4.1" - esbuild: "npm:^0.25.9" - find-up: "npm:^7.0.0" - get-port-please: "npm:^3.2.0" + esbuild: "npm:^0.27.0" h3: "npm:^1.15.4" - mlly: "npm:^1.8.0" - mrmime: "npm:^2.0.1" - open: "npm:^10.2.0" - tinyglobby: "npm:^0.2.14" + tinyglobby: "npm:^0.2.15" ws: "npm:^8.18.3" peerDependencies: eslint: ^8.50.0 || ^9.0.0 bin: config-inspector: bin.mjs eslint-config-inspector: bin.mjs - checksum: 10c0/e9b9521ed3a7a3f400413353cff640df9ae7103a09f9a18547f852bac716c58c6b3a49588e9390bd6c4722f5fdf78997e47e6c99a0f264605689e82e0c004a76 + checksum: 10c0/943ccbc22edd9ae99c25fbdec2d157b06256d1ab37535838803c5bbc979f1300505767510a2e206f43464e15b839b75e41f19bcbf59d080a6c0b421733a8b86d languageName: node linkType: hard -"@eslint/core@npm:^0.15.2": - version: 0.15.2 - resolution: "@eslint/core@npm:0.15.2" +"@eslint/core@npm:^0.16.0": + version: 0.16.0 + resolution: "@eslint/core@npm:0.16.0" dependencies: "@types/json-schema": "npm:^7.0.15" - checksum: 10c0/c17a6dc4f5a6006ecb60165cc38bcd21fefb4a10c7a2578a0cfe5813bbd442531a87ed741da5adab5eb678e8e693fda2e2b14555b035355537e32bcec367ea17 + checksum: 10c0/f27496a244ccfdca3e0fbc3331f9da3f603bdf1aa431af0045a3205826789a54493bc619ad6311a9090eaf7bc25798ff4e265dea1eccd2df9ce3b454f7e7da27 languageName: node linkType: hard -"@eslint/core@npm:^0.16.0": - version: 0.16.0 - resolution: "@eslint/core@npm:0.16.0" +"@eslint/core@npm:^0.17.0": + version: 0.17.0 + resolution: "@eslint/core@npm:0.17.0" dependencies: "@types/json-schema": "npm:^7.0.15" - checksum: 10c0/f27496a244ccfdca3e0fbc3331f9da3f603bdf1aa431af0045a3205826789a54493bc619ad6311a9090eaf7bc25798ff4e265dea1eccd2df9ce3b454f7e7da27 + checksum: 10c0/9a580f2246633bc752298e7440dd942ec421860d1946d0801f0423830e67887e4aeba10ab9a23d281727a978eb93d053d1922a587d502942a713607f40ed704e languageName: node linkType: hard @@ -805,27 +1251,27 @@ __metadata: languageName: node linkType: hard -"@eslint/js@npm:9.36.0, @eslint/js@npm:^9.33.0": - version: 9.36.0 - resolution: "@eslint/js@npm:9.36.0" - checksum: 10c0/e3f6fb7d6f117d79615574f7bef4f238bcfed6ece0465d28226c3a75d2b6fac9cc189121e8673562796ca8ccea2bf9861715ee5cf4a3dbef87d17811c0dac22c +"@eslint/js@npm:9.39.1, @eslint/js@npm:^9.38.0": + version: 9.39.1 + resolution: "@eslint/js@npm:9.39.1" + checksum: 10c0/6f7f26f8cdb7ad6327bbf9741973b6278eb946f18f70e35406e88194b0d5c522d0547a34a02f2a208eec95c5d1388cdf7ccb20039efd2e4cb6655615247a50f1 languageName: node linkType: hard -"@eslint/object-schema@npm:^2.1.6": - version: 2.1.6 - resolution: "@eslint/object-schema@npm:2.1.6" - checksum: 10c0/b8cdb7edea5bc5f6a96173f8d768d3554a628327af536da2fc6967a93b040f2557114d98dbcdbf389d5a7b290985ad6a9ce5babc547f36fc1fde42e674d11a56 +"@eslint/object-schema@npm:^2.1.7": + version: 2.1.7 + resolution: "@eslint/object-schema@npm:2.1.7" + checksum: 10c0/936b6e499853d1335803f556d526c86f5fe2259ed241bc665000e1d6353828edd913feed43120d150adb75570cae162cf000b5b0dfc9596726761c36b82f4e87 languageName: node linkType: hard -"@eslint/plugin-kit@npm:^0.3.3, @eslint/plugin-kit@npm:^0.3.5": - version: 0.3.5 - resolution: "@eslint/plugin-kit@npm:0.3.5" +"@eslint/plugin-kit@npm:^0.4.0, @eslint/plugin-kit@npm:^0.4.1": + version: 0.4.1 + resolution: "@eslint/plugin-kit@npm:0.4.1" dependencies: - "@eslint/core": "npm:^0.15.2" + "@eslint/core": "npm:^0.17.0" levn: "npm:^0.4.1" - checksum: 10c0/c178c1b58c574200c0fd125af3e4bc775daba7ce434ba6d1eeaf9bcb64b2e9fea75efabffb3ed3ab28858e55a016a5efa95f509994ee4341b341199ca630b89e + checksum: 10c0/51600f78b798f172a9915dffb295e2ffb44840d583427bc732baf12ecb963eb841b253300e657da91d890f4b323d10a1bd12934bf293e3018d8bb66fdce5217b languageName: node linkType: hard @@ -952,12 +1398,12 @@ __metadata: languageName: node linkType: hard -"@iconify/collections@npm:^1.0.579": - version: 1.0.601 - resolution: "@iconify/collections@npm:1.0.601" +"@iconify/collections@npm:^1.0.608": + version: 1.0.621 + resolution: "@iconify/collections@npm:1.0.621" dependencies: "@iconify/types": "npm:*" - checksum: 10c0/8efbe49cefea9c4e8386f4ec3b54ded4fce5de4146e7d2f19859358c2c846078bffa86c261133bbd75ace344c232547f2c36f220c8e76241006b5a5d948d9fa7 + checksum: 10c0/2b3c550478233144f7a5ae4a1ffca7ad297dcabb3e4823fd1dac9919ae1e5b503d0ed60a7f97f30aae800548c7d1d8e2f13b4ad07e93625b72c11d2a5673cf5c languageName: node linkType: hard @@ -968,7 +1414,7 @@ __metadata: languageName: node linkType: hard -"@iconify/utils@npm:^3.0.0, @iconify/utils@npm:^3.0.1": +"@iconify/utils@npm:^3.0.2": version: 3.0.2 resolution: "@iconify/utils@npm:3.0.2" dependencies: @@ -1421,14 +1867,14 @@ __metadata: languageName: node linkType: hard -"@napi-rs/wasm-runtime@npm:^1.0.1, @napi-rs/wasm-runtime@npm:^1.0.3, @napi-rs/wasm-runtime@npm:^1.0.5": - version: 1.0.5 - resolution: "@napi-rs/wasm-runtime@npm:1.0.5" +"@napi-rs/wasm-runtime@npm:^1.0.7": + version: 1.0.7 + resolution: "@napi-rs/wasm-runtime@npm:1.0.7" dependencies: "@emnapi/core": "npm:^1.5.0" "@emnapi/runtime": "npm:^1.5.0" "@tybys/wasm-util": "npm:^0.10.1" - checksum: 10c0/8d29299933c57b6ead61f46fad5c3dfabc31e1356bbaf25c3a8ae57be0af0db0006a808f2c1bb16e28925e027f20e0856550dac94e015f56dd6ed53b38f9a385 + checksum: 10c0/2d8635498136abb49d6dbf7395b78c63422292240963bf055f307b77aeafbde57ae2c0ceaaef215601531b36d6eb92a2cdd6f5ba90ed2aa8127c27aff9c4ae55 languageName: node linkType: hard @@ -1442,16 +1888,6 @@ __metadata: languageName: node linkType: hard -"@nodelib/fs.scandir@npm:4.0.1": - version: 4.0.1 - resolution: "@nodelib/fs.scandir@npm:4.0.1" - dependencies: - "@nodelib/fs.stat": "npm:4.0.0" - run-parallel: "npm:^1.2.0" - checksum: 10c0/b5d73e3c705ea3fa88795448d330bf02c214a225475793ccb5e7da88a7067e5eb03197691112f0b3f60367d9d5239293a1dd23bd0192435c98b6efae6461e5b5 - languageName: node - linkType: hard - "@nodelib/fs.stat@npm:2.0.5, @nodelib/fs.stat@npm:^2.0.2": version: 2.0.5 resolution: "@nodelib/fs.stat@npm:2.0.5" @@ -1459,13 +1895,6 @@ __metadata: languageName: node linkType: hard -"@nodelib/fs.stat@npm:4.0.0": - version: 4.0.0 - resolution: "@nodelib/fs.stat@npm:4.0.0" - checksum: 10c0/f44ff60c76a83484d929d231510c8d9f8a9162674bf63b03149ed25ab944010b4603770d845ac671ddba1c9615f3201e46fc22b782d8d4b28ad4d62f5fd19125 - languageName: node - linkType: hard - "@nodelib/fs.walk@npm:^1.2.3": version: 1.2.8 resolution: "@nodelib/fs.walk@npm:1.2.8" @@ -1476,16 +1905,6 @@ __metadata: languageName: node linkType: hard -"@nodelib/fs.walk@npm:^3.0.1": - version: 3.0.1 - resolution: "@nodelib/fs.walk@npm:3.0.1" - dependencies: - "@nodelib/fs.scandir": "npm:4.0.1" - fastq: "npm:^1.15.0" - checksum: 10c0/1c14b9bd4d9429fca2c4dd89a07fb7d85421d32bca2c5edf2654afe9600c8137c7785dc055da7ddc8b2a1f194f0987b101706edff408976c6a8808fa0eeb691c - languageName: node - linkType: hard - "@npmcli/agent@npm:^3.0.0": version: 3.0.0 resolution: "@npmcli/agent@npm:3.0.0" @@ -1508,42 +1927,40 @@ __metadata: languageName: node linkType: hard -"@nuxt/cli@npm:^3.28.0": - version: 3.28.0 - resolution: "@nuxt/cli@npm:3.28.0" +"@nuxt/cli@npm:^3.30.0": + version: 3.30.0 + resolution: "@nuxt/cli@npm:3.30.0" dependencies: - c12: "npm:^3.2.0" + c12: "npm:^3.3.1" citty: "npm:^0.1.6" - clipboardy: "npm:^4.0.0" confbox: "npm:^0.2.2" consola: "npm:^3.4.2" + copy-paste: "npm:^2.2.0" defu: "npm:^6.1.4" exsolve: "npm:^1.0.7" fuse.js: "npm:^7.1.0" - get-port-please: "npm:^3.2.0" giget: "npm:^2.0.0" - h3: "npm:^1.15.4" - httpxy: "npm:^0.1.7" - jiti: "npm:^2.5.1" + jiti: "npm:^2.6.1" listhen: "npm:^1.9.0" - nypm: "npm:^0.6.1" - ofetch: "npm:^1.4.1" + nypm: "npm:^0.6.2" + ofetch: "npm:^1.5.1" ohash: "npm:^2.0.11" pathe: "npm:^2.0.3" - perfect-debounce: "npm:^1.0.0" - pkg-types: "npm:^2.2.0" + perfect-debounce: "npm:^2.0.0" + pkg-types: "npm:^2.3.0" scule: "npm:^1.3.0" - semver: "npm:^7.7.2" - std-env: "npm:^3.9.0" + semver: "npm:^7.7.3" + srvx: "npm:^0.9.4" + std-env: "npm:^3.10.0" tinyexec: "npm:^1.0.1" ufo: "npm:^1.6.1" - youch: "npm:^4.1.0-beta.11" + youch: "npm:^4.1.0-beta.12" bin: nuxi: bin/nuxi.mjs nuxi-ng: bin/nuxi.mjs nuxt: bin/nuxi.mjs nuxt-cli: bin/nuxi.mjs - checksum: 10c0/c5bea49c2dc0c100ba0d14e25a94b79283314a78665eb4dfa82317e3c866525bbe1146a112ff855f6aea7a2f039aedc5d94392d60b678f7932a4e7e00d34598d + checksum: 10c0/14e74b0424a244d3569841a48edd26f1cd333f484c6b4427c37372fedfe2171afa9631889687ae14e217ea01252bfe9e13814764d86100524f3c50c0874ca798 languageName: node linkType: hard @@ -1554,7 +1971,7 @@ __metadata: languageName: node linkType: hard -"@nuxt/devtools-kit@npm:2.6.5, @nuxt/devtools-kit@npm:^2.6.2": +"@nuxt/devtools-kit@npm:2.6.5": version: 2.6.5 resolution: "@nuxt/devtools-kit@npm:2.6.5" dependencies: @@ -1566,6 +1983,30 @@ __metadata: languageName: node linkType: hard +"@nuxt/devtools-kit@npm:3.1.1, @nuxt/devtools-kit@npm:^3.0.0": + version: 3.1.1 + resolution: "@nuxt/devtools-kit@npm:3.1.1" + dependencies: + "@nuxt/kit": "npm:^4.2.1" + execa: "npm:^8.0.1" + peerDependencies: + vite: ">=6.0" + checksum: 10c0/10fa47bad38e7e33b4677bfae137597e3e30e84b06e300d31e55d41ba1214ee7dd8b6a012cff127d64dec02b21d3925bd2a45bb540d5ff85ede1ef305dc1b728 + languageName: node + linkType: hard + +"@nuxt/devtools-kit@npm:^2.6.5": + version: 2.7.0 + resolution: "@nuxt/devtools-kit@npm:2.7.0" + dependencies: + "@nuxt/kit": "npm:^3.19.3" + execa: "npm:^8.0.1" + peerDependencies: + vite: ">=6.0" + checksum: 10c0/13fd0fd18cf450ea00790a85801b763975e41cabcc4d9e582903307dbf70b7eadf957daa63f24628a01254c203e5de893ad313a8e3d61132f5074e32fe45eb8f + languageName: node + linkType: hard + "@nuxt/devtools-wizard@npm:2.6.5": version: 2.6.5 resolution: "@nuxt/devtools-wizard@npm:2.6.5" @@ -1584,7 +2025,25 @@ __metadata: languageName: node linkType: hard -"@nuxt/devtools@npm:2.6.5, @nuxt/devtools@npm:^2.6.3": +"@nuxt/devtools-wizard@npm:3.1.1": + version: 3.1.1 + resolution: "@nuxt/devtools-wizard@npm:3.1.1" + dependencies: + consola: "npm:^3.4.2" + diff: "npm:^8.0.2" + execa: "npm:^8.0.1" + magicast: "npm:^0.5.1" + pathe: "npm:^2.0.3" + pkg-types: "npm:^2.3.0" + prompts: "npm:^2.4.2" + semver: "npm:^7.7.3" + bin: + devtools-wizard: cli.mjs + checksum: 10c0/b12dd20fe7f91bb9a034dbcf7588558b004f94f4a3cc7be09276cfaf9d29a69a50086663910c0f54238f3250d8543797168cc69614850c6f32723e461c538db0 + languageName: node + linkType: hard + +"@nuxt/devtools@npm:2.6.5": version: 2.6.5 resolution: "@nuxt/devtools@npm:2.6.5" dependencies: @@ -1611,46 +2070,94 @@ __metadata: pathe: "npm:^2.0.3" perfect-debounce: "npm:^1.0.0" pkg-types: "npm:^2.3.0" - semver: "npm:^7.7.2" - simple-git: "npm:^3.28.0" + semver: "npm:^7.7.2" + simple-git: "npm:^3.28.0" + sirv: "npm:^3.0.2" + structured-clone-es: "npm:^1.0.0" + tinyglobby: "npm:^0.2.15" + vite-plugin-inspect: "npm:^11.3.3" + vite-plugin-vue-tracer: "npm:^1.0.0" + which: "npm:^5.0.0" + ws: "npm:^8.18.3" + peerDependencies: + vite: ">=6.0" + bin: + devtools: cli.mjs + checksum: 10c0/5708c443b438cf98dc9cb14e1cdb534a18eec336c84c1e9d369cce03c32b9e4e1747a9c11d98a53fa27850c12c68ae6c1c5ba60929eb24b402cf131c1050baac + languageName: node + linkType: hard + +"@nuxt/devtools@npm:^3.0.1": + version: 3.1.1 + resolution: "@nuxt/devtools@npm:3.1.1" + dependencies: + "@nuxt/devtools-kit": "npm:3.1.1" + "@nuxt/devtools-wizard": "npm:3.1.1" + "@nuxt/kit": "npm:^4.2.1" + "@vue/devtools-core": "npm:^8.0.5" + "@vue/devtools-kit": "npm:^8.0.5" + birpc: "npm:^2.8.0" + consola: "npm:^3.4.2" + destr: "npm:^2.0.5" + error-stack-parser-es: "npm:^1.0.5" + execa: "npm:^8.0.1" + fast-npm-meta: "npm:^0.4.7" + get-port-please: "npm:^3.2.0" + hookable: "npm:^5.5.3" + image-meta: "npm:^0.2.2" + is-installed-globally: "npm:^1.0.0" + launch-editor: "npm:^2.12.0" + local-pkg: "npm:^1.1.2" + magicast: "npm:^0.5.1" + nypm: "npm:^0.6.2" + ohash: "npm:^2.0.11" + pathe: "npm:^2.0.3" + perfect-debounce: "npm:^2.0.0" + pkg-types: "npm:^2.3.0" + semver: "npm:^7.7.3" + simple-git: "npm:^3.30.0" sirv: "npm:^3.0.2" structured-clone-es: "npm:^1.0.0" tinyglobby: "npm:^0.2.15" vite-plugin-inspect: "npm:^11.3.3" - vite-plugin-vue-tracer: "npm:^1.0.0" + vite-plugin-vue-tracer: "npm:^1.1.3" which: "npm:^5.0.0" ws: "npm:^8.18.3" peerDependencies: + "@vitejs/devtools": "*" vite: ">=6.0" + peerDependenciesMeta: + "@vitejs/devtools": + optional: true bin: devtools: cli.mjs - checksum: 10c0/5708c443b438cf98dc9cb14e1cdb534a18eec336c84c1e9d369cce03c32b9e4e1747a9c11d98a53fa27850c12c68ae6c1c5ba60929eb24b402cf131c1050baac + checksum: 10c0/369914f0f6d1b964521c8b47a393edf05301e6d4eff91ef93a36535bf04c89c051a9c563500e2625e335825e29efef4f1bc469286ef8863a15400ff8afb9a560 languageName: node linkType: hard -"@nuxt/eslint-config@npm:1.9.0": - version: 1.9.0 - resolution: "@nuxt/eslint-config@npm:1.9.0" +"@nuxt/eslint-config@npm:1.10.0": + version: 1.10.0 + resolution: "@nuxt/eslint-config@npm:1.10.0" dependencies: "@antfu/install-pkg": "npm:^1.1.0" "@clack/prompts": "npm:^0.11.0" - "@eslint/js": "npm:^9.33.0" - "@nuxt/eslint-plugin": "npm:1.9.0" - "@stylistic/eslint-plugin": "npm:^5.2.3" - "@typescript-eslint/eslint-plugin": "npm:^8.39.1" - "@typescript-eslint/parser": "npm:^8.39.1" + "@eslint/js": "npm:^9.38.0" + "@nuxt/eslint-plugin": "npm:1.10.0" + "@stylistic/eslint-plugin": "npm:^5.5.0" + "@typescript-eslint/eslint-plugin": "npm:^8.46.2" + "@typescript-eslint/parser": "npm:^8.46.2" eslint-config-flat-gitignore: "npm:^2.1.0" - eslint-flat-config-utils: "npm:^2.1.1" + eslint-flat-config-utils: "npm:^2.1.4" eslint-merge-processors: "npm:^2.0.0" eslint-plugin-import-lite: "npm:^0.3.0" eslint-plugin-import-x: "npm:^4.16.1" - eslint-plugin-jsdoc: "npm:^54.1.0" + eslint-plugin-jsdoc: "npm:^61.1.10" eslint-plugin-regexp: "npm:^2.10.0" - eslint-plugin-unicorn: "npm:^60.0.0" - eslint-plugin-vue: "npm:^10.4.0" + eslint-plugin-unicorn: "npm:^62.0.0" + eslint-plugin-vue: "npm:^10.5.1" eslint-processor-vue-blocks: "npm:^2.0.0" - globals: "npm:^16.3.0" - local-pkg: "npm:^1.1.1" + globals: "npm:^16.4.0" + local-pkg: "npm:^1.1.2" pathe: "npm:^2.0.3" vue-eslint-parser: "npm:^10.2.0" peerDependencies: @@ -1659,39 +2166,39 @@ __metadata: peerDependenciesMeta: eslint-plugin-format: optional: true - checksum: 10c0/f94832b90453ede931e0fc00643a7c016027d0204e39615281fa42a021125fbef60c4c92a696f3a984fcca1547bbfef8969d00a97b9ee42f36973a6796f3b571 + checksum: 10c0/a46db042caa00cace83eeb94f15876170a5fe2eb38871910f11b0da6f589d307ad602bfcbcf94364cb6aa1a25bd3e0e17f30788813e6e18f1125b91c964e9475 languageName: node linkType: hard -"@nuxt/eslint-plugin@npm:1.9.0": - version: 1.9.0 - resolution: "@nuxt/eslint-plugin@npm:1.9.0" +"@nuxt/eslint-plugin@npm:1.10.0": + version: 1.10.0 + resolution: "@nuxt/eslint-plugin@npm:1.10.0" dependencies: - "@typescript-eslint/types": "npm:^8.39.1" - "@typescript-eslint/utils": "npm:^8.39.1" + "@typescript-eslint/types": "npm:^8.46.2" + "@typescript-eslint/utils": "npm:^8.46.2" peerDependencies: eslint: ^9.0.0 - checksum: 10c0/492496606d3378b48edee09f203309aaec68a074773453f11e0a49c036705368f345761e9cb7cf91ad50dc31e5bdd2bc5bf37c7d48c9f3824fc82d8ee7f1140e + checksum: 10c0/031108733319ca44bc88348497f50c665f252a9dd8eb14ffb28b28ceda5ff389327223f635f91c5c6a258478ed8c9e28ca916c7a1d3a996868465ddcb089c698 languageName: node linkType: hard -"@nuxt/eslint@npm:1.9.0": - version: 1.9.0 - resolution: "@nuxt/eslint@npm:1.9.0" +"@nuxt/eslint@npm:1.10.0": + version: 1.10.0 + resolution: "@nuxt/eslint@npm:1.10.0" dependencies: - "@eslint/config-inspector": "npm:^1.2.0" - "@nuxt/devtools-kit": "npm:^2.6.2" - "@nuxt/eslint-config": "npm:1.9.0" - "@nuxt/eslint-plugin": "npm:1.9.0" - "@nuxt/kit": "npm:^4.0.3" + "@eslint/config-inspector": "npm:^1.3.0" + "@nuxt/devtools-kit": "npm:^3.0.0" + "@nuxt/eslint-config": "npm:1.10.0" + "@nuxt/eslint-plugin": "npm:1.10.0" + "@nuxt/kit": "npm:^4.2.0" chokidar: "npm:^4.0.3" - eslint-flat-config-utils: "npm:^2.1.1" + eslint-flat-config-utils: "npm:^2.1.4" eslint-typegen: "npm:^2.3.0" - find-up: "npm:^7.0.0" + find-up: "npm:^8.0.0" get-port-please: "npm:^3.2.0" - mlly: "npm:^1.7.4" + mlly: "npm:^1.8.0" pathe: "npm:^2.0.3" - unimport: "npm:^5.2.0" + unimport: "npm:^5.5.0" peerDependencies: eslint: ^9.0.0 eslint-webpack-plugin: ^4.1.0 @@ -1701,44 +2208,44 @@ __metadata: optional: true vite-plugin-eslint2: optional: true - checksum: 10c0/52656974459c0fe3540616f193a97dc1241945bb97286014e2d0d1c7eb48db7dee9ca83a97e414bbd0577ac426e397333877e7e364ca6cb0741472d6f24471f4 + checksum: 10c0/1cb9078f4115fde8a53e29014c5e2f79babcd3e8ca24e7d52afe56ea96aeb76a6bce82f4c8cab8552a86e65e16cb2e68c0f806f76ce34e254ec390b17098fc1f languageName: node linkType: hard -"@nuxt/icon@npm:2.0.0": - version: 2.0.0 - resolution: "@nuxt/icon@npm:2.0.0" +"@nuxt/icon@npm:2.1.0": + version: 2.1.0 + resolution: "@nuxt/icon@npm:2.1.0" dependencies: - "@iconify/collections": "npm:^1.0.579" + "@iconify/collections": "npm:^1.0.608" "@iconify/types": "npm:^2.0.0" - "@iconify/utils": "npm:^3.0.0" + "@iconify/utils": "npm:^3.0.2" "@iconify/vue": "npm:^5.0.0" - "@nuxt/devtools-kit": "npm:^2.6.2" - "@nuxt/kit": "npm:^4.0.3" + "@nuxt/devtools-kit": "npm:^2.6.5" + "@nuxt/kit": "npm:^4.1.3" consola: "npm:^3.4.2" - local-pkg: "npm:^1.1.1" - mlly: "npm:^1.7.4" + local-pkg: "npm:^1.1.2" + mlly: "npm:^1.8.0" ohash: "npm:^2.0.11" pathe: "npm:^2.0.3" picomatch: "npm:^4.0.3" - std-env: "npm:^3.9.0" - tinyglobby: "npm:^0.2.14" - checksum: 10c0/76bcaf57e520e57ef5cf2ae93b14813d21fbde486e829dbb5e6fa0cb3395e93e7e1a363e8b72861ec7637b09587afaedadd7de9d7b1e586bf84e2ed7d17f8d69 + std-env: "npm:^3.10.0" + tinyglobby: "npm:^0.2.15" + checksum: 10c0/a1540495d40c9f66e7510e9939c1d65df69e274bf475c6f41997a9207ac4b34f85626c7083e05677a1fb1241f399e6dc3800c44613bb29183b14d80b7520a96f languageName: node linkType: hard -"@nuxt/kit@npm:4.1.2, @nuxt/kit@npm:^4.0.0, @nuxt/kit@npm:^4.0.3": - version: 4.1.2 - resolution: "@nuxt/kit@npm:4.1.2" +"@nuxt/kit@npm:4.2.1, @nuxt/kit@npm:^4.1.2, @nuxt/kit@npm:^4.1.3, @nuxt/kit@npm:^4.2.0, @nuxt/kit@npm:^4.2.1": + version: 4.2.1 + resolution: "@nuxt/kit@npm:4.2.1" dependencies: - c12: "npm:^3.2.0" + c12: "npm:^3.3.1" consola: "npm:^3.4.2" defu: "npm:^6.1.4" destr: "npm:^2.0.5" errx: "npm:^0.1.0" exsolve: "npm:^1.0.7" ignore: "npm:^7.0.5" - jiti: "npm:^2.5.1" + jiti: "npm:^2.6.1" klona: "npm:^2.0.6" mlly: "npm:^1.8.0" ohash: "npm:^2.0.11" @@ -1746,18 +2253,16 @@ __metadata: pkg-types: "npm:^2.3.0" rc9: "npm:^2.1.2" scule: "npm:^1.3.0" - semver: "npm:^7.7.2" - std-env: "npm:^3.9.0" + semver: "npm:^7.7.3" tinyglobby: "npm:^0.2.15" ufo: "npm:^1.6.1" unctx: "npm:^2.4.1" - unimport: "npm:^5.2.0" untyped: "npm:^2.0.0" - checksum: 10c0/96f3b109e198d2b04840e67e3dfa8cc88097a24eb220e85376412d33a29f0ea9d51ea428c75d49d755ccab9b942bd6cea2914ac0660a3c5df9ac4f755644877a + checksum: 10c0/d8f267019d8f2b91fbeeb087c5a6f5bcc76321ad411af9f245cc4d3b324ddad454181c77a6c75df3ede9bf6677f5aac1d6cd0e6d27fd37f841212c362e4dda23 languageName: node linkType: hard -"@nuxt/kit@npm:^3.11.2, @nuxt/kit@npm:^3.13.2, @nuxt/kit@npm:^3.15.4, @nuxt/kit@npm:^3.17.5, @nuxt/kit@npm:^3.17.6, @nuxt/kit@npm:^3.18.1, @nuxt/kit@npm:^3.19.2, @nuxt/kit@npm:^3.9.0": +"@nuxt/kit@npm:^3.13.2, @nuxt/kit@npm:^3.15.4, @nuxt/kit@npm:^3.17.5, @nuxt/kit@npm:^3.17.6, @nuxt/kit@npm:^3.18.1, @nuxt/kit@npm:^3.19.2": version: 3.19.2 resolution: "@nuxt/kit@npm:3.19.2" dependencies: @@ -1788,18 +2293,111 @@ __metadata: languageName: node linkType: hard -"@nuxt/schema@npm:4.1.2": +"@nuxt/kit@npm:^3.19.3": + version: 3.20.1 + resolution: "@nuxt/kit@npm:3.20.1" + dependencies: + c12: "npm:^3.3.1" + consola: "npm:^3.4.2" + defu: "npm:^6.1.4" + destr: "npm:^2.0.5" + errx: "npm:^0.1.0" + exsolve: "npm:^1.0.7" + ignore: "npm:^7.0.5" + jiti: "npm:^2.6.1" + klona: "npm:^2.0.6" + knitwork: "npm:^1.2.0" + mlly: "npm:^1.8.0" + ohash: "npm:^2.0.11" + pathe: "npm:^2.0.3" + pkg-types: "npm:^2.3.0" + rc9: "npm:^2.1.2" + scule: "npm:^1.3.0" + semver: "npm:^7.7.3" + tinyglobby: "npm:^0.2.15" + ufo: "npm:^1.6.1" + unctx: "npm:^2.4.1" + untyped: "npm:^2.0.0" + checksum: 10c0/91eb1dc68ad4b07e5ac9b103cf9b4241adb8c33d79992db54d72cd9e8cf8566e2d891056f4724cd0fe94e004652e950843f10c8c755702c8c8c9ccc4dd5cf727 + languageName: node + linkType: hard + +"@nuxt/kit@npm:^4.0.3": version: 4.1.2 - resolution: "@nuxt/schema@npm:4.1.2" + resolution: "@nuxt/kit@npm:4.1.2" dependencies: - "@vue/shared": "npm:^3.5.21" + c12: "npm:^3.2.0" consola: "npm:^3.4.2" defu: "npm:^6.1.4" + destr: "npm:^2.0.5" + errx: "npm:^0.1.0" + exsolve: "npm:^1.0.7" + ignore: "npm:^7.0.5" + jiti: "npm:^2.5.1" + klona: "npm:^2.0.6" + mlly: "npm:^1.8.0" + ohash: "npm:^2.0.11" pathe: "npm:^2.0.3" pkg-types: "npm:^2.3.0" + rc9: "npm:^2.1.2" + scule: "npm:^1.3.0" + semver: "npm:^7.7.2" std-env: "npm:^3.9.0" - ufo: "npm:1.6.1" - checksum: 10c0/fd8a4c4bc3a9c23773ea15d90bd99ea93565541c4ec6c2e33cc08e5b3bb6f17d62cb62e2989f1e3c20e6b67b2902262b9c776fac373c765087c5b0bf162d065d + tinyglobby: "npm:^0.2.15" + ufo: "npm:^1.6.1" + unctx: "npm:^2.4.1" + unimport: "npm:^5.2.0" + untyped: "npm:^2.0.0" + checksum: 10c0/96f3b109e198d2b04840e67e3dfa8cc88097a24eb220e85376412d33a29f0ea9d51ea428c75d49d755ccab9b942bd6cea2914ac0660a3c5df9ac4f755644877a + languageName: node + linkType: hard + +"@nuxt/nitro-server@npm:4.2.1": + version: 4.2.1 + resolution: "@nuxt/nitro-server@npm:4.2.1" + dependencies: + "@nuxt/devalue": "npm:^2.0.2" + "@nuxt/kit": "npm:4.2.1" + "@unhead/vue": "npm:^2.0.19" + "@vue/shared": "npm:^3.5.23" + consola: "npm:^3.4.2" + defu: "npm:^6.1.4" + destr: "npm:^2.0.5" + devalue: "npm:^5.4.2" + errx: "npm:^0.1.0" + escape-string-regexp: "npm:^5.0.0" + exsolve: "npm:^1.0.7" + h3: "npm:^1.15.4" + impound: "npm:^1.0.0" + klona: "npm:^2.0.6" + mocked-exports: "npm:^0.1.1" + nitropack: "npm:^2.12.9" + pathe: "npm:^2.0.3" + pkg-types: "npm:^2.3.0" + radix3: "npm:^1.1.2" + std-env: "npm:^3.10.0" + ufo: "npm:^1.6.1" + unctx: "npm:^2.4.1" + unstorage: "npm:^1.17.2" + vue: "npm:^3.5.23" + vue-bundle-renderer: "npm:^2.2.0" + vue-devtools-stub: "npm:^0.1.0" + peerDependencies: + nuxt: ^4.2.1 + checksum: 10c0/f883b6e522cf7c1d3c0e425866ee7d24d478307e840f87828f42650c43a57683ebdf49c9cb16ae8107dad605bc2d9abb0835a913c9519a34dbda8c61b6a30ff5 + languageName: node + linkType: hard + +"@nuxt/schema@npm:4.2.1": + version: 4.2.1 + resolution: "@nuxt/schema@npm:4.2.1" + dependencies: + "@vue/shared": "npm:^3.5.23" + defu: "npm:^6.1.4" + pathe: "npm:^2.0.3" + pkg-types: "npm:^2.3.0" + std-env: "npm:^3.10.0" + checksum: 10c0/4ad96241b6d82edb08238963e752d17122669938b96997da9a893cf31202f000860511b50a6f4bc1897c5f86e1e6ba72692e67d57a6df1ad69641de3b15a7081 languageName: node linkType: hard @@ -1825,7 +2423,70 @@ __metadata: languageName: node linkType: hard -"@nuxt/test-utils@npm:3.19.2, @nuxt/test-utils@npm:>=3.13.1": +"@nuxt/test-utils@npm:3.20.1": + version: 3.20.1 + resolution: "@nuxt/test-utils@npm:3.20.1" + dependencies: + "@nuxt/kit": "npm:^4.1.3" + c12: "npm:^3.3.1" + consola: "npm:^3.4.2" + defu: "npm:^6.1.4" + destr: "npm:^2.0.5" + estree-walker: "npm:^3.0.3" + fake-indexeddb: "npm:^6.2.4" + get-port-please: "npm:^3.2.0" + h3: "npm:^1.15.4" + local-pkg: "npm:^1.1.2" + magic-string: "npm:^0.30.19" + node-fetch-native: "npm:^1.6.7" + node-mock-http: "npm:^1.0.3" + ofetch: "npm:^1.4.1" + pathe: "npm:^2.0.3" + perfect-debounce: "npm:^2.0.0" + radix3: "npm:^1.1.2" + scule: "npm:^1.3.0" + std-env: "npm:^3.10.0" + tinyexec: "npm:^1.0.1" + ufo: "npm:^1.6.1" + unplugin: "npm:^2.3.10" + vitest-environment-nuxt: "npm:^1.0.1" + vue: "npm:^3.5.22" + peerDependencies: + "@cucumber/cucumber": ^10.3.1 || >=11.0.0 + "@jest/globals": ^29.5.0 || >=30.0.0 + "@playwright/test": ^1.43.1 + "@testing-library/vue": ^7.0.0 || ^8.0.1 + "@vue/test-utils": ^2.4.2 + happy-dom: "*" + jsdom: "*" + playwright-core: ^1.43.1 + vitest: ^3.2.0 + peerDependenciesMeta: + "@cucumber/cucumber": + optional: true + "@jest/globals": + optional: true + "@playwright/test": + optional: true + "@testing-library/vue": + optional: true + "@vitest/ui": + optional: true + "@vue/test-utils": + optional: true + happy-dom: + optional: true + jsdom: + optional: true + playwright-core: + optional: true + vitest: + optional: true + checksum: 10c0/509aee1edd50a451961c77d24edc3730f5596f5940414ad080d9bbcf094c47dcb338131757019593105e6ed1fd58bc5f59e1f9308ef8c550e97a5ad599b04837 + languageName: node + linkType: hard + +"@nuxt/test-utils@npm:>=3.13.1": version: 3.19.2 resolution: "@nuxt/test-utils@npm:3.19.2" dependencies: @@ -1888,42 +2549,48 @@ __metadata: languageName: node linkType: hard -"@nuxt/vite-builder@npm:4.1.2": - version: 4.1.2 - resolution: "@nuxt/vite-builder@npm:4.1.2" +"@nuxt/vite-builder@npm:4.2.1": + version: 4.2.1 + resolution: "@nuxt/vite-builder@npm:4.2.1" dependencies: - "@nuxt/kit": "npm:4.1.2" - "@rollup/plugin-replace": "npm:^6.0.2" + "@nuxt/kit": "npm:4.2.1" + "@rollup/plugin-replace": "npm:^6.0.3" "@vitejs/plugin-vue": "npm:^6.0.1" "@vitejs/plugin-vue-jsx": "npm:^5.1.1" autoprefixer: "npm:^10.4.21" consola: "npm:^3.4.2" - cssnano: "npm:^7.1.1" + cssnano: "npm:^7.1.2" defu: "npm:^6.1.4" - esbuild: "npm:^0.25.9" + esbuild: "npm:^0.25.12" escape-string-regexp: "npm:^5.0.0" exsolve: "npm:^1.0.7" get-port-please: "npm:^3.2.0" h3: "npm:^1.15.4" - jiti: "npm:^2.5.1" + jiti: "npm:^2.6.1" knitwork: "npm:^1.2.0" - magic-string: "npm:^0.30.19" + magic-string: "npm:^0.30.21" mlly: "npm:^1.8.0" mocked-exports: "npm:^0.1.1" pathe: "npm:^2.0.3" pkg-types: "npm:^2.3.0" postcss: "npm:^8.5.6" - rollup-plugin-visualizer: "npm:^6.0.3" - std-env: "npm:^3.9.0" + rollup-plugin-visualizer: "npm:^6.0.5" + seroval: "npm:^1.3.2" + std-env: "npm:^3.10.0" ufo: "npm:^1.6.1" - unenv: "npm:^2.0.0-rc.21" - vite: "npm:^7.1.5" - vite-node: "npm:^3.2.4" - vite-plugin-checker: "npm:^0.10.3" - vue-bundle-renderer: "npm:^2.1.2" + unenv: "npm:^2.0.0-rc.24" + vite: "npm:^7.2.1" + vite-node: "npm:^5.0.0" + vite-plugin-checker: "npm:^0.11.0" + vue-bundle-renderer: "npm:^2.2.0" peerDependencies: + nuxt: 4.2.1 + rolldown: ^1.0.0-beta.38 vue: ^3.3.4 - checksum: 10c0/16411ba1c7ecdd97770a5e39c661dfb319cf828e4baddbdfc190c1acbce032d2427e326a383acb157d59ae7e1f3a658c9a80eab8c663357a67a3b6fc2b68bfde + peerDependenciesMeta: + rolldown: + optional: true + checksum: 10c0/09d4fd9d7894a2792292c0afc652d4ba217e505eef193d52532dde5a05f28a6289de553c4415a7752cf2df599516e3387a2803f5cf0273341aa0ca1de98647d4 languageName: node linkType: hard @@ -1948,9 +2615,9 @@ __metadata: languageName: node linkType: hard -"@nuxtjs/i18n@npm:10.1.0": - version: 10.1.0 - resolution: "@nuxtjs/i18n@npm:10.1.0" +"@nuxtjs/i18n@npm:10.2.1": + version: 10.2.1 + resolution: "@nuxtjs/i18n@npm:10.2.1" dependencies: "@intlify/core": "npm:^11.1.11" "@intlify/h3": "npm:^0.7.1" @@ -1958,30 +2625,29 @@ __metadata: "@intlify/unplugin-vue-i18n": "npm:^11.0.0" "@intlify/utils": "npm:^0.13.0" "@miyaneee/rollup-plugin-json5": "npm:^1.2.0" - "@nuxt/kit": "npm:^4.0.0" + "@nuxt/kit": "npm:^4.1.2" "@rollup/plugin-yaml": "npm:^4.1.2" - "@vue/compiler-sfc": "npm:^3.5.21" - cookie-es: "npm:^2.0.0" + "@vue/compiler-sfc": "npm:^3.5.22" defu: "npm:^6.1.4" devalue: "npm:^5.1.1" h3: "npm:^1.15.4" knitwork: "npm:^1.2.0" - magic-string: "npm:^0.30.17" + magic-string: "npm:^0.30.21" mlly: "npm:^1.7.4" nuxt-define: "npm:^1.0.0" ohash: "npm:^2.0.11" - oxc-parser: "npm:^0.81.0" - oxc-transform: "npm:^0.81.0" - oxc-walker: "npm:^0.4.0" + oxc-parser: "npm:^0.95.0" + oxc-transform: "npm:^0.95.0" + oxc-walker: "npm:^0.5.2" pathe: "npm:^2.0.3" typescript: "npm:^5.9.2" ufo: "npm:^1.6.1" unplugin: "npm:^2.3.5" - unplugin-vue-router: "npm:^0.14.0" + unplugin-vue-router: "npm:^0.16.0" unstorage: "npm:^1.16.1" vue-i18n: "npm:^11.1.11" - vue-router: "npm:^4.5.1" - checksum: 10c0/a3065728bc3085f044a882dda4e1c81af7a60113fa24ebe6d8ff37fb75cafd5904049b43f5756a67da63c63fb7fa1562d4c51802b0b2a17605a3aa382d27c7a3 + vue-router: "npm:^4.6.3" + checksum: 10c0/cda6dc300c6c5aa7b85fff8ea225535361a7501655a5497bfea9e82c36256166855236537d9e06b80d8ae29fbb2e4b7340949d491b9ec8208b62ef4be2bd4759 languageName: node linkType: hard @@ -2011,551 +2677,551 @@ __metadata: languageName: node linkType: hard -"@oxc-minify/binding-android-arm64@npm:0.87.0": - version: 0.87.0 - resolution: "@oxc-minify/binding-android-arm64@npm:0.87.0" +"@oxc-minify/binding-android-arm64@npm:0.96.0": + version: 0.96.0 + resolution: "@oxc-minify/binding-android-arm64@npm:0.96.0" conditions: os=android & cpu=arm64 languageName: node linkType: hard -"@oxc-minify/binding-darwin-arm64@npm:0.87.0": - version: 0.87.0 - resolution: "@oxc-minify/binding-darwin-arm64@npm:0.87.0" +"@oxc-minify/binding-darwin-arm64@npm:0.96.0": + version: 0.96.0 + resolution: "@oxc-minify/binding-darwin-arm64@npm:0.96.0" conditions: os=darwin & cpu=arm64 languageName: node linkType: hard -"@oxc-minify/binding-darwin-x64@npm:0.87.0": - version: 0.87.0 - resolution: "@oxc-minify/binding-darwin-x64@npm:0.87.0" +"@oxc-minify/binding-darwin-x64@npm:0.96.0": + version: 0.96.0 + resolution: "@oxc-minify/binding-darwin-x64@npm:0.96.0" conditions: os=darwin & cpu=x64 languageName: node linkType: hard -"@oxc-minify/binding-freebsd-x64@npm:0.87.0": - version: 0.87.0 - resolution: "@oxc-minify/binding-freebsd-x64@npm:0.87.0" +"@oxc-minify/binding-freebsd-x64@npm:0.96.0": + version: 0.96.0 + resolution: "@oxc-minify/binding-freebsd-x64@npm:0.96.0" conditions: os=freebsd & cpu=x64 languageName: node linkType: hard -"@oxc-minify/binding-linux-arm-gnueabihf@npm:0.87.0": - version: 0.87.0 - resolution: "@oxc-minify/binding-linux-arm-gnueabihf@npm:0.87.0" +"@oxc-minify/binding-linux-arm-gnueabihf@npm:0.96.0": + version: 0.96.0 + resolution: "@oxc-minify/binding-linux-arm-gnueabihf@npm:0.96.0" conditions: os=linux & cpu=arm languageName: node linkType: hard -"@oxc-minify/binding-linux-arm-musleabihf@npm:0.87.0": - version: 0.87.0 - resolution: "@oxc-minify/binding-linux-arm-musleabihf@npm:0.87.0" +"@oxc-minify/binding-linux-arm-musleabihf@npm:0.96.0": + version: 0.96.0 + resolution: "@oxc-minify/binding-linux-arm-musleabihf@npm:0.96.0" conditions: os=linux & cpu=arm languageName: node linkType: hard -"@oxc-minify/binding-linux-arm64-gnu@npm:0.87.0": - version: 0.87.0 - resolution: "@oxc-minify/binding-linux-arm64-gnu@npm:0.87.0" +"@oxc-minify/binding-linux-arm64-gnu@npm:0.96.0": + version: 0.96.0 + resolution: "@oxc-minify/binding-linux-arm64-gnu@npm:0.96.0" conditions: os=linux & cpu=arm64 & libc=glibc languageName: node linkType: hard -"@oxc-minify/binding-linux-arm64-musl@npm:0.87.0": - version: 0.87.0 - resolution: "@oxc-minify/binding-linux-arm64-musl@npm:0.87.0" +"@oxc-minify/binding-linux-arm64-musl@npm:0.96.0": + version: 0.96.0 + resolution: "@oxc-minify/binding-linux-arm64-musl@npm:0.96.0" conditions: os=linux & cpu=arm64 & libc=musl languageName: node linkType: hard -"@oxc-minify/binding-linux-riscv64-gnu@npm:0.87.0": - version: 0.87.0 - resolution: "@oxc-minify/binding-linux-riscv64-gnu@npm:0.87.0" +"@oxc-minify/binding-linux-riscv64-gnu@npm:0.96.0": + version: 0.96.0 + resolution: "@oxc-minify/binding-linux-riscv64-gnu@npm:0.96.0" conditions: os=linux & cpu=riscv64 & libc=glibc languageName: node linkType: hard -"@oxc-minify/binding-linux-s390x-gnu@npm:0.87.0": - version: 0.87.0 - resolution: "@oxc-minify/binding-linux-s390x-gnu@npm:0.87.0" +"@oxc-minify/binding-linux-s390x-gnu@npm:0.96.0": + version: 0.96.0 + resolution: "@oxc-minify/binding-linux-s390x-gnu@npm:0.96.0" conditions: os=linux & cpu=s390x & libc=glibc languageName: node linkType: hard -"@oxc-minify/binding-linux-x64-gnu@npm:0.87.0": - version: 0.87.0 - resolution: "@oxc-minify/binding-linux-x64-gnu@npm:0.87.0" +"@oxc-minify/binding-linux-x64-gnu@npm:0.96.0": + version: 0.96.0 + resolution: "@oxc-minify/binding-linux-x64-gnu@npm:0.96.0" conditions: os=linux & cpu=x64 & libc=glibc languageName: node linkType: hard -"@oxc-minify/binding-linux-x64-musl@npm:0.87.0": - version: 0.87.0 - resolution: "@oxc-minify/binding-linux-x64-musl@npm:0.87.0" +"@oxc-minify/binding-linux-x64-musl@npm:0.96.0": + version: 0.96.0 + resolution: "@oxc-minify/binding-linux-x64-musl@npm:0.96.0" conditions: os=linux & cpu=x64 & libc=musl languageName: node linkType: hard -"@oxc-minify/binding-wasm32-wasi@npm:0.87.0": - version: 0.87.0 - resolution: "@oxc-minify/binding-wasm32-wasi@npm:0.87.0" +"@oxc-minify/binding-wasm32-wasi@npm:0.96.0": + version: 0.96.0 + resolution: "@oxc-minify/binding-wasm32-wasi@npm:0.96.0" dependencies: - "@napi-rs/wasm-runtime": "npm:^1.0.3" + "@napi-rs/wasm-runtime": "npm:^1.0.7" conditions: cpu=wasm32 languageName: node linkType: hard -"@oxc-minify/binding-win32-arm64-msvc@npm:0.87.0": - version: 0.87.0 - resolution: "@oxc-minify/binding-win32-arm64-msvc@npm:0.87.0" +"@oxc-minify/binding-win32-arm64-msvc@npm:0.96.0": + version: 0.96.0 + resolution: "@oxc-minify/binding-win32-arm64-msvc@npm:0.96.0" conditions: os=win32 & cpu=arm64 languageName: node linkType: hard -"@oxc-minify/binding-win32-x64-msvc@npm:0.87.0": - version: 0.87.0 - resolution: "@oxc-minify/binding-win32-x64-msvc@npm:0.87.0" +"@oxc-minify/binding-win32-x64-msvc@npm:0.96.0": + version: 0.96.0 + resolution: "@oxc-minify/binding-win32-x64-msvc@npm:0.96.0" conditions: os=win32 & cpu=x64 languageName: node linkType: hard -"@oxc-parser/binding-android-arm64@npm:0.81.0": - version: 0.81.0 - resolution: "@oxc-parser/binding-android-arm64@npm:0.81.0" +"@oxc-parser/binding-android-arm64@npm:0.95.0": + version: 0.95.0 + resolution: "@oxc-parser/binding-android-arm64@npm:0.95.0" conditions: os=android & cpu=arm64 languageName: node linkType: hard -"@oxc-parser/binding-android-arm64@npm:0.87.0": - version: 0.87.0 - resolution: "@oxc-parser/binding-android-arm64@npm:0.87.0" +"@oxc-parser/binding-android-arm64@npm:0.96.0": + version: 0.96.0 + resolution: "@oxc-parser/binding-android-arm64@npm:0.96.0" conditions: os=android & cpu=arm64 languageName: node linkType: hard -"@oxc-parser/binding-darwin-arm64@npm:0.81.0": - version: 0.81.0 - resolution: "@oxc-parser/binding-darwin-arm64@npm:0.81.0" +"@oxc-parser/binding-darwin-arm64@npm:0.95.0": + version: 0.95.0 + resolution: "@oxc-parser/binding-darwin-arm64@npm:0.95.0" conditions: os=darwin & cpu=arm64 languageName: node linkType: hard -"@oxc-parser/binding-darwin-arm64@npm:0.87.0": - version: 0.87.0 - resolution: "@oxc-parser/binding-darwin-arm64@npm:0.87.0" +"@oxc-parser/binding-darwin-arm64@npm:0.96.0": + version: 0.96.0 + resolution: "@oxc-parser/binding-darwin-arm64@npm:0.96.0" conditions: os=darwin & cpu=arm64 languageName: node linkType: hard -"@oxc-parser/binding-darwin-x64@npm:0.81.0": - version: 0.81.0 - resolution: "@oxc-parser/binding-darwin-x64@npm:0.81.0" +"@oxc-parser/binding-darwin-x64@npm:0.95.0": + version: 0.95.0 + resolution: "@oxc-parser/binding-darwin-x64@npm:0.95.0" conditions: os=darwin & cpu=x64 languageName: node linkType: hard -"@oxc-parser/binding-darwin-x64@npm:0.87.0": - version: 0.87.0 - resolution: "@oxc-parser/binding-darwin-x64@npm:0.87.0" +"@oxc-parser/binding-darwin-x64@npm:0.96.0": + version: 0.96.0 + resolution: "@oxc-parser/binding-darwin-x64@npm:0.96.0" conditions: os=darwin & cpu=x64 languageName: node linkType: hard -"@oxc-parser/binding-freebsd-x64@npm:0.81.0": - version: 0.81.0 - resolution: "@oxc-parser/binding-freebsd-x64@npm:0.81.0" +"@oxc-parser/binding-freebsd-x64@npm:0.95.0": + version: 0.95.0 + resolution: "@oxc-parser/binding-freebsd-x64@npm:0.95.0" conditions: os=freebsd & cpu=x64 languageName: node linkType: hard -"@oxc-parser/binding-freebsd-x64@npm:0.87.0": - version: 0.87.0 - resolution: "@oxc-parser/binding-freebsd-x64@npm:0.87.0" +"@oxc-parser/binding-freebsd-x64@npm:0.96.0": + version: 0.96.0 + resolution: "@oxc-parser/binding-freebsd-x64@npm:0.96.0" conditions: os=freebsd & cpu=x64 languageName: node linkType: hard -"@oxc-parser/binding-linux-arm-gnueabihf@npm:0.81.0": - version: 0.81.0 - resolution: "@oxc-parser/binding-linux-arm-gnueabihf@npm:0.81.0" +"@oxc-parser/binding-linux-arm-gnueabihf@npm:0.95.0": + version: 0.95.0 + resolution: "@oxc-parser/binding-linux-arm-gnueabihf@npm:0.95.0" conditions: os=linux & cpu=arm languageName: node linkType: hard -"@oxc-parser/binding-linux-arm-gnueabihf@npm:0.87.0": - version: 0.87.0 - resolution: "@oxc-parser/binding-linux-arm-gnueabihf@npm:0.87.0" +"@oxc-parser/binding-linux-arm-gnueabihf@npm:0.96.0": + version: 0.96.0 + resolution: "@oxc-parser/binding-linux-arm-gnueabihf@npm:0.96.0" conditions: os=linux & cpu=arm languageName: node linkType: hard -"@oxc-parser/binding-linux-arm-musleabihf@npm:0.81.0": - version: 0.81.0 - resolution: "@oxc-parser/binding-linux-arm-musleabihf@npm:0.81.0" +"@oxc-parser/binding-linux-arm-musleabihf@npm:0.95.0": + version: 0.95.0 + resolution: "@oxc-parser/binding-linux-arm-musleabihf@npm:0.95.0" conditions: os=linux & cpu=arm languageName: node linkType: hard -"@oxc-parser/binding-linux-arm-musleabihf@npm:0.87.0": - version: 0.87.0 - resolution: "@oxc-parser/binding-linux-arm-musleabihf@npm:0.87.0" +"@oxc-parser/binding-linux-arm-musleabihf@npm:0.96.0": + version: 0.96.0 + resolution: "@oxc-parser/binding-linux-arm-musleabihf@npm:0.96.0" conditions: os=linux & cpu=arm languageName: node linkType: hard -"@oxc-parser/binding-linux-arm64-gnu@npm:0.81.0": - version: 0.81.0 - resolution: "@oxc-parser/binding-linux-arm64-gnu@npm:0.81.0" +"@oxc-parser/binding-linux-arm64-gnu@npm:0.95.0": + version: 0.95.0 + resolution: "@oxc-parser/binding-linux-arm64-gnu@npm:0.95.0" conditions: os=linux & cpu=arm64 & libc=glibc languageName: node linkType: hard -"@oxc-parser/binding-linux-arm64-gnu@npm:0.87.0": - version: 0.87.0 - resolution: "@oxc-parser/binding-linux-arm64-gnu@npm:0.87.0" +"@oxc-parser/binding-linux-arm64-gnu@npm:0.96.0": + version: 0.96.0 + resolution: "@oxc-parser/binding-linux-arm64-gnu@npm:0.96.0" conditions: os=linux & cpu=arm64 & libc=glibc languageName: node linkType: hard -"@oxc-parser/binding-linux-arm64-musl@npm:0.81.0": - version: 0.81.0 - resolution: "@oxc-parser/binding-linux-arm64-musl@npm:0.81.0" +"@oxc-parser/binding-linux-arm64-musl@npm:0.95.0": + version: 0.95.0 + resolution: "@oxc-parser/binding-linux-arm64-musl@npm:0.95.0" conditions: os=linux & cpu=arm64 & libc=musl languageName: node linkType: hard -"@oxc-parser/binding-linux-arm64-musl@npm:0.87.0": - version: 0.87.0 - resolution: "@oxc-parser/binding-linux-arm64-musl@npm:0.87.0" +"@oxc-parser/binding-linux-arm64-musl@npm:0.96.0": + version: 0.96.0 + resolution: "@oxc-parser/binding-linux-arm64-musl@npm:0.96.0" conditions: os=linux & cpu=arm64 & libc=musl languageName: node linkType: hard -"@oxc-parser/binding-linux-riscv64-gnu@npm:0.81.0": - version: 0.81.0 - resolution: "@oxc-parser/binding-linux-riscv64-gnu@npm:0.81.0" +"@oxc-parser/binding-linux-riscv64-gnu@npm:0.95.0": + version: 0.95.0 + resolution: "@oxc-parser/binding-linux-riscv64-gnu@npm:0.95.0" conditions: os=linux & cpu=riscv64 & libc=glibc languageName: node linkType: hard -"@oxc-parser/binding-linux-riscv64-gnu@npm:0.87.0": - version: 0.87.0 - resolution: "@oxc-parser/binding-linux-riscv64-gnu@npm:0.87.0" +"@oxc-parser/binding-linux-riscv64-gnu@npm:0.96.0": + version: 0.96.0 + resolution: "@oxc-parser/binding-linux-riscv64-gnu@npm:0.96.0" conditions: os=linux & cpu=riscv64 & libc=glibc languageName: node linkType: hard -"@oxc-parser/binding-linux-s390x-gnu@npm:0.81.0": - version: 0.81.0 - resolution: "@oxc-parser/binding-linux-s390x-gnu@npm:0.81.0" +"@oxc-parser/binding-linux-s390x-gnu@npm:0.95.0": + version: 0.95.0 + resolution: "@oxc-parser/binding-linux-s390x-gnu@npm:0.95.0" conditions: os=linux & cpu=s390x & libc=glibc languageName: node linkType: hard -"@oxc-parser/binding-linux-s390x-gnu@npm:0.87.0": - version: 0.87.0 - resolution: "@oxc-parser/binding-linux-s390x-gnu@npm:0.87.0" +"@oxc-parser/binding-linux-s390x-gnu@npm:0.96.0": + version: 0.96.0 + resolution: "@oxc-parser/binding-linux-s390x-gnu@npm:0.96.0" conditions: os=linux & cpu=s390x & libc=glibc languageName: node linkType: hard -"@oxc-parser/binding-linux-x64-gnu@npm:0.81.0": - version: 0.81.0 - resolution: "@oxc-parser/binding-linux-x64-gnu@npm:0.81.0" +"@oxc-parser/binding-linux-x64-gnu@npm:0.95.0": + version: 0.95.0 + resolution: "@oxc-parser/binding-linux-x64-gnu@npm:0.95.0" conditions: os=linux & cpu=x64 & libc=glibc languageName: node linkType: hard -"@oxc-parser/binding-linux-x64-gnu@npm:0.87.0": - version: 0.87.0 - resolution: "@oxc-parser/binding-linux-x64-gnu@npm:0.87.0" +"@oxc-parser/binding-linux-x64-gnu@npm:0.96.0": + version: 0.96.0 + resolution: "@oxc-parser/binding-linux-x64-gnu@npm:0.96.0" conditions: os=linux & cpu=x64 & libc=glibc languageName: node linkType: hard -"@oxc-parser/binding-linux-x64-musl@npm:0.81.0": - version: 0.81.0 - resolution: "@oxc-parser/binding-linux-x64-musl@npm:0.81.0" +"@oxc-parser/binding-linux-x64-musl@npm:0.95.0": + version: 0.95.0 + resolution: "@oxc-parser/binding-linux-x64-musl@npm:0.95.0" conditions: os=linux & cpu=x64 & libc=musl languageName: node linkType: hard -"@oxc-parser/binding-linux-x64-musl@npm:0.87.0": - version: 0.87.0 - resolution: "@oxc-parser/binding-linux-x64-musl@npm:0.87.0" +"@oxc-parser/binding-linux-x64-musl@npm:0.96.0": + version: 0.96.0 + resolution: "@oxc-parser/binding-linux-x64-musl@npm:0.96.0" conditions: os=linux & cpu=x64 & libc=musl languageName: node linkType: hard -"@oxc-parser/binding-wasm32-wasi@npm:0.81.0": - version: 0.81.0 - resolution: "@oxc-parser/binding-wasm32-wasi@npm:0.81.0" +"@oxc-parser/binding-wasm32-wasi@npm:0.95.0": + version: 0.95.0 + resolution: "@oxc-parser/binding-wasm32-wasi@npm:0.95.0" dependencies: - "@napi-rs/wasm-runtime": "npm:^1.0.1" + "@napi-rs/wasm-runtime": "npm:^1.0.7" conditions: cpu=wasm32 languageName: node linkType: hard -"@oxc-parser/binding-wasm32-wasi@npm:0.87.0": - version: 0.87.0 - resolution: "@oxc-parser/binding-wasm32-wasi@npm:0.87.0" +"@oxc-parser/binding-wasm32-wasi@npm:0.96.0": + version: 0.96.0 + resolution: "@oxc-parser/binding-wasm32-wasi@npm:0.96.0" dependencies: - "@napi-rs/wasm-runtime": "npm:^1.0.3" + "@napi-rs/wasm-runtime": "npm:^1.0.7" conditions: cpu=wasm32 languageName: node linkType: hard -"@oxc-parser/binding-win32-arm64-msvc@npm:0.81.0": - version: 0.81.0 - resolution: "@oxc-parser/binding-win32-arm64-msvc@npm:0.81.0" +"@oxc-parser/binding-win32-arm64-msvc@npm:0.95.0": + version: 0.95.0 + resolution: "@oxc-parser/binding-win32-arm64-msvc@npm:0.95.0" conditions: os=win32 & cpu=arm64 languageName: node linkType: hard -"@oxc-parser/binding-win32-arm64-msvc@npm:0.87.0": - version: 0.87.0 - resolution: "@oxc-parser/binding-win32-arm64-msvc@npm:0.87.0" +"@oxc-parser/binding-win32-arm64-msvc@npm:0.96.0": + version: 0.96.0 + resolution: "@oxc-parser/binding-win32-arm64-msvc@npm:0.96.0" conditions: os=win32 & cpu=arm64 languageName: node linkType: hard -"@oxc-parser/binding-win32-x64-msvc@npm:0.81.0": - version: 0.81.0 - resolution: "@oxc-parser/binding-win32-x64-msvc@npm:0.81.0" +"@oxc-parser/binding-win32-x64-msvc@npm:0.95.0": + version: 0.95.0 + resolution: "@oxc-parser/binding-win32-x64-msvc@npm:0.95.0" conditions: os=win32 & cpu=x64 languageName: node linkType: hard -"@oxc-parser/binding-win32-x64-msvc@npm:0.87.0": - version: 0.87.0 - resolution: "@oxc-parser/binding-win32-x64-msvc@npm:0.87.0" +"@oxc-parser/binding-win32-x64-msvc@npm:0.96.0": + version: 0.96.0 + resolution: "@oxc-parser/binding-win32-x64-msvc@npm:0.96.0" conditions: os=win32 & cpu=x64 languageName: node linkType: hard -"@oxc-project/types@npm:^0.81.0": - version: 0.81.0 - resolution: "@oxc-project/types@npm:0.81.0" - checksum: 10c0/10a96658d007c16e0b65b5f058f2a4affee29511330ca6abcb7d542aa381b2c2aa313fa34dec246816303836ed30f812418f732de836cd8d7beacf326f898ab4 +"@oxc-project/types@npm:^0.95.0": + version: 0.95.0 + resolution: "@oxc-project/types@npm:0.95.0" + checksum: 10c0/3ab486ff14eaa87d0b7d84763db001791e9d103281eefa87934c0d46d7fd721b83fc4b72ad3435a1974ecba04c2e902ce249cb664e16d58e691a438acd26dd4b languageName: node linkType: hard -"@oxc-project/types@npm:^0.87.0": - version: 0.87.0 - resolution: "@oxc-project/types@npm:0.87.0" - checksum: 10c0/5488baedf6c4c8864a700321fa0d59f89e1ac492addbd7583cd73a8cd824ed8564eb06a78fc0b79593fb61eb40c6c688920e8674bca313423f4f71b384559766 +"@oxc-project/types@npm:^0.96.0": + version: 0.96.0 + resolution: "@oxc-project/types@npm:0.96.0" + checksum: 10c0/8d2770c551e0cb2efc77fb7d0711a2e19080dd9cd1e221ff95b3fae6d29c7f38165b1443eafc712956a215d607a4c8d4c1aad6bf26cb7069a391b27290aa6e8e languageName: node linkType: hard -"@oxc-transform/binding-android-arm64@npm:0.81.0": - version: 0.81.0 - resolution: "@oxc-transform/binding-android-arm64@npm:0.81.0" +"@oxc-transform/binding-android-arm64@npm:0.95.0": + version: 0.95.0 + resolution: "@oxc-transform/binding-android-arm64@npm:0.95.0" conditions: os=android & cpu=arm64 languageName: node linkType: hard -"@oxc-transform/binding-android-arm64@npm:0.87.0": - version: 0.87.0 - resolution: "@oxc-transform/binding-android-arm64@npm:0.87.0" +"@oxc-transform/binding-android-arm64@npm:0.96.0": + version: 0.96.0 + resolution: "@oxc-transform/binding-android-arm64@npm:0.96.0" conditions: os=android & cpu=arm64 languageName: node linkType: hard -"@oxc-transform/binding-darwin-arm64@npm:0.81.0": - version: 0.81.0 - resolution: "@oxc-transform/binding-darwin-arm64@npm:0.81.0" +"@oxc-transform/binding-darwin-arm64@npm:0.95.0": + version: 0.95.0 + resolution: "@oxc-transform/binding-darwin-arm64@npm:0.95.0" conditions: os=darwin & cpu=arm64 languageName: node linkType: hard -"@oxc-transform/binding-darwin-arm64@npm:0.87.0": - version: 0.87.0 - resolution: "@oxc-transform/binding-darwin-arm64@npm:0.87.0" +"@oxc-transform/binding-darwin-arm64@npm:0.96.0": + version: 0.96.0 + resolution: "@oxc-transform/binding-darwin-arm64@npm:0.96.0" conditions: os=darwin & cpu=arm64 languageName: node linkType: hard -"@oxc-transform/binding-darwin-x64@npm:0.81.0": - version: 0.81.0 - resolution: "@oxc-transform/binding-darwin-x64@npm:0.81.0" +"@oxc-transform/binding-darwin-x64@npm:0.95.0": + version: 0.95.0 + resolution: "@oxc-transform/binding-darwin-x64@npm:0.95.0" conditions: os=darwin & cpu=x64 languageName: node linkType: hard -"@oxc-transform/binding-darwin-x64@npm:0.87.0": - version: 0.87.0 - resolution: "@oxc-transform/binding-darwin-x64@npm:0.87.0" +"@oxc-transform/binding-darwin-x64@npm:0.96.0": + version: 0.96.0 + resolution: "@oxc-transform/binding-darwin-x64@npm:0.96.0" conditions: os=darwin & cpu=x64 languageName: node linkType: hard -"@oxc-transform/binding-freebsd-x64@npm:0.81.0": - version: 0.81.0 - resolution: "@oxc-transform/binding-freebsd-x64@npm:0.81.0" +"@oxc-transform/binding-freebsd-x64@npm:0.95.0": + version: 0.95.0 + resolution: "@oxc-transform/binding-freebsd-x64@npm:0.95.0" conditions: os=freebsd & cpu=x64 languageName: node linkType: hard -"@oxc-transform/binding-freebsd-x64@npm:0.87.0": - version: 0.87.0 - resolution: "@oxc-transform/binding-freebsd-x64@npm:0.87.0" +"@oxc-transform/binding-freebsd-x64@npm:0.96.0": + version: 0.96.0 + resolution: "@oxc-transform/binding-freebsd-x64@npm:0.96.0" conditions: os=freebsd & cpu=x64 languageName: node linkType: hard -"@oxc-transform/binding-linux-arm-gnueabihf@npm:0.81.0": - version: 0.81.0 - resolution: "@oxc-transform/binding-linux-arm-gnueabihf@npm:0.81.0" +"@oxc-transform/binding-linux-arm-gnueabihf@npm:0.95.0": + version: 0.95.0 + resolution: "@oxc-transform/binding-linux-arm-gnueabihf@npm:0.95.0" conditions: os=linux & cpu=arm languageName: node linkType: hard -"@oxc-transform/binding-linux-arm-gnueabihf@npm:0.87.0": - version: 0.87.0 - resolution: "@oxc-transform/binding-linux-arm-gnueabihf@npm:0.87.0" +"@oxc-transform/binding-linux-arm-gnueabihf@npm:0.96.0": + version: 0.96.0 + resolution: "@oxc-transform/binding-linux-arm-gnueabihf@npm:0.96.0" conditions: os=linux & cpu=arm languageName: node linkType: hard -"@oxc-transform/binding-linux-arm-musleabihf@npm:0.81.0": - version: 0.81.0 - resolution: "@oxc-transform/binding-linux-arm-musleabihf@npm:0.81.0" +"@oxc-transform/binding-linux-arm-musleabihf@npm:0.95.0": + version: 0.95.0 + resolution: "@oxc-transform/binding-linux-arm-musleabihf@npm:0.95.0" conditions: os=linux & cpu=arm languageName: node linkType: hard -"@oxc-transform/binding-linux-arm-musleabihf@npm:0.87.0": - version: 0.87.0 - resolution: "@oxc-transform/binding-linux-arm-musleabihf@npm:0.87.0" +"@oxc-transform/binding-linux-arm-musleabihf@npm:0.96.0": + version: 0.96.0 + resolution: "@oxc-transform/binding-linux-arm-musleabihf@npm:0.96.0" conditions: os=linux & cpu=arm languageName: node linkType: hard -"@oxc-transform/binding-linux-arm64-gnu@npm:0.81.0": - version: 0.81.0 - resolution: "@oxc-transform/binding-linux-arm64-gnu@npm:0.81.0" +"@oxc-transform/binding-linux-arm64-gnu@npm:0.95.0": + version: 0.95.0 + resolution: "@oxc-transform/binding-linux-arm64-gnu@npm:0.95.0" conditions: os=linux & cpu=arm64 & libc=glibc languageName: node linkType: hard -"@oxc-transform/binding-linux-arm64-gnu@npm:0.87.0": - version: 0.87.0 - resolution: "@oxc-transform/binding-linux-arm64-gnu@npm:0.87.0" +"@oxc-transform/binding-linux-arm64-gnu@npm:0.96.0": + version: 0.96.0 + resolution: "@oxc-transform/binding-linux-arm64-gnu@npm:0.96.0" conditions: os=linux & cpu=arm64 & libc=glibc languageName: node linkType: hard -"@oxc-transform/binding-linux-arm64-musl@npm:0.81.0": - version: 0.81.0 - resolution: "@oxc-transform/binding-linux-arm64-musl@npm:0.81.0" +"@oxc-transform/binding-linux-arm64-musl@npm:0.95.0": + version: 0.95.0 + resolution: "@oxc-transform/binding-linux-arm64-musl@npm:0.95.0" conditions: os=linux & cpu=arm64 & libc=musl languageName: node linkType: hard -"@oxc-transform/binding-linux-arm64-musl@npm:0.87.0": - version: 0.87.0 - resolution: "@oxc-transform/binding-linux-arm64-musl@npm:0.87.0" +"@oxc-transform/binding-linux-arm64-musl@npm:0.96.0": + version: 0.96.0 + resolution: "@oxc-transform/binding-linux-arm64-musl@npm:0.96.0" conditions: os=linux & cpu=arm64 & libc=musl languageName: node linkType: hard -"@oxc-transform/binding-linux-riscv64-gnu@npm:0.81.0": - version: 0.81.0 - resolution: "@oxc-transform/binding-linux-riscv64-gnu@npm:0.81.0" +"@oxc-transform/binding-linux-riscv64-gnu@npm:0.95.0": + version: 0.95.0 + resolution: "@oxc-transform/binding-linux-riscv64-gnu@npm:0.95.0" conditions: os=linux & cpu=riscv64 & libc=glibc languageName: node linkType: hard -"@oxc-transform/binding-linux-riscv64-gnu@npm:0.87.0": - version: 0.87.0 - resolution: "@oxc-transform/binding-linux-riscv64-gnu@npm:0.87.0" +"@oxc-transform/binding-linux-riscv64-gnu@npm:0.96.0": + version: 0.96.0 + resolution: "@oxc-transform/binding-linux-riscv64-gnu@npm:0.96.0" conditions: os=linux & cpu=riscv64 & libc=glibc languageName: node linkType: hard -"@oxc-transform/binding-linux-s390x-gnu@npm:0.81.0": - version: 0.81.0 - resolution: "@oxc-transform/binding-linux-s390x-gnu@npm:0.81.0" +"@oxc-transform/binding-linux-s390x-gnu@npm:0.95.0": + version: 0.95.0 + resolution: "@oxc-transform/binding-linux-s390x-gnu@npm:0.95.0" conditions: os=linux & cpu=s390x & libc=glibc languageName: node linkType: hard -"@oxc-transform/binding-linux-s390x-gnu@npm:0.87.0": - version: 0.87.0 - resolution: "@oxc-transform/binding-linux-s390x-gnu@npm:0.87.0" +"@oxc-transform/binding-linux-s390x-gnu@npm:0.96.0": + version: 0.96.0 + resolution: "@oxc-transform/binding-linux-s390x-gnu@npm:0.96.0" conditions: os=linux & cpu=s390x & libc=glibc languageName: node linkType: hard -"@oxc-transform/binding-linux-x64-gnu@npm:0.81.0": - version: 0.81.0 - resolution: "@oxc-transform/binding-linux-x64-gnu@npm:0.81.0" +"@oxc-transform/binding-linux-x64-gnu@npm:0.95.0": + version: 0.95.0 + resolution: "@oxc-transform/binding-linux-x64-gnu@npm:0.95.0" conditions: os=linux & cpu=x64 & libc=glibc languageName: node linkType: hard -"@oxc-transform/binding-linux-x64-gnu@npm:0.87.0": - version: 0.87.0 - resolution: "@oxc-transform/binding-linux-x64-gnu@npm:0.87.0" +"@oxc-transform/binding-linux-x64-gnu@npm:0.96.0": + version: 0.96.0 + resolution: "@oxc-transform/binding-linux-x64-gnu@npm:0.96.0" conditions: os=linux & cpu=x64 & libc=glibc languageName: node linkType: hard -"@oxc-transform/binding-linux-x64-musl@npm:0.81.0": - version: 0.81.0 - resolution: "@oxc-transform/binding-linux-x64-musl@npm:0.81.0" +"@oxc-transform/binding-linux-x64-musl@npm:0.95.0": + version: 0.95.0 + resolution: "@oxc-transform/binding-linux-x64-musl@npm:0.95.0" conditions: os=linux & cpu=x64 & libc=musl languageName: node linkType: hard -"@oxc-transform/binding-linux-x64-musl@npm:0.87.0": - version: 0.87.0 - resolution: "@oxc-transform/binding-linux-x64-musl@npm:0.87.0" +"@oxc-transform/binding-linux-x64-musl@npm:0.96.0": + version: 0.96.0 + resolution: "@oxc-transform/binding-linux-x64-musl@npm:0.96.0" conditions: os=linux & cpu=x64 & libc=musl languageName: node linkType: hard -"@oxc-transform/binding-wasm32-wasi@npm:0.81.0": - version: 0.81.0 - resolution: "@oxc-transform/binding-wasm32-wasi@npm:0.81.0" +"@oxc-transform/binding-wasm32-wasi@npm:0.95.0": + version: 0.95.0 + resolution: "@oxc-transform/binding-wasm32-wasi@npm:0.95.0" dependencies: - "@napi-rs/wasm-runtime": "npm:^1.0.1" + "@napi-rs/wasm-runtime": "npm:^1.0.7" conditions: cpu=wasm32 languageName: node linkType: hard -"@oxc-transform/binding-wasm32-wasi@npm:0.87.0": - version: 0.87.0 - resolution: "@oxc-transform/binding-wasm32-wasi@npm:0.87.0" +"@oxc-transform/binding-wasm32-wasi@npm:0.96.0": + version: 0.96.0 + resolution: "@oxc-transform/binding-wasm32-wasi@npm:0.96.0" dependencies: - "@napi-rs/wasm-runtime": "npm:^1.0.3" + "@napi-rs/wasm-runtime": "npm:^1.0.7" conditions: cpu=wasm32 languageName: node linkType: hard -"@oxc-transform/binding-win32-arm64-msvc@npm:0.81.0": - version: 0.81.0 - resolution: "@oxc-transform/binding-win32-arm64-msvc@npm:0.81.0" +"@oxc-transform/binding-win32-arm64-msvc@npm:0.95.0": + version: 0.95.0 + resolution: "@oxc-transform/binding-win32-arm64-msvc@npm:0.95.0" conditions: os=win32 & cpu=arm64 languageName: node linkType: hard -"@oxc-transform/binding-win32-arm64-msvc@npm:0.87.0": - version: 0.87.0 - resolution: "@oxc-transform/binding-win32-arm64-msvc@npm:0.87.0" +"@oxc-transform/binding-win32-arm64-msvc@npm:0.96.0": + version: 0.96.0 + resolution: "@oxc-transform/binding-win32-arm64-msvc@npm:0.96.0" conditions: os=win32 & cpu=arm64 languageName: node linkType: hard -"@oxc-transform/binding-win32-x64-msvc@npm:0.81.0": - version: 0.81.0 - resolution: "@oxc-transform/binding-win32-x64-msvc@npm:0.81.0" +"@oxc-transform/binding-win32-x64-msvc@npm:0.95.0": + version: 0.95.0 + resolution: "@oxc-transform/binding-win32-x64-msvc@npm:0.95.0" conditions: os=win32 & cpu=x64 languageName: node linkType: hard -"@oxc-transform/binding-win32-x64-msvc@npm:0.87.0": - version: 0.87.0 - resolution: "@oxc-transform/binding-win32-x64-msvc@npm:0.87.0" +"@oxc-transform/binding-win32-x64-msvc@npm:0.96.0": + version: 0.96.0 + resolution: "@oxc-transform/binding-win32-x64-msvc@npm:0.96.0" conditions: os=win32 & cpu=x64 languageName: node linkType: hard @@ -2722,14 +3388,14 @@ __metadata: languageName: node linkType: hard -"@pinia/nuxt@npm:0.11.2": - version: 0.11.2 - resolution: "@pinia/nuxt@npm:0.11.2" +"@pinia/nuxt@npm:0.11.3": + version: 0.11.3 + resolution: "@pinia/nuxt@npm:0.11.3" dependencies: - "@nuxt/kit": "npm:^3.9.0" + "@nuxt/kit": "npm:^4.2.0" peerDependencies: - pinia: ^3.0.3 - checksum: 10c0/baf0c496ce81043f2881d5d00124517b14983638d1ee28061786d3a26d23a2f3b899a841b348ab8ebaaca65613640ddbaa560cefecda4f4f9ca0cf7a5815c750 + pinia: ^3.0.4 + checksum: 10c0/3aba259e997e0a332713c3a092339e5256f26325ec70640be438a225e7d3adc45443b03c4a36b37a5959d8610781fcd3a67d47dec3582aaef05e6579042240b7 languageName: node linkType: hard @@ -2796,6 +3462,17 @@ __metadata: languageName: node linkType: hard +"@poppinss/dumper@npm:^0.6.5": + version: 0.6.5 + resolution: "@poppinss/dumper@npm:0.6.5" + dependencies: + "@poppinss/colors": "npm:^4.1.5" + "@sindresorhus/is": "npm:^7.0.2" + supports-color: "npm:^10.0.0" + checksum: 10c0/7a0916fe4ce543cac1e61f09218e5c88b903ad0d853301b790686c772b7f5c595a04beccbf13172e0c77dc64b132b27ad438b888816fd7f6128c816290f9dc1f + languageName: node + linkType: hard + "@poppinss/exception@npm:^1.2.2": version: 1.2.2 resolution: "@poppinss/exception@npm:1.2.2" @@ -2865,6 +3542,26 @@ __metadata: languageName: node linkType: hard +"@rollup/plugin-commonjs@npm:^28.0.9": + version: 28.0.9 + resolution: "@rollup/plugin-commonjs@npm:28.0.9" + dependencies: + "@rollup/pluginutils": "npm:^5.0.1" + commondir: "npm:^1.0.1" + estree-walker: "npm:^2.0.2" + fdir: "npm:^6.2.0" + is-reference: "npm:1.2.1" + magic-string: "npm:^0.30.3" + picomatch: "npm:^4.0.2" + peerDependencies: + rollup: ^2.68.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + checksum: 10c0/b7af70614a53c549a1ba1e9647879b644bcf44ec78850f04018b929f4ee414274f867fa438308409a06ef8a3a179ed24c25e4f8ef77eb341dfddd2b0cb88c389 + languageName: node + linkType: hard + "@rollup/plugin-inject@npm:^5.0.5": version: 5.0.5 resolution: "@rollup/plugin-inject@npm:5.0.5" @@ -2913,6 +3610,24 @@ __metadata: languageName: node linkType: hard +"@rollup/plugin-node-resolve@npm:^16.0.3": + version: 16.0.3 + resolution: "@rollup/plugin-node-resolve@npm:16.0.3" + dependencies: + "@rollup/pluginutils": "npm:^5.0.1" + "@types/resolve": "npm:1.20.2" + deepmerge: "npm:^4.2.2" + is-module: "npm:^1.0.0" + resolve: "npm:^1.22.1" + peerDependencies: + rollup: ^2.78.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + checksum: 10c0/5bafff8e51cd28b5b3b8f415c30a893f5bfdb1a54469e00b3dc6530f26051620f3dfa172f40544361948b46cc3070cb34fd44ade66d38c0677d74c846fbc58dc + languageName: node + linkType: hard + "@rollup/plugin-replace@npm:^6.0.2": version: 6.0.2 resolution: "@rollup/plugin-replace@npm:6.0.2" @@ -2928,6 +3643,21 @@ __metadata: languageName: node linkType: hard +"@rollup/plugin-replace@npm:^6.0.3": + version: 6.0.3 + resolution: "@rollup/plugin-replace@npm:6.0.3" + dependencies: + "@rollup/pluginutils": "npm:^5.0.1" + magic-string: "npm:^0.30.3" + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + checksum: 10c0/93217c52fe86b03363bc534b5f07963ac4fd6b91f19f6070a66809b0c65a036b3b234624a65a8bfcc01a8dc0e42838feae03c021b0d1e5e91753c503c4d70cd6 + languageName: node + linkType: hard + "@rollup/plugin-terser@npm:^0.4.4": version: 0.4.4 resolution: "@rollup/plugin-terser@npm:0.4.4" @@ -2983,6 +3713,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-android-arm-eabi@npm:4.53.3": + version: 4.53.3 + resolution: "@rollup/rollup-android-arm-eabi@npm:4.53.3" + conditions: os=android & cpu=arm + languageName: node + linkType: hard + "@rollup/rollup-android-arm64@npm:4.52.3": version: 4.52.3 resolution: "@rollup/rollup-android-arm64@npm:4.52.3" @@ -2990,6 +3727,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-android-arm64@npm:4.53.3": + version: 4.53.3 + resolution: "@rollup/rollup-android-arm64@npm:4.53.3" + conditions: os=android & cpu=arm64 + languageName: node + linkType: hard + "@rollup/rollup-darwin-arm64@npm:4.52.3": version: 4.52.3 resolution: "@rollup/rollup-darwin-arm64@npm:4.52.3" @@ -2997,6 +3741,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-darwin-arm64@npm:4.53.3": + version: 4.53.3 + resolution: "@rollup/rollup-darwin-arm64@npm:4.53.3" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + "@rollup/rollup-darwin-x64@npm:4.52.3": version: 4.52.3 resolution: "@rollup/rollup-darwin-x64@npm:4.52.3" @@ -3004,6 +3755,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-darwin-x64@npm:4.53.3": + version: 4.53.3 + resolution: "@rollup/rollup-darwin-x64@npm:4.53.3" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + "@rollup/rollup-freebsd-arm64@npm:4.52.3": version: 4.52.3 resolution: "@rollup/rollup-freebsd-arm64@npm:4.52.3" @@ -3011,6 +3769,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-freebsd-arm64@npm:4.53.3": + version: 4.53.3 + resolution: "@rollup/rollup-freebsd-arm64@npm:4.53.3" + conditions: os=freebsd & cpu=arm64 + languageName: node + linkType: hard + "@rollup/rollup-freebsd-x64@npm:4.52.3": version: 4.52.3 resolution: "@rollup/rollup-freebsd-x64@npm:4.52.3" @@ -3018,6 +3783,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-freebsd-x64@npm:4.53.3": + version: 4.53.3 + resolution: "@rollup/rollup-freebsd-x64@npm:4.53.3" + conditions: os=freebsd & cpu=x64 + languageName: node + linkType: hard + "@rollup/rollup-linux-arm-gnueabihf@npm:4.52.3": version: 4.52.3 resolution: "@rollup/rollup-linux-arm-gnueabihf@npm:4.52.3" @@ -3025,6 +3797,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-linux-arm-gnueabihf@npm:4.53.3": + version: 4.53.3 + resolution: "@rollup/rollup-linux-arm-gnueabihf@npm:4.53.3" + conditions: os=linux & cpu=arm & libc=glibc + languageName: node + linkType: hard + "@rollup/rollup-linux-arm-musleabihf@npm:4.52.3": version: 4.52.3 resolution: "@rollup/rollup-linux-arm-musleabihf@npm:4.52.3" @@ -3032,6 +3811,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-linux-arm-musleabihf@npm:4.53.3": + version: 4.53.3 + resolution: "@rollup/rollup-linux-arm-musleabihf@npm:4.53.3" + conditions: os=linux & cpu=arm & libc=musl + languageName: node + linkType: hard + "@rollup/rollup-linux-arm64-gnu@npm:4.52.3": version: 4.52.3 resolution: "@rollup/rollup-linux-arm64-gnu@npm:4.52.3" @@ -3039,6 +3825,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-linux-arm64-gnu@npm:4.53.3": + version: 4.53.3 + resolution: "@rollup/rollup-linux-arm64-gnu@npm:4.53.3" + conditions: os=linux & cpu=arm64 & libc=glibc + languageName: node + linkType: hard + "@rollup/rollup-linux-arm64-musl@npm:4.52.3": version: 4.52.3 resolution: "@rollup/rollup-linux-arm64-musl@npm:4.52.3" @@ -3046,6 +3839,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-linux-arm64-musl@npm:4.53.3": + version: 4.53.3 + resolution: "@rollup/rollup-linux-arm64-musl@npm:4.53.3" + conditions: os=linux & cpu=arm64 & libc=musl + languageName: node + linkType: hard + "@rollup/rollup-linux-loong64-gnu@npm:4.52.3": version: 4.52.3 resolution: "@rollup/rollup-linux-loong64-gnu@npm:4.52.3" @@ -3053,6 +3853,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-linux-loong64-gnu@npm:4.53.3": + version: 4.53.3 + resolution: "@rollup/rollup-linux-loong64-gnu@npm:4.53.3" + conditions: os=linux & cpu=loong64 & libc=glibc + languageName: node + linkType: hard + "@rollup/rollup-linux-ppc64-gnu@npm:4.52.3": version: 4.52.3 resolution: "@rollup/rollup-linux-ppc64-gnu@npm:4.52.3" @@ -3060,6 +3867,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-linux-ppc64-gnu@npm:4.53.3": + version: 4.53.3 + resolution: "@rollup/rollup-linux-ppc64-gnu@npm:4.53.3" + conditions: os=linux & cpu=ppc64 & libc=glibc + languageName: node + linkType: hard + "@rollup/rollup-linux-riscv64-gnu@npm:4.52.3": version: 4.52.3 resolution: "@rollup/rollup-linux-riscv64-gnu@npm:4.52.3" @@ -3067,6 +3881,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-linux-riscv64-gnu@npm:4.53.3": + version: 4.53.3 + resolution: "@rollup/rollup-linux-riscv64-gnu@npm:4.53.3" + conditions: os=linux & cpu=riscv64 & libc=glibc + languageName: node + linkType: hard + "@rollup/rollup-linux-riscv64-musl@npm:4.52.3": version: 4.52.3 resolution: "@rollup/rollup-linux-riscv64-musl@npm:4.52.3" @@ -3074,6 +3895,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-linux-riscv64-musl@npm:4.53.3": + version: 4.53.3 + resolution: "@rollup/rollup-linux-riscv64-musl@npm:4.53.3" + conditions: os=linux & cpu=riscv64 & libc=musl + languageName: node + linkType: hard + "@rollup/rollup-linux-s390x-gnu@npm:4.52.3": version: 4.52.3 resolution: "@rollup/rollup-linux-s390x-gnu@npm:4.52.3" @@ -3081,6 +3909,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-linux-s390x-gnu@npm:4.53.3": + version: 4.53.3 + resolution: "@rollup/rollup-linux-s390x-gnu@npm:4.53.3" + conditions: os=linux & cpu=s390x & libc=glibc + languageName: node + linkType: hard + "@rollup/rollup-linux-x64-gnu@npm:4.52.3": version: 4.52.3 resolution: "@rollup/rollup-linux-x64-gnu@npm:4.52.3" @@ -3088,6 +3923,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-linux-x64-gnu@npm:4.53.3": + version: 4.53.3 + resolution: "@rollup/rollup-linux-x64-gnu@npm:4.53.3" + conditions: os=linux & cpu=x64 & libc=glibc + languageName: node + linkType: hard + "@rollup/rollup-linux-x64-musl@npm:4.52.3": version: 4.52.3 resolution: "@rollup/rollup-linux-x64-musl@npm:4.52.3" @@ -3095,6 +3937,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-linux-x64-musl@npm:4.53.3": + version: 4.53.3 + resolution: "@rollup/rollup-linux-x64-musl@npm:4.53.3" + conditions: os=linux & cpu=x64 & libc=musl + languageName: node + linkType: hard + "@rollup/rollup-openharmony-arm64@npm:4.52.3": version: 4.52.3 resolution: "@rollup/rollup-openharmony-arm64@npm:4.52.3" @@ -3102,6 +3951,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-openharmony-arm64@npm:4.53.3": + version: 4.53.3 + resolution: "@rollup/rollup-openharmony-arm64@npm:4.53.3" + conditions: os=openharmony & cpu=arm64 + languageName: node + linkType: hard + "@rollup/rollup-win32-arm64-msvc@npm:4.52.3": version: 4.52.3 resolution: "@rollup/rollup-win32-arm64-msvc@npm:4.52.3" @@ -3109,6 +3965,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-win32-arm64-msvc@npm:4.53.3": + version: 4.53.3 + resolution: "@rollup/rollup-win32-arm64-msvc@npm:4.53.3" + conditions: os=win32 & cpu=arm64 + languageName: node + linkType: hard + "@rollup/rollup-win32-ia32-msvc@npm:4.52.3": version: 4.52.3 resolution: "@rollup/rollup-win32-ia32-msvc@npm:4.52.3" @@ -3116,6 +3979,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-win32-ia32-msvc@npm:4.53.3": + version: 4.53.3 + resolution: "@rollup/rollup-win32-ia32-msvc@npm:4.53.3" + conditions: os=win32 & cpu=ia32 + languageName: node + linkType: hard + "@rollup/rollup-win32-x64-gnu@npm:4.52.3": version: 4.52.3 resolution: "@rollup/rollup-win32-x64-gnu@npm:4.52.3" @@ -3123,6 +3993,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-win32-x64-gnu@npm:4.53.3": + version: 4.53.3 + resolution: "@rollup/rollup-win32-x64-gnu@npm:4.53.3" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + "@rollup/rollup-win32-x64-msvc@npm:4.52.3": version: 4.52.3 resolution: "@rollup/rollup-win32-x64-msvc@npm:4.52.3" @@ -3130,6 +4007,13 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-win32-x64-msvc@npm:4.53.3": + version: 4.53.3 + resolution: "@rollup/rollup-win32-x64-msvc@npm:4.53.3" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + "@sidebase/nuxt-auth@npm:1.1.0": version: 1.1.0 resolution: "@sidebase/nuxt-auth@npm:1.1.0" @@ -3148,6 +4032,13 @@ __metadata: languageName: node linkType: hard +"@sindresorhus/base62@npm:^1.0.0": + version: 1.0.0 + resolution: "@sindresorhus/base62@npm:1.0.0" + checksum: 10c0/9a14df0f058fdf4731c30f0f05728a4822144ee42236030039d7fa5a1a1072c2879feba8091fd4a17c8922d1056bc07bada77c31fddc3e15836fc05a266fd918 + languageName: node + linkType: hard + "@sindresorhus/is@npm:^7.0.2": version: 7.1.0 resolution: "@sindresorhus/is@npm:7.1.0" @@ -3162,6 +4053,13 @@ __metadata: languageName: node linkType: hard +"@sindresorhus/merge-streams@npm:^4.0.0": + version: 4.0.0 + resolution: "@sindresorhus/merge-streams@npm:4.0.0" + checksum: 10c0/482ee543629aa1933b332f811a1ae805a213681ecdd98c042b1c1b89387df63e7812248bb4df3910b02b3cc5589d3d73e4393f30e197c9dde18046ccd471fc6b + languageName: node + linkType: hard + "@somushq/vue3-friendly-captcha@npm:1.0.2": version: 1.0.2 resolution: "@somushq/vue3-friendly-captcha@npm:1.0.2" @@ -3180,6 +4078,13 @@ __metadata: languageName: node linkType: hard +"@speed-highlight/core@npm:^1.2.9": + version: 1.2.12 + resolution: "@speed-highlight/core@npm:1.2.12" + checksum: 10c0/37613c7a031af3b239282cc09211cefc1c9e164df07110520d2116ae7b4741512c1905bf1e011706ee2e652cef24df834f2068c4c4fff71e0257ec22fa655ff2 + languageName: node + linkType: hard + "@standard-schema/spec@npm:^1.0.0": version: 1.0.0 resolution: "@standard-schema/spec@npm:1.0.0" @@ -3187,146 +4092,144 @@ __metadata: languageName: node linkType: hard -"@stylistic/eslint-plugin@npm:^5.2.3": - version: 5.4.0 - resolution: "@stylistic/eslint-plugin@npm:5.4.0" +"@stylistic/eslint-plugin@npm:^5.5.0": + version: 5.6.1 + resolution: "@stylistic/eslint-plugin@npm:5.6.1" dependencies: "@eslint-community/eslint-utils": "npm:^4.9.0" - "@typescript-eslint/types": "npm:^8.44.0" + "@typescript-eslint/types": "npm:^8.47.0" eslint-visitor-keys: "npm:^4.2.1" espree: "npm:^10.4.0" estraverse: "npm:^5.3.0" picomatch: "npm:^4.0.3" peerDependencies: eslint: ">=9.0.0" - checksum: 10c0/02db4ec387c75300f07417641fb26eb41fd2a202608d1d752ed799cb72a8cea270abcc0a36eafa2ab7488e8cbe5a51e778afa56100f69ade572d1ec4051e8883 + checksum: 10c0/dfd33107209dac554a6b88d40813bfadd938e901ee7b853dfff9b66c9ffe0601790591865ad67e05f1f1061fc3c958087ad25efc1117375fe2487bfa69c44352 languageName: node linkType: hard -"@tailwindcss/node@npm:4.1.14": - version: 4.1.14 - resolution: "@tailwindcss/node@npm:4.1.14" +"@tailwindcss/node@npm:4.1.17": + version: 4.1.17 + resolution: "@tailwindcss/node@npm:4.1.17" dependencies: "@jridgewell/remapping": "npm:^2.3.4" enhanced-resolve: "npm:^5.18.3" - jiti: "npm:^2.6.0" - lightningcss: "npm:1.30.1" - magic-string: "npm:^0.30.19" + jiti: "npm:^2.6.1" + lightningcss: "npm:1.30.2" + magic-string: "npm:^0.30.21" source-map-js: "npm:^1.2.1" - tailwindcss: "npm:4.1.14" - checksum: 10c0/dcdb53217534b5220e8ffd0357848b542935aa5ebcae691ff1ac2924c5c0b89d6150d938ff69c776d9835e53fdb29b6db78367930985bd50b367ddb646778239 + tailwindcss: "npm:4.1.17" + checksum: 10c0/80b542e9b7eb09499dd14d65fd7d9544321d6bcdc00d29914396001d00e009906392cf493d20cc655dfd42769c823060cb9bf2eacacb43838a47e897634a446b languageName: node linkType: hard -"@tailwindcss/oxide-android-arm64@npm:4.1.14": - version: 4.1.14 - resolution: "@tailwindcss/oxide-android-arm64@npm:4.1.14" +"@tailwindcss/oxide-android-arm64@npm:4.1.17": + version: 4.1.17 + resolution: "@tailwindcss/oxide-android-arm64@npm:4.1.17" conditions: os=android & cpu=arm64 languageName: node linkType: hard -"@tailwindcss/oxide-darwin-arm64@npm:4.1.14": - version: 4.1.14 - resolution: "@tailwindcss/oxide-darwin-arm64@npm:4.1.14" +"@tailwindcss/oxide-darwin-arm64@npm:4.1.17": + version: 4.1.17 + resolution: "@tailwindcss/oxide-darwin-arm64@npm:4.1.17" conditions: os=darwin & cpu=arm64 languageName: node linkType: hard -"@tailwindcss/oxide-darwin-x64@npm:4.1.14": - version: 4.1.14 - resolution: "@tailwindcss/oxide-darwin-x64@npm:4.1.14" +"@tailwindcss/oxide-darwin-x64@npm:4.1.17": + version: 4.1.17 + resolution: "@tailwindcss/oxide-darwin-x64@npm:4.1.17" conditions: os=darwin & cpu=x64 languageName: node linkType: hard -"@tailwindcss/oxide-freebsd-x64@npm:4.1.14": - version: 4.1.14 - resolution: "@tailwindcss/oxide-freebsd-x64@npm:4.1.14" +"@tailwindcss/oxide-freebsd-x64@npm:4.1.17": + version: 4.1.17 + resolution: "@tailwindcss/oxide-freebsd-x64@npm:4.1.17" conditions: os=freebsd & cpu=x64 languageName: node linkType: hard -"@tailwindcss/oxide-linux-arm-gnueabihf@npm:4.1.14": - version: 4.1.14 - resolution: "@tailwindcss/oxide-linux-arm-gnueabihf@npm:4.1.14" +"@tailwindcss/oxide-linux-arm-gnueabihf@npm:4.1.17": + version: 4.1.17 + resolution: "@tailwindcss/oxide-linux-arm-gnueabihf@npm:4.1.17" conditions: os=linux & cpu=arm languageName: node linkType: hard -"@tailwindcss/oxide-linux-arm64-gnu@npm:4.1.14": - version: 4.1.14 - resolution: "@tailwindcss/oxide-linux-arm64-gnu@npm:4.1.14" +"@tailwindcss/oxide-linux-arm64-gnu@npm:4.1.17": + version: 4.1.17 + resolution: "@tailwindcss/oxide-linux-arm64-gnu@npm:4.1.17" conditions: os=linux & cpu=arm64 & libc=glibc languageName: node linkType: hard -"@tailwindcss/oxide-linux-arm64-musl@npm:4.1.14": - version: 4.1.14 - resolution: "@tailwindcss/oxide-linux-arm64-musl@npm:4.1.14" +"@tailwindcss/oxide-linux-arm64-musl@npm:4.1.17": + version: 4.1.17 + resolution: "@tailwindcss/oxide-linux-arm64-musl@npm:4.1.17" conditions: os=linux & cpu=arm64 & libc=musl languageName: node linkType: hard -"@tailwindcss/oxide-linux-x64-gnu@npm:4.1.14": - version: 4.1.14 - resolution: "@tailwindcss/oxide-linux-x64-gnu@npm:4.1.14" +"@tailwindcss/oxide-linux-x64-gnu@npm:4.1.17": + version: 4.1.17 + resolution: "@tailwindcss/oxide-linux-x64-gnu@npm:4.1.17" conditions: os=linux & cpu=x64 & libc=glibc languageName: node linkType: hard -"@tailwindcss/oxide-linux-x64-musl@npm:4.1.14": - version: 4.1.14 - resolution: "@tailwindcss/oxide-linux-x64-musl@npm:4.1.14" +"@tailwindcss/oxide-linux-x64-musl@npm:4.1.17": + version: 4.1.17 + resolution: "@tailwindcss/oxide-linux-x64-musl@npm:4.1.17" conditions: os=linux & cpu=x64 & libc=musl languageName: node linkType: hard -"@tailwindcss/oxide-wasm32-wasi@npm:4.1.14": - version: 4.1.14 - resolution: "@tailwindcss/oxide-wasm32-wasi@npm:4.1.14" +"@tailwindcss/oxide-wasm32-wasi@npm:4.1.17": + version: 4.1.17 + resolution: "@tailwindcss/oxide-wasm32-wasi@npm:4.1.17" dependencies: - "@emnapi/core": "npm:^1.5.0" - "@emnapi/runtime": "npm:^1.5.0" + "@emnapi/core": "npm:^1.6.0" + "@emnapi/runtime": "npm:^1.6.0" "@emnapi/wasi-threads": "npm:^1.1.0" - "@napi-rs/wasm-runtime": "npm:^1.0.5" + "@napi-rs/wasm-runtime": "npm:^1.0.7" "@tybys/wasm-util": "npm:^0.10.1" tslib: "npm:^2.4.0" conditions: cpu=wasm32 languageName: node linkType: hard -"@tailwindcss/oxide-win32-arm64-msvc@npm:4.1.14": - version: 4.1.14 - resolution: "@tailwindcss/oxide-win32-arm64-msvc@npm:4.1.14" +"@tailwindcss/oxide-win32-arm64-msvc@npm:4.1.17": + version: 4.1.17 + resolution: "@tailwindcss/oxide-win32-arm64-msvc@npm:4.1.17" conditions: os=win32 & cpu=arm64 languageName: node linkType: hard -"@tailwindcss/oxide-win32-x64-msvc@npm:4.1.14": - version: 4.1.14 - resolution: "@tailwindcss/oxide-win32-x64-msvc@npm:4.1.14" +"@tailwindcss/oxide-win32-x64-msvc@npm:4.1.17": + version: 4.1.17 + resolution: "@tailwindcss/oxide-win32-x64-msvc@npm:4.1.17" conditions: os=win32 & cpu=x64 languageName: node linkType: hard -"@tailwindcss/oxide@npm:4.1.14": - version: 4.1.14 - resolution: "@tailwindcss/oxide@npm:4.1.14" - dependencies: - "@tailwindcss/oxide-android-arm64": "npm:4.1.14" - "@tailwindcss/oxide-darwin-arm64": "npm:4.1.14" - "@tailwindcss/oxide-darwin-x64": "npm:4.1.14" - "@tailwindcss/oxide-freebsd-x64": "npm:4.1.14" - "@tailwindcss/oxide-linux-arm-gnueabihf": "npm:4.1.14" - "@tailwindcss/oxide-linux-arm64-gnu": "npm:4.1.14" - "@tailwindcss/oxide-linux-arm64-musl": "npm:4.1.14" - "@tailwindcss/oxide-linux-x64-gnu": "npm:4.1.14" - "@tailwindcss/oxide-linux-x64-musl": "npm:4.1.14" - "@tailwindcss/oxide-wasm32-wasi": "npm:4.1.14" - "@tailwindcss/oxide-win32-arm64-msvc": "npm:4.1.14" - "@tailwindcss/oxide-win32-x64-msvc": "npm:4.1.14" - detect-libc: "npm:^2.0.4" - tar: "npm:^7.5.1" +"@tailwindcss/oxide@npm:4.1.17": + version: 4.1.17 + resolution: "@tailwindcss/oxide@npm:4.1.17" + dependencies: + "@tailwindcss/oxide-android-arm64": "npm:4.1.17" + "@tailwindcss/oxide-darwin-arm64": "npm:4.1.17" + "@tailwindcss/oxide-darwin-x64": "npm:4.1.17" + "@tailwindcss/oxide-freebsd-x64": "npm:4.1.17" + "@tailwindcss/oxide-linux-arm-gnueabihf": "npm:4.1.17" + "@tailwindcss/oxide-linux-arm64-gnu": "npm:4.1.17" + "@tailwindcss/oxide-linux-arm64-musl": "npm:4.1.17" + "@tailwindcss/oxide-linux-x64-gnu": "npm:4.1.17" + "@tailwindcss/oxide-linux-x64-musl": "npm:4.1.17" + "@tailwindcss/oxide-wasm32-wasi": "npm:4.1.17" + "@tailwindcss/oxide-win32-arm64-msvc": "npm:4.1.17" + "@tailwindcss/oxide-win32-x64-msvc": "npm:4.1.17" dependenciesMeta: "@tailwindcss/oxide-android-arm64": optional: true @@ -3352,20 +4255,20 @@ __metadata: optional: true "@tailwindcss/oxide-win32-x64-msvc": optional: true - checksum: 10c0/7fdf5345272d0348624cd003f431f10715372d585f0180d32d3c8dd18f5417cdfe7e8c4e86fc504fa1aefd19324fb4c4b174bbefdc054882ae6919ed1160d86c + checksum: 10c0/cdd292760dde90976ac5cd486600687f9ac4043d9796001b356d43bfc4d0e1972d23844fe045970afdc4b4cda8451f262db15a9da4152c26e2b696a985e3686c languageName: node linkType: hard -"@tailwindcss/vite@npm:4.1.14": - version: 4.1.14 - resolution: "@tailwindcss/vite@npm:4.1.14" +"@tailwindcss/vite@npm:4.1.17": + version: 4.1.17 + resolution: "@tailwindcss/vite@npm:4.1.17" dependencies: - "@tailwindcss/node": "npm:4.1.14" - "@tailwindcss/oxide": "npm:4.1.14" - tailwindcss: "npm:4.1.14" + "@tailwindcss/node": "npm:4.1.17" + "@tailwindcss/oxide": "npm:4.1.17" + tailwindcss: "npm:4.1.17" peerDependencies: vite: ^5.2.0 || ^6 || ^7 - checksum: 10c0/38a34602a29fcad23eb80bdb6f3162473d191a1d8ec0bffdee73a1c7b9716de13a1c3705b95baf40eed6ed70e806df35ba03f3cfe541f1758a3440ddb03b6e81 + checksum: 10c0/47d9bdfb7bf7d2df0661b50e91656779863146cca97571e21e2c3f9351f468c27cbc7ed1d1d6c373f1e721dca66d32a3f12f77e9d3e74bed344e27afec199ad3 languageName: node linkType: hard @@ -3882,12 +4785,12 @@ __metadata: languageName: node linkType: hard -"@types/node@npm:24.6.2": - version: 24.6.2 - resolution: "@types/node@npm:24.6.2" +"@types/node@npm:24.10.1": + version: 24.10.1 + resolution: "@types/node@npm:24.10.1" dependencies: - undici-types: "npm:~7.13.0" - checksum: 10c0/d029757711be85ec468686f66cd8eca78f5996d7e2b1a5b818436e0299b19925b0fb4f7509a6b62750abdc72d66f5750ce22fb8b55559baca86df89a9c44722e + undici-types: "npm:~7.16.0" + checksum: 10c0/d6bca7a78f550fbb376f236f92b405d676003a8a09a1b411f55920ef34286ee3ee51f566203920e835478784df52662b5b2af89159d9d319352e9ea21801c002 languageName: node linkType: hard @@ -3949,40 +4852,40 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/eslint-plugin@npm:^8.39.1": - version: 8.45.0 - resolution: "@typescript-eslint/eslint-plugin@npm:8.45.0" +"@typescript-eslint/eslint-plugin@npm:^8.46.2": + version: 8.48.0 + resolution: "@typescript-eslint/eslint-plugin@npm:8.48.0" dependencies: "@eslint-community/regexpp": "npm:^4.10.0" - "@typescript-eslint/scope-manager": "npm:8.45.0" - "@typescript-eslint/type-utils": "npm:8.45.0" - "@typescript-eslint/utils": "npm:8.45.0" - "@typescript-eslint/visitor-keys": "npm:8.45.0" + "@typescript-eslint/scope-manager": "npm:8.48.0" + "@typescript-eslint/type-utils": "npm:8.48.0" + "@typescript-eslint/utils": "npm:8.48.0" + "@typescript-eslint/visitor-keys": "npm:8.48.0" graphemer: "npm:^1.4.0" ignore: "npm:^7.0.0" natural-compare: "npm:^1.4.0" ts-api-utils: "npm:^2.1.0" peerDependencies: - "@typescript-eslint/parser": ^8.45.0 + "@typescript-eslint/parser": ^8.48.0 eslint: ^8.57.0 || ^9.0.0 typescript: ">=4.8.4 <6.0.0" - checksum: 10c0/0c60a0e5d07fa8618348db38b5a81e66143d528e1b3cdb5678bbc6c60590cd559b27c98c36f5663230fc4cf6920dff2cd604de30b58df26a37fcfcc5dc1dbd45 + checksum: 10c0/5f4f9ac3ace3f615bac428859026b70fb7fa236666cfe8856fed3add7e4ba73c7113264c2df7a9d68247b679dfcc21b0414488bda7b9b3de1c209b1807ed7842 languageName: node linkType: hard -"@typescript-eslint/parser@npm:^8.39.1": - version: 8.45.0 - resolution: "@typescript-eslint/parser@npm:8.45.0" +"@typescript-eslint/parser@npm:^8.46.2": + version: 8.48.0 + resolution: "@typescript-eslint/parser@npm:8.48.0" dependencies: - "@typescript-eslint/scope-manager": "npm:8.45.0" - "@typescript-eslint/types": "npm:8.45.0" - "@typescript-eslint/typescript-estree": "npm:8.45.0" - "@typescript-eslint/visitor-keys": "npm:8.45.0" + "@typescript-eslint/scope-manager": "npm:8.48.0" + "@typescript-eslint/types": "npm:8.48.0" + "@typescript-eslint/typescript-estree": "npm:8.48.0" + "@typescript-eslint/visitor-keys": "npm:8.48.0" debug: "npm:^4.3.4" peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: ">=4.8.4 <6.0.0" - checksum: 10c0/8b419bcf795b112a39fcac05dcf147835059345b6399035ffa3f76a9d8e320f3fac79cae2fe4320dcda83fa059b017ca7626a7b4e3da08a614415c8867d169b8 + checksum: 10c0/180753e1dc55cd5174a236b738d3b0dd6dd6c131797cd417b3b3b8fac344168f3d21bd49eae6c0a075be29ed69b7bc74d97cadd917f1f4d4c113c29e76c1f9cd languageName: node linkType: hard @@ -3999,7 +4902,30 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/scope-manager@npm:8.45.0, @typescript-eslint/scope-manager@npm:^8.13.0": +"@typescript-eslint/project-service@npm:8.48.0": + version: 8.48.0 + resolution: "@typescript-eslint/project-service@npm:8.48.0" + dependencies: + "@typescript-eslint/tsconfig-utils": "npm:^8.48.0" + "@typescript-eslint/types": "npm:^8.48.0" + debug: "npm:^4.3.4" + peerDependencies: + typescript: ">=4.8.4 <6.0.0" + checksum: 10c0/6e1d08312fe55a91ba37eb19131af91ad7834bafd15d1cddb83a1e35e5134382e10dc0b14531036ba1c075ce4cba627123625ed6f2e209fb3355f3dda25da0a1 + languageName: node + linkType: hard + +"@typescript-eslint/scope-manager@npm:8.48.0": + version: 8.48.0 + resolution: "@typescript-eslint/scope-manager@npm:8.48.0" + dependencies: + "@typescript-eslint/types": "npm:8.48.0" + "@typescript-eslint/visitor-keys": "npm:8.48.0" + checksum: 10c0/0766e365901a8af9d9e41fa70464254aacf8b4d167734d88b6cdaa0235e86bfdffc57a3e39a20e105929b8df499d252090f64f81f86770f74626ca809afe54b6 + languageName: node + linkType: hard + +"@typescript-eslint/scope-manager@npm:^8.13.0": version: 8.45.0 resolution: "@typescript-eslint/scope-manager@npm:8.45.0" dependencies: @@ -4018,30 +4944,65 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/type-utils@npm:8.45.0": - version: 8.45.0 - resolution: "@typescript-eslint/type-utils@npm:8.45.0" +"@typescript-eslint/tsconfig-utils@npm:8.48.0, @typescript-eslint/tsconfig-utils@npm:^8.48.0": + version: 8.48.0 + resolution: "@typescript-eslint/tsconfig-utils@npm:8.48.0" + peerDependencies: + typescript: ">=4.8.4 <6.0.0" + checksum: 10c0/52e9ce8ffbaf32f3c6f4b8fa8af6e3901c430411e137a0baf650fcefdd8edf3dcc4569eba726a28424471d4d1d96b815aa4cf7b63aa7b67380efd6a8dd354222 + languageName: node + linkType: hard + +"@typescript-eslint/type-utils@npm:8.48.0": + version: 8.48.0 + resolution: "@typescript-eslint/type-utils@npm:8.48.0" dependencies: - "@typescript-eslint/types": "npm:8.45.0" - "@typescript-eslint/typescript-estree": "npm:8.45.0" - "@typescript-eslint/utils": "npm:8.45.0" + "@typescript-eslint/types": "npm:8.48.0" + "@typescript-eslint/typescript-estree": "npm:8.48.0" + "@typescript-eslint/utils": "npm:8.48.0" debug: "npm:^4.3.4" ts-api-utils: "npm:^2.1.0" peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: ">=4.8.4 <6.0.0" - checksum: 10c0/ce0f4c209c2418ebeb65e7de053499fb68bf6000bdd71068594fdb8c8ac3dbbd62935a3cea233989491f7da3ef5db87e7efd2910133c6abf6d0cbf57248f6442 + checksum: 10c0/72ab5c7d183b844e4870bfa5dfeb68e2e7ce5f3e1b33c06d5a8e70f0d0a012c9152ad15071d41ba3788266109804a9f4cdb85d664b11df8948bc930e29e0c244 languageName: node linkType: hard -"@typescript-eslint/types@npm:8.45.0, @typescript-eslint/types@npm:^8.34.0, @typescript-eslint/types@npm:^8.34.1, @typescript-eslint/types@npm:^8.35.0, @typescript-eslint/types@npm:^8.39.1, @typescript-eslint/types@npm:^8.42.0, @typescript-eslint/types@npm:^8.44.0, @typescript-eslint/types@npm:^8.45.0": +"@typescript-eslint/types@npm:8.45.0, @typescript-eslint/types@npm:^8.34.0, @typescript-eslint/types@npm:^8.35.0, @typescript-eslint/types@npm:^8.45.0": version: 8.45.0 resolution: "@typescript-eslint/types@npm:8.45.0" checksum: 10c0/0213a0573c671d13bc91961a2b2e814ec7f6381ff093bce6704017bd96b2fc7fee25906c815cedb32a0601cf5071ca6c7c5f940d087c3b0d3dd7d4bc03478278 languageName: node linkType: hard -"@typescript-eslint/typescript-estree@npm:8.45.0, @typescript-eslint/typescript-estree@npm:^8.13.0": +"@typescript-eslint/types@npm:8.48.0, @typescript-eslint/types@npm:^8.38.0, @typescript-eslint/types@npm:^8.46.0, @typescript-eslint/types@npm:^8.46.2, @typescript-eslint/types@npm:^8.47.0, @typescript-eslint/types@npm:^8.48.0": + version: 8.48.0 + resolution: "@typescript-eslint/types@npm:8.48.0" + checksum: 10c0/865a8f4ae4a50aa8976f3d7e0f874f1a1c80227ec53ded68644d41011c729a489bb59f70683b29237ab945716ea0258e1d47387163379eab3edaaf5e5cc3b757 + languageName: node + linkType: hard + +"@typescript-eslint/typescript-estree@npm:8.48.0": + version: 8.48.0 + resolution: "@typescript-eslint/typescript-estree@npm:8.48.0" + dependencies: + "@typescript-eslint/project-service": "npm:8.48.0" + "@typescript-eslint/tsconfig-utils": "npm:8.48.0" + "@typescript-eslint/types": "npm:8.48.0" + "@typescript-eslint/visitor-keys": "npm:8.48.0" + debug: "npm:^4.3.4" + minimatch: "npm:^9.0.4" + semver: "npm:^7.6.0" + tinyglobby: "npm:^0.2.15" + ts-api-utils: "npm:^2.1.0" + peerDependencies: + typescript: ">=4.8.4 <6.0.0" + checksum: 10c0/f17dd35f7b82654fae9fe83c2eb650572464dbce0170d55b3ef94b99e9aae010f2cbadd436089c8e59eef97d41719ace3a2deb4ac3cdfac26d43b36f34df5590 + languageName: node + linkType: hard + +"@typescript-eslint/typescript-estree@npm:^8.13.0": version: 8.45.0 resolution: "@typescript-eslint/typescript-estree@npm:8.45.0" dependencies: @@ -4061,18 +5022,18 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/utils@npm:8.45.0, @typescript-eslint/utils@npm:^8.34.1, @typescript-eslint/utils@npm:^8.39.1": - version: 8.45.0 - resolution: "@typescript-eslint/utils@npm:8.45.0" +"@typescript-eslint/utils@npm:8.48.0, @typescript-eslint/utils@npm:^8.38.0, @typescript-eslint/utils@npm:^8.46.2": + version: 8.48.0 + resolution: "@typescript-eslint/utils@npm:8.48.0" dependencies: "@eslint-community/eslint-utils": "npm:^4.7.0" - "@typescript-eslint/scope-manager": "npm:8.45.0" - "@typescript-eslint/types": "npm:8.45.0" - "@typescript-eslint/typescript-estree": "npm:8.45.0" + "@typescript-eslint/scope-manager": "npm:8.48.0" + "@typescript-eslint/types": "npm:8.48.0" + "@typescript-eslint/typescript-estree": "npm:8.48.0" peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: ">=4.8.4 <6.0.0" - checksum: 10c0/b3c83a23813b15e20e303d7153789508c01e06dec355b1a80547c59aa36998d498102f45fcd13f111031fac57270608abb04d20560248d4448fd00b1cf4dc4ab + checksum: 10c0/56334312d1dc114a5c8b05dac4da191c40a416a5705fa76797ebdc9f6a96d35727fd0993cf8776f5c4411837e5fc2151bfa61d3eecc98b24f5a821a63a4d56f3 languageName: node linkType: hard @@ -4086,288 +5047,298 @@ __metadata: languageName: node linkType: hard -"@unhead/vue@npm:^2.0.14": - version: 2.0.17 - resolution: "@unhead/vue@npm:2.0.17" +"@typescript-eslint/visitor-keys@npm:8.48.0": + version: 8.48.0 + resolution: "@typescript-eslint/visitor-keys@npm:8.48.0" + dependencies: + "@typescript-eslint/types": "npm:8.48.0" + eslint-visitor-keys: "npm:^4.2.1" + checksum: 10c0/20ae9ec255a786de40cdba281b63f634a642dcc34d2a79c5ffc160109f7f6227c28ae2c64be32cbc53dc68dc398c3da715bfcce90422b5024f15f7124a3c1704 + languageName: node + linkType: hard + +"@unhead/vue@npm:^2.0.19": + version: 2.0.19 + resolution: "@unhead/vue@npm:2.0.19" dependencies: hookable: "npm:^5.5.3" - unhead: "npm:2.0.17" + unhead: "npm:2.0.19" peerDependencies: vue: ">=3.5.18" - checksum: 10c0/b4fa62eb8565d98c570fa151add62fa3373a1c860862e146196488c938eff8b68adac35c65eb4297ed048e3c6265f861016f39c95dff818c30c962f38c058de4 + checksum: 10c0/a8acb37e03a2ae0d2a59960c5c894d620e9d61c538e10e5dcc668c1333f3f6a4390aa88300c6c6ffd306276e6fef38fd93cb79d9d36f9fd79bdc206899281d1a languageName: node linkType: hard -"@unocss/astro@npm:66.5.2": - version: 66.5.2 - resolution: "@unocss/astro@npm:66.5.2" +"@unocss/astro@npm:66.5.9": + version: 66.5.9 + resolution: "@unocss/astro@npm:66.5.9" dependencies: - "@unocss/core": "npm:66.5.2" - "@unocss/reset": "npm:66.5.2" - "@unocss/vite": "npm:66.5.2" + "@unocss/core": "npm:66.5.9" + "@unocss/reset": "npm:66.5.9" + "@unocss/vite": "npm:66.5.9" peerDependencies: vite: ^2.9.0 || ^3.0.0-0 || ^4.0.0 || ^5.0.0-0 || ^6.0.0-0 || ^7.0.0-0 peerDependenciesMeta: vite: optional: true - checksum: 10c0/f45c4bf661c87e8c1ae91b4308acbec2a273f6df665ae2c4f9571ebfed5a8faa801651cf2d1819f6cdd6f4fd69b6a881d5e1dc22c3740544df83e1359898cd89 + checksum: 10c0/24bdf63d0f7917f2e61908e911d07b5b13d67ef997f497a844a96f70b7b30cce6a15fd26c78e2d1bc861ec221d2585662f0979495897ffac12a65e73aa1b51df languageName: node linkType: hard -"@unocss/cli@npm:66.5.2": - version: 66.5.2 - resolution: "@unocss/cli@npm:66.5.2" +"@unocss/cli@npm:66.5.9": + version: 66.5.9 + resolution: "@unocss/cli@npm:66.5.9" dependencies: "@jridgewell/remapping": "npm:^2.3.5" - "@unocss/config": "npm:66.5.2" - "@unocss/core": "npm:66.5.2" - "@unocss/preset-uno": "npm:66.5.2" + "@unocss/config": "npm:66.5.9" + "@unocss/core": "npm:66.5.9" + "@unocss/preset-uno": "npm:66.5.9" cac: "npm:^6.7.14" chokidar: "npm:^3.6.0" colorette: "npm:^2.0.20" consola: "npm:^3.4.2" - magic-string: "npm:^0.30.18" + magic-string: "npm:^0.30.21" pathe: "npm:^2.0.3" perfect-debounce: "npm:^1.0.0" - tinyglobby: "npm:^0.2.14" - unplugin-utils: "npm:^0.3.0" + tinyglobby: "npm:^0.2.15" + unplugin-utils: "npm:^0.3.1" bin: unocss: bin/unocss.mjs - checksum: 10c0/87677922acd11f34acf03068fac6a073582f1f2b24ee692e844fe01d4c43d2b914ecb78af5caf0b38346d1d32c319830e0cdf4fe4f925d682a641b5ad52d3909 + checksum: 10c0/22e2b81ac372355ffe63273e7109afcba8f4a151a831cf8929458ac0aba9b43c2f2743c2f410a1aba4ed4579093552dd508936751cc51f92e8eb2b42dedcda6a languageName: node linkType: hard -"@unocss/config@npm:66.5.2": - version: 66.5.2 - resolution: "@unocss/config@npm:66.5.2" +"@unocss/config@npm:66.5.9": + version: 66.5.9 + resolution: "@unocss/config@npm:66.5.9" dependencies: - "@unocss/core": "npm:66.5.2" - unconfig: "npm:^7.3.3" - checksum: 10c0/2978a80e97b84ea510d10172e52e444b2226c45e2db32df4d2d2374e8543fc2d674e05f4a06c3179e59e4857deabb34eeeca40dc7fe0b5b216995943f6822d32 + "@unocss/core": "npm:66.5.9" + unconfig: "npm:^7.4.1" + checksum: 10c0/474581a945b939ce1761dfd10d5cd25359f4c96080bef51bdaf45dace8a91b4f5604ffc5b1b9eec194cc22274195d74f603e33cc2767071994ec396fa5e6a0ea languageName: node linkType: hard -"@unocss/core@npm:66.5.2, @unocss/core@npm:^66.5.2": - version: 66.5.2 - resolution: "@unocss/core@npm:66.5.2" - checksum: 10c0/c07b6f0873838f6e6ebeaf9b91173e2e9d963933c1461b4ab56987af489772d694a134559c65f4ed93fdd32ecb6f9a5d634f1d39da1467b3b43c3bfd835a72d6 +"@unocss/core@npm:66.5.9, @unocss/core@npm:^66.5.9": + version: 66.5.9 + resolution: "@unocss/core@npm:66.5.9" + checksum: 10c0/5eaf0e9b7b1ee1897e6f2bd7cfedfabb04feb90ebf0ecaefd39318db9c5d052ee74d0bd8b0342e56f40bf489300c12a6bda6f454b4ee4c0c11ec59187c6a0d28 languageName: node linkType: hard -"@unocss/extractor-arbitrary-variants@npm:66.5.2": - version: 66.5.2 - resolution: "@unocss/extractor-arbitrary-variants@npm:66.5.2" +"@unocss/extractor-arbitrary-variants@npm:66.5.9": + version: 66.5.9 + resolution: "@unocss/extractor-arbitrary-variants@npm:66.5.9" dependencies: - "@unocss/core": "npm:66.5.2" - checksum: 10c0/4fb30d200fb5d5f3f7ea9ae836e9ae2beb746592134d37049f65e73fdd89f9c3ec167d360b5674f28c961a33b966ba6a4663839e645c05b54005d290259a1d79 + "@unocss/core": "npm:66.5.9" + checksum: 10c0/236abcd173b65f4dbfbe20dc5c536c0f37dc8c75a884e52173e78c82cd65a7e78d3371265c7613ef31c1ff9fd004152bc320cee9e728dc74e6525c450a2bcbe8 languageName: node linkType: hard -"@unocss/inspector@npm:66.5.2": - version: 66.5.2 - resolution: "@unocss/inspector@npm:66.5.2" +"@unocss/inspector@npm:66.5.9": + version: 66.5.9 + resolution: "@unocss/inspector@npm:66.5.9" dependencies: - "@unocss/core": "npm:66.5.2" - "@unocss/rule-utils": "npm:66.5.2" + "@unocss/core": "npm:66.5.9" + "@unocss/rule-utils": "npm:66.5.9" colorette: "npm:^2.0.20" gzip-size: "npm:^6.0.0" - sirv: "npm:^3.0.1" + sirv: "npm:^3.0.2" vue-flow-layout: "npm:^0.2.0" - checksum: 10c0/a05dea1fc834dab1e45298dd7067303444cd6f80aea0311054e9ea57f40d3f89e85b1b8e114ae9b4888002109fb9765be6003b5bbf325dd20225772cb42e5bb1 + checksum: 10c0/dd3499eeab6720c57455b7bef836aaf3c768358414e2189dc30569e8f34b805e9238621cef3969cbcaf858864c411075f545d49775501b73e74e859f3a5793d6 languageName: node linkType: hard -"@unocss/postcss@npm:66.5.2": - version: 66.5.2 - resolution: "@unocss/postcss@npm:66.5.2" +"@unocss/postcss@npm:66.5.9": + version: 66.5.9 + resolution: "@unocss/postcss@npm:66.5.9" dependencies: - "@unocss/config": "npm:66.5.2" - "@unocss/core": "npm:66.5.2" - "@unocss/rule-utils": "npm:66.5.2" + "@unocss/config": "npm:66.5.9" + "@unocss/core": "npm:66.5.9" + "@unocss/rule-utils": "npm:66.5.9" css-tree: "npm:^3.1.0" postcss: "npm:^8.5.6" - tinyglobby: "npm:^0.2.14" + tinyglobby: "npm:^0.2.15" peerDependencies: postcss: ^8.4.21 - checksum: 10c0/02f05e94e0bf9c2562a63958874bd8e0540046b60e9f8bc54d88e2d1ab10814e5e18c8671c65b8b56638f0588896264da020fe8138ef066071d813f4b5e8e621 + checksum: 10c0/1b36fba040c080cf238e1b54d36b4ef23a2db4cc026fcbb8d68ef31141d9f69f15e079210790d32d652da2576e7dd2760a9ce27074774b7fc17e0327e9e37fe3 languageName: node linkType: hard -"@unocss/preset-attributify@npm:66.5.2": - version: 66.5.2 - resolution: "@unocss/preset-attributify@npm:66.5.2" +"@unocss/preset-attributify@npm:66.5.9": + version: 66.5.9 + resolution: "@unocss/preset-attributify@npm:66.5.9" dependencies: - "@unocss/core": "npm:66.5.2" - checksum: 10c0/5d62f22d966905201ab947747ab1cb989ffe6a8692f65d22cfc4302daa56aa94fbdaeecc99cabc8b143d6b8c0d72c944127952f12722c209f33410d26c9f218f + "@unocss/core": "npm:66.5.9" + checksum: 10c0/45e4d254a67eda6c178d80f7a8f1c01479a0ffa4809c8cd2153d2cfca48597f6574f8fdb55b871dc086a41ead389750501a07ab8297035776d910f9ba661b13b languageName: node linkType: hard -"@unocss/preset-icons@npm:66.5.2": - version: 66.5.2 - resolution: "@unocss/preset-icons@npm:66.5.2" +"@unocss/preset-icons@npm:66.5.9": + version: 66.5.9 + resolution: "@unocss/preset-icons@npm:66.5.9" dependencies: - "@iconify/utils": "npm:^3.0.1" - "@unocss/core": "npm:66.5.2" - ofetch: "npm:^1.4.1" - checksum: 10c0/92b681c10ba333a3808df13d386412633e3bb99b80dc7b975ca102ff019563023362dcb2a57c714ce9aaf4136f31f55c9eeb48c7720954d291d797d1ee60b081 + "@iconify/utils": "npm:^3.0.2" + "@unocss/core": "npm:66.5.9" + ofetch: "npm:^1.5.1" + checksum: 10c0/62a5aec7b8155b2aa2b77beb12034e02e6acbcf5691ad4713bb31341a0782d136123083c526688bf011fb5e0711f5e8aebd74fd0211c0ae191c47dd6c6df97a7 languageName: node linkType: hard -"@unocss/preset-mini@npm:66.5.2": - version: 66.5.2 - resolution: "@unocss/preset-mini@npm:66.5.2" +"@unocss/preset-mini@npm:66.5.9": + version: 66.5.9 + resolution: "@unocss/preset-mini@npm:66.5.9" dependencies: - "@unocss/core": "npm:66.5.2" - "@unocss/extractor-arbitrary-variants": "npm:66.5.2" - "@unocss/rule-utils": "npm:66.5.2" - checksum: 10c0/f87a965fc8a55bdcc636d6908eb38bc69d0b08463c81f2b1413a3d895f893b5a0e1c9ef3aeb336bab6296d3de09db39175637c5fe68f3b3bdce523d7773350b8 + "@unocss/core": "npm:66.5.9" + "@unocss/extractor-arbitrary-variants": "npm:66.5.9" + "@unocss/rule-utils": "npm:66.5.9" + checksum: 10c0/97bd53ef24efe7213e6a62d62d4b1fe26caf1a9230fcad9f5d5af1782c8f486c8771035c642dad05cdf3245eb4fc23fa498debd3a8048486144fc5b86811bd8d languageName: node linkType: hard -"@unocss/preset-tagify@npm:66.5.2": - version: 66.5.2 - resolution: "@unocss/preset-tagify@npm:66.5.2" +"@unocss/preset-tagify@npm:66.5.9": + version: 66.5.9 + resolution: "@unocss/preset-tagify@npm:66.5.9" dependencies: - "@unocss/core": "npm:66.5.2" - checksum: 10c0/784b1dde2d38876e821a25e7bfdf8b304f2ac4c0e145a16d6ed859547cdd1584f1a36ee314e89bfec53a01ffa0190665d7627025b51a36577a4e03f1b20ed3dd + "@unocss/core": "npm:66.5.9" + checksum: 10c0/d3f1bacc838beae8caa6333b5df9c69731ae1f2f50eacd187f2a0aef5aa55a01777b7bbb2144b6144032eb58f5495e8257f040cd34493b98e58488ad21c2d6b6 languageName: node linkType: hard -"@unocss/preset-typography@npm:66.5.2": - version: 66.5.2 - resolution: "@unocss/preset-typography@npm:66.5.2" +"@unocss/preset-typography@npm:66.5.9": + version: 66.5.9 + resolution: "@unocss/preset-typography@npm:66.5.9" dependencies: - "@unocss/core": "npm:66.5.2" - "@unocss/rule-utils": "npm:66.5.2" - checksum: 10c0/75a7dc38710ceb95441c835071ac875eb8c0ae6444fd66704368757faffaa265bc4f9a978c5464aa0546ea3404c9f84e563d7560e0872abec70eababb641ab93 + "@unocss/core": "npm:66.5.9" + "@unocss/rule-utils": "npm:66.5.9" + checksum: 10c0/e10003f7aaed0cbfd5a250ae79ab47dfcacfdd0d1562d2fbeef62ed9d5a83d180a3f175e73907e0647f5c5f22fc85b0b46f6ff78df264a2df5dac866d09dfa9f languageName: node linkType: hard -"@unocss/preset-uno@npm:66.5.2": - version: 66.5.2 - resolution: "@unocss/preset-uno@npm:66.5.2" +"@unocss/preset-uno@npm:66.5.9": + version: 66.5.9 + resolution: "@unocss/preset-uno@npm:66.5.9" dependencies: - "@unocss/core": "npm:66.5.2" - "@unocss/preset-wind3": "npm:66.5.2" - checksum: 10c0/cf4c6e09598f1a6078d7a86e3ed36500dc96bea9c0eda2f3237175ad2664eb157fe76efd550e7e87f808f24f2919aeb079373909ea2106e0399afbbf9f8a0002 + "@unocss/core": "npm:66.5.9" + "@unocss/preset-wind3": "npm:66.5.9" + checksum: 10c0/de113625465c3932ac732e2d5c8265a90ca3ddbdf957b42e34f7725843fa1a861217e80d944a5f8d8be59710abeed5758c9bc47c3a49ffb0f84c037617f8c33d languageName: node linkType: hard -"@unocss/preset-web-fonts@npm:66.5.2": - version: 66.5.2 - resolution: "@unocss/preset-web-fonts@npm:66.5.2" +"@unocss/preset-web-fonts@npm:66.5.9": + version: 66.5.9 + resolution: "@unocss/preset-web-fonts@npm:66.5.9" dependencies: - "@unocss/core": "npm:66.5.2" - ofetch: "npm:^1.4.1" - checksum: 10c0/7d1c5e7edfdae4f74c0c016d305624ca1fc1a23b0f0cbddb28438c2213590f4ab96ce974dd1e9e753db1f7b351093e6895bc31f26452bcfc14194f174fa10488 + "@unocss/core": "npm:66.5.9" + ofetch: "npm:^1.5.1" + checksum: 10c0/320498739eed34fac85a3a115074ee336723f179d393125a670dd6fc6bef9aaadb9525fade816fa0f48be8f5d0c2104fa66864d7bdd8c085355c67d796e7c23c languageName: node linkType: hard -"@unocss/preset-wind3@npm:66.5.2": - version: 66.5.2 - resolution: "@unocss/preset-wind3@npm:66.5.2" +"@unocss/preset-wind3@npm:66.5.9": + version: 66.5.9 + resolution: "@unocss/preset-wind3@npm:66.5.9" dependencies: - "@unocss/core": "npm:66.5.2" - "@unocss/preset-mini": "npm:66.5.2" - "@unocss/rule-utils": "npm:66.5.2" - checksum: 10c0/b5c4a5cffb521611da4ca97c90299a26ab5b8c8875fd28f92f186c939f835f28b16bc0b005bc30e5cfd4d3e2ac57b76658b7f28387e5782f13ea94f3caea1fd4 + "@unocss/core": "npm:66.5.9" + "@unocss/preset-mini": "npm:66.5.9" + "@unocss/rule-utils": "npm:66.5.9" + checksum: 10c0/b7927c5366ce7c6da6c024b9f8a8053cb2d488fb6a81a666e2149bd1ed731c00a20fc9362dbcd502e901f3403a820b693603f9e23631e8e72f0decc9425ad6e1 languageName: node linkType: hard -"@unocss/preset-wind4@npm:66.5.2": - version: 66.5.2 - resolution: "@unocss/preset-wind4@npm:66.5.2" +"@unocss/preset-wind4@npm:66.5.9": + version: 66.5.9 + resolution: "@unocss/preset-wind4@npm:66.5.9" dependencies: - "@unocss/core": "npm:66.5.2" - "@unocss/extractor-arbitrary-variants": "npm:66.5.2" - "@unocss/rule-utils": "npm:66.5.2" - checksum: 10c0/97059bfb37657fb8f4d00dfb1ea544a451c073484bcb4dd2b60504a322fb5cd29fae4a2a57e7ff2bf159a5e9faeb98a29c472afcef4ead7ff675d64f9041f992 + "@unocss/core": "npm:66.5.9" + "@unocss/extractor-arbitrary-variants": "npm:66.5.9" + "@unocss/rule-utils": "npm:66.5.9" + checksum: 10c0/006925e72c42e171e6f1e552958b7d15f101be005afc2eaaf3e7abe41f7a33da96d91461de10c0320b1dcdd45e06fe1054b2ec30ebf37d861fefef014d299a12 languageName: node linkType: hard -"@unocss/preset-wind@npm:66.5.2": - version: 66.5.2 - resolution: "@unocss/preset-wind@npm:66.5.2" +"@unocss/preset-wind@npm:66.5.9": + version: 66.5.9 + resolution: "@unocss/preset-wind@npm:66.5.9" dependencies: - "@unocss/core": "npm:66.5.2" - "@unocss/preset-wind3": "npm:66.5.2" - checksum: 10c0/9ecc60147a0f919f840ecc6ac99b604311c62aa7a3546a9363f98cc012f1b58c541f647a9b091f50c05dd5c5c5f48b8500bc884c6a4bed16bb6cfe56006c5a6d + "@unocss/core": "npm:66.5.9" + "@unocss/preset-wind3": "npm:66.5.9" + checksum: 10c0/0d80ee71b4461d8bcae5f530684cbe20c81bd3d7330fa8513f07bbc4e9eda7ffbeef550cdaea438be93766aa78817fdd46011d9bc5a09b5e19c519c209d4e420 languageName: node linkType: hard -"@unocss/reset@npm:66.5.2": - version: 66.5.2 - resolution: "@unocss/reset@npm:66.5.2" - checksum: 10c0/72a688edaae84324c7c77de3f5b3bc9295cb16ad7fa1174c45d1ba79fbbd90ad0a562e8d8663096442dae5827f238b41fbc7671586b659c6135b23ee2608a303 +"@unocss/reset@npm:66.5.9": + version: 66.5.9 + resolution: "@unocss/reset@npm:66.5.9" + checksum: 10c0/c74c23650f93d3ed153d565f15f45f30a409b078d4a4ff28b010922e860d11f315d7e5fbfd9ec30e73d553628abe51f795094c6bee97a7cf6ceb30cb549a84da languageName: node linkType: hard -"@unocss/rule-utils@npm:66.5.2": - version: 66.5.2 - resolution: "@unocss/rule-utils@npm:66.5.2" +"@unocss/rule-utils@npm:66.5.9": + version: 66.5.9 + resolution: "@unocss/rule-utils@npm:66.5.9" dependencies: - "@unocss/core": "npm:^66.5.2" - magic-string: "npm:^0.30.18" - checksum: 10c0/b1e9be7a6cf17b929dd974713b8c55b40c74aec764aa0311f04ee67b2d36c891a06dd9611b90ef1c23133eca48340f7cbfe314e16ca5dc41498b8bec0e8f8dc5 + "@unocss/core": "npm:^66.5.9" + magic-string: "npm:^0.30.21" + checksum: 10c0/b4326ab1a2b33b4d1f443de7c6b337833836af05c41e000c19ca07ee23b6959ee002505248341c4d09797dc918104eb4af52b50f8ac126f936dcfb3eb07dcb33 languageName: node linkType: hard -"@unocss/transformer-attributify-jsx@npm:66.5.2": - version: 66.5.2 - resolution: "@unocss/transformer-attributify-jsx@npm:66.5.2" +"@unocss/transformer-attributify-jsx@npm:66.5.9": + version: 66.5.9 + resolution: "@unocss/transformer-attributify-jsx@npm:66.5.9" dependencies: "@babel/parser": "npm:7.27.7" "@babel/traverse": "npm:7.27.7" - "@unocss/core": "npm:66.5.2" - checksum: 10c0/8230fe637a580d3339126421ccc37074c94229a462f53382ba7450fb3feb5ebcc471634936be1103b57398c85f3bb3ecb2541ee1cb8d62c5f9756bcd0d435d36 + "@unocss/core": "npm:66.5.9" + checksum: 10c0/e4fbb867a6387a3bb7f2d2376ab2524939d03b9f709c140d1e4a0707f0bae589d171612c7b3791e163ad56f5a39a96e0b47f55179a71ee1a66a446210c15d506 languageName: node linkType: hard -"@unocss/transformer-compile-class@npm:66.5.2": - version: 66.5.2 - resolution: "@unocss/transformer-compile-class@npm:66.5.2" +"@unocss/transformer-compile-class@npm:66.5.9": + version: 66.5.9 + resolution: "@unocss/transformer-compile-class@npm:66.5.9" dependencies: - "@unocss/core": "npm:66.5.2" - checksum: 10c0/df5d4f6305bb39d48946c9376fb407228960188a950e04d140da97a4979b376595e2ab3612677dcc4502f490e20281177a61ecd0f5d2caa4c9a1f350eaee2c46 + "@unocss/core": "npm:66.5.9" + checksum: 10c0/8881cee866bd83c3acc66d73996b2fc8714c57163181b7a865471beaaf33d5c36a6c5ae60492b8c5bf46891d0a5089c7369eefb937f68023edfd46a465a22a6b languageName: node linkType: hard -"@unocss/transformer-directives@npm:66.5.2": - version: 66.5.2 - resolution: "@unocss/transformer-directives@npm:66.5.2" +"@unocss/transformer-directives@npm:66.5.9": + version: 66.5.9 + resolution: "@unocss/transformer-directives@npm:66.5.9" dependencies: - "@unocss/core": "npm:66.5.2" - "@unocss/rule-utils": "npm:66.5.2" + "@unocss/core": "npm:66.5.9" + "@unocss/rule-utils": "npm:66.5.9" css-tree: "npm:^3.1.0" - checksum: 10c0/eb840197c4e06e6e3e5123853e25428f7f5401e939486feca4950901f13bb6d8c35e06877a940d0f9300ce3329a8a29ab2d5f0669209a5749dcca5e4ff8f94d0 + checksum: 10c0/3e7c655b28fbfb3b7859aac4b2c5eae2bbba61409cffd68de7e46d7299c393b7a8ccac062e0d1b0b2f5be99ca9817775766e75213771fc35ef455e46efca77e2 languageName: node linkType: hard -"@unocss/transformer-variant-group@npm:66.5.2": - version: 66.5.2 - resolution: "@unocss/transformer-variant-group@npm:66.5.2" +"@unocss/transformer-variant-group@npm:66.5.9": + version: 66.5.9 + resolution: "@unocss/transformer-variant-group@npm:66.5.9" dependencies: - "@unocss/core": "npm:66.5.2" - checksum: 10c0/17e81b0298509ac2f8b1b4ee519aa7855aea3b42b1a84945171b11b22cea1704d34473e76ccfc1148e7a2a7fc0f37ebbc95ae28c013a5d83a47ad5c7706e306f + "@unocss/core": "npm:66.5.9" + checksum: 10c0/85d551a8af02aa565d4f3311233e087689d4916820b7f3878751a03d8041f1149fec29c5ecc7c791ff9504ebf077b365af5cb3d916ac82c362bc03c84565e881 languageName: node linkType: hard -"@unocss/vite@npm:66.5.2": - version: 66.5.2 - resolution: "@unocss/vite@npm:66.5.2" +"@unocss/vite@npm:66.5.9": + version: 66.5.9 + resolution: "@unocss/vite@npm:66.5.9" dependencies: "@jridgewell/remapping": "npm:^2.3.5" - "@unocss/config": "npm:66.5.2" - "@unocss/core": "npm:66.5.2" - "@unocss/inspector": "npm:66.5.2" + "@unocss/config": "npm:66.5.9" + "@unocss/core": "npm:66.5.9" + "@unocss/inspector": "npm:66.5.9" chokidar: "npm:^3.6.0" - magic-string: "npm:^0.30.18" + magic-string: "npm:^0.30.21" pathe: "npm:^2.0.3" - tinyglobby: "npm:^0.2.14" - unplugin-utils: "npm:^0.3.0" + tinyglobby: "npm:^0.2.15" + unplugin-utils: "npm:^0.3.1" peerDependencies: vite: ^2.9.0 || ^3.0.0-0 || ^4.0.0 || ^5.0.0-0 || ^6.0.0-0 || ^7.0.0-0 - checksum: 10c0/cf5cdb9f64c8a6dc3d8357bf302c6de596d734386e708924c2585fe57b3969a938f1922e8101d74f5e5bfbef746293acffc0e3b70416d93dcd042b17d901a9f1 + checksum: 10c0/c7df1d0e0f73cccca60d0302cedce36c0efbfc473b43cce948facef0da4420aefdb25f03ac7ee1637f53370e9b7ce9fd977bb4a097f2c839229aee7c0cf0bbd6 languageName: node linkType: hard @@ -4540,6 +5511,28 @@ __metadata: languageName: node linkType: hard +"@vercel/nft@npm:^0.30.3": + version: 0.30.4 + resolution: "@vercel/nft@npm:0.30.4" + dependencies: + "@mapbox/node-pre-gyp": "npm:^2.0.0" + "@rollup/pluginutils": "npm:^5.1.3" + acorn: "npm:^8.6.0" + acorn-import-attributes: "npm:^1.9.5" + async-sema: "npm:^3.1.1" + bindings: "npm:^1.4.0" + estree-walker: "npm:2.0.2" + glob: "npm:^10.5.0" + graceful-fs: "npm:^4.2.9" + node-gyp-build: "npm:^4.2.2" + picomatch: "npm:^4.0.2" + resolve-from: "npm:^5.0.0" + bin: + nft: out/cli.js + checksum: 10c0/1d7e372377e5e5c5bd74bd691f1d95136753626f9832b09fb448de512a8996ed63cdfac7878bd1301389149edd685c60372d3b1d00a3c1491068fe56e47d4c64 + languageName: node + linkType: hard + "@vitejs/plugin-vue-jsx@npm:^5.1.1": version: 5.1.1 resolution: "@vitejs/plugin-vue-jsx@npm:5.1.1" @@ -4705,39 +5698,21 @@ __metadata: languageName: node linkType: hard -"@vue-macros/common@npm:3.0.0-beta.15": - version: 3.0.0-beta.15 - resolution: "@vue-macros/common@npm:3.0.0-beta.15" - dependencies: - "@vue/compiler-sfc": "npm:^3.5.17" - ast-kit: "npm:^2.1.0" - local-pkg: "npm:^1.1.1" - magic-string-ast: "npm:^1.0.0" - unplugin-utils: "npm:^0.2.4" - peerDependencies: - vue: ^2.7.0 || ^3.2.25 - peerDependenciesMeta: - vue: - optional: true - checksum: 10c0/6d8e90a3c8170a8fd5837fbb459690986c7d704b730365c80c5ab57cdf2f4506f30959f48f298f1af3cb38ac13b2186624364c76b47524e1a07fa0ac39f32db3 - languageName: node - linkType: hard - -"@vue-macros/common@npm:3.0.0-beta.16": - version: 3.0.0-beta.16 - resolution: "@vue-macros/common@npm:3.0.0-beta.16" +"@vue-macros/common@npm:^3.1.1": + version: 3.1.1 + resolution: "@vue-macros/common@npm:3.1.1" dependencies: - "@vue/compiler-sfc": "npm:^3.5.17" - ast-kit: "npm:^2.1.1" - local-pkg: "npm:^1.1.1" - magic-string-ast: "npm:^1.0.0" - unplugin-utils: "npm:^0.2.4" + "@vue/compiler-sfc": "npm:^3.5.22" + ast-kit: "npm:^2.1.2" + local-pkg: "npm:^1.1.2" + magic-string-ast: "npm:^1.0.2" + unplugin-utils: "npm:^0.3.0" peerDependencies: vue: ^2.7.0 || ^3.2.25 peerDependenciesMeta: vue: optional: true - checksum: 10c0/615f3ac33d9f5d85d8661270d1c4e892c134cf2e8028f8317d9213ae44baaf7b0e3cb0a380d5b91b2c943098b0b11c2728bbcbfc54ff2c0337dd52ba78156269 + checksum: 10c0/015fc0faa31a14c75fe28e38f4187dc05aa2f6d6bb5b3a0c19f6ae44b5f126a3814a41002c135c7cab11bed212138e5a08cd6ec699c8e0ad00d6760239608220 languageName: node linkType: hard @@ -4798,6 +5773,19 @@ __metadata: languageName: node linkType: hard +"@vue/compiler-core@npm:3.5.25": + version: 3.5.25 + resolution: "@vue/compiler-core@npm:3.5.25" + dependencies: + "@babel/parser": "npm:^7.28.5" + "@vue/shared": "npm:3.5.25" + entities: "npm:^4.5.0" + estree-walker: "npm:^2.0.2" + source-map-js: "npm:^1.2.1" + checksum: 10c0/aa04eadb7751d825257949c7a2813833eff815795ea9c145cc8a603fb2d461c3a0f29714ff601f54331a79fca627d1e9654308a5fc4b4fef9a032847cb8380b3 + languageName: node + linkType: hard + "@vue/compiler-dom@npm:3.5.22, @vue/compiler-dom@npm:^3.2.45, @vue/compiler-dom@npm:^3.5.0": version: 3.5.22 resolution: "@vue/compiler-dom@npm:3.5.22" @@ -4808,7 +5796,17 @@ __metadata: languageName: node linkType: hard -"@vue/compiler-sfc@npm:3.5.22, @vue/compiler-sfc@npm:^3.5.17, @vue/compiler-sfc@npm:^3.5.18, @vue/compiler-sfc@npm:^3.5.21": +"@vue/compiler-dom@npm:3.5.25": + version: 3.5.25 + resolution: "@vue/compiler-dom@npm:3.5.25" + dependencies: + "@vue/compiler-core": "npm:3.5.25" + "@vue/shared": "npm:3.5.25" + checksum: 10c0/d02fce117e9633294bc697db7b037f98b91807bb9408db914e9ed5cccb8f29b260230f3771e2f9dcc2f66a252399efea623091853e6bf8469c5861c24032bf8e + languageName: node + linkType: hard + +"@vue/compiler-sfc@npm:3.5.22, @vue/compiler-sfc@npm:^3.5.18": version: 3.5.22 resolution: "@vue/compiler-sfc@npm:3.5.22" dependencies: @@ -4825,6 +5823,23 @@ __metadata: languageName: node linkType: hard +"@vue/compiler-sfc@npm:3.5.25, @vue/compiler-sfc@npm:^3.5.22": + version: 3.5.25 + resolution: "@vue/compiler-sfc@npm:3.5.25" + dependencies: + "@babel/parser": "npm:^7.28.5" + "@vue/compiler-core": "npm:3.5.25" + "@vue/compiler-dom": "npm:3.5.25" + "@vue/compiler-ssr": "npm:3.5.25" + "@vue/shared": "npm:3.5.25" + estree-walker: "npm:^2.0.2" + magic-string: "npm:^0.30.21" + postcss: "npm:^8.5.6" + source-map-js: "npm:^1.2.1" + checksum: 10c0/8325cc69a288501f700fed093ca20f2fac8a405035998dcb75bceeef961a294b1047506dc554a6cd66840cbdab048792c2451fdfe01a0f23a4a7cfccfbb5f777 + languageName: node + linkType: hard + "@vue/compiler-ssr@npm:3.5.22": version: 3.5.22 resolution: "@vue/compiler-ssr@npm:3.5.22" @@ -4835,6 +5850,16 @@ __metadata: languageName: node linkType: hard +"@vue/compiler-ssr@npm:3.5.25": + version: 3.5.25 + resolution: "@vue/compiler-ssr@npm:3.5.25" + dependencies: + "@vue/compiler-dom": "npm:3.5.25" + "@vue/shared": "npm:3.5.25" + checksum: 10c0/bdaa962d7b35e8bfee97769d47c49ae1a3b5449b5720460fdc188df3d6680223c2f7bb27813da6ad6d248a6dc02983b400585fb3d061ce35df34698f19afc78b + languageName: node + linkType: hard + "@vue/devtools-api@npm:^6.5.0, @vue/devtools-api@npm:^6.6.4": version: 6.6.4 resolution: "@vue/devtools-api@npm:6.6.4" @@ -4842,7 +5867,7 @@ __metadata: languageName: node linkType: hard -"@vue/devtools-api@npm:^7.5.2, @vue/devtools-api@npm:^7.7.2": +"@vue/devtools-api@npm:^7.5.2": version: 7.7.7 resolution: "@vue/devtools-api@npm:7.7.7" dependencies: @@ -4851,6 +5876,15 @@ __metadata: languageName: node linkType: hard +"@vue/devtools-api@npm:^7.7.7": + version: 7.7.9 + resolution: "@vue/devtools-api@npm:7.7.9" + dependencies: + "@vue/devtools-kit": "npm:^7.7.9" + checksum: 10c0/ca09265a88dcacbb51d13bd92a019fc14cc520b1704e0c7ea0b14ecbd0cfadeabeacfb3c94e69cdf5d038ba24bb4c2ab9abe8721a22efcd148e20b0611bd2298 + languageName: node + linkType: hard + "@vue/devtools-core@npm:^7.7.7": version: 7.7.7 resolution: "@vue/devtools-core@npm:7.7.7" @@ -4867,6 +5901,22 @@ __metadata: languageName: node linkType: hard +"@vue/devtools-core@npm:^8.0.5": + version: 8.0.5 + resolution: "@vue/devtools-core@npm:8.0.5" + dependencies: + "@vue/devtools-kit": "npm:^8.0.5" + "@vue/devtools-shared": "npm:^8.0.5" + mitt: "npm:^3.0.1" + nanoid: "npm:^5.1.5" + pathe: "npm:^2.0.3" + vite-hot-client: "npm:^2.1.0" + peerDependencies: + vue: ^3.0.0 + checksum: 10c0/00737358cd1835d404c527097fb60d5e7a79ef4a56459440e72b630cf4854ec4bbf32cc92e020c106ffd0f4e194a6415ecd0fd90144f9a31769d17f1fd1028cd + languageName: node + linkType: hard + "@vue/devtools-kit@npm:^7.7.7": version: 7.7.7 resolution: "@vue/devtools-kit@npm:7.7.7" @@ -4882,6 +5932,36 @@ __metadata: languageName: node linkType: hard +"@vue/devtools-kit@npm:^7.7.9": + version: 7.7.9 + resolution: "@vue/devtools-kit@npm:7.7.9" + dependencies: + "@vue/devtools-shared": "npm:^7.7.9" + birpc: "npm:^2.3.0" + hookable: "npm:^5.5.3" + mitt: "npm:^3.0.1" + perfect-debounce: "npm:^1.0.0" + speakingurl: "npm:^14.0.1" + superjson: "npm:^2.2.2" + checksum: 10c0/e6d488b18c379dbcffbbec0ea32383ea65a9d8445ad0616bc6c3f51e1a63d5971bc88bc7b074686d22f362516c388dbd0e07f3ced2e8ed8057cb5cc12a83eef1 + languageName: node + linkType: hard + +"@vue/devtools-kit@npm:^8.0.5": + version: 8.0.5 + resolution: "@vue/devtools-kit@npm:8.0.5" + dependencies: + "@vue/devtools-shared": "npm:^8.0.5" + birpc: "npm:^2.6.1" + hookable: "npm:^5.5.3" + mitt: "npm:^3.0.1" + perfect-debounce: "npm:^2.0.0" + speakingurl: "npm:^14.0.1" + superjson: "npm:^2.2.2" + checksum: 10c0/6cf90e38b80be0d8b3876465fa8b42e834889f5e3c89ef94921167def5719b8c7968406aca960df9ef35e31b21c4c253d62886b5059e2a894bc2c4dad32975bb + languageName: node + linkType: hard + "@vue/devtools-shared@npm:^7.7.7": version: 7.7.7 resolution: "@vue/devtools-shared@npm:7.7.7" @@ -4891,9 +5971,27 @@ __metadata: languageName: node linkType: hard -"@vue/language-core@npm:3.1.0, @vue/language-core@npm:^3.0.1": - version: 3.1.0 - resolution: "@vue/language-core@npm:3.1.0" +"@vue/devtools-shared@npm:^7.7.9": + version: 7.7.9 + resolution: "@vue/devtools-shared@npm:7.7.9" + dependencies: + rfdc: "npm:^1.4.1" + checksum: 10c0/f012f71689d453424d458b8dabf3f7fb92aa0ef03142f28270a8bc633a8605c3b80a1d96bd6011112c073025b301a3ca2121feb9966ad293d8c9d3a00a66ddeb + languageName: node + linkType: hard + +"@vue/devtools-shared@npm:^8.0.5": + version: 8.0.5 + resolution: "@vue/devtools-shared@npm:8.0.5" + dependencies: + rfdc: "npm:^1.4.1" + checksum: 10c0/e234bc5ea0e312dd3774599a0f566bc034abb5c90d053ec5355cea9bc66317096e2229c894d4249bd9b9e3976ad565b911026ee1f915cdcc21ce94c3ad65062e + languageName: node + linkType: hard + +"@vue/language-core@npm:3.1.5, @vue/language-core@npm:^3.1.3": + version: 3.1.5 + resolution: "@vue/language-core@npm:3.1.5" dependencies: "@volar/language-core": "npm:2.4.23" "@vue/compiler-dom": "npm:^3.5.0" @@ -4907,7 +6005,7 @@ __metadata: peerDependenciesMeta: typescript: optional: true - checksum: 10c0/e8ff90870416b56bccd12597984f4a96958013b6fe3b8d3031598fc5ac0dcd856112ce538d613b33fe7ee31d40dbf02c7835d44402c83344e32f690ae535de90 + checksum: 10c0/10e960d85c06edff0d5217cc4e54dd59ad2308ce0d6202720cd4d56fc2237f01f1f51cf04bdb0ea80796b40beecffa8f9428eaaceaa5642d4a8a0ae60bd5bc15 languageName: node linkType: hard @@ -4920,6 +6018,15 @@ __metadata: languageName: node linkType: hard +"@vue/reactivity@npm:3.5.25": + version: 3.5.25 + resolution: "@vue/reactivity@npm:3.5.25" + dependencies: + "@vue/shared": "npm:3.5.25" + checksum: 10c0/a0171f981ba466fe28e1d74edc23a43c3485065ae615b3123dc1efa999a371621fcd6bf7aec1528d47a862d7b85e7e8802aff26cb3fc101f642cc3b6d7c0904f + languageName: node + linkType: hard + "@vue/runtime-core@npm:3.5.22": version: 3.5.22 resolution: "@vue/runtime-core@npm:3.5.22" @@ -4930,6 +6037,16 @@ __metadata: languageName: node linkType: hard +"@vue/runtime-core@npm:3.5.25": + version: 3.5.25 + resolution: "@vue/runtime-core@npm:3.5.25" + dependencies: + "@vue/reactivity": "npm:3.5.25" + "@vue/shared": "npm:3.5.25" + checksum: 10c0/be2efe5300daeaecdd6313139bbf39c5aa2113a0d2619ef1fb3b6d7bf0b33c54d7defe54001459ea4b0ecb01c1d1103ed0a1108534ea55abc2ba1d17ae0eb8bf + languageName: node + linkType: hard + "@vue/runtime-dom@npm:3.5.22": version: 3.5.22 resolution: "@vue/runtime-dom@npm:3.5.22" @@ -4942,6 +6059,18 @@ __metadata: languageName: node linkType: hard +"@vue/runtime-dom@npm:3.5.25": + version: 3.5.25 + resolution: "@vue/runtime-dom@npm:3.5.25" + dependencies: + "@vue/reactivity": "npm:3.5.25" + "@vue/runtime-core": "npm:3.5.25" + "@vue/shared": "npm:3.5.25" + csstype: "npm:^3.1.3" + checksum: 10c0/e71e6dde9254dda52b2fed17c882fa4d174735b94436cdf847b44d32554b94b77cc76cdf6f6e2d6d0bdbeec070b2cf3f1416a5efd85c2e682cbe6842b1bb3969 + languageName: node + linkType: hard + "@vue/server-renderer@npm:3.5.22": version: 3.5.22 resolution: "@vue/server-renderer@npm:3.5.22" @@ -4954,13 +6083,32 @@ __metadata: languageName: node linkType: hard -"@vue/shared@npm:3.5.22, @vue/shared@npm:^3.5.0, @vue/shared@npm:^3.5.18, @vue/shared@npm:^3.5.21": +"@vue/server-renderer@npm:3.5.25": + version: 3.5.25 + resolution: "@vue/server-renderer@npm:3.5.25" + dependencies: + "@vue/compiler-ssr": "npm:3.5.25" + "@vue/shared": "npm:3.5.25" + peerDependencies: + vue: 3.5.25 + checksum: 10c0/d49a21380db4416a3b24f4c8cc45c44432a1a4aac75970cf3d5654ecca3b04cfe2b1f4b218e19deac4dea9398f4ddbfab06de10661f76b19554208fe1826a620 + languageName: node + linkType: hard + +"@vue/shared@npm:3.5.22, @vue/shared@npm:^3.5.0, @vue/shared@npm:^3.5.18": version: 3.5.22 resolution: "@vue/shared@npm:3.5.22" checksum: 10c0/5866eab1dd6caa949f4ae2da2a7bac69612b35e316a298785279fb4de101bfe89a3572db56448aa35023b01d069b80a664be4fe22847ce5e5fbc1990e5970ec5 languageName: node linkType: hard +"@vue/shared@npm:3.5.25, @vue/shared@npm:^3.5.23": + version: 3.5.25 + resolution: "@vue/shared@npm:3.5.25" + checksum: 10c0/8beda92b7c4b70eaffd7ecf30fe366f36f0ed57573696bbd277ad289d367dd23159e2a61a10a67a7d77e525f7a8f994c7f5c6b4736baf184f4b91ab053a7573d + languageName: node + linkType: hard + "@vue/test-utils@npm:2.4.6, @vue/test-utils@npm:^2.4.1": version: 2.4.6 resolution: "@vue/test-utils@npm:2.4.6" @@ -5085,21 +6233,21 @@ __metadata: "@headlessui/vue": "npm:1.7.23" "@maplibre/maplibre-gl-directions": "npm:0.8.0" "@nuxt/devtools": "npm:2.6.5" - "@nuxt/eslint": "npm:1.9.0" - "@nuxt/eslint-config": "npm:1.9.0" - "@nuxt/icon": "npm:2.0.0" - "@nuxt/test-utils": "npm:3.19.2" + "@nuxt/eslint": "npm:1.10.0" + "@nuxt/eslint-config": "npm:1.10.0" + "@nuxt/icon": "npm:2.1.0" + "@nuxt/test-utils": "npm:3.20.1" "@nuxtjs/color-mode": "npm:3.5.2" "@nuxtjs/device": "npm:3.2.4" - "@nuxtjs/i18n": "npm:10.1.0" + "@nuxtjs/i18n": "npm:10.2.1" "@nuxtjs/plausible": "npm:2.0.1" "@opentelemetry/api": "npm:1.9.0" - "@pinia/nuxt": "npm:0.11.2" + "@pinia/nuxt": "npm:0.11.3" "@playwright/test": "npm:1.55.1" "@popperjs/core": "npm:2.11.8" "@sidebase/nuxt-auth": "npm:1.1.0" "@somushq/vue3-friendly-captcha": "npm:1.0.2" - "@tailwindcss/vite": "npm:4.1.14" + "@tailwindcss/vite": "npm:4.1.17" "@testing-library/vue": "npm:8.1.0" "@tiptap/core": "npm:3.8.0" "@tiptap/extension-link": "npm:3.8.0" @@ -5111,39 +6259,39 @@ __metadata: "@tiptap/suggestion": "npm:3.8.0" "@tiptap/vue-3": "npm:3.8.0" "@types/geojson": "npm:7946.0.16" - "@types/node": "npm:24.6.2" + "@types/node": "npm:24.10.1" "@types/zxcvbn": "npm:4.4.5" - "@unocss/reset": "npm:66.5.2" + "@unocss/reset": "npm:66.5.9" "@vee-validate/zod": "npm:4.15.1" "@vitest/coverage-v8": "npm:3.2.4" "@vue/test-utils": "npm:2.4.6" "@vueuse/core": "npm:13.9.0" "@vueuse/math": "npm:13.9.0" "@vueuse/nuxt": "npm:13.9.0" - autoprefixer: "npm:10.4.21" + autoprefixer: "npm:10.4.22" axe-core: "npm:4.10.2" axe-html-reporter: "npm:2.2.11" axios: "npm:1.12.2" - commander: "npm:14.0.1" + commander: "npm:14.0.2" cross-env: "npm:10.1.0" - dompurify: "npm:3.2.7" + dompurify: "npm:3.3.0" dotenv: "npm:17.2.3" env-cmd: "npm:11.0.0" - eslint: "npm:9.36.0" + eslint: "npm:9.39.1" eslint-config-prettier: "npm:10.1.8" eslint-flat-config-utils: "npm:2.1.4" - eslint-plugin-perfectionist: "npm:4.15.0" - eslint-plugin-vue: "npm:10.5.0" + eslint-plugin-perfectionist: "npm:4.15.1" + eslint-plugin-vue: "npm:10.6.0" eslint-plugin-vuejs-accessibility: "npm:2.4.1" floating-vue: "npm:5.2.2" happy-dom: "npm:16.8.1" kill-port: "npm:2.0.1" - lint-staged: "npm:16.1.0" + lint-staged: "npm:16.2.7" maplibre-gl: "npm:5.8.0" next-auth: "npm:~4.21.1" - nuxt: "npm:4.1.2" - nuxt-security: "npm:2.4.0" - pinia: "npm:3.0.3" + nuxt: "npm:4.2.1" + nuxt-security: "npm:2.5.0" + pinia: "npm:3.0.4" pinia-plugin-persistedstate: "npm:4.5.0" playwright-core: "npm:1.55.1" postcss: "npm:8.5.6" @@ -5154,26 +6302,26 @@ __metadata: qrcode.vue: "npm:3.6.0" reduced-motion: "npm:1.0.4" rollup: "npm:4.52.3" - swiper: "npm:12.0.2" + swiper: "npm:12.0.3" tailwind-scrollbar: "npm:4.0.2" - tailwindcss: "npm:4.1.14" + tailwindcss: "npm:4.1.17" tippy.js: "npm:6.3.7" tiptap-markdown: "npm:0.9.0" typescript: "npm:5.9.3" - unocss: "npm:66.5.2" + unocss: "npm:66.5.9" uuid: "npm:13.0.0" v-calendar: "npm:3.1.2" vee-validate: "npm:4.15.1" - vite: "npm:7.1.12" + vite: "npm:7.2.4" vitest: "npm:3.2.4" - vue: "npm:3.5.22" + vue: "npm:3.5.25" vue-eslint-parser: "npm:10.2.0" vue-i18n: "npm:11.1.12" vue-socials: "npm:2.0.5" vue-sonner: "npm:2.0.9" - vue-tsc: "npm:3.1.0" + vue-tsc: "npm:3.1.5" vuedraggable: "npm:4.1.0" - wait-on: "npm:9.0.1" + wait-on: "npm:9.0.3" zod: "npm:3.25.76" zxcvbn: "npm:4.4.2" languageName: unknown @@ -5244,14 +6392,14 @@ __metadata: languageName: node linkType: hard -"ansi-styles@npm:^6.0.0, ansi-styles@npm:^6.1.0, ansi-styles@npm:^6.2.1": +"ansi-styles@npm:^6.1.0, ansi-styles@npm:^6.2.1": version: 6.2.3 resolution: "ansi-styles@npm:6.2.3" checksum: 10c0/23b8a4ce14e18fb854693b95351e286b771d23d8844057ed2e7d083cd3e708376c3323707ec6a24365f7d7eda3ca00327fe04092e29e551499ec4c8b7bfac868 languageName: node linkType: hard -"ansis@npm:^4.1.0": +"ansis@npm:^4.1.0, ansis@npm:^4.2.0": version: 4.2.0 resolution: "ansis@npm:4.2.0" checksum: 10c0/cd6a7a681ecd36e72e0d79c1e34f1f3bcb1b15bcbb6f0f8969b4228062d3bfebbef468e09771b00d93b2294370b34f707599d4a113542a876de26823b795b5d2 @@ -5345,7 +6493,7 @@ __metadata: languageName: node linkType: hard -"ast-kit@npm:^2.1.0, ast-kit@npm:^2.1.1, ast-kit@npm:^2.1.2": +"ast-kit@npm:^2.1.2": version: 2.1.2 resolution: "ast-kit@npm:2.1.2" dependencies: @@ -5355,6 +6503,16 @@ __metadata: languageName: node linkType: hard +"ast-kit@npm:^2.1.3": + version: 2.2.0 + resolution: "ast-kit@npm:2.2.0" + dependencies: + "@babel/parser": "npm:^7.28.5" + pathe: "npm:^2.0.3" + checksum: 10c0/d885f3a4e9837e730451a667d26936eef34773d6e5ecacd771a3e9d1f82fdc45d38958ab35e18880e0cf667896604a599497c5186f2578cf73c0d802ed7fc697 + languageName: node + linkType: hard + "ast-v8-to-istanbul@npm:^0.3.3": version: 0.3.8 resolution: "ast-v8-to-istanbul@npm:0.3.8" @@ -5366,13 +6524,13 @@ __metadata: languageName: node linkType: hard -"ast-walker-scope@npm:^0.8.1": - version: 0.8.2 - resolution: "ast-walker-scope@npm:0.8.2" +"ast-walker-scope@npm:^0.8.3": + version: 0.8.3 + resolution: "ast-walker-scope@npm:0.8.3" dependencies: - "@babel/parser": "npm:^7.28.3" - ast-kit: "npm:^2.1.2" - checksum: 10c0/f86e0fd4b27353908deeb3dd90a050ef97006436d16d3088f5bb8681af6a16adb006bd3061505f7fa5e7b8b918f703113610f7f5665d44b2cb3c3ef9a4b4c518 + "@babel/parser": "npm:^7.28.4" + ast-kit: "npm:^2.1.3" + checksum: 10c0/9c98bf1311e798ca95e33ea6315856c31b2870860175b7dd78f87bfacb3600b224350bcc23df511d3f9cae765ad254deea996302f4056b0ffc08909d7382bb24 languageName: node linkType: hard @@ -5411,7 +6569,25 @@ __metadata: languageName: node linkType: hard -"autoprefixer@npm:10.4.21, autoprefixer@npm:^10.4.21": +"autoprefixer@npm:10.4.22": + version: 10.4.22 + resolution: "autoprefixer@npm:10.4.22" + dependencies: + browserslist: "npm:^4.27.0" + caniuse-lite: "npm:^1.0.30001754" + fraction.js: "npm:^5.3.4" + normalize-range: "npm:^0.1.2" + picocolors: "npm:^1.1.1" + postcss-value-parser: "npm:^4.2.0" + peerDependencies: + postcss: ^8.1.0 + bin: + autoprefixer: bin/autoprefixer + checksum: 10c0/2ae8d135af2deaaa5065a3a466c877787373c0ed766b8a8e8259d7871db79c1a7e1d9f6c9541c54fa95647511d3c2066bb08a30160e58c9bfa75506f9c18f3aa + languageName: node + linkType: hard + +"autoprefixer@npm:^10.4.21": version: 10.4.21 resolution: "autoprefixer@npm:10.4.21" dependencies: @@ -5463,7 +6639,7 @@ __metadata: languageName: node linkType: hard -"axios@npm:1.12.2, axios@npm:^1.12.2": +"axios@npm:1.12.2": version: 1.12.2 resolution: "axios@npm:1.12.2" dependencies: @@ -5474,6 +6650,17 @@ __metadata: languageName: node linkType: hard +"axios@npm:^1.13.2": + version: 1.13.2 + resolution: "axios@npm:1.13.2" + dependencies: + follow-redirects: "npm:^1.15.6" + form-data: "npm:^4.0.4" + proxy-from-env: "npm:^1.1.0" + checksum: 10c0/e8a42e37e5568ae9c7a28c348db0e8cf3e43d06fcbef73f0048669edfe4f71219664da7b6cc991b0c0f01c28a48f037c515263cb79be1f1ae8ff034cd813867b + languageName: node + linkType: hard + "b4a@npm:^1.6.4": version: 1.7.3 resolution: "b4a@npm:1.7.3" @@ -5507,6 +6694,15 @@ __metadata: languageName: node linkType: hard +"baseline-browser-mapping@npm:^2.8.25": + version: 2.8.31 + resolution: "baseline-browser-mapping@npm:2.8.31" + bin: + baseline-browser-mapping: dist/cli.js + checksum: 10c0/e0b2fcb41bf36c5e27e122a4d4cc9e5f6b9747d31cd0bd1f771aee9c490eb1e01cd11a31db32286bd4b9221139ee332b5ab7e3893c18a4dbd0ce8915a9e180ed + languageName: node + linkType: hard + "baseline-browser-mapping@npm:^2.8.9": version: 2.8.10 resolution: "baseline-browser-mapping@npm:2.8.10" @@ -5548,6 +6744,13 @@ __metadata: languageName: node linkType: hard +"birpc@npm:^2.6.1, birpc@npm:^2.8.0": + version: 2.8.0 + resolution: "birpc@npm:2.8.0" + checksum: 10c0/03441ed726afa79c218c4681574fca231b3571a2f2c702587a656aa47474794483bcbbc2fc48760340f35f71484b19194923786829c00e72da7ade1c11391760 + languageName: node + linkType: hard + "boolbase@npm:^1.0.0": version: 1.0.0 resolution: "boolbase@npm:1.0.0" @@ -5583,7 +6786,7 @@ __metadata: languageName: node linkType: hard -"browserslist@npm:^4.0.0, browserslist@npm:^4.24.0, browserslist@npm:^4.24.4, browserslist@npm:^4.25.1, browserslist@npm:^4.25.3": +"browserslist@npm:^4.0.0, browserslist@npm:^4.24.0, browserslist@npm:^4.24.4, browserslist@npm:^4.25.1": version: 4.26.3 resolution: "browserslist@npm:4.26.3" dependencies: @@ -5598,6 +6801,21 @@ __metadata: languageName: node linkType: hard +"browserslist@npm:^4.27.0, browserslist@npm:^4.28.0": + version: 4.28.0 + resolution: "browserslist@npm:4.28.0" + dependencies: + baseline-browser-mapping: "npm:^2.8.25" + caniuse-lite: "npm:^1.0.30001754" + electron-to-chromium: "npm:^1.5.249" + node-releases: "npm:^2.0.27" + update-browserslist-db: "npm:^1.1.4" + bin: + browserslist: cli.js + checksum: 10c0/4284fd568f7d40a496963083860d488cb2a89fb055b6affd316bebc59441fec938e090b3e62c0ee065eb0bc88cd1bc145f4300a16c75f3f565621c5823715ae1 + languageName: node + linkType: hard + "buffer-crc32@npm:^1.0.0": version: 1.0.0 resolution: "buffer-crc32@npm:1.0.0" @@ -5674,6 +6892,31 @@ __metadata: languageName: node linkType: hard +"c12@npm:^3.3.1": + version: 3.3.2 + resolution: "c12@npm:3.3.2" + dependencies: + chokidar: "npm:^4.0.3" + confbox: "npm:^0.2.2" + defu: "npm:^6.1.4" + dotenv: "npm:^17.2.3" + exsolve: "npm:^1.0.8" + giget: "npm:^2.0.0" + jiti: "npm:^2.6.1" + ohash: "npm:^2.0.11" + pathe: "npm:^2.0.3" + perfect-debounce: "npm:^2.0.0" + pkg-types: "npm:^2.3.0" + rc9: "npm:^2.1.2" + peerDependencies: + magicast: "*" + peerDependenciesMeta: + magicast: + optional: true + checksum: 10c0/5d8df4509e2dcb26d880d99b5c01b66b2b8690b3bdb06f64d6bcac6802ffcc96b6ee0b3a604f0e471b1ebe3c147f16536b8c4544b314a4c6c287ca8a06af829c + languageName: node + linkType: hard + "cac@npm:^6.7.14": version: 6.7.14 resolution: "cac@npm:6.7.14" @@ -5766,6 +7009,13 @@ __metadata: languageName: node linkType: hard +"caniuse-lite@npm:^1.0.30001754": + version: 1.0.30001757 + resolution: "caniuse-lite@npm:1.0.30001757" + checksum: 10c0/3ccb71fa2bf1f8c96ff1bf9b918b08806fed33307e20a3ce3259155fda131eaf96cfcd88d3d309c8fd7f8285cc71d89a3b93648a1c04814da31c301f98508d42 + languageName: node + linkType: hard + "chai@npm:^5.2.0": version: 5.3.3 resolution: "chai@npm:5.3.3" @@ -5789,13 +7039,6 @@ __metadata: languageName: node linkType: hard -"chalk@npm:^5.4.1": - version: 5.6.2 - resolution: "chalk@npm:5.6.2" - checksum: 10c0/99a4b0f0e7991796b1e7e3f52dceb9137cae2a9dfc8fc0784a550dc4c558e15ab32ed70b14b21b52beb2679b4892b41a0aa44249bcb996f01e125d58477c6976 - languageName: node - linkType: hard - "change-case@npm:^5.4.4": version: 5.4.4 resolution: "change-case@npm:5.4.4" @@ -5836,10 +7079,10 @@ __metadata: languageName: node linkType: hard -"ci-info@npm:^4.3.0": - version: 4.3.0 - resolution: "ci-info@npm:4.3.0" - checksum: 10c0/60d3dfe95d75c01454ec1cfd5108617dd598a28a2a3e148bd7e1523c1c208b5f5a3007cafcbe293e6fd0a5a310cc32217c5dc54743eeabc0a2bec80072fc055c +"ci-info@npm:^4.3.1": + version: 4.3.1 + resolution: "ci-info@npm:4.3.1" + checksum: 10c0/7dd82000f514d76ddfe7775e4cb0d66e5c638f5fa0e2a3be29557e898da0d32ac04f231217d414d07fb968b1fbc6d980ee17ddde0d2c516f23da9cfff608f6c1 languageName: node linkType: hard @@ -5870,13 +7113,13 @@ __metadata: languageName: node linkType: hard -"cli-truncate@npm:^4.0.0": - version: 4.0.0 - resolution: "cli-truncate@npm:4.0.0" +"cli-truncate@npm:^5.0.0": + version: 5.1.1 + resolution: "cli-truncate@npm:5.1.1" dependencies: - slice-ansi: "npm:^5.0.0" - string-width: "npm:^7.0.0" - checksum: 10c0/d7f0b73e3d9b88cb496e6c086df7410b541b56a43d18ade6a573c9c18bd001b1c3fba1ad578f741a4218fdc794d042385f8ac02c25e1c295a2d8b9f3cb86eb4c + slice-ansi: "npm:^7.1.0" + string-width: "npm:^8.0.0" + checksum: 10c0/3842920829a62f3e041ce39199050c42706c3c9c756a4efc8b86d464e102d1fa031d8b1b9b2e3bb36e1017c763558275472d031bdc884c1eff22a2f20e4f6b0a languageName: node linkType: hard @@ -5966,10 +7209,10 @@ __metadata: languageName: node linkType: hard -"commander@npm:14.0.1": - version: 14.0.1 - resolution: "commander@npm:14.0.1" - checksum: 10c0/64439c0651ddd01c1d0f48c8f08e97c18a0a1fa693879451f1203ad01132af2c2aa85da24cf0d8e098ab9e6dc385a756be670d2999a3c628ec745c3ec124587b +"commander@npm:14.0.2": + version: 14.0.2 + resolution: "commander@npm:14.0.2" + checksum: 10c0/245abd1349dbad5414cb6517b7b5c584895c02c4f7836ff5395f301192b8566f9796c82d7bd6c92d07eba8775fe4df86602fca5d86d8d10bcc2aded1e21c2aeb languageName: node linkType: hard @@ -6089,12 +7332,21 @@ __metadata: languageName: node linkType: hard -"core-js-compat@npm:^3.44.0": - version: 3.45.1 - resolution: "core-js-compat@npm:3.45.1" +"copy-paste@npm:^2.2.0": + version: 2.2.0 + resolution: "copy-paste@npm:2.2.0" + dependencies: + iconv-lite: "npm:^0.4.8" + checksum: 10c0/19478195a75ef150a6f29ef9450bd95f3d1488ea00f70c655c98993156439e713725151ab132ef06b125cd8151f90f785b076a583fef6fae5e1f71f4283ac2e8 + languageName: node + linkType: hard + +"core-js-compat@npm:^3.46.0": + version: 3.47.0 + resolution: "core-js-compat@npm:3.47.0" dependencies: - browserslist: "npm:^4.25.3" - checksum: 10c0/b22996d3ca7e4f6758725f9ebbb61d422466d7ec0359158563264069ec066e7d2539fc7daebaa8aaf7b0bde73114ce42519611a0f0edb471139349e0cd11e183 + browserslist: "npm:^4.28.0" + checksum: 10c0/71da415899633120db7638dd7b250eee56031f63c4560dcba8eeeafd1168fae171d59b223e3fd2e0aa543a490d64bac7d946764721e2c05897056fdfb22cce33 languageName: node linkType: hard @@ -6243,25 +7495,25 @@ __metadata: languageName: node linkType: hard -"cssnano-preset-default@npm:^7.0.9": - version: 7.0.9 - resolution: "cssnano-preset-default@npm:7.0.9" +"cssnano-preset-default@npm:^7.0.10": + version: 7.0.10 + resolution: "cssnano-preset-default@npm:7.0.10" dependencies: - browserslist: "npm:^4.25.1" + browserslist: "npm:^4.27.0" css-declaration-sorter: "npm:^7.2.0" cssnano-utils: "npm:^5.0.1" postcss-calc: "npm:^10.1.1" - postcss-colormin: "npm:^7.0.4" - postcss-convert-values: "npm:^7.0.7" - postcss-discard-comments: "npm:^7.0.4" + postcss-colormin: "npm:^7.0.5" + postcss-convert-values: "npm:^7.0.8" + postcss-discard-comments: "npm:^7.0.5" postcss-discard-duplicates: "npm:^7.0.2" postcss-discard-empty: "npm:^7.0.1" postcss-discard-overridden: "npm:^7.0.1" postcss-merge-longhand: "npm:^7.0.5" - postcss-merge-rules: "npm:^7.0.6" + postcss-merge-rules: "npm:^7.0.7" postcss-minify-font-values: "npm:^7.0.1" postcss-minify-gradients: "npm:^7.0.1" - postcss-minify-params: "npm:^7.0.4" + postcss-minify-params: "npm:^7.0.5" postcss-minify-selectors: "npm:^7.0.5" postcss-normalize-charset: "npm:^7.0.1" postcss-normalize-display-values: "npm:^7.0.1" @@ -6269,17 +7521,17 @@ __metadata: postcss-normalize-repeat-style: "npm:^7.0.1" postcss-normalize-string: "npm:^7.0.1" postcss-normalize-timing-functions: "npm:^7.0.1" - postcss-normalize-unicode: "npm:^7.0.4" + postcss-normalize-unicode: "npm:^7.0.5" postcss-normalize-url: "npm:^7.0.1" postcss-normalize-whitespace: "npm:^7.0.1" postcss-ordered-values: "npm:^7.0.2" - postcss-reduce-initial: "npm:^7.0.4" + postcss-reduce-initial: "npm:^7.0.5" postcss-reduce-transforms: "npm:^7.0.1" postcss-svgo: "npm:^7.1.0" postcss-unique-selectors: "npm:^7.0.4" peerDependencies: postcss: ^8.4.32 - checksum: 10c0/5590f751596a8f782418a9dc72b8f365a9d53d3e42e606d9ce1db5f8ad74daee044b880e228565c36bfe701094738fa04f4f4429ad34087580d1e84b2a7b7ff9 + checksum: 10c0/40803294c3a2d7dec919c5f0ecc2370d6abf5f6514875a27f58caf7f42cbb86c9ce32f72624a80d0b6289d70e2675312368bc1e4cc0d9e5c320172866b89681e languageName: node linkType: hard @@ -6292,15 +7544,15 @@ __metadata: languageName: node linkType: hard -"cssnano@npm:^7.1.1": - version: 7.1.1 - resolution: "cssnano@npm:7.1.1" +"cssnano@npm:^7.1.2": + version: 7.1.2 + resolution: "cssnano@npm:7.1.2" dependencies: - cssnano-preset-default: "npm:^7.0.9" + cssnano-preset-default: "npm:^7.0.10" lilconfig: "npm:^3.1.3" peerDependencies: postcss: ^8.4.32 - checksum: 10c0/d761e86277dabfa986a34de4c8c79c555b0982b66b9e80a4a4c60956b5d34ae94c5464d74ab8c222578ee5f78c157ff7310386827a0f9cb847263797f738b300 + checksum: 10c0/3879ea5d3dd649b8dc1b96d3917c505895be6da9e7251b88f9d4a7567fcead95fdc0bd96482284df06997b08f0e36b9f917a2f6d6710d46186dd92dc953c6216 languageName: node linkType: hard @@ -6338,7 +7590,7 @@ __metadata: languageName: node linkType: hard -"db0@npm:^0.3.2": +"db0@npm:^0.3.2, db0@npm:^0.3.4": version: 0.3.4 resolution: "db0@npm:0.3.4" peerDependencies: @@ -6365,7 +7617,7 @@ __metadata: languageName: node linkType: hard -"debug@npm:4, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.3.3, debug@npm:^4.3.4, debug@npm:^4.3.5, debug@npm:^4.4.0, debug@npm:^4.4.1": +"debug@npm:4, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.3.3, debug@npm:^4.3.4, debug@npm:^4.3.5, debug@npm:^4.4.0, debug@npm:^4.4.1, debug@npm:^4.4.3": version: 4.4.3 resolution: "debug@npm:4.4.3" dependencies: @@ -6491,7 +7743,7 @@ __metadata: languageName: node linkType: hard -"defu@npm:^6.1.1, defu@npm:^6.1.4": +"defu@npm:^6.1.4": version: 6.1.4 resolution: "defu@npm:6.1.4" checksum: 10c0/2d6cc366262dc0cb8096e429368e44052fdf43ed48e53ad84cc7c9407f890301aa5fcb80d0995abaaf842b3949f154d060be4160f7a46cb2bc2f7726c81526f5 @@ -6535,20 +7787,27 @@ __metadata: languageName: node linkType: hard -"detect-libc@npm:^2.0.0, detect-libc@npm:^2.0.3, detect-libc@npm:^2.0.4": +"detect-libc@npm:^2.0.0, detect-libc@npm:^2.0.3": version: 2.1.1 resolution: "detect-libc@npm:2.1.1" checksum: 10c0/97053299c1f68c7c4adf7b78c8d506e1d5f3a3fbc775920aaa0ecf7f8fcc6dfa46338a6ca82fe4500b4a51937def314584265a4ec9d565577485c4496aa7d64e languageName: node linkType: hard -"devalue@npm:^5.1.1, devalue@npm:^5.3.2": +"devalue@npm:^5.1.1": version: 5.3.2 resolution: "devalue@npm:5.3.2" checksum: 10c0/2dab403779233224285afe4b30eaded038df10cb89b8f2c1e41dd855a8e6b634aa24175b87f64df665204bb9a6a6e7758d172682719b9c5cf3cef336ff9fa507 languageName: node linkType: hard +"devalue@npm:^5.4.2": + version: 5.5.0 + resolution: "devalue@npm:5.5.0" + checksum: 10c0/7604b11f2afc83e006922a211b1e975109e260c58fba740a5706f148be7345bafdf3ece1012a78bc9dab68f7d88da53e6d6e403e06358932a86f2cd3541e4297 + languageName: node + linkType: hard + "diff@npm:^8.0.2": version: 8.0.2 resolution: "diff@npm:8.0.2" @@ -6597,15 +7856,15 @@ __metadata: languageName: node linkType: hard -"dompurify@npm:3.2.7": - version: 3.2.7 - resolution: "dompurify@npm:3.2.7" +"dompurify@npm:3.3.0": + version: 3.3.0 + resolution: "dompurify@npm:3.3.0" dependencies: "@types/trusted-types": "npm:^2.0.7" dependenciesMeta: "@types/trusted-types": optional: true - checksum: 10c0/d41bb31a72f1acdf9b84c56723c549924b05d92a39a15bd8c40bec9007ff80d5fccf844bc53ee12af5b69044f9a7ce24a1e71c267a4f49cf38711379ed8c1363 + checksum: 10c0/66b1787b0bc8250d8f58e13284cf7f5f6bb400a0a55515e7a2a030316a4bb0d8306fdb669c17ed86ed58ff7e53c62b5da4488c2f261d11c58870fe01b8fcc486 languageName: node linkType: hard @@ -6620,6 +7879,15 @@ __metadata: languageName: node linkType: hard +"dot-prop@npm:^10.1.0": + version: 10.1.0 + resolution: "dot-prop@npm:10.1.0" + dependencies: + type-fest: "npm:^5.0.0" + checksum: 10c0/b034a06f017909ed55c6c164ddea962ccdce3d88b9b092f7106a9b738116a4cd003db5d47a0c6e140e93adbf1922b5e3a147e3d4a124c8556862940446ba5f75 + languageName: node + linkType: hard + "dot-prop@npm:^9.0.0": version: 9.0.0 resolution: "dot-prop@npm:9.0.0" @@ -6629,7 +7897,7 @@ __metadata: languageName: node linkType: hard -"dotenv@npm:17.2.3, dotenv@npm:^17.2.2": +"dotenv@npm:17.2.3, dotenv@npm:^17.2.2, dotenv@npm:^17.2.3": version: 17.2.3 resolution: "dotenv@npm:17.2.3" checksum: 10c0/c884403209f713214a1b64d4d1defa4934c2aa5b0002f5a670ae298a51e3c3ad3ba79dfee2f8df49f01ae74290fcd9acdb1ab1d09c7bfb42b539036108bb2ba0 @@ -6703,6 +7971,13 @@ __metadata: languageName: node linkType: hard +"electron-to-chromium@npm:^1.5.249": + version: 1.5.260 + resolution: "electron-to-chromium@npm:1.5.260" + checksum: 10c0/5be308adbe7f9b370f628eb3ae35528bccc8e8592ee4848f9dfa308af658deaa87e915dd6929b6993e712929e7e6828f40434814506476ae11051381ee423fdf + languageName: node + linkType: hard + "emoji-regex@npm:^10.0.0, emoji-regex@npm:^10.3.0": version: 10.5.0 resolution: "emoji-regex@npm:10.5.0" @@ -6953,6 +8228,184 @@ __metadata: languageName: node linkType: hard +"esbuild@npm:^0.25.11, esbuild@npm:^0.25.12": + version: 0.25.12 + resolution: "esbuild@npm:0.25.12" + dependencies: + "@esbuild/aix-ppc64": "npm:0.25.12" + "@esbuild/android-arm": "npm:0.25.12" + "@esbuild/android-arm64": "npm:0.25.12" + "@esbuild/android-x64": "npm:0.25.12" + "@esbuild/darwin-arm64": "npm:0.25.12" + "@esbuild/darwin-x64": "npm:0.25.12" + "@esbuild/freebsd-arm64": "npm:0.25.12" + "@esbuild/freebsd-x64": "npm:0.25.12" + "@esbuild/linux-arm": "npm:0.25.12" + "@esbuild/linux-arm64": "npm:0.25.12" + "@esbuild/linux-ia32": "npm:0.25.12" + "@esbuild/linux-loong64": "npm:0.25.12" + "@esbuild/linux-mips64el": "npm:0.25.12" + "@esbuild/linux-ppc64": "npm:0.25.12" + "@esbuild/linux-riscv64": "npm:0.25.12" + "@esbuild/linux-s390x": "npm:0.25.12" + "@esbuild/linux-x64": "npm:0.25.12" + "@esbuild/netbsd-arm64": "npm:0.25.12" + "@esbuild/netbsd-x64": "npm:0.25.12" + "@esbuild/openbsd-arm64": "npm:0.25.12" + "@esbuild/openbsd-x64": "npm:0.25.12" + "@esbuild/openharmony-arm64": "npm:0.25.12" + "@esbuild/sunos-x64": "npm:0.25.12" + "@esbuild/win32-arm64": "npm:0.25.12" + "@esbuild/win32-ia32": "npm:0.25.12" + "@esbuild/win32-x64": "npm:0.25.12" + dependenciesMeta: + "@esbuild/aix-ppc64": + optional: true + "@esbuild/android-arm": + optional: true + "@esbuild/android-arm64": + optional: true + "@esbuild/android-x64": + optional: true + "@esbuild/darwin-arm64": + optional: true + "@esbuild/darwin-x64": + optional: true + "@esbuild/freebsd-arm64": + optional: true + "@esbuild/freebsd-x64": + optional: true + "@esbuild/linux-arm": + optional: true + "@esbuild/linux-arm64": + optional: true + "@esbuild/linux-ia32": + optional: true + "@esbuild/linux-loong64": + optional: true + "@esbuild/linux-mips64el": + optional: true + "@esbuild/linux-ppc64": + optional: true + "@esbuild/linux-riscv64": + optional: true + "@esbuild/linux-s390x": + optional: true + "@esbuild/linux-x64": + optional: true + "@esbuild/netbsd-arm64": + optional: true + "@esbuild/netbsd-x64": + optional: true + "@esbuild/openbsd-arm64": + optional: true + "@esbuild/openbsd-x64": + optional: true + "@esbuild/openharmony-arm64": + optional: true + "@esbuild/sunos-x64": + optional: true + "@esbuild/win32-arm64": + optional: true + "@esbuild/win32-ia32": + optional: true + "@esbuild/win32-x64": + optional: true + bin: + esbuild: bin/esbuild + checksum: 10c0/c205357531423220a9de8e1e6c6514242bc9b1666e762cd67ccdf8fdfdc3f1d0bd76f8d9383958b97ad4c953efdb7b6e8c1f9ca5951cd2b7c5235e8755b34a6b + languageName: node + linkType: hard + +"esbuild@npm:^0.27.0": + version: 0.27.0 + resolution: "esbuild@npm:0.27.0" + dependencies: + "@esbuild/aix-ppc64": "npm:0.27.0" + "@esbuild/android-arm": "npm:0.27.0" + "@esbuild/android-arm64": "npm:0.27.0" + "@esbuild/android-x64": "npm:0.27.0" + "@esbuild/darwin-arm64": "npm:0.27.0" + "@esbuild/darwin-x64": "npm:0.27.0" + "@esbuild/freebsd-arm64": "npm:0.27.0" + "@esbuild/freebsd-x64": "npm:0.27.0" + "@esbuild/linux-arm": "npm:0.27.0" + "@esbuild/linux-arm64": "npm:0.27.0" + "@esbuild/linux-ia32": "npm:0.27.0" + "@esbuild/linux-loong64": "npm:0.27.0" + "@esbuild/linux-mips64el": "npm:0.27.0" + "@esbuild/linux-ppc64": "npm:0.27.0" + "@esbuild/linux-riscv64": "npm:0.27.0" + "@esbuild/linux-s390x": "npm:0.27.0" + "@esbuild/linux-x64": "npm:0.27.0" + "@esbuild/netbsd-arm64": "npm:0.27.0" + "@esbuild/netbsd-x64": "npm:0.27.0" + "@esbuild/openbsd-arm64": "npm:0.27.0" + "@esbuild/openbsd-x64": "npm:0.27.0" + "@esbuild/openharmony-arm64": "npm:0.27.0" + "@esbuild/sunos-x64": "npm:0.27.0" + "@esbuild/win32-arm64": "npm:0.27.0" + "@esbuild/win32-ia32": "npm:0.27.0" + "@esbuild/win32-x64": "npm:0.27.0" + dependenciesMeta: + "@esbuild/aix-ppc64": + optional: true + "@esbuild/android-arm": + optional: true + "@esbuild/android-arm64": + optional: true + "@esbuild/android-x64": + optional: true + "@esbuild/darwin-arm64": + optional: true + "@esbuild/darwin-x64": + optional: true + "@esbuild/freebsd-arm64": + optional: true + "@esbuild/freebsd-x64": + optional: true + "@esbuild/linux-arm": + optional: true + "@esbuild/linux-arm64": + optional: true + "@esbuild/linux-ia32": + optional: true + "@esbuild/linux-loong64": + optional: true + "@esbuild/linux-mips64el": + optional: true + "@esbuild/linux-ppc64": + optional: true + "@esbuild/linux-riscv64": + optional: true + "@esbuild/linux-s390x": + optional: true + "@esbuild/linux-x64": + optional: true + "@esbuild/netbsd-arm64": + optional: true + "@esbuild/netbsd-x64": + optional: true + "@esbuild/openbsd-arm64": + optional: true + "@esbuild/openbsd-x64": + optional: true + "@esbuild/openharmony-arm64": + optional: true + "@esbuild/sunos-x64": + optional: true + "@esbuild/win32-arm64": + optional: true + "@esbuild/win32-ia32": + optional: true + "@esbuild/win32-x64": + optional: true + bin: + esbuild: bin/esbuild + checksum: 10c0/a3a1deec285337b7dfe25cbb9aa8765d27a0192b610a8477a39bf5bd907a6bdb75e98898b61fb4337114cfadb13163bd95977db14e241373115f548e235b40a2 + languageName: node + linkType: hard + "escalade@npm:^3.1.1, escalade@npm:^3.2.0": version: 3.2.0 resolution: "escalade@npm:3.2.0" @@ -7028,7 +8481,7 @@ __metadata: languageName: node linkType: hard -"eslint-flat-config-utils@npm:2.1.4, eslint-flat-config-utils@npm:^2.1.1": +"eslint-flat-config-utils@npm:2.1.4, eslint-flat-config-utils@npm:^2.1.4": version: 2.1.4 resolution: "eslint-flat-config-utils@npm:2.1.4" dependencies: @@ -7103,36 +8556,40 @@ __metadata: languageName: node linkType: hard -"eslint-plugin-jsdoc@npm:^54.1.0": - version: 54.7.0 - resolution: "eslint-plugin-jsdoc@npm:54.7.0" +"eslint-plugin-jsdoc@npm:^61.1.10": + version: 61.4.1 + resolution: "eslint-plugin-jsdoc@npm:61.4.1" dependencies: - "@es-joy/jsdoccomment": "npm:~0.56.0" + "@es-joy/jsdoccomment": "npm:~0.76.0" + "@es-joy/resolve.exports": "npm:1.2.0" are-docs-informative: "npm:^0.0.2" comment-parser: "npm:1.4.1" - debug: "npm:^4.4.1" + debug: "npm:^4.4.3" escape-string-regexp: "npm:^4.0.0" espree: "npm:^10.4.0" esquery: "npm:^1.6.0" + html-entities: "npm:^2.6.0" + object-deep-merge: "npm:^2.0.0" parse-imports-exports: "npm:^0.2.4" - semver: "npm:^7.7.2" + semver: "npm:^7.7.3" spdx-expression-parse: "npm:^4.0.0" + to-valid-identifier: "npm:^1.0.0" peerDependencies: eslint: ^7.0.0 || ^8.0.0 || ^9.0.0 - checksum: 10c0/a3a7177fac149d0ef9e70484adb5e0e2939ce9c170f94d4ac8e1b2f0da42bbd9b0357840c3a96871085363d2d66a15fd8003e780d00496787326dbe594bdbf32 + checksum: 10c0/564f89bad71dcdbf6a45c27d16113333a5251f97a60bcc0e7346ea1b19dc1258991e1f585c89a2978e279288be2e180dde58c57f63cd49ac3db6604a5d4c581c languageName: node linkType: hard -"eslint-plugin-perfectionist@npm:4.15.0": - version: 4.15.0 - resolution: "eslint-plugin-perfectionist@npm:4.15.0" +"eslint-plugin-perfectionist@npm:4.15.1": + version: 4.15.1 + resolution: "eslint-plugin-perfectionist@npm:4.15.1" dependencies: - "@typescript-eslint/types": "npm:^8.34.1" - "@typescript-eslint/utils": "npm:^8.34.1" + "@typescript-eslint/types": "npm:^8.38.0" + "@typescript-eslint/utils": "npm:^8.38.0" natural-orderby: "npm:^5.0.0" peerDependencies: eslint: ">=8.45.0" - checksum: 10c0/e6fd86a083be063580bde3947e885b6f3a18b8c47a77874a64d0e22a54e6bafcb356bcc5f0c00634ea7ee1c8744757b3e6f2dc478f01e4874090ffc1aea9efa6 + checksum: 10c0/65db8d0962c208578937cffa0da9678ce51bbecae2b8d2bd0eb4e9f9b691ac71fc85c0c14368584f02abda6c82cf19495d1fbe8584d7ef09c0d2126d62925d1c languageName: node linkType: hard @@ -7153,42 +8610,42 @@ __metadata: languageName: node linkType: hard -"eslint-plugin-unicorn@npm:^60.0.0": - version: 60.0.0 - resolution: "eslint-plugin-unicorn@npm:60.0.0" +"eslint-plugin-unicorn@npm:^62.0.0": + version: 62.0.0 + resolution: "eslint-plugin-unicorn@npm:62.0.0" dependencies: - "@babel/helper-validator-identifier": "npm:^7.27.1" - "@eslint-community/eslint-utils": "npm:^4.7.0" - "@eslint/plugin-kit": "npm:^0.3.3" + "@babel/helper-validator-identifier": "npm:^7.28.5" + "@eslint-community/eslint-utils": "npm:^4.9.0" + "@eslint/plugin-kit": "npm:^0.4.0" change-case: "npm:^5.4.4" - ci-info: "npm:^4.3.0" + ci-info: "npm:^4.3.1" clean-regexp: "npm:^1.0.0" - core-js-compat: "npm:^3.44.0" + core-js-compat: "npm:^3.46.0" esquery: "npm:^1.6.0" find-up-simple: "npm:^1.0.1" - globals: "npm:^16.3.0" + globals: "npm:^16.4.0" indent-string: "npm:^5.0.0" is-builtin-module: "npm:^5.0.0" jsesc: "npm:^3.1.0" pluralize: "npm:^8.0.0" regexp-tree: "npm:^0.1.27" - regjsparser: "npm:^0.12.0" - semver: "npm:^7.7.2" - strip-indent: "npm:^4.0.0" + regjsparser: "npm:^0.13.0" + semver: "npm:^7.7.3" + strip-indent: "npm:^4.1.1" peerDependencies: - eslint: ">=9.29.0" - checksum: 10c0/8411c0cf7c1c331e6c3f7ac30a8892d5ec8993e2b958ceda7a435167a7c9f7c94abf5ae03a8f04a70586bf0214041601b61d80db2fefbe70036c77501a57315b + eslint: ">=9.38.0" + checksum: 10c0/29a8651a75e088a774319f752320625ce8944da53011dca2f9ddcbf2e7297651e90139e808d702041b1df320706b1ecc32d04daef52ab105cb9e8d540e03a0d2 languageName: node linkType: hard -"eslint-plugin-vue@npm:10.5.0, eslint-plugin-vue@npm:^10.4.0": - version: 10.5.0 - resolution: "eslint-plugin-vue@npm:10.5.0" +"eslint-plugin-vue@npm:10.6.0, eslint-plugin-vue@npm:^10.5.1": + version: 10.6.0 + resolution: "eslint-plugin-vue@npm:10.6.0" dependencies: "@eslint-community/eslint-utils": "npm:^4.4.0" natural-compare: "npm:^1.4.0" nth-check: "npm:^2.1.1" - postcss-selector-parser: "npm:^6.0.15" + postcss-selector-parser: "npm:^7.1.0" semver: "npm:^7.6.3" xml-name-validator: "npm:^4.0.0" peerDependencies: @@ -7201,7 +8658,7 @@ __metadata: optional: true "@typescript-eslint/parser": optional: true - checksum: 10c0/28d8a9abfef9dcdb5d8df10ce16f8b2d94ff82514b22c2c9623f426ddf4b10b1c3dcf841992a74a460e6bc1637f57db3adf3b3cbb2e5348cc42572e559dde87b + checksum: 10c0/78fa8d69dbd65f6166a990f45f562b81d555bad5902437fbc3026ef2e90c76888e22e054eff592476a7e685e551a1d2aef49798ec74109ca98f1331c193bc7bd languageName: node linkType: hard @@ -7274,23 +8731,22 @@ __metadata: languageName: node linkType: hard -"eslint@npm:9.36.0": - version: 9.36.0 - resolution: "eslint@npm:9.36.0" +"eslint@npm:9.39.1": + version: 9.39.1 + resolution: "eslint@npm:9.39.1" dependencies: "@eslint-community/eslint-utils": "npm:^4.8.0" "@eslint-community/regexpp": "npm:^4.12.1" - "@eslint/config-array": "npm:^0.21.0" - "@eslint/config-helpers": "npm:^0.3.1" - "@eslint/core": "npm:^0.15.2" + "@eslint/config-array": "npm:^0.21.1" + "@eslint/config-helpers": "npm:^0.4.2" + "@eslint/core": "npm:^0.17.0" "@eslint/eslintrc": "npm:^3.3.1" - "@eslint/js": "npm:9.36.0" - "@eslint/plugin-kit": "npm:^0.3.5" + "@eslint/js": "npm:9.39.1" + "@eslint/plugin-kit": "npm:^0.4.1" "@humanfs/node": "npm:^0.16.6" "@humanwhocodes/module-importer": "npm:^1.0.1" "@humanwhocodes/retry": "npm:^0.4.2" "@types/estree": "npm:^1.0.6" - "@types/json-schema": "npm:^7.0.15" ajv: "npm:^6.12.4" chalk: "npm:^4.0.0" cross-spawn: "npm:^7.0.6" @@ -7320,7 +8776,7 @@ __metadata: optional: true bin: eslint: bin/eslint.js - checksum: 10c0/0e2705a94847813b03f2f3c1367c0708319cbb66458250a09b2d056a088c56e079a1c1d76c44feebf51971d9ce64d010373b2a4f007cd1026fc24f95c89836df + checksum: 10c0/59b2480639404ba24578ca480f973683b87b7aac8aa7e349240474a39067804fd13cd8b9cb22fee074170b8c7c563b57bab703ec0f0d3f81ea017e5d2cad299d languageName: node linkType: hard @@ -7479,6 +8935,13 @@ __metadata: languageName: node linkType: hard +"exsolve@npm:^1.0.8": + version: 1.0.8 + resolution: "exsolve@npm:1.0.8" + checksum: 10c0/65e44ae05bd4a4a5d87cfdbbd6b8f24389282cf9f85fa5feb17ca87ad3f354877e6af4cd99e02fc29044174891f82d1d68c77f69234410eb8f163530e6278c67 + languageName: node + linkType: hard + "fake-indexeddb@npm:^6.0.1": version: 6.2.2 resolution: "fake-indexeddb@npm:6.2.2" @@ -7486,6 +8949,13 @@ __metadata: languageName: node linkType: hard +"fake-indexeddb@npm:^6.2.4": + version: 6.2.5 + resolution: "fake-indexeddb@npm:6.2.5" + checksum: 10c0/6c5e2fe84a61daa06d7ad63699d1041fe61847f15f92db12415634b3db94f363a64be9e08a3c3c4434af9c3c0132086b85c4d5dc5e8e06edae1e7daf70ce1f3c + languageName: node + linkType: hard + "fast-deep-equal@npm:^3.1.1, fast-deep-equal@npm:^3.1.3": version: 3.1.3 resolution: "fast-deep-equal@npm:3.1.3" @@ -7527,14 +8997,14 @@ __metadata: languageName: node linkType: hard -"fast-npm-meta@npm:^0.4.6": +"fast-npm-meta@npm:^0.4.6, fast-npm-meta@npm:^0.4.7": version: 0.4.7 resolution: "fast-npm-meta@npm:0.4.7" checksum: 10c0/c3d4a624f8714a4a73e93a5a7851b9065f195c30511f7193b621cfa959caf9cdbc59fdc0575f18a59cdbc6b9f10dd1fbe924661885e6c2e876feb63329112816 languageName: node linkType: hard -"fastq@npm:^1.15.0, fastq@npm:^1.6.0": +"fastq@npm:^1.6.0": version: 1.19.1 resolution: "fastq@npm:1.19.1" dependencies: @@ -7607,14 +9077,13 @@ __metadata: languageName: node linkType: hard -"find-up@npm:^7.0.0": - version: 7.0.0 - resolution: "find-up@npm:7.0.0" +"find-up@npm:^8.0.0": + version: 8.0.0 + resolution: "find-up@npm:8.0.0" dependencies: - locate-path: "npm:^7.2.0" - path-exists: "npm:^5.0.0" - unicorn-magic: "npm:^0.1.0" - checksum: 10c0/e6ee3e6154560bc0ab3bc3b7d1348b31513f9bdf49a5dd2e952495427d559fa48cdf33953e85a309a323898b43fa1bfbc8b80c880dfc16068384783034030008 + locate-path: "npm:^8.0.0" + unicorn-magic: "npm:^0.3.0" + checksum: 10c0/4c6d2cb92f74bd42ec7344c881a46f6455010d3993f8f55b09bb64298c0a13e11e10200147624db8938590890a15ade69c40f0172698388d0999899f0f2a70a5 languageName: node linkType: hard @@ -7700,6 +9169,13 @@ __metadata: languageName: node linkType: hard +"fraction.js@npm:^5.3.4": + version: 5.3.4 + resolution: "fraction.js@npm:5.3.4" + checksum: 10c0/f90079fe9bfc665e0a07079938e8ff71115bce9462f17b32fc283f163b0540ec34dc33df8ed41bb56f028316b04361b9a9995b9ee9258617f8338e0b05c5f95a + languageName: node + linkType: hard + "fresh@npm:^2.0.0": version: 2.0.0 resolution: "fresh@npm:2.0.0" @@ -7824,7 +9300,7 @@ __metadata: languageName: node linkType: hard -"get-east-asian-width@npm:^1.0.0, get-east-asian-width@npm:^1.3.1": +"get-east-asian-width@npm:^1.0.0, get-east-asian-width@npm:^1.3.0, get-east-asian-width@npm:^1.3.1": version: 1.4.0 resolution: "get-east-asian-width@npm:1.4.0" checksum: 10c0/4e481d418e5a32061c36fbb90d1b225a254cc5b2df5f0b25da215dcd335a3c111f0c2023ffda43140727a9cafb62dac41d022da82c08f31083ee89f714ee3b83 @@ -7975,6 +9451,22 @@ __metadata: languageName: node linkType: hard +"glob@npm:^10.5.0": + version: 10.5.0 + resolution: "glob@npm:10.5.0" + dependencies: + foreground-child: "npm:^3.1.0" + jackspeak: "npm:^3.1.2" + minimatch: "npm:^9.0.4" + minipass: "npm:^7.1.2" + package-json-from-dist: "npm:^1.0.0" + path-scurry: "npm:^1.11.1" + bin: + glob: dist/esm/bin.mjs + checksum: 10c0/100705eddbde6323e7b35e1d1ac28bcb58322095bd8e63a7d0bef1a2cdafe0d0f7922a981b2b48369a4f8c1b077be5c171804534c3509dfe950dde15fbe6d828 + languageName: node + linkType: hard + "global-directory@npm:^4.0.1": version: 4.0.1 resolution: "global-directory@npm:4.0.1" @@ -8005,10 +9497,10 @@ __metadata: languageName: node linkType: hard -"globals@npm:^16.3.0": - version: 16.4.0 - resolution: "globals@npm:16.4.0" - checksum: 10c0/a14b447a78b664b42f6d324e8675fcae6fe5e57924fecc1f6328dce08af9b2ca3a3138501e1b1f244a49814a732dc60cfc1aa24e714e0b64ac8bd18910bfac90 +"globals@npm:^16.4.0": + version: 16.5.0 + resolution: "globals@npm:16.5.0" + checksum: 10c0/615241dae7851c8012f5aa0223005b1ed6607713d6813de0741768bd4ddc39353117648f1a7086b4b0fa45eae733f1c0a0fe369aa4e543bb63f8de8990178ea9 languageName: node linkType: hard @@ -8026,6 +9518,20 @@ __metadata: languageName: node linkType: hard +"globby@npm:^15.0.0": + version: 15.0.0 + resolution: "globby@npm:15.0.0" + dependencies: + "@sindresorhus/merge-streams": "npm:^4.0.0" + fast-glob: "npm:^3.3.3" + ignore: "npm:^7.0.5" + path-type: "npm:^6.0.0" + slash: "npm:^5.1.0" + unicorn-magic: "npm:^0.3.0" + checksum: 10c0/e4107be0579bcdd9642b8dff86aeafeaf62b2b9dd116669ab6e02e0e0c07ada0d972c2db182dee7588b460fe8c8919ddcc6b1cc4db405ca3a2adc9d35fa6eb21 + languageName: node + linkType: hard + "gopd@npm:^1.0.1, gopd@npm:^1.2.0": version: 1.2.0 resolution: "gopd@npm:1.2.0" @@ -8147,6 +9653,13 @@ __metadata: languageName: node linkType: hard +"html-entities@npm:^2.6.0": + version: 2.6.0 + resolution: "html-entities@npm:2.6.0" + checksum: 10c0/7c8b15d9ea0cd00dc9279f61bab002ba6ca8a7a0f3c36ed2db3530a67a9621c017830d1d2c1c65beb9b8e3436ea663e9cf8b230472e0e413359399413b27c8b7 + languageName: node + linkType: hard + "html-escaper@npm:^2.0.0": version: 2.0.2 resolution: "html-escaper@npm:2.0.2" @@ -8215,6 +9728,15 @@ __metadata: languageName: node linkType: hard +"iconv-lite@npm:^0.4.8": + version: 0.4.24 + resolution: "iconv-lite@npm:0.4.24" + dependencies: + safer-buffer: "npm:>= 2.1.2 < 3" + checksum: 10c0/c6886a24cc00f2a059767440ec1bc00d334a89f250db8e0f7feb4961c8727118457e27c495ba94d082e51d3baca378726cd110aaf7ded8b9bbfd6a44760cf1d4 + languageName: node + linkType: hard + "iconv-lite@npm:^0.6.2": version: 0.6.3 resolution: "iconv-lite@npm:0.6.3" @@ -8252,6 +9774,13 @@ __metadata: languageName: node linkType: hard +"image-meta@npm:^0.2.2": + version: 0.2.2 + resolution: "image-meta@npm:0.2.2" + checksum: 10c0/4c821b9f09e5117f4aab2864e07d8aea9e1079a432f2d93ec79aafbdc4fbd164451388bb6d6c0a1f1e3b5b5838e42533b0feedba05fab2e1a6c0242c559ca40b + languageName: node + linkType: hard + "import-fresh@npm:^3.2.1": version: 3.3.1 resolution: "import-fresh@npm:3.3.1" @@ -8338,6 +9867,23 @@ __metadata: languageName: node linkType: hard +"ioredis@npm:^5.8.2": + version: 5.8.2 + resolution: "ioredis@npm:5.8.2" + dependencies: + "@ioredis/commands": "npm:1.4.0" + cluster-key-slot: "npm:^1.1.0" + debug: "npm:^4.3.4" + denque: "npm:^2.1.0" + lodash.defaults: "npm:^4.2.0" + lodash.isarguments: "npm:^3.1.0" + redis-errors: "npm:^1.2.0" + redis-parser: "npm:^3.0.0" + standard-as-callback: "npm:^2.1.0" + checksum: 10c0/305e385f811d49908899e32c2de69616cd059f909afd9e0a53e54f596b1a5835ee3449bfc6a3c49afbc5a2fd27990059e316cc78f449c94024957bd34c826d88 + languageName: node + linkType: hard + "ip-address@npm:^10.0.1": version: 10.0.1 resolution: "ip-address@npm:10.0.1" @@ -8468,13 +10014,6 @@ __metadata: languageName: node linkType: hard -"is-fullwidth-code-point@npm:^4.0.0": - version: 4.0.0 - resolution: "is-fullwidth-code-point@npm:4.0.0" - checksum: 10c0/df2a717e813567db0f659c306d61f2f804d480752526886954a2a3e2246c7745fd07a52b5fecf2b68caf0a6c79dcdace6166fdf29cc76ed9975cc334f0a018b8 - languageName: node - linkType: hard - "is-fullwidth-code-point@npm:^5.0.0": version: 5.1.0 resolution: "is-fullwidth-code-point@npm:5.1.0" @@ -8764,7 +10303,7 @@ __metadata: languageName: node linkType: hard -"jiti@npm:^2.1.2, jiti@npm:^2.4.2, jiti@npm:^2.5.1, jiti@npm:^2.6.0": +"jiti@npm:^2.1.2, jiti@npm:^2.4.2, jiti@npm:^2.5.1, jiti@npm:^2.6.1": version: 2.6.1 resolution: "jiti@npm:2.6.1" bin: @@ -8851,14 +10390,14 @@ __metadata: languageName: node linkType: hard -"jsdoc-type-pratt-parser@npm:~5.1.0": - version: 5.1.1 - resolution: "jsdoc-type-pratt-parser@npm:5.1.1" - checksum: 10c0/4d7760e048d7620ff793a77b5194a2d7816f9ab667b016a05cc81af40d052474b23a320db3af17d08e48b354de068482a44bbcbf65704ad3c593bf016345f59a +"jsdoc-type-pratt-parser@npm:~6.10.0": + version: 6.10.0 + resolution: "jsdoc-type-pratt-parser@npm:6.10.0" + checksum: 10c0/8ea395df0cae0e41d4bdba5f8d81b8d3e467fe53d1e4182a5d4e653235a5f17d60ed137343d68dbc74fa10e767f1c58fb85b1f6d5489c2cf16fc7216cc6d3e1a languageName: node linkType: hard -"jsesc@npm:^3.0.2, jsesc@npm:^3.1.0": +"jsesc@npm:^3.0.2, jsesc@npm:^3.1.0, jsesc@npm:~3.1.0": version: 3.1.0 resolution: "jsesc@npm:3.1.0" bin: @@ -8867,15 +10406,6 @@ __metadata: languageName: node linkType: hard -"jsesc@npm:~3.0.2": - version: 3.0.2 - resolution: "jsesc@npm:3.0.2" - bin: - jsesc: bin/jsesc - checksum: 10c0/ef22148f9e793180b14d8a145ee6f9f60f301abf443288117b4b6c53d0ecd58354898dc506ccbb553a5f7827965cd38bc5fb726575aae93c5e8915e2de8290e1 - languageName: node - linkType: hard - "json-buffer@npm:3.0.1": version: 3.0.1 resolution: "json-buffer@npm:3.0.1" @@ -9008,6 +10538,16 @@ __metadata: languageName: node linkType: hard +"launch-editor@npm:^2.12.0": + version: 2.12.0 + resolution: "launch-editor@npm:2.12.0" + dependencies: + picocolors: "npm:^1.1.1" + shell-quote: "npm:^1.8.3" + checksum: 10c0/fac5e7ad90bf185594cad4c831a52419eef50e667c4eddb5b0a58eb5f944e16d947636ee767b9896ffd46a51db34925edd3b854c48efb47f6d767ffd7d904e71 + languageName: node + linkType: hard + "lazystream@npm:^1.0.0": version: 1.0.1 resolution: "lazystream@npm:1.0.1" @@ -9027,92 +10567,102 @@ __metadata: languageName: node linkType: hard -"lightningcss-darwin-arm64@npm:1.30.1": - version: 1.30.1 - resolution: "lightningcss-darwin-arm64@npm:1.30.1" +"lightningcss-android-arm64@npm:1.30.2": + version: 1.30.2 + resolution: "lightningcss-android-arm64@npm:1.30.2" + conditions: os=android & cpu=arm64 + languageName: node + linkType: hard + +"lightningcss-darwin-arm64@npm:1.30.2": + version: 1.30.2 + resolution: "lightningcss-darwin-arm64@npm:1.30.2" conditions: os=darwin & cpu=arm64 languageName: node linkType: hard -"lightningcss-darwin-x64@npm:1.30.1": - version: 1.30.1 - resolution: "lightningcss-darwin-x64@npm:1.30.1" +"lightningcss-darwin-x64@npm:1.30.2": + version: 1.30.2 + resolution: "lightningcss-darwin-x64@npm:1.30.2" conditions: os=darwin & cpu=x64 languageName: node linkType: hard -"lightningcss-freebsd-x64@npm:1.30.1": - version: 1.30.1 - resolution: "lightningcss-freebsd-x64@npm:1.30.1" +"lightningcss-freebsd-x64@npm:1.30.2": + version: 1.30.2 + resolution: "lightningcss-freebsd-x64@npm:1.30.2" conditions: os=freebsd & cpu=x64 languageName: node linkType: hard -"lightningcss-linux-arm-gnueabihf@npm:1.30.1": - version: 1.30.1 - resolution: "lightningcss-linux-arm-gnueabihf@npm:1.30.1" +"lightningcss-linux-arm-gnueabihf@npm:1.30.2": + version: 1.30.2 + resolution: "lightningcss-linux-arm-gnueabihf@npm:1.30.2" conditions: os=linux & cpu=arm languageName: node linkType: hard -"lightningcss-linux-arm64-gnu@npm:1.30.1": - version: 1.30.1 - resolution: "lightningcss-linux-arm64-gnu@npm:1.30.1" +"lightningcss-linux-arm64-gnu@npm:1.30.2": + version: 1.30.2 + resolution: "lightningcss-linux-arm64-gnu@npm:1.30.2" conditions: os=linux & cpu=arm64 & libc=glibc languageName: node linkType: hard -"lightningcss-linux-arm64-musl@npm:1.30.1": - version: 1.30.1 - resolution: "lightningcss-linux-arm64-musl@npm:1.30.1" +"lightningcss-linux-arm64-musl@npm:1.30.2": + version: 1.30.2 + resolution: "lightningcss-linux-arm64-musl@npm:1.30.2" conditions: os=linux & cpu=arm64 & libc=musl languageName: node linkType: hard -"lightningcss-linux-x64-gnu@npm:1.30.1": - version: 1.30.1 - resolution: "lightningcss-linux-x64-gnu@npm:1.30.1" +"lightningcss-linux-x64-gnu@npm:1.30.2": + version: 1.30.2 + resolution: "lightningcss-linux-x64-gnu@npm:1.30.2" conditions: os=linux & cpu=x64 & libc=glibc languageName: node linkType: hard -"lightningcss-linux-x64-musl@npm:1.30.1": - version: 1.30.1 - resolution: "lightningcss-linux-x64-musl@npm:1.30.1" +"lightningcss-linux-x64-musl@npm:1.30.2": + version: 1.30.2 + resolution: "lightningcss-linux-x64-musl@npm:1.30.2" conditions: os=linux & cpu=x64 & libc=musl languageName: node linkType: hard -"lightningcss-win32-arm64-msvc@npm:1.30.1": - version: 1.30.1 - resolution: "lightningcss-win32-arm64-msvc@npm:1.30.1" +"lightningcss-win32-arm64-msvc@npm:1.30.2": + version: 1.30.2 + resolution: "lightningcss-win32-arm64-msvc@npm:1.30.2" conditions: os=win32 & cpu=arm64 languageName: node linkType: hard -"lightningcss-win32-x64-msvc@npm:1.30.1": - version: 1.30.1 - resolution: "lightningcss-win32-x64-msvc@npm:1.30.1" +"lightningcss-win32-x64-msvc@npm:1.30.2": + version: 1.30.2 + resolution: "lightningcss-win32-x64-msvc@npm:1.30.2" conditions: os=win32 & cpu=x64 languageName: node linkType: hard -"lightningcss@npm:1.30.1": - version: 1.30.1 - resolution: "lightningcss@npm:1.30.1" +"lightningcss@npm:1.30.2": + version: 1.30.2 + resolution: "lightningcss@npm:1.30.2" dependencies: detect-libc: "npm:^2.0.3" - lightningcss-darwin-arm64: "npm:1.30.1" - lightningcss-darwin-x64: "npm:1.30.1" - lightningcss-freebsd-x64: "npm:1.30.1" - lightningcss-linux-arm-gnueabihf: "npm:1.30.1" - lightningcss-linux-arm64-gnu: "npm:1.30.1" - lightningcss-linux-arm64-musl: "npm:1.30.1" - lightningcss-linux-x64-gnu: "npm:1.30.1" - lightningcss-linux-x64-musl: "npm:1.30.1" - lightningcss-win32-arm64-msvc: "npm:1.30.1" - lightningcss-win32-x64-msvc: "npm:1.30.1" + lightningcss-android-arm64: "npm:1.30.2" + lightningcss-darwin-arm64: "npm:1.30.2" + lightningcss-darwin-x64: "npm:1.30.2" + lightningcss-freebsd-x64: "npm:1.30.2" + lightningcss-linux-arm-gnueabihf: "npm:1.30.2" + lightningcss-linux-arm64-gnu: "npm:1.30.2" + lightningcss-linux-arm64-musl: "npm:1.30.2" + lightningcss-linux-x64-gnu: "npm:1.30.2" + lightningcss-linux-x64-musl: "npm:1.30.2" + lightningcss-win32-arm64-msvc: "npm:1.30.2" + lightningcss-win32-x64-msvc: "npm:1.30.2" dependenciesMeta: + lightningcss-android-arm64: + optional: true lightningcss-darwin-arm64: optional: true lightningcss-darwin-x64: @@ -9133,7 +10683,7 @@ __metadata: optional: true lightningcss-win32-x64-msvc: optional: true - checksum: 10c0/1e1ad908f3c68bf39d964a6735435a8dd5474fb2765076732d64a7b6aa2af1f084da65a9462443a9adfebf7dcfb02fb532fce1d78697f2a9de29c8f40f09aee3 + checksum: 10c0/5c0c73a33946dab65908d5cd1325df4efa290efb77f940b60f40448b5ab9a87d3ea665ef9bcf00df4209705050ecf2f7ecc649f44d6dfa5905bb50f15717e78d languageName: node linkType: hard @@ -9160,23 +10710,20 @@ __metadata: languageName: node linkType: hard -"lint-staged@npm:16.1.0": - version: 16.1.0 - resolution: "lint-staged@npm:16.1.0" +"lint-staged@npm:16.2.7": + version: 16.2.7 + resolution: "lint-staged@npm:16.2.7" dependencies: - chalk: "npm:^5.4.1" - commander: "npm:^14.0.0" - debug: "npm:^4.4.1" - lilconfig: "npm:^3.1.3" - listr2: "npm:^8.3.3" + commander: "npm:^14.0.2" + listr2: "npm:^9.0.5" micromatch: "npm:^4.0.8" - nano-spawn: "npm:^1.0.2" + nano-spawn: "npm:^2.0.0" pidtree: "npm:^0.6.0" string-argv: "npm:^0.3.2" - yaml: "npm:^2.8.0" + yaml: "npm:^2.8.1" bin: lint-staged: bin/lint-staged.js - checksum: 10c0/5cc33d61ec2c682e488eb3fcea5c153ce486623b80314f2c56af438ad78d73c7fcd3e7c911d273ac740bd34f1e030d35d4fb92d8e476984150c0c59724ac7fa4 + checksum: 10c0/9a677c21a8112d823ae5bc565ba2c9e7b803786f2a021c46827a55fe44ed59def96edb24fc99c06a2545cdbbf366022ad82addcb3bf60c712f3b98ef92069717 languageName: node linkType: hard @@ -9209,17 +10756,17 @@ __metadata: languageName: node linkType: hard -"listr2@npm:^8.3.3": - version: 8.3.3 - resolution: "listr2@npm:8.3.3" +"listr2@npm:^9.0.5": + version: 9.0.5 + resolution: "listr2@npm:9.0.5" dependencies: - cli-truncate: "npm:^4.0.0" + cli-truncate: "npm:^5.0.0" colorette: "npm:^2.0.20" eventemitter3: "npm:^5.0.1" log-update: "npm:^6.1.0" rfdc: "npm:^1.4.1" wrap-ansi: "npm:^9.0.0" - checksum: 10c0/0792f8a7fd482fa516e21689e012e07081cab3653172ca606090622cfa0024c784a1eba8095a17948a0e9a4aa98a80f7c9c90f78a0dd35173d6802f9cc123a82 + checksum: 10c0/46448d1ba0addc9d71aeafd05bb8e86ded9641ccad930ac302c2bd2ad71580375604743e18586fcb8f11906edf98e8e17fca75ba0759947bf275d381f68e311d languageName: node linkType: hard @@ -9259,12 +10806,12 @@ __metadata: languageName: node linkType: hard -"locate-path@npm:^7.2.0": - version: 7.2.0 - resolution: "locate-path@npm:7.2.0" +"locate-path@npm:^8.0.0": + version: 8.0.0 + resolution: "locate-path@npm:8.0.0" dependencies: p-locate: "npm:^6.0.0" - checksum: 10c0/139e8a7fe11cfbd7f20db03923cacfa5db9e14fa14887ea121345597472b4a63c1a42a8a5187defeeff6acf98fd568da7382aa39682d38f0af27433953a97751 + checksum: 10c0/4c837878b6d1b8557c5d1c624d11d6721d77f4627c14bd84182c85cbb9ec8fc01b5b5f089e21fab17b30fc5ecc14216a3d31f754dfcc5299f1fe9f4c83482fee languageName: node linkType: hard @@ -9379,16 +10926,16 @@ __metadata: languageName: node linkType: hard -"magic-string-ast@npm:^1.0.0": - version: 1.0.2 - resolution: "magic-string-ast@npm:1.0.2" +"magic-string-ast@npm:^1.0.2": + version: 1.0.3 + resolution: "magic-string-ast@npm:1.0.3" dependencies: - magic-string: "npm:^0.30.17" - checksum: 10c0/eb9a3dd4746d2cf2c54cd7fcb084a031f343e1c932dfada278bdf571b1d50ded632229a2e4a92293a66b46c01146b5c26b6885bc424652c8fd3ba167de29c5d8 + magic-string: "npm:^0.30.19" + checksum: 10c0/77c05960ef6f7261274e847008488d6c8b6d26a47d5ff54724f1e6c43d18b64d6d27feba313e8189160637e2f52475a26df8e665312b237140ba0be46c8ab35f languageName: node linkType: hard -"magic-string@npm:^0.30.11, magic-string@npm:^0.30.12, magic-string@npm:^0.30.17, magic-string@npm:^0.30.18, magic-string@npm:^0.30.19, magic-string@npm:^0.30.3": +"magic-string@npm:^0.30.11, magic-string@npm:^0.30.12, magic-string@npm:^0.30.17, magic-string@npm:^0.30.19, magic-string@npm:^0.30.3": version: 0.30.19 resolution: "magic-string@npm:0.30.19" dependencies: @@ -9397,6 +10944,15 @@ __metadata: languageName: node linkType: hard +"magic-string@npm:^0.30.21": + version: 0.30.21 + resolution: "magic-string@npm:0.30.21" + dependencies: + "@jridgewell/sourcemap-codec": "npm:^1.5.5" + checksum: 10c0/299378e38f9a270069fc62358522ddfb44e94244baa0d6a8980ab2a9b2490a1d03b236b447eee309e17eb3bddfa482c61259d47960eb018a904f0ded52780c4a + languageName: node + linkType: hard + "magicast@npm:^0.3.5": version: 0.3.5 resolution: "magicast@npm:0.3.5" @@ -9408,6 +10964,17 @@ __metadata: languageName: node linkType: hard +"magicast@npm:^0.5.0, magicast@npm:^0.5.1": + version: 0.5.1 + resolution: "magicast@npm:0.5.1" + dependencies: + "@babel/parser": "npm:^7.28.5" + "@babel/types": "npm:^7.28.5" + source-map-js: "npm:^1.2.1" + checksum: 10c0/a00bbf3688b9b3e83c10b3bfe3f106cc2ccbf20c4f2dc1c9020a10556dfe0a6a6605a445ee8e86a6e2b484ec519a657b5e405532684f72678c62e4c0d32f962c + languageName: node + linkType: hard + "make-dir@npm:^4.0.0": version: 4.0.0 resolution: "make-dir@npm:4.0.0" @@ -9582,7 +11149,7 @@ __metadata: languageName: node linkType: hard -"mime@npm:^4.0.7": +"mime@npm:^4.0.7, mime@npm:^4.1.0": version: 4.1.0 resolution: "mime@npm:4.1.0" bin: @@ -9759,7 +11326,7 @@ __metadata: languageName: node linkType: hard -"mrmime@npm:^2.0.0, mrmime@npm:^2.0.1": +"mrmime@npm:^2.0.0": version: 2.0.1 resolution: "mrmime@npm:2.0.1" checksum: 10c0/af05afd95af202fdd620422f976ad67dc18e6ee29beb03dd1ce950ea6ef664de378e44197246df4c7cdd73d47f2e7143a6e26e473084b9e4aa2095c0ad1e1761 @@ -9796,10 +11363,10 @@ __metadata: languageName: node linkType: hard -"nano-spawn@npm:^1.0.2": - version: 1.0.3 - resolution: "nano-spawn@npm:1.0.3" - checksum: 10c0/ea18857e493710a50ded333dd71677953bd9bd9e6a17ade74af957763c50a9a02205ef31bc0d6784f5b3ad82db3d9f47531e9baac2acf01118f9b7c35bd9d5de +"nano-spawn@npm:^2.0.0": + version: 2.0.0 + resolution: "nano-spawn@npm:2.0.0" + checksum: 10c0/d00f9b5739f86e28cb732ffd774793e110810cded246b8393c75c4f22674af47f98ee37b19f022ada2d8c9425f800e841caa0662fbff4c0930a10e39339fb366 languageName: node linkType: hard @@ -9812,7 +11379,7 @@ __metadata: languageName: node linkType: hard -"nanoid@npm:^5.0.6, nanoid@npm:^5.1.0": +"nanoid@npm:^5.0.6, nanoid@npm:^5.1.0, nanoid@npm:^5.1.5": version: 5.1.6 resolution: "nanoid@npm:5.1.6" bin: @@ -9890,7 +11457,7 @@ __metadata: languageName: node linkType: hard -"nitropack@npm:^2.11.13, nitropack@npm:^2.12.5": +"nitropack@npm:^2.11.13": version: 2.12.6 resolution: "nitropack@npm:2.12.6" dependencies: @@ -9976,6 +11543,92 @@ __metadata: languageName: node linkType: hard +"nitropack@npm:^2.12.9": + version: 2.12.9 + resolution: "nitropack@npm:2.12.9" + dependencies: + "@cloudflare/kv-asset-handler": "npm:^0.4.0" + "@rollup/plugin-alias": "npm:^5.1.1" + "@rollup/plugin-commonjs": "npm:^28.0.9" + "@rollup/plugin-inject": "npm:^5.0.5" + "@rollup/plugin-json": "npm:^6.1.0" + "@rollup/plugin-node-resolve": "npm:^16.0.3" + "@rollup/plugin-replace": "npm:^6.0.2" + "@rollup/plugin-terser": "npm:^0.4.4" + "@vercel/nft": "npm:^0.30.3" + archiver: "npm:^7.0.1" + c12: "npm:^3.3.1" + chokidar: "npm:^4.0.3" + citty: "npm:^0.1.6" + compatx: "npm:^0.2.0" + confbox: "npm:^0.2.2" + consola: "npm:^3.4.2" + cookie-es: "npm:^2.0.0" + croner: "npm:^9.1.0" + crossws: "npm:^0.3.5" + db0: "npm:^0.3.4" + defu: "npm:^6.1.4" + destr: "npm:^2.0.5" + dot-prop: "npm:^10.1.0" + esbuild: "npm:^0.25.11" + escape-string-regexp: "npm:^5.0.0" + etag: "npm:^1.8.1" + exsolve: "npm:^1.0.7" + globby: "npm:^15.0.0" + gzip-size: "npm:^7.0.0" + h3: "npm:^1.15.4" + hookable: "npm:^5.5.3" + httpxy: "npm:^0.1.7" + ioredis: "npm:^5.8.2" + jiti: "npm:^2.6.1" + klona: "npm:^2.0.6" + knitwork: "npm:^1.2.0" + listhen: "npm:^1.9.0" + magic-string: "npm:^0.30.21" + magicast: "npm:^0.5.0" + mime: "npm:^4.1.0" + mlly: "npm:^1.8.0" + node-fetch-native: "npm:^1.6.7" + node-mock-http: "npm:^1.0.3" + ofetch: "npm:^1.5.0" + ohash: "npm:^2.0.11" + pathe: "npm:^2.0.3" + perfect-debounce: "npm:^2.0.0" + pkg-types: "npm:^2.3.0" + pretty-bytes: "npm:^7.1.0" + radix3: "npm:^1.1.2" + rollup: "npm:^4.52.5" + rollup-plugin-visualizer: "npm:^6.0.5" + scule: "npm:^1.3.0" + semver: "npm:^7.7.3" + serve-placeholder: "npm:^2.0.2" + serve-static: "npm:^2.2.0" + source-map: "npm:^0.7.6" + std-env: "npm:^3.10.0" + ufo: "npm:^1.6.1" + ultrahtml: "npm:^1.6.0" + uncrypto: "npm:^0.1.3" + unctx: "npm:^2.4.1" + unenv: "npm:^2.0.0-rc.23" + unimport: "npm:^5.5.0" + unplugin-utils: "npm:^0.3.1" + unstorage: "npm:^1.17.1" + untyped: "npm:^2.0.0" + unwasm: "npm:^0.3.11" + youch: "npm:^4.1.0-beta.11" + youch-core: "npm:^0.3.3" + peerDependencies: + xml2js: ^0.6.2 + peerDependenciesMeta: + xml2js: + optional: true + bin: + nitro: dist/cli/index.mjs + nitropack: dist/cli/index.mjs + checksum: 10c0/5702d6494fbe52fe7579c05afee51ba970e0cf178f637253c23ca1ff2c8a7d3f77e39907202aea1ed20eca799212486a2f91d72a2ecaa095540f99362fb8d74f + languageName: node + linkType: hard + "node-addon-api@npm:^7.0.0": version: 7.1.1 resolution: "node-addon-api@npm:7.1.1" @@ -10058,6 +11711,13 @@ __metadata: languageName: node linkType: hard +"node-releases@npm:^2.0.27": + version: 2.0.27 + resolution: "node-releases@npm:2.0.27" + checksum: 10c0/f1e6583b7833ea81880627748d28a3a7ff5703d5409328c216ae57befbced10ce2c991bea86434e8ec39003bd017f70481e2e5f8c1f7e0a7663241f81d6e00e2 + languageName: node + linkType: hard + "nopt@npm:^7.2.1": version: 7.2.1 resolution: "nopt@npm:7.2.1" @@ -10140,88 +11800,82 @@ __metadata: languageName: node linkType: hard -"nuxt-security@npm:2.4.0": - version: 2.4.0 - resolution: "nuxt-security@npm:2.4.0" +"nuxt-security@npm:2.5.0": + version: 2.5.0 + resolution: "nuxt-security@npm:2.5.0" dependencies: - "@nuxt/kit": "npm:^3.11.2" + "@nuxt/kit": "npm:^4.2.1" basic-auth: "npm:^2.0.1" - defu: "npm:^6.1.1" + defu: "npm:^6.1.4" nuxt-csurf: "npm:^1.6.5" - pathe: "npm:^1.0.0" + pathe: "npm:^2.0.3" unplugin-remove: "npm:^1.0.3" - xss: "npm:^1.0.14" - checksum: 10c0/b7a4c3f00b1419507f27c2359054c0b69d81e04441a9c950f139777a5b60d31b6b6df5411c5d1096ececcf2c48594666ece37df7a01c28421bc6b45eb7ab4fad + xss: "npm:^1.0.15" + checksum: 10c0/73158add8727d1dc8a288eb43d089ebb5dbd8062810d519f2f48800020eec92aa21f848b2594291ca58d4a86c4412504ab39ef5059b5c0901c4ad06e9b1f5897 languageName: node linkType: hard -"nuxt@npm:4.1.2": - version: 4.1.2 - resolution: "nuxt@npm:4.1.2" - dependencies: - "@nuxt/cli": "npm:^3.28.0" - "@nuxt/devalue": "npm:^2.0.2" - "@nuxt/devtools": "npm:^2.6.3" - "@nuxt/kit": "npm:4.1.2" - "@nuxt/schema": "npm:4.1.2" +"nuxt@npm:4.2.1": + version: 4.2.1 + resolution: "nuxt@npm:4.2.1" + dependencies: + "@dxup/nuxt": "npm:^0.2.1" + "@nuxt/cli": "npm:^3.30.0" + "@nuxt/devtools": "npm:^3.0.1" + "@nuxt/kit": "npm:4.2.1" + "@nuxt/nitro-server": "npm:4.2.1" + "@nuxt/schema": "npm:4.2.1" "@nuxt/telemetry": "npm:^2.6.6" - "@nuxt/vite-builder": "npm:4.1.2" - "@unhead/vue": "npm:^2.0.14" - "@vue/shared": "npm:^3.5.21" - c12: "npm:^3.2.0" + "@nuxt/vite-builder": "npm:4.2.1" + "@unhead/vue": "npm:^2.0.19" + "@vue/shared": "npm:^3.5.23" + c12: "npm:^3.3.1" chokidar: "npm:^4.0.3" compatx: "npm:^0.2.0" consola: "npm:^3.4.2" cookie-es: "npm:^2.0.0" defu: "npm:^6.1.4" destr: "npm:^2.0.5" - devalue: "npm:^5.3.2" + devalue: "npm:^5.4.2" errx: "npm:^0.1.0" - esbuild: "npm:^0.25.9" escape-string-regexp: "npm:^5.0.0" - estree-walker: "npm:^3.0.3" exsolve: "npm:^1.0.7" h3: "npm:^1.15.4" hookable: "npm:^5.5.3" ignore: "npm:^7.0.5" impound: "npm:^1.0.0" - jiti: "npm:^2.5.1" + jiti: "npm:^2.6.1" klona: "npm:^2.0.6" knitwork: "npm:^1.2.0" - magic-string: "npm:^0.30.19" + magic-string: "npm:^0.30.21" mlly: "npm:^1.8.0" - mocked-exports: "npm:^0.1.1" nanotar: "npm:^0.2.0" - nitropack: "npm:^2.12.5" - nypm: "npm:^0.6.1" - ofetch: "npm:^1.4.1" + nypm: "npm:^0.6.2" + ofetch: "npm:^1.5.1" ohash: "npm:^2.0.11" - on-change: "npm:^5.0.1" - oxc-minify: "npm:^0.87.0" - oxc-parser: "npm:^0.87.0" - oxc-transform: "npm:^0.87.0" + on-change: "npm:^6.0.1" + oxc-minify: "npm:^0.96.0" + oxc-parser: "npm:^0.96.0" + oxc-transform: "npm:^0.96.0" oxc-walker: "npm:^0.5.2" pathe: "npm:^2.0.3" perfect-debounce: "npm:^2.0.0" pkg-types: "npm:^2.3.0" radix3: "npm:^1.1.2" scule: "npm:^1.3.0" - semver: "npm:^7.7.2" - std-env: "npm:^3.9.0" + semver: "npm:^7.7.3" + std-env: "npm:^3.10.0" tinyglobby: "npm:^0.2.15" ufo: "npm:^1.6.1" ultrahtml: "npm:^1.6.0" uncrypto: "npm:^0.1.3" unctx: "npm:^2.4.1" - unimport: "npm:^5.2.0" + unimport: "npm:^5.5.0" unplugin: "npm:^2.3.10" - unplugin-vue-router: "npm:^0.15.0" - unstorage: "npm:^1.17.1" + unplugin-vue-router: "npm:^0.16.1" untyped: "npm:^2.0.0" - vue: "npm:^3.5.21" - vue-bundle-renderer: "npm:^2.1.2" - vue-devtools-stub: "npm:^0.1.0" - vue-router: "npm:^4.5.1" + vue: "npm:^3.5.23" + vue-router: "npm:^4.6.3" peerDependencies: "@parcel/watcher": ^2.1.0 "@types/node": ">=18.12.0" @@ -10233,11 +11887,11 @@ __metadata: bin: nuxi: bin/nuxt.mjs nuxt: bin/nuxt.mjs - checksum: 10c0/79d91ad8df6f32304a934c4147e9676dcab581455ebff6104bf6e6410e3ce88dac83945aafae69980e2b90eb42c8ebda79889ee1157bf3d851af5a7c6e29b955 + checksum: 10c0/adba812270c09548876ca7e0363ffd80b289ea86f70b7e9e35f59b757ab150d26670dce1e37664a91fc019297f6034baa025c0f785bf8affbcebd02195253160 languageName: node linkType: hard -"nypm@npm:^0.6.0, nypm@npm:^0.6.1, nypm@npm:^0.6.2": +"nypm@npm:^0.6.0, nypm@npm:^0.6.2": version: 0.6.2 resolution: "nypm@npm:0.6.2" dependencies: @@ -10266,6 +11920,13 @@ __metadata: languageName: node linkType: hard +"object-deep-merge@npm:^2.0.0": + version: 2.0.0 + resolution: "object-deep-merge@npm:2.0.0" + checksum: 10c0/69e8741131ad49fa8720fb96007a3c82dca1119b5d874151d2ecbcc3b44ccd46e8553c7a30b0abcba752c099ba361bbba97f33a68c9ae54c57eed7be116ffc97 + languageName: node + linkType: hard + "object-hash@npm:^2.2.0": version: 2.2.0 resolution: "object-hash@npm:2.2.0" @@ -10311,6 +11972,13 @@ __metadata: languageName: node linkType: hard +"obug@npm:^2.0.0": + version: 2.1.1 + resolution: "obug@npm:2.1.1" + checksum: 10c0/59dccd7de72a047e08f8649e94c1015ec72f94eefb6ddb57fb4812c4b425a813bc7e7cd30c9aca20db3c59abc3c85cc7a62bb656a968741d770f4e8e02bc2e78 + languageName: node + linkType: hard + "ofetch@npm:^1.4.1": version: 1.4.1 resolution: "ofetch@npm:1.4.1" @@ -10322,6 +11990,17 @@ __metadata: languageName: node linkType: hard +"ofetch@npm:^1.5.0, ofetch@npm:^1.5.1": + version: 1.5.1 + resolution: "ofetch@npm:1.5.1" + dependencies: + destr: "npm:^2.0.5" + node-fetch-native: "npm:^1.6.7" + ufo: "npm:^1.6.1" + checksum: 10c0/97ebc600512ea0ab401e97c73313218cc53c9b530b32ec8c995c347b0c68887129993168d1753f527761a64c6f93a5d823ce1378ccec95fc65a606f323a79a6c + languageName: node + linkType: hard + "ohash@npm:^2.0.11": version: 2.0.11 resolution: "ohash@npm:2.0.11" @@ -10336,10 +12015,10 @@ __metadata: languageName: node linkType: hard -"on-change@npm:^5.0.1": - version: 5.0.1 - resolution: "on-change@npm:5.0.1" - checksum: 10c0/3be9929f45af820288ff3c104290e8bf6346889a51f7b0ccb6eb20802e5b84e34917811a5f267c3fa94729061be99c7aeb99036d1ce6099c673551e8beb04d0a +"on-change@npm:^6.0.1": + version: 6.0.1 + resolution: "on-change@npm:6.0.1" + checksum: 10c0/a0e81061210234486dccc8ced9265d09a55eb28db479fab6ce24cc226d73d86516b887cde2fcf381ae9434b6f16840c64caf5a64a7080710369256008d689711 languageName: node linkType: hard @@ -10426,25 +12105,25 @@ __metadata: languageName: node linkType: hard -"oxc-minify@npm:^0.87.0": - version: 0.87.0 - resolution: "oxc-minify@npm:0.87.0" - dependencies: - "@oxc-minify/binding-android-arm64": "npm:0.87.0" - "@oxc-minify/binding-darwin-arm64": "npm:0.87.0" - "@oxc-minify/binding-darwin-x64": "npm:0.87.0" - "@oxc-minify/binding-freebsd-x64": "npm:0.87.0" - "@oxc-minify/binding-linux-arm-gnueabihf": "npm:0.87.0" - "@oxc-minify/binding-linux-arm-musleabihf": "npm:0.87.0" - "@oxc-minify/binding-linux-arm64-gnu": "npm:0.87.0" - "@oxc-minify/binding-linux-arm64-musl": "npm:0.87.0" - "@oxc-minify/binding-linux-riscv64-gnu": "npm:0.87.0" - "@oxc-minify/binding-linux-s390x-gnu": "npm:0.87.0" - "@oxc-minify/binding-linux-x64-gnu": "npm:0.87.0" - "@oxc-minify/binding-linux-x64-musl": "npm:0.87.0" - "@oxc-minify/binding-wasm32-wasi": "npm:0.87.0" - "@oxc-minify/binding-win32-arm64-msvc": "npm:0.87.0" - "@oxc-minify/binding-win32-x64-msvc": "npm:0.87.0" +"oxc-minify@npm:^0.96.0": + version: 0.96.0 + resolution: "oxc-minify@npm:0.96.0" + dependencies: + "@oxc-minify/binding-android-arm64": "npm:0.96.0" + "@oxc-minify/binding-darwin-arm64": "npm:0.96.0" + "@oxc-minify/binding-darwin-x64": "npm:0.96.0" + "@oxc-minify/binding-freebsd-x64": "npm:0.96.0" + "@oxc-minify/binding-linux-arm-gnueabihf": "npm:0.96.0" + "@oxc-minify/binding-linux-arm-musleabihf": "npm:0.96.0" + "@oxc-minify/binding-linux-arm64-gnu": "npm:0.96.0" + "@oxc-minify/binding-linux-arm64-musl": "npm:0.96.0" + "@oxc-minify/binding-linux-riscv64-gnu": "npm:0.96.0" + "@oxc-minify/binding-linux-s390x-gnu": "npm:0.96.0" + "@oxc-minify/binding-linux-x64-gnu": "npm:0.96.0" + "@oxc-minify/binding-linux-x64-musl": "npm:0.96.0" + "@oxc-minify/binding-wasm32-wasi": "npm:0.96.0" + "@oxc-minify/binding-win32-arm64-msvc": "npm:0.96.0" + "@oxc-minify/binding-win32-x64-msvc": "npm:0.96.0" dependenciesMeta: "@oxc-minify/binding-android-arm64": optional: true @@ -10476,30 +12155,30 @@ __metadata: optional: true "@oxc-minify/binding-win32-x64-msvc": optional: true - checksum: 10c0/fa27a08b3f15270efd897904eb8544663e030ca9269e430ec682dabf626436c967ee98004239d4ccd082bd5c9554b48a6abc63fb6bf9abbb90495a19109123b8 - languageName: node - linkType: hard - -"oxc-parser@npm:^0.81.0": - version: 0.81.0 - resolution: "oxc-parser@npm:0.81.0" - dependencies: - "@oxc-parser/binding-android-arm64": "npm:0.81.0" - "@oxc-parser/binding-darwin-arm64": "npm:0.81.0" - "@oxc-parser/binding-darwin-x64": "npm:0.81.0" - "@oxc-parser/binding-freebsd-x64": "npm:0.81.0" - "@oxc-parser/binding-linux-arm-gnueabihf": "npm:0.81.0" - "@oxc-parser/binding-linux-arm-musleabihf": "npm:0.81.0" - "@oxc-parser/binding-linux-arm64-gnu": "npm:0.81.0" - "@oxc-parser/binding-linux-arm64-musl": "npm:0.81.0" - "@oxc-parser/binding-linux-riscv64-gnu": "npm:0.81.0" - "@oxc-parser/binding-linux-s390x-gnu": "npm:0.81.0" - "@oxc-parser/binding-linux-x64-gnu": "npm:0.81.0" - "@oxc-parser/binding-linux-x64-musl": "npm:0.81.0" - "@oxc-parser/binding-wasm32-wasi": "npm:0.81.0" - "@oxc-parser/binding-win32-arm64-msvc": "npm:0.81.0" - "@oxc-parser/binding-win32-x64-msvc": "npm:0.81.0" - "@oxc-project/types": "npm:^0.81.0" + checksum: 10c0/4462b9f8381f2c882feb57e1a9e9ac1523484278bd5c9bec5df48d32b39b6442ece0db83f255fe1de6ed9b1a36b44a863a1e65d988db9cfa78cd9b1db0d79606 + languageName: node + linkType: hard + +"oxc-parser@npm:^0.95.0": + version: 0.95.0 + resolution: "oxc-parser@npm:0.95.0" + dependencies: + "@oxc-parser/binding-android-arm64": "npm:0.95.0" + "@oxc-parser/binding-darwin-arm64": "npm:0.95.0" + "@oxc-parser/binding-darwin-x64": "npm:0.95.0" + "@oxc-parser/binding-freebsd-x64": "npm:0.95.0" + "@oxc-parser/binding-linux-arm-gnueabihf": "npm:0.95.0" + "@oxc-parser/binding-linux-arm-musleabihf": "npm:0.95.0" + "@oxc-parser/binding-linux-arm64-gnu": "npm:0.95.0" + "@oxc-parser/binding-linux-arm64-musl": "npm:0.95.0" + "@oxc-parser/binding-linux-riscv64-gnu": "npm:0.95.0" + "@oxc-parser/binding-linux-s390x-gnu": "npm:0.95.0" + "@oxc-parser/binding-linux-x64-gnu": "npm:0.95.0" + "@oxc-parser/binding-linux-x64-musl": "npm:0.95.0" + "@oxc-parser/binding-wasm32-wasi": "npm:0.95.0" + "@oxc-parser/binding-win32-arm64-msvc": "npm:0.95.0" + "@oxc-parser/binding-win32-x64-msvc": "npm:0.95.0" + "@oxc-project/types": "npm:^0.95.0" dependenciesMeta: "@oxc-parser/binding-android-arm64": optional: true @@ -10531,30 +12210,30 @@ __metadata: optional: true "@oxc-parser/binding-win32-x64-msvc": optional: true - checksum: 10c0/a0c38c1afa3d5bfae3c6ba34d59ae7c8828cc634ac0dccb0ce9b8fe800f284e7c3b2bac37fc6779abf27d362fb21df837fadcb63e98c79a41c7ac11d2508b8ba - languageName: node - linkType: hard - -"oxc-parser@npm:^0.87.0": - version: 0.87.0 - resolution: "oxc-parser@npm:0.87.0" - dependencies: - "@oxc-parser/binding-android-arm64": "npm:0.87.0" - "@oxc-parser/binding-darwin-arm64": "npm:0.87.0" - "@oxc-parser/binding-darwin-x64": "npm:0.87.0" - "@oxc-parser/binding-freebsd-x64": "npm:0.87.0" - "@oxc-parser/binding-linux-arm-gnueabihf": "npm:0.87.0" - "@oxc-parser/binding-linux-arm-musleabihf": "npm:0.87.0" - "@oxc-parser/binding-linux-arm64-gnu": "npm:0.87.0" - "@oxc-parser/binding-linux-arm64-musl": "npm:0.87.0" - "@oxc-parser/binding-linux-riscv64-gnu": "npm:0.87.0" - "@oxc-parser/binding-linux-s390x-gnu": "npm:0.87.0" - "@oxc-parser/binding-linux-x64-gnu": "npm:0.87.0" - "@oxc-parser/binding-linux-x64-musl": "npm:0.87.0" - "@oxc-parser/binding-wasm32-wasi": "npm:0.87.0" - "@oxc-parser/binding-win32-arm64-msvc": "npm:0.87.0" - "@oxc-parser/binding-win32-x64-msvc": "npm:0.87.0" - "@oxc-project/types": "npm:^0.87.0" + checksum: 10c0/342b017c12480fd152a4da21bdae5b076f951f2606b1487340a5bdb20b24d342e64a302176e5f175c330eacd262faf53bd5fc2731018f06366bde9be550ba9a4 + languageName: node + linkType: hard + +"oxc-parser@npm:^0.96.0": + version: 0.96.0 + resolution: "oxc-parser@npm:0.96.0" + dependencies: + "@oxc-parser/binding-android-arm64": "npm:0.96.0" + "@oxc-parser/binding-darwin-arm64": "npm:0.96.0" + "@oxc-parser/binding-darwin-x64": "npm:0.96.0" + "@oxc-parser/binding-freebsd-x64": "npm:0.96.0" + "@oxc-parser/binding-linux-arm-gnueabihf": "npm:0.96.0" + "@oxc-parser/binding-linux-arm-musleabihf": "npm:0.96.0" + "@oxc-parser/binding-linux-arm64-gnu": "npm:0.96.0" + "@oxc-parser/binding-linux-arm64-musl": "npm:0.96.0" + "@oxc-parser/binding-linux-riscv64-gnu": "npm:0.96.0" + "@oxc-parser/binding-linux-s390x-gnu": "npm:0.96.0" + "@oxc-parser/binding-linux-x64-gnu": "npm:0.96.0" + "@oxc-parser/binding-linux-x64-musl": "npm:0.96.0" + "@oxc-parser/binding-wasm32-wasi": "npm:0.96.0" + "@oxc-parser/binding-win32-arm64-msvc": "npm:0.96.0" + "@oxc-parser/binding-win32-x64-msvc": "npm:0.96.0" + "@oxc-project/types": "npm:^0.96.0" dependenciesMeta: "@oxc-parser/binding-android-arm64": optional: true @@ -10586,29 +12265,29 @@ __metadata: optional: true "@oxc-parser/binding-win32-x64-msvc": optional: true - checksum: 10c0/356028e7d0d1a461de0d76bfd434e4257d4b24646c604643206a2b75f591d8acb1781237c95cbf1ec32ff7cf79b6a42ce9c86ec5d93fb131cbf6e210b1d8b564 - languageName: node - linkType: hard - -"oxc-transform@npm:^0.81.0": - version: 0.81.0 - resolution: "oxc-transform@npm:0.81.0" - dependencies: - "@oxc-transform/binding-android-arm64": "npm:0.81.0" - "@oxc-transform/binding-darwin-arm64": "npm:0.81.0" - "@oxc-transform/binding-darwin-x64": "npm:0.81.0" - "@oxc-transform/binding-freebsd-x64": "npm:0.81.0" - "@oxc-transform/binding-linux-arm-gnueabihf": "npm:0.81.0" - "@oxc-transform/binding-linux-arm-musleabihf": "npm:0.81.0" - "@oxc-transform/binding-linux-arm64-gnu": "npm:0.81.0" - "@oxc-transform/binding-linux-arm64-musl": "npm:0.81.0" - "@oxc-transform/binding-linux-riscv64-gnu": "npm:0.81.0" - "@oxc-transform/binding-linux-s390x-gnu": "npm:0.81.0" - "@oxc-transform/binding-linux-x64-gnu": "npm:0.81.0" - "@oxc-transform/binding-linux-x64-musl": "npm:0.81.0" - "@oxc-transform/binding-wasm32-wasi": "npm:0.81.0" - "@oxc-transform/binding-win32-arm64-msvc": "npm:0.81.0" - "@oxc-transform/binding-win32-x64-msvc": "npm:0.81.0" + checksum: 10c0/0b896e08630509d55f379f9eb7b7918c5f83efadb76a03fd37652c634a9594a51d0bf269886b8d565cb7a3d7d23cea86a2174c6574de404292313214f3a8a567 + languageName: node + linkType: hard + +"oxc-transform@npm:^0.95.0": + version: 0.95.0 + resolution: "oxc-transform@npm:0.95.0" + dependencies: + "@oxc-transform/binding-android-arm64": "npm:0.95.0" + "@oxc-transform/binding-darwin-arm64": "npm:0.95.0" + "@oxc-transform/binding-darwin-x64": "npm:0.95.0" + "@oxc-transform/binding-freebsd-x64": "npm:0.95.0" + "@oxc-transform/binding-linux-arm-gnueabihf": "npm:0.95.0" + "@oxc-transform/binding-linux-arm-musleabihf": "npm:0.95.0" + "@oxc-transform/binding-linux-arm64-gnu": "npm:0.95.0" + "@oxc-transform/binding-linux-arm64-musl": "npm:0.95.0" + "@oxc-transform/binding-linux-riscv64-gnu": "npm:0.95.0" + "@oxc-transform/binding-linux-s390x-gnu": "npm:0.95.0" + "@oxc-transform/binding-linux-x64-gnu": "npm:0.95.0" + "@oxc-transform/binding-linux-x64-musl": "npm:0.95.0" + "@oxc-transform/binding-wasm32-wasi": "npm:0.95.0" + "@oxc-transform/binding-win32-arm64-msvc": "npm:0.95.0" + "@oxc-transform/binding-win32-x64-msvc": "npm:0.95.0" dependenciesMeta: "@oxc-transform/binding-android-arm64": optional: true @@ -10640,29 +12319,29 @@ __metadata: optional: true "@oxc-transform/binding-win32-x64-msvc": optional: true - checksum: 10c0/539638080dde7b8dc5d7678dd4404265f63cce7ddd9ad3b0c791ed0a858776818270111993f9e9d58232ebcf5d6e7613a6b6c800d945af60c6866a5a677fb7ff - languageName: node - linkType: hard - -"oxc-transform@npm:^0.87.0": - version: 0.87.0 - resolution: "oxc-transform@npm:0.87.0" - dependencies: - "@oxc-transform/binding-android-arm64": "npm:0.87.0" - "@oxc-transform/binding-darwin-arm64": "npm:0.87.0" - "@oxc-transform/binding-darwin-x64": "npm:0.87.0" - "@oxc-transform/binding-freebsd-x64": "npm:0.87.0" - "@oxc-transform/binding-linux-arm-gnueabihf": "npm:0.87.0" - "@oxc-transform/binding-linux-arm-musleabihf": "npm:0.87.0" - "@oxc-transform/binding-linux-arm64-gnu": "npm:0.87.0" - "@oxc-transform/binding-linux-arm64-musl": "npm:0.87.0" - "@oxc-transform/binding-linux-riscv64-gnu": "npm:0.87.0" - "@oxc-transform/binding-linux-s390x-gnu": "npm:0.87.0" - "@oxc-transform/binding-linux-x64-gnu": "npm:0.87.0" - "@oxc-transform/binding-linux-x64-musl": "npm:0.87.0" - "@oxc-transform/binding-wasm32-wasi": "npm:0.87.0" - "@oxc-transform/binding-win32-arm64-msvc": "npm:0.87.0" - "@oxc-transform/binding-win32-x64-msvc": "npm:0.87.0" + checksum: 10c0/4f2c738c98652f682febd4dee6fc3707dff004b72f13374a9d25f69dee9fa405085ceef2f7353bb42cc1b648b5d63311dc5c797da6ed086c453f82327bb376f7 + languageName: node + linkType: hard + +"oxc-transform@npm:^0.96.0": + version: 0.96.0 + resolution: "oxc-transform@npm:0.96.0" + dependencies: + "@oxc-transform/binding-android-arm64": "npm:0.96.0" + "@oxc-transform/binding-darwin-arm64": "npm:0.96.0" + "@oxc-transform/binding-darwin-x64": "npm:0.96.0" + "@oxc-transform/binding-freebsd-x64": "npm:0.96.0" + "@oxc-transform/binding-linux-arm-gnueabihf": "npm:0.96.0" + "@oxc-transform/binding-linux-arm-musleabihf": "npm:0.96.0" + "@oxc-transform/binding-linux-arm64-gnu": "npm:0.96.0" + "@oxc-transform/binding-linux-arm64-musl": "npm:0.96.0" + "@oxc-transform/binding-linux-riscv64-gnu": "npm:0.96.0" + "@oxc-transform/binding-linux-s390x-gnu": "npm:0.96.0" + "@oxc-transform/binding-linux-x64-gnu": "npm:0.96.0" + "@oxc-transform/binding-linux-x64-musl": "npm:0.96.0" + "@oxc-transform/binding-wasm32-wasi": "npm:0.96.0" + "@oxc-transform/binding-win32-arm64-msvc": "npm:0.96.0" + "@oxc-transform/binding-win32-x64-msvc": "npm:0.96.0" dependenciesMeta: "@oxc-transform/binding-android-arm64": optional: true @@ -10694,19 +12373,7 @@ __metadata: optional: true "@oxc-transform/binding-win32-x64-msvc": optional: true - checksum: 10c0/b9fc5d9995393c4fe405db835604783232de608f579e86db8c9be48bbe15b2f558760969a858950aeb62723ad177a3b201d2d039994855d0b500e22ffd6993ae - languageName: node - linkType: hard - -"oxc-walker@npm:^0.4.0": - version: 0.4.0 - resolution: "oxc-walker@npm:0.4.0" - dependencies: - estree-walker: "npm:^3.0.3" - magic-regexp: "npm:^0.10.0" - peerDependencies: - oxc-parser: ">=0.72.0" - checksum: 10c0/b10071429307a82e3c9a319ffe5f9821a60c806e340edf51eced7466053031b03c10a51fafb3d2201b1fc5723139366608f9a71c9b58282ca823dc7b0c6c8059 + checksum: 10c0/3b070e02d490597143f98e04b9d8be6b7fde8d18bf9b16e5af2b10e88f0bd2736beb1fc6818b648366c570c92c37b54f84331275c565a6b8249f7523451d58da languageName: node linkType: hard @@ -10868,13 +12535,6 @@ __metadata: languageName: node linkType: hard -"path-exists@npm:^5.0.0": - version: 5.0.0 - resolution: "path-exists@npm:5.0.0" - checksum: 10c0/b170f3060b31604cde93eefdb7392b89d832dfbc1bed717c9718cbe0f230c1669b7e75f87e19901da2250b84d092989a0f9e44d2ef41deb09aa3ad28e691a40a - languageName: node - linkType: hard - "path-key@npm:^3.1.0": version: 3.1.1 resolution: "path-key@npm:3.1.1" @@ -10913,7 +12573,7 @@ __metadata: languageName: node linkType: hard -"pathe@npm:^1.0.0, pathe@npm:^1.1.1, pathe@npm:^1.1.2": +"pathe@npm:^1.1.1, pathe@npm:^1.1.2": version: 1.1.2 resolution: "pathe@npm:1.1.2" checksum: 10c0/64ee0a4e587fb0f208d9777a6c56e4f9050039268faaaaecd50e959ef01bf847b7872785c36483fa5cdcdbdfdb31fef2ff222684d4fc21c330ab60395c681897 @@ -11011,18 +12671,18 @@ __metadata: languageName: node linkType: hard -"pinia@npm:3.0.3": - version: 3.0.3 - resolution: "pinia@npm:3.0.3" +"pinia@npm:3.0.4": + version: 3.0.4 + resolution: "pinia@npm:3.0.4" dependencies: - "@vue/devtools-api": "npm:^7.7.2" + "@vue/devtools-api": "npm:^7.7.7" peerDependencies: - typescript: ">=4.4.4" - vue: ^2.7.0 || ^3.5.11 + typescript: ">=4.5.0" + vue: ^3.5.11 peerDependenciesMeta: typescript: optional: true - checksum: 10c0/486f464ce402e33c502caf564bebcd00c364e7d57432373f64201da0cdbbc96a6357fecb5ee09882228d3c17d400af1f0c9550ddf2d19f2ef2931860a427cc23 + checksum: 10c0/4123efbf28c6d11f3dd052dbcf08da7ef85747fd3499807b9ca1d32dcda7807b2e68f9a7e31a06148b05093a9ebcd0c3b26e6a31b62775c6421429bd03265949 languageName: node linkType: hard @@ -11105,29 +12765,29 @@ __metadata: languageName: node linkType: hard -"postcss-colormin@npm:^7.0.4": - version: 7.0.4 - resolution: "postcss-colormin@npm:7.0.4" +"postcss-colormin@npm:^7.0.5": + version: 7.0.5 + resolution: "postcss-colormin@npm:7.0.5" dependencies: - browserslist: "npm:^4.25.1" + browserslist: "npm:^4.27.0" caniuse-api: "npm:^3.0.0" colord: "npm:^2.9.3" postcss-value-parser: "npm:^4.2.0" peerDependencies: postcss: ^8.4.32 - checksum: 10c0/5f91709acc8dfd6ae5ea31435c01ca1e61bc40730ce68c4ff2312649d95c48c26e3a86dde06280e3b16abaaf4bb86b7f55677ac845e9725c785f6611566e2cba + checksum: 10c0/ccd470f416fcbd6db34226eda38df40a4a48578f5b17e5a8d638e01577ba74cda7a511d07ca070c6e15225688f33e1cf2d83c4459492e16e8a23da9c16b077b5 languageName: node linkType: hard -"postcss-convert-values@npm:^7.0.7": - version: 7.0.7 - resolution: "postcss-convert-values@npm:7.0.7" +"postcss-convert-values@npm:^7.0.8": + version: 7.0.8 + resolution: "postcss-convert-values@npm:7.0.8" dependencies: - browserslist: "npm:^4.25.1" + browserslist: "npm:^4.27.0" postcss-value-parser: "npm:^4.2.0" peerDependencies: postcss: ^8.4.32 - checksum: 10c0/b50c3d6bdda07597514a09c7d320c721244026ac78d86a27bc40e2153753cf28caeae007ec5dee219ac008ed127e2c62cfe1c01fa4ab77003b3fabdbd1074808 + checksum: 10c0/d99316bc868292120b1a2abab0bea2c037bef84a7c67c5a4fbd757918e7c84d83803bbfc72bc25f331f088cb21989066879bc640fe434585e249ffbf3a77a07d languageName: node linkType: hard @@ -11146,14 +12806,14 @@ __metadata: languageName: node linkType: hard -"postcss-discard-comments@npm:^7.0.4": - version: 7.0.4 - resolution: "postcss-discard-comments@npm:7.0.4" +"postcss-discard-comments@npm:^7.0.5": + version: 7.0.5 + resolution: "postcss-discard-comments@npm:7.0.5" dependencies: postcss-selector-parser: "npm:^7.1.0" peerDependencies: postcss: ^8.4.32 - checksum: 10c0/30081465fec33baa8507782d25cd96559cb3487c023d331a517cf94027d065c26227962a40b1806885400d76d3d27d27f9e7b14807866c7d9bb63c3030b5312a + checksum: 10c0/6a0cf4b08878cbb9a63a3ce158c251cc710379257a2e4ab810c3c4eb26349c257f28a1a46d7da02b977701efa48a225968ded9e1f24ac1cec0dc976986a9d2fd languageName: node linkType: hard @@ -11196,17 +12856,17 @@ __metadata: languageName: node linkType: hard -"postcss-merge-rules@npm:^7.0.6": - version: 7.0.6 - resolution: "postcss-merge-rules@npm:7.0.6" +"postcss-merge-rules@npm:^7.0.7": + version: 7.0.7 + resolution: "postcss-merge-rules@npm:7.0.7" dependencies: - browserslist: "npm:^4.25.1" + browserslist: "npm:^4.27.0" caniuse-api: "npm:^3.0.0" cssnano-utils: "npm:^5.0.1" postcss-selector-parser: "npm:^7.1.0" peerDependencies: postcss: ^8.4.32 - checksum: 10c0/1708d2e862825f79077aff1f7d82ff815c015929f0fb5bb3fb58dbc83f9bc79ef9aa40ef585afbe2dcb2563ea3516f21332be926e746189649459eb9399cc95e + checksum: 10c0/14ffcb250857ba2ae6ebf31be4cc31b23108541d4595b7f46e4f93fa27280182912be43c4c3df03df8a4514e1bca1b77da2ae148fdd63439c2100a1110af3bb0 languageName: node linkType: hard @@ -11234,16 +12894,16 @@ __metadata: languageName: node linkType: hard -"postcss-minify-params@npm:^7.0.4": - version: 7.0.4 - resolution: "postcss-minify-params@npm:7.0.4" +"postcss-minify-params@npm:^7.0.5": + version: 7.0.5 + resolution: "postcss-minify-params@npm:7.0.5" dependencies: - browserslist: "npm:^4.25.1" + browserslist: "npm:^4.27.0" cssnano-utils: "npm:^5.0.1" postcss-value-parser: "npm:^4.2.0" peerDependencies: postcss: ^8.4.32 - checksum: 10c0/412faa91082d4ef3c1540982fc0b69a0aefebfcc4d1b3763613167e0560e0a142cea80092c0b636cafd08c7d348359b04dd00398b2b307383c505e62dffdb3ad + checksum: 10c0/13e9b052e452c903e8f5c9c60ad8155b7cfc011722f093cc76e976b10cd0f334342e15add23afdf696f166b79510baf4c0a5fad7c7a5e0520907fd23a5f54c51 languageName: node linkType: hard @@ -11323,15 +12983,15 @@ __metadata: languageName: node linkType: hard -"postcss-normalize-unicode@npm:^7.0.4": - version: 7.0.4 - resolution: "postcss-normalize-unicode@npm:7.0.4" +"postcss-normalize-unicode@npm:^7.0.5": + version: 7.0.5 + resolution: "postcss-normalize-unicode@npm:7.0.5" dependencies: - browserslist: "npm:^4.25.1" + browserslist: "npm:^4.27.0" postcss-value-parser: "npm:^4.2.0" peerDependencies: postcss: ^8.4.32 - checksum: 10c0/20efa7e55e94d8f3068ca11c4e24d9023a07dd99c7795a1d4ec755d6004cd3f8452e7c541ed41274ee81d6e37516132b2430ebfa695340c5fe93beac39a6ddb5 + checksum: 10c0/533b487e4c3c9419fd1ebe8aedad428502265733c025e0ab38d144e0757909033349b4f26f2eba800d27d5a765aa443e82ed0224da15d4cc37761c9cd506b102 languageName: node linkType: hard @@ -11369,15 +13029,15 @@ __metadata: languageName: node linkType: hard -"postcss-reduce-initial@npm:^7.0.4": - version: 7.0.4 - resolution: "postcss-reduce-initial@npm:7.0.4" +"postcss-reduce-initial@npm:^7.0.5": + version: 7.0.5 + resolution: "postcss-reduce-initial@npm:7.0.5" dependencies: - browserslist: "npm:^4.25.1" + browserslist: "npm:^4.27.0" caniuse-api: "npm:^3.0.0" peerDependencies: postcss: ^8.4.32 - checksum: 10c0/2763fc58094bf0aca050c8adca62fdc69093777e0af858fc0d95515ce25bc883470c7d27b67886a1aeecadd289a6a87c35da9afd5529bfc22995bf5a13cabcb9 + checksum: 10c0/e12117c82033df1f061e052e865c3c4d506d8941117318c397a9009ee3ae7d64d6fb71316e746fe092790cab0f74ddc0bf1152c53547c9705d9afaf6f731bbed languageName: node linkType: hard @@ -11392,16 +13052,6 @@ __metadata: languageName: node linkType: hard -"postcss-selector-parser@npm:^6.0.15": - version: 6.1.2 - resolution: "postcss-selector-parser@npm:6.1.2" - dependencies: - cssesc: "npm:^3.0.0" - util-deprecate: "npm:^1.0.2" - checksum: 10c0/523196a6bd8cf660bdf537ad95abd79e546d54180f9afb165a4ab3e651ac705d0f8b8ce6b3164fb9e3279ce482c5f751a69eb2d3a1e8eb0fd5e82294fb3ef13e - languageName: node - linkType: hard - "postcss-selector-parser@npm:^7.0.0, postcss-selector-parser@npm:^7.1.0": version: 7.1.0 resolution: "postcss-selector-parser@npm:7.1.0" @@ -11558,7 +13208,7 @@ __metadata: languageName: node linkType: hard -"pretty-bytes@npm:^7.0.1": +"pretty-bytes@npm:^7.0.1, pretty-bytes@npm:^7.1.0": version: 7.1.0 resolution: "pretty-bytes@npm:7.1.0" checksum: 10c0/9458f17007bbf8b61d11ef82091bfe7f87bb2f3fd7e6aa917744eb6dc709346995f698ecbf6597ac353b0d44bb7982054f7a2325f3260c9909d09aaafbdab5ca @@ -12073,14 +13723,14 @@ __metadata: languageName: node linkType: hard -"regjsparser@npm:^0.12.0": - version: 0.12.0 - resolution: "regjsparser@npm:0.12.0" +"regjsparser@npm:^0.13.0": + version: 0.13.0 + resolution: "regjsparser@npm:0.13.0" dependencies: - jsesc: "npm:~3.0.2" + jsesc: "npm:~3.1.0" bin: regjsparser: bin/parser - checksum: 10c0/99d3e4e10c8c7732eb7aa843b8da2fd8b647fe144d3711b480e4647dc3bff4b1e96691ccf17f3ace24aa866a50b064236177cb25e6e4fbbb18285d99edaed83b + checksum: 10c0/4702f85cda09f67747c1b2fb673a0f0e5d1ba39d55f177632265a0be471ba59e3f320623f411649141f752b126b8126eac3ff4c62d317921e430b0472bfc6071 languageName: node linkType: hard @@ -12105,6 +13755,13 @@ __metadata: languageName: node linkType: hard +"reserved-identifiers@npm:^1.0.0": + version: 1.2.0 + resolution: "reserved-identifiers@npm:1.2.0" + checksum: 10c0/b82651b12e6c608e80463c3753d275bc20fd89294d0415f04e670aeec3611ae3582ddc19e8fedd497e7d0bcbfaddab6a12823ec86e855b1e6a245e0a734eb43d + languageName: node + linkType: hard + "resolve-from@npm:^4.0.0": version: 4.0.0 resolution: "resolve-from@npm:4.0.0" @@ -12214,6 +13871,28 @@ __metadata: languageName: node linkType: hard +"rollup-plugin-visualizer@npm:^6.0.5": + version: 6.0.5 + resolution: "rollup-plugin-visualizer@npm:6.0.5" + dependencies: + open: "npm:^8.0.0" + picomatch: "npm:^4.0.2" + source-map: "npm:^0.7.4" + yargs: "npm:^17.5.1" + peerDependencies: + rolldown: 1.x || ^1.0.0-beta + rollup: 2.x || 3.x || 4.x + peerDependenciesMeta: + rolldown: + optional: true + rollup: + optional: true + bin: + rollup-plugin-visualizer: dist/bin/cli.js + checksum: 10c0/3824626e97d5033fbb3aa1bbe93c8c17a8569bc47e33c941bde6b90404f2cae70b26fec1b623bd393c3e076338014196c91726ed2c96218edc67e1f21676f7ef + languageName: node + linkType: hard + "rollup@npm:4.52.3, rollup@npm:^4.43.0, rollup@npm:^4.50.1": version: 4.52.3 resolution: "rollup@npm:4.52.3" @@ -12291,7 +13970,88 @@ __metadata: optional: true bin: rollup: dist/bin/rollup - checksum: 10c0/5a7a3a2e8c7558df5652ecc126e0d9133df4d58c5a001777377202b52517fa48b43be5e21a2cbab6d85975b765991af72666b5132813da6e86ea47ae963b4e71 + checksum: 10c0/5a7a3a2e8c7558df5652ecc126e0d9133df4d58c5a001777377202b52517fa48b43be5e21a2cbab6d85975b765991af72666b5132813da6e86ea47ae963b4e71 + languageName: node + linkType: hard + +"rollup@npm:^4.52.5": + version: 4.53.3 + resolution: "rollup@npm:4.53.3" + dependencies: + "@rollup/rollup-android-arm-eabi": "npm:4.53.3" + "@rollup/rollup-android-arm64": "npm:4.53.3" + "@rollup/rollup-darwin-arm64": "npm:4.53.3" + "@rollup/rollup-darwin-x64": "npm:4.53.3" + "@rollup/rollup-freebsd-arm64": "npm:4.53.3" + "@rollup/rollup-freebsd-x64": "npm:4.53.3" + "@rollup/rollup-linux-arm-gnueabihf": "npm:4.53.3" + "@rollup/rollup-linux-arm-musleabihf": "npm:4.53.3" + "@rollup/rollup-linux-arm64-gnu": "npm:4.53.3" + "@rollup/rollup-linux-arm64-musl": "npm:4.53.3" + "@rollup/rollup-linux-loong64-gnu": "npm:4.53.3" + "@rollup/rollup-linux-ppc64-gnu": "npm:4.53.3" + "@rollup/rollup-linux-riscv64-gnu": "npm:4.53.3" + "@rollup/rollup-linux-riscv64-musl": "npm:4.53.3" + "@rollup/rollup-linux-s390x-gnu": "npm:4.53.3" + "@rollup/rollup-linux-x64-gnu": "npm:4.53.3" + "@rollup/rollup-linux-x64-musl": "npm:4.53.3" + "@rollup/rollup-openharmony-arm64": "npm:4.53.3" + "@rollup/rollup-win32-arm64-msvc": "npm:4.53.3" + "@rollup/rollup-win32-ia32-msvc": "npm:4.53.3" + "@rollup/rollup-win32-x64-gnu": "npm:4.53.3" + "@rollup/rollup-win32-x64-msvc": "npm:4.53.3" + "@types/estree": "npm:1.0.8" + fsevents: "npm:~2.3.2" + dependenciesMeta: + "@rollup/rollup-android-arm-eabi": + optional: true + "@rollup/rollup-android-arm64": + optional: true + "@rollup/rollup-darwin-arm64": + optional: true + "@rollup/rollup-darwin-x64": + optional: true + "@rollup/rollup-freebsd-arm64": + optional: true + "@rollup/rollup-freebsd-x64": + optional: true + "@rollup/rollup-linux-arm-gnueabihf": + optional: true + "@rollup/rollup-linux-arm-musleabihf": + optional: true + "@rollup/rollup-linux-arm64-gnu": + optional: true + "@rollup/rollup-linux-arm64-musl": + optional: true + "@rollup/rollup-linux-loong64-gnu": + optional: true + "@rollup/rollup-linux-ppc64-gnu": + optional: true + "@rollup/rollup-linux-riscv64-gnu": + optional: true + "@rollup/rollup-linux-riscv64-musl": + optional: true + "@rollup/rollup-linux-s390x-gnu": + optional: true + "@rollup/rollup-linux-x64-gnu": + optional: true + "@rollup/rollup-linux-x64-musl": + optional: true + "@rollup/rollup-openharmony-arm64": + optional: true + "@rollup/rollup-win32-arm64-msvc": + optional: true + "@rollup/rollup-win32-ia32-msvc": + optional: true + "@rollup/rollup-win32-x64-gnu": + optional: true + "@rollup/rollup-win32-x64-msvc": + optional: true + fsevents: + optional: true + bin: + rollup: dist/bin/rollup + checksum: 10c0/a21305aac72013083bd0dec92162b0f7f24cacf57c876ca601ec76e892895952c9ea592c1c07f23b8c125f7979c2b17f7fb565e386d03ee4c1f0952ac4ab0d75 languageName: node linkType: hard @@ -12309,7 +14069,7 @@ __metadata: languageName: node linkType: hard -"run-parallel@npm:^1.1.9, run-parallel@npm:^1.2.0": +"run-parallel@npm:^1.1.9": version: 1.2.0 resolution: "run-parallel@npm:1.2.0" dependencies: @@ -12359,7 +14119,7 @@ __metadata: languageName: node linkType: hard -"safer-buffer@npm:>= 2.1.2 < 3.0.0": +"safer-buffer@npm:>= 2.1.2 < 3, safer-buffer@npm:>= 2.1.2 < 3.0.0": version: 2.1.2 resolution: "safer-buffer@npm:2.1.2" checksum: 10c0/7e3c8b2e88a1841c9671094bbaeebd94448111dd90a81a1f606f3f67708a6ec57763b3b47f06da09fc6054193e0e6709e77325415dc8422b04497a8070fa02d4 @@ -12409,6 +14169,15 @@ __metadata: languageName: node linkType: hard +"semver@npm:^7.7.3": + version: 7.7.3 + resolution: "semver@npm:7.7.3" + bin: + semver: bin/semver.js + checksum: 10c0/4afe5c986567db82f44c8c6faef8fe9df2a9b1d98098fc1721f57c696c4c21cebd572f297fc21002f81889492345b8470473bc6f4aff5fb032a6ea59ea2bc45e + languageName: node + linkType: hard + "send@npm:^1.2.0": version: 1.2.0 resolution: "send@npm:1.2.0" @@ -12437,6 +14206,13 @@ __metadata: languageName: node linkType: hard +"seroval@npm:^1.3.2": + version: 1.4.0 + resolution: "seroval@npm:1.4.0" + checksum: 10c0/020262db5572c16ae5d22ecefa089112a0b1b9a9c78229dbc9c6059c172ed7f0b5005c7990b80714ff8638ac86274195c2084537e0c2a9178690acacff4b705f + languageName: node + linkType: hard + "serve-placeholder@npm:^2.0.2": version: 2.0.2 resolution: "serve-placeholder@npm:2.0.2" @@ -12601,6 +14377,17 @@ __metadata: languageName: node linkType: hard +"simple-git@npm:^3.30.0": + version: 3.30.0 + resolution: "simple-git@npm:3.30.0" + dependencies: + "@kwsites/file-exists": "npm:^1.1.1" + "@kwsites/promise-deferred": "npm:^1.1.1" + debug: "npm:^4.4.0" + checksum: 10c0/ca123580944d55c7f93d17f7a89b39cd43245916b35ed3a8b1269d1f2ae9200e17f098e42af4a2572313726f1273641cbe0748a1ea37d8c803c181fb69068b1d + languageName: node + linkType: hard + "sirv@npm:^3.0.1, sirv@npm:^3.0.2": version: 3.0.2 resolution: "sirv@npm:3.0.2" @@ -12626,16 +14413,6 @@ __metadata: languageName: node linkType: hard -"slice-ansi@npm:^5.0.0": - version: 5.0.0 - resolution: "slice-ansi@npm:5.0.0" - dependencies: - ansi-styles: "npm:^6.0.0" - is-fullwidth-code-point: "npm:^4.0.0" - checksum: 10c0/2d4d40b2a9d5cf4e8caae3f698fe24ae31a4d778701724f578e984dcb485ec8c49f0c04dab59c401821e80fcdfe89cace9c66693b0244e40ec485d72e543914f - languageName: node - linkType: hard - "slice-ansi@npm:^7.1.0": version: 7.1.2 resolution: "slice-ansi@npm:7.1.2" @@ -12750,6 +14527,15 @@ __metadata: languageName: node linkType: hard +"srvx@npm:^0.9.4": + version: 0.9.6 + resolution: "srvx@npm:0.9.6" + bin: + srvx: bin/srvx.mjs + checksum: 10c0/4d09a5b4c6bf4b9338b6899438d5e0b50225eff09ca8a413cd3d941d8ea2886f98ca54f14f3e29984d35fbed015f5a67db96297849b928b626014671cca686eb + languageName: node + linkType: hard + "ssri@npm:^12.0.0": version: 12.0.0 resolution: "ssri@npm:12.0.0" @@ -12794,6 +14580,13 @@ __metadata: languageName: node linkType: hard +"std-env@npm:^3.10.0": + version: 3.10.0 + resolution: "std-env@npm:3.10.0" + checksum: 10c0/1814927a45004d36dde6707eaf17552a546769bc79a6421be2c16ce77d238158dfe5de30910b78ec30d95135cc1c59ea73ee22d2ca170f8b9753f84da34c427f + languageName: node + linkType: hard + "std-env@npm:^3.7.0, std-env@npm:^3.8.1, std-env@npm:^3.9.0": version: 3.9.0 resolution: "std-env@npm:3.9.0" @@ -12862,6 +14655,16 @@ __metadata: languageName: node linkType: hard +"string-width@npm:^8.0.0": + version: 8.1.0 + resolution: "string-width@npm:8.1.0" + dependencies: + get-east-asian-width: "npm:^1.3.0" + strip-ansi: "npm:^7.1.0" + checksum: 10c0/749b5d0dab2532b4b6b801064230f4da850f57b3891287023117ab63a464ad79dd208f42f793458f48f3ad121fe2e1f01dd525ff27ead957ed9f205e27406593 + languageName: node + linkType: hard + "string_decoder@npm:^1.3.0": version: 1.3.0 resolution: "string_decoder@npm:1.3.0" @@ -12905,10 +14708,10 @@ __metadata: languageName: node linkType: hard -"strip-indent@npm:^4.0.0": - version: 4.1.0 - resolution: "strip-indent@npm:4.1.0" - checksum: 10c0/ea8193b60a85769ca42d3589c865d4bc743017c1e6ce846332f0f49f103d127dfc25af81849bd00aa98420474fa171ecc2dbe8c1ccd7b9260c43477a5e79431a +"strip-indent@npm:^4.1.1": + version: 4.1.1 + resolution: "strip-indent@npm:4.1.1" + checksum: 10c0/5b23dd5934be0ef6b6fe1b802887f83e56ad9dcd9f6c3896a637da2c6c3a6da3fdf3e51354a98e6cccb6f1c41863e7b9b9deaa348639dfd35f71f3549edb4dff languageName: node linkType: hard @@ -13005,10 +14808,10 @@ __metadata: languageName: node linkType: hard -"swiper@npm:12.0.2": - version: 12.0.2 - resolution: "swiper@npm:12.0.2" - checksum: 10c0/e7826eb5973828c622a30f321dc7740300a32666d26810b381e2498ad60f2f84e070058a1aa0ee32acce8a6d31f2d2919c388b2e5921db45690c59af41b1c7b8 +"swiper@npm:12.0.3": + version: 12.0.3 + resolution: "swiper@npm:12.0.3" + checksum: 10c0/a854531791ba45cb0f6086904a99878fd207b42024820df68fbf64e01c20d542438c023ec4765b223f4dbffabf6858d311ec749bdca4ea4b4a7b74150a010162 languageName: node linkType: hard @@ -13019,6 +14822,13 @@ __metadata: languageName: node linkType: hard +"tagged-tag@npm:^1.0.0": + version: 1.0.0 + resolution: "tagged-tag@npm:1.0.0" + checksum: 10c0/91d25c9ffb86a91f20522cefb2cbec9b64caa1febe27ad0df52f08993ff60888022d771e868e6416cf2e72dab68449d2139e8709ba009b74c6c7ecd4000048d1 + languageName: node + linkType: hard + "tailwind-scrollbar@npm:4.0.2": version: 4.0.2 resolution: "tailwind-scrollbar@npm:4.0.2" @@ -13030,10 +14840,10 @@ __metadata: languageName: node linkType: hard -"tailwindcss@npm:4.1.14": - version: 4.1.14 - resolution: "tailwindcss@npm:4.1.14" - checksum: 10c0/c7e9ebfb241707b2a3eb7d465fd326cc8fcfa22e7215e01f67cccec32db8a49a19e17d1f694fc5d0435d55350ea3f863521c52c9bbe6bd790c2009dc8ff516a1 +"tailwindcss@npm:4.1.17": + version: 4.1.17 + resolution: "tailwindcss@npm:4.1.17" + checksum: 10c0/1fecf618ba9895e068e5a6d842b978f56a815bc849a28338cebbcb07b13df763715c2f8848def938403c73d59f08ffff33a4b83a977a9e38fa56adc60d1d56c8 languageName: node linkType: hard @@ -13055,7 +14865,7 @@ __metadata: languageName: node linkType: hard -"tar@npm:^7.4.0, tar@npm:^7.4.3, tar@npm:^7.5.1": +"tar@npm:^7.4.0, tar@npm:^7.4.3": version: 7.5.2 resolution: "tar@npm:7.5.2" dependencies: @@ -13200,6 +15010,16 @@ __metadata: languageName: node linkType: hard +"to-valid-identifier@npm:^1.0.0": + version: 1.0.0 + resolution: "to-valid-identifier@npm:1.0.0" + dependencies: + "@sindresorhus/base62": "npm:^1.0.0" + reserved-identifiers: "npm:^1.0.0" + checksum: 10c0/569b49f43b5aaaa20677e67f0f1cdcff344855149934cfb80c793c7ac7c30e191b224bc81cab40fb57641af9ca73795c78053c164a2addc617671e2d22c13a4a + languageName: node + linkType: hard + "toidentifier@npm:1.0.1": version: 1.0.1 resolution: "toidentifier@npm:1.0.1" @@ -13260,6 +15080,15 @@ __metadata: languageName: node linkType: hard +"type-fest@npm:^5.0.0": + version: 5.2.0 + resolution: "type-fest@npm:5.2.0" + dependencies: + tagged-tag: "npm:^1.0.0" + checksum: 10c0/5fd6c651c08d735213257c1b9498dc4a5b78ce94748901da5945a8e0cde5152dfba59c4bd749845072278ebf92be4351369ed5c79cc695402d3aa4fe1d3f9aa5 + languageName: node + linkType: hard + "type-level-regexp@npm:~0.1.17": version: 0.1.17 resolution: "type-level-regexp@npm:0.1.17" @@ -13294,7 +15123,7 @@ __metadata: languageName: node linkType: hard -"ufo@npm:1.6.1, ufo@npm:^1.5.4, ufo@npm:^1.6.1": +"ufo@npm:^1.5.4, ufo@npm:^1.6.1": version: 1.6.1 resolution: "ufo@npm:1.6.1" checksum: 10c0/5a9f041e5945fba7c189d5410508cbcbefef80b253ed29aa2e1f8a2b86f4bd51af44ee18d4485e6d3468c92be9bf4a42e3a2b72dcaf27ce39ce947ec994f1e6b @@ -13308,15 +15137,26 @@ __metadata: languageName: node linkType: hard -"unconfig@npm:^7.3.3": - version: 7.3.3 - resolution: "unconfig@npm:7.3.3" +"unconfig-core@npm:7.4.1": + version: 7.4.1 + resolution: "unconfig-core@npm:7.4.1" + dependencies: + "@quansync/fs": "npm:^0.1.5" + quansync: "npm:^0.2.11" + checksum: 10c0/075b51349836234cf4b1e571d4e78a59f1de27db99bda9952dbe691f1c8aaae68b2ec22243584921403b30406b9bfed1bdd9ca8fcd194bdcefdb19e87e980b38 + languageName: node + linkType: hard + +"unconfig@npm:^7.4.1": + version: 7.4.1 + resolution: "unconfig@npm:7.4.1" dependencies: "@quansync/fs": "npm:^0.1.5" defu: "npm:^6.1.4" - jiti: "npm:^2.5.1" + jiti: "npm:^2.6.1" quansync: "npm:^0.2.11" - checksum: 10c0/7c1b0688ce7ba36a92cfeb36f248a61b86e27807b25a4504acc3e0fbf19a217fc74ba80fe45e3205def7648666de51d2b28551e61c86d1c54dcb8e129a011e58 + unconfig-core: "npm:7.4.1" + checksum: 10c0/8bed07b6d848169eabd58ffff3d6cfc0419291632f2796a18389a749ad9857b66d171d4f9e698d7abfd52bcef8e3d0229f11b2cb38b079a971069423463c7154 languageName: node linkType: hard @@ -13346,10 +15186,10 @@ __metadata: languageName: node linkType: hard -"undici-types@npm:~7.13.0": - version: 7.13.0 - resolution: "undici-types@npm:7.13.0" - checksum: 10c0/44bbb0935425291351bfd8039571f017295b5d6dc5727045d0a4fea8c6ffe73a6703b48ce010f9cb539b9041a75b463f8cfe1e7309cab7486452505fb0d66151 +"undici-types@npm:~7.16.0": + version: 7.16.0 + resolution: "undici-types@npm:7.16.0" + checksum: 10c0/3033e2f2b5c9f1504bdc5934646cb54e37ecaca0f9249c983f7b1fc2e87c6d18399ebb05dc7fd5419e02b2e915f734d872a65da2e3eeed1813951c427d33cc9a languageName: node linkType: hard @@ -13366,19 +15206,21 @@ __metadata: languageName: node linkType: hard -"unhead@npm:2.0.17": - version: 2.0.17 - resolution: "unhead@npm:2.0.17" +"unenv@npm:^2.0.0-rc.23, unenv@npm:^2.0.0-rc.24": + version: 2.0.0-rc.24 + resolution: "unenv@npm:2.0.0-rc.24" dependencies: - hookable: "npm:^5.5.3" - checksum: 10c0/c5e28c56f29e8b88fe36ea7f411f278f5504c10ee9f882c7b27557f1a58932efa14c3232914a4221eaf36f579ea4e0d0eb43324d9e37de5086673c4df6c15226 + pathe: "npm:^2.0.3" + checksum: 10c0/e8556b4287fcf647f23db790eea2782cc79f182370718680e3aba4753d5fb7177abf5d6df489c8f74f7e3ad6cd554b8623cc01caf3e6f2d5548e69178adb1691 languageName: node linkType: hard -"unicorn-magic@npm:^0.1.0": - version: 0.1.0 - resolution: "unicorn-magic@npm:0.1.0" - checksum: 10c0/e4ed0de05b0a05e735c7d8a2930881e5efcfc3ec897204d5d33e7e6247f4c31eac92e383a15d9a6bccb7319b4271ee4bea946e211bf14951fec6ff2cbbb66a92 +"unhead@npm:2.0.19": + version: 2.0.19 + resolution: "unhead@npm:2.0.19" + dependencies: + hookable: "npm:^5.5.3" + checksum: 10c0/a7904c1e9041cc1b15fafbbeb2526f180a9bd05751eb1bd2b1f3f9f5f3caada6372cbbf72e0879df6be78773ed1dbc6a724aaee35031e3b664cf6f6fb5f9e27a languageName: node linkType: hard @@ -13411,6 +15253,28 @@ __metadata: languageName: node linkType: hard +"unimport@npm:^5.5.0": + version: 5.5.0 + resolution: "unimport@npm:5.5.0" + dependencies: + acorn: "npm:^8.15.0" + escape-string-regexp: "npm:^5.0.0" + estree-walker: "npm:^3.0.3" + local-pkg: "npm:^1.1.2" + magic-string: "npm:^0.30.19" + mlly: "npm:^1.8.0" + pathe: "npm:^2.0.3" + picomatch: "npm:^4.0.3" + pkg-types: "npm:^2.3.0" + scule: "npm:^1.3.0" + strip-literal: "npm:^3.1.0" + tinyglobby: "npm:^0.2.15" + unplugin: "npm:^2.3.10" + unplugin-utils: "npm:^0.3.0" + checksum: 10c0/c38953c04ade372e80c41a465d836187294a0a1081c6cf53c09100a6a75718f65cac9a884feef957c350cd6e72d3998f4ff5e4ad3909f54b2a989b979103c093 + languageName: node + linkType: hard + "unique-filename@npm:^4.0.0": version: 4.0.0 resolution: "unique-filename@npm:4.0.0" @@ -13429,38 +15293,38 @@ __metadata: languageName: node linkType: hard -"unocss@npm:66.5.2": - version: 66.5.2 - resolution: "unocss@npm:66.5.2" - dependencies: - "@unocss/astro": "npm:66.5.2" - "@unocss/cli": "npm:66.5.2" - "@unocss/core": "npm:66.5.2" - "@unocss/postcss": "npm:66.5.2" - "@unocss/preset-attributify": "npm:66.5.2" - "@unocss/preset-icons": "npm:66.5.2" - "@unocss/preset-mini": "npm:66.5.2" - "@unocss/preset-tagify": "npm:66.5.2" - "@unocss/preset-typography": "npm:66.5.2" - "@unocss/preset-uno": "npm:66.5.2" - "@unocss/preset-web-fonts": "npm:66.5.2" - "@unocss/preset-wind": "npm:66.5.2" - "@unocss/preset-wind3": "npm:66.5.2" - "@unocss/preset-wind4": "npm:66.5.2" - "@unocss/transformer-attributify-jsx": "npm:66.5.2" - "@unocss/transformer-compile-class": "npm:66.5.2" - "@unocss/transformer-directives": "npm:66.5.2" - "@unocss/transformer-variant-group": "npm:66.5.2" - "@unocss/vite": "npm:66.5.2" - peerDependencies: - "@unocss/webpack": 66.5.2 +"unocss@npm:66.5.9": + version: 66.5.9 + resolution: "unocss@npm:66.5.9" + dependencies: + "@unocss/astro": "npm:66.5.9" + "@unocss/cli": "npm:66.5.9" + "@unocss/core": "npm:66.5.9" + "@unocss/postcss": "npm:66.5.9" + "@unocss/preset-attributify": "npm:66.5.9" + "@unocss/preset-icons": "npm:66.5.9" + "@unocss/preset-mini": "npm:66.5.9" + "@unocss/preset-tagify": "npm:66.5.9" + "@unocss/preset-typography": "npm:66.5.9" + "@unocss/preset-uno": "npm:66.5.9" + "@unocss/preset-web-fonts": "npm:66.5.9" + "@unocss/preset-wind": "npm:66.5.9" + "@unocss/preset-wind3": "npm:66.5.9" + "@unocss/preset-wind4": "npm:66.5.9" + "@unocss/transformer-attributify-jsx": "npm:66.5.9" + "@unocss/transformer-compile-class": "npm:66.5.9" + "@unocss/transformer-directives": "npm:66.5.9" + "@unocss/transformer-variant-group": "npm:66.5.9" + "@unocss/vite": "npm:66.5.9" + peerDependencies: + "@unocss/webpack": 66.5.9 vite: ^2.9.0 || ^3.0.0-0 || ^4.0.0 || ^5.0.0-0 || ^6.0.0-0 || ^7.0.0-0 peerDependenciesMeta: "@unocss/webpack": optional: true vite: optional: true - checksum: 10c0/a1e4e6d183510b18bba8934d7a03f23de9a88ccc3b1953bb7f91883c549a1c72a8f49a361ac14e80046d68e4604cb688fd5d54451c2251b73ab229af478ebea4 + checksum: 10c0/7db7741c568b426f5dec79d30502a93bc4a0d76a1950b605e81b79e93b075d615ade914ed5ad5caa696a266bb4894c0f9df719f1a071f81d30606f643d91bff5 languageName: node linkType: hard @@ -13499,61 +15363,44 @@ __metadata: languageName: node linkType: hard -"unplugin-vue-router@npm:^0.14.0": - version: 0.14.0 - resolution: "unplugin-vue-router@npm:0.14.0" +"unplugin-utils@npm:^0.3.1": + version: 0.3.1 + resolution: "unplugin-utils@npm:0.3.1" dependencies: - "@vue-macros/common": "npm:3.0.0-beta.15" - ast-walker-scope: "npm:^0.8.1" - chokidar: "npm:^4.0.3" - fast-glob: "npm:^3.3.3" - json5: "npm:^2.2.3" - local-pkg: "npm:^1.1.1" - magic-string: "npm:^0.30.17" - mlly: "npm:^1.7.4" pathe: "npm:^2.0.3" - picomatch: "npm:^4.0.2" - scule: "npm:^1.3.0" - unplugin: "npm:^2.3.5" - unplugin-utils: "npm:^0.2.4" - yaml: "npm:^2.8.0" - peerDependencies: - "@vue/compiler-sfc": ^3.5.17 - vue-router: ^4.5.1 - peerDependenciesMeta: - vue-router: - optional: true - checksum: 10c0/caa8995995f543f288479c8123edb07d876bed6fab2ce51d345e06ccae54997bd759a0c66ff727bbfd574116e76855cde9a5073829becf9aadb87d0a7c26f48e + picomatch: "npm:^4.0.3" + checksum: 10c0/e563b15f2ae604d4f84ac664a7b1738585d2e82a068e59612589e61e555b3d93aa7379a4b6938df3788fe5658cae53d752dd72f6072bd4a642b6e0385c0e4eab languageName: node linkType: hard -"unplugin-vue-router@npm:^0.15.0": - version: 0.15.0 - resolution: "unplugin-vue-router@npm:0.15.0" +"unplugin-vue-router@npm:^0.16.0, unplugin-vue-router@npm:^0.16.1": + version: 0.16.2 + resolution: "unplugin-vue-router@npm:0.16.2" dependencies: - "@vue-macros/common": "npm:3.0.0-beta.16" - "@vue/language-core": "npm:^3.0.1" - ast-walker-scope: "npm:^0.8.1" + "@babel/generator": "npm:^7.28.5" + "@vue-macros/common": "npm:^3.1.1" + "@vue/language-core": "npm:^3.1.3" + ast-walker-scope: "npm:^0.8.3" chokidar: "npm:^4.0.3" json5: "npm:^2.2.3" - local-pkg: "npm:^1.1.1" - magic-string: "npm:^0.30.17" - mlly: "npm:^1.7.4" + local-pkg: "npm:^1.1.2" + magic-string: "npm:^0.30.21" + mlly: "npm:^1.8.0" muggle-string: "npm:^0.4.1" pathe: "npm:^2.0.3" picomatch: "npm:^4.0.3" scule: "npm:^1.3.0" - tinyglobby: "npm:^0.2.14" - unplugin: "npm:^2.3.5" - unplugin-utils: "npm:^0.2.4" - yaml: "npm:^2.8.0" + tinyglobby: "npm:^0.2.15" + unplugin: "npm:^2.3.10" + unplugin-utils: "npm:^0.3.1" + yaml: "npm:^2.8.1" peerDependencies: "@vue/compiler-sfc": ^3.5.17 - vue-router: ^4.5.1 + vue-router: ^4.6.0 peerDependenciesMeta: vue-router: optional: true - checksum: 10c0/9efa96e04189cac5948234cefc6821ac0a31f747d9747fc6fa0356949bc043c8c56decff2ebce54f22ed5683e21f6d7d31d3237114163b04bca65f2b2b45c772 + checksum: 10c0/26d786fbe6f1436f71e542460da1e5128525e35ae5ee6b471a16fb2b625f2ec27f6207d9de5f78d261be0603b24696b6852cfe5a10d8754147f342e9a51e1f69 languageName: node linkType: hard @@ -13721,6 +15568,81 @@ __metadata: languageName: node linkType: hard +"unstorage@npm:^1.17.2": + version: 1.17.3 + resolution: "unstorage@npm:1.17.3" + dependencies: + anymatch: "npm:^3.1.3" + chokidar: "npm:^4.0.3" + destr: "npm:^2.0.5" + h3: "npm:^1.15.4" + lru-cache: "npm:^10.4.3" + node-fetch-native: "npm:^1.6.7" + ofetch: "npm:^1.5.1" + ufo: "npm:^1.6.1" + peerDependencies: + "@azure/app-configuration": ^1.8.0 + "@azure/cosmos": ^4.2.0 + "@azure/data-tables": ^13.3.0 + "@azure/identity": ^4.6.0 + "@azure/keyvault-secrets": ^4.9.0 + "@azure/storage-blob": ^12.26.0 + "@capacitor/preferences": ^6.0.3 || ^7.0.0 + "@deno/kv": ">=0.9.0" + "@netlify/blobs": ^6.5.0 || ^7.0.0 || ^8.1.0 || ^9.0.0 || ^10.0.0 + "@planetscale/database": ^1.19.0 + "@upstash/redis": ^1.34.3 + "@vercel/blob": ">=0.27.1" + "@vercel/functions": ^2.2.12 || ^3.0.0 + "@vercel/kv": ^1.0.1 + aws4fetch: ^1.0.20 + db0: ">=0.2.1" + idb-keyval: ^6.2.1 + ioredis: ^5.4.2 + uploadthing: ^7.4.4 + peerDependenciesMeta: + "@azure/app-configuration": + optional: true + "@azure/cosmos": + optional: true + "@azure/data-tables": + optional: true + "@azure/identity": + optional: true + "@azure/keyvault-secrets": + optional: true + "@azure/storage-blob": + optional: true + "@capacitor/preferences": + optional: true + "@deno/kv": + optional: true + "@netlify/blobs": + optional: true + "@planetscale/database": + optional: true + "@upstash/redis": + optional: true + "@vercel/blob": + optional: true + "@vercel/functions": + optional: true + "@vercel/kv": + optional: true + aws4fetch: + optional: true + db0: + optional: true + idb-keyval: + optional: true + ioredis: + optional: true + uploadthing: + optional: true + checksum: 10c0/46d920a79790a6d22273d5972d220a0b26fce7d8b40b5c563c1f71bec12ae7b0b403b59001773b061fa5a099de3ff5e7fd6b2a65198e89a21a5dbfd9225a217f + languageName: node + linkType: hard + "untun@npm:^0.1.3": version: 0.1.3 resolution: "untun@npm:0.1.3" @@ -13777,6 +15699,20 @@ __metadata: languageName: node linkType: hard +"update-browserslist-db@npm:^1.1.4": + version: 1.1.4 + resolution: "update-browserslist-db@npm:1.1.4" + dependencies: + escalade: "npm:^3.2.0" + picocolors: "npm:^1.1.1" + peerDependencies: + browserslist: ">= 4.21.0" + bin: + update-browserslist-db: cli.js + checksum: 10c0/db0c9aaecf1258a6acda5e937fc27a7996ccca7a7580a1b4aa8bba6a9b0e283e5e65c49ebbd74ec29288ef083f1b88d4da13e3d4d326c1e5fc55bf72d7390702 + languageName: node + linkType: hard + "uqr@npm:^0.1.2": version: 0.1.2 resolution: "uqr@npm:0.1.2" @@ -13875,7 +15811,7 @@ __metadata: languageName: node linkType: hard -"vite-node@npm:3.2.4, vite-node@npm:^3.2.4": +"vite-node@npm:3.2.4": version: 3.2.4 resolution: "vite-node@npm:3.2.4" dependencies: @@ -13890,16 +15826,30 @@ __metadata: languageName: node linkType: hard -"vite-plugin-checker@npm:^0.10.3": - version: 0.10.3 - resolution: "vite-plugin-checker@npm:0.10.3" +"vite-node@npm:^5.0.0": + version: 5.2.0 + resolution: "vite-node@npm:5.2.0" + dependencies: + cac: "npm:^6.7.14" + es-module-lexer: "npm:^1.7.0" + obug: "npm:^2.0.0" + pathe: "npm:^2.0.3" + vite: "npm:^7.2.2" + bin: + vite-node: dist/cli.mjs + checksum: 10c0/16115db777a98eb6185b54e919f78edfc6031eebccf1b307ff3fba9ee54e4f2e024e3236c3d05bab0c3b9bb0a8486677dd66864a12c7b8d8d0c3ad8e5d4ef787 + languageName: node + linkType: hard + +"vite-plugin-checker@npm:^0.11.0": + version: 0.11.0 + resolution: "vite-plugin-checker@npm:0.11.0" dependencies: "@babel/code-frame": "npm:^7.27.1" chokidar: "npm:^4.0.3" npm-run-path: "npm:^6.0.0" picocolors: "npm:^1.1.1" picomatch: "npm:^4.0.3" - strip-ansi: "npm:^7.1.0" tiny-invariant: "npm:^1.3.3" tinyglobby: "npm:^0.2.14" vscode-uri: "npm:^3.1.0" @@ -13908,9 +15858,10 @@ __metadata: eslint: ">=7" meow: ^13.2.0 optionator: ^0.9.4 + oxlint: ">=1" stylelint: ">=16" typescript: "*" - vite: ">=2.0.0" + vite: ">=5.4.20" vls: "*" vti: "*" vue-tsc: ~2.2.10 || ^3.0.0 @@ -13923,6 +15874,8 @@ __metadata: optional: true optionator: optional: true + oxlint: + optional: true stylelint: optional: true typescript: @@ -13933,7 +15886,7 @@ __metadata: optional: true vue-tsc: optional: true - checksum: 10c0/09668f0a0e78ec03ac73d7aee1b956afd557a59bfd1b2fa822dc177b8bb2a7f996e3e7abc41b84c6469bf60961b364a1cced7c01a11461b7cb2309f29ab368c7 + checksum: 10c0/48feed9dc8896e288b95ad6e631b091bfca0f619c041fa6189264df53b0e6955ec0c067b0129f6baf732f734af02495bdbb292bf8eae372c2df1428ecdd256b7 languageName: node linkType: hard @@ -13975,9 +15928,25 @@ __metadata: languageName: node linkType: hard -"vite@npm:7.1.12": - version: 7.1.12 - resolution: "vite@npm:7.1.12" +"vite-plugin-vue-tracer@npm:^1.1.3": + version: 1.1.3 + resolution: "vite-plugin-vue-tracer@npm:1.1.3" + dependencies: + estree-walker: "npm:^3.0.3" + exsolve: "npm:^1.0.7" + magic-string: "npm:^0.30.21" + pathe: "npm:^2.0.3" + source-map-js: "npm:^1.2.1" + peerDependencies: + vite: ^6.0.0 || ^7.0.0 + vue: ^3.5.0 + checksum: 10c0/c5443ecd4212b057e4f1fd14c468e21fd55f72d0b947a435400bd8fc7700a7d56af727e4712fbfb2014a7a515aa7db8c9e5fdefe964dbe333631cad75be7dd7c + languageName: node + linkType: hard + +"vite@npm:7.2.4, vite@npm:^7.2.1, vite@npm:^7.2.2": + version: 7.2.4 + resolution: "vite@npm:7.2.4" dependencies: esbuild: "npm:^0.25.0" fdir: "npm:^6.5.0" @@ -14026,11 +15995,11 @@ __metadata: optional: true bin: vite: bin/vite.js - checksum: 10c0/cef4d4b4a84e663e09b858964af36e916892ac8540068df42a05ced637ceeae5e9ef71c72d54f3cfc1f3c254af16634230e221b6e2327c2a66d794bb49203262 + checksum: 10c0/26aa0cad01d6e00f17c837b2a0587ab52f6bd0d0e64606b4220cfc58fa5fa76a4095ef3ea27c886bea542a346363912c4fad9f9462ef1e6757262fedfd5196b2 languageName: node linkType: hard -"vite@npm:^5.0.0 || ^6.0.0 || ^7.0.0-0, vite@npm:^7.1.5": +"vite@npm:^5.0.0 || ^6.0.0 || ^7.0.0-0": version: 7.1.8 resolution: "vite@npm:7.1.8" dependencies: @@ -14157,7 +16126,7 @@ __metadata: languageName: node linkType: hard -"vue-bundle-renderer@npm:^2.1.2": +"vue-bundle-renderer@npm:^2.2.0": version: 2.2.0 resolution: "vue-bundle-renderer@npm:2.2.0" dependencies: @@ -14255,14 +16224,14 @@ __metadata: languageName: node linkType: hard -"vue-router@npm:^4.5.1": - version: 4.5.1 - resolution: "vue-router@npm:4.5.1" +"vue-router@npm:^4.6.3": + version: 4.6.3 + resolution: "vue-router@npm:4.6.3" dependencies: "@vue/devtools-api": "npm:^6.6.4" peerDependencies: - vue: ^3.2.0 - checksum: 10c0/89fbc11e46c19a4c4d62b807596a0210726dc09bd9e6a319ded1ac0951e6933e581c56acd1b846d3891673b9bad7348564d28ecd8424126d63578b3b5d291d96 + vue: ^3.5.0 + checksum: 10c0/4528f4fded4b45b07d4e096116e9b5fd602af463e735f7e5133b1a258ce325ebc894dd7bc962a937556615fd42d06b0fb290dfae90e96f237862b7f74baa22a0 languageName: node linkType: hard @@ -14302,21 +16271,39 @@ __metadata: languageName: node linkType: hard -"vue-tsc@npm:3.1.0": - version: 3.1.0 - resolution: "vue-tsc@npm:3.1.0" +"vue-tsc@npm:3.1.5": + version: 3.1.5 + resolution: "vue-tsc@npm:3.1.5" dependencies: "@volar/typescript": "npm:2.4.23" - "@vue/language-core": "npm:3.1.0" + "@vue/language-core": "npm:3.1.5" peerDependencies: typescript: ">=5.0.0" bin: vue-tsc: ./bin/vue-tsc.js - checksum: 10c0/32a676f5c349948b1fc5bf9de4af7ebfe94efa8baba6f402f42abca9bc461d26e3b798854cf851121ac03e5a2beeace93d42d590f2fddd9aeaacd9e9c62b70c9 + checksum: 10c0/f8125efe95208d70d9c52c3a0481514fac44bda0957ff9a4b19635874adcad97b4f25a05ac7fd4ae07cfe25bfd44d22c836d36cfc3d08c3ed093878cd0b555e4 + languageName: node + linkType: hard + +"vue@npm:3.5.25, vue@npm:^3.5.22, vue@npm:^3.5.23": + version: 3.5.25 + resolution: "vue@npm:3.5.25" + dependencies: + "@vue/compiler-dom": "npm:3.5.25" + "@vue/compiler-sfc": "npm:3.5.25" + "@vue/runtime-dom": "npm:3.5.25" + "@vue/server-renderer": "npm:3.5.25" + "@vue/shared": "npm:3.5.25" + peerDependencies: + typescript: "*" + peerDependenciesMeta: + typescript: + optional: true + checksum: 10c0/2b77f9b934e212218d07eb2aa17d02e91578b08673be95553539dfa4246748ef7bc9ce4a380539c9265d85c4d0432329e9cb02eb1b1aec0f3a358433a1b108c2 languageName: node linkType: hard -"vue@npm:3.5.22, vue@npm:^3.5.14, vue@npm:^3.5.17, vue@npm:^3.5.21": +"vue@npm:^3.5.14, vue@npm:^3.5.17": version: 3.5.22 resolution: "vue@npm:3.5.22" dependencies: @@ -14352,18 +16339,18 @@ __metadata: languageName: node linkType: hard -"wait-on@npm:9.0.1": - version: 9.0.1 - resolution: "wait-on@npm:9.0.1" +"wait-on@npm:9.0.3": + version: 9.0.3 + resolution: "wait-on@npm:9.0.3" dependencies: - axios: "npm:^1.12.2" + axios: "npm:^1.13.2" joi: "npm:^18.0.1" lodash: "npm:^4.17.21" minimist: "npm:^1.2.8" rxjs: "npm:^7.8.2" bin: wait-on: bin/wait-on - checksum: 10c0/c90673257cedb50f5e3cb4f4a407d22a0d1c9f79295f04fee91246ed195042229fd45d2d02f331827b06728fa24706e2b8ee5c88c85de2ada87bf4bf17a73fe3 + checksum: 10c0/f5f8cb57be2dbf89edacd104916d5ee0211064c86324ab466c1da0965af77638615258ff612aa4cb16207770b772524bf2d9e583e890d96dd80315dca5733591 languageName: node linkType: hard @@ -14575,7 +16562,7 @@ __metadata: languageName: node linkType: hard -"xss@npm:^1.0.14": +"xss@npm:^1.0.15": version: 1.0.15 resolution: "xss@npm:1.0.15" dependencies: @@ -14632,7 +16619,7 @@ __metadata: languageName: node linkType: hard -"yaml@npm:^2.0.0, yaml@npm:^2.8.0": +"yaml@npm:^2.0.0, yaml@npm:^2.8.1": version: 2.8.1 resolution: "yaml@npm:2.8.1" bin: @@ -14729,6 +16716,19 @@ __metadata: languageName: node linkType: hard +"youch@npm:^4.1.0-beta.12": + version: 4.1.0-beta.13 + resolution: "youch@npm:4.1.0-beta.13" + dependencies: + "@poppinss/colors": "npm:^4.1.5" + "@poppinss/dumper": "npm:^0.6.5" + "@speed-highlight/core": "npm:^1.2.9" + cookie-es: "npm:^2.0.0" + youch-core: "npm:^0.3.3" + checksum: 10c0/bd078f6cc3a0c6d4e22579484522ce194c7af198c50e99dd216ea04301a79261a7c202d91a51d2e0e926bb13aeb9667618577799326ea1d474b2ee8abf31d538 + languageName: node + linkType: hard + "zip-stream@npm:^6.0.1": version: 6.0.1 resolution: "zip-stream@npm:6.0.1" From 421d5d6299fdf89e2d1d0ede2444743d54be0919 Mon Sep 17 00:00:00 2001 From: Ntale Swamadu Date: Wed, 26 Nov 2025 23:58:19 +0300 Subject: [PATCH 036/243] feat: add alphabetical sorting to topics combobox (#1757) * feat: add alphabetical sorting to topics combobox * fix: Sort topics alphabetically on initial load * style: Fix Prettier formatting --- .../app/components/combobox/ComboboxTopics.vue | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/frontend/app/components/combobox/ComboboxTopics.vue b/frontend/app/components/combobox/ComboboxTopics.vue index 7de9fbe38..08f96afbb 100644 --- a/frontend/app/components/combobox/ComboboxTopics.vue +++ b/frontend/app/components/combobox/ComboboxTopics.vue @@ -100,11 +100,14 @@ const { t } = useI18n(); const { data: topics } = useGetTopics(); const options = ref<{ label: string; value: TopicEnum; id: string }[]>([]); -options.value = topics.value.map((topic: Topic) => ({ - label: t(GLOBAL_TOPICS.find((t) => t.topic === topic.type)?.label || ""), - value: topic.type as TopicEnum, - id: topic.id, -})); +options.value = topics.value + .map((topic: Topic) => ({ + label: t(GLOBAL_TOPICS.find((t) => t.topic === topic.type)?.label || ""), + value: topic.type as TopicEnum, + id: topic.id, + })) + .sort((a, b) => a.label.localeCompare(b.label)); + const emit = defineEmits<{ (e: "update:selectedTopics", value: TopicEnum[]): void; }>(); @@ -142,9 +145,8 @@ watch( } if (!aSelected && bSelected) { return 1; - } else { - return 0; } + return a.label.localeCompare(b.label); }); // Emit only the values of the selected topics. emit( From 3f0fa071fede480a672945c37b48e00c69860212 Mon Sep 17 00:00:00 2001 From: Aasim Syed Date: Fri, 28 Nov 2025 04:50:22 -0600 Subject: [PATCH 037/243] test: Add comprehensive unit tests for all Pinia store entities (#1754) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * test: add mock data factories for store tests Create helpers.ts with factory functions for creating mock store data: - Mock factories for all entity types (Event, Organization, Group, Topic) - Mock factories for related types (ContentImage, User, PhysicalLocation, etc.) - Factory functions accept optional overrides for test customization - Inline PhysicalLocation definition to avoid location.ts vs location.d.ts conflict This provides a foundation for implementing comprehensive store entity tests as requested in issue #1749. * test: add comprehensive tests for event store Create event.spec.ts with full test coverage for useEventStore: - Initial state tests (4 tests) - Getter actions tests (4 tests) - Setter actions tests (4 tests) - Integration tests (6 tests) - Edge cases tests (5 tests) Total: 23 tests covering all store actions and state management. Implements part of issue #1749 - store entity tests. * fix: move license header to first line and fix TypeScript errors in event store tests - Move SPDX license identifier to first line (required by pre-commit hooks) - Add CommunityEvent type import - Fix import order (pinia before vitest) - Add type assertions for event overrides with id property * test: add comprehensive tests for organization store Create organization.spec.ts with full test coverage for useOrganizationStore: - Initial state tests (5 tests) - Getter actions tests (5 tests) - Setter actions tests (5 tests) - Special actions tests (2 tests for clearImages) - Integration tests (5 tests) - Edge cases tests (4 tests) Total: 26 tests covering all store actions including images management. Implements part of issue #1749 - store entity tests. * test: add comprehensive tests for group store Create group.spec.ts with full test coverage for useGroupStore: - Initial state tests (3 tests) - Getter actions tests (3 tests) - Setter actions tests (3 tests) - Conditional clear actions tests (7 tests) - Integration tests (5 tests) - Edge cases tests (5 tests) Total: 26 tests covering all store actions including conditional clearing logic based on group.id matching. Note: Tests document that clearGroupImages() and clearGroup() throw errors when group is null (current store implementation behavior). Implements part of issue #1749 - store entity tests. * test: add comprehensive tests for topics store Create topics.spec.ts with full test coverage for useTopics store: - Initial state tests (1 test) - Getter actions tests (1 test) - Setter actions tests (1 test) - Integration tests (2 tests) - Edge cases tests (4 tests) Total: 9 tests covering all store actions. This is the simplest store with only a topics array and basic getter/setter functionality. Implements part of issue #1749 - store entity tests. * refactor: move mock factories to test/mocks/ for better discoverability Move helpers.ts from test/stores/ to test/mocks/factories.ts to make mock factory functions accessible to all tests, not just store tests. Changes: - Create frontend/test/mocks/ directory - Move and rename helpers.ts → mocks/factories.ts - Update all store test imports to reference new location - Single factories.ts file kept (215 lines, well-organized with MARK sections) This addresses PR review feedback about making mock factories more discoverable and follows the preference for 'factories' over 'helpers' naming. * refactor: separate default mock data into data.ts file Move default mock data constants from factories.ts to a dedicated data.ts file to improve maintainability and scalability. Changes: - Create frontend/test/mocks/data.ts with all default mock data constants - Update factories.ts to import and use data from data.ts - Maintain backward compatibility - all tests still pass This addresses PR review feedback about separating data from factory logic. * fix: typescript and changed naming of location to location-type * fix:typing issues from renaming file * chore: trigger CI rebuild * fix: update prettier to 3.7.1 to match CI and format events.ts accordingly * Misc formatting --------- Co-authored-by: nicki182 Co-authored-by: Andrew Tavis McAllister --- frontend/package.json | 2 +- .../{location.d.ts => location-type.d.ts} | 0 frontend/shared/types/map.d.ts | 2 - frontend/shared/types/resource.d.ts | 1 - .../test-e2e/actions/navigation/events.ts | 5 +- frontend/test/mocks/data.ts | 128 +++++++ frontend/test/mocks/factories.ts | 169 +++++++++ frontend/test/stores/event.spec.ts | 237 ++++++++++++ frontend/test/stores/group.spec.ts | 343 ++++++++++++++++++ frontend/test/stores/organization.spec.ts | 273 ++++++++++++++ frontend/test/stores/topics.spec.ts | 124 +++++++ frontend/yarn.lock | 10 +- 12 files changed, 1282 insertions(+), 12 deletions(-) rename frontend/shared/types/{location.d.ts => location-type.d.ts} (100%) create mode 100644 frontend/test/mocks/data.ts create mode 100644 frontend/test/mocks/factories.ts create mode 100644 frontend/test/stores/event.spec.ts create mode 100644 frontend/test/stores/group.spec.ts create mode 100644 frontend/test/stores/organization.spec.ts create mode 100644 frontend/test/stores/topics.spec.ts diff --git a/frontend/package.json b/frontend/package.json index e0b106c3f..592b738b2 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -60,7 +60,7 @@ "nuxt": "4.2.1", "nuxt-security": "2.5.0", "playwright-core": "1.55.1", - "prettier": "3.6.2", + "prettier": "3.7.1", "prettier-plugin-tailwindcss": "0.6.14", "rollup": "4.52.3", "typescript": "5.9.3", diff --git a/frontend/shared/types/location.d.ts b/frontend/shared/types/location-type.d.ts similarity index 100% rename from frontend/shared/types/location.d.ts rename to frontend/shared/types/location-type.d.ts diff --git a/frontend/shared/types/map.d.ts b/frontend/shared/types/map.d.ts index a7add6b2a..7bfdfc16d 100644 --- a/frontend/shared/types/map.d.ts +++ b/frontend/shared/types/map.d.ts @@ -1,6 +1,4 @@ // SPDX-License-Identifier: AGPL-3.0-or-later -import type { PhysicalLocation } from "~/shared/types/content/location"; - export interface RouteProfile { profile: string; api: string; diff --git a/frontend/shared/types/resource.d.ts b/frontend/shared/types/resource.d.ts index 4fb46512f..e6fe7e35a 100644 --- a/frontend/shared/types/resource.d.ts +++ b/frontend/shared/types/resource.d.ts @@ -1,7 +1,6 @@ // SPDX-License-Identifier: AGPL-3.0-or-later import type { User } from "#shared/types/auth/user"; import type { Organization } from "#shared/types/communities/organization"; -import type { PhysicalLocation } from "#shared/types/content/location"; import type { TopicEnum } from "#shared/types/content/topics"; // MARK: Main Table diff --git a/frontend/test-e2e/actions/navigation/events.ts b/frontend/test-e2e/actions/navigation/events.ts index 4992b4db5..da6d5b8d4 100644 --- a/frontend/test-e2e/actions/navigation/events.ts +++ b/frontend/test-e2e/actions/navigation/events.ts @@ -64,9 +64,8 @@ export async function navigateToFirstEvent(page: Page) { // Verify we're on the event page. await expect(page.getByRole("heading", { level: 1 })).toBeVisible(); - const { newEventPage } = await import( - "~/test-e2e/page-objects/event/EventPage" - ); + const { newEventPage } = + await import("~/test-e2e/page-objects/event/EventPage"); const eventPage = newEventPage(page); return { diff --git a/frontend/test/mocks/data.ts b/frontend/test/mocks/data.ts new file mode 100644 index 000000000..2dfef1211 --- /dev/null +++ b/frontend/test/mocks/data.ts @@ -0,0 +1,128 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +/** + * Default mock data constants for test factories. + * These values are used as defaults when creating mock entities in tests. + */ +import { TopicEnum } from "../../shared/types/topics"; + +// MARK: ContentImage + +export const defaultContentImageData = { + id: "img-1", + fileObject: "/test/image.png", + creation_date: "2024-01-01T00:00:00Z", + sequence_index: 0, +} as const; + +// MARK: User + +export const defaultUserData = { + id: "user-1", + userName: "testuser", + name: "Test User", + socialLinks: [] as string[], +} as const; + +// MARK: PhysicalLocation + +export const defaultPhysicalLocationData = { + id: "loc-1", + lat: "0.0", + lon: "0.0", + bbox: [] as string[], + displayName: "Test Location", +} as const; + +// MARK: SocialLink + +export const defaultSocialLinkData = { + id: "social-1", + link: "https://example.com", + label: "Website", + order: 0, + creationDate: "2024-01-01T00:00:00Z", + lastUpdated: "2024-01-01T00:00:00Z", +} as const; + +// MARK: Event + +export const defaultEventTextData = { + id: 1, + eventId: "event-1", + iso: "en", + primary: true, + description: "Test event description", + getInvolved: "Get involved text", +} as const; + +export const defaultEventData = { + id: "event-1", + name: "Test Event", + type: "action", + startTime: "2024-01-01T10:00:00Z", + socialLinks: [], + texts: [], +} as const; + +export const defaultEventFiltersData = { + setting: "action", + locationType: "online", +} as const; + +// MARK: Organization + +export const defaultOrganizationTextData = { + id: 1, + orgId: "org-1", + iso: "en", + primary: true, + description: "Test organization description", + getInvolved: "Get involved text", + donationPrompt: "Donate now", +} as const; + +export const defaultOrganizationData = { + id: "org-1", + name: "Test Organization", + orgName: "Test Organization", + tagline: "Test tagline", + socialLinks: [], + status: 1, + texts: [], +} as const; + +export const defaultOrganizationFiltersData = { + name: "Test", +} as const; + +// MARK: Group + +export const defaultGroupTextData = { + id: 1, + groupId: "group-1", + iso: "en", + primary: true, + description: "Test group description", + getInvolved: "Get involved text", + donationPrompt: "Donate now", +} as const; + +export const defaultGroupData = { + id: "group-1", + name: "Test Group", + groupName: "Test Group", + tagline: "Test tagline", + socialLinks: [], + texts: [], +} as const; + +// MARK: Topic + +export const defaultTopicData = { + id: "topic-1", + type: TopicEnum.ENVIRONMENT, + active: true, + // Dates stored as strings to avoid sharing Date instances. + creation_date: "2024-01-01", + last_updated: "2024-01-01", +} as const; diff --git a/frontend/test/mocks/factories.ts b/frontend/test/mocks/factories.ts new file mode 100644 index 000000000..795067ba4 --- /dev/null +++ b/frontend/test/mocks/factories.ts @@ -0,0 +1,169 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +/** + * Factory functions for creating mock store data. + * These ensure consistent test data structure across all store tests. + */ +import type { + CommunityEvent, + EventFilters, + EventText, +} from "../../shared/types/event"; +import type { ContentImage } from "../../shared/types/file-type"; +import type { Group, GroupText } from "../../shared/types/group"; +import type { PhysicalLocation } from "../../shared/types/location-type"; +import type { + Organization, + OrganizationFilters, + OrganizationText, +} from "../../shared/types/organization"; +import type { SocialLink } from "../../shared/types/social-link"; +import type { Topic } from "../../shared/types/topics-type"; +import type { User } from "../../shared/types/user"; + +import { + defaultContentImageData, + defaultEventData, + defaultEventFiltersData, + defaultEventTextData, + defaultGroupData, + defaultGroupTextData, + defaultOrganizationData, + defaultOrganizationFiltersData, + defaultOrganizationTextData, + defaultPhysicalLocationData, + defaultSocialLinkData, + defaultTopicData, + defaultUserData, +} from "./data"; + +// MARK: ContentImage + +export function createMockContentImage( + overrides?: Partial +): ContentImage { + return { + ...defaultContentImageData, + ...overrides, + }; +} + +// MARK: User + +export function createMockUser(overrides?: Partial): User { + return { + ...defaultUserData, + ...overrides, + }; +} + +// MARK: PhysicalLocation + +export function createMockPhysicalLocation( + overrides?: Partial +): PhysicalLocation { + return { + ...defaultPhysicalLocationData, + ...overrides, + }; +} + +// MARK: SocialLink + +export function createMockSocialLink( + overrides?: Partial +): SocialLink { + return { + ...defaultSocialLinkData, + ...overrides, + }; +} + +// MARK: Event + +export function createMockEventText(overrides?: Partial): EventText { + return { + ...defaultEventTextData, + ...overrides, + }; +} + +export function createMockEvent( + overrides?: Partial +): CommunityEvent { + return { + ...defaultEventData, + createdBy: createMockUser(), + orgs: createMockOrganization(), + ...overrides, + } as CommunityEvent; +} + +export function createMockEventFilters( + overrides?: Partial +): EventFilters { + return { + ...defaultEventFiltersData, + ...overrides, + }; +} + +// MARK: Organization + +export function createMockOrganizationText( + overrides?: Partial +): OrganizationText { + return { + ...defaultOrganizationTextData, + ...overrides, + }; +} + +export function createMockOrganization( + overrides?: Partial +): Organization { + return { + ...defaultOrganizationData, + createdBy: createMockUser(), + location: createMockPhysicalLocation(), + ...overrides, + } as Organization; +} + +export function createMockOrganizationFilters( + overrides?: Partial +): OrganizationFilters { + return { + ...defaultOrganizationFiltersData, + ...overrides, + }; +} + +// MARK: Group + +export function createMockGroupText(overrides?: Partial): GroupText { + return { + ...defaultGroupTextData, + ...overrides, + }; +} + +export function createMockGroup(overrides?: Partial): Group { + return { + ...defaultGroupData, + createdBy: createMockUser(), + location: createMockPhysicalLocation(), + org: createMockOrganization(), + ...overrides, + } as Group; +} + +// MARK: Topic + +export function createMockTopic(overrides?: Partial): Topic { + return { + ...defaultTopicData, + creation_date: new Date(defaultTopicData.creation_date), + last_updated: new Date(defaultTopicData.last_updated), + ...overrides, + }; +} diff --git a/frontend/test/stores/event.spec.ts b/frontend/test/stores/event.spec.ts new file mode 100644 index 000000000..9b3af58d8 --- /dev/null +++ b/frontend/test/stores/event.spec.ts @@ -0,0 +1,237 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +import { createPinia, setActivePinia } from "pinia"; +import { beforeEach, describe, expect, it } from "vitest"; + +import type { CommunityEvent } from "../../shared/types/event"; + +import { useEventStore } from "../../app/stores/event"; +import { createMockEvent, createMockEventFilters } from "../mocks/factories"; + +describe("useEventStore", () => { + beforeEach(() => { + // Create fresh Pinia instance for each test to ensure isolation. + setActivePinia(createPinia()); + }); + + // MARK: Initial State + + describe("Initial State", () => { + it("initializes with null event", () => { + const store = useEventStore(); + expect(store.event).toBeNull(); + }); + + it("initializes with empty events array", () => { + const store = useEventStore(); + expect(store.events).toEqual([]); + }); + + it("initializes with empty filters object", () => { + const store = useEventStore(); + expect(store.filters).toEqual({}); + }); + + it("initializes with page 0", () => { + const store = useEventStore(); + expect(store.page).toBe(0); + }); + }); + + // MARK: Getter Actions + + describe("Getter Actions", () => { + it("getEvent returns current event", () => { + const store = useEventStore(); + const mockEvent = createMockEvent({ + id: "event-1", + } as Partial); + store.setEvent(mockEvent); + expect(store.getEvent()).toEqual(mockEvent); + }); + + it("getEvents returns current events array", () => { + const store = useEventStore(); + const mockEvents = [ + createMockEvent({ id: "event-1" } as Partial), + ]; + store.setEvents(mockEvents); + expect(store.getEvents()).toEqual(mockEvents); + }); + + it("getFilters returns current filters", () => { + const store = useEventStore(); + const mockFilters = createMockEventFilters({ setting: "action" }); + store.setFilters(mockFilters); + expect(store.getFilters()).toEqual(mockFilters); + }); + + it("getPage returns current page number", () => { + const store = useEventStore(); + store.setPage(5); + expect(store.getPage()).toBe(5); + }); + }); + + // MARK: Setter Actions + + describe("Setter Actions", () => { + it("setEvent updates event state", () => { + const store = useEventStore(); + const mockEvent = createMockEvent({ + id: "event-1", + } as Partial); + store.setEvent(mockEvent); + expect(store.event).toEqual(mockEvent); + }); + + it("setEvents updates events array", () => { + const store = useEventStore(); + const mockEvents = [ + createMockEvent({ id: "event-1" } as Partial), + createMockEvent({ id: "event-2" } as Partial), + ]; + store.setEvents(mockEvents); + expect(store.events).toEqual(mockEvents); + expect(store.events).toHaveLength(2); + }); + + it("setFilters updates filters object", () => { + const store = useEventStore(); + const mockFilters = createMockEventFilters({ + setting: "learn", + locationType: "offline", + }); + store.setFilters(mockFilters); + expect(store.filters).toEqual(mockFilters); + }); + + it("setPage updates page number", () => { + const store = useEventStore(); + store.setPage(10); + expect(store.page).toBe(10); + }); + }); + + // MARK: Integration Tests + + describe("Integration Tests", () => { + it("setting event then getting it returns the same event", () => { + const store = useEventStore(); + const mockEvent = createMockEvent({ + id: "event-1", + name: "Test Event", + } as Partial); + store.setEvent(mockEvent); + expect(store.getEvent()).toEqual(mockEvent); + expect(store.getEvent().id).toBe("event-1"); + }); + + it("setting events array then getting it returns the same array", () => { + const store = useEventStore(); + const mockEvents = [ + createMockEvent({ id: "event-1" } as Partial), + createMockEvent({ id: "event-2" } as Partial), + ]; + store.setEvents(mockEvents); + expect(store.getEvents()).toEqual(mockEvents); + expect(store.getEvents()).toHaveLength(2); + }); + + it("setting filters then getting them returns the same filters", () => { + const store = useEventStore(); + const mockFilters = createMockEventFilters({ + setting: "action", + locationType: "online", + name: "test", + }); + store.setFilters(mockFilters); + expect(store.getFilters()).toEqual(mockFilters); + }); + + it("setting page then getting it returns the same page", () => { + const store = useEventStore(); + store.setPage(3); + expect(store.getPage()).toBe(3); + expect(store.page).toBe(3); + }); + + it("can set event independently of events array", () => { + const store = useEventStore(); + const singleEvent = createMockEvent({ + id: "event-single", + } as Partial); + const eventsArray = [ + createMockEvent({ id: "event-1" } as Partial), + createMockEvent({ id: "event-2" } as Partial), + ]; + + store.setEvent(singleEvent); + store.setEvents(eventsArray); + + expect(store.getEvent()).toEqual(singleEvent); + expect(store.getEvents()).toEqual(eventsArray); + expect(store.event.id).toBe("event-single"); + expect(store.events).toHaveLength(2); + }); + + it("can update filters without affecting other state", () => { + const store = useEventStore(); + const mockEvent = createMockEvent({ + id: "event-1", + } as Partial); + const mockEvents = [mockEvent]; + const mockFilters = createMockEventFilters({ setting: "action" }); + + store.setEvent(mockEvent); + store.setEvents(mockEvents); + store.setPage(5); + store.setFilters(mockFilters); + + // Verify all state remains intact. + expect(store.getEvent()).toEqual(mockEvent); + expect(store.getEvents()).toEqual(mockEvents); + expect(store.getPage()).toBe(5); + expect(store.getFilters()).toEqual(mockFilters); + }); + }); + + // MARK: Edge Cases + + describe("Edge Cases", () => { + it("handles setting empty events array", () => { + const store = useEventStore(); + store.setEvents([createMockEvent()]); + store.setEvents([]); + expect(store.events).toEqual([]); + expect(store.getEvents()).toHaveLength(0); + }); + + it("handles setting empty filters object", () => { + const store = useEventStore(); + store.setFilters(createMockEventFilters({ setting: "action" })); + store.setFilters({}); + expect(store.filters).toEqual({}); + }); + + it("can set page to 0", () => { + const store = useEventStore(); + store.setPage(5); + store.setPage(0); + expect(store.page).toBe(0); + expect(store.getPage()).toBe(0); + }); + + it("can set page to negative number", () => { + const store = useEventStore(); + store.setPage(-1); + expect(store.page).toBe(-1); + expect(store.getPage()).toBe(-1); + }); + it("can set page to large number", () => { + const store = useEventStore(); + store.setPage(999); + expect(store.page).toBe(999); + expect(store.getPage()).toBe(999); + }); + }); +}); diff --git a/frontend/test/stores/group.spec.ts b/frontend/test/stores/group.spec.ts new file mode 100644 index 000000000..03db29e5d --- /dev/null +++ b/frontend/test/stores/group.spec.ts @@ -0,0 +1,343 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +import { createPinia, setActivePinia } from "pinia"; +import { beforeEach, describe, expect, it } from "vitest"; + +import type { Group } from "../../shared/types/group"; + +import { useGroupStore } from "../../app/stores/group"; +import { createMockContentImage, createMockGroup } from "../mocks/factories"; + +describe("useGroupStore", () => { + beforeEach(() => { + // Create fresh Pinia instance for each test to ensure isolation. + setActivePinia(createPinia()); + }); + + // MARK: Initial State + + describe("Initial State", () => { + it("initializes with null group", () => { + const store = useGroupStore(); + expect(store.group).toBeNull(); + }); + + it("initializes with empty groups array", () => { + const store = useGroupStore(); + expect(store.groups).toEqual([]); + }); + + it("initializes with empty images array", () => { + const store = useGroupStore(); + expect(store.images).toEqual([]); + }); + }); + + // MARK: Getter Actions + + describe("Getter Actions", () => { + it("getGroup returns current group", () => { + const store = useGroupStore(); + const mockGroup = createMockGroup({ id: "group-1" } as Partial); + store.setGroup(mockGroup); + expect(store.getGroup()).toEqual(mockGroup); + }); + + it("getGroups returns current groups array", () => { + const store = useGroupStore(); + const mockGroups = [createMockGroup({ id: "group-1" } as Partial)]; + store.setGroups(mockGroups); + expect(store.getGroups()).toEqual(mockGroups); + }); + + it("getGroupImages returns current images array", () => { + const store = useGroupStore(); + const mockImages = [createMockContentImage({ id: "img-1" })]; + store.setGroupImages(mockImages); + expect(store.getGroupImages()).toEqual(mockImages); + }); + }); + + // MARK: Setter Actions + + describe("Setter Actions", () => { + it("setGroup updates group state", () => { + const store = useGroupStore(); + const mockGroup = createMockGroup({ id: "group-1" } as Partial); + store.setGroup(mockGroup); + expect(store.group).toEqual(mockGroup); + }); + + it("setGroups updates groups array", () => { + const store = useGroupStore(); + const mockGroups = [ + createMockGroup({ id: "group-1" } as Partial), + createMockGroup({ id: "group-2" } as Partial), + ]; + store.setGroups(mockGroups); + expect(store.groups).toEqual(mockGroups); + expect(store.groups).toHaveLength(2); + }); + + it("setGroupImages updates images array", () => { + const store = useGroupStore(); + const mockImages = [ + createMockContentImage({ id: "img-1" }), + createMockContentImage({ id: "img-2" }), + ]; + store.setGroupImages(mockImages); + expect(store.images).toEqual(mockImages); + expect(store.images).toHaveLength(2); + }); + }); + + // MARK: Conditional Clear Actions + + describe("Conditional Clear Actions", () => { + it("clearGroupImages clears images only if group.id matches", () => { + const store = useGroupStore(); + const mockGroup = createMockGroup({ id: "group-1" } as Partial); + const mockImages = [ + createMockContentImage({ id: "img-1" }), + createMockContentImage({ id: "img-2" }), + ]; + + store.setGroup(mockGroup); + store.setGroupImages(mockImages); + + // Clear with matching id - should clear images. + store.clearGroupImages("group-1"); + expect(store.getGroupImages()).toEqual([]); + expect(store.images).toHaveLength(0); + }); + + it("clearGroupImages does nothing if group.id does not match", () => { + const store = useGroupStore(); + const mockGroup = createMockGroup({ id: "group-1" } as Partial); + const mockImages = [ + createMockContentImage({ id: "img-1" }), + createMockContentImage({ id: "img-2" }), + ]; + + store.setGroup(mockGroup); + store.setGroupImages(mockImages); + + // Clear with non-matching id - should not clear images. + store.clearGroupImages("group-2"); + expect(store.getGroupImages()).toEqual(mockImages); + expect(store.images).toHaveLength(2); + }); + + it("clearGroupImages throws error when group is null", () => { + const store = useGroupStore(); + const mockImages = [createMockContentImage({ id: "img-1" })]; + + // Group is null by default. + expect(store.group).toBeNull(); + store.setGroupImages(mockImages); + + // Attempt to clear - should throw error when accessing group.id on null. + expect(() => { + store.clearGroupImages("group-1"); + }).toThrow(); + // Images should remain since error prevents clearing. + expect(store.getGroupImages()).toEqual(mockImages); + }); + + it("clearGroup sets group to null only if group.id matches", () => { + const store = useGroupStore(); + const mockGroup = createMockGroup({ id: "group-1" } as Partial); + + store.setGroup(mockGroup); + + // Clear with matching id - should set group to null. + store.clearGroup("group-1"); + expect(store.getGroup()).toBeNull(); + expect(store.group).toBeNull(); + }); + + it("clearGroup does nothing if group.id does not match", () => { + const store = useGroupStore(); + const mockGroup = createMockGroup({ id: "group-1" } as Partial); + + store.setGroup(mockGroup); + + // Clear with non-matching id - should not clear group. + store.clearGroup("group-2"); + expect(store.getGroup()).toEqual(mockGroup); + expect(store.group.id).toBe("group-1"); + }); + + it("clearGroup throws error when group is null", () => { + const store = useGroupStore(); + + // Group is null by default. + expect(store.group).toBeNull(); + + // Attempt to clear - should throw error when accessing group.id on null. + expect(() => { + store.clearGroup("group-1"); + }).toThrow(); + expect(store.getGroup()).toBeNull(); + expect(store.group).toBeNull(); + }); + + it("clearGroups always clears groups array regardless of state", () => { + const store = useGroupStore(); + const mockGroups = [ + createMockGroup({ id: "group-1" } as Partial), + createMockGroup({ id: "group-2" } as Partial), + ]; + + store.setGroups(mockGroups); + store.clearGroups(); + + expect(store.getGroups()).toEqual([]); + expect(store.groups).toHaveLength(0); + }); + }); + + // MARK: Integration Tests + + describe("Integration Tests", () => { + it("setting group with specific id, then clearing images with same id clears images", () => { + const store = useGroupStore(); + const mockGroup = createMockGroup({ id: "group-1" } as Partial); + const mockImages = [ + createMockContentImage({ id: "img-1" }), + createMockContentImage({ id: "img-2" }), + ]; + + store.setGroup(mockGroup); + store.setGroupImages(mockImages); + expect(store.getGroupImages()).toHaveLength(2); + + store.clearGroupImages("group-1"); + expect(store.getGroupImages()).toEqual([]); + expect(store.getGroup()).toEqual(mockGroup); // group should remain + }); + + it("setting group with specific id, then clearing images with different id doesn't clear images", () => { + const store = useGroupStore(); + const mockGroup = createMockGroup({ id: "group-1" } as Partial); + const mockImages = [ + createMockContentImage({ id: "img-1" }), + createMockContentImage({ id: "img-2" }), + ]; + + store.setGroup(mockGroup); + store.setGroupImages(mockImages); + + store.clearGroupImages("group-2"); // different id + expect(store.getGroupImages()).toEqual(mockImages); + expect(store.getGroupImages()).toHaveLength(2); + }); + + it("setting group, then clearing it sets group to null", () => { + const store = useGroupStore(); + const mockGroup = createMockGroup({ id: "group-1" } as Partial); + + store.setGroup(mockGroup); + expect(store.getGroup()).toEqual(mockGroup); + + store.clearGroup("group-1"); + expect(store.getGroup()).toBeNull(); + }); + + it("can set groups array independently of single group", () => { + const store = useGroupStore(); + const singleGroup = createMockGroup({ + id: "group-single", + } as Partial); + const groupsArray = [ + createMockGroup({ id: "group-1" } as Partial), + createMockGroup({ id: "group-2" } as Partial), + ]; + + store.setGroup(singleGroup); + store.setGroups(groupsArray); + + expect(store.getGroup()).toEqual(singleGroup); + expect(store.getGroups()).toEqual(groupsArray); + expect(store.group.id).toBe("group-single"); + expect(store.groups).toHaveLength(2); + }); + + it("clearing groups doesn't affect single group", () => { + const store = useGroupStore(); + const mockGroup = createMockGroup({ id: "group-1" } as Partial); + const mockGroups = [ + createMockGroup({ id: "group-2" } as Partial), + createMockGroup({ id: "group-3" } as Partial), + ]; + + store.setGroup(mockGroup); + store.setGroups(mockGroups); + + store.clearGroups(); + + expect(store.getGroup()).toEqual(mockGroup); // single group should remain + expect(store.getGroups()).toEqual([]); // groups array should be cleared + }); + }); + + // MARK: Edge Cases + + describe("Edge Cases", () => { + it("clearing images when group is null throws error", () => { + const store = useGroupStore(); + const mockImages = [createMockContentImage({ id: "img-1" })]; + + expect(store.group).toBeNull(); + store.setGroupImages(mockImages); + + // Should throw error when accessing group.id on null + expect(() => { + store.clearGroupImages("group-1"); + }).toThrow(); + expect(store.getGroupImages()).toEqual(mockImages); // Images should remain + }); + + it("clearing group when group is null throws error", () => { + const store = useGroupStore(); + + expect(store.group).toBeNull(); + + // Should throw error when accessing group.id on null. + expect(() => { + store.clearGroup("group-1"); + }).toThrow(); + expect(store.getGroup()).toBeNull(); + }); + + it("clearing images when group.id doesn't match should not clear", () => { + const store = useGroupStore(); + const mockGroup = createMockGroup({ id: "group-1" } as Partial); + const mockImages = [ + createMockContentImage({ id: "img-1" }), + createMockContentImage({ id: "img-2" }), + ]; + + store.setGroup(mockGroup); + store.setGroupImages(mockImages); + + store.clearGroupImages("different-id"); + expect(store.getGroupImages()).toEqual(mockImages); + expect(store.getGroupImages()).toHaveLength(2); + }); + + it("handles setting empty groups array", () => { + const store = useGroupStore(); + store.setGroups([createMockGroup()]); + store.setGroups([]); + expect(store.groups).toEqual([]); + expect(store.getGroups()).toHaveLength(0); + }); + it("handles setting empty images array", () => { + const store = useGroupStore(); + store.setGroupImages([createMockContentImage()]); + store.setGroupImages([]); + expect(store.images).toEqual([]); + expect(store.getGroupImages()).toHaveLength(0); + }); + }); +}); diff --git a/frontend/test/stores/organization.spec.ts b/frontend/test/stores/organization.spec.ts new file mode 100644 index 000000000..8198d84f8 --- /dev/null +++ b/frontend/test/stores/organization.spec.ts @@ -0,0 +1,273 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +import { createPinia, setActivePinia } from "pinia"; +import { beforeEach, describe, expect, it } from "vitest"; + +import type { Organization } from "../../shared/types/organization"; + +import { useOrganizationStore } from "../../app/stores/organization"; +import { + createMockContentImage, + createMockOrganization, + createMockOrganizationFilters, +} from "../mocks/factories"; + +describe("useOrganizationStore", () => { + beforeEach(() => { + // Create fresh Pinia instance for each test to ensure isolation. + setActivePinia(createPinia()); + }); + + // MARK: Initial State + + describe("Initial State", () => { + it("initializes with null organization", () => { + const store = useOrganizationStore(); + expect(store.organization).toBeNull(); + }); + + it("initializes with empty organizations array", () => { + const store = useOrganizationStore(); + expect(store.organizations).toEqual([]); + }); + + it("initializes with empty images array", () => { + const store = useOrganizationStore(); + expect(store.images).toEqual([]); + }); + + it("initializes with empty filters object", () => { + const store = useOrganizationStore(); + expect(store.filters).toEqual({}); + }); + + it("initializes with page 0", () => { + const store = useOrganizationStore(); + expect(store.page).toBe(0); + }); + }); + + // MARK: Getter Actions + + describe("Getter Actions", () => { + it("getOrganization returns current organization", () => { + const store = useOrganizationStore(); + const mockOrg = createMockOrganization({ + id: "org-1", + } as Partial); + store.setOrganization(mockOrg); + expect(store.getOrganization()).toEqual(mockOrg); + }); + + it("getOrganizations returns current organizations array", () => { + const store = useOrganizationStore(); + const mockOrgs = [ + createMockOrganization({ id: "org-1" } as Partial), + ]; + store.setOrganizations(mockOrgs); + expect(store.getOrganizations()).toEqual(mockOrgs); + }); + + it("getImages returns current images array", () => { + const store = useOrganizationStore(); + const mockImages = [createMockContentImage({ id: "img-1" })]; + store.setImages(mockImages); + expect(store.getImages()).toEqual(mockImages); + }); + + it("getFilters returns current filters", () => { + const store = useOrganizationStore(); + const mockFilters = createMockOrganizationFilters({ name: "Test Org" }); + store.setFilters(mockFilters); + expect(store.getFilters()).toEqual(mockFilters); + }); + + it("getPage returns current page number", () => { + const store = useOrganizationStore(); + store.setPage(5); + expect(store.getPage()).toBe(5); + }); + }); + + // MARK: Setter Actions + + describe("Setter Actions", () => { + it("setOrganization updates organization state", () => { + const store = useOrganizationStore(); + const mockOrg = createMockOrganization({ + id: "org-1", + } as Partial); + store.setOrganization(mockOrg); + expect(store.organization).toEqual(mockOrg); + }); + + it("setOrganizations updates organizations array", () => { + const store = useOrganizationStore(); + const mockOrgs = [ + createMockOrganization({ id: "org-1" } as Partial), + createMockOrganization({ id: "org-2" } as Partial), + ]; + store.setOrganizations(mockOrgs); + expect(store.organizations).toEqual(mockOrgs); + expect(store.organizations).toHaveLength(2); + }); + + it("setImages updates images array", () => { + const store = useOrganizationStore(); + const mockImages = [ + createMockContentImage({ id: "img-1" }), + createMockContentImage({ id: "img-2" }), + ]; + store.setImages(mockImages); + expect(store.images).toEqual(mockImages); + expect(store.images).toHaveLength(2); + }); + + it("setFilters updates filters object", () => { + const store = useOrganizationStore(); + const mockFilters = createMockOrganizationFilters({ name: "Test" }); + store.setFilters(mockFilters); + expect(store.filters).toEqual(mockFilters); + }); + + it("setPage updates page number", () => { + const store = useOrganizationStore(); + store.setPage(10); + expect(store.page).toBe(10); + }); + }); + + // MARK: Special Actions + + describe("Special Actions", () => { + it("clearImages sets images to empty array", () => { + const store = useOrganizationStore(); + const mockImages = [ + createMockContentImage({ id: "img-1" }), + createMockContentImage({ id: "img-2" }), + ]; + store.setImages(mockImages); + store.clearImages(); + expect(store.images).toEqual([]); + expect(store.getImages()).toHaveLength(0); + }); + + it("clearImages clears images regardless of current state", () => { + const store = useOrganizationStore(); + // Clear when already empty. + store.clearImages(); + expect(store.images).toEqual([]); + + // Clear when populated. + store.setImages([createMockContentImage()]); + store.clearImages(); + expect(store.images).toEqual([]); + }); + }); + + // MARK: Integration Tests + + describe("Integration Tests", () => { + it("setting organization then getting it returns the same organization", () => { + const store = useOrganizationStore(); + const mockOrg = createMockOrganization({ + id: "org-1", + name: "Test Organization", + } as Partial); + store.setOrganization(mockOrg); + expect(store.getOrganization()).toEqual(mockOrg); + expect(store.getOrganization().id).toBe("org-1"); + }); + + it("setting images then clearing them results in empty array", () => { + const store = useOrganizationStore(); + const mockImages = [ + createMockContentImage({ id: "img-1" }), + createMockContentImage({ id: "img-2" }), + ]; + store.setImages(mockImages); + expect(store.getImages()).toHaveLength(2); + store.clearImages(); + expect(store.getImages()).toEqual([]); + expect(store.getImages()).toHaveLength(0); + }); + + it("setting filters doesn't affect images array", () => { + const store = useOrganizationStore(); + const mockImages = [createMockContentImage({ id: "img-1" })]; + const mockFilters = createMockOrganizationFilters({ name: "Test" }); + + store.setImages(mockImages); + store.setFilters(mockFilters); + + expect(store.getImages()).toEqual(mockImages); + expect(store.getFilters()).toEqual(mockFilters); + }); + + it("setting page doesn't affect organization", () => { + const store = useOrganizationStore(); + const mockOrg = createMockOrganization({ + id: "org-1", + } as Partial); + + store.setOrganization(mockOrg); + store.setPage(5); + + expect(store.getOrganization()).toEqual(mockOrg); + expect(store.getPage()).toBe(5); + }); + + it("can set images independently of organization", () => { + const store = useOrganizationStore(); + const mockOrg = createMockOrganization({ + id: "org-1", + } as Partial); + const mockImages = [ + createMockContentImage({ id: "img-1" }), + createMockContentImage({ id: "img-2" }), + ]; + + store.setOrganization(mockOrg); + store.setImages(mockImages); + + expect(store.getOrganization()).toEqual(mockOrg); + expect(store.getImages()).toEqual(mockImages); + expect(store.organization.id).toBe("org-1"); + expect(store.images).toHaveLength(2); + }); + }); + + // MARK: Edge Cases + + describe("Edge Cases", () => { + it("handles setting empty organizations array", () => { + const store = useOrganizationStore(); + store.setOrganizations([createMockOrganization()]); + store.setOrganizations([]); + expect(store.organizations).toEqual([]); + expect(store.getOrganizations()).toHaveLength(0); + }); + + it("handles setting empty images array", () => { + const store = useOrganizationStore(); + store.setImages([createMockContentImage()]); + store.setImages([]); + expect(store.images).toEqual([]); + expect(store.getImages()).toHaveLength(0); + }); + + it("handles clearing images when array is already empty", () => { + const store = useOrganizationStore(); + expect(store.images).toEqual([]); + store.clearImages(); + expect(store.images).toEqual([]); + expect(store.getImages()).toHaveLength(0); + }); + it("can set page to 0", () => { + const store = useOrganizationStore(); + store.setPage(5); + store.setPage(0); + expect(store.page).toBe(0); + expect(store.getPage()).toBe(0); + }); + }); +}); diff --git a/frontend/test/stores/topics.spec.ts b/frontend/test/stores/topics.spec.ts new file mode 100644 index 000000000..806681913 --- /dev/null +++ b/frontend/test/stores/topics.spec.ts @@ -0,0 +1,124 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +import { createPinia, setActivePinia } from "pinia"; +import { beforeEach, describe, expect, it } from "vitest"; + +import { useTopics } from "../../app/stores/topics"; +import { createMockTopic } from "../mocks/factories"; + +describe("useTopics", () => { + beforeEach(() => { + // Create fresh Pinia instance for each test to ensure isolation. + setActivePinia(createPinia()); + }); + + // MARK: Initial State + + describe("Initial State", () => { + it("initializes with empty topics array", () => { + const store = useTopics(); + expect(store.topics).toEqual([]); + }); + }); + + // MARK: Getter Actions + + describe("Getter Actions", () => { + it("getTopics returns current topics array", () => { + const store = useTopics(); + const mockTopics = [createMockTopic({ id: "topic-1" })]; + store.setTopics(mockTopics); + expect(store.getTopics()).toEqual(mockTopics); + }); + }); + + // MARK: Setter Actions + + describe("Setter Actions", () => { + it("setTopics updates topics array", () => { + const store = useTopics(); + const mockTopics = [ + createMockTopic({ id: "topic-1" }), + createMockTopic({ id: "topic-2" }), + ]; + store.setTopics(mockTopics); + expect(store.topics).toEqual(mockTopics); + expect(store.topics).toHaveLength(2); + }); + }); + + // MARK: Integration Tests + + describe("Integration Tests", () => { + it("setting topics then getting them returns the same array", () => { + const store = useTopics(); + const mockTopics = [ + createMockTopic({ id: "topic-1" }), + createMockTopic({ id: "topic-2" }), + ]; + store.setTopics(mockTopics); + expect(store.getTopics()).toEqual(mockTopics); + expect(store.getTopics()).toHaveLength(2); + }); + + it("topics array is reactive - changes reflect immediately", () => { + const store = useTopics(); + const mockTopics = [createMockTopic({ id: "topic-1" })]; + + store.setTopics(mockTopics); + expect(store.topics).toEqual(mockTopics); + expect(store.getTopics()).toEqual(mockTopics); + + // Update topics. + const updatedTopics = [ + createMockTopic({ id: "topic-1" }), + createMockTopic({ id: "topic-2" }), + ]; + store.setTopics(updatedTopics); + expect(store.topics).toEqual(updatedTopics); + expect(store.getTopics()).toEqual(updatedTopics); + expect(store.topics).toHaveLength(2); + }); + }); + + // MARK: Edge Cases + + describe("Edge Cases", () => { + it("handles setting empty topics array", () => { + const store = useTopics(); + store.setTopics([createMockTopic()]); + store.setTopics([]); + expect(store.topics).toEqual([]); + expect(store.getTopics()).toHaveLength(0); + }); + + it("handles setting topics with single topic", () => { + const store = useTopics(); + const singleTopic = [createMockTopic({ id: "topic-1" })]; + store.setTopics(singleTopic); + expect(store.getTopics()).toEqual(singleTopic); + expect(store.getTopics()).toHaveLength(1); + expect(store.topics[0]?.id).toBe("topic-1"); + }); + + it("handles setting topics with multiple topics", () => { + const store = useTopics(); + const multipleTopics = [ + createMockTopic({ id: "topic-1" }), + createMockTopic({ id: "topic-2" }), + createMockTopic({ id: "topic-3" }), + ]; + store.setTopics(multipleTopics); + expect(store.getTopics()).toEqual(multipleTopics); + expect(store.getTopics()).toHaveLength(3); + expect(store.topics[0]?.id).toBe("topic-1"); + expect(store.topics[1]?.id).toBe("topic-2"); + expect(store.topics[2]?.id).toBe("topic-3"); + }); + it("getting topics when array is empty returns empty array", () => { + const store = useTopics(); + expect(store.getTopics()).toEqual([]); + expect(store.getTopics()).toHaveLength(0); + expect(store.topics).toEqual([]); + }); + }); +}); diff --git a/frontend/yarn.lock b/frontend/yarn.lock index ee32d988c..a0bb56577 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -6296,7 +6296,7 @@ __metadata: playwright-core: "npm:1.55.1" postcss: "npm:8.5.6" postcss-custom-properties: "npm:14.0.6" - prettier: "npm:3.6.2" + prettier: "npm:3.7.1" prettier-plugin-tailwindcss: "npm:0.6.14" qrcode: "npm:1.5.4" qrcode.vue: "npm:3.6.0" @@ -13199,12 +13199,12 @@ __metadata: languageName: node linkType: hard -"prettier@npm:3.6.2": - version: 3.6.2 - resolution: "prettier@npm:3.6.2" +"prettier@npm:3.7.1": + version: 3.7.1 + resolution: "prettier@npm:3.7.1" bin: prettier: bin/prettier.cjs - checksum: 10c0/488cb2f2b99ec13da1e50074912870217c11edaddedeadc649b1244c749d15ba94e846423d062e2c4c9ae683e2d65f754de28889ba06e697ac4f988d44f45812 + checksum: 10c0/a6610043ee0a64a3251a948bf82fad3e59d984a8e8dea206400cfa190585417e3343b32c1f6ae7d8f40798a9b4bd91affc08fa7795dd99a9dec5c9bccdf31500 languageName: node linkType: hard From 418a0f00309be1d5f7a1cd487b02d6d4b253fc1a Mon Sep 17 00:00:00 2001 From: Aasim Syed Date: Fri, 28 Nov 2025 12:57:18 -0600 Subject: [PATCH 038/243] fix: add page validation to stores and update tests (#1767) - Add validation to setPage in event and organization stores - Ensure page is always >= 1 by clamping invalid values to 1 - Update initial page state from 0 to 1 (1-indexed pagination) - Update tests to reflect validation behavior: - Remove negative number test (now clamps to 1) - Update page 0 test to expect clamping to 1 - Update initial state test to expect page 1 This addresses the edge case where invalid page numbers were accepted and ensures consistent 1-indexed pagination throughout the application. --- frontend/app/stores/event.ts | 5 +++-- frontend/app/stores/organization.ts | 5 +++-- frontend/test/stores/event.spec.ts | 16 ++++++++-------- frontend/test/stores/organization.spec.ts | 17 ++++++++++++----- 4 files changed, 26 insertions(+), 17 deletions(-) diff --git a/frontend/app/stores/event.ts b/frontend/app/stores/event.ts index 17c73797a..94307d3ba 100644 --- a/frontend/app/stores/event.ts +++ b/frontend/app/stores/event.ts @@ -13,7 +13,7 @@ export const useEventStore = defineStore("event", { event: null as unknown as CommunityEvent, events: [], filters: {} as EventFilters, - page: 0, + page: 1, }), actions: { setEvent(event: CommunityEvent) { @@ -24,7 +24,8 @@ export const useEventStore = defineStore("event", { return this.page; }, setPage(page: number) { - this.page = page; + // Ensure page is always >= 1. Invalid values are clamped to 1. + this.page = Math.max(1, page); }, getEvent(): CommunityEvent { diff --git a/frontend/app/stores/organization.ts b/frontend/app/stores/organization.ts index 4c8747b6e..7bd2c2d5e 100644 --- a/frontend/app/stores/organization.ts +++ b/frontend/app/stores/organization.ts @@ -13,7 +13,7 @@ export const useOrganizationStore = defineStore("organization", { images: [] as ContentImage[], organizations: [], filters: {} as OrganizationFilters, - page: 0, + page: 1, }), actions: { // MARK: Set Organizations @@ -21,7 +21,8 @@ export const useOrganizationStore = defineStore("organization", { return this.page; }, setPage(page: number) { - this.page = page; + // Ensure page is always >= 1. Invalid values are clamped to 1. + this.page = Math.max(1, page); }, setOrganizations(organizations: Organization[]) { this.organizations = organizations; diff --git a/frontend/test/stores/event.spec.ts b/frontend/test/stores/event.spec.ts index 9b3af58d8..e8a685b6d 100644 --- a/frontend/test/stores/event.spec.ts +++ b/frontend/test/stores/event.spec.ts @@ -31,9 +31,9 @@ describe("useEventStore", () => { expect(store.filters).toEqual({}); }); - it("initializes with page 0", () => { + it("initializes with page 1", () => { const store = useEventStore(); - expect(store.page).toBe(0); + expect(store.page).toBe(1); }); }); @@ -213,19 +213,19 @@ describe("useEventStore", () => { expect(store.filters).toEqual({}); }); - it("can set page to 0", () => { + it("clamps page to 1 when setting to 0", () => { const store = useEventStore(); store.setPage(5); store.setPage(0); - expect(store.page).toBe(0); - expect(store.getPage()).toBe(0); + expect(store.page).toBe(1); + expect(store.getPage()).toBe(1); }); - it("can set page to negative number", () => { + it("clamps page to 1 when setting to negative number", () => { const store = useEventStore(); store.setPage(-1); - expect(store.page).toBe(-1); - expect(store.getPage()).toBe(-1); + expect(store.page).toBe(1); + expect(store.getPage()).toBe(1); }); it("can set page to large number", () => { const store = useEventStore(); diff --git a/frontend/test/stores/organization.spec.ts b/frontend/test/stores/organization.spec.ts index 8198d84f8..a8d28c795 100644 --- a/frontend/test/stores/organization.spec.ts +++ b/frontend/test/stores/organization.spec.ts @@ -40,9 +40,9 @@ describe("useOrganizationStore", () => { expect(store.filters).toEqual({}); }); - it("initializes with page 0", () => { + it("initializes with page 1", () => { const store = useOrganizationStore(); - expect(store.page).toBe(0); + expect(store.page).toBe(1); }); }); @@ -262,12 +262,19 @@ describe("useOrganizationStore", () => { expect(store.images).toEqual([]); expect(store.getImages()).toHaveLength(0); }); - it("can set page to 0", () => { + it("clamps page to 1 when setting to 0", () => { const store = useOrganizationStore(); store.setPage(5); store.setPage(0); - expect(store.page).toBe(0); - expect(store.getPage()).toBe(0); + expect(store.page).toBe(1); + expect(store.getPage()).toBe(1); + }); + + it("clamps page to 1 when setting to negative number", () => { + const store = useOrganizationStore(); + store.setPage(-1); + expect(store.page).toBe(1); + expect(store.getPage()).toBe(1); }); }); }); From e49a6e1b4594d4e67b6f813f8cfc02fcdb2377d1 Mon Sep 17 00:00:00 2001 From: Aasim Syed Date: Sat, 29 Nov 2025 10:42:48 -0600 Subject: [PATCH 039/243] test: Add comprehensive test coverage for FormTextEntity component (#1769) * test: add comprehensive unit tests for FormTextEntity component Add comprehensive unit tests covering: - Logic (text validation, change events, dynamic props) - Style coverage (class changes, dynamic styling) - Accessibility (ARIA attributes, focus handling) - Edge cases and incorrect prop usage - Form data initialization All 56 tests passing. Addresses issue #1759. * fix: correct FormTextEntity validation bugs and improve error messages - Fix max length error message showing 25000 instead of 2500 characters - Ensure custom error messages override vee-validate's built-in 'Required' message - Improve URL validation to treat whitespace-only URLs as empty - Update tests to expect correct validation behavior * test: add edge case coverage for FormTextEntity - Test description with only whitespace fails validation - Test URL without protocol is rejected - Enhance handleSubmit payload verification to check complete structure - Verify optional fields are undefined when not provided * refactor: use Zod's native min/max with transform for description validation Replace refine() calls with Zod's built-in min() and max() methods. Trim the value first using transform() before applying validation, as suggested in code review. Addresses review feedback on PR #1769. --- .../app/components/form/FormTextEntity.vue | 34 +- .../components/form/FormTextEntity.spec.ts | 1527 +++++++++++++++-- 2 files changed, 1437 insertions(+), 124 deletions(-) diff --git a/frontend/app/components/form/FormTextEntity.vue b/frontend/app/components/form/FormTextEntity.vue index 22fe88378..43ce836f8 100644 --- a/frontend/app/components/form/FormTextEntity.vue +++ b/frontend/app/components/form/FormTextEntity.vue @@ -80,12 +80,18 @@ const schema = z .object({ description: z .string() - .min(1, t("i18n.components.form_text_entity.description_required")) - .max( - 2500, - t("i18n.components.form_text_entity.max_text_length", { - max_text_length: 25000, - }) + .default("") + .transform((val) => val.trim()) + .pipe( + z + .string() + .min(1, t("i18n.components.form_text_entity.description_required")) + .max( + 2500, + t("i18n.components.form_text_entity.max_text_length", { + max_text_length: 2500, + }) + ) ), getInvolved: z .string() @@ -99,9 +105,19 @@ const schema = z getInvolvedUrl: z .string() .optional() - .refine((value) => !value || z.string().url().safeParse(value).success, { - message: t("i18n.components.form._global.valid_url_required"), - }), + .refine( + (value) => { + if (!value) return true; + const trimmed = value.trim(); + // Treat whitespace-only as empty (no URL validation needed) + if (trimmed.length === 0) return true; + // Validate trimmed URL + return z.string().url().safeParse(trimmed).success; + }, + { + message: t("i18n.components.form._global.valid_url_required"), + } + ), }) .superRefine(({ getInvolved, getInvolvedUrl }, ctx) => { const hasGetInvolvedText = getInvolved && getInvolved.trim().length > 0; diff --git a/frontend/test/components/form/FormTextEntity.spec.ts b/frontend/test/components/form/FormTextEntity.spec.ts index 757b90795..0ddb6d674 100644 --- a/frontend/test/components/form/FormTextEntity.spec.ts +++ b/frontend/test/components/form/FormTextEntity.spec.ts @@ -1,11 +1,24 @@ // SPDX-License-Identifier: AGPL-3.0-or-later -import { fireEvent, screen } from "@testing-library/vue"; +import { fireEvent, screen, waitFor } from "@testing-library/vue"; import { describe, expect, it, vi } from "vitest"; +import type { OrganizationUpdateTextFormData } from "../../../shared/types/organization"; + import FormTextEntity from "../../../app/components/form/FormTextEntity.vue"; import { getEnglishText } from "../../../shared/utils/i18n"; import render from "../../../test/render"; +/** + * Comprehensive unit tests for FormTextEntity component + * + * Coverage includes: + * - Logic (Text validation, change events, dynamic props) + * - Style coverage (class changes, dynamic styling) + * - Accessibility (ARIA attributes, focus handling) + * - Edge cases and incorrect prop usage + * - Reference to frontend/app/assets/css/tailwind.css for style verification + */ + // Note: Auto-import mocks (useI18n, etc.) are handled globally in test/setup.ts. describe("FormTextEntity component", () => { @@ -19,164 +32,1448 @@ describe("FormTextEntity component", () => { "i18n.components.modal_text_organization.join_organization_link", }; - it("shows validation error when neither getInvolved text nor URL is provided", async () => { - await render(FormTextEntity, { - props: defaultProps, + // MARK: Basic Rendering + + describe("Basic Rendering", () => { + it("renders with required props", async () => { + await render(FormTextEntity, { + props: defaultProps, + }); + + const descriptionInput = screen.getByLabelText( + getEnglishText("i18n._global.description") + ); + expect(descriptionInput).toBeDefined(); + + const getInvolvedInput = screen.getByLabelText( + getEnglishText("i18n.components._global.get_involved") + ); + expect(getInvolvedInput).toBeDefined(); + + const getInvolvedUrlInput = screen.getByLabelText( + getEnglishText( + "i18n.components.modal_text_organization.join_organization_link" + ) + ); + expect(getInvolvedUrlInput).toBeDefined(); + }); + + it("renders title when title prop is provided", async () => { + await render(FormTextEntity, { + props: { + ...defaultProps, + title: + "i18n.components.modal_text_organization.edit_organization_texts", + }, + }); + + const title = screen.getByRole("heading", { level: 2 }); + expect(title).toBeDefined(); + expect(title.textContent).toContain( + getEnglishText( + "i18n.components.modal_text_organization.edit_organization_texts" + ) + ); + }); + + it("does not render title when title prop is not provided", async () => { + await render(FormTextEntity, { + props: defaultProps, + }); + + const title = screen.queryByRole("heading", { level: 2 }); + expect(title).toBeNull(); + }); + + it("renders submit button with translated label", async () => { + await render(FormTextEntity, { + props: defaultProps, + }); + + const submitBtn = screen.getByRole("button", { + name: "Submit the form", + }); + expect(submitBtn).toBeDefined(); + }); + }); + + // MARK: Logic - Text Validation + + describe("Logic - Text Validation", () => { + describe("Description Validation", () => { + it("shows validation error when description is empty", async () => { + await render(FormTextEntity, { + props: defaultProps, + }); + + const submitBtn = screen.getByRole("button", { + name: "Submit the form", + }); + await fireEvent.click(submitBtn); + + const descriptionError = await screen.findByTestId( + "form-item-description-error" + ); + // Schema uses .refine() to ensure custom error message is always displayed + expect(descriptionError.textContent).toBe( + getEnglishText( + "i18n.components.form_text_entity.description_required" + ) + ); + }); + + it("allows valid description", async () => { + await render(FormTextEntity, { + props: defaultProps, + }); + + const descriptionInput = screen.getByLabelText( + getEnglishText("i18n._global.description") + ); + await fireEvent.update(descriptionInput, "Valid description"); + + const getInvolvedInput = screen.getByLabelText( + getEnglishText("i18n.components._global.get_involved") + ); + await fireEvent.update(getInvolvedInput, "Join us"); + + const submitBtn = screen.getByRole("button", { + name: "Submit the form", + }); + await fireEvent.click(submitBtn); + + expect(screen.queryByTestId("form-item-description-error")).toBeNull(); + }); + + it("shows validation error when description is only whitespace", async () => { + await render(FormTextEntity, { + props: defaultProps, + }); + + const descriptionInput = screen.getByLabelText( + getEnglishText("i18n._global.description") + ); + await fireEvent.update(descriptionInput, " "); + + const submitBtn = screen.getByRole("button", { + name: "Submit the form", + }); + await fireEvent.click(submitBtn); + + const descriptionError = await screen.findByTestId( + "form-item-description-error" + ); + expect(descriptionError.textContent).toBe( + getEnglishText( + "i18n.components.form_text_entity.description_required" + ) + ); + }); + + it("shows validation error when description exceeds max length (2500 characters)", async () => { + await render(FormTextEntity, { + props: defaultProps, + }); + + const descriptionInput = screen.getByLabelText( + getEnglishText("i18n._global.description") + ); + const longDescription = "a".repeat(2501); + await fireEvent.update(descriptionInput, longDescription); + + const getInvolvedInput = screen.getByLabelText( + getEnglishText("i18n.components._global.get_involved") + ); + await fireEvent.update(getInvolvedInput, "Join us"); + + const submitBtn = screen.getByRole("button", { + name: "Submit the form", + }); + await fireEvent.click(submitBtn); + + const descriptionError = await screen.findByTestId( + "form-item-description-error" + ); + // Error message is interpolated with actual number + expect(descriptionError.textContent).toContain("2500"); + expect(descriptionError.textContent).toContain("characters"); + }); + + it("allows description at max length (2500 characters)", async () => { + await render(FormTextEntity, { + props: defaultProps, + }); + + const descriptionInput = screen.getByLabelText( + getEnglishText("i18n._global.description") + ); + const maxLengthDescription = "a".repeat(2500); + await fireEvent.update(descriptionInput, maxLengthDescription); + + const getInvolvedInput = screen.getByLabelText( + getEnglishText("i18n.components._global.get_involved") + ); + await fireEvent.update(getInvolvedInput, "Join us"); + + const submitBtn = screen.getByRole("button", { + name: "Submit the form", + }); + await fireEvent.click(submitBtn); + + expect(screen.queryByTestId("form-item-description-error")).toBeNull(); + }); + }); + + describe("GetInvolved Validation", () => { + it("shows validation error when getInvolved exceeds max length (500 characters)", async () => { + await render(FormTextEntity, { + props: defaultProps, + }); + + const descriptionInput = screen.getByLabelText( + getEnglishText("i18n._global.description") + ); + await fireEvent.update(descriptionInput, "Valid description"); + + const getInvolvedInput = screen.getByLabelText( + getEnglishText("i18n.components._global.get_involved") + ); + const longGetInvolved = "a".repeat(501); + await fireEvent.update(getInvolvedInput, longGetInvolved); + + const submitBtn = screen.getByRole("button", { + name: "Submit the form", + }); + await fireEvent.click(submitBtn); + + const getInvolvedError = await screen.findByTestId( + "form-item-getInvolved-error" + ); + // Error message is interpolated with actual number + expect(getInvolvedError.textContent).toContain("500"); + expect(getInvolvedError.textContent).toContain("characters"); + }); + + it("allows getInvolved at max length (500 characters)", async () => { + await render(FormTextEntity, { + props: defaultProps, + }); + + const descriptionInput = screen.getByLabelText( + getEnglishText("i18n._global.description") + ); + await fireEvent.update(descriptionInput, "Valid description"); + + const getInvolvedInput = screen.getByLabelText( + getEnglishText("i18n.components._global.get_involved") + ); + const maxLengthGetInvolved = "a".repeat(500); + await fireEvent.update(getInvolvedInput, maxLengthGetInvolved); + + const submitBtn = screen.getByRole("button", { + name: "Submit the form", + }); + await fireEvent.click(submitBtn); + + expect(screen.queryByTestId("form-item-getInvolved-error")).toBeNull(); + }); + }); + + describe("GetInvolved URL Validation", () => { + it("shows validation error for invalid URL format", async () => { + await render(FormTextEntity, { + props: defaultProps, + }); + + const descriptionInput = screen.getByLabelText( + getEnglishText("i18n._global.description") + ); + await fireEvent.update(descriptionInput, "Valid description"); + + const getInvolvedUrlInput = screen.getByLabelText( + getEnglishText( + "i18n.components.modal_text_organization.join_organization_link" + ) + ); + await fireEvent.update(getInvolvedUrlInput, "not-a-valid-url"); + + const submitBtn = screen.getByRole("button", { + name: "Submit the form", + }); + await fireEvent.click(submitBtn); + + const getInvolvedUrlError = await screen.findByTestId( + "form-item-getInvolvedUrl-error" + ); + expect(getInvolvedUrlError.textContent).toBe( + getEnglishText("i18n.components.form._global.valid_url_required") + ); + }); + + it("accepts valid HTTP URL", async () => { + await render(FormTextEntity, { + props: defaultProps, + }); + + const descriptionInput = screen.getByLabelText( + getEnglishText("i18n._global.description") + ); + await fireEvent.update(descriptionInput, "Valid description"); + + const getInvolvedUrlInput = screen.getByLabelText( + getEnglishText( + "i18n.components.modal_text_organization.join_organization_link" + ) + ); + await fireEvent.update(getInvolvedUrlInput, "http://example.com"); + + const submitBtn = screen.getByRole("button", { + name: "Submit the form", + }); + await fireEvent.click(submitBtn); + + expect( + screen.queryByTestId("form-item-getInvolvedUrl-error") + ).toBeNull(); + }); + + it("accepts valid HTTPS URL", async () => { + await render(FormTextEntity, { + props: defaultProps, + }); + + const descriptionInput = screen.getByLabelText( + getEnglishText("i18n._global.description") + ); + await fireEvent.update(descriptionInput, "Valid description"); + + const getInvolvedUrlInput = screen.getByLabelText( + getEnglishText( + "i18n.components.modal_text_organization.join_organization_link" + ) + ); + await fireEvent.update(getInvolvedUrlInput, "https://example.com"); + + const submitBtn = screen.getByRole("button", { + name: "Submit the form", + }); + await fireEvent.click(submitBtn); + + expect( + screen.queryByTestId("form-item-getInvolvedUrl-error") + ).toBeNull(); + }); + + it("accepts valid URL with path", async () => { + await render(FormTextEntity, { + props: defaultProps, + }); + + const descriptionInput = screen.getByLabelText( + getEnglishText("i18n._global.description") + ); + await fireEvent.update(descriptionInput, "Valid description"); + + const getInvolvedUrlInput = screen.getByLabelText( + getEnglishText( + "i18n.components.modal_text_organization.join_organization_link" + ) + ); + await fireEvent.update( + getInvolvedUrlInput, + "https://example.com/path/to/page" + ); + + const submitBtn = screen.getByRole("button", { + name: "Submit the form", + }); + await fireEvent.click(submitBtn); + + expect( + screen.queryByTestId("form-item-getInvolvedUrl-error") + ).toBeNull(); + }); + + it("shows validation error for URL without protocol", async () => { + await render(FormTextEntity, { + props: defaultProps, + }); + + const descriptionInput = screen.getByLabelText( + getEnglishText("i18n._global.description") + ); + await fireEvent.update(descriptionInput, "Valid description"); + + const getInvolvedUrlInput = screen.getByLabelText( + getEnglishText( + "i18n.components.modal_text_organization.join_organization_link" + ) + ); + await fireEvent.update(getInvolvedUrlInput, "example.com"); + + const submitBtn = screen.getByRole("button", { + name: "Submit the form", + }); + await fireEvent.click(submitBtn); + + const getInvolvedUrlError = await screen.findByTestId( + "form-item-getInvolvedUrl-error" + ); + expect(getInvolvedUrlError.textContent).toBe( + getEnglishText("i18n.components.form._global.valid_url_required") + ); + }); + }); + + describe("GetInvolved Text or URL Requirement", () => { + it("shows validation error when neither getInvolved text nor URL is provided", async () => { + await render(FormTextEntity, { + props: defaultProps, + }); + + const descriptionInput = screen.getByLabelText( + getEnglishText("i18n._global.description") + ); + await fireEvent.update(descriptionInput, "Test description"); + + const submitBtn = screen.getByRole("button", { + name: "Submit the form", + }); + await fireEvent.click(submitBtn); + + const getInvolvedError = await screen.findByTestId( + "form-item-getInvolved-error" + ); + const getInvolvedUrlError = await screen.findByTestId( + "form-item-getInvolvedUrl-error" + ); + + expect(getInvolvedError.textContent).toBe( + getEnglishText( + "i18n.components.form_text_entity.get_involved_text_or_url_required" + ) + ); + expect(getInvolvedUrlError.textContent).toBe( + getEnglishText( + "i18n.components.form_text_entity.get_involved_text_or_url_required" + ) + ); + }); + + it("allows submission when getInvolved text is provided", async () => { + await render(FormTextEntity, { + props: defaultProps, + }); + + const descriptionInput = screen.getByLabelText( + getEnglishText("i18n._global.description") + ); + await fireEvent.update(descriptionInput, "Test description"); + + const getInvolvedInput = screen.getByLabelText( + getEnglishText("i18n.components._global.get_involved") + ); + await fireEvent.update(getInvolvedInput, "Join our events"); + + const submitBtn = screen.getByRole("button", { + name: "Submit the form", + }); + await fireEvent.click(submitBtn); + + expect(screen.queryByTestId("form-item-getInvolved-error")).toBeNull(); + expect( + screen.queryByTestId("form-item-getInvolvedUrl-error") + ).toBeNull(); + }); + + it("allows submission when getInvolvedUrl is provided", async () => { + await render(FormTextEntity, { + props: defaultProps, + }); + + const descriptionInput = screen.getByLabelText( + getEnglishText("i18n._global.description") + ); + await fireEvent.update(descriptionInput, "Test description"); + + const getInvolvedUrlInput = screen.getByLabelText( + getEnglishText( + "i18n.components.modal_text_organization.join_organization_link" + ) + ); + await fireEvent.update(getInvolvedUrlInput, "https://example.com"); + + const submitBtn = screen.getByRole("button", { + name: "Submit the form", + }); + await fireEvent.click(submitBtn); + + expect(screen.queryByTestId("form-item-getInvolved-error")).toBeNull(); + expect( + screen.queryByTestId("form-item-getInvolvedUrl-error") + ).toBeNull(); + }); + + it("allows submission when both getInvolved text and URL are provided", async () => { + await render(FormTextEntity, { + props: defaultProps, + }); + + const descriptionInput = screen.getByLabelText( + getEnglishText("i18n._global.description") + ); + await fireEvent.update(descriptionInput, "Test description"); + + const getInvolvedInput = screen.getByLabelText( + getEnglishText("i18n.components._global.get_involved") + ); + await fireEvent.update(getInvolvedInput, "Join our events"); + + const getInvolvedUrlInput = screen.getByLabelText( + getEnglishText( + "i18n.components.modal_text_organization.join_organization_link" + ) + ); + await fireEvent.update(getInvolvedUrlInput, "https://example.com"); + + const submitBtn = screen.getByRole("button", { + name: "Submit the form", + }); + await fireEvent.click(submitBtn); + + expect(screen.queryByTestId("form-item-getInvolved-error")).toBeNull(); + expect( + screen.queryByTestId("form-item-getInvolvedUrl-error") + ).toBeNull(); + }); + + it("shows validation error when getInvolved text is only whitespace", async () => { + await render(FormTextEntity, { + props: defaultProps, + }); + + const descriptionInput = screen.getByLabelText( + getEnglishText("i18n._global.description") + ); + await fireEvent.update(descriptionInput, "Test description"); + + const getInvolvedInput = screen.getByLabelText( + getEnglishText("i18n.components._global.get_involved") + ); + await fireEvent.update(getInvolvedInput, " "); + + const submitBtn = screen.getByRole("button", { + name: "Submit the form", + }); + await fireEvent.click(submitBtn); + + const getInvolvedError = await screen.findByTestId( + "form-item-getInvolved-error" + ); + const getInvolvedUrlError = await screen.findByTestId( + "form-item-getInvolvedUrl-error" + ); + + expect(getInvolvedError.textContent).toBe( + getEnglishText( + "i18n.components.form_text_entity.get_involved_text_or_url_required" + ) + ); + expect(getInvolvedUrlError.textContent).toBe( + getEnglishText( + "i18n.components.form_text_entity.get_involved_text_or_url_required" + ) + ); + }); + + it("shows validation error when getInvolvedUrl is only whitespace", async () => { + await render(FormTextEntity, { + props: defaultProps, + }); + + const descriptionInput = screen.getByLabelText( + getEnglishText("i18n._global.description") + ); + await fireEvent.update(descriptionInput, "Test description"); + + const getInvolvedUrlInput = screen.getByLabelText( + getEnglishText( + "i18n.components.modal_text_organization.join_organization_link" + ) + ); + await fireEvent.update(getInvolvedUrlInput, " "); + + const submitBtn = screen.getByRole("button", { + name: "Submit the form", + }); + await fireEvent.click(submitBtn); + + // Whitespace-only URLs are treated as empty, so they trigger the "text or URL required" error + const getInvolvedError = await screen.findByTestId( + "form-item-getInvolved-error" + ); + const getInvolvedUrlError = await screen.findByTestId( + "form-item-getInvolvedUrl-error" + ); + + expect(getInvolvedError.textContent).toBe( + getEnglishText( + "i18n.components.form_text_entity.get_involved_text_or_url_required" + ) + ); + // Whitespace-only URLs are treated as empty (not invalid URLs) + expect(getInvolvedUrlError.textContent).toBe( + getEnglishText( + "i18n.components.form_text_entity.get_involved_text_or_url_required" + ) + ); + }); + }); + }); + + // MARK: Logic - Change Events + + describe("Logic - Change Events", () => { + it("updates description field on input", async () => { + await render(FormTextEntity, { + props: defaultProps, + }); + + const descriptionInput = screen.getByLabelText( + getEnglishText("i18n._global.description") + ) as HTMLTextAreaElement; + + await fireEvent.update(descriptionInput, "New description"); + expect(descriptionInput.value).toBe("New description"); + }); + + it("updates getInvolved field on input", async () => { + await render(FormTextEntity, { + props: defaultProps, + }); + + const getInvolvedInput = screen.getByLabelText( + getEnglishText("i18n.components._global.get_involved") + ) as HTMLTextAreaElement; + + await fireEvent.update(getInvolvedInput, "New get involved text"); + expect(getInvolvedInput.value).toBe("New get involved text"); + }); + + it("updates getInvolvedUrl field on input", async () => { + await render(FormTextEntity, { + props: defaultProps, + }); + + const getInvolvedUrlInput = screen.getByLabelText( + getEnglishText( + "i18n.components.modal_text_organization.join_organization_link" + ) + ) as HTMLInputElement; + + await fireEvent.update(getInvolvedUrlInput, "https://new-url.com"); + expect(getInvolvedUrlInput.value).toBe("https://new-url.com"); + }); + + it("clears validation errors when fields become valid", async () => { + await render(FormTextEntity, { + props: defaultProps, + }); + + const submitBtn = screen.getByRole("button", { + name: "Submit the form", + }); + await fireEvent.click(submitBtn); + + const descriptionError = await screen.findByTestId( + "form-item-description-error" + ); + expect(descriptionError).toBeDefined(); + + const descriptionInput = screen.getByLabelText( + getEnglishText("i18n._global.description") + ); + await fireEvent.update(descriptionInput, "Valid description"); + + const getInvolvedInput = screen.getByLabelText( + getEnglishText("i18n.components._global.get_involved") + ); + await fireEvent.update(getInvolvedInput, "Join us"); + + await waitFor(() => { + expect(screen.queryByTestId("form-item-description-error")).toBeNull(); + }); }); + }); + + // MARK: Logic - Dynamic Props + + describe("Logic - Dynamic Props", () => { + it("renders rememberHttpsLabel when provided", async () => { + await render(FormTextEntity, { + props: { + ...defaultProps, + rememberHttpsLabel: + "i18n.components.modal.text._global.remember_https", + }, + }); - const descriptionInput = screen.getByLabelText( - getEnglishText("i18n._global.description") - ); - await fireEvent.update(descriptionInput, "Test description"); + const getInvolvedUrlInput = screen.getByLabelText( + getEnglishText( + "i18n.components.modal_text_organization.join_organization_link" + ) + ); + const container = getInvolvedUrlInput.closest(".primary-text"); + const label = container?.querySelector("label"); - const submitBtn = screen.getByRole("button", { - name: "Submit the form", + // The label should include the rememberHttpsLabel text + expect(label?.textContent).toContain( + getEnglishText("i18n.components.modal.text._global.remember_https") + ); }); - await fireEvent.click(submitBtn); + it("does not render rememberHttpsLabel when not provided", async () => { + await render(FormTextEntity, { + props: defaultProps, + }); + + // FormItem's label (accessible label) should still be present + const formLabel = document.querySelector( + 'label[for="form-item-getInvolvedUrl"].text-base.font-semibold' + ); + expect(formLabel?.textContent).toBe( + getEnglishText( + "i18n.components.modal_text_organization.join_organization_link" + ) + ); + + // FormTextInput's floating label should be empty when rememberHttpsLabel is not provided + const getInvolvedUrlInput = screen.getByLabelText( + getEnglishText( + "i18n.components.modal_text_organization.join_organization_link" + ) + ); + const container = getInvolvedUrlInput.closest(".primary-text"); + const floatingLabel = container?.querySelector("label"); - const getInvolvedError = await screen.findByTestId( - "form-item-getInvolved-error" - ); - const getInvolvedUrlError = await screen.findByTestId( - "form-item-getInvolvedUrl-error" - ); + // When rememberHttpsLabel is not provided, label prop is empty string + // The floating label will be empty when there's no value + expect(floatingLabel?.textContent).toBe(""); - expect(getInvolvedError.textContent).toBe( - getEnglishText( - "i18n.components.form_text_entity.get_involved_text_or_url_required" - ) - ); - expect(getInvolvedUrlError.textContent).toBe( - getEnglishText( - "i18n.components.form_text_entity.get_involved_text_or_url_required" - ) - ); + // The accessible label comes from FormItem's FormLabel, which is still present + expect(getInvolvedUrlInput).toBeDefined(); + }); + + it("uses custom getInvolvedLabel prop", async () => { + await render(FormTextEntity, { + props: { + ...defaultProps, + getInvolvedLabel: "i18n.components._global.participate", + }, + }); + + const getInvolvedInput = screen.getByLabelText( + getEnglishText("i18n.components._global.participate") + ); + expect(getInvolvedInput).toBeDefined(); + }); + + it("uses custom getInvolvedUrlLabel prop", async () => { + await render(FormTextEntity, { + props: { + ...defaultProps, + getInvolvedUrlLabel: + "i18n.components.modal_text_event.offer_to_help_link", + }, + }); + + const getInvolvedUrlInput = screen.getByLabelText( + getEnglishText("i18n.components.modal_text_event.offer_to_help_link") + ); + expect(getInvolvedUrlInput).toBeDefined(); + }); }); - it("allows submission when getInvolved text is provided", async () => { - await render(FormTextEntity, { - props: defaultProps, + // MARK: Logic - Form Data Initialization + + describe("Logic - Form Data Initialization", () => { + it("initializes form with formData prop", async () => { + const formData = { + description: "Initial description", + getInvolved: "Initial get involved text", + getInvolvedUrl: "https://initial-url.com", + }; + + await render(FormTextEntity, { + props: { + ...defaultProps, + formData: formData as OrganizationUpdateTextFormData, + }, + }); + + const descriptionInput = screen.getByLabelText( + getEnglishText("i18n._global.description") + ) as HTMLTextAreaElement; + expect(descriptionInput.value).toBe("Initial description"); + + const getInvolvedInput = screen.getByLabelText( + getEnglishText("i18n.components._global.get_involved") + ) as HTMLTextAreaElement; + expect(getInvolvedInput.value).toBe("Initial get involved text"); + + const getInvolvedUrlInput = screen.getByLabelText( + getEnglishText( + "i18n.components.modal_text_organization.join_organization_link" + ) + ) as HTMLInputElement; + expect(getInvolvedUrlInput.value).toBe("https://initial-url.com"); }); - // Fill in thr required fields. - const descriptionInput = screen.getByLabelText( - getEnglishText("i18n._global.description") - ); - await fireEvent.update(descriptionInput, "Test description"); + it("handles empty formData prop", async () => { + await render(FormTextEntity, { + props: { + ...defaultProps, + formData: { + description: "", + getInvolved: "", + getInvolvedUrl: "", + } as OrganizationUpdateTextFormData, + }, + }); - const getInvolvedInput = screen.getByLabelText( - getEnglishText("i18n.components._global.get_involved") - ); - await fireEvent.update(getInvolvedInput, "Join our events"); + const descriptionInput = screen.getByLabelText( + getEnglishText("i18n._global.description") + ) as HTMLTextAreaElement; + expect(descriptionInput.value).toBe(""); - const submitBtn = screen.getByRole("button", { - name: "Submit the form", + const getInvolvedInput = screen.getByLabelText( + getEnglishText("i18n.components._global.get_involved") + ) as HTMLTextAreaElement; + expect(getInvolvedInput.value).toBe(""); + + const getInvolvedUrlInput = screen.getByLabelText( + getEnglishText( + "i18n.components.modal_text_organization.join_organization_link" + ) + ) as HTMLInputElement; + expect(getInvolvedUrlInput.value).toBe(""); + }); + + it("handles undefined formData prop", async () => { + await render(FormTextEntity, { + props: defaultProps, + }); + + const descriptionInput = screen.getByLabelText( + getEnglishText("i18n._global.description") + ) as HTMLTextAreaElement; + expect(descriptionInput.value).toBe(""); + + const getInvolvedInput = screen.getByLabelText( + getEnglishText("i18n.components._global.get_involved") + ) as HTMLTextAreaElement; + expect(getInvolvedInput.value).toBe(""); + + const getInvolvedUrlInput = screen.getByLabelText( + getEnglishText( + "i18n.components.modal_text_organization.join_organization_link" + ) + ) as HTMLInputElement; + expect(getInvolvedUrlInput.value).toBe(""); }); - await fireEvent.click(submitBtn); + it("handles formData with partial fields", async () => { + const formData = { + description: "Only description", + getInvolved: "", + getInvolvedUrl: "", + }; + + await render(FormTextEntity, { + props: { + ...defaultProps, + formData: formData as OrganizationUpdateTextFormData, + }, + }); + + const descriptionInput = screen.getByLabelText( + getEnglishText("i18n._global.description") + ) as HTMLTextAreaElement; + expect(descriptionInput.value).toBe("Only description"); - // Shouldn't show validation errors. - expect(screen.queryByTestId("form-item-getInvolved-error")).toBeNull(); - expect(screen.queryByTestId("form-item-getInvolvedUrl-error")).toBeNull(); + const getInvolvedInput = screen.getByLabelText( + getEnglishText("i18n.components._global.get_involved") + ) as HTMLTextAreaElement; + expect(getInvolvedInput.value).toBe(""); + + const getInvolvedUrlInput = screen.getByLabelText( + getEnglishText( + "i18n.components.modal_text_organization.join_organization_link" + ) + ) as HTMLInputElement; + expect(getInvolvedUrlInput.value).toBe(""); + }); }); - it("allows submission when getInvolvedUrl is provided", async () => { - await render(FormTextEntity, { - props: defaultProps, + // MARK: Style Coverage + + describe("Style Coverage", () => { + it("applies flex flex-col space-y-7 classes to form fields container", async () => { + await render(FormTextEntity, { + props: defaultProps, + }); + + const formContainer = document.querySelector(".flex.flex-col.space-y-7"); + expect(formContainer).toBeDefined(); + }); + + it("applies error styling to description field when validation fails", async () => { + await render(FormTextEntity, { + props: defaultProps, + }); + + const submitBtn = screen.getByRole("button", { + name: "Submit the form", + }); + await fireEvent.click(submitBtn); + + await screen.findByTestId("form-item-description-error"); + + const descriptionInput = screen.getByLabelText( + getEnglishText("i18n._global.description") + ); + // FormTextArea applies border-action-red when hasError is true + expect(descriptionInput.className).toContain("border-action-red"); }); - const descriptionInput = screen.getByLabelText( - getEnglishText("i18n._global.description") - ); - await fireEvent.update(descriptionInput, "Test description"); + it("applies error styling to getInvolved field when validation fails", async () => { + await render(FormTextEntity, { + props: defaultProps, + }); + + const descriptionInput = screen.getByLabelText( + getEnglishText("i18n._global.description") + ); + await fireEvent.update(descriptionInput, "Valid description"); + + const submitBtn = screen.getByRole("button", { + name: "Submit the form", + }); + await fireEvent.click(submitBtn); - const getInvolvedUrlInput = screen.getByLabelText( - getEnglishText( - "i18n.components.modal_text_organization.join_organization_link" - ) - ); - await fireEvent.update(getInvolvedUrlInput, "https://example.com"); + await screen.findByTestId("form-item-getInvolved-error"); - const submitBtn = screen.getByRole("button", { - name: "Submit the form", + const getInvolvedInput = screen.getByLabelText( + getEnglishText("i18n.components._global.get_involved") + ); + expect(getInvolvedInput.className).toContain("border-action-red"); }); - await fireEvent.click(submitBtn); + it("applies error styling to getInvolvedUrl field when validation fails", async () => { + await render(FormTextEntity, { + props: defaultProps, + }); + + const descriptionInput = screen.getByLabelText( + getEnglishText("i18n._global.description") + ); + await fireEvent.update(descriptionInput, "Valid description"); + + const getInvolvedUrlInput = screen.getByLabelText( + getEnglishText( + "i18n.components.modal_text_organization.join_organization_link" + ) + ); + await fireEvent.update(getInvolvedUrlInput, "invalid-url"); + + const submitBtn = screen.getByRole("button", { + name: "Submit the form", + }); + await fireEvent.click(submitBtn); + + await screen.findByTestId("form-item-getInvolvedUrl-error"); + + // FormTextInput applies border-action-red via fieldset when hasError is true + const container = getInvolvedUrlInput.closest(".primary-text"); + const fieldset = container?.querySelector("fieldset"); + expect(fieldset?.className).toContain("border-action-red"); + }); - expect(screen.queryByTestId("form-item-getInvolved-error")).toBeNull(); - expect(screen.queryByTestId("form-item-getInvolvedUrl-error")).toBeNull(); + it("removes error styling when field becomes valid", async () => { + await render(FormTextEntity, { + props: defaultProps, + }); + + const submitBtn = screen.getByRole("button", { + name: "Submit the form", + }); + await fireEvent.click(submitBtn); + + await screen.findByTestId("form-item-description-error"); + + const descriptionInput = screen.getByLabelText( + getEnglishText("i18n._global.description") + ); + expect(descriptionInput.className).toContain("border-action-red"); + + await fireEvent.update(descriptionInput, "Valid description"); + + const getInvolvedInput = screen.getByLabelText( + getEnglishText("i18n.components._global.get_involved") + ); + await fireEvent.update(getInvolvedInput, "Join us"); + + await waitFor(() => { + expect(screen.queryByTestId("form-item-description-error")).toBeNull(); + }); + + // Error styling should be removed + await waitFor(() => { + expect(descriptionInput.className).not.toContain("border-action-red"); + }); + }); }); - it("allows submission when both getInvolved text and URL are provided", async () => { - await render(FormTextEntity, { - props: defaultProps, + // MARK: Accessibility + + describe("Accessibility", () => { + it("associates label with description field via id and for attributes", async () => { + await render(FormTextEntity, { + props: defaultProps, + }); + + const descriptionInput = screen.getByLabelText( + getEnglishText("i18n._global.description") + ); + const label = document.querySelector( + `label[for="${descriptionInput.id}"]` + ); + + expect(label).toBeDefined(); + expect(label?.textContent).toContain( + getEnglishText("i18n._global.description") + ); + }); + + it("shows required indicator for description field", async () => { + await render(FormTextEntity, { + props: defaultProps, + }); + + // Required indicator is in a separate span element (not in label text) + const requiredIndicators = screen.getAllByText("*"); + expect(requiredIndicators.length).toBeGreaterThan(0); + + // Check that the required indicator is associated with description field + // The FormLabel component renders the asterisk in a separate span + const descriptionContainer = document.querySelector( + 'div[name="description"]' + ); + const requiredIndicator = descriptionContainer?.querySelector( + "span.text-action-red" + ); + expect(requiredIndicator).toBeDefined(); + expect(requiredIndicator?.textContent).toBe("*"); + + // Verify the label itself doesn't contain the asterisk (it's in a sibling span) + const descriptionLabel = descriptionContainer?.querySelector( + "label.text-base.font-semibold" + ); + expect(descriptionLabel?.textContent).not.toContain("*"); + expect(descriptionLabel?.textContent).toBe( + getEnglishText("i18n._global.description") + ); + }); + + it("does not show required indicator for optional fields", async () => { + await render(FormTextEntity, { + props: defaultProps, + }); + + const requiredIndicators = screen.getAllByText("*"); + // Only description should be required + expect(requiredIndicators.length).toBe(1); + }); + + it("associates label with getInvolved field via id and for attributes", async () => { + await render(FormTextEntity, { + props: defaultProps, + }); + + const getInvolvedInput = screen.getByLabelText( + getEnglishText("i18n.components._global.get_involved") + ); + const label = document.querySelector( + `label[for="${getInvolvedInput.id}"]` + ); + + expect(label).toBeDefined(); + expect(label?.textContent).toContain( + getEnglishText("i18n.components._global.get_involved") + ); + }); + + it("associates label with getInvolvedUrl field via id and for attributes", async () => { + await render(FormTextEntity, { + props: defaultProps, + }); + + const getInvolvedUrlInput = screen.getByLabelText( + getEnglishText( + "i18n.components.modal_text_organization.join_organization_link" + ) + ); + + // FormTextInput uses a floating label that's empty when there's no value + // The accessible label comes from FormItem's FormLabel component + const formLabel = document.querySelector( + `label[for="${getInvolvedUrlInput.id}"].text-base.font-semibold` + ); + + expect(getInvolvedUrlInput).toBeDefined(); + expect(getInvolvedUrlInput.id).toBe("form-item-getInvolvedUrl"); + // The FormLabel from FormItem provides the accessible label + expect(formLabel?.textContent).toBe( + getEnglishText( + "i18n.components.modal_text_organization.join_organization_link" + ) + ); }); - const descriptionInput = screen.getByLabelText( - getEnglishText("i18n._global.description") - ); - await fireEvent.update(descriptionInput, "Test description"); + it("has textbox role on description field", async () => { + await render(FormTextEntity, { + props: defaultProps, + }); - const getInvolvedInput = screen.getByLabelText( - getEnglishText("i18n.components._global.get_involved") - ); - await fireEvent.update(getInvolvedInput, "Join our events"); + const descriptionInput = screen.getByLabelText( + getEnglishText("i18n._global.description") + ); + expect(descriptionInput.getAttribute("role")).toBe("textbox"); + }); - const getInvolvedUrlInput = screen.getByLabelText( - getEnglishText( - "i18n.components.modal_text_organization.join_organization_link" - ) - ); - await fireEvent.update(getInvolvedUrlInput, "https://example.com"); + it("has textbox role on getInvolved field", async () => { + await render(FormTextEntity, { + props: defaultProps, + }); - const submitBtn = screen.getByRole("button", { - name: "Submit the form", + const getInvolvedInput = screen.getByLabelText( + getEnglishText("i18n.components._global.get_involved") + ); + expect(getInvolvedInput.getAttribute("role")).toBe("textbox"); }); - await fireEvent.click(submitBtn); + it("has textbox role on getInvolvedUrl field", async () => { + await render(FormTextEntity, { + props: defaultProps, + }); + + const getInvolvedUrlInput = screen.getByLabelText( + getEnglishText( + "i18n.components.modal_text_organization.join_organization_link" + ) + ); + expect(getInvolvedUrlInput.getAttribute("role")).toBe("textbox"); + }); + + it("provides error message with correct test id for accessibility", async () => { + await render(FormTextEntity, { + props: defaultProps, + }); + + const submitBtn = screen.getByRole("button", { + name: "Submit the form", + }); + await fireEvent.click(submitBtn); - expect(screen.queryByTestId("form-item-getInvolved-error")).toBeNull(); - expect(screen.queryByTestId("form-item-getInvolvedUrl-error")).toBeNull(); + const descriptionError = await screen.findByTestId( + "form-item-description-error" + ); + expect(descriptionError).toBeDefined(); + expect(descriptionError.id).toBe("form-item-description-error"); + }); + + it("maintains focus order for form fields", async () => { + await render(FormTextEntity, { + props: defaultProps, + }); + + const descriptionInput = screen.getByLabelText( + getEnglishText("i18n._global.description") + ) as HTMLElement; + const getInvolvedInput = screen.getByLabelText( + getEnglishText("i18n.components._global.get_involved") + ) as HTMLElement; + const getInvolvedUrlInput = screen.getByLabelText( + getEnglishText( + "i18n.components.modal_text_organization.join_organization_link" + ) + ) as HTMLElement; + + // Verify fields are accessible and can be found by their labels + // Form fields (textarea/input) are naturally focusable via keyboard navigation + expect(descriptionInput).toBeDefined(); + expect(getInvolvedInput).toBeDefined(); + expect(getInvolvedUrlInput).toBeDefined(); + + // Verify all fields are form controls that support keyboard navigation + expect(descriptionInput.tagName).toBe("TEXTAREA"); + expect(getInvolvedInput.tagName).toBe("TEXTAREA"); + expect(getInvolvedUrlInput.tagName).toBe("INPUT"); + + // Verify fields are not disabled (disabled elements have tabIndex of -1) + expect(descriptionInput.hasAttribute("disabled")).toBe(false); + expect(getInvolvedInput.hasAttribute("disabled")).toBe(false); + expect(getInvolvedUrlInput.hasAttribute("disabled")).toBe(false); + }); }); - it("shows validation error when getInvolved text is only whitespace", async () => { - await render(FormTextEntity, { - props: defaultProps, + // MARK: Edge Cases + + describe("Edge Cases", () => { + it("handles very long description input gracefully", async () => { + await render(FormTextEntity, { + props: defaultProps, + }); + + const descriptionInput = screen.getByLabelText( + getEnglishText("i18n._global.description") + ) as HTMLTextAreaElement; + const veryLongText = "a".repeat(10000); + await fireEvent.update(descriptionInput, veryLongText); + + expect(descriptionInput.value.length).toBe(10000); + }); + + it("handles special characters in description", async () => { + await render(FormTextEntity, { + props: defaultProps, + }); + + const descriptionInput = screen.getByLabelText( + getEnglishText("i18n._global.description") + ) as HTMLTextAreaElement; + const specialText = + "Description with special chars: @#$%^&*()[]{}|\\/<>?"; + await fireEvent.update(descriptionInput, specialText); + + expect(descriptionInput.value).toBe(specialText); + }); + + it("handles unicode characters in text fields", async () => { + await render(FormTextEntity, { + props: defaultProps, + }); + + const descriptionInput = screen.getByLabelText( + getEnglishText("i18n._global.description") + ) as HTMLTextAreaElement; + const unicodeText = "Description with unicode: 🌍 日本語 العربية"; + await fireEvent.update(descriptionInput, unicodeText); + + expect(descriptionInput.value).toBe(unicodeText); + }); + + it("handles URL with query parameters", async () => { + await render(FormTextEntity, { + props: defaultProps, + }); + + const descriptionInput = screen.getByLabelText( + getEnglishText("i18n._global.description") + ); + await fireEvent.update(descriptionInput, "Valid description"); + + const getInvolvedUrlInput = screen.getByLabelText( + getEnglishText( + "i18n.components.modal_text_organization.join_organization_link" + ) + ); + await fireEvent.update( + getInvolvedUrlInput, + "https://example.com?param=value&other=123" + ); + + const submitBtn = screen.getByRole("button", { + name: "Submit the form", + }); + await fireEvent.click(submitBtn); + + expect(screen.queryByTestId("form-item-getInvolvedUrl-error")).toBeNull(); }); - const descriptionInput = screen.getByLabelText( - getEnglishText("i18n._global.description") - ); - await fireEvent.update(descriptionInput, "Test description"); + it("handles URL with hash fragment", async () => { + await render(FormTextEntity, { + props: defaultProps, + }); - const getInvolvedInput = screen.getByLabelText( - getEnglishText("i18n.components._global.get_involved") - ); - await fireEvent.update(getInvolvedInput, " "); + const descriptionInput = screen.getByLabelText( + getEnglishText("i18n._global.description") + ); + await fireEvent.update(descriptionInput, "Valid description"); - const submitBtn = screen.getByRole("button", { - name: "Submit the form", + const getInvolvedUrlInput = screen.getByLabelText( + getEnglishText( + "i18n.components.modal_text_organization.join_organization_link" + ) + ); + await fireEvent.update( + getInvolvedUrlInput, + "https://example.com#section" + ); + + const submitBtn = screen.getByRole("button", { + name: "Submit the form", + }); + await fireEvent.click(submitBtn); + + expect(screen.queryByTestId("form-item-getInvolvedUrl-error")).toBeNull(); }); - await fireEvent.click(submitBtn); + it("handles empty strings in formData", async () => { + const formData = { + description: "", + getInvolved: "", + getInvolvedUrl: "", + }; + + await render(FormTextEntity, { + props: { + ...defaultProps, + formData: formData as OrganizationUpdateTextFormData, + }, + }); + + const descriptionInput = screen.getByLabelText( + getEnglishText("i18n._global.description") + ) as HTMLTextAreaElement; + expect(descriptionInput.value).toBe(""); + + const getInvolvedInput = screen.getByLabelText( + getEnglishText("i18n.components._global.get_involved") + ) as HTMLTextAreaElement; + expect(getInvolvedInput.value).toBe(""); + }); + + it("calls handleSubmit with correct values on successful submission", async () => { + mockHandleSubmit.mockResolvedValue(undefined); + + await render(FormTextEntity, { + props: defaultProps, + }); + + const descriptionInput = screen.getByLabelText( + getEnglishText("i18n._global.description") + ); + await fireEvent.update(descriptionInput, "Test description"); + + const getInvolvedInput = screen.getByLabelText( + getEnglishText("i18n.components._global.get_involved") + ); + await fireEvent.update(getInvolvedInput, "Join us"); + + const submitBtn = screen.getByRole("button", { + name: "Submit the form", + }); + await fireEvent.click(submitBtn); + + await waitFor(() => { + expect(mockHandleSubmit).toHaveBeenCalledTimes(1); + }); + + const callArgs = mockHandleSubmit.mock.calls[0]?.[0]; + // Verify all fields are included in the payload + expect(callArgs).toHaveProperty("description", "Test description"); + expect(callArgs).toHaveProperty("getInvolved", "Join us"); + expect(callArgs).toHaveProperty("getInvolvedUrl"); + // Verify the payload structure is complete + expect(Object.keys(callArgs)).toEqual([ + "description", + "getInvolved", + "getInvolvedUrl", + ]); + }); + + it("calls handleSubmit with all fields including optional fields as undefined when not provided", async () => { + mockHandleSubmit.mockResolvedValue(undefined); + + await render(FormTextEntity, { + props: defaultProps, + }); + + const descriptionInput = screen.getByLabelText( + getEnglishText("i18n._global.description") + ); + await fireEvent.update(descriptionInput, "Test description"); - const getInvolvedError = await screen.findByTestId( - "form-item-getInvolved-error" - ); - const getInvolvedUrlError = await screen.findByTestId( - "form-item-getInvolvedUrl-error" - ); + const getInvolvedInput = screen.getByLabelText( + getEnglishText("i18n.components._global.get_involved") + ); + await fireEvent.update(getInvolvedInput, "Join us"); - expect(getInvolvedError.textContent).toBe( - getEnglishText( - "i18n.components.form_text_entity.get_involved_text_or_url_required" - ) - ); - expect(getInvolvedUrlError.textContent).toBe( - getEnglishText( - "i18n.components.form_text_entity.get_involved_text_or_url_required" - ) - ); + const submitBtn = screen.getByRole("button", { + name: "Submit the form", + }); + await fireEvent.click(submitBtn); + + await waitFor(() => { + expect(mockHandleSubmit).toHaveBeenCalledTimes(1); + }); + + const callArgs = mockHandleSubmit.mock.calls[0]?.[0]; + // Verify optional field is undefined when not provided + expect(callArgs).toHaveProperty("description", "Test description"); + expect(callArgs).toHaveProperty("getInvolved", "Join us"); + expect(callArgs).toHaveProperty("getInvolvedUrl", undefined); + }); + + it("handles handleSubmit rejection gracefully", async () => { + mockHandleSubmit.mockRejectedValue(new Error("Submission failed")); + + await render(FormTextEntity, { + props: defaultProps, + }); + + const descriptionInput = screen.getByLabelText( + getEnglishText("i18n._global.description") + ); + await fireEvent.update(descriptionInput, "Test description"); + + const getInvolvedInput = screen.getByLabelText( + getEnglishText("i18n.components._global.get_involved") + ); + await fireEvent.update(getInvolvedInput, "Join us"); + + const submitBtn = screen.getByRole("button", { + name: "Submit the form", + }); + await fireEvent.click(submitBtn); + + await waitFor(() => { + expect(mockHandleSubmit).toHaveBeenCalledTimes(1); + }); + + // Form should still be visible after rejection + expect(descriptionInput).toBeDefined(); + }); + + it("handles multiple rapid validation attempts", async () => { + await render(FormTextEntity, { + props: defaultProps, + }); + + const submitBtn = screen.getByRole("button", { + name: "Submit the form", + }); + + // Submit multiple times rapidly + await fireEvent.click(submitBtn); + await fireEvent.click(submitBtn); + await fireEvent.click(submitBtn); + + const descriptionError = await screen.findByTestId( + "form-item-description-error" + ); + expect(descriptionError).toBeDefined(); + }); }); }); From b8570df4577e063b65ea3f3d46f34368e810629d Mon Sep 17 00:00:00 2001 From: Marvelous Ukuesa <112257437+MarvelousUkuesa@users.noreply.github.com> Date: Sat, 29 Nov 2025 18:06:52 +0100 Subject: [PATCH 040/243] =?UTF-8?q?Added=20FormSelectorCombobox.spec.ts=20?= =?UTF-8?q?under=20frontend/test/components/for=E2=80=A6=20(#1763)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Added FormSelectorCombobox.spec.ts under frontend/test/components/form/selector/ * test: sync commit email * cleanup * Fix for license header and component import * Minor mark and comment formatting * Move file for rename * Move file for rename - second move --------- Co-authored-by: Ukuesa Marvelous Co-authored-by: Andrew Tavis McAllister --- .../selector/FormSelectorCombobox.spec.ts | 165 ++++++++++++++++++ 1 file changed, 165 insertions(+) create mode 100644 frontend/test/components/form/selector/FormSelectorCombobox.spec.ts diff --git a/frontend/test/components/form/selector/FormSelectorCombobox.spec.ts b/frontend/test/components/form/selector/FormSelectorCombobox.spec.ts new file mode 100644 index 000000000..7c5f8c086 --- /dev/null +++ b/frontend/test/components/form/selector/FormSelectorCombobox.spec.ts @@ -0,0 +1,165 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +import { fireEvent, screen } from "@testing-library/vue"; +import { describe, expect, it } from "vitest"; + +import FormSelectorCombobox from "../../../../app/components/form/selector/FormSelectorCombobox.vue"; +import render from "../../../render"; + +const OPTIONS = [ + { id: 1, label: "Option one", value: "one" }, + { id: 2, label: "Option two", value: "two" }, + { id: 3, label: "Another option", value: "three" }, +]; + +const defaultProps = { + id: "topics", + label: "Topics", + options: OPTIONS, + selectedOptions: [] as unknown[], + hasColOptions: true, +}; + +const renderCombobox = (propsOverride: Partial = {}) => { + return render(FormSelectorCombobox, { + props: { + ...defaultProps, + ...propsOverride, + }, + }); +}; + +describe("FormSelectorCombobox", () => { + // MARK: Logic Testing + + it("renders a textbox with the given label as placeholder and no chips when nothing is selected", async () => { + await renderCombobox(); + + // FormTextInput renders an . + const input = screen.getByRole("textbox"); + expect(input).toBeTruthy(); + expect(input.getAttribute("placeholder")).toBe("Topics"); + + // No chips should be rendered when selectedOptions is empty. + const chipsList = screen.queryByRole("list"); + expect(chipsList).toBeNull(); + const chip = screen.queryByText("Option one"); + expect(chip).toBeNull(); + }); + + it("maps selectedOptions values to chips using the options prop", async () => { + await renderCombobox({ + selectedOptions: ["one", "two"], + }); + + // Only the labels of selected options should appear as chips. + const chipOne = screen.getByText("Option one"); + const chipTwo = screen.getByText("Option two"); + + expect(chipOne).toBeTruthy(); + expect(chipTwo).toBeTruthy(); + }); + + it("emits update:selectedOptions when a chip is clicked (option removed)", async () => { + const { emitted } = await renderCombobox({ + selectedOptions: ["one", "two"], + }); + + // Clicking a chip should call onClick and update the v-model, causing an emit. + const chipOne = screen.getByText("Option one"); + await fireEvent.click(chipOne); + + const updateEvents = emitted()["update:selectedOptions"] as + | unknown[][] + | undefined; + expect(updateEvents).toBeTruthy(); + + const lastEvent = updateEvents![updateEvents!.length - 1] as unknown[]; + const lastPayload = lastEvent[0] as string[]; + expect(lastPayload.length).toBe(1); + expect(lastPayload).toEqual(["two"]); + }); + + it("ignores selectedOptions values that do not exist in options", async () => { + await renderCombobox({ + selectedOptions: ["one", "missing"], + }); + + // Only the existing option should show as chip. + const existingChip = screen.getByText("Option one"); + expect(existingChip).toBeTruthy(); + + const missingChip = screen.queryByText("missing"); + expect(missingChip).toBeNull(); + }); + + // MARK: Style Testing + + it("uses column layout classes when hasColOptions is true", async () => { + await renderCombobox({ + selectedOptions: ["one"], + hasColOptions: true, + }); + + // The chips
    is rendered as a flex column with vertical spacing. + const chipsList = screen.getByRole("list") as HTMLElement; + expect(chipsList).toBeTruthy(); + + const { className } = chipsList; + expect(className).toContain("flex"); + expect(className).toContain("flex-col"); + expect(className).toContain("space-y-2"); + }); + + it("uses row layout classes when hasColOptions is false", async () => { + await renderCombobox({ + selectedOptions: ["one"], + hasColOptions: false, + }); + + const chipsList = screen.getByRole("list") as HTMLElement; + expect(chipsList).toBeTruthy(); + + const { className } = chipsList; + expect(className).toContain("flex"); + expect(className).toContain("space-x-1"); + expect(className).not.toContain("flex-col"); + }); + + // MARK: Accessibility + + it("exposes combobox and textbox roles with basic ARIA attributes", async () => { + await renderCombobox(); + + // Headless UI wraps the input in an element with role="combobox". + const combobox = screen.getByRole("combobox"); + expect(combobox).toBeTruthy(); + + // It should advertise that it autocompletes from a list. + expect(combobox.getAttribute("aria-autocomplete")).toBe("list"); + + // By default the menu should not be expanded. + // Headless UI sets aria-expanded on the combobox root. + expect(combobox.getAttribute("aria-expanded")).toBe("false"); + + // Inner input should be exposed as textbox. + const input = screen.getByRole("textbox"); + expect(input).toBeTruthy(); + }); + + // MARK: Edge Cases + + it("renders safely when options is empty", async () => { + await renderCombobox({ + options: [], + selectedOptions: [], + }); + + // Still renders an input. + const input = screen.getByRole("textbox"); + expect(input).toBeTruthy(); + + // No chips list should be rendered. + const chipsList = screen.queryByRole("list"); + expect(chipsList).toBeNull(); + }); +}); From 8ad9ec0db8f281e8092e51bea65eae677f7806db Mon Sep 17 00:00:00 2001 From: Andrew Tavis McAllister Date: Sat, 29 Nov 2025 17:12:59 +0000 Subject: [PATCH 041/243] Misc comment fixes in frontend unit testing files --- .../app/components/form/FormTextEntity.vue | 4 +- .../components/form/FormTextEntity.spec.ts | 62 +++++++++---------- 2 files changed, 33 insertions(+), 33 deletions(-) diff --git a/frontend/app/components/form/FormTextEntity.vue b/frontend/app/components/form/FormTextEntity.vue index 43ce836f8..4d9762c84 100644 --- a/frontend/app/components/form/FormTextEntity.vue +++ b/frontend/app/components/form/FormTextEntity.vue @@ -109,9 +109,9 @@ const schema = z (value) => { if (!value) return true; const trimmed = value.trim(); - // Treat whitespace-only as empty (no URL validation needed) + // Treat whitespace-only as empty (no URL validation needed). if (trimmed.length === 0) return true; - // Validate trimmed URL + // Validate trimmed URL. return z.string().url().safeParse(trimmed).success; }, { diff --git a/frontend/test/components/form/FormTextEntity.spec.ts b/frontend/test/components/form/FormTextEntity.spec.ts index 0ddb6d674..d72a6bc4c 100644 --- a/frontend/test/components/form/FormTextEntity.spec.ts +++ b/frontend/test/components/form/FormTextEntity.spec.ts @@ -114,7 +114,7 @@ describe("FormTextEntity component", () => { const descriptionError = await screen.findByTestId( "form-item-description-error" ); - // Schema uses .refine() to ensure custom error message is always displayed + // Schema uses .refine() to ensure custom error message is always displayed. expect(descriptionError.textContent).toBe( getEnglishText( "i18n.components.form_text_entity.description_required" @@ -194,7 +194,7 @@ describe("FormTextEntity component", () => { const descriptionError = await screen.findByTestId( "form-item-description-error" ); - // Error message is interpolated with actual number + // Error message is interpolated with actual number. expect(descriptionError.textContent).toContain("2500"); expect(descriptionError.textContent).toContain("characters"); }); @@ -249,7 +249,7 @@ describe("FormTextEntity component", () => { const getInvolvedError = await screen.findByTestId( "form-item-getInvolved-error" ); - // Error message is interpolated with actual number + // Error message is interpolated with actual number. expect(getInvolvedError.textContent).toContain("500"); expect(getInvolvedError.textContent).toContain("characters"); }); @@ -608,7 +608,7 @@ describe("FormTextEntity component", () => { }); await fireEvent.click(submitBtn); - // Whitespace-only URLs are treated as empty, so they trigger the "text or URL required" error + // Whitespace-only URLs are treated as empty, so they trigger the "text or URL required" error. const getInvolvedError = await screen.findByTestId( "form-item-getInvolved-error" ); @@ -621,7 +621,7 @@ describe("FormTextEntity component", () => { "i18n.components.form_text_entity.get_involved_text_or_url_required" ) ); - // Whitespace-only URLs are treated as empty (not invalid URLs) + // Whitespace-only URLs are treated as empty (not invalid URLs). expect(getInvolvedUrlError.textContent).toBe( getEnglishText( "i18n.components.form_text_entity.get_involved_text_or_url_required" @@ -726,7 +726,7 @@ describe("FormTextEntity component", () => { const container = getInvolvedUrlInput.closest(".primary-text"); const label = container?.querySelector("label"); - // The label should include the rememberHttpsLabel text + // The label should include the rememberHttpsLabel text. expect(label?.textContent).toContain( getEnglishText("i18n.components.modal.text._global.remember_https") ); @@ -737,7 +737,7 @@ describe("FormTextEntity component", () => { props: defaultProps, }); - // FormItem's label (accessible label) should still be present + // FormItem's label (accessible label) should still be present. const formLabel = document.querySelector( 'label[for="form-item-getInvolvedUrl"].text-base.font-semibold' ); @@ -747,7 +747,7 @@ describe("FormTextEntity component", () => { ) ); - // FormTextInput's floating label should be empty when rememberHttpsLabel is not provided + // FormTextInput's floating label should be empty when rememberHttpsLabel is not provided. const getInvolvedUrlInput = screen.getByLabelText( getEnglishText( "i18n.components.modal_text_organization.join_organization_link" @@ -756,11 +756,11 @@ describe("FormTextEntity component", () => { const container = getInvolvedUrlInput.closest(".primary-text"); const floatingLabel = container?.querySelector("label"); - // When rememberHttpsLabel is not provided, label prop is empty string - // The floating label will be empty when there's no value + // When rememberHttpsLabel is not provided, label prop is empty string. + // The floating label will be empty when there's no value. expect(floatingLabel?.textContent).toBe(""); - // The accessible label comes from FormItem's FormLabel, which is still present + // The accessible label comes from FormItem's FormLabel, which is still present. expect(getInvolvedUrlInput).toBeDefined(); }); @@ -942,7 +942,7 @@ describe("FormTextEntity component", () => { const descriptionInput = screen.getByLabelText( getEnglishText("i18n._global.description") ); - // FormTextArea applies border-action-red when hasError is true + // FormTextArea applies border-action-red when hasError is true. expect(descriptionInput.className).toContain("border-action-red"); }); @@ -993,7 +993,7 @@ describe("FormTextEntity component", () => { await screen.findByTestId("form-item-getInvolvedUrl-error"); - // FormTextInput applies border-action-red via fieldset when hasError is true + // FormTextInput applies border-action-red via fieldset when hasError is true. const container = getInvolvedUrlInput.closest(".primary-text"); const fieldset = container?.querySelector("fieldset"); expect(fieldset?.className).toContain("border-action-red"); @@ -1027,7 +1027,7 @@ describe("FormTextEntity component", () => { expect(screen.queryByTestId("form-item-description-error")).toBeNull(); }); - // Error styling should be removed + // Error styling should be removed. await waitFor(() => { expect(descriptionInput.className).not.toContain("border-action-red"); }); @@ -1060,12 +1060,12 @@ describe("FormTextEntity component", () => { props: defaultProps, }); - // Required indicator is in a separate span element (not in label text) + // Required indicator is in a separate span element (not in label text). const requiredIndicators = screen.getAllByText("*"); expect(requiredIndicators.length).toBeGreaterThan(0); - // Check that the required indicator is associated with description field - // The FormLabel component renders the asterisk in a separate span + // Check that the required indicator is associated with description field. + // The FormLabel component renders the asterisk in a separate span. const descriptionContainer = document.querySelector( 'div[name="description"]' ); @@ -1075,7 +1075,7 @@ describe("FormTextEntity component", () => { expect(requiredIndicator).toBeDefined(); expect(requiredIndicator?.textContent).toBe("*"); - // Verify the label itself doesn't contain the asterisk (it's in a sibling span) + // Verify the label itself doesn't contain the asterisk (it's in a sibling span). const descriptionLabel = descriptionContainer?.querySelector( "label.text-base.font-semibold" ); @@ -1091,7 +1091,7 @@ describe("FormTextEntity component", () => { }); const requiredIndicators = screen.getAllByText("*"); - // Only description should be required + // Only description should be required. expect(requiredIndicators.length).toBe(1); }); @@ -1124,15 +1124,15 @@ describe("FormTextEntity component", () => { ) ); - // FormTextInput uses a floating label that's empty when there's no value - // The accessible label comes from FormItem's FormLabel component + // FormTextInput uses a floating label that's empty when there's no value. + // The accessible label comes from FormItem's FormLabel component. const formLabel = document.querySelector( `label[for="${getInvolvedUrlInput.id}"].text-base.font-semibold` ); expect(getInvolvedUrlInput).toBeDefined(); expect(getInvolvedUrlInput.id).toBe("form-item-getInvolvedUrl"); - // The FormLabel from FormItem provides the accessible label + // The FormLabel from FormItem provides the accessible label. expect(formLabel?.textContent).toBe( getEnglishText( "i18n.components.modal_text_organization.join_organization_link" @@ -1209,18 +1209,18 @@ describe("FormTextEntity component", () => { ) ) as HTMLElement; - // Verify fields are accessible and can be found by their labels - // Form fields (textarea/input) are naturally focusable via keyboard navigation + // Verify fields are accessible and can be found by their labels. + // Form fields (textarea/input) are naturally focusable via keyboard navigation. expect(descriptionInput).toBeDefined(); expect(getInvolvedInput).toBeDefined(); expect(getInvolvedUrlInput).toBeDefined(); - // Verify all fields are form controls that support keyboard navigation + // Verify all fields are form controls that support keyboard navigation. expect(descriptionInput.tagName).toBe("TEXTAREA"); expect(getInvolvedInput.tagName).toBe("TEXTAREA"); expect(getInvolvedUrlInput.tagName).toBe("INPUT"); - // Verify fields are not disabled (disabled elements have tabIndex of -1) + // Verify fields are not disabled (disabled elements have tabIndex of -1). expect(descriptionInput.hasAttribute("disabled")).toBe(false); expect(getInvolvedInput.hasAttribute("disabled")).toBe(false); expect(getInvolvedUrlInput.hasAttribute("disabled")).toBe(false); @@ -1381,11 +1381,11 @@ describe("FormTextEntity component", () => { }); const callArgs = mockHandleSubmit.mock.calls[0]?.[0]; - // Verify all fields are included in the payload + // Verify all fields are included in the payload. expect(callArgs).toHaveProperty("description", "Test description"); expect(callArgs).toHaveProperty("getInvolved", "Join us"); expect(callArgs).toHaveProperty("getInvolvedUrl"); - // Verify the payload structure is complete + // Verify the payload structure is complete. expect(Object.keys(callArgs)).toEqual([ "description", "getInvolved", @@ -1420,7 +1420,7 @@ describe("FormTextEntity component", () => { }); const callArgs = mockHandleSubmit.mock.calls[0]?.[0]; - // Verify optional field is undefined when not provided + // Verify optional field is undefined when not provided. expect(callArgs).toHaveProperty("description", "Test description"); expect(callArgs).toHaveProperty("getInvolved", "Join us"); expect(callArgs).toHaveProperty("getInvolvedUrl", undefined); @@ -1452,7 +1452,7 @@ describe("FormTextEntity component", () => { expect(mockHandleSubmit).toHaveBeenCalledTimes(1); }); - // Form should still be visible after rejection + // Form should still be visible after rejection. expect(descriptionInput).toBeDefined(); }); @@ -1465,7 +1465,7 @@ describe("FormTextEntity component", () => { name: "Submit the form", }); - // Submit multiple times rapidly + // Submit multiple times rapidly. await fireEvent.click(submitBtn); await fireEvent.click(submitBtn); await fireEvent.click(submitBtn); From 8ed633c4be5f52bb53e96adec476e3eab4490a6a Mon Sep 17 00:00:00 2001 From: Marvelous Ukuesa <112257437+MarvelousUkuesa@users.noreply.github.com> Date: Sat, 29 Nov 2025 18:54:10 +0100 Subject: [PATCH 042/243] Task/unit tests form socia link component 1760 (#1764) * Added FormSelectorCombobox.spec.ts under frontend/test/components/form/selector/ * add comprehensive unit tests for FormSocialLink component (#1760) * test: sync commit email * deleted: frontend/test/components/form/selector/FormSelectorComboBox.spec.ts * Marking out file and fixing TS errors * Switch example website for social links testing --------- Co-authored-by: Ukuesa Marvelous Co-authored-by: Andrew Tavis McAllister --- .../components/form/FormSocialLink.spec.ts | 336 ++++++++++++++++++ 1 file changed, 336 insertions(+) create mode 100644 frontend/test/components/form/FormSocialLink.spec.ts diff --git a/frontend/test/components/form/FormSocialLink.spec.ts b/frontend/test/components/form/FormSocialLink.spec.ts new file mode 100644 index 000000000..90c084d8d --- /dev/null +++ b/frontend/test/components/form/FormSocialLink.spec.ts @@ -0,0 +1,336 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +import { fireEvent, screen, waitFor } from "@testing-library/vue"; +import { describe, expect, it, vi } from "vitest"; + +import type { SocialLinkFormData } from "../../../app/constants/social-link"; + +import FormSocialLink from "../../../app/components/form/FormSocialLink.vue"; +import render from "../../render"; + +/** + * This suite focuses on: + * - Logic: + * - Text validation via zod schema (required label, valid URL) + * - Change events propagating through FormItem/FormTextInput + * - Dynamic props (formData, submitLabel, title) + * - Style: + * - Tailwind layout classes from frontend/app/assets/css/tailwind.css + * - Error border color via "border-action-red" + * - Accessibility: + * - Draggable rows marked via data-draggable="true" + * - Presence of textbox roles and submit button + * - Edge cases: + * - Empty socialLinks array + * - Invalid values (empty label, non-URL link) + */ + +// Helper to build a minimal valid social link entry. +const createSocialLink = (overrides: Partial = {}) => ({ + id: overrides.id ?? "id-1", + label: overrides.label ?? "My site", + link: overrides.link ?? "https://example.com", + order: overrides.order ?? 0, +}); + +describe("FormSocialLink", () => { + // MARK: Logic Testing + + it("renders title, initial social links and add-link button", async () => { + const handleSubmit = vi.fn().mockResolvedValue(undefined); + + const formData = { + socialLinks: [ + createSocialLink({ + id: "link-1", + label: "activist", + link: "https://activist.org/example", + order: 0, + }), + ], + }; + + await render(FormSocialLink, { + props: { + formData, + handleSubmit, + submitLabel: "i18n.components.form_social_link._global.save", + title: "i18n.components.form_social_link._global.edit_links", + }, + }); + + // Title from `title` prop (raw i18n key in test env). + const heading = screen.getByText( + "i18n.components.form_social_link._global.edit_links" + ); + expect(heading.tagName).toBe("H2"); + + // Initial label + URL inputs populated from formData. + const labelInput = screen.getByTestId( + "social-link-label-0" + ) as HTMLInputElement; + const urlInput = screen.getByTestId( + "social-link-url-0" + ) as HTMLInputElement; + + expect(labelInput.value).toBe("activist"); + expect(urlInput.value).toBe("https://activist.org/example"); + + // Add-link CTA button from BtnAction (accessible name comes from aria-label). + const addButton = screen.getByRole("button", { + name: /add a new social link/i, + }); + expect(addButton).toBeTruthy(); + }); + + it("validates invalid URL and prevents submit when data is invalid", async () => { + const handleSubmit = vi.fn().mockResolvedValue(undefined); + + // Start with invalid data: empty label or invalid URL. + const formData = { + socialLinks: [ + createSocialLink({ + label: "", + link: "not-a-url", + order: 0, + }), + ], + }; + + await render(FormSocialLink, { + props: { + formData, + handleSubmit, + submitLabel: "i18n.components.form_social_link._global.save", + }, + }); + + const submitButton = screen.getByRole("button", { + name: /submit the form/i, + }); + + await fireEvent.click(submitButton); + + // Under invalid data, handleSubmit should NOT be called. + await waitFor(() => { + expect(handleSubmit).not.toHaveBeenCalled(); + }); + + // URL validation error from zod schema / translations. + const urlError = await screen.findByTestId( + "form-item-socialLinks.0.link-error" + ); + expect(urlError.textContent || "").toMatch(/valid url/i); + + // Style coverage: invalid URL field should get error border color. + const urlBorder = screen.getByTestId( + "form-item-socialLinks.0.link-border" + ) as HTMLElement; + + await waitFor(() => { + expect(urlBorder.className).toContain("border-action-red"); + }); + }); + + it("submits valid social link data", async () => { + const handleSubmit = vi.fn().mockResolvedValue(undefined); + + const formData = { + socialLinks: [ + createSocialLink({ + id: "link-1", + label: "", + link: "", + order: 0, + }), + ], + }; + + await render(FormSocialLink, { + props: { + formData, + handleSubmit, + submitLabel: "i18n.components.form_social_link._global.save", + }, + }); + + const labelInput = screen.getByTestId( + "social-link-label-0" + ) as HTMLInputElement; + const urlInput = screen.getByTestId( + "social-link-url-0" + ) as HTMLInputElement; + + // Simulate user filling in valid values. + await fireEvent.update(labelInput, "Homepage"); + await fireEvent.update(urlInput, "https://example.org"); + + const submitButton = screen.getByRole("button", { + name: /submit the form/i, + }); + await fireEvent.click(submitButton); + + await waitFor(() => { + expect(handleSubmit).toHaveBeenCalledTimes(1); + }); + + const firstCallArg = handleSubmit.mock.calls[0][0] as { + socialLinks: Array<{ label: string; link: string }>; + }; + + expect(firstCallArg.socialLinks).toHaveLength(1); + expect(firstCallArg.socialLinks[0].label).toBe("Homepage"); + expect(firstCallArg.socialLinks[0].link).toBe("https://example.org"); + }); + + it("adds a new social link entry when the add-link button is clicked", async () => { + const handleSubmit = vi.fn().mockResolvedValue(undefined); + + const formData = { + socialLinks: [createSocialLink({ label: "Existing", order: 0 })], + }; + + await render(FormSocialLink, { + props: { + formData, + handleSubmit, + submitLabel: "i18n.components.form_social_link._global.save", + }, + }); + + let labelInputs = screen.getAllByTestId(/social-link-label-/); + expect(labelInputs.length).toBe(1); + + const addButton = screen.getByRole("button", { + name: /add a new social link/i, + }); + await fireEvent.click(addButton); + + // After pushing a new link, there should be two rows. + await waitFor(() => { + labelInputs = screen.getAllByTestId(/social-link-label-/); + expect(labelInputs.length).toBe(2); + }); + }); + + /** + * We do not test the delete icon click directly here, because IconDelete does + * not expose a DOM-level data-testid or role that we can reliably select. + * Its behavior is assumed to be covered in its own unit tests. The integration + * of socialLinks list length is instead covered in the "adds a new social link" + * and empty-state tests. + */ + + // MARK: Style Testing + + it("uses flex layout classes on list containers and entries", async () => { + const handleSubmit = vi.fn().mockResolvedValue(undefined); + + const formData = { + socialLinks: [createSocialLink({ label: "Styled link", order: 0 })], + }; + + await render(FormSocialLink, { + props: { + formData, + handleSubmit, + submitLabel: "i18n.components.form_social_link._global.save", + title: "i18n.components.form_social_link._global.edit_links", + }, + }); + + // The inner "Social links" heading is wrapped in a flex/column container. + const heading = screen.getByRole("heading", { + level: 2, + name: /social links/i, + }); + const wrapper = heading.closest("div"); + expect(wrapper).toBeTruthy(); + if (wrapper) { + const { className } = wrapper; + expect(className).toContain("flex"); + expect(className).toContain("flex-col"); + expect(className).toContain("space-y-3"); + } + + // Each list entry uses `flex items-center space-x-5`. + const entry = screen.getByTestId("social-link-entry-0") as HTMLElement; + expect(entry.className).toContain("flex"); + expect(entry.className).toContain("items-center"); + expect(entry.className).toContain("space-x-5"); + }); + + // MARK: Accessibility Testing + + it("marks list entries as draggable via data-draggable attribute", async () => { + const handleSubmit = vi.fn().mockResolvedValue(undefined); + + const formData = { + socialLinks: [createSocialLink()], + }; + + await render(FormSocialLink, { + props: { + formData, + handleSubmit, + submitLabel: "i18n.components.form_social_link._global.save", + }, + }); + + const entry = screen.getByTestId("social-link-entry-0"); + expect(entry.getAttribute("data-draggable")).toBe("true"); + }); + + // MARK: Edge Cases + + it("renders safely when socialLinks is empty and allows adding the first entry", async () => { + const handleSubmit = vi.fn().mockResolvedValue(undefined); + + const formData = { + socialLinks: [], + }; + + await render(FormSocialLink, { + props: { + formData, + handleSubmit, + submitLabel: "i18n.components.form_social_link._global.save", + }, + }); + + // No rows initially. + let labelInputs = screen.queryAllByTestId(/social-link-label-/); + expect(labelInputs.length).toBe(0); + + const addButton = screen.getByRole("button", { + name: /add a new social link/i, + }); + await fireEvent.click(addButton); + + await waitFor(() => { + labelInputs = screen.getAllByTestId(/social-link-label-/); + expect(labelInputs.length).toBe(1); + }); + }); + + it("handles absence of title prop while still rendering the built-in Social links heading", async () => { + const handleSubmit = vi.fn().mockResolvedValue(undefined); + + const formData = { + socialLinks: [createSocialLink()], + }; + + await render(FormSocialLink, { + props: { + formData, + handleSubmit, + submitLabel: "i18n.components.form_social_link._global.save", + // No title prop here. + }, + }); + + // There should be exactly one h2 heading "Social links". + const headings = screen.getAllByRole("heading", { level: 2 }); + expect(headings.length).toBe(1); + expect(headings[0].textContent || "").toMatch(/social links/i); + }); +}); From 2daed695c82a1bdb6278ab0fcb807857b0eff102 Mon Sep 17 00:00:00 2001 From: Giuseppe Elefante <116946078+Peppe-elefante@users.noreply.github.com> Date: Sat, 29 Nov 2025 19:59:07 +0100 Subject: [PATCH 043/243] Expanded backend views testing (#1741) * refactored tests on authentication/views.py * formatted code * expanded testing on events/views.py * expanded testing in communities/groups/views.py * expanded communities/organizations test * expanded testing in content/views.py * added test for content topic API * removed unaccessible code in content/views.py * fixed bug in views.py * Complete URLs in tests, fix comments misc fixes --------- Co-authored-by: Andrew Tavis McAllister --- .../tests/flag/test_user_flag_list.py | 23 +++ .../tests/test_reset_password.py | 73 ++++++--- backend/authentication/tests/test_signup.py | 117 ++++++++++---- .../authentication/tests/test_verify_email.py | 49 +----- .../tests/test_verify_email_for_pwreset.py | 75 ++++----- .../groups/tests/faq/test_group_faq_delete.py | 87 +++++++++++ .../groups/tests/faq/test_group_faq_update.py | 41 ++++- .../test_group_social_link_update.py | 1 + .../groups/tests/test_group_api.py | 21 ++- .../groups/tests/test_group_list.py | 16 ++ .../tests/faq/test_org_faq_create.py | 60 +++---- .../tests/faq/test_org_faq_delete.py | 132 ++++++++++++++++ .../tests/faq/test_org_faq_update.py | 54 ++++--- .../organizations/tests/test_org_api.py | 23 ++- .../entry/test_discussion_entry_update.py | 45 +++--- .../discussion/test_discussion_create.py | 34 ++-- .../discussion/test_discussion_update.py | 32 ++-- backend/content/tests/topic/test_topic_api.py | 63 ++++++++ backend/content/views.py | 15 +- .../events/tests/faq/test_event_faq_create.py | 60 +++---- .../tests/faq/test_event_faq_destroy.py | 147 ++++++++++++++++++ .../events/tests/faq/test_event_faq_update.py | 31 ++++ .../events/tests/flag/test_event_flag_list.py | 22 +++ .../test_event_social_link_delete.py | 35 +++++ backend/events/tests/test_event_api.py | 71 +++++++-- 25 files changed, 999 insertions(+), 328 deletions(-) create mode 100644 backend/communities/groups/tests/faq/test_group_faq_delete.py create mode 100644 backend/communities/organizations/tests/faq/test_org_faq_delete.py create mode 100644 backend/content/tests/topic/test_topic_api.py create mode 100644 backend/events/tests/faq/test_event_faq_destroy.py diff --git a/backend/authentication/tests/flag/test_user_flag_list.py b/backend/authentication/tests/flag/test_user_flag_list.py index f010c5327..74dc942e1 100644 --- a/backend/authentication/tests/flag/test_user_flag_list.py +++ b/backend/authentication/tests/flag/test_user_flag_list.py @@ -1,5 +1,6 @@ # SPDX-License-Identifier: AGPL-3.0-or-later import logging +from unittest.mock import patch import pytest @@ -19,3 +20,25 @@ def test_user_flag_list(authenticated_client): assert response.status_code == 200 logger.info("User flag list test completed successfully") + + +def test_user_flag_list_no_pagination(authenticated_client): + """ + Test list all user flags in case of no pagination. + """ + + client, user = authenticated_client + + with patch( + "authentication.views.UserFlagAPIView.paginate_queryset" + ) as mock_paginate: + mock_paginate.return_value = None + + logger.debug("Making request to user flag endpoint") + response = client.get(path="/v1/auth/user_flags") + + assert response.status_code == 200 + + # Verify that paginate_queryset was called. + mock_paginate.assert_called_once() + logger.info("User flag list test completed successfully") diff --git a/backend/authentication/tests/test_reset_password.py b/backend/authentication/tests/test_reset_password.py index 98bb73f64..df6608d3f 100644 --- a/backend/authentication/tests/test_reset_password.py +++ b/backend/authentication/tests/test_reset_password.py @@ -1,11 +1,9 @@ # SPDX-License-Identifier: AGPL-3.0-or-later import logging +from unittest.mock import patch import pytest from django.core import mail -from rest_framework.test import APIClient - -from authentication.factories import UserFactory logger = logging.getLogger(__name__) @@ -15,47 +13,72 @@ # MARK: Password Reset -def test_pwreset(client: APIClient) -> None: +def test_pwreset_email_sent_successfully(authenticated_client) -> None: """ - Test password reset view. - - This test covers various password reset scenarios: - 1. Password reset email is sent successfully for a valid user. - 2. Password reset attempt with an invalid email. - 3. Password reset is performed successfully with a valid verification code. - 4. Password reset attempt with an invalid verification code. + Test that password reset email is sent successfully for a valid user. - Parameters - ---------- - client : APIClient - An authenticated client. """ - logger.info("Starting password reset test with various scenarios") - - # Setup - old_password = "password123!?" - new_password = "Activist@123!?" + logger.info("Testing password reset email request for valid user") + client, user = authenticated_client - # 1. User exists and password reset is successful. - logger.info("Testing password reset email request") - user = UserFactory(plaintext_password=old_password) response = client.post( path="/v1/auth/pwreset", data={"email": user.email}, ) + assert response.status_code == 200 assert len(mail.outbox) == 1 logger.info(f"Password reset email sent successfully to: {user.email}") - # 2. Password reset with invalid email. + +def test_pwreset_invalid_email(authenticated_client) -> None: + """ + Test password reset attempt with an invalid email. + """ + logger.info("Testing password reset with invalid email") + client, user = authenticated_client response = client.post( path="/v1/auth/pwreset", data={"email": "invalid_email@example.com"} ) + assert response.status_code == 404 - # 4. Password reset with invalid verification code. + +def test_pwreset_invalid_verification_code(authenticated_client) -> None: + """ + Test password reset attempt with an invalid verification code. + """ + logger.info("Testing password reset with invalid verification code") + client, user = authenticated_client + new_password = "Activist@123!?" + response = client.post( path="/v1/auth/pwreset/invalid_code", data={"password": new_password}, ) + assert response.status_code == 404 + + +def test_pwreset_email_sending_failure(authenticated_client) -> None: + """ + Test password reset when email sending fails. + + This test verifies that the except block in PasswordResetView is triggered + when send_mail raises an exception, and that the view returns a 500 error. + """ + logger.info("Testing password reset email sending failure") + client, user = authenticated_client + + # Mock send_mail to raise an exception. + with patch("authentication.views.send_mail") as mock_send_mail: + mock_send_mail.side_effect = Exception("SMTP server error") + + response = client.post( + path="/v1/auth/pwreset", + data={"email": user.email}, + ) + + assert response.status_code == 500 + assert response.data["detail"] == "Failed to send password reset email." + mock_send_mail.assert_called_once() diff --git a/backend/authentication/tests/test_signup.py b/backend/authentication/tests/test_signup.py index 1ae09a287..6143f1056 100644 --- a/backend/authentication/tests/test_signup.py +++ b/backend/authentication/tests/test_signup.py @@ -17,38 +17,23 @@ # MARK: Sign Up -def test_sign_up(client: APIClient) -> None: +def test_sign_up_with_weak_password(client: APIClient) -> None: """ - Test the sign-up function. - - This test checks various user registration scenarios: - - Password too weak - - Password mismatch - - Successful registration - - Duplicate user - - Missing email + Test that sign-up fails when password is too weak. Parameters ---------- - client : Client - An authenticated client. + client : APIClient + An API client. """ - logger.info("Starting sign-up test with various scenarios") + logger.info("Testing password strength validation") fake = Faker() username = fake.user_name() email = fake.email() - strong_password = fake.password( - length=12, special_chars=True, digits=True, upper_case=True - ) - wrong_password_confirmed = fake.password( - length=12, special_chars=True, digits=True, upper_case=True - ) weak_password = fake.password( length=8, special_chars=False, digits=False, upper_case=False ) - # 1. Password strength fails. - logger.info("Testing password strength validation") response = client.post( path="/v1/auth/sign_up", data={ @@ -62,7 +47,27 @@ def test_sign_up(client: APIClient) -> None: assert response.status_code == 400 assert not UserModel.objects.filter(username=username).exists() - # 2. Password confirmation fails. + +def test_sign_up_with_password_mismatch(client: APIClient) -> None: + """ + Test that sign-up fails when passwords don't match. + + Parameters + ---------- + client : APIClient + An API client. + """ + logger.info("Testing password confirmation validation") + fake = Faker() + username = fake.user_name() + email = fake.email() + strong_password = fake.password( + length=12, special_chars=True, digits=True, upper_case=True + ) + wrong_password_confirmed = fake.password( + length=12, special_chars=True, digits=True, upper_case=True + ) + response = client.post( path="/v1/auth/sign_up", data={ @@ -75,7 +80,24 @@ def test_sign_up(client: APIClient) -> None: assert response.status_code == 400 assert not UserModel.objects.filter(username=username).exists() - # 5. User is not created without an email. + + +def test_sign_up_without_email(client: APIClient) -> None: + """ + Test that sign-up fails when email is missing. + + Parameters + ---------- + client : APIClient + An API client. + """ + logger.info("Testing sign-up without email") + fake = Faker() + username = fake.user_name() + strong_password = fake.password( + length=12, special_chars=True, digits=True, upper_case=True + ) + response = client.post( path="/v1/auth/sign_up", data={ @@ -89,8 +111,25 @@ def test_sign_up(client: APIClient) -> None: assert user is None assert response.status_code == 400 assert not UserModel.objects.filter(username=username).exists() - # 3. User is created successfully. + + +def test_sign_up_successful(client: APIClient) -> None: + """ + Test successful user registration. + + Parameters + ---------- + client : APIClient + An API client. + """ logger.info("Testing successful user creation") + fake = Faker() + username = fake.user_name() + email = fake.email() + strong_password = fake.password( + length=12, special_chars=True, digits=True, upper_case=True + ) + response = client.post( path="/v1/auth/sign_up", data={ @@ -104,18 +143,42 @@ def test_sign_up(client: APIClient) -> None: assert response.status_code == 201 assert UserModel.objects.filter(username=username) - # Code for Email confirmation is generated and is a UUID. assert isinstance(user.verification_code, UUID) assert user.is_confirmed is False - # Confirmation Email was sent. assert len(mail.outbox) == 1 - # Assert that the password within the dashboard is hashed and not the original string. assert user.password != strong_password logger.info( f"Successfully created user: {username} with verification code: {user.verification_code}" ) - # 4. User already exists. + +def test_sign_up_with_duplicate_user(client: APIClient) -> None: + """ + Test that sign-up fails when user already exists. + + Parameters + ---------- + client : APIClient + An API client. + """ + logger.info("Testing duplicate user registration") + fake = Faker() + username = fake.user_name() + email = fake.email() + strong_password = fake.password( + length=12, special_chars=True, digits=True, upper_case=True + ) + + client.post( + path="/v1/auth/sign_up", + data={ + "username": username, + "password": strong_password, + "password_confirmed": strong_password, + "email": email, + }, + ) + response = client.post( path="/v1/auth/sign_up", data={ diff --git a/backend/authentication/tests/test_verify_email.py b/backend/authentication/tests/test_verify_email.py index 7efdc42e3..f6744d0aa 100644 --- a/backend/authentication/tests/test_verify_email.py +++ b/backend/authentication/tests/test_verify_email.py @@ -3,10 +3,6 @@ import uuid import pytest -from faker import Faker -from rest_framework.test import APIClient - -from authentication.factories import UserFactory logger = logging.getLogger(__name__) @@ -16,52 +12,23 @@ # MARK: Verify Email -def test_verify_email(client: APIClient) -> None: +def test_verify_email(authenticated_client) -> None: """ Test email verification view. - This test covers several email verification scenarios: - 1. Valid verification code. - 2. Invalid verification code. - 3. Reusing an already used verification code. - - Parameters - ---------- - client : APIClient - An authenticated client. + This test covers several email verification in two scenarios: + 1) Using a valid code + 2) Using an invalid code """ - logger.info("Starting email verification test with various scenarios") - fake = Faker() - username = fake.user_name() - email = fake.email() - password = fake.password( - length=12, special_chars=True, digits=True, upper_case=True - ) - new_password = fake.password( - length=12, special_chars=True, digits=True, upper_case=True - ) - user = UserFactory( - username=username, - email=email, - plaintext_password=password, - is_confirmed=False, - verification_code=uuid.uuid4(), - ) + client, user = authenticated_client + user.verification_code = uuid.uuid4() + user.save() # 1. Valid verification code. logger.info("Testing valid email verification") - response = client.post( - path=f"/v1/auth/verify_email_password/{user.verification_code}", - data={"new_password": new_password}, - ) + response = client.post(path=f"/v1/auth/verify_email/{user.verification_code}") assert response.status_code == 200 - user.refresh_from_db() - assert user.verification_code is None - assert user.check_password(new_password) is True - assert user.check_password(password) is False - logger.info(f"Successfully verified email for user: {username}") - # 2. Invalid verification code. logger.info("Testing invalid email verification") response = client.post(path="/v1/auth/verify_email/invalid_code") diff --git a/backend/authentication/tests/test_verify_email_for_pwreset.py b/backend/authentication/tests/test_verify_email_for_pwreset.py index b6cec9b81..2560cd329 100644 --- a/backend/authentication/tests/test_verify_email_for_pwreset.py +++ b/backend/authentication/tests/test_verify_email_for_pwreset.py @@ -3,10 +3,6 @@ import uuid import pytest -from faker import Faker -from rest_framework.test import APIClient - -from authentication.factories import UserFactory logger = logging.getLogger(__name__) @@ -16,40 +12,14 @@ # MARK: Verify Email Pwd Reset -def test_verify_email_for_reset_password(client: APIClient) -> None: +def test_verify_email_for_reset_password_valid_code(authenticated_client) -> None: """ - Test email verification view. - - This test covers several reset password verification scenarios: - 1. Valid verification code and new password. - 2. Invalid verification code and new password. + Test email verification with valid verification code and new password. - Parameters - ---------- - client : APIClient - An authenticated client. """ - logger.info("Starting email verification test with various scenarios") - fake = Faker() - username = fake.user_name() - email = fake.email() - password = fake.password( - length=12, special_chars=True, digits=True, upper_case=True - ) - new_password = fake.password( - length=12, special_chars=True, digits=True, upper_case=True - ) - - user = UserFactory( - username=username, - email=email, - plaintext_password=password, - is_confirmed=False, - verification_code=uuid.uuid4(), - ) - - # 1. Valid verification code. - logger.info("Testing valid email verification") + logger.info("Testing valid email verification for password reset") + client, user = authenticated_client + new_password = "Activist@123!?" response = client.post( path=f"/v1/auth/verify_email_password/{user.verification_code}", data={"new_password": new_password}, @@ -58,16 +28,39 @@ def test_verify_email_for_reset_password(client: APIClient) -> None: user.refresh_from_db() assert user.check_password(new_password) is True - assert user.check_password(password) is False - logger.info(f"Successfully verified email for user: {username}") + assert user.check_password(user.password) is False + logger.info(f"Successfully verified email for user: {user.username}") + - # 2. Invalid verification code. - logger.info("Testing invalid email verification") - response = client.post(path="/v1/auth/verify_email_password/invalid_code") +def test_verify_email_for_reset_password_invalid_code(authenticated_client) -> None: + """ + Test email verification with invalid verification code. + + """ + client, user = authenticated_client + invalid_code = uuid.uuid4() + logger.info("Testing invalid email verification code for password reset") + response = client.post(path=f"/v1/auth/verify_email_password/{invalid_code}") assert response.status_code == 404 + assert response.data["detail"] == "User does not exist." - # 3. Reusing an already used verification code. + +def test_verify_email_for_reset_password_reused_code(authenticated_client) -> None: + """ + Test that already used verification code cannot be reused. + """ logger.info("Testing reuse of already used verification code") + client, user = authenticated_client + new_password = "Activist@123!?" + + # Use the verification code once. + response = client.post( + path=f"/v1/auth/verify_email_password/{user.verification_code}", + data={"new_password": new_password}, + ) + assert response.status_code == 200 + + # Attempt to reuse the same verification code. response = client.post( path=f"/v1/auth/verify_email_password/{user.verification_code}" ) diff --git a/backend/communities/groups/tests/faq/test_group_faq_delete.py b/backend/communities/groups/tests/faq/test_group_faq_delete.py new file mode 100644 index 000000000..6fe7df1a8 --- /dev/null +++ b/backend/communities/groups/tests/faq/test_group_faq_delete.py @@ -0,0 +1,87 @@ +# SPDX-License-Identifier: AGPL-3.0-or-later +""" +Test cases for the group FAQ delete methods. +""" + +from uuid import uuid4 + +import pytest + +from communities.groups.factories import GroupFactory, GroupFaqFactory + +pytestmark = pytest.mark.django_db + + +def test_group_faq_delete_204(authenticated_client): + """ + Test successful deletion of a group FAQ. + + Parameters + ---------- + authenticated_client : tuple + A tuple containing the authenticated client and user. + + Returns + ------- + None + This test asserts that the FAQ is deleted successfully with status code 204. + """ + client, user = authenticated_client + + group = GroupFactory(created_by=user) + faq = GroupFaqFactory(group=group) + + response = client.delete(path=f"/v1/communities/group_faqs/{faq.id}") + + assert response.status_code == 204 + + +def test_group_faq_delete_404(authenticated_client): + """ + Test deletion of a non-existent FAQ. + + Parameters + ---------- + authenticated_client : tuple + A tuple containing the authenticated client and user. + + Returns + ------- + None + This test asserts that attempting to delete a non-existent FAQ returns 404. + """ + client, user = authenticated_client + + bad_uuid = uuid4() + + response = client.delete( + path=f"/v1/communities/group_faqs/{bad_uuid}", + ) + + assert response.status_code == 404 + + +def test_group_faq_delete_403(authenticated_client): + """ + Test unauthorized deletion of a group FAQ. + + Parameters + ---------- + authenticated_client : tuple + A tuple containing the authenticated client and user. + + Returns + ------- + None + This test asserts that a user cannot delete an FAQ for a group they didn't create (403). + """ + client, user = authenticated_client + + group = GroupFactory() + faq = GroupFaqFactory(group=group) + + response = client.delete(path=f"/v1/communities/group_faqs/{faq.id}") + + response_body = response.json() + assert response.status_code == 403 + assert response_body["detail"] == "You are not authorized to delete this FAQ." diff --git a/backend/communities/groups/tests/faq/test_group_faq_update.py b/backend/communities/groups/tests/faq/test_group_faq_update.py index a5f8966cf..f3e84b775 100644 --- a/backend/communities/groups/tests/faq/test_group_faq_update.py +++ b/backend/communities/groups/tests/faq/test_group_faq_update.py @@ -18,11 +18,6 @@ def test_group_faq_update(authenticated_client) -> None: """ Test Group FAQ updates. - Parameters - ---------- - client : Client - A Django test client used to send HTTP requests to the application. - Returns ------- None @@ -69,3 +64,39 @@ def test_group_faq_update(authenticated_client) -> None: ) assert response.status_code == 404 + + +def test_group_faq_update_unauthorized(authenticated_client) -> None: + """ + Test Group FAQ updates. + + Returns + ------- + None + This test asserts the correctness of status codes (200 for success, 404 for not found). + """ + client, user = authenticated_client + user.is_staff = False + user.save() + group = GroupFactory() + + faqs = GroupFaqFactory(group=group) + test_id = faqs.id + test_question = faqs.question + test_answer = faqs.answer + test_order = faqs.order + + response = client.put( + path=f"/v1/communities/group_faqs/{test_id}", + data={ + "id": test_id, + "iso": "en", + "primary": True, + "question": test_question, + "answer": test_answer, + "order": test_order, + }, + format="json", + ) + + assert response.status_code == 403 diff --git a/backend/communities/groups/tests/social_link/test_group_social_link_update.py b/backend/communities/groups/tests/social_link/test_group_social_link_update.py index 45d2d759d..7c20e777d 100644 --- a/backend/communities/groups/tests/social_link/test_group_social_link_update.py +++ b/backend/communities/groups/tests/social_link/test_group_social_link_update.py @@ -15,6 +15,7 @@ def test_group_social_link_update(authenticated_client) -> None: """ Test Group Social Link updates. + Returns ------- None diff --git a/backend/communities/groups/tests/test_group_api.py b/backend/communities/groups/tests/test_group_api.py index ec972e7ca..97ba88928 100644 --- a/backend/communities/groups/tests/test_group_api.py +++ b/backend/communities/groups/tests/test_group_api.py @@ -14,9 +14,6 @@ from communities.organizations.factories import OrganizationFactory from content.factories import EntityLocationFactory -# Endpoint used for these tests: -GROUPS_URL = "/v1/communities/groups" - class UserDict(TypedDict): user: UserModel @@ -113,13 +110,13 @@ def test_GroupAPIView(logged_in_user, status_types): GroupFactory.create_batch(number_of_groups) assert Group.objects.count() == number_of_groups - response = client.get(GROUPS_URL) + response = client.get("/v1/communities/groups") assert response.status_code == 200 pagination_keys = ["count", "next", "previous", "results"] assert all(key in response.data for key in pagination_keys) - response = client.get(f"{GROUPS_URL}?pageSize={test_page_size}") + response = client.get(f"{'/v1/communities/groups'}?pageSize={test_page_size}") assert response.status_code == 200 assert len(response.data["results"]) == test_page_size @@ -129,7 +126,7 @@ def test_GroupAPIView(logged_in_user, status_types): # MARK: List POST # Not Authenticated. - response = client.post(GROUPS_URL) + response = client.post("/v1/communities/groups") assert response.status_code == 401 # Authenticated and successful. @@ -155,7 +152,7 @@ def test_GroupAPIView(logged_in_user, status_types): } client.credentials(HTTP_AUTHORIZATION=f"Token {token}") - response = client.post(GROUPS_URL, data=payload, format="json") + response = client.post("/v1/communities/groups", data=payload, format="json") assert response.status_code == 201 assert Group.objects.filter(name=new_group.name).exists() @@ -192,7 +189,7 @@ def test_GroupDetailAPIView(logged_in_user, logged_in_created_by_user) -> None: # MARK: Detail GET - response = client.get(f"{GROUPS_URL}/{new_group.id}") + response = client.get(f"{'/v1/communities/groups'}/{new_group.id}") assert response.status_code == 200 assert response.data["group_name"] == new_group.group_name @@ -200,7 +197,7 @@ def test_GroupDetailAPIView(logged_in_user, logged_in_created_by_user) -> None: updated_payload = {"group_name": "updated_group_name"} response = client.put( - f"{GROUPS_URL}/{new_group.id}", + f"{'/v1/communities/groups'}/{new_group.id}", data=updated_payload, format="json", ) @@ -208,7 +205,7 @@ def test_GroupDetailAPIView(logged_in_user, logged_in_created_by_user) -> None: client.credentials(HTTP_AUTHORIZATION=f"Token {access}") response = client.put( - f"{GROUPS_URL}/{new_group.id}", + f"{'/v1/communities/groups'}/{new_group.id}", data=updated_payload, format="json", ) @@ -220,10 +217,10 @@ def test_GroupDetailAPIView(logged_in_user, logged_in_created_by_user) -> None: # MARK: Detail DELETE client.credentials() - response = client.delete(f"{GROUPS_URL}/{new_group.id}") + response = client.delete(f"{'/v1/communities/groups'}/{new_group.id}") assert response.status_code == 401 client.credentials(HTTP_AUTHORIZATION=f"Token {access}") - response = client.delete(f"{GROUPS_URL}/{new_group.id}") + response = client.delete(f"{'/v1/communities/groups'}/{new_group.id}") assert response.status_code == 204 diff --git a/backend/communities/groups/tests/test_group_list.py b/backend/communities/groups/tests/test_group_list.py index 1ee9f9e36..502cba7c6 100644 --- a/backend/communities/groups/tests/test_group_list.py +++ b/backend/communities/groups/tests/test_group_list.py @@ -3,6 +3,8 @@ Test group_list module. """ +from unittest.mock import patch + import pytest from django.test import Client @@ -26,3 +28,17 @@ def test_group_list(client: Client) -> None: response = client.get(path="/v1/communities/groups") assert response.status_code == 200 + + +def test_group_list_no_pagination(client: Client) -> None: + with patch( + "communities.groups.views.GroupAPIView.paginate_queryset" + ) as mock_paginate: + mock_paginate.return_value = None + + response = client.get(path="/v1/communities/groups") + + assert response.status_code == 200 + + # Verify that paginate_queryset was called. + mock_paginate.assert_called_once() diff --git a/backend/communities/organizations/tests/faq/test_org_faq_create.py b/backend/communities/organizations/tests/faq/test_org_faq_create.py index f6dae017f..369b725f7 100644 --- a/backend/communities/organizations/tests/faq/test_org_faq_create.py +++ b/backend/communities/organizations/tests/faq/test_org_faq_create.py @@ -4,9 +4,7 @@ """ import pytest -from rest_framework.test import APIClient -from authentication.factories import UserFactory from communities.organizations.factories import ( OrganizationFactory, OrganizationFaqFactory, @@ -17,25 +15,16 @@ # MARK: Update -def test_org_faq_create() -> None: +def test_org_faq_create(authenticated_client) -> None: """ - Test Organization FAQ updates. - - Parameters - ---------- - client : Client - A Django test client used to send HTTP requests to the application. + Test Organization FAQ creations. Returns ------- None This test asserts the correctness of status codes (200 for success, 404 for not found). """ - client = APIClient() - - test_username = "test_user" - test_password = "test_password" - user = UserFactory(username=test_username, plaintext_password=test_password) + client, user = authenticated_client user.is_confirmed = True user.verified = True user.is_staff = True @@ -48,21 +37,6 @@ def test_org_faq_create() -> None: test_answer = faqs.answer test_order = faqs.order - # Login to get token. - login_response = client.post( - path="/v1/auth/sign_in", - data={"username": test_username, "password": test_password}, - ) - - assert login_response.status_code == 200 - - # MARK: Update Success - - login_body = login_response.json() - token = login_body["access"] - - client.credentials(HTTP_AUTHORIZATION=f"Token {token}") - response = client.post( path="/v1/communities/organization_faqs", data={ @@ -92,3 +66,31 @@ def test_org_faq_create() -> None: ) assert response.status_code == 400 + + +def test_org_faq_create_unathorized(authenticated_client) -> None: + client, user = authenticated_client + user.is_staff = False + user.save() + + org = OrganizationFactory() + + faqs = OrganizationFaqFactory() + test_question = faqs.question + test_answer = faqs.answer + test_order = faqs.order + + response = client.post( + path="/v1/communities/organization_faqs", + data={ + "iso": "en", + "primary": True, + "question": test_question, + "answer": test_answer, + "order": test_order, + "org": org.id, + }, + format="json", + ) + + assert response.status_code == 403 diff --git a/backend/communities/organizations/tests/faq/test_org_faq_delete.py b/backend/communities/organizations/tests/faq/test_org_faq_delete.py new file mode 100644 index 000000000..dfb9f5e3b --- /dev/null +++ b/backend/communities/organizations/tests/faq/test_org_faq_delete.py @@ -0,0 +1,132 @@ +# SPDX-License-Identifier: AGPL-3.0-or-later +import pytest +from rest_framework.test import APIClient + +from authentication.factories import UserFactory +from authentication.models import UserModel +from communities.organizations.factories import ( + OrganizationFactory, + OrganizationFaqFactory, +) +from communities.organizations.models import OrganizationFaq + + +@pytest.mark.django_db +def test_OrganizationFaqViewSet_destroy(authenticated_client) -> None: + """ + Test OrganizationFaqViewSet destroy method (DELETE request) + + Test cases: + 1. Verify that an unauthenticated user cannot delete an FAQ + 2. Verify that a non-creator, non-staff user cannot delete an FAQ (403 Forbidden) + 3. Verify that the creator can successfully delete an FAQ + 4. Verify that a staff user can delete an FAQ even if they are not the creator + 5. Verify that deleting a non-existent FAQ returns 404 + 6. Verify that the FAQ is actually removed from the database after deletion + """ + client, _ = authenticated_client + unauthenticated_client = APIClient() + + # Create organization creator. + creator: UserModel = UserFactory.create(is_confirmed=True) + + # Create staff user. + staff_user: UserModel = UserFactory.create(is_confirmed=True, is_staff=True) + + # Create an organization with the creator. + org = OrganizationFactory.create(created_by=creator) + + # Create an FAQ for the organization. + faq = OrganizationFaqFactory.create(org=org) + assert OrganizationFaq.objects.filter(id=faq.id).exists() + + # MARK: Unauthenticated DELETE + + # Test 1: Unauthenticated user cannot delete FAQ + response = unauthenticated_client.delete( + f"{'/v1/communities/organization_faqs'}/{faq.id}" + ) + assert response.status_code == 401 + assert OrganizationFaq.objects.filter(id=faq.id).exists() + + # MARK: Unauthorized DELETE + + # Test 2: Regular user (not creator, not staff) cannot delete FAQ. + # Using the authenticated_client fixture user who is not the creator. + response = client.delete(f"{'/v1/communities/organization_faqs'}/{faq.id}") + assert response.status_code == 403 + assert response.data["detail"] == "You are not authorized to delete this FAQ." + assert OrganizationFaq.objects.filter(id=faq.id).exists() + + # MARK: Staff DELETE + + # Test 3: Staff user can delete FAQ even if not the creator. + faq_for_staff = OrganizationFaqFactory.create(org=org) + assert OrganizationFaq.objects.filter(id=faq_for_staff.id).exists() + + staff_client = APIClient() + staff_client.force_authenticate(user=staff_user) + response = staff_client.delete( + f"{'/v1/communities/organization_faqs'}/{faq_for_staff.id}" + ) + assert response.status_code == 204 + assert response.data["message"] == "FAQ deleted successfully." + assert not OrganizationFaq.objects.filter(id=faq_for_staff.id).exists() + + # MARK: Creator DELETE + + # Test 4: Creator can successfully delete their own FAQ. + creator_client = APIClient() + creator_client.force_authenticate(user=creator) + response = creator_client.delete(f"{'/v1/communities/organization_faqs'}/{faq.id}") + assert response.status_code == 204 + assert response.data["message"] == "FAQ deleted successfully." + + # Verify FAQ is removed from database. + assert not OrganizationFaq.objects.filter(id=faq.id).exists() + + # MARK: Non-existent FAQ DELETE + + # Test 5: Deleting a non-existent FAQ returns 404. + fake_uuid = "00000000-0000-0000-0000-000000000000" + response = creator_client.delete( + f"{'/v1/communities/organization_faqs'}/{fake_uuid}" + ) + assert response.status_code == 404 + assert response.data["detail"] == "FAQ not found." + + +@pytest.mark.django_db +def test_OrganizationFaqViewSet_destroy_multiple_faqs(authenticated_client) -> None: + """ + Test that multiple FAQs can be deleted independently. + + This verifies that deleting one FAQ doesn't affect other FAQs + in the same organization. + """ + client, user = authenticated_client + + # Create an organization with the authenticated user as creator. + org = OrganizationFactory.create(created_by=user) + + # Create multiple FAQs for the organization. + faq1 = OrganizationFaqFactory.create(org=org) + faq2 = OrganizationFaqFactory.create(org=org) + faq3 = OrganizationFaqFactory.create(org=org) + + assert OrganizationFaq.objects.filter(org=org).count() == 3 + + # Delete first FAQ. + response = client.delete(f"{'/v1/communities/organization_faqs'}/{faq1.id}") + assert response.status_code == 204 + assert not OrganizationFaq.objects.filter(id=faq1.id).exists() + assert OrganizationFaq.objects.filter(org=org).count() == 2 + + # Delete second FAQ. + response = client.delete(f"{'/v1/communities/organization_faqs'}/{faq2.id}") + assert response.status_code == 204 + assert not OrganizationFaq.objects.filter(id=faq2.id).exists() + assert OrganizationFaq.objects.filter(org=org).count() == 1 + + # Verify third FAQ still exists. + assert OrganizationFaq.objects.filter(id=faq3.id).exists() diff --git a/backend/communities/organizations/tests/faq/test_org_faq_update.py b/backend/communities/organizations/tests/faq/test_org_faq_update.py index 772930c9d..1059b57a3 100644 --- a/backend/communities/organizations/tests/faq/test_org_faq_update.py +++ b/backend/communities/organizations/tests/faq/test_org_faq_update.py @@ -6,9 +6,7 @@ from uuid import uuid4 import pytest -from rest_framework.test import APIClient -from authentication.factories import UserFactory from communities.organizations.factories import ( OrganizationFactory, OrganizationFaqFactory, @@ -19,7 +17,7 @@ # MARK: Update -def test_org_faq_update() -> None: +def test_org_faq_update(authenticated_client) -> None: """ Test Organization FAQ updates. @@ -33,11 +31,7 @@ def test_org_faq_update() -> None: None This test asserts the correctness of status codes (200 for success, 404 for not found). """ - client = APIClient() - - test_username = "test_user" - test_password = "test_password" - user = UserFactory(username=test_username, plaintext_password=test_password) + client, user = authenticated_client user.is_confirmed = True user.verified = True user.is_staff = True @@ -51,19 +45,6 @@ def test_org_faq_update() -> None: test_answer = faqs.answer test_order = faqs.order - # Login to get token. - login_response = client.post( - path="/v1/auth/sign_in", - data={"username": test_username, "password": test_password}, - ) - - assert login_response.status_code == 200 - - # MARK: Update Success - - login_body = login_response.json() - token = login_body["access"] - response = client.put( path=f"/v1/communities/organization_faqs/{test_id}", data={ @@ -74,7 +55,6 @@ def test_org_faq_update() -> None: "answer": test_answer, "order": test_order, }, - headers={"Authorization": f"Token {token}"}, content_type="application/json", ) @@ -91,7 +71,6 @@ def test_org_faq_update() -> None: "answer": test_answer, "order": test_order, }, - headers={"Authorization": f"Token {token}"}, content_type="application/json", ) @@ -99,3 +78,32 @@ def test_org_faq_update() -> None: response_body = response.json() assert response_body["detail"] == "FAQ not found." + + +def test_org_faq_update_unathorized(authenticated_client) -> None: + client, user = authenticated_client + user.is_staff = False + user.save() + + org = OrganizationFactory() + + faqs = OrganizationFaqFactory(org=org) + test_id = faqs.id + test_question = faqs.question + test_answer = faqs.answer + test_order = faqs.order + + response = client.put( + path=f"/v1/communities/organization_faqs/{test_id}", + data={ + "id": test_id, + "iso": "en", + "primary": True, + "question": test_question, + "answer": test_answer, + "order": test_order, + }, + content_type="application/json", + ) + + assert response.status_code == 403 diff --git a/backend/communities/organizations/tests/test_org_api.py b/backend/communities/organizations/tests/test_org_api.py index 95f1e0088..7b4cb8ae9 100644 --- a/backend/communities/organizations/tests/test_org_api.py +++ b/backend/communities/organizations/tests/test_org_api.py @@ -13,9 +13,6 @@ from communities.organizations.models import Organization, OrganizationApplication from content.factories import EntityLocationFactory -# Endpoint used for these tests: -ORGS_URL = "/v1/communities/organizations" - class UserDict(TypedDict): user: UserModel @@ -112,13 +109,15 @@ def test_OrganizationAPIView(logged_in_user, status_types) -> None: OrganizationFactory.create_batch(number_of_orgs) assert Organization.objects.count() == number_of_orgs - response = client.get(ORGS_URL) + response = client.get("/v1/communities/organizations") assert response.status_code == 200 pagination_keys = ["count", "next", "previous", "results"] assert all(key in response.data for key in pagination_keys) - response = client.get(f"{ORGS_URL}?pageSize={test_page_size}") + response = client.get( + f"{'/v1/communities/organizations'}?pageSize={test_page_size}" + ) assert response.status_code == 200 assert len(response.data["results"]) == test_page_size @@ -128,7 +127,7 @@ def test_OrganizationAPIView(logged_in_user, status_types) -> None: # MARK: List POST # Not Authenticated. - response = client.post(ORGS_URL) + response = client.post("/v1/communities/organizations") assert response.status_code == 401 # Authenticated and successful. @@ -151,7 +150,7 @@ def test_OrganizationAPIView(logged_in_user, status_types) -> None: } client.credentials(HTTP_AUTHORIZATION=f"Token {access}") - response = client.post(ORGS_URL, data=payload, format="json") + response = client.post("/v1/communities/organizations", data=payload, format="json") assert response.status_code == 201 assert Organization.objects.filter(org_name=new_org.org_name).exists() @@ -190,7 +189,7 @@ def test_organizationDetailAPIView(logged_in_user, logged_in_created_by_user) -> # MARK: Detail GET - response = client.get(f"{ORGS_URL}/{new_org.id}") + response = client.get(f"{'/v1/communities/organizations'}/{new_org.id}") assert response.status_code == 200 assert response.data["org_name"] == new_org.org_name @@ -198,7 +197,7 @@ def test_organizationDetailAPIView(logged_in_user, logged_in_created_by_user) -> updated_payload = {"org_name": "updated_org_name"} response = client.put( - f"{ORGS_URL}/{new_org.id}", + f"{'/v1/communities/organizations'}/{new_org.id}", data=updated_payload, format="json", ) @@ -206,7 +205,7 @@ def test_organizationDetailAPIView(logged_in_user, logged_in_created_by_user) -> client.credentials(HTTP_AUTHORIZATION=f"Token {access}") response = client.put( - f"{ORGS_URL}/{new_org.id}", + f"{'/v1/communities/organizations'}/{new_org.id}", data=updated_payload, format="json", ) @@ -218,10 +217,10 @@ def test_organizationDetailAPIView(logged_in_user, logged_in_created_by_user) -> # MARK: Detail DELETE client.credentials() - response = client.delete(f"{ORGS_URL}/{new_org.id}") + response = client.delete(f"{'/v1/communities/organizations'}/{new_org.id}") assert response.status_code == 401 client.credentials(HTTP_AUTHORIZATION=f"Token {access}") - response = client.delete(f"{ORGS_URL}/{new_org.id}") + response = client.delete(f"{'/v1/communities/organizations'}/{new_org.id}") assert response.status_code == 204 diff --git a/backend/content/tests/discussion/entry/test_discussion_entry_update.py b/backend/content/tests/discussion/entry/test_discussion_entry_update.py index b8991e1aa..9caf8c85b 100644 --- a/backend/content/tests/discussion/entry/test_discussion_entry_update.py +++ b/backend/content/tests/discussion/entry/test_discussion_entry_update.py @@ -1,44 +1,20 @@ # SPDX-License-Identifier: AGPL-3.0-or-later import pytest -from rest_framework.test import APIClient -from authentication.factories import UserFactory from content.factories import DiscussionEntryFactory, DiscussionFactory pytestmark = pytest.mark.django_db -def test_disc_entry_update(): +def test_disc_entry_update(authenticated_client): """ Test for updating a discussion entry. """ - client = APIClient() - - test_username = "test_user" - test_pass = "test_pass" - user = UserFactory( - username=test_username, - plaintext_password=test_pass, - is_confirmed=True, - verified=True, - ) + client, user = authenticated_client discussion_thread = DiscussionFactory(created_by=user) entry_instance = DiscussionEntryFactory(created_by=user) - # Login to get token. - login_response = client.post( - path="/v1/auth/sign_in", - data={"username": test_username, "password": test_pass}, - ) - - assert login_response.status_code == 200 - - login_body = login_response.json() - token = login_body["access"] - - # Authorized owner updates the entry. - client.credentials(HTTP_AUTHORIZATION=f"Token {token}") response = client.put( path=f"/v1/content/discussion_entries/{entry_instance.id}", data={"discussion": discussion_thread.id}, @@ -57,3 +33,20 @@ def test_disc_entry_update(): response.json()["detail"] == "You are not allowed to update this discussion entry." ) + + +def test_disc_entry_update_not_authorized(authenticated_client): + """ + Test for updating a discussion entry. + """ + client, user = authenticated_client + + discussion_thread = DiscussionFactory() + entry_instance = DiscussionEntryFactory() + + response = client.put( + path=f"/v1/content/discussion_entries/{entry_instance.id}", + data={"discussion": discussion_thread.id}, + ) + + assert response.status_code == 403 diff --git a/backend/content/tests/discussion/test_discussion_create.py b/backend/content/tests/discussion/test_discussion_create.py index 24566b895..4b456a404 100644 --- a/backend/content/tests/discussion/test_discussion_create.py +++ b/backend/content/tests/discussion/test_discussion_create.py @@ -2,18 +2,13 @@ import pytest from rest_framework.test import APIClient -from authentication.factories import UserFactory from content.factories import DiscussionFactory pytestmark = pytest.mark.django_db -def test_discussion_create(): - client = APIClient() - - test_username = "test_user" - test_pass = "test_pass" - user = UserFactory(username=test_username, plaintext_password=test_pass) +def test_discussion_create(authenticated_client): + client, user = authenticated_client user.is_confirmed = True user.verified = True user.save() @@ -22,21 +17,28 @@ def test_discussion_create(): title="Unique Title", category="Unique Category" ) - # Login to get token. - login_response = client.post( - path="/v1/auth/sign_in", - data={"username": test_username, "password": test_pass}, + response = client.post( + path="/v1/content/discussions", + data={"title": discussion_thread.title, "category": discussion_thread.category}, ) - assert login_response.status_code == 200 + assert response.status_code == 201 - login_body = login_response.json() - token = login_body["access"] - client.credentials(HTTP_AUTHORIZATION=f"Token {token}") +def test_discussion_create_not_authorized(): + """ + Test that unauthenticated users cannot create discussions. + """ + client = APIClient() + + discussion_thread = DiscussionFactory( + title="Unique Title", category="Unique Category" + ) + response = client.post( path="/v1/content/discussions", data={"title": discussion_thread.title, "category": discussion_thread.category}, ) - assert response.status_code == 201 + # IsAuthenticatedOrReadOnly returns 401 for unauthenticated write requests. + assert response.status_code == 401 diff --git a/backend/content/tests/discussion/test_discussion_update.py b/backend/content/tests/discussion/test_discussion_update.py index bfe14cdd3..8d55470b8 100644 --- a/backend/content/tests/discussion/test_discussion_update.py +++ b/backend/content/tests/discussion/test_discussion_update.py @@ -1,40 +1,38 @@ # SPDX-License-Identifier: AGPL-3.0-or-later import pytest -from rest_framework.test import APIClient -from authentication.factories import UserFactory from content.factories import DiscussionFactory pytestmark = pytest.mark.django_db -def test_discussion_update(): - client = APIClient() - - test_username = "test_user" - test_pass = "test_pass" - user = UserFactory(username=test_username, plaintext_password=test_pass) +def test_discussion_update(authenticated_client): + client, user = authenticated_client user.is_confirmed = True user.verified = True user.save() thread = DiscussionFactory(created_by=user) - # Login to get token. - login_response = client.post( - path="/v1/auth/sign_in", - data={"username": test_username, "password": test_pass}, + response = client.put( + path=f"/v1/content/discussions/{thread.id}", + data={"title": thread.title}, ) - assert login_response.status_code == 200 + assert response.status_code == 200 - login_body = login_response.json() - token = login_body["access"] - client.credentials(HTTP_AUTHORIZATION=f"Token {token}") +def test_discussion_update_not_authorized(authenticated_client): + client, user = authenticated_client + user.is_confirmed = True + user.verified = True + user.save() + + thread = DiscussionFactory() + response = client.put( path=f"/v1/content/discussions/{thread.id}", data={"title": thread.title}, ) - assert response.status_code == 200 + assert response.status_code == 403 diff --git a/backend/content/tests/topic/test_topic_api.py b/backend/content/tests/topic/test_topic_api.py new file mode 100644 index 000000000..c5274639f --- /dev/null +++ b/backend/content/tests/topic/test_topic_api.py @@ -0,0 +1,63 @@ +# SPDX-License-Identifier: AGPL-3.0-or-later +import pytest +from rest_framework.test import APIClient + +from content.factories import TopicFactory + +pytestmark = pytest.mark.django_db + + +def test_topic_list(): + """ + Test to list all active topics. + """ + client = APIClient() + + # Create active topics. + active_topic_1 = TopicFactory(active=True, type="Environment") + active_topic_2 = TopicFactory(active=True, type="Education") + + # Create inactive topic (should not appear in results). + TopicFactory(active=False, type="Inactive") + + response = client.get(path="/v1/content/topics") + + assert response.status_code == 200 + assert len(response.data) == 2 + + # Verify that only active topics are returned. + returned_ids = [topic["id"] for topic in response.data] + assert str(active_topic_1.id) in returned_ids + assert str(active_topic_2.id) in returned_ids + + # Verify topic data structure. + assert "type" in response.data[0] + assert "active" in response.data[0] + + +def test_topic_list_empty(): + """ + Test to list topics when no active topics exist. + """ + client = APIClient() + + # Create only inactive topics. + TopicFactory(active=False, type="Inactive1") + TopicFactory(active=False, type="Inactive2") + + response = client.get(path="/v1/content/topics") + + assert response.status_code == 200 + assert len(response.data) == 0 + + +def test_topic_list_no_topics(): + """ + Test to list topics when no topics exist at all. + """ + client = APIClient() + + response = client.get(path="/v1/content/topics") + + assert response.status_code == 200 + assert len(response.data) == 0 diff --git a/backend/content/views.py b/backend/content/views.py index eebf4f02f..5737f8673 100644 --- a/backend/content/views.py +++ b/backend/content/views.py @@ -77,13 +77,7 @@ def retrieve(self, request: Request, pk: str | None = None) -> Response: return Response(serializer.data, status=status.HTTP_200_OK) def list(self, request: Request) -> Response: - if request.user.is_authenticated: - query = self.queryset.filter( - Q(is_private=False) | Q(is_private=True, created_by=request.user) - ) - - else: - query = self.queryset.filter() + query = self.queryset.filter() serializer = self.get_serializer(query, many=True) @@ -255,12 +249,7 @@ def retrieve(self, request: Request, pk: str | None = None) -> Response: return Response(serializer.data, status=status.HTTP_200_OK) def list(self, request: Request) -> Response: - if request.user.is_authenticated: - query = self.queryset.filter( - Q(is_private=False) | Q(is_private=True, created_by=request.user) - ) - else: - query = self.queryset.filter(is_private=False) + query = self.queryset.filter(is_private=False) serializer = self.get_serializer(query, many=True) diff --git a/backend/events/tests/faq/test_event_faq_create.py b/backend/events/tests/faq/test_event_faq_create.py index c4ef9e6d8..ec7889318 100644 --- a/backend/events/tests/faq/test_event_faq_create.py +++ b/backend/events/tests/faq/test_event_faq_create.py @@ -4,9 +4,7 @@ """ import pytest -from rest_framework.test import APIClient -from authentication.factories import UserFactory from events.factories import EventFactory, EventFaqFactory pytestmark = pytest.mark.django_db @@ -14,25 +12,16 @@ # MARK: Update -def test_event_faq_create() -> None: +def test_event_faq_create(authenticated_client) -> None: """ Test Event FAQ updates. - Parameters - ---------- - client : Client - A Django test client used to send HTTP requests to the application. - Returns ------- None This test asserts the correctness of status codes (200 for success, 404 for not found). """ - client = APIClient() - - test_username = "test_user" - test_password = "test_password" - user = UserFactory(username=test_username, plaintext_password=test_password) + client, user = authenticated_client user.is_confirmed = True user.verified = True user.is_staff = True @@ -45,21 +34,6 @@ def test_event_faq_create() -> None: test_answer = faqs.answer test_order = faqs.order - # Login to get token. - login_response = client.post( - path="/v1/auth/sign_in", - data={"username": test_username, "password": test_password}, - ) - - assert login_response.status_code == 200 - - # MARK: Update Success - - login_body = login_response.json() - token = login_body["access"] - - client.credentials(HTTP_AUTHORIZATION=f"Token {token}") - response = client.post( path="/v1/events/event_faqs", data={ @@ -89,3 +63,33 @@ def test_event_faq_create() -> None: ) assert response.status_code == 400 + + +def test_event_faq_create_not_authorized(authenticated_client) -> None: + client, user = authenticated_client + user.is_confirmed = True + user.verified = True + user.is_staff = False + user.save() + + event = EventFactory() + + faqs = EventFaqFactory() + test_question = faqs.question + test_answer = faqs.answer + test_order = faqs.order + + response = client.post( + path="/v1/events/event_faqs", + data={ + "iso": "en", + "primary": True, + "question": test_question, + "answer": test_answer, + "order": test_order, + "event": event.id, + }, + format="json", + ) + + assert response.status_code == 403 diff --git a/backend/events/tests/faq/test_event_faq_destroy.py b/backend/events/tests/faq/test_event_faq_destroy.py new file mode 100644 index 000000000..63ddecae6 --- /dev/null +++ b/backend/events/tests/faq/test_event_faq_destroy.py @@ -0,0 +1,147 @@ +# SPDX-License-Identifier: AGPL-3.0-or-later +""" +Test cases for the event FAQ destroy methods. +""" + +from uuid import uuid4 + +import pytest + +from events.factories import EventFactory, EventFaqFactory +from events.models import EventFaq + +pytestmark = pytest.mark.django_db + +# MARK: Destroy + + +def test_event_faq_destroy_success(authenticated_client) -> None: + """ + Test successful Event FAQ deletion by the creator. + + Parameters + ---------- + authenticated_client : tuple + A tuple containing the Django test client and the authenticated user. + + Returns + ------- + None + This test asserts that the FAQ is deleted successfully (status 204) + and that the FAQ no longer exists in the database. + """ + client, user = authenticated_client + + event = EventFactory(created_by=user) + faq = EventFaqFactory(event=event) + test_id = faq.id + + response = client.delete( + path=f"/v1/events/event_faqs/{test_id}", + content_type="application/json", + ) + + assert response.status_code == 204 + assert response.data["message"] == "FAQ deleted successfully." + + # Verify the FAQ was actually deleted from the database. + assert not EventFaq.objects.filter(id=test_id).exists() + + +def test_event_faq_destroy_by_staff(authenticated_client) -> None: + """ + Test successful Event FAQ deletion by a staff member. + + Parameters + ---------- + authenticated_client : tuple + A tuple containing the Django test client and the authenticated user. + + Returns + ------- + None + This test asserts that staff members can delete any FAQ (status 204). + """ + client, user = authenticated_client + user.is_staff = True + user.save() + + # Create an event with a different creator. + other_event = EventFactory() + faq = EventFaqFactory(event=other_event) + test_id = faq.id + + response = client.delete( + path=f"/v1/events/event_faqs/{test_id}", + content_type="application/json", + ) + + assert response.status_code == 204 + assert response.data["message"] == "FAQ deleted successfully." + + # Verify the FAQ was actually deleted from the database. + assert not EventFaq.objects.filter(id=test_id).exists() + + +def test_event_faq_destroy_not_found(authenticated_client) -> None: + """ + Test Event FAQ deletion with non-existent FAQ ID. + + Parameters + ---------- + authenticated_client : tuple + A tuple containing the Django test client and the authenticated user. + + Returns + ------- + None + This test asserts that attempting to delete a non-existent FAQ + returns a 404 status code with appropriate error message. + """ + client, _ = authenticated_client + + test_uuid = uuid4() + + response = client.delete( + path=f"/v1/events/event_faqs/{test_uuid}", + content_type="application/json", + ) + + assert response.status_code == 404 + assert response.data["detail"] == "FAQ not found." + + +def test_event_faq_destroy_not_authorized(authenticated_client) -> None: + """ + Test Event FAQ deletion by unauthorized user. + + Parameters + ---------- + authenticated_client : tuple + A tuple containing the Django test client and the authenticated user. + + Returns + ------- + None + This test asserts that users who are neither the creator nor staff + cannot delete a FAQ (status 403). + """ + client, user = authenticated_client + user.is_staff = False + user.save() + + # Create an event with a different creator. + other_event = EventFactory() + faq = EventFaqFactory(event=other_event) + test_id = faq.id + + response = client.delete( + path=f"/v1/events/event_faqs/{test_id}", + content_type="application/json", + ) + + assert response.status_code == 403 + assert response.data["detail"] == "You are not authorized to delete this FAQ." + + # Verify the FAQ still exists in the database. + assert EventFaq.objects.filter(id=test_id).exists() diff --git a/backend/events/tests/faq/test_event_faq_update.py b/backend/events/tests/faq/test_event_faq_update.py index 0643e5b1f..17cfcb627 100644 --- a/backend/events/tests/faq/test_event_faq_update.py +++ b/backend/events/tests/faq/test_event_faq_update.py @@ -69,3 +69,34 @@ def test_event_faq_update(authenticated_client) -> None: ) assert response.status_code == 404 + + +def test_event_faq_update_not_authorized(authenticated_client) -> None: + client, user = authenticated_client + user.is_confirmed = True + user.verified = True + user.is_staff = False + user.save() + + event = EventFactory() + + faqs = EventFaqFactory(event=event) + test_id = faqs.id + test_question = faqs.question + test_answer = faqs.answer + test_order = faqs.order + + response = client.put( + path=f"/v1/events/event_faqs/{test_id}", + data={ + "id": test_id, + "iso": "en", + "primary": True, + "question": test_question, + "answer": test_answer, + "order": test_order, + }, + content_type="application/json", + ) + + assert response.status_code == 403 diff --git a/backend/events/tests/flag/test_event_flag_list.py b/backend/events/tests/flag/test_event_flag_list.py index b8ca740bd..fe85bb5ff 100644 --- a/backend/events/tests/flag/test_event_flag_list.py +++ b/backend/events/tests/flag/test_event_flag_list.py @@ -1,4 +1,6 @@ # SPDX-License-Identifier: AGPL-3.0-or-later +from unittest.mock import patch + import pytest pytestmark = pytest.mark.django_db @@ -9,3 +11,23 @@ def test_event_flag_list(authenticated_client): response = client.get(path="/v1/events/event_flags") assert response.status_code == 200 + + +def test_event_flag_list_no_pagination(authenticated_client): + """ + Test to list all user flags in case of no pagination. + """ + + client, user = authenticated_client + + with patch( + "authentication.views.UserFlagAPIView.paginate_queryset" + ) as mock_paginate: + mock_paginate.return_value = None + + response = client.get(path="/v1/auth/user_flags") + + assert response.status_code == 200 + + # Verify that paginate_queryset was called. + mock_paginate.assert_called_once() diff --git a/backend/events/tests/social_link/test_event_social_link_delete.py b/backend/events/tests/social_link/test_event_social_link_delete.py index ea4c06e6e..9275bdc78 100644 --- a/backend/events/tests/social_link/test_event_social_link_delete.py +++ b/backend/events/tests/social_link/test_event_social_link_delete.py @@ -31,3 +31,38 @@ def test_social_link_delete_404(authenticated_client): ) assert response.status_code == 404 + + +def test_event_faq_destroy_not_authorized(authenticated_client) -> None: + """ + Test Event FAQ deletion by unauthorized user. + + Parameters + ---------- + authenticated_client : tuple + A tuple containing the Django test client and the authenticated user. + + Returns + ------- + None + This test asserts that users who are neither the creator nor staff + cannot delete a FAQ (status 403). + """ + client, user = authenticated_client + user.is_staff = False + user.save() + + # Create an event with a different creator. + event = EventFactory() + social_links = EventSocialLinkFactory(event=event) + + test_id = social_links.id + + response = client.delete( + path=f"/v1/events/event_social_links/{test_id}", + ) + + assert response.status_code == 403 + assert ( + response.data["detail"] == "You are not authorized to delete this social link." + ) diff --git a/backend/events/tests/test_event_api.py b/backend/events/tests/test_event_api.py index 4aa6dbba0..3b970634f 100644 --- a/backend/events/tests/test_event_api.py +++ b/backend/events/tests/test_event_api.py @@ -1,6 +1,8 @@ # SPDX-License-Identifier: AGPL-3.0-or-later from datetime import datetime from typing import Any, TypedDict +from unittest.mock import patch +from uuid import uuid4 import pytest from dateutil.relativedelta import relativedelta @@ -13,9 +15,6 @@ from events.factories import EventFactory from events.models import Event -# Endpoint used for these tests: -EVENTS_URL = "/v1/events/events" - class UserDict(TypedDict): user: UserModel @@ -84,13 +83,13 @@ def test_EventListAPIView(logged_in_user) -> None: EventFactory.create_batch(number_of_events) assert Event.objects.count() == number_of_events - response = client.get(EVENTS_URL) + response = client.get("/v1/events/events") assert response.status_code == 200 pagination_key = ["count", "next", "previous", "results"] assert all(key in response.data for key in pagination_key) - response = client.get(f"{EVENTS_URL}?pageSize={test_page_size}") + response = client.get(f"{'/v1/events/events'}?pageSize={test_page_size}") assert response.status_code == 200 assert len(response.data["results"]) == test_page_size @@ -100,7 +99,7 @@ def test_EventListAPIView(logged_in_user) -> None: # MARK: List POST # Not Authenticated. - response = client.post(EVENTS_URL) + response = client.post("/v1/events/events") assert response.status_code == 401 # Authenticated and successful. @@ -127,7 +126,7 @@ def test_EventListAPIView(logged_in_user) -> None: } client.credentials(HTTP_AUTHORIZATION=f"Token {token}") - response = client.post(EVENTS_URL, data=payload, format="json") + response = client.post("/v1/events/events", data=payload, format="json") assert response.status_code == 201 assert Event.objects.filter(name=new_event.name).exists() @@ -137,11 +136,25 @@ def test_EventListAPIView(logged_in_user) -> None: new_event.end_time = "2025-10-20T06:00:00Z" payload["start_time"] = new_event.start_time payload["end_time"] = new_event.end_time - response = client.post(EVENTS_URL, data=payload, format="json") + response = client.post("/v1/events/events", data=payload, format="json") assert response.status_code == 400 assert "start time must be before the end time" in str(response.data).lower() +@pytest.mark.django_db +def test_EventListAPIView_no_pagination(authenticated_client) -> None: + client, user = authenticated_client + + with patch("events.views.EventAPIView.paginate_queryset") as mock_paginate: + mock_paginate.return_value = None + + response = client.get(path="/v1/events/events") + assert response.status_code == 200 + + # Verify that paginate_queryset was called. + mock_paginate.assert_called_once() + + @pytest.mark.django_db def test_EventDetailAPIView(logged_in_user) -> None: # type: ignore[no-untyped-def] client = APIClient() @@ -153,7 +166,7 @@ def test_EventDetailAPIView(logged_in_user) -> None: # type: ignore[no-untyped- # MARK: Detail GET - response = client.get(f"{EVENTS_URL}/{new_event.id}") + response = client.get(f"{'/v1/events/events'}/{new_event.id}") assert response.status_code == 200 assert response.data["name"] == new_event.name @@ -169,12 +182,16 @@ def test_EventDetailAPIView(logged_in_user) -> None: # type: ignore[no-untyped- "end_time": end_dt, "terms_checked": True, } - response = client.put(f"{EVENTS_URL}/{new_event.id}", data=payload, format="json") + response = client.put( + f"{'/v1/events/events'}/{new_event.id}", data=payload, format="json" + ) assert response.status_code == 401 client.credentials(HTTP_AUTHORIZATION=f"Token {access}") - response = client.put(f"{EVENTS_URL}/{new_event.id}", data=payload, format="json") + response = client.put( + f"{'/v1/events/events'}/{new_event.id}", data=payload, format="json" + ) assert response.status_code == 200 assert payload["name"] == Event.objects.get(id=new_event.id).name @@ -182,11 +199,39 @@ def test_EventDetailAPIView(logged_in_user) -> None: # type: ignore[no-untyped- # MARK: Detail DELETE client.credentials() - response = client.delete(f"{EVENTS_URL}/{new_event.id}") + response = client.delete(f"{'/v1/events/events'}/{new_event.id}") assert response.status_code == 401 client.credentials(HTTP_AUTHORIZATION=f"Token {access}") - response = client.delete(f"{EVENTS_URL}/{new_event.id}") + response = client.delete(f"{'/v1/events/events'}/{new_event.id}") assert response.status_code == 204 assert not Event.objects.filter(id=new_event.id).exists() + + +@pytest.mark.django_db +def test_EventDetailAPIView_failure(authenticated_client) -> None: + client, user = authenticated_client + uuid = uuid4() + with patch("events.views.EventDetailAPIView.queryset.get") as mock_queryset: + mock_queryset.side_effect = Event.DoesNotExist + + response = client.get(path=f"{'/v1/events/events'}/{uuid}") + + assert response.status_code == 404 + assert response.data["detail"] == "Event Not Found." + + # Verify that paginate_queryset was called. + mock_queryset.assert_called_once() + + # verify for PUT method. + response = client.put(path=f"{'/v1/events/events'}/{uuid}") + + assert response.status_code == 404 + assert response.data["detail"] == "Event Not Found." + + # verify for DELETE method. + response = client.delete(path=f"{'/v1/events/events'}/{uuid}") + + assert response.status_code == 404 + assert response.data["detail"] == "Event Not Found." From 7df22478e671a1a6a274d71b1d6ce724c4c2d2b1 Mon Sep 17 00:00:00 2001 From: Aasim Syed Date: Sat, 29 Nov 2025 13:38:57 -0600 Subject: [PATCH 044/243] Fix: Topic filtering URL persistence, query parameter handling, and combobox compatibility (#1761) * fix: prevent header ComboboxTopics from clearing topics when route changes The header ComboboxTopics component was emitting update:selectedTopics whenever selectedTopics changed, even when the change came from a prop update (route change). This caused it to emit an empty array, which triggered handleSelectedTopicsUpdate and cleared topics from the URL. Fix: Add isUpdatingFromProps flag to track when updates come from props. Skip emitting update:selectedTopics when changes are from prop updates. Only emit when user actually interacts with the combobox. This ensures the header combobox only updates the route when the user selects/deselects topics, not when it receives prop changes from route updates. * fix: add ComboboxButton and setSelectionRange handling to FormSelectorCombobox - Add visible ComboboxButton with chevron icon to open dropdown (required for Headless UI v1) - Add setupInputWrapper function to forward setSelectionRange calls to actual input element - Wrap input and button in relative container with absolute positioned ComboboxOptions - Cache input element references to prevent setSelectionRange errors - This ensures the combobox dropdown opens correctly when clicking the button or input * test: add tests for topics query parameter stability - Add test to verify single topic selection stays in URL - Add test to verify multiple topic selections stay in URL - Both tests verify URL remains stable and doesn't revert - Tests verify the fix for header ComboboxTopics clearing topics * refactor: remove redundant onMounted hook from FormSelectorCombobox The watch with immediate: true already handles initialization, making the onMounted hook unnecessary. This simplifies the code without changing behavior. * fix: normalize topics query parameter to array on page refresh When refreshing a page with a single topic filter, Vue Router returns route.query.topics as a string, but listEvents/listOrganizations expect an array. This caused 's.forEach is not a function' errors. Fix: Normalize topics in filters computed property to always be an array, matching the pattern used in selectedTopics watcher. * refactor: extract topics query normalization to routeUtils Extract normalizeTopicsQuery function to routeUtils.ts for better organization. This groups route-related utilities together and follows the pattern of having specific utility files instead of generic utils.ts. Changes: - Add normalizeTopicsQuery to routeUtils.ts - Update imports in events/index.vue and organizations/index.vue * refactor: update comment about ComboboxButton requirement * fix: use #shared alias for routeUtils import * fix: resolve TypeScript errors in FormSelectorCombobox and routeUtils - Update normalizeTopicsQuery to accept LocationQueryValue type (includes null) - Fix formInputRef and actualInputRef type assertions for Headless UI compatibility - Add proper null handling for Vue Router query parameters * fix:changes selected values correctly by url or manually * chore: changed the naming of normalizeTopicsQuery to normalizeArrayFromURLQuery since it can be used in a generic fashion for url with array values and removed unnecesary imports * fix: when deselcting all options so its reflected * fix: fix formating issue * fix: add sync guard to prevent infinite loops in ComboboxTopics watchers Add isSyncingFromProps flag to prevent emitting updates during prop changes, ensuring the component only emits when users interact with the combobox, not when syncing from route updates. Changes: - Add sync guard flag to track when updates come from props - Prevent emitting during prop updates to avoid circular watcher loops - Ensure empty arrays are properly emitted to clear URL query params - Use nextTick to clear sync flag after prop update completes This prevents the infinite loop issue where prop changes would trigger emissions that updated the route, which triggered new prop changes. --------- Co-authored-by: Nicole <31940739+nicki182@users.noreply.github.com> Co-authored-by: nicki182 --- .../components/combobox/ComboboxTopics.vue | 101 +++++---- .../form/selector/FormSelectorCombobox.vue | 185 ++++++++++++---- .../left/filter/SidebarLeftFilterEvents.vue | 3 +- frontend/app/pages/events/index.vue | 17 +- frontend/app/pages/organizations/index.vue | 17 +- frontend/shared/utils/routeUtils.ts | 22 ++ .../route-query-topics-stability.spec.ts | 203 ++++++++++++++++++ 7 files changed, 446 insertions(+), 102 deletions(-) create mode 100644 frontend/test-e2e/specs/all/navigation/route-query-topics-stability.spec.ts diff --git a/frontend/app/components/combobox/ComboboxTopics.vue b/frontend/app/components/combobox/ComboboxTopics.vue index 08f96afbb..1bc09e2da 100644 --- a/frontend/app/components/combobox/ComboboxTopics.vue +++ b/frontend/app/components/combobox/ComboboxTopics.vue @@ -48,7 +48,7 @@ v-slot="{ selected, active }" @click="inputFocussed = false" as="template" - :value="topic" + :value="topic.value" >
  • ([]); -options.value = topics.value - .map((topic: Topic) => ({ - label: t(GLOBAL_TOPICS.find((t) => t.topic === topic.type)?.label || ""), - value: topic.type as TopicEnum, - id: topic.id, - })) - .sort((a, b) => a.label.localeCompare(b.label)); - const emit = defineEmits<{ (e: "update:selectedTopics", value: TopicEnum[]): void; }>(); -const selectedTopics = ref<{ label: string; value: TopicEnum; id: string }[]>( - [] -); +const selectedTopics = ref([]); +// Flag to prevent emitting when updating from props (prevents infinite loop) +const isSyncingFromProps = ref(false); -watch( - () => props.receivedSelectedTopics, - (newVal) => { - selectedTopics.value = options.value.filter((option) => - newVal?.includes(option.value) - ); - }, - { immediate: true } -); - -// Re-sort options when selectedTopics changes to keep selected items on top. -watch( - selectedTopics, - (newVal) => { - options.value = options.value.sort((a, b) => { - const aSelected = newVal.some( - (selected: { label: string; value: TopicEnum; id: string }) => - selected.value === a.value +const options = computed<{ label: string; value: TopicEnum; id: string }[]>( + () => { + const topicsOptions = topics.value + .map((topic: Topic) => ({ + label: t( + GLOBAL_TOPICS.find((t) => t.topic === topic.type)?.label || "" + ), + value: topic.type as TopicEnum, + id: topic.id, + })) + .sort((a, b) => a.label.localeCompare(b.label)); + return topicsOptions.sort((a, b) => { + const aSelected = selectedTopics.value.some( + (selected: TopicEnum) => selected === a.value ); - const bSelected = newVal.some( - (selected: { label: string; value: TopicEnum; id: string }) => - selected.value === b.value + const bSelected = selectedTopics.value.some( + (selected: TopicEnum) => selected === b.value ); if (aSelected && !bSelected) { @@ -148,20 +134,41 @@ watch( } return a.label.localeCompare(b.label); }); - // Emit only the values of the selected topics. - emit( - "update:selectedTopics", - options.value - .filter((option) => - newVal.some( - (selected: { label: string; value: TopicEnum; id: string }) => - selected.value === option.value - ) - ) - .map((option) => option.value) - ); + } +); +watch( + () => props.receivedSelectedTopics, + (newValReceived: TopicEnum[] | undefined) => { + // Set flag to prevent emission during prop update + isSyncingFromProps.value = true; + + // if incoming prop is empty, clear the local selection + if (!newValReceived || newValReceived.length === 0) { + selectedTopics.value = []; + } else { + // sync selected topics (store primitives) + selectedTopics.value = [...newValReceived]; + } + + // Clear flag after sync completes + nextTick(() => { + isSyncingFromProps.value = false; + }); }, - { immediate: true, deep: true } + { immediate: true } +); +// Re-sort options when selectedTopics changes to keep selected items on top. +watch( + selectedTopics, + (newVal) => { + // Only emit if this change came from user interaction, not from prop update + if (!isSyncingFromProps.value) { + // Emit all changes, including empty arrays to allow URL query param clearing + // Empty arrays are emitted when user deselects all options + emit("update:selectedTopics", newVal ? [...newVal] : []); + } + }, + { immediate: true } ); const query = ref(""); const inputFocussed = ref(false); diff --git a/frontend/app/components/form/selector/FormSelectorCombobox.vue b/frontend/app/components/form/selector/FormSelectorCombobox.vue index 72cced752..46f1655b6 100644 --- a/frontend/app/components/form/selector/FormSelectorCombobox.vue +++ b/frontend/app/components/form/selector/FormSelectorCombobox.vue @@ -1,48 +1,71 @@