mirror of
https://github.com/duckietm/Nitro-V3.git
synced 2026-06-19 23:16:21 +00:00
Extract useCatalogFavorites pure helpers + 16 Vitest cases
The 5 pure functions inside useCatalogFavorites
(normalizeCatalogType, getOffersStorageKey, getPagesStorageKey,
parseOffers, parsePages) handle the v2 -> v3 storage-key migration
that runs once per user the first time they open the v3 client. The
parseOffers branch in particular silently morphs the legacy number[]
shape into IFavoriteOffer[] — exactly the kind of one-shot migration
code that should have coverage so a refactor doesn't break old saves.
Move them into useCatalogFavorites.helpers.ts (sibling file, matching
the WiredCreatorTools / useInventoryFurni.reducers / avatarInfo.reducers
convention). useCatalogFavorites imports them back, plus re-exports
the IFavoriteOffer type from the helper module for the public API.
Both helpers import CatalogType from the concrete file path
('../../api/catalog/CatalogType') rather than the api barrel, so the
test file doesn't drag in the renderer SDK and run aground in jsdom.
Tests cover:
- normalizeCatalogType fallback to NORMAL on undefined/garbage/explicit
- storage-key routing for NORMAL / BUILDER / missing arg
- parseOffers: invalid JSON, non-array, empty array, v2 number[] migration,
v3 IFavoriteOffer[] passthrough, mixed-array passthrough
- parsePages: invalid JSON, non-array, normal array
Net Vitest count: 83 -> 99 (7 test files).
This commit is contained in:
@@ -0,0 +1,85 @@
|
||||
import { CatalogType } from '../../api/catalog/CatalogType';
|
||||
|
||||
/**
|
||||
* Pure helpers consumed by useCatalogFavorites. Extracted standalone
|
||||
* so localStorage parse/migration logic can be covered by Vitest
|
||||
* without React or the renderer SDK in the loop.
|
||||
*
|
||||
* The favorites system tracks two parallel lists per catalog type
|
||||
* (NORMAL vs BUILDER): a list of favorited offers (offer id + display
|
||||
* metadata) and a list of favorited page ids. Both persist in
|
||||
* localStorage under per-type keys; a v1 → v3 migration runs for the
|
||||
* NORMAL catalog when the v3 key is empty but the legacy key exists.
|
||||
*/
|
||||
|
||||
export interface IFavoriteOffer
|
||||
{
|
||||
offerId: number;
|
||||
name?: string;
|
||||
iconUrl?: string;
|
||||
}
|
||||
|
||||
export const LEGACY_STORAGE_KEY_OFFERS = 'catalog_fav_offers_v2';
|
||||
export const LEGACY_STORAGE_KEY_PAGES = 'catalog_fav_pages';
|
||||
export const STORAGE_KEY_OFFERS_NORMAL = 'catalog_fav_offers_v3_normal';
|
||||
export const STORAGE_KEY_OFFERS_BUILDER = 'catalog_fav_offers_v3_builder';
|
||||
export const STORAGE_KEY_PAGES_NORMAL = 'catalog_fav_pages_v2_normal';
|
||||
export const STORAGE_KEY_PAGES_BUILDER = 'catalog_fav_pages_v2_builder';
|
||||
|
||||
export const normalizeCatalogType = (catalogType?: string): string =>
|
||||
((catalogType === CatalogType.BUILDER) ? CatalogType.BUILDER : CatalogType.NORMAL);
|
||||
|
||||
export const getOffersStorageKey = (catalogType?: string): string =>
|
||||
((normalizeCatalogType(catalogType) === CatalogType.BUILDER) ? STORAGE_KEY_OFFERS_BUILDER : STORAGE_KEY_OFFERS_NORMAL);
|
||||
|
||||
export const getPagesStorageKey = (catalogType?: string): string =>
|
||||
((normalizeCatalogType(catalogType) === CatalogType.BUILDER) ? STORAGE_KEY_PAGES_BUILDER : STORAGE_KEY_PAGES_NORMAL);
|
||||
|
||||
/**
|
||||
* Parse a serialized offers list from localStorage. Handles three
|
||||
* cases:
|
||||
* - well-formed `IFavoriteOffer[]` → returned as-is
|
||||
* - legacy `number[]` (v2 format) → migrated to `IFavoriteOffer[]`
|
||||
* with only offerId populated
|
||||
* - anything else (corrupt JSON, wrong shape) → empty array
|
||||
*/
|
||||
export const parseOffers = (raw: string): IFavoriteOffer[] =>
|
||||
{
|
||||
try
|
||||
{
|
||||
const parsed = JSON.parse(raw);
|
||||
|
||||
if(!Array.isArray(parsed)) return [];
|
||||
|
||||
// migrate from old format (number[]) to new format (IFavoriteOffer[])
|
||||
if(parsed.length > 0 && typeof parsed[0] === 'number')
|
||||
{
|
||||
return (parsed as number[]).map(id => ({ offerId: id }));
|
||||
}
|
||||
|
||||
return parsed;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Parse a serialized pages list from localStorage. Accepts any
|
||||
* array, rejects everything else. (The pages list has always been
|
||||
* `number[]`, no legacy format to migrate.)
|
||||
*/
|
||||
export const parsePages = (raw: string): number[] =>
|
||||
{
|
||||
try
|
||||
{
|
||||
const parsed = JSON.parse(raw);
|
||||
|
||||
return Array.isArray(parsed) ? parsed : [];
|
||||
}
|
||||
catch
|
||||
{
|
||||
return [];
|
||||
}
|
||||
};
|
||||
@@ -2,59 +2,9 @@ import { useCallback, useEffect, useState } from 'react';
|
||||
import { useBetween } from 'use-between';
|
||||
import { CatalogType } from '../../api';
|
||||
import { useCatalog } from './useCatalog';
|
||||
import { getOffersStorageKey, getPagesStorageKey, IFavoriteOffer, LEGACY_STORAGE_KEY_OFFERS, LEGACY_STORAGE_KEY_PAGES, normalizeCatalogType, parseOffers, parsePages } from './useCatalogFavorites.helpers';
|
||||
|
||||
export interface IFavoriteOffer
|
||||
{
|
||||
offerId: number;
|
||||
name?: string;
|
||||
iconUrl?: string;
|
||||
}
|
||||
|
||||
const LEGACY_STORAGE_KEY_OFFERS = 'catalog_fav_offers_v2';
|
||||
const LEGACY_STORAGE_KEY_PAGES = 'catalog_fav_pages';
|
||||
const STORAGE_KEY_OFFERS_NORMAL = 'catalog_fav_offers_v3_normal';
|
||||
const STORAGE_KEY_OFFERS_BUILDER = 'catalog_fav_offers_v3_builder';
|
||||
const STORAGE_KEY_PAGES_NORMAL = 'catalog_fav_pages_v2_normal';
|
||||
const STORAGE_KEY_PAGES_BUILDER = 'catalog_fav_pages_v2_builder';
|
||||
|
||||
const normalizeCatalogType = (catalogType?: string) => ((catalogType === CatalogType.BUILDER) ? CatalogType.BUILDER : CatalogType.NORMAL);
|
||||
|
||||
const getOffersStorageKey = (catalogType?: string) => ((normalizeCatalogType(catalogType) === CatalogType.BUILDER) ? STORAGE_KEY_OFFERS_BUILDER : STORAGE_KEY_OFFERS_NORMAL);
|
||||
const getPagesStorageKey = (catalogType?: string) => ((normalizeCatalogType(catalogType) === CatalogType.BUILDER) ? STORAGE_KEY_PAGES_BUILDER : STORAGE_KEY_PAGES_NORMAL);
|
||||
|
||||
const parseOffers = (raw: string): IFavoriteOffer[] =>
|
||||
{
|
||||
try
|
||||
{
|
||||
const parsed = JSON.parse(raw);
|
||||
if(!Array.isArray(parsed)) return [];
|
||||
|
||||
// migrate from old format (number[]) to new format (IFavoriteOffer[])
|
||||
if(parsed.length > 0 && typeof parsed[0] === 'number')
|
||||
{
|
||||
return (parsed as number[]).map(id => ({ offerId: id }));
|
||||
}
|
||||
|
||||
return parsed;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
const parsePages = (raw: string): number[] =>
|
||||
{
|
||||
try
|
||||
{
|
||||
const parsed = JSON.parse(raw);
|
||||
return Array.isArray(parsed) ? parsed : [];
|
||||
}
|
||||
catch
|
||||
{
|
||||
return [];
|
||||
}
|
||||
};
|
||||
export type { IFavoriteOffer } from './useCatalogFavorites.helpers';
|
||||
|
||||
const readOffers = (catalogType?: string): IFavoriteOffer[] =>
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user