From e6f627f62b8de2ec301aed2ccabb3963c9098d5c Mon Sep 17 00:00:00 2001 From: LivingLikeKrillin <143606756+LivingLikeKrillin@users.noreply.github.com> Date: Sun, 22 Feb 2026 22:31:38 +0900 Subject: [PATCH 01/41] =?UTF-8?q?test:=20=EB=B9=84=EC=A6=88=EB=8B=88?= =?UTF-8?q?=EC=8A=A4=20=EB=A1=9C=EC=A7=81=20=EB=8B=A8=EC=9C=84=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=206=EA=B0=9C=20=EC=B6=94=EA=B0=80=20(Permiss?= =?UTF-8?q?ion,=20me=20model,=20=ED=8F=BC=20=EA=B2=80=EC=A6=9D,=20PKCE)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 권한 제어, 서비스 진입점, 파티룸/플레이리스트/프로필 폼 검증, PKCE 인증 함수에 대한 테스트를 추가하여 커버리지를 개선한다. Co-Authored-By: Claude Opus 4.6 --- .../model/crew.model.test.ts | 232 ++++++++++++++++++ src/entities/me/model/me.model.test.ts | 72 ++++++ .../partyroom-info/model/form.model.test.ts | 102 ++++++++ .../model/playlist-form.model.test.ts | 36 +++ .../edit-profile-bio/model/form.model.test.ts | 63 +++++ src/shared/lib/functions/pkce.test.ts | 105 ++++++++ 6 files changed, 610 insertions(+) create mode 100644 src/entities/current-partyroom/model/crew.model.test.ts create mode 100644 src/entities/me/model/me.model.test.ts create mode 100644 src/entities/partyroom-info/model/form.model.test.ts create mode 100644 src/entities/playlist/model/playlist-form.model.test.ts create mode 100644 src/features/edit-profile-bio/model/form.model.test.ts create mode 100644 src/shared/lib/functions/pkce.test.ts diff --git a/src/entities/current-partyroom/model/crew.model.test.ts b/src/entities/current-partyroom/model/crew.model.test.ts new file mode 100644 index 00000000..2a7ba20b --- /dev/null +++ b/src/entities/current-partyroom/model/crew.model.test.ts @@ -0,0 +1,232 @@ +import { GradeType } from '@/shared/api/http/types/@enums'; +import { GradeComparator, Permission } from './crew.model'; + +// of()가 undefined를 반환할 수 있는 타입이므로, 테스트에서 타입 단언용 헬퍼 +const comparator = (base: GradeType) => GradeComparator.of(base) as GradeComparator; +const perm = (base: GradeType) => Permission.of(base) as Permission; + +describe('GradeComparator', () => { + beforeEach(() => { + // @ts-expect-error private 접근 — 싱글톤 캐시 초기화 + GradeComparator.instances = {}; + }); + + describe('isHigherThan', () => { + it.each([ + [GradeType.HOST, GradeType.MODERATOR, true], + [GradeType.CLUBBER, GradeType.LISTENER, true], + [GradeType.MODERATOR, GradeType.MODERATOR, false], + [GradeType.LISTENER, GradeType.HOST, false], + [GradeType.CLUBBER, GradeType.MODERATOR, false], + ])('%s > %s → %s', (base, target, expected) => { + expect(comparator(base).isHigherThan(target)).toBe(expected); + }); + }); + + describe('isHigherThanOrEqualTo', () => { + it.each([ + [GradeType.HOST, GradeType.HOST, true], + [GradeType.MODERATOR, GradeType.MODERATOR, true], + [GradeType.HOST, GradeType.MODERATOR, true], + [GradeType.LISTENER, GradeType.HOST, false], + ])('%s >= %s → %s', (base, target, expected) => { + expect(comparator(base).isHigherThanOrEqualTo(target)).toBe(expected); + }); + }); + + describe('isLowerThan', () => { + it.each([ + [GradeType.LISTENER, GradeType.HOST, true], + [GradeType.CLUBBER, GradeType.MODERATOR, true], + [GradeType.MODERATOR, GradeType.MODERATOR, false], + [GradeType.HOST, GradeType.LISTENER, false], + ])('%s < %s → %s', (base, target, expected) => { + expect(comparator(base).isLowerThan(target)).toBe(expected); + }); + }); + + describe('isLowerThanOrEqualTo', () => { + it.each([ + [GradeType.LISTENER, GradeType.HOST, true], + [GradeType.MODERATOR, GradeType.MODERATOR, true], + [GradeType.HOST, GradeType.LISTENER, false], + ])('%s <= %s → %s', (base, target, expected) => { + expect(comparator(base).isLowerThanOrEqualTo(target)).toBe(expected); + }); + }); + + describe('higherGrades', () => { + test('MODERATOR보다 높은 등급 반환', () => { + expect(comparator(GradeType.MODERATOR).higherGrades).toEqual([ + GradeType.HOST, + GradeType.COMMUNITY_MANAGER, + ]); + }); + + test('HOST보다 높은 등급은 없음', () => { + expect(comparator(GradeType.HOST).higherGrades).toEqual([]); + }); + }); + + describe('lowerGrades', () => { + test('MODERATOR보다 낮은 등급 반환', () => { + expect(comparator(GradeType.MODERATOR).lowerGrades).toEqual([ + GradeType.CLUBBER, + GradeType.LISTENER, + ]); + }); + + test('LISTENER보다 낮은 등급은 없음', () => { + expect(comparator(GradeType.LISTENER).lowerGrades).toEqual([]); + }); + }); + + describe('싱글톤 캐싱', () => { + test('같은 등급으로 of() 호출 시 동일 인스턴스 반환', () => { + const a = GradeComparator.of(GradeType.MODERATOR); + const b = GradeComparator.of(GradeType.MODERATOR); + expect(a).toBe(b); + }); + }); +}); + +describe('Permission', () => { + beforeEach(() => { + // @ts-expect-error private 접근 + Permission.instances = {}; + // @ts-expect-error private 접근 + GradeComparator.instances = {}; + }); + + describe('canAdjustGrade', () => { + it.each([ + [GradeType.HOST, GradeType.MODERATOR, true], + [GradeType.MODERATOR, GradeType.CLUBBER, true], + [GradeType.MODERATOR, GradeType.MODERATOR, false], + [GradeType.CLUBBER, GradeType.LISTENER, false], + [GradeType.LISTENER, GradeType.CLUBBER, false], + ])('%s가 %s 등급 조정 → %s', (base, target, expected) => { + expect(perm(base).canAdjustGrade(target)).toBe(expected); + }); + }); + + describe('canRemoveChatMessage', () => { + it.each([ + [GradeType.HOST, GradeType.CLUBBER, true], + [GradeType.MODERATOR, GradeType.LISTENER, true], + [GradeType.MODERATOR, GradeType.MODERATOR, false], + [GradeType.CLUBBER, GradeType.LISTENER, false], + ])('%s가 %s 채팅 삭제 → %s', (base, target, expected) => { + expect(perm(base).canRemoveChatMessage(target)).toBe(expected); + }); + }); + + describe('MODERATOR 이상이면 true인 권한들', () => { + const moderatorOrAbove = [GradeType.HOST, GradeType.COMMUNITY_MANAGER, GradeType.MODERATOR]; + const belowModerator = [GradeType.CLUBBER, GradeType.LISTENER]; + + it.each(moderatorOrAbove)('%s는 canViewPenalties true', (grade) => { + expect(perm(grade).canViewPenalties()).toBe(true); + }); + + it.each(belowModerator)('%s는 canViewPenalties false', (grade) => { + expect(perm(grade).canViewPenalties()).toBe(false); + }); + + it.each(moderatorOrAbove)('%s는 canSkipPlayback true', (grade) => { + expect(perm(grade).canSkipPlayback()).toBe(true); + }); + + it.each(belowModerator)('%s는 canSkipPlayback false', (grade) => { + expect(perm(grade).canSkipPlayback()).toBe(false); + }); + + it.each(moderatorOrAbove)('%s는 canLockDjingQueue true', (grade) => { + expect(perm(grade).canLockDjingQueue()).toBe(true); + }); + + it.each(belowModerator)('%s는 canLockDjingQueue false', (grade) => { + expect(perm(grade).canLockDjingQueue()).toBe(false); + }); + + it.each(moderatorOrAbove)('%s는 canUnlockDjingQueue true', (grade) => { + expect(perm(grade).canUnlockDjingQueue()).toBe(true); + }); + + it.each(belowModerator)('%s는 canUnlockDjingQueue false', (grade) => { + expect(perm(grade).canUnlockDjingQueue()).toBe(false); + }); + + it.each(moderatorOrAbove)('%s는 canDeleteDjFromQueue true', (grade) => { + expect(perm(grade).canDeleteDjFromQueue()).toBe(true); + }); + + it.each(belowModerator)('%s는 canDeleteDjFromQueue false', (grade) => { + expect(perm(grade).canDeleteDjFromQueue()).toBe(false); + }); + }); + + describe('HOST만 true인 권한들', () => { + test('HOST는 canEdit true', () => { + expect(perm(GradeType.HOST).canEdit()).toBe(true); + }); + + it.each([ + GradeType.COMMUNITY_MANAGER, + GradeType.MODERATOR, + GradeType.CLUBBER, + GradeType.LISTENER, + ])('%s는 canEdit false', (grade) => { + expect(perm(grade).canEdit()).toBe(false); + }); + + test('HOST는 canClose true', () => { + expect(perm(GradeType.HOST).canClose()).toBe(true); + }); + + it.each([ + GradeType.COMMUNITY_MANAGER, + GradeType.MODERATOR, + GradeType.CLUBBER, + GradeType.LISTENER, + ])('%s는 canClose false', (grade) => { + expect(perm(grade).canClose()).toBe(false); + }); + }); + + describe('미구현 메서드 Error throw', () => { + test('canRegisterDj 호출 시 에러 발생', () => { + expect(() => perm(GradeType.HOST).canRegisterDj()).toThrow('Not Impl yet'); + }); + + test('canUnregisterDj 호출 시 에러 발생', () => { + expect(() => perm(GradeType.HOST).canUnregisterDj()).toThrow('Not Impl yet'); + }); + }); + + describe('adjustableGrades', () => { + test('HOST의 adjustableGrades는 자기보다 낮은 모든 등급', () => { + expect(perm(GradeType.HOST).adjustableGrades).toEqual([ + GradeType.COMMUNITY_MANAGER, + GradeType.MODERATOR, + GradeType.CLUBBER, + GradeType.LISTENER, + ]); + }); + + test('MODERATOR의 adjustableGrades는 CLUBBER, LISTENER', () => { + expect(perm(GradeType.MODERATOR).adjustableGrades).toEqual([ + GradeType.CLUBBER, + GradeType.LISTENER, + ]); + }); + }); + + describe('싱글톤 캐싱', () => { + test('같은 등급으로 of() 호출 시 동일 인스턴스 반환', () => { + const a = Permission.of(GradeType.HOST); + const b = Permission.of(GradeType.HOST); + expect(a).toBe(b); + }); + }); +}); diff --git a/src/entities/me/model/me.model.test.ts b/src/entities/me/model/me.model.test.ts new file mode 100644 index 00000000..021042d6 --- /dev/null +++ b/src/entities/me/model/me.model.test.ts @@ -0,0 +1,72 @@ +import { ActivityType, AuthorityTier } from '@/shared/api/http/types/@enums'; +import type { Model } from './me.model'; +import { serviceEntry, score, registrationDate } from './me.model'; + +const createModel = (overrides: Partial = {}): Model => ({ + uid: 'test-uid', + authorityTier: AuthorityTier.FM, + registrationDate: '2024-06-23', + profileUpdated: true, + nickname: 'tester', + avatarBodyUri: '', + avatarFaceUri: '', + avatarIconUri: '', + activitySummaries: [], + offsetX: 0, + offsetY: 0, + scale: 1, + ...overrides, +}); + +describe('me model', () => { + describe('serviceEntry', () => { + test('null이면 루트 경로 반환', () => { + expect(serviceEntry(null)).toBe('/'); + }); + + test('프로필 미완성이면 설정 페이지 반환', () => { + const model = createModel({ profileUpdated: false }); + expect(serviceEntry(model)).toBe('/settings/profile'); + }); + + test('프로필 완성이면 파티 목록 반환', () => { + const model = createModel({ profileUpdated: true }); + expect(serviceEntry(model)).toBe('/parties'); + }); + }); + + describe('score', () => { + test('activityType이 summaries에 존재하면 해당 score 반환', () => { + const model = createModel({ + activitySummaries: [ + { activityType: ActivityType.DJ_PNT, score: 150 }, + { activityType: ActivityType.REF_LINK, score: 30 }, + ], + }); + expect(score(model, ActivityType.DJ_PNT)).toBe(150); + }); + + test('activityType이 summaries에 미존재하면 0 반환', () => { + const model = createModel({ + activitySummaries: [{ activityType: ActivityType.DJ_PNT, score: 150 }], + }); + expect(score(model, ActivityType.REF_LINK)).toBe(0); + }); + + test('activitySummaries 빈 배열이면 0 반환', () => { + const model = createModel({ activitySummaries: [] }); + expect(score(model, ActivityType.DJ_PNT)).toBe(0); + }); + }); + + describe('registrationDate', () => { + it.each([ + ['2024-06-23', '2024.06.23'], + ['2023-01-05', '2023.01.05'], + ['2025-12-31', '2025.12.31'], + ])('%s → %s', (input, expected) => { + const model = createModel({ registrationDate: input }); + expect(registrationDate(model)).toBe(expected); + }); + }); +}); diff --git a/src/entities/partyroom-info/model/form.model.test.ts b/src/entities/partyroom-info/model/form.model.test.ts new file mode 100644 index 00000000..672c67b5 --- /dev/null +++ b/src/entities/partyroom-info/model/form.model.test.ts @@ -0,0 +1,102 @@ +import { getSchema } from './form.model'; + +const mockDictionary = { + common: { + ec: { + char_field_required: '필수 입력입니다', + char_limit_30: '30자 이내로 입력해주세요', + char_limit_50: '50자 이내로 입력해주세요', + }, + }, + createparty: { + para: { + noti_djing_limit: '최소 3명 이상이어야 합니다', + }, + }, +} as any; + +const schema = getSchema(mockDictionary); + +const validBase = { + name: '파티룸', + introduce: '소개글입니다', + limit: 10, +}; + +describe('partyroom form schema', () => { + describe('name 필드', () => { + it.each(['한글', 'Test', '123', '한Test123'])('유효: "%s"', (name) => { + expect(schema.safeParse({ ...validBase, name }).success).toBe(true); + }); + + test('경계값: 1자', () => { + expect(schema.safeParse({ ...validBase, name: '가' }).success).toBe(true); + }); + + test('경계값: 30자', () => { + expect(schema.safeParse({ ...validBase, name: '가'.repeat(30) }).success).toBe(true); + }); + + test('무효: 빈 문자열', () => { + expect(schema.safeParse({ ...validBase, name: '' }).success).toBe(false); + }); + + test('무효: 31자 초과', () => { + expect(schema.safeParse({ ...validBase, name: '가'.repeat(31) }).success).toBe(false); + }); + + it.each(['파티$', 'Test Room', '파티!@#'])('무효: 특수문자/공백 "%s"', (name) => { + expect(schema.safeParse({ ...validBase, name }).success).toBe(false); + }); + }); + + describe('introduce 필드', () => { + test('유효: 일반 텍스트', () => { + expect(schema.safeParse({ ...validBase, introduce: '안녕하세요' }).success).toBe(true); + }); + + test('경계값: 1자', () => { + expect(schema.safeParse({ ...validBase, introduce: '가' }).success).toBe(true); + }); + + test('경계값: 50자', () => { + expect(schema.safeParse({ ...validBase, introduce: '가'.repeat(50) }).success).toBe(true); + }); + + test('무효: 빈 문자열', () => { + expect(schema.safeParse({ ...validBase, introduce: '' }).success).toBe(false); + }); + + test('무효: 51자 초과', () => { + expect(schema.safeParse({ ...validBase, introduce: '가'.repeat(51) }).success).toBe(false); + }); + }); + + describe('domain 필드', () => { + test('유효: undefined', () => { + expect(schema.safeParse({ ...validBase, domain: undefined }).success).toBe(true); + }); + + it.each(['example', 'sub.domain'])('유효: "%s"', (domain) => { + expect(schema.safeParse({ ...validBase, domain }).success).toBe(true); + }); + + test('무효: 공백 포함', () => { + expect(schema.safeParse({ ...validBase, domain: 'my domain' }).success).toBe(false); + }); + }); + + describe('limit 필드', () => { + it.each([3, 100])('유효: %d', (limit) => { + expect(schema.safeParse({ ...validBase, limit }).success).toBe(true); + }); + + test('무효: 최소값 미만 (2)', () => { + expect(schema.safeParse({ ...validBase, limit: 2 }).success).toBe(false); + }); + + test('무효: 음수', () => { + expect(schema.safeParse({ ...validBase, limit: -5 }).success).toBe(false); + }); + }); +}); diff --git a/src/entities/playlist/model/playlist-form.model.test.ts b/src/entities/playlist/model/playlist-form.model.test.ts new file mode 100644 index 00000000..2c0dcc44 --- /dev/null +++ b/src/entities/playlist/model/playlist-form.model.test.ts @@ -0,0 +1,36 @@ +import { getSchema } from './playlist-form.model'; + +const mockDictionary = { + common: { + ec: { + char_field_required: '필수 입력입니다', + char_limit_20: '20자 이내로 입력해주세요', + }, + }, +} as any; + +const schema = getSchema(mockDictionary); + +describe('playlist form schema', () => { + describe('name 필드', () => { + test('유효: 일반 텍스트', () => { + expect(schema.safeParse({ name: '플리이름' }).success).toBe(true); + }); + + test('경계값: 1자', () => { + expect(schema.safeParse({ name: '가' }).success).toBe(true); + }); + + test('경계값: 20자', () => { + expect(schema.safeParse({ name: '가'.repeat(20) }).success).toBe(true); + }); + + test('무효: 빈 문자열', () => { + expect(schema.safeParse({ name: '' }).success).toBe(false); + }); + + test('무효: 21자 초과', () => { + expect(schema.safeParse({ name: '가'.repeat(21) }).success).toBe(false); + }); + }); +}); diff --git a/src/features/edit-profile-bio/model/form.model.test.ts b/src/features/edit-profile-bio/model/form.model.test.ts new file mode 100644 index 00000000..191f684a --- /dev/null +++ b/src/features/edit-profile-bio/model/form.model.test.ts @@ -0,0 +1,63 @@ +import { getSchema } from './form.model'; + +const mockDictionary = { + common: { + ec: { + char_field_required: '필수 입력입니다', + char_limit_12: '12자 이내로 입력해주세요', + char_limit_50: '50자 이내로 입력해주세요', + }, + }, +} as any; + +const schema = getSchema(mockDictionary); + +describe('edit-profile-bio form schema', () => { + describe('nickname 필드', () => { + it.each(['한글', 'Test', '123', '한Test123'])('유효: "%s"', (nickname) => { + expect(schema.safeParse({ nickname }).success).toBe(true); + }); + + test('경계값: 1자', () => { + expect(schema.safeParse({ nickname: '가' }).success).toBe(true); + }); + + test('경계값: 12자', () => { + expect(schema.safeParse({ nickname: '가'.repeat(12) }).success).toBe(true); + }); + + test('무효: 빈 문자열', () => { + expect(schema.safeParse({ nickname: '' }).success).toBe(false); + }); + + test('무효: 13자 초과', () => { + expect(schema.safeParse({ nickname: '가'.repeat(13) }).success).toBe(false); + }); + + it.each(['닉네임$', 'Test Name', '유저!@#'])('무효: 특수문자/공백 "%s"', (nickname) => { + expect(schema.safeParse({ nickname }).success).toBe(false); + }); + }); + + describe('introduction 필드', () => { + test('유효: undefined', () => { + expect(schema.safeParse({ nickname: '테스트' }).success).toBe(true); + }); + + test('유효: 빈 문자열', () => { + expect(schema.safeParse({ nickname: '테스트', introduction: '' }).success).toBe(true); + }); + + test('유효: 50자 텍스트', () => { + expect(schema.safeParse({ nickname: '테스트', introduction: '가'.repeat(50) }).success).toBe( + true + ); + }); + + test('무효: 51자 초과', () => { + expect(schema.safeParse({ nickname: '테스트', introduction: '가'.repeat(51) }).success).toBe( + false + ); + }); + }); +}); diff --git a/src/shared/lib/functions/pkce.test.ts b/src/shared/lib/functions/pkce.test.ts new file mode 100644 index 00000000..e01d4bed --- /dev/null +++ b/src/shared/lib/functions/pkce.test.ts @@ -0,0 +1,105 @@ +import { + parseCallbackParams, + setStoredState, + getStoredState, + clearStoredState, + getStoredCodeVerifier, + clearStoredCodeVerifier, + createPKCEParams, +} from './pkce'; + +describe('PKCE', () => { + describe('parseCallbackParams', () => { + const originalLocation = window.location; + + afterEach(() => { + Object.defineProperty(window, 'location', { + value: originalLocation, + writable: true, + }); + }); + + test('code와 state 파라미터 파싱', () => { + Object.defineProperty(window, 'location', { + value: { search: '?code=abc123&state=xyz' }, + writable: true, + }); + expect(parseCallbackParams()).toEqual({ + code: 'abc123', + state: 'xyz', + error: undefined, + error_description: undefined, + }); + }); + + test('에러 파라미터 파싱', () => { + Object.defineProperty(window, 'location', { + value: { search: '?error=access_denied&error_description=User+denied' }, + writable: true, + }); + const result = parseCallbackParams(); + expect(result.error).toBe('access_denied'); + expect(result.error_description).toBe('User denied'); + }); + + test('파라미터 없으면 undefined 필드 반환', () => { + Object.defineProperty(window, 'location', { + value: { search: '' }, + writable: true, + }); + expect(parseCallbackParams()).toEqual({ + code: undefined, + state: undefined, + error: undefined, + error_description: undefined, + }); + }); + }); + + describe('Storage 함수', () => { + beforeEach(() => { + sessionStorage.clear(); + }); + + test('state 저장 → 조회 → 삭제 라이프사이클', () => { + expect(getStoredState()).toBeNull(); + + setStoredState('my-state'); + expect(getStoredState()).toBe('my-state'); + + clearStoredState(); + expect(getStoredState()).toBeNull(); + }); + + test('codeVerifier 조회 → 삭제 라이프사이클', async () => { + expect(getStoredCodeVerifier()).toBeNull(); + + await createPKCEParams(); + expect(getStoredCodeVerifier()).not.toBeNull(); + + clearStoredCodeVerifier(); + expect(getStoredCodeVerifier()).toBeNull(); + }); + }); + + describe('createPKCEParams', () => { + beforeEach(() => { + sessionStorage.clear(); + }); + + test('codeVerifier가 생성되고 storage에 저장됨', async () => { + const { codeVerifier } = await createPKCEParams(); + + expect(codeVerifier).toBeDefined(); + expect(typeof codeVerifier).toBe('string'); + expect(codeVerifier.length).toBeGreaterThan(0); + expect(getStoredCodeVerifier()).toBe(codeVerifier); + }); + + test('URL-safe Base64 형식 (+ / = 미포함)', async () => { + const { codeVerifier } = await createPKCEParams(); + + expect(codeVerifier).not.toMatch(/[+/=]/); + }); + }); +}); From a5a249a8a1acf46894b0838f6a57b7053df4754e Mon Sep 17 00:00:00 2001 From: LivingLikeKrillin <143606756+LivingLikeKrillin@users.noreply.github.com> Date: Sun, 22 Feb 2026 22:48:37 +0900 Subject: [PATCH 02/41] =?UTF-8?q?test:=202=EB=8B=A8=EA=B3=84=20=EB=8B=A8?= =?UTF-8?q?=EC=9C=84=20=ED=85=8C=EC=8A=A4=ED=8A=B8=206=EA=B0=9C=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20(alert-message,=20dj,=20avatar-body,=20eve?= =?UTF-8?q?nt-emitter,=20silent,=20parse-href)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 타입 가드, 데이터 변환, 아바타 잠금 판단, 이벤트 발행자, Promise 처리, URL 파라미터 처리에 대한 테스트를 추가한다. Co-Authored-By: Claude Opus 4.6 --- .../model/alert-message.model.test.ts | 48 ++++++++ .../current-partyroom/model/dj.model.test.ts | 34 ++++++ .../model/avatar-body.model.test.ts | 106 ++++++++++++++++++ .../lib/functions/event-emitter.test.ts | 76 +++++++++++++ src/shared/lib/functions/silent.test.ts | 55 +++++++++ src/shared/lib/router/parse-href.test.ts | 42 +++++++ 6 files changed, 361 insertions(+) create mode 100644 src/entities/current-partyroom/model/alert-message.model.test.ts create mode 100644 src/entities/current-partyroom/model/dj.model.test.ts create mode 100644 src/features/edit-profile-avatar/model/avatar-body.model.test.ts create mode 100644 src/shared/lib/functions/event-emitter.test.ts create mode 100644 src/shared/lib/functions/silent.test.ts create mode 100644 src/shared/lib/router/parse-href.test.ts diff --git a/src/entities/current-partyroom/model/alert-message.model.test.ts b/src/entities/current-partyroom/model/alert-message.model.test.ts new file mode 100644 index 00000000..b2ecc27a --- /dev/null +++ b/src/entities/current-partyroom/model/alert-message.model.test.ts @@ -0,0 +1,48 @@ +import { GradeType, PenaltyType } from '@/shared/api/http/types/@enums'; +import { + isPenaltyAlertMessage, + isGradeAdjustedAlertMessage, + type Model, +} from './alert-message.model'; + +describe('alert-message model', () => { + describe('isPenaltyAlertMessage', () => { + it.each([ + PenaltyType.CHAT_BAN_30_SECONDS, + PenaltyType.ONE_TIME_EXPULSION, + PenaltyType.PERMANENT_EXPULSION, + ])('PenaltyType.%s → true', (type) => { + const message: Model = { type, reason: '규칙 위반' }; + expect(isPenaltyAlertMessage(message)).toBe(true); + }); + + test('grade-adjusted 타입은 false', () => { + const message: Model = { + type: 'grade-adjusted', + prev: GradeType.LISTENER, + next: GradeType.CLUBBER, + }; + expect(isPenaltyAlertMessage(message)).toBe(false); + }); + }); + + describe('isGradeAdjustedAlertMessage', () => { + test('grade-adjusted 타입은 true', () => { + const message: Model = { + type: 'grade-adjusted', + prev: GradeType.LISTENER, + next: GradeType.MODERATOR, + }; + expect(isGradeAdjustedAlertMessage(message)).toBe(true); + }); + + it.each([ + PenaltyType.CHAT_BAN_30_SECONDS, + PenaltyType.ONE_TIME_EXPULSION, + PenaltyType.PERMANENT_EXPULSION, + ])('PenaltyType.%s → false', (type) => { + const message: Model = { type, reason: '규칙 위반' }; + expect(isGradeAdjustedAlertMessage(message)).toBe(false); + }); + }); +}); diff --git a/src/entities/current-partyroom/model/dj.model.test.ts b/src/entities/current-partyroom/model/dj.model.test.ts new file mode 100644 index 00000000..6effc3d7 --- /dev/null +++ b/src/entities/current-partyroom/model/dj.model.test.ts @@ -0,0 +1,34 @@ +import type { Dj } from '@/shared/api/http/types/partyrooms'; +import { toListItemConfig } from './dj.model'; + +describe('dj model', () => { + describe('toListItemConfig', () => { + test('Dj 모델을 DjListItemUserConfig로 변환', () => { + const dj: Dj = { + crewId: 1, + orderNumber: 1, + nickname: 'DJ테스트', + avatarIconUri: '/images/avatar.png', + }; + + expect(toListItemConfig(dj)).toEqual({ + username: 'DJ테스트', + src: '/images/avatar.png', + }); + }); + + test('빈 문자열 필드도 정상 변환', () => { + const dj: Dj = { + crewId: 2, + orderNumber: 3, + nickname: '', + avatarIconUri: '', + }; + + expect(toListItemConfig(dj)).toEqual({ + username: '', + src: '', + }); + }); + }); +}); diff --git a/src/features/edit-profile-avatar/model/avatar-body.model.test.ts b/src/features/edit-profile-avatar/model/avatar-body.model.test.ts new file mode 100644 index 00000000..4252d01e --- /dev/null +++ b/src/features/edit-profile-avatar/model/avatar-body.model.test.ts @@ -0,0 +1,106 @@ +import type { Me } from '@/entities/me'; +import { ActivityType, AuthorityTier, ObtainmentType } from '@/shared/api/http/types/@enums'; +import type { AvatarBody } from '@/shared/api/http/types/users'; +import { locked } from './avatar-body.model'; + +const createMe = (overrides: Partial = {}): Me.Model => ({ + uid: 'test-uid', + authorityTier: AuthorityTier.FM, + registrationDate: '2024-06-23', + profileUpdated: true, + nickname: 'tester', + avatarBodyUri: '', + avatarFaceUri: '', + avatarIconUri: '', + activitySummaries: [], + offsetX: 0, + offsetY: 0, + scale: 1, + ...overrides, +}); + +const createBody = (overrides: Partial = {}): AvatarBody => ({ + id: 1, + name: 'body1', + resourceUri: '/body.png', + available: true, + obtainableType: ObtainmentType.BASIC, + obtainableScore: 0, + combinable: true, + defaultSetting: false, + ...overrides, +}); + +const mockDictionary = { + common: { + para: { + 'points_to_unlock\t': '{{points}} 포인트가 필요합니다', + }, + }, +} as any; + +describe('avatar-body model', () => { + describe('locked', () => { + test('me가 undefined이면 잠김', () => { + const body = createBody(); + const result = locked(body, undefined, mockDictionary); + expect(result).toEqual({ is: true }); + }); + + test('BASIC 타입은 항상 잠기지 않음', () => { + const body = createBody({ obtainableType: ObtainmentType.BASIC }); + const me = createMe(); + const result = locked(body, me, mockDictionary); + expect(result).toEqual({ is: false }); + }); + + test('DJ_PNT 타입 — 점수 충분하면 잠기지 않음', () => { + const body = createBody({ + obtainableType: ObtainmentType.DJ_PNT, + obtainableScore: 100, + }); + const me = createMe({ + activitySummaries: [{ activityType: ActivityType.DJ_PNT, score: 150 }], + }); + const result = locked(body, me, mockDictionary); + expect(result).toEqual({ is: false }); + }); + + test('DJ_PNT 타입 — 점수 부족하면 잠김 + reason 포함', () => { + const body = createBody({ + obtainableType: ObtainmentType.DJ_PNT, + obtainableScore: 100, + }); + const me = createMe({ + activitySummaries: [{ activityType: ActivityType.DJ_PNT, score: 30 }], + }); + const result = locked(body, me, mockDictionary); + expect(result.is).toBe(true); + expect(result.reason).toBeDefined(); + expect(result.reason).toContain('70 DJ'); + }); + + test('REF_LINK 타입 — 점수 부족하면 잠김', () => { + const body = createBody({ + obtainableType: ObtainmentType.REF_LINK, + obtainableScore: 50, + }); + const me = createMe({ activitySummaries: [] }); + const result = locked(body, me, mockDictionary); + expect(result.is).toBe(true); + expect(result.reason).toContain('50 Refferal Link'); + }); + + test('ROOM_ACT 타입 — 점수 동일하면 잠기지 않음', () => { + const body = createBody({ + obtainableType: ObtainmentType.ROOM_ACT, + obtainableScore: 200, + }); + const me = createMe({ + activitySummaries: [{ activityType: ActivityType.ROOM_ACT, score: 200 }], + }); + const result = locked(body, me, mockDictionary); + expect(result).toEqual({ is: false }); + }); + }); +}); diff --git a/src/shared/lib/functions/event-emitter.test.ts b/src/shared/lib/functions/event-emitter.test.ts new file mode 100644 index 00000000..e764f4a6 --- /dev/null +++ b/src/shared/lib/functions/event-emitter.test.ts @@ -0,0 +1,76 @@ +import { EventEmitter } from './event-emitter'; + +describe('EventEmitter', () => { + test('on으로 등록한 콜백이 emit 시 호출됨', () => { + const emitter = new EventEmitter(); + const callback = jest.fn(); + + emitter.on('test', callback); + emitter.emit('test'); + + expect(callback).toHaveBeenCalledTimes(1); + }); + + test('같은 이벤트에 여러 콜백 등록 가능', () => { + const emitter = new EventEmitter(); + const cb1 = jest.fn(); + const cb2 = jest.fn(); + + emitter.on('test', cb1); + emitter.on('test', cb2); + emitter.emit('test'); + + expect(cb1).toHaveBeenCalledTimes(1); + expect(cb2).toHaveBeenCalledTimes(1); + }); + + test('다른 이벤트의 콜백은 호출되지 않음', () => { + const emitter = new EventEmitter(); + const callback = jest.fn(); + + emitter.on('other', callback); + emitter.emit('test'); + + expect(callback).not.toHaveBeenCalled(); + }); + + test('on 반환값으로 구독 해제 가능', () => { + const emitter = new EventEmitter(); + const callback = jest.fn(); + + const unsubscribe = emitter.on('test', callback); + unsubscribe(); + emitter.emit('test'); + + expect(callback).not.toHaveBeenCalled(); + }); + + test('구독 해제 후에도 다른 콜백은 정상 동작', () => { + const emitter = new EventEmitter(); + const cb1 = jest.fn(); + const cb2 = jest.fn(); + + const unsub1 = emitter.on('test', cb1); + emitter.on('test', cb2); + unsub1(); + emitter.emit('test'); + + expect(cb1).not.toHaveBeenCalled(); + expect(cb2).toHaveBeenCalledTimes(1); + }); + + test('등록되지 않은 이벤트를 emit해도 에러 없음', () => { + const emitter = new EventEmitter(); + expect(() => emitter.emit('nonexistent')).not.toThrow(); + }); + + test('제네릭 타입 이벤트 지원', () => { + const emitter = new EventEmitter<'play' | 'pause'>(); + const callback = jest.fn(); + + emitter.on('play', callback); + emitter.emit('play'); + + expect(callback).toHaveBeenCalledTimes(1); + }); +}); diff --git a/src/shared/lib/functions/silent.test.ts b/src/shared/lib/functions/silent.test.ts new file mode 100644 index 00000000..2fbe6f14 --- /dev/null +++ b/src/shared/lib/functions/silent.test.ts @@ -0,0 +1,55 @@ +import silent from './silent'; + +describe('silent', () => { + test('성공 시 true 반환', async () => { + const result = await silent(Promise.resolve('data')); + expect(result).toBe(true); + }); + + test('실패 시 false 반환', async () => { + const result = await silent(Promise.reject(new Error('fail'))); + expect(result).toBe(false); + }); + + test('성공 시 onSuccess 콜백 호출', async () => { + const onSuccess = jest.fn(); + await silent(Promise.resolve('data'), { onSuccess }); + expect(onSuccess).toHaveBeenCalledWith('data'); + }); + + test('실패 시 onError 콜백 호출', async () => { + const error = new Error('fail'); + const onError = jest.fn(); + await silent(Promise.reject(error), { onError }); + expect(onError).toHaveBeenCalledWith(error); + }); + + test('성공 시 onSettled 콜백 호출', async () => { + const onSettled = jest.fn(); + await silent(Promise.resolve('data'), { onSettled }); + expect(onSettled).toHaveBeenCalledTimes(1); + }); + + test('실패 시에도 onSettled 콜백 호출', async () => { + const onSettled = jest.fn(); + await silent(Promise.reject(new Error('fail')), { onSettled }); + expect(onSettled).toHaveBeenCalledTimes(1); + }); + + test('성공 시 onError 호출되지 않음', async () => { + const onError = jest.fn(); + await silent(Promise.resolve('data'), { onError }); + expect(onError).not.toHaveBeenCalled(); + }); + + test('실패 시 onSuccess 호출되지 않음', async () => { + const onSuccess = jest.fn(); + await silent(Promise.reject(new Error('fail')), { onSuccess }); + expect(onSuccess).not.toHaveBeenCalled(); + }); + + test('옵션 없이도 정상 동작', async () => { + await expect(silent(Promise.resolve('data'))).resolves.toBe(true); + await expect(silent(Promise.reject(new Error('fail')))).resolves.toBe(false); + }); +}); diff --git a/src/shared/lib/router/parse-href.test.ts b/src/shared/lib/router/parse-href.test.ts new file mode 100644 index 00000000..ac1b6a0c --- /dev/null +++ b/src/shared/lib/router/parse-href.test.ts @@ -0,0 +1,42 @@ +import { parseHref } from './parse-href'; + +describe('parseHref', () => { + test('파라미터 없이 href 그대로 반환', () => { + expect(parseHref('/parties')).toBe('/parties'); + }); + + test('path 변수 치환', () => { + expect(parseHref('/parties/[id]', { path: { id: 123 } })).toBe('/parties/123'); + }); + + test('여러 path 변수 치환', () => { + expect( + parseHref('/parties/[id]/members/[memberId]', { + path: { id: 1, memberId: 42 }, + }) + ).toBe('/parties/1/members/42'); + }); + + test('존재하지 않는 path 변수는 원본 유지', () => { + expect(parseHref('/parties/[id]', { path: {} })).toBe('/parties/[id]'); + }); + + test('query 파라미터 추가', () => { + const result = parseHref('/parties', { query: { page: 1, sort: 'latest' } }); + expect(result).toContain('/parties?'); + expect(result).toContain('page=1'); + expect(result).toContain('sort=latest'); + }); + + test('path와 query 동시 사용', () => { + const result = parseHref('/parties/[id]', { + path: { id: 5 }, + query: { tab: 'info' }, + }); + expect(result).toBe('/parties/5?tab=info'); + }); + + test('문자열 path 변수 치환', () => { + expect(parseHref('/rooms/[slug]', { path: { slug: 'my-room' } })).toBe('/rooms/my-room'); + }); +}); From dc4ab398c336d2dd80e316edbbdce6109a08651d Mon Sep 17 00:00:00 2001 From: LivingLikeKrillin <143606756+LivingLikeKrillin@users.noreply.github.com> Date: Sun, 22 Feb 2026 22:56:22 +0900 Subject: [PATCH 03/41] =?UTF-8?q?test:=203=EB=8B=A8=EA=B3=84=20=EB=8B=A8?= =?UTF-8?q?=EC=9C=84=20=ED=85=8C=EC=8A=A4=ED=8A=B8=206=EA=B0=9C=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20(Chat,=20i18n=20processors,=20delay,=20Sin?= =?UTF-8?q?gleton)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Chat 클래스, VariableProcessor/BoldProcessor/LineBreakProcessor, delay 유틸, Singleton 데코레이터에 대한 테스트를 추가한다. Co-Authored-By: Claude Opus 4.6 --- src/shared/lib/chat/lib/chat.test.ts | 92 +++++++++++++++++++ .../singleton/singleton.decorator.test.ts | 45 +++++++++ src/shared/lib/functions/delay.test.ts | 41 +++++++++ .../processors/bold-processor.test.ts | 25 +++++ .../processors/line-break-processor.test.ts | 23 +++++ .../processors/variable-processor.test.ts | 41 +++++++++ 6 files changed, 267 insertions(+) create mode 100644 src/shared/lib/chat/lib/chat.test.ts create mode 100644 src/shared/lib/decorators/singleton/singleton.decorator.test.ts create mode 100644 src/shared/lib/functions/delay.test.ts create mode 100644 src/shared/lib/localization/renderer/processors/bold-processor.test.ts create mode 100644 src/shared/lib/localization/renderer/processors/line-break-processor.test.ts create mode 100644 src/shared/lib/localization/renderer/processors/variable-processor.test.ts diff --git a/src/shared/lib/chat/lib/chat.test.ts b/src/shared/lib/chat/lib/chat.test.ts new file mode 100644 index 00000000..8e3a6116 --- /dev/null +++ b/src/shared/lib/chat/lib/chat.test.ts @@ -0,0 +1,92 @@ +import Chat from './chat'; + +describe('Chat', () => { + test('빈 초기 메시지로 생성', () => { + const chat = Chat.create([]); + expect(chat.getMessages()).toEqual([]); + }); + + test('초기 메시지와 함께 생성', () => { + const chat = Chat.create(['hello', 'world']); + expect(chat.getMessages()).toEqual(['hello', 'world']); + }); + + describe('appendMessage', () => { + test('메시지 추가', () => { + const chat = Chat.create([]); + chat.appendMessage('hello'); + chat.appendMessage('world'); + expect(chat.getMessages()).toEqual(['hello', 'world']); + }); + + test('메시지 추가 시 리스너에 알림', () => { + const chat = Chat.create([]); + const listener = jest.fn(); + chat.addMessageListener(listener); + + chat.appendMessage('hello'); + + expect(listener).toHaveBeenCalledWith('hello'); + }); + }); + + describe('updateMessage', () => { + test('조건에 맞는 메시지 업데이트', () => { + const chat = Chat.create([ + { id: 1, text: 'hello' }, + { id: 2, text: 'world' }, + ]); + + chat.updateMessage( + (msg) => msg.id === 1, + (msg) => ({ ...msg, text: 'updated' }) + ); + + expect(chat.getMessages()).toEqual([ + { id: 1, text: 'updated' }, + { id: 2, text: 'world' }, + ]); + }); + + test('업데이트 시 리스너에 알림', () => { + const chat = Chat.create([{ id: 1, text: 'hello' }]); + const listener = jest.fn(); + chat.addMessageListener(listener); + + chat.updateMessage( + (msg) => msg.id === 1, + (msg) => ({ ...msg, text: 'updated' }) + ); + + expect(listener).toHaveBeenCalledWith({ id: 1, text: 'updated' }); + }); + + test('조건에 맞는 메시지가 없으면 리스너 호출 안 됨', () => { + const chat = Chat.create([{ id: 1, text: 'hello' }]); + const listener = jest.fn(); + chat.addMessageListener(listener); + + chat.updateMessage( + (msg) => msg.id === 999, + (msg) => ({ ...msg, text: 'updated' }) + ); + + expect(listener).not.toHaveBeenCalled(); + }); + }); + + describe('clear', () => { + test('메시지와 리스너 모두 초기화', () => { + const chat = Chat.create(['hello', 'world']); + const listener = jest.fn(); + chat.addMessageListener(listener); + + chat.clear(); + + expect(chat.getMessages()).toEqual([]); + + chat.appendMessage('after clear'); + expect(listener).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/src/shared/lib/decorators/singleton/singleton.decorator.test.ts b/src/shared/lib/decorators/singleton/singleton.decorator.test.ts new file mode 100644 index 00000000..14ac30e3 --- /dev/null +++ b/src/shared/lib/decorators/singleton/singleton.decorator.test.ts @@ -0,0 +1,45 @@ +import Singleton from './singleton.decorator'; + +describe('Singleton decorator', () => { + test('여러 번 인스턴스화해도 동일 인스턴스 반환', () => { + @Singleton + class TestService { + public value = 0; + } + + const a = new TestService(); + const b = new TestService(); + + expect(a).toBe(b); + }); + + test('첫 번째 인스턴스의 상태가 유지됨', () => { + @Singleton + class Counter { + public count = 0; + public increment() { + this.count++; + } + } + + const first = new Counter(); + first.increment(); + first.increment(); + + const second = new Counter(); + expect(second.count).toBe(2); + }); + + test('프로토타입 체인이 유지됨', () => { + @Singleton + class MyClass { + public greet() { + return 'hello'; + } + } + + const instance = new MyClass(); + expect(instance.greet()).toBe('hello'); + expect(instance).toBeInstanceOf(MyClass); + }); +}); diff --git a/src/shared/lib/functions/delay.test.ts b/src/shared/lib/functions/delay.test.ts new file mode 100644 index 00000000..34cbb94b --- /dev/null +++ b/src/shared/lib/functions/delay.test.ts @@ -0,0 +1,41 @@ +import { delay } from './delay'; + +describe('delay', () => { + beforeEach(() => { + jest.useFakeTimers(); + }); + + afterEach(() => { + jest.useRealTimers(); + }); + + test('지정된 시간 후 resolve', async () => { + const callback = jest.fn(); + + delay(1000).then(callback); + + expect(callback).not.toHaveBeenCalled(); + + jest.advanceTimersByTime(1000); + await Promise.resolve(); + + expect(callback).toHaveBeenCalledTimes(1); + }); + + test('0ms delay도 정상 동작', async () => { + const callback = jest.fn(); + + delay(0).then(callback); + + jest.advanceTimersByTime(0); + await Promise.resolve(); + + expect(callback).toHaveBeenCalledTimes(1); + }); + + test('Promise 반환', async () => { + const promise = delay(100); + jest.advanceTimersByTime(100); + await expect(promise).resolves.toBeUndefined(); + }); +}); diff --git a/src/shared/lib/localization/renderer/processors/bold-processor.test.ts b/src/shared/lib/localization/renderer/processors/bold-processor.test.ts new file mode 100644 index 00000000..53404cd4 --- /dev/null +++ b/src/shared/lib/localization/renderer/processors/bold-processor.test.ts @@ -0,0 +1,25 @@ +import BoldProcessor from './bold-processor'; + +describe('BoldProcessor', () => { + test('**text**를 태그로 변환', () => { + const processor = new BoldProcessor(); + expect(processor.process('**굵게**')).toBe('굵게'); + }); + + test('여러 볼드 텍스트 변환', () => { + const processor = new BoldProcessor(); + expect(processor.process('**A**와 **B**')).toBe( + 'AB' + ); + }); + + test('볼드 마크업 없으면 그대로 반환', () => { + const processor = new BoldProcessor(); + expect(processor.process('일반 텍스트')).toBe('일반 텍스트'); + }); + + test('빈 문자열', () => { + const processor = new BoldProcessor(); + expect(processor.process('')).toBe(''); + }); +}); diff --git a/src/shared/lib/localization/renderer/processors/line-break-processor.test.ts b/src/shared/lib/localization/renderer/processors/line-break-processor.test.ts new file mode 100644 index 00000000..9ab4a480 --- /dev/null +++ b/src/shared/lib/localization/renderer/processors/line-break-processor.test.ts @@ -0,0 +1,23 @@ +import LineBreakProcessor from './line-break-processor'; + +describe('LineBreakProcessor', () => { + test('줄바꿈을
태그로 변환', () => { + const processor = new LineBreakProcessor(); + expect(processor.process('첫째 줄\n둘째 줄')).toBe('첫째 줄
둘째 줄'); + }); + + test('여러 줄바꿈 변환', () => { + const processor = new LineBreakProcessor(); + expect(processor.process('A\nB\nC')).toBe('A
B
C'); + }); + + test('줄바꿈 없으면 그대로 반환', () => { + const processor = new LineBreakProcessor(); + expect(processor.process('한 줄 텍스트')).toBe('한 줄 텍스트'); + }); + + test('빈 문자열', () => { + const processor = new LineBreakProcessor(); + expect(processor.process('')).toBe(''); + }); +}); diff --git a/src/shared/lib/localization/renderer/processors/variable-processor.test.ts b/src/shared/lib/localization/renderer/processors/variable-processor.test.ts new file mode 100644 index 00000000..5347084c --- /dev/null +++ b/src/shared/lib/localization/renderer/processors/variable-processor.test.ts @@ -0,0 +1,41 @@ +import VariableProcessor from './variable-processor'; +import { processI18nString } from './variable-processor-util'; + +describe('VariableProcessor', () => { + test('변수 치환', () => { + const processor = new VariableProcessor({ name: 'Alice' }); + expect(processor.process('Hello {{name}}')).toBe('Hello Alice'); + }); + + test('여러 변수 동시 치환', () => { + const processor = new VariableProcessor({ a: '1', b: '2' }); + expect(processor.process('{{a}} + {{b}}')).toBe('1 + 2'); + }); + + test('존재하지 않는 변수는 빈 문자열로 치환', () => { + const processor = new VariableProcessor({}); + expect(processor.process('Hello {{name}}')).toBe('Hello '); + }); + + test('변수가 없는 문자열은 그대로 반환', () => { + const processor = new VariableProcessor({ name: 'Alice' }); + expect(processor.process('Hello World')).toBe('Hello World'); + }); + + test('같은 변수 여러 번 사용', () => { + const processor = new VariableProcessor({ x: 'Y' }); + expect(processor.process('{{x}} and {{x}}')).toBe('Y and Y'); + }); +}); + +describe('processI18nString', () => { + test('변수 치환 유틸 함수', () => { + expect(processI18nString('{{points}} 포인트 필요', { points: '50 DJ' })).toBe( + '50 DJ 포인트 필요' + ); + }); + + test('빈 변수 객체', () => { + expect(processI18nString('텍스트', {})).toBe('텍스트'); + }); +}); From ba3f37551517e1df8f5c0cfad10b3540f6c3394d Mon Sep 17 00:00:00 2001 From: LivingLikeKrillin <143606756+LivingLikeKrillin@users.noreply.github.com> Date: Sun, 22 Feb 2026 23:12:19 +0900 Subject: [PATCH 04/41] =?UTF-8?q?test:=204=EB=8B=A8=EA=B3=84=20=EB=8B=A8?= =?UTF-8?q?=EC=9C=84=20=ED=85=8C=EC=8A=A4=ED=8A=B8=206=EA=B0=9C=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20(preview-helpers,=20cn,=20repeatAnimationF?= =?UTF-8?q?rame,=20Mock/SkipGlobalErrorHandling=20=EB=8D=B0=EC=BD=94?= =?UTF-8?q?=EB=A0=88=EC=9D=B4=ED=84=B0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 미리보기 트랙 변환, CSS 클래스 병합, 애니메이션 프레임, MockReturn/MockResolve/SkipGlobalErrorHandling 데코레이터에 대한 테스트를 추가한다. Co-Authored-By: Claude Opus 4.6 --- .../music-preview/lib/preview-helpers.test.ts | 78 +++++++++++++++++ .../mock/mock-resolve.decorator.test.ts | 78 +++++++++++++++++ .../mock/mock-return.decorator.test.ts | 65 ++++++++++++++ ...ip-global-error-handling.decorator.test.ts | 87 +++++++++++++++++++ src/shared/lib/functions/cn.test.ts | 29 +++++++ .../functions/repeat-animation-frame.test.ts | 40 +++++++++ 6 files changed, 377 insertions(+) create mode 100644 src/entities/music-preview/lib/preview-helpers.test.ts create mode 100644 src/shared/lib/decorators/mock/mock-resolve.decorator.test.ts create mode 100644 src/shared/lib/decorators/mock/mock-return.decorator.test.ts create mode 100644 src/shared/lib/decorators/skip-global-error-handling/skip-global-error-handling.decorator.test.ts create mode 100644 src/shared/lib/functions/cn.test.ts create mode 100644 src/shared/lib/functions/repeat-animation-frame.test.ts diff --git a/src/entities/music-preview/lib/preview-helpers.test.ts b/src/entities/music-preview/lib/preview-helpers.test.ts new file mode 100644 index 00000000..f00d57dc --- /dev/null +++ b/src/entities/music-preview/lib/preview-helpers.test.ts @@ -0,0 +1,78 @@ +import type { PlaylistTrack } from '@/shared/api/http/types/playlists'; +import type { Music } from '@/shared/api/http/types/playlists'; +import { + convertPlaylistTrackToPreview, + convertSearchMusicToPreview, + extractVideoIdFromUrl, + safeDecodeTitle, +} from './preview-helpers'; + +describe('preview-helpers', () => { + describe('convertPlaylistTrackToPreview', () => { + test('PlaylistTrack을 PreviewTrack으로 변환', () => { + const track: PlaylistTrack = { + trackId: 1, + linkId: 12345, + name: '테스트 곡', + orderNumber: 1, + duration: '03:30', + thumbnailImage: 'https://img.youtube.com/vi/12345/0.jpg', + }; + + expect(convertPlaylistTrackToPreview(track)).toEqual({ + id: '12345', + title: '테스트 곡', + thumbnailUrl: 'https://img.youtube.com/vi/12345/0.jpg', + videoUrl: 'https://www.youtube.com/watch?v=12345', + source: 'playlist-track', + }); + }); + }); + + describe('convertSearchMusicToPreview', () => { + test('Music을 PreviewTrack으로 변환', () => { + const music: Music = { + videoId: 'abc123', + videoTitle: '검색 결과 곡', + thumbnailUrl: 'https://img.youtube.com/vi/abc123/0.jpg', + runningTime: '04:15', + }; + + expect(convertSearchMusicToPreview(music)).toEqual({ + id: 'abc123', + title: '검색 결과 곡', + thumbnailUrl: 'https://img.youtube.com/vi/abc123/0.jpg', + videoUrl: 'https://www.youtube.com/watch?v=abc123', + source: 'search-result', + }); + }); + }); + + describe('extractVideoIdFromUrl', () => { + it.each([ + ['https://www.youtube.com/watch?v=dQw4w9WgXcQ', 'dQw4w9WgXcQ'], + ['https://youtu.be/dQw4w9WgXcQ', 'dQw4w9WgXcQ'], + ['https://www.youtube.com/watch?v=abc123&t=10', 'abc123'], + ])('"%s" → "%s"', (url, expected) => { + expect(extractVideoIdFromUrl(url)).toBe(expected); + }); + + it.each(['https://www.google.com', 'not-a-url', ''])('유효하지 않은 URL "%s" → null', (url) => { + expect(extractVideoIdFromUrl(url)).toBeNull(); + }); + }); + + describe('safeDecodeTitle', () => { + test('인코딩된 문자열 디코딩', () => { + expect(safeDecodeTitle('%ED%85%8C%EC%8A%A4%ED%8A%B8')).toBe('테스트'); + }); + + test('일반 문자열은 그대로 반환', () => { + expect(safeDecodeTitle('일반 텍스트')).toBe('일반 텍스트'); + }); + + test('잘못된 인코딩은 원본 반환', () => { + expect(safeDecodeTitle('%E0%A4%A')).toBe('%E0%A4%A'); + }); + }); +}); diff --git a/src/shared/lib/decorators/mock/mock-resolve.decorator.test.ts b/src/shared/lib/decorators/mock/mock-resolve.decorator.test.ts new file mode 100644 index 00000000..a6bfdd32 --- /dev/null +++ b/src/shared/lib/decorators/mock/mock-resolve.decorator.test.ts @@ -0,0 +1,78 @@ +import MockResolve from './mock-resolve.decorator'; + +describe('MockResolve decorator', () => { + const originalEnv = process.env; + + beforeEach(() => { + jest.useFakeTimers(); + process.env = { ...originalEnv, NODE_ENV: 'test', NEXT_PUBLIC_USE_MOCK: 'true' }; + }); + + afterEach(() => { + jest.useRealTimers(); + process.env = originalEnv; + }); + + test('mock 환경에서 data resolve', async () => { + class TestService { + @MockResolve({ data: { id: 1 } }) + public async fetchData() { + return { id: 999 }; + } + } + + const service = new TestService(); + const promise = service.fetchData(); + jest.runAllTimers(); + await expect(promise).resolves.toEqual({ id: 1 }); + }); + + test('mock 환경에서 error reject', async () => { + class TestService { + @MockResolve({ error: new Error('mock error') }) + public async fetchData() { + return 'real'; + } + } + + const service = new TestService(); + const promise = service.fetchData(); + jest.runAllTimers(); + await expect(promise).rejects.toThrow('mock error'); + }); + + test('delay 옵션 적용', async () => { + class TestService { + @MockResolve({ data: 'delayed' }, { delay: 500 }) + public async fetchData() { + return 'real'; + } + } + + const service = new TestService(); + const callback = jest.fn(); + service.fetchData().then(callback); + + jest.advanceTimersByTime(499); + await Promise.resolve(); + expect(callback).not.toHaveBeenCalled(); + + jest.advanceTimersByTime(1); + await Promise.resolve(); + expect(callback).toHaveBeenCalledWith('delayed'); + }); + + test('production 환경에서는 원래 메서드 실행', async () => { + process.env.NODE_ENV = 'production'; + + class TestService { + @MockResolve({ data: 'mock' }) + public async fetchData() { + return 'real'; + } + } + + const service = new TestService(); + await expect(service.fetchData()).resolves.toBe('real'); + }); +}); diff --git a/src/shared/lib/decorators/mock/mock-return.decorator.test.ts b/src/shared/lib/decorators/mock/mock-return.decorator.test.ts new file mode 100644 index 00000000..929faf53 --- /dev/null +++ b/src/shared/lib/decorators/mock/mock-return.decorator.test.ts @@ -0,0 +1,65 @@ +import MockReturn from './mock-return.decorator'; + +describe('MockReturn decorator', () => { + const originalEnv = process.env; + + beforeEach(() => { + process.env = { ...originalEnv, NODE_ENV: 'test', NEXT_PUBLIC_USE_MOCK: 'true' }; + }); + + afterEach(() => { + process.env = originalEnv; + }); + + test('mock 환경에서 data 반환', () => { + class TestService { + @MockReturn({ data: { id: 1, name: 'mock' } }) + public getData() { + return { id: 999, name: 'real' }; + } + } + + const service = new TestService(); + expect(service.getData()).toEqual({ id: 1, name: 'mock' }); + }); + + test('mock 환경에서 error throw', () => { + class TestService { + @MockReturn({ error: new Error('mock error') }) + public getData() { + return 'real'; + } + } + + const service = new TestService(); + expect(() => service.getData()).toThrow('mock error'); + }); + + test('production 환경에서는 원래 메서드 실행', () => { + process.env.NODE_ENV = 'production'; + + class TestService { + @MockReturn({ data: 'mock' }) + public getData() { + return 'real'; + } + } + + const service = new TestService(); + expect(service.getData()).toBe('real'); + }); + + test('NEXT_PUBLIC_USE_MOCK이 false면 원래 메서드 실행', () => { + process.env.NEXT_PUBLIC_USE_MOCK = 'false'; + + class TestService { + @MockReturn({ data: 'mock' }) + public getData() { + return 'real'; + } + } + + const service = new TestService(); + expect(service.getData()).toBe('real'); + }); +}); diff --git a/src/shared/lib/decorators/skip-global-error-handling/skip-global-error-handling.decorator.test.ts b/src/shared/lib/decorators/skip-global-error-handling/skip-global-error-handling.decorator.test.ts new file mode 100644 index 00000000..0c195b57 --- /dev/null +++ b/src/shared/lib/decorators/skip-global-error-handling/skip-global-error-handling.decorator.test.ts @@ -0,0 +1,87 @@ +import SkipGlobalErrorHandling, { + shouldSkipGlobalErrorHandling, +} from './skip-global-error-handling.decorator'; + +describe('SkipGlobalErrorHandling decorator', () => { + test('에러 발생 시 skipGlobalErrorHandling 프로퍼티 추가', async () => { + class TestService { + @SkipGlobalErrorHandling() + public async failingMethod() { + throw new Error('test error'); + } + } + + const service = new TestService(); + try { + await service.failingMethod(); + } catch (error) { + expect(shouldSkipGlobalErrorHandling(error)).toBe(true); + } + }); + + test('when: false이면 프로퍼티 추가 안 됨', async () => { + class TestService { + @SkipGlobalErrorHandling({ when: false }) + public async failingMethod() { + throw new Error('test error'); + } + } + + const service = new TestService(); + try { + await service.failingMethod(); + } catch (error) { + expect(shouldSkipGlobalErrorHandling(error)).toBe(false); + } + }); + + test('when 함수가 조건부로 프로퍼티 추가', async () => { + class TestService { + @SkipGlobalErrorHandling({ when: (err) => err.message === 'skip me' }) + public async failingMethod(msg: string) { + throw new Error(msg); + } + } + + const service = new TestService(); + + try { + await service.failingMethod('skip me'); + } catch (error) { + expect(shouldSkipGlobalErrorHandling(error)).toBe(true); + } + + try { + await service.failingMethod('do not skip'); + } catch (error) { + expect(shouldSkipGlobalErrorHandling(error)).toBe(false); + } + }); + + test('성공 시 원래 반환값 유지', async () => { + class TestService { + @SkipGlobalErrorHandling() + public async successMethod() { + return 'ok'; + } + } + + const service = new TestService(); + await expect(service.successMethod()).resolves.toBe('ok'); + }); +}); + +describe('shouldSkipGlobalErrorHandling', () => { + test('일반 Error는 false', () => { + expect(shouldSkipGlobalErrorHandling(new Error('normal'))).toBe(false); + }); + + test('null/undefined는 false', () => { + expect(shouldSkipGlobalErrorHandling(null)).toBe(false); + expect(shouldSkipGlobalErrorHandling(undefined)).toBe(false); + }); + + test('문자열은 false', () => { + expect(shouldSkipGlobalErrorHandling('error string')).toBe(false); + }); +}); diff --git a/src/shared/lib/functions/cn.test.ts b/src/shared/lib/functions/cn.test.ts new file mode 100644 index 00000000..4b3e24f1 --- /dev/null +++ b/src/shared/lib/functions/cn.test.ts @@ -0,0 +1,29 @@ +import { cn } from './cn'; + +describe('cn', () => { + test('단일 클래스', () => { + expect(cn('text-red-500')).toBe('text-red-500'); + }); + + test('여러 클래스 병합', () => { + expect(cn('px-2', 'py-1')).toBe('px-2 py-1'); + }); + + test('조건부 클래스', () => { + const isHidden = false; + const isVisible = true; + expect(cn('base', isHidden && 'hidden', isVisible && 'visible')).toBe('base visible'); + }); + + test('tailwind 충돌 클래스 병합 (후자 우선)', () => { + expect(cn('px-2', 'px-4')).toBe('px-4'); + }); + + test('빈 입력', () => { + expect(cn()).toBe(''); + }); + + test('undefined/null 무시', () => { + expect(cn('base', undefined, null)).toBe('base'); + }); +}); diff --git a/src/shared/lib/functions/repeat-animation-frame.test.ts b/src/shared/lib/functions/repeat-animation-frame.test.ts new file mode 100644 index 00000000..3b7a2527 --- /dev/null +++ b/src/shared/lib/functions/repeat-animation-frame.test.ts @@ -0,0 +1,40 @@ +import { repeatAnimationFrame } from './repeat-animation-frame'; + +describe('repeatAnimationFrame', () => { + let rafCallback: FrameRequestCallback; + + beforeEach(() => { + jest.spyOn(window, 'requestAnimationFrame').mockImplementation((cb) => { + rafCallback = cb; + return 0; + }); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + test('repeat이 0이면 즉시 콜백 실행', () => { + const callback = jest.fn(); + repeatAnimationFrame(callback, 0); + expect(callback).toHaveBeenCalledTimes(1); + expect(window.requestAnimationFrame).not.toHaveBeenCalled(); + }); + + test('repeat이 1이면 requestAnimationFrame 1회 후 콜백 실행', () => { + const callback = jest.fn(); + repeatAnimationFrame(callback, 1); + + expect(callback).not.toHaveBeenCalled(); + expect(window.requestAnimationFrame).toHaveBeenCalledTimes(1); + + rafCallback(0); + expect(callback).toHaveBeenCalledTimes(1); + }); + + test('repeat이 음수면 즉시 콜백 실행', () => { + const callback = jest.fn(); + repeatAnimationFrame(callback, -1); + expect(callback).toHaveBeenCalledTimes(1); + }); +}); From 08906b0ffac8cd396e616c19b917882415bc2046 Mon Sep 17 00:00:00 2001 From: LivingLikeKrillin <143606756+LivingLikeKrillin@users.noreply.github.com> Date: Sun, 22 Feb 2026 23:37:31 +0900 Subject: [PATCH 05/41] =?UTF-8?q?test:=20categorizeByGradeType=20=ED=81=AC?= =?UTF-8?q?=EB=A3=A8=20=EB=93=B1=EA=B8=89=20=EB=B6=84=EB=A5=98=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 등급별 분류, 빈 배열 처리, gradePriorities 순서 보장을 검증한다. Co-Authored-By: Claude Opus 4.6 --- .../list-crews/model/crews.model.test.ts | 72 +++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 src/features/partyroom/list-crews/model/crews.model.test.ts diff --git a/src/features/partyroom/list-crews/model/crews.model.test.ts b/src/features/partyroom/list-crews/model/crews.model.test.ts new file mode 100644 index 00000000..91753d7a --- /dev/null +++ b/src/features/partyroom/list-crews/model/crews.model.test.ts @@ -0,0 +1,72 @@ +import type { Model } from '@/entities/current-partyroom/model/crew.model'; +import { GradeType, MotionType } from '@/shared/api/http/types/@enums'; + +// barrel export에 JSX 포함된 훅이 있어 직접 모듈 경로로 모킹 +jest.mock('@/entities/current-partyroom', () => ({ + Crew: jest.requireActual('@/entities/current-partyroom/model/crew.model'), +})); + +import { categorizeByGradeType } from './crews.model'; + +const createCrew = (overrides: Partial = {}): Model => ({ + crewId: 1, + nickname: 'tester', + gradeType: GradeType.CLUBBER, + avatarBodyUri: '', + avatarFaceUri: '', + avatarIconUri: '', + combinePositionX: 0, + combinePositionY: 0, + offsetX: 0, + offsetY: 0, + scale: 1, + motionType: MotionType.NONE, + ...overrides, +}); + +describe('crews model', () => { + describe('categorizeByGradeType', () => { + test('등급별로 크루를 분류', () => { + const crews = [ + createCrew({ crewId: 1, gradeType: GradeType.HOST }), + createCrew({ crewId: 2, gradeType: GradeType.CLUBBER }), + createCrew({ crewId: 3, gradeType: GradeType.CLUBBER }), + createCrew({ crewId: 4, gradeType: GradeType.LISTENER }), + ]; + + const result = categorizeByGradeType(crews); + + expect(result[GradeType.HOST]).toHaveLength(1); + expect(result[GradeType.CLUBBER]).toHaveLength(2); + expect(result[GradeType.LISTENER]).toHaveLength(1); + }); + + test('해당 등급의 크루가 없으면 키가 없음', () => { + const crews = [createCrew({ gradeType: GradeType.HOST })]; + + const result = categorizeByGradeType(crews); + + expect(result[GradeType.HOST]).toHaveLength(1); + expect(result[GradeType.MODERATOR]).toBeUndefined(); + expect(result[GradeType.CLUBBER]).toBeUndefined(); + }); + + test('빈 배열이면 빈 객체 반환', () => { + expect(categorizeByGradeType([])).toEqual({}); + }); + + test('gradePriorities 순서에 따라 키 순서 보장', () => { + const crews = [ + createCrew({ crewId: 1, gradeType: GradeType.LISTENER }), + createCrew({ crewId: 2, gradeType: GradeType.HOST }), + createCrew({ crewId: 3, gradeType: GradeType.MODERATOR }), + ]; + + const result = categorizeByGradeType(crews); + const keys = Object.keys(result); + + expect(keys.indexOf(GradeType.HOST)).toBeLessThan(keys.indexOf(GradeType.MODERATOR)); + expect(keys.indexOf(GradeType.MODERATOR)).toBeLessThan(keys.indexOf(GradeType.LISTENER)); + }); + }); +}); From 6975637cc868630db287b48cedd31e39c2fe9508 Mon Sep 17 00:00:00 2001 From: LivingLikeKrillin <143606756+LivingLikeKrillin@users.noreply.github.com> Date: Mon, 23 Feb 2026 00:03:44 +0900 Subject: [PATCH 06/41] =?UTF-8?q?test:=20=EB=AA=A8=ED=82=B9=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=206=EA=B0=9C=20=EC=B6=94=EA=B0=80=20(network?= =?UTF-8?q?-log,=20with-log,=20capture-dom,=20nft.model,=20preview.store,?= =?UTF-8?q?=20user-preference.store)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.6 --- .../music-preview/model/preview.store.test.ts | 109 ++++++++++++++++++ .../model/user-preference.store.test.ts | 34 ++++++ src/entities/wallet/model/nft.model.test.ts | 70 +++++++++++ src/shared/lib/functions/capture-dom.test.ts | 40 +++++++ .../lib/functions/log/network-log.test.ts | 73 ++++++++++++ src/shared/lib/functions/log/with-log.test.ts | 59 ++++++++++ 6 files changed, 385 insertions(+) create mode 100644 src/entities/music-preview/model/preview.store.test.ts create mode 100644 src/entities/preference/model/user-preference.store.test.ts create mode 100644 src/entities/wallet/model/nft.model.test.ts create mode 100644 src/shared/lib/functions/capture-dom.test.ts create mode 100644 src/shared/lib/functions/log/network-log.test.ts create mode 100644 src/shared/lib/functions/log/with-log.test.ts diff --git a/src/entities/music-preview/model/preview.store.test.ts b/src/entities/music-preview/model/preview.store.test.ts new file mode 100644 index 00000000..52d8d770 --- /dev/null +++ b/src/entities/music-preview/model/preview.store.test.ts @@ -0,0 +1,109 @@ +import type { PreviewTrack } from './preview.model'; +import { createPreviewStore } from './preview.store'; + +const createTrack = (overrides: Partial = {}): PreviewTrack => ({ + id: 'track-1', + title: '테스트 곡', + thumbnailUrl: 'https://img.youtube.com/vi/test/0.jpg', + videoUrl: 'https://www.youtube.com/watch?v=test', + source: 'playlist-track', + ...overrides, +}); + +describe('preview store', () => { + test('초기 상태', () => { + const store = createPreviewStore(); + const state = store.getState(); + + expect(state.currentTrack).toBeNull(); + expect(state.playState).toBe('stopped'); + expect(state.playerReady).toBe(false); + }); + + describe('startPreview', () => { + test('트랙 재생 시작', () => { + const store = createPreviewStore(); + const track = createTrack(); + + store.getState().startPreview(track); + const state = store.getState(); + + expect(state.currentTrack).toEqual(track); + expect(state.playState).toBe('playing'); + expect(state.playerReady).toBe(false); + }); + + test('같은 트랙이 이미 재생중이면 상태 변경 없음', () => { + const store = createPreviewStore(); + const track = createTrack(); + + store.getState().startPreview(track); + store.getState().setPlayerReady(true); + + store.getState().startPreview(track); + + expect(store.getState().playerReady).toBe(true); + }); + + test('다른 트랙으로 전환', () => { + const store = createPreviewStore(); + const track1 = createTrack({ id: 'track-1', title: '곡1' }); + const track2 = createTrack({ id: 'track-2', title: '곡2' }); + + store.getState().startPreview(track1); + store.getState().startPreview(track2); + + expect(store.getState().currentTrack).toEqual(track2); + expect(store.getState().playerReady).toBe(false); + }); + }); + + describe('stopPreview', () => { + test('재생 중단', () => { + const store = createPreviewStore(); + store.getState().startPreview(createTrack()); + + store.getState().stopPreview(); + const state = store.getState(); + + expect(state.currentTrack).toBeNull(); + expect(state.playState).toBe('stopped'); + expect(state.playerReady).toBe(false); + }); + }); + + describe('setPlayerReady', () => { + test('플레이어 준비 상태 설정', () => { + const store = createPreviewStore(); + store.getState().startPreview(createTrack()); + + store.getState().setPlayerReady(true); + + expect(store.getState().playerReady).toBe(true); + }); + }); + + describe('isTrackPlaying', () => { + test('재생중인 트랙 ID와 일치하면 true', () => { + const store = createPreviewStore(); + store.getState().startPreview(createTrack({ id: 'abc' })); + + expect(store.getState().isTrackPlaying('abc')).toBe(true); + }); + + test('다른 트랙 ID면 false', () => { + const store = createPreviewStore(); + store.getState().startPreview(createTrack({ id: 'abc' })); + + expect(store.getState().isTrackPlaying('xyz')).toBe(false); + }); + + test('재생 중지 상태면 false', () => { + const store = createPreviewStore(); + store.getState().startPreview(createTrack({ id: 'abc' })); + store.getState().stopPreview(); + + expect(store.getState().isTrackPlaying('abc')).toBe(false); + }); + }); +}); diff --git a/src/entities/preference/model/user-preference.store.test.ts b/src/entities/preference/model/user-preference.store.test.ts new file mode 100644 index 00000000..3a1bf4b1 --- /dev/null +++ b/src/entities/preference/model/user-preference.store.test.ts @@ -0,0 +1,34 @@ +import { useUserPreferenceStore } from './user-preference.store'; + +describe('user-preference store', () => { + beforeEach(() => { + // 각 테스트 전 스토어 초기화 + useUserPreferenceStore.getState().reset(); + }); + + test('초기 상태: djingGuideHidden은 false', () => { + expect(useUserPreferenceStore.getState().djingGuideHidden).toBe(false); + }); + + describe('setDjingGuideHidden', () => { + test('true로 설정', () => { + useUserPreferenceStore.getState().setDjingGuideHidden(true); + expect(useUserPreferenceStore.getState().djingGuideHidden).toBe(true); + }); + + test('false로 다시 설정', () => { + useUserPreferenceStore.getState().setDjingGuideHidden(true); + useUserPreferenceStore.getState().setDjingGuideHidden(false); + expect(useUserPreferenceStore.getState().djingGuideHidden).toBe(false); + }); + }); + + describe('reset', () => { + test('초기 상태로 복원', () => { + useUserPreferenceStore.getState().setDjingGuideHidden(true); + useUserPreferenceStore.getState().reset(); + + expect(useUserPreferenceStore.getState().djingGuideHidden).toBe(false); + }); + }); +}); diff --git a/src/entities/wallet/model/nft.model.test.ts b/src/entities/wallet/model/nft.model.test.ts new file mode 100644 index 00000000..76db7ed5 --- /dev/null +++ b/src/entities/wallet/model/nft.model.test.ts @@ -0,0 +1,70 @@ +import axios from 'axios'; +import { refineList } from './nft.model'; + +jest.mock('axios'); +const mockedAxios = axios as jest.Mocked; + +const createNft = (thumbnailUrl: string | null = 'https://example.com/nft.png', name = 'TestNFT') => + ({ + name, + image: { thumbnailUrl }, + }) as any; + +describe('nft model', () => { + describe('refineList', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + test('이미지 헬스체크 통과한 NFT만 반환', async () => { + mockedAxios.get.mockResolvedValue({ status: 200 }); + + const result = await refineList([createNft('https://ok.com/1.png', 'NFT1')]); + + expect(result).toEqual([ + { + name: 'NFT1', + resourceUri: 'https://ok.com/1.png', + available: true, + }, + ]); + }); + + test('이미지 헬스체크 실패한 NFT는 제외', async () => { + mockedAxios.get.mockRejectedValue(new Error('network error')); + + const result = await refineList([createNft('https://fail.com/1.png')]); + + expect(result).toEqual([]); + }); + + test('thumbnailUrl이 없는 NFT는 제외', async () => { + const result = await refineList([createNft(null)]); + + expect(result).toEqual([]); + expect(mockedAxios.get).not.toHaveBeenCalled(); + }); + + test('여러 NFT 중 성공/실패 혼합', async () => { + mockedAxios.get + .mockResolvedValueOnce({ status: 200 }) + .mockRejectedValueOnce(new Error('fail')) + .mockResolvedValueOnce({ status: 200 }); + + const result = await refineList([ + createNft('https://ok1.com/1.png', 'A'), + createNft('https://fail.com/2.png', 'B'), + createNft('https://ok2.com/3.png', 'C'), + ]); + + expect(result).toHaveLength(2); + expect(result[0].name).toBe('A'); + expect(result[1].name).toBe('C'); + }); + + test('빈 배열이면 빈 배열 반환', async () => { + const result = await refineList([]); + expect(result).toEqual([]); + }); + }); +}); diff --git a/src/shared/lib/functions/capture-dom.test.ts b/src/shared/lib/functions/capture-dom.test.ts new file mode 100644 index 00000000..4b6bb3fa --- /dev/null +++ b/src/shared/lib/functions/capture-dom.test.ts @@ -0,0 +1,40 @@ +import { captureDOMToBlob, convertToFormData } from './capture-dom'; + +jest.mock('html2canvas', () => + jest.fn().mockResolvedValue({ + toBlob: (cb: (blob: Blob | null) => void) => { + cb(new Blob(['test'], { type: 'image/webp' })); + }, + }) +); + +describe('capture-dom', () => { + describe('captureDOMToBlob', () => { + test('DOM이 없으면 에러 throw', async () => { + const ref = { current: null }; + await expect(captureDOMToBlob(ref)).rejects.toThrow('DOM is not found'); + }); + + test('DOM을 캡처하여 Blob 반환', async () => { + const mockElement = document.createElement('div'); + jest + .spyOn(mockElement, 'querySelectorAll') + .mockReturnValue([] as unknown as NodeListOf); + const ref = { current: mockElement }; + + const blob = await captureDOMToBlob(ref); + + expect(blob).toBeInstanceOf(Blob); + }); + }); + + describe('convertToFormData', () => { + test('Blob을 FormData로 변환', () => { + const blob = new Blob(['test'], { type: 'image/webp' }); + const formData = convertToFormData(blob); + + expect(formData).toBeInstanceOf(FormData); + expect(formData.get('image')).toBeInstanceOf(Blob); + }); + }); +}); diff --git a/src/shared/lib/functions/log/network-log.test.ts b/src/shared/lib/functions/log/network-log.test.ts new file mode 100644 index 00000000..8bf8da44 --- /dev/null +++ b/src/shared/lib/functions/log/network-log.test.ts @@ -0,0 +1,73 @@ +jest.mock('./logger', () => ({ + infoLog: jest.fn(), + successLog: jest.fn(), + errorLog: jest.fn(), +})); + +import { infoLog, successLog, errorLog } from './logger'; +import { printRequestLog, printResponseLog, printErrorLog } from './network-log'; + +beforeEach(() => { + jest.clearAllMocks(); +}); + +describe('network-log', () => { + describe('printRequestLog', () => { + test('클라이언트 환경에서 요청 바디 로그 출력', () => { + printRequestLog({ + method: 'post', + endPoint: '/api/users', + requestData: { name: 'test' }, + }); + + expect(infoLog).toHaveBeenCalledWith('POST /api/users [REQ BODY]', { name: 'test' }); + }); + + test('클라이언트 환경에서 요청 파라미터가 있으면 파라미터 로그도 출력', () => { + printRequestLog({ + method: 'get', + endPoint: '/api/users', + requestParams: { page: 1 }, + }); + + expect(infoLog).toHaveBeenCalledWith('GET /api/users [REQ PARAMS]', { page: 1 }); + }); + + test('빈 requestParams는 파라미터 로그 출력 안 함', () => { + printRequestLog({ + method: 'get', + endPoint: '/api/users', + requestParams: {}, + }); + + expect(infoLog).toHaveBeenCalledTimes(1); + expect(infoLog).toHaveBeenCalledWith('GET /api/users [REQ BODY]', undefined); + }); + }); + + describe('printResponseLog', () => { + test('클라이언트 환경에서 응답 로그 출력', () => { + printResponseLog({ + method: 'get', + endPoint: '/api/users', + response: { data: [] }, + }); + + expect(successLog).toHaveBeenCalledWith('GET /api/users [RES BODY]', { data: [] }); + }); + }); + + describe('printErrorLog', () => { + test('클라이언트 환경에서 에러 로그 출력', () => { + const error = new Error('fail'); + printErrorLog({ + method: 'post', + endPoint: '/api/users', + errorMessage: 'fail', + error, + }); + + expect(errorLog).toHaveBeenCalledWith('POST /api/users [ERR]', 'fail', error); + }); + }); +}); diff --git a/src/shared/lib/functions/log/with-log.test.ts b/src/shared/lib/functions/log/with-log.test.ts new file mode 100644 index 00000000..b031ee11 --- /dev/null +++ b/src/shared/lib/functions/log/with-log.test.ts @@ -0,0 +1,59 @@ +jest.mock('./network-log', () => ({ + printRequestLog: jest.fn(), + printResponseLog: jest.fn(), + printErrorLog: jest.fn(), +})); + +jest.mock('@/shared/api/http/error/get-error-message', () => ({ + getErrorMessage: jest.fn((err: Error) => err.message), +})); + +import { printRequestLog, printResponseLog, printErrorLog } from './network-log'; +import withLog from './with-log'; + +beforeEach(() => { + jest.clearAllMocks(); +}); + +describe('withLog', () => { + test('성공 시 요청/응답 로그 출력 후 결과 반환', async () => { + const fn = Object.defineProperty(async (a: number) => a * 2, 'name', { value: 'testFn' }); + const wrapped = withLog(fn, 'get'); + + const result = await wrapped(5); + + expect(result).toBe(10); + expect(printRequestLog).toHaveBeenCalledWith({ + method: 'get', + endPoint: 'testFn', + requestData: [5], + }); + expect(printResponseLog).toHaveBeenCalledWith({ + method: 'get', + endPoint: 'testFn', + response: 10, + }); + expect(printErrorLog).not.toHaveBeenCalled(); + }); + + test('에러 시 에러 로그 출력 후 에러 재throw', async () => { + const error = new Error('test error'); + const fn = Object.defineProperty( + async () => { + throw error; + }, + 'name', + { value: 'failFn' } + ); + const wrapped = withLog(fn, 'post'); + + await expect(wrapped()).rejects.toThrow('test error'); + expect(printErrorLog).toHaveBeenCalledWith({ + method: 'post', + endPoint: 'failFn', + errorMessage: 'test error', + error, + }); + expect(printResponseLog).not.toHaveBeenCalled(); + }); +}); From 693345bcc50bcb38791e571fc775151a39fba4d3 Mon Sep 17 00:00:00 2001 From: LivingLikeKrillin <143606756+LivingLikeKrillin@users.noreply.github.com> Date: Mon, 23 Feb 2026 01:47:39 +0900 Subject: [PATCH 07/41] =?UTF-8?q?test:=20=ED=85=8C=EC=8A=A4=ED=8A=B8=2017?= =?UTF-8?q?=EA=B0=9C=20=EC=B6=94=EA=B0=80=20=E2=80=94=20API=20=EC=97=90?= =?UTF-8?q?=EB=9F=AC,=20Zustand=20=EC=8A=A4=ED=86=A0=EC=96=B4,=20=ED=9B=85?= =?UTF-8?q?,=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20(Phase=20A-E)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase A: API 에러 처리 + 인터셉터 (get-error-message, get-error-code, is-auth-error, error-emitter, response interceptors) Phase B: Zustand 스토어 (current-partyroom.store, ui-state.store) Phase C: 데이터 변환 + 스토리지 (restriction-panel-list-item, pkce-storage) Phase D: React 훅 renderHook (useDisclosure, useDebounce, useDidMountEffect, useDidUpdateEffect) Phase E: React 컴포넌트 render (Input, InputNumber, PreviewOverlay, Checkbox) Co-Authored-By: Claude Opus 4.6 --- .../model/current-partyroom.store.test.ts | 356 ++++++++++++++++++ .../ui/preview-overlay.component.test.tsx | 86 +++++ .../ui-state/model/ui-state.store.test.ts | 106 ++++++ .../model/pkce-storage.model.test.ts | 43 +++ .../http/client/interceptors/response.test.ts | 208 ++++++++++ .../api/http/error/error-emitter.test.ts | 65 ++++ .../api/http/error/get-error-code.test.ts | 65 ++++ .../api/http/error/get-error-message.test.ts | 61 +++ .../api/http/error/is-auth-error.test.ts | 48 +++ .../lib/hooks/use-debounce.hook.test.ts | 124 ++++++ .../lib/hooks/use-did-mount-effect.test.ts | 34 ++ .../lib/hooks/use-did-update-effect.test.ts | 38 ++ .../lib/hooks/use-disclosure.hook.test.ts | 89 +++++ .../checkbox/checkbox.component.test.tsx | 61 +++ .../input-number.component.test.tsx | 67 ++++ .../components/input/input.component.test.tsx | 92 +++++ .../restriction-panel-list-item.model.test.ts | 153 ++++++++ 17 files changed, 1696 insertions(+) create mode 100644 src/entities/current-partyroom/model/current-partyroom.store.test.ts create mode 100644 src/entities/music-preview/ui/preview-overlay.component.test.tsx create mode 100644 src/entities/ui-state/model/ui-state.store.test.ts create mode 100644 src/features/sign-in/by-social/model/pkce-storage.model.test.ts create mode 100644 src/shared/api/http/client/interceptors/response.test.ts create mode 100644 src/shared/api/http/error/error-emitter.test.ts create mode 100644 src/shared/api/http/error/get-error-code.test.ts create mode 100644 src/shared/api/http/error/get-error-message.test.ts create mode 100644 src/shared/api/http/error/is-auth-error.test.ts create mode 100644 src/shared/lib/hooks/use-debounce.hook.test.ts create mode 100644 src/shared/lib/hooks/use-did-mount-effect.test.ts create mode 100644 src/shared/lib/hooks/use-did-update-effect.test.ts create mode 100644 src/shared/lib/hooks/use-disclosure.hook.test.ts create mode 100644 src/shared/ui/components/checkbox/checkbox.component.test.tsx create mode 100644 src/shared/ui/components/input-number/input-number.component.test.tsx create mode 100644 src/shared/ui/components/input/input.component.test.tsx create mode 100644 src/widgets/partyroom-crews-panel/model/restriction-panel-list-item.model.test.ts diff --git a/src/entities/current-partyroom/model/current-partyroom.store.test.ts b/src/entities/current-partyroom/model/current-partyroom.store.test.ts new file mode 100644 index 00000000..4ebe7466 --- /dev/null +++ b/src/entities/current-partyroom/model/current-partyroom.store.test.ts @@ -0,0 +1,356 @@ +import { GradeType, MotionType } from '@/shared/api/http/types/@enums'; +import type { PartyroomCrew } from '@/shared/api/http/types/partyrooms'; +import type * as ChatMessage from './chat-message.model'; +import type * as Crew from './crew.model'; +import { createCurrentPartyroomStore } from './current-partyroom.store'; + +/* ------------------------------------------------------------------ */ +/* Factory helpers */ +/* ------------------------------------------------------------------ */ + +const createPartyroomCrew = (overrides: Partial = {}): PartyroomCrew => ({ + crewId: 1, + nickname: '테스트유저', + gradeType: GradeType.CLUBBER, + avatarBodyUri: 'body.png', + avatarFaceUri: 'face.png', + avatarIconUri: 'icon.png', + combinePositionX: 0, + combinePositionY: 0, + offsetX: 0, + offsetY: 0, + scale: 1, + ...overrides, +}); + +const createCrew = (overrides: Partial = {}): Crew.Model => ({ + ...createPartyroomCrew(), + motionType: MotionType.NONE, + ...overrides, +}); + +const createSystemChat = ( + overrides: Partial = {} +): ChatMessage.SystemChat => ({ + from: 'system', + content: '시스템 메시지', + receivedAt: Date.now(), + ...overrides, +}); + +const createUserChat = (overrides: Partial = {}): ChatMessage.UserChat => ({ + from: 'user', + crew: createPartyroomCrew(), + message: '안녕하세요', + receivedAt: Date.now(), + ...overrides, +}); + +/* ------------------------------------------------------------------ */ +/* Tests */ +/* ------------------------------------------------------------------ */ + +describe('current-partyroom store', () => { + test('초기 상태 검증', () => { + const store = createCurrentPartyroomStore(); + const state = store.getState(); + + expect(state.id).toBeUndefined(); + expect(state.exitedOnBackend).toBe(false); + expect(state.me).toBeUndefined(); + expect(state.playbackActivated).toBe(false); + expect(state.playback).toBeUndefined(); + expect(state.crews).toEqual([]); + expect(state.reaction).toEqual({ + history: { isLiked: false, isDisliked: false, isGrabbed: false }, + aggregation: { likeCount: 0, dislikeCount: 0, grabCount: 0 }, + motion: [], + }); + expect(state.currentDj).toBeUndefined(); + expect(state.notice).toBe(''); + expect(state.chat).toBeDefined(); + expect(state.alert).toBeDefined(); + }); + + describe('markExitedOnBackend', () => { + test('exitedOnBackend가 true로 변경된다', () => { + const store = createCurrentPartyroomStore(); + + store.getState().markExitedOnBackend(); + + expect(store.getState().exitedOnBackend).toBe(true); + }); + }); + + describe('updateMe', () => { + test('me 정보를 부분 업데이트한다', () => { + const store = createCurrentPartyroomStore(); + const initial = { crewId: 1, gradeType: GradeType.CLUBBER }; + + // 먼저 init으로 me를 설정 + store.getState().init({ + id: 1, + me: initial, + playbackActivated: false, + crews: [], + notice: '', + }); + + store.getState().updateMe({ gradeType: GradeType.MODERATOR }); + + expect(store.getState().me).toEqual({ + crewId: 1, + gradeType: GradeType.MODERATOR, + }); + }); + }); + + describe('updatePlaybackActivated', () => { + test('playbackActivated 값을 변경한다', () => { + const store = createCurrentPartyroomStore(); + + store.getState().updatePlaybackActivated(true); + + expect(store.getState().playbackActivated).toBe(true); + }); + }); + + describe('updatePlayback', () => { + test('playback 정보를 업데이트한다', () => { + const store = createCurrentPartyroomStore(); + const playback = { + id: 100, + name: '테스트 곡', + linkId: 'abc123', + duration: '03:45', + thumbnailImage: 'thumb.jpg', + endTime: 1722750394821, + }; + + store.getState().updatePlayback(() => playback); + + expect(store.getState().playback).toEqual(playback); + }); + + test('playback 정보를 부분 업데이트한다', () => { + const store = createCurrentPartyroomStore(); + const playback = { + id: 100, + name: '테스트 곡', + linkId: 'abc123', + duration: '03:45', + thumbnailImage: 'thumb.jpg', + endTime: 1722750394821, + }; + + store.getState().updatePlayback(() => playback); + store.getState().updatePlayback({ name: '변경된 곡' }); + + expect(store.getState().playback).toEqual({ + ...playback, + name: '변경된 곡', + }); + }); + }); + + describe('updateReaction', () => { + test('reaction aggregation을 업데이트한다', () => { + const store = createCurrentPartyroomStore(); + + store.getState().updateReaction({ + aggregation: { likeCount: 5, dislikeCount: 2, grabCount: 1 }, + }); + + const reaction = store.getState().reaction; + expect(reaction.aggregation).toEqual({ + likeCount: 5, + dislikeCount: 2, + grabCount: 1, + }); + // history는 변경되지 않음 + expect(reaction.history).toEqual({ + isLiked: false, + isDisliked: false, + isGrabbed: false, + }); + }); + }); + + describe('updateCrews', () => { + test('crews 배열을 설정한다', () => { + const store = createCurrentPartyroomStore(); + const crews = [ + createCrew({ crewId: 1, nickname: '유저1' }), + createCrew({ crewId: 2, nickname: '유저2' }), + ]; + + store.getState().updateCrews(() => crews); + + expect(store.getState().crews).toHaveLength(2); + expect(store.getState().crews[0].nickname).toBe('유저1'); + expect(store.getState().crews[1].nickname).toBe('유저2'); + }); + }); + + describe('resetCrewsMotion', () => { + test('모든 크루의 motionType이 NONE으로 초기화된다', () => { + const store = createCurrentPartyroomStore(); + const crews = [ + createCrew({ crewId: 1, motionType: MotionType.DANCE_TYPE_1 }), + createCrew({ crewId: 2, motionType: MotionType.DANCE_TYPE_2 }), + ]; + + store.getState().updateCrews(() => crews); + store.getState().resetCrewsMotion(); + + const result = store.getState().crews; + expect(result[0].motionType).toBe(MotionType.NONE); + expect(result[1].motionType).toBe(MotionType.NONE); + }); + + test('crews가 비어있으면 빈 배열을 유지한다', () => { + const store = createCurrentPartyroomStore(); + + store.getState().resetCrewsMotion(); + + expect(store.getState().crews).toEqual([]); + }); + }); + + describe('updateCurrentDj', () => { + test('현재 DJ를 설정한다', () => { + const store = createCurrentPartyroomStore(); + + store.getState().updateCurrentDj({ crewId: 42 }); + + expect(store.getState().currentDj).toEqual({ crewId: 42 }); + }); + + test('현재 DJ를 undefined로 해제한다', () => { + const store = createCurrentPartyroomStore(); + store.getState().updateCurrentDj({ crewId: 42 }); + + store.getState().updateCurrentDj(undefined); + + expect(store.getState().currentDj).toBeUndefined(); + }); + }); + + describe('updateNotice', () => { + test('공지사항 문자열을 설정한다', () => { + const store = createCurrentPartyroomStore(); + + store.getState().updateNotice('새로운 공지사항입니다'); + + expect(store.getState().notice).toBe('새로운 공지사항입니다'); + }); + }); + + describe('appendChatMessage', () => { + test('채팅 메시지를 추가한다', () => { + const store = createCurrentPartyroomStore(); + const message = createSystemChat({ content: '환영합니다' }); + + store.getState().appendChatMessage(message); + + const messages = store.getState().chat.getMessages(); + expect(messages).toHaveLength(1); + expect(messages[0]).toEqual(message); + }); + + test('여러 메시지를 순서대로 추가한다', () => { + const store = createCurrentPartyroomStore(); + const msg1 = createSystemChat({ content: '메시지1', receivedAt: 1000 }); + const msg2 = createUserChat({ message: '메시지2', receivedAt: 2000 }); + + store.getState().appendChatMessage(msg1); + store.getState().appendChatMessage(msg2); + + const messages = store.getState().chat.getMessages(); + expect(messages).toHaveLength(2); + expect(messages[0]).toEqual(msg1); + expect(messages[1]).toEqual(msg2); + }); + }); + + describe('updateChatMessage', () => { + test('조건에 맞는 메시지를 업데이트한다', () => { + const store = createCurrentPartyroomStore(); + const msg = createSystemChat({ content: '원본 메시지', receivedAt: 1000 }); + + store.getState().appendChatMessage(msg); + store.getState().updateChatMessage( + (m) => m.from === 'system' && m.content === '원본 메시지', + (m) => ({ ...m, content: '수정된 메시지' }) as ChatMessage.Model + ); + + const messages = store.getState().chat.getMessages(); + expect(messages[0]).toEqual(expect.objectContaining({ content: '수정된 메시지' })); + }); + }); + + describe('init', () => { + test('초기 상태를 리셋하고 전달된 값으로 병합한다', () => { + const store = createCurrentPartyroomStore(); + // 먼저 상태를 변경 + store.getState().updateNotice('이전 공지'); + store.getState().markExitedOnBackend(); + + const crews = [createCrew({ crewId: 10 })]; + store.getState().init({ + id: 99, + me: { crewId: 10, gradeType: GradeType.HOST }, + playbackActivated: true, + crews, + notice: '새 공지', + }); + + const state = store.getState(); + expect(state.id).toBe(99); + expect(state.me).toEqual({ crewId: 10, gradeType: GradeType.HOST }); + expect(state.playbackActivated).toBe(true); + expect(state.crews).toEqual(crews); + expect(state.notice).toBe('새 공지'); + // 초기 상태로 리셋된 값 + expect(state.exitedOnBackend).toBe(false); + }); + }); + + describe('reset', () => { + test('모든 상태를 초기 상태로 복원한다', () => { + const store = createCurrentPartyroomStore(); + // 상태를 변경 + store.getState().init({ + id: 99, + me: { crewId: 10, gradeType: GradeType.HOST }, + playbackActivated: true, + crews: [createCrew()], + notice: '공지사항', + }); + store.getState().markExitedOnBackend(); + + store.getState().reset(); + + const state = store.getState(); + expect(state.id).toBeUndefined(); + expect(state.exitedOnBackend).toBe(false); + expect(state.me).toBeUndefined(); + expect(state.playbackActivated).toBe(false); + expect(state.playback).toBeUndefined(); + expect(state.crews).toEqual([]); + expect(state.notice).toBe(''); + expect(state.currentDj).toBeUndefined(); + }); + + test('reset 후 chat의 메시지가 비워진다', () => { + const store = createCurrentPartyroomStore(); + store.getState().appendChatMessage(createSystemChat()); + + const chatRef = store.getState().chat; + store.getState().reset(); + + // chat.clear()가 호출되어 메시지가 비워짐 + expect(chatRef.getMessages()).toHaveLength(0); + }); + }); +}); diff --git a/src/entities/music-preview/ui/preview-overlay.component.test.tsx b/src/entities/music-preview/ui/preview-overlay.component.test.tsx new file mode 100644 index 00000000..01160e8c --- /dev/null +++ b/src/entities/music-preview/ui/preview-overlay.component.test.tsx @@ -0,0 +1,86 @@ +import React from 'react'; +import { render, fireEvent } from '@testing-library/react'; +import PreviewOverlay from './preview-overlay.component'; + +jest.mock('react', () => { + const actual = jest.requireActual('react'); + globalThis.React = actual; + return actual; +}); + +describe('PreviewOverlay', () => { + const defaultProps = { + isPlaying: false, + onPlay: jest.fn(), + onStop: jest.fn(), + }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + test('초기 상태에서 오버레이 내부 컨텐츠가 숨겨져 있다', () => { + const { container } = render(); + + const root = container.firstElementChild as HTMLElement; + expect(root.querySelector('.bg-white')).toBeNull(); + }); + + test('hover 시 오버레이 내부가 표시된다', () => { + const { container } = render(); + + const root = container.firstElementChild as HTMLElement; + fireEvent.mouseEnter(root); + + expect(root.querySelector('.bg-white')).not.toBeNull(); + }); + + test('unhover 시 오버레이 내부가 다시 숨겨진다', () => { + const { container } = render(); + + const root = container.firstElementChild as HTMLElement; + fireEvent.mouseEnter(root); + expect(root.querySelector('.bg-white')).not.toBeNull(); + + fireEvent.mouseLeave(root); + expect(root.querySelector('.bg-white')).toBeNull(); + }); + + test('isPlaying=false 상태에서 클릭하면 onPlay 콜백이 호출된다', () => { + const onPlay = jest.fn(); + const { container } = render( + + ); + + const root = container.firstElementChild as HTMLElement; + fireEvent.click(root); + + expect(onPlay).toHaveBeenCalledTimes(1); + }); + + test('isPlaying=true 상태에서 클릭하면 onStop 콜백이 호출된다', () => { + const onStop = jest.fn(); + const { container } = render( + + ); + + const root = container.firstElementChild as HTMLElement; + fireEvent.click(root); + + expect(onStop).toHaveBeenCalledTimes(1); + }); + + test('클릭 시 이벤트 전파가 차단된다 (stopPropagation)', () => { + const outerHandler = jest.fn(); + const { container } = render( +
+ +
+ ); + + const overlay = container.querySelector('.cursor-pointer') as HTMLElement; + fireEvent.click(overlay); + + expect(outerHandler).not.toHaveBeenCalled(); + }); +}); diff --git a/src/entities/ui-state/model/ui-state.store.test.ts b/src/entities/ui-state/model/ui-state.store.test.ts new file mode 100644 index 00000000..8d5ad1d9 --- /dev/null +++ b/src/entities/ui-state/model/ui-state.store.test.ts @@ -0,0 +1,106 @@ +jest.mock('@/shared/lib/functions/log/with-debugger', () => ({ + __esModule: true, + default: () => (fn: any) => fn, +})); + +jest.mock('@/shared/lib/functions/log/logger', () => ({ + warnLog: jest.fn(), +})); + +import { warnLog } from '@/shared/lib/functions/log/logger'; +import { createUIStateStore } from './ui-state.store'; + +const mockedWarnLog = warnLog as jest.Mock; + +describe('ui-state store', () => { + beforeEach(() => jest.clearAllMocks()); + + test('초기 상태: open=false, interactable=true, zIndex=30, selectedPlaylist=undefined', () => { + const store = createUIStateStore(); + const { playlistDrawer } = store.getState(); + + expect(playlistDrawer.open).toBe(false); + expect(playlistDrawer.interactable).toBe(true); + expect(playlistDrawer.zIndex).toBe(30); + expect(playlistDrawer.selectedPlaylist).toBeUndefined(); + }); + + describe('setPlaylistDrawer', () => { + test('open을 true로 변경한다', () => { + const store = createUIStateStore(); + + store.getState().setPlaylistDrawer({ open: true }); + + const { playlistDrawer } = store.getState(); + expect(playlistDrawer.open).toBe(true); + expect(playlistDrawer.interactable).toBe(true); + expect(playlistDrawer.zIndex).toBe(30); + }); + + test('open=true일 때 interactable과 zIndex를 변경할 수 있다', () => { + const store = createUIStateStore(); + + store.getState().setPlaylistDrawer({ open: true, interactable: false, zIndex: 50 }); + + const { playlistDrawer } = store.getState(); + expect(playlistDrawer.open).toBe(true); + expect(playlistDrawer.interactable).toBe(false); + expect(playlistDrawer.zIndex).toBe(50); + }); + + test('open=false로 변경하면 interactable, zIndex, selectedPlaylist가 자동 초기화된다', () => { + const store = createUIStateStore(); + + // 먼저 drawer를 열고 값 변경 + store.getState().setPlaylistDrawer({ + open: true, + interactable: false, + zIndex: 999, + selectedPlaylist: { + id: 1, + name: '테스트', + orderNumber: 1, + type: 'PLAYLIST' as any, + musicCount: 5, + }, + }); + + // drawer 닫기 + store.getState().setPlaylistDrawer({ open: false }); + + const { playlistDrawer } = store.getState(); + expect(playlistDrawer.open).toBe(false); + expect(playlistDrawer.interactable).toBe(true); + expect(playlistDrawer.zIndex).toBe(30); + expect(playlistDrawer.selectedPlaylist).toBeUndefined(); + }); + + test('open=false일 때 초기값과 다른 값이 있으면 warnLog를 호출한다', () => { + const store = createUIStateStore(); + + // drawer를 열고 값 변경 + store.getState().setPlaylistDrawer({ + open: true, + interactable: false, + zIndex: 999, + }); + + // drawer 닫기 - 변경된 interactable과 zIndex에 대해 warnLog 호출 예상 + store.getState().setPlaylistDrawer({ open: false }); + + expect(mockedWarnLog).toHaveBeenCalledWith(expect.stringContaining('interactable')); + expect(mockedWarnLog).toHaveBeenCalledWith(expect.stringContaining('zIndex')); + }); + + test('open=false일 때 값이 이미 초기값이면 warnLog를 호출하지 않는다', () => { + const store = createUIStateStore(); + + // drawer를 열었다가 값 변경 없이 닫기 + store.getState().setPlaylistDrawer({ open: true }); + store.getState().setPlaylistDrawer({ open: false }); + + // interactable, zIndex, selectedPlaylist 모두 이미 초기값이므로 로그 없음 + expect(mockedWarnLog).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/src/features/sign-in/by-social/model/pkce-storage.model.test.ts b/src/features/sign-in/by-social/model/pkce-storage.model.test.ts new file mode 100644 index 00000000..70d7077b --- /dev/null +++ b/src/features/sign-in/by-social/model/pkce-storage.model.test.ts @@ -0,0 +1,43 @@ +import { StorageKey, PKCEStorage } from './pkce-storage.model'; + +describe('PKCEStorage', () => { + beforeEach(() => { + sessionStorage.clear(); + }); + + test('setItem 후 getItem으로 저장된 값 반환', () => { + PKCEStorage.setItem(StorageKey.CODE_VERIFIER, 'test-verifier'); + + expect(PKCEStorage.getItem(StorageKey.CODE_VERIFIER)).toBe('test-verifier'); + }); + + test('removeItem 후 getItem은 null 반환', () => { + PKCEStorage.setItem(StorageKey.STATE, 'test-state'); + PKCEStorage.removeItem(StorageKey.STATE); + + expect(PKCEStorage.getItem(StorageKey.STATE)).toBeNull(); + }); + + test('각 StorageKey(CODE_VERIFIER, STATE) 독립적으로 동작', () => { + PKCEStorage.setItem(StorageKey.CODE_VERIFIER, 'verifier-value'); + PKCEStorage.setItem(StorageKey.STATE, 'state-value'); + + expect(PKCEStorage.getItem(StorageKey.CODE_VERIFIER)).toBe('verifier-value'); + expect(PKCEStorage.getItem(StorageKey.STATE)).toBe('state-value'); + }); + + test('서로 다른 키는 독립적으로 저장되어 하나를 삭제해도 다른 키에 영향 없음', () => { + PKCEStorage.setItem(StorageKey.CODE_VERIFIER, 'verifier-value'); + PKCEStorage.setItem(StorageKey.STATE, 'state-value'); + + PKCEStorage.removeItem(StorageKey.CODE_VERIFIER); + + expect(PKCEStorage.getItem(StorageKey.CODE_VERIFIER)).toBeNull(); + expect(PKCEStorage.getItem(StorageKey.STATE)).toBe('state-value'); + }); + + test('저장되지 않은 키를 조회하면 null 반환', () => { + expect(PKCEStorage.getItem(StorageKey.CODE_VERIFIER)).toBeNull(); + expect(PKCEStorage.getItem(StorageKey.STATE)).toBeNull(); + }); +}); diff --git a/src/shared/api/http/client/interceptors/response.test.ts b/src/shared/api/http/client/interceptors/response.test.ts new file mode 100644 index 00000000..e64470ec --- /dev/null +++ b/src/shared/api/http/client/interceptors/response.test.ts @@ -0,0 +1,208 @@ +jest.mock('@/shared/lib/functions/log/with-debugger', () => ({ + __esModule: true, + default: () => (fn: any) => fn, +})); + +jest.mock('@/shared/lib/functions/log/network-log', () => ({ + printResponseLog: jest.fn(), + printErrorLog: jest.fn(), +})); + +jest.mock('@/shared/api/http/error/get-error-message', () => ({ + getErrorMessage: jest.fn(() => 'mocked error message'), +})); + +jest.mock('@/shared/api/http/error/get-error-code', () => ({ + getErrorCode: jest.fn(), +})); + +jest.mock('@/shared/api/http/error/error-emitter', () => ({ + __esModule: true, + default: { emit: jest.fn() }, +})); + +jest.mock('@/shared/lib/functions/is-pure-object', () => ({ + isPureObject: jest.fn( + (obj: unknown) => obj !== null && typeof obj === 'object' && !Array.isArray(obj) + ), +})); + +import { AxiosError, AxiosResponse } from 'axios'; +import errorEmitter from '@/shared/api/http/error/error-emitter'; +import { getErrorCode } from '@/shared/api/http/error/get-error-code'; +import { getErrorMessage } from '@/shared/api/http/error/get-error-message'; +import { printErrorLog, printResponseLog } from '@/shared/lib/functions/log/network-log'; +import { logResponse, unwrapResponse, logError, unwrapError, emitError } from './response'; + +beforeEach(() => { + jest.clearAllMocks(); +}); + +function createAxiosResponse(overrides: { + data?: any; + method?: string; + url?: string; +}): AxiosResponse { + return { + data: overrides.data, + status: 200, + statusText: 'OK', + headers: {}, + config: { + method: overrides.method ?? 'get', + url: overrides.url ?? '/api/test', + headers: {} as any, + }, + }; +} + +function createAxiosError(overrides: { + method?: string; + url?: string; + responseData?: any; + hasResponse?: boolean; +}): AxiosError { + return { + message: 'Request failed', + name: 'AxiosError', + isAxiosError: true, + toJSON: () => ({}), + config: { + method: overrides.method ?? 'post', + url: overrides.url ?? '/api/test', + headers: {} as any, + }, + response: + overrides.hasResponse === false + ? undefined + : { + data: overrides.responseData ?? {}, + status: 500, + statusText: 'Internal Server Error', + headers: {}, + config: { headers: {} as any }, + }, + } as AxiosError; +} + +describe('response interceptors', () => { + describe('logResponse', () => { + test('응답 로그 출력 후 response 그대로 반환', () => { + const response = createAxiosResponse({ + data: { data: { id: 1 } }, + method: 'get', + url: '/api/users', + }); + + const result = logResponse(response); + + expect(printResponseLog).toHaveBeenCalledWith({ + method: 'get', + endPoint: '/api/users', + response: { id: 1 }, + }); + expect(result).toBe(response); + }); + + test('data.data 없으면 data 자체를 로그에 사용', () => { + const response = createAxiosResponse({ + data: { message: 'ok' }, + method: 'post', + url: '/api/health', + }); + + logResponse(response); + + expect(printResponseLog).toHaveBeenCalledWith({ + method: 'post', + endPoint: '/api/health', + response: { message: 'ok' }, + }); + }); + }); + + describe('unwrapResponse', () => { + test('data.data 있으면 data.data 반환', () => { + const response = createAxiosResponse({ data: { data: { id: 1, name: 'test' } } }); + + expect(unwrapResponse(response)).toEqual({ id: 1, name: 'test' }); + }); + + test('data.data 없으면 data 반환', () => { + const response = createAxiosResponse({ data: { message: 'ok' } }); + + expect(unwrapResponse(response)).toEqual({ message: 'ok' }); + }); + }); + + describe('logError', () => { + test('getErrorMessage 호출, printErrorLog 호출, Promise.reject 반환', async () => { + const error = createAxiosError({ method: 'post', url: '/api/users' }); + + await expect(logError(error)).rejects.toBe(error); + + expect(getErrorMessage).toHaveBeenCalledWith(error); + expect(printErrorLog).toHaveBeenCalledWith({ + method: 'post', + endPoint: '/api/users', + errorMessage: 'mocked error message', + error, + }); + }); + }); + + describe('unwrapError', () => { + test('response.data에 data 프로퍼티 있으면 e.response.data를 data.data로 교체', async () => { + const error = createAxiosError({ + responseData: { data: { errorCode: 'JWT-001' } }, + }); + + await expect(unwrapError(error)).rejects.toBe(error); + const response = error.response as { data: unknown }; + expect(response.data).toEqual({ errorCode: 'JWT-001' }); + }); + + test('response.data에 data 프로퍼티 없으면 그대로 유지', async () => { + const originalData = { message: 'error' }; + const error = createAxiosError({ responseData: originalData }); + + await expect(unwrapError(error)).rejects.toBe(error); + const response = error.response as { data: unknown }; + expect(response.data).toEqual(originalData); + }); + + test('response 없으면 그대로 reject', async () => { + const error = createAxiosError({ hasResponse: false }); + + await expect(unwrapError(error)).rejects.toBe(error); + }); + }); + + describe('emitError', () => { + test('유효한 ErrorCode 있으면 errorEmitter.emit 호출', async () => { + (getErrorCode as jest.Mock).mockReturnValue('JWT-001'); + + const error = createAxiosError({}); + + await expect(emitError(error)).rejects.toBe(error); + expect(errorEmitter.emit).toHaveBeenCalledWith('JWT-001'); + }); + + test('ErrorCode 없으면 emit 호출 안 함', async () => { + (getErrorCode as jest.Mock).mockReturnValue(undefined); + + const error = createAxiosError({}); + + await expect(emitError(error)).rejects.toBe(error); + expect(errorEmitter.emit).not.toHaveBeenCalled(); + }); + + test('항상 Promise.reject 반환', async () => { + (getErrorCode as jest.Mock).mockReturnValue(undefined); + + const error = createAxiosError({}); + + await expect(emitError(error)).rejects.toBe(error); + }); + }); +}); diff --git a/src/shared/api/http/error/error-emitter.test.ts b/src/shared/api/http/error/error-emitter.test.ts new file mode 100644 index 00000000..1b47243c --- /dev/null +++ b/src/shared/api/http/error/error-emitter.test.ts @@ -0,0 +1,65 @@ +import errorEmitter from './error-emitter'; +import { ErrorCode } from '../types/@shared'; + +beforeEach(() => { + jest.clearAllMocks(); +}); + +describe('ErrorEmitter', () => { + test('싱글턴 인스턴스 — 동일한 객체 참조', () => { + // 모듈 레벨 export가 항상 같은 인스턴스를 반환하는지 확인 + expect(errorEmitter).toBeDefined(); + expect(typeof errorEmitter.on).toBe('function'); + expect(typeof errorEmitter.emit).toBe('function'); + }); + + test('emit(ErrorCode) → 구독자에게 전달', () => { + const callback = jest.fn(); + const unsubscribe = errorEmitter.on(ErrorCode.ACCESS_TOKEN_EXPIRED, callback); + + errorEmitter.emit(ErrorCode.ACCESS_TOKEN_EXPIRED); + + expect(callback).toHaveBeenCalledTimes(1); + + unsubscribe(); + }); + + test('on/emit/unsubscribe 라이프사이클 — 구독 해제 후 콜백 호출 안 됨', () => { + const callback = jest.fn(); + const unsubscribe = errorEmitter.on(ErrorCode.UNAUTHORIZED_SESSION, callback); + + errorEmitter.emit(ErrorCode.UNAUTHORIZED_SESSION); + expect(callback).toHaveBeenCalledTimes(1); + + unsubscribe(); + + errorEmitter.emit(ErrorCode.UNAUTHORIZED_SESSION); + expect(callback).toHaveBeenCalledTimes(1); + }); + + test('다른 ErrorCode 이벤트는 구독자에게 전달되지 않음', () => { + const callback = jest.fn(); + const unsubscribe = errorEmitter.on(ErrorCode.ACCESS_TOKEN_NOT_FOUND, callback); + + errorEmitter.emit(ErrorCode.ALREADY_BLOCKED_CREW); + + expect(callback).not.toHaveBeenCalled(); + + unsubscribe(); + }); + + test('같은 ErrorCode에 여러 구독자 등록 가능', () => { + const cb1 = jest.fn(); + const cb2 = jest.fn(); + const unsub1 = errorEmitter.on(ErrorCode.NOT_FOUND_ROOM, cb1); + const unsub2 = errorEmitter.on(ErrorCode.NOT_FOUND_ROOM, cb2); + + errorEmitter.emit(ErrorCode.NOT_FOUND_ROOM); + + expect(cb1).toHaveBeenCalledTimes(1); + expect(cb2).toHaveBeenCalledTimes(1); + + unsub1(); + unsub2(); + }); +}); diff --git a/src/shared/api/http/error/get-error-code.test.ts b/src/shared/api/http/error/get-error-code.test.ts new file mode 100644 index 00000000..b5eab582 --- /dev/null +++ b/src/shared/api/http/error/get-error-code.test.ts @@ -0,0 +1,65 @@ +jest.mock('axios', () => ({ + isAxiosError: jest.fn(), +})); + +jest.mock('@/shared/lib/functions/log/with-debugger', () => ({ + __esModule: true, + default: () => (fn: any) => fn, +})); + +jest.mock('@/shared/lib/functions/log/logger', () => ({ + warnLog: jest.fn(), +})); + +import { isAxiosError } from 'axios'; +import { warnLog } from '@/shared/lib/functions/log/logger'; +import { getErrorCode } from './get-error-code'; +import { ErrorCode } from '../types/@shared'; + +beforeEach(() => { + jest.clearAllMocks(); +}); + +function createAxiosErrorWithCode(errorCode: string, nested = false) { + const response = nested ? { data: { data: { errorCode } } } : { data: { errorCode } }; + + return { response, isAxiosError: true }; +} + +describe('getErrorCode', () => { + test('AxiosError + 유효한 ErrorCode → 해당 코드 반환', () => { + const error = createAxiosErrorWithCode(ErrorCode.ACCESS_TOKEN_EXPIRED); + (isAxiosError as jest.Mock).mockReturnValue(true); + + expect(getErrorCode(error)).toBe(ErrorCode.ACCESS_TOKEN_EXPIRED); + }); + + test('AxiosError + 중첩 data.data.errorCode 구조 → 코드 추출', () => { + const error = createAxiosErrorWithCode(ErrorCode.UNAUTHORIZED_SESSION, true); + (isAxiosError as jest.Mock).mockReturnValue(true); + + expect(getErrorCode(error)).toBe(ErrorCode.UNAUTHORIZED_SESSION); + }); + + test('AxiosError + 알 수 없는 errorCode → undefined 반환, warnLog 호출', () => { + const error = createAxiosErrorWithCode('UNKNOWN-999'); + (isAxiosError as jest.Mock).mockReturnValue(true); + + expect(getErrorCode(error)).toBeUndefined(); + expect(warnLog).toHaveBeenCalledWith('Unknown errorCode: UNKNOWN-999'); + }); + + test('AxiosError + errorCode 없음 → undefined', () => { + const error = { response: { data: {} }, isAxiosError: true }; + (isAxiosError as jest.Mock).mockReturnValue(true); + + expect(getErrorCode(error)).toBeUndefined(); + }); + + test('일반 Error → undefined', () => { + (isAxiosError as jest.Mock).mockReturnValue(false); + + const error = new Error('일반 에러'); + expect(getErrorCode(error)).toBeUndefined(); + }); +}); diff --git a/src/shared/api/http/error/get-error-message.test.ts b/src/shared/api/http/error/get-error-message.test.ts new file mode 100644 index 00000000..b7b5b4ee --- /dev/null +++ b/src/shared/api/http/error/get-error-message.test.ts @@ -0,0 +1,61 @@ +jest.mock('axios', () => ({ + isAxiosError: jest.fn(), +})); + +import { isAxiosError } from 'axios'; +import { getErrorMessage } from './get-error-message'; + +beforeEach(() => { + jest.clearAllMocks(); +}); + +function createAxiosError(overrides: { message?: string; responseData?: Record }) { + return { + message: overrides.message ?? 'Request failed', + response: overrides.responseData ? { data: overrides.responseData } : undefined, + isAxiosError: true, + }; +} + +describe('getErrorMessage', () => { + test('string 입력 → 그대로 반환', () => { + (isAxiosError as jest.Mock).mockReturnValue(false); + + expect(getErrorMessage('서버 오류')).toBe('서버 오류'); + }); + + test('AxiosError + response.data.message 있음 → data.message 반환', () => { + const error = createAxiosError({ + message: 'Request failed', + responseData: { message: '인증이 만료되었습니다' }, + }); + (isAxiosError as jest.Mock).mockReturnValue(true); + + expect(getErrorMessage(error)).toBe('인증이 만료되었습니다'); + }); + + test('AxiosError + response.data.message 없음 → err.message 반환', () => { + const error = createAxiosError({ + message: 'Network Error', + responseData: {}, + }); + (isAxiosError as jest.Mock).mockReturnValue(true); + + expect(getErrorMessage(error)).toBe('Network Error'); + }); + + test('Error 인스턴스 → err.message 반환', () => { + (isAxiosError as jest.Mock).mockReturnValue(false); + + const error = new Error('일반 에러'); + expect(getErrorMessage(error)).toBe('일반 에러'); + }); + + test('unknown 타입 → "Unknown error occurred" 반환', () => { + (isAxiosError as jest.Mock).mockReturnValue(false); + + expect(getErrorMessage(12345)).toBe('Unknown error occurred'); + expect(getErrorMessage(null)).toBe('Unknown error occurred'); + expect(getErrorMessage(undefined)).toBe('Unknown error occurred'); + }); +}); diff --git a/src/shared/api/http/error/is-auth-error.test.ts b/src/shared/api/http/error/is-auth-error.test.ts new file mode 100644 index 00000000..7e00a5a9 --- /dev/null +++ b/src/shared/api/http/error/is-auth-error.test.ts @@ -0,0 +1,48 @@ +jest.mock('axios', () => ({ + isAxiosError: jest.fn(), +})); + +import { isAxiosError } from 'axios'; +import isAuthError from './is-auth-error'; + +beforeEach(() => { + jest.clearAllMocks(); +}); + +function createAxiosError(overrides: { status?: number; responseStatus?: number }) { + return { + status: overrides.status, + response: overrides.responseStatus != null ? { status: overrides.responseStatus } : undefined, + isAxiosError: true, + }; +} + +describe('isAuthError', () => { + test('AxiosError + status 401 → true', () => { + const error = createAxiosError({ status: 401 }); + (isAxiosError as jest.Mock).mockReturnValue(true); + + expect(isAuthError(error)).toBe(true); + }); + + test('AxiosError + response.status 401 → true', () => { + const error = createAxiosError({ responseStatus: 401 }); + (isAxiosError as jest.Mock).mockReturnValue(true); + + expect(isAuthError(error)).toBe(true); + }); + + test('AxiosError + status 403 → false', () => { + const error = createAxiosError({ status: 403, responseStatus: 403 }); + (isAxiosError as jest.Mock).mockReturnValue(true); + + expect(isAuthError(error)).toBe(false); + }); + + test('일반 Error → false', () => { + (isAxiosError as jest.Mock).mockReturnValue(false); + + const error = new Error('일반 에러'); + expect(isAuthError(error)).toBe(false); + }); +}); diff --git a/src/shared/lib/hooks/use-debounce.hook.test.ts b/src/shared/lib/hooks/use-debounce.hook.test.ts new file mode 100644 index 00000000..febabd0b --- /dev/null +++ b/src/shared/lib/hooks/use-debounce.hook.test.ts @@ -0,0 +1,124 @@ +import { ChangeEvent } from 'react'; +import { renderHook, act } from '@testing-library/react'; +import { useDebounce } from './use-debounce.hook'; + +describe('useDebounce', () => { + beforeEach(() => { + jest.useFakeTimers(); + }); + + afterEach(() => { + jest.useRealTimers(); + }); + + test('value는 handleChange 호출 즉시 업데이트', () => { + const callback = jest.fn(); + const { result } = renderHook(() => useDebounce(callback)); + + act(() => { + result.current.handleChange({ + target: { value: 'test' }, + } as ChangeEvent); + }); + + expect(result.current.value).toBe('test'); + }); + + test('callback은 기본 interval(500ms) 후 호출', () => { + const callback = jest.fn(); + const { result } = renderHook(() => useDebounce(callback)); + + act(() => { + result.current.handleChange({ + target: { value: 'hello' }, + } as ChangeEvent); + }); + + expect(callback).not.toHaveBeenCalled(); + + act(() => { + jest.advanceTimersByTime(500); + }); + + expect(callback).toHaveBeenCalledTimes(1); + expect(callback).toHaveBeenCalledWith('hello'); + }); + + test('연속 입력 시 이전 타이머 취소, 마지막 값만 콜백', () => { + const callback = jest.fn(); + const { result } = renderHook(() => useDebounce(callback)); + + act(() => { + result.current.handleChange({ + target: { value: 'h' }, + } as ChangeEvent); + }); + + act(() => { + jest.advanceTimersByTime(200); + }); + + act(() => { + result.current.handleChange({ + target: { value: 'he' }, + } as ChangeEvent); + }); + + act(() => { + jest.advanceTimersByTime(200); + }); + + act(() => { + result.current.handleChange({ + target: { value: 'hel' }, + } as ChangeEvent); + }); + + act(() => { + jest.advanceTimersByTime(500); + }); + + expect(callback).toHaveBeenCalledTimes(1); + expect(callback).toHaveBeenCalledWith('hel'); + }); + + test('커스텀 interval 적용', () => { + const callback = jest.fn(); + const { result } = renderHook(() => useDebounce(callback, { interval: 1000 })); + + act(() => { + result.current.handleChange({ + target: { value: 'test' }, + } as ChangeEvent); + }); + + act(() => { + jest.advanceTimersByTime(500); + }); + + expect(callback).not.toHaveBeenCalled(); + + act(() => { + jest.advanceTimersByTime(500); + }); + + expect(callback).toHaveBeenCalledTimes(1); + expect(callback).toHaveBeenCalledWith('test'); + }); + + test('callback이 undefined여도 에러 없음', () => { + const { result } = renderHook(() => useDebounce(undefined)); + + act(() => { + result.current.handleChange({ + target: { value: 'test' }, + } as ChangeEvent); + }); + + act(() => { + jest.advanceTimersByTime(500); + }); + + expect(result.current.value).toBe('test'); + }); +}); diff --git a/src/shared/lib/hooks/use-did-mount-effect.test.ts b/src/shared/lib/hooks/use-did-mount-effect.test.ts new file mode 100644 index 00000000..d2bc77da --- /dev/null +++ b/src/shared/lib/hooks/use-did-mount-effect.test.ts @@ -0,0 +1,34 @@ +import { renderHook } from '@testing-library/react'; +import useDidMountEffect from './use-did-mount-effect'; + +describe('useDidMountEffect', () => { + test('마운트 완료 후 effect 실행', () => { + const effect = jest.fn(); + + renderHook(() => useDidMountEffect(effect)); + + expect(effect).toHaveBeenCalledTimes(1); + }); + + test('cleanup 함수 호출 확인', () => { + const cleanup = jest.fn(); + const effect = jest.fn(() => cleanup); + + const { unmount } = renderHook(() => useDidMountEffect(effect)); + + unmount(); + + expect(cleanup).toHaveBeenCalledTimes(1); + }); + + test('effect가 한 번만 실행됨', () => { + const effect = jest.fn(); + + const { rerender } = renderHook(() => useDidMountEffect(effect)); + + rerender(); + rerender(); + + expect(effect).toHaveBeenCalledTimes(1); + }); +}); diff --git a/src/shared/lib/hooks/use-did-update-effect.test.ts b/src/shared/lib/hooks/use-did-update-effect.test.ts new file mode 100644 index 00000000..b6c29ebd --- /dev/null +++ b/src/shared/lib/hooks/use-did-update-effect.test.ts @@ -0,0 +1,38 @@ +import { renderHook } from '@testing-library/react'; +import useDidUpdateEffect from './use-did-update-effect'; + +describe('useDidUpdateEffect', () => { + test('mount 시 effect 실행 안 됨', () => { + const effect = jest.fn(); + + renderHook(({ dep }) => useDidUpdateEffect(effect, [dep]), { + initialProps: { dep: 1 }, + }); + + expect(effect).not.toHaveBeenCalled(); + }); + + test('dependency 변경 시 effect 실행', () => { + const effect = jest.fn(); + + const { rerender } = renderHook(({ dep }) => useDidUpdateEffect(effect, [dep]), { + initialProps: { dep: 1 }, + }); + + rerender({ dep: 2 }); + + expect(effect).toHaveBeenCalledTimes(1); + }); + + test('dependency 미변경 시 effect 실행 안 됨', () => { + const effect = jest.fn(); + + const { rerender } = renderHook(({ dep }) => useDidUpdateEffect(effect, [dep]), { + initialProps: { dep: 1 }, + }); + + rerender({ dep: 1 }); + + expect(effect).not.toHaveBeenCalled(); + }); +}); diff --git a/src/shared/lib/hooks/use-disclosure.hook.test.ts b/src/shared/lib/hooks/use-disclosure.hook.test.ts new file mode 100644 index 00000000..c927cfde --- /dev/null +++ b/src/shared/lib/hooks/use-disclosure.hook.test.ts @@ -0,0 +1,89 @@ +import { renderHook, act } from '@testing-library/react'; +import { useDisclosure } from './use-disclosure.hook'; + +describe('useDisclosure', () => { + test('기본 상태: open=false', () => { + const { result } = renderHook(() => useDisclosure()); + + expect(result.current.open).toBe(false); + }); + + test('defaultOpen: true → 초기 open=true', () => { + const { result } = renderHook(() => useDisclosure({ defaultOpen: true })); + + expect(result.current.open).toBe(true); + }); + + test('onOpen() 호출 → open=true', () => { + const { result } = renderHook(() => useDisclosure()); + + act(() => { + result.current.onOpen(); + }); + + expect(result.current.open).toBe(true); + }); + + test('onClose() 호출 → open=false', () => { + const { result } = renderHook(() => useDisclosure({ defaultOpen: true })); + + act(() => { + result.current.onClose(); + }); + + expect(result.current.open).toBe(false); + }); + + test('onToggle() 호출 → 상태 반전', () => { + const { result } = renderHook(() => useDisclosure()); + + expect(result.current.open).toBe(false); + + act(() => { + result.current.onToggle(); + }); + expect(result.current.open).toBe(true); + + act(() => { + result.current.onToggle(); + }); + expect(result.current.open).toBe(false); + }); + + test('제어 모드: open prop이 내부 상태를 오버라이드', () => { + const { result } = renderHook(() => useDisclosure({ open: true })); + + expect(result.current.open).toBe(true); + + act(() => { + result.current.onClose(); + }); + + // 제어 모드이므로 내부 상태 변경 없이 open prop 값 유지 + expect(result.current.open).toBe(true); + }); + + test('onOpen 콜백 함수 호출 확인', () => { + const onOpenCallback = jest.fn(); + const { result } = renderHook(() => useDisclosure({ onOpen: onOpenCallback })); + + act(() => { + result.current.onOpen(); + }); + + expect(onOpenCallback).toHaveBeenCalledTimes(1); + }); + + test('onClose 콜백 함수 호출 확인', () => { + const onCloseCallback = jest.fn(); + const { result } = renderHook(() => + useDisclosure({ defaultOpen: true, onClose: onCloseCallback }) + ); + + act(() => { + result.current.onClose(); + }); + + expect(onCloseCallback).toHaveBeenCalledTimes(1); + }); +}); diff --git a/src/shared/ui/components/checkbox/checkbox.component.test.tsx b/src/shared/ui/components/checkbox/checkbox.component.test.tsx new file mode 100644 index 00000000..4bbdcdcf --- /dev/null +++ b/src/shared/ui/components/checkbox/checkbox.component.test.tsx @@ -0,0 +1,61 @@ +import React, { createRef } from 'react'; +import { render, fireEvent } from '@testing-library/react'; +import Checkbox from './checkbox.component'; + +jest.mock('react', () => { + const actual = jest.requireActual('react'); + globalThis.React = actual; + return actual; +}); + +jest.mock('@/shared/ui/icons', () => ({ + PFCheckMark: (props: any) => , +})); + +describe('Checkbox', () => { + test('클릭으로 체크/해제 토글이 동작한다', () => { + const { container } = render(); + + const checkbox = container.querySelector('input[type="checkbox"]') as HTMLInputElement; + const label = container.querySelector('label') as HTMLElement; + + expect(checkbox.checked).toBe(false); + + fireEvent.click(label); + expect(checkbox.checked).toBe(true); + + fireEvent.click(label); + expect(checkbox.checked).toBe(false); + }); + + test('disabled 상태에서는 클릭해도 토글되지 않는다', () => { + const { container } = render(); + + const checkbox = container.querySelector('input[type="checkbox"]') as HTMLInputElement; + const label = container.querySelector('label') as HTMLElement; + + expect(checkbox.checked).toBe(false); + + fireEvent.click(label); + expect(checkbox.checked).toBe(false); + }); + + test('onChange 콜백이 호출된다', () => { + const onChange = jest.fn(); + const { container } = render(); + + const label = container.querySelector('label') as HTMLElement; + fireEvent.click(label); + + expect(onChange).toHaveBeenCalledTimes(1); + }); + + test('ref가 input 요소에 전달된다', () => { + const ref = createRef(); + render(); + + expect(ref.current).not.toBeNull(); + expect(ref.current).toBeInstanceOf(HTMLInputElement); + expect((ref.current as HTMLInputElement).type).toBe('checkbox'); + }); +}); diff --git a/src/shared/ui/components/input-number/input-number.component.test.tsx b/src/shared/ui/components/input-number/input-number.component.test.tsx new file mode 100644 index 00000000..761626d1 --- /dev/null +++ b/src/shared/ui/components/input-number/input-number.component.test.tsx @@ -0,0 +1,67 @@ +import React from 'react'; +import { render, screen, fireEvent } from '@testing-library/react'; +import InputNumber from './input-number.component'; + +jest.mock('react', () => { + const actual = jest.requireActual('react'); + globalThis.React = actual; + return actual; +}); + +jest.mock('@/shared/lib/functions/combine-ref', () => ({ + combineRef: (refs: any[]) => (el: any) => { + refs.forEach((ref) => { + if (typeof ref === 'function') ref(el); + else if (ref && typeof ref === 'object') ref.current = el; + }); + }, +})); + +describe('InputNumber', () => { + test('숫자만 입력 가능 — 문자가 포함된 값은 숫자만 추출된다', () => { + render(); + + const input = screen.getByPlaceholderText('숫자') as HTMLInputElement; + fireEvent.change(input, { target: { value: '12abc3' } }); + + expect(input.value).toBe('123'); + }); + + test('min 값 미만 입력 시 min으로 보정된다', () => { + render(); + + const input = screen.getByPlaceholderText('숫자') as HTMLInputElement; + fireEvent.change(input, { target: { value: '5' } }); + + expect(input.value).toBe('10'); + }); + + test('max 값 초과 입력 시 max로 보정된다', () => { + render(); + + const input = screen.getByPlaceholderText('숫자') as HTMLInputElement; + fireEvent.change(input, { target: { value: '200' } }); + + expect(input.value).toBe('100'); + }); + + test('비제어 모드: defaultValue 설정 후 타이핑하면 value가 변경된다', () => { + render(); + + const input = screen.getByPlaceholderText('숫자') as HTMLInputElement; + expect(input.value).toBe('42'); + + fireEvent.change(input, { target: { value: '99' } }); + expect(input.value).toBe('99'); + }); + + test('onChange 콜백이 호출된다', () => { + const onChange = jest.fn(); + render(); + + const input = screen.getByPlaceholderText('숫자'); + fireEvent.change(input, { target: { value: '7' } }); + + expect(onChange).toHaveBeenCalledTimes(1); + }); +}); diff --git a/src/shared/ui/components/input/input.component.test.tsx b/src/shared/ui/components/input/input.component.test.tsx new file mode 100644 index 00000000..de83f105 --- /dev/null +++ b/src/shared/ui/components/input/input.component.test.tsx @@ -0,0 +1,92 @@ +import React from 'react'; +import { render, screen, fireEvent } from '@testing-library/react'; +import Input from './input.component'; + +jest.mock('react', () => { + const actual = jest.requireActual('react'); + globalThis.React = actual; + return actual; +}); + +jest.mock('../typography', () => ({ + Typography: ({ children, className }: any) => {children}, +})); + +jest.mock('@/shared/lib/functions/combine-ref', () => ({ + combineRef: (refs: any[]) => (el: any) => { + refs.forEach((ref) => { + if (typeof ref === 'function') ref(el); + else if (ref && typeof ref === 'object') ref.current = el; + }); + }, +})); + +describe('Input', () => { + test('기본 렌더링 — input 요소가 존재한다', () => { + render(); + + expect(screen.getByPlaceholderText('테스트')).toBeTruthy(); + }); + + test('비제어 모드: defaultValue 설정 후 타이핑하면 value가 변경된다', () => { + render(); + + const input = screen.getByPlaceholderText('입력') as HTMLInputElement; + expect(input.value).toBe('초기값'); + + fireEvent.change(input, { target: { value: '새로운 값' } }); + expect(input.value).toBe('새로운 값'); + }); + + test('제어 모드: value prop이 반영된다', () => { + render(); + + const input = screen.getByPlaceholderText('입력') as HTMLInputElement; + expect(input.value).toBe('제어값'); + }); + + test('maxLength 카운터가 0/10 형식으로 표시된다', () => { + const { container } = render(); + + expect(container.textContent).toContain('/10'); + expect(container.textContent).toContain('00'); + }); + + test('maxLength 초과 시 빨간색 카운터 클래스가 적용된다', () => { + const { container } = render(); + + const strong = container.querySelector('strong'); + expect(strong).not.toBeNull(); + expect((strong as HTMLElement).className).toContain('text-red-300'); + }); + + test('Enter 키를 누르면 onPressEnter 콜백이 호출된다', () => { + const onPressEnter = jest.fn(); + render(); + + const input = screen.getByPlaceholderText('입력'); + fireEvent.keyDown(input, { key: 'Enter', nativeEvent: { isComposing: false } }); + + expect(onPressEnter).toHaveBeenCalledTimes(1); + }); + + test('Prefix와 Suffix가 렌더링된다', () => { + render(접두사} Suffix={접미사} placeholder='입력' />); + + expect(screen.getByText('접두사')).toBeTruthy(); + expect(screen.getByText('접미사')).toBeTruthy(); + }); + + test('wrapper 클릭 시 input이 포커스된다', () => { + const { container } = render(); + + const input = screen.getByPlaceholderText('입력'); + const wrapper = container.firstElementChild as HTMLElement; + + const focusSpy = jest.spyOn(input, 'focus'); + fireEvent.click(wrapper); + + expect(focusSpy).toHaveBeenCalled(); + focusSpy.mockRestore(); + }); +}); diff --git a/src/widgets/partyroom-crews-panel/model/restriction-panel-list-item.model.test.ts b/src/widgets/partyroom-crews-panel/model/restriction-panel-list-item.model.test.ts new file mode 100644 index 00000000..a8de868e --- /dev/null +++ b/src/widgets/partyroom-crews-panel/model/restriction-panel-list-item.model.test.ts @@ -0,0 +1,153 @@ +import { PenaltyType } from '@/shared/api/http/types/@enums'; +import type { BlockedCrew } from '@/shared/api/http/types/crews'; +import type { Penalty } from '@/shared/api/http/types/partyrooms'; +import { + getCategoryLabel, + listOfPenalties, + listOfMyBlockedCrews, + categorize, +} from './restriction-panel-list-item.model'; + +const mockDictionary = { auth: { para: { penalty: '패널티', block: '차단' } } } as any; + +const createPenalty = (overrides: Partial = {}): Penalty => ({ + penaltyId: 1, + penaltyType: PenaltyType.PERMANENT_EXPULSION, + crewId: 100, + avatarIconUri: 'https://example.com/avatar.png', + nickname: 'testUser', + ...overrides, +}); + +const createBlockedCrew = (overrides: Partial = {}): BlockedCrew => ({ + blockId: 1, + blockedCrewId: 200, + nickname: 'blockedUser', + avatarIconUri: 'https://example.com/blocked-avatar.png', + ...overrides, +}); + +describe('restriction-panel-list-item model', () => { + describe('getCategoryLabel', () => { + test('PERMANENT_EXPULSION 카테고리는 penalty 라벨 반환', () => { + expect(getCategoryLabel('PERMANENT_EXPULSION', mockDictionary)).toBe('패널티'); + }); + + test('BLOCK 카테고리는 block 라벨 반환', () => { + expect(getCategoryLabel('BLOCK', mockDictionary)).toBe('차단'); + }); + + test('알 수 없는 카테고리는 빈 문자열 반환', () => { + expect(getCategoryLabel('UNKNOWN', mockDictionary)).toBe(''); + }); + }); + + describe('listOfPenalties', () => { + test('Penalty 배열을 Model 배열로 변환하고 PERMANENT_EXPULSION 카테고리 매핑', () => { + const penalties = [ + createPenalty({ penaltyId: 1, crewId: 10, nickname: 'user1' }), + createPenalty({ penaltyId: 2, crewId: 20, nickname: 'user2' }), + ]; + const suffixRender = jest.fn(() => 'suffix'); + + const result = listOfPenalties(penalties, suffixRender); + + expect(result).toHaveLength(2); + expect(result[0]).toEqual({ + category: 'PERMANENT_EXPULSION', + crewId: 10, + avatarIconUri: 'https://example.com/avatar.png', + nickname: 'user1', + suffix: 'suffix', + }); + expect(result[1].crewId).toBe(20); + expect(suffixRender).toHaveBeenCalledTimes(2); + expect(suffixRender).toHaveBeenCalledWith(penalties[0]); + expect(suffixRender).toHaveBeenCalledWith(penalties[1]); + }); + + test('PERMANENT_EXPULSION이 아닌 PenaltyType은 빈 문자열 카테고리로 매핑', () => { + const penalties = [ + createPenalty({ penaltyType: PenaltyType.ONE_TIME_EXPULSION }), + createPenalty({ penaltyType: PenaltyType.CHAT_BAN_30_SECONDS }), + ]; + const suffixRender = jest.fn(() => 'suffix'); + + const result = listOfPenalties(penalties, suffixRender); + + expect(result[0].category).toBe(''); + expect(result[1].category).toBe(''); + }); + }); + + describe('listOfMyBlockedCrews', () => { + test('BlockedCrew 배열을 BLOCK 카테고리의 Model 배열로 변환', () => { + const blockedCrews = [ + createBlockedCrew({ blockId: 1, blockedCrewId: 301, nickname: 'blocked1' }), + createBlockedCrew({ blockId: 2, blockedCrewId: 302, nickname: 'blocked2' }), + ]; + const suffixRender = jest.fn(() => 'suffix'); + + const result = listOfMyBlockedCrews(blockedCrews, suffixRender); + + expect(result).toHaveLength(2); + expect(result[0]).toEqual({ + category: 'BLOCK', + crewId: 301, + avatarIconUri: 'https://example.com/blocked-avatar.png', + nickname: 'blocked1', + suffix: 'suffix', + }); + expect(result[1].category).toBe('BLOCK'); + expect(result[1].crewId).toBe(302); + expect(suffixRender).toHaveBeenCalledTimes(2); + expect(suffixRender).toHaveBeenCalledWith(blockedCrews[0]); + }); + }); + + describe('categorize', () => { + test('카테고리별로 그룹화하고 [PERMANENT_EXPULSION, BLOCK] 순서 유지', () => { + const models = [ + { category: 'BLOCK', crewId: 1, avatarIconUri: '', nickname: 'a', suffix: null }, + { + category: 'PERMANENT_EXPULSION', + crewId: 2, + avatarIconUri: '', + nickname: 'b', + suffix: null, + }, + { + category: 'PERMANENT_EXPULSION', + crewId: 3, + avatarIconUri: '', + nickname: 'c', + suffix: null, + }, + { category: 'BLOCK', crewId: 4, avatarIconUri: '', nickname: 'd', suffix: null }, + ]; + + const result = categorize(models); + + const keys = Object.keys(result); + expect(keys).toEqual(['PERMANENT_EXPULSION', 'BLOCK']); + expect(result['PERMANENT_EXPULSION']).toHaveLength(2); + expect(result['BLOCK']).toHaveLength(2); + expect(result['PERMANENT_EXPULSION'][0].crewId).toBe(2); + expect(result['PERMANENT_EXPULSION'][1].crewId).toBe(3); + }); + + test('단일 카테고리만 있을 때 해당 카테고리만 포함', () => { + const models = [ + { category: 'BLOCK', crewId: 1, avatarIconUri: '', nickname: 'a', suffix: null }, + { category: 'BLOCK', crewId: 2, avatarIconUri: '', nickname: 'b', suffix: null }, + ]; + + const result = categorize(models); + + const keys = Object.keys(result); + expect(keys).toEqual(['BLOCK']); + expect(result['BLOCK']).toHaveLength(2); + expect(result['PERMANENT_EXPULSION']).toBeUndefined(); + }); + }); +}); From 999a2651e6c755a31d77ae5abcd74086f9e4e204 Mon Sep 17 00:00:00 2001 From: LivingLikeKrillin <143606756+LivingLikeKrillin@users.noreply.github.com> Date: Wed, 25 Feb 2026 23:27:12 +0900 Subject: [PATCH 08/41] =?UTF-8?q?refactor:=20UI=20=EC=BB=B4=ED=8F=AC?= =?UTF-8?q?=EB=84=8C=ED=8A=B8=20barrel=20export=EB=A5=BC=20index.ui.ts?= =?UTF-8?q?=EB=A1=9C=20=EB=B6=84=EB=A6=AC=20=EB=B0=8F=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=84=A4=EC=A0=95=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SSR 안전성을 위해 클라이언트 전용 UI 컴포넌트 export를 index.ts에서 index.ui.ts로 분리하고, SWC에 TSX 파싱 및 React automatic runtime 설정을 추가하여 테스트 환경을 개선합니다. Co-Authored-By: Claude Opus 4.6 --- .swcrc | 7 +- jest.setup.js | 1 + src/app/layout.tsx | 2 +- .../lib/alerts/use-penalty-alert.hook.tsx | 3 +- src/entities/me/index.ts | 1 - src/entities/me/index.ui.ts | 1 + src/entities/music-preview/index.ts | 6 - src/entities/music-preview/index.ui.ts | 4 + .../ui/preview-overlay.component.test.tsx | 7 - src/entities/playlist/index.ts | 4 - src/entities/playlist/index.ui.ts | 4 + .../ui-state/model/ui-state.store.test.ts | 5 - src/entities/wallet/index.ts | 1 - src/entities/wallet/index.ui.ts | 1 + .../ui/connect-wallet-button.component.tsx | 2 +- .../lib/use-select-grade.hook.tsx | 3 +- .../close/lib/use-close-partyroom.hook.tsx | 3 +- .../partyroom/edit/ui/trigger.component.tsx | 3 +- .../lib/use-impose-penalty.hook.tsx | 3 +- .../ui/use-select-playlist.hook.tsx | 3 +- .../ui/search-list-item.component.tsx | 3 +- .../playlist/add/ui/form.component.tsx | 5 +- .../djing-guide/ui/guide-1.component.tsx | 3 +- .../djing-guide/ui/guide-2.component.tsx | 3 +- .../djing-guide/ui/guide-3.component.tsx | 3 +- .../playlist/edit/ui/form.component.tsx | 3 +- .../list-tracks/ui/track.component.tsx | 3 +- .../playlist/list/ui/list.component.tsx | 3 +- .../http/client/interceptors/response.test.ts | 5 - .../api/http/error/get-error-code.test.ts | 5 - src/shared/lib/localization/renderer/index.ts | 2 - .../lib/localization/renderer/index.ui.ts | 1 + .../checkbox/checkbox.component.test.tsx | 8 +- .../input-number.component.test.tsx | 7 - .../components/input/input.component.test.tsx | 7 - .../ui/player-container.component.tsx | 2 +- .../my-playlist/ui/my-playlist.component.tsx | 3 +- .../ui/body.component.tsx | 3 +- .../ui/empty-body.component.tsx | 3 +- ...e-open-edit-profile-avatar-dialog.hook.tsx | 3 +- yarn.lock | 2546 ++++++----------- 41 files changed, 963 insertions(+), 1722 deletions(-) create mode 100644 src/entities/me/index.ui.ts create mode 100644 src/entities/music-preview/index.ui.ts create mode 100644 src/entities/playlist/index.ui.ts create mode 100644 src/entities/wallet/index.ui.ts create mode 100644 src/shared/lib/localization/renderer/index.ui.ts diff --git a/.swcrc b/.swcrc index d745ad23..d4f9d79b 100644 --- a/.swcrc +++ b/.swcrc @@ -12,13 +12,16 @@ "jsc": { "parser": { "syntax": "typescript", - "tsx": false, + "tsx": true, "decorators": true, "dynamicImport": true }, "transform": { "legacyDecorator": true, - "decoratorMetadata": true + "decoratorMetadata": true, + "react": { + "runtime": "automatic" + } } }, "module": { diff --git a/jest.setup.js b/jest.setup.js index d9d93a3f..068f3b36 100644 --- a/jest.setup.js +++ b/jest.setup.js @@ -2,3 +2,4 @@ import 'jest-plugin-context/setup'; import 'given2/setup'; global.console.warn = () => {}; +window.debugLevel = Infinity; diff --git a/src/app/layout.tsx b/src/app/layout.tsx index d16cdadf..eb5fce9d 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -5,7 +5,7 @@ import '@rainbow-me/rainbowkit/styles.css'; import '@/shared/ui/foundation/globals.css'; import { PropsWithChildren } from 'react'; -import { MeHydration } from '@/entities/me'; +import { MeHydration } from '@/entities/me/index.ui'; import { DomId } from '@/shared/config/dom-id'; import { Language } from '@/shared/lib/localization/constants'; import { LANGUAGE_COOKIE_KEY } from '@/shared/lib/localization/constants'; diff --git a/src/entities/current-partyroom/lib/alerts/use-penalty-alert.hook.tsx b/src/entities/current-partyroom/lib/alerts/use-penalty-alert.hook.tsx index 36023a08..07a0640b 100644 --- a/src/entities/current-partyroom/lib/alerts/use-penalty-alert.hook.tsx +++ b/src/entities/current-partyroom/lib/alerts/use-penalty-alert.hook.tsx @@ -1,7 +1,8 @@ import { ReactNode, useCallback } from 'react'; import { PenaltyType } from '@/shared/api/http/types/@enums'; import { useI18n } from '@/shared/lib/localization/i18n.context'; -import { Trans, LineBreakProcessor } from '@/shared/lib/localization/renderer'; +import { LineBreakProcessor } from '@/shared/lib/localization/renderer'; +import { Trans } from '@/shared/lib/localization/renderer/index.ui'; import { useStores } from '@/shared/lib/store/stores.context'; import { Dialog, useDialog } from '@/shared/ui/components/dialog'; import { Typography } from '@/shared/ui/components/typography'; diff --git a/src/entities/me/index.ts b/src/entities/me/index.ts index d3710c95..b827a8d3 100644 --- a/src/entities/me/index.ts +++ b/src/entities/me/index.ts @@ -4,4 +4,3 @@ export { useFetchMe, useSuspenseFetchMe } from './api/use-fetch-me.query'; export { default as usePrefetchMe } from './api/use-prefetch-me.query'; export { useFetchMeAsync } from './api/use-fetch-me-async'; export { useGetMyServiceEntry } from './api/use-get-my-service-entry'; -export { default as MeHydration } from './ui/hydration.component'; diff --git a/src/entities/me/index.ui.ts b/src/entities/me/index.ui.ts new file mode 100644 index 00000000..b0b1f36b --- /dev/null +++ b/src/entities/me/index.ui.ts @@ -0,0 +1 @@ +export { default as MeHydration } from './ui/hydration.component'; diff --git a/src/entities/music-preview/index.ts b/src/entities/music-preview/index.ts index 803e8d9a..4d3f34b5 100644 --- a/src/entities/music-preview/index.ts +++ b/src/entities/music-preview/index.ts @@ -1,9 +1,3 @@ export * as Preview from './model/preview.model'; export { createPreviewStore } from './model/preview.store'; export * from './lib/preview-helpers'; - -// UI Components -export { default as ThumbnailWithPreview } from './ui/thumbnail-with-preview.component'; -export { default as PreviewOverlay } from './ui/preview-overlay.component'; -export { default as PreviewIndicator } from './ui/preview-indicator.component'; -export { default as YouTubePreviewPlayer } from './ui/youtube-preview-player.component'; diff --git a/src/entities/music-preview/index.ui.ts b/src/entities/music-preview/index.ui.ts new file mode 100644 index 00000000..3dfa3c91 --- /dev/null +++ b/src/entities/music-preview/index.ui.ts @@ -0,0 +1,4 @@ +export { default as ThumbnailWithPreview } from './ui/thumbnail-with-preview.component'; +export { default as PreviewOverlay } from './ui/preview-overlay.component'; +export { default as PreviewIndicator } from './ui/preview-indicator.component'; +export { default as YouTubePreviewPlayer } from './ui/youtube-preview-player.component'; diff --git a/src/entities/music-preview/ui/preview-overlay.component.test.tsx b/src/entities/music-preview/ui/preview-overlay.component.test.tsx index 01160e8c..0a7df06f 100644 --- a/src/entities/music-preview/ui/preview-overlay.component.test.tsx +++ b/src/entities/music-preview/ui/preview-overlay.component.test.tsx @@ -1,13 +1,6 @@ -import React from 'react'; import { render, fireEvent } from '@testing-library/react'; import PreviewOverlay from './preview-overlay.component'; -jest.mock('react', () => { - const actual = jest.requireActual('react'); - globalThis.React = actual; - return actual; -}); - describe('PreviewOverlay', () => { const defaultProps = { isPlaying: false, diff --git a/src/entities/playlist/index.ts b/src/entities/playlist/index.ts index 7ad7e297..4ab08e5d 100644 --- a/src/entities/playlist/index.ts +++ b/src/entities/playlist/index.ts @@ -1,7 +1,3 @@ -export { - default as PlaylistForm, - type FormProps as PlaylistFormProps, -} from './ui/playlist-form.component'; export type { Model as PlaylistFormValues } from './model/playlist-form.model'; export { PlaylistActionContext, diff --git a/src/entities/playlist/index.ui.ts b/src/entities/playlist/index.ui.ts new file mode 100644 index 00000000..447d3f7f --- /dev/null +++ b/src/entities/playlist/index.ui.ts @@ -0,0 +1,4 @@ +export { + default as PlaylistForm, + type FormProps as PlaylistFormProps, +} from './ui/playlist-form.component'; diff --git a/src/entities/ui-state/model/ui-state.store.test.ts b/src/entities/ui-state/model/ui-state.store.test.ts index 8d5ad1d9..37acc822 100644 --- a/src/entities/ui-state/model/ui-state.store.test.ts +++ b/src/entities/ui-state/model/ui-state.store.test.ts @@ -1,8 +1,3 @@ -jest.mock('@/shared/lib/functions/log/with-debugger', () => ({ - __esModule: true, - default: () => (fn: any) => fn, -})); - jest.mock('@/shared/lib/functions/log/logger', () => ({ warnLog: jest.fn(), })); diff --git a/src/entities/wallet/index.ts b/src/entities/wallet/index.ts index cb483b7d..03ad4178 100644 --- a/src/entities/wallet/index.ts +++ b/src/entities/wallet/index.ts @@ -2,5 +2,4 @@ export * as Nft from './model/nft.model'; export { default as useNfts } from './lib/use-nfts.hook'; export { default as useIsWalletLinked } from './lib/use-is-wallet-linked.hook'; export { default as useGlobalWalletSync } from './lib/use-global-wallet-sync.hook'; -export { default as ConnectWallet } from './ui/connect-wallet.component'; export { default as useInformWalletLinkage } from './ui/use-inform-wallet-linkage.hook'; diff --git a/src/entities/wallet/index.ui.ts b/src/entities/wallet/index.ui.ts new file mode 100644 index 00000000..fdedb748 --- /dev/null +++ b/src/entities/wallet/index.ui.ts @@ -0,0 +1 @@ +export { default as ConnectWallet } from './ui/connect-wallet.component'; diff --git a/src/features/edit-profile-avatar/ui/connect-wallet-button.component.tsx b/src/features/edit-profile-avatar/ui/connect-wallet-button.component.tsx index f4302d8f..223cff5f 100644 --- a/src/features/edit-profile-avatar/ui/connect-wallet-button.component.tsx +++ b/src/features/edit-profile-avatar/ui/connect-wallet-button.component.tsx @@ -1,5 +1,5 @@ 'use client'; -import { ConnectWallet } from '@/entities/wallet'; +import { ConnectWallet } from '@/entities/wallet/index.ui'; import SuspenseWithErrorBoundary from '@/shared/api/http/error/suspense-with-error-boundary.component'; import { Button } from '@/shared/ui/components/button'; diff --git a/src/features/partyroom/adjust-grade/lib/use-select-grade.hook.tsx b/src/features/partyroom/adjust-grade/lib/use-select-grade.hook.tsx index da01fd9e..2003ee64 100644 --- a/src/features/partyroom/adjust-grade/lib/use-select-grade.hook.tsx +++ b/src/features/partyroom/adjust-grade/lib/use-select-grade.hook.tsx @@ -2,7 +2,8 @@ import { useState } from 'react'; import { GRADE_TYPE_LABEL } from '@/entities/current-partyroom'; import { GradeType } from '@/shared/api/http/types/@enums'; import { useI18n } from '@/shared/lib/localization/i18n.context'; -import { Trans, BoldProcessor, VariableProcessor } from '@/shared/lib/localization/renderer'; +import { BoldProcessor, VariableProcessor } from '@/shared/lib/localization/renderer'; +import { Trans } from '@/shared/lib/localization/renderer/index.ui'; import { Dialog, useDialog } from '@/shared/ui/components/dialog'; import { Select } from '@/shared/ui/components/select'; import { Tag } from '@/shared/ui/components/tag'; diff --git a/src/features/partyroom/close/lib/use-close-partyroom.hook.tsx b/src/features/partyroom/close/lib/use-close-partyroom.hook.tsx index adddb71f..bb098a3b 100644 --- a/src/features/partyroom/close/lib/use-close-partyroom.hook.tsx +++ b/src/features/partyroom/close/lib/use-close-partyroom.hook.tsx @@ -1,6 +1,7 @@ import { useState } from 'react'; import { useI18n } from '@/shared/lib/localization/i18n.context'; -import { Trans, LineBreakProcessor, VariableProcessor } from '@/shared/lib/localization/renderer'; +import { LineBreakProcessor, VariableProcessor } from '@/shared/lib/localization/renderer'; +import { Trans } from '@/shared/lib/localization/renderer/index.ui'; import { useDialog } from '@/shared/ui/components/dialog'; import Dialog from '@/shared/ui/components/dialog/dialog.component'; import { Input } from '@/shared/ui/components/input'; diff --git a/src/features/partyroom/edit/ui/trigger.component.tsx b/src/features/partyroom/edit/ui/trigger.component.tsx index 33584833..a5dad666 100644 --- a/src/features/partyroom/edit/ui/trigger.component.tsx +++ b/src/features/partyroom/edit/ui/trigger.component.tsx @@ -4,7 +4,8 @@ import { PartyroomMutationFormModel } from '@/entities/partyroom-info'; import { Language } from '@/shared/lib/localization/constants'; import { useI18n } from '@/shared/lib/localization/i18n.context'; import { useLang } from '@/shared/lib/localization/lang.context'; -import { Trans, LineBreakProcessor } from '@/shared/lib/localization/renderer'; +import { LineBreakProcessor } from '@/shared/lib/localization/renderer'; +import { Trans } from '@/shared/lib/localization/renderer/index.ui'; import { Button } from '@/shared/ui/components/button'; import { useDialog } from '@/shared/ui/components/dialog'; import { PFEdit } from '@/shared/ui/icons'; diff --git a/src/features/partyroom/impose-penalty/lib/use-impose-penalty.hook.tsx b/src/features/partyroom/impose-penalty/lib/use-impose-penalty.hook.tsx index 693337c2..97acb294 100644 --- a/src/features/partyroom/impose-penalty/lib/use-impose-penalty.hook.tsx +++ b/src/features/partyroom/impose-penalty/lib/use-impose-penalty.hook.tsx @@ -2,7 +2,8 @@ import { ReactNode, useState } from 'react'; import { GradeType, PenaltyType } from '@/shared/api/http/types/@enums'; import { PartyroomCrew } from '@/shared/api/http/types/partyrooms'; import { useI18n } from '@/shared/lib/localization/i18n.context'; -import { Trans, LineBreakProcessor } from '@/shared/lib/localization/renderer'; +import { LineBreakProcessor } from '@/shared/lib/localization/renderer'; +import { Trans } from '@/shared/lib/localization/renderer/index.ui'; import { useStores } from '@/shared/lib/store/stores.context'; import { Dialog, useDialog } from '@/shared/ui/components/dialog'; import { Input } from '@/shared/ui/components/input'; diff --git a/src/features/partyroom/select-playlist-for-djing/ui/use-select-playlist.hook.tsx b/src/features/partyroom/select-playlist-for-djing/ui/use-select-playlist.hook.tsx index 5c5c8145..31091e12 100644 --- a/src/features/partyroom/select-playlist-for-djing/ui/use-select-playlist.hook.tsx +++ b/src/features/partyroom/select-playlist-for-djing/ui/use-select-playlist.hook.tsx @@ -2,7 +2,8 @@ import { useEffect, useState } from 'react'; import { Playlist } from '@/shared/api/http/types/playlists'; import useDidMountEffect from '@/shared/lib/hooks/use-did-mount-effect'; import { useI18n } from '@/shared/lib/localization/i18n.context'; -import { Trans, VariableProcessor } from '@/shared/lib/localization/renderer'; +import { VariableProcessor } from '@/shared/lib/localization/renderer'; +import { Trans } from '@/shared/lib/localization/renderer/index.ui'; import { useStores } from '@/shared/lib/store/stores.context'; import { useDialog } from '@/shared/ui/components/dialog'; import Dialog from '@/shared/ui/components/dialog/dialog.component'; diff --git a/src/features/playlist/add-tracks/ui/search-list-item.component.tsx b/src/features/playlist/add-tracks/ui/search-list-item.component.tsx index d16d29ed..0273da38 100644 --- a/src/features/playlist/add-tracks/ui/search-list-item.component.tsx +++ b/src/features/playlist/add-tracks/ui/search-list-item.component.tsx @@ -1,5 +1,6 @@ import { ReactNode } from 'react'; -import { ThumbnailWithPreview, convertSearchMusicToPreview } from '@/entities/music-preview'; +import { convertSearchMusicToPreview } from '@/entities/music-preview'; +import { ThumbnailWithPreview } from '@/entities/music-preview/index.ui'; import { Music } from '@/shared/api/http/types/playlists'; import { safeDecodeURI } from '@/shared/lib/functions/safe-decode-uri'; import { Typography } from '@/shared/ui/components/typography'; diff --git a/src/features/playlist/add/ui/form.component.tsx b/src/features/playlist/add/ui/form.component.tsx index 172f525f..1e8e9030 100644 --- a/src/features/playlist/add/ui/form.component.tsx +++ b/src/features/playlist/add/ui/form.component.tsx @@ -1,7 +1,8 @@ import { useCallback } from 'react'; import { SubmitHandler } from 'react-hook-form'; -import { PlaylistForm, PlaylistFormProps, PlaylistFormValues } from '@/entities/playlist'; -import { ConnectWallet } from '@/entities/wallet'; +import { PlaylistFormValues } from '@/entities/playlist'; +import { PlaylistForm, PlaylistFormProps } from '@/entities/playlist/index.ui'; +import { ConnectWallet } from '@/entities/wallet/index.ui'; import useOnError from '@/shared/api/http/error/use-on-error.hook'; import { ErrorCode } from '@/shared/api/http/types/@shared'; import { useI18n } from '@/shared/lib/localization/i18n.context'; diff --git a/src/features/playlist/djing-guide/ui/guide-1.component.tsx b/src/features/playlist/djing-guide/ui/guide-1.component.tsx index def69039..5ad3a2ec 100644 --- a/src/features/playlist/djing-guide/ui/guide-1.component.tsx +++ b/src/features/playlist/djing-guide/ui/guide-1.component.tsx @@ -1,6 +1,7 @@ import Image from 'next/image'; import { useI18n } from '@/shared/lib/localization/i18n.context'; -import { BoldProcessor, LineBreakProcessor, Trans } from '@/shared/lib/localization/renderer'; +import { BoldProcessor, LineBreakProcessor } from '@/shared/lib/localization/renderer'; +import { Trans } from '@/shared/lib/localization/renderer/index.ui'; import { Typography } from '@/shared/ui/components/typography'; export default function Guide1() { diff --git a/src/features/playlist/djing-guide/ui/guide-2.component.tsx b/src/features/playlist/djing-guide/ui/guide-2.component.tsx index a0de09c5..c4abda39 100644 --- a/src/features/playlist/djing-guide/ui/guide-2.component.tsx +++ b/src/features/playlist/djing-guide/ui/guide-2.component.tsx @@ -1,6 +1,7 @@ import Image from 'next/image'; import { useI18n } from '@/shared/lib/localization/i18n.context'; -import { BoldProcessor, LineBreakProcessor, Trans } from '@/shared/lib/localization/renderer'; +import { BoldProcessor, LineBreakProcessor } from '@/shared/lib/localization/renderer'; +import { Trans } from '@/shared/lib/localization/renderer/index.ui'; import { Typography } from '@/shared/ui/components/typography'; export default function Guide2() { diff --git a/src/features/playlist/djing-guide/ui/guide-3.component.tsx b/src/features/playlist/djing-guide/ui/guide-3.component.tsx index 4577c70e..6812b39a 100644 --- a/src/features/playlist/djing-guide/ui/guide-3.component.tsx +++ b/src/features/playlist/djing-guide/ui/guide-3.component.tsx @@ -1,5 +1,6 @@ import Image from 'next/image'; -import { BoldProcessor, LineBreakProcessor, Trans } from '@/shared/lib/localization/renderer'; +import { BoldProcessor, LineBreakProcessor } from '@/shared/lib/localization/renderer'; +import { Trans } from '@/shared/lib/localization/renderer/index.ui'; import { Typography } from '@/shared/ui/components/typography'; export default function Guide3() { diff --git a/src/features/playlist/edit/ui/form.component.tsx b/src/features/playlist/edit/ui/form.component.tsx index 01f17cff..d90e08d0 100644 --- a/src/features/playlist/edit/ui/form.component.tsx +++ b/src/features/playlist/edit/ui/form.component.tsx @@ -1,6 +1,7 @@ import { useCallback } from 'react'; import { SubmitHandler } from 'react-hook-form'; -import { PlaylistForm, PlaylistFormProps, PlaylistFormValues } from '@/entities/playlist'; +import { PlaylistFormValues } from '@/entities/playlist'; +import { PlaylistForm, PlaylistFormProps } from '@/entities/playlist/index.ui'; import { Playlist } from '@/shared/api/http/types/playlists'; import { useI18n } from '@/shared/lib/localization/i18n.context'; import { useStores } from '@/shared/lib/store/stores.context'; diff --git a/src/features/playlist/list-tracks/ui/track.component.tsx b/src/features/playlist/list-tracks/ui/track.component.tsx index 59de6b9f..89c721a9 100644 --- a/src/features/playlist/list-tracks/ui/track.component.tsx +++ b/src/features/playlist/list-tracks/ui/track.component.tsx @@ -1,7 +1,8 @@ 'use client'; import { useSortable } from '@dnd-kit/sortable'; import { CSS } from '@dnd-kit/utilities'; -import { ThumbnailWithPreview, convertPlaylistTrackToPreview } from '@/entities/music-preview'; +import { convertPlaylistTrackToPreview } from '@/entities/music-preview'; +import { ThumbnailWithPreview } from '@/entities/music-preview/index.ui'; import { PlaylistTrack } from '@/shared/api/http/types/playlists'; import { cn } from '@/shared/lib/functions/cn'; import { IconMenu } from '@/shared/ui/components/icon-menu'; diff --git a/src/features/playlist/list/ui/list.component.tsx b/src/features/playlist/list/ui/list.component.tsx index 9cb1c8ed..7756bc4d 100644 --- a/src/features/playlist/list/ui/list.component.tsx +++ b/src/features/playlist/list/ui/list.component.tsx @@ -1,7 +1,8 @@ 'use client'; import { usePlaylistAction } from '@/entities/playlist'; import { Playlist } from '@/shared/api/http/types/playlists'; -import { Trans, VariableProcessor } from '@/shared/lib/localization/renderer'; +import { VariableProcessor } from '@/shared/lib/localization/renderer'; +import { Trans } from '@/shared/lib/localization/renderer/index.ui'; import ListItem from './list-item.component'; type Props = { diff --git a/src/shared/api/http/client/interceptors/response.test.ts b/src/shared/api/http/client/interceptors/response.test.ts index e64470ec..0d0f6819 100644 --- a/src/shared/api/http/client/interceptors/response.test.ts +++ b/src/shared/api/http/client/interceptors/response.test.ts @@ -1,8 +1,3 @@ -jest.mock('@/shared/lib/functions/log/with-debugger', () => ({ - __esModule: true, - default: () => (fn: any) => fn, -})); - jest.mock('@/shared/lib/functions/log/network-log', () => ({ printResponseLog: jest.fn(), printErrorLog: jest.fn(), diff --git a/src/shared/api/http/error/get-error-code.test.ts b/src/shared/api/http/error/get-error-code.test.ts index b5eab582..52b34cf4 100644 --- a/src/shared/api/http/error/get-error-code.test.ts +++ b/src/shared/api/http/error/get-error-code.test.ts @@ -2,11 +2,6 @@ jest.mock('axios', () => ({ isAxiosError: jest.fn(), })); -jest.mock('@/shared/lib/functions/log/with-debugger', () => ({ - __esModule: true, - default: () => (fn: any) => fn, -})); - jest.mock('@/shared/lib/functions/log/logger', () => ({ warnLog: jest.fn(), })); diff --git a/src/shared/lib/localization/renderer/index.ts b/src/shared/lib/localization/renderer/index.ts index a3954eaa..6cc17fe4 100644 --- a/src/shared/lib/localization/renderer/index.ts +++ b/src/shared/lib/localization/renderer/index.ts @@ -1,5 +1,3 @@ -export { default as Trans } from './trans.component'; - export { default as LineBreakProcessor } from './processors/line-break-processor'; export { default as BoldProcessor } from './processors/bold-processor'; export { default as VariableProcessor } from './processors/variable-processor'; diff --git a/src/shared/lib/localization/renderer/index.ui.ts b/src/shared/lib/localization/renderer/index.ui.ts new file mode 100644 index 00000000..ed5a6ed8 --- /dev/null +++ b/src/shared/lib/localization/renderer/index.ui.ts @@ -0,0 +1 @@ +export { default as Trans } from './trans.component'; diff --git a/src/shared/ui/components/checkbox/checkbox.component.test.tsx b/src/shared/ui/components/checkbox/checkbox.component.test.tsx index 4bbdcdcf..54081d67 100644 --- a/src/shared/ui/components/checkbox/checkbox.component.test.tsx +++ b/src/shared/ui/components/checkbox/checkbox.component.test.tsx @@ -1,13 +1,7 @@ -import React, { createRef } from 'react'; +import { createRef } from 'react'; import { render, fireEvent } from '@testing-library/react'; import Checkbox from './checkbox.component'; -jest.mock('react', () => { - const actual = jest.requireActual('react'); - globalThis.React = actual; - return actual; -}); - jest.mock('@/shared/ui/icons', () => ({ PFCheckMark: (props: any) => , })); diff --git a/src/shared/ui/components/input-number/input-number.component.test.tsx b/src/shared/ui/components/input-number/input-number.component.test.tsx index 761626d1..a456fa82 100644 --- a/src/shared/ui/components/input-number/input-number.component.test.tsx +++ b/src/shared/ui/components/input-number/input-number.component.test.tsx @@ -1,13 +1,6 @@ -import React from 'react'; import { render, screen, fireEvent } from '@testing-library/react'; import InputNumber from './input-number.component'; -jest.mock('react', () => { - const actual = jest.requireActual('react'); - globalThis.React = actual; - return actual; -}); - jest.mock('@/shared/lib/functions/combine-ref', () => ({ combineRef: (refs: any[]) => (el: any) => { refs.forEach((ref) => { diff --git a/src/shared/ui/components/input/input.component.test.tsx b/src/shared/ui/components/input/input.component.test.tsx index de83f105..0b969f3e 100644 --- a/src/shared/ui/components/input/input.component.test.tsx +++ b/src/shared/ui/components/input/input.component.test.tsx @@ -1,13 +1,6 @@ -import React from 'react'; import { render, screen, fireEvent } from '@testing-library/react'; import Input from './input.component'; -jest.mock('react', () => { - const actual = jest.requireActual('react'); - globalThis.React = actual; - return actual; -}); - jest.mock('../typography', () => ({ Typography: ({ children, className }: any) => {children}, })); diff --git a/src/widgets/music-preview-player/ui/player-container.component.tsx b/src/widgets/music-preview-player/ui/player-container.component.tsx index 7f976e9e..5ce88978 100644 --- a/src/widgets/music-preview-player/ui/player-container.component.tsx +++ b/src/widgets/music-preview-player/ui/player-container.component.tsx @@ -1,7 +1,7 @@ 'use client'; -import { YouTubePreviewPlayer } from '@/entities/music-preview'; import { PREVIEW_PLAYER_SIZES } from '@/entities/music-preview/config/youtube-player.config'; +import { YouTubePreviewPlayer } from '@/entities/music-preview/index.ui'; import { useStores } from '@/shared/lib/store/stores.context'; type PlayerContainerProps = { diff --git a/src/widgets/my-playlist/ui/my-playlist.component.tsx b/src/widgets/my-playlist/ui/my-playlist.component.tsx index ac67de8e..de1d39e6 100644 --- a/src/widgets/my-playlist/ui/my-playlist.component.tsx +++ b/src/widgets/my-playlist/ui/my-playlist.component.tsx @@ -7,7 +7,8 @@ import { TracksInPlaylist } from '@/features/playlist/list-tracks'; import { RemovePlaylistButton } from '@/features/playlist/remove'; import { Playlist } from '@/shared/api/http/types/playlists'; import { useI18n } from '@/shared/lib/localization/i18n.context'; -import { Trans, VariableProcessor } from '@/shared/lib/localization/renderer'; +import { VariableProcessor } from '@/shared/lib/localization/renderer'; +import { Trans } from '@/shared/lib/localization/renderer/index.ui'; import { useStores } from '@/shared/lib/store/stores.context'; import { Button } from '@/shared/ui/components/button'; import { Drawer, DrawerProps } from '@/shared/ui/components/drawer'; diff --git a/src/widgets/partyroom-djing-dialog/ui/body.component.tsx b/src/widgets/partyroom-djing-dialog/ui/body.component.tsx index 7f2788c5..1cad4d5b 100644 --- a/src/widgets/partyroom-djing-dialog/ui/body.component.tsx +++ b/src/widgets/partyroom-djing-dialog/ui/body.component.tsx @@ -9,7 +9,8 @@ import { } from '@/features/partyroom/unlock-djing-queue'; import { QueueStatus } from '@/shared/api/http/types/@enums'; import { useI18n } from '@/shared/lib/localization/i18n.context'; -import { BoldProcessor, Trans, VariableProcessor } from '@/shared/lib/localization/renderer'; +import { BoldProcessor, VariableProcessor } from '@/shared/lib/localization/renderer'; +import { Trans } from '@/shared/lib/localization/renderer/index.ui'; import { useStores } from '@/shared/lib/store/stores.context'; import { Button, ButtonProps } from '@/shared/ui/components/button'; import { DjListItem } from '@/shared/ui/components/dj-list-item'; diff --git a/src/widgets/partyroom-djing-dialog/ui/empty-body.component.tsx b/src/widgets/partyroom-djing-dialog/ui/empty-body.component.tsx index ddebf9bc..0b65177f 100644 --- a/src/widgets/partyroom-djing-dialog/ui/empty-body.component.tsx +++ b/src/widgets/partyroom-djing-dialog/ui/empty-body.component.tsx @@ -1,5 +1,6 @@ import { useI18n } from '@/shared/lib/localization/i18n.context'; -import { Trans, LineBreakProcessor } from '@/shared/lib/localization/renderer'; +import { LineBreakProcessor } from '@/shared/lib/localization/renderer'; +import { Trans } from '@/shared/lib/localization/renderer/index.ui'; import { TextButton } from '@/shared/ui/components/text-button'; import { Typography } from '@/shared/ui/components/typography'; import { PFClose } from '@/shared/ui/icons'; diff --git a/src/widgets/partyroom-edit-profile-avatar-dialog/ui/use-open-edit-profile-avatar-dialog.hook.tsx b/src/widgets/partyroom-edit-profile-avatar-dialog/ui/use-open-edit-profile-avatar-dialog.hook.tsx index 286900f6..4e7c7858 100644 --- a/src/widgets/partyroom-edit-profile-avatar-dialog/ui/use-open-edit-profile-avatar-dialog.hook.tsx +++ b/src/widgets/partyroom-edit-profile-avatar-dialog/ui/use-open-edit-profile-avatar-dialog.hook.tsx @@ -1,6 +1,7 @@ import { AvatarEditDone, ProfileAvatarEditPanel } from '@/features/edit-profile-avatar'; import { useI18n } from '@/shared/lib/localization/i18n.context'; -import { Trans, LineBreakProcessor } from '@/shared/lib/localization/renderer'; +import { LineBreakProcessor } from '@/shared/lib/localization/renderer'; +import { Trans } from '@/shared/lib/localization/renderer/index.ui'; import { Button } from '@/shared/ui/components/button'; import { useDialog } from '@/shared/ui/components/dialog'; import { Typography } from '@/shared/ui/components/typography'; diff --git a/yarn.lock b/yarn.lock index 72f62ce3..e2ce70f4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,16 +2,16 @@ # yarn lockfile v1 -"@adraffy/ens-normalize@^1.10.1": - version "1.11.0" - resolved "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.11.0.tgz" - integrity sha512-/3DDPKHqqIqxUULp8yP4zODUY1i+2xvVWsv8A79xGWdCAG+8sb0hRh0Rk2QyOJUnnbyPUAZYcpBuRe3nS2OIUg== - "@adraffy/ens-normalize@1.10.0": version "1.10.0" resolved "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.10.0.tgz" integrity sha512-nA9XHtlAkYfJxY7bce8DcN7eKxWWCWkU+1GR9d+U6MbNpfwQp8TI7vqOsBsMcHoT4mBu2kypKoSKnghEzOOq5Q== +"@adraffy/ens-normalize@^1.10.1": + version "1.11.0" + resolved "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.11.0.tgz" + integrity sha512-/3DDPKHqqIqxUULp8yP4zODUY1i+2xvVWsv8A79xGWdCAG+8sb0hRh0Rk2QyOJUnnbyPUAZYcpBuRe3nS2OIUg== + "@alloc/quick-lru@^5.2.0": version "5.2.0" resolved "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz" @@ -46,7 +46,7 @@ resolved "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.25.2.tgz" integrity sha512-bYcppcpKBvX4znYaPEeFau03bp89ShqNMLs+rmdptMw+heSZh9+z84d2YG+K7cYLbWwzdjtDoW/uqZmPjulClQ== -"@babel/core@*", "@babel/core@^7.0.0", "@babel/core@^7.0.0-0", "@babel/core@^7.0.0-0 || ^8.0.0-0 <8.0.0", "@babel/core@^7.11.6", "@babel/core@^7.12.0", "@babel/core@^7.12.3", "@babel/core@^7.13.0", "@babel/core@^7.18.9", "@babel/core@^7.21.3", "@babel/core@^7.22.0", "@babel/core@^7.23.0", "@babel/core@^7.23.2", "@babel/core@^7.23.9", "@babel/core@^7.25.2", "@babel/core@^7.4.0 || ^8.0.0-0 <8.0.0", "@babel/core@^7.8.0": +"@babel/core@^7.11.6", "@babel/core@^7.12.3", "@babel/core@^7.18.9", "@babel/core@^7.21.3", "@babel/core@^7.23.0", "@babel/core@^7.23.2", "@babel/core@^7.23.9", "@babel/core@^7.25.2": version "7.25.2" resolved "https://registry.npmjs.org/@babel/core/-/core-7.25.2.tgz" integrity sha512-BBt3opiCOxUr9euZ5/ro/Xv8/V7yJ5bjYMqG/C1YAo8MIKAnumZalCN+msbci3Pigy4lIQfPUpfMM27HMGaYEA== @@ -67,7 +67,7 @@ json5 "^2.2.3" semver "^6.3.1" -"@babel/generator@^7.23.0", "@babel/generator@^7.25.0", "@babel/generator@^7.28.0", "@babel/generator@^7.7.2": +"@babel/generator@^7.23.0", "@babel/generator@^7.25.0", "@babel/generator@^7.7.2": version "7.28.0" resolved "https://registry.npmjs.org/@babel/generator/-/generator-7.28.0.tgz" integrity sha512-lJjzvrbEeWrhB4P3QBsH7tey117PjLZnDbLiQEKjQ/fNJTjuq4HSqgFA+UNSwZT8D7dxxbnuSBMsa1lrWzKlQg== @@ -137,11 +137,6 @@ lodash.debounce "^4.0.8" resolve "^1.14.2" -"@babel/helper-globals@^7.28.0": - version "7.28.0" - resolved "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz" - integrity sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw== - "@babel/helper-member-expression-to-functions@^7.24.8": version "7.24.8" resolved "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.24.8.tgz" @@ -925,7 +920,7 @@ "@babel/helper-create-regexp-features-plugin" "^7.24.7" "@babel/helper-plugin-utils" "^7.24.7" -"@babel/preset-env@^7.1.6", "@babel/preset-env@^7.23.2": +"@babel/preset-env@^7.23.2": version "7.25.3" resolved "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.25.3.tgz" integrity sha512-QsYW7UeAaXvLPX9tdVliMJE7MD7M6MLYVTovRTIwhoYQVFHR1rM4wO8wqAezYi3/BpSD+NzVCZ69R6smWiIi8g== @@ -1071,14 +1066,14 @@ resolved "https://registry.npmjs.org/@babel/regjsgen/-/regjsgen-0.8.0.tgz" integrity sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA== -"@babel/runtime@^7.12.5", "@babel/runtime@^7.13.10", "@babel/runtime@^7.17.8", "@babel/runtime@^7.19.4", "@babel/runtime@^7.20.6", "@babel/runtime@^7.21.0", "@babel/runtime@^7.22.5", "@babel/runtime@^7.23.2", "@babel/runtime@^7.25.0", "@babel/runtime@^7.8.4": +"@babel/runtime@^7.12.5", "@babel/runtime@^7.13.10", "@babel/runtime@^7.17.8", "@babel/runtime@^7.19.4", "@babel/runtime@^7.20.6", "@babel/runtime@^7.21.0", "@babel/runtime@^7.23.2", "@babel/runtime@^7.8.4": version "7.25.0" resolved "https://registry.npmjs.org/@babel/runtime/-/runtime-7.25.0.tgz" integrity sha512-7dRy4DwXwtzBrPbZflqxnvfxLF8kdZXPkhymtDeFoFqE6ldzjQFgYTtYIFARcLEYDrqfBfYcZt1WqFxRoyC9Rw== dependencies: regenerator-runtime "^0.14.0" -"@babel/template@^7.24.7", "@babel/template@^7.25.0", "@babel/template@^7.27.2", "@babel/template@^7.3.3": +"@babel/template@^7.24.7", "@babel/template@^7.25.0", "@babel/template@^7.3.3": version "7.27.2" resolved "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz" integrity sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw== @@ -1087,19 +1082,6 @@ "@babel/parser" "^7.27.2" "@babel/types" "^7.27.1" -"@babel/traverse--for-generate-function-map@npm:@babel/traverse@^7.25.3": - version "7.28.0" - resolved "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.0.tgz" - integrity sha512-mGe7UK5wWyh0bKRfupsUchrQGqvDbZDbKJw+kcRGSmdHVYrv+ltd0pnpDTVpiTqnaBru9iEvA8pz8W46v0Amwg== - dependencies: - "@babel/code-frame" "^7.27.1" - "@babel/generator" "^7.28.0" - "@babel/helper-globals" "^7.28.0" - "@babel/parser" "^7.28.0" - "@babel/template" "^7.27.2" - "@babel/types" "^7.28.0" - debug "^4.3.1" - "@babel/traverse@^7.18.9", "@babel/traverse@^7.23.2", "@babel/traverse@^7.24.7", "@babel/traverse@^7.24.8", "@babel/traverse@^7.25.0", "@babel/traverse@^7.25.1", "@babel/traverse@^7.25.2", "@babel/traverse@^7.25.3": version "7.25.3" resolved "https://registry.npmjs.org/@babel/traverse/-/traverse-7.25.3.tgz" @@ -1131,6 +1113,20 @@ resolved "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz" integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== +"@cfcs/core@^0.0.6": + version "0.0.6" + resolved "https://registry.yarnpkg.com/@cfcs/core/-/core-0.0.6.tgz#9f8499dcd2ad29fd96d8fa72055411cd4a249121" + integrity sha512-FxfJMwoLB8MEMConeXUCqtMGqxdtePQxRBOiGip9ULcYYam3WfCgoY6xdnMaSkYvRvmosp5iuG+TiPofm65+Pw== + dependencies: + "@egjs/component" "^3.0.2" + +"@cfcs/core@^0.1.0": + version "0.1.0" + resolved "https://registry.yarnpkg.com/@cfcs/core/-/core-0.1.0.tgz#64346f7634b46de6e9b1d4c195743b9b8e603162" + integrity sha512-kvYX0RpL45XTHJ5sW7teNbKeAa7pK3nNqaJPoFfZDPTIBJOkTtRD3QhkBG+O3Hu69a8xeMoPvF6y/RtJ6JUOdA== + dependencies: + "@egjs/component" "^3.0.4" + "@coinbase/wallet-sdk@4.0.4": version "4.0.4" resolved "https://registry.npmjs.org/@coinbase/wallet-sdk/-/wallet-sdk-4.0.4.tgz" @@ -1158,6 +1154,11 @@ resolved "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-3.1.1.tgz" integrity sha512-a7cxGcJ2wIlMFLlh8z2ONm+715QkPHiyJcxwQlKOz/03GPw1COpfhcmC9wm4xlZfp//jWHNNMwzjtqHXVWU9KA== +"@daybrush/utils@^1.0.0", "@daybrush/utils@^1.1.1", "@daybrush/utils@^1.10.2", "@daybrush/utils@^1.13.0", "@daybrush/utils@^1.4.0", "@daybrush/utils@^1.6.0", "@daybrush/utils@^1.7.1": + version "1.13.0" + resolved "https://registry.yarnpkg.com/@daybrush/utils/-/utils-1.13.0.tgz#ea70a60864130da476406fdd1d465e3068aea0ff" + integrity sha512-ALK12C6SQNNHw1enXK+UO8bdyQ+jaWNQ1Af7Z3FNxeAwjYhQT7do+TRE4RASAJ3ObaS2+TJ7TXR3oz2Gzbw0PQ== + "@discoveryjs/json-ext@^0.5.3": version "0.5.7" resolved "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz" @@ -1170,7 +1171,7 @@ dependencies: tslib "^2.0.0" -"@dnd-kit/core@^6.3.0", "@dnd-kit/core@^6.3.1": +"@dnd-kit/core@^6.3.1": version "6.3.1" resolved "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz" integrity sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ== @@ -1194,6 +1195,28 @@ dependencies: tslib "^2.0.0" +"@egjs/agent@^2.2.1": + version "2.4.4" + resolved "https://registry.yarnpkg.com/@egjs/agent/-/agent-2.4.4.tgz#bd2ef4a17427d425332b01f3e79953cd413b532d" + integrity sha512-cvAPSlUILhBBOakn2krdPnOGv5hAZq92f1YHxYcfu0p7uarix2C6Ia3AVizpS1SGRZGiEkIS5E+IVTLg1I2Iog== + +"@egjs/children-differ@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@egjs/children-differ/-/children-differ-1.0.1.tgz#5465fa80671d5ca3564ebe912f48b05b3e8a14fd" + integrity sha512-DRvyqMf+CPCOzAopQKHtW+X8iN6Hy6SFol+/7zCUiE5y4P/OB8JP8FtU4NxtZwtafvSL4faD5KoQYPj3JHzPFQ== + dependencies: + "@egjs/list-differ" "^1.0.0" + +"@egjs/component@^3.0.2", "@egjs/component@^3.0.4": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@egjs/component/-/component-3.0.5.tgz#2dc86e835d5dc5055cdf46c7cd794eb45330e1b6" + integrity sha512-cLcGizTrrUNA2EYE3MBmEDt2tQv1joVP1Q3oDisZ5nw0MZDx2kcgEXM+/kZpfa/PAkFvYVhRUZwytIQWoN3V/w== + +"@egjs/list-differ@^1.0.0": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@egjs/list-differ/-/list-differ-1.0.1.tgz#5772b0f8b87973bb67827f6c7d7df8d7f64a22eb" + integrity sha512-OTFTDQcWS+1ZREOdCWuk5hCBgYO4OsD30lXcOCyVOAjXMhgL5rBRDnt/otb6Nz8CzU0L/igdcaQBDLWc4t9gvg== + "@emotion/hash@^0.9.0": version "0.9.2" resolved "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.2.tgz" @@ -1204,11 +1227,116 @@ resolved "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.1.0.tgz" integrity sha512-+wBOcIV5snwGgI2ya3u99D7/FJquOIniQT1IKyDsBmEgwvpxMNeS65Oib7OnE2d2aY+3BU4OiH+0Wchf8yk3Hw== +"@esbuild/android-arm64@0.18.20": + version "0.18.20" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz#984b4f9c8d0377443cc2dfcef266d02244593622" + integrity sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ== + +"@esbuild/android-arm@0.18.20": + version "0.18.20" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.18.20.tgz#fedb265bc3a589c84cc11f810804f234947c3682" + integrity sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw== + +"@esbuild/android-x64@0.18.20": + version "0.18.20" + resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.18.20.tgz#35cf419c4cfc8babe8893d296cd990e9e9f756f2" + integrity sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg== + "@esbuild/darwin-arm64@0.18.20": version "0.18.20" resolved "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz" integrity sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA== +"@esbuild/darwin-x64@0.18.20": + version "0.18.20" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz#d70d5790d8bf475556b67d0f8b7c5bdff053d85d" + integrity sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ== + +"@esbuild/freebsd-arm64@0.18.20": + version "0.18.20" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz#98755cd12707f93f210e2494d6a4b51b96977f54" + integrity sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw== + +"@esbuild/freebsd-x64@0.18.20": + version "0.18.20" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz#c1eb2bff03915f87c29cece4c1a7fa1f423b066e" + integrity sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ== + +"@esbuild/linux-arm64@0.18.20": + version "0.18.20" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz#bad4238bd8f4fc25b5a021280c770ab5fc3a02a0" + integrity sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA== + +"@esbuild/linux-arm@0.18.20": + version "0.18.20" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz#3e617c61f33508a27150ee417543c8ab5acc73b0" + integrity sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg== + +"@esbuild/linux-ia32@0.18.20": + version "0.18.20" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz#699391cccba9aee6019b7f9892eb99219f1570a7" + integrity sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA== + +"@esbuild/linux-loong64@0.18.20": + version "0.18.20" + resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz#e6fccb7aac178dd2ffb9860465ac89d7f23b977d" + integrity sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg== + +"@esbuild/linux-mips64el@0.18.20": + version "0.18.20" + resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz#eeff3a937de9c2310de30622a957ad1bd9183231" + integrity sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ== + +"@esbuild/linux-ppc64@0.18.20": + version "0.18.20" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz#2f7156bde20b01527993e6881435ad79ba9599fb" + integrity sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA== + +"@esbuild/linux-riscv64@0.18.20": + version "0.18.20" + resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz#6628389f210123d8b4743045af8caa7d4ddfc7a6" + integrity sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A== + +"@esbuild/linux-s390x@0.18.20": + version "0.18.20" + resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz#255e81fb289b101026131858ab99fba63dcf0071" + integrity sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ== + +"@esbuild/linux-x64@0.18.20": + version "0.18.20" + resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz#c7690b3417af318a9b6f96df3031a8865176d338" + integrity sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w== + +"@esbuild/netbsd-x64@0.18.20": + version "0.18.20" + resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz#30e8cd8a3dded63975e2df2438ca109601ebe0d1" + integrity sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A== + +"@esbuild/openbsd-x64@0.18.20": + version "0.18.20" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz#7812af31b205055874c8082ea9cf9ab0da6217ae" + integrity sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg== + +"@esbuild/sunos-x64@0.18.20": + version "0.18.20" + resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz#d5c275c3b4e73c9b0ecd38d1ca62c020f887ab9d" + integrity sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ== + +"@esbuild/win32-arm64@0.18.20": + version "0.18.20" + resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz#73bc7f5a9f8a77805f357fab97f290d0e4820ac9" + integrity sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg== + +"@esbuild/win32-ia32@0.18.20": + version "0.18.20" + resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz#ec93cbf0ef1085cc12e71e0d661d20569ff42102" + integrity sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g== + +"@esbuild/win32-x64@0.18.20": + version "0.18.20" + resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz#786c5f41f043b07afb1af37683d7c33668858f6d" + integrity sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ== + "@eslint-community/eslint-utils@^4.2.0", "@eslint-community/eslint-utils@^4.4.0": version "4.4.1" resolved "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.1.tgz" @@ -1252,7 +1380,7 @@ minimatch "^3.1.2" strip-json-comments "^3.1.1" -"@eslint/js@^9.18.0", "@eslint/js@9.18.0": +"@eslint/js@9.18.0", "@eslint/js@^9.18.0": version "9.18.0" resolved "https://registry.npmjs.org/@eslint/js/-/js-9.18.0.tgz" integrity sha512-fK6L7rxcq6/z+AaQMtiFTkvbHkBLNlwyRxHpKawP0x3u9+NC6MQTnFW+AdpwC6gfHTW0051cokQgtTN2FqlxQA== @@ -1753,11 +1881,6 @@ wrap-ansi "^8.1.0" wrap-ansi-cjs "npm:wrap-ansi@^7.0.0" -"@isaacs/ttlcache@^1.4.1": - version "1.4.1" - resolved "https://registry.npmjs.org/@isaacs/ttlcache/-/ttlcache-1.4.1.tgz" - integrity sha512-RQgQ4uQ+pLbqXfOmieB91ejmLwvSgv9nLx6sT6sD83s7umBypgg+OIBOBbEUiJXrfpnp9j0mRhYYdzp9uqq3lA== - "@istanbuljs/load-nyc-config@^1.0.0": version "1.1.0" resolved "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz" @@ -2297,7 +2420,7 @@ resolved "https://registry.npmjs.org/@next/env/-/env-14.2.15.tgz" integrity sha512-S1qaj25Wru2dUpcIZMjxeMVSwkt8BK4dmWHHiBuRstcIyOsMapqT4A4jSB6onvqeygkSSmOkyny9VVx8JIGamQ== -"@next/eslint-plugin-next@^15.1.5", "@next/eslint-plugin-next@15.1.5": +"@next/eslint-plugin-next@15.1.5", "@next/eslint-plugin-next@^15.1.5": version "15.1.5" resolved "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-15.1.5.tgz" integrity sha512-3cCrXBybsqe94UxD6DBQCYCCiP9YohBMgZ5IzzPYHmPzj8oqNlhBii5b6o1HDDaRHdz2pVnSsAROCtrczy8O0g== @@ -2309,72 +2432,92 @@ resolved "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.15.tgz" integrity sha512-Rvh7KU9hOUBnZ9TJ28n2Oa7dD9cvDBKua9IKx7cfQQ0GoYUwg9ig31O2oMwH3wm+pE3IkAQ67ZobPfEgurPZIA== -"@noble/curves@^1.4.0", "@noble/curves@^1.6.0", "@noble/curves@~1.7.0", "@noble/curves@1.7.0": - version "1.7.0" - resolved "https://registry.npmjs.org/@noble/curves/-/curves-1.7.0.tgz" - integrity sha512-UTMhXK9SeDhFJVrHeUJ5uZlI6ajXg10O6Ddocf9S6GjbSBVZsJo88HzKwXznNfGpMTRDyJkqMjNDPYgf0qFWnw== - dependencies: - "@noble/hashes" "1.6.0" +"@next/swc-darwin-x64@14.2.15": + version "14.2.15" + resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.15.tgz#b7baeedc6a28f7545ad2bc55adbab25f7b45cb89" + integrity sha512-5TGyjFcf8ampZP3e+FyCax5zFVHi+Oe7sZyaKOngsqyaNEpOgkKB3sqmymkZfowy3ufGA/tUgDPPxpQx931lHg== + +"@next/swc-linux-arm64-gnu@14.2.15": + version "14.2.15" + resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.15.tgz#fa13c59d3222f70fb4cb3544ac750db2c6e34d02" + integrity sha512-3Bwv4oc08ONiQ3FiOLKT72Q+ndEMyLNsc/D3qnLMbtUYTQAmkx9E/JRu0DBpHxNddBmNT5hxz1mYBphJ3mfrrw== + +"@next/swc-linux-arm64-musl@14.2.15": + version "14.2.15" + resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.15.tgz#30e45b71831d9a6d6d18d7ac7d611a8d646a17f9" + integrity sha512-k5xf/tg1FBv/M4CMd8S+JL3uV9BnnRmoe7F+GWC3DxkTCD9aewFRH1s5rJ1zkzDa+Do4zyN8qD0N8c84Hu96FQ== + +"@next/swc-linux-x64-gnu@14.2.15": + version "14.2.15" + resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.15.tgz#5065db17fc86f935ad117483f21f812dc1b39254" + integrity sha512-kE6q38hbrRbKEkkVn62reLXhThLRh6/TvgSP56GkFNhU22TbIrQDEMrO7j0IcQHcew2wfykq8lZyHFabz0oBrA== + +"@next/swc-linux-x64-musl@14.2.15": + version "14.2.15" + resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.15.tgz#3c4a4568d8be7373a820f7576cf33388b5dab47e" + integrity sha512-PZ5YE9ouy/IdO7QVJeIcyLn/Rc4ml9M2G4y3kCM9MNf1YKvFY4heg3pVa/jQbMro+tP6yc4G2o9LjAz1zxD7tQ== + +"@next/swc-win32-arm64-msvc@14.2.15": + version "14.2.15" + resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.15.tgz#fb812cc4ca0042868e32a6a021da91943bb08b98" + integrity sha512-2raR16703kBvYEQD9HNLyb0/394yfqzmIeyp2nDzcPV4yPjqNUG3ohX6jX00WryXz6s1FXpVhsCo3i+g4RUX+g== + +"@next/swc-win32-ia32-msvc@14.2.15": + version "14.2.15" + resolved "https://registry.yarnpkg.com/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.15.tgz#ec26e6169354f8ced240c1427be7fd485c5df898" + integrity sha512-fyTE8cklgkyR1p03kJa5zXEaZ9El+kDNM5A+66+8evQS5e/6v0Gk28LqA0Jet8gKSOyP+OTm/tJHzMlGdQerdQ== + +"@next/swc-win32-x64-msvc@14.2.15": + version "14.2.15" + resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.15.tgz#18d68697002b282006771f8d92d79ade9efd35c4" + integrity sha512-SzqGbsLsP9OwKNUG9nekShTwhj6JSB9ZLMWQ8g1gG6hdE5gQLncbnbymrwy2yVmH9nikSLYRYxYMFu78Ggp7/g== -"@noble/curves@~1.2.0", "@noble/curves@1.2.0": +"@noble/curves@1.2.0", "@noble/curves@~1.2.0": version "1.2.0" resolved "https://registry.npmjs.org/@noble/curves/-/curves-1.2.0.tgz" integrity sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw== dependencies: "@noble/hashes" "1.3.2" -"@noble/curves@~1.4.0", "@noble/curves@1.4.2": +"@noble/curves@1.4.2", "@noble/curves@~1.4.0": version "1.4.2" resolved "https://registry.npmjs.org/@noble/curves/-/curves-1.4.2.tgz" integrity sha512-TavHr8qycMChk8UwMld0ZDRvatedkzWfH8IiaeGCfymOP5i0hSCozz9vHOL0nkwk7HRMlFnAiKpS2jrUmSybcw== dependencies: "@noble/hashes" "1.4.0" -"@noble/hashes@^1.3.1", "@noble/hashes@~1.4.0", "@noble/hashes@1.4.0": - version "1.4.0" - resolved "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz" - integrity sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg== - -"@noble/hashes@^1.4.0": - version "1.6.1" - resolved "https://registry.npmjs.org/@noble/hashes/-/hashes-1.6.1.tgz" - integrity sha512-pq5D8h10hHBjyqX+cfBm0i8JUXJ0UhczFc4r74zbuT9XgewFo2E3J1cOaGtdZynILNmQ685YWGzGE1Zv6io50w== - -"@noble/hashes@^1.5.0": - version "1.6.1" - resolved "https://registry.npmjs.org/@noble/hashes/-/hashes-1.6.1.tgz" - integrity sha512-pq5D8h10hHBjyqX+cfBm0i8JUXJ0UhczFc4r74zbuT9XgewFo2E3J1cOaGtdZynILNmQ685YWGzGE1Zv6io50w== - -"@noble/hashes@~1.3.0": - version "1.3.3" - resolved "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.3.tgz" - integrity sha512-V7/fPHgl+jsVPXqqeOzT8egNj2iBIVt+ECeMMG8TdcnTikP3oaBtUVqpT/gYCR68aEBJSF+XbYUxStjbFMqIIA== - -"@noble/hashes@~1.3.2": - version "1.3.3" - resolved "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.3.tgz" - integrity sha512-V7/fPHgl+jsVPXqqeOzT8egNj2iBIVt+ECeMMG8TdcnTikP3oaBtUVqpT/gYCR68aEBJSF+XbYUxStjbFMqIIA== - -"@noble/hashes@~1.6.0": - version "1.6.1" - resolved "https://registry.npmjs.org/@noble/hashes/-/hashes-1.6.1.tgz" - integrity sha512-pq5D8h10hHBjyqX+cfBm0i8JUXJ0UhczFc4r74zbuT9XgewFo2E3J1cOaGtdZynILNmQ685YWGzGE1Zv6io50w== +"@noble/curves@1.7.0", "@noble/curves@^1.4.0", "@noble/curves@^1.6.0", "@noble/curves@~1.7.0": + version "1.7.0" + resolved "https://registry.npmjs.org/@noble/curves/-/curves-1.7.0.tgz" + integrity sha512-UTMhXK9SeDhFJVrHeUJ5uZlI6ajXg10O6Ddocf9S6GjbSBVZsJo88HzKwXznNfGpMTRDyJkqMjNDPYgf0qFWnw== + dependencies: + "@noble/hashes" "1.6.0" "@noble/hashes@1.3.2": version "1.3.2" resolved "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.2.tgz" integrity sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ== +"@noble/hashes@1.4.0", "@noble/hashes@^1.3.1", "@noble/hashes@~1.4.0": + version "1.4.0" + resolved "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz" + integrity sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg== + "@noble/hashes@1.6.0": version "1.6.0" resolved "https://registry.npmjs.org/@noble/hashes/-/hashes-1.6.0.tgz" integrity sha512-YUULf0Uk4/mAA89w+k3+yUYh6NrEvxZa5T6SY3wlMvE2chHkxFUUIDI8/XW1QSC357iA5pSnqt7XEhvFOqmDyQ== -"@noble/hashes@1.6.1": +"@noble/hashes@1.6.1", "@noble/hashes@^1.4.0", "@noble/hashes@^1.5.0", "@noble/hashes@~1.6.0": version "1.6.1" resolved "https://registry.npmjs.org/@noble/hashes/-/hashes-1.6.1.tgz" integrity sha512-pq5D8h10hHBjyqX+cfBm0i8JUXJ0UhczFc4r74zbuT9XgewFo2E3J1cOaGtdZynILNmQ685YWGzGE1Zv6io50w== +"@noble/hashes@~1.3.0", "@noble/hashes@~1.3.2": + version "1.3.3" + resolved "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.3.tgz" + integrity sha512-V7/fPHgl+jsVPXqqeOzT8egNj2iBIVt+ECeMMG8TdcnTikP3oaBtUVqpT/gYCR68aEBJSF+XbYUxStjbFMqIIA== + "@nodelib/fs.scandir@2.1.5": version "2.1.5" resolved "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz" @@ -2383,7 +2526,7 @@ "@nodelib/fs.stat" "2.0.5" run-parallel "^1.1.9" -"@nodelib/fs.stat@^2.0.2", "@nodelib/fs.stat@2.0.5": +"@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2": version "2.0.5" resolved "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz" integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== @@ -2401,11 +2544,51 @@ resolved "https://registry.npmjs.org/@nolyfill/is-core-module/-/is-core-module-1.0.39.tgz" integrity sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA== +"@parcel/watcher-android-arm64@2.4.1": + version "2.4.1" + resolved "https://registry.yarnpkg.com/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.4.1.tgz#c2c19a3c442313ff007d2d7a9c2c1dd3e1c9ca84" + integrity sha512-LOi/WTbbh3aTn2RYddrO8pnapixAziFl6SMxHM69r3tvdSm94JtCenaKgk1GRg5FJ5wpMCpHeW+7yqPlvZv7kg== + "@parcel/watcher-darwin-arm64@2.4.1": version "2.4.1" resolved "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.4.1.tgz" integrity sha512-ln41eihm5YXIY043vBrrHfn94SIBlqOWmoROhsMVTSXGh0QahKGy77tfEywQ7v3NywyxBBkGIfrWRHm0hsKtzA== +"@parcel/watcher-darwin-x64@2.4.1": + version "2.4.1" + resolved "https://registry.yarnpkg.com/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.4.1.tgz#1a3f69d9323eae4f1c61a5f480a59c478d2cb020" + integrity sha512-yrw81BRLjjtHyDu7J61oPuSoeYWR3lDElcPGJyOvIXmor6DEo7/G2u1o7I38cwlcoBHQFULqF6nesIX3tsEXMg== + +"@parcel/watcher-freebsd-x64@2.4.1": + version "2.4.1" + resolved "https://registry.yarnpkg.com/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.4.1.tgz#0d67fef1609f90ba6a8a662bc76a55fc93706fc8" + integrity sha512-TJa3Pex/gX3CWIx/Co8k+ykNdDCLx+TuZj3f3h7eOjgpdKM+Mnix37RYsYU4LHhiYJz3DK5nFCCra81p6g050w== + +"@parcel/watcher-linux-arm-glibc@2.4.1": + version "2.4.1" + resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.4.1.tgz#ce5b340da5829b8e546bd00f752ae5292e1c702d" + integrity sha512-4rVYDlsMEYfa537BRXxJ5UF4ddNwnr2/1O4MHM5PjI9cvV2qymvhwZSFgXqbS8YoTk5i/JR0L0JDs69BUn45YA== + +"@parcel/watcher-linux-arm64-glibc@2.4.1": + version "2.4.1" + resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.4.1.tgz#6d7c00dde6d40608f9554e73998db11b2b1ff7c7" + integrity sha512-BJ7mH985OADVLpbrzCLgrJ3TOpiZggE9FMblfO65PlOCdG++xJpKUJ0Aol74ZUIYfb8WsRlUdgrZxKkz3zXWYA== + +"@parcel/watcher-linux-arm64-musl@2.4.1": + version "2.4.1" + resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.4.1.tgz#bd39bc71015f08a4a31a47cd89c236b9d6a7f635" + integrity sha512-p4Xb7JGq3MLgAfYhslU2SjoV9G0kI0Xry0kuxeG/41UfpjHGOhv7UoUDAz/jb1u2elbhazy4rRBL8PegPJFBhA== + +"@parcel/watcher-linux-x64-glibc@2.4.1": + version "2.4.1" + resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.4.1.tgz#0ce29966b082fb6cdd3de44f2f74057eef2c9e39" + integrity sha512-s9O3fByZ/2pyYDPoLM6zt92yu6P4E39a03zvO0qCHOTjxmt3GHRMLuRZEWhWLASTMSrrnVNWdVI/+pUElJBBBg== + +"@parcel/watcher-linux-x64-musl@2.4.1": + version "2.4.1" + resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.4.1.tgz#d2ebbf60e407170bb647cd6e447f4f2bab19ad16" + integrity sha512-L2nZTYR1myLNST0O632g0Dx9LyMNHrn6TOt76sYxWLdff3cB22/GZX2UPtJnaqQPdCRoszoY5rcOj4oMTtp5fQ== + "@parcel/watcher-wasm@^2.4.1": version "2.4.1" resolved "https://registry.npmjs.org/@parcel/watcher-wasm/-/watcher-wasm-2.4.1.tgz" @@ -2415,6 +2598,21 @@ micromatch "^4.0.5" napi-wasm "^1.1.0" +"@parcel/watcher-win32-arm64@2.4.1": + version "2.4.1" + resolved "https://registry.yarnpkg.com/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.4.1.tgz#eb4deef37e80f0b5e2f215dd6d7a6d40a85f8adc" + integrity sha512-Uq2BPp5GWhrq/lcuItCHoqxjULU1QYEcyjSO5jqqOK8RNFDBQnenMMx4gAl3v8GiWa59E9+uDM7yZ6LxwUIfRg== + +"@parcel/watcher-win32-ia32@2.4.1": + version "2.4.1" + resolved "https://registry.yarnpkg.com/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.4.1.tgz#94fbd4b497be39fd5c8c71ba05436927842c9df7" + integrity sha512-maNRit5QQV2kgHFSYwftmPBxiuK5u4DXjbXx7q6eKjq5dsLXZ4FJiVvlcw35QXzk0KrUecJmuVFbj4uV9oYrcw== + +"@parcel/watcher-win32-x64@2.4.1": + version "2.4.1" + resolved "https://registry.yarnpkg.com/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.4.1.tgz#4bf920912f67cae5f2d264f58df81abfea68dadf" + integrity sha512-+DvS92F9ezicfswqrvIRM2njcYJbd5mb9CUgtrHCHmvn7pPPa+nMDRu1o1bYYz/l5IB2NVGNJWiH7h1E58IF2A== + "@parcel/watcher@^2.4.1": version "2.4.1" resolved "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.4.1.tgz" @@ -2912,81 +3110,6 @@ "@swc/helpers" "^0.5.0" clsx "^2.0.0" -"@react-native/assets-registry@0.80.1": - version "0.80.1" - resolved "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.80.1.tgz" - integrity sha512-T3C8OthBHfpFIjaGFa0q6rc58T2AsJ+jKAa+qPquMKBtYGJMc75WgNbk/ZbPBxeity6FxZsmg3bzoUaWQo4Mow== - -"@react-native/codegen@0.80.1": - version "0.80.1" - resolved "https://registry.npmjs.org/@react-native/codegen/-/codegen-0.80.1.tgz" - integrity sha512-CFhOYkXmExOeZDZnd0UJCK9A4AOSAyFBoVgmFZsf+fv8JqnwIx/SD6RxY1+Jzz9EWPQcH2v+WgwPP/4qVmjtKw== - dependencies: - glob "^7.1.1" - hermes-parser "0.28.1" - invariant "^2.2.4" - nullthrows "^1.1.1" - yargs "^17.6.2" - -"@react-native/community-cli-plugin@0.80.1": - version "0.80.1" - resolved "https://registry.npmjs.org/@react-native/community-cli-plugin/-/community-cli-plugin-0.80.1.tgz" - integrity sha512-M1lzLvZUz6zb6rn4Oyc3HUY72wye8mtdm1bJSYIBoK96ejMvQGoM+Lih/6k3c1xL7LSruNHfsEXXePLjCbhE8Q== - dependencies: - "@react-native/dev-middleware" "0.80.1" - chalk "^4.0.0" - debug "^4.4.0" - invariant "^2.2.4" - metro "^0.82.2" - metro-config "^0.82.2" - metro-core "^0.82.2" - semver "^7.1.3" - -"@react-native/debugger-frontend@0.80.1": - version "0.80.1" - resolved "https://registry.npmjs.org/@react-native/debugger-frontend/-/debugger-frontend-0.80.1.tgz" - integrity sha512-5dQJdX1ZS4dINNw51KNsDIL+A06sZQd2hqN2Pldq5SavxAwEJh5NxAx7K+lutKhwp1By5gxd6/9ruVt+9NCvKA== - -"@react-native/dev-middleware@0.80.1": - version "0.80.1" - resolved "https://registry.npmjs.org/@react-native/dev-middleware/-/dev-middleware-0.80.1.tgz" - integrity sha512-EBnZ3s6+hGAlUggDvo9uI37Xh0vG55H2rr3A6l6ww7+sgNuUz+wEJ63mGINiU6DwzQSgr6av7rjrVERxKH6vxg== - dependencies: - "@isaacs/ttlcache" "^1.4.1" - "@react-native/debugger-frontend" "0.80.1" - chrome-launcher "^0.15.2" - chromium-edge-launcher "^0.2.0" - connect "^3.6.5" - debug "^4.4.0" - invariant "^2.2.4" - nullthrows "^1.1.1" - open "^7.0.3" - serve-static "^1.16.2" - ws "^6.2.3" - -"@react-native/gradle-plugin@0.80.1": - version "0.80.1" - resolved "https://registry.npmjs.org/@react-native/gradle-plugin/-/gradle-plugin-0.80.1.tgz" - integrity sha512-6B7bWUk27ne/g/wCgFF4MZFi5iy6hWOcBffqETJoab6WURMyZ6nU+EAMn+Vjhl5ishhUvTVSrJ/1uqrxxYQO2Q== - -"@react-native/js-polyfills@0.80.1": - version "0.80.1" - resolved "https://registry.npmjs.org/@react-native/js-polyfills/-/js-polyfills-0.80.1.tgz" - integrity sha512-cWd5Cd2kBMRM37dor8N9Ck4X0NzjYM3m8K6HtjodcOdOvzpXfrfhhM56jdseTl5Z4iB+pohzPJpSmFJctmuIpA== - -"@react-native/normalize-colors@0.80.1": - version "0.80.1" - resolved "https://registry.npmjs.org/@react-native/normalize-colors/-/normalize-colors-0.80.1.tgz" - integrity sha512-YP12bjz0bzo2lFxZDOPkRJSOkcqAzXCQQIV1wd7lzCTXE0NJNwoaeNBobJvcPhiODEWUYCXPANrZveFhtFu5vw== - -"@react-native/virtualized-lists@0.80.1": - version "0.80.1" - resolved "https://registry.npmjs.org/@react-native/virtualized-lists/-/virtualized-lists-0.80.1.tgz" - integrity sha512-nqQAeHheSNZBV+syhLVMgKBZv+FhCANfxAWVvfEXZa4rm5jGHsj3yA9vqrh2lcJL3pjd7PW5nMX7TcuJThEAgQ== - dependencies: - invariant "^2.2.4" - nullthrows "^1.1.1" - "@react-stately/utils@^3.10.2": version "3.10.2" resolved "https://registry.npmjs.org/@react-stately/utils/-/utils-3.10.2.tgz" @@ -3017,7 +3140,7 @@ "@safe-global/safe-apps-sdk" "^8.1.0" events "^3.3.0" -"@safe-global/safe-apps-sdk@^8.1.0", "@safe-global/safe-apps-sdk@8.1.0": +"@safe-global/safe-apps-sdk@8.1.0", "@safe-global/safe-apps-sdk@^8.1.0": version "8.1.0" resolved "https://registry.npmjs.org/@safe-global/safe-apps-sdk/-/safe-apps-sdk-8.1.0.tgz" integrity sha512-XJbEPuaVc7b9n23MqlF6c+ToYIS3f7P2Sel8f3cSBQ9WORE4xrSuvhMpK9fDSFqJ7by/brc+rmJR/5HViRr0/w== @@ -3030,6 +3153,28 @@ resolved "https://registry.npmjs.org/@safe-global/safe-gateway-typescript-sdk/-/safe-gateway-typescript-sdk-3.22.2.tgz" integrity sha512-Y0yAxRaB98LFp2Dm+ACZqBSdAmI3FlpH/LjxOZ94g/ouuDJecSq0iR26XZ5QDuEL8Rf+L4jBJaoDC08CD0KkJw== +"@scena/dragscroll@^1.4.0": + version "1.4.0" + resolved "https://registry.yarnpkg.com/@scena/dragscroll/-/dragscroll-1.4.0.tgz#220b2430c16119cd3e70044ee533a5b9a43cffd7" + integrity sha512-3O8daaZD9VXA9CP3dra6xcgt/qrm0mg0xJCwiX6druCteQ9FFsXffkF8PrqxY4Z4VJ58fFKEa0RlKqbsi/XnRA== + dependencies: + "@daybrush/utils" "^1.6.0" + "@scena/event-emitter" "^1.0.2" + +"@scena/event-emitter@^1.0.2", "@scena/event-emitter@^1.0.3", "@scena/event-emitter@^1.0.5": + version "1.0.5" + resolved "https://registry.yarnpkg.com/@scena/event-emitter/-/event-emitter-1.0.5.tgz#047e3acef93cf238d7ce3a8cc5a12ec6bd9c3bb1" + integrity sha512-AzY4OTb0+7ynefmWFQ6hxDdk0CySAq/D4efljfhtRHCOP7MBF9zUfhKG3TJiroVjASqVgkRJFdenS8ArZo6Olg== + dependencies: + "@daybrush/utils" "^1.1.1" + +"@scena/matrix@^1.0.0", "@scena/matrix@^1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@scena/matrix/-/matrix-1.1.1.tgz#5297f71825c72e2c2c8f802f924f482ed200c43c" + integrity sha512-JVKBhN0tm2Srl+Yt+Ywqu0oLgLcdemDQlD1OxmN9jaCTwaFPZ7tY8n6dhVgMEaR9qcR7r+kAlMXnSfNyYdE+Vg== + dependencies: + "@daybrush/utils" "^1.4.0" + "@scure/base@^1.1.3", "@scure/base@~1.1.0", "@scure/base@~1.1.2", "@scure/base@~1.1.6": version "1.1.7" resolved "https://registry.npmjs.org/@scure/base/-/base-1.1.7.tgz" @@ -3040,15 +3185,6 @@ resolved "https://registry.npmjs.org/@scure/base/-/base-1.2.1.tgz" integrity sha512-DGmGtC8Tt63J5GfHgfl5CuAXh96VF/LD8K9Hr/Gv0J2lAoRGlPOMpqMpMbCTOoOJMZCk2Xt+DskdDyn6dEFdzQ== -"@scure/bip32@^1.5.0", "@scure/bip32@1.6.0": - version "1.6.0" - resolved "https://registry.npmjs.org/@scure/bip32/-/bip32-1.6.0.tgz" - integrity sha512-82q1QfklrUUdXJzjuRU7iG7D7XiFx5PHYVS0+oeNKhyDLT7WPqs6pBcM2W5ZdwOwKCwoE1Vy1se+DHjcXwCYnA== - dependencies: - "@noble/curves" "~1.7.0" - "@noble/hashes" "~1.6.0" - "@scure/base" "~1.2.1" - "@scure/bip32@1.3.2": version "1.3.2" resolved "https://registry.npmjs.org/@scure/bip32/-/bip32-1.3.2.tgz" @@ -3067,11 +3203,12 @@ "@noble/hashes" "~1.4.0" "@scure/base" "~1.1.6" -"@scure/bip39@^1.4.0", "@scure/bip39@1.5.0": - version "1.5.0" - resolved "https://registry.npmjs.org/@scure/bip39/-/bip39-1.5.0.tgz" - integrity sha512-Dop+ASYhnrwm9+HA/HwXg7j2ZqM6yk2fyLWb5znexjctFY3+E+eU8cIWI0Pql0Qx4hPZCijlGq4OL71g+Uz30A== +"@scure/bip32@1.6.0", "@scure/bip32@^1.5.0": + version "1.6.0" + resolved "https://registry.npmjs.org/@scure/bip32/-/bip32-1.6.0.tgz" + integrity sha512-82q1QfklrUUdXJzjuRU7iG7D7XiFx5PHYVS0+oeNKhyDLT7WPqs6pBcM2W5ZdwOwKCwoE1Vy1se+DHjcXwCYnA== dependencies: + "@noble/curves" "~1.7.0" "@noble/hashes" "~1.6.0" "@scure/base" "~1.2.1" @@ -3091,6 +3228,14 @@ "@noble/hashes" "~1.4.0" "@scure/base" "~1.1.6" +"@scure/bip39@1.5.0", "@scure/bip39@^1.4.0": + version "1.5.0" + resolved "https://registry.npmjs.org/@scure/bip39/-/bip39-1.5.0.tgz" + integrity sha512-Dop+ASYhnrwm9+HA/HwXg7j2ZqM6yk2fyLWb5znexjctFY3+E+eU8cIWI0Pql0Qx4hPZCijlGq4OL71g+Uz30A== + dependencies: + "@noble/hashes" "~1.6.0" + "@scure/base" "~1.2.1" + "@sinclair/typebox@^0.27.8": version "0.27.8" resolved "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz" @@ -3132,14 +3277,6 @@ resolved "https://registry.npmjs.org/@stablelib/bytes/-/bytes-1.0.1.tgz" integrity sha512-Kre4Y4kdwuqL8BR2E9hV/R5sOrUj6NanZaZis0V6lX5yzqC3hBuVSDXUIBqQv/sCpmuWRiHLwqiT1pqqjuBXoQ== -"@stablelib/chacha@^1.0.1": - version "1.0.1" - resolved "https://registry.npmjs.org/@stablelib/chacha/-/chacha-1.0.1.tgz" - integrity sha512-Pmlrswzr0pBzDofdFuVe1q7KdsHKhhU24e8gkEwnTGOmlC7PADzLVxGdn2PoNVBBabdg0l/IfLKg6sHAbTQugg== - dependencies: - "@stablelib/binary" "^1.0.1" - "@stablelib/wipe" "^1.0.1" - "@stablelib/chacha20poly1305@1.0.1": version "1.0.1" resolved "https://registry.npmjs.org/@stablelib/chacha20poly1305/-/chacha20poly1305-1.0.1.tgz" @@ -3152,6 +3289,14 @@ "@stablelib/poly1305" "^1.0.1" "@stablelib/wipe" "^1.0.1" +"@stablelib/chacha@^1.0.1": + version "1.0.1" + resolved "https://registry.npmjs.org/@stablelib/chacha/-/chacha-1.0.1.tgz" + integrity sha512-Pmlrswzr0pBzDofdFuVe1q7KdsHKhhU24e8gkEwnTGOmlC7PADzLVxGdn2PoNVBBabdg0l/IfLKg6sHAbTQugg== + dependencies: + "@stablelib/binary" "^1.0.1" + "@stablelib/wipe" "^1.0.1" + "@stablelib/constant-time@^1.0.1": version "1.0.1" resolved "https://registry.npmjs.org/@stablelib/constant-time/-/constant-time-1.0.1.tgz" @@ -3209,7 +3354,7 @@ "@stablelib/constant-time" "^1.0.1" "@stablelib/wipe" "^1.0.1" -"@stablelib/random@^1.0.1", "@stablelib/random@^1.0.2", "@stablelib/random@1.0.2": +"@stablelib/random@1.0.2", "@stablelib/random@^1.0.1", "@stablelib/random@^1.0.2": version "1.0.2" resolved "https://registry.npmjs.org/@stablelib/random/-/random-1.0.2.tgz" integrity sha512-rIsE83Xpb7clHPVRlBj8qNe5L8ISQOzjghYQm/dZ7VaM2KHYwMW5adjQjrzTZCchFnNCNhkwtnOBa9HTMJCI8w== @@ -3254,7 +3399,7 @@ resolved "https://registry.npmjs.org/@stomp/stompjs/-/stompjs-7.0.0.tgz" integrity sha512-fGdq4wPDnSV/KyOsjq4P+zLc8MFWC3lMmP5FBgLWKPJTYcuCbAIrnRGjB7q2jHZdYCOD5vxLuFoKIYLy5/u8Pw== -"@storybook/addon-actions@^7.5.2", "@storybook/addon-actions@7.6.20": +"@storybook/addon-actions@7.6.20", "@storybook/addon-actions@^7.5.2": version "7.6.20" resolved "https://registry.npmjs.org/@storybook/addon-actions/-/addon-actions-7.6.20.tgz" integrity sha512-c/GkEQ2U9BC/Ew/IMdh+zvsh4N6y6n7Zsn2GIhJgcu9YEAa5aF2a9/pNgEGBMOABH959XE8DAOMERw/5qiLR8g== @@ -3395,7 +3540,7 @@ dependencies: memoizerific "^1.11.3" -"@storybook/blocks@^7.5.2", "@storybook/blocks@7.6.20": +"@storybook/blocks@7.6.20", "@storybook/blocks@^7.5.2": version "7.6.20" resolved "https://registry.npmjs.org/@storybook/blocks/-/blocks-7.6.20.tgz" integrity sha512-xADKGEOJWkG0UD5jbY4mBXRlmj2C+CIupDL0/hpzvLvwobxBMFPKZIkcZIMvGvVnI/Ui+tJxQxLSuJ5QsPthUw== @@ -3829,6 +3974,11 @@ tsconfig-paths "^4.0.0" tsconfig-paths-webpack-plugin "^4.0.1" +"@storybook/node-logger@7.6.20": + version "7.6.20" + resolved "https://registry.npmjs.org/@storybook/node-logger/-/node-logger-7.6.20.tgz" + integrity sha512-l2i4qF1bscJkOplNffcRTsgQWYR7J51ewmizj5YrTM8BK6rslWT1RntgVJWB1RgPqvx6VsCz1gyP3yW1oKxvYw== + "@storybook/node-logger@^6.1.14": version "6.5.16" resolved "https://registry.npmjs.org/@storybook/node-logger/-/node-logger-6.5.16.tgz" @@ -3840,11 +3990,6 @@ npmlog "^5.0.1" pretty-hrtime "^1.0.3" -"@storybook/node-logger@7.6.20": - version "7.6.20" - resolved "https://registry.npmjs.org/@storybook/node-logger/-/node-logger-7.6.20.tgz" - integrity sha512-l2i4qF1bscJkOplNffcRTsgQWYR7J51ewmizj5YrTM8BK6rslWT1RntgVJWB1RgPqvx6VsCz1gyP3yW1oKxvYw== - "@storybook/postinstall@7.6.20": version "7.6.20" resolved "https://registry.npmjs.org/@storybook/postinstall/-/postinstall-7.6.20.tgz" @@ -3916,7 +4061,7 @@ resolved "https://registry.npmjs.org/@storybook/react-dom-shim/-/react-dom-shim-7.6.20.tgz" integrity sha512-SRvPDr9VWcS24ByQOVmbfZ655y5LvjXRlsF1I6Pr9YZybLfYbu3L5IicfEHT4A8lMdghzgbPFVQaJez46DTrkg== -"@storybook/react@^7.5.2", "@storybook/react@7.6.20": +"@storybook/react@7.6.20", "@storybook/react@^7.5.2": version "7.6.20" resolved "https://registry.npmjs.org/@storybook/react/-/react-7.6.20.tgz" integrity sha512-i5tKNgUbTNwlqBWGwPveDhh9ktlS0wGtd97A1ZgKZc3vckLizunlAFc7PRC1O/CMq5PTyxbuUb4RvRD2jWKwDA== @@ -4049,7 +4194,7 @@ "@svgr/babel-plugin-transform-react-native-svg" "8.1.0" "@svgr/babel-plugin-transform-svg-component" "8.0.0" -"@svgr/core@*", "@svgr/core@^8.1.0": +"@svgr/core@^8.1.0": version "8.1.0" resolved "https://registry.npmjs.org/@svgr/core/-/core-8.1.0.tgz" integrity sha512-8QqtOQT5ACVlmsvKOJNEaWmRPmcojMOzCz4Hs2BGG/toAp/K38LcsMRyLp349glq5AzJbCEeimEoxaX6v/fLrA== @@ -4091,7 +4236,52 @@ resolved "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.7.11.tgz" integrity sha512-HRQv4qIeMBPThZ6Y/4yYW52rGsS6yrpusvuxLGyoFo45Y0y12/V2yXkOIA/0HIQyrqoUAxn1k4zQXpPaPNCmnw== -"@swc/core@*", "@swc/core@^1.2.147", "@swc/core@^1.3.67", "@swc/core@^1.3.82": +"@swc/core-darwin-x64@1.7.11": + version "1.7.11" + resolved "https://registry.yarnpkg.com/@swc/core-darwin-x64/-/core-darwin-x64-1.7.11.tgz#c0e3f248d075160b86f12b21b9dafee48196f52e" + integrity sha512-vtMQj0F3oYwDu5yhO7SKDRg1XekRSi6/TbzHAbBXv+dBhlGGvcZZynT1H90EVFTv+7w7Sh+lOFvRv5Z4ZTcxow== + +"@swc/core-linux-arm-gnueabihf@1.7.11": + version "1.7.11" + resolved "https://registry.yarnpkg.com/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.7.11.tgz#677f87c806261243afe4903fde3dfac11e9f159b" + integrity sha512-mHtzWKxhtyreI4CSxs+3+ENv8t/Qo35WFoYG66qHEgJz/Z2Lh6jv1E+MYgHdYwnpQHgHbdvAco7HsBu/Dt6xXw== + +"@swc/core-linux-arm64-gnu@1.7.11": + version "1.7.11" + resolved "https://registry.yarnpkg.com/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.7.11.tgz#ad38860e7ebed7ece215ea02f1a134798275ce2c" + integrity sha512-FRwe/x0GfXSQjGP2lIk+NO0pUFS/lI/RorCLBPiK808EVE9JTbh9DKCc/4Bbb4jgScAjNkrFCUVObQYl3YKmpA== + +"@swc/core-linux-arm64-musl@1.7.11": + version "1.7.11" + resolved "https://registry.yarnpkg.com/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.7.11.tgz#ffe7cf7e23b6c4022c66b274cc2ff068c0a7cede" + integrity sha512-GY/rs0+GUq14Gbnza90KOrQd/9yHd5qQMii5jcSWcUCT5A8QTa8kiicsM2NxZeTJ69xlKmT7sLod5l99lki/2A== + +"@swc/core-linux-x64-gnu@1.7.11": + version "1.7.11" + resolved "https://registry.yarnpkg.com/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.7.11.tgz#697fb7dcb453509d8a08da781e7ec337b112f54b" + integrity sha512-QDkGRwSPmp2RBOlSs503IUXlWYlny8DyznTT0QuK0ML2RpDFlXWU94K/EZhS0RBEUkMY/W51OacM8P8aS/dkCg== + +"@swc/core-linux-x64-musl@1.7.11": + version "1.7.11" + resolved "https://registry.yarnpkg.com/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.7.11.tgz#70deeedd81d77deb062c71d68cab79b36219f79f" + integrity sha512-SBEfKrXy6zQ6ksnyxw1FaCftrIH4fLfA81xNnKb7x/6iblv7Ko6H0aK3P5C86jyqF/82+ONl9C7ImGkUFQADig== + +"@swc/core-win32-arm64-msvc@1.7.11": + version "1.7.11" + resolved "https://registry.yarnpkg.com/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.7.11.tgz#f232c2d5ea93a0aa6650e5a8c49f5b23db6a218b" + integrity sha512-a2Y4xxEsLLYHJN7sMnw9+YQJDi3M1BxEr9hklfopPuGGnYLFNnx5CypH1l9ReijEfWjIAHNi7pq3m023lzW1Hg== + +"@swc/core-win32-ia32-msvc@1.7.11": + version "1.7.11" + resolved "https://registry.yarnpkg.com/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.7.11.tgz#3fa43c3bf4b1593fa9abe017b55080651e7fff06" + integrity sha512-ZbZFMwZO+j8ulhegJ7EhJ/QVZPoQ5qc30ylJQSxizizTJaen71Q7/13lXWc6ksuCKvg6dUKrp/TPgoxOOtSrFA== + +"@swc/core-win32-x64-msvc@1.7.11": + version "1.7.11" + resolved "https://registry.yarnpkg.com/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.7.11.tgz#433bac0a04a0a49c9d9c8f1fe45f5555c88deca7" + integrity sha512-IUohZedSJyDu/ReEBG/mqX6uG29uA7zZ9z6dIAF+p6eFxjXmh9MuHryyM+H8ebUyoq/Ad3rL+rUCksnuYNnI0w== + +"@swc/core@^1.3.67", "@swc/core@^1.3.82": version "1.7.11" resolved "https://registry.npmjs.org/@swc/core/-/core-1.7.11.tgz" integrity sha512-AB+qc45UrJrDfbhPKcUXk+9z/NmFfYYwJT6G7/iur0fCse9kXjx45gi40+u/O2zgarG/30/zV6E3ps8fUvjh7g== @@ -4115,13 +4305,6 @@ resolved "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz" integrity sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ== -"@swc/helpers@*", "@swc/helpers@^0.5.0": - version "0.5.12" - resolved "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.12.tgz" - integrity sha512-KMZNXiGibsW9kvZAO1Pam2JPTDBm+KSHMMHWdsyI/1DbIZjT2A6Gy3hblVXUMEDvUAKq+e0vL0X0o54owWji7g== - dependencies: - tslib "^2.4.0" - "@swc/helpers@0.5.5": version "0.5.5" resolved "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.5.tgz" @@ -4130,6 +4313,13 @@ "@swc/counter" "^0.1.3" tslib "^2.4.0" +"@swc/helpers@^0.5.0": + version "0.5.12" + resolved "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.12.tgz" + integrity sha512-KMZNXiGibsW9kvZAO1Pam2JPTDBm+KSHMMHWdsyI/1DbIZjT2A6Gy3hblVXUMEDvUAKq+e0vL0X0o54owWji7g== + dependencies: + tslib "^2.4.0" + "@swc/jest@^0.2.26": version "0.2.36" resolved "https://registry.npmjs.org/@swc/jest/-/jest-0.2.36.tgz" @@ -4146,7 +4336,7 @@ dependencies: "@swc/counter" "^0.1.3" -"@tanstack/query-core@>=5.0.0", "@tanstack/query-core@5.51.21": +"@tanstack/query-core@5.51.21": version "5.51.21" resolved "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.51.21.tgz" integrity sha512-POQxm42IUp6n89kKWF4IZi18v3fxQWFRolvBA6phNVmA8psdfB1MvDnGacCJdS+EOX12w/CyHM62z//rHmYmvw== @@ -4168,7 +4358,7 @@ resolved "https://registry.npmjs.org/@tanstack/react-query-next-experimental/-/react-query-next-experimental-5.51.23.tgz" integrity sha512-5fPPLbnASRdea24AxdpMd3gs21q1ZB8ZsWDQDzrYih1SkaT3gADOixMsqM0l5MUvfj2O/u1ZcAizG/IA0EA1bw== -"@tanstack/react-query@^5.45.1", "@tanstack/react-query@^5.51.23", "@tanstack/react-query@>=5.0.0": +"@tanstack/react-query@^5.45.1": version "5.51.23" resolved "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.51.23.tgz" integrity sha512-CfJCfX45nnVIZjQBRYYtvVMIsGgWLKLYC4xcUiYEey671n1alvTZoCBaU9B85O8mF/tx9LPyrI04A6Bs2THv4A== @@ -4187,20 +4377,6 @@ resolved "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.9.0.tgz" integrity sha512-Saga7/QRGej/IDCVP5BgJ1oDqlDT2d9rQyoflS3fgMS8ntJ8JGw/LBqK2GorHa06+VrNFc0tGz65XQHJQJetFQ== -"@testing-library/dom@^10.0.0", "@testing-library/dom@>=7.21.4": - version "10.4.0" - resolved "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.0.tgz" - integrity sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ== - dependencies: - "@babel/code-frame" "^7.10.4" - "@babel/runtime" "^7.12.5" - "@types/aria-query" "^5.0.1" - aria-query "5.3.0" - chalk "^4.1.0" - dom-accessibility-api "^0.5.9" - lz-string "^1.5.0" - pretty-format "^27.0.2" - "@testing-library/dom@^9.0.0": version "9.3.4" resolved "https://registry.npmjs.org/@testing-library/dom/-/dom-9.3.4.tgz" @@ -4546,7 +4722,7 @@ resolved "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz" integrity sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ== -"@types/react-dom@*", "@types/react-dom@^18.0.0 || ^19.0.0", "@types/react-dom@18.0.6": +"@types/react-dom@18.0.6": version "18.0.6" resolved "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.0.6.tgz" integrity sha512-/5OFZgfIPSwy+YuIBP/FgJnQnsxhZhjjrnxudMddeblOouIodEQ75X14Rr4wGSG/bknL+Omy9iWlLo1u/9GzAA== @@ -4560,7 +4736,7 @@ dependencies: "@types/react" "*" -"@types/react@*", "@types/react@^16.8.0 || ^17.0.0 || ^18.0.0", "@types/react@^16.9.0 || ^17.0.0 || ^18.0.0", "@types/react@^18.0.0 || ^19.0.0", "@types/react@^18.2.33", "@types/react@^19.0.0", "@types/react@^19.1.0", "@types/react@>=16", "@types/react@>=16.8": +"@types/react@*", "@types/react@>=16", "@types/react@^18.2.33": version "18.3.3" resolved "https://registry.npmjs.org/@types/react/-/react-18.3.3.tgz" integrity sha512-hti/R0pS0q1/xx+TsI73XIqk26eBsISZ2R0wUijXIngRK9R/e7Xw/cXVxQK7R5JjW+SV4zGcn5hXjudkN/pLIw== @@ -4646,7 +4822,7 @@ dependencies: "@types/yargs-parser" "*" -"@typescript-eslint/eslint-plugin@^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0", "@typescript-eslint/eslint-plugin@^6.0.0 || ^7.0.0 || ^8.0.0", "@typescript-eslint/eslint-plugin@^8.0.0-0 || ^7.0.0 || ^6.0.0 || ^5.0.0", "@typescript-eslint/eslint-plugin@8.20.0": +"@typescript-eslint/eslint-plugin@8.20.0", "@typescript-eslint/eslint-plugin@^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0": version "8.20.0" resolved "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.20.0.tgz" integrity sha512-naduuphVw5StFfqp4Gq4WhIBE2gN1GEmMUExpJYknZJdRnc+2gDzB8Z3+5+/Kv33hPQRDGzQO/0opHE72lZZ6A== @@ -4661,7 +4837,7 @@ natural-compare "^1.4.0" ts-api-utils "^2.0.0" -"@typescript-eslint/parser@^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0", "@typescript-eslint/parser@^8.0.0 || ^8.0.0-alpha.0", "@typescript-eslint/parser@8.20.0": +"@typescript-eslint/parser@8.20.0", "@typescript-eslint/parser@^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0": version "8.20.0" resolved "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.20.0.tgz" integrity sha512-gKXG7A5HMyjDIedBi6bUrDcun8GIjnI8qOwVLiY3rx6T/sHP/19XLJOnIq/FgQvWLHja5JN/LSE7eklNBr612g== @@ -4721,7 +4897,7 @@ semver "^7.6.0" ts-api-utils "^2.0.0" -"@typescript-eslint/utils@^6.0.0 || ^7.0.0 || ^8.0.0", "@typescript-eslint/utils@^8.8.1", "@typescript-eslint/utils@8.20.0": +"@typescript-eslint/utils@8.20.0", "@typescript-eslint/utils@^6.0.0 || ^7.0.0 || ^8.0.0", "@typescript-eslint/utils@^8.8.1": version "8.20.0" resolved "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.20.0.tgz" integrity sha512-dq70RUw6UK9ei7vxc4KQtBRk7qkHZv447OUZ6RPQMQl71I3NZxQJX/f32Smr+iqWrB02pHKn2yAdHBb0KNrRMA== @@ -4739,7 +4915,7 @@ "@typescript-eslint/types" "8.20.0" eslint-visitor-keys "^4.2.0" -"@vanilla-extract/css@^1.0.0", "@vanilla-extract/css@1.15.5": +"@vanilla-extract/css@1.15.5": version "1.15.5" resolved "https://registry.npmjs.org/@vanilla-extract/css/-/css-1.15.5.tgz" integrity sha512-N1nQebRWnXvlcmu9fXKVUs145EVwmWtMD95bpiEKtvehHDpUhmO1l2bauS7FGYKbi3dU1IurJbGpQhBclTr1ng== @@ -4842,7 +5018,7 @@ "@walletconnect/utils" "2.13.0" events "3.3.0" -"@walletconnect/events@^1.0.1", "@walletconnect/events@1.0.1": +"@walletconnect/events@1.0.1", "@walletconnect/events@^1.0.1": version "1.0.1" resolved "https://registry.npmjs.org/@walletconnect/events/-/events-1.0.1.tgz" integrity sha512-NPTqaoi0oPBVNuLv7qPaJazmGHs5JGyO8eEAk5VGKmJzDR7AHzD4k6ilox5kxk1iwiOnFopBOOMLs86Oa76HpQ== @@ -4878,7 +5054,7 @@ "@walletconnect/safe-json" "^1.0.2" events "^3.3.0" -"@walletconnect/jsonrpc-types@^1.0.2", "@walletconnect/jsonrpc-types@^1.0.3", "@walletconnect/jsonrpc-types@1.0.4": +"@walletconnect/jsonrpc-types@1.0.4", "@walletconnect/jsonrpc-types@^1.0.2", "@walletconnect/jsonrpc-types@^1.0.3": version "1.0.4" resolved "https://registry.npmjs.org/@walletconnect/jsonrpc-types/-/jsonrpc-types-1.0.4.tgz" integrity sha512-P6679fG/M+wuWg9TY8mh6xFSdYnFyFjwFelxyISxMDrlbXokorEVXYOxiqEbrU3x1BmBoCAJJ+vtEaEoMlpCBQ== @@ -4886,7 +5062,7 @@ events "^3.3.0" keyvaluestorage-interface "^1.0.0" -"@walletconnect/jsonrpc-utils@^1.0.6", "@walletconnect/jsonrpc-utils@^1.0.8", "@walletconnect/jsonrpc-utils@1.0.8": +"@walletconnect/jsonrpc-utils@1.0.8", "@walletconnect/jsonrpc-utils@^1.0.6", "@walletconnect/jsonrpc-utils@^1.0.8": version "1.0.8" resolved "https://registry.npmjs.org/@walletconnect/jsonrpc-utils/-/jsonrpc-utils-1.0.8.tgz" integrity sha512-vdeb03bD8VzJUL6ZtzRYsFMq1eZQcM3EAzT0a3st59dyLfJ0wq+tKMpmGH7HlB7waD858UWgfIcudbPFsbzVdw== @@ -4966,7 +5142,7 @@ tslib "1.14.1" uint8arrays "^3.0.0" -"@walletconnect/safe-json@^1.0.1", "@walletconnect/safe-json@^1.0.2", "@walletconnect/safe-json@1.0.2": +"@walletconnect/safe-json@1.0.2", "@walletconnect/safe-json@^1.0.1", "@walletconnect/safe-json@^1.0.2": version "1.0.2" resolved "https://registry.npmjs.org/@walletconnect/safe-json/-/safe-json-1.0.2.tgz" integrity sha512-Ogb7I27kZ3LPC3ibn8ldyUr5544t3/STow9+lzz7Sfo808YD7SBWk7SAsdBFlYgP2zDRy2hS3sKRcuSRM0OTmA== @@ -4988,7 +5164,7 @@ "@walletconnect/utils" "2.13.0" events "3.3.0" -"@walletconnect/time@^1.0.2", "@walletconnect/time@1.0.2": +"@walletconnect/time@1.0.2", "@walletconnect/time@^1.0.2": version "1.0.2" resolved "https://registry.npmjs.org/@walletconnect/time/-/time-1.0.2.tgz" integrity sha512-uzdd9woDcJ1AaBZRhqy5rNC9laqWGErfc4dxA9a87mPdKOgWMD85mcFo9dIYIts/Jwocfwn07EC6EzclKubk/g== @@ -5042,7 +5218,7 @@ query-string "7.1.3" uint8arrays "3.1.0" -"@walletconnect/window-getters@^1.0.1", "@walletconnect/window-getters@1.0.1": +"@walletconnect/window-getters@1.0.1", "@walletconnect/window-getters@^1.0.1": version "1.0.1" resolved "https://registry.npmjs.org/@walletconnect/window-getters/-/window-getters-1.0.1.tgz" integrity sha512-vHp+HqzGxORPAN8gY03qnbTMnhqIwjeRJNOMOAzePRg4xVEEE2WvYsI9G2NMjOknA8hnuYbU3/hwLcKbjhc8+Q== @@ -5057,7 +5233,7 @@ "@walletconnect/window-getters" "^1.0.1" tslib "1.14.1" -"@webassemblyjs/ast@^1.12.1", "@webassemblyjs/ast@1.12.1": +"@webassemblyjs/ast@1.12.1", "@webassemblyjs/ast@^1.12.1": version "1.12.1" resolved "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.12.1.tgz" integrity sha512-EKfMUOPRRUTy5UII4qJDGPpqfwjOmZ5jeGFwid9mnoqIFK+e0vqoi1qH56JpmZSzEL53jKnNzScdmftJyG5xWg== @@ -5158,7 +5334,7 @@ "@webassemblyjs/wasm-gen" "1.12.1" "@webassemblyjs/wasm-parser" "1.12.1" -"@webassemblyjs/wasm-parser@^1.12.1", "@webassemblyjs/wasm-parser@1.12.1": +"@webassemblyjs/wasm-parser@1.12.1", "@webassemblyjs/wasm-parser@^1.12.1": version "1.12.1" resolved "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.12.1.tgz" integrity sha512-xikIi7c2FHXysxXe3COrVUPSheuBtpcfhbpFj4gmu7KRLYOzANztwUU0IbsqvMqzuNK2+glRGWCEqZo1WCLyAQ== @@ -5203,7 +5379,7 @@ "@yarnpkg/libzip" "^2.3.0" tslib "^1.13.0" -"@yarnpkg/libzip@^2.3.0", "@yarnpkg/libzip@2.3.0": +"@yarnpkg/libzip@2.3.0", "@yarnpkg/libzip@^2.3.0": version "2.3.0" resolved "https://registry.npmjs.org/@yarnpkg/libzip/-/libzip-2.3.0.tgz" integrity sha512-6xm38yGVIa6mKm/DUCF2zFFJhERh/QWp1ufm4cNUvxsONBmfPg8uZ9pZBdOmF6qFGr/HlT6ABBkCSx/dlEtvWg== @@ -5216,11 +5392,6 @@ abab@^2.0.6: resolved "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz" integrity sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA== -abitype@^1.0.6: - version "1.0.8" - resolved "https://registry.npmjs.org/abitype/-/abitype-1.0.8.tgz" - integrity sha512-ZeiI6h3GnW06uYDLx0etQtX/p8E24UaHHBj57RSjK7YBFe7iuVn07EDpOeP451D06sF27VOz9JJPlIKJmXgkEg== - abitype@0.9.8: version "0.9.8" resolved "https://registry.npmjs.org/abitype/-/abitype-0.9.8.tgz" @@ -5231,6 +5402,11 @@ abitype@1.0.7: resolved "https://registry.npmjs.org/abitype/-/abitype-1.0.7.tgz" integrity sha512-ZfYYSktDQUwc2eduYu8C4wOs+RDPmnRYMh7zNfzeMtGGgb0U+6tLGjixUic6mXf5xKKCcgT5Qp6cv39tOARVFw== +abitype@^1.0.6: + version "1.0.8" + resolved "https://registry.npmjs.org/abitype/-/abitype-1.0.8.tgz" + integrity sha512-ZeiI6h3GnW06uYDLx0etQtX/p8E24UaHHBj57RSjK7YBFe7iuVn07EDpOeP451D06sF27VOz9JJPlIKJmXgkEg== + abort-controller@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz" @@ -5238,7 +5414,7 @@ abort-controller@^3.0.0: dependencies: event-target-shim "^5.0.0" -accepts@^1.3.7, accepts@~1.3.5, accepts@~1.3.8: +accepts@~1.3.5, accepts@~1.3.8: version "1.3.8" resolved "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz" integrity sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw== @@ -5259,12 +5435,7 @@ acorn-import-attributes@^1.9.5: resolved "https://registry.npmjs.org/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz" integrity sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ== -acorn-jsx@^5.3.1: - version "5.3.2" - resolved "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz" - integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== - -acorn-jsx@^5.3.2: +acorn-jsx@^5.3.1, acorn-jsx@^5.3.2: version "5.3.2" resolved "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz" integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== @@ -5281,12 +5452,12 @@ acorn-walk@^8.0.2: dependencies: acorn "^8.11.0" -"acorn@^6.0.0 || ^7.0.0 || ^8.0.0", acorn@^7.4.1: +acorn@^7.4.1: version "7.4.1" resolved "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz" integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A== -acorn@^8, acorn@^8.1.0, acorn@^8.11.0, acorn@^8.11.3, acorn@^8.12.1, acorn@^8.7.1, acorn@^8.8.1, acorn@^8.8.2: +acorn@^8.1.0, acorn@^8.11.0, acorn@^8.11.3, acorn@^8.12.1, acorn@^8.7.1, acorn@^8.8.1, acorn@^8.8.2: version "8.12.1" resolved "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz" integrity sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg== @@ -5314,13 +5485,6 @@ aes-js@3.0.0: resolved "https://registry.npmjs.org/aes-js/-/aes-js-3.0.0.tgz" integrity sha512-H7wUZRn8WpTq9jocdxQ2c8x2sKo9ZVmzfRE13GiNJXfp7NcKYEdvl3vspKjXox6RIG2VtaRe4JFvxG4rqp2Zuw== -agent-base@^7.0.2, agent-base@^7.1.0, agent-base@^7.1.1: - version "7.1.1" - resolved "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz" - integrity sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA== - dependencies: - debug "^4.3.4" - agent-base@5: version "5.1.1" resolved "https://registry.npmjs.org/agent-base/-/agent-base-5.1.1.tgz" @@ -5333,6 +5497,13 @@ agent-base@6: dependencies: debug "4" +agent-base@^7.0.2, agent-base@^7.1.0, agent-base@^7.1.1: + version "7.1.1" + resolved "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz" + integrity sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA== + dependencies: + debug "^4.3.4" + aggregate-error@^3.0.0: version "3.1.0" resolved "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz" @@ -5360,7 +5531,7 @@ ajv-keywords@^5.1.0: dependencies: fast-deep-equal "^3.1.3" -ajv@^6.12.4, ajv@^6.12.5, ajv@^6.12.6, ajv@^6.9.1: +ajv@^6.12.4, ajv@^6.12.5, ajv@^6.12.6: version "6.12.6" resolved "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz" integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== @@ -5370,7 +5541,7 @@ ajv@^6.12.4, ajv@^6.12.5, ajv@^6.12.6, ajv@^6.9.1: json-schema-traverse "^0.4.1" uri-js "^4.2.2" -ajv@^8.0.0, ajv@^8.8.2, ajv@^8.9.0: +ajv@^8.0.0, ajv@^8.9.0: version "8.17.1" resolved "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz" integrity sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g== @@ -5407,16 +5578,11 @@ amp-message@~0.1.1: dependencies: amp "0.3.1" -amp@~0.3.1, amp@0.3.1: +amp@0.3.1, amp@~0.3.1: version "0.3.1" resolved "https://registry.npmjs.org/amp/-/amp-0.3.1.tgz" integrity sha512-OwIuC4yZaRogHKiuU5WlMR5Xk/jAcpPtawWL05Gj8Lvm2F6mwoJt4O/bHI+DHwG79vWd+8OFYM4/BzYqyRd3qw== -anser@^1.4.9: - version "1.4.10" - resolved "https://registry.npmjs.org/anser/-/anser-1.4.10.tgz" - integrity sha512-hCv9AqTQ8ycjpSd3upOJd7vFwW1JaoYQ7tpham03GJ1ca8/65rqn0RpaWpItOAd6ylW9wAw6luXYPJIyPFVOww== - ansi-colors@^4.1.1: version "4.1.3" resolved "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz" @@ -5446,7 +5612,7 @@ ansi-html@^0.0.9: resolved "https://registry.npmjs.org/ansi-html/-/ansi-html-0.0.9.tgz" integrity sha512-ozbS3LuenHVxNRh/wdnN16QapUHzauqSomAl1jwwJRRsGwFwtj644lIhxfWu0Fy0acCij2+AEgHvjscq3dlVXg== -ansi-regex@^5.0.0, ansi-regex@^5.0.1: +ansi-regex@^5.0.1: version "5.0.1" resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz" integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== @@ -5468,12 +5634,7 @@ ansi-styles@^5.0.0: resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz" integrity sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA== -ansi-styles@^6.0.0: - version "6.2.1" - resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz" - integrity sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug== - -ansi-styles@^6.1.0: +ansi-styles@^6.0.0, ansi-styles@^6.1.0: version "6.2.1" resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz" integrity sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug== @@ -5578,11 +5739,6 @@ aria-hidden@^1.1.1: dependencies: tslib "^2.0.0" -aria-query@^5.3.2: - version "5.3.2" - resolved "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz" - integrity sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw== - aria-query@5.1.3: version "5.1.3" resolved "https://registry.npmjs.org/aria-query/-/aria-query-5.1.3.tgz" @@ -5590,12 +5746,10 @@ aria-query@5.1.3: dependencies: deep-equal "^2.0.5" -aria-query@5.3.0: - version "5.3.0" - resolved "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz" - integrity sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A== - dependencies: - dequal "^2.0.3" +aria-query@^5.3.2: + version "5.3.2" + resolved "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz" + integrity sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw== array-buffer-byte-length@^1.0.0, array-buffer-byte-length@^1.0.1, array-buffer-byte-length@^1.0.2: version "1.0.2" @@ -5695,11 +5849,6 @@ arraybuffer.prototype.slice@^1.0.4: get-intrinsic "^1.2.6" is-array-buffer "^3.0.4" -asap@~2.0.6: - version "2.0.6" - resolved "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz" - integrity sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA== - asn1.js@^4.10.1: version "4.10.1" resolved "https://registry.npmjs.org/asn1.js/-/asn1.js-4.10.1.tgz" @@ -5751,7 +5900,7 @@ async-mutex@^0.2.6: dependencies: tslib "^2.0.0" -async@^2.6.3: +async@^2.6.3, async@~2.6.1: version "2.6.4" resolved "https://registry.npmjs.org/async/-/async-2.6.4.tgz" integrity sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA== @@ -5763,13 +5912,6 @@ async@^3.2.0, async@^3.2.3, async@^3.2.4, async@~3.2.0: resolved "https://registry.npmjs.org/async/-/async-3.2.5.tgz" integrity sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg== -async@~2.6.1: - version "2.6.4" - resolved "https://registry.npmjs.org/async/-/async-2.6.4.tgz" - integrity sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA== - dependencies: - lodash "^4.17.14" - asynckit@^0.4.0: version "0.4.0" resolved "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz" @@ -5899,13 +6041,6 @@ babel-plugin-polyfill-regenerator@^0.6.1: dependencies: "@babel/helper-define-polyfill-provider" "^0.6.2" -babel-plugin-syntax-hermes-parser@0.28.1: - version "0.28.1" - resolved "https://registry.npmjs.org/babel-plugin-syntax-hermes-parser/-/babel-plugin-syntax-hermes-parser-0.28.1.tgz" - integrity sha512-meT17DOuUElMNsL5LZN56d+KBp22hb0EfxWfuPUeoSi54e40v1W4C2V36P75FpsH9fVEfDKpw5Nnkahc8haSsQ== - dependencies: - hermes-parser "0.28.1" - babel-preset-current-node-syntax@^1.0.0: version "1.1.0" resolved "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.1.0.tgz" @@ -5978,7 +6113,7 @@ base64-arraybuffer@^1.0.2: resolved "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz" integrity sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ== -base64-js@^1.3.1, base64-js@^1.5.1: +base64-js@^1.3.1: version "1.5.1" resolved "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz" integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== @@ -6047,12 +6182,7 @@ bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.11.9: resolved "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz" integrity sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA== -bn.js@^5.0.0: - version "5.2.1" - resolved "https://registry.npmjs.org/bn.js/-/bn.js-5.2.1.tgz" - integrity sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ== - -bn.js@^5.2.1: +bn.js@^5.0.0, bn.js@^5.2.1: version "5.2.1" resolved "https://registry.npmjs.org/bn.js/-/bn.js-5.2.1.tgz" integrity sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ== @@ -6198,7 +6328,7 @@ browserify-zlib@^0.2.0: dependencies: pako "~1.0.5" -browserslist@^4.21.10, browserslist@^4.23.1, browserslist@^4.23.3, "browserslist@>= 4.21.0": +browserslist@^4.21.10, browserslist@^4.23.1, browserslist@^4.23.3: version "4.23.3" resolved "https://registry.npmjs.org/browserslist/-/browserslist-4.23.3.tgz" integrity sha512-btwCFJVjI4YWDNfau8RhZ+B1Q/VLoUITrm3RlP6y1tYGWIOa+InuYiRGXUBXo8nA1qKmHMyLB/iVQg5TT4eFoA== @@ -6311,25 +6441,6 @@ call-bound@^1.0.2, call-bound@^1.0.3: call-bind-apply-helpers "^1.0.1" get-intrinsic "^1.2.6" -caller-callsite@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/caller-callsite/-/caller-callsite-2.0.0.tgz" - integrity sha512-JuG3qI4QOftFsZyOn1qq87fq5grLIyk1JYd5lJmdA+fG7aQ9pA/i3JIJGcO3q0MrRcHlOt1U+ZeHW8Dq9axALQ== - dependencies: - callsites "^2.0.0" - -caller-path@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/caller-path/-/caller-path-2.0.0.tgz" - integrity sha512-MCL3sf6nCSXOwCTzvPKhN18TU7AHTvdtam8DAogxcrJ8Rjfbbg7Lgng64H9Iy+vUV6VGFClN/TyxBkAebLRR4A== - dependencies: - caller-callsite "^2.0.0" - -callsites@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/callsites/-/callsites-2.0.0.tgz" - integrity sha512-ksWePWBloaWPxJYQ8TL0JHvtci6G5QTKwQ95RcWAa/lzoAKuAOflGdAK92hpHXjkwb8zLxoLNUoNYZgVsaJzvQ== - callsites@^3.0.0: version "3.1.0" resolved "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz" @@ -6390,23 +6501,7 @@ chainsaw@~0.1.0: dependencies: traverse ">=0.3.0 <0.4" -chalk@^4.0.0, chalk@^4.0.2, chalk@^4.1.0, chalk@^4.1.2: - version "4.1.2" - resolved "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz" - integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== - dependencies: - ansi-styles "^4.1.0" - supports-color "^7.1.0" - -chalk@~3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz" - integrity sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg== - dependencies: - ansi-styles "^4.1.0" - supports-color "^7.1.0" - -chalk@3.0.0: +chalk@3.0.0, chalk@~3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz" integrity sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg== @@ -6419,6 +6514,14 @@ chalk@5.3.0: resolved "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz" integrity sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w== +chalk@^4.0.0, chalk@^4.0.2, chalk@^4.1.0, chalk@^4.1.2: + version "4.1.2" + resolved "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz" + integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + char-regex@^1.0.2: version "1.0.2" resolved "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz" @@ -6454,38 +6557,11 @@ chownr@^2.0.0: resolved "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz" integrity sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ== -chrome-launcher@^0.15.2: - version "0.15.2" - resolved "https://registry.npmjs.org/chrome-launcher/-/chrome-launcher-0.15.2.tgz" - integrity sha512-zdLEwNo3aUVzIhKhTtXfxhdvZhUghrnmkvcAq2NoDd+LeOHKf03H5jwZ8T/STsAlzyALkBVK552iaG1fGf1xVQ== - dependencies: - "@types/node" "*" - escape-string-regexp "^4.0.0" - is-wsl "^2.2.0" - lighthouse-logger "^1.0.0" - chrome-trace-event@^1.0.2: version "1.0.4" resolved "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz" integrity sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ== -chromium-edge-launcher@^0.2.0: - version "0.2.0" - resolved "https://registry.npmjs.org/chromium-edge-launcher/-/chromium-edge-launcher-0.2.0.tgz" - integrity sha512-JfJjUnq25y9yg4FABRRVPmBGWPZZi+AQXT4mxupb67766/0UlhG8PAZCz6xzEMXTbW3CsSoE8PcCWA49n35mKg== - dependencies: - "@types/node" "*" - escape-string-regexp "^4.0.0" - is-wsl "^2.2.0" - lighthouse-logger "^1.0.0" - mkdirp "^1.0.4" - rimraf "^3.0.2" - -ci-info@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz" - integrity sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ== - ci-info@^3.2.0: version "3.9.0" resolved "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz" @@ -6617,20 +6693,15 @@ clone@^1.0.2: resolved "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz" integrity sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg== -clsx@^1.2.1: - version "1.2.1" - resolved "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz" - integrity sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg== - -clsx@^2.0.0: +clsx@2.1.1, clsx@^2.0.0: version "2.1.1" resolved "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz" integrity sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA== -clsx@2.1.1: - version "2.1.1" - resolved "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz" - integrity sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA== +clsx@^1.2.1: + version "1.2.1" + resolved "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz" + integrity sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg== co@^4.6.0: version "4.6.0" @@ -6687,10 +6758,15 @@ combined-stream@^1.0.8: dependencies: delayed-stream "~1.0.0" -commander@^12.0.0: - version "12.1.0" - resolved "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz" - integrity sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA== +commander@11.0.0: + version "11.0.0" + resolved "https://registry.npmjs.org/commander/-/commander-11.0.0.tgz" + integrity sha512-9HMlXtt/BNoYr8ooyjjNRdIilOTkVJXB+GhxMTtOKwk0R4j4lS4NpjuqmRxroBfnfTSHQIHQB7wryHhXarNjmQ== + +commander@2.15.1: + version "2.15.1" + resolved "https://registry.npmjs.org/commander/-/commander-2.15.1.tgz" + integrity sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag== commander@^2.20.0: version "2.20.3" @@ -6712,16 +6788,6 @@ commander@^8.3.0: resolved "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz" integrity sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww== -commander@11.0.0: - version "11.0.0" - resolved "https://registry.npmjs.org/commander/-/commander-11.0.0.tgz" - integrity sha512-9HMlXtt/BNoYr8ooyjjNRdIilOTkVJXB+GhxMTtOKwk0R4j4lS4NpjuqmRxroBfnfTSHQIHQB7wryHhXarNjmQ== - -commander@2.15.1: - version "2.15.1" - resolved "https://registry.npmjs.org/commander/-/commander-2.15.1.tgz" - integrity sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag== - common-path-prefix@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/common-path-prefix/-/common-path-prefix-3.0.0.tgz" @@ -6782,16 +6848,6 @@ confbox@^0.1.7: resolved "https://registry.npmjs.org/confbox/-/confbox-0.1.7.tgz" integrity sha512-uJcB/FKZtBMCJpK8MQji6bJHgu1tixKPxRLeGkNzBoOZzpnZUJm0jm2/sBDWcuBx1dYgxV4JU+g5hmNxCyAmdA== -connect@^3.6.5: - version "3.7.0" - resolved "https://registry.npmjs.org/connect/-/connect-3.7.0.tgz" - integrity sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ== - dependencies: - debug "2.6.9" - finalhandler "1.1.2" - parseurl "~1.3.3" - utils-merge "1.0.1" - consola@^3.2.3: version "3.2.3" resolved "https://registry.npmjs.org/consola/-/consola-3.2.3.tgz" @@ -6844,7 +6900,7 @@ cookie-signature@1.0.6: resolved "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz" integrity sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ== -cookie@^0.6.0, cookie@0.6.0: +cookie@0.6.0, cookie@^0.6.0: version "0.6.0" resolved "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz" integrity sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw== @@ -6879,16 +6935,6 @@ core-util-is@~1.0.0: resolved "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz" integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ== -cosmiconfig@^5.0.5: - version "5.2.1" - resolved "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-5.2.1.tgz" - integrity sha512-H65gsXo1SKjf8zmrJ67eJk8aIRKV5ff2D4uKZIBZShbhGSpEmsQOPW/SKMKYhSTrqR7ufy6RP69rPogdaPh/kA== - dependencies: - import-fresh "^2.0.0" - is-directory "^0.3.1" - js-yaml "^3.13.1" - parse-json "^4.0.0" - cosmiconfig@^7.0.0, cosmiconfig@^7.0.1: version "7.1.0" resolved "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz" @@ -6900,17 +6946,7 @@ cosmiconfig@^7.0.0, cosmiconfig@^7.0.1: path-type "^4.0.0" yaml "^1.10.0" -cosmiconfig@^8.1.3: - version "8.3.6" - resolved "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz" - integrity sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA== - dependencies: - import-fresh "^3.3.0" - js-yaml "^4.1.0" - parse-json "^5.2.0" - path-type "^4.0.0" - -cosmiconfig@^8.3.5: +cosmiconfig@^8.1.3, cosmiconfig@^8.3.5: version "8.3.6" resolved "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz" integrity sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA== @@ -7092,6 +7128,21 @@ css-select@^4.1.3: domutils "^2.8.0" nth-check "^2.0.1" +css-styled@^1.0.6, css-styled@^1.0.8, css-styled@~1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/css-styled/-/css-styled-1.0.8.tgz#c9c05dc4abdef5571033090bfb8cfc5e19429974" + integrity sha512-tCpP7kLRI8dI95rCh3Syl7I+v7PP+2JYOzWkl0bUEoSbJM+u8ITbutjlQVf0NC2/g4ULROJPi16sfwDIO8/84g== + dependencies: + "@daybrush/utils" "^1.13.0" + +css-to-mat@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/css-to-mat/-/css-to-mat-1.1.1.tgz#0dd10dcf9ec17df15708c8ff07a74fbd0b9a3fe5" + integrity sha512-kvpxFYZb27jRd2vium35G7q5XZ2WJ9rWjDUMNT36M3Hc41qCrLXFM5iEKMGXcrPsKfXEN+8l/riB4QzwwwiEyQ== + dependencies: + "@daybrush/utils" "^1.13.0" + "@scena/matrix" "^1.0.0" + css-what@^6.0.1, css-what@^6.1.0: version "6.1.0" resolved "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz" @@ -7129,14 +7180,6 @@ culvert@^0.1.2: resolved "https://registry.npmjs.org/culvert/-/culvert-0.1.2.tgz" integrity sha512-yi1x3EAWKjQTreYWeSd98431AV+IEE0qoDyOoaHJ7KJ21gv6HtBXHVLX74opVSGqcR8/AbjJBHAHpcOy2bj5Gg== -d@^1.0.1, d@^1.0.2, d@1: - version "1.0.2" - resolved "https://registry.npmjs.org/d/-/d-1.0.2.tgz" - integrity sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw== - dependencies: - es5-ext "^0.10.64" - type "^2.7.2" - "d3-dispatch@1 - 3": version "3.0.1" resolved "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz" @@ -7161,6 +7204,14 @@ d3-force@^3.0.0: resolved "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz" integrity sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA== +d@1, d@^1.0.1, d@^1.0.2: + version "1.0.2" + resolved "https://registry.npmjs.org/d/-/d-1.0.2.tgz" + integrity sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw== + dependencies: + es5-ext "^0.10.64" + type "^2.7.2" + damerau-levenshtein@^1.0.8: version "1.0.8" resolved "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz" @@ -7224,76 +7275,41 @@ dayjs@~1.8.24: resolved "https://registry.npmjs.org/dayjs/-/dayjs-1.8.36.tgz" integrity sha512-3VmRXEtw7RZKAf+4Tv1Ym9AGeo8r8+CjDi26x+7SYQil1UqtqdaokhzoEJohqlzt0m5kacJSDhJQkG/LWhpRBw== -debug@^2.1.3: - version "2.6.9" - resolved "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz" - integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== - dependencies: - ms "2.0.0" - -debug@^2.2.0: +debug@2.6.9, debug@^2.1.3, debug@^2.2.0, debug@^2.6.9: version "2.6.9" resolved "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz" integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== dependencies: ms "2.0.0" -debug@^2.6.9: - version "2.6.9" - resolved "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz" - integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== +debug@4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4, debug@^4.3.7: + version "4.4.0" + resolved "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz" + integrity sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA== dependencies: - ms "2.0.0" + ms "^2.1.3" -debug@^3.2.6: - version "3.2.7" - resolved "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz" - integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ== +debug@4.3.4: + version "4.3.4" + resolved "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz" + integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== dependencies: - ms "^2.1.1" + ms "2.1.2" -debug@^3.2.7: +debug@^3.2.6, debug@^3.2.7: version "3.2.7" resolved "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz" integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ== dependencies: ms "^2.1.1" -debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4, debug@^4.3.7, debug@^4.4.0, debug@4: - version "4.4.0" - resolved "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz" - integrity sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA== - dependencies: - ms "^2.1.3" - -debug@~4.3.1: - version "4.3.6" - resolved "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz" - integrity sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg== - dependencies: - ms "2.1.2" - -debug@~4.3.2: +debug@~4.3.1, debug@~4.3.2: version "4.3.6" resolved "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz" integrity sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg== dependencies: ms "2.1.2" -debug@2.6.9: - version "2.6.9" - resolved "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz" - integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== - dependencies: - ms "2.0.0" - -debug@4.3.4: - version "4.3.4" - resolved "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz" - integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== - dependencies: - ms "2.1.2" - decamelize@^1.2.0: version "1.2.0" resolved "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz" @@ -7451,7 +7467,7 @@ depd@2.0.0: resolved "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz" integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== -dequal@^2.0.2, dequal@^2.0.3: +dequal@^2.0.2: version "2.0.3" resolved "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz" integrity sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA== @@ -7474,7 +7490,7 @@ destroy@1.2.0: resolved "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz" integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg== -detect-browser@^5.2.0, detect-browser@5.3.0: +detect-browser@5.3.0, detect-browser@^5.2.0: version "5.3.0" resolved "https://registry.npmjs.org/detect-browser/-/detect-browser-5.3.0.tgz" integrity sha512-53rsFbGdwMwlF7qvCt0ypLM5V5/Mbl0szB7GPN8y9NCcbknYOeVVXdrXEq+90IwAfrrzt6Hd+u2E2ntakICU8w== @@ -7682,7 +7698,7 @@ eastasianwidth@^0.2.0: resolved "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz" integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA== -eciesjs@^0.3.15, eciesjs@^0.3.16: +eciesjs@^0.3.15: version "0.3.19" resolved "https://registry.npmjs.org/eciesjs/-/eciesjs-0.3.19.tgz" integrity sha512-b+PkRDZ3ym7HEcnbxc22CMVCpgsnr8+gGgST3U5PtgeX1luvINgfXW7efOyUtmn/jFtA/lg5ywBi/Uazf4oeaA== @@ -7708,10 +7724,10 @@ electron-to-chromium@^1.5.4: resolved "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.10.tgz" integrity sha512-C3RDERDjrNW262GCRvpoer3a0Ksd66CtgDLxMHhzShQ8fhL4kwnpVXsJPAKg9xJjIROXUbLBrvtOzVAjALMIWA== -elliptic@^6.5.3, elliptic@^6.5.4, elliptic@^6.5.5: - version "6.5.7" - resolved "https://registry.npmjs.org/elliptic/-/elliptic-6.5.7.tgz" - integrity sha512-ESVCtTwiA+XhY3wyh24QqRGBoP3rEdDUl3EDUUo9tft074fi19IrdpH7hLCMMP3CIj7jb3W96rn8lt/BqIlt5Q== +elliptic@6.5.4: + version "6.5.4" + resolved "https://registry.npmjs.org/elliptic/-/elliptic-6.5.4.tgz" + integrity sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ== dependencies: bn.js "^4.11.9" brorand "^1.1.0" @@ -7721,10 +7737,10 @@ elliptic@^6.5.3, elliptic@^6.5.4, elliptic@^6.5.5: minimalistic-assert "^1.0.1" minimalistic-crypto-utils "^1.0.1" -elliptic@6.5.4: - version "6.5.4" - resolved "https://registry.npmjs.org/elliptic/-/elliptic-6.5.4.tgz" - integrity sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ== +elliptic@^6.5.3, elliptic@^6.5.4, elliptic@^6.5.5: + version "6.5.7" + resolved "https://registry.npmjs.org/elliptic/-/elliptic-6.5.7.tgz" + integrity sha512-ESVCtTwiA+XhY3wyh24QqRGBoP3rEdDUl3EDUUo9tft074fi19IrdpH7hLCMMP3CIj7jb3W96rn8lt/BqIlt5Q== dependencies: bn.js "^4.11.9" brorand "^1.1.0" @@ -7764,11 +7780,6 @@ encodeurl@~1.0.2: resolved "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz" integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w== -encodeurl@~2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz" - integrity sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg== - end-of-stream@^1.0.0, end-of-stream@^1.1.0, end-of-stream@^1.4.0, end-of-stream@^1.4.1: version "1.4.4" resolved "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz" @@ -7801,15 +7812,7 @@ engine.io-parser@~5.2.1: resolved "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz" integrity sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q== -enhanced-resolve@^5.15.0: - version "5.18.0" - resolved "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.0.tgz" - integrity sha512-0/r0MySGYG8YqlayBZ6MuCfECmHFdJ5qyPh8s8wa5Hnm6SaFLSK1VYCbj+NKp090Nm1caZhD+QTnmxO7esYGyQ== - dependencies: - graceful-fs "^4.2.4" - tapable "^2.2.0" - -enhanced-resolve@^5.17.1: +enhanced-resolve@^5.15.0, enhanced-resolve@^5.17.1: version "5.18.0" resolved "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.0.tgz" integrity sha512-0/r0MySGYG8YqlayBZ6MuCfECmHFdJ5qyPh8s8wa5Hnm6SaFLSK1VYCbj+NKp090Nm1caZhD+QTnmxO7esYGyQ== @@ -7825,7 +7828,7 @@ enhanced-resolve@^5.7.0: graceful-fs "^4.2.4" tapable "^2.2.0" -"enquirer@>= 2.3.0 < 3", enquirer@2.3.6: +enquirer@2.3.6: version "2.3.6" resolved "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz" integrity sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg== @@ -8042,7 +8045,7 @@ esbuild-register@^3.5.0: dependencies: debug "^4.3.4" -esbuild@^0.18.0, esbuild@>=0.10.0, "esbuild@>=0.12 <1": +esbuild@^0.18.0: version "0.18.20" resolved "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz" integrity sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA== @@ -8080,7 +8083,7 @@ escape-html@~1.0.3: resolved "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz" integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow== -escape-string-regexp@^2.0.0, escape-string-regexp@2.0.0: +escape-string-regexp@2.0.0, escape-string-regexp@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz" integrity sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w== @@ -8155,7 +8158,7 @@ eslint-plugin-i18next@^6.1.1: lodash "^4.17.21" requireindex "~1.1.0" -eslint-plugin-import@*, eslint-plugin-import@^2.31.0: +eslint-plugin-import@^2.31.0: version "2.31.0" resolved "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.31.0.tgz" integrity sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A== @@ -8256,14 +8259,6 @@ eslint-plugin-unused-imports@^4.1.4: resolved "https://registry.npmjs.org/eslint-plugin-unused-imports/-/eslint-plugin-unused-imports-4.1.4.tgz" integrity sha512-YptD6IzQjDardkl0POxnnRBhU1OEePMV0nd6siHaRBbd+lyh6NAhFEobiznKU7kTsSsDeSD62Pe7kAM1b7dAZQ== -eslint-scope@^8.2.0: - version "8.2.0" - resolved "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.2.0.tgz" - integrity sha512-PHlWUfG6lvPc3yvP5A4PNyBL1W8fkDUccmI21JUu/+GKZBoH/W5u6usENXUrWFRsyoW5ACUjFGgAFQp5gUlb/A== - dependencies: - esrecurse "^4.3.0" - estraverse "^5.2.0" - eslint-scope@5.1.1: version "5.1.1" resolved "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz" @@ -8272,6 +8267,14 @@ eslint-scope@5.1.1: esrecurse "^4.3.0" estraverse "^4.1.1" +eslint-scope@^8.2.0: + version "8.2.0" + resolved "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.2.0.tgz" + integrity sha512-PHlWUfG6lvPc3yvP5A4PNyBL1W8fkDUccmI21JUu/+GKZBoH/W5u6usENXUrWFRsyoW5ACUjFGgAFQp5gUlb/A== + dependencies: + esrecurse "^4.3.0" + estraverse "^5.2.0" + eslint-visitor-keys@^3.4.3: version "3.4.3" resolved "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz" @@ -8282,7 +8285,7 @@ eslint-visitor-keys@^4.2.0: resolved "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz" integrity sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw== -eslint@*, "eslint@^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9", "eslint@^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9", "eslint@^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7", "eslint@^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0", "eslint@^6.0.0 || ^7.0.0 || >=8.0.0", "eslint@^7.0.0 || ^8.0.0 || ^9.0.0", "eslint@^7.23.0 || ^8.0.0 || ^9.0.0", "eslint@^8.57.0 || ^9.0.0", "eslint@^9.0.0 || ^8.0.0", eslint@^9.18.0, eslint@>=5.0.0, eslint@>=8: +eslint@^9.18.0: version "9.18.0" resolved "https://registry.npmjs.org/eslint/-/eslint-9.18.0.tgz" integrity sha512-+waTfRWQlSbpt3KWE+CjrPPYnbq9kfZIYUqapc0uBXyjTp8aYXZDsUH16m39Ryq3NjAVP4tjuF7KaukeqoCoaA== @@ -8440,6 +8443,11 @@ event-target-shim@^5.0.0: resolved "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz" integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ== +eventemitter2@5.0.1, eventemitter2@~5.0.1: + version "5.0.1" + resolved "https://registry.npmjs.org/eventemitter2/-/eventemitter2-5.0.1.tgz" + integrity sha512-5EM1GHXycJBS6mauYAbVKT1cVs7POKWb2NXD4Vyt8dDqeZa7LaDK1/sjtL+Zb0lzTpSNil4596Dyu97hz37QLg== + eventemitter2@^6.3.1, eventemitter2@^6.4.7: version "6.4.9" resolved "https://registry.npmjs.org/eventemitter2/-/eventemitter2-6.4.9.tgz" @@ -8450,22 +8458,12 @@ eventemitter2@~0.4.14: resolved "https://registry.npmjs.org/eventemitter2/-/eventemitter2-0.4.14.tgz" integrity sha512-K7J4xq5xAD5jHsGM5ReWXRTFa3JRGofHiMcVgQ8PRwgWxzjHpMWCIzsmyf60+mh8KLsqYPcjUMa0AC4hd6lPyQ== -eventemitter2@~5.0.1: - version "5.0.1" - resolved "https://registry.npmjs.org/eventemitter2/-/eventemitter2-5.0.1.tgz" - integrity sha512-5EM1GHXycJBS6mauYAbVKT1cVs7POKWb2NXD4Vyt8dDqeZa7LaDK1/sjtL+Zb0lzTpSNil4596Dyu97hz37QLg== - -eventemitter2@5.0.1: - version "5.0.1" - resolved "https://registry.npmjs.org/eventemitter2/-/eventemitter2-5.0.1.tgz" - integrity sha512-5EM1GHXycJBS6mauYAbVKT1cVs7POKWb2NXD4Vyt8dDqeZa7LaDK1/sjtL+Zb0lzTpSNil4596Dyu97hz37QLg== - -eventemitter3@^5.0.1, eventemitter3@5.0.1: +eventemitter3@5.0.1, eventemitter3@^5.0.1: version "5.0.1" resolved "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz" integrity sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA== -events@^3.2.0, events@^3.3.0, events@3.3.0: +events@3.3.0, events@^3.2.0, events@^3.3.0: version "3.3.0" resolved "https://registry.npmjs.org/events/-/events-3.3.0.tgz" integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== @@ -8493,6 +8491,21 @@ exceljs@^4.4.0: unzipper "^0.10.11" uuid "^8.3.0" +execa@7.2.0: + version "7.2.0" + resolved "https://registry.npmjs.org/execa/-/execa-7.2.0.tgz" + integrity sha512-UduyVP7TLB5IcAQl+OzLyLcS/l32W/GLg+AhHJ+ow40FOk2U3SAllPwR44v4vmdFwIWqpdwxxpQbF1n5ta9seA== + dependencies: + cross-spawn "^7.0.3" + get-stream "^6.0.1" + human-signals "^4.3.0" + is-stream "^3.0.0" + merge-stream "^2.0.0" + npm-run-path "^5.1.0" + onetime "^6.0.0" + signal-exit "^3.0.7" + strip-final-newline "^3.0.0" + execa@^5.0.0, execa@^5.1.1: version "5.1.1" resolved "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz" @@ -8523,21 +8536,6 @@ execa@^8.0.1: signal-exit "^4.1.0" strip-final-newline "^3.0.0" -execa@7.2.0: - version "7.2.0" - resolved "https://registry.npmjs.org/execa/-/execa-7.2.0.tgz" - integrity sha512-UduyVP7TLB5IcAQl+OzLyLcS/l32W/GLg+AhHJ+ow40FOk2U3SAllPwR44v4vmdFwIWqpdwxxpQbF1n5ta9seA== - dependencies: - cross-spawn "^7.0.3" - get-stream "^6.0.1" - human-signals "^4.3.0" - is-stream "^3.0.0" - merge-stream "^2.0.0" - npm-run-path "^5.1.0" - onetime "^6.0.0" - signal-exit "^3.0.7" - strip-final-newline "^3.0.0" - exit@^0.1.2: version "0.1.2" resolved "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz" @@ -8559,11 +8557,6 @@ expect@^29.0.0, expect@^29.7.0: jest-message-util "^29.7.0" jest-util "^29.7.0" -exponential-backoff@^3.1.1: - version "3.1.2" - resolved "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.2.tgz" - integrity sha512-8QxYTVXUkuy7fIIoitQkPwGonB8F3Zj8eEO8Sqg9Zv/bkI7RJAzowee4gr81Hak/dUTpA2Z7VfQgoijjPNlUZA== - express@^4.17.3: version "4.19.2" resolved "https://registry.npmjs.org/express/-/express-4.19.2.tgz" @@ -8656,27 +8649,27 @@ fast-fifo@^1.2.0, fast-fifo@^1.3.2: resolved "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz" integrity sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ== -fast-glob@^3.2.9, fast-glob@^3.3.0, fast-glob@^3.3.2: - version "3.3.3" - resolved "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz" - integrity sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg== +fast-glob@3.3.1: + version "3.3.1" + resolved "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz" + integrity sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg== dependencies: "@nodelib/fs.stat" "^2.0.2" "@nodelib/fs.walk" "^1.2.3" glob-parent "^5.1.2" merge2 "^1.3.0" - micromatch "^4.0.8" + micromatch "^4.0.4" -fast-glob@3.3.1: - version "3.3.1" - resolved "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz" - integrity sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg== +fast-glob@^3.2.9, fast-glob@^3.3.0, fast-glob@^3.3.2: + version "3.3.3" + resolved "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz" + integrity sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg== dependencies: "@nodelib/fs.stat" "^2.0.2" "@nodelib/fs.walk" "^1.2.3" glob-parent "^5.1.2" merge2 "^1.3.0" - micromatch "^4.0.4" + micromatch "^4.0.8" fast-json-parse@^1.0.3: version "1.0.3" @@ -8727,7 +8720,7 @@ fb-watchman@^2.0.0: dependencies: bser "2.1.1" -fclone@~1.0.11, fclone@1.0.11: +fclone@1.0.11, fclone@~1.0.11: version "1.0.11" resolved "https://registry.npmjs.org/fclone/-/fclone-1.0.11.tgz" integrity sha512-GDqVQezKzRABdeqflsgMr7ktzgF9CyS+p2oe0jJqUY6izSSbhPIQJDpoU4PtGcD7VPM9xh/dVrTu6z1nwgmEGw== @@ -8783,19 +8776,6 @@ filter-obj@^2.0.2: resolved "https://registry.npmjs.org/filter-obj/-/filter-obj-2.0.2.tgz" integrity sha512-lO3ttPjHZRfjMcxWKb1j1eDhTFsu4meeR3lnMcnBFhk6RuLhvEiuALu2TlfL310ph4lCYYwgF/ElIjdP739tdg== -finalhandler@1.1.2: - version "1.1.2" - resolved "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz" - integrity sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA== - dependencies: - debug "2.6.9" - encodeurl "~1.0.2" - escape-html "~1.0.3" - on-finished "~2.3.0" - parseurl "~1.3.3" - statuses "~1.5.0" - unpipe "~1.0.0" - finalhandler@1.2.0: version "1.2.0" resolved "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz" @@ -8842,15 +8822,7 @@ find-up@^3.0.0: dependencies: locate-path "^3.0.0" -find-up@^4.0.0: - version "4.1.0" - resolved "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz" - integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw== - dependencies: - locate-path "^5.0.0" - path-exists "^4.0.0" - -find-up@^4.1.0: +find-up@^4.0.0, find-up@^4.1.0: version "4.1.0" resolved "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz" integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw== @@ -8896,11 +8868,6 @@ flatted@^3.2.9: resolved "https://registry.npmjs.org/flatted/-/flatted-3.3.2.tgz" integrity sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA== -flow-enums-runtime@^0.0.6: - version "0.0.6" - resolved "https://registry.npmjs.org/flow-enums-runtime/-/flow-enums-runtime-0.0.6.tgz" - integrity sha512-3PYnM29RFXwvAN6Pc/scUfkI7RwhQ/xqyLUyPNlXUp9S40zI8nup9tUSrTLSVnWGBN38FNiGWbwZOB6uR4OGdw== - flow-parser@0.*: version "0.243.0" resolved "https://registry.npmjs.org/flow-parser/-/flow-parser-0.243.0.tgz" @@ -8963,6 +8930,11 @@ fraction.js@^4.3.7: resolved "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz" integrity sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew== +framework-utils@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/framework-utils/-/framework-utils-1.1.0.tgz#a3b528bce838dfd623148847dc92371b09d0da2d" + integrity sha512-KAfqli5PwpFJ8o3psRNs8svpMGyCSAe8nmGcjQ0zZBWN2H6dZDnq+ABp3N3hdUmFeMrLtjOCTXD4yplUJIWceg== + fresh@0.5.2: version "0.5.2" resolved "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz" @@ -8973,6 +8945,15 @@ fs-constants@^1.0.0: resolved "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz" integrity sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow== +fs-extra@11.1.1: + version "11.1.1" + resolved "https://registry.npmjs.org/fs-extra/-/fs-extra-11.1.1.tgz" + integrity sha512-MGIE4HOvQCeUCzmlHs0vXpih4ysz4wg9qiSAu6cd42lVwPbTM1TjV7RusoyQqMmk/95gdQZX72u+YW+c3eEpFQ== + dependencies: + graceful-fs "^4.2.0" + jsonfile "^6.0.1" + universalify "^2.0.0" + fs-extra@^10.0.0: version "10.1.0" resolved "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz" @@ -8991,15 +8972,6 @@ fs-extra@^11.1.0, fs-extra@^11.2.0: jsonfile "^6.0.1" universalify "^2.0.0" -fs-extra@11.1.1: - version "11.1.1" - resolved "https://registry.npmjs.org/fs-extra/-/fs-extra-11.1.1.tgz" - integrity sha512-MGIE4HOvQCeUCzmlHs0vXpih4ysz4wg9qiSAu6cd42lVwPbTM1TjV7RusoyQqMmk/95gdQZX72u+YW+c3eEpFQ== - dependencies: - graceful-fs "^4.2.0" - jsonfile "^6.0.1" - universalify "^2.0.0" - fs-minipass@^2.0.0: version "2.1.0" resolved "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz" @@ -9079,6 +9051,14 @@ gensync@^1.0.0-beta.2: resolved "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz" integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg== +gesto@^1.19.3, gesto@^1.19.4: + version "1.19.4" + resolved "https://registry.yarnpkg.com/gesto/-/gesto-1.19.4.tgz#14921ca89e4e70c14307c4d942df04f59eb00749" + integrity sha512-hfr/0dWwh0Bnbb88s3QVJd1ZRJeOWcgHPPwmiH6NnafDYvhTsxg+SLYu+q/oPNh9JS3V+nlr6fNs8kvPAtcRDQ== + dependencies: + "@daybrush/utils" "^1.13.0" + "@scena/event-emitter" "^1.0.2" + get-caller-file@^2.0.1, get-caller-file@^2.0.5: version "2.0.5" resolved "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz" @@ -9239,43 +9219,7 @@ glob@^10.0.0, glob@^10.3.10: package-json-from-dist "^1.0.0" path-scurry "^1.11.1" -glob@^7.1.1: - version "7.2.3" - resolved "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz" - integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== - dependencies: - fs.realpath "^1.0.0" - inflight "^1.0.4" - inherits "2" - minimatch "^3.1.1" - once "^1.3.0" - path-is-absolute "^1.0.0" - -glob@^7.1.3: - version "7.2.3" - resolved "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz" - integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== - dependencies: - fs.realpath "^1.0.0" - inflight "^1.0.4" - inherits "2" - minimatch "^3.1.1" - once "^1.3.0" - path-is-absolute "^1.0.0" - -glob@^7.1.4: - version "7.2.3" - resolved "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz" - integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== - dependencies: - fs.realpath "^1.0.0" - inflight "^1.0.4" - inherits "2" - minimatch "^3.1.1" - once "^1.3.0" - path-is-absolute "^1.0.0" - -glob@^7.2.3: +glob@^7.1.3, glob@^7.1.4, glob@^7.2.3: version "7.2.3" resolved "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz" integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== @@ -9435,7 +9379,7 @@ hash-base@~3.0: inherits "^2.0.1" safe-buffer "^5.0.1" -hash.js@^1.0.0, hash.js@^1.0.3, hash.js@1.1.7: +hash.js@1.1.7, hash.js@^1.0.0, hash.js@^1.0.3: version "1.1.7" resolved "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz" integrity sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA== @@ -9455,30 +9399,6 @@ he@^1.2.0: resolved "https://registry.npmjs.org/he/-/he-1.2.0.tgz" integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== -hermes-estree@0.28.1: - version "0.28.1" - resolved "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.28.1.tgz" - integrity sha512-w3nxl/RGM7LBae0v8LH2o36+8VqwOZGv9rX1wyoWT6YaKZLqpJZ0YQ5P0LVr3tuRpf7vCx0iIG4i/VmBJejxTQ== - -hermes-estree@0.29.1: - version "0.29.1" - resolved "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.29.1.tgz" - integrity sha512-jl+x31n4/w+wEqm0I2r4CMimukLbLQEYpisys5oCre611CI5fc9TxhqkBBCJ1edDG4Kza0f7CgNz8xVMLZQOmQ== - -hermes-parser@0.28.1: - version "0.28.1" - resolved "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.28.1.tgz" - integrity sha512-nf8o+hE8g7UJWParnccljHumE9Vlq8F7MqIdeahl+4x0tvCUJYRrT0L7h0MMg/X9YJmkNwsfbaNNrzPtFXOscg== - dependencies: - hermes-estree "0.28.1" - -hermes-parser@0.29.1: - version "0.29.1" - resolved "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.29.1.tgz" - integrity sha512-xBHWmUtRC5e/UL0tI7Ivt2riA/YBq9+SiYFU7C1oBa/j2jYGlIF9043oak1F47ihuDIxQ5nbsKueYJDRY02UgA== - dependencies: - hermes-estree "0.29.1" - hey-listen@^1.0.8: version "1.0.8" resolved "https://registry.npmjs.org/hey-listen/-/hey-listen-1.0.8.tgz" @@ -9658,14 +9578,14 @@ i18next-browser-languagedetector@7.1.0: dependencies: "@babel/runtime" "^7.19.4" -"i18next@>= 23.2.3", i18next@22.5.1: +i18next@22.5.1: version "22.5.1" resolved "https://registry.npmjs.org/i18next/-/i18next-22.5.1.tgz" integrity sha512-8TGPgM3pAD+VRsMtUMNknRz3kzqwp/gPALrWMsDnmC1mKqJwpWyooQRLMcbTwq8z8YwSmuj+ZYvc+xCuEpkssA== dependencies: "@babel/runtime" "^7.20.6" -iconv-lite@^0.4.4, iconv-lite@0.4.24: +iconv-lite@0.4.24, iconv-lite@^0.4.4: version "0.4.24" resolved "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz" integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== @@ -9686,12 +9606,7 @@ icss-utils@^4.0.0, icss-utils@^4.1.1: dependencies: postcss "^7.0.14" -icss-utils@^5.0.0: - version "5.1.0" - resolved "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz" - integrity sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA== - -icss-utils@^5.1.0: +icss-utils@^5.0.0, icss-utils@^5.1.0: version "5.1.0" resolved "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz" integrity sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA== @@ -9711,7 +9626,7 @@ ignore@^5.2.0, ignore@^5.3.1: resolved "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz" integrity sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g== -image-size@^1.0.0, image-size@^1.0.2: +image-size@^1.0.0: version "1.1.1" resolved "https://registry.npmjs.org/image-size/-/image-size-1.1.1.tgz" integrity sha512-541xKlUw6jr/6gGuk92F+mYM5zaFAc5ahphvkqvNe2bQ6gVBkd6bfrmVJ2t4KDAfikAYZyIqTnktX3i6/aQDrQ== @@ -9723,14 +9638,6 @@ immediate@~3.0.5: resolved "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz" integrity sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ== -import-fresh@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/import-fresh/-/import-fresh-2.0.0.tgz" - integrity sha512-eZ5H8rcgYazHbKC3PG4ClHNykCSxtAhxSSEM+2mb+7evD2CKF5V7c0dNum7AdpDh0ZdICwZY9sRSn8f+KH96sg== - dependencies: - caller-path "^2.0.0" - resolve-from "^3.0.0" - import-fresh@^3.2.1, import-fresh@^3.3.0: version "3.3.0" resolved "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz" @@ -9765,7 +9672,7 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.0, inherits@~2.0.3, inherits@~2.0.4, inherits@2, inherits@2.0.4: +inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.0, inherits@~2.0.3, inherits@~2.0.4: version "2.0.4" resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== @@ -9784,7 +9691,7 @@ internal-slot@^1.1.0: hasown "^2.0.2" side-channel "^1.1.0" -invariant@^2.2.4, invariant@2.2.4: +invariant@2.2.4, invariant@^2.2.4: version "2.2.4" resolved "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz" integrity sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA== @@ -9914,11 +9821,6 @@ is-deflate@^1.0.0: resolved "https://registry.npmjs.org/is-deflate/-/is-deflate-1.0.0.tgz" integrity sha512-YDoFpuZWu1VRXlsnlYMzKyVRITXj7Ej/V9gXQ2/pAe7X1J7M/RNOqaIYi6qUn+B7nGyB9pDXrv02dsB58d2ZAQ== -is-directory@^0.3.1: - version "0.3.1" - resolved "https://registry.npmjs.org/is-directory/-/is-directory-0.3.1.tgz" - integrity sha512-yVChGzahRFvbkscn2MlwGismPO12i9+znNruC5gVEntG3qu0xQMzsGg/JFbrsqDOHtHFPci+V5aP5T9I+yeKqw== - is-docker@^2.0.0, is-docker@^2.1.1: version "2.2.1" resolved "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz" @@ -10033,6 +9935,11 @@ is-path-inside@^3.0.2: resolved "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz" integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ== +is-plain-object@5.0.0: + version "5.0.0" + resolved "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz" + integrity sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q== + is-plain-object@^2.0.4: version "2.0.4" resolved "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz" @@ -10040,11 +9947,6 @@ is-plain-object@^2.0.4: dependencies: isobject "^3.0.1" -is-plain-object@5.0.0: - version "5.0.0" - resolved "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz" - integrity sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q== - is-potential-custom-element-name@^1.0.1: version "1.0.1" resolved "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz" @@ -10136,7 +10038,7 @@ is-weakset@^2.0.3: call-bound "^1.0.3" get-intrinsic "^1.2.6" -is-wsl@^2.1.1, is-wsl@^2.2.0: +is-wsl@^2.2.0: version "2.2.0" resolved "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz" integrity sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww== @@ -10260,9 +10162,9 @@ iterator.prototype@^1.1.4: has-symbols "^1.1.0" set-function-name "^2.0.2" -jackspeak@^3.1.2: +jackspeak@3.1.2, jackspeak@^3.1.2: version "3.1.2" - resolved "https://registry.npmjs.org/jackspeak/-/jackspeak-3.1.2.tgz" + resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-3.1.2.tgz#eada67ea949c6b71de50f1b09c92a961897b90ab" integrity sha512-kWmLKn2tRtfYMF/BakihVVRzBKOxz4gJMiL2Rj91WnAB5TPZumSH99R/Yf1qE1u4uRimvCSJfm6hnxohXeEXjQ== dependencies: "@isaacs/cliui" "^8.0.2" @@ -10510,7 +10412,7 @@ jest-resolve-dependencies@^29.7.0: jest-regex-util "^29.6.3" jest-snapshot "^29.7.0" -jest-resolve@*, jest-resolve@^29.7.0: +jest-resolve@^29.7.0: version "29.7.0" resolved "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz" integrity sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA== @@ -10663,7 +10565,7 @@ jest-worker@^29.7.0: merge-stream "^2.0.0" supports-color "^8.0.0" -jest@*, jest@^29.7.0: +jest@^29.7.0: version "29.7.0" resolved "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz" integrity sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw== @@ -10673,7 +10575,7 @@ jest@*, jest@^29.7.0: import-local "^3.0.2" jest-cli "^29.7.0" -jiti@*, jiti@^1.20.0, jiti@^1.21.0: +jiti@^1.20.0, jiti@^1.21.0: version "1.21.6" resolved "https://registry.npmjs.org/jiti/-/jiti-1.21.6.tgz" integrity sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w== @@ -10718,11 +10620,6 @@ jsbn@1.1.0: resolved "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz" integrity sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A== -jsc-safe-url@^0.2.2: - version "0.2.4" - resolved "https://registry.npmjs.org/jsc-safe-url/-/jsc-safe-url-0.2.4.tgz" - integrity sha512-0wM3YBWtYePOjfyXQH5MWQ8H7sdk5EXSwZvmSLKk2RboVQ2Bu239jycHDz5J/8Blf3K0Qnoy2b6xD+z10MFB+Q== - jscodeshift@^0.15.1: version "0.15.2" resolved "https://registry.npmjs.org/jscodeshift/-/jscodeshift-0.15.2.tgz" @@ -10796,11 +10693,6 @@ json-buffer@3.0.1: resolved "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz" integrity sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ== -json-parse-better-errors@^1.0.1: - version "1.0.2" - resolved "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz" - integrity sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw== - json-parse-even-better-errors@^2.3.0, json-parse-even-better-errors@^2.3.1: version "2.3.1" resolved "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz" @@ -10839,14 +10731,7 @@ json-stringify-safe@^5.0.1: resolved "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz" integrity sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA== -json5@^1.0.1: - version "1.0.2" - resolved "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz" - integrity sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA== - dependencies: - minimist "^1.2.0" - -json5@^1.0.2: +json5@^1.0.1, json5@^1.0.2: version "1.0.2" resolved "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz" integrity sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA== @@ -10908,6 +10793,21 @@ keccak@^3.0.3: node-gyp-build "^4.2.0" readable-stream "^3.6.0" +keycode@^2.2.0: + version "2.2.1" + resolved "https://registry.yarnpkg.com/keycode/-/keycode-2.2.1.tgz#09c23b2be0611d26117ea2501c2c391a01f39eff" + integrity sha512-Rdgz9Hl9Iv4QKi8b0OlCRQEzp4AgVxyCtz5S/+VIHezDmrDhkp2N2TqBWOLz0/gbeREXOOiI9/4b8BY9uw2vFg== + +keycon@^1.2.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/keycon/-/keycon-1.4.0.tgz#bf2a633f3c3b659ea564045938cff33e584cebd5" + integrity sha512-p1NAIxiRMH3jYfTeXRs2uWbVJ1WpEjpi8ktzUyBJsX7/wn2qu2VRXktneBLNtKNxJmlUYxRi9gOJt1DuthXR7A== + dependencies: + "@cfcs/core" "^0.0.6" + "@daybrush/utils" "^1.7.1" + "@scena/event-emitter" "^1.0.2" + keycode "^2.2.0" + keyv@^4.5.3, keyv@^4.5.4: version "4.5.4" resolved "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz" @@ -10988,15 +10888,7 @@ lie@~3.3.0: dependencies: immediate "~3.0.5" -lighthouse-logger@^1.0.0: - version "1.4.2" - resolved "https://registry.npmjs.org/lighthouse-logger/-/lighthouse-logger-1.4.2.tgz" - integrity sha512-gPWxznF6TKmUHrOQjlVo2UbaL2EJ71mb2CCeRs/2qBpi4L/g4LUVc9+3lKQ6DTUZwJswfM7ainGrLO1+fOqa2g== - dependencies: - debug "^2.6.9" - marky "^1.2.2" - -lilconfig@^2.1.0, lilconfig@2.1.0: +lilconfig@2.1.0, lilconfig@^2.1.0: version "2.1.0" resolved "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz" integrity sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ== @@ -11190,7 +11082,7 @@ lodash.isboolean@^3.0.3: resolved "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz" integrity sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg== -lodash.isequal@^4.5.0, lodash.isequal@4.5.0: +lodash.isequal@4.5.0, lodash.isequal@^4.5.0: version "4.5.0" resolved "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz" integrity sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ== @@ -11215,16 +11107,11 @@ lodash.isundefined@^3.0.1: resolved "https://registry.npmjs.org/lodash.isundefined/-/lodash.isundefined-3.0.1.tgz" integrity sha512-MXB1is3s899/cD8jheYYE2V9qTHwKvt+npCwpD+1Sxm3Q3cECXCiYHjeHWXNwr6Q0SOBPrYUDxendrO6goVTEA== -lodash.merge@^4.6.2, lodash.merge@4.6.2: +lodash.merge@4.6.2, lodash.merge@^4.6.2: version "4.6.2" resolved "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz" integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== -lodash.throttle@^4.1.1: - version "4.1.1" - resolved "https://registry.npmjs.org/lodash.throttle/-/lodash.throttle-4.1.1.tgz" - integrity sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ== - lodash.union@^4.6.0: version "4.6.0" resolved "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz" @@ -11360,11 +11247,6 @@ markdown-to-jsx@^7.1.8: resolved "https://registry.npmjs.org/markdown-to-jsx/-/markdown-to-jsx-7.4.7.tgz" integrity sha512-0+ls1IQZdU6cwM1yu0ZjjiVWYtkbExSyUIFU2ZeDIFuZM1W42Mh4OlJ4nb4apX4H8smxDHRdFaoIVJGwfv5hkg== -marky@^1.2.2: - version "1.3.0" - resolved "https://registry.npmjs.org/marky/-/marky-1.3.0.tgz" - integrity sha512-ocnPZQLNpvbedwTy9kNrQEsknEfgvcLMvOtz3sFeWApDq1MXH1TqkCIx58xlpESsfwQOnuBO9beyQuNGzVvuhQ== - math-intrinsics@^1.1.0: version "1.1.0" resolved "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz" @@ -11410,7 +11292,7 @@ memfs@^3.4.1, memfs@^3.4.12: dependencies: fs-monkey "^1.0.4" -memoize-one@^5.0.0, memoize-one@^5.1.1: +memoize-one@^5.1.1: version "5.2.1" resolved "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz" integrity sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q== @@ -11442,244 +11324,56 @@ methods@~1.1.2: resolved "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz" integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w== -metro-babel-transformer@0.82.5: - version "0.82.5" - resolved "https://registry.npmjs.org/metro-babel-transformer/-/metro-babel-transformer-0.82.5.tgz" - integrity sha512-W/scFDnwJXSccJYnOFdGiYr9srhbHPdxX9TvvACOFsIXdLilh3XuxQl/wXW6jEJfgIb0jTvoTlwwrqvuwymr6Q== - dependencies: - "@babel/core" "^7.25.2" - flow-enums-runtime "^0.0.6" - hermes-parser "0.29.1" - nullthrows "^1.1.1" +micro-ftch@^0.3.1: + version "0.3.1" + resolved "https://registry.npmjs.org/micro-ftch/-/micro-ftch-0.3.1.tgz" + integrity sha512-/0LLxhzP0tfiR5hcQebtudP56gUurs2CLkGarnCiB/OqEyUFQ6U3paQi/tgLv0hBJYt2rnr9MNpxz4fiiugstg== -metro-cache-key@0.82.5: - version "0.82.5" - resolved "https://registry.npmjs.org/metro-cache-key/-/metro-cache-key-0.82.5.tgz" - integrity sha512-qpVmPbDJuRLrT4kcGlUouyqLGssJnbTllVtvIgXfR7ZuzMKf0mGS+8WzcqzNK8+kCyakombQWR0uDd8qhWGJcA== +micromatch@4.0.5: + version "4.0.5" + resolved "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz" + integrity sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA== dependencies: - flow-enums-runtime "^0.0.6" + braces "^3.0.2" + picomatch "^2.3.1" -metro-cache@0.82.5: - version "0.82.5" - resolved "https://registry.npmjs.org/metro-cache/-/metro-cache-0.82.5.tgz" - integrity sha512-AwHV9607xZpedu1NQcjUkua8v7HfOTKfftl6Vc9OGr/jbpiJX6Gpy8E/V9jo/U9UuVYX2PqSUcVNZmu+LTm71Q== +micromatch@^4.0.2, micromatch@^4.0.4, micromatch@^4.0.5, micromatch@^4.0.8: + version "4.0.8" + resolved "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz" + integrity sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA== dependencies: - exponential-backoff "^3.1.1" - flow-enums-runtime "^0.0.6" - https-proxy-agent "^7.0.5" - metro-core "0.82.5" + braces "^3.0.3" + picomatch "^2.3.1" -metro-config@^0.82.2, metro-config@0.82.5: - version "0.82.5" - resolved "https://registry.npmjs.org/metro-config/-/metro-config-0.82.5.tgz" - integrity sha512-/r83VqE55l0WsBf8IhNmc/3z71y2zIPe5kRSuqA5tY/SL/ULzlHUJEMd1szztd0G45JozLwjvrhAzhDPJ/Qo/g== +miller-rabin@^4.0.0: + version "4.0.1" + resolved "https://registry.npmjs.org/miller-rabin/-/miller-rabin-4.0.1.tgz" + integrity sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA== dependencies: - connect "^3.6.5" - cosmiconfig "^5.0.5" - flow-enums-runtime "^0.0.6" - jest-validate "^29.7.0" - metro "0.82.5" - metro-cache "0.82.5" - metro-core "0.82.5" - metro-runtime "0.82.5" - -metro-core@^0.82.2, metro-core@0.82.5: - version "0.82.5" - resolved "https://registry.npmjs.org/metro-core/-/metro-core-0.82.5.tgz" - integrity sha512-OJL18VbSw2RgtBm1f2P3J5kb892LCVJqMvslXxuxjAPex8OH7Eb8RBfgEo7VZSjgb/LOf4jhC4UFk5l5tAOHHA== - dependencies: - flow-enums-runtime "^0.0.6" - lodash.throttle "^4.1.1" - metro-resolver "0.82.5" - -metro-file-map@0.82.5: - version "0.82.5" - resolved "https://registry.npmjs.org/metro-file-map/-/metro-file-map-0.82.5.tgz" - integrity sha512-vpMDxkGIB+MTN8Af5hvSAanc6zXQipsAUO+XUx3PCQieKUfLwdoa8qaZ1WAQYRpaU+CJ8vhBcxtzzo3d9IsCIQ== - dependencies: - debug "^4.4.0" - fb-watchman "^2.0.0" - flow-enums-runtime "^0.0.6" - graceful-fs "^4.2.4" - invariant "^2.2.4" - jest-worker "^29.7.0" - micromatch "^4.0.4" - nullthrows "^1.1.1" - walker "^1.0.7" + bn.js "^4.0.0" + brorand "^1.0.1" -metro-minify-terser@0.82.5: - version "0.82.5" - resolved "https://registry.npmjs.org/metro-minify-terser/-/metro-minify-terser-0.82.5.tgz" - integrity sha512-v6Nx7A4We6PqPu/ta1oGTqJ4Usz0P7c+3XNeBxW9kp8zayS3lHUKR0sY0wsCHInxZlNAEICx791x+uXytFUuwg== - dependencies: - flow-enums-runtime "^0.0.6" - terser "^5.15.0" +mime-db@1.52.0: + version "1.52.0" + resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz" + integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== -metro-resolver@0.82.5: - version "0.82.5" - resolved "https://registry.npmjs.org/metro-resolver/-/metro-resolver-0.82.5.tgz" - integrity sha512-kFowLnWACt3bEsuVsaRNgwplT8U7kETnaFHaZePlARz4Fg8tZtmRDUmjaD68CGAwc0rwdwNCkWizLYpnyVcs2g== - dependencies: - flow-enums-runtime "^0.0.6" +"mime-db@>= 1.43.0 < 2": + version "1.53.0" + resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.53.0.tgz" + integrity sha512-oHlN/w+3MQ3rba9rqFr6V/ypF10LSkdwUysQL7GkXoTgIWeV+tcXGA852TBxH+gsh8UWoyhR1hKcoMJTuWflpg== -metro-runtime@^0.82.2, metro-runtime@0.82.5: - version "0.82.5" - resolved "https://registry.npmjs.org/metro-runtime/-/metro-runtime-0.82.5.tgz" - integrity sha512-rQZDoCUf7k4Broyw3Ixxlq5ieIPiR1ULONdpcYpbJQ6yQ5GGEyYjtkztGD+OhHlw81LCR2SUAoPvtTus2WDK5g== +mime-types@^2.1.12, mime-types@^2.1.25, mime-types@^2.1.27, mime-types@^2.1.31, mime-types@~2.1.24, mime-types@~2.1.34: + version "2.1.35" + resolved "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz" + integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== dependencies: - "@babel/runtime" "^7.25.0" - flow-enums-runtime "^0.0.6" + mime-db "1.52.0" -metro-source-map@^0.82.2, metro-source-map@0.82.5: - version "0.82.5" - resolved "https://registry.npmjs.org/metro-source-map/-/metro-source-map-0.82.5.tgz" - integrity sha512-wH+awTOQJVkbhn2SKyaw+0cd+RVSCZ3sHVgyqJFQXIee/yLs3dZqKjjeKKhhVeudgjXo7aE/vSu/zVfcQEcUfw== - dependencies: - "@babel/traverse" "^7.25.3" - "@babel/traverse--for-generate-function-map" "npm:@babel/traverse@^7.25.3" - "@babel/types" "^7.25.2" - flow-enums-runtime "^0.0.6" - invariant "^2.2.4" - metro-symbolicate "0.82.5" - nullthrows "^1.1.1" - ob1 "0.82.5" - source-map "^0.5.6" - vlq "^1.0.0" - -metro-symbolicate@0.82.5: - version "0.82.5" - resolved "https://registry.npmjs.org/metro-symbolicate/-/metro-symbolicate-0.82.5.tgz" - integrity sha512-1u+07gzrvYDJ/oNXuOG1EXSvXZka/0JSW1q2EYBWerVKMOhvv9JzDGyzmuV7hHbF2Hg3T3S2uiM36sLz1qKsiw== - dependencies: - flow-enums-runtime "^0.0.6" - invariant "^2.2.4" - metro-source-map "0.82.5" - nullthrows "^1.1.1" - source-map "^0.5.6" - vlq "^1.0.0" - -metro-transform-plugins@0.82.5: - version "0.82.5" - resolved "https://registry.npmjs.org/metro-transform-plugins/-/metro-transform-plugins-0.82.5.tgz" - integrity sha512-57Bqf3rgq9nPqLrT2d9kf/2WVieTFqsQ6qWHpEng5naIUtc/Iiw9+0bfLLWSAw0GH40iJ4yMjFcFJDtNSYynMA== - dependencies: - "@babel/core" "^7.25.2" - "@babel/generator" "^7.25.0" - "@babel/template" "^7.25.0" - "@babel/traverse" "^7.25.3" - flow-enums-runtime "^0.0.6" - nullthrows "^1.1.1" - -metro-transform-worker@0.82.5: - version "0.82.5" - resolved "https://registry.npmjs.org/metro-transform-worker/-/metro-transform-worker-0.82.5.tgz" - integrity sha512-mx0grhAX7xe+XUQH6qoHHlWedI8fhSpDGsfga7CpkO9Lk9W+aPitNtJWNGrW8PfjKEWbT9Uz9O50dkI8bJqigw== - dependencies: - "@babel/core" "^7.25.2" - "@babel/generator" "^7.25.0" - "@babel/parser" "^7.25.3" - "@babel/types" "^7.25.2" - flow-enums-runtime "^0.0.6" - metro "0.82.5" - metro-babel-transformer "0.82.5" - metro-cache "0.82.5" - metro-cache-key "0.82.5" - metro-minify-terser "0.82.5" - metro-source-map "0.82.5" - metro-transform-plugins "0.82.5" - nullthrows "^1.1.1" - -metro@^0.82.2, metro@0.82.5: - version "0.82.5" - resolved "https://registry.npmjs.org/metro/-/metro-0.82.5.tgz" - integrity sha512-8oAXxL7do8QckID/WZEKaIFuQJFUTLzfVcC48ghkHhNK2RGuQq8Xvf4AVd+TUA0SZtX0q8TGNXZ/eba1ckeGCg== - dependencies: - "@babel/code-frame" "^7.24.7" - "@babel/core" "^7.25.2" - "@babel/generator" "^7.25.0" - "@babel/parser" "^7.25.3" - "@babel/template" "^7.25.0" - "@babel/traverse" "^7.25.3" - "@babel/types" "^7.25.2" - accepts "^1.3.7" - chalk "^4.0.0" - ci-info "^2.0.0" - connect "^3.6.5" - debug "^4.4.0" - error-stack-parser "^2.0.6" - flow-enums-runtime "^0.0.6" - graceful-fs "^4.2.4" - hermes-parser "0.29.1" - image-size "^1.0.2" - invariant "^2.2.4" - jest-worker "^29.7.0" - jsc-safe-url "^0.2.2" - lodash.throttle "^4.1.1" - metro-babel-transformer "0.82.5" - metro-cache "0.82.5" - metro-cache-key "0.82.5" - metro-config "0.82.5" - metro-core "0.82.5" - metro-file-map "0.82.5" - metro-resolver "0.82.5" - metro-runtime "0.82.5" - metro-source-map "0.82.5" - metro-symbolicate "0.82.5" - metro-transform-plugins "0.82.5" - metro-transform-worker "0.82.5" - mime-types "^2.1.27" - nullthrows "^1.1.1" - serialize-error "^2.1.0" - source-map "^0.5.6" - throat "^5.0.0" - ws "^7.5.10" - yargs "^17.6.2" - -micro-ftch@^0.3.1: - version "0.3.1" - resolved "https://registry.npmjs.org/micro-ftch/-/micro-ftch-0.3.1.tgz" - integrity sha512-/0LLxhzP0tfiR5hcQebtudP56gUurs2CLkGarnCiB/OqEyUFQ6U3paQi/tgLv0hBJYt2rnr9MNpxz4fiiugstg== - -micromatch@^4.0.2, micromatch@^4.0.4, micromatch@^4.0.5, micromatch@^4.0.8: - version "4.0.8" - resolved "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz" - integrity sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA== - dependencies: - braces "^3.0.3" - picomatch "^2.3.1" - -micromatch@4.0.5: - version "4.0.5" - resolved "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz" - integrity sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA== - dependencies: - braces "^3.0.2" - picomatch "^2.3.1" - -miller-rabin@^4.0.0: - version "4.0.1" - resolved "https://registry.npmjs.org/miller-rabin/-/miller-rabin-4.0.1.tgz" - integrity sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA== - dependencies: - bn.js "^4.0.0" - brorand "^1.0.1" - -"mime-db@>= 1.43.0 < 2": - version "1.53.0" - resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.53.0.tgz" - integrity sha512-oHlN/w+3MQ3rba9rqFr6V/ypF10LSkdwUysQL7GkXoTgIWeV+tcXGA852TBxH+gsh8UWoyhR1hKcoMJTuWflpg== - -mime-db@1.52.0: - version "1.52.0" - resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz" - integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== - -mime-types@^2.1.12, mime-types@^2.1.25, mime-types@^2.1.27, mime-types@^2.1.31, mime-types@~2.1.24, mime-types@~2.1.34: - version "2.1.35" - resolved "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz" - integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== - dependencies: - mime-db "1.52.0" +mime@1.6.0: + version "1.6.0" + resolved "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz" + integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== mime@^2.0.3: version "2.6.0" @@ -11691,11 +11385,6 @@ mime@^3.0.0: resolved "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz" integrity sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A== -mime@1.6.0: - version "1.6.0" - resolved "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz" - integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== - mimic-fn@^2.1.0: version "2.1.0" resolved "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz" @@ -11733,14 +11422,7 @@ minimatch@^3.0.2, minimatch@^3.0.4, minimatch@^3.1.1, minimatch@^3.1.2: dependencies: brace-expansion "^1.1.7" -minimatch@^5.0.1: - version "5.1.6" - resolved "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz" - integrity sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g== - dependencies: - brace-expansion "^2.0.1" - -minimatch@^5.1.0: +minimatch@^5.0.1, minimatch@^5.1.0: version "5.1.6" resolved "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz" integrity sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g== @@ -11766,17 +11448,12 @@ minipass@^3.0.0: dependencies: yallist "^4.0.0" -"minipass@^5.0.0 || ^6.0.2 || ^7.0.0": - version "7.1.2" - resolved "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz" - integrity sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw== - minipass@^5.0.0: version "5.0.0" resolved "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz" integrity sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ== -minipass@^7.1.2: +"minipass@^5.0.0 || ^6.0.2 || ^7.0.0", minipass@^7.1.2: version "7.1.2" resolved "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz" integrity sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw== @@ -11801,28 +11478,18 @@ mkdirp-classic@^0.5.2, mkdirp-classic@^0.5.3: resolved "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz" integrity sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A== -mkdirp@^0.5.4, "mkdirp@>=0.5 0": +mkdirp@1.0.4, mkdirp@^1.0.3: + version "1.0.4" + resolved "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz" + integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== + +"mkdirp@>=0.5 0", mkdirp@^0.5.4: version "0.5.6" resolved "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz" integrity sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw== dependencies: minimist "^1.2.6" -mkdirp@^1.0.3: - version "1.0.4" - resolved "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz" - integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== - -mkdirp@^1.0.4: - version "1.0.4" - resolved "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz" - integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== - -mkdirp@1.0.4: - version "1.0.4" - resolved "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz" - integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== - mlly@^1.6.1, mlly@^1.7.1: version "1.7.1" resolved "https://registry.npmjs.org/mlly/-/mlly-1.7.1.tgz" @@ -11855,16 +11522,19 @@ motion@10.16.2: "@motionone/utils" "^10.15.1" "@motionone/vue" "^10.16.2" +moveable-helper@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/moveable-helper/-/moveable-helper-0.4.0.tgz#95dbbc20258d827223a715d9d694f5d2f1b14d65" + integrity sha512-t1FK9PO187Gn0N6GVZcrQgePjiHmuj8eUhmJjH38LvTMnVVxiHzWYRx6ARFZvSFIIW4yb6BEAv4C99Bsx84nFw== + dependencies: + "@daybrush/utils" "^1.0.0" + scenejs "^1.4.2" + mri@^1.2.0: version "1.2.0" resolved "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz" integrity sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA== -ms@^2.1.1, ms@^2.1.3, ms@2.1.3: - version "2.1.3" - resolved "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz" - integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== - ms@2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz" @@ -11875,6 +11545,11 @@ ms@2.1.2: resolved "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== +ms@2.1.3, ms@^2.1.1, ms@^2.1.3: + version "2.1.3" + resolved "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + multiformats@^9.4.2: version "9.9.0" resolved "https://registry.npmjs.org/multiformats/-/multiformats-9.9.0.tgz" @@ -11943,7 +11618,7 @@ next-tick@^1.1.0: resolved "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz" integrity sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ== -"next@^13 || ^14 || ^15", "next@^9.0.0 || ^10.0.0 || ^11.0.0 || ^12.0.0 || ^13.0.0 || ^14.0.0", next@14.2.15: +next@14.2.15: version "14.2.15" resolved "https://registry.npmjs.org/next/-/next-14.2.15.tgz" integrity sha512-h9ctmOokpoDphRvMGnwOJAedT6zKhwqyZML9mDtspgf4Rh3Pn7UTYKqePNoDvhsWBAO5GoPNYshnAUGIazVGmw== @@ -12135,11 +11810,6 @@ nth-check@^2.0.1: dependencies: boolbase "^1.0.0" -nullthrows@^1.1.1: - version "1.1.1" - resolved "https://registry.npmjs.org/nullthrows/-/nullthrows-1.1.1.tgz" - integrity sha512-2vPPEi+Z7WqML2jZYddDIfy5Dqb0r2fze2zTxNNknZaFpVHU3mFB3R+DWeJWGVx0ecvttSGlJTI+WG+8Z4cDWw== - nwsapi@^2.2.2: version "2.2.12" resolved "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.12.tgz" @@ -12157,13 +11827,6 @@ nypm@^0.3.8: pkg-types "^1.1.1" ufo "^1.5.3" -ob1@0.82.5: - version "0.82.5" - resolved "https://registry.npmjs.org/ob1/-/ob1-0.82.5.tgz" - integrity sha512-QyQQ6e66f+Ut/qUVjEce0E/wux5nAGLXYZDn1jr15JWstHsCH3l6VVrg8NKDptW9NEiBXKOJeGF/ydxeSDF3IQ== - dependencies: - flow-enums-runtime "^0.0.6" - obj-multiplex@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/obj-multiplex/-/obj-multiplex-1.0.0.tgz" @@ -12275,13 +11938,6 @@ on-exit-leak-free@^0.2.0: resolved "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-0.2.0.tgz" integrity sha512-dqaz3u44QbRXQooZLTUKU41ZrzYrcvLISVgbrzbyCMxpmSLJvZ3ZamIJIZ29P6OhZIkNIQKosdeM6t1LYbA9hg== -on-finished@~2.3.0: - version "2.3.0" - resolved "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz" - integrity sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww== - dependencies: - ee-first "1.1.1" - on-finished@2.4.1: version "2.4.1" resolved "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz" @@ -12315,14 +11971,6 @@ onetime@^6.0.0: dependencies: mimic-fn "^4.0.0" -open@^7.0.3: - version "7.4.2" - resolved "https://registry.npmjs.org/open/-/open-7.4.2.tgz" - integrity sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q== - dependencies: - is-docker "^2.0.0" - is-wsl "^2.1.1" - open@^8.0.4, open@^8.4.0: version "8.4.2" resolved "https://registry.npmjs.org/open/-/open-8.4.2.tgz" @@ -12359,11 +12007,23 @@ ora@^5.4.1: strip-ansi "^6.0.0" wcwidth "^1.0.1" +order-map@^0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/order-map/-/order-map-0.3.1.tgz#0d519e2ad79c1606eb037d59fb191ab9bd5ce37b" + integrity sha512-RSuElIGwzPuBLzS9Io7G8fpcnQeudg0XswOyOiwRNLX7lkf+eQ/KUp+kcAP7z7nTOdkrfxhZycyXwzFW75iJ6A== + os-browserify@^0.3.0: version "0.3.0" resolved "https://registry.npmjs.org/os-browserify/-/os-browserify-0.3.0.tgz" integrity sha512-gjcpUc3clBf9+210TRaDWbf+rZZZEshZ+DlXMRCeAjp0xhTrnQsKHypIy1J3d5hKdUzj69t708EHtU8P6bUn0A== +overlap-area@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/overlap-area/-/overlap-area-1.1.0.tgz#1fcaa21bdb9cb1ace973d9aa299ae6b56557a4c2" + integrity sha512-3dlJgJCaVeXH0/eZjYVJvQiLVVrPO4U1ZGqlATtx6QGO3b5eNM6+JgUKa7oStBTdYuGTk7gVoABCW6Tp+dhRdw== + dependencies: + "@daybrush/utils" "^1.7.1" + own-keys@^1.0.1: version "1.0.1" resolved "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz" @@ -12386,14 +12046,7 @@ ox@0.4.4: abitype "^1.0.6" eventemitter3 "5.0.1" -p-limit@^2.0.0: - version "2.3.0" - resolved "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz" - integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w== - dependencies: - p-try "^2.0.0" - -p-limit@^2.2.0: +p-limit@^2.0.0, p-limit@^2.2.0: version "2.3.0" resolved "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz" integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w== @@ -12486,12 +12139,7 @@ pako@^0.2.5, pako@~0.2.0: resolved "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz" integrity sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA== -pako@~1.0.2: - version "1.0.11" - resolved "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz" - integrity sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw== - -pako@~1.0.5: +pako@~1.0.2, pako@~1.0.5: version "1.0.11" resolved "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz" integrity sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw== @@ -12523,14 +12171,6 @@ parse-asn1@^5.0.0, parse-asn1@^5.1.7: pbkdf2 "^3.1.2" safe-buffer "^5.2.1" -parse-json@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz" - integrity sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw== - dependencies: - error-ex "^1.3.1" - json-parse-better-errors "^1.0.1" - parse-json@^5.0.0, parse-json@^5.2.0: version "5.2.0" resolved "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz" @@ -13005,7 +12645,7 @@ postcss-nesting@^12.0.1: "@csstools/selector-specificity" "^3.1.1" postcss-selector-parser "^6.1.0" -postcss-selector-parser@^6.0.0, postcss-selector-parser@^6.0.11, postcss-selector-parser@^6.0.13, postcss-selector-parser@^6.0.2, postcss-selector-parser@^6.0.4, postcss-selector-parser@^6.1.0, postcss-selector-parser@^6.1.1: +postcss-selector-parser@^6.0.0, postcss-selector-parser@^6.0.11, postcss-selector-parser@^6.0.2, postcss-selector-parser@^6.0.4, postcss-selector-parser@^6.1.0, postcss-selector-parser@^6.1.1: version "6.1.2" resolved "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz" integrity sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg== @@ -13018,24 +12658,16 @@ postcss-value-parser@^4.0.0, postcss-value-parser@^4.1.0, postcss-value-parser@^ resolved "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz" integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ== -"postcss@^7.0.0 || ^8.0.1", postcss@^8.0.0, postcss@^8.1.0, postcss@^8.2.14, postcss@^8.4, postcss@^8.4.18, postcss@^8.4.21, postcss@^8.4.23, postcss@^8.4.33, postcss@>=8.0.9: - version "8.4.41" - resolved "https://registry.npmjs.org/postcss/-/postcss-8.4.41.tgz" - integrity sha512-TesUflQ0WKZqAvg52PWL6kHgLKP6xB6heTOdoYM0Wt2UHyxNa4K25EZZMgKns3BH1RLVbZCREPpLY0rhnNoHVQ== - dependencies: - nanoid "^3.3.7" - picocolors "^1.0.1" - source-map-js "^1.2.0" - -postcss@^7.0.14: - version "7.0.39" - resolved "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz" - integrity sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA== +postcss@8.4.31: + version "8.4.31" + resolved "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz" + integrity sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ== dependencies: - picocolors "^0.2.1" - source-map "^0.6.1" + nanoid "^3.3.6" + picocolors "^1.0.0" + source-map-js "^1.0.2" -postcss@^7.0.32, postcss@^7.0.35, postcss@^7.0.5, postcss@^7.0.6: +postcss@^7.0.14, postcss@^7.0.32, postcss@^7.0.35, postcss@^7.0.5, postcss@^7.0.6: version "7.0.39" resolved "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz" integrity sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA== @@ -13043,14 +12675,14 @@ postcss@^7.0.32, postcss@^7.0.35, postcss@^7.0.5, postcss@^7.0.6: picocolors "^0.2.1" source-map "^0.6.1" -postcss@8.4.31: - version "8.4.31" - resolved "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz" - integrity sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ== +postcss@^8.2.14, postcss@^8.4.18, postcss@^8.4.21, postcss@^8.4.23, postcss@^8.4.33: + version "8.4.41" + resolved "https://registry.npmjs.org/postcss/-/postcss-8.4.41.tgz" + integrity sha512-TesUflQ0WKZqAvg52PWL6kHgLKP6xB6heTOdoYM0Wt2UHyxNa4K25EZZMgKns3BH1RLVbZCREPpLY0rhnNoHVQ== dependencies: - nanoid "^3.3.6" - picocolors "^1.0.0" - source-map-js "^1.0.2" + nanoid "^3.3.7" + picocolors "^1.0.1" + source-map-js "^1.2.0" preact@^10.16.0: version "10.23.2" @@ -13080,12 +12712,7 @@ prelude-ls@^1.2.1: resolved "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz" integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== -prettier@^2.8.0: - version "2.8.8" - resolved "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz" - integrity sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q== - -prettier@^2.8.7: +prettier@^2.8.0, prettier@^2.8.7: version "2.8.8" resolved "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz" integrity sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q== @@ -13146,13 +12773,6 @@ progress@^2.0.1: resolved "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz" integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA== -promise@^8.3.0: - version "8.3.0" - resolved "https://registry.npmjs.org/promise/-/promise-8.3.0.tgz" - integrity sha512-rZPNPKTOYVNEEKFaq1HqTgOwZD+4/YHS5ukLzQCypkj+OkYx7iv0mA91lJlpPPZ8vMau3IIGj5Qlwrx+8iiSmg== - dependencies: - asap "~2.0.6" - promptly@^2: version "2.2.0" resolved "https://registry.npmjs.org/promptly/-/promptly-2.2.0.tgz" @@ -13318,13 +12938,6 @@ qrcode@1.5.4: pngjs "^5.0.0" yargs "^15.3.1" -qs@^6.10.0, qs@^6.12.3: - version "6.13.0" - resolved "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz" - integrity sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg== - dependencies: - side-channel "^1.0.6" - qs@6.11.0: version "6.11.0" resolved "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz" @@ -13332,6 +12945,13 @@ qs@6.11.0: dependencies: side-channel "^1.0.4" +qs@^6.10.0, qs@^6.12.3: + version "6.13.0" + resolved "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz" + integrity sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg== + dependencies: + side-channel "^1.0.6" + query-string@7.1.3: version "7.1.3" resolved "https://registry.npmjs.org/query-string/-/query-string-7.1.3.tgz" @@ -13429,13 +13049,13 @@ react-colorful@^5.1.2: resolved "https://registry.npmjs.org/react-colorful/-/react-colorful-5.6.1.tgz" integrity sha512-1exovf0uGTGyq5mXQT0zgQ80uvj2PCwvF8zY1RN9/vbJVSjSo3fsB/4L3ObbF7u70NduSiK4xu4Y6q1MHoUGEw== -react-devtools-core@^6.1.1: - version "6.1.5" - resolved "https://registry.npmjs.org/react-devtools-core/-/react-devtools-core-6.1.5.tgz" - integrity sha512-ePrwPfxAnB+7hgnEr8vpKxL9cmnp7F322t8oqcPshbIQQhDKgFDW4tjhF2wjVbdXF9O/nyuy3sQWd9JGpiLPvA== +react-css-styled@^1.1.9: + version "1.1.9" + resolved "https://registry.yarnpkg.com/react-css-styled/-/react-css-styled-1.1.9.tgz#a7cc948e49f72b2f7fb1393bd85416a8293afab3" + integrity sha512-M7fJZ3IWFaIHcZEkoFOnkjdiUFmwd8d+gTh2bpqMOcnxy/0Gsykw4dsL4QBiKsxcGow6tETUa4NAUcmJF+/nfw== dependencies: - shell-quote "^1.6.1" - ws "^7" + css-styled "~1.0.8" + framework-utils "^1.1.0" react-docgen-typescript@^2.2.2: version "2.2.2" @@ -13458,7 +13078,7 @@ react-docgen@^7.0.0: resolve "^1.22.1" strip-indent "^4.0.0" -"react-dom@^0.14.8 || ^15.0.1 || ^16.0.0 || ^17.0.1 || ^18.0.0", "react-dom@^16.8 || ^17.0 || ^18.0", "react-dom@^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom@^16.8.0 || ^17.0.0 || ^18.0.0", react-dom@^18, "react-dom@^18.0.0 || ^19.0.0", react-dom@^18.2.0, "react-dom@>= 16.8.0 || ^18.0.0", react-dom@>=16.8.0, react-dom@>=18, "react-dom@16.8.0 - 18": +react-dom@^18.2.0: version "18.3.1" resolved "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz" integrity sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw== @@ -13492,18 +13112,15 @@ react-fast-marquee@^1.6.4: resolved "https://registry.npmjs.org/react-fast-marquee/-/react-fast-marquee-1.6.5.tgz" integrity sha512-swDnPqrT2XISAih0o74zQVE2wQJFMvkx+9VZXYYNSLb/CUcAzU9pNj637Ar2+hyRw6b4tP6xh4GQZip2ZCpQpg== -react-hook-form@^7.0.0, react-hook-form@^7.41.5: +react-hook-form@^7.41.5: version "7.52.2" resolved "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.52.2.tgz" integrity sha512-pqfPEbERnxxiNMPd0bzmt1tuaPcVccywFDpyk2uV5xCIBphHV5T8SVnX9/o3kplPE1zzKt77+YIoq+EMwJp56A== -react-i18next@^13.2.2: - version "13.5.0" - resolved "https://registry.npmjs.org/react-i18next/-/react-i18next-13.5.0.tgz" - integrity sha512-CFJ5NDGJ2MUyBohEHxljOq/39NQ972rh1ajnadG9BjTk+UXbHLq4z5DKEbEQBDoIhUmmbuS/fIMJKo6VOax1HA== - dependencies: - "@babel/runtime" "^7.22.5" - html-parse-stringify "^3.0.1" +react-is@18.1.0: + version "18.1.0" + resolved "https://registry.npmjs.org/react-is/-/react-is-18.1.0.tgz" + integrity sha512-Fl7FuabXsJnV5Q1qIOQwx/sagGF18kogb4gpfcG4gjLBWO0WDiiz1ko/ExayuxE7InyQkBLkxRFG5oxY6Uu3Kg== react-is@^16.13.1: version "16.13.1" @@ -13520,10 +13137,24 @@ react-is@^18.0.0: resolved "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz" integrity sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg== -react-is@18.1.0: - version "18.1.0" - resolved "https://registry.npmjs.org/react-is/-/react-is-18.1.0.tgz" - integrity sha512-Fl7FuabXsJnV5Q1qIOQwx/sagGF18kogb4gpfcG4gjLBWO0WDiiz1ko/ExayuxE7InyQkBLkxRFG5oxY6Uu3Kg== +react-moveable@^0.56.0: + version "0.56.0" + resolved "https://registry.yarnpkg.com/react-moveable/-/react-moveable-0.56.0.tgz#3537565079468aa9d241c17d0a5e78c56a562843" + integrity sha512-FmJNmIOsOA36mdxbrc/huiE4wuXSRlmon/o+/OrfNhSiYYYL0AV5oObtPluEhb2Yr/7EfYWBHTxF5aWAvjg1SA== + dependencies: + "@daybrush/utils" "^1.13.0" + "@egjs/agent" "^2.2.1" + "@egjs/children-differ" "^1.0.1" + "@egjs/list-differ" "^1.0.0" + "@scena/dragscroll" "^1.4.0" + "@scena/event-emitter" "^1.0.5" + "@scena/matrix" "^1.1.1" + css-to-mat "^1.1.1" + framework-utils "^1.1.0" + gesto "^1.19.3" + overlap-area "^1.1.0" + react-css-styled "^1.1.9" + react-selecto "^1.25.0" react-native-webview@^11.26.0: version "11.26.1" @@ -13533,47 +13164,6 @@ react-native-webview@^11.26.0: escape-string-regexp "2.0.0" invariant "2.2.4" -react-native@*: - version "0.80.1" - resolved "https://registry.npmjs.org/react-native/-/react-native-0.80.1.tgz" - integrity sha512-cIiJiPItdC2+Z9n30FmE2ef1y4522kgmOjMIoDtlD16jrOMNTUdB2u+CylLTy3REkWkWTS6w8Ub7skUthkeo5w== - dependencies: - "@jest/create-cache-key-function" "^29.7.0" - "@react-native/assets-registry" "0.80.1" - "@react-native/codegen" "0.80.1" - "@react-native/community-cli-plugin" "0.80.1" - "@react-native/gradle-plugin" "0.80.1" - "@react-native/js-polyfills" "0.80.1" - "@react-native/normalize-colors" "0.80.1" - "@react-native/virtualized-lists" "0.80.1" - abort-controller "^3.0.0" - anser "^1.4.9" - ansi-regex "^5.0.0" - babel-jest "^29.7.0" - babel-plugin-syntax-hermes-parser "0.28.1" - base64-js "^1.5.1" - chalk "^4.0.0" - commander "^12.0.0" - flow-enums-runtime "^0.0.6" - glob "^7.1.1" - invariant "^2.2.4" - jest-environment-node "^29.7.0" - memoize-one "^5.0.0" - metro-runtime "^0.82.2" - metro-source-map "^0.82.2" - nullthrows "^1.1.1" - pretty-format "^29.7.0" - promise "^8.3.0" - react-devtools-core "^6.1.1" - react-refresh "^0.14.0" - regenerator-runtime "^0.13.2" - scheduler "0.26.0" - semver "^7.1.3" - stacktrace-parser "^0.1.10" - whatwg-fetch "^3.0.0" - ws "^6.2.3" - yargs "^17.6.2" - react-player@^2.16.0: version "2.16.0" resolved "https://registry.npmjs.org/react-player/-/react-player-2.16.0.tgz" @@ -13585,7 +13175,7 @@ react-player@^2.16.0: prop-types "^15.7.2" react-fast-compare "^3.0.1" -react-refresh@^0.14.0, "react-refresh@>=0.10.0 <1.0.0": +react-refresh@^0.14.0: version "0.14.2" resolved "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz" integrity sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA== @@ -13620,6 +13210,13 @@ react-remove-scroll@2.6.0: use-callback-ref "^1.3.0" use-sidecar "^1.1.2" +react-selecto@^1.25.0: + version "1.26.3" + resolved "https://registry.yarnpkg.com/react-selecto/-/react-selecto-1.26.3.tgz#f9081c006cee2e2fed85ac1811cfe17136cf81a5" + integrity sha512-Ubik7kWSnZyQEBNro+1k38hZaI1tJarE+5aD/qsqCOA1uUBSjgKVBy3EWRzGIbdmVex7DcxznFZLec/6KZNvwQ== + dependencies: + selecto "~1.26.3" + react-share@^5.1.0: version "5.1.0" resolved "https://registry.npmjs.org/react-share/-/react-share-5.1.0.tgz" @@ -13645,7 +13242,7 @@ react-style-singleton@^2.2.2: get-nonce "^1.0.0" tslib "^2.0.0" -react@*, "react@^0.14.8 || ^15.0.1 || ^16.0.0 || ^17.0.1 || ^18.0.0", "react@^16.8 || ^17.0 || ^18.0", "react@^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react@^16.8.0 || ^17 || ^18 || ^19", "react@^16.8.0 || ^17.0.0 || ^18.0.0", "react@^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react@^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc", "react@^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0", "react@^17 || ^18", react@^18, "react@^18 || ^19", react@^18.0.0, "react@^18.0.0 || ^19.0.0", react@^18.2.0, react@^18.3.1, react@^19.1.0, "react@>= 0.14.0", "react@>= 16.8.0", "react@>= 16.8.0 || ^18.0.0", "react@>= 16.8.0 || 17.x.x || ^18.0.0-0", react@>=16, react@>=16.13.1, react@>=16.6.0, react@>=16.8, react@>=16.8.0, react@>=18, "react@16.8.0 - 18": +react@^18.2.0: version "18.3.1" resolved "https://registry.npmjs.org/react/-/react-18.3.1.tgz" integrity sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ== @@ -13685,7 +13282,7 @@ read@^1.0.4: dependencies: mute-stream "~0.0.4" -readable-stream@^2.0.0: +readable-stream@^2.0.0, readable-stream@^2.0.2, readable-stream@^2.0.5, readable-stream@^2.2.2, readable-stream@^2.3.3, readable-stream@^2.3.8, readable-stream@~2.3.6: version "2.3.8" resolved "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz" integrity sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA== @@ -13698,119 +13295,30 @@ readable-stream@^2.0.0: string_decoder "~1.1.1" util-deprecate "~1.0.1" -readable-stream@^2.0.2: - version "2.3.8" - resolved "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz" - integrity sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA== +readable-stream@^3.1.1, readable-stream@^3.4.0, readable-stream@^3.5.0, readable-stream@^3.6.0, readable-stream@^3.6.2: + version "3.6.2" + resolved "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz" + integrity sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA== dependencies: - core-util-is "~1.0.0" - inherits "~2.0.3" - isarray "~1.0.0" - process-nextick-args "~2.0.0" - safe-buffer "~5.1.1" - string_decoder "~1.1.1" - util-deprecate "~1.0.1" + inherits "^2.0.3" + string_decoder "^1.1.1" + util-deprecate "^1.0.1" -readable-stream@^2.0.5: - version "2.3.8" - resolved "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz" - integrity sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA== +"readable-stream@^3.6.2 || ^4.4.2", readable-stream@^4.0.0: + version "4.5.2" + resolved "https://registry.npmjs.org/readable-stream/-/readable-stream-4.5.2.tgz" + integrity sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g== dependencies: - core-util-is "~1.0.0" - inherits "~2.0.3" - isarray "~1.0.0" - process-nextick-args "~2.0.0" - safe-buffer "~5.1.1" - string_decoder "~1.1.1" - util-deprecate "~1.0.1" + abort-controller "^3.0.0" + buffer "^6.0.3" + events "^3.3.0" + process "^0.11.10" + string_decoder "^1.3.0" -readable-stream@^2.2.2: - version "2.3.8" - resolved "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz" - integrity sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA== - dependencies: - core-util-is "~1.0.0" - inherits "~2.0.3" - isarray "~1.0.0" - process-nextick-args "~2.0.0" - safe-buffer "~5.1.1" - string_decoder "~1.1.1" - util-deprecate "~1.0.1" - -readable-stream@^2.3.3: - version "2.3.8" - resolved "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz" - integrity sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA== - dependencies: - core-util-is "~1.0.0" - inherits "~2.0.3" - isarray "~1.0.0" - process-nextick-args "~2.0.0" - safe-buffer "~5.1.1" - string_decoder "~1.1.1" - util-deprecate "~1.0.1" - -readable-stream@^2.3.8: - version "2.3.8" - resolved "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz" - integrity sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA== - dependencies: - core-util-is "~1.0.0" - inherits "~2.0.3" - isarray "~1.0.0" - process-nextick-args "~2.0.0" - safe-buffer "~5.1.1" - string_decoder "~1.1.1" - util-deprecate "~1.0.1" - -readable-stream@^3.1.1, readable-stream@^3.4.0, readable-stream@^3.5.0, readable-stream@^3.6.0, readable-stream@^3.6.2: - version "3.6.2" - resolved "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz" - integrity sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA== - dependencies: - inherits "^2.0.3" - string_decoder "^1.1.1" - util-deprecate "^1.0.1" - -"readable-stream@^3.6.2 || ^4.4.2": - version "4.5.2" - resolved "https://registry.npmjs.org/readable-stream/-/readable-stream-4.5.2.tgz" - integrity sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g== - dependencies: - abort-controller "^3.0.0" - buffer "^6.0.3" - events "^3.3.0" - process "^0.11.10" - string_decoder "^1.3.0" - -readable-stream@^4.0.0: - version "4.5.2" - resolved "https://registry.npmjs.org/readable-stream/-/readable-stream-4.5.2.tgz" - integrity sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g== - dependencies: - abort-controller "^3.0.0" - buffer "^6.0.3" - events "^3.3.0" - process "^0.11.10" - string_decoder "^1.3.0" - -readable-stream@~2.3.6: - version "2.3.8" - resolved "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz" - integrity sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA== - dependencies: - core-util-is "~1.0.0" - inherits "~2.0.3" - isarray "~1.0.0" - process-nextick-args "~2.0.0" - safe-buffer "~5.1.1" - string_decoder "~1.1.1" - util-deprecate "~1.0.1" - -readdir-glob@^1.1.2: - version "1.1.3" - resolved "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.3.tgz" - integrity sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA== +readdir-glob@^1.1.2: + version "1.1.3" + resolved "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.3.tgz" + integrity sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA== dependencies: minimatch "^5.1.0" @@ -13863,11 +13371,6 @@ regenerate@^1.4.2: resolved "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz" integrity sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A== -regenerator-runtime@^0.13.2: - version "0.13.11" - resolved "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz" - integrity sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg== - regenerator-runtime@^0.14.0: version "0.14.1" resolved "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz" @@ -13993,11 +13496,6 @@ resolve-cwd@^3.0.0: dependencies: resolve-from "^5.0.0" -resolve-from@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz" - integrity sha512-GnlH6vxLymXJNMBo7XP1fJIzBFbdYt49CuTwmB/6N53t+kMPRMFKz783LlQ4tv28XoQfMWinAJX6WCGf2IlaIw== - resolve-from@^4.0.0: version "4.0.0" resolved "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz" @@ -14038,16 +13536,7 @@ resolve@^1.1.7, resolve@^1.10.0, resolve@^1.14.2, resolve@^1.20.0, resolve@^1.22 path-parse "^1.0.7" supports-preserve-symlinks-flag "^1.0.0" -resolve@^1.22.1: - version "1.22.10" - resolved "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz" - integrity sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w== - dependencies: - is-core-module "^2.16.0" - path-parse "^1.0.7" - supports-preserve-symlinks-flag "^1.0.0" - -resolve@^1.22.4: +resolve@^1.22.1, resolve@^1.22.4: version "1.22.10" resolved "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz" integrity sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w== @@ -14091,7 +13580,7 @@ rfdc@^1.3.0: resolved "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz" integrity sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA== -rimraf@^2.6.1, rimraf@2: +rimraf@2, rimraf@^2.6.1: version "2.7.1" resolved "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz" integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w== @@ -14153,20 +13642,15 @@ safe-array-concat@^1.1.3: has-symbols "^1.1.0" isarray "^2.0.5" -safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@^5.2.0, safe-buffer@^5.2.1, safe-buffer@~5.2.0, safe-buffer@5.2.1: - version "5.2.1" - resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz" - integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== - -safe-buffer@~5.1.0, safe-buffer@~5.1.1: +safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: version "5.1.2" resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz" integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== -safe-buffer@5.1.2: - version "5.1.2" - resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz" - integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== +safe-buffer@5.2.1, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@^5.2.0, safe-buffer@^5.2.1, safe-buffer@~5.2.0: + version "5.2.1" + resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== safe-push-apply@^1.0.0: version "1.0.0" @@ -14222,6 +13706,17 @@ saxes@^6.0.0: dependencies: xmlchars "^2.2.0" +scenejs@^1.4.2: + version "1.10.3" + resolved "https://registry.yarnpkg.com/scenejs/-/scenejs-1.10.3.tgz#8ace9dab59ecb5db0a8eb2c07268cb30876ad60f" + integrity sha512-o1Xrz5sRMeVOD5R9MizY2tYVPYeRnQNttiNRD7vtfi4j4+su1nuP2R/1yv3jDNol1zFfFHwwh2G0jxyt0SIqUA== + dependencies: + "@cfcs/core" "^0.1.0" + "@daybrush/utils" "^1.10.2" + "@scena/event-emitter" "^1.0.3" + css-styled "^1.0.6" + order-map "^0.3.1" + scheduler@^0.23.2: version "0.23.2" resolved "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz" @@ -14229,11 +13724,6 @@ scheduler@^0.23.2: dependencies: loose-envify "^1.1.0" -scheduler@0.26.0: - version "0.26.0" - resolved "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz" - integrity sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA== - schema-utils@^2.7.0: version "2.7.1" resolved "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.1.tgz" @@ -14243,7 +13733,7 @@ schema-utils@^2.7.0: ajv "^6.12.4" ajv-keywords "^3.5.2" -schema-utils@^3.0.0: +schema-utils@^3.0.0, schema-utils@^3.1.1, schema-utils@^3.2.0: version "3.3.0" resolved "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz" integrity sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg== @@ -14252,26 +13742,7 @@ schema-utils@^3.0.0: ajv "^6.12.5" ajv-keywords "^3.5.2" -schema-utils@^3.1.1, schema-utils@^3.2.0: - version "3.3.0" - resolved "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz" - integrity sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg== - dependencies: - "@types/json-schema" "^7.0.8" - ajv "^6.12.5" - ajv-keywords "^3.5.2" - -schema-utils@^4.0.0: - version "4.2.0" - resolved "https://registry.npmjs.org/schema-utils/-/schema-utils-4.2.0.tgz" - integrity sha512-L0jRsrPpjdckP3oPug3/VxNKt2trR8TcabrM6FOAAlvC/9Phcmm+cuAgTlxBqdBR1WJx7Naj9WHw+aOmheSVbw== - dependencies: - "@types/json-schema" "^7.0.9" - ajv "^8.9.0" - ajv-formats "^2.1.1" - ajv-keywords "^5.1.0" - -schema-utils@^4.2.0: +schema-utils@^4.0.0, schema-utils@^4.2.0: version "4.2.0" resolved "https://registry.npmjs.org/schema-utils/-/schema-utils-4.2.0.tgz" integrity sha512-L0jRsrPpjdckP3oPug3/VxNKt2trR8TcabrM6FOAAlvC/9Phcmm+cuAgTlxBqdBR1WJx7Naj9WHw+aOmheSVbw== @@ -14295,55 +13766,44 @@ secp256k1@^5.0.0: node-addon-api "^5.0.0" node-gyp-build "^4.2.0" -semver@^5.6.0: +selecto@~1.26.3: + version "1.26.3" + resolved "https://registry.yarnpkg.com/selecto/-/selecto-1.26.3.tgz#12f259112b943d395731524e3bb0115da7372212" + integrity sha512-gZHgqMy5uyB6/2YDjv3Qqaf7bd2hTDOpPdxXlrez4R3/L0GiEWDCFaUfrflomgqdb3SxHF2IXY0Jw0EamZi7cw== + dependencies: + "@daybrush/utils" "^1.13.0" + "@egjs/children-differ" "^1.0.1" + "@scena/dragscroll" "^1.4.0" + "@scena/event-emitter" "^1.0.5" + css-styled "^1.0.8" + css-to-mat "^1.1.1" + framework-utils "^1.1.0" + gesto "^1.19.4" + keycon "^1.2.0" + overlap-area "^1.1.0" + +"semver@2 || 3 || 4 || 5", semver@^5.6.0: version "5.7.2" resolved "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz" integrity sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g== -semver@^6.0.0: +semver@^6.0.0, semver@^6.3.0, semver@^6.3.1: version "6.3.1" resolved "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz" integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== -semver@^6.3.0: - version "6.3.1" - resolved "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz" - integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== - -semver@^6.3.1: - version "6.3.1" - resolved "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz" - integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== - -semver@^7.1.3, semver@^7.2, semver@^7.3.5, semver@^7.3.7, semver@^7.3.8, semver@^7.5.3, semver@^7.5.4, semver@^7.6.0, semver@^7.6.3: +semver@^7.2, semver@^7.3.4, semver@^7.3.5, semver@^7.3.7, semver@^7.3.8, semver@^7.5.3, semver@^7.5.4, semver@^7.6.0, semver@^7.6.3: version "7.6.3" resolved "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz" integrity sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A== -semver@^7.3.4: - version "7.6.3" - resolved "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz" - integrity sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A== - -semver@~7.5.0: - version "7.5.4" - resolved "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz" - integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA== - dependencies: - lru-cache "^6.0.0" - -semver@~7.5.4: +semver@~7.5.0, semver@~7.5.4: version "7.5.4" resolved "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz" integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA== dependencies: lru-cache "^6.0.0" -"semver@2 || 3 || 4 || 5": - version "5.7.2" - resolved "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz" - integrity sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g== - send@0.18.0: version "0.18.0" resolved "https://registry.npmjs.org/send/-/send-0.18.0.tgz" @@ -14363,30 +13823,6 @@ send@0.18.0: range-parser "~1.2.1" statuses "2.0.1" -send@0.19.0: - version "0.19.0" - resolved "https://registry.npmjs.org/send/-/send-0.19.0.tgz" - integrity sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw== - dependencies: - debug "2.6.9" - depd "2.0.0" - destroy "1.2.0" - encodeurl "~1.0.2" - escape-html "~1.0.3" - etag "~1.8.1" - fresh "0.5.2" - http-errors "2.0.0" - mime "1.6.0" - ms "2.1.3" - on-finished "2.4.1" - range-parser "~1.2.1" - statuses "2.0.1" - -serialize-error@^2.1.0: - version "2.1.0" - resolved "https://registry.npmjs.org/serialize-error/-/serialize-error-2.1.0.tgz" - integrity sha512-ghgmKt5o4Tly5yEG/UJp8qTd0AN7Xalw4XBtDEKP655B699qMEtra1WlXeE6WIvdEG481JvRxULKsInq/iNysw== - serialize-javascript@^6.0.1: version "6.0.2" resolved "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz" @@ -14394,16 +13830,6 @@ serialize-javascript@^6.0.1: dependencies: randombytes "^2.1.0" -serve-static@^1.16.2: - version "1.16.2" - resolved "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz" - integrity sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw== - dependencies: - encodeurl "~2.0.0" - escape-html "~1.0.3" - parseurl "~1.3.3" - send "0.19.0" - serve-static@1.15.0: version "1.15.0" resolved "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz" @@ -14501,11 +13927,6 @@ shebang-regex@^3.0.0: resolved "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz" integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== -shell-quote@^1.6.1: - version "1.8.3" - resolved "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz" - integrity sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw== - shimmer@^1.2.0: version "1.2.1" resolved "https://registry.npmjs.org/shimmer/-/shimmer-1.2.1.tgz" @@ -14556,12 +13977,7 @@ signal-exit@^3.0.0, signal-exit@^3.0.2, signal-exit@^3.0.3, signal-exit@^3.0.7: resolved "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz" integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== -signal-exit@^4.0.1: - version "4.1.0" - resolved "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz" - integrity sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw== - -signal-exit@^4.1.0: +signal-exit@^4.0.1, signal-exit@^4.1.0: version "4.1.0" resolved "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz" integrity sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw== @@ -14665,14 +14081,6 @@ source-map-js@^1.0.2, source-map-js@^1.2.0: resolved "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz" integrity sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg== -source-map-support@^0.5.16, source-map-support@~0.5.20, source-map-support@0.5.21: - version "0.5.21" - resolved "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz" - integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w== - dependencies: - buffer-from "^1.0.0" - source-map "^0.6.0" - source-map-support@0.5.13: version "0.5.13" resolved "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz" @@ -14681,22 +14089,20 @@ source-map-support@0.5.13: buffer-from "^1.0.0" source-map "^0.6.0" -source-map@^0.5.6: - version "0.5.7" - resolved "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz" - integrity sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ== +source-map-support@0.5.21, source-map-support@^0.5.16, source-map-support@~0.5.20: + version "0.5.21" + resolved "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz" + integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w== + dependencies: + buffer-from "^1.0.0" + source-map "^0.6.0" -source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.0, source-map@~0.6.1, source-map@0.6.1: +source-map@0.6.1, source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.0, source-map@~0.6.1: version "0.6.1" resolved "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz" integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== -source-map@^0.7.3: - version "0.7.4" - resolved "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz" - integrity sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA== - -source-map@^0.7.4: +source-map@^0.7.3, source-map@^0.7.4: version "0.7.4" resolved "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz" integrity sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA== @@ -14742,6 +14148,11 @@ split2@^4.0.0: resolved "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz" integrity sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg== +sprintf-js@1.1.2: + version "1.1.2" + resolved "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.2.tgz" + integrity sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug== + sprintf-js@^1.1.3: version "1.1.3" resolved "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz" @@ -14752,11 +14163,6 @@ sprintf-js@~1.0.2: resolved "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz" integrity sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g== -sprintf-js@1.1.2: - version "1.1.2" - resolved "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.2.tgz" - integrity sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug== - stable-hash@^0.0.4: version "0.0.4" resolved "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.4.tgz" @@ -14774,18 +14180,6 @@ stackframe@^1.3.4: resolved "https://registry.npmjs.org/stackframe/-/stackframe-1.3.4.tgz" integrity sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw== -stacktrace-parser@^0.1.10: - version "0.1.11" - resolved "https://registry.npmjs.org/stacktrace-parser/-/stacktrace-parser-0.1.11.tgz" - integrity sha512-WjlahMgHmCJpqzU8bIBy4qtsZdU9lRlcZE3Lvyej6t4tuOuv1vk57OW3MBrj6hXBFx/nNoC9MPMTcr5YA7NQbg== - dependencies: - type-fest "^0.7.1" - -statuses@~1.5.0: - version "1.5.0" - resolved "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz" - integrity sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA== - statuses@2.0.1: version "2.0.1" resolved "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz" @@ -14860,20 +14254,6 @@ strict-uri-encode@^2.0.0: resolved "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz" integrity sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ== -string_decoder@^1.1.1, string_decoder@^1.3.0: - version "1.3.0" - resolved "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz" - integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== - dependencies: - safe-buffer "~5.2.0" - -string_decoder@~1.1.1: - version "1.1.1" - resolved "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz" - integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== - dependencies: - safe-buffer "~5.1.0" - string-argv@0.3.2: version "0.3.2" resolved "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz" @@ -14905,25 +14285,7 @@ string-length@^4.0.1: is-fullwidth-code-point "^3.0.0" strip-ansi "^6.0.1" -string-width@^5.0.0: - version "5.1.2" - resolved "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz" - integrity sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA== - dependencies: - eastasianwidth "^0.2.0" - emoji-regex "^9.2.2" - strip-ansi "^7.0.1" - -string-width@^5.0.1: - version "5.1.2" - resolved "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz" - integrity sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA== - dependencies: - eastasianwidth "^0.2.0" - emoji-regex "^9.2.2" - strip-ansi "^7.0.1" - -string-width@^5.1.2: +string-width@^5.0.0, string-width@^5.0.1, string-width@^5.1.2: version "5.1.2" resolved "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz" integrity sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA== @@ -15000,6 +14362,20 @@ string.prototype.trimstart@^1.0.8: define-properties "^1.2.1" es-object-atoms "^1.0.0" +string_decoder@^1.1.1, string_decoder@^1.3.0: + version "1.3.0" + resolved "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz" + integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== + dependencies: + safe-buffer "~5.2.0" + +string_decoder@~1.1.1: + version "1.1.1" + resolved "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz" + integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== + dependencies: + safe-buffer "~5.1.0" + "strip-ansi-cjs@npm:strip-ansi@^6.0.1": version "6.0.1" resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz" @@ -15294,7 +14670,7 @@ terser-webpack-plugin@^5.3.1, terser-webpack-plugin@^5.3.10: serialize-javascript "^6.0.1" terser "^5.26.0" -terser@^5.10.0, terser@^5.15.0, terser@^5.26.0: +terser@^5.10.0, terser@^5.26.0: version "5.31.6" resolved "https://registry.npmjs.org/terser/-/terser-5.31.6.tgz" integrity sha512-PQ4DAriWzKj+qgehQ7LK5bQqCFNMmlhjR2PFFLuqGCpuCAauxemVBWwWOxo3UIwWQx8+Pr61Df++r76wDmkQBg== @@ -15348,11 +14724,6 @@ thread-stream@^0.15.1: dependencies: real-require "^0.1.0" -throat@^5.0.0: - version "5.0.0" - resolved "https://registry.npmjs.org/throat/-/throat-5.0.0.tgz" - integrity sha512-fcwX4mndzpLQKBS1DVYhGAcYaYt7vsHNIvQV+WXMvnow5cgjPphq5CaayLaGsjRdSCKZFNGt7/GYAuXaNOiYCA== - through2@^2.0.3: version "2.0.5" resolved "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz" @@ -15475,17 +14846,7 @@ tsconfig-paths@^4.0.0, tsconfig-paths@^4.1.2: minimist "^1.2.6" strip-bom "^3.0.0" -tslib@^1.13.0: - version "1.14.1" - resolved "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz" - integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== - -tslib@^2.0.0, tslib@^2.0.1, tslib@^2.0.3, tslib@^2.1.0, tslib@^2.3.1, tslib@^2.4.0: - version "2.6.3" - resolved "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz" - integrity sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ== - -tslib@1.14.1: +tslib@1.14.1, tslib@^1.13.0: version "1.14.1" resolved "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz" integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== @@ -15495,6 +14856,11 @@ tslib@1.9.3: resolved "https://registry.npmjs.org/tslib/-/tslib-1.9.3.tgz" integrity sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ== +tslib@^2.0.0, tslib@^2.0.1, tslib@^2.0.3, tslib@^2.1.0, tslib@^2.3.1, tslib@^2.4.0: + version "2.6.3" + resolved "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz" + integrity sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ== + tty-browserify@^0.0.1: version "0.0.1" resolved "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.1.tgz" @@ -15546,11 +14912,6 @@ type-fest@^0.6.0: resolved "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz" integrity sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg== -type-fest@^0.7.1: - version "0.7.1" - resolved "https://registry.npmjs.org/type-fest/-/type-fest-0.7.1.tgz" - integrity sha512-Ne2YiiGN8bmrmJJEuTWTLJR32nh/JdL1+PSicowtNb0WFpn59GK8/lfD61bVtzguz7b3PBt74nxpv/Pw5po5Rg== - type-fest@^0.8.1: version "0.8.1" resolved "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz" @@ -15561,26 +14922,16 @@ type-fest@^1.0.2: resolved "https://registry.npmjs.org/type-fest/-/type-fest-1.4.0.tgz" integrity sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA== -type-fest@^2.14.0: +type-fest@^2.14.0, type-fest@^2.19.0, type-fest@~2.19: version "2.19.0" resolved "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz" integrity sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA== -type-fest@^2.19.0: - version "2.19.0" - resolved "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz" - integrity sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA== - -type-fest@^4.20.1, "type-fest@>=0.17.0 <5.0.0": +type-fest@^4.20.1: version "4.25.0" resolved "https://registry.npmjs.org/type-fest/-/type-fest-4.25.0.tgz" integrity sha512-bRkIGlXsnGBRBQRAY56UXBm//9qH4bmJfFvq83gSz41N282df+fjy8ofcEgc1sM8geNt5cl6mC2g9Fht1cs8Aw== -type-fest@~2.19: - version "2.19.0" - resolved "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz" - integrity sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA== - type-is@~1.6.18: version "1.6.18" resolved "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz" @@ -15660,7 +15011,7 @@ typescript-eslint@^8.20.0: "@typescript-eslint/parser" "8.20.0" "@typescript-eslint/utils" "8.20.0" -typescript@*, typescript@^5.5.4, "typescript@>= 4.3.x", "typescript@>= 4.x", typescript@>=3.3.1, typescript@>=4.8.4, "typescript@>=4.8.4 <5.8.0", typescript@>=4.9.5, typescript@>=5.0.4, typescript@>=5.4.0, typescript@>3.6.0: +typescript@^5.5.4: version "5.5.4" resolved "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz" integrity sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q== @@ -15680,13 +15031,6 @@ uglify-js@^3.1.4: resolved "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.2.tgz" integrity sha512-S8KA6DDI47nQXJSi2ctQ629YzwOVs+bQML6DAtvy0wgNdpi+0ySpQK0g2pxBq2xfF2z3YCscu7NNA8nXT9PlIQ== -uint8arrays@^3.0.0: - version "3.1.1" - resolved "https://registry.npmjs.org/uint8arrays/-/uint8arrays-3.1.1.tgz" - integrity sha512-+QJa8QRnbdXVpHYjLoTpJIdCTiw9Ir62nocClWuXIq2JIh4Uta0cQsTSpFL678p2CN8B+XSApwcU+pQEqVpKWg== - dependencies: - multiformats "^9.4.2" - uint8arrays@3.1.0: version "3.1.0" resolved "https://registry.npmjs.org/uint8arrays/-/uint8arrays-3.1.0.tgz" @@ -15694,6 +15038,13 @@ uint8arrays@3.1.0: dependencies: multiformats "^9.4.2" +uint8arrays@^3.0.0: + version "3.1.1" + resolved "https://registry.npmjs.org/uint8arrays/-/uint8arrays-3.1.1.tgz" + integrity sha512-+QJa8QRnbdXVpHYjLoTpJIdCTiw9Ir62nocClWuXIq2JIh4Uta0cQsTSpFL678p2CN8B+XSApwcU+pQEqVpKWg== + dependencies: + multiformats "^9.4.2" + unbox-primitive@^1.1.0: version "1.1.0" resolved "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz" @@ -15714,11 +15065,6 @@ undici-types@~5.26.4: resolved "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz" integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== -undici-types@~6.19.2: - version "6.19.6" - resolved "https://registry.npmjs.org/undici-types/-/undici-types-6.19.6.tgz" - integrity sha512-e/vggGopEfTKSvj4ihnOLTsqhrKRN3LeO6qSN/GxohhuRv8qH9bNQ4B8W7e/vFL+0XTnmHPB4/kegunZGA4Org== - unenv@^1.9.0: version "1.10.0" resolved "https://registry.npmjs.org/unenv/-/unenv-1.10.0.tgz" @@ -15797,7 +15143,7 @@ universalify@^2.0.0: resolved "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz" integrity sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw== -unpipe@~1.0.0, unpipe@1.0.0: +unpipe@1.0.0, unpipe@~1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz" integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ== @@ -15926,7 +15272,7 @@ use-sync-external-store@1.2.2: resolved "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.2.tgz" integrity sha512-PElTlVMwpblvbNqQ82d2n6RjStvdSoNe9FG28kNfz3WiXilJm4DdNkEzRhCZuIDwY8U08WVihhGR5iRqAwfDiw== -utf-8-validate@^5.0.2, utf-8-validate@>=5.0.2: +utf-8-validate@^5.0.2: version "5.0.10" resolved "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.10.tgz" integrity sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ== @@ -15978,12 +15324,7 @@ uuid@^8.3.0, uuid@^8.3.2: resolved "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz" integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== -uuid@^9.0.0: - version "9.0.1" - resolved "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz" - integrity sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA== - -uuid@^9.0.1: +uuid@^9.0.0, uuid@^9.0.1: version "9.0.1" resolved "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz" integrity sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA== @@ -16018,21 +15359,22 @@ vary@~1.1.2: resolved "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz" integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg== -viem@^1.0.0: - version "1.21.4" - resolved "https://registry.npmjs.org/viem/-/viem-1.21.4.tgz" - integrity sha512-BNVYdSaUjeS2zKQgPs+49e5JKocfo60Ib2yiXOWBT6LuVxY1I/6fFX3waEtpXvL1Xn4qu+BVitVtMh9lyThyhQ== +viem@2.21.58: + version "2.21.58" + resolved "https://registry.npmjs.org/viem/-/viem-2.21.58.tgz" + integrity sha512-mGVKlu3ici7SueEQatn44I7KePP8Nwb5JUjZaQOciWxWHCFP/WLyjdZDIK09qyaozHNTH/t78K3ptXCe+AnMuQ== dependencies: - "@adraffy/ens-normalize" "1.10.0" - "@noble/curves" "1.2.0" - "@noble/hashes" "1.3.2" - "@scure/bip32" "1.3.2" - "@scure/bip39" "1.2.1" - abitype "0.9.8" - isows "1.0.3" - ws "8.13.0" + "@noble/curves" "1.7.0" + "@noble/hashes" "1.6.1" + "@scure/bip32" "1.6.0" + "@scure/bip39" "1.5.0" + abitype "1.0.7" + isows "1.0.6" + ox "0.4.4" + webauthn-p256 "0.0.10" + ws "8.18.0" -viem@^1.1.4: +viem@^1.0.0, viem@^1.1.4: version "1.21.4" resolved "https://registry.npmjs.org/viem/-/viem-1.21.4.tgz" integrity sha512-BNVYdSaUjeS2zKQgPs+49e5JKocfo60Ib2yiXOWBT6LuVxY1I/6fFX3waEtpXvL1Xn4qu+BVitVtMh9lyThyhQ== @@ -16046,21 +15388,6 @@ viem@^1.1.4: isows "1.0.3" ws "8.13.0" -viem@2.21.58, viem@2.x: - version "2.21.58" - resolved "https://registry.npmjs.org/viem/-/viem-2.21.58.tgz" - integrity sha512-mGVKlu3ici7SueEQatn44I7KePP8Nwb5JUjZaQOciWxWHCFP/WLyjdZDIK09qyaozHNTH/t78K3ptXCe+AnMuQ== - dependencies: - "@noble/curves" "1.7.0" - "@noble/hashes" "1.6.1" - "@scure/bip32" "1.6.0" - "@scure/bip39" "1.5.0" - abitype "1.0.7" - isows "1.0.6" - ox "0.4.4" - webauthn-p256 "0.0.10" - ws "8.18.0" - vizion@~2.2.1: version "2.2.1" resolved "https://registry.npmjs.org/vizion/-/vizion-2.2.1.tgz" @@ -16071,11 +15398,6 @@ vizion@~2.2.1: ini "^1.3.5" js-git "^0.7.8" -vlq@^1.0.0: - version "1.0.1" - resolved "https://registry.npmjs.org/vlq/-/vlq-1.0.1.tgz" - integrity sha512-gQpnTgkubC6hQgdIcRdYGDSDc+SaujOdyesZQMv6JlfQee/9Mp0Qhnys6WxDWvQnL5WZdT7o2Ul187aSt0Rq+w== - vm-browserify@^1.1.2: version "1.1.2" resolved "https://registry.npmjs.org/vm-browserify/-/vm-browserify-1.1.2.tgz" @@ -16093,7 +15415,7 @@ w3c-xmlserializer@^4.0.0: dependencies: xml-name-validator "^4.0.0" -wagmi@^2.9.0, wagmi@2.10.8: +wagmi@2.10.8: version "2.10.8" resolved "https://registry.npmjs.org/wagmi/-/wagmi-2.10.8.tgz" integrity sha512-25xJCTEQ3ug6tl86MnngzhXOJUo4tJufUUxlnb2qRz+aZFAcRGL+hhuBBZOJ552T49UPF0Hs9c6Rd4BKvwHLrg== @@ -16102,7 +15424,7 @@ wagmi@^2.9.0, wagmi@2.10.8: "@wagmi/core" "2.11.5" use-sync-external-store "1.2.0" -walker@^1.0.7, walker@^1.0.8: +walker@^1.0.8: version "1.0.8" resolved "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz" integrity sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ== @@ -16132,16 +15454,16 @@ webauthn-p256@0.0.10: "@noble/curves" "^1.4.0" "@noble/hashes" "^1.4.0" -webextension-polyfill@^0.10.0: - version "0.10.0" - resolved "https://registry.npmjs.org/webextension-polyfill/-/webextension-polyfill-0.10.0.tgz" - integrity sha512-c5s35LgVa5tFaHhrZDnr3FpQpjj1BB+RXhLTYUxGqBVN460HkbM8TBtEqdXWbpTKfzwCcjAZVF7zXCYSKtcp9g== - "webextension-polyfill@>=0.10.0 <1.0": version "0.12.0" resolved "https://registry.npmjs.org/webextension-polyfill/-/webextension-polyfill-0.12.0.tgz" integrity sha512-97TBmpoWJEE+3nFBQ4VocyCdLKfw54rFaJ6EVQYLBCXqCIpLSZkwGgASpv4oPt9gdKCJ80RJlcmNzNn008Ag6Q== +webextension-polyfill@^0.10.0: + version "0.10.0" + resolved "https://registry.npmjs.org/webextension-polyfill/-/webextension-polyfill-0.10.0.tgz" + integrity sha512-c5s35LgVa5tFaHhrZDnr3FpQpjj1BB+RXhLTYUxGqBVN460HkbM8TBtEqdXWbpTKfzwCcjAZVF7zXCYSKtcp9g== + webidl-conversions@^3.0.0: version "3.0.1" resolved "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz" @@ -16163,7 +15485,7 @@ webpack-dev-middleware@^6.1.1: range-parser "^1.2.1" schema-utils "^4.0.0" -webpack-hot-middleware@^2.25.1, webpack-hot-middleware@2.x: +webpack-hot-middleware@^2.25.1: version "2.26.1" resolved "https://registry.npmjs.org/webpack-hot-middleware/-/webpack-hot-middleware-2.26.1.tgz" integrity sha512-khZGfAeJx6I8K9zKohEWWYN6KDlVw2DHownoe+6Vtwj1LP9WFgegXnVMSkZ/dBEBtXFwrkkydsaPFlB7f8wU2A== @@ -16187,7 +15509,7 @@ webpack-virtual-modules@^0.6.2: resolved "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz" integrity sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ== -"webpack@^4.0.0 || ^5.0.0", webpack@^5.0.0, webpack@^5.1.0, webpack@^5.11.0, webpack@^5.20.0, "webpack@>= 4", webpack@>=2, "webpack@>=4.43.0 <6.0.0", webpack@>=5, webpack@5, webpack@5.94.0: +webpack@5, webpack@5.94.0: version "5.94.0" resolved "https://registry.npmjs.org/webpack/-/webpack-5.94.0.tgz" integrity sha512-KcsGn50VT+06JH/iunZJedYGUJS5FGjow8wb9c0v5n1Om8O1g4L6LjtfxwlXIATopoQu+vOXXa7gYisWxCoPyg== @@ -16235,11 +15557,6 @@ whatwg-encoding@^2.0.0: dependencies: iconv-lite "0.6.3" -whatwg-fetch@^3.0.0: - version "3.6.20" - resolved "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.20.tgz" - integrity sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg== - whatwg-mimetype@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz" @@ -16400,7 +15717,17 @@ write-file-atomic@^4.0.2: imurmurhash "^0.1.4" signal-exit "^3.0.7" -ws@*, ws@^8.11.0, ws@^8.2.3, ws@8.18.0: +ws@7.4.6: + version "7.4.6" + resolved "https://registry.npmjs.org/ws/-/ws-7.4.6.tgz" + integrity sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A== + +ws@8.13.0: + version "8.13.0" + resolved "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz" + integrity sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA== + +ws@8.18.0, ws@^8.11.0, ws@^8.2.3: version "8.18.0" resolved "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz" integrity sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw== @@ -16412,34 +15739,7 @@ ws@^6.1.0: dependencies: async-limiter "~1.0.0" -ws@^6.2.3: - version "6.2.3" - resolved "https://registry.npmjs.org/ws/-/ws-6.2.3.tgz" - integrity sha512-jmTjYU0j60B+vHey6TfR3Z7RD61z/hmxBS3VMSGIrroOWXQEneK1zNuotOUrGyBHQj0yrpsLHPWtigEFd13ndA== - dependencies: - async-limiter "~1.0.0" - -ws@^7: - version "7.5.10" - resolved "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz" - integrity sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ== - -ws@^7.0.0: - version "7.5.10" - resolved "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz" - integrity sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ== - -ws@^7.5.1: - version "7.5.10" - resolved "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz" - integrity sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ== - -ws@^7.5.10: - version "7.5.10" - resolved "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz" - integrity sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ== - -ws@~7.5.10: +ws@^7.0.0, ws@^7.5.1, ws@~7.5.10: version "7.5.10" resolved "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz" integrity sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ== @@ -16449,16 +15749,6 @@ ws@~8.17.1: resolved "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz" integrity sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ== -ws@7.4.6: - version "7.4.6" - resolved "https://registry.npmjs.org/ws/-/ws-7.4.6.tgz" - integrity sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A== - -ws@8.13.0: - version "8.13.0" - resolved "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz" - integrity sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA== - xml-name-validator@^4.0.0: version "4.0.0" resolved "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz" @@ -16504,6 +15794,11 @@ yallist@^4.0.0: resolved "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz" integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== +yaml@2.3.1: + version "2.3.1" + resolved "https://registry.npmjs.org/yaml/-/yaml-2.3.1.tgz" + integrity sha512-2eHWfjaoXgTBC2jNM1LRef62VQa0umtvRiDSk6HSzW7RvS5YtkabJrwYLLEKWBc8a5U2PTSCs+dJjUTJdlHsWQ== + yaml@^1.10.0: version "1.10.2" resolved "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz" @@ -16514,11 +15809,6 @@ yaml@^2.3.4: resolved "https://registry.npmjs.org/yaml/-/yaml-2.5.0.tgz" integrity sha512-2wWLbGbYDiSqqIKoPjar3MPgB94ErzCtrNE1FdqGuaO0pi2JGjmE8aW8TDZwzU7vuxcGRdL/4gPQwQ7hD5AMSw== -yaml@2.3.1: - version "2.3.1" - resolved "https://registry.npmjs.org/yaml/-/yaml-2.3.1.tgz" - integrity sha512-2eHWfjaoXgTBC2jNM1LRef62VQa0umtvRiDSk6HSzW7RvS5YtkabJrwYLLEKWBc8a5U2PTSCs+dJjUTJdlHsWQ== - yargs-parser@^18.1.2: version "18.1.3" resolved "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz" @@ -16549,33 +15839,7 @@ yargs@^15.3.1: y18n "^4.0.0" yargs-parser "^18.1.2" -yargs@^17.3.1: - version "17.7.2" - resolved "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz" - integrity sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w== - dependencies: - cliui "^8.0.1" - escalade "^3.1.1" - get-caller-file "^2.0.5" - require-directory "^2.1.1" - string-width "^4.2.3" - y18n "^5.0.5" - yargs-parser "^21.1.1" - -yargs@^17.5.1: - version "17.7.2" - resolved "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz" - integrity sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w== - dependencies: - cliui "^8.0.1" - escalade "^3.1.1" - get-caller-file "^2.0.5" - require-directory "^2.1.1" - string-width "^4.2.3" - y18n "^5.0.5" - yargs-parser "^21.1.1" - -yargs@^17.6.2: +yargs@^17.3.1, yargs@^17.5.1: version "17.7.2" resolved "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz" integrity sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w== @@ -16615,21 +15879,21 @@ zip-stream@^4.1.0: compress-commons "^4.1.2" readable-stream "^3.6.0" -"zod@^3 >=3.19.1", "zod@^3 >=3.22.0", zod@^3.21.4: +zod@^3.21.4: version "3.23.8" resolved "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz" integrity sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g== -zustand@^4.5.4: - version "4.5.5" - resolved "https://registry.npmjs.org/zustand/-/zustand-4.5.5.tgz" - integrity sha512-+0PALYNJNgK6hldkgDq2vLrw5f6g/jCInz52n9RTpropGgeAf/ioFUCdtsjCqu4gNhW9D01rUQBROoRjdzyn2Q== - dependencies: - use-sync-external-store "1.2.2" - zustand@4.4.1: version "4.4.1" resolved "https://registry.npmjs.org/zustand/-/zustand-4.4.1.tgz" integrity sha512-QCPfstAS4EBiTQzlaGP1gmorkh/UL1Leaj2tdj+zZCZ/9bm0WS7sI2wnfD5lpOszFqWJ1DcPnGoY8RDL61uokw== dependencies: use-sync-external-store "1.2.0" + +zustand@^4.5.4: + version "4.5.5" + resolved "https://registry.npmjs.org/zustand/-/zustand-4.5.5.tgz" + integrity sha512-+0PALYNJNgK6hldkgDq2vLrw5f6g/jCInz52n9RTpropGgeAf/ioFUCdtsjCqu4gNhW9D01rUQBROoRjdzyn2Q== + dependencies: + use-sync-external-store "1.2.2" From e7438f06e3ce9f3a5ede7302b65243fb58b1e0dd Mon Sep 17 00:00:00 2001 From: LivingLikeKrillin <143606756+LivingLikeKrillin@users.noreply.github.com> Date: Mon, 2 Mar 2026 08:27:45 +0900 Subject: [PATCH 09/41] =?UTF-8?q?test:=20=EA=B3=A0=20ROI=20=EB=AA=A8?= =?UTF-8?q?=EB=93=88=20=ED=85=8C=EC=8A=A4=ED=8A=B8=208=EA=B0=9C=20?= =?UTF-8?q?=ED=8C=8C=EC=9D=BC=2076=EA=B0=9C=20=EC=B6=94=EA=B0=80=20(?= =?UTF-8?q?=ED=81=B4=EB=9E=98=EC=8A=A4,=20=EC=96=B4=EB=8C=91=ED=84=B0,=20?= =?UTF-8?q?=EC=86=8C=EC=BC=93,=20=EC=9D=B4=EB=B2=A4=ED=8A=B8=20=ED=95=B8?= =?UTF-8?q?=EB=93=A4=EB=9F=AC)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit variable-processor-util, MoveableHelper, ReactPlayerAPI, CircularBufferAdapter, ObserverAdapter, PartyroomClient, SocketClient, useHandleSubscriptionEvent Co-Authored-By: Claude Opus 4.6 --- .../ui/react-moveable/moveable.helper.test.ts | 82 ++++++ .../lib/react-player.api.test.ts | 153 +++++++++++ .../lib/handle-subscription-event.test.ts | 238 ++++++++++++++++++ .../lib/partyroom-client.test.ts | 81 ++++++ src/shared/api/websocket/client.test.ts | 215 ++++++++++++++++ .../chat/lib/circular-buffer-adapter.test.ts | 48 ++++ .../lib/chat/lib/observer-adapter.test.ts | 54 ++++ .../variable-processor-util.test.ts | 27 ++ 8 files changed, 898 insertions(+) create mode 100644 src/entities/avatar/ui/react-moveable/moveable.helper.test.ts create mode 100644 src/entities/music-preview/lib/react-player.api.test.ts create mode 100644 src/entities/partyroom-client/lib/handle-subscription-event.test.ts create mode 100644 src/entities/partyroom-client/lib/partyroom-client.test.ts create mode 100644 src/shared/api/websocket/client.test.ts create mode 100644 src/shared/lib/chat/lib/circular-buffer-adapter.test.ts create mode 100644 src/shared/lib/chat/lib/observer-adapter.test.ts create mode 100644 src/shared/lib/localization/renderer/processors/variable-processor-util.test.ts diff --git a/src/entities/avatar/ui/react-moveable/moveable.helper.test.ts b/src/entities/avatar/ui/react-moveable/moveable.helper.test.ts new file mode 100644 index 00000000..1c2bdbaf --- /dev/null +++ b/src/entities/avatar/ui/react-moveable/moveable.helper.test.ts @@ -0,0 +1,82 @@ +import { MoveableHelper } from './moveable.helper'; + +const FACE_WIDTH = 200; +const FACE_HEIGHT = 300; + +function createHelper( + initialFacePos?: { offsetX: number; offsetY: number; scale: number }, + onFacePosChange = jest.fn() +) { + return { + helper: new MoveableHelper(initialFacePos, onFacePosChange, FACE_WIDTH, FACE_HEIGHT), + onFacePosChange, + }; +} + +describe('MoveableHelper', () => { + describe('constructor', () => { + test('initialFacePos가 undefined이면 기본값 { offsetX: 0, offsetY: 0, scale: 1 }을 사용한다', () => { + const { helper, onFacePosChange } = createHelper(undefined); + // 기본값을 확인하기 위해 onScale 호출로 현재 facePos를 드러낸다 + helper.onScale({ scale: [2] }); + expect(onFacePosChange).toHaveBeenCalledWith({ offsetX: 0, offsetY: 0, scale: 2 }); + }); + + test('initialFacePos가 제공되면 그대로 저장한다', () => { + const initial = { offsetX: 0.5, offsetY: 0.3, scale: 1.5 }; + const { helper, onFacePosChange } = createHelper(initial); + // onScale로 scale만 변경하여 나머지 값 확인 + helper.onScale({ scale: [2] }); + expect(onFacePosChange).toHaveBeenCalledWith({ offsetX: 0.5, offsetY: 0.3, scale: 2 }); + }); + }); + + describe('onDrag', () => { + test('offsetX = x / faceWidth, offsetY = y / faceHeight로 계산한다', () => { + const { helper, onFacePosChange } = createHelper(); + helper.onDrag({ beforeTranslate: [100, 150] }); + expect(onFacePosChange).toHaveBeenCalledWith({ + offsetX: 100 / FACE_WIDTH, + offsetY: 150 / FACE_HEIGHT, + scale: 1, + }); + }); + + test('기존 scale 값을 유지한다', () => { + const { helper, onFacePosChange } = createHelper({ offsetX: 0, offsetY: 0, scale: 2.5 }); + helper.onDrag({ beforeTranslate: [50, 60] }); + expect(onFacePosChange).toHaveBeenCalledWith({ + offsetX: 50 / FACE_WIDTH, + offsetY: 60 / FACE_HEIGHT, + scale: 2.5, + }); + }); + }); + + describe('onScale', () => { + test('scale = zoomRatio로 설정하고 기존 offset을 유지한다', () => { + const initial = { offsetX: 0.4, offsetY: 0.6, scale: 1 }; + const { helper, onFacePosChange } = createHelper(initial); + helper.onScale({ scale: [3] }); + expect(onFacePosChange).toHaveBeenCalledWith({ offsetX: 0.4, offsetY: 0.6, scale: 3 }); + }); + }); + + test('drag → scale 연속 호출 시 모든 필드가 올바르게 유지된다', () => { + const { helper, onFacePosChange } = createHelper(); + + helper.onDrag({ beforeTranslate: [80, 120] }); + expect(onFacePosChange).toHaveBeenLastCalledWith({ + offsetX: 80 / FACE_WIDTH, + offsetY: 120 / FACE_HEIGHT, + scale: 1, + }); + + helper.onScale({ scale: [1.5] }); + expect(onFacePosChange).toHaveBeenLastCalledWith({ + offsetX: 80 / FACE_WIDTH, + offsetY: 120 / FACE_HEIGHT, + scale: 1.5, + }); + }); +}); diff --git a/src/entities/music-preview/lib/react-player.api.test.ts b/src/entities/music-preview/lib/react-player.api.test.ts new file mode 100644 index 00000000..07cecffb --- /dev/null +++ b/src/entities/music-preview/lib/react-player.api.test.ts @@ -0,0 +1,153 @@ +import { ReactPlayerAPI } from './react-player.api'; + +function createMockPlayer(overrides?: Record) { + const internalPlayer = { + mute: jest.fn(), + unMute: jest.fn(), + setVolume: jest.fn(), + }; + return { + seekTo: jest.fn(), + forceUpdate: jest.fn(), + getCurrentTime: jest.fn(() => 42), + getDuration: jest.fn(() => 180), + getInternalPlayer: jest.fn(() => internalPlayer), + __internalPlayer: internalPlayer, + ...overrides, + } as any; +} + +describe('ReactPlayerAPI', () => { + let api: ReactPlayerAPI; + + beforeEach(() => { + api = new ReactPlayerAPI(); + }); + + describe('isReady', () => { + test('player 설정 전에는 false를 반환한다', () => { + expect(api.isReady()).toBe(false); + }); + + test('player 설정 후에는 true를 반환한다', () => { + api.setPlayer(createMockPlayer()); + expect(api.isReady()).toBe(true); + }); + + test('cleanup 후에는 false를 반환한다', () => { + api.setPlayer(createMockPlayer()); + api.cleanup(); + expect(api.isReady()).toBe(false); + }); + }); + + describe('play', () => { + test('player가 null이면 아무 동작도 하지 않는다', () => { + expect(() => api.play()).not.toThrow(); + }); + + test('player가 있으면 seekTo(0)과 forceUpdate를 호출한다', () => { + const player = createMockPlayer(); + api.setPlayer(player); + api.play(); + expect(player.seekTo).toHaveBeenCalledWith(0, 'seconds'); + expect(player.forceUpdate).toHaveBeenCalled(); + }); + }); + + describe('stop', () => { + test('player가 null이면 아무 동작도 하지 않는다', () => { + expect(() => api.stop()).not.toThrow(); + }); + + test('player가 있으면 seekTo(0)을 호출한다', () => { + const player = createMockPlayer(); + api.setPlayer(player); + api.stop(); + expect(player.seekTo).toHaveBeenCalledWith(0, 'seconds'); + }); + }); + + describe('setMuted', () => { + test('muted=true → mute()를 호출한다', () => { + const player = createMockPlayer(); + api.setPlayer(player); + api.setMuted(true); + expect(player.__internalPlayer.mute).toHaveBeenCalled(); + }); + + test('muted=false → unMute()를 호출한다', () => { + const player = createMockPlayer(); + api.setPlayer(player); + api.setMuted(false); + expect(player.__internalPlayer.unMute).toHaveBeenCalled(); + }); + + test('internalPlayer가 null이면 아무 동작도 하지 않는다', () => { + const player = createMockPlayer({ getInternalPlayer: jest.fn(() => null) }); + api.setPlayer(player); + expect(() => api.setMuted(true)).not.toThrow(); + }); + + test('mute가 함수가 아니면 아무 동작도 하지 않는다', () => { + const player = createMockPlayer({ + getInternalPlayer: jest.fn(() => ({ mute: 'not-a-function' })), + }); + api.setPlayer(player); + expect(() => api.setMuted(true)).not.toThrow(); + }); + }); + + describe('setVolume', () => { + test('유효한 볼륨 값을 설정한다', () => { + const player = createMockPlayer(); + api.setPlayer(player); + api.setVolume(50); + expect(player.__internalPlayer.setVolume).toHaveBeenCalledWith(50); + }); + + test('150 → 100으로 클램핑한다', () => { + const player = createMockPlayer(); + api.setPlayer(player); + api.setVolume(150); + expect(player.__internalPlayer.setVolume).toHaveBeenCalledWith(100); + }); + + test('-5 → 0으로 클램핑한다', () => { + const player = createMockPlayer(); + api.setPlayer(player); + api.setVolume(-5); + expect(player.__internalPlayer.setVolume).toHaveBeenCalledWith(0); + }); + }); + + describe('getCurrentTime', () => { + test('player가 null이면 0을 반환한다', () => { + expect(api.getCurrentTime()).toBe(0); + }); + + test('player가 있으면 위임값을 반환한다', () => { + api.setPlayer(createMockPlayer()); + expect(api.getCurrentTime()).toBe(42); + }); + }); + + describe('getDuration', () => { + test('player가 null이면 0을 반환한다', () => { + expect(api.getDuration()).toBe(0); + }); + + test('player가 있으면 위임값을 반환한다', () => { + api.setPlayer(createMockPlayer()); + expect(api.getDuration()).toBe(180); + }); + }); + + describe('cleanup', () => { + test('player를 null로 초기화한다', () => { + api.setPlayer(createMockPlayer()); + api.cleanup(); + expect(api.isReady()).toBe(false); + }); + }); +}); diff --git a/src/entities/partyroom-client/lib/handle-subscription-event.test.ts b/src/entities/partyroom-client/lib/handle-subscription-event.test.ts new file mode 100644 index 00000000..b204e4e3 --- /dev/null +++ b/src/entities/partyroom-client/lib/handle-subscription-event.test.ts @@ -0,0 +1,238 @@ +jest.mock('@/shared/lib/functions/log/logger', () => ({ + specificLog: jest.fn(), + warnLog: jest.fn(), +})); + +jest.mock('@/shared/lib/functions/log/with-debugger', () => ({ + __esModule: true, + default: () => (fn: any) => fn, +})); + +jest.mock('./subscription-callbacks/use-chat-callback.hook', () => ({ + __esModule: true, + default: jest.fn(), +})); +jest.mock('./subscription-callbacks/use-crew-grade-callback.hook', () => ({ + __esModule: true, + default: jest.fn(), +})); +jest.mock('./subscription-callbacks/use-crew-penalty-callback.hook', () => ({ + __esModule: true, + default: jest.fn(), +})); +jest.mock('./subscription-callbacks/use-crew-profile-callback.hook', () => ({ + __esModule: true, + default: jest.fn(), +})); +jest.mock('./subscription-callbacks/use-partyroom-access-callback.hook', () => ({ + __esModule: true, + default: jest.fn(), +})); +jest.mock('./subscription-callbacks/use-partyroom-close-callback.hook', () => ({ + __esModule: true, + default: jest.fn(), +})); +jest.mock('./subscription-callbacks/use-partyroom-deactivation-callback.hook', () => ({ + __esModule: true, + default: jest.fn(), +})); +jest.mock('./subscription-callbacks/use-partyroom-notice-callback.hook', () => ({ + __esModule: true, + default: jest.fn(), +})); +jest.mock('./subscription-callbacks/use-playback-skip-callback.hook', () => ({ + __esModule: true, + default: jest.fn(), +})); +jest.mock('./subscription-callbacks/use-playback-start-callback.hook', () => ({ + __esModule: true, + default: jest.fn(), +})); +jest.mock('./subscription-callbacks/use-reaction-aggregation-callback.hook', () => ({ + __esModule: true, + default: jest.fn(), +})); +jest.mock('./subscription-callbacks/use-reaction-motion-callback.hook', () => ({ + __esModule: true, + default: jest.fn(), +})); + +import { renderHook } from '@testing-library/react'; +import { PartyroomEventType } from '@/shared/api/websocket/types/partyroom'; +import { warnLog } from '@/shared/lib/functions/log/logger'; +import useHandleSubscriptionEvent from './handle-subscription-event'; +import useChatCallback from './subscription-callbacks/use-chat-callback.hook'; +import useCrewGradeCallback from './subscription-callbacks/use-crew-grade-callback.hook'; +import useCrewPenaltyCallback from './subscription-callbacks/use-crew-penalty-callback.hook'; +import useCrewProfileCallback from './subscription-callbacks/use-crew-profile-callback.hook'; +import usePartyroomAccessCallback from './subscription-callbacks/use-partyroom-access-callback.hook'; +import usePartyroomCloseCallback from './subscription-callbacks/use-partyroom-close-callback.hook'; +import usePartyroomDeactivationCallback from './subscription-callbacks/use-partyroom-deactivation-callback.hook'; +import usePartyroomNoticeCallback from './subscription-callbacks/use-partyroom-notice-callback.hook'; +import usePlaybackSkipCallback from './subscription-callbacks/use-playback-skip-callback.hook'; +import usePlaybackStartCallback from './subscription-callbacks/use-playback-start-callback.hook'; +import useReactionAggregationCallback from './subscription-callbacks/use-reaction-aggregation-callback.hook'; +import useReactionMotionCallback from './subscription-callbacks/use-reaction-motion-callback.hook'; + +type EventTypeToHook = { + eventType: PartyroomEventType; + hook: jest.Mock; + label: string; +}; + +const CALLBACK_MAP: EventTypeToHook[] = [ + { + eventType: PartyroomEventType.PARTYROOM_CLOSE, + hook: usePartyroomCloseCallback as jest.Mock, + label: 'usePartyroomCloseCallback', + }, + { + eventType: PartyroomEventType.PARTYROOM_DEACTIVATION, + hook: usePartyroomDeactivationCallback as jest.Mock, + label: 'usePartyroomDeactivationCallback', + }, + { + eventType: PartyroomEventType.PARTYROOM_ACCESS, + hook: usePartyroomAccessCallback as jest.Mock, + label: 'usePartyroomAccessCallback', + }, + { + eventType: PartyroomEventType.PARTYROOM_NOTICE, + hook: usePartyroomNoticeCallback as jest.Mock, + label: 'usePartyroomNoticeCallback', + }, + { + eventType: PartyroomEventType.REACTION_AGGREGATION, + hook: useReactionAggregationCallback as jest.Mock, + label: 'useReactionAggregationCallback', + }, + { + eventType: PartyroomEventType.REACTION_MOTION, + hook: useReactionMotionCallback as jest.Mock, + label: 'useReactionMotionCallback', + }, + { + eventType: PartyroomEventType.PLAYBACK_START, + hook: usePlaybackStartCallback as jest.Mock, + label: 'usePlaybackStartCallback', + }, + { + eventType: PartyroomEventType.PLAYBACK_SKIP, + hook: usePlaybackSkipCallback as jest.Mock, + label: 'usePlaybackSkipCallback', + }, + { + eventType: PartyroomEventType.CHAT, + hook: useChatCallback as jest.Mock, + label: 'useChatCallback', + }, + { + eventType: PartyroomEventType.CREW_GRADE, + hook: useCrewGradeCallback as jest.Mock, + label: 'useCrewGradeCallback', + }, + { + eventType: PartyroomEventType.CREW_PENALTY, + hook: useCrewPenaltyCallback as jest.Mock, + label: 'useCrewPenaltyCallback', + }, + { + eventType: PartyroomEventType.CREW_PROFILE, + hook: useCrewProfileCallback as jest.Mock, + label: 'useCrewProfileCallback', + }, +]; + +function createMessage(body: string) { + return { body } as any; +} + +function setupCallbacks() { + const callbacks = new Map(); + + for (const { eventType, hook } of CALLBACK_MAP) { + const cb = jest.fn(); + hook.mockReturnValue(cb); + callbacks.set(eventType, cb); + } + + return callbacks; +} + +describe('useHandleSubscriptionEvent', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + test('유효한 JSON + 알려진 eventType → 해당 콜백이 호출된다', () => { + const callbacks = setupCallbacks(); + const { result } = renderHook(() => useHandleSubscriptionEvent()); + const handler = result.current; + + const event = { eventType: PartyroomEventType.CHAT, message: { content: 'hi' } }; + handler(createMessage(JSON.stringify(event))); + + expect(callbacks.get(PartyroomEventType.CHAT)).toHaveBeenCalledWith(event); + }); + + test('유효한 JSON + eventType 없음 → 아무 콜백도 호출되지 않는다', () => { + const callbacks = setupCallbacks(); + const { result } = renderHook(() => useHandleSubscriptionEvent()); + const handler = result.current; + + handler(createMessage(JSON.stringify({ data: 'no event type' }))); + + for (const cb of callbacks.values()) { + expect(cb).not.toHaveBeenCalled(); + } + }); + + test('유효한 JSON + 알 수 없는 eventType → warn 로그 + 아무 콜백도 호출되지 않는다', () => { + const callbacks = setupCallbacks(); + const { result } = renderHook(() => useHandleSubscriptionEvent()); + const handler = result.current; + + handler(createMessage(JSON.stringify({ eventType: 'UNKNOWN_EVENT' }))); + + expect(warnLog).toHaveBeenCalled(); + for (const cb of callbacks.values()) { + expect(cb).not.toHaveBeenCalled(); + } + }); + + test('유효하지 않은 JSON → 파싱 실패 warn + 아무 콜백도 호출되지 않는다', () => { + const callbacks = setupCallbacks(); + const { result } = renderHook(() => useHandleSubscriptionEvent()); + const handler = result.current; + + handler(createMessage('not valid json{{{')); + + expect(warnLog).toHaveBeenCalled(); + for (const cb of callbacks.values()) { + expect(cb).not.toHaveBeenCalled(); + } + }); + + describe.each(CALLBACK_MAP.map(({ eventType, label }) => ({ eventType, label })))( + '$eventType', + ({ eventType, label }) => { + test(`${label}이 올바르게 호출된다`, () => { + const callbacks = setupCallbacks(); + const { result } = renderHook(() => useHandleSubscriptionEvent()); + const handler = result.current; + + const event = { eventType }; + handler(createMessage(JSON.stringify(event))); + + expect(callbacks.get(eventType)).toHaveBeenCalledWith(event); + + // 다른 콜백은 호출되지 않아야 한다 + for (const [type, cb] of callbacks.entries()) { + if (type !== eventType) { + expect(cb).not.toHaveBeenCalled(); + } + } + }); + } + ); +}); diff --git a/src/entities/partyroom-client/lib/partyroom-client.test.ts b/src/entities/partyroom-client/lib/partyroom-client.test.ts new file mode 100644 index 00000000..601f34be --- /dev/null +++ b/src/entities/partyroom-client/lib/partyroom-client.test.ts @@ -0,0 +1,81 @@ +jest.mock('@/shared/api/websocket/client'); + +import SocketClient from '@/shared/api/websocket/client'; +import PartyroomClient from './partyroom-client'; + +const MockSocketClient = SocketClient as jest.MockedClass; + +describe('PartyroomClient', () => { + let client: PartyroomClient; + let mockSocketInstance: jest.Mocked; + + beforeEach(() => { + jest.clearAllMocks(); + client = new PartyroomClient(); + mockSocketInstance = MockSocketClient.mock.instances[0] as jest.Mocked; + }); + + test('connect() → socketClient.connect()를 위임한다', () => { + client.connect(); + expect(mockSocketInstance.connect).toHaveBeenCalled(); + }); + + test('connected → socketClient.connected를 위임한다', () => { + Object.defineProperty(mockSocketInstance, 'connected', { get: () => true }); + expect(client.connected).toBe(true); + }); + + test('onConnect → socketClient.onConnect를 위임한다', () => { + const callback = jest.fn(); + const options = { once: true }; + client.onConnect(callback, options); + expect(mockSocketInstance.onConnect).toHaveBeenCalledWith(callback, options); + }); + + describe('subscribe', () => { + test('이미 구독이 있으면 Error를 throw한다', () => { + mockSocketInstance.subscriptions = [{ destination: '/sub/partyrooms/1' } as any]; + + expect(() => client.subscribe(2, jest.fn())).toThrow( + 'Cannot connect to multiple partyrooms at the same time.' + ); + }); + + test('구독이 없으면 올바른 경로로 subscribe를 호출한다', () => { + mockSocketInstance.subscriptions = []; + const handler = jest.fn(); + + client.subscribe(42, handler); + + expect(mockSocketInstance.subscribe).toHaveBeenCalledWith('/sub/partyrooms/42', handler); + }); + }); + + test('unsubscribeCurrentRoom → 올바른 경로로 unsubscribe를 호출한다', () => { + mockSocketInstance.subscriptions = []; + client.subscribe(10, jest.fn()); + + client.unsubscribeCurrentRoom(); + + expect(mockSocketInstance.unsubscribe).toHaveBeenCalledWith('/sub/partyrooms/10'); + }); + + describe('sendChatMessage', () => { + test('구독 전에 호출하면 Error를 throw한다', () => { + expect(() => client.sendChatMessage('hello')).toThrow( + 'Cannot send chat message without subscribing to a partyroom.' + ); + }); + + test('구독 후에 호출하면 올바른 경로와 내용으로 send를 호출한다', () => { + mockSocketInstance.subscriptions = []; + client.subscribe(7, jest.fn()); + + client.sendChatMessage('hi there'); + + expect(mockSocketInstance.send).toHaveBeenCalledWith('/pub/groups/7/send', { + content: 'hi there', + }); + }); + }); +}); diff --git a/src/shared/api/websocket/client.test.ts b/src/shared/api/websocket/client.test.ts new file mode 100644 index 00000000..2d76f8ba --- /dev/null +++ b/src/shared/api/websocket/client.test.ts @@ -0,0 +1,215 @@ +jest.mock('@stomp/stompjs', () => { + return { + Client: jest.fn().mockImplementation((config: any) => ({ + connected: false, + activate: jest.fn(), + deactivate: jest.fn(), + subscribe: jest.fn((dest: string) => ({ + id: `sub-${dest}`, + unsubscribe: jest.fn(), + destination: dest, + })), + publish: jest.fn(), + // 설정 저장하여 나중에 핸들러를 호출할 수 있도록 한다 + __config: config, + })), + }; +}); + +jest.mock('@/shared/lib/functions/log/logger', () => ({ + specificLog: jest.fn(), + warnLog: jest.fn(), +})); + +jest.mock('@/shared/lib/functions/log/with-debugger', () => ({ + __esModule: true, + default: () => (fn: any) => fn, +})); + +import SocketClient from './client'; + +function getStompClient(socketClient: SocketClient): any { + return (socketClient as any).client; +} + +function triggerConnect(socketClient: SocketClient) { + const stompClient = getStompClient(socketClient); + stompClient.connected = true; + stompClient.__config.onConnect(); +} + +describe('SocketClient', () => { + beforeEach(() => { + jest.clearAllMocks(); + jest.useFakeTimers(); + }); + + afterEach(() => { + jest.useRealTimers(); + }); + + describe('connect', () => { + test('연결 안 됨 → activate()를 호출한다', () => { + const sc = new SocketClient(); + sc.connect(); + expect(getStompClient(sc).activate).toHaveBeenCalled(); + }); + + test('이미 연결됨 → activate()를 호출하지 않는다', () => { + const sc = new SocketClient(); + getStompClient(sc).connected = true; + sc.connect(); + expect(getStompClient(sc).activate).not.toHaveBeenCalled(); + }); + }); + + describe('disconnect', () => { + test('연결됨 → deactivate()를 호출한다', async () => { + const sc = new SocketClient(); + getStompClient(sc).connected = true; + await sc.disconnect(); + expect(getStompClient(sc).deactivate).toHaveBeenCalled(); + }); + + test('연결 안 됨 → deactivate()를 호출하지 않는다', async () => { + const sc = new SocketClient(); + await sc.disconnect(); + expect(getStompClient(sc).deactivate).not.toHaveBeenCalled(); + }); + }); + + describe('onConnect', () => { + test('이미 연결 상태 → 콜백을 즉시 실행한다', () => { + const sc = new SocketClient(); + getStompClient(sc).connected = true; + const callback = jest.fn(); + + sc.onConnect(callback); + + expect(callback).toHaveBeenCalled(); + }); + + test('미연결 상태 → 큐에 추가 후 연결 시 실행한다', () => { + const sc = new SocketClient(); + const callback = jest.fn(); + + sc.onConnect(callback); + expect(callback).not.toHaveBeenCalled(); + + triggerConnect(sc); + expect(callback).toHaveBeenCalled(); + }); + + test('once: true + 이미 연결 → 즉시 실행하고 큐에 추가하지 않는다', () => { + const sc = new SocketClient(); + getStompClient(sc).connected = true; + const callback = jest.fn(); + + sc.onConnect(callback, { once: true }); + expect(callback).toHaveBeenCalledTimes(1); + + // handleConnect를 다시 호출해도 once 콜백은 다시 실행되지 않아야 한다 + callback.mockClear(); + triggerConnect(sc); + expect(callback).not.toHaveBeenCalled(); + }); + }); + + describe('handleConnect (내부)', () => { + test('큐의 모든 콜백을 실행하고 once 항목을 제거한다', () => { + const sc = new SocketClient(); + const persistent = jest.fn(); + const once = jest.fn(); + + sc.onConnect(persistent); + sc.onConnect(once, { once: true }); + + triggerConnect(sc); + expect(persistent).toHaveBeenCalledTimes(1); + expect(once).toHaveBeenCalledTimes(1); + + // 두 번째 connect + persistent.mockClear(); + once.mockClear(); + triggerConnect(sc); + expect(persistent).toHaveBeenCalledTimes(1); + expect(once).not.toHaveBeenCalled(); + }); + }); + + describe('subscribe', () => { + test('onConnect를 경유하여 client.subscribe를 호출하고 subscriptions에 추가한다', () => { + const sc = new SocketClient(); + const handler = jest.fn(); + + sc.subscribe('/sub/test' as any, handler); + triggerConnect(sc); + + expect(getStompClient(sc).subscribe).toHaveBeenCalledWith('/sub/test', handler); + expect(sc.subscriptions).toHaveLength(1); + expect(sc.subscriptions[0].destination).toBe('/sub/test'); + }); + }); + + describe('unsubscribe', () => { + test('연결 안 됨 → 아무 동작도 하지 않는다', () => { + const sc = new SocketClient(); + sc.unsubscribe('/sub/test' as any); + // 에러 없이 통과 + }); + + test('해당 destination이 없으면 아무 동작도 하지 않는다', () => { + const sc = new SocketClient(); + triggerConnect(sc); + sc.unsubscribe('/sub/nonexistent' as any); + // 에러 없이 통과 + }); + + test('해당 destination이 있으면 해제하고 배열에서 제거한다', () => { + const sc = new SocketClient(); + sc.subscribe('/sub/room' as any, jest.fn()); + triggerConnect(sc); + + expect(sc.subscriptions).toHaveLength(1); + const unsubFn = sc.subscriptions[0].unsubscribe; + + sc.unsubscribe('/sub/room' as any); + + expect(unsubFn).toHaveBeenCalled(); + expect(sc.subscriptions).toHaveLength(0); + }); + }); + + describe('unsubscribeAll', () => { + test('모든 구독을 해제하고 배열을 초기화한다', () => { + const sc = new SocketClient(); + sc.subscribe('/sub/a' as any, jest.fn()); + sc.subscribe('/sub/b' as any, jest.fn()); + triggerConnect(sc); + + expect(sc.subscriptions).toHaveLength(2); + const unsub0 = sc.subscriptions[0].unsubscribe; + const unsub1 = sc.subscriptions[1].unsubscribe; + + sc.unsubscribeAll(); + + expect(unsub0).toHaveBeenCalled(); + expect(unsub1).toHaveBeenCalled(); + expect(sc.subscriptions).toHaveLength(0); + }); + }); + + describe('send', () => { + test('onConnect({ once: true })를 경유하여 client.publish를 호출한다', () => { + const sc = new SocketClient(); + sc.send('/pub/test' as any, { data: 'hello' }); + + triggerConnect(sc); + + expect(getStompClient(sc).publish).toHaveBeenCalledWith({ + destination: '/pub/test', + body: JSON.stringify({ data: 'hello' }), + }); + }); + }); +}); diff --git a/src/shared/lib/chat/lib/circular-buffer-adapter.test.ts b/src/shared/lib/chat/lib/circular-buffer-adapter.test.ts new file mode 100644 index 00000000..b525312c --- /dev/null +++ b/src/shared/lib/chat/lib/circular-buffer-adapter.test.ts @@ -0,0 +1,48 @@ +import CircularBuffer from './circular-buffer'; +import CircularBufferAdapter from './circular-buffer-adapter'; + +describe('CircularBufferAdapter', () => { + let adapter: CircularBufferAdapter; + + beforeEach(() => { + adapter = new CircularBufferAdapter(new CircularBuffer([], 10)); + }); + + test('append → list에 반영된다', () => { + adapter.append('hello'); + adapter.append('world'); + expect(adapter.list).toEqual(['hello', 'world']); + }); + + test('clear → list가 비어진다', () => { + adapter.append('a'); + adapter.append('b'); + adapter.clear(); + expect(adapter.list).toEqual([]); + }); + + test('update → 조건에 맞는 항목을 변경하고 변경된 항목을 반환한다', () => { + adapter.append('apple'); + adapter.append('banana'); + + const result = adapter.update( + (msg) => msg === 'apple', + () => 'APPLE' + ); + + expect(result).toBe('APPLE'); + expect(adapter.list).toEqual(['APPLE', 'banana']); + }); + + test('update → 조건에 맞는 항목이 없으면 undefined를 반환한다', () => { + adapter.append('apple'); + + const result = adapter.update( + (msg) => msg === 'cherry', + () => 'CHERRY' + ); + + expect(result).toBeUndefined(); + expect(adapter.list).toEqual(['apple']); + }); +}); diff --git a/src/shared/lib/chat/lib/observer-adapter.test.ts b/src/shared/lib/chat/lib/observer-adapter.test.ts new file mode 100644 index 00000000..94e7f04e --- /dev/null +++ b/src/shared/lib/chat/lib/observer-adapter.test.ts @@ -0,0 +1,54 @@ +import ObserverAdapter from './observer-adapter'; +import Observer from '../../functions/observer'; +import type { ChatObserverEvent } from '../model/chat-message-listener.model'; + +describe('ObserverAdapter', () => { + let adapter: ObserverAdapter; + + beforeEach(() => { + adapter = new ObserverAdapter(new Observer>()); + }); + + test('register + notify → 리스너가 event.message만 수신한다', () => { + const listener = jest.fn(); + adapter.register(listener); + + adapter.notify({ type: 'add', message: 'hello' }); + + expect(listener).toHaveBeenCalledWith('hello'); + }); + + test('복수 리스너 등록 → 모두 호출된다', () => { + const listener1 = jest.fn(); + const listener2 = jest.fn(); + adapter.register(listener1); + adapter.register(listener2); + + adapter.notify({ type: 'add', message: 'test' }); + + expect(listener1).toHaveBeenCalledWith('test'); + expect(listener2).toHaveBeenCalledWith('test'); + }); + + test('deregisterAll → 이후 notify 시 리스너가 호출되지 않는다', () => { + const listener = jest.fn(); + adapter.register(listener); + adapter.deregisterAll(); + + adapter.notify({ type: 'add', message: 'hello' }); + + expect(listener).not.toHaveBeenCalled(); + }); + + test('deregister → 참조 불일치로 리스너가 제거되지 않는다 (known limitation)', () => { + const listener = jest.fn(); + adapter.register(listener); + adapter.deregister(listener); + + // deregister는 새로운 래퍼 함수를 생성하므로 원래 래퍼와 참조가 다르다. + // 따라서 리스너가 제거되지 않고 여전히 호출된다. + adapter.notify({ type: 'update', message: 'still here' }); + + expect(listener).toHaveBeenCalledWith('still here'); + }); +}); diff --git a/src/shared/lib/localization/renderer/processors/variable-processor-util.test.ts b/src/shared/lib/localization/renderer/processors/variable-processor-util.test.ts new file mode 100644 index 00000000..76458beb --- /dev/null +++ b/src/shared/lib/localization/renderer/processors/variable-processor-util.test.ts @@ -0,0 +1,27 @@ +import { processI18nString } from './variable-processor-util'; + +describe('processI18nString', () => { + test('단일 변수를 치환한다', () => { + expect(processI18nString('Hello, {{name}}!', { name: 'World' })).toBe('Hello, World!'); + }); + + test('복수 변수를 치환한다', () => { + expect(processI18nString('{{greeting}}, {{name}}!', { greeting: 'Hi', name: 'Alice' })).toBe( + 'Hi, Alice!' + ); + }); + + test('누락된 키는 빈 문자열로 치환한다', () => { + expect(processI18nString('Hello, {{name}}!', {})).toBe('Hello, !'); + }); + + test('플레이스홀더가 없는 문자열은 그대로 반환한다', () => { + expect(processI18nString('No placeholders here', { name: 'World' })).toBe( + 'No placeholders here' + ); + }); + + test('빈 문자열을 입력하면 빈 문자열을 반환한다', () => { + expect(processI18nString('', { name: 'World' })).toBe(''); + }); +}); From 25de6fb014ffae859e3ba0c9567e3fe908bc3c04 Mon Sep 17 00:00:00 2001 From: LivingLikeKrillin <143606756+LivingLikeKrillin@users.noreply.github.com> Date: Mon, 2 Mar 2026 10:13:02 +0900 Subject: [PATCH 10/41] =?UTF-8?q?test:=20subscription=20=EC=BD=9C=EB=B0=B1?= =?UTF-8?q?=2011=EA=B0=9C=20+=20=EA=B3=B5=EC=9C=A0=20=ED=9B=85=202?= =?UTF-8?q?=EA=B0=9C=20+=20=EC=9D=B8=ED=84=B0=EC=85=89=ED=84=B0=201?= =?UTF-8?q?=EA=B0=9C=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=B6=94=EA=B0=80=20?= =?UTF-8?q?(14=ED=8C=8C=EC=9D=BC=2039=EC=BC=80=EC=9D=B4=EC=8A=A4)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.6 --- .../use-chat-callback.hook.test.ts | 84 ++++++++ .../use-crew-grade-callback.hook.test.ts | 189 ++++++++++++++++++ .../use-crew-penalty-callback.hook.test.ts | 167 ++++++++++++++++ .../use-crew-profile-callback.hook.test.ts | 99 +++++++++ ...use-partyroom-access-callback.hook.test.ts | 97 +++++++++ .../use-partyroom-close-callback.hook.test.ts | 107 ++++++++++ ...rtyroom-deactivation-callback.hook.test.ts | 71 +++++++ ...use-partyroom-notice-callback.hook.test.ts | 44 ++++ .../use-playback-start-callback.hook.test.ts | 112 +++++++++++ ...reaction-aggregation-callback.hook.test.ts | 61 ++++++ .../use-reaction-motion-callback.hook.test.ts | 76 +++++++ .../http/client/interceptors/request.test.ts | 71 +++++++ .../lib/hooks/use-click-outside.hook.test.tsx | 72 +++++++ .../use-isomorphic-layout-effect.hook.test.ts | 9 + 14 files changed, 1259 insertions(+) create mode 100644 src/entities/partyroom-client/lib/subscription-callbacks/use-chat-callback.hook.test.ts create mode 100644 src/entities/partyroom-client/lib/subscription-callbacks/use-crew-grade-callback.hook.test.ts create mode 100644 src/entities/partyroom-client/lib/subscription-callbacks/use-crew-penalty-callback.hook.test.ts create mode 100644 src/entities/partyroom-client/lib/subscription-callbacks/use-crew-profile-callback.hook.test.ts create mode 100644 src/entities/partyroom-client/lib/subscription-callbacks/use-partyroom-access-callback.hook.test.ts create mode 100644 src/entities/partyroom-client/lib/subscription-callbacks/use-partyroom-close-callback.hook.test.ts create mode 100644 src/entities/partyroom-client/lib/subscription-callbacks/use-partyroom-deactivation-callback.hook.test.ts create mode 100644 src/entities/partyroom-client/lib/subscription-callbacks/use-partyroom-notice-callback.hook.test.ts create mode 100644 src/entities/partyroom-client/lib/subscription-callbacks/use-playback-start-callback.hook.test.ts create mode 100644 src/entities/partyroom-client/lib/subscription-callbacks/use-reaction-aggregation-callback.hook.test.ts create mode 100644 src/entities/partyroom-client/lib/subscription-callbacks/use-reaction-motion-callback.hook.test.ts create mode 100644 src/shared/api/http/client/interceptors/request.test.ts create mode 100644 src/shared/lib/hooks/use-click-outside.hook.test.tsx create mode 100644 src/shared/lib/hooks/use-isomorphic-layout-effect.hook.test.ts diff --git a/src/entities/partyroom-client/lib/subscription-callbacks/use-chat-callback.hook.test.ts b/src/entities/partyroom-client/lib/subscription-callbacks/use-chat-callback.hook.test.ts new file mode 100644 index 00000000..9b264482 --- /dev/null +++ b/src/entities/partyroom-client/lib/subscription-callbacks/use-chat-callback.hook.test.ts @@ -0,0 +1,84 @@ +jest.mock('@/shared/lib/store/stores.context'); +jest.mock('@/shared/lib/functions/log/logger', () => ({ + warnLog: jest.fn(), +})); +jest.mock('@/shared/lib/functions/log/with-debugger', () => ({ + __esModule: true, + // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type + default: () => (logFn: Function) => logFn, +})); + +import { renderHook } from '@testing-library/react'; +import type * as Crew from '@/entities/current-partyroom/model/crew.model'; +import { createCurrentPartyroomStore } from '@/entities/current-partyroom/model/current-partyroom.store'; +import { GradeType, MotionType } from '@/shared/api/http/types/@enums'; +import { PartyroomEventType } from '@/shared/api/websocket/types/partyroom'; +import { warnLog } from '@/shared/lib/functions/log/logger'; +import { useStores } from '@/shared/lib/store/stores.context'; +import useChatCallback from './use-chat-callback.hook'; + +let store: ReturnType; + +beforeEach(() => { + jest.clearAllMocks(); + store = createCurrentPartyroomStore(); + (useStores as jest.Mock).mockReturnValue({ useCurrentPartyroom: store }); +}); + +const createCrew = (overrides: Partial = {}): Crew.Model => ({ + crewId: 1, + nickname: '테스트유저', + gradeType: GradeType.CLUBBER, + avatarBodyUri: 'body.png', + avatarFaceUri: 'face.png', + avatarIconUri: 'icon.png', + combinePositionX: 0, + combinePositionY: 0, + offsetX: 0, + offsetY: 0, + scale: 1, + motionType: MotionType.NONE, + ...overrides, +}); + +describe('useChatCallback', () => { + test('크루를 찾아서 user 채팅 메시지를 append한다', () => { + const crew = createCrew({ crewId: 5, nickname: '채팅유저' }); + store.getState().updateCrews(() => [crew]); + + const { result } = renderHook(() => useChatCallback()); + const callback = result.current; + + callback({ + eventType: PartyroomEventType.CHAT, + partyroomId: { id: 1 }, + crew: { crewId: 5 }, + message: { messageId: 'msg-1', content: '안녕하세요' }, + }); + + const messages = store.getState().chat.getMessages(); + expect(messages).toHaveLength(1); + expect(messages[0].from).toBe('user'); + if (messages[0].from === 'user') { + expect(messages[0].crew).toEqual(crew); + expect(messages[0].message).toEqual({ messageId: 'msg-1', content: '안녕하세요' }); + } + }); + + test('크루를 찾지 못하면 warn 로그 + 메시지 append하지 않음', () => { + store.getState().updateCrews(() => [createCrew({ crewId: 1 })]); + + const { result } = renderHook(() => useChatCallback()); + const callback = result.current; + + callback({ + eventType: PartyroomEventType.CHAT, + partyroomId: { id: 1 }, + crew: { crewId: 999 }, + message: { messageId: 'msg-2', content: '메시지' }, + }); + + expect(warnLog).toHaveBeenCalled(); + expect(store.getState().chat.getMessages()).toHaveLength(0); + }); +}); diff --git a/src/entities/partyroom-client/lib/subscription-callbacks/use-crew-grade-callback.hook.test.ts b/src/entities/partyroom-client/lib/subscription-callbacks/use-crew-grade-callback.hook.test.ts new file mode 100644 index 00000000..05ea4afd --- /dev/null +++ b/src/entities/partyroom-client/lib/subscription-callbacks/use-crew-grade-callback.hook.test.ts @@ -0,0 +1,189 @@ +jest.mock('@/shared/lib/store/stores.context'); +jest.mock('@/shared/lib/functions/log/logger', () => ({ + errorLog: jest.fn(), +})); +jest.mock('@/shared/lib/functions/log/with-debugger', () => ({ + __esModule: true, + // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type + default: () => (logFn: Function) => logFn, +})); + +import { renderHook } from '@testing-library/react'; +import type * as Crew from '@/entities/current-partyroom/model/crew.model'; +import { createCurrentPartyroomStore } from '@/entities/current-partyroom/model/current-partyroom.store'; +import { GradeType, MotionType } from '@/shared/api/http/types/@enums'; +import { PartyroomEventType } from '@/shared/api/websocket/types/partyroom'; +import { errorLog } from '@/shared/lib/functions/log/logger'; +import { useStores } from '@/shared/lib/store/stores.context'; +import useCrewGradeCallback from './use-crew-grade-callback.hook'; + +let store: ReturnType; + +beforeEach(() => { + jest.clearAllMocks(); + store = createCurrentPartyroomStore(); + (useStores as jest.Mock).mockReturnValue({ useCurrentPartyroom: store }); +}); + +const createCrew = (overrides: Partial = {}): Crew.Model => ({ + crewId: 1, + nickname: '테스트유저', + gradeType: GradeType.CLUBBER, + avatarBodyUri: 'body.png', + avatarFaceUri: 'face.png', + avatarIconUri: 'icon.png', + combinePositionX: 0, + combinePositionY: 0, + offsetX: 0, + offsetY: 0, + scale: 1, + motionType: MotionType.NONE, + ...overrides, +}); + +describe('useCrewGradeCallback', () => { + test('대상 크루의 gradeType을 업데이트한다', () => { + store + .getState() + .updateCrews(() => [ + createCrew({ crewId: 1, nickname: '관리자', gradeType: GradeType.HOST }), + createCrew({ crewId: 2, nickname: '대상유저', gradeType: GradeType.CLUBBER }), + ]); + + const { result } = renderHook(() => useCrewGradeCallback()); + const callback = result.current; + + callback({ + eventType: PartyroomEventType.CREW_GRADE, + adjuster: { crewId: 1 }, + adjusted: { + crewId: 2, + prevGradeType: GradeType.CLUBBER, + currGradeType: GradeType.MODERATOR, + }, + }); + + const crews = store.getState().crews; + expect(crews[1].gradeType).toBe(GradeType.MODERATOR); + // adjuster의 등급은 변경되지 않음 + expect(crews[0].gradeType).toBe(GradeType.HOST); + }); + + test('등급 변경 시스템 채팅 메시지를 append한다', () => { + store + .getState() + .updateCrews(() => [ + createCrew({ crewId: 1, nickname: '관리자' }), + createCrew({ crewId: 2, nickname: '대상유저' }), + ]); + + const { result } = renderHook(() => useCrewGradeCallback()); + const callback = result.current; + + callback({ + eventType: PartyroomEventType.CREW_GRADE, + adjuster: { crewId: 1 }, + adjusted: { + crewId: 2, + prevGradeType: GradeType.CLUBBER, + currGradeType: GradeType.MODERATOR, + }, + }); + + const messages = store.getState().chat.getMessages(); + expect(messages).toHaveLength(1); + expect(messages[0].from).toBe('system'); + if (messages[0].from === 'system') { + expect(messages[0].content).toContain('관리자'); + expect(messages[0].content).toContain('대상유저'); + expect(messages[0].content).toContain('Mod'); + } + }); + + test('조정 대상이 본인이면 me.gradeType도 업데이트 + alert.notify 호출', () => { + store.getState().init({ + id: 1, + me: { crewId: 2, gradeType: GradeType.CLUBBER }, + playbackActivated: false, + crews: [ + createCrew({ crewId: 1, nickname: '관리자' }), + createCrew({ crewId: 2, nickname: '본인' }), + ], + notice: '', + }); + + const alertNotify = jest.spyOn(store.getState().alert, 'notify'); + + const { result } = renderHook(() => useCrewGradeCallback()); + const callback = result.current; + + callback({ + eventType: PartyroomEventType.CREW_GRADE, + adjuster: { crewId: 1 }, + adjusted: { + crewId: 2, + prevGradeType: GradeType.CLUBBER, + currGradeType: GradeType.MODERATOR, + }, + }); + + expect(store.getState().me?.gradeType).toBe(GradeType.MODERATOR); + expect(alertNotify).toHaveBeenCalledWith({ + type: 'grade-adjusted', + prev: GradeType.CLUBBER, + next: GradeType.MODERATOR, + }); + }); + + test('조정 대상이 본인이 아니면 me 업데이트/alert 호출 안 함', () => { + store.getState().init({ + id: 1, + me: { crewId: 3, gradeType: GradeType.CLUBBER }, + playbackActivated: false, + crews: [ + createCrew({ crewId: 1, nickname: '관리자' }), + createCrew({ crewId: 2, nickname: '대상유저' }), + createCrew({ crewId: 3, nickname: '본인' }), + ], + notice: '', + }); + + const alertNotify = jest.spyOn(store.getState().alert, 'notify'); + + const { result } = renderHook(() => useCrewGradeCallback()); + const callback = result.current; + + callback({ + eventType: PartyroomEventType.CREW_GRADE, + adjuster: { crewId: 1 }, + adjusted: { + crewId: 2, + prevGradeType: GradeType.CLUBBER, + currGradeType: GradeType.MODERATOR, + }, + }); + + expect(store.getState().me?.gradeType).toBe(GradeType.CLUBBER); + expect(alertNotify).not.toHaveBeenCalled(); + }); + + test('adjuster/adjusted를 찾지 못하면 에러 로그 + 아무 동작 안 함', () => { + store.getState().updateCrews(() => [createCrew({ crewId: 1 })]); + + const { result } = renderHook(() => useCrewGradeCallback()); + const callback = result.current; + + callback({ + eventType: PartyroomEventType.CREW_GRADE, + adjuster: { crewId: 999 }, + adjusted: { + crewId: 888, + prevGradeType: GradeType.CLUBBER, + currGradeType: GradeType.MODERATOR, + }, + }); + + expect(errorLog).toHaveBeenCalledWith('Adjuster or Adjusted not found'); + expect(store.getState().chat.getMessages()).toHaveLength(0); + }); +}); diff --git a/src/entities/partyroom-client/lib/subscription-callbacks/use-crew-penalty-callback.hook.test.ts b/src/entities/partyroom-client/lib/subscription-callbacks/use-crew-penalty-callback.hook.test.ts new file mode 100644 index 00000000..a494da6c --- /dev/null +++ b/src/entities/partyroom-client/lib/subscription-callbacks/use-crew-penalty-callback.hook.test.ts @@ -0,0 +1,167 @@ +jest.mock('@/shared/lib/store/stores.context'); +jest.mock('@/shared/lib/functions/log/logger', () => ({ + errorLog: jest.fn(), +})); +jest.mock('@/shared/lib/functions/log/with-debugger', () => ({ + __esModule: true, + // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type + default: () => (logFn: Function) => logFn, +})); +jest.mock('@/shared/lib/localization/i18n.context', () => ({ + useI18n: () => ({ + chat: { para: { remove_chat: '{subject}님이 {target}님의 채팅을 삭제했습니다.' } }, + }), +})); +jest.mock('@/shared/lib/localization/renderer/processors/variable-processor-util', () => ({ + processI18nString: (template: string, vars: Record) => + template.replace(/\{(\w+)\}/g, (_, key) => vars[key] ?? ''), +})); + +import { renderHook } from '@testing-library/react'; +import type * as Crew from '@/entities/current-partyroom/model/crew.model'; +import { createCurrentPartyroomStore } from '@/entities/current-partyroom/model/current-partyroom.store'; +import { GradeType, MotionType, PenaltyType } from '@/shared/api/http/types/@enums'; +import { PartyroomEventType } from '@/shared/api/websocket/types/partyroom'; +import { errorLog } from '@/shared/lib/functions/log/logger'; +import { useStores } from '@/shared/lib/store/stores.context'; +import useCrewPenaltyCallback from './use-crew-penalty-callback.hook'; + +let store: ReturnType; + +beforeEach(() => { + jest.clearAllMocks(); + store = createCurrentPartyroomStore(); + (useStores as jest.Mock).mockReturnValue({ useCurrentPartyroom: store }); +}); + +const createCrew = (overrides: Partial = {}): Crew.Model => ({ + crewId: 1, + nickname: '테스트유저', + gradeType: GradeType.CLUBBER, + avatarBodyUri: 'body.png', + avatarFaceUri: 'face.png', + avatarIconUri: 'icon.png', + combinePositionX: 0, + combinePositionY: 0, + offsetX: 0, + offsetY: 0, + scale: 1, + motionType: MotionType.NONE, + ...overrides, +}); + +describe('useCrewPenaltyCallback', () => { + describe('CHAT_MESSAGE_REMOVAL', () => { + test('해당 messageId의 채팅을 시스템 메시지로 교체한다', () => { + const punisher = createCrew({ crewId: 1, nickname: '관리자' }); + const punished = createCrew({ crewId: 2, nickname: '위반자' }); + store.getState().updateCrews(() => [punisher, punished]); + + // user 채팅 메시지 추가 + store.getState().appendChatMessage({ + from: 'user', + crew: punished, + message: { messageId: 'target-msg', content: '위반 메시지' }, + receivedAt: 1000, + }); + + const { result } = renderHook(() => useCrewPenaltyCallback()); + const callback = result.current; + + callback({ + eventType: PartyroomEventType.CREW_PENALTY, + penaltyType: PenaltyType.CHAT_MESSAGE_REMOVAL, + detail: 'target-msg', + punisher: { crewId: 1 }, + punished: { crewId: 2 }, + }); + + const messages = store.getState().chat.getMessages(); + expect(messages).toHaveLength(1); + expect(messages[0].from).toBe('system'); + if (messages[0].from === 'system') { + expect(messages[0].content).toContain('관리자'); + expect(messages[0].content).toContain('위반자'); + } + }); + + test('punisher/punished 못 찾으면 에러 로그 + 아무 동작 안 함', () => { + store.getState().updateCrews(() => [createCrew({ crewId: 1 })]); + + store.getState().appendChatMessage({ + from: 'user', + crew: createCrew({ crewId: 99 }), + message: { messageId: 'some-msg', content: '내용' }, + receivedAt: 1000, + }); + + const { result } = renderHook(() => useCrewPenaltyCallback()); + const callback = result.current; + + callback({ + eventType: PartyroomEventType.CREW_PENALTY, + penaltyType: PenaltyType.CHAT_MESSAGE_REMOVAL, + detail: 'some-msg', + punisher: { crewId: 999 }, + punished: { crewId: 888 }, + }); + + expect(errorLog).toHaveBeenCalledWith('Punisher or Punished not found'); + }); + }); + + describe('기타 패널티', () => { + test('본인이 대상 → alert.notify 호출', () => { + store.getState().init({ + id: 1, + me: { crewId: 2, gradeType: GradeType.CLUBBER }, + playbackActivated: false, + crews: [createCrew({ crewId: 1 }), createCrew({ crewId: 2 })], + notice: '', + }); + + const alertNotify = jest.spyOn(store.getState().alert, 'notify'); + + const { result } = renderHook(() => useCrewPenaltyCallback()); + const callback = result.current; + + callback({ + eventType: PartyroomEventType.CREW_PENALTY, + penaltyType: PenaltyType.CHAT_BAN_30_SECONDS, + detail: '도배 행위', + punisher: { crewId: 1 }, + punished: { crewId: 2 }, + }); + + expect(alertNotify).toHaveBeenCalledWith({ + type: PenaltyType.CHAT_BAN_30_SECONDS, + reason: '도배 행위', + }); + }); + + test('본인이 아님 → alert 호출 안 함', () => { + store.getState().init({ + id: 1, + me: { crewId: 3, gradeType: GradeType.CLUBBER }, + playbackActivated: false, + crews: [createCrew({ crewId: 1 }), createCrew({ crewId: 2 }), createCrew({ crewId: 3 })], + notice: '', + }); + + const alertNotify = jest.spyOn(store.getState().alert, 'notify'); + + const { result } = renderHook(() => useCrewPenaltyCallback()); + const callback = result.current; + + callback({ + eventType: PartyroomEventType.CREW_PENALTY, + penaltyType: PenaltyType.ONE_TIME_EXPULSION, + detail: '부적절한 행위', + punisher: { crewId: 1 }, + punished: { crewId: 2 }, + }); + + expect(alertNotify).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/src/entities/partyroom-client/lib/subscription-callbacks/use-crew-profile-callback.hook.test.ts b/src/entities/partyroom-client/lib/subscription-callbacks/use-crew-profile-callback.hook.test.ts new file mode 100644 index 00000000..045773e1 --- /dev/null +++ b/src/entities/partyroom-client/lib/subscription-callbacks/use-crew-profile-callback.hook.test.ts @@ -0,0 +1,99 @@ +jest.mock('@/shared/lib/store/stores.context'); + +import { renderHook } from '@testing-library/react'; +import type * as Crew from '@/entities/current-partyroom/model/crew.model'; +import { createCurrentPartyroomStore } from '@/entities/current-partyroom/model/current-partyroom.store'; +import { GradeType, MotionType } from '@/shared/api/http/types/@enums'; +import { PartyroomEventType } from '@/shared/api/websocket/types/partyroom'; +import { useStores } from '@/shared/lib/store/stores.context'; +import useCrewProfileCallback from './use-crew-profile-callback.hook'; + +let store: ReturnType; + +beforeEach(() => { + jest.clearAllMocks(); + store = createCurrentPartyroomStore(); + (useStores as jest.Mock).mockReturnValue({ useCurrentPartyroom: store }); +}); + +const createCrew = (overrides: Partial = {}): Crew.Model => ({ + crewId: 1, + nickname: '테스트유저', + gradeType: GradeType.CLUBBER, + avatarBodyUri: 'body.png', + avatarFaceUri: 'face.png', + avatarIconUri: 'icon.png', + combinePositionX: 0, + combinePositionY: 0, + offsetX: 0, + offsetY: 0, + scale: 1, + motionType: MotionType.NONE, + ...overrides, +}); + +describe('useCrewProfileCallback', () => { + test('해당 crewId의 프로필 필드를 업데이트한다 (eventType 제외)', () => { + store.getState().updateCrews(() => [createCrew({ crewId: 1, nickname: '원래이름' })]); + + const { result } = renderHook(() => useCrewProfileCallback()); + const callback = result.current; + + callback({ + eventType: PartyroomEventType.CREW_PROFILE, + crewId: 1, + nickname: '새이름', + avatarBodyUri: 'new-body.png', + avatarFaceUri: 'new-face.png', + avatarIconUri: 'new-icon.png', + combinePositionX: 10, + combinePositionY: 20, + offsetX: 5, + offsetY: 5, + scale: 2, + }); + + const crew = store.getState().crews[0]; + expect(crew.nickname).toBe('새이름'); + expect(crew.avatarBodyUri).toBe('new-body.png'); + expect(crew.avatarFaceUri).toBe('new-face.png'); + expect(crew.avatarIconUri).toBe('new-icon.png'); + expect(crew.combinePositionX).toBe(10); + expect(crew.combinePositionY).toBe(20); + expect(crew.offsetX).toBe(5); + expect(crew.offsetY).toBe(5); + expect(crew.scale).toBe(2); + // motionType은 기존 값 유지 + expect(crew.motionType).toBe(MotionType.NONE); + }); + + test('다른 크루는 변경되지 않는다', () => { + store + .getState() + .updateCrews(() => [ + createCrew({ crewId: 1, nickname: '유저1' }), + createCrew({ crewId: 2, nickname: '유저2' }), + ]); + + const { result } = renderHook(() => useCrewProfileCallback()); + const callback = result.current; + + callback({ + eventType: PartyroomEventType.CREW_PROFILE, + crewId: 1, + nickname: '변경된이름', + avatarBodyUri: 'body.png', + avatarFaceUri: 'face.png', + avatarIconUri: 'icon.png', + combinePositionX: 0, + combinePositionY: 0, + offsetX: 0, + offsetY: 0, + scale: 1, + }); + + const crews = store.getState().crews; + expect(crews[0].nickname).toBe('변경된이름'); + expect(crews[1].nickname).toBe('유저2'); + }); +}); diff --git a/src/entities/partyroom-client/lib/subscription-callbacks/use-partyroom-access-callback.hook.test.ts b/src/entities/partyroom-client/lib/subscription-callbacks/use-partyroom-access-callback.hook.test.ts new file mode 100644 index 00000000..52ae96e3 --- /dev/null +++ b/src/entities/partyroom-client/lib/subscription-callbacks/use-partyroom-access-callback.hook.test.ts @@ -0,0 +1,97 @@ +jest.mock('@/shared/lib/store/stores.context'); + +import { renderHook } from '@testing-library/react'; +import type * as Crew from '@/entities/current-partyroom/model/crew.model'; +import { createCurrentPartyroomStore } from '@/entities/current-partyroom/model/current-partyroom.store'; +import { AccessType, GradeType, MotionType } from '@/shared/api/http/types/@enums'; +import type { PartyroomCrew } from '@/shared/api/http/types/partyrooms'; +import { PartyroomEventType } from '@/shared/api/websocket/types/partyroom'; +import { useStores } from '@/shared/lib/store/stores.context'; +import usePartyroomAccessCallback from './use-partyroom-access-callback.hook'; + +let store: ReturnType; + +beforeEach(() => { + jest.clearAllMocks(); + store = createCurrentPartyroomStore(); + (useStores as jest.Mock).mockReturnValue({ useCurrentPartyroom: store }); +}); + +const createPartyroomCrew = (overrides: Partial = {}): PartyroomCrew => ({ + crewId: 1, + nickname: '테스트유저', + gradeType: GradeType.CLUBBER, + avatarBodyUri: 'body.png', + avatarFaceUri: 'face.png', + avatarIconUri: 'icon.png', + combinePositionX: 0, + combinePositionY: 0, + offsetX: 0, + offsetY: 0, + scale: 1, + ...overrides, +}); + +const createCrew = (overrides: Partial = {}): Crew.Model => ({ + ...createPartyroomCrew(), + motionType: MotionType.NONE, + ...overrides, +}); + +describe('usePartyroomAccessCallback', () => { + test('ENTER → crews 배열에 새 크루 추가 (motionType=NONE 포함)', () => { + const { result } = renderHook(() => usePartyroomAccessCallback()); + const callback = result.current; + + const newCrew = createPartyroomCrew({ crewId: 10, nickname: '새유저' }); + + callback({ + eventType: PartyroomEventType.PARTYROOM_ACCESS, + accessType: AccessType.ENTER, + crew: newCrew, + }); + + const crews = store.getState().crews; + expect(crews).toHaveLength(1); + expect(crews[0]).toEqual({ ...newCrew, motionType: MotionType.NONE }); + }); + + test('EXIT → crews 배열에서 해당 crewId 제거', () => { + const existing = [ + createCrew({ crewId: 1, nickname: '유저1' }), + createCrew({ crewId: 2, nickname: '유저2' }), + ]; + store.getState().updateCrews(() => existing); + + const { result } = renderHook(() => usePartyroomAccessCallback()); + const callback = result.current; + + callback({ + eventType: PartyroomEventType.PARTYROOM_ACCESS, + accessType: AccessType.EXIT, + crew: { crewId: 1 }, + }); + + const crews = store.getState().crews; + expect(crews).toHaveLength(1); + expect(crews[0].crewId).toBe(2); + }); + + test('EXIT → 존재하지 않는 crewId → 배열 변동 없음', () => { + const existing = [createCrew({ crewId: 1 })]; + store.getState().updateCrews(() => existing); + + const { result } = renderHook(() => usePartyroomAccessCallback()); + const callback = result.current; + + callback({ + eventType: PartyroomEventType.PARTYROOM_ACCESS, + accessType: AccessType.EXIT, + crew: { crewId: 999 }, + }); + + const crews = store.getState().crews; + expect(crews).toHaveLength(1); + expect(crews[0].crewId).toBe(1); + }); +}); diff --git a/src/entities/partyroom-client/lib/subscription-callbacks/use-partyroom-close-callback.hook.test.ts b/src/entities/partyroom-client/lib/subscription-callbacks/use-partyroom-close-callback.hook.test.ts new file mode 100644 index 00000000..1e5d1b5e --- /dev/null +++ b/src/entities/partyroom-client/lib/subscription-callbacks/use-partyroom-close-callback.hook.test.ts @@ -0,0 +1,107 @@ +jest.mock('@/shared/lib/store/stores.context'); +jest.mock('@/entities/current-partyroom', () => ({ + useRemoveCurrentPartyroomCaches: jest.fn(), +})); +jest.mock('@/shared/lib/router/use-app-router.hook', () => ({ + useAppRouter: jest.fn(), +})); +jest.mock('@/shared/ui/components/dialog', () => ({ + useDialog: jest.fn(), +})); +jest.mock('@/shared/lib/localization/i18n.context', () => ({ + useI18n: () => ({ + party: { para: { closed: '파티룸이 닫혔습니다.' } }, + }), +})); + +import { renderHook } from '@testing-library/react'; +import { useRemoveCurrentPartyroomCaches } from '@/entities/current-partyroom'; +import type * as Crew from '@/entities/current-partyroom/model/crew.model'; +import { createCurrentPartyroomStore } from '@/entities/current-partyroom/model/current-partyroom.store'; +import { GradeType, MotionType } from '@/shared/api/http/types/@enums'; +import { PartyroomEventType } from '@/shared/api/websocket/types/partyroom'; +import { useAppRouter } from '@/shared/lib/router/use-app-router.hook'; +import { useStores } from '@/shared/lib/store/stores.context'; +import { useDialog } from '@/shared/ui/components/dialog'; +import usePartyroomCloseCallback from './use-partyroom-close-callback.hook'; + +let store: ReturnType; +const mockReplace = jest.fn(); +const mockRemoveCaches = jest.fn(); +const mockOpenAlertDialog = jest.fn(); + +beforeEach(() => { + jest.clearAllMocks(); + store = createCurrentPartyroomStore(); + (useStores as jest.Mock).mockReturnValue({ useCurrentPartyroom: store }); + (useAppRouter as jest.Mock).mockReturnValue({ replace: mockReplace }); + (useRemoveCurrentPartyroomCaches as jest.Mock).mockReturnValue(mockRemoveCaches); + (useDialog as jest.Mock).mockReturnValue({ openAlertDialog: mockOpenAlertDialog }); +}); + +const createCrew = (overrides: Partial = {}): Crew.Model => ({ + crewId: 1, + nickname: '테스트유저', + gradeType: GradeType.CLUBBER, + avatarBodyUri: 'body.png', + avatarFaceUri: 'face.png', + avatarIconUri: 'icon.png', + combinePositionX: 0, + combinePositionY: 0, + offsetX: 0, + offsetY: 0, + scale: 1, + motionType: MotionType.NONE, + ...overrides, +}); + +describe('usePartyroomCloseCallback', () => { + test("router.replace('/parties') 호출", () => { + const { result } = renderHook(() => usePartyroomCloseCallback()); + const callback = result.current; + + callback({ eventType: PartyroomEventType.PARTYROOM_CLOSE }); + + expect(mockReplace).toHaveBeenCalledWith('/parties'); + }); + + test('reset 호출 → 상태 초기화', () => { + store.getState().init({ + id: 99, + me: { crewId: 1, gradeType: GradeType.HOST }, + playbackActivated: true, + crews: [createCrew()], + notice: '공지', + }); + + const { result } = renderHook(() => usePartyroomCloseCallback()); + const callback = result.current; + + callback({ eventType: PartyroomEventType.PARTYROOM_CLOSE }); + + const state = store.getState(); + expect(state.id).toBeUndefined(); + expect(state.playbackActivated).toBe(false); + expect(state.crews).toEqual([]); + }); + + test('removeCurrentPartyroomCaches 호출', () => { + const { result } = renderHook(() => usePartyroomCloseCallback()); + const callback = result.current; + + callback({ eventType: PartyroomEventType.PARTYROOM_CLOSE }); + + expect(mockRemoveCaches).toHaveBeenCalledTimes(1); + }); + + test('openAlertDialog 호출 (t.party.para.closed 내용)', () => { + const { result } = renderHook(() => usePartyroomCloseCallback()); + const callback = result.current; + + callback({ eventType: PartyroomEventType.PARTYROOM_CLOSE }); + + expect(mockOpenAlertDialog).toHaveBeenCalledWith({ + content: '파티룸이 닫혔습니다.', + }); + }); +}); diff --git a/src/entities/partyroom-client/lib/subscription-callbacks/use-partyroom-deactivation-callback.hook.test.ts b/src/entities/partyroom-client/lib/subscription-callbacks/use-partyroom-deactivation-callback.hook.test.ts new file mode 100644 index 00000000..000dd682 --- /dev/null +++ b/src/entities/partyroom-client/lib/subscription-callbacks/use-partyroom-deactivation-callback.hook.test.ts @@ -0,0 +1,71 @@ +jest.mock('@/shared/lib/store/stores.context'); +jest.mock('./utils/use-invalidate-djing-queue.hook'); + +import { renderHook } from '@testing-library/react'; +import type * as Crew from '@/entities/current-partyroom/model/crew.model'; +import { createCurrentPartyroomStore } from '@/entities/current-partyroom/model/current-partyroom.store'; +import { GradeType, MotionType } from '@/shared/api/http/types/@enums'; +import { PartyroomEventType } from '@/shared/api/websocket/types/partyroom'; +import { useStores } from '@/shared/lib/store/stores.context'; +import usePartyroomDeactivationCallback from './use-partyroom-deactivation-callback.hook'; +import useInvalidateDjingQueue from './utils/use-invalidate-djing-queue.hook'; + +let store: ReturnType; +const mockInvalidateDjingQueue = jest.fn(); + +beforeEach(() => { + jest.clearAllMocks(); + store = createCurrentPartyroomStore(); + (useStores as jest.Mock).mockReturnValue({ useCurrentPartyroom: store }); + (useInvalidateDjingQueue as jest.Mock).mockReturnValue(mockInvalidateDjingQueue); +}); + +const createCrew = (overrides: Partial = {}): Crew.Model => ({ + crewId: 1, + nickname: '테스트유저', + gradeType: GradeType.CLUBBER, + avatarBodyUri: 'body.png', + avatarFaceUri: 'face.png', + avatarIconUri: 'icon.png', + combinePositionX: 0, + combinePositionY: 0, + offsetX: 0, + offsetY: 0, + scale: 1, + motionType: MotionType.NONE, + ...overrides, +}); + +describe('usePartyroomDeactivationCallback', () => { + test('reset 호출 → 상태 초기화됨', () => { + // 상태를 변경해둠 + store.getState().init({ + id: 99, + me: { crewId: 1, gradeType: GradeType.HOST }, + playbackActivated: true, + crews: [createCrew()], + notice: '공지사항', + }); + + const { result } = renderHook(() => usePartyroomDeactivationCallback()); + const callback = result.current; + + callback({ eventType: PartyroomEventType.PARTYROOM_DEACTIVATION }); + + const state = store.getState(); + expect(state.id).toBeUndefined(); + expect(state.me).toBeUndefined(); + expect(state.playbackActivated).toBe(false); + expect(state.crews).toEqual([]); + expect(state.notice).toBe(''); + }); + + test('invalidateDjingQueue 호출됨', () => { + const { result } = renderHook(() => usePartyroomDeactivationCallback()); + const callback = result.current; + + callback({ eventType: PartyroomEventType.PARTYROOM_DEACTIVATION }); + + expect(mockInvalidateDjingQueue).toHaveBeenCalledTimes(1); + }); +}); diff --git a/src/entities/partyroom-client/lib/subscription-callbacks/use-partyroom-notice-callback.hook.test.ts b/src/entities/partyroom-client/lib/subscription-callbacks/use-partyroom-notice-callback.hook.test.ts new file mode 100644 index 00000000..5c4a92ff --- /dev/null +++ b/src/entities/partyroom-client/lib/subscription-callbacks/use-partyroom-notice-callback.hook.test.ts @@ -0,0 +1,44 @@ +jest.mock('@/shared/lib/store/stores.context'); + +import { renderHook } from '@testing-library/react'; +import { createCurrentPartyroomStore } from '@/entities/current-partyroom/model/current-partyroom.store'; +import { PartyroomEventType } from '@/shared/api/websocket/types/partyroom'; +import { useStores } from '@/shared/lib/store/stores.context'; +import usePartyroomNoticeCallback from './use-partyroom-notice-callback.hook'; + +let store: ReturnType; + +beforeEach(() => { + jest.clearAllMocks(); + store = createCurrentPartyroomStore(); + (useStores as jest.Mock).mockReturnValue({ useCurrentPartyroom: store }); +}); + +describe('usePartyroomNoticeCallback', () => { + test('notice를 event.content로 업데이트한다', () => { + const { result } = renderHook(() => usePartyroomNoticeCallback()); + const callback = result.current; + + callback({ + eventType: PartyroomEventType.PARTYROOM_NOTICE, + content: '새로운 공지사항입니다', + }); + + expect(store.getState().notice).toBe('새로운 공지사항입니다'); + }); + + test('빈 문자열로도 업데이트할 수 있다', () => { + const { result } = renderHook(() => usePartyroomNoticeCallback()); + const callback = result.current; + + // 먼저 공지 설정 + store.getState().updateNotice('기존 공지'); + + callback({ + eventType: PartyroomEventType.PARTYROOM_NOTICE, + content: '', + }); + + expect(store.getState().notice).toBe(''); + }); +}); diff --git a/src/entities/partyroom-client/lib/subscription-callbacks/use-playback-start-callback.hook.test.ts b/src/entities/partyroom-client/lib/subscription-callbacks/use-playback-start-callback.hook.test.ts new file mode 100644 index 00000000..2ee83e08 --- /dev/null +++ b/src/entities/partyroom-client/lib/subscription-callbacks/use-playback-start-callback.hook.test.ts @@ -0,0 +1,112 @@ +jest.mock('@/shared/lib/store/stores.context'); +jest.mock('./utils/use-invalidate-djing-queue.hook'); + +import { renderHook } from '@testing-library/react'; +import type * as Crew from '@/entities/current-partyroom/model/crew.model'; +import { createCurrentPartyroomStore } from '@/entities/current-partyroom/model/current-partyroom.store'; +import { GradeType, MotionType } from '@/shared/api/http/types/@enums'; +import { PartyroomEventType } from '@/shared/api/websocket/types/partyroom'; +import { useStores } from '@/shared/lib/store/stores.context'; +import usePlaybackStartCallback from './use-playback-start-callback.hook'; +import useInvalidateDjingQueue from './utils/use-invalidate-djing-queue.hook'; + +let store: ReturnType; +const mockInvalidateDjingQueue = jest.fn(); + +beforeEach(() => { + jest.clearAllMocks(); + store = createCurrentPartyroomStore(); + (useStores as jest.Mock).mockReturnValue({ useCurrentPartyroom: store }); + (useInvalidateDjingQueue as jest.Mock).mockReturnValue(mockInvalidateDjingQueue); +}); + +const createCrew = (overrides: Partial = {}): Crew.Model => ({ + crewId: 1, + nickname: '테스트유저', + gradeType: GradeType.CLUBBER, + avatarBodyUri: 'body.png', + avatarFaceUri: 'face.png', + avatarIconUri: 'icon.png', + combinePositionX: 0, + combinePositionY: 0, + offsetX: 0, + offsetY: 0, + scale: 1, + motionType: MotionType.NONE, + ...overrides, +}); + +const createPlaybackEvent = () => ({ + eventType: PartyroomEventType.PLAYBACK_START as const, + crewId: 10, + playback: { + linkId: 'yt-abc123', + name: '테스트 곡', + duration: '03:45', + thumbnailImage: 'thumb.jpg', + likeCount: 5, + dislikeCount: 1, + grabCount: 2, + }, +}); + +describe('usePlaybackStartCallback', () => { + test('playbackActivated=true, playback 업데이트, currentDj 설정', () => { + const { result } = renderHook(() => usePlaybackStartCallback()); + const callback = result.current; + const event = createPlaybackEvent(); + + callback(event); + + const state = store.getState(); + expect(state.playbackActivated).toBe(true); + expect(state.currentDj).toEqual({ crewId: 10 }); + }); + + test('reaction 리셋 후 aggregation 설정', () => { + // 기존 reaction history 설정 + store.getState().updateReaction({ + history: { isLiked: true, isDisliked: false, isGrabbed: false }, + }); + + const { result } = renderHook(() => usePlaybackStartCallback()); + const callback = result.current; + const event = createPlaybackEvent(); + + callback(event); + + const reaction = store.getState().reaction; + expect(reaction.aggregation).toEqual({ + likeCount: 5, + dislikeCount: 1, + grabCount: 2, + }); + }); + + test('모든 크루 motionType NONE으로 리셋', () => { + store + .getState() + .updateCrews(() => [ + createCrew({ crewId: 1, motionType: MotionType.DANCE_TYPE_1 }), + createCrew({ crewId: 2, motionType: MotionType.DANCE_TYPE_2 }), + ]); + + const { result } = renderHook(() => usePlaybackStartCallback()); + const callback = result.current; + + callback(createPlaybackEvent()); + + const crews = store.getState().crews; + expect(crews[0].motionType).toBe(MotionType.NONE); + expect(crews[1].motionType).toBe(MotionType.NONE); + }); + + test('invalidateDjingQueue 호출됨', () => { + const { result } = renderHook(() => usePlaybackStartCallback()); + const callback = result.current; + + callback(createPlaybackEvent()); + + expect(mockInvalidateDjingQueue).toHaveBeenCalledTimes(1); + }); +}); diff --git a/src/entities/partyroom-client/lib/subscription-callbacks/use-reaction-aggregation-callback.hook.test.ts b/src/entities/partyroom-client/lib/subscription-callbacks/use-reaction-aggregation-callback.hook.test.ts new file mode 100644 index 00000000..ca116249 --- /dev/null +++ b/src/entities/partyroom-client/lib/subscription-callbacks/use-reaction-aggregation-callback.hook.test.ts @@ -0,0 +1,61 @@ +jest.mock('@/shared/lib/store/stores.context'); + +import { renderHook } from '@testing-library/react'; +import { createCurrentPartyroomStore } from '@/entities/current-partyroom/model/current-partyroom.store'; +import { PartyroomEventType } from '@/shared/api/websocket/types/partyroom'; +import { useStores } from '@/shared/lib/store/stores.context'; +import useReactionAggregationCallback from './use-reaction-aggregation-callback.hook'; + +let store: ReturnType; + +beforeEach(() => { + jest.clearAllMocks(); + store = createCurrentPartyroomStore(); + (useStores as jest.Mock).mockReturnValue({ useCurrentPartyroom: store }); +}); + +describe('useReactionAggregationCallback', () => { + test('aggregation 값을 이벤트 데이터로 교체한다', () => { + const { result } = renderHook(() => useReactionAggregationCallback()); + const callback = result.current; + + callback({ + eventType: PartyroomEventType.REACTION_AGGREGATION, + aggregation: { likeCount: 10, dislikeCount: 3, grabCount: 5 }, + }); + + const reaction = store.getState().reaction; + expect(reaction.aggregation).toEqual({ + likeCount: 10, + dislikeCount: 3, + grabCount: 5, + }); + }); + + test('history는 변경되지 않는다', () => { + // history를 먼저 변경 + store.getState().updateReaction({ + history: { isLiked: true, isDisliked: false, isGrabbed: true }, + }); + + const { result } = renderHook(() => useReactionAggregationCallback()); + const callback = result.current; + + callback({ + eventType: PartyroomEventType.REACTION_AGGREGATION, + aggregation: { likeCount: 99, dislikeCount: 0, grabCount: 0 }, + }); + + const reaction = store.getState().reaction; + expect(reaction.history).toEqual({ + isLiked: true, + isDisliked: false, + isGrabbed: true, + }); + expect(reaction.aggregation).toEqual({ + likeCount: 99, + dislikeCount: 0, + grabCount: 0, + }); + }); +}); diff --git a/src/entities/partyroom-client/lib/subscription-callbacks/use-reaction-motion-callback.hook.test.ts b/src/entities/partyroom-client/lib/subscription-callbacks/use-reaction-motion-callback.hook.test.ts new file mode 100644 index 00000000..423b25eb --- /dev/null +++ b/src/entities/partyroom-client/lib/subscription-callbacks/use-reaction-motion-callback.hook.test.ts @@ -0,0 +1,76 @@ +jest.mock('@/shared/lib/store/stores.context'); + +import { renderHook } from '@testing-library/react'; +import type * as Crew from '@/entities/current-partyroom/model/crew.model'; +import { createCurrentPartyroomStore } from '@/entities/current-partyroom/model/current-partyroom.store'; +import { GradeType, MotionType, ReactionType } from '@/shared/api/http/types/@enums'; +import { PartyroomEventType } from '@/shared/api/websocket/types/partyroom'; +import { useStores } from '@/shared/lib/store/stores.context'; +import useReactionMotionCallback from './use-reaction-motion-callback.hook'; + +let store: ReturnType; + +beforeEach(() => { + jest.clearAllMocks(); + store = createCurrentPartyroomStore(); + (useStores as jest.Mock).mockReturnValue({ useCurrentPartyroom: store }); +}); + +const createCrew = (overrides: Partial = {}): Crew.Model => ({ + crewId: 1, + nickname: '테스트유저', + gradeType: GradeType.CLUBBER, + avatarBodyUri: 'body.png', + avatarFaceUri: 'face.png', + avatarIconUri: 'icon.png', + combinePositionX: 0, + combinePositionY: 0, + offsetX: 0, + offsetY: 0, + scale: 1, + motionType: MotionType.NONE, + ...overrides, +}); + +describe('useReactionMotionCallback', () => { + test('해당 crewId의 motionType, reactionType을 업데이트한다', () => { + store.getState().updateCrews(() => [createCrew({ crewId: 1 }), createCrew({ crewId: 2 })]); + + const { result } = renderHook(() => useReactionMotionCallback()); + const callback = result.current; + + callback({ + eventType: PartyroomEventType.REACTION_MOTION, + motionType: MotionType.DANCE_TYPE_1, + reactionType: ReactionType.LIKE, + crew: { crewId: 1 }, + }); + + const crews = store.getState().crews; + expect(crews[0].motionType).toBe(MotionType.DANCE_TYPE_1); + expect(crews[0].reactionType).toBe(ReactionType.LIKE); + }); + + test('다른 크루는 변경되지 않는다', () => { + store + .getState() + .updateCrews(() => [ + createCrew({ crewId: 1 }), + createCrew({ crewId: 2, nickname: '다른유저' }), + ]); + + const { result } = renderHook(() => useReactionMotionCallback()); + const callback = result.current; + + callback({ + eventType: PartyroomEventType.REACTION_MOTION, + motionType: MotionType.DANCE_TYPE_2, + reactionType: ReactionType.DISLIKE, + crew: { crewId: 1 }, + }); + + const crews = store.getState().crews; + expect(crews[1].motionType).toBe(MotionType.NONE); + expect(crews[1].reactionType).toBeUndefined(); + }); +}); diff --git a/src/shared/api/http/client/interceptors/request.test.ts b/src/shared/api/http/client/interceptors/request.test.ts new file mode 100644 index 00000000..72c0043c --- /dev/null +++ b/src/shared/api/http/client/interceptors/request.test.ts @@ -0,0 +1,71 @@ +jest.mock('@/shared/lib/functions/log/network-log', () => ({ + printRequestLog: jest.fn(), +})); +jest.mock('@/shared/lib/functions/log/with-debugger', () => ({ + __esModule: true, + // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type + default: () => (logFn: Function) => logFn, +})); + +import { InternalAxiosRequestConfig } from 'axios'; +import { printRequestLog } from '@/shared/lib/functions/log/network-log'; +import { logRequest } from './request'; + +beforeEach(() => { + jest.clearAllMocks(); +}); + +function createAxiosRequestConfig( + overrides: Partial = {} +): InternalAxiosRequestConfig { + return { + method: 'get', + url: '/api/test', + headers: {} as any, + ...overrides, + }; +} + +describe('logRequest', () => { + test('config를 그대로 반환한다', () => { + const config = createAxiosRequestConfig(); + + const result = logRequest(config); + + expect(result).toBe(config); + }); + + test('로거에 method, url, params, data를 전달한다', () => { + const config = createAxiosRequestConfig({ + method: 'post', + url: '/api/users', + params: { page: 1 }, + data: { name: 'test' }, + }); + + logRequest(config); + + expect(printRequestLog).toHaveBeenCalledWith({ + method: 'post', + endPoint: '/api/users', + requestParams: { page: 1 }, + requestData: { name: 'test' }, + }); + }); + + test('params와 data가 없으면 undefined로 전달한다', () => { + const config = createAxiosRequestConfig({ + method: 'get', + url: '/api/health', + }); + + logRequest(config); + + expect(printRequestLog).toHaveBeenCalledWith({ + method: 'get', + endPoint: '/api/health', + requestParams: undefined, + requestData: undefined, + }); + }); +}); diff --git a/src/shared/lib/hooks/use-click-outside.hook.test.tsx b/src/shared/lib/hooks/use-click-outside.hook.test.tsx new file mode 100644 index 00000000..2c084695 --- /dev/null +++ b/src/shared/lib/hooks/use-click-outside.hook.test.tsx @@ -0,0 +1,72 @@ +import { renderHook, act } from '@testing-library/react'; +import useClickOutside from './use-click-outside.hook'; + +describe('useClickOutside', () => { + test('ref 외부 클릭 → callback 호출', () => { + const callback = jest.fn(); + const { result } = renderHook(() => useClickOutside(callback)); + + // ref에 DOM 요소 연결 + const inside = document.createElement('div'); + document.body.appendChild(inside); + Object.defineProperty(result.current, 'current', { + value: inside, + writable: true, + }); + + // 외부 클릭 + const outside = document.createElement('div'); + document.body.appendChild(outside); + + act(() => { + outside.click(); + }); + + expect(callback).toHaveBeenCalledTimes(1); + + // 정리 + document.body.removeChild(inside); + document.body.removeChild(outside); + }); + + test('ref 내부 클릭 → callback 호출 안 함', () => { + const callback = jest.fn(); + const { result } = renderHook(() => useClickOutside(callback)); + + const inside = document.createElement('div'); + document.body.appendChild(inside); + Object.defineProperty(result.current, 'current', { + value: inside, + writable: true, + }); + + act(() => { + inside.click(); + }); + + expect(callback).not.toHaveBeenCalled(); + + document.body.removeChild(inside); + }); + + test('언마운트 시 이벤트 리스너 정리됨', () => { + const callback = jest.fn(); + const removeSpy = jest.spyOn(document, 'removeEventListener'); + + const { result, unmount } = renderHook(() => useClickOutside(callback)); + + const inside = document.createElement('div'); + document.body.appendChild(inside); + Object.defineProperty(result.current, 'current', { + value: inside, + writable: true, + }); + + unmount(); + + expect(removeSpy).toHaveBeenCalledWith('click', expect.any(Function)); + + removeSpy.mockRestore(); + document.body.removeChild(inside); + }); +}); diff --git a/src/shared/lib/hooks/use-isomorphic-layout-effect.hook.test.ts b/src/shared/lib/hooks/use-isomorphic-layout-effect.hook.test.ts new file mode 100644 index 00000000..f119bcc3 --- /dev/null +++ b/src/shared/lib/hooks/use-isomorphic-layout-effect.hook.test.ts @@ -0,0 +1,9 @@ +import { useLayoutEffect } from 'react'; +import { useIsomorphicLayoutEffect } from './use-isomorphic-layout-effect.hook'; + +describe('useIsomorphicLayoutEffect', () => { + test('브라우저 환경(window 존재) → useLayoutEffect를 export한다', () => { + // jsdom 환경에서는 window가 존재하므로 useLayoutEffect가 선택됨 + expect(useIsomorphicLayoutEffect).toBe(useLayoutEffect); + }); +}); From 0fc8fedd3af4ea633109a2f6095e9f27e541a96f Mon Sep 17 00:00:00 2001 From: LivingLikeKrillin <143606756+LivingLikeKrillin@users.noreply.github.com> Date: Mon, 2 Mar 2026 10:33:13 +0900 Subject: [PATCH 11/41] =?UTF-8?q?test:=20=EC=88=9C=EC=88=98=20=EC=9C=A0?= =?UTF-8?q?=ED=8B=B8=EB=A6=AC=ED=8B=B0=C2=B7=EB=8F=84=EB=A9=94=EC=9D=B8=20?= =?UTF-8?q?=EB=AA=A8=EB=8D=B8=20=ED=85=8C=EC=8A=A4=ED=8A=B8=2016=EA=B0=9C?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80=20(5=ED=8C=8C=EC=9D=BC,=20594=E2=86=92610?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit update, mergeDeep, cloneDeep, playback.model, calculateDimensions 기존 테스트에 엣지 케이스 보강 (원시값, immutability, 음수 방지 등) Co-Authored-By: Claude Opus 4.6 --- .../avatar/lib/calculate-dimensions.test.ts | 18 ++++++ .../model/playback.model.test.ts | 55 +++++++++++++++++++ src/shared/lib/functions/clone-deep.test.ts | 19 +++++++ src/shared/lib/functions/merge-deep.test.ts | 28 ++++++++++ src/shared/lib/functions/update.test.ts | 14 +++++ 5 files changed, 134 insertions(+) diff --git a/src/entities/avatar/lib/calculate-dimensions.test.ts b/src/entities/avatar/lib/calculate-dimensions.test.ts index 4c165a9a..d4d464cf 100644 --- a/src/entities/avatar/lib/calculate-dimensions.test.ts +++ b/src/entities/avatar/lib/calculate-dimensions.test.ts @@ -57,4 +57,22 @@ describe('calculateDimensions', () => { offsetY: BASE_Y, }); }); + + test('절반 높이(80) → width=60', () => { + const result = calculateDimensions(80); + expect(result.width).toBe(60); + }); + + test('scale → zoom 반영', () => { + const result = calculateDimensions(BODY_BASE_HEIGHT, 0, 0, 0, 0, 2.5); + expect(result.zoom).toBe(2.5); + }); + + test('x, y offset 계산', () => { + const result = calculateDimensions(BODY_BASE_HEIGHT, 0, 0, 0.5, 0.3, 1); + const expectedFaceWidth = BODY_BASE_WIDTH * FACE_BASE_WIDTH_RATIO; + const expectedFaceHeight = BODY_BASE_HEIGHT * FACE_BASE_HEIGHT_RATIO; + expect(result.offsetX).toBeCloseTo(0.5 * expectedFaceWidth); + expect(result.offsetY).toBeCloseTo(0.3 * expectedFaceHeight); + }); }); diff --git a/src/entities/current-partyroom/model/playback.model.test.ts b/src/entities/current-partyroom/model/playback.model.test.ts index 67192364..a7bc6559 100644 --- a/src/entities/current-partyroom/model/playback.model.test.ts +++ b/src/entities/current-partyroom/model/playback.model.test.ts @@ -34,5 +34,60 @@ describe('playback model', () => { expect(result).toBe((ONE_MINUTE * 3) / 1000); }); + + test('재생 시작 직후 (seek ≈ 0)', () => { + const model: Partial = { + duration: '03:00', + endTime: MOCK_CURRENT_DATE.getTime() + 3 * ONE_MINUTE, + }; + + const result = Playback.getInitialSeek(model as Playback.Model); + + expect(result).toBe(0); + }); + + test('거의 끝남 (seek ≈ duration)', () => { + const model: Partial = { + duration: '03:00', + endTime: MOCK_CURRENT_DATE.getTime() + 1000, + }; + + const result = Playback.getInitialSeek(model as Playback.Model); + + expect(result).toBe(179); + }); + + test('endTime 과거 → 음수 방지 (max 0)', () => { + const model: Partial = { + duration: '00:10', + endTime: MOCK_CURRENT_DATE.getTime() - ONE_MINUTE, + }; + + const result = Playback.getInitialSeek(model as Playback.Model); + + expect(result).toBeGreaterThanOrEqual(0); + }); + + test('duration "00:30" → 30초 기준 계산', () => { + const model: Partial = { + duration: '00:30', + endTime: MOCK_CURRENT_DATE.getTime() + 10 * 1000, + }; + + const result = Playback.getInitialSeek(model as Playback.Model); + + expect(result).toBe(20); + }); + + test('duration "05:00" → 300초 기준 계산', () => { + const model: Partial = { + duration: '05:00', + endTime: MOCK_CURRENT_DATE.getTime() + ONE_MINUTE, + }; + + const result = Playback.getInitialSeek(model as Playback.Model); + + expect(result).toBe(240); + }); }); }); diff --git a/src/shared/lib/functions/clone-deep.test.ts b/src/shared/lib/functions/clone-deep.test.ts index 82217447..78973735 100644 --- a/src/shared/lib/functions/clone-deep.test.ts +++ b/src/shared/lib/functions/clone-deep.test.ts @@ -27,4 +27,23 @@ describe('cloneDeep', () => { expect(isOneDepthRefSame).toEqual(false); expect(isTwoDepthRefSame).toEqual(false); }); + + test('원시값 그대로 반환', () => { + expect(cloneDeep(42)).toBe(42); + expect(cloneDeep('hello')).toBe('hello'); + expect(cloneDeep(null)).toBe(null); + expect(cloneDeep(undefined)).toBe(undefined); + expect(cloneDeep(true)).toBe(true); + }); + + test('원본 변경이 클론에 영향 없음', () => { + const original = { a: { b: 1 }, c: [1, 2] }; + const copy = cloneDeep(original); + + original.a.b = 999; + original.c.push(3); + + expect(copy.a.b).toBe(1); + expect(copy.c).toEqual([1, 2]); + }); }); diff --git a/src/shared/lib/functions/merge-deep.test.ts b/src/shared/lib/functions/merge-deep.test.ts index 588ddc40..73a483f8 100644 --- a/src/shared/lib/functions/merge-deep.test.ts +++ b/src/shared/lib/functions/merge-deep.test.ts @@ -27,6 +27,34 @@ describe('mergeDeep', () => { expect(result).toStrictEqual(expected); }); + + test('source에만 있는 key 유지', () => { + const result = mergeDeep({ a: 1, b: 2 }, { a: 10 }); + + expect(result).toStrictEqual({ a: 10, b: 2 }); + }); + + test('override에만 있는 key 추가', () => { + const result = mergeDeep({ a: 1 }, { b: 2 } as Record); + + expect(result).toStrictEqual({ a: 1, b: 2 }); + }); + + test('배열은 merge하지 않고 교체', () => { + const result = mergeDeep({ items: [1, 2, 3] }, { items: [4, 5] }); + + expect(result).toStrictEqual({ items: [4, 5] }); + }); + + test('원본 mutation 없음 (immutability)', () => { + const initial = { a: { nested: 1 }, b: 2 }; + const override = { a: { nested: 99 } }; + const initialCopy = JSON.parse(JSON.stringify(initial)); + + mergeDeep(initial, override); + + expect(initial).toStrictEqual(initialCopy); + }); }); type MergeTestGroup = { diff --git a/src/shared/lib/functions/update.test.ts b/src/shared/lib/functions/update.test.ts index e97c029c..0afad3e2 100644 --- a/src/shared/lib/functions/update.test.ts +++ b/src/shared/lib/functions/update.test.ts @@ -31,4 +31,18 @@ describe('update', () => { expect(result).toEqual({ a: 2, b: 2 }); }); + + test('prev가 원시값이면 next로 대체', () => { + const result = update(10, 20 as unknown as number); + + expect(result).toBe(20); + }); + + test('prev가 undefined + 함수 next → 함수 실행', () => { + const next = (prev: undefined) => (prev === undefined ? 'created' : 'other'); + + const result = update(undefined, next); + + expect(result).toBe('created'); + }); }); From 0f185528d6da3820d0b073015a88e47dc0c1a88b Mon Sep 17 00:00:00 2001 From: LivingLikeKrillin <143606756+LivingLikeKrillin@users.noreply.github.com> Date: Mon, 2 Mar 2026 11:27:31 +0900 Subject: [PATCH 12/41] =?UTF-8?q?test:=20MSW=20=EB=8F=84=EC=9E=85=20+=20?= =?UTF-8?q?=ED=86=B5=ED=95=A9=20=ED=85=8C=EC=8A=A4=ED=8A=B8=2014=EA=B0=9C?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80=20(3=ED=8C=8C=EC=9D=BC,=20610=E2=86=92624?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - msw@2 설치 및 Jest 인프라 구성 (커스텀 환경, 리졸버, 핸들러) - PlaylistsService 통합 테스트 6개 (axios → 인터셉터 → MSW 파이프라인) - useCreatePlaylist 뮤테이션 통합 테스트 4개 (캐시 무효화, errorEmitter) - useSearchMusics 쿼리 통합 테스트 4개 (enabled, 캐시, 에러 전이) Co-Authored-By: Claude Opus 4.6 --- jest.config.js | 2 + jest.resolver.js | 63 ++++++ package.json | 1 + .../api/use-search-musics.integration.test.ts | 83 ++++++++ .../use-create-playlist.integration.test.ts | 88 ++++++++ src/shared/api/__test__/handlers.ts | 75 +++++++ src/shared/api/__test__/jest-msw-env.ts | 27 +++ src/shared/api/__test__/msw-server.ts | 8 + src/shared/api/__test__/test-utils.tsx | 33 +++ .../__test__/playlists.integration.test.ts | 98 +++++++++ yarn.lock | 194 +++++++++++++++++- 11 files changed, 671 insertions(+), 1 deletion(-) create mode 100644 jest.resolver.js create mode 100644 src/features/playlist/add-tracks/api/use-search-musics.integration.test.ts create mode 100644 src/features/playlist/add/api/use-create-playlist.integration.test.ts create mode 100644 src/shared/api/__test__/handlers.ts create mode 100644 src/shared/api/__test__/jest-msw-env.ts create mode 100644 src/shared/api/__test__/msw-server.ts create mode 100644 src/shared/api/__test__/test-utils.tsx create mode 100644 src/shared/api/http/services/__test__/playlists.integration.test.ts diff --git a/jest.config.js b/jest.config.js index cca577b3..257eeb57 100644 --- a/jest.config.js +++ b/jest.config.js @@ -17,4 +17,6 @@ module.exports = { moduleNameMapper: { '@/(.*)': '/src/$1', }, + resolver: '/jest.resolver.js', + transformIgnorePatterns: ['node_modules/(?!(msw|@mswjs|until-async)/)'], }; diff --git a/jest.resolver.js b/jest.resolver.js new file mode 100644 index 00000000..f8d381ed --- /dev/null +++ b/jest.resolver.js @@ -0,0 +1,63 @@ +/** + * Custom Jest resolver that handles package.json "exports" field for MSW v2. + * + * jsdom testEnvironment sets browser:true which resolves to ESM browser bundles. + * MSW and @mswjs/interceptors ship ESM (.mjs) browser bundles that Jest cannot parse. + * This resolver forces node/CJS resolution for those packages. + */ +const path = require('path'); +const { resolve: resolveExports } = require('resolve.exports'); + +const PACKAGES_NEEDING_EXPORTS = ['msw', '@mswjs/interceptors']; + +function needsExportsResolution(modulePath) { + return PACKAGES_NEEDING_EXPORTS.some( + (pkg) => modulePath === pkg || modulePath.startsWith(pkg + '/') + ); +} + +function resolveViaExports(modulePath, options) { + const segments = modulePath.split('/'); + let pkgName, subpath; + + if (modulePath.startsWith('@')) { + pkgName = segments.slice(0, 2).join('/'); + subpath = segments.slice(2).join('/'); + } else { + pkgName = segments[0]; + subpath = segments.slice(1).join('/'); + } + + const entry = subpath ? `./${subpath}` : '.'; + + // Find package.json + const pkgDir = path.join(options.rootDir || process.cwd(), 'node_modules', pkgName); + const pkgJson = require(path.join(pkgDir, 'package.json')); + + if (!pkgJson.exports) return null; + + // Force node + require conditions (CJS) + const resolved = resolveExports(pkgJson, entry, { + conditions: ['node', 'require', 'default'], + require: true, + }); + + if (resolved && resolved[0]) { + return path.join(pkgDir, resolved[0]); + } + + return null; +} + +module.exports = (modulePath, options) => { + if (needsExportsResolution(modulePath)) { + try { + const resolved = resolveViaExports(modulePath, options); + if (resolved) return resolved; + } catch { + // fall through to default + } + } + + return options.defaultResolver(modulePath, options); +}; diff --git a/package.json b/package.json index c535f38a..ac5c9266 100644 --- a/package.json +++ b/package.json @@ -109,6 +109,7 @@ "jest-environment-jsdom": "^29.7.0", "jest-plugin-context": "^2.9.0", "lint-staged": "^13.2.3", + "msw": "^2.12.10", "postcss": "^8.4.18", "prettier": "^3.4.2", "storybook": "^7.5.2", diff --git a/src/features/playlist/add-tracks/api/use-search-musics.integration.test.ts b/src/features/playlist/add-tracks/api/use-search-musics.integration.test.ts new file mode 100644 index 00000000..86c0dc79 --- /dev/null +++ b/src/features/playlist/add-tracks/api/use-search-musics.integration.test.ts @@ -0,0 +1,83 @@ +/** + * @jest-environment /src/shared/api/__test__/jest-msw-env.ts + */ +import { waitFor } from '@testing-library/react'; +import { http, HttpResponse } from 'msw'; +import { server } from '@/shared/api/__test__/msw-server'; +import { renderWithClient } from '@/shared/api/__test__/test-utils'; +import { QueryKeys } from '@/shared/api/http/query-keys'; +import { useSearchMusics } from './use-search-musics.query'; + +describe('useSearchMusics integration (query → service → MSW)', () => { + it('returns search results for a given keyword', async () => { + const { result } = renderWithClient( + (props: { search: string }) => useSearchMusics(props.search), + { + initialProps: { search: 'lofi' }, + } + ); + + await waitFor(() => expect(result.current.isSuccess).toBe(true)); + + const data = result.current.data; + expect(data).toHaveLength(2); + expect(data?.[0].videoTitle).toBe('lofi - Result 1'); + expect(data?.[1].videoId).toBe('def456'); + }); + + it('does not fire query when search is empty (enabled: false)', async () => { + const { result } = renderWithClient( + (props: { search: string }) => useSearchMusics(props.search), + { + initialProps: { search: '' }, + } + ); + + // fetchStatus stays idle because the query is disabled + await waitFor(() => expect(result.current.fetchStatus).toBe('idle')); + expect(result.current.data).toBeUndefined(); + }); + + it('uses cached results for the same search term', async () => { + const { result, queryClient } = renderWithClient( + (props: { search: string }) => useSearchMusics(props.search), + { initialProps: { search: 'cached' } } + ); + + await waitFor(() => expect(result.current.isSuccess).toBe(true)); + + // Cache should contain data for this key + const cached = queryClient.getQueryData([QueryKeys.Musics, 'cached']); + expect(cached).toBeDefined(); + }); + + it('transitions to error state on API failure', async () => { + server.use( + http.get('http://localhost:8080/api/v1/music-search', () => { + return HttpResponse.json( + { + data: { + status: 'INTERNAL_SERVER_ERROR', + code: 500, + message: 'Server Error', + errorCode: 'SYS-001', + }, + }, + { status: 500 } + ); + }) + ); + + const { result } = renderWithClient( + (props: { search: string }) => useSearchMusics(props.search), + { + initialProps: { search: 'fail-query' }, + } + ); + + await waitFor(() => expect(result.current.isError).toBe(true)); + + expect(result.current.error?.isAxiosError).toBe(true); + expect(result.current.error?.response?.status).toBe(500); + }); +}); diff --git a/src/features/playlist/add/api/use-create-playlist.integration.test.ts b/src/features/playlist/add/api/use-create-playlist.integration.test.ts new file mode 100644 index 00000000..bb35161f --- /dev/null +++ b/src/features/playlist/add/api/use-create-playlist.integration.test.ts @@ -0,0 +1,88 @@ +/** + * @jest-environment /src/shared/api/__test__/jest-msw-env.ts + */ +import { waitFor } from '@testing-library/react'; +import { http, HttpResponse } from 'msw'; +import { server } from '@/shared/api/__test__/msw-server'; +import { renderWithClient } from '@/shared/api/__test__/test-utils'; +import errorEmitter from '@/shared/api/http/error/error-emitter'; +import { QueryKeys } from '@/shared/api/http/query-keys'; +import { useCreatePlaylist } from './use-create-playlist.mutation'; + +describe('useCreatePlaylist integration (hook → service → MSW)', () => { + it('returns data on successful mutation', async () => { + const { result } = renderWithClient(() => useCreatePlaylist()); + + result.current.mutate({ name: 'My New Playlist' }); + + await waitFor(() => expect(result.current.isSuccess).toBe(true)); + + expect(result.current.data).toEqual({ + id: 1, + orderNumber: 1, + name: 'My New Playlist', + type: 'PLAYLIST', + }); + }); + + it('invalidates Playlist query cache on success', async () => { + const { result, queryClient } = renderWithClient(() => useCreatePlaylist()); + + // Seed the playlist cache + queryClient.setQueryData([QueryKeys.Playlist], { playlists: [] }); + const invalidateSpy = jest.spyOn(queryClient, 'invalidateQueries'); + + result.current.mutate({ name: 'Cache Test' }); + + await waitFor(() => expect(result.current.isSuccess).toBe(true)); + + expect(invalidateSpy).toHaveBeenCalledWith( + expect.objectContaining({ queryKey: [QueryKeys.Playlist] }) + ); + }); + + it('propagates AxiosError and emits errorCode on API failure', async () => { + server.use( + http.post('http://localhost:8080/api/v1/playlists', () => { + return HttpResponse.json( + { + data: { + status: 'BAD_REQUEST', + code: 400, + message: '재생목록 개수 제한을 초과함', + errorCode: 'PLL-002', + }, + }, + { status: 400 } + ); + }) + ); + + const emitted: string[] = []; + const unsub = errorEmitter.on('PLL-002' as any, () => emitted.push('PLL-002')); + + const { result } = renderWithClient(() => useCreatePlaylist()); + + result.current.mutate({ name: 'Over Limit' }); + + await waitFor(() => expect(result.current.isError).toBe(true)); + + expect(result.current.error?.isAxiosError).toBe(true); + expect(result.current.error?.response?.status).toBe(400); + expect(emitted).toContain('PLL-002'); + + unsub(); + }); + + it('transitions to idle → success after mutation', async () => { + const { result } = renderWithClient(() => useCreatePlaylist()); + + expect(result.current.isIdle).toBe(true); + + result.current.mutate({ name: 'Pending Test' }); + + await waitFor(() => expect(result.current.isSuccess).toBe(true)); + expect(result.current.isPending).toBe(false); + expect(result.current.data).toBeDefined(); + }); +}); diff --git a/src/shared/api/__test__/handlers.ts b/src/shared/api/__test__/handlers.ts new file mode 100644 index 00000000..484d984c --- /dev/null +++ b/src/shared/api/__test__/handlers.ts @@ -0,0 +1,75 @@ +import { http, HttpResponse } from 'msw'; + +const BASE_URL = 'http://localhost:8080/api'; + +export const handlers = [ + // POST /api/v1/playlists — createPlaylist + http.post(`${BASE_URL}/v1/playlists`, async ({ request }) => { + const body = (await request.json()) as { name: string }; + return HttpResponse.json({ + data: { + id: 1, + orderNumber: 1, + name: body.name, + type: 'PLAYLIST', + }, + }); + }), + + // GET /api/v1/playlists — getPlaylists + http.get(`${BASE_URL}/v1/playlists`, () => { + return HttpResponse.json({ + data: { + playlists: [ + { id: 1, name: 'My Playlist', orderNumber: 1, type: 'PLAYLIST', musicCount: 3 }, + { id: 2, name: 'Grablist', orderNumber: 2, type: 'GRABLIST', musicCount: 0 }, + ], + }, + }); + }), + + // PATCH /api/v1/playlists/:id — updatePlaylist + http.patch(`${BASE_URL}/v1/playlists/:id`, async ({ request }) => { + const body = (await request.json()) as { name: string }; + return HttpResponse.json({ + data: { id: 1, name: body.name }, + }); + }), + + // DELETE /api/v1/playlists — removePlaylist + http.delete(`${BASE_URL}/v1/playlists`, async ({ request }) => { + const body = (await request.json()) as { playlistIds: number[] }; + return HttpResponse.json({ + data: { playlistIds: body.playlistIds }, + }); + }), + + // POST /api/v1/playlists/:id/tracks — addTrackToPlaylist (void response) + http.post(`${BASE_URL}/v1/playlists/:id/tracks`, () => { + return new HttpResponse(null, { status: 200 }); + }), + + // GET /api/v1/music-search — searchMusics + http.get(`${BASE_URL}/v1/music-search`, ({ request }) => { + const url = new URL(request.url); + const q = url.searchParams.get('q'); + return HttpResponse.json({ + data: { + musicList: [ + { + videoId: 'abc123', + videoTitle: `${q} - Result 1`, + thumbnailUrl: 'https://img.youtube.com/vi/abc123/0.jpg', + runningTime: 'PT3M30S', + }, + { + videoId: 'def456', + videoTitle: `${q} - Result 2`, + thumbnailUrl: 'https://img.youtube.com/vi/def456/0.jpg', + runningTime: 'PT4M15S', + }, + ], + }, + }); + }), +]; diff --git a/src/shared/api/__test__/jest-msw-env.ts b/src/shared/api/__test__/jest-msw-env.ts new file mode 100644 index 00000000..ebf9cc56 --- /dev/null +++ b/src/shared/api/__test__/jest-msw-env.ts @@ -0,0 +1,27 @@ +import type { JestEnvironmentConfig, EnvironmentContext } from '@jest/environment'; +import JsdomEnvironment from 'jest-environment-jsdom'; + +/** + * Custom Jest environment that restores Node.js fetch globals (Request, Response, etc.) + * which jsdom does not provide. Required for MSW v2 to work in jsdom tests. + */ +export default class MswJsdomEnvironment extends JsdomEnvironment { + public constructor(config: JestEnvironmentConfig, context: EnvironmentContext) { + super(config, context); + + // Restore Node.js built-in fetch globals that jsdom strips + this.global.fetch = fetch; + this.global.Headers = Headers; + this.global.Request = Request; + this.global.Response = Response; + this.global.ReadableStream = ReadableStream; + this.global.TextEncoder = TextEncoder; + this.global.TextDecoder = TextDecoder as typeof this.global.TextDecoder; + this.global.BroadcastChannel = BroadcastChannel; + this.global.TransformStream = TransformStream as any; + this.global.WritableStream = WritableStream as any; + + // Set API base URL so axios baseURL resolves correctly + this.global.process.env.NEXT_PUBLIC_API_HOST_NAME = 'http://localhost:8080/api/'; + } +} diff --git a/src/shared/api/__test__/msw-server.ts b/src/shared/api/__test__/msw-server.ts new file mode 100644 index 00000000..cab3d316 --- /dev/null +++ b/src/shared/api/__test__/msw-server.ts @@ -0,0 +1,8 @@ +import { setupServer } from 'msw/node'; +import { handlers } from './handlers'; + +export const server = setupServer(...handlers); + +beforeAll(() => server.listen({ onUnhandledRequest: 'error' })); +afterEach(() => server.resetHandlers()); +afterAll(() => server.close()); diff --git a/src/shared/api/__test__/test-utils.tsx b/src/shared/api/__test__/test-utils.tsx new file mode 100644 index 00000000..ccc43742 --- /dev/null +++ b/src/shared/api/__test__/test-utils.tsx @@ -0,0 +1,33 @@ +import React, { ReactNode } from 'react'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { renderHook, RenderHookOptions } from '@testing-library/react'; + +export function createTestQueryClient() { + return new QueryClient({ + defaultOptions: { + queries: { retry: false }, + mutations: { retry: false }, + }, + }); +} + +export function TestWrapper({ children }: { children: ReactNode }) { + const queryClient = createTestQueryClient(); + return {children}; +} + +export function renderWithClient( + hook: (props: TProps) => TResult, + options?: Omit, 'wrapper'> +) { + const queryClient = createTestQueryClient(); + + const wrapper = ({ children }: { children: ReactNode }) => ( + {children} + ); + + return { + ...renderHook(hook, { ...options, wrapper }), + queryClient, + }; +} diff --git a/src/shared/api/http/services/__test__/playlists.integration.test.ts b/src/shared/api/http/services/__test__/playlists.integration.test.ts new file mode 100644 index 00000000..78da6679 --- /dev/null +++ b/src/shared/api/http/services/__test__/playlists.integration.test.ts @@ -0,0 +1,98 @@ +/** + * @jest-environment /src/shared/api/__test__/jest-msw-env.ts + */ +import { http, HttpResponse } from 'msw'; +import { server } from '@/shared/api/__test__/msw-server'; +import { playlistsService } from '@/shared/api/http/services'; + +describe('PlaylistsService integration (axios → interceptors → MSW)', () => { + describe('createPlaylist', () => { + it('returns unwrapped response on success', async () => { + const result = await playlistsService.createPlaylist({ name: 'New Playlist' }); + + expect(result).toEqual({ + id: 1, + orderNumber: 1, + name: 'New Playlist', + type: 'PLAYLIST', + }); + }); + }); + + describe('getPlaylists', () => { + it('returns unwrapped playlist list', async () => { + const result = await playlistsService.getPlaylists(); + + expect(result.playlists).toHaveLength(2); + expect(result.playlists[0]).toMatchObject({ id: 1, name: 'My Playlist' }); + }); + }); + + describe('searchMusics', () => { + it('returns unwrapped search results', async () => { + const result = await playlistsService.searchMusics({ q: 'test', platform: 'youtube' }); + + expect(result.musicList).toHaveLength(2); + expect(result.musicList[0].videoTitle).toBe('test - Result 1'); + }); + }); + + describe('addTrackToPlaylist', () => { + it('resolves without throwing on success', async () => { + await playlistsService.addTrackToPlaylist(1, { + linkId: 'abc123', + name: 'Test Track', + duration: 'PT3M30S', + thumbnailImage: 'https://example.com/thumb.jpg', + }); + // void endpoint — just verifying the full pipeline doesn't throw + }); + }); + + describe('API error (400)', () => { + it('rejects with AxiosError containing errorCode', async () => { + server.use( + http.post('http://localhost:8080/api/v1/playlists', () => { + return HttpResponse.json( + { + data: { + status: 'BAD_REQUEST', + code: 400, + message: '재생목록 개수 제한을 초과함', + errorCode: 'PLL-002', + }, + }, + { status: 400 } + ); + }) + ); + + try { + await playlistsService.createPlaylist({ name: 'Fail' }); + fail('Expected error to be thrown'); + } catch (e: any) { + expect(e.isAxiosError).toBe(true); + expect(e.response.status).toBe(400); + expect(e.response.data.errorCode).toBe('PLL-002'); + } + }); + }); + + describe('Network error', () => { + it('rejects with AxiosError on network failure', async () => { + server.use( + http.get('http://localhost:8080/api/v1/playlists', () => { + return HttpResponse.error(); + }) + ); + + try { + await playlistsService.getPlaylists(); + fail('Expected error to be thrown'); + } catch (e: any) { + expect(e.isAxiosError).toBe(true); + expect(e.response).toBeUndefined(); + } + }); + }); +}); diff --git a/yarn.lock b/yarn.lock index e2ce70f4..c5b2e629 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1869,6 +1869,43 @@ resolved "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.1.tgz" integrity sha512-c7hNEllBlenFTHBky65mhq8WD2kbN9Q6gk0bTk8lSBvc554jpXSkST1iePudpt7+A/AQvuHs9EMqjHDXMY1lrA== +"@inquirer/ansi@^1.0.2": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@inquirer/ansi/-/ansi-1.0.2.tgz#674a4c4d81ad460695cb2a1fc69d78cd187f337e" + integrity sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ== + +"@inquirer/confirm@^5.0.0": + version "5.1.21" + resolved "https://registry.yarnpkg.com/@inquirer/confirm/-/confirm-5.1.21.tgz#610c4acd7797d94890a6e2dde2c98eb1e891dd12" + integrity sha512-KR8edRkIsUayMXV+o3Gv+q4jlhENF9nMYUZs9PA2HzrXeHI8M5uDag70U7RJn9yyiMZSbtF5/UexBtAVtZGSbQ== + dependencies: + "@inquirer/core" "^10.3.2" + "@inquirer/type" "^3.0.10" + +"@inquirer/core@^10.3.2": + version "10.3.2" + resolved "https://registry.yarnpkg.com/@inquirer/core/-/core-10.3.2.tgz#535979ff3ff4fe1e7cc4f83e2320504c743b7e20" + integrity sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A== + dependencies: + "@inquirer/ansi" "^1.0.2" + "@inquirer/figures" "^1.0.15" + "@inquirer/type" "^3.0.10" + cli-width "^4.1.0" + mute-stream "^2.0.0" + signal-exit "^4.1.0" + wrap-ansi "^6.2.0" + yoctocolors-cjs "^2.1.3" + +"@inquirer/figures@^1.0.15": + version "1.0.15" + resolved "https://registry.yarnpkg.com/@inquirer/figures/-/figures-1.0.15.tgz#dbb49ed80df11df74268023b496ac5d9acd22b3a" + integrity sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g== + +"@inquirer/type@^3.0.10": + version "3.0.10" + resolved "https://registry.yarnpkg.com/@inquirer/type/-/type-3.0.10.tgz#11ed564ec78432a200ea2601a212d24af8150d50" + integrity sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA== + "@isaacs/cliui@^8.0.2": version "8.0.2" resolved "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz" @@ -2406,6 +2443,18 @@ "@motionone/dom" "^10.16.4" tslib "^2.3.1" +"@mswjs/interceptors@^0.41.2": + version "0.41.3" + resolved "https://registry.yarnpkg.com/@mswjs/interceptors/-/interceptors-0.41.3.tgz#d766dc1a168aa315a6a0b2d0f2e0cf1b74f23c82" + integrity sha512-cXu86tF4VQVfwz8W1SPbhoRyHJkti6mjH/XJIxp40jhO4j2k1m4KYrEykxqWPkFF3vrK4rgQppBh//AwyGSXPA== + dependencies: + "@open-draft/deferred-promise" "^2.2.0" + "@open-draft/logger" "^0.3.0" + "@open-draft/until" "^2.0.0" + is-node-process "^1.2.0" + outvariant "^1.4.3" + strict-event-emitter "^0.5.1" + "@ndelangen/get-tarball@^3.0.7": version "3.0.9" resolved "https://registry.npmjs.org/@ndelangen/get-tarball/-/get-tarball-3.0.9.tgz" @@ -2544,6 +2593,24 @@ resolved "https://registry.npmjs.org/@nolyfill/is-core-module/-/is-core-module-1.0.39.tgz" integrity sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA== +"@open-draft/deferred-promise@^2.2.0": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@open-draft/deferred-promise/-/deferred-promise-2.2.0.tgz#4a822d10f6f0e316be4d67b4d4f8c9a124b073bd" + integrity sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA== + +"@open-draft/logger@^0.3.0": + version "0.3.0" + resolved "https://registry.yarnpkg.com/@open-draft/logger/-/logger-0.3.0.tgz#2b3ab1242b360aa0adb28b85f5d7da1c133a0954" + integrity sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ== + dependencies: + is-node-process "^1.2.0" + outvariant "^1.4.0" + +"@open-draft/until@^2.0.0": + version "2.1.0" + resolved "https://registry.yarnpkg.com/@open-draft/until/-/until-2.1.0.tgz#0acf32f470af2ceaf47f095cdecd40d68666efda" + integrity sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg== + "@parcel/watcher-android-arm64@2.4.1": version "2.4.1" resolved "https://registry.yarnpkg.com/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.4.1.tgz#c2c19a3c442313ff007d2d7a9c2c1dd3e1c9ca84" @@ -4783,6 +4850,11 @@ resolved "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz" integrity sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw== +"@types/statuses@^2.0.6": + version "2.0.6" + resolved "https://registry.yarnpkg.com/@types/statuses/-/statuses-2.0.6.tgz#66748315cc9a96d63403baa8671b2c124f8633aa" + integrity sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA== + "@types/tough-cookie@*": version "4.0.5" resolved "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz" @@ -6647,6 +6719,11 @@ cli-truncate@^3.1.0: slice-ansi "^5.0.0" string-width "^5.0.0" +cli-width@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-4.1.0.tgz#42daac41d3c254ef38ad8ac037672130173691c5" + integrity sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ== + client-only@0.0.1: version "0.0.1" resolved "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz" @@ -6905,6 +6982,11 @@ cookie@0.6.0, cookie@^0.6.0: resolved "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz" integrity sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw== +cookie@^1.0.2: + version "1.1.1" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-1.1.1.tgz#3bb9bdfc82369db9c2f69c93c9c3ceb310c88b3c" + integrity sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ== + cookies-next@^4.1.0: version "4.2.1" resolved "https://registry.npmjs.org/cookies-next/-/cookies-next-4.2.1.tgz" @@ -9281,6 +9363,11 @@ graphemer@^1.4.0: resolved "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz" integrity sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag== +graphql@^16.12.0: + version "16.13.0" + resolved "https://registry.yarnpkg.com/graphql/-/graphql-16.13.0.tgz#726857e897e87d54412d62356ec0b6b76bfab409" + integrity sha512-uSisMYERbaB9bkA9M4/4dnqyktaEkf1kMHNKq/7DHyxVeWqHQ2mBmVqm5u6/FVHwF3iCNalKcg82Zfl+tffWoA== + gunzip-maybe@^1.4.2: version "1.4.2" resolved "https://registry.npmjs.org/gunzip-maybe/-/gunzip-maybe-1.4.2.tgz" @@ -9399,6 +9486,11 @@ he@^1.2.0: resolved "https://registry.npmjs.org/he/-/he-1.2.0.tgz" integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== +headers-polyfill@^4.0.2: + version "4.0.3" + resolved "https://registry.yarnpkg.com/headers-polyfill/-/headers-polyfill-4.0.3.tgz#922a0155de30ecc1f785bcf04be77844ca95ad07" + integrity sha512-IScLbePpkvO846sIwOtOTDjutRMWdXdJmXdMvk6gCBHxFO8d+QKOQedyZSxFTTFYRSmlgSTDtXqqq4pcenBXLQ== + hey-listen@^1.0.8: version "1.0.8" resolved "https://registry.npmjs.org/hey-listen/-/hey-listen-1.0.8.tgz" @@ -9912,6 +10004,11 @@ is-nan@^1.3.2: call-bind "^1.0.0" define-properties "^1.1.3" +is-node-process@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/is-node-process/-/is-node-process-1.2.0.tgz#ea02a1b90ddb3934a19aea414e88edef7e11d134" + integrity sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw== + is-number-object@^1.1.1: version "1.1.1" resolved "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz" @@ -11550,11 +11647,40 @@ ms@2.1.3, ms@^2.1.1, ms@^2.1.3: resolved "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== +msw@^2.12.10: + version "2.12.10" + resolved "https://registry.yarnpkg.com/msw/-/msw-2.12.10.tgz#6d3ca80f6d13715d2b65da03f9f07b46647c3e20" + integrity sha512-G3VUymSE0/iegFnuipujpwyTM2GuZAKXNeerUSrG2+Eg391wW63xFs5ixWsK9MWzr1AGoSkYGmyAzNgbR3+urw== + dependencies: + "@inquirer/confirm" "^5.0.0" + "@mswjs/interceptors" "^0.41.2" + "@open-draft/deferred-promise" "^2.2.0" + "@types/statuses" "^2.0.6" + cookie "^1.0.2" + graphql "^16.12.0" + headers-polyfill "^4.0.2" + is-node-process "^1.2.0" + outvariant "^1.4.3" + path-to-regexp "^6.3.0" + picocolors "^1.1.1" + rettime "^0.10.1" + statuses "^2.0.2" + strict-event-emitter "^0.5.1" + tough-cookie "^6.0.0" + type-fest "^5.2.0" + until-async "^3.0.2" + yargs "^17.7.2" + multiformats@^9.4.2: version "9.9.0" resolved "https://registry.npmjs.org/multiformats/-/multiformats-9.9.0.tgz" integrity sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg== +mute-stream@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-2.0.0.tgz#a5446fc0c512b71c83c44d908d5c7b7b4c493b2b" + integrity sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA== + mute-stream@~0.0.4: version "0.0.8" resolved "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz" @@ -12017,6 +12143,11 @@ os-browserify@^0.3.0: resolved "https://registry.npmjs.org/os-browserify/-/os-browserify-0.3.0.tgz" integrity sha512-gjcpUc3clBf9+210TRaDWbf+rZZZEshZ+DlXMRCeAjp0xhTrnQsKHypIy1J3d5hKdUzj69t708EHtU8P6bUn0A== +outvariant@^1.4.0, outvariant@^1.4.3: + version "1.4.3" + resolved "https://registry.yarnpkg.com/outvariant/-/outvariant-1.4.3.tgz#221c1bfc093e8fec7075497e7799fdbf43d14873" + integrity sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA== + overlap-area@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/overlap-area/-/overlap-area-1.1.0.tgz#1fcaa21bdb9cb1ace973d9aa299ae6b56557a4c2" @@ -12254,6 +12385,11 @@ path-to-regexp@0.1.7: resolved "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz" integrity sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ== +path-to-regexp@^6.3.0: + version "6.3.0" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-6.3.0.tgz#2b6a26a337737a8e1416f9272ed0766b1c0389f4" + integrity sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ== + path-type@^4.0.0: version "4.0.0" resolved "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz" @@ -13570,6 +13706,11 @@ restore-cursor@^4.0.0: onetime "^5.1.0" signal-exit "^3.0.2" +rettime@^0.10.1: + version "0.10.1" + resolved "https://registry.yarnpkg.com/rettime/-/rettime-0.10.1.tgz#cc8bb9870343f282b182e5a276899c08b94914be" + integrity sha512-uyDrIlUEH37cinabq0AX4QbgV4HbFZ/gqoiunWQ1UqBtRvTTytwhNYjE++pO/MjPTZL5KQCf2bEoJ/BJNVQ5Kw== + reusify@^1.0.4: version "1.0.4" resolved "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz" @@ -14185,6 +14326,11 @@ statuses@2.0.1: resolved "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz" integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== +statuses@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.2.tgz#8f75eecef765b5e1cfcdc080da59409ed424e382" + integrity sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw== + std-env@^3.7.0: version "3.7.0" resolved "https://registry.npmjs.org/std-env/-/std-env-3.7.0.tgz" @@ -14249,6 +14395,11 @@ streamx@^2.15.0, streamx@^2.18.0: optionalDependencies: bare-events "^2.2.0" +strict-event-emitter@^0.5.1: + version "0.5.1" + resolved "https://registry.yarnpkg.com/strict-event-emitter/-/strict-event-emitter-0.5.1.tgz#1602ece81c51574ca39c6815e09f1a3e8550bd93" + integrity sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ== + strict-uri-encode@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz" @@ -14533,6 +14684,11 @@ tabbable@^6.0.0: resolved "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz" integrity sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew== +tagged-tag@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/tagged-tag/-/tagged-tag-1.0.0.tgz#a0b5917c2864cba54841495abfa3f6b13edcf4d6" + integrity sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng== + tailwind-merge@^1.13.2: version "1.14.0" resolved "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-1.14.0.tgz" @@ -14744,6 +14900,18 @@ tiny-invariant@^1.3.1, tiny-invariant@^1.3.3: resolved "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz" integrity sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg== +tldts-core@^7.0.23: + version "7.0.23" + resolved "https://registry.yarnpkg.com/tldts-core/-/tldts-core-7.0.23.tgz#47bf18282a44641304a399d247703413b5d3e309" + integrity sha512-0g9vrtDQLrNIiCj22HSe9d4mLVG3g5ph5DZ8zCKBr4OtrspmNB6ss7hVyzArAeE88ceZocIEGkyW1Ime7fxPtQ== + +tldts@^7.0.5: + version "7.0.23" + resolved "https://registry.yarnpkg.com/tldts/-/tldts-7.0.23.tgz#444f0f0720fa777839a23ea665e04f61ee57217a" + integrity sha512-ASdhgQIBSay0R/eXggAkQ53G4nTJqTXqC2kbaBbdDwM7SkjyZyO0OaaN1/FH7U/yCeqOHDwFO5j8+Os/IS1dXw== + dependencies: + tldts-core "^7.0.23" + tmp@^0.2.0: version "0.2.3" resolved "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz" @@ -14781,6 +14949,13 @@ tough-cookie@^4.1.2: universalify "^0.2.0" url-parse "^1.5.3" +tough-cookie@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-6.0.0.tgz#11e418b7864a2c0d874702bc8ce0f011261940e5" + integrity sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w== + dependencies: + tldts "^7.0.5" + tr46@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz" @@ -14932,6 +15107,13 @@ type-fest@^4.20.1: resolved "https://registry.npmjs.org/type-fest/-/type-fest-4.25.0.tgz" integrity sha512-bRkIGlXsnGBRBQRAY56UXBm//9qH4bmJfFvq83gSz41N282df+fjy8ofcEgc1sM8geNt5cl6mC2g9Fht1cs8Aw== +type-fest@^5.2.0: + version "5.4.4" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-5.4.4.tgz#577f165b5ecb44cfc686559cc54ca77f62aa374d" + integrity sha512-JnTrzGu+zPV3aXIUhnyWJj4z/wigMsdYajGLIYakqyOW1nPllzXEJee0QQbHj+CTIQtXGlAjuK0UY+2xTyjVAw== + dependencies: + tagged-tag "^1.0.0" + type-is@~1.6.18: version "1.6.18" resolved "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz" @@ -15174,6 +15356,11 @@ unstorage@^1.9.0: ofetch "^1.3.3" ufo "^1.4.0" +until-async@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/until-async/-/until-async-3.0.2.tgz#447f1531fdd7bb2b4c7a98869bdb1a4c2a23865f" + integrity sha512-IiSk4HlzAMqTUseHHe3VhIGyuFmN90zMTpD3Z3y8jeQbzLIq500MVM7Jq2vUAnTKAFPJrqwkzr6PoTcPhGcOiw== + untildify@^4.0.0: version "4.0.0" resolved "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz" @@ -15839,7 +16026,7 @@ yargs@^15.3.1: y18n "^4.0.0" yargs-parser "^18.1.2" -yargs@^17.3.1, yargs@^17.5.1: +yargs@^17.3.1, yargs@^17.5.1, yargs@^17.7.2: version "17.7.2" resolved "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz" integrity sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w== @@ -15870,6 +16057,11 @@ yocto-queue@^1.0.0: resolved "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.1.1.tgz" integrity sha512-b4JR1PFR10y1mKjhHY9LaGo6tmrgjit7hxVIeAmyMw3jegXR4dhYqLaQF5zMXZxY7tLpMyJeLjr1C4rLmkVe8g== +yoctocolors-cjs@^2.1.3: + version "2.1.3" + resolved "https://registry.yarnpkg.com/yoctocolors-cjs/-/yoctocolors-cjs-2.1.3.tgz#7e4964ea8ec422b7a40ac917d3a344cfd2304baa" + integrity sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw== + zip-stream@^4.1.0: version "4.1.1" resolved "https://registry.npmjs.org/zip-stream/-/zip-stream-4.1.1.tgz" From 4cf59eac04acf87bc7c5a58d9d0936e2ff263134 Mon Sep 17 00:00:00 2001 From: LivingLikeKrillin <143606756+LivingLikeKrillin@users.noreply.github.com> Date: Mon, 2 Mar 2026 14:55:20 +0900 Subject: [PATCH 13/41] =?UTF-8?q?test:=20shared/ui=20=EC=BB=B4=ED=8F=AC?= =?UTF-8?q?=EB=84=8C=ED=8A=B8=20=ED=85=8C=EC=8A=A4=ED=8A=B8=2018=ED=8C=8C?= =?UTF-8?q?=EC=9D=BC=20~110=EC=BC=80=EC=9D=B4=EC=8A=A4=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Button, Select, TextArea, RadioSelectList, FormItem, Dialog, Drawer, Tab, CollapseList, Tooltip, Tag, Profile, Loading, Typography, BackButton, InfiniteScroll, UserListItem, DjListItem Co-Authored-By: Claude Opus 4.6 --- .../back-button.component.test.tsx | 42 +++++++ .../button/button.component.test.tsx | 95 ++++++++++++++ .../collapse-list.component.test.tsx | 58 +++++++++ .../dialog/dialog.component.test.tsx | 119 ++++++++++++++++++ .../dj-list-item.component.test.tsx | 54 ++++++++ .../drawer/drawer.component.test.tsx | 97 ++++++++++++++ .../form-item/form-item.component.test.tsx | 92 ++++++++++++++ .../infinite-scroll.component.test.tsx | 87 +++++++++++++ .../loading/loading.component.test.tsx | 36 ++++++ .../profile/profile.component.test.tsx | 24 ++++ .../radio-select-list.component.test.tsx | 56 +++++++++ .../select/select.component.test.tsx | 76 +++++++++++ .../ui/components/tab/tab.component.test.tsx | 75 +++++++++++ .../ui/components/tag/tag.component.test.tsx | 37 ++++++ .../textarea/textarea.component.test.tsx | 72 +++++++++++ .../tooltip/tooltip.component.test.tsx | 60 +++++++++ .../typography/typography.component.test.tsx | 60 +++++++++ .../user-list-item.component.test.tsx | 65 ++++++++++ 18 files changed, 1205 insertions(+) create mode 100644 src/shared/ui/components/back-button/back-button.component.test.tsx create mode 100644 src/shared/ui/components/button/button.component.test.tsx create mode 100644 src/shared/ui/components/collapse-list/collapse-list.component.test.tsx create mode 100644 src/shared/ui/components/dialog/dialog.component.test.tsx create mode 100644 src/shared/ui/components/dj-list-item/dj-list-item.component.test.tsx create mode 100644 src/shared/ui/components/drawer/drawer.component.test.tsx create mode 100644 src/shared/ui/components/form-item/form-item.component.test.tsx create mode 100644 src/shared/ui/components/infinite-scroll/infinite-scroll.component.test.tsx create mode 100644 src/shared/ui/components/loading/loading.component.test.tsx create mode 100644 src/shared/ui/components/profile/profile.component.test.tsx create mode 100644 src/shared/ui/components/radio-select-list/radio-select-list.component.test.tsx create mode 100644 src/shared/ui/components/select/select.component.test.tsx create mode 100644 src/shared/ui/components/tab/tab.component.test.tsx create mode 100644 src/shared/ui/components/tag/tag.component.test.tsx create mode 100644 src/shared/ui/components/textarea/textarea.component.test.tsx create mode 100644 src/shared/ui/components/tooltip/tooltip.component.test.tsx create mode 100644 src/shared/ui/components/typography/typography.component.test.tsx create mode 100644 src/shared/ui/components/user-list-item/user-list-item.component.test.tsx diff --git a/src/shared/ui/components/back-button/back-button.component.test.tsx b/src/shared/ui/components/back-button/back-button.component.test.tsx new file mode 100644 index 00000000..e2714fb3 --- /dev/null +++ b/src/shared/ui/components/back-button/back-button.component.test.tsx @@ -0,0 +1,42 @@ +import { render, screen, fireEvent } from '@testing-library/react'; +import BackButton from './back-button.component'; + +const mockBack = jest.fn(); +jest.mock('next/navigation', () => ({ + useRouter: () => ({ back: mockBack }), +})); + +jest.mock('../text-button', () => ({ + TextButton: ({ children, onClick, Icon }: any) => ( + + ), +})); + +jest.mock('@/shared/ui/icons', () => ({ + PFArrowLeft: (props: any) => , +})); + +describe('BackButton', () => { + beforeEach(() => { + mockBack.mockClear(); + }); + + test('텍스트가 렌더링된다', () => { + render(); + expect(screen.getByText('뒤로가기')).toBeTruthy(); + }); + + test('클릭 시 router.back()이 호출된다', () => { + render(); + fireEvent.click(screen.getByTestId('text-button')); + expect(mockBack).toHaveBeenCalledTimes(1); + }); + + test('화살표 아이콘이 렌더링된다', () => { + render(); + expect(screen.getByTestId('arrow-left')).toBeTruthy(); + }); +}); diff --git a/src/shared/ui/components/button/button.component.test.tsx b/src/shared/ui/components/button/button.component.test.tsx new file mode 100644 index 00000000..4a373de8 --- /dev/null +++ b/src/shared/ui/components/button/button.component.test.tsx @@ -0,0 +1,95 @@ +import { createRef } from 'react'; +import { render, screen, fireEvent } from '@testing-library/react'; +import Button from './button.component'; + +jest.mock('../typography', () => ({ + Typography: ({ children }: any) => {children}, + TypographyType: {}, +})); + +jest.mock('../loading', () => ({ + Loading: () => , +})); + +describe('Button', () => { + test('children 텍스트가 렌더링된다', () => { + render(); + expect(screen.getByText('확인')).toBeTruthy(); + }); + + test('onClick 콜백이 호출된다', () => { + const onClick = jest.fn(); + render(); + + fireEvent.click(screen.getByRole('button')); + expect(onClick).toHaveBeenCalledTimes(1); + }); + + test('disabled 상태에서 클릭해도 onClick이 호출되지 않는다', () => { + const onClick = jest.fn(); + render( + + ); + + fireEvent.click(screen.getByRole('button')); + expect(onClick).not.toHaveBeenCalled(); + }); + + test('disabled 상태에서 button 요소에 disabled 속성이 적용된다', () => { + render(); + expect((screen.getByRole('button') as HTMLButtonElement).disabled).toBe(true); + }); + + test('loading 상태에서 Loading 컴포넌트가 표시된다', () => { + render(); + + expect(screen.getByTestId('loading-spinner')).toBeTruthy(); + }); + + test('loading 상태에서 클릭해도 onClick이 호출되지 않는다', () => { + const onClick = jest.fn(); + render( + + ); + + fireEvent.click(screen.getByRole('button')); + expect(onClick).not.toHaveBeenCalled(); + }); + + test('loading 상태에서 button 요소에 disabled 속성이 적용된다', () => { + render(); + expect((screen.getByRole('button') as HTMLButtonElement).disabled).toBe(true); + }); + + test('Icon이 렌더링된다', () => { + render(); + expect(screen.getByTestId('icon')).toBeTruthy(); + }); + + test('iconPlacement="right"일 때 아이콘이 텍스트 뒤에 위치한다', () => { + const { container } = render( + + ); + + const button = container.querySelector('button') as HTMLElement; + const children = Array.from(button.children); + const textIndex = children.findIndex((c) => c.textContent === '텍스트'); + const iconIndex = children.findIndex((c) => c.getAttribute('data-testid') === 'icon'); + + expect(iconIndex).toBeGreaterThan(textIndex); + }); + + test('ref가 button 요소에 전달된다', () => { + const ref = createRef(); + render(); + + expect(ref.current).not.toBeNull(); + expect(ref.current).toBeInstanceOf(HTMLButtonElement); + }); +}); diff --git a/src/shared/ui/components/collapse-list/collapse-list.component.test.tsx b/src/shared/ui/components/collapse-list/collapse-list.component.test.tsx new file mode 100644 index 00000000..a9fead15 --- /dev/null +++ b/src/shared/ui/components/collapse-list/collapse-list.component.test.tsx @@ -0,0 +1,58 @@ +import { render, screen, fireEvent } from '@testing-library/react'; +import CollapseList from './collapse-list.component'; + +jest.mock('../typography', () => ({ + Typography: ({ children }: any) => {children}, +})); + +jest.mock('@/shared/ui/icons', () => ({ + PFChevronDown: () => , + PFChevronUp: () => , +})); + +describe('CollapseList', () => { + test('제목이 렌더링된다', () => { + render(내용); + expect(screen.getByText('접을 수 있는 목록')).toBeTruthy(); + }); + + test('초기 상태에서 내용이 숨겨져 있다', () => { + render(숨겨진 내용); + expect(screen.queryByText('숨겨진 내용')).toBeNull(); + }); + + test('버튼 클릭 시 내용이 표시된다', () => { + render(펼쳐진 내용); + + fireEvent.click(screen.getByText('목록')); + expect(screen.getByText('펼쳐진 내용')).toBeTruthy(); + }); + + test('infoText가 렌더링된다', () => { + render( + + 내용 + + ); + expect(screen.getByText('3개')).toBeTruthy(); + }); + + test('displaySuffix=false일 때 chevron 아이콘이 표시되지 않는다', () => { + render( + + 내용 + + ); + expect(screen.queryByTestId('chevron-down')).toBeNull(); + expect(screen.queryByTestId('chevron-up')).toBeNull(); + }); + + test('PrefixIcon이 렌더링된다', () => { + render( + }> + 내용 + + ); + expect(screen.getByTestId('prefix')).toBeTruthy(); + }); +}); diff --git a/src/shared/ui/components/dialog/dialog.component.test.tsx b/src/shared/ui/components/dialog/dialog.component.test.tsx new file mode 100644 index 00000000..12fd1ef0 --- /dev/null +++ b/src/shared/ui/components/dialog/dialog.component.test.tsx @@ -0,0 +1,119 @@ +import { render, screen, fireEvent, waitFor } from '@testing-library/react'; +import Dialog from './dialog.component'; + +jest.mock('../typography', () => ({ + Typography: ({ children }: any) => {children}, + TypographyType: {}, +})); + +jest.mock('../button', () => ({ + Button: ({ children, ...rest }: any) => , + ButtonProps: {}, +})); + +jest.mock('@/shared/ui/components/text-button', () => ({ + TextButton: ({ onClick, Icon }: any) => ( + + ), +})); + +jest.mock('@/shared/ui/icons', () => ({ + PFClose: () => , +})); + +jest.mock('@/shared/ui/foundation/theme', () => ({ + __esModule: true, + default: { zIndex: { dialog: 50 } }, +})); + + +global.ResizeObserver = class ResizeObserver { + public observe() { + /* noop */ + } + public unobserve() { + /* noop */ + } + public disconnect() { + /* noop */ + } +} as any; + +describe('Dialog', () => { + const defaultProps = { + open: true, + onClose: jest.fn(), + Body:
바디 내용
, + }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + test('open=true일 때 Body가 렌더링된다', () => { + render(); + expect(screen.getByText('바디 내용')).toBeTruthy(); + }); + + test('open=false일 때 Body가 렌더링되지 않는다', () => { + render(); + expect(screen.queryByText('바디 내용')).toBeNull(); + }); + + test('문자열 title이 렌더링된다', () => { + render(); + expect(screen.getByText('다이얼로그 제목')).toBeTruthy(); + }); + + test('함수형 title이 렌더링된다', () => { + render( +

커스텀 제목

} + /> + ); + expect(screen.getByText('커스텀 제목')).toBeTruthy(); + }); + + test('Body가 FC일 때 렌더링된다', () => { + const BodyComponent = () =>
함수형 바디
; + render(); + expect(screen.getByText('함수형 바디')).toBeTruthy(); + }); + + test('showCloseIcon=true일 때 닫기 아이콘이 표시된다', () => { + render(); + expect(screen.getByTestId('close-icon')).toBeTruthy(); + }); + + test('닫기 아이콘 클릭 시 onClose가 호출된다', async () => { + const onClose = jest.fn(); + render(); + + fireEvent.click(screen.getByTestId('close-icon')); + await waitFor(() => expect(onClose).toHaveBeenCalledTimes(1)); + }); + + test('Sub가 렌더링된다', () => { + render(부제목
} />); + expect(screen.getByText('부제목')).toBeTruthy(); + }); +}); + +describe('Dialog.ButtonGroup / Dialog.Button', () => { + test('ButtonGroup이 children을 렌더링한다', () => { + render( + + 버튼들 + + ); + expect(screen.getByText('버튼들')).toBeTruthy(); + }); + + test('Dialog.Button이 children을 렌더링한다', () => { + render(확인); + expect(screen.getByText('확인')).toBeTruthy(); + }); +}); diff --git a/src/shared/ui/components/dj-list-item/dj-list-item.component.test.tsx b/src/shared/ui/components/dj-list-item/dj-list-item.component.test.tsx new file mode 100644 index 00000000..c6ca1237 --- /dev/null +++ b/src/shared/ui/components/dj-list-item/dj-list-item.component.test.tsx @@ -0,0 +1,54 @@ +import { render, screen } from '@testing-library/react'; +import DjListItem from './dj-list-item.component'; + +jest.mock('next/image', () => ({ + __esModule: true, + default: ({ src, alt, ...rest }: any) => {alt}, +})); + +jest.mock('@/shared/ui/components/typography', () => ({ + Typography: ({ children }: any) => {children}, +})); + +jest.mock('@/shared/ui/components/tag', () => ({ + Tag: ({ value }: any) => {value}, +})); + +describe('DjListItem', () => { + const defaultConfig = { + username: 'DJ One', + src: 'https://example.com/dj.png', + }; + + test('username이 렌더링된다', () => { + render(); + expect(screen.getByText('DJ One')).toBeTruthy(); + }); + + test('아바타 이미지가 렌더링된다', () => { + render(); + const img = screen.getByAltText('DJ One'); + expect(img.getAttribute('src')).toBe('https://example.com/dj.png'); + }); + + test('order가 렌더링된다', () => { + render(); + expect(screen.getByText('1')).toBeTruthy(); + }); + + test('suffixTagValue가 있으면 Tag가 렌더링된다', () => { + render(); + expect(screen.getByTestId('tag')).toBeTruthy(); + expect(screen.getByText('NOW')).toBeTruthy(); + }); + + test('variant="accent"일 때 border 클래스가 적용된다', () => { + const { container } = render(); + expect(container.firstElementChild?.className).toContain('border-red-300'); + }); + + test('variant="filled"일 때 bg 클래스가 적용된다', () => { + const { container } = render(); + expect(container.firstElementChild?.className).toContain('bg-gray-800'); + }); +}); diff --git a/src/shared/ui/components/drawer/drawer.component.test.tsx b/src/shared/ui/components/drawer/drawer.component.test.tsx new file mode 100644 index 00000000..5b3d62ab --- /dev/null +++ b/src/shared/ui/components/drawer/drawer.component.test.tsx @@ -0,0 +1,97 @@ +import { render, screen, fireEvent } from '@testing-library/react'; +import Drawer from './drawer.component'; + +jest.mock('../typography', () => ({ + Typography: ({ children }: any) => {children}, +})); + +jest.mock('@/shared/ui/components/text-button', () => ({ + TextButton: ({ onClick, Icon }: any) => ( + + ), +})); + +jest.mock('@/shared/ui/icons', () => ({ + PFClose: () => , +})); + +jest.mock('@/shared/lib/hooks/use-portal-root.hook', () => ({ + __esModule: true, + default: () => document.body, +})); + +describe('Drawer', () => { + test('isOpen=true일 때 children이 렌더링된다', () => { + render( + +
서랍 내용
+
+ ); + + expect(screen.getByText('서랍 내용')).toBeTruthy(); + }); + + test('isOpen=false일 때 children이 렌더링되지 않는다', () => { + render( + +
서랍 내용
+
+ ); + + expect(screen.queryByText('서랍 내용')).toBeNull(); + }); + + test('title이 렌더링된다', () => { + render( + +
내용
+
+ ); + + expect(screen.getByText('제목입니다')).toBeTruthy(); + }); + + test('close 콜백이 있으면 닫기 버튼이 표시된다', () => { + render( + +
내용
+
+ ); + + expect(screen.getByTestId('close-button')).toBeTruthy(); + }); + + test('닫기 버튼 클릭 시 close가 호출된다', () => { + const close = jest.fn(); + render( + +
내용
+
+ ); + + fireEvent.click(screen.getByTestId('close-button')); + expect(close).toHaveBeenCalledTimes(1); + }); + + test('isOpen=true일 때 body에 scroll-hidden 클래스가 추가된다', () => { + render( + +
내용
+
+ ); + + expect(document.body.classList.contains('scroll-hidden')).toBe(true); + }); + + test('HeaderExtra가 렌더링된다', () => { + render( + 추가 헤더}> +
내용
+
+ ); + + expect(screen.getByText('추가 헤더')).toBeTruthy(); + }); +}); diff --git a/src/shared/ui/components/form-item/form-item.component.test.tsx b/src/shared/ui/components/form-item/form-item.component.test.tsx new file mode 100644 index 00000000..94426782 --- /dev/null +++ b/src/shared/ui/components/form-item/form-item.component.test.tsx @@ -0,0 +1,92 @@ +import { render, screen } from '@testing-library/react'; +import FormItem, { FormItemError } from './form-item.component'; + +jest.mock('../typography', () => ({ + Typography: ({ children, className, ...rest }: any) => ( + + {children} + + ), +})); + +describe('FormItem', () => { + test('문자열 label이 렌더링된다', () => { + render( + + + + ); + + expect(screen.getByText('이름')).toBeTruthy(); + }); + + test('ReactNode label이 렌더링된다', () => { + render( + 커스텀 라벨}> + + + ); + + expect(screen.getByText('커스텀 라벨')).toBeTruthy(); + }); + + test('children이 렌더링된다', () => { + render( + + + + ); + + expect(screen.getByPlaceholderText('입력하세요')).toBeTruthy(); + }); + + test('error 문자열이 표시된다', () => { + render( + + + + ); + + expect(screen.getByText('필수 항목입니다')).toBeTruthy(); + }); + + test('error가 boolean true일 때 에러 메시지는 표시되지 않지만 에러 스타일은 적용된다', () => { + const { container } = render( + + + + ); + + const childWrapper = container.querySelector('.outline-red-300'); + expect(childWrapper).not.toBeNull(); + }); + + test('error가 없으면 에러 영역이 렌더링되지 않는다', () => { + const { container } = render( + + + + ); + + expect(container.querySelector('.outline-red-300')).toBeNull(); + }); + + test('required일 때 label에 필수 마커 클래스가 적용된다', () => { + const { container } = render( + + + + ); + + const labelEl = container.querySelector('[data-custom-role="form-item-title"]'); + expect(labelEl).not.toBeNull(); + expect(labelEl?.className).toContain('after:content-["*"]'); + }); +}); + +describe('FormItemError', () => { + test('에러 메시지를 렌더링한다', () => { + render(오류가 발생했습니다); + expect(screen.getByText('오류가 발생했습니다')).toBeTruthy(); + }); +}); diff --git a/src/shared/ui/components/infinite-scroll/infinite-scroll.component.test.tsx b/src/shared/ui/components/infinite-scroll/infinite-scroll.component.test.tsx new file mode 100644 index 00000000..fe8c64ba --- /dev/null +++ b/src/shared/ui/components/infinite-scroll/infinite-scroll.component.test.tsx @@ -0,0 +1,87 @@ +import { render, screen } from '@testing-library/react'; +import InfiniteScroll from './infinite-scroll.component'; + +jest.mock('../loading', () => ({ + Loading: () => , +})); + +let mockIsIntersecting = false; +jest.mock('@/shared/lib/hooks/use-intersection-observer.hook', () => ({ + __esModule: true, + default: () => ({ + setRef: jest.fn(), + isIntersecting: mockIsIntersecting, + }), +})); + +describe('InfiniteScroll', () => { + beforeEach(() => { + mockIsIntersecting = false; + }); + + test('children이 렌더링된다', () => { + render( + +
아이템 목록
+
+ ); + + expect(screen.getByText('아이템 목록')).toBeTruthy(); + }); + + test('hasMore=true일 때 Loading이 표시된다', () => { + render( + +
목록
+
+ ); + + expect(screen.getByTestId('loading')).toBeTruthy(); + }); + + test('hasMore=false일 때 endMessage가 표시된다', () => { + render( + +
목록
+
+ ); + + expect(screen.getByText('더 이상 없습니다')).toBeTruthy(); + }); + + test('hasMore=false + endMessage 미지정 시 기본 메시지가 표시된다', () => { + render( + +
목록
+
+ ); + + expect(screen.getByText('No more data')).toBeTruthy(); + }); + + test('isIntersecting + hasMore일 때 loadMore가 호출된다', () => { + mockIsIntersecting = true; + const loadMore = jest.fn(); + + render( + +
목록
+
+ ); + + expect(loadMore).toHaveBeenCalled(); + }); + + test('isIntersecting + hasMore=false일 때 loadMore가 호출되지 않는다', () => { + mockIsIntersecting = true; + const loadMore = jest.fn(); + + render( + +
목록
+
+ ); + + expect(loadMore).not.toHaveBeenCalled(); + }); +}); diff --git a/src/shared/ui/components/loading/loading.component.test.tsx b/src/shared/ui/components/loading/loading.component.test.tsx new file mode 100644 index 00000000..a57d801c --- /dev/null +++ b/src/shared/ui/components/loading/loading.component.test.tsx @@ -0,0 +1,36 @@ +import { render } from '@testing-library/react'; +import Loading from './loading.component'; + +describe('Loading', () => { + test('기본 렌더링 — SVG가 표시된다', () => { + const { container } = render(); + const svg = container.querySelector('svg'); + expect(svg).not.toBeNull(); + }); + + test('기본 size는 1em이다', () => { + const { container } = render(); + const svg = container.querySelector('svg') as SVGElement; + expect(svg.getAttribute('width')).toBe('1em'); + expect(svg.getAttribute('height')).toBe('1em'); + }); + + test('size prop이 반영된다', () => { + const { container } = render(); + const svg = container.querySelector('svg') as SVGElement; + expect(svg.getAttribute('width')).toBe('32'); + expect(svg.getAttribute('height')).toBe('32'); + }); + + test('color prop이 stroke에 반영된다', () => { + const { container } = render(); + const path = container.querySelector('path'); + expect(path?.getAttribute('stroke')).toBe('#ff0000'); + }); + + test('animate-loading 클래스가 적용된다', () => { + const { container } = render(); + const svg = container.querySelector('svg'); + expect(svg?.className.baseVal).toContain('animate-loading'); + }); +}); diff --git a/src/shared/ui/components/profile/profile.component.test.tsx b/src/shared/ui/components/profile/profile.component.test.tsx new file mode 100644 index 00000000..c78119ed --- /dev/null +++ b/src/shared/ui/components/profile/profile.component.test.tsx @@ -0,0 +1,24 @@ +import { render, screen } from '@testing-library/react'; +import Profile from './profile.component'; + +describe('Profile', () => { + test('src가 없으면 빈 프로필 SVG가 렌더링된다', () => { + render(); + const svg = screen.getAllByRole('presentation')[0]; + expect(svg.tagName).toBe('svg'); + }); + + test('src가 있으면 div로 렌더링된다 (SVG가 아닌)', () => { + render(); + const el = screen.getByRole('presentation'); + expect(el.tagName).toBe('DIV'); + expect(el.className).toContain('rounded-full'); + }); + + test('size가 width/height에 반영된다', () => { + render(); + const el = screen.getByRole('presentation'); + expect(el.style.width).toBe('64px'); + expect(el.style.height).toBe('64px'); + }); +}); diff --git a/src/shared/ui/components/radio-select-list/radio-select-list.component.test.tsx b/src/shared/ui/components/radio-select-list/radio-select-list.component.test.tsx new file mode 100644 index 00000000..228d779b --- /dev/null +++ b/src/shared/ui/components/radio-select-list/radio-select-list.component.test.tsx @@ -0,0 +1,56 @@ +import { render, screen, fireEvent } from '@testing-library/react'; +import { RadioSelectList, RadioSelectListItem } from './radio-select-list.component'; + +jest.mock('@/shared/ui/icons', () => ({ + PFChevronRight: (props: any) => , +})); + +describe('RadioSelectList', () => { + const items: RadioSelectListItem[] = [ + { value: 'a', label: '옵션 A' }, + { value: 'b', label: '옵션 B' }, + { value: 'c', label: '옵션 C' }, + ]; + + test('모든 항목이 렌더링된다', () => { + render(); + + expect(screen.getByText('옵션 A')).toBeTruthy(); + expect(screen.getByText('옵션 B')).toBeTruthy(); + expect(screen.getByText('옵션 C')).toBeTruthy(); + }); + + test('항목 클릭 시 onChange가 해당 value로 호출된다', () => { + const onChange = jest.fn(); + render(); + + fireEvent.click(screen.getByText('옵션 B')); + expect(onChange).toHaveBeenCalledWith('b'); + }); + + test('선택된 항목에 체크 인디케이터가 표시된다', () => { + const { container } = render(); + + const radios = container.querySelectorAll('[data-checked]'); + const checkedValues = Array.from(radios).map((el) => el.getAttribute('data-checked')); + + expect(checkedValues).toEqual(['false', 'true', 'false']); + }); + + test('선택된 항목 내부에 흰색 원이 표시된다', () => { + const { container } = render(); + + const checked = container.querySelector('[data-checked="true"]'); + expect(checked).not.toBeNull(); + + const innerDot = checked?.querySelector('div'); + expect(innerDot).not.toBeNull(); + }); + + test('빈 목록은 아무것도 렌더링하지 않는다', () => { + const { container } = render(); + + const labels = container.querySelectorAll('label'); + expect(labels.length).toBe(0); + }); +}); diff --git a/src/shared/ui/components/select/select.component.test.tsx b/src/shared/ui/components/select/select.component.test.tsx new file mode 100644 index 00000000..1e66f830 --- /dev/null +++ b/src/shared/ui/components/select/select.component.test.tsx @@ -0,0 +1,76 @@ +import { render, screen, fireEvent } from '@testing-library/react'; +import Select, { SelectOption } from './select.component'; + +// Headless UI uses ResizeObserver + +global.ResizeObserver = class ResizeObserver { + public observe() { + /* noop */ + } + public unobserve() { + /* noop */ + } + public disconnect() { + /* noop */ + } +} as any; + +jest.mock('../typography', () => ({ + Typography: ({ children }: any) => {children}, +})); + +jest.mock('@/shared/ui/icons', () => ({ + PFChevronDown: () => , + PFChevronUp: () => , +})); + +const options: SelectOption[] = [ + { value: 'apple', label: '사과' }, + { value: 'banana', label: '바나나' }, + { value: 'cherry', label: '체리' }, +]; + +describe('Select', () => { + test('기본 렌더링 — ListboxButton이 표시된다', () => { + render(); + + expect(screen.getByText('바나나')).toBeTruthy(); + }); + + test('버튼 클릭 시 옵션 목록이 열린다', async () => { + render(); + + fireEvent.click(screen.getByRole('button')); + + const listbox = await screen.findByRole('listbox'); + const optionEls = listbox.querySelectorAll('[role="option"]'); + fireEvent.click(optionEls[2]); // '체리' + + expect(onChange).toHaveBeenCalledWith('cherry'); + }); + + test('prefix/suffix가 있는 옵션이 렌더링된다', async () => { + const optionsWithExtra: SelectOption[] = [ + { value: 'a', label: '항목', prefix: P }, + ]; + + render(