Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 17 additions & 5 deletions frontend/app/components/card/discussion/CardDiscussionInput.vue

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please put back the changes from this file. Its a component not being used but will be

Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,9 @@
<Icon class="mx-1" :name="IconMap.MARKDOWN" size="1.25em"></Icon>
</p>
<div class="flex items-center space-x-3">
<Button
<!-- Native button: there is no registered `Button` component, so
using it crashed the app with "Failed to resolve component". -->
<button
@blur="showTooltip = false"
@click="showTooltip = showTooltip == true ? false : true"
@focus="showTooltip = true"
Expand All @@ -130,6 +132,7 @@
'style-action': discussionInput.highRisk,
'style-warn': !discussionInput.highRisk,
}"
type="button"
>
<Icon
v-if="discussionInput.highRisk"
Expand All @@ -141,7 +144,7 @@
v-show="showTooltip"
class="-mt-64 md:-mt-56"
/>
</Button>
</button>
<BtnAction
ariaLabel="i18n.components.card_discussion_input.comment_aria_label"
class="w-small inline-flex items-center justify-center"
Expand All @@ -156,6 +159,7 @@
</template>

<script setup lang="ts">
import { TooltipMentionList } from "#components";
import Link from "@tiptap/extension-link";
import Mention from "@tiptap/extension-mention";
import Placeholder from "@tiptap/extension-placeholder";
Expand All @@ -170,6 +174,9 @@ const props = defineProps<{

const { t } = useI18n();
const markdown = ref("");
// Drives the @mention autocomplete dropdown below; previously this called
// an undefined `Suggestion` identifier, which crashed on mount.
const { getItems, renderer } = useMentionSuggestion(TooltipMentionList);

const isMarkdownPreview = ref("Write");
const isMarkdown = ref(true);
Expand All @@ -189,7 +196,9 @@ watch(markdown, () => {
});

const writeEditor = useEditor({
content: markdown,
// `.value`: useEditor expects the initial string content, not the ref itself
// (passing the ref threw "Unknown node type" from prosemirror).
content: markdown.value,
editable: !isMarkdownPreview.value,
extensions: [
StarterKit,
Expand All @@ -204,8 +213,11 @@ const writeEditor = useEditor({
class:
"hover:underline font-bold rounded-2xl box-decoration-clone px-1 py-0.5",
},
// @ts-expect-error: Ignore mismatched types.
suggestion: Suggestion,
suggestion: {
items: getItems,
// @ts-expect-error: `useMentionSuggestion`'s renderer types are looser than tiptap's.
render: renderer,
},
}),
Markdown,
],
Expand Down
6 changes: 5 additions & 1 deletion frontend/app/components/feed/FeedItem.vue

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here, please return the file on the previous state

Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,12 @@
class="cursor-pointer rounded-md border border-section-div bg-layer-2 p-2 elem-shadow-sm sm:p-3"
>
<div class="flex items-center space-x-3 pb-2">
<!-- `===`, not `=`: the assignment form always evaluated truthy,
which both showed the PEOPLE icon unconditionally and
overwrote item.itemType on every render, making the
mastodon/facebook/instagram branches below unreachable. -->
<Icon
v-if="item.itemType = 'group'"
v-if="item.itemType === 'group'"
:name="IconMap.PEOPLE"
size="1.5em"
/>
Expand Down
7 changes: 6 additions & 1 deletion frontend/app/components/footer/FooterWebsite.vue
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,13 @@
</p>
<div class="mt-1 flex gap-10 sm:mt-0 sm:flex-col sm:gap-0">
<template v-for="(connect, index) in links.connectLinks">
<!-- aria-label uses `connect.ariaLabel`, not `connect.name` (the

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove this comment, its too long and unnecessary

visible label): each link defines its own descriptive
`ariaLabel` text, which was previously being shadowed by a
redundant repeat of the visible "GitHub"/"Matrix"/"Instagram"
label. -->
<a
:aria-label="$t(connect.name)"
:aria-label="$t(connect.ariaLabel)"
class="mt-2 flex items-center space-x-2 text-base text-primary-text focus-brand hover:text-distinct-text"
:class="{ 'mt-3': index === 0 }"
:href="connect.url"
Expand Down
91 changes: 91 additions & 0 deletions frontend/test/components/Collapsable.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import { fireEvent, screen } from "@testing-library/vue";
import { describe, expect, it } from "vitest";

import Collapsable from "../../app/components/Collapsable.vue";
import render from "../render";

const stubs = {
Icon: {
template: '<span :class="$attrs.class" />',
},
};

describe("Collapsable", () => {
it("renders the label on the button", async () => {
await render(Collapsable, {
props: { label: "Section title" },
global: { stubs },
});

expect(screen.getByRole("button", { name: /section title/i })).toBeTruthy();
});

it("hides slot content by default", async () => {
await render(Collapsable, {
props: { label: "Section title" },
slots: { default: "<p>Hidden content</p>" },
global: { stubs },
});

expect(screen.queryByText("Hidden content")).toBeNull();
});

it("shows slot content when isOpen prop is true", async () => {
await render(Collapsable, {
props: { label: "Section title", isOpen: true },
slots: { default: "<p>Visible content</p>" },
global: { stubs },
});

expect(screen.getByText("Visible content")).toBeTruthy();
});

it("toggles slot content open on button click", async () => {
await render(Collapsable, {
props: { label: "Section title" },
slots: { default: "<p>Toggle content</p>" },
global: { stubs },
});

expect(screen.queryByText("Toggle content")).toBeNull();

await fireEvent.click(screen.getByRole("button"));

expect(screen.getByText("Toggle content")).toBeTruthy();
});

it("toggles slot content closed on second button click", async () => {
await render(Collapsable, {
props: { label: "Section title" },
slots: { default: "<p>Toggle content</p>" },
global: { stubs },
});

await fireEvent.click(screen.getByRole("button"));
expect(screen.getByText("Toggle content")).toBeTruthy();

await fireEvent.click(screen.getByRole("button"));
expect(screen.queryByText("Toggle content")).toBeNull();
});

it("applies rotate class to icon when open", async () => {
const { container } = await render(Collapsable, {
props: { label: "Section title", isOpen: true },
global: { stubs },
});

const icon = container.querySelector("span");
expect(icon?.classList.contains("rotate-180")).toBe(true);
});

it("does not apply rotate class to icon when closed", async () => {
const { container } = await render(Collapsable, {
props: { label: "Section title", isOpen: false },
global: { stubs },
});

const icon = container.querySelector("span");
expect(icon?.classList.contains("rotate-180")).toBe(false);
});
});
127 changes: 127 additions & 0 deletions frontend/test/components/EmptyState.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import { screen } from "@testing-library/vue";
import { describe, expect, it } from "vitest";

import EmptyState from "../../app/components/EmptyState.vue";
import render from "../render";

const stubs = {
PageContent: {
template: "<div><slot /></div>",
},
PageCommunityFooter: {
template: "<div><slot /></div>",
},
BtnRouteInternal: {
template: '<a :href="linkTo" :aria-label="ariaLabel" />',
props: ["linkTo", "ariaLabel", "label", "cta", "fontSize"],
},
};

describe("EmptyState", () => {
it("renders the empty state container", async () => {
await render(EmptyState, {
props: { pageType: "organizations", permission: true },
global: { stubs },
});

expect(screen.getByTestId("empty-state")).toBeTruthy();
});

describe("pageType headers", () => {
const pageTypes = [
"organizations",
"groups",
"events",
"resources",
"faq",
"team",
"affiliates",
"tasks",
"discussions",
] as const;

it.each(pageTypes)(
'renders a heading for pageType "%s"',
async (pageType) => {
await render(EmptyState, {
props: { pageType, permission: true },
global: { stubs },
});

expect(screen.getByRole("heading", { level: 2 })).toBeTruthy();
}
);
});

describe("permission prop", () => {
it("shows return home link when permission is false", async () => {
await render(EmptyState, {
props: { pageType: "organizations", permission: false },
global: { stubs },
});

const links = screen.getAllByRole("link");
expect(links.some((l) => l.getAttribute("href") === "/home")).toBe(true);
});

it("shows create organization link when pageType is organizations and has permission", async () => {
await render(EmptyState, {
props: { pageType: "organizations", permission: true },
global: { stubs },
});

const links = screen.getAllByRole("link");
expect(
links.some((l) => l.getAttribute("href") === "/organizations/create")
).toBe(true);
});

it("shows create group link when pageType is groups and has permission", async () => {
await render(EmptyState, {
props: { pageType: "groups", permission: true },
global: { stubs },
});

const links = screen.getAllByRole("link");
expect(
links.some((l) => l.getAttribute("href") === "/groups/create")
).toBe(true);
});

it("shows create event link when pageType is events and has permission", async () => {
await render(EmptyState, {
props: { pageType: "events", permission: true },
global: { stubs },
});

const links = screen.getAllByRole("link");
expect(
links.some((l) => l.getAttribute("href") === "/events/create")
).toBe(true);
});

it("shows create resource link when pageType is resources and has permission", async () => {
await render(EmptyState, {
props: { pageType: "resources", permission: true },
global: { stubs },
});

const links = screen.getAllByRole("link");
expect(
links.some((l) => l.getAttribute("href") === "/resources/create")
).toBe(true);
});

it("does not show a create link when pageType has no create action", async () => {
await render(EmptyState, {
props: { pageType: "faq", permission: true },
global: { stubs },
});

const links = screen.getAllByRole("link");
expect(links).toHaveLength(1);
expect(links[0].getAttribute("href")).toBe("/home");
});
});
});
30 changes: 30 additions & 0 deletions frontend/test/components/btn/BtnRoadMap.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import { screen } from "@testing-library/vue";
import { describe, expect, it } from "vitest";

import BtnRoadMap from "../../../app/components/btn/BtnRoadMap.vue";
import render from "../../render";

const stubs = {
NuxtLink: {
template: '<a :id="$attrs.id" :href="to" :aria-label="$attrs[\'aria-label\']"><slot /></a>',
props: ["to"],
},
};

describe("BtnRoadMap", () => {
it("renders with the correct id", async () => {
await render(BtnRoadMap, { global: { stubs } });

expect(document.getElementById("btn-roadmap")).toBeTruthy();
});

it("links to the roadmap URL", async () => {
await render(BtnRoadMap, { global: { stubs } });

const link = screen.getByRole("link");
expect(link.getAttribute("href")).toBe(
"https://docs.activist.org/activist/product/about/roadmap"
);
});
});
Loading
Loading