From 53472a7a62573333f888ef83299f2ba5c51d1b0d Mon Sep 17 00:00:00 2001 From: Jade Date: Thu, 26 Dec 2024 17:41:29 +0000 Subject: [PATCH] feat: release --- CHANGELOG.md | 34 +++++++++++ babel.config.js | 3 + constants/search-keys.constants.js | 10 ++++ jest.config.js | 10 ++++ lib/tests/utils.test.js | 92 ++++++++++++++++++++++++++++++ lib/utils.js | 17 ++++-- package.json | 2 +- tests/search.test.js | 59 +++++++++++++++++++ types/searchTypes.js | 1 + 9 files changed, 222 insertions(+), 6 deletions(-) create mode 100644 babel.config.js create mode 100644 constants/search-keys.constants.js create mode 100644 jest.config.js create mode 100644 lib/tests/utils.test.js create mode 100644 tests/search.test.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 0472d78..aa60858 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,37 @@ +## 2.0.5 (2024-12-26) + + +### Bug Fixes + +* **sdk:** sid logic (#43) (570c075) + + +### Features + +* add array to track wish request (da7bc28) +* add autoSendPushToken argument (2826b68) +* add blank search method (#40) (ff10756) +* add cart method (7a0dd8b) +* add custom properties to track purchase request (d58f5af) +* add date check (a9b7069) +* add excluded_merchants property to search types (#46) (0912cee) +* add function to send token after app initialization (7a7bd29) +* add functions to get and push token sent date in storage (75e5ce0) +* add new action step to identify base commit in master (95930a9) +* add persona-synchronization.yaml (17ddf01) +* add types (ffec1d5) +* add types to functions (1cf1b75) +* BREAKING CHANGE use @notifee/react-native for requesting alarm permissions (0e23088) +* check application initialization (152e928) +* check image_url (#44) (0cfcfab) +* connect sdk to dev app (48788fa) +* move to sdk (85a1e1e) +* **sdk:** remove notifee (71e7e79) +* **sdk:** track mobile pushes (eadf5f4) +* swap auto-changelog for standard-changelog (ec88b46) + + + ## 2.0.4 (2024-12-11) diff --git a/babel.config.js b/babel.config.js new file mode 100644 index 0000000..f842b77 --- /dev/null +++ b/babel.config.js @@ -0,0 +1,3 @@ +module.exports = { + presets: ['module:metro-react-native-babel-preset'], +}; diff --git a/constants/search-keys.constants.js b/constants/search-keys.constants.js new file mode 100644 index 0000000..a17c062 --- /dev/null +++ b/constants/search-keys.constants.js @@ -0,0 +1,10 @@ +export const SEARCH_QUERY_KEYS = [ + 'categories', + 'locations', + 'brands', + 'colors', + 'fashion_sizes', + 'exclude', + 'merchants', + 'excluded_merchants' +] diff --git a/jest.config.js b/jest.config.js new file mode 100644 index 0000000..a757ffb --- /dev/null +++ b/jest.config.js @@ -0,0 +1,10 @@ +module.exports = { + preset: 'react-native', + transform: { + '^.+\\.js$': 'babel-jest', + }, + moduleFileExtensions: ['js', 'jsx', 'ts', 'tsx'], + transformIgnorePatterns: [ + 'node_modules/(?!(@react-native|react-native|@react-native-firebase|react-native-push-notification)/)', + ], +}; diff --git a/lib/tests/utils.test.js b/lib/tests/utils.test.js new file mode 100644 index 0000000..ee73a78 --- /dev/null +++ b/lib/tests/utils.test.js @@ -0,0 +1,92 @@ +import DataEncoder from "../utils"; +import { SEARCH_QUERY_KEYS } from '../../constants/search-keys.constants'; + +describe('DataEncoder', () => { + let encoder; + + beforeEach(() => { + encoder = new DataEncoder(); + }); + + describe('encode', () => { + test('encodes a flat object into query string', () => { + const input = { + name: 'Ivan Ivanov', + age: 30, + city: 'New York', + }; + const result = encoder.encode(input); + expect(result).toBe('name=Ivan%20Ivanov&age=30&city=New%20York'); + }); + + test('encodes a nested object into query string', () => { + const input = { + user: { + name: 'Ivan', + details: { + age: 30, + city: 'New York', + }, + }, + }; + const result = encoder.encode(input); + expect(result).toBe('user[name]=Ivan&user[details][age]=30&user[details][city]=New%20York'); + }); + + test('encodes arrays correctly with SEARCH_QUERY_KEYS', () => { + const input = { + categories: ['category1', 'category2', 'category3'], + }; + const result = encoder.encode(input); + expect(result).toBe('categories=category1,category2,category3'); + }); + + test('encodes arrays correctly without SEARCH_QUERY_KEYS', () => { + const input = { + items: ['item1', 'item2'], + }; + const result = encoder.encode(input); + expect(result).toBe('items[0]=item1&items[1]=item2'); + }); + }); + + describe('convertToObject', () => { + test('decodes a query string into an object', () => { + const input = 'name=John%20Doe&age=30&city=New%20York'; + const result = encoder.convertToObject(input); + expect(result).toEqual({ + name: 'John Doe', + age: '30', + city: 'New York', + }); + }); + + test('handles nested query strings', () => { + const input = 'user[name]=John&user[details][age]=30&user[details][city]=New%20York'; + const result = encoder.convertToObject(input); + expect(result).toEqual({ + 'user[name]': 'John', + 'user[details][age]': '30', + 'user[details][city]': 'New York', + }); + }); + }); + + describe('error cases', () => { + test('throws an error when actualKey is not set', () => { + const encoder = new DataEncoder(); + + encoder.actualKey = null; + + expect(() => encoder.__dataEncoding(['item1', 'item2'])).toThrow( + 'Directly passed array does not work' + ); + }); + + test('returns null for non-object inputs', () => { + expect(encoder.encode(null)).toBeNull(); + expect(encoder.encode(42)).toBeNull(); + expect(encoder.encode('string')).toBeNull(); + }); + }); +}); diff --git a/lib/utils.js b/lib/utils.js index e96bedd..8482d66 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -1,3 +1,5 @@ +import { SEARCH_QUERY_KEYS } from '../constants/search-keys.constants'; + const DataEncoder = function () { this.levels = []; this.actualKey = null; @@ -29,13 +31,18 @@ DataEncoder.prototype.__dataEncoding = function (data) { } else if (is("Array", data)) { if (!this.actualKey) throw new Error("Directly passed array does not work"); - const aSize = data.length; + if (SEARCH_QUERY_KEYS.includes(this.actualKey)) { + finalString += uriPart + "=" + data.join(",") + "&"; + } else { + const aSize = data.length; - for (let b = 0; b < aSize; b++) { - let aVal = data[b]; - this.levels.push(b); - finalString += this.__dataEncoding(aVal); + for (let b = 0; b < aSize; b++) { + let aVal = data[b]; + this.levels.push(b); + finalString += this.__dataEncoding(aVal); + } } + } else { finalString += uriPart + "=" + encodeURIComponent(data) + "&"; } diff --git a/package.json b/package.json index 9c811f0..8979fb3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@personaclick/rn-sdk", - "version": "2.0.4", + "version": "2.0.5", "description": "PersonaClick React Native SDK", "exports": { ".": "./index.js" diff --git a/tests/search.test.js b/tests/search.test.js new file mode 100644 index 0000000..df963dc --- /dev/null +++ b/tests/search.test.js @@ -0,0 +1,59 @@ +import REES46 from '../index'; + +jest.mock('@react-native-community/push-notification-ios', () => { +}); +jest.mock('react-native-device-info', () => { +}); +jest.mock('@react-native-firebase/messaging', () => { +}); +jest.mock('@react-native-async-storage/async-storage', () => { +}); + +describe('Search', () => { + let sdk; + + beforeEach(() => { + sdk = new REES46('357382bf66ac0ce2f1722677c59511', 'android', true) + jest.spyOn(sdk, 'push').mockImplementation((callback) => { + callback(); + }); + + jest.clearAllMocks(); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + test('should call search with correct parameters for instant search and resolve', async () => { + const searchOptions = {type: 'instant_search', search_query: 'phone'}; + + const response = await sdk.search(searchOptions); + + expect(response).toHaveProperty('categories'); + expect(response).toHaveProperty('html'); + expect(response).toHaveProperty('products'); + expect(response).toHaveProperty('products_total'); + }); + + test('should call search with correct parameters for full search and resolve', async () => { + const searchOptions = {type: 'full_search', search_query: 'coat'}; + + const response = await sdk.search(searchOptions); + + expect(response).toHaveProperty('categories'); + expect(response).toHaveProperty('html'); + expect(response).toHaveProperty('products'); + expect(response).toHaveProperty('products_total'); + }); + + test('should return error when calling search with missing type parameter', async () => { + const searchOptions = {search_query: 'phone'}; + + try { + await sdk.search(searchOptions); + } catch (error) { + expect(error.message).toContain('Request failed with status code 400'); + } + }); +}); diff --git a/types/searchTypes.js b/types/searchTypes.js index 2b89893..ad4a043 100644 --- a/types/searchTypes.js +++ b/types/searchTypes.js @@ -19,6 +19,7 @@ * @property {string} [exclude] - Comma separated list of products IDs to exclude from search results. * @property {string} [email] - Email. * @property {string} [merchants] - Comma separated list of merchants. + * @property {string} [excluded_merchants] - Comma separated list of excluded merchants. * @property {string} [filters_search_by] - Available options for filter: name, quantity, popularity. * @property {number} [brand_limit=1000] - Limits the number of brands in the response. */