UNPKG

sanity

Version:

Sanity is a real-time content infrastructure with a scalable, hosted backend featuring a Graph Oriented Query Language (GROQ), asset pipelines and fast edge caches

386 lines (335 loc) • 11.2 kB
import {beforeEach, describe, expect, it, jest} from '@jest/globals' import {type SanityClient} from '@sanity/client' import { concat, type ConnectableObservable, EMPTY, firstValueFrom, lastValueFrom, type Observable, of, Subject, timer, } from 'rxjs' import {buffer, publish, takeWhile} from 'rxjs/operators' import {createMockSanityClient} from '../../../../../../test/mocks/mockSanityClient' import {getFallbackLocaleSource} from '../../../../i18n/fallback' import {type DocumentAvailability, type DraftsModelDocumentAvailability} from '../../../../preview' import {createSchema} from '../../../../schema' import {editState, type EditStateFor} from './editState' import {validation} from './validation' // Mock `./editState` const mockEditState = editState as jest.Mock<typeof editState> jest.mock('./editState', () => ({editState: jest.fn()})) const schema = createSchema({ name: 'default', types: [ { name: 'movie', title: 'Movie', type: 'document', fields: [ {name: 'title', type: 'string'}, {name: 'exampleRef', type: 'reference', to: [{type: 'movie'}]}, {name: 'exampleRefTwo', type: 'reference', to: [{type: 'movie'}]}, ], }, ], }) const AVAILABLE: DocumentAvailability = {available: true, reason: 'READABLE'} const NOT_FOUND: DocumentAvailability = {available: false, reason: 'NOT_FOUND'} // A fixture used to set up a validation stream/subscription and wait // for certain events (e.g. when validation is finished running) function createSubscription( client: SanityClient, observeDocumentPairAvailability: (id: string) => Observable<DraftsModelDocumentAvailability>, ) { const getClient = () => client const stream = validation( {client, getClient, schema, observeDocumentPairAvailability, i18n: getFallbackLocaleSource()}, {publishedId: 'example-id', draftId: 'drafts.example-id'}, 'movie', ).pipe(publish()) // Publish and connect this for the tests ;(stream as ConnectableObservable<unknown>).connect() // Create a subject we can use to notify via `done.next()` const done = new Subject<void>() // Create a subscription that collects all emissions until `done.next()` const subscription = firstValueFrom(stream.pipe(buffer(done))) return { subscription, closeSubscription: () => done.next(), doneValidating: () => { return lastValueFrom(stream.pipe(takeWhile((e) => e.isValidating, true))) }, } } // @todo: fix mock sanity client is not compatible with SanityClient function getMockClient() { return createMockSanityClient() as any as SanityClient } /** * READ THIS: Each tests passes when being run individually, but not when run as part of the whole suite. * It's probably a symptom of something wrong with the caching and should be investigated. */ describe('validation', () => { beforeEach(() => { mockEditState.mockReset() }) it('runs `editState` through `validateDocument` to create a stream of validation statuses', async () => { const client = getMockClient() const mockEditStateSubject = new Subject<EditStateFor>() mockEditState.mockImplementation(() => mockEditStateSubject.asObservable()) const {subscription, closeSubscription, doneValidating} = createSubscription( client, () => EMPTY, ) // simulate first emission from validation listener mockEditStateSubject.next({ id: 'example-id', draft: { _id: 'example-id', _createdAt: '2021-09-07T16:23:52.256Z', _rev: 'exampleRev1', _type: 'movie', _updatedAt: '2021-09-07T16:23:52.256Z', title: 5, }, transactionSyncLock: null, liveEdit: false, published: null, type: 'movie', ready: true, }) await doneValidating() closeSubscription() await expect(subscription).resolves.toMatchObject([ { isValidating: true, validation: [], }, { isValidating: false, validation: [ { item: {message: 'Expected type "String", got "Number"'}, level: 'error', path: ['title'], }, ], }, ]) mockEditStateSubject.complete() }) it.skip('re-runs validation when the edit state changes', async () => { const client = getMockClient() const mockEditStateSubject = new Subject<EditStateFor>() mockEditState.mockImplementation(() => mockEditStateSubject.asObservable()) const {subscription, closeSubscription, doneValidating} = createSubscription( client, () => EMPTY, ) // simulate first emission from validation listener mockEditStateSubject.next({ id: 'example-id', draft: { _id: 'example-id', _createdAt: '2021-09-07T16:23:52.256Z', _rev: 'exampleRev2', _type: 'movie', _updatedAt: '2021-09-07T16:23:52.256Z', title: 5, }, transactionSyncLock: null, liveEdit: false, published: null, type: 'movie', ready: true, }) // wait till validation is done before pushing a valid value await doneValidating() // push a valid value mockEditStateSubject.next({ id: 'example-id', draft: { _id: 'example-id', _createdAt: '2021-09-07T16:23:52.256Z', _rev: 'exampleRev3', _type: 'movie', _updatedAt: '2021-09-07T16:23:52.256Z', title: 'valid title', }, transactionSyncLock: null, liveEdit: false, published: null, type: 'movie', ready: true, }) await doneValidating() closeSubscription() await expect(subscription).resolves.toMatchObject([ {isValidating: true, validation: []}, { isValidating: false, validation: [{item: {message: 'Expected type "String", got "Number"'}}], }, {isValidating: true, validation: [{item: {message: 'Expected type "String", got "Number"'}}]}, {isValidating: false, validation: []}, ]) mockEditStateSubject.complete() }) it.skip('re-runs validation when dependency events change', async () => { const client = getMockClient() const subject = new Subject<DraftsModelDocumentAvailability>() // Mock `editState` const mockEditStateSubject = new Subject<EditStateFor>() mockEditState.mockImplementation(() => mockEditStateSubject.asObservable()) const observeDocumentPairAvailability = ( id: string, ): Observable<DraftsModelDocumentAvailability> => id === 'example-ref-id' ? concat(of({published: AVAILABLE, draft: AVAILABLE}), subject) : concat( of({published: AVAILABLE, draft: AVAILABLE}), of({published: AVAILABLE, draft: AVAILABLE}), ) const {subscription, closeSubscription, doneValidating} = createSubscription( client, observeDocumentPairAvailability, ) // simulate first emission from validation listener mockEditStateSubject.next({ id: 'example-id', draft: { _id: 'example-id', _createdAt: '2021-09-07T16:23:52.256Z', _rev: 'exampleRev4', _type: 'movie', _updatedAt: '2021-09-07T16:23:52.256Z', title: 'testing', exampleRef: {_ref: 'example-ref-id'}, exampleRefTwo: {_ref: 'example-ref-other'}, }, transactionSyncLock: null, liveEdit: false, published: null, type: 'movie', ready: true, }) await doneValidating() subject.next({published: NOT_FOUND, draft: AVAILABLE}) await doneValidating() // close the buffer closeSubscription() expect(await subscription).toMatchObject([ {isValidating: true, validation: [], revision: 'exampleRev4'}, {isValidating: false, validation: [], revision: 'exampleRev4'}, {isValidating: true, validation: [], revision: 'exampleRev4'}, { isValidating: false, validation: [ { item: {message: /.+/}, level: 'error', path: ['exampleRef'], }, ], }, ]) mockEditStateSubject.complete() }) // this means that when you subscribe to the same document, you'll // immediately get the previous value emitted to you // @todo: investigate why this fails it.skip('replays the last known version via `memoize` and `publishReplay`', async () => { const client = getMockClient() // Mock `editState` const mockEditStateSubject = new Subject<EditStateFor>() mockEditState.mockImplementation(() => mockEditStateSubject.asObservable()) const subscription = lastValueFrom( validation( { client, schema, getClient: () => client, observeDocumentPairAvailability: jest.fn(() => EMPTY), i18n: getFallbackLocaleSource(), }, {publishedId: 'example-id', draftId: 'drafts.example-id'}, 'movie', ).pipe(buffer(timer(500))), ) // simulate first emission from validation listener mockEditStateSubject.next({ id: 'example-id', draft: { _id: 'example-id', _createdAt: '2021-09-07T16:23:52.256Z', _rev: 'exampleRev5', _type: 'movie', _updatedAt: '2021-09-07T16:23:52.256Z', title: 5, }, transactionSyncLock: null, liveEdit: false, published: null, type: 'movie', ready: true, }) const result = await subscription expect(result).toMatchObject([ { isValidating: true, validation: [], }, { isValidating: false, validation: [ { item: {message: 'Expected type "String", got "Number"'}, level: 'error', path: ['title'], }, ], }, ]) const immediatePlayback = await firstValueFrom( validation( {client, schema} as any, {publishedId: 'example-id', draftId: 'drafts.example-id'}, 'movie', ), ) const immediatePlaybackAgain = await firstValueFrom( validation( {client, schema} as any, {publishedId: 'example-id', draftId: 'drafts.example-id'}, 'movie', ), ) expect(result[result.length - 1]).toEqual(immediatePlayback) expect(immediatePlayback).toEqual(immediatePlaybackAgain) }) it.skip('returns empty validation message arrays if there is no available published or draft snapshot', async () => { const client = getMockClient() // Mock `editState` const mockEditStateSubject = new Subject<EditStateFor>() mockEditState.mockImplementation(() => mockEditStateSubject.asObservable()) const {subscription, closeSubscription, doneValidating} = createSubscription( client, () => EMPTY, ) mockEditStateSubject.next({ id: 'example-id', draft: null, liveEdit: false, published: null, type: 'movie', ready: true, transactionSyncLock: null, }) await doneValidating() closeSubscription() await expect(subscription).resolves.toMatchObject([{isValidating: false, validation: []}]) }) })