diff --git a/src/components/dialogs/GroupDialog/__tests__/useGroupMembers.test.js b/src/components/dialogs/GroupDialog/__tests__/useGroupMembers.test.js index bc817a50..16ea01a9 100644 --- a/src/components/dialogs/GroupDialog/__tests__/useGroupMembers.test.js +++ b/src/components/dialogs/GroupDialog/__tests__/useGroupMembers.test.js @@ -80,12 +80,11 @@ vi.mock('../../../../services/request', () => ({ })); vi.mock('vue-i18n', () => ({ useI18n: () => ({ - t: (key) => key - , - locale: require('vue').ref('en') - }), + t: (key) => key, + locale: require('vue').ref('en') + }), createI18n: () => ({ - global: { t: (key) => key , locale: require('vue').ref('en') }, + global: { t: (key) => key, locale: require('vue').ref('en') }, install: vi.fn() }) })); @@ -96,7 +95,7 @@ vi.mock('worker-timers', () => ({ import { useGroupMembers } from '../useGroupMembers'; import { groupRequest, queryRequest } from '../../../../api'; -import { groupDialogFilterOptions } from '../../../../shared/constants'; +import { FILTER_EVERYONE } from '../../../../shared/constants'; /** * @@ -409,9 +408,7 @@ describe('useGroupMembers', () => { test('marks done on error', async () => { const groupDialog = createGroupDialog(); - queryRequest.fetch.mockRejectedValue( - new Error('fail') - ); + queryRequest.fetch.mockRejectedValue(new Error('fail')); const { loadMoreGroupMembers, @@ -450,7 +447,7 @@ describe('useGroupMembers', () => { describe('setGroupMemberFilter', () => { test('does not reload when filter unchanged', async () => { const { markRaw } = require('vue'); - const filter = markRaw(groupDialogFilterOptions.everyone); + const filter = markRaw(FILTER_EVERYONE); const groupDialog = createGroupDialog(); // Use markRaw to prevent Vue from wrapping the filter in a Proxy groupDialog.value.memberFilter = filter; diff --git a/src/components/dialogs/GroupDialog/useGroupMembers.js b/src/components/dialogs/GroupDialog/useGroupMembers.js index 6ce81045..f17a7dad 100644 --- a/src/components/dialogs/GroupDialog/useGroupMembers.js +++ b/src/components/dialogs/GroupDialog/useGroupMembers.js @@ -2,7 +2,9 @@ import { computed, ref } from 'vue'; import { groupDialogFilterOptions, - groupDialogSortingOptions + groupDialogSortingOptions, + FILTER_EVERYONE, + FILTER_NO_ROLE } from '../../../shared/constants'; import { groupRequest, queryRequest } from '../../../api'; import { debounce } from '../../../shared/utils'; @@ -39,7 +41,7 @@ export function useGroupMembers( return groupDialog.value?.memberSortOrder?.value ?? ''; }, set(value) { - const option = Object.values(groupDialogSortingOptions).find( + const option = groupDialogSortingOptions.find( (item) => item.value === value ); if (option) { @@ -61,11 +63,11 @@ export function useGroupMembers( if (!key) return; if (key === 'everyone') { - setGroupMemberFilter(groupDialogFilterOptions.everyone); + setGroupMemberFilter(FILTER_EVERYONE); return; } if (key === 'usersWithNoRole') { - setGroupMemberFilter(groupDialogFilterOptions.usersWithNoRole); + setGroupMemberFilter(FILTER_NO_ROLE); return; } @@ -82,18 +84,16 @@ export function useGroupMembers( }); const groupDialogMemberFilterGroups = computed(() => { - const filterItems = Object.values(groupDialogFilterOptions).map( - (item) => ({ - value: - item.id === null - ? 'everyone' - : item.id === '' - ? 'usersWithNoRole' - : `role:${item.id}`, - label: t(item.name), - search: t(item.name) - }) - ); + const filterItems = groupDialogFilterOptions.map((item) => ({ + value: + item.id === null + ? 'everyone' + : item.id === '' + ? 'usersWithNoRole' + : `role:${item.id}`, + label: t(item.name), + search: t(item.name) + })); const roleItems = (groupDialog.value?.ref?.roles ?? []) .filter((role) => !role.defaultRole) diff --git a/src/components/ui/dialog/DialogContent.vue b/src/components/ui/dialog/DialogContent.vue index ca33d2f4..54d5cc45 100644 --- a/src/components/ui/dialog/DialogContent.vue +++ b/src/components/ui/dialog/DialogContent.vue @@ -1,5 +1,12 @@ + + diff --git a/src/views/Feed/__tests__/columns.test.js b/src/views/Feed/__tests__/columns.test.js new file mode 100644 index 00000000..19734f79 --- /dev/null +++ b/src/views/Feed/__tests__/columns.test.js @@ -0,0 +1,121 @@ +import { beforeEach, describe, expect, test, vi } from 'vitest'; + +vi.mock('../../../plugins/i18n', () => ({ + i18n: { + global: { + t: (key) => key + } + } +})); + +vi.mock('../../../stores', () => ({ + useGalleryStore: () => ({ + showFullscreenImageDialog: vi.fn() + }) +})); + +vi.mock('../../../coordinators/userCoordinator', () => ({ + showUserDialog: vi.fn() +})); + +vi.mock('../../../shared/utils', () => ({ + formatDateFilter: (value) => value, + statusClass: (value) => value, + timeToText: (value) => value +})); + +vi.mock('../../../components/AvatarInfo.vue', () => ({ + default: 'AvatarInfo' +})); + +vi.mock('../../../components/Location.vue', () => ({ + default: 'Location' +})); + +vi.mock('../../../components/ui/badge', () => ({ + Badge: 'Badge' +})); + +vi.mock('../../../components/ui/button', () => ({ + Button: 'Button' +})); + +vi.mock('../../../components/ui/tooltip', () => ({ + Tooltip: 'Tooltip', + TooltipContent: 'TooltipContent', + TooltipTrigger: 'TooltipTrigger' +})); + +vi.mock('lucide-vue-next', () => ({ + ArrowDown: 'ArrowDown', + ArrowRight: 'ArrowRight', + ArrowUpDown: 'ArrowUpDown', + ChevronDown: 'ChevronDown', + ChevronRight: 'ChevronRight' +})); + +import { columns } from '../columns.jsx'; + +function createElement(type, props, ...children) { + return { + type, + props: props ?? {}, + children: children.flat() + }; +} + +function findNode(node, predicate) { + if (!node) return null; + if (Array.isArray(node)) { + for (const item of node) { + const result = findNode(item, predicate); + if (result) return result; + } + return null; + } + if (predicate(node)) return node; + if (!node.children) return null; + return findNode(node.children, predicate); +} + +describe('views/Feed/columns.jsx', () => { + beforeEach(() => { + globalThis.React = { createElement }; + }); + + test('renders current bio text in detail cell', () => { + const detailCol = columns.find((c) => c.id === 'detail'); + const row = { + original: { + type: 'Bio', + previousBio: 'hello\nold', + bio: 'hello\nnew' + } + }; + + const vnode = detailCol.cell({ row }); + + expect(vnode.type).toBe('span'); + expect(vnode.children).toContain('hello\nnew'); + expect(vnode.props.innerHTML).toBeUndefined(); + }); + + test('keeps multiline bio diff in expanded row', () => { + const expanderCol = columns.find((c) => c.id === 'expander'); + const expanded = expanderCol.meta.expandedRow({ + row: { + original: { + type: 'Bio', + previousBio: 'line1\nline2', + bio: 'line1\nline3' + } + } + }); + const preNode = findNode(expanded, (n) => n.type === 'pre'); + + expect(preNode).toBeTruthy(); + expect(preNode.props.innerHTML).toContain('x-text-added'); + expect(preNode.props.innerHTML).toContain('x-text-removed'); + expect(preNode.props.innerHTML).toContain('
'); + }); +}); diff --git a/src/views/Feed/columns.jsx b/src/views/Feed/columns.jsx index 33439d46..34ca0d97 100644 --- a/src/views/Feed/columns.jsx +++ b/src/views/Feed/columns.jsx @@ -386,9 +386,9 @@ export const columns = [ if (type === 'Bio') { return ( -
+ {original.bio} -
+ ); } @@ -415,7 +415,7 @@ function formatDifference( markerDeletion = '{{text}}' ) { [oldString, newString] = [oldString, newString].map((s) => - s + String(s ?? '') .replaceAll(/&/g, '&') .replaceAll(//g, '>')