@faststore/core 4.5.0-dev.3 → 4.5.0-dev.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-generate.log +3 -3
- package/.turbo/turbo-test.log +82 -80
- package/@generated/gql.ts +21 -21
- package/@generated/graphql.ts +82 -30
- package/@generated/persisted-documents.json +11 -11
- package/@generated/schema.graphql +54 -5
- package/CHANGELOG.md +12 -0
- package/package.json +6 -6
- package/src/components/product/ProductCard/ProductCard.tsx +1 -0
- package/src/components/search/SearchProductItem/SearchProductItem.tsx +2 -0
- package/src/components/sections/EmptyState/EmptyState.tsx +4 -3
- package/src/components/sections/ProductDetails/ProductDetails.tsx +1 -0
- package/src/components/templates/ProductListingPage/ProductListingPage.tsx +43 -37
- package/src/components/ui/Breadcrumb/Breadcrumb.tsx +5 -12
- package/src/components/ui/LocalizationButton/LocalizationButton.tsx +5 -1
- package/src/components/ui/ProductComparison/ProductComparisonSidebar.tsx +4 -0
- package/src/components/ui/ProductDetails/ProductDetailsSettings.tsx +2 -0
- package/src/components/ui/SKUMatrix/SKUMatrixSidebar.tsx +11 -1
- package/src/experimental/index.ts +2 -0
- package/src/pages/[...slug].tsx +5 -0
- package/src/pages/[slug]/p.tsx +85 -12
- package/src/sdk/account/useReorder.ts +7 -4
- package/src/sdk/cart/index.ts +5 -1
- package/src/sdk/localization/LocalizedProductContext.tsx +65 -0
- package/src/sdk/localization/useBindingSelector.ts +148 -5
- package/src/sdk/offer/index.ts +7 -2
- package/src/server/index.ts +10 -1
- package/test/sdk/localization/useBindingSelector.test.tsx +144 -1
|
@@ -9,8 +9,86 @@ import {
|
|
|
9
9
|
isValidUrl,
|
|
10
10
|
resolveBinding,
|
|
11
11
|
} from './bindingSelector'
|
|
12
|
+
import type { LocalizedProductLocale } from './LocalizedProductContext'
|
|
12
13
|
import type { BindingSelectorError, Locale } from './types'
|
|
13
14
|
|
|
15
|
+
const OTHER_LOCALES_STORAGE_PREFIX = 'fs:otherLocales:'
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Extracts the SKU id from a PDP-shaped pathname ("/.../{slug}-{skuId}/p").
|
|
19
|
+
* Slugs always end with "-{skuId}" (see `getSlug` in the API), so the id is the
|
|
20
|
+
* last "-"-separated segment of the path segment that precedes "/p".
|
|
21
|
+
* Returns null when the path is not a product page or has no numeric id.
|
|
22
|
+
*/
|
|
23
|
+
export function getSkuIdFromPdpPath(pathname: string): string | null {
|
|
24
|
+
const match = pathname.match(/\/([^/]+)\/p\/?$/) // NOSONAR
|
|
25
|
+
const skuId = match?.[1]?.split('-').pop()
|
|
26
|
+
|
|
27
|
+
return skuId && /^\d+$/.test(skuId) ? skuId : null
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Persists the localized slug map for a product keyed by its SKU id. This lets
|
|
32
|
+
* the locale selector rebuild canonical localized product URLs even after the
|
|
33
|
+
* user lands on a context-less page (e.g. a 404 for a locale where the product
|
|
34
|
+
* is unavailable), where `otherLocales` is no longer provided by the page.
|
|
35
|
+
*/
|
|
36
|
+
export function persistOtherLocales(
|
|
37
|
+
otherLocales: LocalizedProductLocale[]
|
|
38
|
+
): void {
|
|
39
|
+
if (typeof window === 'undefined' || !otherLocales.length) return
|
|
40
|
+
|
|
41
|
+
const skuId = otherLocales[0]?.slug.split('-').pop()
|
|
42
|
+
if (!skuId) return
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
globalThis.sessionStorage.setItem(
|
|
46
|
+
`${OTHER_LOCALES_STORAGE_PREFIX}${skuId}`,
|
|
47
|
+
JSON.stringify(otherLocales)
|
|
48
|
+
)
|
|
49
|
+
} catch {
|
|
50
|
+
// sessionStorage may be unavailable (private mode/quota); non-critical.
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function isLocalizedProductLocaleArray(
|
|
55
|
+
value: unknown
|
|
56
|
+
): value is LocalizedProductLocale[] {
|
|
57
|
+
return (
|
|
58
|
+
Array.isArray(value) &&
|
|
59
|
+
value.every(
|
|
60
|
+
(item) =>
|
|
61
|
+
typeof item === 'object' &&
|
|
62
|
+
item !== null &&
|
|
63
|
+
typeof (item as Record<string, unknown>).locale === 'string' &&
|
|
64
|
+
typeof (item as Record<string, unknown>).slug === 'string'
|
|
65
|
+
)
|
|
66
|
+
)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Recovers a previously persisted localized slug map for the product referenced
|
|
71
|
+
* by the current PDP URL. Returns null when not on a PDP or nothing is stored.
|
|
72
|
+
*/
|
|
73
|
+
export function recoverOtherLocales(): LocalizedProductLocale[] | null {
|
|
74
|
+
if (typeof window === 'undefined') return null
|
|
75
|
+
|
|
76
|
+
const skuId = getSkuIdFromPdpPath(globalThis.location.pathname)
|
|
77
|
+
if (!skuId) return null
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
const raw = globalThis.sessionStorage.getItem(
|
|
81
|
+
`${OTHER_LOCALES_STORAGE_PREFIX}${skuId}`
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
if (!raw) return null
|
|
85
|
+
const parsed: unknown = JSON.parse(raw)
|
|
86
|
+
return isLocalizedProductLocaleArray(parsed) ? parsed : null
|
|
87
|
+
} catch {
|
|
88
|
+
return null
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
14
92
|
interface Currency {
|
|
15
93
|
code: string
|
|
16
94
|
name: string
|
|
@@ -64,9 +142,17 @@ export interface UseBindingSelectorReturn {
|
|
|
64
142
|
* Hook that provides state and actions for the localization selector.
|
|
65
143
|
* Manages locale selection, currency filtering, and binding resolution.
|
|
66
144
|
*
|
|
145
|
+
* @param otherLocales - Optional list of localized slugs for the current page.
|
|
146
|
+
* When provided (e.g. on PDP or PLP), the save action navigates to the
|
|
147
|
+
* localized page URL instead of preserving the current page path verbatim.
|
|
148
|
+
* @param urlSuffix - Suffix appended after the slug when building the redirect URL.
|
|
149
|
+
* Use '/p' for product pages (default) and '' for collection/PLP pages.
|
|
67
150
|
* @returns Object with languages, currencies, selections, and actions
|
|
68
151
|
*/
|
|
69
|
-
export function useBindingSelector(
|
|
152
|
+
export function useBindingSelector(
|
|
153
|
+
otherLocales?: Array<{ locale: string; slug: string }> | null,
|
|
154
|
+
urlSuffix = '/p'
|
|
155
|
+
): UseBindingSelectorReturn {
|
|
70
156
|
const { locale: currentLocale, currency: currentCurrency } = useSession()
|
|
71
157
|
const localizationConfig = storeConfig.localization as LocalizationConfig
|
|
72
158
|
|
|
@@ -96,6 +182,15 @@ export function useBindingSelector(): UseBindingSelectorReturn {
|
|
|
96
182
|
}
|
|
97
183
|
}, [currentCurrency?.code])
|
|
98
184
|
|
|
185
|
+
// Persist the product's localized slugs so a later locale switch can rebuild
|
|
186
|
+
// the canonical localized URL even from a context-less page (e.g. a 404).
|
|
187
|
+
// Only relevant on PDPs — PLP otherLocales are collection slugs, not products.
|
|
188
|
+
useEffect(() => {
|
|
189
|
+
if (urlSuffix === '/p' && otherLocales?.length) {
|
|
190
|
+
persistOtherLocales(otherLocales)
|
|
191
|
+
}
|
|
192
|
+
}, [otherLocales, urlSuffix])
|
|
193
|
+
|
|
99
194
|
// Build language options with disambiguation - returns Record<localeCode, languageName>
|
|
100
195
|
const languages = useMemo(
|
|
101
196
|
() => buildLanguageOptions(localizationConfig.locales),
|
|
@@ -181,12 +276,60 @@ export function useBindingSelector(): UseBindingSelectorReturn {
|
|
|
181
276
|
return
|
|
182
277
|
}
|
|
183
278
|
|
|
184
|
-
//
|
|
185
|
-
|
|
279
|
+
// Prefer the current page's localized slugs. When missing (e.g. we're on a
|
|
280
|
+
// 404 for a locale where the product is unavailable) recover the map that was
|
|
281
|
+
// persisted while on a working PDP, so we can still build the canonical
|
|
282
|
+
// localized product URL instead of dropping the user on the locale home.
|
|
283
|
+
const effectiveOtherLocales = otherLocales?.length
|
|
284
|
+
? otherLocales
|
|
285
|
+
: recoverOtherLocales()
|
|
286
|
+
|
|
287
|
+
if (effectiveOtherLocales?.length) {
|
|
288
|
+
// 1. Target locale has a specific translation → use it
|
|
289
|
+
const localizedEntry = effectiveOtherLocales.find(
|
|
290
|
+
(e) => e.locale === localeCode
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
// 2. No translation for target locale → fall back to the default locale slug
|
|
294
|
+
// (IS linkText, always in the default locale) to avoid carrying over a
|
|
295
|
+
// translated slug from a different locale (e.g. Italian slug on es-ES).
|
|
296
|
+
// For an unavailable target this yields a 404 at the product URL (expected).
|
|
297
|
+
const fallbackEntry = effectiveOtherLocales.find(
|
|
298
|
+
(e) => e.locale === localizationConfig.defaultLocale
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
const entry = localizedEntry ?? fallbackEntry
|
|
302
|
+
|
|
303
|
+
if (entry) {
|
|
304
|
+
const baseUrl = binding.url.replace(/\/$/, '')
|
|
305
|
+
globalThis.location.href = `${baseUrl}/${entry.slug}${urlSuffix}${globalThis.location.search}${globalThis.location.hash}`
|
|
306
|
+
return
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// otherLocales is empty/null but we're still on a PDP: strip the stale slug.
|
|
311
|
+
if (globalThis.location.pathname.endsWith('/p')) {
|
|
312
|
+
globalThis.location.href = binding.url
|
|
313
|
+
return
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// No localized slugs available (not even persisted): preserve the current
|
|
317
|
+
// page path under the target binding instead of dropping the user on the
|
|
318
|
+
// locale home. For a default-locale slug this resolves the product; for an
|
|
319
|
+
// unavailable/untranslated slug it yields a 404 at the product URL, never the
|
|
320
|
+
// locale root.
|
|
321
|
+
globalThis.location.href = buildRedirectUrl(
|
|
186
322
|
binding.url,
|
|
187
|
-
`${
|
|
323
|
+
`${globalThis.location.pathname}${globalThis.location.search}${globalThis.location.hash}`
|
|
188
324
|
)
|
|
189
|
-
}, [
|
|
325
|
+
}, [
|
|
326
|
+
localeCode,
|
|
327
|
+
currencyCode,
|
|
328
|
+
localizationConfig.locales,
|
|
329
|
+
localizationConfig.defaultLocale,
|
|
330
|
+
otherLocales,
|
|
331
|
+
urlSuffix,
|
|
332
|
+
])
|
|
190
333
|
|
|
191
334
|
const isSaveEnabled = Boolean(localeCode && currencyCode && !error)
|
|
192
335
|
|
package/src/sdk/offer/index.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { fetcher } from './fetcher'
|
|
|
5
5
|
import { bestOfferFirst } from './sort'
|
|
6
6
|
export { getUrl as getOfferUrl } from './fetcher'
|
|
7
7
|
|
|
8
|
-
const ERROR_DATA = { offers: {}, isValidating: false }
|
|
8
|
+
const ERROR_DATA = { offers: {}, priceToken: null, isValidating: false }
|
|
9
9
|
|
|
10
10
|
export function useOffer(args: { skuId: string }) {
|
|
11
11
|
const { data, error, isValidating } = useSWR(args.skuId, fetcher)
|
|
@@ -41,5 +41,10 @@ export function useOffer(args: { skuId: string }) {
|
|
|
41
41
|
|
|
42
42
|
const offers = aggregateOffer(sellers)
|
|
43
43
|
|
|
44
|
-
|
|
44
|
+
// Fresh Pricing Fallback signed price token from the best seller. Fetched
|
|
45
|
+
// directly from Intelligent Search on the client, so it bypasses the SSG/CDN
|
|
46
|
+
// caches and respects the token's short (~30min) validity window.
|
|
47
|
+
const priceToken = sellers[0]?.PriceToken ?? null
|
|
48
|
+
|
|
49
|
+
return { offers, priceToken, isValidating }
|
|
45
50
|
}
|
package/src/server/index.ts
CHANGED
|
@@ -34,6 +34,7 @@ interface ExecuteOptions<V = Record<string, unknown>> {
|
|
|
34
34
|
operation: Operation
|
|
35
35
|
variables: V
|
|
36
36
|
query?: string | null
|
|
37
|
+
locale?: string
|
|
37
38
|
}
|
|
38
39
|
|
|
39
40
|
const persistedQueries = new Map()
|
|
@@ -98,7 +99,7 @@ export const execute = async <V extends Maybe<{ [key: string]: unknown }>, D>(
|
|
|
98
99
|
cookies: Map<string, Record<string, string>> | null
|
|
99
100
|
}
|
|
100
101
|
}> => {
|
|
101
|
-
const { operation, variables, query: maybeQuery } = options
|
|
102
|
+
const { operation, variables, query: maybeQuery, locale } = options
|
|
102
103
|
const { operationHash, operationName } = operation['__meta__']
|
|
103
104
|
|
|
104
105
|
if (!persistedQueries.size) {
|
|
@@ -127,6 +128,14 @@ export const execute = async <V extends Maybe<{ [key: string]: unknown }>, D>(
|
|
|
127
128
|
|
|
128
129
|
const contextValue = await contextFactory(envelopContext)
|
|
129
130
|
|
|
131
|
+
// Override the store-default locale with the per-request locale (if any) so
|
|
132
|
+
// localization-aware resolvers read it from ctx.storage.locale. Resolvers that
|
|
133
|
+
// carry their own locale (e.g. product/search via locator facets) still
|
|
134
|
+
// override this default inside their own resolution.
|
|
135
|
+
if (locale) {
|
|
136
|
+
contextValue.storage.locale = locale
|
|
137
|
+
}
|
|
138
|
+
|
|
130
139
|
const { data, errors } = (await run({
|
|
131
140
|
schema,
|
|
132
141
|
document: parse(query),
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest'
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
4
|
buildLanguageOptions,
|
|
@@ -6,7 +6,47 @@ import {
|
|
|
6
6
|
isValidUrl,
|
|
7
7
|
resolveBinding,
|
|
8
8
|
} from '../../../src/sdk/localization/bindingSelector'
|
|
9
|
+
import type { LocalizedProductLocale } from '../../../src/sdk/localization/LocalizedProductContext'
|
|
9
10
|
import type { Locale } from '../../../src/sdk/localization/types'
|
|
11
|
+
import {
|
|
12
|
+
getSkuIdFromPdpPath,
|
|
13
|
+
persistOtherLocales,
|
|
14
|
+
recoverOtherLocales,
|
|
15
|
+
} from '../../../src/sdk/localization/useBindingSelector'
|
|
16
|
+
|
|
17
|
+
/** In-memory Storage stub matching the subset used by the helpers. */
|
|
18
|
+
function createFakeStorage() {
|
|
19
|
+
const map = new Map<string, string>()
|
|
20
|
+
|
|
21
|
+
return {
|
|
22
|
+
getItem: (key: string) => (map.has(key) ? (map.get(key) as string) : null),
|
|
23
|
+
setItem: (key: string, value: string) => {
|
|
24
|
+
map.set(key, value)
|
|
25
|
+
},
|
|
26
|
+
removeItem: (key: string) => {
|
|
27
|
+
map.delete(key)
|
|
28
|
+
},
|
|
29
|
+
clear: () => map.clear(),
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Stubs a browser-like global scope with the given pathname and storage.
|
|
35
|
+
*
|
|
36
|
+
* The production helpers guard on `typeof window` but read `globalThis.location`
|
|
37
|
+
* / `globalThis.sessionStorage` (which coincide with `window` in a real browser).
|
|
38
|
+
* Under the node test environment those are distinct, so we stub all three: the
|
|
39
|
+
* `window` keeps the guard passing while `location`/`sessionStorage` back the reads.
|
|
40
|
+
* Pass an existing `sessionStorage` to simulate navigation while storage persists.
|
|
41
|
+
*/
|
|
42
|
+
function stubWindow(pathname: string, sessionStorage = createFakeStorage()) {
|
|
43
|
+
const location = { pathname }
|
|
44
|
+
vi.stubGlobal('window', { location, sessionStorage })
|
|
45
|
+
vi.stubGlobal('location', location)
|
|
46
|
+
vi.stubGlobal('sessionStorage', sessionStorage)
|
|
47
|
+
|
|
48
|
+
return sessionStorage
|
|
49
|
+
}
|
|
10
50
|
|
|
11
51
|
// Test data that mirrors what the hook would receive from discovery.config
|
|
12
52
|
const mockLocales: Record<string, Locale> = {
|
|
@@ -216,4 +256,107 @@ describe('useBindingSelector integration scenarios', () => {
|
|
|
216
256
|
expect(newCurrencies[0]).toBe('EUR')
|
|
217
257
|
})
|
|
218
258
|
})
|
|
259
|
+
|
|
260
|
+
describe('getSkuIdFromPdpPath', () => {
|
|
261
|
+
afterEach(() => {
|
|
262
|
+
vi.unstubAllGlobals()
|
|
263
|
+
})
|
|
264
|
+
|
|
265
|
+
it('extracts the SKU id from a locale-prefixed PDP path', () => {
|
|
266
|
+
expect(
|
|
267
|
+
getSkuIdFromPdpPath(
|
|
268
|
+
'/en-CA/adidas-mens-performance-polo-blast-blue-65/p'
|
|
269
|
+
)
|
|
270
|
+
).toBe('65')
|
|
271
|
+
})
|
|
272
|
+
|
|
273
|
+
it('extracts the SKU id from a default-locale (root) PDP path', () => {
|
|
274
|
+
expect(
|
|
275
|
+
getSkuIdFromPdpPath('/adidas-mens-performance-polo-blast-blue-65/p')
|
|
276
|
+
).toBe('65')
|
|
277
|
+
})
|
|
278
|
+
|
|
279
|
+
it('extracts the SKU id from a custom-path binding PDP', () => {
|
|
280
|
+
expect(getSkuIdFromPdpPath('/europe/it/some-product-slug-12/p')).toBe(
|
|
281
|
+
'12'
|
|
282
|
+
)
|
|
283
|
+
})
|
|
284
|
+
|
|
285
|
+
it('handles a trailing slash after /p', () => {
|
|
286
|
+
expect(getSkuIdFromPdpPath('/pt-BR/some-slug-9/p/')).toBe('9')
|
|
287
|
+
})
|
|
288
|
+
|
|
289
|
+
it('returns null for non-PDP paths', () => {
|
|
290
|
+
expect(getSkuIdFromPdpPath('/en-CA')).toBeNull()
|
|
291
|
+
expect(getSkuIdFromPdpPath('/en-CA/office')).toBeNull()
|
|
292
|
+
expect(getSkuIdFromPdpPath('/')).toBeNull()
|
|
293
|
+
})
|
|
294
|
+
|
|
295
|
+
it('returns null when the slug has no numeric id', () => {
|
|
296
|
+
expect(getSkuIdFromPdpPath('/en-CA/no-numeric-id/p')).toBeNull()
|
|
297
|
+
})
|
|
298
|
+
})
|
|
299
|
+
|
|
300
|
+
describe('persist / recover otherLocales (sessionStorage)', () => {
|
|
301
|
+
afterEach(() => {
|
|
302
|
+
vi.unstubAllGlobals()
|
|
303
|
+
})
|
|
304
|
+
|
|
305
|
+
const otherLocales: LocalizedProductLocale[] = [
|
|
306
|
+
{ locale: 'en-US', slug: 'adidas-mens-performance-polo-blast-blue-65' },
|
|
307
|
+
{
|
|
308
|
+
locale: 'pt-BR',
|
|
309
|
+
slug: 'adidas-polo-performance-masculina-azul-blast-65',
|
|
310
|
+
},
|
|
311
|
+
{
|
|
312
|
+
locale: 'fr-CA',
|
|
313
|
+
slug: 'adidas-polo-performance-homme-bleu-blast-ca-65',
|
|
314
|
+
},
|
|
315
|
+
]
|
|
316
|
+
|
|
317
|
+
it('round-trips the map for the SKU referenced by the current path', () => {
|
|
318
|
+
// Persisted while on a working PDP (SKU 65).
|
|
319
|
+
const storage = stubWindow(
|
|
320
|
+
'/adidas-mens-performance-polo-blast-blue-65/p'
|
|
321
|
+
)
|
|
322
|
+
persistOtherLocales(otherLocales)
|
|
323
|
+
|
|
324
|
+
// Later, on a 404 for en-CA (same SKU 65, default-locale slug in the URL).
|
|
325
|
+
stubWindow('/en-CA/adidas-mens-performance-polo-blast-blue-65/p', storage)
|
|
326
|
+
|
|
327
|
+
expect(recoverOtherLocales()).toEqual(otherLocales)
|
|
328
|
+
})
|
|
329
|
+
|
|
330
|
+
it('returns null when the current path references a different SKU', () => {
|
|
331
|
+
const storage = stubWindow(
|
|
332
|
+
'/adidas-mens-performance-polo-blast-blue-65/p'
|
|
333
|
+
)
|
|
334
|
+
persistOtherLocales(otherLocales)
|
|
335
|
+
|
|
336
|
+
stubWindow('/en-CA/some-other-product-99/p', storage)
|
|
337
|
+
|
|
338
|
+
expect(recoverOtherLocales()).toBeNull()
|
|
339
|
+
})
|
|
340
|
+
|
|
341
|
+
it('returns null when the current path is not a PDP', () => {
|
|
342
|
+
const storage = stubWindow(
|
|
343
|
+
'/adidas-mens-performance-polo-blast-blue-65/p'
|
|
344
|
+
)
|
|
345
|
+
persistOtherLocales(otherLocales)
|
|
346
|
+
|
|
347
|
+
stubWindow('/en-CA', storage)
|
|
348
|
+
|
|
349
|
+
expect(recoverOtherLocales()).toBeNull()
|
|
350
|
+
})
|
|
351
|
+
|
|
352
|
+
it('does not persist when otherLocales is empty', () => {
|
|
353
|
+
const storage = stubWindow(
|
|
354
|
+
'/adidas-mens-performance-polo-blast-blue-65/p'
|
|
355
|
+
)
|
|
356
|
+
persistOtherLocales([])
|
|
357
|
+
|
|
358
|
+
expect(recoverOtherLocales()).toBeNull()
|
|
359
|
+
expect(storage.getItem('fs:otherLocales:65')).toBeNull()
|
|
360
|
+
})
|
|
361
|
+
})
|
|
219
362
|
})
|