import withKlaviyoIos from '../plugin/withKlaviyoIos'; import { KlaviyoPluginIosProps } from '../plugin/types'; import { createMockIosConfig, createMockIosProps } from './utils/testHelpers'; describe('withKlaviyoIos', () => { let mockConfig: any; let mockProps: KlaviyoPluginIosProps; beforeEach(() => { jest.clearAllMocks(); mockConfig = createMockIosConfig(); mockProps = createMockIosProps(); }); describe('withRemoteNotificationsPermissions', () => { it('should add klaviyo_app_group and klaviyo_badge_autoclearing to existing Info.plist', () => { const modifiedConfig = withKlaviyoIos(mockConfig, mockProps) as any; expect(modifiedConfig.modResults).toBeDefined(); expect(modifiedConfig.modResults.klaviyo_app_group).toBe('group.com.test.app.KlaviyoNotificationServiceExtension.shared'); expect(modifiedConfig.modResults.klaviyo_badge_autoclearing).toBe(true); }); it('should add flags to Info.plist with badgeAutoclearing set to false', () => { const propsWithBadgeClearingDisabled = createMockIosProps({ badgeAutoclearing: false, }); const modifiedConfig = withKlaviyoIos(mockConfig, propsWithBadgeClearingDisabled) as any; expect(modifiedConfig.modResults).toBeDefined(); expect(modifiedConfig.modResults.klaviyo_app_group).toBe('group.com.test.app.KlaviyoNotificationServiceExtension.shared'); expect(modifiedConfig.modResults.klaviyo_badge_autoclearing).toBe(false); }); it('should add flags to Info.plist with minimal existing content', () => { const minimalConfig = createMockIosConfig({ modResults: { CFBundleIdentifier: 'com.test.app', }, }); const modifiedConfig = withKlaviyoIos(minimalConfig, mockProps) as any; expect(modifiedConfig.modResults).toBeDefined(); expect(modifiedConfig.modResults.klaviyo_app_group).toBe('group.com.test.app.KlaviyoNotificationServiceExtension.shared'); expect(modifiedConfig.modResults.klaviyo_badge_autoclearing).toBe(true); expect(modifiedConfig.modResults.CFBundleIdentifier).toBe('com.test.app'); }); it('should update existing klaviyo flags if they already exist', () => { const configWithExistingFlags = createMockIosConfig({ modResults: { ...mockConfig.modResults, klaviyo_app_group: 'group.existing.app.shared', klaviyo_badge_autoclearing: false, }, }); const modifiedConfig = withKlaviyoIos(configWithExistingFlags, mockProps) as any; expect(modifiedConfig.modResults).toBeDefined(); expect(modifiedConfig.modResults.klaviyo_app_group).toBe('group.com.test.app.KlaviyoNotificationServiceExtension.shared'); expect(modifiedConfig.modResults.klaviyo_badge_autoclearing).toBe(true); }); it('should handle config without ios bundleIdentifier', () => { const configWithoutBundleId = createMockIosConfig({ ios: {}, }); expect(() => { withKlaviyoIos(configWithoutBundleId, mockProps); }).toThrow('iOS bundle identifier is required but not found in app configuration'); }); it('should handle empty modResults', () => { const configWithEmptyResults = createMockIosConfig({ modResults: {}, }); const modifiedConfig = withKlaviyoIos(configWithEmptyResults, mockProps) as any; expect(modifiedConfig.modResults).toBeDefined(); expect(modifiedConfig.modResults.klaviyo_app_group).toBe('group.com.test.app.KlaviyoNotificationServiceExtension.shared'); expect(modifiedConfig.modResults.klaviyo_badge_autoclearing).toBe(true); }); }); describe('withKlaviyoAppGroup', () => { it('should add app group to entitlements when none exist', () => { const configWithEntitlements = createMockIosConfig({ modResults: {}, }); const modifiedConfig = withKlaviyoIos(configWithEntitlements, mockProps); expect(modifiedConfig).toBeDefined(); }); it('should add app group to existing entitlements', () => { const configWithExistingEntitlements = createMockIosConfig({ modResults: { 'com.apple.security.application-groups': ['group.existing.app'], }, }); const modifiedConfig = withKlaviyoIos(configWithExistingEntitlements, mockProps); expect(modifiedConfig).toBeDefined(); }); }); describe('plugin integration', () => { it('should apply all plugins in the correct order', () => { const modifiedConfig = withKlaviyoIos(mockConfig, mockProps) as any; expect(modifiedConfig.modResults).toBeDefined(); expect(modifiedConfig.modResults.klaviyo_app_group).toBe('group.com.test.app.KlaviyoNotificationServiceExtension.shared'); expect(modifiedConfig.modResults.klaviyo_badge_autoclearing).toBe(true); }); it('should handle missing project name gracefully', () => { const configWithoutProjectName = createMockIosConfig({ modRequest: { ...mockConfig.modRequest, projectName: undefined, }, }); const modifiedConfig = withKlaviyoIos(configWithoutProjectName, mockProps) as any; expect(modifiedConfig.modResults).toBeDefined(); expect(modifiedConfig.modResults.klaviyo_app_group).toBe('group.com.test.app.KlaviyoNotificationServiceExtension.shared'); expect(modifiedConfig.modResults.klaviyo_badge_autoclearing).toBe(true); }); }); describe('app group identifier consistency', () => { const expectedAppGroupName = 'group.com.test.app.KlaviyoNotificationServiceExtension.shared'; const nseTargetName = 'KlaviyoNotificationServiceExtension'; it('should use consistent app group name across all configurations', () => { const modifiedConfig = withKlaviyoIos(mockConfig, mockProps) as any; // Verify main app Info.plist has correct app group expect(modifiedConfig.modResults.klaviyo_app_group).toBe(expectedAppGroupName); }); it('should add correct app group to main app entitlements', () => { const configWithEntitlements = createMockIosConfig({ modResults: {}, }); const modifiedConfig = withKlaviyoIos(configWithEntitlements, mockProps); // The withKlaviyoAppGroup plugin should add the correct app group expect(modifiedConfig).toBeDefined(); }); it('should validate app group name format matches expected pattern', () => { // This test validates that the app group name follows the correct format // and includes the NSE target name as expected expect(expectedAppGroupName).toMatch(/^group\.com\.test\.app\.KlaviyoNotificationServiceExtension\.shared$/); expect(expectedAppGroupName).toContain(nseTargetName); expect(expectedAppGroupName).not.toContain('group.com.test.app.shared'); }); it('should ensure plugin generates consistent app group name', () => { // Test that the plugin consistently generates the same app group name const config1 = withKlaviyoIos(mockConfig, mockProps) as any; const config2 = withKlaviyoIos(mockConfig, mockProps) as any; expect(config1.modResults.klaviyo_app_group).toBe(config2.modResults.klaviyo_app_group); expect(config1.modResults.klaviyo_app_group).toBe(expectedAppGroupName); }); it('should validate app group name is not the incorrect short format', () => { // This test ensures we're not using the incorrect short format const incorrectAppGroupName = 'group.com.test.app.shared'; expect(expectedAppGroupName).not.toBe(incorrectAppGroupName); expect(expectedAppGroupName).toContain(nseTargetName); }); it('should handle different bundle identifiers consistently', () => { const testBundleIds = [ 'com.example.app', 'com.example.app.preview', 'com.example.app.development' ]; testBundleIds.forEach(bundleId => { const configWithBundleId = createMockIosConfig({ ios: { bundleIdentifier: bundleId } }); const modifiedConfig = withKlaviyoIos(configWithBundleId, mockProps) as any; // The app group should use the actual bundle identifier from the config const expectedAppGroupForBundle = `group.${bundleId}.KlaviyoNotificationServiceExtension.shared`; expect(modifiedConfig.modResults.klaviyo_app_group).toBe(expectedAppGroupForBundle); }); }); }); describe('version synchronization', () => { describe('main app target Info.plist', () => { it('should set CFBundleShortVersionString from config.version', () => { const configWithVersion = createMockIosConfig({ version: '0.11.0', ios: { buildNumber: '25' }, }); const modifiedConfig = withKlaviyoIos(configWithVersion, mockProps) as any; expect(modifiedConfig.modResults).toBeDefined(); expect(modifiedConfig.modResults.CFBundleShortVersionString).toBe('0.11.0'); }); it('should set CFBundleVersion from config.ios.buildNumber', () => { const configWithVersion = createMockIosConfig({ version: '0.11.0', ios: { buildNumber: '25' }, }); const modifiedConfig = withKlaviyoIos(configWithVersion, mockProps) as any; expect(modifiedConfig.modResults).toBeDefined(); expect(modifiedConfig.modResults.CFBundleVersion).toBe('25'); }); it('should default CFBundleShortVersionString to "1.0" when config.version is not provided', () => { const configWithoutVersion = createMockIosConfig({ version: undefined, ios: { buildNumber: '25' }, }); const modifiedConfig = withKlaviyoIos(configWithoutVersion, mockProps) as any; expect(modifiedConfig.modResults).toBeDefined(); expect(modifiedConfig.modResults.CFBundleShortVersionString).toBe('1.0'); }); it('should default CFBundleVersion to "1" when config.ios.buildNumber is not provided', () => { const configWithoutBuildNumber = createMockIosConfig({ version: '0.11.0', ios: { buildNumber: undefined }, }); const modifiedConfig = withKlaviyoIos(configWithoutBuildNumber, mockProps) as any; expect(modifiedConfig.modResults).toBeDefined(); expect(modifiedConfig.modResults.CFBundleVersion).toBe('1'); }); it('should override existing CFBundleShortVersionString in Info.plist', () => { const configWithExistingVersion = createMockIosConfig({ version: '0.11.0', ios: { buildNumber: '25' }, modResults: { CFBundleShortVersionString: '2.0.0', CFBundleVersion: '100', }, }); const modifiedConfig = withKlaviyoIos(configWithExistingVersion, mockProps) as any; expect(modifiedConfig.modResults.CFBundleShortVersionString).toBe('0.11.0'); expect(modifiedConfig.modResults.CFBundleVersion).toBe('25'); }); }); describe('NSE extension Info.plist', () => { const { FileManager } = require('../plugin/support/fileManager'); const mockInfoPlistContent = ` CFBundleVersion 1 CFBundleShortVersionString 1.0 `; async function runIosMod(config: any, props: any) { const result = withKlaviyoIos(config, props) as any; if (result.mods && result.mods.ios) { return await result.mods.ios(result); } return result; } beforeEach(() => { jest.clearAllMocks(); FileManager.readFile.mockResolvedValue(mockInfoPlistContent); FileManager.writeFile.mockResolvedValue(undefined); FileManager.copyFile.mockResolvedValue(undefined); FileManager.dirExists.mockReturnValue(true); }); it('should update CFBundleShortVersionString in NSE Info.plist from config.version', async () => { const configWithVersion = createMockIosConfig({ version: '0.11.0', ios: { buildNumber: '25' }, }); await runIosMod(configWithVersion, mockProps); expect(FileManager.writeFile).toHaveBeenCalled(); const writeCall = FileManager.writeFile.mock.calls.find(call => call && call[0] && typeof call[0] === 'string' && call[0].includes('KlaviyoNotificationServiceExtension-Info.plist') ); expect(writeCall).toBeDefined(); const writtenContent = writeCall[1]; expect(writtenContent).toContain('CFBundleShortVersionString'); expect(writtenContent).toContain('0.11.0'); }); it('should update CFBundleVersion in NSE Info.plist from config.ios.buildNumber', async () => { const configWithVersion = createMockIosConfig({ version: '0.11.0', ios: { buildNumber: '25' }, }); await runIosMod(configWithVersion, mockProps); expect(FileManager.writeFile).toHaveBeenCalled(); const writeCall = FileManager.writeFile.mock.calls.find(call => call && call[0] && typeof call[0] === 'string' && call[0].includes('KlaviyoNotificationServiceExtension-Info.plist') ); expect(writeCall).toBeDefined(); const writtenContent = writeCall[1]; expect(writtenContent).toContain('CFBundleVersion'); expect(writtenContent).toContain('25'); }); it('should default CFBundleShortVersionString to "1.0" in NSE when config.version is not provided', async () => { const configWithoutVersion = createMockIosConfig({ version: undefined, ios: { buildNumber: '25' }, }); await runIosMod(configWithoutVersion, mockProps); expect(FileManager.writeFile).toHaveBeenCalled(); const writeCall = FileManager.writeFile.mock.calls.find(call => call && call[0] && typeof call[0] === 'string' && call[0].includes('KlaviyoNotificationServiceExtension-Info.plist') ); const writtenContent = writeCall[1]; expect(writtenContent).toContain('1.0'); }); it('should default CFBundleVersion to "1" in NSE when config.ios.buildNumber is not provided', async () => { const configWithoutBuildNumber = createMockIosConfig({ version: '0.11.0', ios: { buildNumber: undefined }, }); await runIosMod(configWithoutBuildNumber, mockProps); expect(FileManager.writeFile).toHaveBeenCalled(); const writeCall = FileManager.writeFile.mock.calls.find(call => call && call[0] && typeof call[0] === 'string' && call[0].includes('KlaviyoNotificationServiceExtension-Info.plist') ); const writtenContent = writeCall[1]; expect(writtenContent).toContain('1'); }); it('should update both version fields correctly in NSE Info.plist', async () => { const configWithVersion = createMockIosConfig({ version: '2.5.3', ios: { buildNumber: '42' }, }); await runIosMod(configWithVersion, mockProps); expect(FileManager.writeFile).toHaveBeenCalled(); const writeCall = FileManager.writeFile.mock.calls.find(call => call && call[0] && typeof call[0] === 'string' && call[0].includes('KlaviyoNotificationServiceExtension-Info.plist') ); const writtenContent = writeCall[1]; expect(writtenContent).toContain('2.5.3'); expect(writtenContent).toContain('42'); expect(writtenContent).not.toContain('1.0'); expect(writtenContent).not.toContain('1'); }); it('should ensure main app and NSE extension have matching versions', async () => { const configWithVersion = createMockIosConfig({ version: '0.11.0', ios: { buildNumber: '25' }, }); const modifiedConfig = await runIosMod(configWithVersion, mockProps) as any; // Check main app version expect(modifiedConfig.modResults.CFBundleShortVersionString).toBe('0.11.0'); expect(modifiedConfig.modResults.CFBundleVersion).toBe('25'); // Check NSE extension version was written expect(FileManager.writeFile).toHaveBeenCalled(); const writeCall = FileManager.writeFile.mock.calls.find(call => call && call[0] && typeof call[0] === 'string' && call[0].includes('KlaviyoNotificationServiceExtension-Info.plist') ); const writtenContent = writeCall[1]; expect(writtenContent).toContain('0.11.0'); expect(writtenContent).toContain('25'); }); }); }); describe('withGeofencingBackgroundMode', () => { describe('UIBackgroundModes configuration', () => { it('should add location to UIBackgroundModes when geofencingEnabled is true', () => { const propsWithGeofencing = createMockIosProps({ geofencingEnabled: true, }); const modifiedConfig = withKlaviyoIos(mockConfig, propsWithGeofencing) as any; expect(modifiedConfig.modResults).toBeDefined(); expect(modifiedConfig.modResults.UIBackgroundModes).toBeDefined(); expect(Array.isArray(modifiedConfig.modResults.UIBackgroundModes)).toBe(true); expect(modifiedConfig.modResults.UIBackgroundModes).toContain('location'); }); it('should not add location to UIBackgroundModes when geofencingEnabled is false', () => { const propsWithoutGeofencing = createMockIosProps({ geofencingEnabled: false, }); const modifiedConfig = withKlaviyoIos(mockConfig, propsWithoutGeofencing) as any; expect(modifiedConfig.modResults).toBeDefined(); // If UIBackgroundModes exists, it should not contain 'location' if (modifiedConfig.modResults.UIBackgroundModes) { expect(modifiedConfig.modResults.UIBackgroundModes).not.toContain('location'); } }); it('should not add location to UIBackgroundModes when geofencingEnabled is undefined', () => { const propsWithoutGeofencing = createMockIosProps({ geofencingEnabled: undefined, }); const modifiedConfig = withKlaviyoIos(mockConfig, propsWithoutGeofencing) as any; expect(modifiedConfig.modResults).toBeDefined(); // If UIBackgroundModes exists, it should not contain 'location' if (modifiedConfig.modResults.UIBackgroundModes) { expect(modifiedConfig.modResults.UIBackgroundModes).not.toContain('location'); } }); it('should not duplicate location if it already exists in UIBackgroundModes', () => { const configWithLocation = createMockIosConfig({ modResults: { UIBackgroundModes: ['remote-notification', 'location'], }, }); const propsWithGeofencing = createMockIosProps({ geofencingEnabled: true, }); const modifiedConfig = withKlaviyoIos(configWithLocation, propsWithGeofencing) as any; const locationCount = modifiedConfig.modResults.UIBackgroundModes.filter( (mode: string) => mode === 'location' ).length; expect(locationCount).toBe(1); }); }); describe('Swift file', () => { const { FileManager } = require('../plugin/support/fileManager'); async function runIosMod(config: any, props: any) { const result = withKlaviyoIos(config, props) as any; if (result.mods && result.mods.ios) { return await result.mods.ios(result); } return result; } beforeEach(() => { jest.clearAllMocks(); FileManager.readFile.mockResolvedValue(''); FileManager.writeFile.mockResolvedValue(undefined); FileManager.dirExists.mockReturnValue(true); FileManager.copyFile.mockResolvedValue(undefined); }); it('should not write to KlaviyoAppDelegate.swift regardless of geofencingEnabled', async () => { for (const geofencingEnabled of [true, false, undefined]) { jest.clearAllMocks(); FileManager.readFile.mockResolvedValue(''); FileManager.writeFile.mockResolvedValue(undefined); FileManager.dirExists.mockReturnValue(true); FileManager.copyFile.mockResolvedValue(undefined); await runIosMod(mockConfig, createMockIosProps({ geofencingEnabled })); const swiftWriteCall = FileManager.writeFile.mock.calls.find((call: any) => call && call[0] && typeof call[0] === 'string' && call[0].includes('KlaviyoAppDelegate.swift') ); expect(swiftWriteCall).toBeUndefined(); } }); it('should not write to ExpoKlaviyo.podspec', async () => { await runIosMod(mockConfig, createMockIosProps({ geofencingEnabled: true })); const podspecWriteCall = FileManager.writeFile.mock.calls.find((call: any) => call && call[0] && typeof call[0] === 'string' && call[0].includes('ExpoKlaviyo.podspec') ); expect(podspecWriteCall).toBeUndefined(); }); }); }); describe('withKlaviyoPodfileEnvVars', () => { const { FileManager } = require('../plugin/support/fileManager'); const mockPodfileContent = `platform :ios, '13.0' use_frameworks! :linkage => :static target 'TestApp' do config = use_native_modules! use_react_native!( :path => config[:reactNativePath], :hermes_enabled => true ) end `; async function runIosMod(config: any, props: any) { const result = withKlaviyoIos(config, props) as any; if (result.mods && result.mods.ios) { return await result.mods.ios(result); } return result; } beforeEach(() => { jest.clearAllMocks(); FileManager.readFile.mockResolvedValue(mockPodfileContent); FileManager.writeFile.mockResolvedValue(undefined); FileManager.dirExists.mockReturnValue(true); FileManager.copyFile.mockResolvedValue(undefined); }); it('should always write both KLAVIYO_INCLUDE_LOCATION and KLAVIYO_INCLUDE_FORMS', async () => { await runIosMod(mockConfig, createMockIosProps()); const podfileWriteCalls = FileManager.writeFile.mock.calls.filter((call: any) => call && call[0] && typeof call[0] === 'string' && call[0].includes('Podfile') ); const bothVarsWrite = podfileWriteCalls.find((call: any) => call[1].includes("ENV['KLAVIYO_INCLUDE_LOCATION']") && call[1].includes("ENV['KLAVIYO_INCLUDE_FORMS']") ); expect(bothVarsWrite).toBeDefined(); }); it('should set KLAVIYO_INCLUDE_LOCATION to false when geofencingEnabled is false (default)', async () => { await runIosMod(mockConfig, createMockIosProps({ geofencingEnabled: false })); const podfileWriteCalls = FileManager.writeFile.mock.calls.filter((call: any) => call && call[0] && typeof call[0] === 'string' && call[0].includes('Podfile') ); const envVarWrite = podfileWriteCalls.find((call: any) => call[1].includes("ENV['KLAVIYO_INCLUDE_LOCATION'] = 'false'") ); expect(envVarWrite).toBeDefined(); }); it('should set KLAVIYO_INCLUDE_LOCATION to true when geofencingEnabled is true', async () => { await runIosMod(mockConfig, createMockIosProps({ geofencingEnabled: true })); const podfileWriteCalls = FileManager.writeFile.mock.calls.filter((call: any) => call && call[0] && typeof call[0] === 'string' && call[0].includes('Podfile') ); const envVarWrite = podfileWriteCalls.find((call: any) => call[1].includes("ENV['KLAVIYO_INCLUDE_LOCATION'] = 'true'") ); expect(envVarWrite).toBeDefined(); }); it('should set KLAVIYO_INCLUDE_FORMS to false when formsEnabled is false', async () => { await runIosMod(mockConfig, createMockIosProps({ formsEnabled: false })); const podfileWriteCalls = FileManager.writeFile.mock.calls.filter((call: any) => call && call[0] && typeof call[0] === 'string' && call[0].includes('Podfile') ); const envVarWrite = podfileWriteCalls.find((call: any) => call[1].includes("ENV['KLAVIYO_INCLUDE_FORMS'] = 'false'") ); expect(envVarWrite).toBeDefined(); }); it('should set KLAVIYO_INCLUDE_FORMS to true when formsEnabled is true (default)', async () => { await runIosMod(mockConfig, createMockIosProps({ formsEnabled: true })); const podfileWriteCalls = FileManager.writeFile.mock.calls.filter((call: any) => call && call[0] && typeof call[0] === 'string' && call[0].includes('Podfile') ); const envVarWrite = podfileWriteCalls.find((call: any) => call[1].includes("ENV['KLAVIYO_INCLUDE_FORMS'] = 'true'") ); expect(envVarWrite).toBeDefined(); }); it('should set both vars to false when geofencing and forms are both disabled', async () => { await runIosMod(mockConfig, createMockIosProps({ geofencingEnabled: false, formsEnabled: false })); const podfileWriteCalls = FileManager.writeFile.mock.calls.filter((call: any) => call && call[0] && typeof call[0] === 'string' && call[0].includes('Podfile') ); const envVarWrite = podfileWriteCalls.find((call: any) => call[1].includes("ENV['KLAVIYO_INCLUDE_LOCATION'] = 'false'") && call[1].includes("ENV['KLAVIYO_INCLUDE_FORMS'] = 'false'") ); expect(envVarWrite).toBeDefined(); }); it('should set both vars to true when geofencing and forms are both enabled', async () => { await runIosMod(mockConfig, createMockIosProps({ geofencingEnabled: true, formsEnabled: true })); const podfileWriteCalls = FileManager.writeFile.mock.calls.filter((call: any) => call && call[0] && typeof call[0] === 'string' && call[0].includes('Podfile') ); const envVarWrite = podfileWriteCalls.find((call: any) => call[1].includes("ENV['KLAVIYO_INCLUDE_LOCATION'] = 'true'") && call[1].includes("ENV['KLAVIYO_INCLUDE_FORMS'] = 'true'") ); expect(envVarWrite).toBeDefined(); }); it('should remove existing env vars before re-injecting (idempotent)', async () => { const podfileWithExistingVars = `ENV['KLAVIYO_INCLUDE_LOCATION'] = 'false' ENV['KLAVIYO_INCLUDE_FORMS'] = 'false' platform :ios, '13.0' use_frameworks! :linkage => :static target 'TestApp' do config = use_native_modules! end `; FileManager.readFile.mockResolvedValue(podfileWithExistingVars); await runIosMod(mockConfig, createMockIosProps({ geofencingEnabled: true, formsEnabled: false })); const podfileWriteCalls = FileManager.writeFile.mock.calls.filter((call: any) => call && call[0] && typeof call[0] === 'string' && call[0].includes('Podfile') ); // LOCATION should now be 'true', FORMS should be 'false' const envVarWrite = podfileWriteCalls.find((call: any) => call[1].includes("ENV['KLAVIYO_INCLUDE_LOCATION'] = 'true'") && call[1].includes("ENV['KLAVIYO_INCLUDE_FORMS'] = 'false'") ); expect(envVarWrite).toBeDefined(); }); it('should prepend both env vars at the top of the Podfile', async () => { await runIosMod(mockConfig, createMockIosProps({ geofencingEnabled: false, formsEnabled: true })); const podfileWriteCalls = FileManager.writeFile.mock.calls.filter((call: any) => call && call[0] && typeof call[0] === 'string' && call[0].includes('Podfile') ); const envVarWrite = podfileWriteCalls.find((call: any) => call[1].includes("ENV['KLAVIYO_INCLUDE_LOCATION']") && call[1].includes("ENV['KLAVIYO_INCLUDE_FORMS']") ); expect(envVarWrite).toBeDefined(); const content = envVarWrite[1]; // Both env vars should appear before the platform declaration const locationIdx = content.indexOf("ENV['KLAVIYO_INCLUDE_LOCATION']"); const formsIdx = content.indexOf("ENV['KLAVIYO_INCLUDE_FORMS']"); const platformIdx = content.indexOf('platform :ios'); expect(locationIdx).toBeLessThan(platformIdx); expect(formsIdx).toBeLessThan(platformIdx); }); }); describe('withKlaviyoPodfile', () => { const { FileManager } = require('../plugin/support/fileManager'); const mockPodfileContent = `platform :ios, '13.0' use_frameworks! :linkage => :static target 'TestApp' do config = use_native_modules! use_react_native!( :path => config[:reactNativePath], :hermes_enabled => true ) end `; async function runIosMod(config: any, props: any) { const result = withKlaviyoIos(config, props) as any; if (result.mods && result.mods.ios) { return await result.mods.ios(result); } return result; } beforeEach(() => { jest.clearAllMocks(); FileManager.readFile.mockResolvedValue(mockPodfileContent); FileManager.writeFile.mockResolvedValue(undefined); FileManager.dirExists.mockReturnValue(true); FileManager.copyFile.mockResolvedValue(undefined); }); it('should inject KlaviyoNotificationServiceExtension target in Podfile', async () => { const props = createMockIosProps({ geofencingEnabled: true, }); await runIosMod(mockConfig, props); expect(FileManager.readFile).toHaveBeenCalled(); const allWriteCalls = FileManager.writeFile.mock.calls; const writeCall = allWriteCalls.find((call: any) => { if (!call || !call[0]) return false; const filePath = call[0]; return typeof filePath === 'string' && filePath.includes('Podfile') && call[1].includes("target 'KlaviyoNotificationServiceExtension'"); }); expect(writeCall).toBeDefined(); expect(writeCall[0]).toContain('Podfile'); const writtenContent = writeCall[1]; expect(writtenContent).toContain("target 'KlaviyoNotificationServiceExtension' do"); expect(writtenContent).toContain("pod 'KlaviyoSwiftExtension'"); }); }); describe('includeNotificationServiceExtension', () => { const { FileManager } = require('../plugin/support/fileManager'); const mockPodfileContent = `platform :ios, '13.0'\n\ntarget 'TestApp' do\nend\n`; async function runIosMod(config: any, props: any) { const result = withKlaviyoIos(config, props) as any; if (result.mods && result.mods.ios) { return await result.mods.ios(result); } return result; } beforeEach(() => { jest.clearAllMocks(); FileManager.readFile.mockResolvedValue(mockPodfileContent); FileManager.writeFile.mockResolvedValue(undefined); FileManager.copyFile.mockResolvedValue(undefined); FileManager.dirExists.mockReturnValue(true); }); it('should include NSE setup when includeNotificationServiceExtension is true', async () => { await runIosMod(mockConfig, createMockIosProps({ includeNotificationServiceExtension: true })); const podfileWrite = FileManager.writeFile.mock.calls.find( (call: any) => call && call[0] && typeof call[0] === 'string' && call[0].includes('Podfile') && call[1].includes("target 'KlaviyoNotificationServiceExtension'") ); expect(podfileWrite).toBeDefined(); expect(FileManager.copyFile).toHaveBeenCalled(); }); it('should skip NSE setup when includeNotificationServiceExtension is false', async () => { await runIosMod(mockConfig, createMockIosProps({ includeNotificationServiceExtension: false })); const podfileWrite = FileManager.writeFile.mock.calls.find( (call: any) => call && call[0] && typeof call[0] === 'string' && call[0].includes('Podfile') && call[1].includes("target 'KlaviyoNotificationServiceExtension'") ); expect(podfileWrite).toBeUndefined(); expect(FileManager.copyFile).not.toHaveBeenCalled(); }); it('should default to including NSE setup when includeNotificationServiceExtension is not specified', async () => { const propsWithDefault = createMockIosProps(); // default is true, so NSE should be included await runIosMod(mockConfig, propsWithDefault); const podfileWrite = FileManager.writeFile.mock.calls.find( (call: any) => call && call[0] && typeof call[0] === 'string' && call[0].includes('Podfile') && call[1].includes("target 'KlaviyoNotificationServiceExtension'") ); expect(podfileWrite).toBeDefined(); }); }); });