import * as types from '@babel/types' import generator from '@babel/generator' import { ChunkType, FileType, ComponentStructure, ChunkDefinition, } from '@teleporthq/teleport-types' import { createNextArrayMapperPaginationPlugin } from '../src/pagination-plugin' // A `const TestComponent = (props) => { return
}` shell. The pagination // plugin prepends the search/pagination state hooks and splices the URL-sync // effects before the return, so a minimal body is enough to observe them — the // effects are derived from the UIDL registry, not from the JSX wiring. const makeComponentChunk = (): ChunkDefinition => { const body = types.blockStatement([ types.returnStatement( types.jsxElement( types.jsxOpeningElement(types.jsxIdentifier('div'), [], true), null, [], true ) ), ]) const arrow = types.arrowFunctionExpression([types.identifier('props')], body) const declaration = types.variableDeclaration('const', [ types.variableDeclarator(types.identifier('TestComponent'), arrow), ]) return { name: 'jsx-component', type: ChunkType.AST, fileType: FileType.JS, linkAfter: [], content: declaration, meta: {}, } } // A `data-source-list > cms-list-repeater` UIDL: a paginated + search-enabled // products list, optionally bound to a URL search-param key. // tslint:disable-next-line:no-any const makeUidlNode = (searchUrlParamKey?: string): any => ({ type: 'data-source-list', content: { renderPropIdentifier: 'items', resourceDefinition: { dataSourceId: 'ds1', tableName: 'products', dataSourceType: 'postgresql', }, resource: { params: { queryColumns: { content: ['name'] } } }, nodes: { success: { type: 'cms-list-repeater', content: { renderPropIdentifier: 'product', paginated: true, perPage: 20, searchEnabled: true, searchDebounce: 300, ...(searchUrlParamKey ? { searchUrlParamKey } : {}), nodes: { list: { type: 'element', content: { elementType: 'div' } } }, }, }, }, }, }) const runPlugin = async (searchUrlParamKey?: string): Promise => { const chunk = makeComponentChunk() const structure: ComponentStructure = { uidl: { name: 'TestComponent', node: makeUidlNode(searchUrlParamKey) }, chunks: [chunk], dependencies: {}, options: { dataSources: {}, extractedResources: {} }, } as never const plugin = createNextArrayMapperPaginationPlugin() await plugin(structure) return generator(chunk.content as types.Node).code } describe('pagination plugin — search input URL two-way sync (searchUrlParamKey)', () => { it('seeds the query from window.location.search and emits paired read-back/write-back effects', async () => { const code = await runPlugin('searchKeyword') // Initial value seeded from the URL on the client (both the immediate input // state AND the debounced value, so a deep link fetches filtered on mount). const seedMatches = code.match( /new URLSearchParams\(window\.location\.search\)\.get\("searchKeyword"\)/g ) expect(seedMatches).not.toBeNull() expect((seedMatches || []).length).toBe(2) expect(code).toContain('typeof window !== "undefined"') // useRouter is injected for the effects. expect(code).toContain('const router = useRouter()') // Write-back (debounced query → URL), keyed on the DEBOUNCED value. expect(code).toContain('__nextQuery.searchKeyword = String(ds_0_state.debouncedQuery)') expect(code).toContain('if (__nextQuery.searchKeyword === router.query.searchKeyword) return') expect(code).toContain('}, [ds_0_state.debouncedQuery, router.isReady])') // Read-back (URL → input), functional setState bail-out (loop-free). expect(code).toContain('const __urlValue = router.query.searchKeyword') expect(code).toContain('setDs_0_searchQuery(prev => prev === __nextValue ? prev : __nextValue)') expect(code).toContain('}, [router.query.searchKeyword, router.isReady])') }) it('does NOT emit any URL sync when the repeater has no searchUrlParamKey (unchanged behaviour)', async () => { const code = await runPlugin(undefined) // Search still works locally (state + debounce), but nothing touches the URL. expect(code).toContain('ds_0_searchQuery') expect(code).not.toContain('searchKeyword') expect(code).not.toContain('window.location.search') expect(code).not.toContain('router.replace') expect(code).not.toContain('useRouter') }) })