import {
FileType,
ProjectPluginStructure,
UIDLEcommerceSettings,
UIDLInvoiceSettings,
} from '@teleporthq/teleport-types'
import { NextEcommerceProjectPlugin } from '../src/ecommerce/project-plugin'
import {
buildWorkflowEcommerceSettingsPayload,
generateEcommerceSettingsApiRoute,
} from '../src/ecommerce/ecommerce-api-routes-generator'
// The client-side `ecommerce-get-settings` workflow node (see
// teleport-plugin-next-workflows) reads `window.__teleportEcommerceSettings`
// instead of fetching /api/ecommerce/settings. This file pins the supply side
// of that contract:
// 1. the generated ecommerce-context publishes the global at module-eval
// time with EXACTLY the payload the settings route bakes;
// 2. the settings-less backstop context (emitted only so cart-hook imports
// resolve) does NOT publish a defaults-shaped payload the workflow node
// would mistake for real merchant settings.
const APP_CONTENT = [
"import React from 'react'",
'function MyApp({ Component, pageProps }) {',
' return (',
' ',
' )',
'}',
'export default MyApp',
].join('\n')
const SETTINGS: UIDLEcommerceSettings = {
guestCheckout: false,
stockManagement: true,
cashOnDelivery: true,
deliveryEnabled: true,
storePickupEnabled: false,
stockManagementConfig: {
allowBackorders: false,
maxQuantityPerProduct: 5,
lowStockThreshold: 3,
outOfStockVisibility: 'hidden',
},
} as unknown as UIDLEcommerceSettings
const buildStructure = (
withSettings: boolean,
componentContent?: string
): ProjectPluginStructure => {
const files = new Map> }>()
if (componentContent) {
files.set('navigation', {
path: ['components'],
files: [{ name: 'navigation', fileType: FileType.JS, content: componentContent }],
})
}
files.set('_app', {
path: ['pages'],
files: [{ name: '_app', fileType: FileType.JS, content: APP_CONTENT }],
})
return {
uidl: {
globals: { env: {} },
...(withSettings ? { ecommerceSettings: SETTINGS } : {}),
},
files,
dependencies: {},
devDependencies: {},
} as unknown as ProjectPluginStructure
}
const contextContent = (structure: ProjectPluginStructure): string =>
String(structure.files.get('ecommerce-context')?.files?.[0]?.content || '')
describe('workflow settings global in the generated ecommerce-context', () => {
it('publishes window.__teleportEcommerceSettings with the exact route payload', async () => {
const plugin = new NextEcommerceProjectPlugin()
const structure = buildStructure(true)
await plugin.runAfter(structure)
const content = contextContent(structure)
const expectedPayload = JSON.stringify(buildWorkflowEcommerceSettingsPayload(SETTINGS))
expect(content).toContain(`const WORKFLOW_ECOMMERCE_SETTINGS = ${expectedPayload}`)
expect(content).toContain('window.__teleportEcommerceSettings = WORKFLOW_ECOMMERCE_SETTINGS')
// The route bakes the SAME payload — the two can never disagree.
const route = generateEcommerceSettingsApiRoute(SETTINGS)
expect(route).toContain(expectedPayload)
})
it('publishes the global at module scope, not inside the provider', async () => {
const plugin = new NextEcommerceProjectPlugin()
const structure = buildStructure(true)
await plugin.runAfter(structure)
const content = contextContent(structure)
const globalAt = content.indexOf('window.__teleportEcommerceSettings')
const providerAt = content.indexOf('export const EcommerceProvider')
expect(globalAt).toBeGreaterThan(-1)
expect(providerAt).toBeGreaterThan(-1)
expect(globalAt).toBeLessThan(providerAt)
})
it('carries the storefront tax view for workflow consumers (e.g. the AI chat)', () => {
const taxed = { defaultTaxRate: 19, taxIncludedInPrice: false } as UIDLInvoiceSettings
const payload = buildWorkflowEcommerceSettingsPayload(SETTINGS, taxed)
expect(payload.storefrontTaxRate).toBe(19)
expect(payload.taxIncludedInPrice).toBe(false)
expect(payload.defaultTaxRate).toBe(19)
})
it('collapses the tax rate to 0 when tax is included in prices', () => {
const included = { defaultTaxRate: 19, taxIncludedInPrice: true } as UIDLInvoiceSettings
const payload = buildWorkflowEcommerceSettingsPayload(SETTINGS, included)
expect(payload.storefrontTaxRate).toBe(0)
expect(payload.taxIncludedInPrice).toBe(true)
// The configured rate is still reported for consumers that need it.
expect(payload.defaultTaxRate).toBe(19)
})
it('defaults the tax fields when there are no invoice settings', () => {
const payload = buildWorkflowEcommerceSettingsPayload(SETTINGS)
expect(payload.storefrontTaxRate).toBe(0)
expect(payload.taxIncludedInPrice).toBe(false)
expect(payload.defaultTaxRate).toBe(0)
})
it('settings-less backstop context does NOT publish the global', async () => {
const plugin = new NextEcommerceProjectPlugin()
const structure = buildStructure(
false,
"import { useEcommerce } from '@/ecommerce-context'\nexport default function Nav() { return null }"
)
await plugin.runAfter(structure)
const content = contextContent(structure)
expect(content.length).toBeGreaterThan(0)
expect(content).not.toContain('__teleportEcommerceSettings')
})
})