Files
VRCX/src/components/dialogs/AvatarDialog/__tests__/useAvatarDialogCommands.test.js
2026-03-16 16:01:38 +09:00

410 lines
14 KiB
JavaScript

import { describe, it, expect, vi, beforeEach } from 'vitest';
import { ref } from 'vue';
import { useAvatarDialogCommands } from '../useAvatarDialogCommands';
// Mock external modules
vi.mock('../../../../api', () => ({
avatarModerationRequest: {
sendAvatarModeration: vi.fn(),
deleteAvatarModeration: vi.fn()
},
avatarRequest: {
saveAvatar: vi.fn(),
deleteAvatar: vi.fn(),
selectFallbackAvatar: vi.fn(),
deleteImposter: vi.fn(),
createImposter: vi.fn(),
uploadAvatarImage: vi.fn()
},
favoriteRequest: {
deleteFavorite: vi.fn()
}
}));
vi.mock('../../../../shared/utils', () => ({
copyToClipboard: vi.fn(),
openExternalLink: vi.fn(),
replaceVrcPackageUrl: vi.fn((url) => url)
}));
vi.mock('../../../../shared/utils/imageUpload', () => ({
handleImageUploadInput: vi.fn(),
readFileAsBase64: vi.fn(),
withUploadTimeout: vi.fn()
}));
vi.mock('../../../../coordinators/imageUploadCoordinator', () => ({
resizeImageToFitLimits: vi.fn(),
uploadImageLegacy: vi.fn()
}));
vi.mock('../../../../coordinators/avatarCoordinator', () => ({
removeAvatarFromCache: vi.fn()
}));
const { copyToClipboard, openExternalLink } =
await import('../../../../shared/utils');
const { favoriteRequest, avatarRequest, avatarModerationRequest } =
await import('../../../../api');
/**
*
*/
function createMockAvatarDialog() {
return ref({
visible: true,
loading: false,
id: 'avtr_test123',
ref: {
name: 'TestAvatar',
description: 'Test desc',
imageUrl: 'https://example.com/img.png',
thumbnailImageUrl: 'https://example.com/thumb.png',
authorId: 'usr_author',
authorName: 'Author',
releaseStatus: 'private',
tags: ['content_horror'],
unityPackageUrl: 'https://example.com/pkg.unitypackage',
styles: { primary: 'style1', secondary: 'style2' }
},
isBlocked: false,
hasImposter: false,
timeSpent: 0,
galleryLoading: false,
galleryImages: []
});
}
/**
*
* @param overrides
*/
function createMockDeps(overrides = {}) {
return {
t: vi.fn((key) => key),
toast: Object.assign(vi.fn(), {
success: vi.fn(),
error: vi.fn(),
promise: vi.fn()
}),
modalStore: {
confirm: vi.fn(() => Promise.resolve({ ok: true })),
prompt: vi.fn(() =>
Promise.resolve({ ok: true, value: 'new_value' })
)
},
userDialog: ref({ id: 'usr_author' }),
currentUser: ref({ id: 'usr_current', currentAvatar: 'avtr_other' }),
cachedAvatars: new Map([
['avtr_test123', { id: 'avtr_test123', authorId: 'usr_author' }]
]),
cachedAvatarModerations: new Map(),
showAvatarDialog: vi.fn(),
showFavoriteDialog: vi.fn(),
applyAvatarModeration: vi.fn((json) => json),
applyAvatar: vi.fn((json) => json),
sortUserDialogAvatars: vi.fn(),
uiStore: { jumpBackDialogCrumb: vi.fn() },
...overrides
};
}
describe('useAvatarDialogCommands', () => {
let avatarDialog;
let deps;
beforeEach(() => {
vi.clearAllMocks();
avatarDialog = createMockAvatarDialog();
deps = createMockDeps();
});
describe('direct commands', () => {
it('Refresh: should call showAvatarDialog with forceRefresh', () => {
const { avatarDialogCommand } = useAvatarDialogCommands(
avatarDialog,
deps
);
avatarDialogCommand('Refresh');
expect(deps.showAvatarDialog).toHaveBeenCalledWith('avtr_test123', {
forceRefresh: true
});
});
it('Share: should copy avatar URL', () => {
const { avatarDialogCommand } = useAvatarDialogCommands(
avatarDialog,
deps
);
avatarDialogCommand('Share');
expect(copyToClipboard).toHaveBeenCalledWith(
'https://vrchat.com/home/avatar/avtr_test123'
);
});
it('Add Favorite: should call showFavoriteDialog', () => {
const { avatarDialogCommand } = useAvatarDialogCommands(
avatarDialog,
deps
);
avatarDialogCommand('Add Favorite');
expect(deps.showFavoriteDialog).toHaveBeenCalledWith(
'avatar',
'avtr_test123'
);
});
it('Download Unity Package: should open external link', () => {
const { avatarDialogCommand } = useAvatarDialogCommands(
avatarDialog,
deps
);
avatarDialogCommand('Download Unity Package');
expect(openExternalLink).toHaveBeenCalled();
});
it('Rename: should show prompt dialog', () => {
const { avatarDialogCommand } = useAvatarDialogCommands(
avatarDialog,
deps
);
avatarDialogCommand('Rename');
expect(deps.modalStore.prompt).toHaveBeenCalled();
});
it('Change Description: should show prompt dialog', () => {
const { avatarDialogCommand } = useAvatarDialogCommands(
avatarDialog,
deps
);
avatarDialogCommand('Change Description');
expect(deps.modalStore.prompt).toHaveBeenCalled();
});
it('Change Image: triggers file input (DOM-dependent, skip in node)', () => {
// This command calls document.getElementById which is only available in browser
// Just verify no error when command is dispatched (DOM interaction tested in e2e)
if (typeof document === 'undefined') {
return;
}
const mockBtn = { click: vi.fn() };
vi.spyOn(document, 'getElementById').mockReturnValue(mockBtn);
const { avatarDialogCommand } = useAvatarDialogCommands(
avatarDialog,
deps
);
avatarDialogCommand('Change Image');
expect(mockBtn.click).toHaveBeenCalled();
vi.restoreAllMocks();
});
});
describe('string callback commands', () => {
it('should delegate to registered callbacks', () => {
const showSetAvatarTagsDialog = vi.fn();
const { avatarDialogCommand, registerCallbacks } =
useAvatarDialogCommands(avatarDialog, deps);
registerCallbacks({ showSetAvatarTagsDialog });
avatarDialogCommand('Change Content Tags');
expect(showSetAvatarTagsDialog).toHaveBeenCalled();
});
it('should not throw when callback is not registered', () => {
const { avatarDialogCommand } = useAvatarDialogCommands(
avatarDialog,
deps
);
expect(() =>
avatarDialogCommand('Change Content Tags')
).not.toThrow();
});
});
describe('confirmed commands', () => {
it('Delete Favorite: should confirm then delete', async () => {
const { avatarDialogCommand } = useAvatarDialogCommands(
avatarDialog,
deps
);
avatarDialogCommand('Delete Favorite');
await vi.waitFor(() => {
expect(deps.modalStore.confirm).toHaveBeenCalled();
});
await vi.waitFor(() => {
expect(favoriteRequest.deleteFavorite).toHaveBeenCalledWith({
objectId: 'avtr_test123'
});
});
});
it('confirmed command should not execute when cancelled', async () => {
deps.modalStore.confirm = vi.fn(() =>
Promise.resolve({ ok: false })
);
const { avatarDialogCommand } = useAvatarDialogCommands(
avatarDialog,
deps
);
avatarDialogCommand('Delete Favorite');
await vi.waitFor(() => {
expect(deps.modalStore.confirm).toHaveBeenCalled();
});
expect(favoriteRequest.deleteFavorite).not.toHaveBeenCalled();
});
it('Select Fallback Avatar: should confirm then select', async () => {
avatarRequest.selectFallbackAvatar.mockResolvedValue({ json: {} });
const { avatarDialogCommand } = useAvatarDialogCommands(
avatarDialog,
deps
);
avatarDialogCommand('Select Fallback Avatar');
await vi.waitFor(() => {
expect(avatarRequest.selectFallbackAvatar).toHaveBeenCalledWith(
{ avatarId: 'avtr_test123' }
);
});
});
it('Block Avatar: should confirm then send moderation', async () => {
avatarModerationRequest.sendAvatarModeration.mockResolvedValue({
json: { targetAvatarId: 'avtr_test123' }
});
const { avatarDialogCommand } = useAvatarDialogCommands(
avatarDialog,
deps
);
avatarDialogCommand('Block Avatar');
await vi.waitFor(() => {
expect(
avatarModerationRequest.sendAvatarModeration
).toHaveBeenCalledWith({
avatarModerationType: 'block',
targetAvatarId: 'avtr_test123'
});
});
});
it('Unblock Avatar: should confirm then delete moderation', async () => {
avatarModerationRequest.deleteAvatarModeration.mockResolvedValue({
params: {
targetAvatarId: 'avtr_test123',
avatarModerationType: 'block'
}
});
const { avatarDialogCommand } = useAvatarDialogCommands(
avatarDialog,
deps
);
avatarDialogCommand('Unblock Avatar');
await vi.waitFor(() => {
expect(
avatarModerationRequest.deleteAvatarModeration
).toHaveBeenCalledWith({
avatarModerationType: 'block',
targetAvatarId: 'avtr_test123'
});
});
});
it('Make Public: should save avatar with public status', async () => {
avatarRequest.saveAvatar.mockResolvedValue({
json: { releaseStatus: 'public' }
});
const { avatarDialogCommand } = useAvatarDialogCommands(
avatarDialog,
deps
);
avatarDialogCommand('Make Public');
await vi.waitFor(() => {
expect(avatarRequest.saveAvatar).toHaveBeenCalledWith({
id: 'avtr_test123',
releaseStatus: 'public'
});
});
});
it('Make Private: should save avatar with private status', async () => {
avatarRequest.saveAvatar.mockResolvedValue({
json: { releaseStatus: 'private' }
});
const { avatarDialogCommand } = useAvatarDialogCommands(
avatarDialog,
deps
);
avatarDialogCommand('Make Private');
await vi.waitFor(() => {
expect(avatarRequest.saveAvatar).toHaveBeenCalledWith({
id: 'avtr_test123',
releaseStatus: 'private'
});
});
});
it('Delete: should delete avatar and update cache', async () => {
avatarRequest.deleteAvatar.mockResolvedValue({
json: { _id: 'avtr_test123', authorId: 'usr_author' }
});
const { avatarDialogCommand } = useAvatarDialogCommands(
avatarDialog,
deps
);
avatarDialogCommand('Delete');
await vi.waitFor(() => {
expect(avatarRequest.deleteAvatar).toHaveBeenCalledWith({
avatarId: 'avtr_test123'
});
});
});
it('Create Imposter: should create imposter', async () => {
avatarRequest.createImposter.mockResolvedValue({ json: {} });
const { avatarDialogCommand } = useAvatarDialogCommands(
avatarDialog,
deps
);
avatarDialogCommand('Create Imposter');
await vi.waitFor(() => {
expect(avatarRequest.createImposter).toHaveBeenCalledWith({
avatarId: 'avtr_test123'
});
});
});
it('Delete Imposter: should delete imposter and refresh', async () => {
avatarRequest.deleteImposter.mockResolvedValue({ json: {} });
const { avatarDialogCommand } = useAvatarDialogCommands(
avatarDialog,
deps
);
avatarDialogCommand('Delete Imposter');
await vi.waitFor(() => {
expect(avatarRequest.deleteImposter).toHaveBeenCalledWith({
avatarId: 'avtr_test123'
});
});
});
});
describe('unknown command', () => {
it('should do nothing for unknown commands', () => {
const { avatarDialogCommand } = useAvatarDialogCommands(
avatarDialog,
deps
);
expect(() => avatarDialogCommand('NonExistent')).not.toThrow();
expect(deps.modalStore.confirm).not.toHaveBeenCalled();
});
});
describe('image upload state', () => {
it('should expose crop dialog state refs', () => {
const { cropDialogOpen, cropDialogFile, changeAvatarImageLoading } =
useAvatarDialogCommands(avatarDialog, deps);
expect(cropDialogOpen.value).toBe(false);
expect(cropDialogFile.value).toBeNull();
expect(changeAvatarImageLoading.value).toBe(false);
});
});
});