UNPKG

hydrogen-sanity

Version:
696 lines (573 loc) 20.1 kB
import {createClient, SanityClient} from '@sanity/client' import type {QueryStore} from '@sanity/react-loader' import {CacheShort, type WithCache} from '@shopify/hydrogen' import groq from 'groq' import {beforeEach, describe, expect, it, vi} from 'vitest' import {createSanityContext} from './context' import {PreviewSession} from './fixtures' import type {SanityProviderValue} from './provider' import {hashQuery} from './utils' // Mock the global caches object const cache = vi.hoisted<Cache>(() => ({ add: vi.fn().mockResolvedValue(undefined), addAll: vi.fn().mockResolvedValue(undefined), delete: vi.fn().mockResolvedValue(true), keys: vi.fn().mockResolvedValue([]), match: vi.fn().mockResolvedValue(undefined), matchAll: vi.fn().mockResolvedValue([]), put: vi.fn().mockResolvedValue(undefined), })) const loadQuery = vi.hoisted<QueryStore['loadQuery']>(() => vi.fn().mockResolvedValue(null)) const setServerClient = vi.hoisted(() => vi.fn()) let withCache = vi.hoisted<WithCache | null>(() => null) vi.mock('@shopify/hydrogen', async (importOriginal) => { const module = await importOriginal<typeof import('@shopify/hydrogen')>() withCache = module.createWithCache({ cache, waitUntil: () => Promise.resolve(), request: new Request('https://example.com'), }) return { ...module, createWithCache: vi.fn().mockReturnValue(withCache), } }) vi.mock('@sanity/react-loader', async (importOriginal) => { const module = await importOriginal<typeof import('@sanity/react-loader')>() return { ...module, loadQuery, setServerClient, } }) const runWithCache = vi.spyOn(withCache!, 'run') const projectId = 'my-project-id' const client = createClient({projectId, dataset: 'my-dataset'}) const query = groq`true` const params = {} const hashedQuery = await hashQuery(query, params) beforeEach(() => { vi.clearAllMocks() }) describe('the Sanity request context', () => { const request = new Request('https://example.com') let sanity: Awaited<ReturnType<typeof createSanityContext>> beforeEach(async () => { sanity = await createSanityContext({request, cache, client}) }) it('should return a client', () => { expect(sanity.client).toSatisfy((contextClient) => contextClient instanceof SanityClient) }) it('queries should get cached using the default caching strategy', async () => { const defaultStrategy = CacheShort() const contextWithDefaultStrategy = await createSanityContext({ request, cache, client, defaultStrategy, }) await contextWithDefaultStrategy.loadQuery<boolean>(query, params) expect(runWithCache).toHaveBeenNthCalledWith( 1, expect.objectContaining({cacheKey: hashedQuery, cacheStrategy: defaultStrategy}), expect.any(Function), ) expect(cache.put).toHaveBeenCalledOnce() }) it('queries should use the cache strategy passed in `loadQuery`', async () => { const strategy = CacheShort() await sanity.loadQuery<boolean>(query, params, { hydrogen: {cache: strategy}, }) expect(runWithCache).toHaveBeenNthCalledWith( 1, expect.objectContaining({cacheKey: hashedQuery, cacheStrategy: strategy}), expect.any(Function), ) expect(cache.put).toHaveBeenCalledOnce() }) }) describe('when configured for preview', () => { const request = new Request('https://example.com') const previewSession = new PreviewSession() previewSession.set('projectId', projectId) let sanity: Awaited<ReturnType<typeof createSanityContext>> beforeEach(async () => { sanity = await createSanityContext({ request, cache, client, preview: { token: 'my-token', session: previewSession, }, }) }) it('should throw if a token is not provided', async () => { await expect( // @ts-expect-error meant to test invalid configuration createSanityContext({client, preview: {enabled: true}}), ).rejects.toThrowErrorMatchingInlineSnapshot(`[Error: Enabling preview mode requires a token.]`) }) it.todo(`shouldn't use API CDN`, () => { expect(sanity.client.config().useCdn).toBe(false) }) it.todo('should use the `previewDrafts` perspective', () => { expect(sanity.client.config().perspective).toBe('previewDrafts') }) it('should enable preview mode', () => { expect(sanity.preview?.enabled).toBe(true) }) it(`shouldn't cache queries`, async () => { await sanity.loadQuery<boolean>(query) expect(loadQuery).toHaveBeenCalledOnce() expect(cache.put).not.toHaveBeenCalledOnce() }) it(`shouldn't cache fetch calls`, async () => { const spy = vi.spyOn(sanity.client, 'fetch').mockResolvedValue({ result: true, resultSourceMap: undefined, ms: 100, }) await sanity.fetch<boolean>(query) expect(spy).toHaveBeenCalledOnce() expect(cache.put).not.toHaveBeenCalledOnce() }) }) describe('session-based preview detection', () => { const request = new Request('https://example.com') const previewSession = new PreviewSession() previewSession.set('projectId', projectId) beforeEach(() => { vi.clearAllMocks() }) it('should enable preview when provided session contains matching project ID', async () => { const context = await createSanityContext({ request, cache, client, preview: { token: 'my-token', session: previewSession, }, }) expect(context.preview?.enabled).toBe(true) }) it('should disable preview when provided session contains different project ID', async () => { previewSession.set('projectId', 'different-project-id') const context = await createSanityContext({ request, cache, client, preview: { token: 'my-token', session: previewSession, }, }) expect(context.preview?.enabled).toBe(false) }) it('should disable preview when provided session contains no project ID', async () => { previewSession.unset('projectId') const context = await createSanityContext({ request, cache, client, preview: { token: 'my-token', session: previewSession, }, }) expect(context.preview?.enabled).toBe(false) }) }) describe('stegaEnabled serialization', () => { const request = new Request('https://example.com') beforeEach(() => { vi.clearAllMocks() }) it('should set stegaEnabled to false when preview is enabled but stega not configured', async () => { const previewSession = new PreviewSession() previewSession.set('projectId', projectId) const context = await createSanityContext({ request, cache, client, preview: { token: 'my-token', session: previewSession, }, }) const providerProps = ( context.SanityProvider({children: null}) as {props: {value: SanityProviderValue}} ).props expect(providerProps.value.stegaEnabled).toBe(false) // stega is opt-in, not automatic expect(providerProps.value.previewEnabled).toBe(true) }) it('should set stegaEnabled to false when preview is disabled', async () => { const context = await createSanityContext({ request, cache, client, }) const providerProps = ( context.SanityProvider({children: null}) as {props: {value: SanityProviderValue}} ).props expect(providerProps.value.stegaEnabled).toBe(false) expect(providerProps.value.previewEnabled).toBe(false) }) it('should set stegaEnabled to false when preview session contains different project ID', async () => { const previewSession = new PreviewSession() previewSession.set('projectId', 'different-project-id') const context = await createSanityContext({ request, cache, client, preview: { token: 'my-token', session: previewSession, }, }) const providerProps = ( context.SanityProvider({children: null}) as {props: {value: SanityProviderValue}} ).props expect(providerProps.value.stegaEnabled).toBe(false) expect(providerProps.value.previewEnabled).toBe(false) }) it('should set stegaEnabled to false when preview session contains no project ID', async () => { const previewSession = new PreviewSession() // Don't set projectId const context = await createSanityContext({ request, cache, client, preview: { token: 'my-token', session: previewSession, }, }) const providerProps = ( context.SanityProvider({children: null}) as {props: {value: SanityProviderValue}} ).props expect(providerProps.value.stegaEnabled).toBe(false) expect(providerProps.value.previewEnabled).toBe(false) }) it('should work with Hydrogen session for stegaEnabled', async () => { const hydrogenSession = { get: vi.fn().mockImplementation((key: string) => { if (key === 'projectId') return projectId return undefined }), set: vi.fn(), unset: vi.fn(), has: vi.fn(), commit: vi.fn(), destroy: vi.fn(), } const context = await createSanityContext({ request, cache, client, preview: { token: 'my-token', session: hydrogenSession as unknown as PreviewSession, }, }) const providerProps = ( context.SanityProvider({children: null}) as {props: {value: SanityProviderValue}} ).props expect(providerProps.value.stegaEnabled).toBe(false) // stega is opt-in, not automatic expect(providerProps.value.previewEnabled).toBe(true) }) it('should include stegaEnabled in provider value interface', async () => { const context = await createSanityContext({ request, cache, client, }) const providerProps = ( context.SanityProvider({children: null}) as {props: {value: SanityProviderValue}} ).props const providerValue = providerProps.value // Ensure all expected properties are present expect(providerValue).toHaveProperty('projectId') expect(providerValue).toHaveProperty('dataset') expect(providerValue).toHaveProperty('apiHost') expect(providerValue).toHaveProperty('apiVersion') expect(providerValue).toHaveProperty('previewEnabled') expect(providerValue).toHaveProperty('perspective') expect(providerValue).toHaveProperty('stegaEnabled') // Type assertion to ensure stegaEnabled is boolean expect(typeof providerValue.stegaEnabled).toBe('boolean') }) it('should enable stegaEnabled when explicitly configured in client', async () => { const previewSession = new PreviewSession() previewSession.set('projectId', projectId) const clientWithStega = createClient({ projectId, dataset: 'production', stega: { enabled: true, studioUrl: 'https://test.sanity.studio', }, }) const context = await createSanityContext({ request, cache, client: clientWithStega, preview: { token: 'my-token', session: previewSession, }, }) const providerProps = ( context.SanityProvider({children: null}) as {props: {value: SanityProviderValue}} ).props expect(providerProps.value.stegaEnabled).toBe(true) expect(providerProps.value.previewEnabled).toBe(true) }) it('should maintain independence between preview and stegaEnabled flags', async () => { const previewSession = new PreviewSession() previewSession.set('projectId', projectId) const contextWithPreview = await createSanityContext({ request, cache, client, preview: { token: 'my-token', session: previewSession, }, }) const contextWithoutPreview = await createSanityContext({ request, cache, client, }) const providerPropsWithPreview = ( contextWithPreview.SanityProvider({children: null}) as {props: {value: SanityProviderValue}} ).props const providerPropsWithoutPreview = ( contextWithoutPreview.SanityProvider({children: null}) as { props: {value: SanityProviderValue} } ).props // Preview can be true while stegaEnabled remains false (stega is opt-in) expect(providerPropsWithPreview.value.previewEnabled).toBe(true) expect(providerPropsWithPreview.value.stegaEnabled).toBe(false) // Both should be false when preview is disabled expect(providerPropsWithoutPreview.value.previewEnabled).toBe(false) expect(providerPropsWithoutPreview.value.stegaEnabled).toBe(false) }) it('should freeze provider value object', async () => { const context = await createSanityContext({ request, cache, client, }) const providerProps = ( context.SanityProvider({children: null}) as {props: {value: SanityProviderValue}} ).props const providerValue = providerProps.value expect(Object.isFrozen(providerValue)).toBe(true) }) }) describe('perspective resolution priority', () => { beforeEach(() => { vi.clearAllMocks() }) it('should use URL param perspective over session value', async () => { const previewSession = new PreviewSession() previewSession.set('projectId', projectId) previewSession.set('perspective', 'drafts') const request = new Request('https://example.com/?sanity-preview-perspective=releaseId,drafts') const context = await createSanityContext({ request, cache, client, preview: { token: 'my-token', session: previewSession, }, }) expect(context.client.config().perspective).toEqual(['releaseId', 'drafts']) }) it('should fall back to session perspective when URL param is absent', async () => { const previewSession = new PreviewSession() previewSession.set('projectId', projectId) previewSession.set('perspective', 'drafts') const request = new Request('https://example.com/') const context = await createSanityContext({ request, cache, client, preview: { token: 'my-token', session: previewSession, }, }) expect(context.client.config().perspective).toEqual(['drafts']) }) it('should pass perspective explicitly to loadQuery in preview mode', async () => { const previewSession = new PreviewSession() previewSession.set('projectId', projectId) previewSession.set('perspective', 'drafts') const request = new Request('https://example.com/?sanity-preview-perspective=releaseId,drafts') const context = await createSanityContext({ request, cache, client, preview: { token: 'my-token', session: previewSession, }, }) await context.loadQuery<boolean>(query, params) expect(loadQuery).toHaveBeenCalledWith( query, params, expect.objectContaining({ perspective: ['releaseId', 'drafts'], }), ) }) it('should ignore URL param perspective stack when API version is too old', async () => { const previewSession = new PreviewSession() previewSession.set('projectId', projectId) const oldClient = createClient({ projectId, dataset: 'my-dataset', apiVersion: '2024-01-01', }) const request = new Request('https://example.com/?sanity-preview-perspective=releaseId,drafts') const context = await createSanityContext({ request, cache, client: oldClient, preview: { token: 'my-token', session: previewSession, }, }) // Should fall back to previewDrafts since API version doesn't support stacks expect(context.client.config().perspective).toBe('previewDrafts') }) it('should accept single URL param perspective even with old API version', async () => { const previewSession = new PreviewSession() previewSession.set('projectId', projectId) const oldClient = createClient({ projectId, dataset: 'my-dataset', apiVersion: '2024-01-01', }) const request = new Request('https://example.com/?sanity-preview-perspective=drafts') const context = await createSanityContext({ request, cache, client: oldClient, preview: { token: 'my-token', session: previewSession, }, }) expect(context.client.config().perspective).toBe('drafts') }) }) describe('lazy-initialize loaders', () => { const request = new Request('https://example.com') beforeEach(() => { vi.clearAllMocks() }) it("shouldn't call `setServerClient` during context creation", async () => { await createSanityContext({ request, cache, client, }) expect(setServerClient).not.toHaveBeenCalled() }) it('should call `setServerClient` on every `loadQuery` invocation', async () => { const context = await createSanityContext({ request, cache, client, }) await context.loadQuery<boolean>(query, params) expect(setServerClient).toHaveBeenCalledTimes(1) await context.loadQuery<boolean>(query, params) expect(setServerClient).toHaveBeenCalledTimes(2) }) it('should call `setServerClient` with the preview-configured client', async () => { const previewSession = new PreviewSession() previewSession.set('projectId', projectId) const context = await createSanityContext({ request, cache, client, preview: { token: 'my-token', session: previewSession, }, }) await context.loadQuery<boolean>(query, params) const calledWithClient = setServerClient.mock.calls[0][0] expect(calledWithClient.config().useCdn).toBe(false) expect(calledWithClient.config().token).toBe('my-token') }) it('should display warning when `loadQuery` called outside preview mode in development', async () => { const originalNodeEnv = process.env.NODE_ENV process.env.NODE_ENV = 'development' const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => undefined) const context = await createSanityContext({ request, cache, client, }) const callsBefore = warnSpy.mock.calls.length await context.loadQuery<boolean>(query, params) // Should have warned about loadQuery usage (may have other warnings too) expect(warnSpy.mock.calls.length).toBeGreaterThan(callsBefore) expect(warnSpy).toHaveBeenCalledWith( expect.stringContaining('`loadQuery` is being called outside of preview mode'), ) const callsAfterFirst = warnSpy.mock.calls.length // Second call should not warn again about loadQuery await context.loadQuery<boolean>(query, params) expect(warnSpy.mock.calls.length).toBe(callsAfterFirst) warnSpy.mockRestore() process.env.NODE_ENV = originalNodeEnv }) it("shouldn't display warning in production", async () => { const originalNodeEnv = process.env.NODE_ENV process.env.NODE_ENV = 'production' const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => undefined) const context = await createSanityContext({ request, cache, client, }) await context.loadQuery<boolean>(query, params) expect(warnSpy).not.toHaveBeenCalled() warnSpy.mockRestore() process.env.NODE_ENV = originalNodeEnv }) it("shouldn't display warning when in preview mode", async () => { const originalNodeEnv = process.env.NODE_ENV process.env.NODE_ENV = 'development' const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => undefined) const previewSession = new PreviewSession() previewSession.set('projectId', projectId) const context = await createSanityContext({ request, cache, client, preview: { token: 'my-token', session: previewSession, }, }) await context.loadQuery<boolean>(query, params) // Should not warn because we're in preview mode expect(warnSpy).not.toHaveBeenCalled() warnSpy.mockRestore() process.env.NODE_ENV = originalNodeEnv }) })