From 50ef184fa42ace42775d594bbbe56254947f2dac Mon Sep 17 00:00:00 2001 From: pa Date: Wed, 11 Mar 2026 17:50:58 +0900 Subject: [PATCH] refactor: use item component for favorites world item --- src/views/Favorites/FavoritesWorld.vue | 7 +- .../components/FavoritesWorldItem.vue | 334 +++++++----------- .../components/FavoritesWorldLocalItem.vue | 204 ----------- .../__tests__/FavoritesWorldItem.test.js | 221 ++++++++---- 4 files changed, 288 insertions(+), 478 deletions(-) delete mode 100644 src/views/Favorites/components/FavoritesWorldLocalItem.vue diff --git a/src/views/Favorites/FavoritesWorld.vue b/src/views/Favorites/FavoritesWorld.vue index 204ce6c6..16682635 100644 --- a/src/views/Favorites/FavoritesWorld.vue +++ b/src/views/Favorites/FavoritesWorld.vue @@ -305,8 +305,7 @@ :favorite="favorite" :edit-mode="worldEditMode" :selected="selectedFavoriteWorlds.includes(favorite.id)" - @toggle-select="toggleWorldSelection(favorite.id, $event)" - @click="showWorldDialog(favorite.id)" /> + @toggle-select="toggleWorldSelection(favorite.id, $event)" />
@@ -339,9 +338,7 @@ :group="activeLocalGroupName" :favorite="favorite.favorite" :edit-mode="worldEditMode" - is-local-favorite - @remove-local-world-favorite="removeLocalWorldFavorite" - @click="showWorldDialog(favorite.favorite.id)" /> + is-local-favorite />
diff --git a/src/views/Favorites/components/FavoritesWorldItem.vue b/src/views/Favorites/components/FavoritesWorldItem.vue index 73f759e7..79fa111c 100644 --- a/src/views/Favorites/components/FavoritesWorldItem.vue +++ b/src/views/Favorites/components/FavoritesWorldItem.vue @@ -1,175 +1,105 @@ - + function handleSelfInvite() { + newInstanceSelfInvite(props.favorite.id); + } + + function handleDeleteFavorite() { + if (props.isLocalFavorite) { + removeLocalWorldFavorite(props.favorite.id, props.group); + return; + } + favoriteRequest.deleteFavorite({ objectId: props.favorite.id }); + } + diff --git a/src/views/Favorites/components/FavoritesWorldLocalItem.vue b/src/views/Favorites/components/FavoritesWorldLocalItem.vue deleted file mode 100644 index 52e3d794..00000000 --- a/src/views/Favorites/components/FavoritesWorldLocalItem.vue +++ /dev/null @@ -1,204 +0,0 @@ - - - - - diff --git a/src/views/Favorites/components/__tests__/FavoritesWorldItem.test.js b/src/views/Favorites/components/__tests__/FavoritesWorldItem.test.js index 9e7b66e4..659f5332 100644 --- a/src/views/Favorites/components/__tests__/FavoritesWorldItem.test.js +++ b/src/views/Favorites/components/__tests__/FavoritesWorldItem.test.js @@ -1,14 +1,14 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; import { mount } from '@vue/test-utils'; -import { ref } from 'vue'; const mocks = vi.hoisted(() => ({ - favoriteWorldGroups: null, - shiftHeld: null, showFavoriteDialog: vi.fn(), deleteFavorite: vi.fn(), + removeLocalWorldFavorite: vi.fn(), newInstanceSelfInvite: vi.fn(), - createNewInstance: vi.fn() + createNewInstance: vi.fn(), + showWorldDialog: vi.fn(), + canOpenInstanceInGame: false })); vi.mock('pinia', async (importOriginal) => { @@ -21,10 +21,9 @@ vi.mock('pinia', async (importOriginal) => { vi.mock('vue-i18n', () => ({ useI18n: () => ({ - t: (key) => key - , - locale: require('vue').ref('en') - }) + t: (key) => key, + locale: { value: 'en' } + }) })); vi.mock('@/components/ui/context-menu', () => ({ @@ -37,17 +36,50 @@ vi.mock('@/components/ui/context-menu', () => ({ ContextMenuContent: { template: '
' }, + ContextMenuSeparator: { + template: '
' + }, ContextMenuItem: { emits: ['click'], - template: '' + template: '' } })); +vi.mock('@/components/ui/dropdown-menu', () => ({ + DropdownMenu: { + template: '
' + }, + DropdownMenuTrigger: { + template: '
' + }, + DropdownMenuContent: { + template: '
' + }, + DropdownMenuSeparator: { + template: '
' + }, + DropdownMenuItem: { + emits: ['click'], + template: '' + } +})); + +vi.mock('@/components/ui/item', () => ({ + Item: { + emits: ['click'], + template: '
' + }, + ItemActions: { template: '
' }, + ItemMedia: { template: '
' }, + ItemContent: { template: '
' }, + ItemTitle: { template: '
' }, + ItemDescription: { template: '
' } +})); + vi.mock('@/components/ui/button', () => ({ Button: { emits: ['click'], - template: - '' + template: '' } })); @@ -56,33 +88,25 @@ vi.mock('@/components/ui/checkbox', () => ({ props: ['modelValue'], emits: ['update:modelValue'], template: - '' + '' } })); vi.mock('lucide-vue-next', () => ({ AlertTriangle: { template: '' }, Lock: { template: '' }, - Mail: { template: '' }, - Plus: { template: '' }, - Star: { template: '' }, - Trash2: { template: '' } + MoreHorizontal: { template: '' } })); vi.mock('../../../../stores', () => ({ useFavoriteStore: () => ({ - favoriteWorldGroups: mocks.favoriteWorldGroups, showFavoriteDialog: (...args) => mocks.showFavoriteDialog(...args) }), useInviteStore: () => ({ - newInstanceSelfInvite: (...args) => mocks.newInstanceSelfInvite(...args), - canOpenInstanceInGame: false + canOpenInstanceInGame: mocks.canOpenInstanceInGame }), useInstanceStore: () => ({ createNewInstance: (...args) => mocks.createNewInstance(...args) - }), - useUiStore: () => ({ - shiftHeld: mocks.shiftHeld }) })); @@ -92,90 +116,167 @@ vi.mock('../../../../api', () => ({ } })); +vi.mock('../../../../coordinators/inviteCoordinator', () => ({ + runNewInstanceSelfInviteFlow: (...args) => mocks.newInstanceSelfInvite(...args) +})); + +vi.mock('../../../../coordinators/worldCoordinator', () => ({ + showWorldDialog: (...args) => mocks.showWorldDialog(...args) +})); + +vi.mock('../../../../coordinators/favoriteCoordinator', () => ({ + removeLocalWorldFavorite: (...args) => mocks.removeLocalWorldFavorite(...args) +})); + import FavoritesWorldItem from '../FavoritesWorldItem.vue'; /** * - * @param props + * @param {Record} props */ function mountItem(props = {}) { return mount(FavoritesWorldItem, { props: { favorite: { id: 'wrld_default', - name: 'Default World', - authorName: 'Author' + ref: { + name: 'Default World', + authorName: 'Author', + thumbnailImageUrl: '', + releaseStatus: 'public' + } }, group: 'Favorites', - isLocalFavorite: true, + isLocalFavorite: false, editMode: false, + selected: false, ...props - }, - global: { - stubs: { - TooltipWrapper: { - template: '
' - }, - FavoritesMoveDropdown: { - template: '
' - } - } } }); } +/** + * + * @param wrapper + * @param text + */ +async function clickMenuItem(wrapper, text) { + const buttons = wrapper.findAll('button'); + const target = buttons.find((btn) => btn.text().includes(text)); + expect(target, `menu item not found: ${text}`).toBeTruthy(); + await target.trigger('click'); +} + describe('FavoritesWorldItem.vue', () => { beforeEach(() => { - mocks.favoriteWorldGroups = ref([]); - mocks.shiftHeld = ref(false); mocks.showFavoriteDialog.mockReset(); mocks.deleteFavorite.mockReset(); + mocks.removeLocalWorldFavorite.mockReset(); mocks.newInstanceSelfInvite.mockReset(); mocks.createNewInstance.mockReset(); + mocks.showWorldDialog.mockReset(); + mocks.canOpenInstanceInGame = false; }); - it('renders fallback text when local favorite has no name', () => { + it('opens world details when item is clicked', async () => { + const wrapper = mountItem(); + + await wrapper.get('[data-testid="item"]').trigger('click'); + + expect(mocks.showWorldDialog).toHaveBeenCalledWith('wrld_default'); + }); + + it('renders the full 5-item action menu', () => { + const wrapper = mountItem(); + const text = wrapper.text(); + + expect(text).toContain('common.actions.view_details'); + expect(text).toContain('dialog.world.actions.new_instance'); + expect(text).toContain('dialog.world.actions.new_instance_and_self_invite'); + expect(text).toContain('view.favorite.edit_favorite_tooltip'); + expect(text).toContain('view.favorite.unfavorite_tooltip'); + }); + + it('opens world details from menu action', async () => { + const wrapper = mountItem(); + + await clickMenuItem(wrapper, 'common.actions.view_details'); + + expect(mocks.showWorldDialog).toHaveBeenCalledWith('wrld_default'); + }); + + it('opens edit favorite dialog from menu action', async () => { + const wrapper = mountItem(); + + await clickMenuItem(wrapper, 'view.favorite.edit_favorite_tooltip'); + + expect(mocks.showFavoriteDialog).toHaveBeenCalledWith('world', 'wrld_default'); + }); + + it('emits toggle-select in edit mode for remote favorites', async () => { + const wrapper = mountItem({ editMode: true }); + + await wrapper.get('[data-testid="checkbox"]').setValue(true); + + expect(wrapper.emitted('toggle-select')).toEqual([[true]]); + }); + + it('does not show checkbox in edit mode for local favorites', () => { const wrapper = mountItem({ + editMode: true, + isLocalFavorite: true, favorite: { - id: 'wrld_missing_name' + id: 'wrld_local_1', + name: 'Local World' } }); - expect(wrapper.text()).toContain('wrld_missing_name'); + expect(wrapper.find('[data-testid="checkbox"]').exists()).toBe(false); }); - it('emits local remove event in fallback mode when delete is clicked', async () => { + it('renders fallback id when world ref is missing', () => { const wrapper = mountItem({ favorite: { - id: 'wrld_missing_name' + id: 'wrld_missing_ref' }, - group: 'LocalGroup' + isLocalFavorite: true }); - await wrapper.get('[data-testid="btn"]').trigger('click'); - - expect(wrapper.emitted('remove-local-world-favorite')).toEqual([ - ['wrld_missing_name', 'LocalGroup'] - ]); - expect(mocks.deleteFavorite).not.toHaveBeenCalled(); + expect(wrapper.text()).toContain('wrld_missing_ref'); }); - it('opens local favorite dialog in edit mode when shift is not held', async () => { + it('deletes local favorite via coordinator', async () => { const wrapper = mountItem({ favorite: { id: 'wrld_local_1', - name: 'Local World', - authorName: 'Author' + name: 'Local World' }, - editMode: true + group: 'LocalGroup', + isLocalFavorite: true }); - await wrapper.get('[data-testid="btn"]').trigger('click'); + await clickMenuItem(wrapper, 'view.favorite.delete_tooltip'); - expect(mocks.showFavoriteDialog).toHaveBeenCalledWith( - 'world', - 'wrld_local_1' - ); - expect(wrapper.emitted('remove-local-world-favorite')).toBeUndefined(); + expect(mocks.removeLocalWorldFavorite).toHaveBeenCalledWith('wrld_local_1', 'LocalGroup'); + expect(mocks.deleteFavorite).not.toHaveBeenCalled(); + }); + + it('deletes remote favorite via API', async () => { + const wrapper = mountItem({ isLocalFavorite: false }); + + await clickMenuItem(wrapper, 'view.favorite.unfavorite_tooltip'); + + expect(mocks.deleteFavorite).toHaveBeenCalledWith({ objectId: 'wrld_default' }); + expect(mocks.removeLocalWorldFavorite).not.toHaveBeenCalled(); + }); + + it('runs new instance and self invite actions from menu', async () => { + const wrapper = mountItem(); + + await clickMenuItem(wrapper, 'dialog.world.actions.new_instance'); + await clickMenuItem(wrapper, 'dialog.world.actions.new_instance_and_self_invite'); + + expect(mocks.createNewInstance).toHaveBeenCalledWith('wrld_default'); + expect(mocks.newInstanceSelfInvite).toHaveBeenCalledWith('wrld_default'); }); });