import { FileType, ProjectPluginStructure, UIDLEcommerceSettings } from '@teleporthq/teleport-types' import { NextEcommerceProjectPlugin } from '../src/ecommerce/project-plugin' import { generateCategoryCountsApiRoute } from '../src/ecommerce/ecommerce-api-routes-generator' import { generateEcommerceContextFileContent } from '../src/ecommerce/ecommerce-context-generator' // The live product count per category: a GET route counting the active // products over every product's `category_filter_ids` JSON array, and the // ecommerce context stamping its answer on every node of `ecommerceCategories` // as `productCount` — `undefined` until it answers, so a bound text keeps its // drawn number. const APP_CONTENT = [ "import React from 'react'", 'function MyApp({ Component, pageProps }) {', ' return (', ' ', ' )', '}', 'export default MyApp', ].join('\n') const SETTINGS: UIDLEcommerceSettings = { cashOnDelivery: true, deliveryEnabled: false, storePickupEnabled: false, guestCheckout: true, stockManagement: false, orderNotifications: false, deliveryConfig: null, stockManagementConfig: null, orderNotificationConfig: null, paymentProviders: [], } as unknown as UIDLEcommerceSettings const buildStructure = (): ProjectPluginStructure => { const files = new Map> }>() files.set('_app', { path: ['pages'], files: [{ name: '_app', fileType: FileType.JS, content: APP_CONTENT }], }) return { uidl: { globals: { env: {} }, ecommerceSettings: SETTINGS }, files, dependencies: {}, devDependencies: {}, } as unknown as ProjectPluginStructure } describe('ecommerce category product counts', () => { it('registers the counts route beside the variants route, even without a database', async () => { const plugin = new NextEcommerceProjectPlugin() const structure = buildStructure() await plugin.runAfter(structure) const entry = structure.files.get('ecommerce-api-category-counts') expect(entry?.path).toEqual(['pages', 'api', 'ecommerce']) expect(entry?.files[0].name).toBe('category-counts') // Inert without a database: nothing to count, no total. expect(String(entry?.files[0].content)).toContain( 'res.status(200).json({ counts: {}, total: null })' ) }) it('counts over the unnested JSON array on Postgres, in JavaScript when the cast fails', () => { const route = generateCategoryCountsApiRoute('teleport', {}) expect(route).toContain("if (req.method !== 'GET')") expect(route).toContain( "SELECT elem AS id, COUNT(*)::int AS count FROM teleport_products p, jsonb_array_elements_text(COALESCE(NULLIF(p.category_filter_ids, '')::jsonb, '[]'::jsonb)) AS elem WHERE p.status = 'active' GROUP BY elem" ) expect(route).toContain( "SELECT category_filter_ids FROM teleport_products WHERE status = 'active'" ) expect(route).toContain( "SELECT COUNT(*) AS total FROM teleport_products WHERE status = 'active'" ) // No jsonb on MySQL: straight to the JavaScript count. expect(generateCategoryCountsApiRoute('mysql', {})).not.toContain('jsonb_array_elements_text') }) it('stamps productCount on every category node once the route has answered', () => { const source = generateEcommerceContextFileContent(SETTINGS, undefined, 'ds-1') expect(source).toContain("fetch('/api/ecommerce/category-counts')") expect(source).toContain('productCount: counts ? counts[category.id] || 0 : undefined') expect(source).toContain('children: withProductCounts(category.children, counts)') expect(source).toContain('[ecommerceCategoriesRaw, router.locale, categoryCounts]') // The total rides on the provider value, and both are in the memo's deps. expect(source).toContain(' productsTotal,\n') expect(source).toContain('ecommerceCategories, productsTotal])') }) })